20260605 移除audit rule index和DB trace
This commit is contained in:
parent
9579f65501
commit
e597c7c124
@ -27,7 +27,7 @@ cleanup() {
|
|||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
IGNORE_REGEX='src/bin/repository_view_stats\.rs|src/bin/trace_arin_missing_vrps\.rs|src/bin/db_stats\.rs|src/bin/rrdp_state_dump\.rs|src/bin/ccr_dump\.rs|src/bin/ccr_verify\.rs|src/bin/ccr_to_routinator_csv\.rs|src/bin/ccr_to_compare_views\.rs|src/bin/cir_materialize\.rs|src/bin/cir_extract_inputs\.rs|src/bin/cir_drop_report\.rs|src/bin/cir_ta_only_fixture\.rs|src/bin/cir_dump_reject_list\.rs|src/bin/rpki_object_parse\.rs|src/bin/triage_ccr_cir_pair\.rs|src/bin/rpki_artifact_metrics\.rs|src/bin/rpki_daemon\.rs|src/bin/sequence_triage_ccr_cir\.rs|src/tools/rpki_artifact_metrics\.rs|src/ccr/compare_view\.rs|src/progress_log\.rs|src/cli\.rs|src/validation/run_tree_from_tal\.rs|src/validation/tree_parallel\.rs|src/validation/tree_runner\.rs|src/validation/from_tal\.rs|src/sync/store_projection\.rs|src/sync/repo\.rs|src/sync/rrdp\.rs|src/storage\.rs|src/cir/materialize\.rs'
|
IGNORE_REGEX='src/bin/repository_view_stats\.rs|src/bin/db_stats\.rs|src/bin/rrdp_state_dump\.rs|src/bin/ccr_dump\.rs|src/bin/ccr_verify\.rs|src/bin/ccr_to_routinator_csv\.rs|src/bin/ccr_to_compare_views\.rs|src/bin/cir_materialize\.rs|src/bin/cir_extract_inputs\.rs|src/bin/cir_drop_report\.rs|src/bin/cir_ta_only_fixture\.rs|src/bin/cir_dump_reject_list\.rs|src/bin/rpki_object_parse\.rs|src/bin/triage_ccr_cir_pair\.rs|src/bin/rpki_artifact_metrics\.rs|src/bin/rpki_daemon\.rs|src/bin/sequence_triage_ccr_cir\.rs|src/tools/rpki_artifact_metrics\.rs|src/ccr/compare_view\.rs|src/progress_log\.rs|src/cli\.rs|src/validation/run_tree_from_tal\.rs|src/validation/tree_parallel\.rs|src/validation/tree_runner\.rs|src/validation/from_tal\.rs|src/sync/store_projection\.rs|src/sync/repo\.rs|src/sync/rrdp\.rs|src/storage\.rs|src/cir/materialize\.rs'
|
||||||
|
|
||||||
# Preserve colored output even though we post-process output by running under a pseudo-TTY.
|
# Preserve colored output even though we post-process output by running under a pseudo-TTY.
|
||||||
# We run tests only once, then generate both CLI text + HTML reports without rerunning tests.
|
# We run tests only once, then generate both CLI text + HTML reports without rerunning tests.
|
||||||
|
|||||||
@ -75,6 +75,30 @@ def rsync_run_artifacts_from_remote(target: str, source: str | Path, destination
|
|||||||
run_local([*rsync_base, f"{target}:{source}/{name}", f"{destination}/"])
|
run_local([*rsync_base, f"{target}:{source}/{name}", f"{destination}/"])
|
||||||
|
|
||||||
|
|
||||||
|
def rsync_remote_analysis_from_remote(target: str, remote_exp_root: str | Path, local_exp_root: Path) -> None:
|
||||||
|
local_exp_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
rsync_base = ["rsync", "-az", "--ignore-missing-args", "--partial", "--partial-dir=.rsync-partial"]
|
||||||
|
for name in [
|
||||||
|
"left-sequence.jsonl",
|
||||||
|
"right-sequence.jsonl",
|
||||||
|
"run-progress.json",
|
||||||
|
"sequence-triage-time.txt",
|
||||||
|
"sequence-triage/sequence-triage.json",
|
||||||
|
]:
|
||||||
|
destination = local_exp_root / Path(name).parent
|
||||||
|
destination.mkdir(parents=True, exist_ok=True)
|
||||||
|
run_local([*rsync_base, f"{target}:{remote_exp_root}/{name}", f"{destination}/"])
|
||||||
|
|
||||||
|
|
||||||
|
def same_remote_location(
|
||||||
|
left_target: str,
|
||||||
|
left_root: str | Path,
|
||||||
|
right_target: str,
|
||||||
|
right_root: str | Path,
|
||||||
|
) -> bool:
|
||||||
|
return left_target == right_target and str(left_root) == str(right_root)
|
||||||
|
|
||||||
|
|
||||||
def load_json(path: Path) -> Any:
|
def load_json(path: Path) -> Any:
|
||||||
with path.open("r", encoding="utf-8") as handle:
|
with path.open("r", encoding="utf-8") as handle:
|
||||||
return json.load(handle)
|
return json.load(handle)
|
||||||
@ -258,9 +282,31 @@ def prepare_remote(ssh_target: str, remote_root: Path, needs_rpki_client: bool)
|
|||||||
rsync_to_remote(ssh_target, detect_libtls_path(rpki_client_bin), remote_root / "lib" / "libtls.so.28")
|
rsync_to_remote(ssh_target, detect_libtls_path(rpki_client_bin), remote_root / "lib" / "libtls.so.28")
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_remote_once(
|
||||||
|
prepared: dict[tuple[str, str], bool],
|
||||||
|
ssh_target: str,
|
||||||
|
remote_root: Path,
|
||||||
|
needs_rpki_client: bool,
|
||||||
|
) -> None:
|
||||||
|
key = (ssh_target, str(remote_root))
|
||||||
|
already_has_rpki_client = prepared.get(key)
|
||||||
|
if already_has_rpki_client is not None and (already_has_rpki_client or not needs_rpki_client):
|
||||||
|
return
|
||||||
|
prepare_remote(ssh_target, remote_root, needs_rpki_client)
|
||||||
|
prepared[key] = bool(already_has_rpki_client or needs_rpki_client)
|
||||||
|
|
||||||
|
|
||||||
def side_config(name: str) -> dict[str, Any]:
|
def side_config(name: str) -> dict[str, Any]:
|
||||||
if name == "ours-standard":
|
if name == "ours-standard":
|
||||||
return {"rpKind": "ours", "mode": "standard", "protocol": "rrdp+rsync", "rsyncScope": "module-root"}
|
return {"rpKind": "ours", "mode": "standard", "protocol": "rrdp+rsync", "rsyncScope": "module-root"}
|
||||||
|
if name == "ours-strict-all":
|
||||||
|
return {
|
||||||
|
"rpKind": "ours",
|
||||||
|
"mode": "strict-all",
|
||||||
|
"protocol": "rrdp+rsync",
|
||||||
|
"rsyncScope": "module-root",
|
||||||
|
"strictPolicies": "name,cms-der,signed-attrs",
|
||||||
|
}
|
||||||
if name == "rpki-client-standard":
|
if name == "rpki-client-standard":
|
||||||
return {"rpKind": "rpki-client", "mode": "standard", "protocol": "rrdp+rsync"}
|
return {"rpKind": "rpki-client", "mode": "standard", "protocol": "rrdp+rsync"}
|
||||||
raise SystemExit(f"unknown side config: {name}")
|
raise SystemExit(f"unknown side config: {name}")
|
||||||
@ -305,6 +351,8 @@ def build_remote_command(remote_root: Path, side_name: str, side: dict[str, Any]
|
|||||||
"--repo-bytes-db", str(state_dir / "repo-bytes.db"),
|
"--repo-bytes-db", str(state_dir / "repo-bytes.db"),
|
||||||
"--rsync-scope", side.get("rsyncScope", "module-root"),
|
"--rsync-scope", side.get("rsyncScope", "module-root"),
|
||||||
]
|
]
|
||||||
|
if side.get("strictPolicies"):
|
||||||
|
argv.extend(["--strict", str(side["strictPolicies"])])
|
||||||
for rir in rirs:
|
for rir in rirs:
|
||||||
argv.extend(["--tal-path", str(remote_root / "fixtures" / "tal" / fixture_name(rir, "tal"))])
|
argv.extend(["--tal-path", str(remote_root / "fixtures" / "tal" / fixture_name(rir, "tal"))])
|
||||||
argv.extend(["--ta-path", str(remote_root / "fixtures" / "ta" / fixture_name(rir, "ta"))])
|
argv.extend(["--ta-path", str(remote_root / "fixtures" / "ta" / fixture_name(rir, "ta"))])
|
||||||
@ -357,7 +405,22 @@ def build_remote_command(remote_root: Path, side_name: str, side: dict[str, Any]
|
|||||||
"import json, pathlib, sys\n"
|
"import json, pathlib, sys\n"
|
||||||
"run_dir=pathlib.Path(sys.argv[1]); side_name=sys.argv[2]; side_label=sys.argv[3]; seq=int(sys.argv[4]); sync_mode=sys.argv[5]\n"
|
"run_dir=pathlib.Path(sys.argv[1]); side_name=sys.argv[2]; side_label=sys.argv[3]; seq=int(sys.argv[4]); sync_mode=sys.argv[5]\n"
|
||||||
"def read(p):\n return p.read_text().strip() if p.exists() else None\n"
|
"def read(p):\n return p.read_text().strip() if p.exists() else None\n"
|
||||||
"meta={'sideName':side_name,'sideLabel':side_label,'seq':seq,'syncMode':sync_mode,'startedAt':read(run_dir/'started-at.txt'),'finishedAt':read(run_dir/'finished-at.txt'),'exitCode':int(read(run_dir/'exit-code.txt') or '1')}\n"
|
"def counts_from_report():\n"
|
||||||
|
" p=run_dir/'report.json'\n"
|
||||||
|
" if not p.exists():\n"
|
||||||
|
" return {}\n"
|
||||||
|
" try:\n"
|
||||||
|
" report=json.load(open(p))\n"
|
||||||
|
" except Exception:\n"
|
||||||
|
" return {}\n"
|
||||||
|
" meta=report.get('metadata') if isinstance(report, dict) else None\n"
|
||||||
|
" if isinstance(meta, dict):\n"
|
||||||
|
" return {'vrps': int(meta.get('vrps') or 0), 'vaps': int(meta.get('vaps') or meta.get('aspas') or 0), 'publicationPoints': int(meta.get('repositories') or 0), 'warnings': 0}\n"
|
||||||
|
" pps=report.get('publication_points', []) if isinstance(report, dict) else []\n"
|
||||||
|
" tree=report.get('tree', {}) if isinstance(report, dict) else {}\n"
|
||||||
|
" pp_warnings=sum(len(pp.get('warnings', [])) for pp in pps if isinstance(pp, dict)) if isinstance(pps, list) else 0\n"
|
||||||
|
" return {'vrps': len(report.get('vrps', [])), 'vaps': len(report.get('aspas', [])), 'publicationPoints': len(pps) if isinstance(pps, list) else 0, 'warnings': len(tree.get('warnings', [])) + pp_warnings if isinstance(tree, dict) else pp_warnings}\n"
|
||||||
|
"meta={'sideName':side_name,'sideLabel':side_label,'seq':seq,'syncMode':sync_mode,'startedAt':read(run_dir/'started-at.txt'),'finishedAt':read(run_dir/'finished-at.txt'),'exitCode':int(read(run_dir/'exit-code.txt') or '1'),'counts':counts_from_report()}\n"
|
||||||
"json.dump(meta, open(run_dir/'remote-run-meta.json','w'), indent=2, sort_keys=True); print()\n"
|
"json.dump(meta, open(run_dir/'remote-run-meta.json','w'), indent=2, sort_keys=True); print()\n"
|
||||||
"REMOTE_META"
|
"REMOTE_META"
|
||||||
)
|
)
|
||||||
@ -370,6 +433,165 @@ def run_remote_sample(ssh_target: str, remote_root: Path, side_name: str, side:
|
|||||||
return run_dir
|
return run_dir
|
||||||
|
|
||||||
|
|
||||||
|
def append_remote_sequence_item(
|
||||||
|
ssh_target: str,
|
||||||
|
remote_root: Path,
|
||||||
|
side_name: str,
|
||||||
|
side_label: str,
|
||||||
|
seq: int,
|
||||||
|
run_dir: Path,
|
||||||
|
schedule_mode: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
remote_exp_root = remote_root / "experiments" / "sequence"
|
||||||
|
seq_path = remote_exp_root / ("left-sequence.jsonl" if side_label == "A" else "right-sequence.jsonl")
|
||||||
|
side_value = "left" if side_label == "A" else "right"
|
||||||
|
script = f"""
|
||||||
|
set -euo pipefail
|
||||||
|
python3 - <<'REMOTE_SEQUENCE_ITEM' {shlex.quote(str(remote_exp_root))} {shlex.quote(str(seq_path))} {shlex.quote(str(run_dir))} {shlex.quote(str(remote_root / 'bin' / 'cir_dump_reject_list'))} {shlex.quote(side_name)} {shlex.quote(side_label)} {seq} {shlex.quote(side_value)} {shlex.quote(schedule_mode)}
|
||||||
|
import hashlib, json, os, pathlib, subprocess, sys
|
||||||
|
exp_root=pathlib.Path(sys.argv[1])
|
||||||
|
seq_path=pathlib.Path(sys.argv[2])
|
||||||
|
run_dir=pathlib.Path(sys.argv[3])
|
||||||
|
cir_dump=pathlib.Path(sys.argv[4])
|
||||||
|
side_name=sys.argv[5]
|
||||||
|
side_label=sys.argv[6]
|
||||||
|
seq=int(sys.argv[7])
|
||||||
|
side_value=sys.argv[8]
|
||||||
|
schedule_mode=sys.argv[9]
|
||||||
|
def read(p):
|
||||||
|
return p.read_text().strip() if p.exists() else None
|
||||||
|
def sha256_file(p):
|
||||||
|
h=hashlib.sha256()
|
||||||
|
with open(p, 'rb') as f:
|
||||||
|
for chunk in iter(lambda: f.read(1024 * 1024), b''):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
def parse_elapsed_to_ms(raw):
|
||||||
|
raw=raw.strip()
|
||||||
|
if not raw:
|
||||||
|
return 0
|
||||||
|
if '-' in raw:
|
||||||
|
days, raw=raw.split('-', 1)
|
||||||
|
else:
|
||||||
|
days='0'
|
||||||
|
parts=raw.split(':')
|
||||||
|
if len(parts) == 3:
|
||||||
|
hours, minutes, seconds=parts
|
||||||
|
elif len(parts) == 2:
|
||||||
|
hours='0'; minutes, seconds=parts
|
||||||
|
else:
|
||||||
|
hours='0'; minutes='0'; seconds=parts[0]
|
||||||
|
return int(round((int(days)*86400 + int(hours)*3600 + int(minutes)*60 + float(seconds))*1000))
|
||||||
|
def parse_time_file(p):
|
||||||
|
data={{}}
|
||||||
|
if not p.exists():
|
||||||
|
return data
|
||||||
|
for line in p.read_text(errors='replace').splitlines():
|
||||||
|
if 'Elapsed (wall clock) time' in line:
|
||||||
|
elapsed=line.rsplit('):', 1)[1] if '):' in line else line.rsplit(':', 1)[1]
|
||||||
|
data['wallMs']=parse_elapsed_to_ms(elapsed)
|
||||||
|
elif 'Maximum resident set size' in line:
|
||||||
|
try:
|
||||||
|
data['maxRssKb']=int(line.rsplit(':', 1)[1].strip())
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return data
|
||||||
|
def cir_counts(cir):
|
||||||
|
values={{}}
|
||||||
|
if not cir.exists():
|
||||||
|
return {{}}
|
||||||
|
result=subprocess.run([str(cir_dump), '--cir', str(cir), '--limit', '0'], text=True, capture_output=True, check=True)
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
if '=' not in line:
|
||||||
|
continue
|
||||||
|
key, value=line.split('=', 1)
|
||||||
|
if key in ('object_count', 'trust_anchor_count', 'reject_count'):
|
||||||
|
values[key]=int(value)
|
||||||
|
return {{'cirObjectCount': values.get('object_count', 0), 'cirTrustAnchorCount': values.get('trust_anchor_count', 0), 'cirRejectCount': values.get('reject_count', 0)}}
|
||||||
|
meta=json.load(open(run_dir/'remote-run-meta.json'))
|
||||||
|
time_info=parse_time_file(run_dir/'process-time.txt')
|
||||||
|
counts=dict(meta.get('counts') or {{}})
|
||||||
|
ccr=run_dir/'result.ccr'
|
||||||
|
cir=run_dir/'result.cir'
|
||||||
|
if meta.get('exitCode') != 0:
|
||||||
|
raise SystemExit(f"remote run failed: exitCode={{meta.get('exitCode')}} runDir={{run_dir}}")
|
||||||
|
missing=[str(path) for path in (ccr, cir) if not path.exists()]
|
||||||
|
if missing:
|
||||||
|
raise SystemExit(f"remote run missing required artifact(s): {{missing}}")
|
||||||
|
counts.update(cir_counts(cir))
|
||||||
|
item={{
|
||||||
|
'schemaVersion': 1,
|
||||||
|
'rpId': side_name,
|
||||||
|
'side': side_value,
|
||||||
|
'seq': seq,
|
||||||
|
'runId': f'{{side_label}}-{{seq:04d}}',
|
||||||
|
'syncMode': 'snapshot' if seq == 1 else 'delta',
|
||||||
|
'status': 'success' if meta.get('exitCode') == 0 else 'failed',
|
||||||
|
'startTime': meta.get('startedAt'),
|
||||||
|
'finishTime': meta.get('finishedAt'),
|
||||||
|
'validationTime': None,
|
||||||
|
'ccrPath': os.path.relpath(ccr, exp_root),
|
||||||
|
'cirPath': os.path.relpath(cir, exp_root),
|
||||||
|
'ccrSha256': sha256_file(ccr) if ccr.exists() else None,
|
||||||
|
'cirSha256': sha256_file(cir) if cir.exists() else None,
|
||||||
|
'wallMs': time_info.get('wallMs'),
|
||||||
|
'maxRssKb': time_info.get('maxRssKb'),
|
||||||
|
'vrps': counts.get('vrps'),
|
||||||
|
'vaps': counts.get('vaps'),
|
||||||
|
'publicationPoints': counts.get('publicationPoints'),
|
||||||
|
'cirObjectCount': counts.get('cirObjectCount'),
|
||||||
|
'cirRejectCount': counts.get('cirRejectCount'),
|
||||||
|
'cirTrustAnchorCount': counts.get('cirTrustAnchorCount'),
|
||||||
|
'scheduleMode': schedule_mode,
|
||||||
|
}}
|
||||||
|
seq_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(seq_path, 'a', encoding='utf-8') as handle:
|
||||||
|
handle.write(json.dumps(item, sort_keys=True, ensure_ascii=False) + '\\n')
|
||||||
|
print(json.dumps(item, sort_keys=True, ensure_ascii=False))
|
||||||
|
REMOTE_SEQUENCE_ITEM
|
||||||
|
"""
|
||||||
|
result = ssh_script(ssh_target, script, capture=True)
|
||||||
|
lines = [line for line in result.stdout.splitlines() if line.strip()]
|
||||||
|
if not lines:
|
||||||
|
raise SystemExit("remote sequence item append produced no output")
|
||||||
|
return json.loads(lines[-1])
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_remote_run_nonessential(ssh_target: str, run_dir: Path) -> None:
|
||||||
|
keep = {
|
||||||
|
"result.ccr",
|
||||||
|
"result.cir",
|
||||||
|
"process-time.txt",
|
||||||
|
"remote-run-meta.json",
|
||||||
|
"exit-code.txt",
|
||||||
|
"started-at.txt",
|
||||||
|
"finished-at.txt",
|
||||||
|
"stdout.log",
|
||||||
|
"stderr.log",
|
||||||
|
}
|
||||||
|
keep_json = json.dumps(sorted(keep), ensure_ascii=False)
|
||||||
|
script = f"""
|
||||||
|
set -euo pipefail
|
||||||
|
python3 - <<'REMOTE_CLEAN' {shlex.quote(str(run_dir))} {shlex.quote(keep_json)}
|
||||||
|
import json, pathlib, sys
|
||||||
|
run_dir=pathlib.Path(sys.argv[1])
|
||||||
|
keep=set(json.loads(sys.argv[2]))
|
||||||
|
removed=0
|
||||||
|
if run_dir.exists():
|
||||||
|
for path in run_dir.iterdir():
|
||||||
|
if path.name in keep or path.is_dir():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
removed += path.stat().st_size
|
||||||
|
path.unlink()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
print(f'cleaned_nonessential_bytes={{removed}}')
|
||||||
|
REMOTE_CLEAN
|
||||||
|
"""
|
||||||
|
ssh_script(ssh_target, script)
|
||||||
|
|
||||||
|
|
||||||
def cir_counts(cir_path: Path) -> dict[str, int]:
|
def cir_counts(cir_path: Path) -> dict[str, int]:
|
||||||
result = run_local([str(REPO_ROOT / "target" / "release" / "cir_dump_reject_list"), "--cir", str(cir_path), "--limit", "0"], capture=True)
|
result = run_local([str(REPO_ROOT / "target" / "release" / "cir_dump_reject_list"), "--cir", str(cir_path), "--limit", "0"], capture=True)
|
||||||
values: dict[str, int] = {}
|
values: dict[str, int] = {}
|
||||||
@ -413,7 +635,9 @@ def build_sequence_item(local_root: Path, side_name: str, side_label: str, side:
|
|||||||
cir = run_dir / "result.cir"
|
cir = run_dir / "result.cir"
|
||||||
meta = load_json(run_dir / "remote-run-meta.json")
|
meta = load_json(run_dir / "remote-run-meta.json")
|
||||||
time_info = parse_time_file(run_dir / "process-time.txt")
|
time_info = parse_time_file(run_dir / "process-time.txt")
|
||||||
counts = report_counts(run_dir / "report.json", side["rpKind"])
|
counts = dict(meta.get("counts") or {})
|
||||||
|
if not counts:
|
||||||
|
counts = report_counts(run_dir / "report.json", side["rpKind"])
|
||||||
counts.update(cir_counts(cir))
|
counts.update(cir_counts(cir))
|
||||||
return {
|
return {
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
@ -455,8 +679,58 @@ def run_sequence_triage(local_exp_root: Path, args: argparse.Namespace) -> None:
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def run_sequence_triage_remote(ssh_target: str, remote_root: Path, args: argparse.Namespace) -> None:
|
||||||
|
remote_exp_root = remote_root / "experiments" / "sequence"
|
||||||
|
compare_dir = remote_exp_root / "sequence-triage"
|
||||||
|
time_path = remote_exp_root / "sequence-triage-time.txt"
|
||||||
|
command = " ".join(
|
||||||
|
shlex.quote(item)
|
||||||
|
for item in [
|
||||||
|
str(remote_root / "bin" / "sequence_triage_ccr_cir"),
|
||||||
|
"--left-sequence", str(remote_exp_root / "left-sequence.jsonl"),
|
||||||
|
"--right-sequence", str(remote_exp_root / "right-sequence.jsonl"),
|
||||||
|
"--out-dir", str(compare_dir),
|
||||||
|
"--align-window-runs", str(args.align_window_runs),
|
||||||
|
"--align-window-secs", str(args.align_window_secs),
|
||||||
|
"--sample-limit", str(args.sample_limit),
|
||||||
|
"--timeline-sample-limit", str(args.timeline_sample_limit),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
ssh_script(
|
||||||
|
ssh_target,
|
||||||
|
"set -euo pipefail; "
|
||||||
|
f"rm -rf {shlex.quote(str(compare_dir))} {shlex.quote(str(time_path))}; "
|
||||||
|
f"/usr/bin/time -v -o {shlex.quote(str(time_path))} -- {command}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_side_to_analysis_remote(
|
||||||
|
source_ssh_target: str,
|
||||||
|
source_remote_root: Path,
|
||||||
|
analysis_ssh_target: str,
|
||||||
|
analysis_remote_root: Path,
|
||||||
|
side_label: str,
|
||||||
|
) -> None:
|
||||||
|
source_exp_root = source_remote_root / "experiments" / "sequence"
|
||||||
|
analysis_exp_root = analysis_remote_root / "experiments" / "sequence"
|
||||||
|
sequence_name = "left-sequence.jsonl" if side_label == "A" else "right-sequence.jsonl"
|
||||||
|
if same_remote_location(source_ssh_target, source_exp_root, analysis_ssh_target, analysis_exp_root):
|
||||||
|
return
|
||||||
|
side_dir = source_exp_root / side_label
|
||||||
|
script = (
|
||||||
|
"set -euo pipefail; "
|
||||||
|
f"ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new {shlex.quote(analysis_ssh_target)} "
|
||||||
|
f"{shlex.quote('mkdir -p ' + shlex.quote(str(analysis_exp_root / side_label)))}; "
|
||||||
|
f"rsync -az --delete {shlex.quote(str(side_dir))}/ {shlex.quote(analysis_ssh_target)}:{shlex.quote(str(analysis_exp_root / side_label))}/; "
|
||||||
|
f"rsync -az {shlex.quote(str(source_exp_root / sequence_name))} "
|
||||||
|
f"{shlex.quote(analysis_ssh_target)}:{shlex.quote(str(analysis_exp_root / sequence_name))}"
|
||||||
|
)
|
||||||
|
ssh_script(source_ssh_target, script)
|
||||||
|
|
||||||
|
|
||||||
def run_side_sequence(
|
def run_side_sequence(
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
|
ssh_target: str,
|
||||||
remote_root: Path,
|
remote_root: Path,
|
||||||
local_exp_root: Path,
|
local_exp_root: Path,
|
||||||
side_label: str,
|
side_label: str,
|
||||||
@ -468,13 +742,14 @@ def run_side_sequence(
|
|||||||
side_progress: list[dict[str, Any]] = []
|
side_progress: list[dict[str, Any]] = []
|
||||||
for seq in range(1, args.samples_per_side + 1):
|
for seq in range(1, args.samples_per_side + 1):
|
||||||
side_progress.append(
|
side_progress.append(
|
||||||
run_one_side_sample(args, remote_root, local_exp_root, side_label, side_name, side, seq_path, seq, rirs)
|
run_one_side_sample(args, ssh_target, remote_root, local_exp_root, side_label, side_name, side, seq_path, seq, rirs)
|
||||||
)
|
)
|
||||||
return side_progress
|
return side_progress
|
||||||
|
|
||||||
|
|
||||||
def run_one_side_sample(
|
def run_one_side_sample(
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
|
ssh_target: str,
|
||||||
remote_root: Path,
|
remote_root: Path,
|
||||||
local_exp_root: Path,
|
local_exp_root: Path,
|
||||||
side_label: str,
|
side_label: str,
|
||||||
@ -489,11 +764,24 @@ def run_one_side_sample(
|
|||||||
f"[run] {side_label} {side_name} seq={seq} rirs={rir_label} schedule={args.schedule_mode}",
|
f"[run] {side_label} {side_name} seq={seq} rirs={rir_label} schedule={args.schedule_mode}",
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
remote_run_dir = run_remote_sample(args.ssh_target, remote_root, side_name, side, side_label, seq, rirs)
|
remote_run_dir = run_remote_sample(ssh_target, remote_root, side_name, side, side_label, seq, rirs)
|
||||||
local_run_dir = local_exp_root / side_label / f"run_{seq:04d}"
|
if args.remote_triage:
|
||||||
rsync_run_artifacts_from_remote(args.ssh_target, remote_run_dir, local_run_dir)
|
item = append_remote_sequence_item(
|
||||||
item = build_sequence_item(local_exp_root, side_name, side_label, side, seq, local_run_dir)
|
ssh_target,
|
||||||
item["scheduleMode"] = args.schedule_mode
|
remote_root,
|
||||||
|
side_name,
|
||||||
|
side_label,
|
||||||
|
seq,
|
||||||
|
remote_run_dir,
|
||||||
|
args.schedule_mode,
|
||||||
|
)
|
||||||
|
if args.cleanup_run_nonessential:
|
||||||
|
cleanup_remote_run_nonessential(ssh_target, remote_run_dir)
|
||||||
|
else:
|
||||||
|
local_run_dir = local_exp_root / side_label / f"run_{seq:04d}"
|
||||||
|
rsync_run_artifacts_from_remote(ssh_target, remote_run_dir, local_run_dir)
|
||||||
|
item = build_sequence_item(local_exp_root, side_name, side_label, side, seq, local_run_dir)
|
||||||
|
item["scheduleMode"] = args.schedule_mode
|
||||||
append_jsonl(seq_path, item)
|
append_jsonl(seq_path, item)
|
||||||
print(
|
print(
|
||||||
f"[done] {side_label} seq={seq} wallMs={item.get('wallMs')} vrps={item.get('vrps')} vaps={item.get('vaps')} objects={item.get('cirObjectCount')} rejects={item.get('cirRejectCount')}",
|
f"[done] {side_label} seq={seq} wallMs={item.get('wallMs')} vrps={item.get('vrps')} vaps={item.get('vaps')} objects={item.get('cirObjectCount')} rejects={item.get('cirRejectCount')}",
|
||||||
@ -510,6 +798,12 @@ def run_experiment(args: argparse.Namespace) -> None:
|
|||||||
right = side_config(args.right)
|
right = side_config(args.right)
|
||||||
run_root = Path(args.run_root).resolve()
|
run_root = Path(args.run_root).resolve()
|
||||||
remote_root = Path(args.remote_root)
|
remote_root = Path(args.remote_root)
|
||||||
|
left_ssh_target = args.left_ssh_target or args.ssh_target
|
||||||
|
right_ssh_target = args.right_ssh_target or args.ssh_target
|
||||||
|
analysis_ssh_target = args.analysis_ssh_target or left_ssh_target
|
||||||
|
left_remote_root = Path(args.left_remote_root or args.remote_root)
|
||||||
|
right_remote_root = Path(args.right_remote_root or args.remote_root)
|
||||||
|
analysis_remote_root = Path(args.analysis_remote_root or args.remote_root)
|
||||||
run_root.mkdir(parents=True, exist_ok=True)
|
run_root.mkdir(parents=True, exist_ok=True)
|
||||||
write_json(run_root / "experiment-config.json", {
|
write_json(run_root / "experiment-config.json", {
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
@ -520,6 +814,12 @@ def run_experiment(args: argparse.Namespace) -> None:
|
|||||||
"rirs": rirs,
|
"rirs": rirs,
|
||||||
"scheduleMode": args.schedule_mode,
|
"scheduleMode": args.schedule_mode,
|
||||||
"remoteRoot": str(remote_root),
|
"remoteRoot": str(remote_root),
|
||||||
|
"leftSshTarget": left_ssh_target,
|
||||||
|
"rightSshTarget": right_ssh_target,
|
||||||
|
"analysisSshTarget": analysis_ssh_target,
|
||||||
|
"leftRemoteRoot": str(left_remote_root),
|
||||||
|
"rightRemoteRoot": str(right_remote_root),
|
||||||
|
"analysisRemoteRoot": str(analysis_remote_root),
|
||||||
"sshTarget": args.ssh_target,
|
"sshTarget": args.ssh_target,
|
||||||
})
|
})
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
@ -532,7 +832,10 @@ def run_experiment(args: argparse.Namespace) -> None:
|
|||||||
"triage": str(run_root / "experiments" / "sequence" / "sequence-triage" / "sequence-triage.json"),
|
"triage": str(run_root / "experiments" / "sequence" / "sequence-triage" / "sequence-triage.json"),
|
||||||
}, indent=2))
|
}, indent=2))
|
||||||
return
|
return
|
||||||
prepare_remote(args.ssh_target, remote_root, needs_rpki_client=(left["rpKind"] == "rpki-client" or right["rpKind"] == "rpki-client"))
|
prepared_remotes: dict[tuple[str, str], bool] = {}
|
||||||
|
prepare_remote_once(prepared_remotes, left_ssh_target, left_remote_root, needs_rpki_client=(left["rpKind"] == "rpki-client"))
|
||||||
|
prepare_remote_once(prepared_remotes, right_ssh_target, right_remote_root, needs_rpki_client=(right["rpKind"] == "rpki-client"))
|
||||||
|
prepare_remote_once(prepared_remotes, analysis_ssh_target, analysis_remote_root, needs_rpki_client=False)
|
||||||
local_exp_root = run_root / "experiments" / "sequence"
|
local_exp_root = run_root / "experiments" / "sequence"
|
||||||
left_seq_path = local_exp_root / "left-sequence.jsonl"
|
left_seq_path = local_exp_root / "left-sequence.jsonl"
|
||||||
right_seq_path = local_exp_root / "right-sequence.jsonl"
|
right_seq_path = local_exp_root / "right-sequence.jsonl"
|
||||||
@ -541,27 +844,63 @@ def run_experiment(args: argparse.Namespace) -> None:
|
|||||||
progress: list[dict[str, Any]] = []
|
progress: list[dict[str, Any]] = []
|
||||||
if args.schedule_mode == "interleaved":
|
if args.schedule_mode == "interleaved":
|
||||||
for seq in range(1, args.samples_per_side + 1):
|
for seq in range(1, args.samples_per_side + 1):
|
||||||
for side_label, side_name, side, seq_path in [
|
for side_label, ssh_target, side_remote_root, side_name, side, seq_path in [
|
||||||
("A", args.left, left, left_seq_path),
|
("A", left_ssh_target, left_remote_root, args.left, left, left_seq_path),
|
||||||
("B", args.right, right, right_seq_path),
|
("B", right_ssh_target, right_remote_root, args.right, right, right_seq_path),
|
||||||
]:
|
]:
|
||||||
progress.append(
|
progress.append(
|
||||||
run_one_side_sample(args, remote_root, local_exp_root, side_label, side_name, side, seq_path, seq, rirs)
|
run_one_side_sample(
|
||||||
|
args,
|
||||||
|
ssh_target,
|
||||||
|
side_remote_root,
|
||||||
|
local_exp_root,
|
||||||
|
side_label,
|
||||||
|
side_name,
|
||||||
|
side,
|
||||||
|
seq_path,
|
||||||
|
seq,
|
||||||
|
rirs,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||||
futures = [
|
futures = [
|
||||||
executor.submit(run_side_sequence, args, remote_root, local_exp_root, "A", args.left, left, left_seq_path, rirs),
|
executor.submit(run_side_sequence, args, left_ssh_target, left_remote_root, local_exp_root, "A", args.left, left, left_seq_path, rirs),
|
||||||
executor.submit(run_side_sequence, args, remote_root, local_exp_root, "B", args.right, right, right_seq_path, rirs),
|
executor.submit(run_side_sequence, args, right_ssh_target, right_remote_root, local_exp_root, "B", args.right, right, right_seq_path, rirs),
|
||||||
]
|
]
|
||||||
for future in as_completed(futures):
|
for future in as_completed(futures):
|
||||||
progress.extend(future.result())
|
progress.extend(future.result())
|
||||||
progress.sort(key=lambda item: (str(item.get("side")), int(item.get("seq") or 0)))
|
progress.sort(key=lambda item: (str(item.get("side")), int(item.get("seq") or 0)))
|
||||||
write_json(local_exp_root / "run-progress.json", progress)
|
write_json(local_exp_root / "run-progress.json", progress)
|
||||||
run_sequence_triage(local_exp_root, args)
|
if args.remote_triage:
|
||||||
ssh_script(args.ssh_target, f"df -h /data / > {shlex.quote(str(remote_root / 'df-after.txt'))} 2>&1 || true; free -h > {shlex.quote(str(remote_root / 'free-after.txt'))} 2>&1 || true")
|
sync_side_to_analysis_remote(left_ssh_target, left_remote_root, analysis_ssh_target, analysis_remote_root, "A")
|
||||||
|
sync_side_to_analysis_remote(right_ssh_target, right_remote_root, analysis_ssh_target, analysis_remote_root, "B")
|
||||||
|
remote_exp_root = analysis_remote_root / "experiments" / "sequence"
|
||||||
|
remote_progress = json.dumps(progress, sort_keys=True, ensure_ascii=False)
|
||||||
|
ssh_script(
|
||||||
|
analysis_ssh_target,
|
||||||
|
"set -euo pipefail; "
|
||||||
|
f"cat > {shlex.quote(str(remote_exp_root / 'run-progress.json'))} <<'REMOTE_PROGRESS_JSON'\n"
|
||||||
|
f"{remote_progress}\n"
|
||||||
|
"REMOTE_PROGRESS_JSON\n",
|
||||||
|
)
|
||||||
|
run_sequence_triage_remote(analysis_ssh_target, analysis_remote_root, args)
|
||||||
|
if args.fetch_remote_analysis:
|
||||||
|
rsync_remote_analysis_from_remote(analysis_ssh_target, remote_exp_root, local_exp_root)
|
||||||
|
else:
|
||||||
|
run_sequence_triage(local_exp_root, args)
|
||||||
|
ssh_script(analysis_ssh_target, f"df -h /data / > {shlex.quote(str(analysis_remote_root / 'df-after.txt'))} 2>&1 || true; free -h > {shlex.quote(str(analysis_remote_root / 'free-after.txt'))} 2>&1 || true")
|
||||||
compare_dir = local_exp_root / "sequence-triage"
|
compare_dir = local_exp_root / "sequence-triage"
|
||||||
print(json.dumps({"runRoot": str(run_root), "remoteRoot": str(remote_root), "triage": str(compare_dir / "sequence-triage.json")}, indent=2))
|
remote_compare_dir = analysis_remote_root / "experiments" / "sequence" / "sequence-triage"
|
||||||
|
print(json.dumps({
|
||||||
|
"runRoot": str(run_root),
|
||||||
|
"remoteRoot": str(remote_root),
|
||||||
|
"leftRemoteRoot": str(left_remote_root),
|
||||||
|
"rightRemoteRoot": str(right_remote_root),
|
||||||
|
"analysisRemoteRoot": str(analysis_remote_root),
|
||||||
|
"triage": str(compare_dir / "sequence-triage.json") if not args.remote_triage or args.fetch_remote_analysis else None,
|
||||||
|
"remoteTriage": str(remote_compare_dir / "sequence-triage.json") if args.remote_triage else None,
|
||||||
|
}, indent=2))
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@ -569,6 +908,12 @@ def main() -> None:
|
|||||||
parser.add_argument("--run-root", required=True)
|
parser.add_argument("--run-root", required=True)
|
||||||
parser.add_argument("--remote-root", required=True)
|
parser.add_argument("--remote-root", required=True)
|
||||||
parser.add_argument("--ssh-target", default=os.environ.get("SSH_TARGET", "root@47.251.56.108"))
|
parser.add_argument("--ssh-target", default=os.environ.get("SSH_TARGET", "root@47.251.56.108"))
|
||||||
|
parser.add_argument("--left-ssh-target")
|
||||||
|
parser.add_argument("--right-ssh-target")
|
||||||
|
parser.add_argument("--analysis-ssh-target")
|
||||||
|
parser.add_argument("--left-remote-root")
|
||||||
|
parser.add_argument("--right-remote-root")
|
||||||
|
parser.add_argument("--analysis-remote-root")
|
||||||
parser.add_argument("--left", default="ours-standard")
|
parser.add_argument("--left", default="ours-standard")
|
||||||
parser.add_argument("--right", default="rpki-client-standard")
|
parser.add_argument("--right", default="rpki-client-standard")
|
||||||
parser.add_argument("--samples-per-side", type=int, default=3)
|
parser.add_argument("--samples-per-side", type=int, default=3)
|
||||||
@ -581,6 +926,9 @@ def main() -> None:
|
|||||||
parser.add_argument("--dry-run", action="store_true")
|
parser.add_argument("--dry-run", action="store_true")
|
||||||
parser.add_argument("--skip-build", action="store_true", help="reuse existing release binaries")
|
parser.add_argument("--skip-build", action="store_true", help="reuse existing release binaries")
|
||||||
parser.add_argument("--triage-only", action="store_true", help="only rerun local sequence triage for an existing run root")
|
parser.add_argument("--triage-only", action="store_true", help="only rerun local sequence triage for an existing run root")
|
||||||
|
parser.add_argument("--remote-triage", action="store_true", help="keep CIR/CCR on remote, write sequence JSONL remotely, and run triage on remote")
|
||||||
|
parser.add_argument("--fetch-remote-analysis", action="store_true", help="when --remote-triage is set, fetch only small sequence/triage JSON outputs; never fetch CIR/CCR")
|
||||||
|
parser.add_argument("--cleanup-run-nonessential", action="store_true", help="after each successful remote sequence item, remove report/log/CSV files and keep only CIR/CCR/timing/meta")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.samples_per_side < 2:
|
if args.samples_per_side < 2:
|
||||||
raise SystemExit("--samples-per-side must be >= 2")
|
raise SystemExit("--samples-per-side must be >= 2")
|
||||||
|
|||||||
@ -389,7 +389,6 @@ def fmt_db_stats(db: dict) -> str:
|
|||||||
"repository_view",
|
"repository_view",
|
||||||
"raw_by_hash",
|
"raw_by_hash",
|
||||||
"vcir",
|
"vcir",
|
||||||
"audit_rule_index",
|
|
||||||
"rrdp_source",
|
"rrdp_source",
|
||||||
"rrdp_source_member",
|
"rrdp_source_member",
|
||||||
"rrdp_uri_owner",
|
"rrdp_uri_owner",
|
||||||
|
|||||||
@ -1,893 +0,0 @@
|
|||||||
use crate::data_model::aspa::AspaObject;
|
|
||||||
use crate::data_model::manifest::ManifestObject;
|
|
||||||
use crate::data_model::roa::RoaObject;
|
|
||||||
use crate::storage::{
|
|
||||||
AuditRuleIndexEntry, AuditRuleKind, RawByHashEntry, RocksStore, ValidatedCaInstanceResult,
|
|
||||||
VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirLocalOutput,
|
|
||||||
VcirOutputType,
|
|
||||||
};
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum AuditTraceError {
|
|
||||||
#[error("storage error: {0}")]
|
|
||||||
Storage(#[from] crate::storage::StorageError),
|
|
||||||
|
|
||||||
#[error("audit rule index points to missing VCIR: {manifest_rsync_uri}")]
|
|
||||||
MissingVcir { manifest_rsync_uri: String },
|
|
||||||
|
|
||||||
#[error(
|
|
||||||
"audit rule index points to missing local output: rule_hash={rule_hash}, output_id={output_id}, manifest={manifest_rsync_uri}"
|
|
||||||
)]
|
|
||||||
MissingLocalOutput {
|
|
||||||
rule_hash: String,
|
|
||||||
output_id: String,
|
|
||||||
manifest_rsync_uri: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[error("detected VCIR parent cycle at {manifest_rsync_uri}")]
|
|
||||||
ParentCycle { manifest_rsync_uri: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
|
||||||
pub struct AuditTraceRawRef {
|
|
||||||
pub sha256_hex: String,
|
|
||||||
pub raw_present: bool,
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
|
||||||
pub origin_uris: Vec<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub object_type: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub byte_len: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
|
||||||
pub struct AuditTraceArtifact {
|
|
||||||
pub artifact_role: VcirArtifactRole,
|
|
||||||
pub artifact_kind: VcirArtifactKind,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub uri: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub object_type: Option<String>,
|
|
||||||
pub validation_status: VcirArtifactValidationStatus,
|
|
||||||
pub raw: AuditTraceRawRef,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
|
||||||
pub struct AuditTraceChainNode {
|
|
||||||
pub manifest_rsync_uri: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub parent_manifest_rsync_uri: Option<String>,
|
|
||||||
pub tal_id: String,
|
|
||||||
pub ca_subject_name: String,
|
|
||||||
pub ca_ski: String,
|
|
||||||
pub issuer_ski: String,
|
|
||||||
pub current_manifest_rsync_uri: String,
|
|
||||||
pub current_crl_rsync_uri: String,
|
|
||||||
pub last_successful_validation_time_rfc3339_utc: String,
|
|
||||||
pub local_output_count: usize,
|
|
||||||
pub child_count: usize,
|
|
||||||
pub related_artifacts: Vec<AuditTraceArtifact>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
|
||||||
pub struct AuditTraceResolvedOutput {
|
|
||||||
pub output_id: String,
|
|
||||||
pub output_type: VcirOutputType,
|
|
||||||
pub rule_hash: String,
|
|
||||||
pub source_object_uri: String,
|
|
||||||
pub source_object_type: String,
|
|
||||||
pub source_object_hash: String,
|
|
||||||
pub source_ee_cert_hash: String,
|
|
||||||
pub item_effective_until_rfc3339_utc: String,
|
|
||||||
pub payload_json: String,
|
|
||||||
pub validation_path_hint: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
|
||||||
pub struct AuditRuleTrace {
|
|
||||||
pub rule: AuditRuleIndexEntry,
|
|
||||||
pub resolved_output: AuditTraceResolvedOutput,
|
|
||||||
pub source_object_raw: AuditTraceRawRef,
|
|
||||||
pub source_ee_cert_raw: AuditTraceRawRef,
|
|
||||||
pub chain_leaf_to_root: Vec<AuditTraceChainNode>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trace_rule_to_root(
|
|
||||||
store: &RocksStore,
|
|
||||||
kind: AuditRuleKind,
|
|
||||||
rule_hash: &str,
|
|
||||||
) -> Result<Option<AuditRuleTrace>, AuditTraceError> {
|
|
||||||
let Some(rule) = store.get_audit_rule_index_entry(kind, rule_hash)? else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(leaf_vcir) = store.get_vcir(&rule.manifest_rsync_uri)? else {
|
|
||||||
return Err(AuditTraceError::MissingVcir {
|
|
||||||
manifest_rsync_uri: rule.manifest_rsync_uri.clone(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(local_output) = leaf_vcir
|
|
||||||
.local_outputs
|
|
||||||
.iter()
|
|
||||||
.find(|output| output.output_id == rule.output_id && output.rule_hash == rule.rule_hash)
|
|
||||||
.or_else(|| {
|
|
||||||
leaf_vcir
|
|
||||||
.local_outputs
|
|
||||||
.iter()
|
|
||||||
.find(|output| output.rule_hash == rule.rule_hash)
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
else {
|
|
||||||
return Err(AuditTraceError::MissingLocalOutput {
|
|
||||||
rule_hash: rule.rule_hash.clone(),
|
|
||||||
output_id: rule.output_id.clone(),
|
|
||||||
manifest_rsync_uri: rule.manifest_rsync_uri.clone(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let chain = trace_vcir_chain_to_root(store, &leaf_vcir.manifest_rsync_uri)?
|
|
||||||
.expect("leaf VCIR already loaded must exist");
|
|
||||||
|
|
||||||
Ok(Some(AuditRuleTrace {
|
|
||||||
rule,
|
|
||||||
resolved_output: resolved_output_from_local(&local_output),
|
|
||||||
source_object_raw: resolve_raw_ref(
|
|
||||||
store,
|
|
||||||
&local_output.source_object_hash,
|
|
||||||
Some(&local_output.source_object_uri),
|
|
||||||
Some(local_output.source_object_type.as_str()),
|
|
||||||
)?,
|
|
||||||
source_ee_cert_raw: resolve_source_ee_cert_raw_ref(store, &local_output)?,
|
|
||||||
chain_leaf_to_root: chain,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trace_vcir_chain_to_root(
|
|
||||||
store: &RocksStore,
|
|
||||||
manifest_rsync_uri: &str,
|
|
||||||
) -> Result<Option<Vec<AuditTraceChainNode>>, AuditTraceError> {
|
|
||||||
let Some(mut current) = store.get_vcir(manifest_rsync_uri)? else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut seen = HashSet::new();
|
|
||||||
let mut chain = Vec::new();
|
|
||||||
loop {
|
|
||||||
if !seen.insert(current.manifest_rsync_uri.clone()) {
|
|
||||||
return Err(AuditTraceError::ParentCycle {
|
|
||||||
manifest_rsync_uri: current.manifest_rsync_uri,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let parent = current.parent_manifest_rsync_uri.clone();
|
|
||||||
chain.push(trace_chain_node(store, ¤t)?);
|
|
||||||
let Some(parent_manifest_rsync_uri) = parent else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
let Some(parent_vcir) = store.get_vcir(&parent_manifest_rsync_uri)? else {
|
|
||||||
return Err(AuditTraceError::MissingVcir {
|
|
||||||
manifest_rsync_uri: parent_manifest_rsync_uri,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
current = parent_vcir;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(chain))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trace_chain_node(
|
|
||||||
store: &RocksStore,
|
|
||||||
vcir: &ValidatedCaInstanceResult,
|
|
||||||
) -> Result<AuditTraceChainNode, AuditTraceError> {
|
|
||||||
let mut related_artifacts = Vec::with_capacity(vcir.related_artifacts.len());
|
|
||||||
for artifact in &vcir.related_artifacts {
|
|
||||||
related_artifacts.push(AuditTraceArtifact {
|
|
||||||
artifact_role: artifact.artifact_role,
|
|
||||||
artifact_kind: artifact.artifact_kind,
|
|
||||||
uri: artifact.uri.clone(),
|
|
||||||
object_type: artifact.object_type.clone(),
|
|
||||||
validation_status: artifact.validation_status,
|
|
||||||
raw: resolve_raw_ref(
|
|
||||||
store,
|
|
||||||
&artifact.sha256,
|
|
||||||
artifact.uri.as_deref(),
|
|
||||||
artifact.object_type.as_deref(),
|
|
||||||
)?,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(AuditTraceChainNode {
|
|
||||||
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
|
|
||||||
parent_manifest_rsync_uri: vcir.parent_manifest_rsync_uri.clone(),
|
|
||||||
tal_id: vcir.tal_id.clone(),
|
|
||||||
ca_subject_name: vcir.ca_subject_name.clone(),
|
|
||||||
ca_ski: vcir.ca_ski.clone(),
|
|
||||||
issuer_ski: vcir.issuer_ski.clone(),
|
|
||||||
current_manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
|
|
||||||
current_crl_rsync_uri: vcir.current_crl_rsync_uri.clone(),
|
|
||||||
last_successful_validation_time_rfc3339_utc: vcir
|
|
||||||
.last_successful_validation_time
|
|
||||||
.rfc3339_utc
|
|
||||||
.clone(),
|
|
||||||
local_output_count: vcir.local_outputs.len(),
|
|
||||||
child_count: vcir.child_entries.len(),
|
|
||||||
related_artifacts,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolved_output_from_local(local: &VcirLocalOutput) -> AuditTraceResolvedOutput {
|
|
||||||
AuditTraceResolvedOutput {
|
|
||||||
output_id: local.output_id.clone(),
|
|
||||||
output_type: local.output_type,
|
|
||||||
rule_hash: local.rule_hash.clone(),
|
|
||||||
source_object_uri: local.source_object_uri.clone(),
|
|
||||||
source_object_type: local.source_object_type.clone(),
|
|
||||||
source_object_hash: local.source_object_hash.clone(),
|
|
||||||
source_ee_cert_hash: local.source_ee_cert_hash.clone(),
|
|
||||||
item_effective_until_rfc3339_utc: local.item_effective_until.rfc3339_utc.clone(),
|
|
||||||
payload_json: local.payload_json.clone(),
|
|
||||||
validation_path_hint: local.validation_path_hint.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_raw_ref(
|
|
||||||
store: &RocksStore,
|
|
||||||
sha256_hex: &str,
|
|
||||||
fallback_uri: Option<&str>,
|
|
||||||
fallback_object_type: Option<&str>,
|
|
||||||
) -> Result<AuditTraceRawRef, AuditTraceError> {
|
|
||||||
let raw = store.get_raw_by_hash_entry(sha256_hex)?;
|
|
||||||
if raw.is_some() {
|
|
||||||
return Ok(raw_ref_from_entry(sha256_hex, raw.as_ref()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let blob = store.get_blob_bytes(sha256_hex)?;
|
|
||||||
match blob {
|
|
||||||
Some(bytes) => Ok(AuditTraceRawRef {
|
|
||||||
sha256_hex: sha256_hex.to_string(),
|
|
||||||
raw_present: true,
|
|
||||||
origin_uris: fallback_uri
|
|
||||||
.map(|uri| vec![uri.to_string()])
|
|
||||||
.unwrap_or_default(),
|
|
||||||
object_type: fallback_object_type.map(str::to_string),
|
|
||||||
byte_len: Some(bytes.len()),
|
|
||||||
}),
|
|
||||||
None => Ok(raw_ref_from_entry(sha256_hex, None)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_source_ee_cert_raw_ref(
|
|
||||||
store: &RocksStore,
|
|
||||||
local: &VcirLocalOutput,
|
|
||||||
) -> Result<AuditTraceRawRef, AuditTraceError> {
|
|
||||||
let raw = store.get_raw_by_hash_entry(&local.source_ee_cert_hash)?;
|
|
||||||
if raw.is_some() {
|
|
||||||
return Ok(raw_ref_from_entry(&local.source_ee_cert_hash, raw.as_ref()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let source_bytes = store.get_blob_bytes(&local.source_object_hash)?;
|
|
||||||
let Some(source_bytes) = source_bytes else {
|
|
||||||
return Ok(raw_ref_from_entry(&local.source_ee_cert_hash, None));
|
|
||||||
};
|
|
||||||
|
|
||||||
let derived = match local.source_object_type.as_str() {
|
|
||||||
"roa" => RoaObject::decode_der(&source_bytes).ok().and_then(|roa| {
|
|
||||||
roa.signed_object
|
|
||||||
.signed_data
|
|
||||||
.certificates
|
|
||||||
.first()
|
|
||||||
.map(|cert| cert.raw_der.to_vec())
|
|
||||||
}),
|
|
||||||
"aspa" => AspaObject::decode_der(&source_bytes).ok().and_then(|aspa| {
|
|
||||||
aspa.signed_object
|
|
||||||
.signed_data
|
|
||||||
.certificates
|
|
||||||
.first()
|
|
||||||
.map(|cert| cert.raw_der.to_vec())
|
|
||||||
}),
|
|
||||||
"mft" => ManifestObject::decode_der(&source_bytes)
|
|
||||||
.ok()
|
|
||||||
.and_then(|manifest| {
|
|
||||||
manifest
|
|
||||||
.signed_object
|
|
||||||
.signed_data
|
|
||||||
.certificates
|
|
||||||
.first()
|
|
||||||
.map(|cert| cert.raw_der.to_vec())
|
|
||||||
}),
|
|
||||||
"router_key" => Some(source_bytes),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(ee_der) = derived else {
|
|
||||||
return Ok(raw_ref_from_entry(&local.source_ee_cert_hash, None));
|
|
||||||
};
|
|
||||||
if crate::audit::sha256_hex(ee_der.as_slice()) != local.source_ee_cert_hash {
|
|
||||||
return Ok(raw_ref_from_entry(&local.source_ee_cert_hash, None));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(AuditTraceRawRef {
|
|
||||||
sha256_hex: local.source_ee_cert_hash.clone(),
|
|
||||||
raw_present: true,
|
|
||||||
origin_uris: Vec::new(),
|
|
||||||
object_type: Some("cer".to_string()),
|
|
||||||
byte_len: Some(ee_der.len()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn raw_ref_from_entry(sha256_hex: &str, entry: Option<&RawByHashEntry>) -> AuditTraceRawRef {
|
|
||||||
match entry {
|
|
||||||
Some(entry) => AuditTraceRawRef {
|
|
||||||
sha256_hex: sha256_hex.to_string(),
|
|
||||||
raw_present: true,
|
|
||||||
origin_uris: entry.origin_uris.clone(),
|
|
||||||
object_type: entry.object_type.clone(),
|
|
||||||
byte_len: Some(entry.bytes.len()),
|
|
||||||
},
|
|
||||||
None => AuditTraceRawRef {
|
|
||||||
sha256_hex: sha256_hex.to_string(),
|
|
||||||
raw_present: false,
|
|
||||||
origin_uris: Vec::new(),
|
|
||||||
object_type: None,
|
|
||||||
byte_len: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::audit::sha256_hex;
|
|
||||||
use crate::data_model::roa::RoaObject;
|
|
||||||
use crate::storage::{
|
|
||||||
PackTime, ValidatedManifestMeta, VcirAuditSummary, VcirCcrManifestProjection,
|
|
||||||
VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
|
|
||||||
};
|
|
||||||
use base64::Engine as _;
|
|
||||||
|
|
||||||
fn sample_vcir(
|
|
||||||
manifest_rsync_uri: &str,
|
|
||||||
parent_manifest_rsync_uri: Option<&str>,
|
|
||||||
tal_id: &str,
|
|
||||||
local_output: Option<VcirLocalOutput>,
|
|
||||||
related_artifacts: Vec<VcirRelatedArtifact>,
|
|
||||||
) -> ValidatedCaInstanceResult {
|
|
||||||
let now = time::OffsetDateTime::now_utc();
|
|
||||||
let next = PackTime::from_utc_offset_datetime(now + time::Duration::hours(1));
|
|
||||||
let local_outputs: Vec<VcirLocalOutput> = local_output.into_iter().collect();
|
|
||||||
let ccr_manifest_projection = VcirCcrManifestProjection {
|
|
||||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
|
||||||
manifest_sha256: vec![0x44; 32],
|
|
||||||
manifest_size: 2048,
|
|
||||||
manifest_ee_aki: vec![0x55; 20],
|
|
||||||
manifest_number_be: vec![1],
|
|
||||||
manifest_this_update: PackTime::from_utc_offset_datetime(now),
|
|
||||||
manifest_sia_locations_der: vec![vec![
|
|
||||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
|
||||||
b'r', b's', b'y', b'n', b'c',
|
|
||||||
]],
|
|
||||||
subordinate_skis: vec![vec![0x33; 20]],
|
|
||||||
};
|
|
||||||
ValidatedCaInstanceResult {
|
|
||||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
|
||||||
parent_manifest_rsync_uri: parent_manifest_rsync_uri.map(str::to_string),
|
|
||||||
tal_id: tal_id.to_string(),
|
|
||||||
ca_subject_name: format!("CN={manifest_rsync_uri}"),
|
|
||||||
ca_ski: "11".repeat(20),
|
|
||||||
issuer_ski: "22".repeat(20),
|
|
||||||
last_successful_validation_time: PackTime::from_utc_offset_datetime(now),
|
|
||||||
current_manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
|
||||||
current_crl_rsync_uri: manifest_rsync_uri.replace(".mft", ".crl"),
|
|
||||||
validated_manifest_meta: ValidatedManifestMeta {
|
|
||||||
validated_manifest_number: vec![1],
|
|
||||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(now),
|
|
||||||
validated_manifest_next_update: next.clone(),
|
|
||||||
},
|
|
||||||
ccr_manifest_projection,
|
|
||||||
instance_gate: VcirInstanceGate {
|
|
||||||
manifest_next_update: next.clone(),
|
|
||||||
current_crl_next_update: next.clone(),
|
|
||||||
self_ca_not_after: PackTime::from_utc_offset_datetime(
|
|
||||||
now + time::Duration::hours(2),
|
|
||||||
),
|
|
||||||
instance_effective_until: next,
|
|
||||||
},
|
|
||||||
child_entries: vec![VcirChildEntry {
|
|
||||||
child_manifest_rsync_uri: "rsync://example.test/child/child.mft".to_string(),
|
|
||||||
child_cert_rsync_uri: "rsync://example.test/parent/child.cer".to_string(),
|
|
||||||
child_cert_hash: sha256_hex(b"child-cert"),
|
|
||||||
child_ski: "33".repeat(20),
|
|
||||||
child_rsync_base_uri: "rsync://example.test/child/".to_string(),
|
|
||||||
child_publication_point_rsync_uri: "rsync://example.test/child/".to_string(),
|
|
||||||
child_rrdp_notification_uri: Some(
|
|
||||||
"https://example.test/child/notify.xml".to_string(),
|
|
||||||
),
|
|
||||||
child_effective_ip_resources: None,
|
|
||||||
child_effective_as_resources: None,
|
|
||||||
accepted_at_validation_time: PackTime::from_utc_offset_datetime(now),
|
|
||||||
}],
|
|
||||||
summary: VcirSummary {
|
|
||||||
local_vrp_count: local_outputs
|
|
||||||
.iter()
|
|
||||||
.filter(|output| output.output_type == VcirOutputType::Vrp)
|
|
||||||
.count() as u32,
|
|
||||||
local_aspa_count: local_outputs
|
|
||||||
.iter()
|
|
||||||
.filter(|output| output.output_type == VcirOutputType::Aspa)
|
|
||||||
.count() as u32,
|
|
||||||
local_router_key_count: local_outputs
|
|
||||||
.iter()
|
|
||||||
.filter(|output| output.output_type == VcirOutputType::RouterKey)
|
|
||||||
.count() as u32,
|
|
||||||
child_count: 1,
|
|
||||||
accepted_object_count: related_artifacts.len() as u32,
|
|
||||||
rejected_object_count: 0,
|
|
||||||
},
|
|
||||||
local_outputs,
|
|
||||||
related_artifacts,
|
|
||||||
audit_summary: VcirAuditSummary {
|
|
||||||
failed_fetch_eligible: true,
|
|
||||||
last_failed_fetch_reason: None,
|
|
||||||
warning_count: 0,
|
|
||||||
audit_flags: Vec::new(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_local_output(manifest_rsync_uri: &str) -> VcirLocalOutput {
|
|
||||||
let now = time::OffsetDateTime::now_utc();
|
|
||||||
VcirLocalOutput {
|
|
||||||
output_id: sha256_hex(b"vrp-output"),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(
|
|
||||||
now + time::Duration::minutes(30),
|
|
||||||
),
|
|
||||||
source_object_uri: "rsync://example.test/leaf/a.roa".to_string(),
|
|
||||||
source_object_type: "roa".to_string(),
|
|
||||||
source_object_hash: sha256_hex(b"roa-raw"),
|
|
||||||
source_ee_cert_hash: sha256_hex(b"roa-ee"),
|
|
||||||
payload_json:
|
|
||||||
serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24})
|
|
||||||
.to_string(),
|
|
||||||
rule_hash: sha256_hex(b"roa-rule"),
|
|
||||||
validation_path_hint: vec![
|
|
||||||
manifest_rsync_uri.to_string(),
|
|
||||||
"rsync://example.test/leaf/a.roa".to_string(),
|
|
||||||
sha256_hex(b"roa-raw"),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_artifacts(manifest_rsync_uri: &str, roa_hash: &str) -> Vec<VcirRelatedArtifact> {
|
|
||||||
vec![
|
|
||||||
VcirRelatedArtifact {
|
|
||||||
artifact_role: VcirArtifactRole::Manifest,
|
|
||||||
artifact_kind: VcirArtifactKind::Mft,
|
|
||||||
uri: Some(manifest_rsync_uri.to_string()),
|
|
||||||
sha256: sha256_hex(manifest_rsync_uri.as_bytes()),
|
|
||||||
object_type: Some("mft".to_string()),
|
|
||||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
||||||
},
|
|
||||||
VcirRelatedArtifact {
|
|
||||||
artifact_role: VcirArtifactRole::CurrentCrl,
|
|
||||||
artifact_kind: VcirArtifactKind::Crl,
|
|
||||||
uri: Some(manifest_rsync_uri.replace(".mft", ".crl")),
|
|
||||||
sha256: sha256_hex(format!("{}-crl", manifest_rsync_uri).as_bytes()),
|
|
||||||
object_type: Some("crl".to_string()),
|
|
||||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
||||||
},
|
|
||||||
VcirRelatedArtifact {
|
|
||||||
artifact_role: VcirArtifactRole::SignedObject,
|
|
||||||
artifact_kind: VcirArtifactKind::Roa,
|
|
||||||
uri: Some("rsync://example.test/leaf/a.roa".to_string()),
|
|
||||||
sha256: roa_hash.to_string(),
|
|
||||||
object_type: Some("roa".to_string()),
|
|
||||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn put_raw_evidence(store: &RocksStore, bytes: &[u8], uri: &str, object_type: &str) {
|
|
||||||
let mut entry = RawByHashEntry::from_bytes(sha256_hex(bytes), bytes.to_vec());
|
|
||||||
entry.origin_uris.push(uri.to_string());
|
|
||||||
entry.object_type = Some(object_type.to_string());
|
|
||||||
entry.encoding = Some("der".to_string());
|
|
||||||
store
|
|
||||||
.put_raw_by_hash_entry(&entry)
|
|
||||||
.expect("put raw evidence");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn put_blob_only(store: &RocksStore, bytes: &[u8]) {
|
|
||||||
store
|
|
||||||
.put_blob_bytes_batch(&[(sha256_hex(bytes), bytes.to_vec())])
|
|
||||||
.expect("put blob bytes");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_rule_to_root_returns_leaf_to_root_chain_and_evidence_refs() {
|
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
|
||||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
|
||||||
|
|
||||||
let root_manifest = "rsync://example.test/root/root.mft";
|
|
||||||
let leaf_manifest = "rsync://example.test/leaf/leaf.mft";
|
|
||||||
let local = sample_local_output(leaf_manifest);
|
|
||||||
let leaf_vcir = sample_vcir(
|
|
||||||
leaf_manifest,
|
|
||||||
Some(root_manifest),
|
|
||||||
"test-tal",
|
|
||||||
Some(local.clone()),
|
|
||||||
sample_artifacts(leaf_manifest, &local.source_object_hash),
|
|
||||||
);
|
|
||||||
let root_vcir = sample_vcir(
|
|
||||||
root_manifest,
|
|
||||||
None,
|
|
||||||
"test-tal",
|
|
||||||
None,
|
|
||||||
sample_artifacts(root_manifest, &sha256_hex(b"root-object")),
|
|
||||||
);
|
|
||||||
store.put_vcir(&leaf_vcir).expect("put leaf vcir");
|
|
||||||
store.put_vcir(&root_vcir).expect("put root vcir");
|
|
||||||
|
|
||||||
let rule_entry = AuditRuleIndexEntry {
|
|
||||||
kind: AuditRuleKind::Roa,
|
|
||||||
rule_hash: local.rule_hash.clone(),
|
|
||||||
manifest_rsync_uri: leaf_manifest.to_string(),
|
|
||||||
source_object_uri: local.source_object_uri.clone(),
|
|
||||||
source_object_hash: local.source_object_hash.clone(),
|
|
||||||
output_id: local.output_id.clone(),
|
|
||||||
item_effective_until: local.item_effective_until.clone(),
|
|
||||||
};
|
|
||||||
store
|
|
||||||
.put_audit_rule_index_entry(&rule_entry)
|
|
||||||
.expect("put rule index");
|
|
||||||
|
|
||||||
put_raw_evidence(&store, leaf_manifest.as_bytes(), leaf_manifest, "mft");
|
|
||||||
put_raw_evidence(
|
|
||||||
&store,
|
|
||||||
format!("{}-crl", leaf_manifest).as_bytes(),
|
|
||||||
&leaf_manifest.replace(".mft", ".crl"),
|
|
||||||
"crl",
|
|
||||||
);
|
|
||||||
put_raw_evidence(&store, b"roa-raw", &local.source_object_uri, "roa");
|
|
||||||
put_raw_evidence(&store, b"roa-ee", "rsync://example.test/leaf/a.ee", "cer");
|
|
||||||
put_raw_evidence(&store, root_manifest.as_bytes(), root_manifest, "mft");
|
|
||||||
put_raw_evidence(
|
|
||||||
&store,
|
|
||||||
format!("{}-crl", root_manifest).as_bytes(),
|
|
||||||
&root_manifest.replace(".mft", ".crl"),
|
|
||||||
"crl",
|
|
||||||
);
|
|
||||||
|
|
||||||
let trace = trace_rule_to_root(&store, AuditRuleKind::Roa, &local.rule_hash)
|
|
||||||
.expect("trace rule")
|
|
||||||
.expect("trace exists");
|
|
||||||
|
|
||||||
assert_eq!(trace.rule, rule_entry);
|
|
||||||
assert_eq!(trace.resolved_output.output_id, local.output_id);
|
|
||||||
assert_eq!(trace.chain_leaf_to_root.len(), 2);
|
|
||||||
assert_eq!(
|
|
||||||
trace.chain_leaf_to_root[0].manifest_rsync_uri,
|
|
||||||
leaf_manifest
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
trace.chain_leaf_to_root[1].manifest_rsync_uri,
|
|
||||||
root_manifest
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
trace.chain_leaf_to_root[0]
|
|
||||||
.parent_manifest_rsync_uri
|
|
||||||
.as_deref(),
|
|
||||||
Some(root_manifest)
|
|
||||||
);
|
|
||||||
assert!(trace.source_object_raw.raw_present);
|
|
||||||
assert!(trace.source_ee_cert_raw.raw_present);
|
|
||||||
assert!(
|
|
||||||
trace.chain_leaf_to_root[0]
|
|
||||||
.related_artifacts
|
|
||||||
.iter()
|
|
||||||
.any(|artifact| {
|
|
||||||
artifact.uri.as_deref() == Some(leaf_manifest) && artifact.raw.raw_present
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_rule_to_root_supports_router_key_rules() {
|
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
|
||||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
|
||||||
let manifest = "rsync://example.test/router/leaf.mft";
|
|
||||||
let mut local = sample_local_output(manifest);
|
|
||||||
local.output_type = VcirOutputType::RouterKey;
|
|
||||||
local.source_object_uri = "rsync://example.test/router/router.cer".to_string();
|
|
||||||
local.source_object_type = "router_key".to_string();
|
|
||||||
local.payload_json = serde_json::json!({
|
|
||||||
"as_id": 64496,
|
|
||||||
"ski_hex": "11".repeat(20),
|
|
||||||
"spki_der_base64": base64::engine::general_purpose::STANDARD.encode([0x30u8, 0x00]),
|
|
||||||
})
|
|
||||||
.to_string();
|
|
||||||
let mut vcir = sample_vcir(
|
|
||||||
manifest,
|
|
||||||
None,
|
|
||||||
"test-tal",
|
|
||||||
Some(local),
|
|
||||||
sample_artifacts(manifest, &sha256_hex(b"router-object")),
|
|
||||||
);
|
|
||||||
vcir.local_outputs[0].output_type = VcirOutputType::RouterKey;
|
|
||||||
vcir.local_outputs[0].source_object_uri =
|
|
||||||
"rsync://example.test/router/router.cer".to_string();
|
|
||||||
vcir.local_outputs[0].source_object_type = "router_key".to_string();
|
|
||||||
vcir.local_outputs[0].payload_json = serde_json::json!({
|
|
||||||
"as_id": 64496,
|
|
||||||
"ski_hex": "11".repeat(20),
|
|
||||||
"spki_der_base64": base64::engine::general_purpose::STANDARD.encode([0x30u8, 0x00]),
|
|
||||||
})
|
|
||||||
.to_string();
|
|
||||||
vcir.summary.local_vrp_count = 0;
|
|
||||||
vcir.summary.local_router_key_count = 1;
|
|
||||||
store.put_vcir(&vcir).expect("put vcir");
|
|
||||||
let rule_entry = AuditRuleIndexEntry {
|
|
||||||
kind: AuditRuleKind::RouterKey,
|
|
||||||
rule_hash: vcir.local_outputs[0].rule_hash.clone(),
|
|
||||||
manifest_rsync_uri: manifest.to_string(),
|
|
||||||
source_object_uri: vcir.local_outputs[0].source_object_uri.clone(),
|
|
||||||
source_object_hash: vcir.local_outputs[0].source_object_hash.clone(),
|
|
||||||
output_id: vcir.local_outputs[0].output_id.clone(),
|
|
||||||
item_effective_until: vcir.local_outputs[0].item_effective_until.clone(),
|
|
||||||
};
|
|
||||||
store
|
|
||||||
.put_audit_rule_index_entry(&rule_entry)
|
|
||||||
.expect("put rule");
|
|
||||||
let trace = trace_rule_to_root(&store, AuditRuleKind::RouterKey, &rule_entry.rule_hash)
|
|
||||||
.expect("trace rule")
|
|
||||||
.expect("trace exists");
|
|
||||||
assert_eq!(trace.rule.kind, AuditRuleKind::RouterKey);
|
|
||||||
assert_eq!(trace.resolved_output.output_type, VcirOutputType::RouterKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_rule_to_root_lazily_derives_source_ee_cert_when_raw_is_missing() {
|
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
|
||||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
|
||||||
let manifest = "rsync://example.test/leaf/leaf.mft";
|
|
||||||
let roa_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
||||||
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142071.roa");
|
|
||||||
let roa_bytes = std::fs::read(&roa_path).expect("read ROA fixture");
|
|
||||||
let roa = RoaObject::decode_der(&roa_bytes).expect("decode ROA fixture");
|
|
||||||
let local = VcirLocalOutput {
|
|
||||||
output_id: sha256_hex(b"lazy-vrp-output"),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(
|
|
||||||
time::OffsetDateTime::now_utc() + time::Duration::minutes(30),
|
|
||||||
),
|
|
||||||
source_object_uri: "rsync://example.test/leaf/a.roa".to_string(),
|
|
||||||
source_object_type: "roa".to_string(),
|
|
||||||
source_object_hash: sha256_hex(&roa_bytes),
|
|
||||||
source_ee_cert_hash: sha256_hex(
|
|
||||||
roa.signed_object.signed_data.certificates[0]
|
|
||||||
.raw_der
|
|
||||||
.as_slice(),
|
|
||||||
),
|
|
||||||
payload_json:
|
|
||||||
serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24})
|
|
||||||
.to_string(),
|
|
||||||
rule_hash: sha256_hex(b"lazy-roa-rule"),
|
|
||||||
validation_path_hint: vec![manifest.to_string()],
|
|
||||||
};
|
|
||||||
let vcir = sample_vcir(
|
|
||||||
manifest,
|
|
||||||
None,
|
|
||||||
"test-tal",
|
|
||||||
Some(local.clone()),
|
|
||||||
sample_artifacts(manifest, &local.source_object_hash),
|
|
||||||
);
|
|
||||||
store.put_vcir(&vcir).expect("put vcir");
|
|
||||||
let rule_entry = AuditRuleIndexEntry {
|
|
||||||
kind: AuditRuleKind::Roa,
|
|
||||||
rule_hash: local.rule_hash.clone(),
|
|
||||||
manifest_rsync_uri: manifest.to_string(),
|
|
||||||
source_object_uri: local.source_object_uri.clone(),
|
|
||||||
source_object_hash: local.source_object_hash.clone(),
|
|
||||||
output_id: local.output_id.clone(),
|
|
||||||
item_effective_until: local.item_effective_until.clone(),
|
|
||||||
};
|
|
||||||
store
|
|
||||||
.put_audit_rule_index_entry(&rule_entry)
|
|
||||||
.expect("put rule index");
|
|
||||||
|
|
||||||
put_raw_evidence(&store, manifest.as_bytes(), manifest, "mft");
|
|
||||||
put_raw_evidence(
|
|
||||||
&store,
|
|
||||||
format!("{}-crl", manifest).as_bytes(),
|
|
||||||
&manifest.replace(".mft", ".crl"),
|
|
||||||
"crl",
|
|
||||||
);
|
|
||||||
put_raw_evidence(&store, &roa_bytes, &local.source_object_uri, "roa");
|
|
||||||
|
|
||||||
let trace = trace_rule_to_root(&store, AuditRuleKind::Roa, &local.rule_hash)
|
|
||||||
.expect("trace rule")
|
|
||||||
.expect("trace exists");
|
|
||||||
assert!(trace.source_object_raw.raw_present);
|
|
||||||
assert!(trace.source_ee_cert_raw.raw_present);
|
|
||||||
assert_eq!(trace.source_ee_cert_raw.object_type.as_deref(), Some("cer"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_rule_to_root_uses_blob_only_fallback_for_source_object_raw() {
|
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
|
||||||
let main_db = store_dir.path().join("main-db");
|
|
||||||
let raw_db = store_dir.path().join("raw-store.db");
|
|
||||||
let store =
|
|
||||||
RocksStore::open_with_external_raw_store(&main_db, &raw_db).expect("open rocksdb");
|
|
||||||
let manifest = "rsync://example.test/leaf/leaf.mft";
|
|
||||||
let roa_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
||||||
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS142071.roa");
|
|
||||||
let roa_bytes = std::fs::read(&roa_path).expect("read ROA fixture");
|
|
||||||
let roa = RoaObject::decode_der(&roa_bytes).expect("decode ROA fixture");
|
|
||||||
let local = VcirLocalOutput {
|
|
||||||
output_id: sha256_hex(b"blob-only-vrp-output"),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(
|
|
||||||
time::OffsetDateTime::now_utc() + time::Duration::minutes(30),
|
|
||||||
),
|
|
||||||
source_object_uri: "rsync://example.test/leaf/blob-only.roa".to_string(),
|
|
||||||
source_object_type: "roa".to_string(),
|
|
||||||
source_object_hash: sha256_hex(&roa_bytes),
|
|
||||||
source_ee_cert_hash: sha256_hex(
|
|
||||||
roa.signed_object.signed_data.certificates[0]
|
|
||||||
.raw_der
|
|
||||||
.as_slice(),
|
|
||||||
),
|
|
||||||
payload_json:
|
|
||||||
serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24})
|
|
||||||
.to_string(),
|
|
||||||
rule_hash: sha256_hex(b"blob-only-roa-rule"),
|
|
||||||
validation_path_hint: vec![manifest.to_string()],
|
|
||||||
};
|
|
||||||
let vcir = sample_vcir(
|
|
||||||
manifest,
|
|
||||||
None,
|
|
||||||
"test-tal",
|
|
||||||
Some(local.clone()),
|
|
||||||
sample_artifacts(manifest, &local.source_object_hash),
|
|
||||||
);
|
|
||||||
store.put_vcir(&vcir).expect("put vcir");
|
|
||||||
let rule_entry = AuditRuleIndexEntry {
|
|
||||||
kind: AuditRuleKind::Roa,
|
|
||||||
rule_hash: local.rule_hash.clone(),
|
|
||||||
manifest_rsync_uri: manifest.to_string(),
|
|
||||||
source_object_uri: local.source_object_uri.clone(),
|
|
||||||
source_object_hash: local.source_object_hash.clone(),
|
|
||||||
output_id: local.output_id.clone(),
|
|
||||||
item_effective_until: local.item_effective_until.clone(),
|
|
||||||
};
|
|
||||||
store
|
|
||||||
.put_audit_rule_index_entry(&rule_entry)
|
|
||||||
.expect("put rule index");
|
|
||||||
|
|
||||||
put_raw_evidence(&store, manifest.as_bytes(), manifest, "mft");
|
|
||||||
put_raw_evidence(
|
|
||||||
&store,
|
|
||||||
format!("{}-crl", manifest).as_bytes(),
|
|
||||||
&manifest.replace(".mft", ".crl"),
|
|
||||||
"crl",
|
|
||||||
);
|
|
||||||
put_blob_only(&store, &roa_bytes);
|
|
||||||
|
|
||||||
let trace = trace_rule_to_root(&store, AuditRuleKind::Roa, &local.rule_hash)
|
|
||||||
.expect("trace rule")
|
|
||||||
.expect("trace exists");
|
|
||||||
assert!(trace.source_object_raw.raw_present);
|
|
||||||
assert_eq!(
|
|
||||||
trace.source_object_raw.origin_uris,
|
|
||||||
vec![local.source_object_uri.clone()]
|
|
||||||
);
|
|
||||||
assert_eq!(trace.source_object_raw.object_type.as_deref(), Some("roa"));
|
|
||||||
assert_eq!(trace.source_object_raw.byte_len, Some(roa_bytes.len()));
|
|
||||||
assert!(trace.source_ee_cert_raw.raw_present);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_rule_to_root_returns_none_for_missing_rule_index() {
|
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
|
||||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
trace_rule_to_root(&store, AuditRuleKind::Roa, &sha256_hex(b"missing"))
|
|
||||||
.expect("missing trace ok")
|
|
||||||
.is_none()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_rule_to_root_errors_when_index_points_to_missing_vcir() {
|
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
|
||||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
|
||||||
let rule_hash = sha256_hex(b"missing-vcir-rule");
|
|
||||||
store
|
|
||||||
.put_audit_rule_index_entry(&AuditRuleIndexEntry {
|
|
||||||
kind: AuditRuleKind::Roa,
|
|
||||||
rule_hash: rule_hash.clone(),
|
|
||||||
manifest_rsync_uri: "rsync://example.test/missing.mft".to_string(),
|
|
||||||
source_object_uri: "rsync://example.test/missing.roa".to_string(),
|
|
||||||
source_object_hash: sha256_hex(b"missing-source"),
|
|
||||||
output_id: sha256_hex(b"missing-output"),
|
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(
|
|
||||||
time::OffsetDateTime::now_utc() + time::Duration::minutes(1),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.expect("put rule index");
|
|
||||||
|
|
||||||
let err = trace_rule_to_root(&store, AuditRuleKind::Roa, &rule_hash).unwrap_err();
|
|
||||||
assert!(matches!(
|
|
||||||
err,
|
|
||||||
AuditTraceError::MissingVcir { manifest_rsync_uri }
|
|
||||||
if manifest_rsync_uri == "rsync://example.test/missing.mft"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_rule_to_root_errors_when_vcir_local_output_is_missing() {
|
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
|
||||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
|
||||||
let manifest = "rsync://example.test/leaf/leaf.mft";
|
|
||||||
let vcir = sample_vcir(
|
|
||||||
manifest,
|
|
||||||
None,
|
|
||||||
"test-tal",
|
|
||||||
None,
|
|
||||||
sample_artifacts(manifest, &sha256_hex(b"leaf-object")),
|
|
||||||
);
|
|
||||||
store.put_vcir(&vcir).expect("put vcir");
|
|
||||||
let rule_hash = sha256_hex(b"missing-output-rule");
|
|
||||||
store
|
|
||||||
.put_audit_rule_index_entry(&AuditRuleIndexEntry {
|
|
||||||
kind: AuditRuleKind::Roa,
|
|
||||||
rule_hash: rule_hash.clone(),
|
|
||||||
manifest_rsync_uri: manifest.to_string(),
|
|
||||||
source_object_uri: "rsync://example.test/leaf/a.roa".to_string(),
|
|
||||||
source_object_hash: sha256_hex(b"leaf-object"),
|
|
||||||
output_id: sha256_hex(b"missing-output"),
|
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(
|
|
||||||
time::OffsetDateTime::now_utc() + time::Duration::minutes(1),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.expect("put rule index");
|
|
||||||
|
|
||||||
let err = trace_rule_to_root(&store, AuditRuleKind::Roa, &rule_hash).unwrap_err();
|
|
||||||
assert!(matches!(err, AuditTraceError::MissingLocalOutput { .. }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_vcir_chain_to_root_detects_parent_cycle() {
|
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
|
||||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
|
||||||
let a_manifest = "rsync://example.test/a.mft";
|
|
||||||
let b_manifest = "rsync://example.test/b.mft";
|
|
||||||
let a_vcir = sample_vcir(
|
|
||||||
a_manifest,
|
|
||||||
Some(b_manifest),
|
|
||||||
"test-tal",
|
|
||||||
None,
|
|
||||||
sample_artifacts(a_manifest, &sha256_hex(b"a-object")),
|
|
||||||
);
|
|
||||||
let b_vcir = sample_vcir(
|
|
||||||
b_manifest,
|
|
||||||
Some(a_manifest),
|
|
||||||
"test-tal",
|
|
||||||
None,
|
|
||||||
sample_artifacts(b_manifest, &sha256_hex(b"b-object")),
|
|
||||||
);
|
|
||||||
store.put_vcir(&a_vcir).expect("put a");
|
|
||||||
store.put_vcir(&b_vcir).expect("put b");
|
|
||||||
|
|
||||||
let err = trace_vcir_chain_to_root(&store, a_manifest).unwrap_err();
|
|
||||||
assert!(matches!(
|
|
||||||
err,
|
|
||||||
AuditTraceError::ParentCycle { manifest_rsync_uri }
|
|
||||||
if manifest_rsync_uri == a_manifest
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,9 +4,8 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use rocksdb::{DB, IteratorMode, Options};
|
use rocksdb::{DB, IteratorMode, Options};
|
||||||
use rpki::storage::{
|
use rpki::storage::{
|
||||||
ALL_COLUMN_FAMILY_NAMES, CF_AUDIT_RULE_INDEX, CF_MANIFEST_REPLAY_META, CF_RAW_BY_HASH,
|
ALL_COLUMN_FAMILY_NAMES, CF_MANIFEST_REPLAY_META, CF_RAW_BY_HASH, CF_REPOSITORY_VIEW,
|
||||||
CF_REPOSITORY_VIEW, CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER, CF_VCIR,
|
CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER, CF_VCIR, column_family_descriptors,
|
||||||
column_family_descriptors,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@ -99,7 +98,7 @@ Output:
|
|||||||
|
|
||||||
Output groups:
|
Output groups:
|
||||||
- current_repository_view: repository_view + raw_by_hash
|
- current_repository_view: repository_view + raw_by_hash
|
||||||
- current_validation_state: vcir + audit_rule_index
|
- current_validation_state: vcir + manifest_replay_meta
|
||||||
- current_rrdp_state: rrdp_source + rrdp_source_member + rrdp_uri_owner
|
- current_rrdp_state: rrdp_source + rrdp_source_member + rrdp_uri_owner
|
||||||
"
|
"
|
||||||
)
|
)
|
||||||
@ -183,7 +182,7 @@ fn collect_db_file_stats(db_path: &Path) -> Result<DbFileStats, Box<dyn std::err
|
|||||||
fn cf_group(cf_name: &str) -> CfGroup {
|
fn cf_group(cf_name: &str) -> CfGroup {
|
||||||
match cf_name {
|
match cf_name {
|
||||||
CF_REPOSITORY_VIEW | CF_RAW_BY_HASH => CfGroup::CurrentRepositoryView,
|
CF_REPOSITORY_VIEW | CF_RAW_BY_HASH => CfGroup::CurrentRepositoryView,
|
||||||
CF_VCIR | CF_MANIFEST_REPLAY_META | CF_AUDIT_RULE_INDEX => CfGroup::CurrentValidationState,
|
CF_VCIR | CF_MANIFEST_REPLAY_META => CfGroup::CurrentValidationState,
|
||||||
CF_RRDP_SOURCE | CF_RRDP_SOURCE_MEMBER | CF_RRDP_URI_OWNER => CfGroup::CurrentRrdpState,
|
CF_RRDP_SOURCE | CF_RRDP_SOURCE_MEMBER | CF_RRDP_URI_OWNER => CfGroup::CurrentRrdpState,
|
||||||
_ => CfGroup::LegacyCompatibility,
|
_ => CfGroup::LegacyCompatibility,
|
||||||
}
|
}
|
||||||
@ -375,10 +374,6 @@ mod tests {
|
|||||||
cf_group(CF_MANIFEST_REPLAY_META),
|
cf_group(CF_MANIFEST_REPLAY_META),
|
||||||
CfGroup::CurrentValidationState
|
CfGroup::CurrentValidationState
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
cf_group(CF_AUDIT_RULE_INDEX),
|
|
||||||
CfGroup::CurrentValidationState
|
|
||||||
);
|
|
||||||
assert_eq!(cf_group(CF_RRDP_SOURCE), CfGroup::CurrentRrdpState);
|
assert_eq!(cf_group(CF_RRDP_SOURCE), CfGroup::CurrentRrdpState);
|
||||||
assert_eq!(cf_group(CF_RRDP_URI_OWNER), CfGroup::CurrentRrdpState);
|
assert_eq!(cf_group(CF_RRDP_URI_OWNER), CfGroup::CurrentRrdpState);
|
||||||
assert_eq!(cf_group("unknown_legacy"), CfGroup::LegacyCompatibility);
|
assert_eq!(cf_group("unknown_legacy"), CfGroup::LegacyCompatibility);
|
||||||
@ -390,7 +385,7 @@ mod tests {
|
|||||||
(CF_REPOSITORY_VIEW, 5),
|
(CF_REPOSITORY_VIEW, 5),
|
||||||
(CF_RAW_BY_HASH, 7),
|
(CF_RAW_BY_HASH, 7),
|
||||||
(CF_VCIR, 11),
|
(CF_VCIR, 11),
|
||||||
(CF_AUDIT_RULE_INDEX, 13),
|
(CF_MANIFEST_REPLAY_META, 13),
|
||||||
(CF_RRDP_SOURCE_MEMBER, 19),
|
(CF_RRDP_SOURCE_MEMBER, 19),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -420,7 +415,7 @@ mod tests {
|
|||||||
live_sst_size_bytes: 55,
|
live_sst_size_bytes: 55,
|
||||||
live_sst_files: 2,
|
live_sst_files: 2,
|
||||||
};
|
};
|
||||||
let audit = CfStats {
|
let replay_meta = CfStats {
|
||||||
keys: 5,
|
keys: 5,
|
||||||
key_bytes: 50,
|
key_bytes: 50,
|
||||||
value_bytes: 500,
|
value_bytes: 500,
|
||||||
@ -433,7 +428,7 @@ mod tests {
|
|||||||
let grouped = summarize_cf_stats([
|
let grouped = summarize_cf_stats([
|
||||||
(CF_REPOSITORY_VIEW, &repo),
|
(CF_REPOSITORY_VIEW, &repo),
|
||||||
(CF_VCIR, &vcir),
|
(CF_VCIR, &vcir),
|
||||||
(CF_AUDIT_RULE_INDEX, &audit),
|
(CF_MANIFEST_REPLAY_META, &replay_meta),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert_eq!(grouped.get(&CfGroup::CurrentRepositoryView), Some(&repo));
|
assert_eq!(grouped.get(&CfGroup::CurrentRepositoryView), Some(&repo));
|
||||||
|
|||||||
@ -1,103 +0,0 @@
|
|||||||
use rpki::audit_trace::trace_rule_to_root;
|
|
||||||
use rpki::storage::{AuditRuleKind, RocksStore, VcirOutputType};
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::env;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
if args.len() < 3 {
|
|
||||||
eprintln!("usage: trace_arin_missing_vrps <db> <row> [<row> ...]");
|
|
||||||
std::process::exit(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
let store = RocksStore::open(Path::new(&args[1])).expect("open db");
|
|
||||||
let vcirs = store.list_vcirs().expect("list vcirs");
|
|
||||||
|
|
||||||
for row in &args[2..] {
|
|
||||||
let parts: Vec<&str> = row.split(',').collect();
|
|
||||||
if parts.len() != 4 {
|
|
||||||
println!("ROW {row}");
|
|
||||||
println!("ERROR invalid compare row");
|
|
||||||
println!();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let asn: u32 = parts[0]
|
|
||||||
.trim_start_matches("AS")
|
|
||||||
.parse()
|
|
||||||
.expect("parse asn");
|
|
||||||
let prefix = parts[1].to_string();
|
|
||||||
let max_length: u8 = parts[2].parse().expect("parse max length");
|
|
||||||
let mut found = false;
|
|
||||||
|
|
||||||
println!("ROW {row}");
|
|
||||||
for vcir in &vcirs {
|
|
||||||
for output in &vcir.local_outputs {
|
|
||||||
if output.output_type != VcirOutputType::Vrp {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let payload: Value = match serde_json::from_str(&output.payload_json) {
|
|
||||||
Ok(value) => value,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
let payload_asn = payload
|
|
||||||
.get("asn")
|
|
||||||
.and_then(|v| v.as_u64())
|
|
||||||
.map(|v| v as u32);
|
|
||||||
let payload_prefix = payload
|
|
||||||
.get("prefix")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(|v| v.to_string());
|
|
||||||
let payload_max = payload
|
|
||||||
.get("max_length")
|
|
||||||
.and_then(|v| v.as_u64())
|
|
||||||
.map(|v| v as u8);
|
|
||||||
if payload_asn == Some(asn)
|
|
||||||
&& payload_prefix.as_ref() == Some(&prefix)
|
|
||||||
&& payload_max == Some(max_length)
|
|
||||||
{
|
|
||||||
found = true;
|
|
||||||
println!("manifest_rsync_uri={}", vcir.manifest_rsync_uri);
|
|
||||||
println!("source_object_uri={}", output.source_object_uri);
|
|
||||||
println!("source_object_hash={}", output.source_object_hash);
|
|
||||||
println!("source_ee_cert_hash={}", output.source_ee_cert_hash);
|
|
||||||
println!("rule_hash={}", output.rule_hash);
|
|
||||||
println!("validation_path_hint={:?}", output.validation_path_hint);
|
|
||||||
|
|
||||||
if let Some(trace) =
|
|
||||||
trace_rule_to_root(&store, AuditRuleKind::Roa, &output.rule_hash)
|
|
||||||
.expect("trace rule")
|
|
||||||
{
|
|
||||||
println!(
|
|
||||||
"trace_leaf_manifest={}",
|
|
||||||
trace
|
|
||||||
.chain_leaf_to_root
|
|
||||||
.first()
|
|
||||||
.map(|node| node.manifest_rsync_uri.as_str())
|
|
||||||
.unwrap_or("")
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"trace_source_object_uri={}",
|
|
||||||
trace.resolved_output.source_object_uri
|
|
||||||
);
|
|
||||||
println!("trace_chain_len={}", trace.chain_leaf_to_root.len());
|
|
||||||
for (idx, node) in trace.chain_leaf_to_root.iter().enumerate() {
|
|
||||||
println!("chain[{idx}].manifest={}", node.manifest_rsync_uri);
|
|
||||||
println!(
|
|
||||||
"chain[{idx}].current_manifest={}",
|
|
||||||
node.current_manifest_rsync_uri
|
|
||||||
);
|
|
||||||
println!("chain[{idx}].current_crl={}", node.current_crl_rsync_uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
println!("NOT_FOUND");
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,7 +3,10 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use rocksdb::{DB, Options, WriteBatch};
|
use rocksdb::{DB, Options, WriteBatch};
|
||||||
|
|
||||||
use crate::storage::{RawByHashEntry, RocksStore, StorageError, StorageResult};
|
use crate::storage::{
|
||||||
|
RawByHashEntry, RocksDbMemoryDbSnapshot, RocksStore, StorageError, StorageResult,
|
||||||
|
memory_db_snapshot_for_column_families,
|
||||||
|
};
|
||||||
|
|
||||||
const RAW_BY_HASH_KEY_PREFIX: &str = "rawbyhash:";
|
const RAW_BY_HASH_KEY_PREFIX: &str = "rawbyhash:";
|
||||||
const RAW_BLOB_KEY_PREFIX: &str = "rawblob:";
|
const RAW_BLOB_KEY_PREFIX: &str = "rawblob:";
|
||||||
@ -167,6 +170,10 @@ impl ExternalRawStoreDb {
|
|||||||
pub fn path(&self) -> &PathBuf {
|
pub fn path(&self) -> &PathBuf {
|
||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn memory_snapshot(&self, label: impl Into<String>) -> RocksDbMemoryDbSnapshot {
|
||||||
|
memory_db_snapshot_for_column_families(label, self.db.as_ref(), None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExternalRepoBytesDb {
|
impl ExternalRepoBytesDb {
|
||||||
@ -234,6 +241,10 @@ impl ExternalRepoBytesDb {
|
|||||||
pub fn path(&self) -> &PathBuf {
|
pub fn path(&self) -> &PathBuf {
|
||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn memory_snapshot(&self, label: impl Into<String>) -> RocksDbMemoryDbSnapshot {
|
||||||
|
memory_db_snapshot_for_column_families(label, self.db.as_ref(), None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RawObjectStore for RocksStore {
|
impl RawObjectStore for RocksStore {
|
||||||
|
|||||||
@ -26,6 +26,21 @@ pub struct CcrManifestContribution {
|
|||||||
pub subordinate_skis: Vec<Vec<u8>>,
|
pub subordinate_skis: Vec<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct CcrAccumulatorMemoryStats {
|
||||||
|
pub trust_anchor_count: u64,
|
||||||
|
pub manifest_count: u64,
|
||||||
|
pub estimated_heap_bytes: u64,
|
||||||
|
pub string_bytes: u64,
|
||||||
|
pub string_capacity_bytes: u64,
|
||||||
|
pub vec_payload_bytes: u64,
|
||||||
|
pub vec_capacity_bytes: u64,
|
||||||
|
pub locations_der_count: u64,
|
||||||
|
pub subordinate_ski_count: u64,
|
||||||
|
pub btree_key_capacity_bytes: u64,
|
||||||
|
pub btree_entry_shallow_bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
impl CcrManifestContribution {
|
impl CcrManifestContribution {
|
||||||
fn from_projection(projection: &VcirCcrManifestProjection) -> Result<Self, String> {
|
fn from_projection(projection: &VcirCcrManifestProjection) -> Result<Self, String> {
|
||||||
let this_update = projection
|
let this_update = projection
|
||||||
@ -57,6 +72,36 @@ impl CcrManifestContribution {
|
|||||||
subordinates: self.subordinate_skis.clone(),
|
subordinates: self.subordinate_skis.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_memory_stats(&self, stats: &mut CcrAccumulatorMemoryStats) {
|
||||||
|
stats.string_bytes += self.manifest_rsync_uri.len() as u64;
|
||||||
|
stats.string_capacity_bytes += self.manifest_rsync_uri.capacity() as u64;
|
||||||
|
stats.estimated_heap_bytes += self.manifest_rsync_uri.capacity() as u64;
|
||||||
|
|
||||||
|
add_vec_stats(&self.hash, stats);
|
||||||
|
add_vec_stats(&self.aki, stats);
|
||||||
|
add_vec_stats(&self.manifest_number_be, stats);
|
||||||
|
add_vec_of_vec_stats(&self.locations_der, stats);
|
||||||
|
add_vec_of_vec_stats(&self.subordinate_skis, stats);
|
||||||
|
stats.locations_der_count += self.locations_der.len() as u64;
|
||||||
|
stats.subordinate_ski_count += self.subordinate_skis.len() as u64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_vec_stats(value: &Vec<u8>, stats: &mut CcrAccumulatorMemoryStats) {
|
||||||
|
stats.vec_payload_bytes += value.len() as u64;
|
||||||
|
stats.vec_capacity_bytes += value.capacity() as u64;
|
||||||
|
stats.estimated_heap_bytes += value.capacity() as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_vec_of_vec_stats(values: &Vec<Vec<u8>>, stats: &mut CcrAccumulatorMemoryStats) {
|
||||||
|
let outer_capacity = values.capacity() * std::mem::size_of::<Vec<u8>>();
|
||||||
|
stats.vec_payload_bytes += (values.len() * std::mem::size_of::<Vec<u8>>()) as u64;
|
||||||
|
stats.vec_capacity_bytes += outer_capacity as u64;
|
||||||
|
stats.estimated_heap_bytes += outer_capacity as u64;
|
||||||
|
for value in values {
|
||||||
|
add_vec_stats(value, stats);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
@ -138,6 +183,61 @@ impl CcrAccumulator {
|
|||||||
pub fn manifest_count(&self) -> usize {
|
pub fn manifest_count(&self) -> usize {
|
||||||
self.manifests_by_hash.len()
|
self.manifests_by_hash.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn memory_stats(&self) -> CcrAccumulatorMemoryStats {
|
||||||
|
let mut stats = CcrAccumulatorMemoryStats {
|
||||||
|
trust_anchor_count: self.trust_anchors.len() as u64,
|
||||||
|
manifest_count: self.manifests_by_hash.len() as u64,
|
||||||
|
..CcrAccumulatorMemoryStats::default()
|
||||||
|
};
|
||||||
|
stats.estimated_heap_bytes +=
|
||||||
|
(self.trust_anchors.capacity() * std::mem::size_of::<TrustAnchor>()) as u64;
|
||||||
|
for trust_anchor in &self.trust_anchors {
|
||||||
|
add_vec_stats(&trust_anchor.tal.raw, &mut stats);
|
||||||
|
add_vec_of_string_stats(&trust_anchor.tal.comments, &mut stats);
|
||||||
|
stats.vec_payload_bytes +=
|
||||||
|
(trust_anchor.tal.ta_uris.len() * std::mem::size_of::<url::Url>()) as u64;
|
||||||
|
stats.vec_capacity_bytes +=
|
||||||
|
(trust_anchor.tal.ta_uris.capacity() * std::mem::size_of::<url::Url>()) as u64;
|
||||||
|
stats.estimated_heap_bytes +=
|
||||||
|
(trust_anchor.tal.ta_uris.capacity() * std::mem::size_of::<url::Url>()) as u64;
|
||||||
|
for uri in &trust_anchor.tal.ta_uris {
|
||||||
|
stats.string_bytes += uri.as_str().len() as u64;
|
||||||
|
stats.string_capacity_bytes += uri.as_str().len() as u64;
|
||||||
|
stats.estimated_heap_bytes += uri.as_str().len() as u64;
|
||||||
|
}
|
||||||
|
add_vec_stats(&trust_anchor.tal.subject_public_key_info_der, &mut stats);
|
||||||
|
add_vec_stats(&trust_anchor.ta_certificate.raw_der, &mut stats);
|
||||||
|
if let Some(uri) = &trust_anchor.resolved_ta_uri {
|
||||||
|
stats.string_bytes += uri.as_str().len() as u64;
|
||||||
|
stats.string_capacity_bytes += uri.as_str().len() as u64;
|
||||||
|
stats.estimated_heap_bytes += uri.as_str().len() as u64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.btree_entry_shallow_bytes = (self.manifests_by_hash.len()
|
||||||
|
* (std::mem::size_of::<Vec<u8>>() + std::mem::size_of::<CcrManifestContribution>()))
|
||||||
|
as u64;
|
||||||
|
stats.estimated_heap_bytes += stats.btree_entry_shallow_bytes;
|
||||||
|
for (key, contribution) in &self.manifests_by_hash {
|
||||||
|
stats.btree_key_capacity_bytes += key.capacity() as u64;
|
||||||
|
stats.estimated_heap_bytes += key.capacity() as u64;
|
||||||
|
contribution.add_memory_stats(&mut stats);
|
||||||
|
}
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_vec_of_string_stats(values: &Vec<String>, stats: &mut CcrAccumulatorMemoryStats) {
|
||||||
|
let outer_capacity = values.capacity() * std::mem::size_of::<String>();
|
||||||
|
stats.vec_payload_bytes += (values.len() * std::mem::size_of::<String>()) as u64;
|
||||||
|
stats.vec_capacity_bytes += outer_capacity as u64;
|
||||||
|
stats.estimated_heap_bytes += outer_capacity as u64;
|
||||||
|
for value in values {
|
||||||
|
stats.string_bytes += value.len() as u64;
|
||||||
|
stats.string_capacity_bytes += value.capacity() as u64;
|
||||||
|
stats.estimated_heap_bytes += value.capacity() as u64;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
625
src/cli.rs
625
src/cli.rs
@ -7,17 +7,23 @@ use crate::cir::{CirTrustAnchorBinding, export_cir_from_run_multi};
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::analysis::timing::{TimingHandle, TimingMeta, TimingMetaUpdate};
|
use crate::analysis::timing::{TimingHandle, TimingMeta, TimingMetaUpdate};
|
||||||
|
use crate::audit::AuditRepoSyncStats;
|
||||||
|
#[cfg(test)]
|
||||||
use crate::audit::{
|
use crate::audit::{
|
||||||
AspaOutput, AuditRepoSyncStats, AuditReportV2, AuditRunMeta, AuditWarning, TreeSummary,
|
AspaOutput, AuditReportV2, AuditRunMeta, AuditWarning, TreeSummary, VrpOutput,
|
||||||
VrpOutput, format_roa_ip_prefix,
|
format_roa_ip_prefix,
|
||||||
};
|
};
|
||||||
use crate::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
use crate::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
||||||
use crate::fetch::rsync::LocalDirRsyncFetcher;
|
use crate::fetch::rsync::LocalDirRsyncFetcher;
|
||||||
use crate::fetch::rsync_system::{RsyncScopePolicy, SystemRsyncConfig, SystemRsyncFetcher};
|
use crate::fetch::rsync_system::{RsyncScopePolicy, SystemRsyncConfig, SystemRsyncFetcher};
|
||||||
|
use crate::memory_telemetry::{
|
||||||
|
MallocTrimProbe, MemoryTelemetryCheckpoint, MemoryTelemetrySummary, ObjectGraphMemoryMetric,
|
||||||
|
ObjectGraphMemorySection, ObjectGraphMemorySummary,
|
||||||
|
};
|
||||||
use crate::parallel::config::{ParallelPhase1Config, ParallelPhase2Config};
|
use crate::parallel::config::{ParallelPhase1Config, ParallelPhase2Config};
|
||||||
use crate::parallel::types::TalInputSpec;
|
use crate::parallel::types::TalInputSpec;
|
||||||
use crate::policy::{Policy, StrictPolicy};
|
use crate::policy::{Policy, StrictPolicy};
|
||||||
use crate::storage::RocksStore;
|
use crate::storage::{RocksStore, VcirStorageSummary};
|
||||||
use crate::validation::run_tree_from_tal::{
|
use crate::validation::run_tree_from_tal::{
|
||||||
RunTreeFromTalAuditOutput, run_tree_from_multiple_tals_parallel_phase2_audit,
|
RunTreeFromTalAuditOutput, run_tree_from_multiple_tals_parallel_phase2_audit,
|
||||||
run_tree_from_tal_and_ta_der_parallel_phase2_audit,
|
run_tree_from_tal_and_ta_der_parallel_phase2_audit,
|
||||||
@ -28,7 +34,11 @@ use crate::validation::run_tree_from_tal::{
|
|||||||
run_tree_from_tal_url_parallel_phase2_audit,
|
run_tree_from_tal_url_parallel_phase2_audit,
|
||||||
};
|
};
|
||||||
use crate::validation::tree::TreeRunConfig;
|
use crate::validation::tree::TreeRunConfig;
|
||||||
use output::{ReportJsonFormat, run_compare_view_task, write_json, write_stage_timing};
|
#[cfg(test)]
|
||||||
|
use output::write_json;
|
||||||
|
use output::{
|
||||||
|
ReportJsonFormat, run_compare_view_task, write_report_json_from_shared, write_stage_timing,
|
||||||
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@ -53,6 +63,35 @@ struct RunStageTiming {
|
|||||||
rrdp_download_ms_total: u64,
|
rrdp_download_ms_total: u64,
|
||||||
rsync_download_ms_total: u64,
|
rsync_download_ms_total: u64,
|
||||||
download_bytes_total: u64,
|
download_bytes_total: u64,
|
||||||
|
vcir_storage_summary_ms: Option<u64>,
|
||||||
|
vcir_storage: Option<VcirStorageSummary>,
|
||||||
|
memory_telemetry: Option<MemoryTelemetrySummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_memory_checkpoint(
|
||||||
|
checkpoints: &mut Vec<MemoryTelemetryCheckpoint>,
|
||||||
|
label: &str,
|
||||||
|
total_started: &std::time::Instant,
|
||||||
|
store: &RocksStore,
|
||||||
|
) {
|
||||||
|
checkpoints.push(MemoryTelemetryCheckpoint {
|
||||||
|
label: label.to_string(),
|
||||||
|
elapsed_ms: total_started.elapsed().as_millis() as u64,
|
||||||
|
process: crate::memory_telemetry::process_memory_snapshot(label),
|
||||||
|
rocksdb: store.memory_snapshot(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_trim_probe_enabled() -> bool {
|
||||||
|
std::env::var("RPKI_MEMORY_TRIM_PROBE")
|
||||||
|
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vcir_storage_summary_enabled() -> bool {
|
||||||
|
std::env::var("RPKI_VCIR_STORAGE_SUMMARY")
|
||||||
|
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
|
||||||
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
@ -92,6 +131,7 @@ pub struct CliArgs {
|
|||||||
pub payload_base_validation_time: Option<time::OffsetDateTime>,
|
pub payload_base_validation_time: Option<time::OffsetDateTime>,
|
||||||
pub payload_delta_archive: Option<PathBuf>,
|
pub payload_delta_archive: Option<PathBuf>,
|
||||||
pub payload_delta_locks: Option<PathBuf>,
|
pub payload_delta_locks: Option<PathBuf>,
|
||||||
|
pub memory_trim_after_validation: bool,
|
||||||
|
|
||||||
pub rsync_local_dir: Option<PathBuf>,
|
pub rsync_local_dir: Option<PathBuf>,
|
||||||
pub disable_rrdp: bool,
|
pub disable_rrdp: bool,
|
||||||
@ -144,6 +184,7 @@ Options:
|
|||||||
--payload-base-validation-time <rfc3339> Validation time for the base bootstrap inside offline delta replay
|
--payload-base-validation-time <rfc3339> Validation time for the base bootstrap inside offline delta replay
|
||||||
--payload-delta-archive <path> Use local delta payload archive root (offline delta replay)
|
--payload-delta-archive <path> Use local delta payload archive root (offline delta replay)
|
||||||
--payload-delta-locks <path> Use local locks-delta.json (offline delta replay)
|
--payload-delta-locks <path> Use local locks-delta.json (offline delta replay)
|
||||||
|
--memory-trim-after-validation Call malloc_trim(0) after validation/report memory checkpoints (Linux glibc only; default off)
|
||||||
|
|
||||||
--tal-url <url> TAL URL (repeatable; URL mode)
|
--tal-url <url> TAL URL (repeatable; URL mode)
|
||||||
--tal-path <path> TAL file path (repeatable; file mode)
|
--tal-path <path> TAL file path (repeatable; file mode)
|
||||||
@ -221,6 +262,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
|
|||||||
let mut payload_base_validation_time: Option<time::OffsetDateTime> = None;
|
let mut payload_base_validation_time: Option<time::OffsetDateTime> = None;
|
||||||
let mut payload_delta_archive: Option<PathBuf> = None;
|
let mut payload_delta_archive: Option<PathBuf> = None;
|
||||||
let mut payload_delta_locks: Option<PathBuf> = None;
|
let mut payload_delta_locks: Option<PathBuf> = None;
|
||||||
|
let mut memory_trim_after_validation = false;
|
||||||
|
|
||||||
let mut rsync_local_dir: Option<PathBuf> = None;
|
let mut rsync_local_dir: Option<PathBuf> = None;
|
||||||
let mut disable_rrdp: bool = false;
|
let mut disable_rrdp: bool = false;
|
||||||
@ -497,6 +539,9 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
|
|||||||
.ok_or("--payload-delta-locks requires a value")?;
|
.ok_or("--payload-delta-locks requires a value")?;
|
||||||
payload_delta_locks = Some(PathBuf::from(v));
|
payload_delta_locks = Some(PathBuf::from(v));
|
||||||
}
|
}
|
||||||
|
"--memory-trim-after-validation" => {
|
||||||
|
memory_trim_after_validation = true;
|
||||||
|
}
|
||||||
"--rsync-local-dir" => {
|
"--rsync-local-dir" => {
|
||||||
i += 1;
|
i += 1;
|
||||||
let v = argv.get(i).ok_or("--rsync-local-dir requires a value")?;
|
let v = argv.get(i).ok_or("--rsync-local-dir requires a value")?;
|
||||||
@ -841,6 +886,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
|
|||||||
payload_base_validation_time,
|
payload_base_validation_time,
|
||||||
payload_delta_archive,
|
payload_delta_archive,
|
||||||
payload_delta_locks,
|
payload_delta_locks,
|
||||||
|
memory_trim_after_validation,
|
||||||
rsync_local_dir,
|
rsync_local_dir,
|
||||||
disable_rrdp,
|
disable_rrdp,
|
||||||
rsync_command,
|
rsync_command,
|
||||||
@ -880,10 +926,12 @@ fn unique_rrdp_repos_from_publication_points(
|
|||||||
set.len()
|
set.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn unique_rrdp_repos(report: &AuditReportV2) -> usize {
|
fn unique_rrdp_repos(report: &AuditReportV2) -> usize {
|
||||||
unique_rrdp_repos_from_publication_points(&report.publication_points)
|
unique_rrdp_repos_from_publication_points(&report.publication_points)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn print_summary(report: &AuditReportV2) {
|
fn print_summary(report: &AuditReportV2) {
|
||||||
let rrdp_repos = unique_rrdp_repos(report);
|
let rrdp_repos = unique_rrdp_repos(report);
|
||||||
println!("RPKI stage2 serial run summary");
|
println!("RPKI stage2 serial run summary");
|
||||||
@ -1014,6 +1062,459 @@ impl PostValidationShared {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ObjectGraphSectionBuilder {
|
||||||
|
name: String,
|
||||||
|
item_count: u64,
|
||||||
|
shallow_bytes: u64,
|
||||||
|
heap_bytes: u64,
|
||||||
|
string_count: u64,
|
||||||
|
string_bytes: u64,
|
||||||
|
string_capacity_bytes: u64,
|
||||||
|
vec_count: u64,
|
||||||
|
vec_heap_bytes: u64,
|
||||||
|
vec_capacity_bytes: u64,
|
||||||
|
details: Vec<ObjectGraphMemoryMetric>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectGraphSectionBuilder {
|
||||||
|
fn new(name: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn items(&mut self, count: usize, item_size: usize) {
|
||||||
|
self.item_count += count as u64;
|
||||||
|
self.shallow_bytes += (count as u64) * (item_size as u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn heap_bytes(&mut self, value: usize) {
|
||||||
|
self.heap_bytes += value as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string(&mut self, value: &str) {
|
||||||
|
self.string_count += 1;
|
||||||
|
self.string_bytes += value.len() as u64;
|
||||||
|
self.string_capacity_bytes += value.len() as u64;
|
||||||
|
self.heap_bytes += value.len() as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn owned_string(&mut self, value: &String) {
|
||||||
|
self.string_count += 1;
|
||||||
|
self.string_bytes += value.len() as u64;
|
||||||
|
self.string_capacity_bytes += value.capacity() as u64;
|
||||||
|
self.heap_bytes += value.capacity() as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn optional_string(&mut self, value: Option<&String>) {
|
||||||
|
if let Some(value) = value {
|
||||||
|
self.owned_string(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vec_header_with_capacity(&mut self, len: usize, capacity: usize, element_size: usize) {
|
||||||
|
self.vec_count += 1;
|
||||||
|
let payload_bytes = len * element_size;
|
||||||
|
let capacity_bytes = capacity * element_size;
|
||||||
|
self.vec_heap_bytes += payload_bytes as u64;
|
||||||
|
self.vec_capacity_bytes += capacity_bytes as u64;
|
||||||
|
self.heap_bytes += capacity_bytes as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn byte_vec_owned(&mut self, value: &Vec<u8>) {
|
||||||
|
self.vec_header_with_capacity(value.len(), value.capacity(), std::mem::size_of::<u8>());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_vec_owned(&mut self, values: &Vec<String>) {
|
||||||
|
self.vec_header_with_capacity(
|
||||||
|
values.len(),
|
||||||
|
values.capacity(),
|
||||||
|
std::mem::size_of::<String>(),
|
||||||
|
);
|
||||||
|
for value in values {
|
||||||
|
self.owned_string(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn metric(&mut self, name: impl Into<String>, value: u64) {
|
||||||
|
self.details.push(ObjectGraphMemoryMetric {
|
||||||
|
name: name.into(),
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self) -> ObjectGraphMemorySection {
|
||||||
|
let estimated_bytes = self.shallow_bytes + self.heap_bytes;
|
||||||
|
ObjectGraphMemorySection {
|
||||||
|
name: self.name,
|
||||||
|
item_count: self.item_count,
|
||||||
|
shallow_bytes: self.shallow_bytes,
|
||||||
|
heap_bytes: self.heap_bytes,
|
||||||
|
estimated_bytes,
|
||||||
|
string_count: self.string_count,
|
||||||
|
string_bytes: self.string_bytes,
|
||||||
|
string_capacity_bytes: self.string_capacity_bytes,
|
||||||
|
vec_count: self.vec_count,
|
||||||
|
vec_heap_bytes: self.vec_heap_bytes,
|
||||||
|
vec_capacity_bytes: self.vec_capacity_bytes,
|
||||||
|
details: self.details,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_shared_object_graph(shared: &PostValidationShared) -> ObjectGraphMemorySummary {
|
||||||
|
let mut sections = Vec::new();
|
||||||
|
sections.push(estimate_publication_points_graph(
|
||||||
|
shared.publication_points.as_ref(),
|
||||||
|
));
|
||||||
|
sections.push(estimate_vrps_graph(shared.vrps.as_ref()));
|
||||||
|
sections.push(estimate_aspas_graph(shared.aspas.as_ref()));
|
||||||
|
sections.push(estimate_router_keys_graph(shared.router_keys.as_ref()));
|
||||||
|
sections.push(estimate_warnings_graph(
|
||||||
|
"tree_warnings",
|
||||||
|
shared.tree_warnings.as_ref(),
|
||||||
|
));
|
||||||
|
sections.push(estimate_downloads_graph(shared.downloads.as_ref()));
|
||||||
|
sections.push(estimate_current_repo_objects_graph(
|
||||||
|
shared.current_repo_objects.as_ref(),
|
||||||
|
));
|
||||||
|
sections.push(estimate_trust_anchor_graph(shared));
|
||||||
|
sections.push(estimate_ccr_accumulator_graph(
|
||||||
|
shared.ccr_accumulator.as_ref(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let total_estimated_bytes = sections
|
||||||
|
.iter()
|
||||||
|
.map(|section| section.estimated_bytes)
|
||||||
|
.sum::<u64>();
|
||||||
|
ObjectGraphMemorySummary {
|
||||||
|
captured_at_label: "after_validation".to_string(),
|
||||||
|
total_estimated_bytes,
|
||||||
|
sections,
|
||||||
|
notes: vec![
|
||||||
|
"Estimated bytes are Rust object graph approximations based on struct sizes and owned String/Vec payload lengths.".to_string(),
|
||||||
|
"The estimate intentionally excludes allocator metadata, fragmentation, freed-but-retained arenas, RocksDB C++ heap, and transient worker allocations.".to_string(),
|
||||||
|
"Large RSS minus this estimate points to allocator retention or structures not yet modeled by this telemetry.".to_string(),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_publication_points_graph(
|
||||||
|
publication_points: &[crate::audit::PublicationPointAudit],
|
||||||
|
) -> ObjectGraphMemorySection {
|
||||||
|
let mut builder = ObjectGraphSectionBuilder::new("publication_points");
|
||||||
|
builder.items(
|
||||||
|
publication_points.len(),
|
||||||
|
std::mem::size_of::<crate::audit::PublicationPointAudit>(),
|
||||||
|
);
|
||||||
|
builder.metric("publication_point_count", publication_points.len() as u64);
|
||||||
|
let mut object_count = 0u64;
|
||||||
|
let mut pp_warning_count = 0u64;
|
||||||
|
let mut pp_discovered_from_count = 0u64;
|
||||||
|
let mut object_detail_count = 0u64;
|
||||||
|
|
||||||
|
for pp in publication_points {
|
||||||
|
builder.owned_string(&pp.rsync_base_uri);
|
||||||
|
builder.owned_string(&pp.manifest_rsync_uri);
|
||||||
|
builder.owned_string(&pp.publication_point_rsync_uri);
|
||||||
|
builder.optional_string(pp.rrdp_notification_uri.as_ref());
|
||||||
|
builder.owned_string(&pp.source);
|
||||||
|
builder.optional_string(pp.repo_sync_source.as_ref());
|
||||||
|
builder.optional_string(pp.repo_sync_phase.as_ref());
|
||||||
|
builder.optional_string(pp.repo_sync_error.as_ref());
|
||||||
|
builder.owned_string(&pp.repo_terminal_state);
|
||||||
|
builder.owned_string(&pp.this_update_rfc3339_utc);
|
||||||
|
builder.owned_string(&pp.next_update_rfc3339_utc);
|
||||||
|
builder.owned_string(&pp.verified_at_rfc3339_utc);
|
||||||
|
|
||||||
|
if let Some(discovered_from) = &pp.discovered_from {
|
||||||
|
pp_discovered_from_count += 1;
|
||||||
|
builder.heap_bytes(std::mem::size_of::<crate::audit::DiscoveredFrom>());
|
||||||
|
builder.owned_string(&discovered_from.parent_manifest_rsync_uri);
|
||||||
|
builder.owned_string(&discovered_from.child_ca_certificate_rsync_uri);
|
||||||
|
builder.owned_string(&discovered_from.child_ca_certificate_sha256_hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
pp_warning_count += pp.warnings.len() as u64;
|
||||||
|
builder.vec_header_with_capacity(
|
||||||
|
pp.warnings.len(),
|
||||||
|
pp.warnings.capacity(),
|
||||||
|
std::mem::size_of::<crate::audit::AuditWarning>(),
|
||||||
|
);
|
||||||
|
for warning in &pp.warnings {
|
||||||
|
builder.owned_string(&warning.message);
|
||||||
|
builder.string_vec_owned(&warning.rfc_refs);
|
||||||
|
builder.optional_string(warning.context.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
object_count += pp.objects.len() as u64;
|
||||||
|
builder.vec_header_with_capacity(
|
||||||
|
pp.objects.len(),
|
||||||
|
pp.objects.capacity(),
|
||||||
|
std::mem::size_of::<crate::audit::ObjectAuditEntry>(),
|
||||||
|
);
|
||||||
|
for object in &pp.objects {
|
||||||
|
builder.owned_string(&object.rsync_uri);
|
||||||
|
builder.owned_string(&object.sha256_hex);
|
||||||
|
if object.detail.is_some() {
|
||||||
|
object_detail_count += 1;
|
||||||
|
}
|
||||||
|
builder.optional_string(object.detail.as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.metric("object_audit_entry_count", object_count);
|
||||||
|
builder.metric("publication_point_warning_count", pp_warning_count);
|
||||||
|
builder.metric(
|
||||||
|
"publication_point_discovered_from_count",
|
||||||
|
pp_discovered_from_count,
|
||||||
|
);
|
||||||
|
builder.metric("object_detail_count", object_detail_count);
|
||||||
|
builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_vrps_graph(vrps: &[crate::validation::objects::Vrp]) -> ObjectGraphMemorySection {
|
||||||
|
let mut builder = ObjectGraphSectionBuilder::new("vrps");
|
||||||
|
builder.items(
|
||||||
|
vrps.len(),
|
||||||
|
std::mem::size_of::<crate::validation::objects::Vrp>(),
|
||||||
|
);
|
||||||
|
builder.metric("vrp_count", vrps.len() as u64);
|
||||||
|
builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_aspas_graph(
|
||||||
|
aspas: &[crate::validation::objects::AspaAttestation],
|
||||||
|
) -> ObjectGraphMemorySection {
|
||||||
|
let mut builder = ObjectGraphSectionBuilder::new("aspas");
|
||||||
|
builder.items(
|
||||||
|
aspas.len(),
|
||||||
|
std::mem::size_of::<crate::validation::objects::AspaAttestation>(),
|
||||||
|
);
|
||||||
|
let mut providers_total = 0u64;
|
||||||
|
for aspa in aspas {
|
||||||
|
providers_total += aspa.provider_as_ids.len() as u64;
|
||||||
|
builder.vec_header_with_capacity(
|
||||||
|
aspa.provider_as_ids.len(),
|
||||||
|
aspa.provider_as_ids.capacity(),
|
||||||
|
std::mem::size_of::<u32>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
builder.metric("aspa_count", aspas.len() as u64);
|
||||||
|
builder.metric("provider_asn_count", providers_total);
|
||||||
|
builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_router_keys_graph(
|
||||||
|
router_keys: &[crate::validation::objects::RouterKeyPayload],
|
||||||
|
) -> ObjectGraphMemorySection {
|
||||||
|
let mut builder = ObjectGraphSectionBuilder::new("router_keys");
|
||||||
|
builder.items(
|
||||||
|
router_keys.len(),
|
||||||
|
std::mem::size_of::<crate::validation::objects::RouterKeyPayload>(),
|
||||||
|
);
|
||||||
|
for router_key in router_keys {
|
||||||
|
builder.byte_vec_owned(&router_key.ski);
|
||||||
|
builder.byte_vec_owned(&router_key.spki_der);
|
||||||
|
builder.owned_string(&router_key.source_object_uri);
|
||||||
|
builder.owned_string(&router_key.source_object_hash);
|
||||||
|
builder.owned_string(&router_key.source_ee_cert_hash);
|
||||||
|
}
|
||||||
|
builder.metric("router_key_count", router_keys.len() as u64);
|
||||||
|
builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_warnings_graph(
|
||||||
|
name: &str,
|
||||||
|
warnings: &[crate::report::Warning],
|
||||||
|
) -> ObjectGraphMemorySection {
|
||||||
|
let mut builder = ObjectGraphSectionBuilder::new(name);
|
||||||
|
builder.items(
|
||||||
|
warnings.len(),
|
||||||
|
std::mem::size_of::<crate::report::Warning>(),
|
||||||
|
);
|
||||||
|
for warning in warnings {
|
||||||
|
builder.owned_string(&warning.message);
|
||||||
|
builder.vec_header_with_capacity(
|
||||||
|
warning.rfc_refs.len(),
|
||||||
|
warning.rfc_refs.capacity(),
|
||||||
|
std::mem::size_of::<crate::report::RfcRef>(),
|
||||||
|
);
|
||||||
|
builder.optional_string(warning.context.as_ref());
|
||||||
|
}
|
||||||
|
builder.metric("warning_count", warnings.len() as u64);
|
||||||
|
builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_downloads_graph(
|
||||||
|
downloads: &[crate::audit::AuditDownloadEvent],
|
||||||
|
) -> ObjectGraphMemorySection {
|
||||||
|
let mut builder = ObjectGraphSectionBuilder::new("downloads");
|
||||||
|
builder.items(
|
||||||
|
downloads.len(),
|
||||||
|
std::mem::size_of::<crate::audit::AuditDownloadEvent>(),
|
||||||
|
);
|
||||||
|
let mut error_count = 0u64;
|
||||||
|
let mut bytes_count = 0u64;
|
||||||
|
let mut objects_stat_count = 0u64;
|
||||||
|
for event in downloads {
|
||||||
|
builder.owned_string(&event.uri);
|
||||||
|
builder.owned_string(&event.started_at_rfc3339_utc);
|
||||||
|
builder.owned_string(&event.finished_at_rfc3339_utc);
|
||||||
|
if event.error.is_some() {
|
||||||
|
error_count += 1;
|
||||||
|
}
|
||||||
|
if event.bytes.is_some() {
|
||||||
|
bytes_count += 1;
|
||||||
|
}
|
||||||
|
if event.objects.is_some() {
|
||||||
|
objects_stat_count += 1;
|
||||||
|
}
|
||||||
|
builder.optional_string(event.error.as_ref());
|
||||||
|
}
|
||||||
|
builder.metric("download_event_count", downloads.len() as u64);
|
||||||
|
builder.metric("download_error_count", error_count);
|
||||||
|
builder.metric("download_bytes_field_count", bytes_count);
|
||||||
|
builder.metric("download_objects_stat_count", objects_stat_count);
|
||||||
|
builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_current_repo_objects_graph(
|
||||||
|
objects: &[crate::current_repo_index::CurrentRepoObject],
|
||||||
|
) -> ObjectGraphMemorySection {
|
||||||
|
let mut builder = ObjectGraphSectionBuilder::new("current_repo_objects");
|
||||||
|
builder.items(
|
||||||
|
objects.len(),
|
||||||
|
std::mem::size_of::<crate::current_repo_index::CurrentRepoObject>(),
|
||||||
|
);
|
||||||
|
let mut object_type_count = 0u64;
|
||||||
|
for object in objects {
|
||||||
|
builder.owned_string(&object.rsync_uri);
|
||||||
|
builder.owned_string(&object.current_hash_hex);
|
||||||
|
builder.owned_string(&object.repository_source);
|
||||||
|
if object.object_type.is_some() {
|
||||||
|
object_type_count += 1;
|
||||||
|
}
|
||||||
|
builder.optional_string(object.object_type.as_ref());
|
||||||
|
}
|
||||||
|
builder.metric("current_repo_object_count", objects.len() as u64);
|
||||||
|
builder.metric("current_repo_object_type_count", object_type_count);
|
||||||
|
builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_trust_anchor_graph(shared: &PostValidationShared) -> ObjectGraphMemorySection {
|
||||||
|
let mut builder = ObjectGraphSectionBuilder::new("trust_anchors_and_tal_inputs");
|
||||||
|
builder.items(
|
||||||
|
1,
|
||||||
|
std::mem::size_of::<crate::validation::from_tal::DiscoveredRootCaInstance>(),
|
||||||
|
);
|
||||||
|
estimate_discovered_root(&mut builder, &shared.discovery);
|
||||||
|
builder.items(
|
||||||
|
shared.discoveries.len(),
|
||||||
|
std::mem::size_of::<crate::validation::from_tal::DiscoveredRootCaInstance>(),
|
||||||
|
);
|
||||||
|
for discovery in shared.discoveries.iter() {
|
||||||
|
estimate_discovered_root(&mut builder, discovery);
|
||||||
|
}
|
||||||
|
builder.items(
|
||||||
|
shared.successful_tal_inputs.len(),
|
||||||
|
std::mem::size_of::<TalInputSpec>(),
|
||||||
|
);
|
||||||
|
for tal_input in shared.successful_tal_inputs.iter() {
|
||||||
|
estimate_tal_input(&mut builder, tal_input);
|
||||||
|
}
|
||||||
|
builder.metric("discoveries_count", shared.discoveries.len() as u64);
|
||||||
|
builder.metric(
|
||||||
|
"successful_tal_inputs_count",
|
||||||
|
shared.successful_tal_inputs.len() as u64,
|
||||||
|
);
|
||||||
|
builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_discovered_root(
|
||||||
|
builder: &mut ObjectGraphSectionBuilder,
|
||||||
|
discovery: &crate::validation::from_tal::DiscoveredRootCaInstance,
|
||||||
|
) {
|
||||||
|
builder.optional_string(discovery.tal_url.as_ref());
|
||||||
|
estimate_trust_anchor(builder, &discovery.trust_anchor);
|
||||||
|
builder.owned_string(&discovery.ca_instance.rsync_base_uri);
|
||||||
|
builder.owned_string(&discovery.ca_instance.manifest_rsync_uri);
|
||||||
|
builder.owned_string(&discovery.ca_instance.publication_point_rsync_uri);
|
||||||
|
builder.optional_string(discovery.ca_instance.rrdp_notification_uri.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_trust_anchor(
|
||||||
|
builder: &mut ObjectGraphSectionBuilder,
|
||||||
|
trust_anchor: &crate::data_model::ta::TrustAnchor,
|
||||||
|
) {
|
||||||
|
builder.byte_vec_owned(&trust_anchor.tal.raw);
|
||||||
|
builder.string_vec_owned(&trust_anchor.tal.comments);
|
||||||
|
builder.vec_header_with_capacity(
|
||||||
|
trust_anchor.tal.ta_uris.len(),
|
||||||
|
trust_anchor.tal.ta_uris.capacity(),
|
||||||
|
std::mem::size_of::<url::Url>(),
|
||||||
|
);
|
||||||
|
for uri in &trust_anchor.tal.ta_uris {
|
||||||
|
builder.string(uri.as_str());
|
||||||
|
}
|
||||||
|
builder.byte_vec_owned(&trust_anchor.tal.subject_public_key_info_der);
|
||||||
|
builder.byte_vec_owned(&trust_anchor.ta_certificate.raw_der);
|
||||||
|
if let Some(uri) = &trust_anchor.resolved_ta_uri {
|
||||||
|
builder.string(uri.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_tal_input(builder: &mut ObjectGraphSectionBuilder, tal_input: &TalInputSpec) {
|
||||||
|
builder.owned_string(&tal_input.tal_id);
|
||||||
|
builder.owned_string(&tal_input.rir_id);
|
||||||
|
match &tal_input.source {
|
||||||
|
crate::parallel::types::TalSource::Url(url) => builder.owned_string(url),
|
||||||
|
crate::parallel::types::TalSource::DerBytes {
|
||||||
|
tal_url,
|
||||||
|
tal_bytes,
|
||||||
|
ta_der,
|
||||||
|
} => {
|
||||||
|
builder.owned_string(tal_url);
|
||||||
|
builder.byte_vec_owned(tal_bytes);
|
||||||
|
builder.byte_vec_owned(ta_der);
|
||||||
|
}
|
||||||
|
crate::parallel::types::TalSource::FilePath(path) => {
|
||||||
|
builder.string(&path.to_string_lossy());
|
||||||
|
}
|
||||||
|
crate::parallel::types::TalSource::FilePathWithTa { tal_path, ta_path } => {
|
||||||
|
builder.string(&tal_path.to_string_lossy());
|
||||||
|
builder.string(&ta_path.to_string_lossy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_ccr_accumulator_graph(
|
||||||
|
accumulator: Option<&CcrAccumulator>,
|
||||||
|
) -> ObjectGraphMemorySection {
|
||||||
|
let mut builder = ObjectGraphSectionBuilder::new("ccr_accumulator");
|
||||||
|
if let Some(accumulator) = accumulator {
|
||||||
|
builder.items(1, std::mem::size_of::<CcrAccumulator>());
|
||||||
|
let stats = accumulator.memory_stats();
|
||||||
|
builder.heap_bytes(stats.estimated_heap_bytes as usize);
|
||||||
|
builder.metric("trust_anchor_count", stats.trust_anchor_count);
|
||||||
|
builder.metric("manifest_count", stats.manifest_count);
|
||||||
|
builder.metric("string_bytes", stats.string_bytes);
|
||||||
|
builder.metric("string_capacity_bytes", stats.string_capacity_bytes);
|
||||||
|
builder.metric("vec_payload_bytes", stats.vec_payload_bytes);
|
||||||
|
builder.metric("vec_capacity_bytes", stats.vec_capacity_bytes);
|
||||||
|
builder.metric("locations_der_count", stats.locations_der_count);
|
||||||
|
builder.metric("subordinate_ski_count", stats.subordinate_ski_count);
|
||||||
|
builder.metric("btree_key_capacity_bytes", stats.btree_key_capacity_bytes);
|
||||||
|
builder.metric("btree_entry_shallow_bytes", stats.btree_entry_shallow_bytes);
|
||||||
|
} else {
|
||||||
|
builder.metric("manifest_count", 0);
|
||||||
|
}
|
||||||
|
builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn build_report(
|
fn build_report(
|
||||||
policy: &Policy,
|
policy: &Policy,
|
||||||
validation_time: time::OffsetDateTime,
|
validation_time: time::OffsetDateTime,
|
||||||
@ -1072,7 +1573,6 @@ fn build_report(
|
|||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
struct ReportTaskOutput {
|
struct ReportTaskOutput {
|
||||||
report: Option<AuditReportV2>,
|
|
||||||
report_build_ms: u64,
|
report_build_ms: u64,
|
||||||
report_write_ms: Option<u64>,
|
report_write_ms: Option<u64>,
|
||||||
}
|
}
|
||||||
@ -1080,7 +1580,6 @@ struct ReportTaskOutput {
|
|||||||
impl ReportTaskOutput {
|
impl ReportTaskOutput {
|
||||||
fn skipped() -> Self {
|
fn skipped() -> Self {
|
||||||
Self {
|
Self {
|
||||||
report: None,
|
|
||||||
report_build_ms: 0,
|
report_build_ms: 0,
|
||||||
report_write_ms: None,
|
report_write_ms: None,
|
||||||
}
|
}
|
||||||
@ -1094,23 +1593,21 @@ fn run_report_task(
|
|||||||
report_json_path: Option<&Path>,
|
report_json_path: Option<&Path>,
|
||||||
report_json_format: ReportJsonFormat,
|
report_json_format: ReportJsonFormat,
|
||||||
) -> Result<ReportTaskOutput, String> {
|
) -> Result<ReportTaskOutput, String> {
|
||||||
let report_started = std::time::Instant::now();
|
if let Some(path) = report_json_path {
|
||||||
let report = build_report(policy, validation_time, shared);
|
let timing = write_report_json_from_shared(
|
||||||
let report_build_ms = report_started.elapsed().as_millis() as u64;
|
path,
|
||||||
|
policy,
|
||||||
let report_write_ms = if let Some(path) = report_json_path {
|
validation_time,
|
||||||
let started = std::time::Instant::now();
|
shared,
|
||||||
write_json(path, &report, report_json_format)?;
|
report_json_format,
|
||||||
Some(started.elapsed().as_millis() as u64)
|
)?;
|
||||||
|
Ok(ReportTaskOutput {
|
||||||
|
report_build_ms: timing.build_ms,
|
||||||
|
report_write_ms: Some(timing.write_ms),
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
Ok(ReportTaskOutput::skipped())
|
||||||
};
|
}
|
||||||
|
|
||||||
Ok(ReportTaskOutput {
|
|
||||||
report: Some(report),
|
|
||||||
report_build_ms,
|
|
||||||
report_write_ms,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
@ -1443,6 +1940,15 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let total_started = std::time::Instant::now();
|
let total_started = std::time::Instant::now();
|
||||||
|
let mut memory_checkpoints: Vec<MemoryTelemetryCheckpoint> = Vec::new();
|
||||||
|
let mut malloc_trim_probes: Vec<MallocTrimProbe> = Vec::new();
|
||||||
|
let enable_memory_trim_probe = memory_trim_probe_enabled() || args.memory_trim_after_validation;
|
||||||
|
record_memory_checkpoint(
|
||||||
|
&mut memory_checkpoints,
|
||||||
|
"after_store_open",
|
||||||
|
&total_started,
|
||||||
|
store.as_ref(),
|
||||||
|
);
|
||||||
let validation_started = std::time::Instant::now();
|
let validation_started = std::time::Instant::now();
|
||||||
let collect_current_repo_objects = false;
|
let collect_current_repo_objects = false;
|
||||||
let out = if delta_replay_mode {
|
let out = if delta_replay_mode {
|
||||||
@ -1607,6 +2113,34 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
|||||||
|
|
||||||
let validation_ms = validation_started.elapsed().as_millis() as u64;
|
let validation_ms = validation_started.elapsed().as_millis() as u64;
|
||||||
let shared = PostValidationShared::from_run_output(out);
|
let shared = PostValidationShared::from_run_output(out);
|
||||||
|
let vcir_storage_summary_enabled = vcir_storage_summary_enabled();
|
||||||
|
let vcir_storage_summary_started = std::time::Instant::now();
|
||||||
|
let vcir_storage = if config.persist_vcir && vcir_storage_summary_enabled {
|
||||||
|
Some(
|
||||||
|
store
|
||||||
|
.summarize_vcir_storage()
|
||||||
|
.map_err(|e| format!("summarize VCIR storage failed: {e}"))?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let vcir_storage_summary_ms = (config.persist_vcir && vcir_storage_summary_enabled)
|
||||||
|
.then(|| vcir_storage_summary_started.elapsed().as_millis() as u64);
|
||||||
|
record_memory_checkpoint(
|
||||||
|
&mut memory_checkpoints,
|
||||||
|
"after_validation",
|
||||||
|
&total_started,
|
||||||
|
store.as_ref(),
|
||||||
|
);
|
||||||
|
if enable_memory_trim_probe {
|
||||||
|
malloc_trim_probes.push(crate::memory_telemetry::malloc_trim_probe());
|
||||||
|
record_memory_checkpoint(
|
||||||
|
&mut memory_checkpoints,
|
||||||
|
"after_validation_malloc_trim",
|
||||||
|
&total_started,
|
||||||
|
store.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((_out_dir, t)) = timing.as_ref() {
|
if let Some((_out_dir, t)) = timing.as_ref() {
|
||||||
t.record_count("instances_processed", shared.instances_processed as u64);
|
t.record_count("instances_processed", shared.instances_processed as u64);
|
||||||
@ -1705,7 +2239,21 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
|||||||
};
|
};
|
||||||
let report_output = report_result?;
|
let report_output = report_result?;
|
||||||
let ccr_output = ccr_result?;
|
let ccr_output = ccr_result?;
|
||||||
let report = report_output.report;
|
record_memory_checkpoint(
|
||||||
|
&mut memory_checkpoints,
|
||||||
|
"after_report_and_ccr",
|
||||||
|
&total_started,
|
||||||
|
store.as_ref(),
|
||||||
|
);
|
||||||
|
if enable_memory_trim_probe {
|
||||||
|
malloc_trim_probes.push(crate::memory_telemetry::malloc_trim_probe());
|
||||||
|
record_memory_checkpoint(
|
||||||
|
&mut memory_checkpoints,
|
||||||
|
"after_report_and_ccr_malloc_trim",
|
||||||
|
&total_started,
|
||||||
|
store.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
let report_build_ms = report_output.report_build_ms;
|
let report_build_ms = report_output.report_build_ms;
|
||||||
let report_write_ms = report_output.report_write_ms;
|
let report_write_ms = report_output.report_write_ms;
|
||||||
let ccr_build_ms = ccr_output.ccr_build_ms;
|
let ccr_build_ms = ccr_output.ccr_build_ms;
|
||||||
@ -1723,6 +2271,12 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
|||||||
)?;
|
)?;
|
||||||
let compare_view_build_ms = compare_view_output.build_ms;
|
let compare_view_build_ms = compare_view_output.build_ms;
|
||||||
let compare_view_write_ms = compare_view_output.write_ms;
|
let compare_view_write_ms = compare_view_output.write_ms;
|
||||||
|
record_memory_checkpoint(
|
||||||
|
&mut memory_checkpoints,
|
||||||
|
"after_compare_view",
|
||||||
|
&total_started,
|
||||||
|
store.as_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
let mut cir_build_cir_ms = None;
|
let mut cir_build_cir_ms = None;
|
||||||
let mut cir_write_cir_ms = None;
|
let mut cir_write_cir_ms = None;
|
||||||
@ -1775,7 +2329,19 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
|||||||
summary.timing.write_cir_ms,
|
summary.timing.write_cir_ms,
|
||||||
summary.timing.total_ms
|
summary.timing.total_ms
|
||||||
);
|
);
|
||||||
|
record_memory_checkpoint(
|
||||||
|
&mut memory_checkpoints,
|
||||||
|
"after_cir",
|
||||||
|
&total_started,
|
||||||
|
store.as_ref(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
record_memory_checkpoint(
|
||||||
|
&mut memory_checkpoints,
|
||||||
|
"before_stage_timing",
|
||||||
|
&total_started,
|
||||||
|
store.as_ref(),
|
||||||
|
);
|
||||||
let stage_timing = RunStageTiming {
|
let stage_timing = RunStageTiming {
|
||||||
validation_ms,
|
validation_ms,
|
||||||
report_build_ms,
|
report_build_ms,
|
||||||
@ -1796,6 +2362,13 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
|||||||
rrdp_download_ms_total,
|
rrdp_download_ms_total,
|
||||||
rsync_download_ms_total,
|
rsync_download_ms_total,
|
||||||
download_bytes_total,
|
download_bytes_total,
|
||||||
|
vcir_storage_summary_ms,
|
||||||
|
vcir_storage,
|
||||||
|
memory_telemetry: Some(MemoryTelemetrySummary {
|
||||||
|
checkpoints: memory_checkpoints,
|
||||||
|
object_graph: Some(estimate_shared_object_graph(&shared)),
|
||||||
|
malloc_trim_probes,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
let stage_timing_anchor_path = args
|
let stage_timing_anchor_path = args
|
||||||
.report_json_path
|
.report_json_path
|
||||||
@ -1849,11 +2422,7 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
|||||||
eprintln!("analysis: wrote {}", pb_path.display());
|
eprintln!("analysis: wrote {}", pb_path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(report) = report.as_ref() {
|
print_summary_from_shared(validation_time, &shared);
|
||||||
print_summary(report);
|
|
||||||
} else {
|
|
||||||
print_summary_from_shared(validation_time, &shared);
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
use std::io::BufWriter;
|
use std::io::BufWriter;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::audit::AuditReportV2;
|
use serde::Serialize;
|
||||||
|
use serde::ser::SerializeSeq;
|
||||||
|
|
||||||
|
use crate::audit::{AspaOutput, AuditRunMeta, AuditWarning, VrpOutput};
|
||||||
use crate::ccr::canonical_vrp_prefix;
|
use crate::ccr::canonical_vrp_prefix;
|
||||||
|
|
||||||
use super::{PostValidationShared, RunStageTiming};
|
use super::{PostValidationShared, RunStageTiming};
|
||||||
@ -12,9 +15,9 @@ pub(super) enum ReportJsonFormat {
|
|||||||
Compact,
|
Compact,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn write_json(
|
pub(super) fn write_json<T: Serialize>(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
report: &AuditReportV2,
|
report: &T,
|
||||||
format: ReportJsonFormat,
|
format: ReportJsonFormat,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let f = std::fs::File::create(path)
|
let f = std::fs::File::create(path)
|
||||||
@ -28,6 +31,128 @@ pub(super) fn write_json(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub(super) struct ReportJsonWriteTiming {
|
||||||
|
pub(super) build_ms: u64,
|
||||||
|
pub(super) write_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct BorrowedAuditReportV2<'a> {
|
||||||
|
format_version: u32,
|
||||||
|
meta: AuditRunMeta,
|
||||||
|
policy: &'a crate::policy::Policy,
|
||||||
|
tree: BorrowedTreeSummary<'a>,
|
||||||
|
publication_points: &'a [crate::audit::PublicationPointAudit],
|
||||||
|
vrps: VrpReportSequence<'a>,
|
||||||
|
aspas: AspaReportSequence<'a>,
|
||||||
|
downloads: &'a [crate::audit::AuditDownloadEvent],
|
||||||
|
download_stats: &'a crate::audit::AuditDownloadStats,
|
||||||
|
repo_sync_stats: crate::audit::AuditRepoSyncStats,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct BorrowedTreeSummary<'a> {
|
||||||
|
instances_processed: usize,
|
||||||
|
instances_failed: usize,
|
||||||
|
warnings: WarningReportSequence<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WarningReportSequence<'a>(&'a [crate::report::Warning]);
|
||||||
|
|
||||||
|
impl Serialize for WarningReportSequence<'_> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
||||||
|
for warning in self.0 {
|
||||||
|
seq.serialize_element(&AuditWarning::from(warning))?;
|
||||||
|
}
|
||||||
|
seq.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VrpReportSequence<'a>(&'a [crate::validation::objects::Vrp]);
|
||||||
|
|
||||||
|
impl Serialize for VrpReportSequence<'_> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
||||||
|
for vrp in self.0 {
|
||||||
|
seq.serialize_element(&VrpOutput {
|
||||||
|
asn: vrp.asn,
|
||||||
|
prefix: crate::audit::format_roa_ip_prefix(&vrp.prefix),
|
||||||
|
max_length: vrp.max_length,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
seq.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AspaReportSequence<'a>(&'a [crate::validation::objects::AspaAttestation]);
|
||||||
|
|
||||||
|
impl Serialize for AspaReportSequence<'_> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
||||||
|
for aspa in self.0 {
|
||||||
|
seq.serialize_element(&AspaOutput {
|
||||||
|
customer_as_id: aspa.customer_as_id,
|
||||||
|
provider_as_ids: aspa.provider_as_ids.clone(),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
seq.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn write_report_json_from_shared(
|
||||||
|
path: &Path,
|
||||||
|
policy: &crate::policy::Policy,
|
||||||
|
validation_time: time::OffsetDateTime,
|
||||||
|
shared: &PostValidationShared,
|
||||||
|
format: ReportJsonFormat,
|
||||||
|
) -> Result<ReportJsonWriteTiming, String> {
|
||||||
|
use time::format_description::well_known::Rfc3339;
|
||||||
|
|
||||||
|
let build_started = std::time::Instant::now();
|
||||||
|
let validation_time_rfc3339_utc = validation_time
|
||||||
|
.to_offset(time::UtcOffset::UTC)
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.expect("format validation_time");
|
||||||
|
let repo_sync_stats = super::build_repo_sync_stats(shared.publication_points.as_ref());
|
||||||
|
let report = BorrowedAuditReportV2 {
|
||||||
|
format_version: 2,
|
||||||
|
meta: AuditRunMeta {
|
||||||
|
validation_time_rfc3339_utc,
|
||||||
|
},
|
||||||
|
policy,
|
||||||
|
tree: BorrowedTreeSummary {
|
||||||
|
instances_processed: shared.instances_processed,
|
||||||
|
instances_failed: shared.instances_failed,
|
||||||
|
warnings: WarningReportSequence(shared.tree_warnings.as_ref()),
|
||||||
|
},
|
||||||
|
publication_points: shared.publication_points.as_ref(),
|
||||||
|
vrps: VrpReportSequence(shared.vrps.as_ref()),
|
||||||
|
aspas: AspaReportSequence(shared.aspas.as_ref()),
|
||||||
|
downloads: shared.downloads.as_ref(),
|
||||||
|
download_stats: &shared.download_stats,
|
||||||
|
repo_sync_stats,
|
||||||
|
};
|
||||||
|
let build_ms = build_started.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
|
let write_started = std::time::Instant::now();
|
||||||
|
write_json(path, &report, format)?;
|
||||||
|
Ok(ReportJsonWriteTiming {
|
||||||
|
build_ms,
|
||||||
|
write_ms: write_started.elapsed().as_millis() as u64,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub(super) struct CompareViewTaskOutput {
|
pub(super) struct CompareViewTaskOutput {
|
||||||
pub(super) build_ms: Option<u64>,
|
pub(super) build_ms: Option<u64>,
|
||||||
|
|||||||
224
src/cli/tests.rs
224
src/cli/tests.rs
@ -1,4 +1,12 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::memory_telemetry::{
|
||||||
|
MemoryTelemetryCheckpoint, MemoryTelemetrySummary, ProcessMemorySnapshot,
|
||||||
|
};
|
||||||
|
use crate::storage::{
|
||||||
|
RocksDbMemorySnapshot, RocksDbMemoryTotals, VcirCcrProjectionSizeBreakdown,
|
||||||
|
VcirChildResourceSizeBreakdown, VcirCoreFieldSizeBreakdown, VcirFieldSizeBreakdown,
|
||||||
|
VcirStorageEntrySummary, VcirStorageSummary,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_help_returns_usage() {
|
fn parse_help_returns_usage() {
|
||||||
@ -9,6 +17,7 @@ fn parse_help_returns_usage() {
|
|||||||
assert!(err.contains("--rsync-mirror-root"), "{err}");
|
assert!(err.contains("--rsync-mirror-root"), "{err}");
|
||||||
assert!(err.contains("--rsync-scope"), "{err}");
|
assert!(err.contains("--rsync-scope"), "{err}");
|
||||||
assert!(err.contains("--parallel-phase2-object-workers"), "{err}");
|
assert!(err.contains("--parallel-phase2-object-workers"), "{err}");
|
||||||
|
assert!(err.contains("--memory-trim-after-validation"), "{err}");
|
||||||
assert!(!err.contains("--parallel-phase1"), "{err}");
|
assert!(!err.contains("--parallel-phase1"), "{err}");
|
||||||
assert!(!err.contains("--parallel-phase2 "), "{err}");
|
assert!(!err.contains("--parallel-phase2 "), "{err}");
|
||||||
}
|
}
|
||||||
@ -82,6 +91,37 @@ fn parse_accepts_ccr_out_path() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_accepts_memory_trim_after_validation() {
|
||||||
|
let argv = vec![
|
||||||
|
"rpki".to_string(),
|
||||||
|
"--db".to_string(),
|
||||||
|
"db".to_string(),
|
||||||
|
"--tal-path".to_string(),
|
||||||
|
"x.tal".to_string(),
|
||||||
|
"--ta-path".to_string(),
|
||||||
|
"x.cer".to_string(),
|
||||||
|
"--memory-trim-after-validation".to_string(),
|
||||||
|
];
|
||||||
|
let args = parse_args(&argv).expect("parse args");
|
||||||
|
assert!(args.memory_trim_after_validation);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_disables_memory_trim_after_validation_by_default() {
|
||||||
|
let argv = vec![
|
||||||
|
"rpki".to_string(),
|
||||||
|
"--db".to_string(),
|
||||||
|
"db".to_string(),
|
||||||
|
"--tal-path".to_string(),
|
||||||
|
"x.tal".to_string(),
|
||||||
|
"--ta-path".to_string(),
|
||||||
|
"x.cer".to_string(),
|
||||||
|
];
|
||||||
|
let args = parse_args(&argv).expect("parse args");
|
||||||
|
assert!(!args.memory_trim_after_validation);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_accepts_report_json_compact_when_report_json_is_set() {
|
fn parse_accepts_report_json_compact_when_report_json_is_set() {
|
||||||
let argv = vec![
|
let argv = vec![
|
||||||
@ -1414,13 +1454,14 @@ fn run_report_task_and_stage_timing_work() {
|
|||||||
)
|
)
|
||||||
.expect("run report task");
|
.expect("run report task");
|
||||||
|
|
||||||
let report = report_output.report.as_ref().expect("report built");
|
|
||||||
assert_eq!(report.vrps.len(), 2);
|
|
||||||
assert_eq!(report.aspas.len(), 1);
|
|
||||||
assert!(report_output.report_write_ms.is_some());
|
assert!(report_output.report_write_ms.is_some());
|
||||||
|
|
||||||
let report_json = std::fs::read_to_string(&report_path).expect("read report json");
|
let report_json = std::fs::read_to_string(&report_path).expect("read report json");
|
||||||
assert!(!report_json.contains('\n'), "{report_json}");
|
assert!(!report_json.contains('\n'), "{report_json}");
|
||||||
|
let report: serde_json::Value =
|
||||||
|
serde_json::from_str(&report_json).expect("parse compact report json");
|
||||||
|
assert_eq!(report["vrps"].as_array().unwrap().len(), 2);
|
||||||
|
assert_eq!(report["aspas"].as_array().unwrap().len(), 1);
|
||||||
|
|
||||||
let stage_timing = RunStageTiming {
|
let stage_timing = RunStageTiming {
|
||||||
validation_ms: 1,
|
validation_ms: 1,
|
||||||
@ -1442,12 +1483,62 @@ fn run_report_task_and_stage_timing_work() {
|
|||||||
rrdp_download_ms_total: 13,
|
rrdp_download_ms_total: 13,
|
||||||
rsync_download_ms_total: 14,
|
rsync_download_ms_total: 14,
|
||||||
download_bytes_total: 15,
|
download_bytes_total: 15,
|
||||||
|
vcir_storage_summary_ms: Some(16),
|
||||||
|
vcir_storage: Some(VcirStorageSummary {
|
||||||
|
entry_count: 2,
|
||||||
|
vcir_value_bytes: 100,
|
||||||
|
vcir_value_bytes_max: 60,
|
||||||
|
vcir_value_bytes_max_manifest_rsync_uri: Some(
|
||||||
|
"rsync://example.test/repo/max.mft".to_string(),
|
||||||
|
),
|
||||||
|
core_fields: VcirCoreFieldSizeBreakdown {
|
||||||
|
manifest_rsync_uri_bytes: 10,
|
||||||
|
..VcirCoreFieldSizeBreakdown::default()
|
||||||
|
},
|
||||||
|
ccr_projection: VcirCcrProjectionSizeBreakdown {
|
||||||
|
manifest_sha256_bytes: 32,
|
||||||
|
..VcirCcrProjectionSizeBreakdown::default()
|
||||||
|
},
|
||||||
|
child_resources: VcirChildResourceSizeBreakdown {
|
||||||
|
effective_ip_resource_cbor_bytes: 12,
|
||||||
|
effective_as_resource_cbor_bytes: 6,
|
||||||
|
},
|
||||||
|
field_sizes: VcirFieldSizeBreakdown {
|
||||||
|
local_output_count: 1,
|
||||||
|
local_output_payload_json_bytes: 70,
|
||||||
|
local_output_payload_typed_body_bytes: 20,
|
||||||
|
..VcirFieldSizeBreakdown::default()
|
||||||
|
},
|
||||||
|
local_output_old_projection_bytes: 80,
|
||||||
|
local_output_typed_projection_bytes: 30,
|
||||||
|
local_output_projection_saved_bytes: 50,
|
||||||
|
top_entries_by_vcir_value_bytes: vec![VcirStorageEntrySummary {
|
||||||
|
manifest_rsync_uri: "rsync://example.test/repo/max.mft".to_string(),
|
||||||
|
vcir_value_bytes: 60,
|
||||||
|
local_vrp_count: 1,
|
||||||
|
local_aspa_count: 0,
|
||||||
|
local_router_key_count: 0,
|
||||||
|
accepted_object_count: 1,
|
||||||
|
rejected_object_count: 0,
|
||||||
|
child_count: 0,
|
||||||
|
core_fields: VcirCoreFieldSizeBreakdown::default(),
|
||||||
|
ccr_projection: VcirCcrProjectionSizeBreakdown::default(),
|
||||||
|
child_resources: VcirChildResourceSizeBreakdown::default(),
|
||||||
|
field_sizes: VcirFieldSizeBreakdown::default(),
|
||||||
|
local_output_old_projection_bytes: 1,
|
||||||
|
local_output_typed_projection_bytes: 1,
|
||||||
|
local_output_projection_saved_bytes: 0,
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
memory_telemetry: None,
|
||||||
};
|
};
|
||||||
write_stage_timing(Some(&report_path), &stage_timing).expect("write stage timing");
|
write_stage_timing(Some(&report_path), &stage_timing).expect("write stage timing");
|
||||||
let stage_timing_json =
|
let stage_timing_json =
|
||||||
std::fs::read_to_string(dir.path().join("stage-timing.json")).expect("read timing");
|
std::fs::read_to_string(dir.path().join("stage-timing.json")).expect("read timing");
|
||||||
assert!(stage_timing_json.contains("\"validation_ms\""));
|
assert!(stage_timing_json.contains("\"validation_ms\""));
|
||||||
assert!(stage_timing_json.contains("\"ccr_build_ms\""));
|
assert!(stage_timing_json.contains("\"ccr_build_ms\""));
|
||||||
|
assert!(stage_timing_json.contains("\"vcir_storage\""));
|
||||||
|
assert!(stage_timing_json.contains("\"local_output_projection_saved_bytes\""));
|
||||||
|
|
||||||
let ccr_path = dir.path().join("result.ccr");
|
let ccr_path = dir.path().join("result.ccr");
|
||||||
write_stage_timing(Some(&ccr_path), &stage_timing).expect("write stage timing via ccr path");
|
write_stage_timing(Some(&ccr_path), &stage_timing).expect("write stage timing via ccr path");
|
||||||
@ -1457,11 +1548,136 @@ fn run_report_task_and_stage_timing_work() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let skipped = ReportTaskOutput::skipped();
|
let skipped = ReportTaskOutput::skipped();
|
||||||
assert!(skipped.report.is_none());
|
|
||||||
assert_eq!(skipped.report_build_ms, 0);
|
assert_eq!(skipped.report_build_ms, 0);
|
||||||
assert!(skipped.report_write_ms.is_none());
|
assert!(skipped.report_write_ms.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stage_timing_serializes_memory_telemetry() {
|
||||||
|
let dir = tempfile::tempdir().expect("tmpdir");
|
||||||
|
let report_path = dir.path().join("report.json");
|
||||||
|
let stage_timing = RunStageTiming {
|
||||||
|
validation_ms: 1,
|
||||||
|
report_build_ms: 2,
|
||||||
|
report_write_ms: None,
|
||||||
|
ccr_build_ms: None,
|
||||||
|
ccr_build_breakdown: None,
|
||||||
|
ccr_write_ms: None,
|
||||||
|
compare_view_build_ms: None,
|
||||||
|
compare_view_write_ms: None,
|
||||||
|
cir_build_cir_ms: None,
|
||||||
|
cir_write_cir_ms: None,
|
||||||
|
cir_total_ms: None,
|
||||||
|
total_ms: 3,
|
||||||
|
publication_points: 4,
|
||||||
|
repo_sync_ms_total: 5,
|
||||||
|
publication_point_repo_sync_ms_total: 6,
|
||||||
|
download_event_count: 7,
|
||||||
|
rrdp_download_ms_total: 8,
|
||||||
|
rsync_download_ms_total: 9,
|
||||||
|
download_bytes_total: 10,
|
||||||
|
vcir_storage_summary_ms: None,
|
||||||
|
vcir_storage: None,
|
||||||
|
memory_telemetry: Some(MemoryTelemetrySummary {
|
||||||
|
checkpoints: vec![MemoryTelemetryCheckpoint {
|
||||||
|
label: "after_validation".to_string(),
|
||||||
|
elapsed_ms: 11,
|
||||||
|
process: ProcessMemorySnapshot {
|
||||||
|
label: "after_validation".to_string(),
|
||||||
|
vm_rss_kb: Some(12),
|
||||||
|
vm_size_kb: None,
|
||||||
|
vm_data_kb: None,
|
||||||
|
vm_swap_kb: None,
|
||||||
|
rss_anon_kb: Some(13),
|
||||||
|
rss_file_kb: None,
|
||||||
|
rss_shmem_kb: None,
|
||||||
|
threads: Some(14),
|
||||||
|
fd_count: Some(15),
|
||||||
|
smaps_rollup: None,
|
||||||
|
smaps_mapping_summary: None,
|
||||||
|
errors: Vec::new(),
|
||||||
|
},
|
||||||
|
rocksdb: RocksDbMemorySnapshot {
|
||||||
|
databases: Vec::new(),
|
||||||
|
totals: RocksDbMemoryTotals {
|
||||||
|
cur_size_all_mem_tables: 16,
|
||||||
|
size_all_mem_tables: 17,
|
||||||
|
estimate_table_readers_mem: 18,
|
||||||
|
block_cache_capacity: 19,
|
||||||
|
block_cache_usage: 20,
|
||||||
|
block_cache_pinned_usage: 21,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
object_graph: None,
|
||||||
|
malloc_trim_probes: Vec::new(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
write_stage_timing(Some(&report_path), &stage_timing).expect("write stage timing");
|
||||||
|
let value: serde_json::Value = serde_json::from_str(
|
||||||
|
&std::fs::read_to_string(dir.path().join("stage-timing.json")).unwrap(),
|
||||||
|
)
|
||||||
|
.expect("parse stage timing json");
|
||||||
|
let checkpoint = &value["memory_telemetry"]["checkpoints"][0];
|
||||||
|
assert_eq!(checkpoint["label"], "after_validation");
|
||||||
|
assert_eq!(checkpoint["process"]["vm_rss_kb"], 12);
|
||||||
|
assert_eq!(
|
||||||
|
checkpoint["rocksdb"]["totals"]["cur_size_all_mem_tables"],
|
||||||
|
16
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
value["memory_telemetry"]
|
||||||
|
.as_object()
|
||||||
|
.expect("memory telemetry object")
|
||||||
|
.get("malloc_trim_probes")
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shared_object_graph_estimate_counts_audit_and_outputs() {
|
||||||
|
let mut shared = synthetic_post_validation_shared();
|
||||||
|
let mut publication_points = shared
|
||||||
|
.publication_points
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
publication_points[0].rsync_base_uri = "rsync://example.test/repo/".to_string();
|
||||||
|
publication_points[0].manifest_rsync_uri = "rsync://example.test/repo/a.mft".to_string();
|
||||||
|
publication_points[0].publication_point_rsync_uri = "rsync://example.test/repo/".to_string();
|
||||||
|
publication_points[0].objects = vec![crate::audit::ObjectAuditEntry {
|
||||||
|
rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
|
||||||
|
sha256_hex: "11".repeat(32),
|
||||||
|
kind: crate::audit::AuditObjectKind::Roa,
|
||||||
|
result: crate::audit::AuditObjectResult::Ok,
|
||||||
|
detail: None,
|
||||||
|
}];
|
||||||
|
shared.publication_points = publication_points.into();
|
||||||
|
|
||||||
|
let graph = estimate_shared_object_graph(&shared);
|
||||||
|
let publication_points_section = graph
|
||||||
|
.sections
|
||||||
|
.iter()
|
||||||
|
.find(|section| section.name == "publication_points")
|
||||||
|
.expect("publication points section");
|
||||||
|
let object_count = publication_points_section
|
||||||
|
.details
|
||||||
|
.iter()
|
||||||
|
.find(|metric| metric.name == "object_audit_entry_count")
|
||||||
|
.expect("object count metric");
|
||||||
|
assert_eq!(object_count.value, 1);
|
||||||
|
assert!(publication_points_section.estimated_bytes > 0);
|
||||||
|
|
||||||
|
let vrps_section = graph
|
||||||
|
.sections
|
||||||
|
.iter()
|
||||||
|
.find(|section| section.name == "vrps")
|
||||||
|
.expect("vrps section");
|
||||||
|
assert_eq!(vrps_section.item_count, 2);
|
||||||
|
assert!(graph.total_estimated_bytes >= publication_points_section.estimated_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn run_compare_view_task_writes_csv_from_shared_output() {
|
fn run_compare_view_task_writes_csv_from_shared_output() {
|
||||||
let shared = synthetic_post_validation_shared();
|
let shared = synthetic_post_validation_shared();
|
||||||
|
|||||||
@ -217,7 +217,9 @@ impl RoaObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
|
||||||
|
)]
|
||||||
pub enum RoaAfi {
|
pub enum RoaAfi {
|
||||||
Ipv4,
|
Ipv4,
|
||||||
Ipv6,
|
Ipv6,
|
||||||
|
|||||||
@ -9,8 +9,6 @@ pub mod audit;
|
|||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod audit_downloads;
|
pub mod audit_downloads;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod audit_trace;
|
|
||||||
#[cfg(feature = "full")]
|
|
||||||
pub mod blob_store;
|
pub mod blob_store;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
@ -19,6 +17,8 @@ pub mod current_repo_index;
|
|||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod fetch;
|
pub mod fetch;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
|
pub mod memory_telemetry;
|
||||||
|
#[cfg(feature = "full")]
|
||||||
pub mod parallel;
|
pub mod parallel;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod policy;
|
pub mod policy;
|
||||||
|
|||||||
348
src/memory_telemetry.rs
Normal file
348
src/memory_telemetry.rs
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::storage::RocksDbMemorySnapshot;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct MallocTrimProbe {
|
||||||
|
pub supported: bool,
|
||||||
|
pub return_value: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct ProcessMemorySnapshot {
|
||||||
|
pub label: String,
|
||||||
|
pub vm_rss_kb: Option<u64>,
|
||||||
|
pub vm_size_kb: Option<u64>,
|
||||||
|
pub vm_data_kb: Option<u64>,
|
||||||
|
pub vm_swap_kb: Option<u64>,
|
||||||
|
pub rss_anon_kb: Option<u64>,
|
||||||
|
pub rss_file_kb: Option<u64>,
|
||||||
|
pub rss_shmem_kb: Option<u64>,
|
||||||
|
pub threads: Option<u64>,
|
||||||
|
pub fd_count: Option<u64>,
|
||||||
|
pub smaps_rollup: Option<SmapsRollupSnapshot>,
|
||||||
|
pub smaps_mapping_summary: Option<SmapsMappingSummary>,
|
||||||
|
pub errors: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct SmapsRollupSnapshot {
|
||||||
|
pub rss_kb: Option<u64>,
|
||||||
|
pub pss_kb: Option<u64>,
|
||||||
|
pub shared_clean_kb: Option<u64>,
|
||||||
|
pub shared_dirty_kb: Option<u64>,
|
||||||
|
pub private_clean_kb: Option<u64>,
|
||||||
|
pub private_dirty_kb: Option<u64>,
|
||||||
|
pub anonymous_kb: Option<u64>,
|
||||||
|
pub swap_kb: Option<u64>,
|
||||||
|
pub swap_pss_kb: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct SmapsMappingSummary {
|
||||||
|
pub heap: SmapsMappingCategory,
|
||||||
|
pub anonymous_mmap: SmapsMappingCategory,
|
||||||
|
pub file_backed: SmapsMappingCategory,
|
||||||
|
pub stack: SmapsMappingCategory,
|
||||||
|
pub special: SmapsMappingCategory,
|
||||||
|
pub total: SmapsMappingCategory,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct SmapsMappingCategory {
|
||||||
|
pub mappings: u64,
|
||||||
|
pub size_kb: u64,
|
||||||
|
pub rss_kb: u64,
|
||||||
|
pub pss_kb: u64,
|
||||||
|
pub private_clean_kb: u64,
|
||||||
|
pub private_dirty_kb: u64,
|
||||||
|
pub anonymous_kb: u64,
|
||||||
|
pub largest_mapping_rss_kb: u64,
|
||||||
|
pub large_mapping_count_64m: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct MemoryTelemetryCheckpoint {
|
||||||
|
pub label: String,
|
||||||
|
pub elapsed_ms: u64,
|
||||||
|
pub process: ProcessMemorySnapshot,
|
||||||
|
pub rocksdb: RocksDbMemorySnapshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct MemoryTelemetrySummary {
|
||||||
|
pub checkpoints: Vec<MemoryTelemetryCheckpoint>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub object_graph: Option<ObjectGraphMemorySummary>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub malloc_trim_probes: Vec<MallocTrimProbe>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct ObjectGraphMemorySummary {
|
||||||
|
pub captured_at_label: String,
|
||||||
|
pub total_estimated_bytes: u64,
|
||||||
|
pub sections: Vec<ObjectGraphMemorySection>,
|
||||||
|
pub notes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct ObjectGraphMemorySection {
|
||||||
|
pub name: String,
|
||||||
|
pub item_count: u64,
|
||||||
|
pub shallow_bytes: u64,
|
||||||
|
pub heap_bytes: u64,
|
||||||
|
pub estimated_bytes: u64,
|
||||||
|
pub string_count: u64,
|
||||||
|
pub string_bytes: u64,
|
||||||
|
pub string_capacity_bytes: u64,
|
||||||
|
pub vec_count: u64,
|
||||||
|
pub vec_heap_bytes: u64,
|
||||||
|
pub vec_capacity_bytes: u64,
|
||||||
|
pub details: Vec<ObjectGraphMemoryMetric>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct ObjectGraphMemoryMetric {
|
||||||
|
pub name: String,
|
||||||
|
pub value: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_memory_snapshot(label: impl Into<String>) -> ProcessMemorySnapshot {
|
||||||
|
let label = label.into();
|
||||||
|
let mut snapshot = ProcessMemorySnapshot {
|
||||||
|
label,
|
||||||
|
vm_rss_kb: None,
|
||||||
|
vm_size_kb: None,
|
||||||
|
vm_data_kb: None,
|
||||||
|
vm_swap_kb: None,
|
||||||
|
rss_anon_kb: None,
|
||||||
|
rss_file_kb: None,
|
||||||
|
rss_shmem_kb: None,
|
||||||
|
threads: None,
|
||||||
|
fd_count: current_fd_count(),
|
||||||
|
smaps_rollup: None,
|
||||||
|
smaps_mapping_summary: None,
|
||||||
|
errors: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match std::fs::read_to_string("/proc/self/status") {
|
||||||
|
Ok(status) => parse_status(&status, &mut snapshot),
|
||||||
|
Err(err) => snapshot
|
||||||
|
.errors
|
||||||
|
.push(format!("read /proc/self/status failed: {err}")),
|
||||||
|
}
|
||||||
|
|
||||||
|
match std::fs::read_to_string("/proc/self/smaps_rollup") {
|
||||||
|
Ok(smaps) => snapshot.smaps_rollup = Some(parse_smaps_rollup(&smaps)),
|
||||||
|
Err(err) => snapshot
|
||||||
|
.errors
|
||||||
|
.push(format!("read /proc/self/smaps_rollup failed: {err}")),
|
||||||
|
}
|
||||||
|
|
||||||
|
match std::fs::read_to_string("/proc/self/smaps") {
|
||||||
|
Ok(smaps) => snapshot.smaps_mapping_summary = Some(parse_smaps_mapping_summary(&smaps)),
|
||||||
|
Err(err) => snapshot
|
||||||
|
.errors
|
||||||
|
.push(format!("read /proc/self/smaps failed: {err}")),
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn malloc_trim_probe() -> MallocTrimProbe {
|
||||||
|
#[cfg(all(target_os = "linux", target_env = "gnu"))]
|
||||||
|
{
|
||||||
|
MallocTrimProbe {
|
||||||
|
supported: true,
|
||||||
|
return_value: Some(unsafe { malloc_trim(0) }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
|
||||||
|
{
|
||||||
|
MallocTrimProbe {
|
||||||
|
supported: false,
|
||||||
|
return_value: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(target_os = "linux", target_env = "gnu"))]
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn malloc_trim(pad: usize) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_fd_count() -> Option<u64> {
|
||||||
|
std::fs::read_dir("/proc/self/fd")
|
||||||
|
.ok()
|
||||||
|
.map(|entries| entries.filter_map(Result::ok).count() as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_status(status: &str, snapshot: &mut ProcessMemorySnapshot) {
|
||||||
|
for line in status.lines() {
|
||||||
|
let Some((key, value)) = line.split_once(':') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let parsed = parse_kb_or_plain_u64(value);
|
||||||
|
match key {
|
||||||
|
"VmRSS" => snapshot.vm_rss_kb = parsed,
|
||||||
|
"VmSize" => snapshot.vm_size_kb = parsed,
|
||||||
|
"VmData" => snapshot.vm_data_kb = parsed,
|
||||||
|
"VmSwap" => snapshot.vm_swap_kb = parsed,
|
||||||
|
"RssAnon" => snapshot.rss_anon_kb = parsed,
|
||||||
|
"RssFile" => snapshot.rss_file_kb = parsed,
|
||||||
|
"RssShmem" => snapshot.rss_shmem_kb = parsed,
|
||||||
|
"Threads" => snapshot.threads = parsed,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_smaps_rollup(smaps: &str) -> SmapsRollupSnapshot {
|
||||||
|
let mut snapshot = SmapsRollupSnapshot::default();
|
||||||
|
for line in smaps.lines() {
|
||||||
|
let Some((key, value)) = line.split_once(':') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let parsed = parse_kb_or_plain_u64(value);
|
||||||
|
match key {
|
||||||
|
"Rss" => snapshot.rss_kb = parsed,
|
||||||
|
"Pss" => snapshot.pss_kb = parsed,
|
||||||
|
"Shared_Clean" => snapshot.shared_clean_kb = parsed,
|
||||||
|
"Shared_Dirty" => snapshot.shared_dirty_kb = parsed,
|
||||||
|
"Private_Clean" => snapshot.private_clean_kb = parsed,
|
||||||
|
"Private_Dirty" => snapshot.private_dirty_kb = parsed,
|
||||||
|
"Anonymous" => snapshot.anonymous_kb = parsed,
|
||||||
|
"Swap" => snapshot.swap_kb = parsed,
|
||||||
|
"SwapPss" => snapshot.swap_pss_kb = parsed,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_smaps_mapping_summary(smaps: &str) -> SmapsMappingSummary {
|
||||||
|
let mut summary = SmapsMappingSummary::default();
|
||||||
|
let mut current_path = String::new();
|
||||||
|
let mut current = SmapsMappingCategory::default();
|
||||||
|
let mut have_mapping = false;
|
||||||
|
|
||||||
|
for line in smaps.lines() {
|
||||||
|
if is_smaps_mapping_header(line) {
|
||||||
|
if have_mapping {
|
||||||
|
add_mapping(&mut summary, ¤t_path, ¤t);
|
||||||
|
}
|
||||||
|
current_path = smaps_header_path(line);
|
||||||
|
current = SmapsMappingCategory {
|
||||||
|
mappings: 1,
|
||||||
|
..SmapsMappingCategory::default()
|
||||||
|
};
|
||||||
|
have_mapping = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !have_mapping {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((key, value)) = line.split_once(':') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let parsed = parse_kb_or_plain_u64(value).unwrap_or(0);
|
||||||
|
match key {
|
||||||
|
"Size" => current.size_kb = parsed,
|
||||||
|
"Rss" => current.rss_kb = parsed,
|
||||||
|
"Pss" => current.pss_kb = parsed,
|
||||||
|
"Private_Clean" => current.private_clean_kb = parsed,
|
||||||
|
"Private_Dirty" => current.private_dirty_kb = parsed,
|
||||||
|
"Anonymous" => current.anonymous_kb = parsed,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if have_mapping {
|
||||||
|
add_mapping(&mut summary, ¤t_path, ¤t);
|
||||||
|
}
|
||||||
|
|
||||||
|
summary
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_smaps_mapping_header(line: &str) -> bool {
|
||||||
|
let mut parts = line.split_whitespace();
|
||||||
|
let Some(range) = parts.next() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(perms) = parts.next() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some((start, end)) = range.split_once('-') else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
!start.is_empty()
|
||||||
|
&& !end.is_empty()
|
||||||
|
&& start.as_bytes().iter().all(u8::is_ascii_hexdigit)
|
||||||
|
&& end.as_bytes().iter().all(u8::is_ascii_hexdigit)
|
||||||
|
&& perms.len() == 4
|
||||||
|
&& perms
|
||||||
|
.as_bytes()
|
||||||
|
.iter()
|
||||||
|
.all(|b| matches!(b, b'r' | b'w' | b'x' | b's' | b'p' | b'-'))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn smaps_header_path(line: &str) -> String {
|
||||||
|
line.split_whitespace()
|
||||||
|
.skip(5)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_mapping(summary: &mut SmapsMappingSummary, path: &str, mapping: &SmapsMappingCategory) {
|
||||||
|
add_category(&mut summary.total, mapping);
|
||||||
|
match mapping_category(path) {
|
||||||
|
MappingCategory::Heap => add_category(&mut summary.heap, mapping),
|
||||||
|
MappingCategory::AnonymousMmap => add_category(&mut summary.anonymous_mmap, mapping),
|
||||||
|
MappingCategory::FileBacked => add_category(&mut summary.file_backed, mapping),
|
||||||
|
MappingCategory::Stack => add_category(&mut summary.stack, mapping),
|
||||||
|
MappingCategory::Special => add_category(&mut summary.special, mapping),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_category(target: &mut SmapsMappingCategory, source: &SmapsMappingCategory) {
|
||||||
|
target.mappings += source.mappings;
|
||||||
|
target.size_kb += source.size_kb;
|
||||||
|
target.rss_kb += source.rss_kb;
|
||||||
|
target.pss_kb += source.pss_kb;
|
||||||
|
target.private_clean_kb += source.private_clean_kb;
|
||||||
|
target.private_dirty_kb += source.private_dirty_kb;
|
||||||
|
target.anonymous_kb += source.anonymous_kb;
|
||||||
|
target.largest_mapping_rss_kb = target.largest_mapping_rss_kb.max(source.rss_kb);
|
||||||
|
if source.rss_kb >= 64 * 1024 {
|
||||||
|
target.large_mapping_count_64m += source.mappings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MappingCategory {
|
||||||
|
Heap,
|
||||||
|
AnonymousMmap,
|
||||||
|
FileBacked,
|
||||||
|
Stack,
|
||||||
|
Special,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mapping_category(path: &str) -> MappingCategory {
|
||||||
|
if path == "[heap]" {
|
||||||
|
MappingCategory::Heap
|
||||||
|
} else if path.starts_with("[stack") {
|
||||||
|
MappingCategory::Stack
|
||||||
|
} else if path.is_empty() {
|
||||||
|
MappingCategory::AnonymousMmap
|
||||||
|
} else if path.starts_with('/') {
|
||||||
|
MappingCategory::FileBacked
|
||||||
|
} else {
|
||||||
|
MappingCategory::Special
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_kb_or_plain_u64(value: &str) -> Option<u64> {
|
||||||
|
value.split_whitespace().next()?.parse::<u64>().ok()
|
||||||
|
}
|
||||||
1080
src/storage.rs
1080
src/storage.rs
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,6 @@ pub const CF_RAW_BY_HASH: &str = "raw_by_hash";
|
|||||||
pub const CF_RAW_BLOB: &str = "raw_blob";
|
pub const CF_RAW_BLOB: &str = "raw_blob";
|
||||||
pub const CF_VCIR: &str = "vcir";
|
pub const CF_VCIR: &str = "vcir";
|
||||||
pub const CF_MANIFEST_REPLAY_META: &str = "manifest_replay_meta";
|
pub const CF_MANIFEST_REPLAY_META: &str = "manifest_replay_meta";
|
||||||
pub const CF_AUDIT_RULE_INDEX: &str = "audit_rule_index";
|
|
||||||
pub const CF_RRDP_SOURCE: &str = "rrdp_source";
|
pub const CF_RRDP_SOURCE: &str = "rrdp_source";
|
||||||
pub const CF_RRDP_SOURCE_MEMBER: &str = "rrdp_source_member";
|
pub const CF_RRDP_SOURCE_MEMBER: &str = "rrdp_source_member";
|
||||||
pub const CF_RRDP_URI_OWNER: &str = "rrdp_uri_owner";
|
pub const CF_RRDP_URI_OWNER: &str = "rrdp_uri_owner";
|
||||||
@ -16,7 +15,6 @@ pub const ALL_COLUMN_FAMILY_NAMES: &[&str] = &[
|
|||||||
CF_RAW_BLOB,
|
CF_RAW_BLOB,
|
||||||
CF_VCIR,
|
CF_VCIR,
|
||||||
CF_MANIFEST_REPLAY_META,
|
CF_MANIFEST_REPLAY_META,
|
||||||
CF_AUDIT_RULE_INDEX,
|
|
||||||
CF_RRDP_SOURCE,
|
CF_RRDP_SOURCE,
|
||||||
CF_RRDP_SOURCE_MEMBER,
|
CF_RRDP_SOURCE_MEMBER,
|
||||||
CF_RRDP_URI_OWNER,
|
CF_RRDP_URI_OWNER,
|
||||||
@ -27,14 +25,12 @@ pub(super) const RAW_BY_HASH_KEY_PREFIX: &str = "rawbyhash:";
|
|||||||
pub(super) const RAW_BLOB_KEY_PREFIX: &str = "rawblob:";
|
pub(super) const RAW_BLOB_KEY_PREFIX: &str = "rawblob:";
|
||||||
pub(super) const VCIR_KEY_PREFIX: &str = "vcir:";
|
pub(super) const VCIR_KEY_PREFIX: &str = "vcir:";
|
||||||
pub(super) const MANIFEST_REPLAY_META_KEY_PREFIX: &str = "manifest_replay_meta:";
|
pub(super) const MANIFEST_REPLAY_META_KEY_PREFIX: &str = "manifest_replay_meta:";
|
||||||
pub(super) const AUDIT_ROA_RULE_KEY_PREFIX: &str = "audit:roa_rule:";
|
|
||||||
pub(super) const AUDIT_ASPA_RULE_KEY_PREFIX: &str = "audit:aspa_rule:";
|
|
||||||
pub(super) const AUDIT_ROUTER_KEY_RULE_KEY_PREFIX: &str = "audit:router_key_rule:";
|
|
||||||
pub(super) const RRDP_SOURCE_KEY_PREFIX: &str = "rrdp_source:";
|
pub(super) const RRDP_SOURCE_KEY_PREFIX: &str = "rrdp_source:";
|
||||||
pub(super) const RRDP_SOURCE_MEMBER_KEY_PREFIX: &str = "rrdp_source_member:";
|
pub(super) const RRDP_SOURCE_MEMBER_KEY_PREFIX: &str = "rrdp_source_member:";
|
||||||
pub(super) const RRDP_URI_OWNER_KEY_PREFIX: &str = "rrdp_uri_owner:";
|
pub(super) const RRDP_URI_OWNER_KEY_PREFIX: &str = "rrdp_uri_owner:";
|
||||||
|
|
||||||
const WORK_DB_BLOB_MODE_ENV: &str = "RPKI_WORK_DB_BLOB_MODE";
|
const WORK_DB_BLOB_MODE_ENV: &str = "RPKI_WORK_DB_BLOB_MODE";
|
||||||
|
const WORK_DB_MEMORY_PROFILE_ENV: &str = "RPKI_WORK_DB_MEMORY_PROFILE";
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub(super) enum WorkDbBlobMode {
|
pub(super) enum WorkDbBlobMode {
|
||||||
@ -43,6 +39,12 @@ pub(super) enum WorkDbBlobMode {
|
|||||||
Lz4,
|
Lz4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(super) enum WorkDbMemoryProfile {
|
||||||
|
Default,
|
||||||
|
Compact,
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn parse_work_db_blob_mode(raw: &str) -> Option<WorkDbBlobMode> {
|
pub(super) fn parse_work_db_blob_mode(raw: &str) -> Option<WorkDbBlobMode> {
|
||||||
match raw.trim().to_ascii_lowercase().as_str() {
|
match raw.trim().to_ascii_lowercase().as_str() {
|
||||||
"" | "default" => Some(default_work_db_blob_mode()),
|
"" | "default" => Some(default_work_db_blob_mode()),
|
||||||
@ -74,8 +76,36 @@ pub(super) fn work_db_blob_mode_from_env() -> WorkDbBlobMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn configure_work_db_options(opts: &mut Options, blob_mode: WorkDbBlobMode) {
|
pub(super) fn parse_work_db_memory_profile(raw: &str) -> Option<WorkDbMemoryProfile> {
|
||||||
|
match raw.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"" | "default" | "off" | "none" => Some(WorkDbMemoryProfile::Default),
|
||||||
|
"compact" | "low" | "low_memory" | "low-memory" => Some(WorkDbMemoryProfile::Compact),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn work_db_memory_profile_from_env() -> WorkDbMemoryProfile {
|
||||||
|
let Ok(raw) = std::env::var(WORK_DB_MEMORY_PROFILE_ENV) else {
|
||||||
|
return WorkDbMemoryProfile::Default;
|
||||||
|
};
|
||||||
|
match parse_work_db_memory_profile(&raw) {
|
||||||
|
Some(profile) => profile,
|
||||||
|
None => {
|
||||||
|
eprintln!(
|
||||||
|
"warning: unsupported {WORK_DB_MEMORY_PROFILE_ENV}={raw:?}; using default work-db memory profile"
|
||||||
|
);
|
||||||
|
WorkDbMemoryProfile::Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn configure_work_db_options(
|
||||||
|
opts: &mut Options,
|
||||||
|
blob_mode: WorkDbBlobMode,
|
||||||
|
memory_profile: WorkDbMemoryProfile,
|
||||||
|
) {
|
||||||
opts.set_compression_type(DBCompressionType::Lz4);
|
opts.set_compression_type(DBCompressionType::Lz4);
|
||||||
|
apply_work_db_memory_profile(opts, memory_profile);
|
||||||
match blob_mode {
|
match blob_mode {
|
||||||
WorkDbBlobMode::Current => enable_blobdb_current(opts),
|
WorkDbBlobMode::Current => enable_blobdb_current(opts),
|
||||||
WorkDbBlobMode::Disabled => {}
|
WorkDbBlobMode::Disabled => {}
|
||||||
@ -86,9 +116,9 @@ pub(super) fn configure_work_db_options(opts: &mut Options, blob_mode: WorkDbBlo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn cf_opts(blob_mode: WorkDbBlobMode) -> Options {
|
pub(super) fn cf_opts(blob_mode: WorkDbBlobMode, memory_profile: WorkDbMemoryProfile) -> Options {
|
||||||
let mut opts = Options::default();
|
let mut opts = Options::default();
|
||||||
configure_work_db_options(&mut opts, blob_mode);
|
configure_work_db_options(&mut opts, blob_mode, memory_profile);
|
||||||
opts
|
opts
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,12 +129,23 @@ pub fn column_family_descriptors() -> Vec<ColumnFamilyDescriptor> {
|
|||||||
pub(super) fn column_family_descriptors_for_blob_mode(
|
pub(super) fn column_family_descriptors_for_blob_mode(
|
||||||
blob_mode: WorkDbBlobMode,
|
blob_mode: WorkDbBlobMode,
|
||||||
) -> Vec<ColumnFamilyDescriptor> {
|
) -> Vec<ColumnFamilyDescriptor> {
|
||||||
|
let memory_profile = work_db_memory_profile_from_env();
|
||||||
ALL_COLUMN_FAMILY_NAMES
|
ALL_COLUMN_FAMILY_NAMES
|
||||||
.iter()
|
.iter()
|
||||||
.map(|name| ColumnFamilyDescriptor::new(*name, cf_opts(blob_mode)))
|
.map(|name| ColumnFamilyDescriptor::new(*name, cf_opts(blob_mode, memory_profile)))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_work_db_memory_profile(opts: &mut Options, memory_profile: WorkDbMemoryProfile) {
|
||||||
|
match memory_profile {
|
||||||
|
WorkDbMemoryProfile::Default => {}
|
||||||
|
WorkDbMemoryProfile::Compact => {
|
||||||
|
opts.set_write_buffer_size(16 * 1024 * 1024);
|
||||||
|
opts.set_max_write_buffer_number(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn enable_blobdb_current(opts: &mut Options) {
|
pub(super) fn enable_blobdb_current(opts: &mut Options) {
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut _enabled = false;
|
let mut _enabled = false;
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use crate::data_model::common::der_take_tlv;
|
|||||||
|
|
||||||
use super::config::*;
|
use super::config::*;
|
||||||
use super::pack::PackTime;
|
use super::pack::PackTime;
|
||||||
use super::{AuditRuleKind, StorageError, StorageResult, VcirOutputType};
|
use super::{StorageError, StorageResult};
|
||||||
|
|
||||||
pub(super) fn repository_view_key(rsync_uri: &str) -> String {
|
pub(super) fn repository_view_key(rsync_uri: &str) -> String {
|
||||||
format!("{REPOSITORY_VIEW_KEY_PREFIX}{rsync_uri}")
|
format!("{REPOSITORY_VIEW_KEY_PREFIX}{rsync_uri}")
|
||||||
@ -30,20 +30,6 @@ pub(super) fn manifest_replay_meta_key(manifest_rsync_uri: &str) -> String {
|
|||||||
format!("{MANIFEST_REPLAY_META_KEY_PREFIX}{manifest_rsync_uri}")
|
format!("{MANIFEST_REPLAY_META_KEY_PREFIX}{manifest_rsync_uri}")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn audit_rule_kind_for_output_type(
|
|
||||||
output_type: VcirOutputType,
|
|
||||||
) -> Option<AuditRuleKind> {
|
|
||||||
match output_type {
|
|
||||||
VcirOutputType::Vrp => Some(AuditRuleKind::Roa),
|
|
||||||
VcirOutputType::Aspa => Some(AuditRuleKind::Aspa),
|
|
||||||
VcirOutputType::RouterKey => Some(AuditRuleKind::RouterKey),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn audit_rule_key(kind: AuditRuleKind, rule_hash: &str) -> String {
|
|
||||||
format!("{}{rule_hash}", kind.key_prefix())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn rrdp_source_key(notify_uri: &str) -> String {
|
pub(super) fn rrdp_source_key(notify_uri: &str) -> String {
|
||||||
format!("{RRDP_SOURCE_KEY_PREFIX}{notify_uri}")
|
format!("{RRDP_SOURCE_KEY_PREFIX}{notify_uri}")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,10 @@ fn sha256_hex(input: &[u8]) -> String {
|
|||||||
hex::encode(compute_sha256_32(input))
|
hex::encode(compute_sha256_32(input))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sha256_32(input: &[u8]) -> [u8; 32] {
|
||||||
|
compute_sha256_32(input)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_work_db_blob_mode_accepts_supported_values() {
|
fn parse_work_db_blob_mode_accepts_supported_values() {
|
||||||
assert_eq!(default_work_db_blob_mode(), WorkDbBlobMode::Disabled);
|
assert_eq!(default_work_db_blob_mode(), WorkDbBlobMode::Disabled);
|
||||||
@ -41,6 +45,61 @@ fn parse_work_db_blob_mode_accepts_supported_values() {
|
|||||||
assert_eq!(parse_work_db_blob_mode("unexpected"), None);
|
assert_eq!(parse_work_db_blob_mode("unexpected"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_work_db_memory_profile_accepts_supported_values() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_work_db_memory_profile("default"),
|
||||||
|
Some(WorkDbMemoryProfile::Default)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_work_db_memory_profile("none"),
|
||||||
|
Some(WorkDbMemoryProfile::Default)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_work_db_memory_profile("compact"),
|
||||||
|
Some(WorkDbMemoryProfile::Compact)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_work_db_memory_profile("low-memory"),
|
||||||
|
Some(WorkDbMemoryProfile::Compact)
|
||||||
|
);
|
||||||
|
assert_eq!(parse_work_db_memory_profile("unexpected"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vcir_field_size_breakdown_counts_local_outputs_and_artifacts() {
|
||||||
|
let vcir = sample_vcir("rsync://example.test/repo/current.mft");
|
||||||
|
let breakdown = VcirFieldSizeBreakdown::from_vcir(&vcir);
|
||||||
|
assert_eq!(breakdown.local_output_count, 2);
|
||||||
|
assert_eq!(
|
||||||
|
breakdown.local_output_payload_json_bytes,
|
||||||
|
vcir.local_outputs
|
||||||
|
.iter()
|
||||||
|
.map(|output| output.payload_json().len() as u64)
|
||||||
|
.sum::<u64>()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
breakdown.local_output_rule_hash_hex_bytes,
|
||||||
|
vcir.local_outputs.len() as u64 * 64
|
||||||
|
);
|
||||||
|
assert_eq!(breakdown.related_artifact_count, 2);
|
||||||
|
assert!(breakdown.related_artifact_uri_bytes > 0);
|
||||||
|
assert_eq!(breakdown.child_entry_count, 1);
|
||||||
|
assert!(breakdown.child_entry_uri_bytes > 0);
|
||||||
|
assert_eq!(
|
||||||
|
breakdown.local_output_old_projection_bytes(),
|
||||||
|
breakdown.local_output_source_type_bytes
|
||||||
|
+ breakdown.local_output_source_hash_hex_bytes
|
||||||
|
+ breakdown.local_output_source_ee_hash_hex_bytes
|
||||||
|
+ breakdown.local_output_payload_json_bytes
|
||||||
|
+ breakdown.local_output_rule_hash_hex_bytes
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
breakdown.local_output_old_projection_bytes()
|
||||||
|
> breakdown.local_output_typed_projection_bytes()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn sample_repository_view_entry(rsync_uri: &str, bytes: &[u8]) -> RepositoryViewEntry {
|
fn sample_repository_view_entry(rsync_uri: &str, bytes: &[u8]) -> RepositoryViewEntry {
|
||||||
RepositoryViewEntry {
|
RepositoryViewEntry {
|
||||||
rsync_uri: rsync_uri.to_string(),
|
rsync_uri: rsync_uri.to_string(),
|
||||||
@ -126,28 +185,37 @@ fn sample_vcir(manifest_rsync_uri: &str) -> ValidatedCaInstanceResult {
|
|||||||
}],
|
}],
|
||||||
local_outputs: vec![
|
local_outputs: vec![
|
||||||
VcirLocalOutput {
|
VcirLocalOutput {
|
||||||
output_id: "vrp-1".to_string(),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
output_type: VcirOutputType::Vrp,
|
||||||
item_effective_until: pack_time(12),
|
item_effective_until: pack_time(12),
|
||||||
source_object_uri: "rsync://example.test/repo/object.roa".to_string(),
|
source_object_uri: "rsync://example.test/repo/object.roa".to_string(),
|
||||||
source_object_type: "roa".to_string(),
|
source_object_type: VcirSourceObjectType::Roa,
|
||||||
source_object_hash: sha256_hex(&roa_bytes),
|
source_object_hash: sha256_32(&roa_bytes),
|
||||||
source_ee_cert_hash: sha256_hex(&ee_bytes),
|
source_ee_cert_hash: sha256_32(&ee_bytes),
|
||||||
payload_json: r#"{"asn":64496,"prefix":"203.0.113.0/24"}"#.to_string(),
|
payload: VcirLocalOutputPayload::Vrp {
|
||||||
rule_hash: sha256_hex(b"vrp-rule-1"),
|
asn: 64496,
|
||||||
validation_path_hint: vec![manifest_rsync_uri.to_string()],
|
afi: crate::data_model::roa::RoaAfi::Ipv4,
|
||||||
|
prefix_len: 24,
|
||||||
|
addr: {
|
||||||
|
let mut addr = [0u8; 16];
|
||||||
|
addr[..4].copy_from_slice(&[203, 0, 113, 0]);
|
||||||
|
addr
|
||||||
|
},
|
||||||
|
max_length: 24,
|
||||||
|
},
|
||||||
|
rule_hash: sha256_32(b"vrp-rule-1"),
|
||||||
},
|
},
|
||||||
VcirLocalOutput {
|
VcirLocalOutput {
|
||||||
output_id: "aspa-1".to_string(),
|
|
||||||
output_type: VcirOutputType::Aspa,
|
output_type: VcirOutputType::Aspa,
|
||||||
item_effective_until: pack_time(10),
|
item_effective_until: pack_time(10),
|
||||||
source_object_uri: "rsync://example.test/repo/object.asa".to_string(),
|
source_object_uri: "rsync://example.test/repo/object.asa".to_string(),
|
||||||
source_object_type: "aspa".to_string(),
|
source_object_type: VcirSourceObjectType::Aspa,
|
||||||
source_object_hash: sha256_hex(b"aspa-object"),
|
source_object_hash: sha256_32(b"aspa-object"),
|
||||||
source_ee_cert_hash: sha256_hex(b"aspa-ee-cert"),
|
source_ee_cert_hash: sha256_32(b"aspa-ee-cert"),
|
||||||
payload_json: r#"{"customer_as":64496,"providers":[64497]}"#.to_string(),
|
payload: VcirLocalOutputPayload::Aspa {
|
||||||
rule_hash: sha256_hex(b"aspa-rule-1"),
|
customer_as_id: 64496,
|
||||||
validation_path_hint: vec![manifest_rsync_uri.to_string()],
|
provider_as_ids: vec![64497],
|
||||||
|
},
|
||||||
|
rule_hash: sha256_32(b"aspa-rule-1"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
related_artifacts: vec![
|
related_artifacts: vec![
|
||||||
@ -230,34 +298,6 @@ fn vcir_ccr_manifest_projection_validate_rejects_invalid_fields() {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sample_audit_rule_entry(kind: AuditRuleKind) -> AuditRuleIndexEntry {
|
|
||||||
AuditRuleIndexEntry {
|
|
||||||
kind,
|
|
||||||
rule_hash: sha256_hex(match kind {
|
|
||||||
AuditRuleKind::Roa => b"roa-index-rule",
|
|
||||||
AuditRuleKind::Aspa => b"aspa-index-rule",
|
|
||||||
AuditRuleKind::RouterKey => b"router-key-index-rule",
|
|
||||||
}),
|
|
||||||
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
|
||||||
source_object_uri: match kind {
|
|
||||||
AuditRuleKind::Roa => "rsync://example.test/repo/object.roa".to_string(),
|
|
||||||
AuditRuleKind::Aspa => "rsync://example.test/repo/object.asa".to_string(),
|
|
||||||
AuditRuleKind::RouterKey => "rsync://example.test/repo/router.cer".to_string(),
|
|
||||||
},
|
|
||||||
source_object_hash: sha256_hex(match kind {
|
|
||||||
AuditRuleKind::Roa => b"roa-object",
|
|
||||||
AuditRuleKind::Aspa => b"aspa-object",
|
|
||||||
AuditRuleKind::RouterKey => b"router-key-object",
|
|
||||||
}),
|
|
||||||
output_id: match kind {
|
|
||||||
AuditRuleKind::Roa => "vrp-1".to_string(),
|
|
||||||
AuditRuleKind::Aspa => "aspa-1".to_string(),
|
|
||||||
AuditRuleKind::RouterKey => "router-key-1".to_string(),
|
|
||||||
},
|
|
||||||
item_effective_until: pack_time(12),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_rrdp_source_record(notify_uri: &str) -> RrdpSourceRecord {
|
fn sample_rrdp_source_record(notify_uri: &str) -> RrdpSourceRecord {
|
||||||
RrdpSourceRecord {
|
RrdpSourceRecord {
|
||||||
notify_uri: notify_uri.to_string(),
|
notify_uri: notify_uri.to_string(),
|
||||||
@ -464,6 +504,32 @@ fn repo_bytes_db_is_physically_separate_from_external_raw_store() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn memory_snapshot_includes_work_db_and_external_stores() {
|
||||||
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
|
let store = RocksStore::open_with_external_stores(
|
||||||
|
&td.path().join("main-db"),
|
||||||
|
Some(&td.path().join("raw-store.db")),
|
||||||
|
Some(&td.path().join("repo-bytes.db")),
|
||||||
|
)
|
||||||
|
.expect("open store");
|
||||||
|
|
||||||
|
let snapshot = store.memory_snapshot();
|
||||||
|
let labels: Vec<&str> = snapshot
|
||||||
|
.databases
|
||||||
|
.iter()
|
||||||
|
.map(|db| db.label.as_str())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(labels, vec!["work-db", "raw-store.db", "repo-bytes.db"]);
|
||||||
|
assert!(
|
||||||
|
snapshot.databases[0]
|
||||||
|
.column_families
|
||||||
|
.iter()
|
||||||
|
.any(|cf| cf.name == CF_REPOSITORY_VIEW)
|
||||||
|
);
|
||||||
|
serde_json::to_value(&snapshot).expect("serialize memory snapshot");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn put_blob_bytes_batch_accepts_empty_batch_with_external_raw_store() {
|
fn put_blob_bytes_batch_accepts_empty_batch_with_external_raw_store() {
|
||||||
let td = tempfile::tempdir().expect("tempdir");
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
@ -784,106 +850,111 @@ fn list_vcirs_returns_all_entries() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn audit_rule_index_roundtrip_for_roa_aspa_and_router_key() {
|
fn summarize_vcir_storage_aggregates_values_and_field_sizes() {
|
||||||
let td = tempfile::tempdir().expect("tempdir");
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
||||||
|
|
||||||
let roa = sample_audit_rule_entry(AuditRuleKind::Roa);
|
let vcir1 = sample_vcir("rsync://example.test/repo/a.mft");
|
||||||
let aspa = sample_audit_rule_entry(AuditRuleKind::Aspa);
|
let vcir2 = sample_vcir("rsync://example.test/repo/b.mft");
|
||||||
let router_key = sample_audit_rule_entry(AuditRuleKind::RouterKey);
|
store.put_vcir(&vcir1).expect("put vcir1");
|
||||||
store
|
store.put_vcir(&vcir2).expect("put vcir2");
|
||||||
.put_audit_rule_index_entry(&roa)
|
|
||||||
.expect("put roa audit rule entry");
|
|
||||||
store
|
|
||||||
.put_audit_rule_index_entry(&aspa)
|
|
||||||
.expect("put aspa audit rule entry");
|
|
||||||
store
|
|
||||||
.put_audit_rule_index_entry(&router_key)
|
|
||||||
.expect("put router key audit rule entry");
|
|
||||||
|
|
||||||
let got_roa = store
|
let summary = store.summarize_vcir_storage().expect("summarize vcirs");
|
||||||
.get_audit_rule_index_entry(AuditRuleKind::Roa, &roa.rule_hash)
|
let mut expected_fields = VcirFieldSizeBreakdown::default();
|
||||||
.expect("get roa audit rule entry")
|
expected_fields.add_assign(&VcirFieldSizeBreakdown::from_vcir(&vcir1));
|
||||||
.expect("roa entry exists");
|
expected_fields.add_assign(&VcirFieldSizeBreakdown::from_vcir(&vcir2));
|
||||||
let got_aspa = store
|
|
||||||
.get_audit_rule_index_entry(AuditRuleKind::Aspa, &aspa.rule_hash)
|
|
||||||
.expect("get aspa audit rule entry")
|
|
||||||
.expect("aspa entry exists");
|
|
||||||
let got_router_key = store
|
|
||||||
.get_audit_rule_index_entry(AuditRuleKind::RouterKey, &router_key.rule_hash)
|
|
||||||
.expect("get router key audit rule entry")
|
|
||||||
.expect("router key entry exists");
|
|
||||||
assert_eq!(got_roa, roa);
|
|
||||||
assert_eq!(got_aspa, aspa);
|
|
||||||
assert_eq!(got_router_key, router_key);
|
|
||||||
|
|
||||||
store
|
assert_eq!(summary.entry_count, 2);
|
||||||
.delete_audit_rule_index_entry(AuditRuleKind::Roa, &roa.rule_hash)
|
assert!(summary.vcir_value_bytes > 0);
|
||||||
.expect("delete roa audit rule entry");
|
assert!(summary.vcir_value_bytes_max > 0);
|
||||||
|
assert!(summary.vcir_value_bytes_max_manifest_rsync_uri.is_some());
|
||||||
|
assert_eq!(summary.top_entries_by_vcir_value_bytes.len(), 2);
|
||||||
assert!(
|
assert!(
|
||||||
store
|
summary.top_entries_by_vcir_value_bytes[0].vcir_value_bytes
|
||||||
.get_audit_rule_index_entry(AuditRuleKind::Roa, &roa.rule_hash)
|
>= summary.top_entries_by_vcir_value_bytes[1].vcir_value_bytes
|
||||||
.expect("get deleted roa audit rule entry")
|
);
|
||||||
.is_none()
|
assert_eq!(summary.field_sizes, expected_fields);
|
||||||
|
assert!(summary.core_fields.manifest_rsync_uri_bytes > 0);
|
||||||
|
assert!(summary.ccr_projection.manifest_sha256_bytes > 0);
|
||||||
|
assert!(summary.child_resources.effective_ip_resource_cbor_bytes > 0);
|
||||||
|
assert_eq!(
|
||||||
|
summary.local_output_old_projection_bytes,
|
||||||
|
expected_fields.local_output_old_projection_bytes()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
summary.local_output_typed_projection_bytes,
|
||||||
|
expected_fields.local_output_typed_projection_bytes()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
summary.local_output_projection_saved_bytes,
|
||||||
|
expected_fields.local_output_projection_saved_bytes()
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut invalid = sample_audit_rule_entry(AuditRuleKind::Roa);
|
|
||||||
invalid.rule_hash = "bad".to_string();
|
|
||||||
let err = store
|
|
||||||
.put_audit_rule_index_entry(&invalid)
|
|
||||||
.expect_err("invalid audit rule hash must fail");
|
|
||||||
assert!(err.to_string().contains("64-character"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn replace_vcir_and_audit_rule_indexes_replaces_previous_entries_in_one_step() {
|
fn replace_vcir_and_manifest_replay_meta_replaces_current_entry() {
|
||||||
let td = tempfile::tempdir().expect("tempdir");
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
||||||
|
|
||||||
let mut previous = sample_vcir("rsync://example.test/repo/current.mft");
|
let mut previous = sample_vcir("rsync://example.test/repo/current.mft");
|
||||||
previous.local_outputs = vec![VcirLocalOutput {
|
previous.local_outputs = vec![VcirLocalOutput {
|
||||||
output_id: "old-output".to_string(),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
output_type: VcirOutputType::Vrp,
|
||||||
item_effective_until: pack_time(10),
|
item_effective_until: pack_time(10),
|
||||||
source_object_uri: "rsync://example.test/repo/old.roa".to_string(),
|
source_object_uri: "rsync://example.test/repo/old.roa".to_string(),
|
||||||
source_object_type: "roa".to_string(),
|
source_object_type: VcirSourceObjectType::Roa,
|
||||||
source_object_hash: sha256_hex(b"old-roa"),
|
source_object_hash: sha256_32(b"old-roa"),
|
||||||
source_ee_cert_hash: sha256_hex(b"old-ee"),
|
source_ee_cert_hash: sha256_32(b"old-ee"),
|
||||||
payload_json: "{}".to_string(),
|
payload: VcirLocalOutputPayload::Vrp {
|
||||||
rule_hash: sha256_hex(b"old-rule"),
|
asn: 64496,
|
||||||
validation_path_hint: vec![previous.manifest_rsync_uri.clone()],
|
afi: crate::data_model::roa::RoaAfi::Ipv4,
|
||||||
|
prefix_len: 24,
|
||||||
|
addr: {
|
||||||
|
let mut addr = [0u8; 16];
|
||||||
|
addr[..4].copy_from_slice(&[203, 0, 113, 0]);
|
||||||
|
addr
|
||||||
|
},
|
||||||
|
max_length: 24,
|
||||||
|
},
|
||||||
|
rule_hash: sha256_32(b"old-rule"),
|
||||||
}];
|
}];
|
||||||
previous.summary.local_vrp_count = 1;
|
previous.summary.local_vrp_count = 1;
|
||||||
previous.summary.local_aspa_count = 0;
|
previous.summary.local_aspa_count = 0;
|
||||||
previous.summary.local_router_key_count = 0;
|
previous.summary.local_router_key_count = 0;
|
||||||
store
|
let previous_timing = store
|
||||||
.replace_vcir_and_audit_rule_indexes(None, &previous)
|
.replace_vcir_and_manifest_replay_meta(&previous)
|
||||||
.expect("store previous vcir");
|
.expect("store previous vcir");
|
||||||
assert!(
|
assert!(previous_timing.vcir_value_bytes > 0);
|
||||||
store
|
assert!(previous_timing.replay_meta_value_bytes > 0);
|
||||||
.get_audit_rule_index_entry(AuditRuleKind::Roa, &previous.local_outputs[0].rule_hash)
|
assert_eq!(
|
||||||
.expect("get old audit entry")
|
previous_timing.total_encoded_bytes,
|
||||||
.is_some()
|
previous_timing.vcir_value_bytes + previous_timing.replay_meta_value_bytes
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut current = sample_vcir("rsync://example.test/repo/current.mft");
|
let mut current = sample_vcir("rsync://example.test/repo/current.mft");
|
||||||
current.local_outputs = vec![VcirLocalOutput {
|
current.local_outputs = vec![VcirLocalOutput {
|
||||||
output_id: "new-output".to_string(),
|
|
||||||
output_type: VcirOutputType::Aspa,
|
output_type: VcirOutputType::Aspa,
|
||||||
item_effective_until: pack_time(11),
|
item_effective_until: pack_time(11),
|
||||||
source_object_uri: "rsync://example.test/repo/new.asa".to_string(),
|
source_object_uri: "rsync://example.test/repo/new.asa".to_string(),
|
||||||
source_object_type: "aspa".to_string(),
|
source_object_type: VcirSourceObjectType::Aspa,
|
||||||
source_object_hash: sha256_hex(b"new-aspa"),
|
source_object_hash: sha256_32(b"new-aspa"),
|
||||||
source_ee_cert_hash: sha256_hex(b"new-ee"),
|
source_ee_cert_hash: sha256_32(b"new-ee"),
|
||||||
payload_json: "{}".to_string(),
|
payload: VcirLocalOutputPayload::Aspa {
|
||||||
rule_hash: sha256_hex(b"new-rule"),
|
customer_as_id: 64496,
|
||||||
validation_path_hint: vec![current.manifest_rsync_uri.clone()],
|
provider_as_ids: vec![64497],
|
||||||
|
},
|
||||||
|
rule_hash: sha256_32(b"new-rule"),
|
||||||
}];
|
}];
|
||||||
current.summary.local_vrp_count = 0;
|
current.summary.local_vrp_count = 0;
|
||||||
current.summary.local_aspa_count = 1;
|
current.summary.local_aspa_count = 1;
|
||||||
store
|
let current_timing = store
|
||||||
.replace_vcir_and_audit_rule_indexes(Some(&previous), ¤t)
|
.replace_vcir_and_manifest_replay_meta(¤t)
|
||||||
.expect("replace vcir and audit indexes");
|
.expect("replace vcir and replay meta");
|
||||||
|
assert!(current_timing.vcir_value_bytes > 0);
|
||||||
|
assert!(current_timing.replay_meta_value_bytes > 0);
|
||||||
|
assert_eq!(
|
||||||
|
current_timing.total_encoded_bytes,
|
||||||
|
current_timing.vcir_value_bytes + current_timing.replay_meta_value_bytes
|
||||||
|
);
|
||||||
|
|
||||||
let got = store
|
let got = store
|
||||||
.get_vcir(¤t.manifest_rsync_uri)
|
.get_vcir(¤t.manifest_rsync_uri)
|
||||||
@ -902,18 +973,6 @@ fn replace_vcir_and_audit_rule_indexes_replaces_previous_entries_in_one_step() {
|
|||||||
replay_meta.manifest_sha256,
|
replay_meta.manifest_sha256,
|
||||||
current.ccr_manifest_projection.manifest_sha256
|
current.ccr_manifest_projection.manifest_sha256
|
||||||
);
|
);
|
||||||
assert!(
|
|
||||||
store
|
|
||||||
.get_audit_rule_index_entry(AuditRuleKind::Roa, &previous.local_outputs[0].rule_hash)
|
|
||||||
.expect("get deleted old audit entry")
|
|
||||||
.is_none()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
store
|
|
||||||
.get_audit_rule_index_entry(AuditRuleKind::Aspa, ¤t.local_outputs[0].rule_hash)
|
|
||||||
.expect("get new audit entry")
|
|
||||||
.is_some()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -7,11 +7,11 @@ mod output;
|
|||||||
mod sandwich;
|
mod sandwich;
|
||||||
|
|
||||||
use args::{Args, parse_args};
|
use args::{Args, parse_args};
|
||||||
use churn::build_intra_rp_churn;
|
use churn::build_intra_rp_churn_streaming;
|
||||||
use loader::load_sequence;
|
use loader::load_sequence_meta;
|
||||||
use model::Side;
|
use model::Side;
|
||||||
use output::{build_output, write_json, write_markdown};
|
use output::{build_output_from_meta, write_json, write_markdown};
|
||||||
use sandwich::build_sandwich_analysis;
|
use sandwich::build_sandwich_analysis_streaming;
|
||||||
|
|
||||||
pub fn main_entry() -> Result<(), String> {
|
pub fn main_entry() -> Result<(), String> {
|
||||||
real_main()
|
real_main()
|
||||||
@ -25,15 +25,15 @@ fn real_main() -> Result<(), String> {
|
|||||||
fn run(args: Args) -> Result<(), String> {
|
fn run(args: Args) -> Result<(), String> {
|
||||||
std::fs::create_dir_all(&args.out_dir)
|
std::fs::create_dir_all(&args.out_dir)
|
||||||
.map_err(|e| format!("create out-dir failed: {}: {e}", args.out_dir.display()))?;
|
.map_err(|e| format!("create out-dir failed: {}: {e}", args.out_dir.display()))?;
|
||||||
let left = load_sequence(&args.left_sequence, Side::Left)?;
|
let left = load_sequence_meta(&args.left_sequence, Side::Left)?;
|
||||||
let right = load_sequence(&args.right_sequence, Side::Right)?;
|
let right = load_sequence_meta(&args.right_sequence, Side::Right)?;
|
||||||
if left.is_empty() || right.is_empty() {
|
if left.is_empty() || right.is_empty() {
|
||||||
return Err("left and right sequences must both contain at least one sample".into());
|
return Err("left and right sequences must both contain at least one sample".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let sandwich = build_sandwich_analysis(&args, &left, &right);
|
let sandwich = build_sandwich_analysis_streaming(&args, &left, &right)?;
|
||||||
let churn = build_intra_rp_churn(&left, &right);
|
let churn = build_intra_rp_churn_streaming(&left, &right)?;
|
||||||
let output = build_output(&args, &left, &right, &sandwich, &churn);
|
let output = build_output_from_meta(&args, &left, &right, &sandwich, &churn);
|
||||||
write_json(&args.out_dir.join("sequence-triage.json"), &output)?;
|
write_json(&args.out_dir.join("sequence-triage.json"), &output)?;
|
||||||
write_markdown(&args.out_dir.join("sequence-triage.md"), &output)?;
|
write_markdown(&args.out_dir.join("sequence-triage.md"), &output)?;
|
||||||
println!("{}", args.out_dir.display());
|
println!("{}", args.out_dir.display());
|
||||||
|
|||||||
@ -1,71 +1,82 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use super::model::{ChurnRecord, ChurnSummaryRecord, IntraRpChurn, SequenceSample, Side};
|
use super::loader::{load_sample_ccr_from_meta, load_sample_cir_from_meta};
|
||||||
|
use super::model::{
|
||||||
|
ChurnRecord, ChurnSummaryRecord, IntraRpChurn, SequenceMeta, SequenceSample, Side,
|
||||||
|
};
|
||||||
|
|
||||||
pub(super) fn build_intra_rp_churn(
|
pub(super) fn build_intra_rp_churn_streaming(
|
||||||
left: &[SequenceSample],
|
left: &[SequenceMeta],
|
||||||
right: &[SequenceSample],
|
right: &[SequenceMeta],
|
||||||
) -> IntraRpChurn {
|
) -> Result<IntraRpChurn, String> {
|
||||||
let left_records = build_side_records(Side::Left, left);
|
let left_records = build_side_records_streaming(Side::Left, left)?;
|
||||||
let right_records = build_side_records(Side::Right, right);
|
let right_records = build_side_records_streaming(Side::Right, right)?;
|
||||||
let mut summary = summarize_records(&left_records);
|
let mut summary = summarize_records(&left_records);
|
||||||
summary.extend(summarize_records(&right_records));
|
summary.extend(summarize_records(&right_records));
|
||||||
IntraRpChurn {
|
Ok(IntraRpChurn {
|
||||||
left: left_records,
|
left: left_records,
|
||||||
right: right_records,
|
right: right_records,
|
||||||
summary,
|
summary,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_side_records(side: Side, samples: &[SequenceSample]) -> Vec<ChurnRecord> {
|
fn build_side_records_streaming(
|
||||||
|
side: Side,
|
||||||
|
samples: &[SequenceMeta],
|
||||||
|
) -> Result<Vec<ChurnRecord>, String> {
|
||||||
let mut records = Vec::new();
|
let mut records = Vec::new();
|
||||||
for pair in samples.windows(2) {
|
for pair in samples.windows(2) {
|
||||||
let from = &pair[0];
|
let from_cir = load_sample_cir_from_meta(&pair[0])?;
|
||||||
let to = &pair[1];
|
let to_cir = load_sample_cir_from_meta(&pair[1])?;
|
||||||
records.push(record_churn(
|
records.push(record_churn(
|
||||||
side,
|
side,
|
||||||
from,
|
&from_cir,
|
||||||
to,
|
&to_cir,
|
||||||
"object",
|
"object",
|
||||||
&from.object_hashes,
|
&from_cir.object_hashes,
|
||||||
&to.object_hashes,
|
&to_cir.object_hashes,
|
||||||
));
|
));
|
||||||
let from_output = output_keys(from);
|
|
||||||
let to_output = output_keys(to);
|
|
||||||
records.push(record_churn(
|
records.push(record_churn(
|
||||||
side,
|
side,
|
||||||
from,
|
&from_cir,
|
||||||
to,
|
&to_cir,
|
||||||
|
"reject",
|
||||||
|
&from_cir.rejects,
|
||||||
|
&to_cir.rejects,
|
||||||
|
));
|
||||||
|
drop(from_cir);
|
||||||
|
drop(to_cir);
|
||||||
|
|
||||||
|
let from_ccr = load_sample_ccr_from_meta(&pair[0])?;
|
||||||
|
let to_ccr = load_sample_ccr_from_meta(&pair[1])?;
|
||||||
|
let from_output = output_keys(&from_ccr);
|
||||||
|
let to_output = output_keys(&to_ccr);
|
||||||
|
records.push(record_churn(
|
||||||
|
side,
|
||||||
|
&from_ccr,
|
||||||
|
&to_ccr,
|
||||||
"output",
|
"output",
|
||||||
&from_output,
|
&from_output,
|
||||||
&to_output,
|
&to_output,
|
||||||
));
|
));
|
||||||
records.push(record_churn(
|
records.push(record_churn(
|
||||||
side,
|
side,
|
||||||
from,
|
&from_ccr,
|
||||||
to,
|
&to_ccr,
|
||||||
"vrp_output",
|
"vrp_output",
|
||||||
&from.vrps,
|
&from_ccr.vrps,
|
||||||
&to.vrps,
|
&to_ccr.vrps,
|
||||||
));
|
));
|
||||||
records.push(record_churn(
|
records.push(record_churn(
|
||||||
side,
|
side,
|
||||||
from,
|
&from_ccr,
|
||||||
to,
|
&to_ccr,
|
||||||
"vap_output",
|
"vap_output",
|
||||||
&from.vaps,
|
&from_ccr.vaps,
|
||||||
&to.vaps,
|
&to_ccr.vaps,
|
||||||
));
|
|
||||||
records.push(record_churn(
|
|
||||||
side,
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
"reject",
|
|
||||||
&from.rejects,
|
|
||||||
&to.rejects,
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
records
|
Ok(records)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn output_keys(sample: &SequenceSample) -> BTreeSet<String> {
|
fn output_keys(sample: &SequenceSample) -> BTreeSet<String> {
|
||||||
|
|||||||
@ -5,9 +5,9 @@ use crate::ccr::{decode_ccr_compare_views, decode_content_info};
|
|||||||
use crate::cir::decode_cir;
|
use crate::cir::decode_cir;
|
||||||
|
|
||||||
use super::io::{object_hash_key, parse_rfc3339, read_file, resolve_path};
|
use super::io::{object_hash_key, parse_rfc3339, read_file, resolve_path};
|
||||||
use super::model::{SequenceItemRaw, SequenceSample, Side};
|
use super::model::{SequenceItemRaw, SequenceMeta, SequenceSample, Side};
|
||||||
|
|
||||||
pub(super) fn load_sequence(path: &Path, side: Side) -> Result<Vec<SequenceSample>, String> {
|
pub(super) fn load_sequence_meta(path: &Path, side: Side) -> Result<Vec<SequenceMeta>, String> {
|
||||||
let base_dir = path.parent().unwrap_or_else(|| Path::new("."));
|
let base_dir = path.parent().unwrap_or_else(|| Path::new("."));
|
||||||
let text = std::fs::read_to_string(path)
|
let text = std::fs::read_to_string(path)
|
||||||
.map_err(|e| format!("read sequence failed: {}: {e}", path.display()))?;
|
.map_err(|e| format!("read sequence failed: {}: {e}", path.display()))?;
|
||||||
@ -25,101 +25,34 @@ pub(super) fn load_sequence(path: &Path, side: Side) -> Result<Vec<SequenceSampl
|
|||||||
line_index + 1
|
line_index + 1
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
if raw.schema_version.unwrap_or(1) != 1 {
|
validate_raw(path, line_index, &raw, side, &mut seen_seq)?;
|
||||||
return Err(format!(
|
|
||||||
"unsupported sequence item schemaVersion in {}:{}",
|
|
||||||
path.display(),
|
|
||||||
line_index + 1
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if !seen_seq.insert(raw.seq) {
|
|
||||||
return Err(format!("duplicate seq {} in {}", raw.seq, path.display()));
|
|
||||||
}
|
|
||||||
if let Some(status) = &raw.status
|
|
||||||
&& status != "success"
|
|
||||||
{
|
|
||||||
return Err(format!(
|
|
||||||
"sequence sample {} has non-success status: {status}",
|
|
||||||
raw.run_id
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let cir_path = resolve_path(base_dir, &raw.cir_path);
|
let cir_path = resolve_path(base_dir, &raw.cir_path);
|
||||||
let ccr_path = resolve_path(base_dir, &raw.ccr_path);
|
let ccr_path = resolve_path(base_dir, &raw.ccr_path);
|
||||||
let cir = decode_cir(&read_file(&cir_path)?).map_err(|e| {
|
let cir = decode_cir(&read_file(&cir_path)?).map_err(|e| {
|
||||||
format!(
|
format!(
|
||||||
"decode CIR failed for sample {} ({}): {e}",
|
"decode CIR metadata failed for sample {} ({}): {e}",
|
||||||
raw.run_id,
|
raw.run_id,
|
||||||
cir_path.display()
|
cir_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let ccr = decode_content_info(&read_file(&ccr_path)?).map_err(|e| {
|
|
||||||
format!(
|
|
||||||
"decode CCR failed for sample {} ({}): {e}",
|
|
||||||
raw.run_id,
|
|
||||||
ccr_path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let _sequence_validation_time = raw
|
let _sequence_validation_time = raw
|
||||||
.validation_time
|
.validation_time
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(parse_rfc3339)
|
.map(parse_rfc3339)
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let validation_time = cir.validation_time;
|
let validation_time = cir.validation_time;
|
||||||
let objects = cir
|
samples.push(SequenceMeta {
|
||||||
.objects
|
cir_object_count: raw.cir_object_count.or(Some(cir.objects.len() as u64)),
|
||||||
.iter()
|
cir_reject_count: raw
|
||||||
.map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256)))
|
.cir_reject_count
|
||||||
.collect::<BTreeMap<_, _>>();
|
.or(Some(cir.rejected_objects.len() as u64)),
|
||||||
let object_uris = objects.keys().cloned().collect::<BTreeSet<_>>();
|
cir_trust_anchor_count: raw
|
||||||
let object_hashes = objects
|
.cir_trust_anchor_count
|
||||||
.iter()
|
.or(Some(cir.trust_anchors.len() as u64)),
|
||||||
.map(|(uri, hash)| object_hash_key(uri, hash))
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
let rejects = cir
|
|
||||||
.rejected_objects
|
|
||||||
.iter()
|
|
||||||
.map(|item| item.object_uri.clone())
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
let trust_anchors = cir
|
|
||||||
.trust_anchors
|
|
||||||
.iter()
|
|
||||||
.map(|item| {
|
|
||||||
format!(
|
|
||||||
"{}|{}|{}|{}",
|
|
||||||
item.ta_rsync_uri,
|
|
||||||
item.tal_uri,
|
|
||||||
hex::encode(crate::cir::sha256(&item.tal_bytes)),
|
|
||||||
hex::encode(&item.ta_certificate_sha256)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
let (vrps, vaps) = decode_ccr_compare_views(&ccr).map_err(|e| {
|
|
||||||
format!(
|
|
||||||
"decode CCR compare views failed for sample {} ({}): {e}",
|
|
||||||
raw.run_id,
|
|
||||||
ccr_path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let vrps = vrps
|
|
||||||
.into_iter()
|
|
||||||
.map(|row| format!("{}|{}|{}", row.asn, row.ip_prefix, row.max_length))
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
let vaps = vaps
|
|
||||||
.into_iter()
|
|
||||||
.map(|row| format!("{}|{}", row.customer_asn, row.providers))
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
samples.push(SequenceSample {
|
|
||||||
raw,
|
raw,
|
||||||
validation_time,
|
validation_time,
|
||||||
ccr_path,
|
ccr_path,
|
||||||
cir_path,
|
cir_path,
|
||||||
objects,
|
|
||||||
object_uris,
|
|
||||||
object_hashes,
|
|
||||||
rejects,
|
|
||||||
trust_anchors,
|
|
||||||
vrps,
|
|
||||||
vaps,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
samples.sort_by(|left, right| {
|
samples.sort_by(|left, right| {
|
||||||
@ -128,17 +61,120 @@ pub(super) fn load_sequence(path: &Path, side: Side) -> Result<Vec<SequenceSampl
|
|||||||
.then_with(|| left.raw.seq.cmp(&right.raw.seq))
|
.then_with(|| left.raw.seq.cmp(&right.raw.seq))
|
||||||
.then_with(|| left.raw.run_id.cmp(&right.raw.run_id))
|
.then_with(|| left.raw.run_id.cmp(&right.raw.run_id))
|
||||||
});
|
});
|
||||||
if samples.iter().any(|sample| {
|
|
||||||
sample
|
|
||||||
.raw
|
|
||||||
.side
|
|
||||||
.as_deref()
|
|
||||||
.is_some_and(|item| item != side.as_str())
|
|
||||||
}) {
|
|
||||||
return Err(format!(
|
|
||||||
"sequence side field does not match expected side: {}",
|
|
||||||
side.as_str()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(samples)
|
Ok(samples)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn load_sample_cir_from_meta(meta: &SequenceMeta) -> Result<SequenceSample, String> {
|
||||||
|
load_sample_parts(meta, true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn load_sample_ccr_from_meta(meta: &SequenceMeta) -> Result<SequenceSample, String> {
|
||||||
|
load_sample_parts(meta, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_sample_parts(
|
||||||
|
meta: &SequenceMeta,
|
||||||
|
include_cir: bool,
|
||||||
|
include_ccr: bool,
|
||||||
|
) -> Result<SequenceSample, String> {
|
||||||
|
let mut objects = BTreeMap::new();
|
||||||
|
let mut object_hashes = BTreeSet::new();
|
||||||
|
let mut rejects = BTreeSet::new();
|
||||||
|
if include_cir {
|
||||||
|
let cir = decode_cir(&read_file(&meta.cir_path)?).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"decode CIR failed for sample {} ({}): {e}",
|
||||||
|
meta.raw.run_id,
|
||||||
|
meta.cir_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
objects = cir
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.map(|item| (item.rsync_uri.clone(), hex::encode(&item.sha256)))
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
object_hashes = objects
|
||||||
|
.iter()
|
||||||
|
.map(|(uri, hash)| object_hash_key(uri, hash))
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
rejects = cir
|
||||||
|
.rejected_objects
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.object_uri.clone())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
}
|
||||||
|
let mut vrps = BTreeSet::new();
|
||||||
|
let mut vaps = BTreeSet::new();
|
||||||
|
if include_ccr {
|
||||||
|
let ccr = decode_content_info(&read_file(&meta.ccr_path)?).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"decode CCR failed for sample {} ({}): {e}",
|
||||||
|
meta.raw.run_id,
|
||||||
|
meta.ccr_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let (decoded_vrps, decoded_vaps) = decode_ccr_compare_views(&ccr).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"decode CCR compare views failed for sample {} ({}): {e}",
|
||||||
|
meta.raw.run_id,
|
||||||
|
meta.ccr_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
vrps = decoded_vrps
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| format!("{}|{}|{}", row.asn, row.ip_prefix, row.max_length))
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
vaps = decoded_vaps
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| format!("{}|{}", row.customer_asn, row.providers))
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
}
|
||||||
|
Ok(SequenceSample {
|
||||||
|
raw: meta.raw.clone(),
|
||||||
|
validation_time: meta.validation_time,
|
||||||
|
objects,
|
||||||
|
object_hashes,
|
||||||
|
rejects,
|
||||||
|
vrps,
|
||||||
|
vaps,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_raw(
|
||||||
|
path: &Path,
|
||||||
|
line_index: usize,
|
||||||
|
raw: &SequenceItemRaw,
|
||||||
|
side: Side,
|
||||||
|
seen_seq: &mut BTreeSet<u32>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if raw.schema_version.unwrap_or(1) != 1 {
|
||||||
|
return Err(format!(
|
||||||
|
"unsupported sequence item schemaVersion in {}:{}",
|
||||||
|
path.display(),
|
||||||
|
line_index + 1
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !seen_seq.insert(raw.seq) {
|
||||||
|
return Err(format!("duplicate seq {} in {}", raw.seq, path.display()));
|
||||||
|
}
|
||||||
|
if let Some(status) = &raw.status
|
||||||
|
&& status != "success"
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"sequence sample {} has non-success status: {status}",
|
||||||
|
raw.run_id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if raw
|
||||||
|
.side
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|item| item != side.as_str())
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"sequence side field does not match expected side in {}:{}",
|
||||||
|
path.display(),
|
||||||
|
line_index + 1
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@ -25,23 +25,34 @@ pub(super) struct SequenceItemRaw {
|
|||||||
pub(super) max_rss_kb: Option<u64>,
|
pub(super) max_rss_kb: Option<u64>,
|
||||||
pub(super) vrps: Option<u64>,
|
pub(super) vrps: Option<u64>,
|
||||||
pub(super) vaps: Option<u64>,
|
pub(super) vaps: Option<u64>,
|
||||||
|
pub(super) publication_points: Option<u64>,
|
||||||
|
pub(super) cir_object_count: Option<u64>,
|
||||||
|
pub(super) cir_reject_count: Option<u64>,
|
||||||
|
pub(super) cir_trust_anchor_count: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(super) struct SequenceSample {
|
pub(super) struct SequenceSample {
|
||||||
pub(super) raw: SequenceItemRaw,
|
pub(super) raw: SequenceItemRaw,
|
||||||
pub(super) validation_time: OffsetDateTime,
|
pub(super) validation_time: OffsetDateTime,
|
||||||
pub(super) ccr_path: PathBuf,
|
|
||||||
pub(super) cir_path: PathBuf,
|
|
||||||
pub(super) objects: BTreeMap<String, String>,
|
pub(super) objects: BTreeMap<String, String>,
|
||||||
pub(super) object_uris: BTreeSet<String>,
|
|
||||||
pub(super) object_hashes: BTreeSet<String>,
|
pub(super) object_hashes: BTreeSet<String>,
|
||||||
pub(super) rejects: BTreeSet<String>,
|
pub(super) rejects: BTreeSet<String>,
|
||||||
pub(super) trust_anchors: BTreeSet<String>,
|
|
||||||
pub(super) vrps: BTreeSet<String>,
|
pub(super) vrps: BTreeSet<String>,
|
||||||
pub(super) vaps: BTreeSet<String>,
|
pub(super) vaps: BTreeSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(super) struct SequenceMeta {
|
||||||
|
pub(super) raw: SequenceItemRaw,
|
||||||
|
pub(super) validation_time: OffsetDateTime,
|
||||||
|
pub(super) ccr_path: PathBuf,
|
||||||
|
pub(super) cir_path: PathBuf,
|
||||||
|
pub(super) cir_object_count: Option<u64>,
|
||||||
|
pub(super) cir_reject_count: Option<u64>,
|
||||||
|
pub(super) cir_trust_anchor_count: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub(super) enum Side {
|
pub(super) enum Side {
|
||||||
#[default]
|
#[default]
|
||||||
|
|||||||
@ -7,13 +7,13 @@ use super::args::Args;
|
|||||||
use super::io::{format_time, path_string};
|
use super::io::{format_time, path_string};
|
||||||
use super::model::{
|
use super::model::{
|
||||||
ChurnRecord, ChurnSummaryRecord, IntraRpChurn, SandwichAnalysis, SandwichGroupStats,
|
ChurnRecord, ChurnSummaryRecord, IntraRpChurn, SandwichAnalysis, SandwichGroupStats,
|
||||||
SandwichHeatmapRow, SandwichRecord, SequenceSample,
|
SandwichHeatmapRow, SandwichRecord, SequenceMeta,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) fn build_output(
|
pub(super) fn build_output_from_meta(
|
||||||
args: &Args,
|
args: &Args,
|
||||||
left: &[SequenceSample],
|
left: &[SequenceMeta],
|
||||||
right: &[SequenceSample],
|
right: &[SequenceMeta],
|
||||||
sandwich: &SandwichAnalysis,
|
sandwich: &SandwichAnalysis,
|
||||||
churn: &IntraRpChurn,
|
churn: &IntraRpChurn,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
@ -30,6 +30,7 @@ pub(super) fn build_output(
|
|||||||
"warmupSamples": args.warmup_samples,
|
"warmupSamples": args.warmup_samples,
|
||||||
"cooldownSamples": args.cooldown_samples,
|
"cooldownSamples": args.cooldown_samples,
|
||||||
"timelineSampleLimit": if args.timeline_sample_limit == 0 { args.sample_limit } else { args.timeline_sample_limit },
|
"timelineSampleLimit": if args.timeline_sample_limit == 0 { args.sample_limit } else { args.timeline_sample_limit },
|
||||||
|
"executionMode": "streaming-window",
|
||||||
"sequenceSemantics": {
|
"sequenceSemantics": {
|
||||||
"timeSource": "cir.validation_time",
|
"timeSource": "cir.validation_time",
|
||||||
"sourceSampleOrder": "each side is sorted by CIR validation_time, with seq/runId used only as stable tie breakers",
|
"sourceSampleOrder": "each side is sorted by CIR validation_time, with seq/runId used only as stable tie breakers",
|
||||||
@ -37,11 +38,11 @@ pub(super) fn build_output(
|
|||||||
"peerWindow": "all peer samples with source_start.time < peer.time < source_end.time are checked independently",
|
"peerWindow": "all peer samples with source_start.time < peer.time < source_end.time are checked independently",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"left": sequence_summary(left),
|
"left": sequence_meta_summary(left),
|
||||||
"right": sequence_summary(right),
|
"right": sequence_meta_summary(right),
|
||||||
"sandwich": {
|
"sandwich": {
|
||||||
"strictTimeWindow": true,
|
"strictTimeWindow": true,
|
||||||
"method": "For each side sorted by CIR validation_time, use two adjacent source samples as a stable interval. If source_start.time < peer.time < source_end.time and the source value is identical at both interval endpoints, every peer sample in the interval is expected to contain the same value.",
|
"method": "Streaming mode: first build sample timing metadata, then process each sandwich triple by loading only source_start, peer, and source_end CIR/CCR artifacts.",
|
||||||
"totals": {
|
"totals": {
|
||||||
"occurrences": sandwich.total_occurrences,
|
"occurrences": sandwich.total_occurrences,
|
||||||
"uniqueKeys": sandwich.unique_keys.len(),
|
"uniqueKeys": sandwich.unique_keys.len(),
|
||||||
@ -59,7 +60,7 @@ pub(super) fn build_output(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"intraRpChurn": {
|
"intraRpChurn": {
|
||||||
"method": "For each side, compare adjacent samples independently. Object keys use object_uri|sha256, output keys use typed VRP/VAP canonical keys, and reject keys use rejected object URI without reason.",
|
"method": "Streaming mode: compare adjacent samples by loading only the two CIR or CCR artifacts needed for the current adjacent pair.",
|
||||||
"left": churn_records_to_json(&churn.left),
|
"left": churn_records_to_json(&churn.left),
|
||||||
"right": churn_records_to_json(&churn.right),
|
"right": churn_records_to_json(&churn.right),
|
||||||
"summary": churn_summary_to_json(&churn.summary),
|
"summary": churn_summary_to_json(&churn.summary),
|
||||||
@ -77,7 +78,7 @@ pub(super) fn build_output(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sequence_summary(samples: &[SequenceSample]) -> Value {
|
fn sequence_meta_summary(samples: &[SequenceMeta]) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"sampleCount": samples.len(),
|
"sampleCount": samples.len(),
|
||||||
"rpIds": samples.iter().map(|sample| sample.raw.rp_id.clone()).collect::<BTreeSet<_>>(),
|
"rpIds": samples.iter().map(|sample| sample.raw.rp_id.clone()).collect::<BTreeSet<_>>(),
|
||||||
@ -99,12 +100,13 @@ fn sequence_summary(samples: &[SequenceSample]) -> Value {
|
|||||||
"cirSha256": sample.raw.cir_sha256,
|
"cirSha256": sample.raw.cir_sha256,
|
||||||
"wallMs": sample.raw.wall_ms,
|
"wallMs": sample.raw.wall_ms,
|
||||||
"maxRssKb": sample.raw.max_rss_kb,
|
"maxRssKb": sample.raw.max_rss_kb,
|
||||||
"vrps": sample.raw.vrps.or(Some(sample.vrps.len() as u64)),
|
"vrps": sample.raw.vrps,
|
||||||
"vaps": sample.raw.vaps.or(Some(sample.vaps.len() as u64)),
|
"vaps": sample.raw.vaps,
|
||||||
"objectCount": sample.object_uris.len(),
|
"publicationPoints": sample.raw.publication_points,
|
||||||
"objectHashCount": sample.object_hashes.len(),
|
"objectCount": sample.cir_object_count,
|
||||||
"rejectCount": sample.rejects.len(),
|
"objectHashCount": sample.cir_object_count,
|
||||||
"trustAnchorCount": sample.trust_anchors.len(),
|
"rejectCount": sample.cir_reject_count,
|
||||||
|
"trustAnchorCount": sample.cir_trust_anchor_count,
|
||||||
})).collect::<Vec<_>>(),
|
})).collect::<Vec<_>>(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,209 +2,222 @@ use std::collections::BTreeSet;
|
|||||||
|
|
||||||
use super::args::Args;
|
use super::args::Args;
|
||||||
use super::io::format_time;
|
use super::io::format_time;
|
||||||
|
use super::loader::{load_sample_ccr_from_meta, load_sample_cir_from_meta};
|
||||||
use super::model::{
|
use super::model::{
|
||||||
SandwichAnalysis, SandwichGroupStats, SandwichHeatmapRow, SandwichRecord, SequenceSample, Side,
|
SandwichAnalysis, SandwichGroupStats, SandwichHeatmapRow, SandwichRecord, SequenceMeta,
|
||||||
|
SequenceSample, Side,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) fn build_sandwich_analysis(
|
pub(super) fn build_sandwich_analysis_streaming(
|
||||||
args: &Args,
|
args: &Args,
|
||||||
left: &[SequenceSample],
|
left: &[SequenceMeta],
|
||||||
right: &[SequenceSample],
|
right: &[SequenceMeta],
|
||||||
) -> SandwichAnalysis {
|
) -> Result<SandwichAnalysis, String> {
|
||||||
let mut analysis = SandwichAnalysis::default();
|
let mut analysis = SandwichAnalysis::default();
|
||||||
analyze_sandwich_objects(&mut analysis, Side::Left, left, right, args);
|
analyze_sandwich_cir_streaming(&mut analysis, Side::Left, left, right, args)?;
|
||||||
analyze_sandwich_objects(&mut analysis, Side::Right, right, left, args);
|
analyze_sandwich_cir_streaming(&mut analysis, Side::Right, right, left, args)?;
|
||||||
analyze_sandwich_sets(
|
analyze_sandwich_ccr_streaming(&mut analysis, Side::Left, left, right, args)?;
|
||||||
&mut analysis,
|
analyze_sandwich_ccr_streaming(&mut analysis, Side::Right, right, left, args)?;
|
||||||
"reject_uri",
|
Ok(analysis)
|
||||||
"PEER_MISSING_STABLE_REJECT",
|
|
||||||
Side::Left,
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
|sample| &sample.rejects,
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
analyze_sandwich_sets(
|
|
||||||
&mut analysis,
|
|
||||||
"reject_uri",
|
|
||||||
"PEER_MISSING_STABLE_REJECT",
|
|
||||||
Side::Right,
|
|
||||||
right,
|
|
||||||
left,
|
|
||||||
|sample| &sample.rejects,
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
analyze_sandwich_sets(
|
|
||||||
&mut analysis,
|
|
||||||
"vrp_output",
|
|
||||||
"PEER_MISSING_STABLE_OUTPUT",
|
|
||||||
Side::Left,
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
|sample| &sample.vrps,
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
analyze_sandwich_sets(
|
|
||||||
&mut analysis,
|
|
||||||
"vrp_output",
|
|
||||||
"PEER_MISSING_STABLE_OUTPUT",
|
|
||||||
Side::Right,
|
|
||||||
right,
|
|
||||||
left,
|
|
||||||
|sample| &sample.vrps,
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
analyze_sandwich_sets(
|
|
||||||
&mut analysis,
|
|
||||||
"vap_output",
|
|
||||||
"PEER_MISSING_STABLE_OUTPUT",
|
|
||||||
Side::Left,
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
|sample| &sample.vaps,
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
analyze_sandwich_sets(
|
|
||||||
&mut analysis,
|
|
||||||
"vap_output",
|
|
||||||
"PEER_MISSING_STABLE_OUTPUT",
|
|
||||||
Side::Right,
|
|
||||||
right,
|
|
||||||
left,
|
|
||||||
|sample| &sample.vaps,
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
analysis
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn analyze_sandwich_objects(
|
fn analyze_sandwich_cir_streaming(
|
||||||
analysis: &mut SandwichAnalysis,
|
analysis: &mut SandwichAnalysis,
|
||||||
source_side: Side,
|
source_side: Side,
|
||||||
source: &[SequenceSample],
|
source: &[SequenceMeta],
|
||||||
peer: &[SequenceSample],
|
peer: &[SequenceMeta],
|
||||||
|
args: &Args,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
for pair in source.windows(2) {
|
||||||
|
let source_start_meta = &pair[0];
|
||||||
|
let source_end_meta = &pair[1];
|
||||||
|
if source_start_meta.validation_time >= source_end_meta.validation_time {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let peer_indices = peer_meta_indices_between(peer, source_start_meta, source_end_meta);
|
||||||
|
if peer_indices.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let source_start = load_sample_cir_from_meta(source_start_meta)?;
|
||||||
|
let source_end = load_sample_cir_from_meta(source_end_meta)?;
|
||||||
|
for peer_index in peer_indices {
|
||||||
|
let peer_sample = load_sample_cir_from_meta(&peer[peer_index])?;
|
||||||
|
analyze_loaded_sandwich_objects(
|
||||||
|
analysis,
|
||||||
|
source_side,
|
||||||
|
&source_start,
|
||||||
|
&source_end,
|
||||||
|
&peer_sample,
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
analyze_loaded_sandwich_set(
|
||||||
|
analysis,
|
||||||
|
"reject_uri",
|
||||||
|
"PEER_MISSING_STABLE_REJECT",
|
||||||
|
source_side,
|
||||||
|
&source_start,
|
||||||
|
&source_end,
|
||||||
|
&peer_sample,
|
||||||
|
|sample| &sample.rejects,
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn analyze_sandwich_ccr_streaming(
|
||||||
|
analysis: &mut SandwichAnalysis,
|
||||||
|
source_side: Side,
|
||||||
|
source: &[SequenceMeta],
|
||||||
|
peer: &[SequenceMeta],
|
||||||
|
args: &Args,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
for pair in source.windows(2) {
|
||||||
|
let source_start_meta = &pair[0];
|
||||||
|
let source_end_meta = &pair[1];
|
||||||
|
if source_start_meta.validation_time >= source_end_meta.validation_time {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let peer_indices = peer_meta_indices_between(peer, source_start_meta, source_end_meta);
|
||||||
|
if peer_indices.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let source_start = load_sample_ccr_from_meta(source_start_meta)?;
|
||||||
|
let source_end = load_sample_ccr_from_meta(source_end_meta)?;
|
||||||
|
for peer_index in peer_indices {
|
||||||
|
let peer_sample = load_sample_ccr_from_meta(&peer[peer_index])?;
|
||||||
|
analyze_loaded_sandwich_set(
|
||||||
|
analysis,
|
||||||
|
"vrp_output",
|
||||||
|
"PEER_MISSING_STABLE_OUTPUT",
|
||||||
|
source_side,
|
||||||
|
&source_start,
|
||||||
|
&source_end,
|
||||||
|
&peer_sample,
|
||||||
|
|sample| &sample.vrps,
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
analyze_loaded_sandwich_set(
|
||||||
|
analysis,
|
||||||
|
"vap_output",
|
||||||
|
"PEER_MISSING_STABLE_OUTPUT",
|
||||||
|
source_side,
|
||||||
|
&source_start,
|
||||||
|
&source_end,
|
||||||
|
&peer_sample,
|
||||||
|
|sample| &sample.vaps,
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peer_meta_indices_between(
|
||||||
|
peer: &[SequenceMeta],
|
||||||
|
source_start: &SequenceMeta,
|
||||||
|
source_end: &SequenceMeta,
|
||||||
|
) -> Vec<usize> {
|
||||||
|
peer.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, sample)| {
|
||||||
|
(source_start.validation_time < sample.validation_time
|
||||||
|
&& sample.validation_time < source_end.validation_time)
|
||||||
|
.then_some(index)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn analyze_loaded_sandwich_objects(
|
||||||
|
analysis: &mut SandwichAnalysis,
|
||||||
|
source_side: Side,
|
||||||
|
source_start: &SequenceSample,
|
||||||
|
source_end: &SequenceSample,
|
||||||
|
peer_sample: &SequenceSample,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
) {
|
) {
|
||||||
for pair in source.windows(2) {
|
for (uri, source_hash) in &source_start.objects {
|
||||||
let source_start = &pair[0];
|
if source_end.objects.get(uri) != Some(source_hash) {
|
||||||
let source_end = &pair[1];
|
|
||||||
if source_start.validation_time >= source_end.validation_time {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let peers = peer_samples_between(peer, source_start, source_end);
|
match peer_sample.objects.get(uri) {
|
||||||
if peers.is_empty() {
|
Some(peer_hash) if peer_hash == source_hash => {}
|
||||||
continue;
|
Some(peer_hash) => analysis.add(
|
||||||
}
|
"PEER_HASH_MISMATCH_STABLE_OBJECT",
|
||||||
for (uri, source_hash) in &source_start.objects {
|
sandwich_record(
|
||||||
if source_end.objects.get(uri) != Some(source_hash) {
|
"PEER_HASH_MISMATCH_STABLE_OBJECT",
|
||||||
continue;
|
"object",
|
||||||
}
|
uri.clone(),
|
||||||
for peer_sample in &peers {
|
source_side,
|
||||||
match peer_sample.objects.get(uri) {
|
source_start,
|
||||||
Some(peer_hash) if peer_hash == source_hash => {}
|
source_end,
|
||||||
Some(peer_hash) => analysis.add(
|
peer_sample,
|
||||||
"PEER_HASH_MISMATCH_STABLE_OBJECT",
|
Some(source_hash.clone()),
|
||||||
sandwich_record(
|
Some(peer_hash.clone()),
|
||||||
"PEER_HASH_MISMATCH_STABLE_OBJECT",
|
"source interval has stable object hash; peer sample has same URI with another hash",
|
||||||
"object",
|
),
|
||||||
uri.clone(),
|
args.sample_limit,
|
||||||
source_side,
|
),
|
||||||
source_start,
|
None => analysis.add(
|
||||||
source_end,
|
"PEER_MISSING_STABLE_OBJECT",
|
||||||
peer_sample,
|
sandwich_record(
|
||||||
Some(source_hash.clone()),
|
"PEER_MISSING_STABLE_OBJECT",
|
||||||
Some(peer_hash.clone()),
|
"object",
|
||||||
"source interval has stable object hash; peer sample has same URI with another hash",
|
uri.clone(),
|
||||||
),
|
source_side,
|
||||||
args.sample_limit,
|
source_start,
|
||||||
),
|
source_end,
|
||||||
None => analysis.add(
|
peer_sample,
|
||||||
"PEER_MISSING_STABLE_OBJECT",
|
Some(source_hash.clone()),
|
||||||
sandwich_record(
|
None,
|
||||||
"PEER_MISSING_STABLE_OBJECT",
|
"source interval has stable object hash; peer sample misses the URI",
|
||||||
"object",
|
),
|
||||||
uri.clone(),
|
args.sample_limit,
|
||||||
source_side,
|
),
|
||||||
source_start,
|
|
||||||
source_end,
|
|
||||||
peer_sample,
|
|
||||||
Some(source_hash.clone()),
|
|
||||||
None,
|
|
||||||
"source interval has stable object hash; peer sample misses the URI",
|
|
||||||
),
|
|
||||||
args.sample_limit,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn analyze_sandwich_sets<F>(
|
fn analyze_loaded_sandwich_set<F>(
|
||||||
analysis: &mut SandwichAnalysis,
|
analysis: &mut SandwichAnalysis,
|
||||||
set_type: &'static str,
|
set_type: &'static str,
|
||||||
classification: &'static str,
|
classification: &'static str,
|
||||||
source_side: Side,
|
source_side: Side,
|
||||||
source: &[SequenceSample],
|
source_start: &SequenceSample,
|
||||||
peer: &[SequenceSample],
|
source_end: &SequenceSample,
|
||||||
|
peer_sample: &SequenceSample,
|
||||||
extract: F,
|
extract: F,
|
||||||
args: &Args,
|
args: &Args,
|
||||||
) where
|
) where
|
||||||
F: for<'a> Fn(&'a SequenceSample) -> &'a BTreeSet<String>,
|
F: for<'a> Fn(&'a SequenceSample) -> &'a BTreeSet<String>,
|
||||||
{
|
{
|
||||||
for pair in source.windows(2) {
|
let start_set = extract(source_start);
|
||||||
let source_start = &pair[0];
|
let end_set = extract(source_end);
|
||||||
let source_end = &pair[1];
|
let peer_set = extract(peer_sample);
|
||||||
if source_start.validation_time >= source_end.validation_time {
|
for key in start_set {
|
||||||
|
if !end_set.contains(key) || peer_set.contains(key) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let peers = peer_samples_between(peer, source_start, source_end);
|
analysis.add(
|
||||||
if peers.is_empty() {
|
classification,
|
||||||
continue;
|
sandwich_record(
|
||||||
}
|
classification,
|
||||||
let start_set = extract(source_start);
|
set_type,
|
||||||
let end_set = extract(source_end);
|
key.clone(),
|
||||||
for key in start_set {
|
source_side,
|
||||||
if !end_set.contains(key) {
|
source_start,
|
||||||
continue;
|
source_end,
|
||||||
}
|
peer_sample,
|
||||||
for peer_sample in &peers {
|
Some(key.clone()),
|
||||||
if extract(peer_sample).contains(key) {
|
None,
|
||||||
continue;
|
"source interval has a stable key; peer sample misses the key",
|
||||||
}
|
),
|
||||||
analysis.add(
|
args.sample_limit,
|
||||||
classification,
|
);
|
||||||
sandwich_record(
|
|
||||||
classification,
|
|
||||||
set_type,
|
|
||||||
key.clone(),
|
|
||||||
source_side,
|
|
||||||
source_start,
|
|
||||||
source_end,
|
|
||||||
peer_sample,
|
|
||||||
Some(key.clone()),
|
|
||||||
None,
|
|
||||||
"source interval has a stable key; peer sample misses the key",
|
|
||||||
),
|
|
||||||
args.sample_limit,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn peer_samples_between<'a>(
|
|
||||||
peer: &'a [SequenceSample],
|
|
||||||
source_start: &SequenceSample,
|
|
||||||
source_end: &SequenceSample,
|
|
||||||
) -> Vec<&'a SequenceSample> {
|
|
||||||
peer.iter()
|
|
||||||
.filter(|sample| {
|
|
||||||
source_start.validation_time < sample.validation_time
|
|
||||||
&& sample.validation_time < source_end.validation_time
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn sandwich_record(
|
fn sandwich_record(
|
||||||
classification: &'static str,
|
classification: &'static str,
|
||||||
@ -239,7 +252,7 @@ fn sandwich_record(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SandwichAnalysis {
|
impl SandwichAnalysis {
|
||||||
fn add(&mut self, class: &'static str, record: SandwichRecord, sample_limit: usize) {
|
pub(super) fn add(&mut self, class: &'static str, record: SandwichRecord, sample_limit: usize) {
|
||||||
self.total_occurrences += 1;
|
self.total_occurrences += 1;
|
||||||
self.unique_keys.insert(sandwich_unique_key(&record));
|
self.unique_keys.insert(sandwich_unique_key(&record));
|
||||||
*self.by_set_type.entry(record.set_type).or_default() += 1;
|
*self.by_set_type.entry(record.set_type).or_default() += 1;
|
||||||
|
|||||||
@ -14,7 +14,10 @@ use crate::parallel::object_worker::{
|
|||||||
};
|
};
|
||||||
use crate::policy::{Policy, SignedObjectFailurePolicy};
|
use crate::policy::{Policy, SignedObjectFailurePolicy};
|
||||||
use crate::report::{RfcRef, Warning};
|
use crate::report::{RfcRef, Warning};
|
||||||
use crate::storage::{PackFile, PackTime, VcirLocalOutput, VcirOutputType};
|
use crate::storage::{
|
||||||
|
PackFile, PackTime, VcirLocalOutput, VcirLocalOutputPayload, VcirOutputType,
|
||||||
|
VcirSourceObjectType,
|
||||||
|
};
|
||||||
use crate::validation::cert_path::{CertPathError, validate_signed_object_ee_cert_path_fast};
|
use crate::validation::cert_path::{CertPathError, validate_signed_object_ee_cert_path_fast};
|
||||||
use crate::validation::manifest::PublicationPointData;
|
use crate::validation::manifest::PublicationPointData;
|
||||||
use crate::validation::publication_point::PublicationPointSnapshot;
|
use crate::validation::publication_point::PublicationPointSnapshot;
|
||||||
@ -28,6 +31,13 @@ const RFC_CRLDP: &[RfcRef] = &[RfcRef("RFC 6487 §4.8.6")];
|
|||||||
const RFC_CRLDP_AND_LOCKED_PACK: &[RfcRef] =
|
const RFC_CRLDP_AND_LOCKED_PACK: &[RfcRef] =
|
||||||
&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §4.2.1")];
|
&[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §4.2.1")];
|
||||||
|
|
||||||
|
fn sha256_hex_to_32(hex_value: &str) -> [u8; 32] {
|
||||||
|
let bytes = hex::decode(hex_value).expect("internal sha256 hex should decode");
|
||||||
|
let mut out = [0u8; 32];
|
||||||
|
out.copy_from_slice(&bytes);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
fn decode_resource_certificate_with_policy(
|
fn decode_resource_certificate_with_policy(
|
||||||
der: &[u8],
|
der: &[u8],
|
||||||
policy: &Policy,
|
policy: &Policy,
|
||||||
@ -132,8 +142,6 @@ pub(crate) struct RoaTaskResult {
|
|||||||
pub(crate) worker_index: usize,
|
pub(crate) worker_index: usize,
|
||||||
pub(crate) queue_wait_ms: u64,
|
pub(crate) queue_wait_ms: u64,
|
||||||
pub(crate) worker_ms: u64,
|
pub(crate) worker_ms: u64,
|
||||||
pub(crate) rsync_uri: String,
|
|
||||||
pub(crate) sha256_hex: String,
|
|
||||||
pub(crate) outcome: Result<RoaTaskOk, ObjectValidateError>,
|
pub(crate) outcome: Result<RoaTaskOk, ObjectValidateError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,8 +426,8 @@ pub fn process_publication_point_for_issuer_with_options<P: PublicationPointData
|
|||||||
local_outputs_cache.extend(ok.local_outputs);
|
local_outputs_cache.extend(ok.local_outputs);
|
||||||
}
|
}
|
||||||
audit.push(ObjectAuditEntry {
|
audit.push(ObjectAuditEntry {
|
||||||
rsync_uri: result.rsync_uri,
|
rsync_uri: file.rsync_uri.clone(),
|
||||||
sha256_hex: result.sha256_hex,
|
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||||
kind: AuditObjectKind::Roa,
|
kind: AuditObjectKind::Roa,
|
||||||
result: AuditObjectResult::Ok,
|
result: AuditObjectResult::Ok,
|
||||||
detail: None,
|
detail: None,
|
||||||
@ -428,8 +436,8 @@ pub fn process_publication_point_for_issuer_with_options<P: PublicationPointData
|
|||||||
Err(e) => match policy.signed_object_failure_policy {
|
Err(e) => match policy.signed_object_failure_policy {
|
||||||
SignedObjectFailurePolicy::DropObject => {
|
SignedObjectFailurePolicy::DropObject => {
|
||||||
audit.push(ObjectAuditEntry {
|
audit.push(ObjectAuditEntry {
|
||||||
rsync_uri: result.rsync_uri.clone(),
|
rsync_uri: file.rsync_uri.clone(),
|
||||||
sha256_hex: result.sha256_hex,
|
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||||
kind: AuditObjectKind::Roa,
|
kind: AuditObjectKind::Roa,
|
||||||
result: AuditObjectResult::Error,
|
result: AuditObjectResult::Error,
|
||||||
detail: Some(e.to_string()),
|
detail: Some(e.to_string()),
|
||||||
@ -437,24 +445,21 @@ pub fn process_publication_point_for_issuer_with_options<P: PublicationPointData
|
|||||||
let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")];
|
let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")];
|
||||||
refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e));
|
refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e));
|
||||||
warnings.push(
|
warnings.push(
|
||||||
Warning::new(format!(
|
Warning::new(format!("dropping invalid ROA: {}: {e}", file.rsync_uri))
|
||||||
"dropping invalid ROA: {}: {e}",
|
.with_rfc_refs(&refs)
|
||||||
result.rsync_uri
|
.with_context(&file.rsync_uri),
|
||||||
))
|
|
||||||
.with_rfc_refs(&refs)
|
|
||||||
.with_context(&result.rsync_uri),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SignedObjectFailurePolicy::DropPublicationPoint => {
|
SignedObjectFailurePolicy::DropPublicationPoint => {
|
||||||
stats.publication_point_dropped = true;
|
stats.publication_point_dropped = true;
|
||||||
audit.push(ObjectAuditEntry {
|
audit.push(ObjectAuditEntry {
|
||||||
rsync_uri: result.rsync_uri.clone(),
|
rsync_uri: file.rsync_uri.clone(),
|
||||||
sha256_hex: result.sha256_hex,
|
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||||
kind: AuditObjectKind::Roa,
|
kind: AuditObjectKind::Roa,
|
||||||
result: AuditObjectResult::Error,
|
result: AuditObjectResult::Error,
|
||||||
detail: Some(e.to_string()),
|
detail: Some(e.to_string()),
|
||||||
});
|
});
|
||||||
for f in locked_files.iter().skip(result.index + 1) {
|
for f in locked_files.iter().skip(idx + 1) {
|
||||||
if f.rsync_uri.ends_with(".roa") {
|
if f.rsync_uri.ends_with(".roa") {
|
||||||
audit.push(ObjectAuditEntry {
|
audit.push(ObjectAuditEntry {
|
||||||
rsync_uri: f.rsync_uri.clone(),
|
rsync_uri: f.rsync_uri.clone(),
|
||||||
@ -484,7 +489,7 @@ pub fn process_publication_point_for_issuer_with_options<P: PublicationPointData
|
|||||||
warnings.push(
|
warnings.push(
|
||||||
Warning::new(format!(
|
Warning::new(format!(
|
||||||
"dropping publication point due to invalid ROA: {}: {e}",
|
"dropping publication point due to invalid ROA: {}: {e}",
|
||||||
result.rsync_uri
|
file.rsync_uri
|
||||||
))
|
))
|
||||||
.with_rfc_refs(&refs)
|
.with_rfc_refs(&refs)
|
||||||
.with_context(manifest_rsync_uri),
|
.with_context(manifest_rsync_uri),
|
||||||
@ -786,19 +791,24 @@ pub fn process_publication_point_for_issuer_parallel_roa_with_pool_options<
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct OwnedRoaTask {
|
pub(crate) struct RoaTaskShared {
|
||||||
pub(crate) publication_point_id: u64,
|
locked_files: Arc<[PackFile]>,
|
||||||
index: usize,
|
manifest_rsync_uri: Arc<str>,
|
||||||
file: PackFile,
|
|
||||||
manifest_rsync_uri: String,
|
|
||||||
issuer_ca_der: Arc<[u8]>,
|
issuer_ca_der: Arc<[u8]>,
|
||||||
issuer_ca: Arc<ResourceCertificate>,
|
issuer_ca: Arc<ResourceCertificate>,
|
||||||
issuer_spki_der: Arc<[u8]>,
|
issuer_spki_der: Arc<[u8]>,
|
||||||
issuer_ca_rsync_uri: Option<String>,
|
issuer_ca_rsync_uri: Option<Arc<str>>,
|
||||||
crl_cache: Arc<Mutex<std::collections::HashMap<String, CachedIssuerCrl>>>,
|
crl_cache: Arc<Mutex<std::collections::HashMap<String, CachedIssuerCrl>>>,
|
||||||
issuer_resources_index: Arc<IssuerResourcesIndex>,
|
issuer_resources_index: Arc<IssuerResourcesIndex>,
|
||||||
issuer_effective_ip: Option<crate::data_model::rc::IpResourceSet>,
|
issuer_effective_ip: Option<Arc<crate::data_model::rc::IpResourceSet>>,
|
||||||
issuer_effective_as: Option<crate::data_model::rc::AsResourceSet>,
|
issuer_effective_as: Option<Arc<crate::data_model::rc::AsResourceSet>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct OwnedRoaTask {
|
||||||
|
pub(crate) publication_point_id: u64,
|
||||||
|
index: usize,
|
||||||
|
shared: Arc<RoaTaskShared>,
|
||||||
validation_time: time::OffsetDateTime,
|
validation_time: time::OffsetDateTime,
|
||||||
collect_vcir_local_outputs: bool,
|
collect_vcir_local_outputs: bool,
|
||||||
strict_cms_der: bool,
|
strict_cms_der: bool,
|
||||||
@ -862,8 +872,13 @@ fn validate_owned_roa_task(worker_index: usize, task: OwnedRoaTask) -> RoaTaskRe
|
|||||||
.map(|submitted_at| worker_started.saturating_duration_since(submitted_at))
|
.map(|submitted_at| worker_started.saturating_duration_since(submitted_at))
|
||||||
.map(|duration| duration.as_millis() as u64)
|
.map(|duration| duration.as_millis() as u64)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let sha256_hex = sha256_hex_from_32(&task.file.sha256);
|
let shared = task.shared.as_ref();
|
||||||
let issuer_spki = match SubjectPublicKeyInfo::from_der(task.issuer_spki_der.as_ref()) {
|
let file = task
|
||||||
|
.shared
|
||||||
|
.locked_files
|
||||||
|
.get(task.index)
|
||||||
|
.expect("ROA task index must reference locked file");
|
||||||
|
let issuer_spki = match SubjectPublicKeyInfo::from_der(shared.issuer_spki_der.as_ref()) {
|
||||||
Ok((rem, spki)) if rem.is_empty() => spki,
|
Ok((rem, spki)) if rem.is_empty() => spki,
|
||||||
Ok((rem, _)) => {
|
Ok((rem, _)) => {
|
||||||
return RoaTaskResult {
|
return RoaTaskResult {
|
||||||
@ -872,8 +887,6 @@ fn validate_owned_roa_task(worker_index: usize, task: OwnedRoaTask) -> RoaTaskRe
|
|||||||
worker_index,
|
worker_index,
|
||||||
queue_wait_ms,
|
queue_wait_ms,
|
||||||
worker_ms: worker_started.elapsed().as_millis() as u64,
|
worker_ms: worker_started.elapsed().as_millis() as u64,
|
||||||
rsync_uri: task.file.rsync_uri,
|
|
||||||
sha256_hex,
|
|
||||||
outcome: Err(ObjectValidateError::CertPath(
|
outcome: Err(ObjectValidateError::CertPath(
|
||||||
CertPathError::IssuerSpkiTrailingBytes(rem.len()),
|
CertPathError::IssuerSpkiTrailingBytes(rem.len()),
|
||||||
)),
|
)),
|
||||||
@ -886,8 +899,6 @@ fn validate_owned_roa_task(worker_index: usize, task: OwnedRoaTask) -> RoaTaskRe
|
|||||||
worker_index,
|
worker_index,
|
||||||
queue_wait_ms,
|
queue_wait_ms,
|
||||||
worker_ms: worker_started.elapsed().as_millis() as u64,
|
worker_ms: worker_started.elapsed().as_millis() as u64,
|
||||||
rsync_uri: task.file.rsync_uri,
|
|
||||||
sha256_hex,
|
|
||||||
outcome: Err(ObjectValidateError::CertPath(
|
outcome: Err(ObjectValidateError::CertPath(
|
||||||
CertPathError::IssuerSpkiParse(e.to_string()),
|
CertPathError::IssuerSpkiParse(e.to_string()),
|
||||||
)),
|
)),
|
||||||
@ -895,16 +906,16 @@ fn validate_owned_roa_task(worker_index: usize, task: OwnedRoaTask) -> RoaTaskRe
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let outcome = process_roa_with_issuer_parallel_cached(
|
let outcome = process_roa_with_issuer_parallel_cached(
|
||||||
&task.file,
|
file,
|
||||||
task.manifest_rsync_uri.as_str(),
|
shared.manifest_rsync_uri.as_ref(),
|
||||||
task.issuer_ca_der.as_ref(),
|
shared.issuer_ca_der.as_ref(),
|
||||||
task.issuer_ca.as_ref(),
|
shared.issuer_ca.as_ref(),
|
||||||
&issuer_spki,
|
&issuer_spki,
|
||||||
task.issuer_ca_rsync_uri.as_deref(),
|
shared.issuer_ca_rsync_uri.as_deref(),
|
||||||
task.crl_cache.as_ref(),
|
shared.crl_cache.as_ref(),
|
||||||
task.issuer_resources_index.as_ref(),
|
shared.issuer_resources_index.as_ref(),
|
||||||
task.issuer_effective_ip.as_ref(),
|
shared.issuer_effective_ip.as_deref(),
|
||||||
task.issuer_effective_as.as_ref(),
|
shared.issuer_effective_as.as_deref(),
|
||||||
task.validation_time,
|
task.validation_time,
|
||||||
None,
|
None,
|
||||||
task.collect_vcir_local_outputs,
|
task.collect_vcir_local_outputs,
|
||||||
@ -922,8 +933,6 @@ fn validate_owned_roa_task(worker_index: usize, task: OwnedRoaTask) -> RoaTaskRe
|
|||||||
worker_index,
|
worker_index,
|
||||||
queue_wait_ms,
|
queue_wait_ms,
|
||||||
worker_ms: worker_started.elapsed().as_millis() as u64,
|
worker_ms: worker_started.elapsed().as_millis() as u64,
|
||||||
rsync_uri: task.file.rsync_uri,
|
|
||||||
sha256_hex,
|
|
||||||
outcome,
|
outcome,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -935,16 +944,7 @@ pub(crate) enum ParallelObjectsPrepare {
|
|||||||
|
|
||||||
pub(crate) struct ParallelObjectsStage {
|
pub(crate) struct ParallelObjectsStage {
|
||||||
pub(crate) publication_point_id: u64,
|
pub(crate) publication_point_id: u64,
|
||||||
pub(crate) locked_files: Vec<PackFile>,
|
shared: Arc<RoaTaskShared>,
|
||||||
pub(crate) manifest_rsync_uri: String,
|
|
||||||
issuer_ca_der: Arc<[u8]>,
|
|
||||||
issuer_ca: Arc<ResourceCertificate>,
|
|
||||||
issuer_spki_der: Arc<[u8]>,
|
|
||||||
issuer_ca_rsync_uri: Option<String>,
|
|
||||||
crl_cache: Arc<Mutex<std::collections::HashMap<String, CachedIssuerCrl>>>,
|
|
||||||
issuer_resources_index: Arc<IssuerResourcesIndex>,
|
|
||||||
issuer_effective_ip: Option<crate::data_model::rc::IpResourceSet>,
|
|
||||||
issuer_effective_as: Option<crate::data_model::rc::AsResourceSet>,
|
|
||||||
validation_time: time::OffsetDateTime,
|
validation_time: time::OffsetDateTime,
|
||||||
collect_vcir_local_outputs: bool,
|
collect_vcir_local_outputs: bool,
|
||||||
strict_cms_der: bool,
|
strict_cms_der: bool,
|
||||||
@ -955,31 +955,41 @@ pub(crate) struct ParallelObjectsStage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ParallelObjectsStage {
|
impl ParallelObjectsStage {
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn build_roa_tasks(&self) -> Vec<OwnedRoaTask> {
|
pub(crate) fn build_roa_tasks(&self) -> Vec<OwnedRoaTask> {
|
||||||
self.locked_files
|
let mut tasks = Vec::with_capacity(self.roa_task_count());
|
||||||
|
self.extend_roa_tasks(|task| tasks.push(task));
|
||||||
|
tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn append_roa_tasks_to(
|
||||||
|
&self,
|
||||||
|
pending: &mut std::collections::VecDeque<OwnedRoaTask>,
|
||||||
|
) {
|
||||||
|
self.extend_roa_tasks(|task| pending.push_back(task));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extend_roa_tasks<F>(&self, mut push: F)
|
||||||
|
where
|
||||||
|
F: FnMut(OwnedRoaTask),
|
||||||
|
{
|
||||||
|
let shared = self.shared.clone();
|
||||||
|
self.locked_files()
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(_, file)| file.rsync_uri.ends_with(".roa"))
|
.filter(|(_, file)| file.rsync_uri.ends_with(".roa"))
|
||||||
.map(|(index, file)| OwnedRoaTask {
|
.for_each(|(index, _)| {
|
||||||
publication_point_id: self.publication_point_id,
|
push(OwnedRoaTask {
|
||||||
index,
|
publication_point_id: self.publication_point_id,
|
||||||
file: file.clone(),
|
index,
|
||||||
manifest_rsync_uri: self.manifest_rsync_uri.clone(),
|
shared: shared.clone(),
|
||||||
issuer_ca_der: self.issuer_ca_der.clone(),
|
validation_time: self.validation_time,
|
||||||
issuer_ca: self.issuer_ca.clone(),
|
collect_vcir_local_outputs: self.collect_vcir_local_outputs,
|
||||||
issuer_spki_der: self.issuer_spki_der.clone(),
|
strict_cms_der: self.strict_cms_der,
|
||||||
issuer_ca_rsync_uri: self.issuer_ca_rsync_uri.clone(),
|
strict_name: self.strict_name,
|
||||||
crl_cache: self.crl_cache.clone(),
|
submitted_at: None,
|
||||||
issuer_resources_index: self.issuer_resources_index.clone(),
|
});
|
||||||
issuer_effective_ip: self.issuer_effective_ip.clone(),
|
});
|
||||||
issuer_effective_as: self.issuer_effective_as.clone(),
|
|
||||||
validation_time: self.validation_time,
|
|
||||||
collect_vcir_local_outputs: self.collect_vcir_local_outputs,
|
|
||||||
strict_cms_der: self.strict_cms_der,
|
|
||||||
strict_name: self.strict_name,
|
|
||||||
submitted_at: None,
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn roa_task_count(&self) -> usize {
|
pub(crate) fn roa_task_count(&self) -> usize {
|
||||||
@ -991,7 +1001,11 @@ impl ParallelObjectsStage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn locked_file_count(&self) -> usize {
|
pub(crate) fn locked_file_count(&self) -> usize {
|
||||||
self.locked_files.len()
|
self.shared.locked_files.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn locked_files(&self) -> &[PackFile] {
|
||||||
|
self.shared.locked_files.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1212,19 +1226,21 @@ pub(crate) fn prepare_publication_point_for_parallel_roa<P: PublicationPointData
|
|||||||
|
|
||||||
ParallelObjectsPrepare::Staged(ParallelObjectsStage {
|
ParallelObjectsPrepare::Staged(ParallelObjectsStage {
|
||||||
publication_point_id,
|
publication_point_id,
|
||||||
locked_files: locked_files.to_vec(),
|
shared: Arc::new(RoaTaskShared {
|
||||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
locked_files: Arc::<[PackFile]>::from(locked_files.to_vec()),
|
||||||
issuer_ca_der: Arc::<[u8]>::from(issuer_ca_der.to_vec()),
|
manifest_rsync_uri: Arc::<str>::from(manifest_rsync_uri),
|
||||||
issuer_spki_der: Arc::<[u8]>::from(issuer_ca.tbs.subject_public_key_info.clone()),
|
issuer_ca_der: Arc::<[u8]>::from(issuer_ca_der.to_vec()),
|
||||||
issuer_ca: Arc::new(issuer_ca),
|
issuer_spki_der: Arc::<[u8]>::from(issuer_ca.tbs.subject_public_key_info.clone()),
|
||||||
issuer_ca_rsync_uri: issuer_ca_rsync_uri.map(ToString::to_string),
|
issuer_ca: Arc::new(issuer_ca),
|
||||||
crl_cache: Arc::new(Mutex::new(crl_cache)),
|
issuer_ca_rsync_uri: issuer_ca_rsync_uri.map(Arc::<str>::from),
|
||||||
issuer_resources_index: Arc::new(build_issuer_resources_index(
|
crl_cache: Arc::new(Mutex::new(crl_cache)),
|
||||||
issuer_effective_ip,
|
issuer_resources_index: Arc::new(build_issuer_resources_index(
|
||||||
issuer_effective_as,
|
issuer_effective_ip,
|
||||||
)),
|
issuer_effective_as,
|
||||||
issuer_effective_ip: issuer_effective_ip.cloned(),
|
)),
|
||||||
issuer_effective_as: issuer_effective_as.cloned(),
|
issuer_effective_ip: issuer_effective_ip.cloned().map(Arc::new),
|
||||||
|
issuer_effective_as: issuer_effective_as.cloned().map(Arc::new),
|
||||||
|
}),
|
||||||
validation_time,
|
validation_time,
|
||||||
collect_vcir_local_outputs,
|
collect_vcir_local_outputs,
|
||||||
strict_cms_der: policy.strict.cms_der,
|
strict_cms_der: policy.strict.cms_der,
|
||||||
@ -1241,18 +1257,20 @@ pub(crate) fn reduce_parallel_roa_stage(
|
|||||||
timing: Option<&TimingHandle>,
|
timing: Option<&TimingHandle>,
|
||||||
) -> Result<ObjectsOutput, String> {
|
) -> Result<ObjectsOutput, String> {
|
||||||
roa_results.sort_by_key(|result| result.index);
|
roa_results.sort_by_key(|result| result.index);
|
||||||
let mut roa_by_index = roa_results
|
let mut roa_results = roa_results.into_iter().peekable();
|
||||||
.into_iter()
|
let shared = stage.shared.clone();
|
||||||
.map(|result| (result.index, result))
|
let mut aspa_crl_cache = shared
|
||||||
.collect::<std::collections::HashMap<_, _>>();
|
|
||||||
let mut aspa_crl_cache = stage
|
|
||||||
.crl_cache
|
.crl_cache
|
||||||
.lock()
|
.lock()
|
||||||
.expect("parallel ROA CRL cache lock")
|
.expect("parallel ROA CRL cache lock")
|
||||||
.clone();
|
.clone();
|
||||||
let issuer_spki = SubjectPublicKeyInfo::from_der(stage.issuer_spki_der.as_ref())
|
let issuer_spki = SubjectPublicKeyInfo::from_der(shared.issuer_spki_der.as_ref())
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.1;
|
.1;
|
||||||
|
let collect_vcir_local_outputs = stage.collect_vcir_local_outputs;
|
||||||
|
let validation_time = stage.validation_time;
|
||||||
|
let strict_cms_der = stage.strict_cms_der;
|
||||||
|
let strict_name = stage.strict_name;
|
||||||
let mut stats = stage.stats;
|
let mut stats = stage.stats;
|
||||||
let mut warnings = stage.warnings;
|
let mut warnings = stage.warnings;
|
||||||
let mut audit = stage.audit;
|
let mut audit = stage.audit;
|
||||||
@ -1260,21 +1278,32 @@ pub(crate) fn reduce_parallel_roa_stage(
|
|||||||
let mut aspas: Vec<AspaAttestation> = Vec::new();
|
let mut aspas: Vec<AspaAttestation> = Vec::new();
|
||||||
let mut local_outputs_cache: Vec<VcirLocalOutput> = Vec::new();
|
let mut local_outputs_cache: Vec<VcirLocalOutput> = Vec::new();
|
||||||
|
|
||||||
for (idx, file) in stage.locked_files.iter().enumerate() {
|
for (idx, file) in shared.locked_files.iter().enumerate() {
|
||||||
if file.rsync_uri.ends_with(".roa") {
|
if file.rsync_uri.ends_with(".roa") {
|
||||||
let result = roa_by_index
|
let result = match roa_results.peek() {
|
||||||
.remove(&idx)
|
Some(result) if result.index == idx => roa_results
|
||||||
.ok_or_else(|| format!("missing ROA task result for {}", file.rsync_uri))?;
|
.next()
|
||||||
|
.expect("peeked ROA task result must be present"),
|
||||||
|
Some(result) => {
|
||||||
|
return Err(format!(
|
||||||
|
"unexpected ROA task result index {} while reducing {} at index {}",
|
||||||
|
result.index, file.rsync_uri, idx
|
||||||
|
));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
return Err(format!("missing ROA task result for {}", file.rsync_uri));
|
||||||
|
}
|
||||||
|
};
|
||||||
match result.outcome {
|
match result.outcome {
|
||||||
Ok(mut ok) => {
|
Ok(mut ok) => {
|
||||||
stats.roa_ok += 1;
|
stats.roa_ok += 1;
|
||||||
vrps.append(&mut ok.vrps);
|
vrps.append(&mut ok.vrps);
|
||||||
if stage.collect_vcir_local_outputs {
|
if collect_vcir_local_outputs {
|
||||||
local_outputs_cache.extend(ok.local_outputs);
|
local_outputs_cache.extend(ok.local_outputs);
|
||||||
}
|
}
|
||||||
audit.push(ObjectAuditEntry {
|
audit.push(ObjectAuditEntry {
|
||||||
rsync_uri: result.rsync_uri,
|
rsync_uri: file.rsync_uri.clone(),
|
||||||
sha256_hex: result.sha256_hex,
|
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||||
kind: AuditObjectKind::Roa,
|
kind: AuditObjectKind::Roa,
|
||||||
result: AuditObjectResult::Ok,
|
result: AuditObjectResult::Ok,
|
||||||
detail: None,
|
detail: None,
|
||||||
@ -1282,8 +1311,8 @@ pub(crate) fn reduce_parallel_roa_stage(
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
audit.push(ObjectAuditEntry {
|
audit.push(ObjectAuditEntry {
|
||||||
rsync_uri: result.rsync_uri.clone(),
|
rsync_uri: file.rsync_uri.clone(),
|
||||||
sha256_hex: result.sha256_hex,
|
sha256_hex: sha256_hex_from_32(&file.sha256),
|
||||||
kind: AuditObjectKind::Roa,
|
kind: AuditObjectKind::Roa,
|
||||||
result: AuditObjectResult::Error,
|
result: AuditObjectResult::Error,
|
||||||
detail: Some(e.to_string()),
|
detail: Some(e.to_string()),
|
||||||
@ -1291,9 +1320,9 @@ pub(crate) fn reduce_parallel_roa_stage(
|
|||||||
let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")];
|
let mut refs = vec![RfcRef("RFC 6488 §3"), RfcRef("RFC 9582 §4-§5")];
|
||||||
refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e));
|
refs.extend_from_slice(extra_rfc_refs_for_crl_selection(&e));
|
||||||
warnings.push(
|
warnings.push(
|
||||||
Warning::new(format!("dropping invalid ROA: {}: {e}", result.rsync_uri))
|
Warning::new(format!("dropping invalid ROA: {}: {e}", file.rsync_uri))
|
||||||
.with_rfc_refs(&refs)
|
.with_rfc_refs(&refs)
|
||||||
.with_context(&result.rsync_uri),
|
.with_context(&file.rsync_uri),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1301,20 +1330,20 @@ pub(crate) fn reduce_parallel_roa_stage(
|
|||||||
let _t = timing.as_ref().map(|t| t.span_phase("objects_aspa_total"));
|
let _t = timing.as_ref().map(|t| t.span_phase("objects_aspa_total"));
|
||||||
match process_aspa_with_issuer(
|
match process_aspa_with_issuer(
|
||||||
file,
|
file,
|
||||||
&stage.manifest_rsync_uri,
|
shared.manifest_rsync_uri.as_ref(),
|
||||||
stage.issuer_ca_der.as_ref(),
|
shared.issuer_ca_der.as_ref(),
|
||||||
stage.issuer_ca.as_ref(),
|
shared.issuer_ca.as_ref(),
|
||||||
&issuer_spki,
|
&issuer_spki,
|
||||||
stage.issuer_ca_rsync_uri.as_deref(),
|
shared.issuer_ca_rsync_uri.as_deref(),
|
||||||
&mut aspa_crl_cache,
|
&mut aspa_crl_cache,
|
||||||
stage.issuer_resources_index.as_ref(),
|
shared.issuer_resources_index.as_ref(),
|
||||||
stage.issuer_effective_ip.as_ref(),
|
shared.issuer_effective_ip.as_deref(),
|
||||||
stage.issuer_effective_as.as_ref(),
|
shared.issuer_effective_as.as_deref(),
|
||||||
stage.validation_time,
|
validation_time,
|
||||||
timing,
|
timing,
|
||||||
stage.collect_vcir_local_outputs,
|
collect_vcir_local_outputs,
|
||||||
stage.strict_cms_der,
|
strict_cms_der,
|
||||||
stage.strict_name,
|
strict_name,
|
||||||
) {
|
) {
|
||||||
Ok((att, local_output)) => {
|
Ok((att, local_output)) => {
|
||||||
stats.aspa_ok += 1;
|
stats.aspa_ok += 1;
|
||||||
@ -1349,6 +1378,12 @@ pub(crate) fn reduce_parallel_roa_stage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(result) = roa_results.next() {
|
||||||
|
return Err(format!(
|
||||||
|
"unexpected trailing ROA task result at index {}",
|
||||||
|
result.index
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(ObjectsOutput {
|
Ok(ObjectsOutput {
|
||||||
vrps,
|
vrps,
|
||||||
@ -1388,8 +1423,9 @@ fn process_publication_point_for_issuer_parallel_roa_inner<P: PublicationPointDa
|
|||||||
ParallelObjectsPrepare::Staged(stage) => stage,
|
ParallelObjectsPrepare::Staged(stage) => stage,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut pending = std::collections::VecDeque::from(stage.build_roa_tasks());
|
|
||||||
let roa_task_count = stage.roa_task_count();
|
let roa_task_count = stage.roa_task_count();
|
||||||
|
let mut pending = std::collections::VecDeque::with_capacity(roa_task_count);
|
||||||
|
stage.append_roa_tasks_to(&mut pending);
|
||||||
let mut worker_pool = pool
|
let mut worker_pool = pool
|
||||||
.pool
|
.pool
|
||||||
.lock()
|
.lock()
|
||||||
@ -1537,7 +1573,6 @@ pub(crate) fn validate_roa_task_serial(
|
|||||||
strict_cms_der: bool,
|
strict_cms_der: bool,
|
||||||
strict_name: bool,
|
strict_name: bool,
|
||||||
) -> RoaTaskResult {
|
) -> RoaTaskResult {
|
||||||
let sha256_hex = sha256_hex_from_32(&task.file.sha256);
|
|
||||||
let outcome = process_roa_with_issuer(
|
let outcome = process_roa_with_issuer(
|
||||||
task.file,
|
task.file,
|
||||||
manifest_rsync_uri,
|
manifest_rsync_uri,
|
||||||
@ -1566,15 +1601,13 @@ pub(crate) fn validate_roa_task_serial(
|
|||||||
worker_index: 0,
|
worker_index: 0,
|
||||||
queue_wait_ms: 0,
|
queue_wait_ms: 0,
|
||||||
worker_ms: 0,
|
worker_ms: 0,
|
||||||
rsync_uri: task.file.rsync_uri.clone(),
|
|
||||||
sha256_hex,
|
|
||||||
outcome,
|
outcome,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_roa_with_issuer(
|
fn process_roa_with_issuer(
|
||||||
file: &PackFile,
|
file: &PackFile,
|
||||||
manifest_rsync_uri: &str,
|
_manifest_rsync_uri: &str,
|
||||||
issuer_ca_der: &[u8],
|
issuer_ca_der: &[u8],
|
||||||
issuer_ca: &ResourceCertificate,
|
issuer_ca: &ResourceCertificate,
|
||||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||||
@ -1659,12 +1692,6 @@ fn process_roa_with_issuer(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|vrp| {
|
.map(|vrp| {
|
||||||
let prefix = vrp_prefix_to_string(vrp);
|
let prefix = vrp_prefix_to_string(vrp);
|
||||||
let payload_json = serde_json::json!({
|
|
||||||
"asn": vrp.asn,
|
|
||||||
"prefix": prefix,
|
|
||||||
"max_length": vrp.max_length,
|
|
||||||
})
|
|
||||||
.to_string();
|
|
||||||
let rule_hash = crate::audit::sha256_hex(
|
let rule_hash = crate::audit::sha256_hex(
|
||||||
format!(
|
format!(
|
||||||
"roa-rule:{}:{}:{}:{}",
|
"roa-rule:{}:{}:{}:{}",
|
||||||
@ -1673,20 +1700,20 @@ fn process_roa_with_issuer(
|
|||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
);
|
);
|
||||||
VcirLocalOutput {
|
VcirLocalOutput {
|
||||||
output_id: rule_hash.clone(),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
output_type: VcirOutputType::Vrp,
|
||||||
item_effective_until: item_effective_until.clone(),
|
item_effective_until: item_effective_until.clone(),
|
||||||
source_object_uri: file.rsync_uri.clone(),
|
source_object_uri: file.rsync_uri.clone(),
|
||||||
source_object_type: "roa".to_string(),
|
source_object_type: VcirSourceObjectType::Roa,
|
||||||
source_object_hash: source_object_hash.clone(),
|
source_object_hash: file.sha256,
|
||||||
source_ee_cert_hash: source_ee_cert_hash.clone(),
|
source_ee_cert_hash: sha256_hex_to_32(&source_ee_cert_hash),
|
||||||
payload_json,
|
payload: VcirLocalOutputPayload::Vrp {
|
||||||
rule_hash,
|
asn: vrp.asn,
|
||||||
validation_path_hint: vec![
|
afi: vrp.prefix.afi,
|
||||||
manifest_rsync_uri.to_string(),
|
prefix_len: vrp.prefix.prefix_len,
|
||||||
file.rsync_uri.clone(),
|
addr: vrp.prefix.addr,
|
||||||
source_object_hash.clone(),
|
max_length: vrp.max_length,
|
||||||
],
|
},
|
||||||
|
rule_hash: sha256_hex_to_32(&rule_hash),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -1696,7 +1723,7 @@ fn process_roa_with_issuer(
|
|||||||
|
|
||||||
fn process_roa_with_issuer_parallel_cached(
|
fn process_roa_with_issuer_parallel_cached(
|
||||||
file: &PackFile,
|
file: &PackFile,
|
||||||
manifest_rsync_uri: &str,
|
_manifest_rsync_uri: &str,
|
||||||
issuer_ca_der: &[u8],
|
issuer_ca_der: &[u8],
|
||||||
issuer_ca: &ResourceCertificate,
|
issuer_ca: &ResourceCertificate,
|
||||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||||
@ -1787,12 +1814,6 @@ fn process_roa_with_issuer_parallel_cached(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|vrp| {
|
.map(|vrp| {
|
||||||
let prefix = vrp_prefix_to_string(vrp);
|
let prefix = vrp_prefix_to_string(vrp);
|
||||||
let payload_json = serde_json::json!({
|
|
||||||
"asn": vrp.asn,
|
|
||||||
"prefix": prefix,
|
|
||||||
"max_length": vrp.max_length,
|
|
||||||
})
|
|
||||||
.to_string();
|
|
||||||
let rule_hash = crate::audit::sha256_hex(
|
let rule_hash = crate::audit::sha256_hex(
|
||||||
format!(
|
format!(
|
||||||
"roa-rule:{}:{}:{}:{}",
|
"roa-rule:{}:{}:{}:{}",
|
||||||
@ -1801,20 +1822,20 @@ fn process_roa_with_issuer_parallel_cached(
|
|||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
);
|
);
|
||||||
VcirLocalOutput {
|
VcirLocalOutput {
|
||||||
output_id: rule_hash.clone(),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
output_type: VcirOutputType::Vrp,
|
||||||
item_effective_until: item_effective_until.clone(),
|
item_effective_until: item_effective_until.clone(),
|
||||||
source_object_uri: file.rsync_uri.clone(),
|
source_object_uri: file.rsync_uri.clone(),
|
||||||
source_object_type: "roa".to_string(),
|
source_object_type: VcirSourceObjectType::Roa,
|
||||||
source_object_hash: source_object_hash.clone(),
|
source_object_hash: file.sha256,
|
||||||
source_ee_cert_hash: source_ee_cert_hash.clone(),
|
source_ee_cert_hash: sha256_hex_to_32(&source_ee_cert_hash),
|
||||||
payload_json,
|
payload: VcirLocalOutputPayload::Vrp {
|
||||||
rule_hash,
|
asn: vrp.asn,
|
||||||
validation_path_hint: vec![
|
afi: vrp.prefix.afi,
|
||||||
manifest_rsync_uri.to_string(),
|
prefix_len: vrp.prefix.prefix_len,
|
||||||
file.rsync_uri.clone(),
|
addr: vrp.prefix.addr,
|
||||||
source_object_hash.clone(),
|
max_length: vrp.max_length,
|
||||||
],
|
},
|
||||||
|
rule_hash: sha256_hex_to_32(&rule_hash),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -1824,7 +1845,7 @@ fn process_roa_with_issuer_parallel_cached(
|
|||||||
|
|
||||||
fn process_aspa_with_issuer(
|
fn process_aspa_with_issuer(
|
||||||
file: &PackFile,
|
file: &PackFile,
|
||||||
manifest_rsync_uri: &str,
|
_manifest_rsync_uri: &str,
|
||||||
issuer_ca_der: &[u8],
|
issuer_ca_der: &[u8],
|
||||||
issuer_ca: &ResourceCertificate,
|
issuer_ca: &ResourceCertificate,
|
||||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||||
@ -1922,24 +1943,17 @@ fn process_aspa_with_issuer(
|
|||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
);
|
);
|
||||||
let local_output = VcirLocalOutput {
|
let local_output = VcirLocalOutput {
|
||||||
output_id: rule_hash.clone(),
|
|
||||||
output_type: VcirOutputType::Aspa,
|
output_type: VcirOutputType::Aspa,
|
||||||
item_effective_until,
|
item_effective_until,
|
||||||
source_object_uri: file.rsync_uri.clone(),
|
source_object_uri: file.rsync_uri.clone(),
|
||||||
source_object_type: "aspa".to_string(),
|
source_object_type: VcirSourceObjectType::Aspa,
|
||||||
source_object_hash: source_object_hash.clone(),
|
source_object_hash: file.sha256,
|
||||||
source_ee_cert_hash,
|
source_ee_cert_hash: sha256_hex_to_32(&source_ee_cert_hash),
|
||||||
payload_json: serde_json::json!({
|
payload: VcirLocalOutputPayload::Aspa {
|
||||||
"customer_as_id": attestation.customer_as_id,
|
customer_as_id: attestation.customer_as_id,
|
||||||
"provider_as_ids": attestation.provider_as_ids,
|
provider_as_ids: attestation.provider_as_ids.clone(),
|
||||||
})
|
},
|
||||||
.to_string(),
|
rule_hash: sha256_hex_to_32(&rule_hash),
|
||||||
rule_hash,
|
|
||||||
validation_path_hint: vec![
|
|
||||||
manifest_rsync_uri.to_string(),
|
|
||||||
file.rsync_uri.clone(),
|
|
||||||
source_object_hash,
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((attestation, Some(local_output)))
|
Ok((attestation, Some(local_output)))
|
||||||
@ -2965,6 +2979,54 @@ mod tests {
|
|||||||
assert_eq!(roa_afi_to_string(RoaAfi::Ipv6), "ipv6");
|
assert_eq!(roa_afi_to_string(RoaAfi::Ipv6), "ipv6");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parallel_stage_roa_tasks_share_stage_owned_payloads() {
|
||||||
|
let stage = ParallelObjectsStage {
|
||||||
|
publication_point_id: 7,
|
||||||
|
shared: Arc::new(RoaTaskShared {
|
||||||
|
locked_files: Arc::<[PackFile]>::from(vec![
|
||||||
|
PackFile::from_bytes_with_sha256(
|
||||||
|
"rsync://example.test/repo/a.roa",
|
||||||
|
vec![1, 2, 3],
|
||||||
|
[1u8; 32],
|
||||||
|
),
|
||||||
|
PackFile::from_bytes_with_sha256(
|
||||||
|
"rsync://example.test/repo/b.roa",
|
||||||
|
vec![4, 5, 6],
|
||||||
|
[2u8; 32],
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
manifest_rsync_uri: Arc::<str>::from("rsync://example.test/repo/manifest.mft"),
|
||||||
|
issuer_ca_der: Arc::from([0x01u8].as_slice()),
|
||||||
|
issuer_ca: Arc::new(
|
||||||
|
ResourceCertificate::decode_der(&fixture_bytes(
|
||||||
|
"tests/fixtures/ta/apnic-ta.cer",
|
||||||
|
))
|
||||||
|
.expect("decode fixture CA certificate"),
|
||||||
|
),
|
||||||
|
issuer_spki_der: Arc::from([0x02u8].as_slice()),
|
||||||
|
issuer_ca_rsync_uri: Some(Arc::<str>::from("rsync://example.test/repo/ca.cer")),
|
||||||
|
crl_cache: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
issuer_resources_index: Arc::new(IssuerResourcesIndex::default()),
|
||||||
|
issuer_effective_ip: None,
|
||||||
|
issuer_effective_as: None,
|
||||||
|
}),
|
||||||
|
validation_time: OffsetDateTime::now_utc(),
|
||||||
|
collect_vcir_local_outputs: false,
|
||||||
|
strict_cms_der: false,
|
||||||
|
strict_name: false,
|
||||||
|
warnings: Vec::new(),
|
||||||
|
stats: ObjectsStats {
|
||||||
|
roa_total: 2,
|
||||||
|
..ObjectsStats::default()
|
||||||
|
},
|
||||||
|
audit: Vec::new(),
|
||||||
|
};
|
||||||
|
let tasks = stage.build_roa_tasks();
|
||||||
|
assert_eq!(tasks.len(), 2);
|
||||||
|
assert!(Arc::ptr_eq(&tasks[0].shared, &tasks[1].shared));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn strict_name_manifest_decode_failure_drops_publication_point() {
|
fn strict_name_manifest_decode_failure_drops_publication_point() {
|
||||||
let publication_point = PublicationPointSnapshot {
|
let publication_point = PublicationPointSnapshot {
|
||||||
|
|||||||
@ -55,8 +55,37 @@ struct InflightPublicationPoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct FinishedPublicationPoint {
|
struct FinishedPublicationPoint {
|
||||||
node: QueuedCaInstance,
|
node: FinishedPublicationPointNode,
|
||||||
result: Result<PublicationPointRunResult, String>,
|
result: FinishedPublicationPointResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct FinishedPublicationPointNode {
|
||||||
|
id: u64,
|
||||||
|
parent_id: Option<u64>,
|
||||||
|
discovered_from: Option<DiscoveredFrom>,
|
||||||
|
manifest_rsync_uri: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FinishedPublicationPointNode {
|
||||||
|
fn from_queued(node: QueuedCaInstance) -> Self {
|
||||||
|
Self {
|
||||||
|
id: node.id,
|
||||||
|
parent_id: node.parent_id,
|
||||||
|
discovered_from: node.discovered_from,
|
||||||
|
manifest_rsync_uri: node.handle.manifest_rsync_uri,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum FinishedPublicationPointResult {
|
||||||
|
Ok {
|
||||||
|
warnings: Vec<Warning>,
|
||||||
|
objects: ObjectsOutput,
|
||||||
|
audit: PublicationPointAudit,
|
||||||
|
},
|
||||||
|
Err(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FinalizeTask {
|
struct FinalizeTask {
|
||||||
@ -310,24 +339,28 @@ fn elapsed_ms(started: Instant) -> u64 {
|
|||||||
fn compact_phase2_finished_result(
|
fn compact_phase2_finished_result(
|
||||||
mut result: PublicationPointRunResult,
|
mut result: PublicationPointRunResult,
|
||||||
compact_audit: bool,
|
compact_audit: bool,
|
||||||
) -> PublicationPointRunResult {
|
) -> FinishedPublicationPointResult {
|
||||||
// Phase2 only needs warnings, objects, audit, and traversal metadata after finalize.
|
result.objects.audit.clear();
|
||||||
// Dropping the snapshot here avoids retaining manifest/files/raw-byte caches until run end.
|
result.objects.local_outputs_cache.clear();
|
||||||
result.snapshot = None;
|
|
||||||
if compact_audit {
|
if compact_audit {
|
||||||
result.audit.objects.clear();
|
result.audit.objects.clear();
|
||||||
result.audit.warnings.clear();
|
result.audit.warnings.clear();
|
||||||
result.objects.audit.clear();
|
|
||||||
result.discovered_children.clear();
|
|
||||||
}
|
}
|
||||||
result
|
FinishedPublicationPointResult::Ok {
|
||||||
|
warnings: result.warnings,
|
||||||
|
objects: result.objects,
|
||||||
|
audit: result.audit,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compact_phase2_finished_result_result(
|
fn compact_phase2_finished_result_result(
|
||||||
result: Result<PublicationPointRunResult, String>,
|
result: Result<PublicationPointRunResult, String>,
|
||||||
compact_audit: bool,
|
compact_audit: bool,
|
||||||
) -> Result<PublicationPointRunResult, String> {
|
) -> FinishedPublicationPointResult {
|
||||||
result.map(|result| compact_phase2_finished_result(result, compact_audit))
|
match result {
|
||||||
|
Ok(result) => compact_phase2_finished_result(result, compact_audit),
|
||||||
|
Err(err) => FinishedPublicationPointResult::Err(err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_tree_parallel_phase2_audit_multi_root(
|
pub fn run_tree_parallel_phase2_audit_multi_root(
|
||||||
@ -712,8 +745,8 @@ fn start_queued_ca_instances(
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
finished.push(FinishedPublicationPoint {
|
finished.push(FinishedPublicationPoint {
|
||||||
node,
|
node: FinishedPublicationPointNode::from_queued(node),
|
||||||
result: Err(err),
|
result: FinishedPublicationPointResult::Err(err),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -781,7 +814,7 @@ fn stage_ready_publication_point(
|
|||||||
metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started);
|
metrics.child_enqueue_ms = elapsed_ms(child_enqueue_started);
|
||||||
}
|
}
|
||||||
finished.push(FinishedPublicationPoint {
|
finished.push(FinishedPublicationPoint {
|
||||||
node: ready.node,
|
node: FinishedPublicationPointNode::from_queued(ready.node),
|
||||||
result: compact_phase2_finished_result_result(fallback, compact_audit),
|
result: compact_phase2_finished_result_result(fallback, compact_audit),
|
||||||
});
|
});
|
||||||
metrics.total_ms = elapsed_ms(publication_point_started);
|
metrics.total_ms = elapsed_ms(publication_point_started);
|
||||||
@ -897,13 +930,10 @@ fn stage_ready_publication_point(
|
|||||||
metrics.locked_files = objects_stage.locked_file_count();
|
metrics.locked_files = objects_stage.locked_file_count();
|
||||||
metrics.aspa_objects = objects_stage.aspa_task_count();
|
metrics.aspa_objects = objects_stage.aspa_task_count();
|
||||||
let build_tasks_started = Instant::now();
|
let build_tasks_started = Instant::now();
|
||||||
let tasks = objects_stage.build_roa_tasks();
|
objects_stage.append_roa_tasks_to(pending_roa_dispatch);
|
||||||
metrics.build_roa_tasks_ms = elapsed_ms(build_tasks_started);
|
metrics.build_roa_tasks_ms = elapsed_ms(build_tasks_started);
|
||||||
let task_count = objects_stage.roa_task_count();
|
let task_count = objects_stage.roa_task_count();
|
||||||
metrics.roa_tasks = task_count;
|
metrics.roa_tasks = task_count;
|
||||||
for task in tasks {
|
|
||||||
pending_roa_dispatch.push_back(task);
|
|
||||||
}
|
|
||||||
if task_count == 0 {
|
if task_count == 0 {
|
||||||
metrics.zero_task_count = 1;
|
metrics.zero_task_count = 1;
|
||||||
match reduce_parallel_roa_stage(objects_stage, Vec::new(), runner.timing.as_ref()) {
|
match reduce_parallel_roa_stage(objects_stage, Vec::new(), runner.timing.as_ref()) {
|
||||||
@ -929,8 +959,8 @@ fn stage_ready_publication_point(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => finished.push(FinishedPublicationPoint {
|
Err(err) => finished.push(FinishedPublicationPoint {
|
||||||
node: ready.node,
|
node: FinishedPublicationPointNode::from_queued(ready.node),
|
||||||
result: Err(err),
|
result: FinishedPublicationPointResult::Err(err),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1022,7 +1052,7 @@ fn finalize_ready_objects(
|
|||||||
)
|
)
|
||||||
.map(|out| out.result);
|
.map(|out| out.result);
|
||||||
finished.push(FinishedPublicationPoint {
|
finished.push(FinishedPublicationPoint {
|
||||||
node,
|
node: FinishedPublicationPointNode::from_queued(node),
|
||||||
result: compact_phase2_finished_result_result(result, compact_audit),
|
result: compact_phase2_finished_result_result(result, compact_audit),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1400,7 +1430,7 @@ fn finalize_publication_point_state(
|
|||||||
elapsed_ms(finalize_started),
|
elapsed_ms(finalize_started),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Err(err) => (Err(err), 0),
|
Err(err) => (FinishedPublicationPointResult::Err(err), 0),
|
||||||
};
|
};
|
||||||
let finalize_worker_ms = elapsed_ms(finalize_worker_started);
|
let finalize_worker_ms = elapsed_ms(finalize_worker_started);
|
||||||
crate::progress_log::emit(
|
crate::progress_log::emit(
|
||||||
@ -1438,7 +1468,10 @@ fn finalize_publication_point_state(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
FinalizeWorkerResult {
|
FinalizeWorkerResult {
|
||||||
finished: FinishedPublicationPoint { node, result },
|
finished: FinishedPublicationPoint {
|
||||||
|
node: FinishedPublicationPointNode::from_queued(node),
|
||||||
|
result,
|
||||||
|
},
|
||||||
metrics: FinalizePublicationPointMetrics {
|
metrics: FinalizePublicationPointMetrics {
|
||||||
reduce_ms,
|
reduce_ms,
|
||||||
finalize_ms,
|
finalize_ms,
|
||||||
@ -1541,25 +1574,29 @@ fn build_tree_output(mut finished: Vec<FinishedPublicationPoint>) -> TreeRunAudi
|
|||||||
|
|
||||||
for item in finished {
|
for item in finished {
|
||||||
match item.result {
|
match item.result {
|
||||||
Ok(result) => {
|
FinishedPublicationPointResult::Ok {
|
||||||
|
warnings: result_warnings,
|
||||||
|
objects,
|
||||||
|
audit,
|
||||||
|
} => {
|
||||||
instances_processed += 1;
|
instances_processed += 1;
|
||||||
warnings.extend(result.warnings);
|
warnings.extend(result_warnings);
|
||||||
warnings.extend(result.objects.warnings);
|
warnings.extend(objects.warnings);
|
||||||
vrps.extend(result.objects.vrps);
|
vrps.extend(objects.vrps);
|
||||||
aspas.extend(result.objects.aspas);
|
aspas.extend(objects.aspas);
|
||||||
router_keys.extend(result.objects.router_keys);
|
router_keys.extend(objects.router_keys);
|
||||||
|
|
||||||
let mut audit: PublicationPointAudit = result.audit;
|
let mut audit: PublicationPointAudit = audit;
|
||||||
audit.node_id = Some(item.node.id);
|
audit.node_id = Some(item.node.id);
|
||||||
audit.parent_node_id = item.node.parent_id;
|
audit.parent_node_id = item.node.parent_id;
|
||||||
audit.discovered_from = item.node.discovered_from;
|
audit.discovered_from = item.node.discovered_from;
|
||||||
publication_points.push(audit);
|
publication_points.push(audit);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
FinishedPublicationPointResult::Err(err) => {
|
||||||
instances_failed += 1;
|
instances_failed += 1;
|
||||||
warnings.push(
|
warnings.push(
|
||||||
Warning::new(format!("publication point failed: {err}"))
|
Warning::new(format!("publication point failed: {err}"))
|
||||||
.with_context(&item.node.handle.manifest_rsync_uri),
|
.with_context(&item.node.manifest_rsync_uri),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1588,7 +1625,10 @@ pub fn run_tree_parallel_phase2_audit(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{compact_phase2_finished_result, compact_phase2_finished_result_result};
|
use super::{
|
||||||
|
FinishedPublicationPointResult, compact_phase2_finished_result,
|
||||||
|
compact_phase2_finished_result_result,
|
||||||
|
};
|
||||||
use crate::audit::PublicationPointAudit;
|
use crate::audit::PublicationPointAudit;
|
||||||
use crate::storage::PackTime;
|
use crate::storage::PackTime;
|
||||||
use crate::validation::manifest::PublicationPointSource;
|
use crate::validation::manifest::PublicationPointSource;
|
||||||
@ -1638,9 +1678,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn compact_phase2_finished_result_drops_snapshot() {
|
fn compact_phase2_finished_result_drops_snapshot() {
|
||||||
let result = compact_phase2_finished_result(sample_result(), false);
|
let result = compact_phase2_finished_result(sample_result(), false);
|
||||||
assert!(result.snapshot.is_none());
|
match result {
|
||||||
assert_eq!(result.source, PublicationPointSource::Fresh);
|
FinishedPublicationPointResult::Ok { warnings, .. } => {
|
||||||
assert!(result.discovered_children.is_empty());
|
assert!(warnings.is_empty());
|
||||||
|
}
|
||||||
|
FinishedPublicationPointResult::Err(err) => panic!("unexpected error: {err}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -1666,15 +1709,21 @@ mod tests {
|
|||||||
detail: None,
|
detail: None,
|
||||||
});
|
});
|
||||||
let result = compact_phase2_finished_result(sample, true);
|
let result = compact_phase2_finished_result(sample, true);
|
||||||
assert!(result.audit.objects.is_empty());
|
match result {
|
||||||
assert!(result.audit.warnings.is_empty());
|
FinishedPublicationPointResult::Ok { objects, audit, .. } => {
|
||||||
assert!(result.objects.audit.is_empty());
|
assert!(audit.objects.is_empty());
|
||||||
|
assert!(audit.warnings.is_empty());
|
||||||
|
assert!(objects.audit.is_empty());
|
||||||
|
}
|
||||||
|
FinishedPublicationPointResult::Err(err) => panic!("unexpected error: {err}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn compact_phase2_finished_result_result_preserves_err() {
|
fn compact_phase2_finished_result_result_preserves_err() {
|
||||||
let err = compact_phase2_finished_result_result(Err("boom".to_string()), false)
|
match compact_phase2_finished_result_result(Err("boom".to_string()), false) {
|
||||||
.expect_err("error should be preserved");
|
FinishedPublicationPointResult::Err(err) => assert_eq!(err, "boom"),
|
||||||
assert_eq!(err, "boom");
|
FinishedPublicationPointResult::Ok { .. } => panic!("error should be preserved"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,8 +27,8 @@ use crate::report::{RfcRef, Warning};
|
|||||||
use crate::storage::{
|
use crate::storage::{
|
||||||
PackFile, PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, VcirArtifactKind,
|
PackFile, PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, VcirArtifactKind,
|
||||||
VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection,
|
VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection,
|
||||||
VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirOutputType, VcirRelatedArtifact,
|
VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirLocalOutputPayload, VcirOutputType,
|
||||||
VcirSummary,
|
VcirRelatedArtifact, VcirReplaceTimingBreakdown, VcirSourceObjectType, VcirSummary,
|
||||||
};
|
};
|
||||||
use crate::sync::repo::{
|
use crate::sync::repo::{
|
||||||
sync_publication_point, sync_publication_point_replay, sync_publication_point_replay_delta,
|
sync_publication_point, sync_publication_point_replay, sync_publication_point_replay_delta,
|
||||||
@ -58,13 +58,17 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::json;
|
|
||||||
use x509_parser::prelude::FromDer;
|
use x509_parser::prelude::FromDer;
|
||||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||||
|
|
||||||
use vcir_der::encode_access_description_der_for_vcir_ccr_projection;
|
use vcir_der::encode_access_description_der_for_vcir_ccr_projection;
|
||||||
|
|
||||||
|
fn sha256_hex_to_32(hex_value: &str) -> [u8; 32] {
|
||||||
|
let mut out = [0u8; 32];
|
||||||
|
hex::decode_to_slice(hex_value, &mut out).expect("internal sha256 hex should decode");
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub(crate) struct BuildVcirTimingBreakdown {
|
pub(crate) struct BuildVcirTimingBreakdown {
|
||||||
pub(crate) select_crl_ms: u64,
|
pub(crate) select_crl_ms: u64,
|
||||||
@ -80,9 +84,9 @@ pub(crate) struct PersistVcirTimingBreakdown {
|
|||||||
pub(crate) embedded_collect_ms: u64,
|
pub(crate) embedded_collect_ms: u64,
|
||||||
pub(crate) embedded_store_ms: u64,
|
pub(crate) embedded_store_ms: u64,
|
||||||
pub(crate) build_vcir_ms: u64,
|
pub(crate) build_vcir_ms: u64,
|
||||||
pub(crate) previous_load_ms: u64,
|
|
||||||
pub(crate) replace_vcir_ms: u64,
|
pub(crate) replace_vcir_ms: u64,
|
||||||
pub(crate) build_vcir: BuildVcirTimingBreakdown,
|
pub(crate) build_vcir: BuildVcirTimingBreakdown,
|
||||||
|
pub(crate) replace_vcir: VcirReplaceTimingBreakdown,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -311,7 +315,7 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
|||||||
self.store,
|
self.store,
|
||||||
ca,
|
ca,
|
||||||
&pack,
|
&pack,
|
||||||
&objects,
|
&mut objects,
|
||||||
&warnings,
|
&warnings,
|
||||||
&child_audits,
|
&child_audits,
|
||||||
&discovered_children,
|
&discovered_children,
|
||||||
@ -666,7 +670,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
|||||||
self.validation_time,
|
self.validation_time,
|
||||||
self.timing.as_ref(),
|
self.timing.as_ref(),
|
||||||
phase2_pool,
|
phase2_pool,
|
||||||
self.persist_vcir,
|
false,
|
||||||
)
|
)
|
||||||
} else if let Some(phase2_config) = self.parallel_phase2_config.as_ref() {
|
} else if let Some(phase2_config) = self.parallel_phase2_config.as_ref() {
|
||||||
process_publication_point_for_issuer_parallel_roa_with_options(
|
process_publication_point_for_issuer_parallel_roa_with_options(
|
||||||
@ -679,7 +683,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
|||||||
self.validation_time,
|
self.validation_time,
|
||||||
self.timing.as_ref(),
|
self.timing.as_ref(),
|
||||||
phase2_config,
|
phase2_config,
|
||||||
self.persist_vcir,
|
false,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
crate::validation::objects::process_publication_point_for_issuer_with_options(
|
crate::validation::objects::process_publication_point_for_issuer_with_options(
|
||||||
@ -691,7 +695,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
|||||||
ca.effective_as_resources.as_ref(),
|
ca.effective_as_resources.as_ref(),
|
||||||
self.validation_time,
|
self.validation_time,
|
||||||
self.timing.as_ref(),
|
self.timing.as_ref(),
|
||||||
self.persist_vcir,
|
false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -747,7 +751,6 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
|||||||
"persist_embedded_collect_ms": persist_vcir_timing.embedded_collect_ms,
|
"persist_embedded_collect_ms": persist_vcir_timing.embedded_collect_ms,
|
||||||
"persist_embedded_store_ms": persist_vcir_timing.embedded_store_ms,
|
"persist_embedded_store_ms": persist_vcir_timing.embedded_store_ms,
|
||||||
"persist_build_vcir_ms": persist_vcir_timing.build_vcir_ms,
|
"persist_build_vcir_ms": persist_vcir_timing.build_vcir_ms,
|
||||||
"persist_previous_load_ms": persist_vcir_timing.previous_load_ms,
|
|
||||||
"persist_replace_vcir_ms": persist_vcir_timing.replace_vcir_ms,
|
"persist_replace_vcir_ms": persist_vcir_timing.replace_vcir_ms,
|
||||||
"persist_select_crl_ms": persist_vcir_timing.build_vcir.select_crl_ms,
|
"persist_select_crl_ms": persist_vcir_timing.build_vcir.select_crl_ms,
|
||||||
"persist_current_ca_decode_ms": persist_vcir_timing.build_vcir.current_ca_decode_ms,
|
"persist_current_ca_decode_ms": persist_vcir_timing.build_vcir.current_ca_decode_ms,
|
||||||
@ -755,6 +758,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
|||||||
"persist_child_entries_ms": persist_vcir_timing.build_vcir.child_entries_ms,
|
"persist_child_entries_ms": persist_vcir_timing.build_vcir.child_entries_ms,
|
||||||
"persist_related_artifacts_ms": persist_vcir_timing.build_vcir.related_artifacts_ms,
|
"persist_related_artifacts_ms": persist_vcir_timing.build_vcir.related_artifacts_ms,
|
||||||
"persist_vcir_struct_ms": persist_vcir_timing.build_vcir.struct_build_ms,
|
"persist_vcir_struct_ms": persist_vcir_timing.build_vcir.struct_build_ms,
|
||||||
|
"persist_replace_breakdown": &persist_vcir_timing.replace_vcir,
|
||||||
"audit_build_ms": audit_build_ms,
|
"audit_build_ms": audit_build_ms,
|
||||||
"warning_count": result.warnings.len(),
|
"warning_count": result.warnings.len(),
|
||||||
"vrp_count": result.objects.vrps.len(),
|
"vrp_count": result.objects.vrps.len(),
|
||||||
@ -790,7 +794,6 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
|||||||
"persist_embedded_collect_ms": persist_vcir_timing.embedded_collect_ms,
|
"persist_embedded_collect_ms": persist_vcir_timing.embedded_collect_ms,
|
||||||
"persist_embedded_store_ms": persist_vcir_timing.embedded_store_ms,
|
"persist_embedded_store_ms": persist_vcir_timing.embedded_store_ms,
|
||||||
"persist_build_vcir_ms": persist_vcir_timing.build_vcir_ms,
|
"persist_build_vcir_ms": persist_vcir_timing.build_vcir_ms,
|
||||||
"persist_previous_load_ms": persist_vcir_timing.previous_load_ms,
|
|
||||||
"persist_replace_vcir_ms": persist_vcir_timing.replace_vcir_ms,
|
"persist_replace_vcir_ms": persist_vcir_timing.replace_vcir_ms,
|
||||||
"persist_select_crl_ms": persist_vcir_timing.build_vcir.select_crl_ms,
|
"persist_select_crl_ms": persist_vcir_timing.build_vcir.select_crl_ms,
|
||||||
"persist_current_ca_decode_ms": persist_vcir_timing.build_vcir.current_ca_decode_ms,
|
"persist_current_ca_decode_ms": persist_vcir_timing.build_vcir.current_ca_decode_ms,
|
||||||
@ -798,6 +801,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
|||||||
"persist_child_entries_ms": persist_vcir_timing.build_vcir.child_entries_ms,
|
"persist_child_entries_ms": persist_vcir_timing.build_vcir.child_entries_ms,
|
||||||
"persist_related_artifacts_ms": persist_vcir_timing.build_vcir.related_artifacts_ms,
|
"persist_related_artifacts_ms": persist_vcir_timing.build_vcir.related_artifacts_ms,
|
||||||
"persist_vcir_struct_ms": persist_vcir_timing.build_vcir.struct_build_ms,
|
"persist_vcir_struct_ms": persist_vcir_timing.build_vcir.struct_build_ms,
|
||||||
|
"persist_replace_breakdown": &persist_vcir_timing.replace_vcir,
|
||||||
"audit_build_ms": audit_build_ms,
|
"audit_build_ms": audit_build_ms,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -1012,26 +1016,6 @@ struct VcirReuseProjection {
|
|||||||
warnings: Vec<Warning>,
|
warnings: Vec<Warning>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct VcirVrpPayload {
|
|
||||||
asn: u32,
|
|
||||||
prefix: String,
|
|
||||||
max_length: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct VcirAspaPayload {
|
|
||||||
customer_as_id: u32,
|
|
||||||
provider_as_ids: Vec<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct VcirRouterKeyPayload {
|
|
||||||
as_id: u32,
|
|
||||||
ski_hex: String,
|
|
||||||
spki_der_base64: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
|
fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
|
||||||
issuer: &CaInstanceHandle,
|
issuer: &CaInstanceHandle,
|
||||||
publication_point: &P,
|
publication_point: &P,
|
||||||
@ -2299,7 +2283,7 @@ fn build_objects_output_from_vcir(
|
|||||||
local.source_object_uri.clone(),
|
local.source_object_uri.clone(),
|
||||||
ObjectAuditEntry {
|
ObjectAuditEntry {
|
||||||
rsync_uri: local.source_object_uri.clone(),
|
rsync_uri: local.source_object_uri.clone(),
|
||||||
sha256_hex: local.source_object_hash.clone(),
|
sha256_hex: local.source_object_hash_hex(),
|
||||||
kind: audit_kind_for_vcir_output_type(local.output_type),
|
kind: audit_kind_for_vcir_output_type(local.output_type),
|
||||||
result: AuditObjectResult::Error,
|
result: AuditObjectResult::Error,
|
||||||
detail: Some(
|
detail: Some(
|
||||||
@ -2315,7 +2299,7 @@ fn build_objects_output_from_vcir(
|
|||||||
.entry(local.source_object_uri.clone())
|
.entry(local.source_object_uri.clone())
|
||||||
.or_insert_with(|| ObjectAuditEntry {
|
.or_insert_with(|| ObjectAuditEntry {
|
||||||
rsync_uri: local.source_object_uri.clone(),
|
rsync_uri: local.source_object_uri.clone(),
|
||||||
sha256_hex: local.source_object_hash.clone(),
|
sha256_hex: local.source_object_hash_hex(),
|
||||||
kind: audit_kind_for_vcir_output_type(local.output_type),
|
kind: audit_kind_for_vcir_output_type(local.output_type),
|
||||||
result: AuditObjectResult::Skipped,
|
result: AuditObjectResult::Skipped,
|
||||||
detail: Some("skipped: cached local output expired".to_string()),
|
detail: Some("skipped: cached local output expired".to_string()),
|
||||||
@ -2332,7 +2316,7 @@ fn build_objects_output_from_vcir(
|
|||||||
local.source_object_uri.clone(),
|
local.source_object_uri.clone(),
|
||||||
ObjectAuditEntry {
|
ObjectAuditEntry {
|
||||||
rsync_uri: local.source_object_uri.clone(),
|
rsync_uri: local.source_object_uri.clone(),
|
||||||
sha256_hex: local.source_object_hash.clone(),
|
sha256_hex: local.source_object_hash_hex(),
|
||||||
kind: AuditObjectKind::Roa,
|
kind: AuditObjectKind::Roa,
|
||||||
result: AuditObjectResult::Ok,
|
result: AuditObjectResult::Ok,
|
||||||
detail: None,
|
detail: None,
|
||||||
@ -2348,7 +2332,7 @@ fn build_objects_output_from_vcir(
|
|||||||
local.source_object_uri.clone(),
|
local.source_object_uri.clone(),
|
||||||
ObjectAuditEntry {
|
ObjectAuditEntry {
|
||||||
rsync_uri: local.source_object_uri.clone(),
|
rsync_uri: local.source_object_uri.clone(),
|
||||||
sha256_hex: local.source_object_hash.clone(),
|
sha256_hex: local.source_object_hash_hex(),
|
||||||
kind: AuditObjectKind::Roa,
|
kind: AuditObjectKind::Roa,
|
||||||
result: AuditObjectResult::Error,
|
result: AuditObjectResult::Error,
|
||||||
detail: Some(format!("cached ROA local output parse failed: {e}")),
|
detail: Some(format!("cached ROA local output parse failed: {e}")),
|
||||||
@ -2364,7 +2348,7 @@ fn build_objects_output_from_vcir(
|
|||||||
local.source_object_uri.clone(),
|
local.source_object_uri.clone(),
|
||||||
ObjectAuditEntry {
|
ObjectAuditEntry {
|
||||||
rsync_uri: local.source_object_uri.clone(),
|
rsync_uri: local.source_object_uri.clone(),
|
||||||
sha256_hex: local.source_object_hash.clone(),
|
sha256_hex: local.source_object_hash_hex(),
|
||||||
kind: AuditObjectKind::Aspa,
|
kind: AuditObjectKind::Aspa,
|
||||||
result: AuditObjectResult::Ok,
|
result: AuditObjectResult::Ok,
|
||||||
detail: None,
|
detail: None,
|
||||||
@ -2380,7 +2364,7 @@ fn build_objects_output_from_vcir(
|
|||||||
local.source_object_uri.clone(),
|
local.source_object_uri.clone(),
|
||||||
ObjectAuditEntry {
|
ObjectAuditEntry {
|
||||||
rsync_uri: local.source_object_uri.clone(),
|
rsync_uri: local.source_object_uri.clone(),
|
||||||
sha256_hex: local.source_object_hash.clone(),
|
sha256_hex: local.source_object_hash_hex(),
|
||||||
kind: AuditObjectKind::Aspa,
|
kind: AuditObjectKind::Aspa,
|
||||||
result: AuditObjectResult::Error,
|
result: AuditObjectResult::Error,
|
||||||
detail: Some(format!("cached ASPA local output parse failed: {e}")),
|
detail: Some(format!("cached ASPA local output parse failed: {e}")),
|
||||||
@ -2395,7 +2379,7 @@ fn build_objects_output_from_vcir(
|
|||||||
local.source_object_uri.clone(),
|
local.source_object_uri.clone(),
|
||||||
ObjectAuditEntry {
|
ObjectAuditEntry {
|
||||||
rsync_uri: local.source_object_uri.clone(),
|
rsync_uri: local.source_object_uri.clone(),
|
||||||
sha256_hex: local.source_object_hash.clone(),
|
sha256_hex: local.source_object_hash_hex(),
|
||||||
kind: AuditObjectKind::RouterCertificate,
|
kind: AuditObjectKind::RouterCertificate,
|
||||||
result: AuditObjectResult::Ok,
|
result: AuditObjectResult::Ok,
|
||||||
detail: Some("cached Router Key local output restored".to_string()),
|
detail: Some("cached Router Key local output restored".to_string()),
|
||||||
@ -2411,7 +2395,7 @@ fn build_objects_output_from_vcir(
|
|||||||
local.source_object_uri.clone(),
|
local.source_object_uri.clone(),
|
||||||
ObjectAuditEntry {
|
ObjectAuditEntry {
|
||||||
rsync_uri: local.source_object_uri.clone(),
|
rsync_uri: local.source_object_uri.clone(),
|
||||||
sha256_hex: local.source_object_hash.clone(),
|
sha256_hex: local.source_object_hash_hex(),
|
||||||
kind: AuditObjectKind::RouterCertificate,
|
kind: AuditObjectKind::RouterCertificate,
|
||||||
result: AuditObjectResult::Error,
|
result: AuditObjectResult::Error,
|
||||||
detail: Some(format!(
|
detail: Some(format!(
|
||||||
@ -2435,68 +2419,55 @@ fn build_objects_output_from_vcir(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_vcir_vrp_output(local: &VcirLocalOutput) -> Result<Vrp, String> {
|
fn parse_vcir_vrp_output(local: &VcirLocalOutput) -> Result<Vrp, String> {
|
||||||
let payload: VcirVrpPayload = serde_json::from_str(&local.payload_json)
|
match &local.payload {
|
||||||
.map_err(|e| format!("invalid VRP payload JSON: {e}"))?;
|
VcirLocalOutputPayload::Vrp {
|
||||||
Ok(Vrp {
|
asn,
|
||||||
asn: payload.asn,
|
afi,
|
||||||
prefix: parse_vcir_prefix(&payload.prefix)?,
|
prefix_len,
|
||||||
max_length: payload.max_length,
|
addr,
|
||||||
})
|
max_length,
|
||||||
|
} => Ok(Vrp {
|
||||||
|
asn: *asn,
|
||||||
|
prefix: crate::data_model::roa::IpPrefix {
|
||||||
|
afi: *afi,
|
||||||
|
prefix_len: *prefix_len,
|
||||||
|
addr: *addr,
|
||||||
|
},
|
||||||
|
max_length: *max_length,
|
||||||
|
}),
|
||||||
|
_ => Err("VCIR local output payload is not VRP".to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_vcir_aspa_output(local: &VcirLocalOutput) -> Result<AspaAttestation, String> {
|
fn parse_vcir_aspa_output(local: &VcirLocalOutput) -> Result<AspaAttestation, String> {
|
||||||
let payload: VcirAspaPayload = serde_json::from_str(&local.payload_json)
|
match &local.payload {
|
||||||
.map_err(|e| format!("invalid ASPA payload JSON: {e}"))?;
|
VcirLocalOutputPayload::Aspa {
|
||||||
Ok(AspaAttestation {
|
customer_as_id,
|
||||||
customer_as_id: payload.customer_as_id,
|
provider_as_ids,
|
||||||
provider_as_ids: payload.provider_as_ids,
|
} => Ok(AspaAttestation {
|
||||||
})
|
customer_as_id: *customer_as_id,
|
||||||
|
provider_as_ids: provider_as_ids.clone(),
|
||||||
|
}),
|
||||||
|
_ => Err("VCIR local output payload is not ASPA".to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_vcir_router_key_output(local: &VcirLocalOutput) -> Result<RouterKeyPayload, String> {
|
fn parse_vcir_router_key_output(local: &VcirLocalOutput) -> Result<RouterKeyPayload, String> {
|
||||||
let payload: VcirRouterKeyPayload = serde_json::from_str(&local.payload_json)
|
match &local.payload {
|
||||||
.map_err(|e| format!("invalid Router Key payload JSON: {e}"))?;
|
VcirLocalOutputPayload::RouterKey {
|
||||||
let ski =
|
as_id,
|
||||||
hex::decode(&payload.ski_hex).map_err(|e| format!("invalid Router Key SKI hex: {e}"))?;
|
ski,
|
||||||
let spki_der = base64::engine::general_purpose::STANDARD
|
spki_der,
|
||||||
.decode(&payload.spki_der_base64)
|
} => Ok(RouterKeyPayload {
|
||||||
.map_err(|e| format!("invalid Router Key SPKI base64: {e}"))?;
|
as_id: *as_id,
|
||||||
Ok(RouterKeyPayload {
|
ski: ski.clone(),
|
||||||
as_id: payload.as_id,
|
spki_der: spki_der.clone(),
|
||||||
ski,
|
source_object_uri: local.source_object_uri.clone(),
|
||||||
spki_der,
|
source_object_hash: local.source_object_hash_hex(),
|
||||||
source_object_uri: local.source_object_uri.clone(),
|
source_ee_cert_hash: local.source_ee_cert_hash_hex(),
|
||||||
source_object_hash: local.source_object_hash.clone(),
|
item_effective_until: local.item_effective_until.clone(),
|
||||||
source_ee_cert_hash: local.source_ee_cert_hash.clone(),
|
|
||||||
item_effective_until: local.item_effective_until.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_vcir_prefix(prefix: &str) -> Result<crate::data_model::roa::IpPrefix, String> {
|
|
||||||
let (addr, len) = prefix
|
|
||||||
.split_once('/')
|
|
||||||
.ok_or_else(|| format!("prefix missing '/': {prefix}"))?;
|
|
||||||
let prefix_len = len
|
|
||||||
.parse::<u16>()
|
|
||||||
.map_err(|e| format!("invalid prefix length '{len}': {e}"))?;
|
|
||||||
let ip = addr
|
|
||||||
.parse::<std::net::IpAddr>()
|
|
||||||
.map_err(|e| format!("invalid IP address '{addr}': {e}"))?;
|
|
||||||
match ip {
|
|
||||||
std::net::IpAddr::V4(v4) => Ok(crate::data_model::roa::IpPrefix {
|
|
||||||
afi: RoaAfi::Ipv4,
|
|
||||||
prefix_len,
|
|
||||||
addr: {
|
|
||||||
let mut addr = [0u8; 16];
|
|
||||||
addr[..4].copy_from_slice(&v4.octets());
|
|
||||||
addr
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
std::net::IpAddr::V6(v6) => Ok(crate::data_model::roa::IpPrefix {
|
|
||||||
afi: RoaAfi::Ipv6,
|
|
||||||
prefix_len,
|
|
||||||
addr: v6.octets(),
|
|
||||||
}),
|
}),
|
||||||
|
_ => Err("VCIR local output payload is not Router Key".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2582,7 +2553,7 @@ fn persist_vcir_for_fresh_result(
|
|||||||
store: &RocksStore,
|
store: &RocksStore,
|
||||||
ca: &CaInstanceHandle,
|
ca: &CaInstanceHandle,
|
||||||
pack: &PublicationPointSnapshot,
|
pack: &PublicationPointSnapshot,
|
||||||
objects: &crate::validation::objects::ObjectsOutput,
|
objects: &mut crate::validation::objects::ObjectsOutput,
|
||||||
warnings: &[Warning],
|
warnings: &[Warning],
|
||||||
child_audits: &[ObjectAuditEntry],
|
child_audits: &[ObjectAuditEntry],
|
||||||
discovered_children: &[DiscoveredChildCaInstance],
|
discovered_children: &[DiscoveredChildCaInstance],
|
||||||
@ -2605,7 +2576,7 @@ fn persist_vcir_for_fresh_result_with_timing(
|
|||||||
store: &RocksStore,
|
store: &RocksStore,
|
||||||
ca: &CaInstanceHandle,
|
ca: &CaInstanceHandle,
|
||||||
pack: &PublicationPointSnapshot,
|
pack: &PublicationPointSnapshot,
|
||||||
objects: &crate::validation::objects::ObjectsOutput,
|
objects: &mut crate::validation::objects::ObjectsOutput,
|
||||||
warnings: &[Warning],
|
warnings: &[Warning],
|
||||||
child_audits: &[ObjectAuditEntry],
|
child_audits: &[ObjectAuditEntry],
|
||||||
discovered_children: &[DiscoveredChildCaInstance],
|
discovered_children: &[DiscoveredChildCaInstance],
|
||||||
@ -2635,17 +2606,12 @@ fn persist_vcir_for_fresh_result_with_timing(
|
|||||||
timing.build_vcir_ms = build_vcir_started.elapsed().as_millis() as u64;
|
timing.build_vcir_ms = build_vcir_started.elapsed().as_millis() as u64;
|
||||||
timing.build_vcir = build_vcir_timing;
|
timing.build_vcir = build_vcir_timing;
|
||||||
|
|
||||||
let previous_load_started = std::time::Instant::now();
|
|
||||||
let previous = store
|
|
||||||
.get_vcir(&pack.manifest_rsync_uri)
|
|
||||||
.map_err(|e| format!("load existing VCIR failed: {e}"))?;
|
|
||||||
timing.previous_load_ms = previous_load_started.elapsed().as_millis() as u64;
|
|
||||||
|
|
||||||
let replace_vcir_started = std::time::Instant::now();
|
let replace_vcir_started = std::time::Instant::now();
|
||||||
store
|
let replace_timing = store
|
||||||
.replace_vcir_and_audit_rule_indexes(previous.as_ref(), &vcir)
|
.replace_vcir_and_manifest_replay_meta(&vcir)
|
||||||
.map_err(|e| format!("store VCIR and audit rule index failed: {e}"))?;
|
.map_err(|e| format!("store VCIR and manifest replay meta failed: {e}"))?;
|
||||||
timing.replace_vcir_ms = replace_vcir_started.elapsed().as_millis() as u64;
|
timing.replace_vcir_ms = replace_vcir_started.elapsed().as_millis() as u64;
|
||||||
|
timing.replace_vcir = replace_timing;
|
||||||
|
|
||||||
Ok(timing)
|
Ok(timing)
|
||||||
}
|
}
|
||||||
@ -2653,7 +2619,7 @@ fn persist_vcir_for_fresh_result_with_timing(
|
|||||||
fn build_vcir_from_fresh_result(
|
fn build_vcir_from_fresh_result(
|
||||||
ca: &CaInstanceHandle,
|
ca: &CaInstanceHandle,
|
||||||
pack: &PublicationPointSnapshot,
|
pack: &PublicationPointSnapshot,
|
||||||
objects: &crate::validation::objects::ObjectsOutput,
|
objects: &mut crate::validation::objects::ObjectsOutput,
|
||||||
warnings: &[Warning],
|
warnings: &[Warning],
|
||||||
child_audits: &[ObjectAuditEntry],
|
child_audits: &[ObjectAuditEntry],
|
||||||
discovered_children: &[DiscoveredChildCaInstance],
|
discovered_children: &[DiscoveredChildCaInstance],
|
||||||
@ -2674,7 +2640,7 @@ fn build_vcir_from_fresh_result(
|
|||||||
fn build_vcir_from_fresh_result_with_timing(
|
fn build_vcir_from_fresh_result_with_timing(
|
||||||
ca: &CaInstanceHandle,
|
ca: &CaInstanceHandle,
|
||||||
pack: &PublicationPointSnapshot,
|
pack: &PublicationPointSnapshot,
|
||||||
objects: &crate::validation::objects::ObjectsOutput,
|
objects: &mut crate::validation::objects::ObjectsOutput,
|
||||||
warnings: &[Warning],
|
warnings: &[Warning],
|
||||||
child_audits: &[ObjectAuditEntry],
|
child_audits: &[ObjectAuditEntry],
|
||||||
discovered_children: &[DiscoveredChildCaInstance],
|
discovered_children: &[DiscoveredChildCaInstance],
|
||||||
@ -2692,7 +2658,7 @@ fn build_vcir_from_fresh_result_with_timing(
|
|||||||
timing.current_ca_decode_ms = current_ca_decode_started.elapsed().as_millis() as u64;
|
timing.current_ca_decode_ms = current_ca_decode_started.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
let local_outputs_started = std::time::Instant::now();
|
let local_outputs_started = std::time::Instant::now();
|
||||||
let local_outputs = build_vcir_local_outputs(ca, pack, objects)?;
|
let local_outputs = take_or_build_vcir_local_outputs(ca, pack, objects)?;
|
||||||
timing.local_outputs_ms = local_outputs_started.elapsed().as_millis() as u64;
|
timing.local_outputs_ms = local_outputs_started.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
let child_entries_started = std::time::Instant::now();
|
let child_entries_started = std::time::Instant::now();
|
||||||
@ -2710,6 +2676,18 @@ fn build_vcir_from_fresh_result_with_timing(
|
|||||||
timing.related_artifacts_ms = related_artifacts_started.elapsed().as_millis() as u64;
|
timing.related_artifacts_ms = related_artifacts_started.elapsed().as_millis() as u64;
|
||||||
let ccr_manifest_projection =
|
let ccr_manifest_projection =
|
||||||
build_vcir_ccr_manifest_projection_from_fresh(ca, pack, &child_entries)?;
|
build_vcir_ccr_manifest_projection_from_fresh(ca, pack, &child_entries)?;
|
||||||
|
let local_vrp_count = local_outputs
|
||||||
|
.iter()
|
||||||
|
.filter(|output| output.output_type == VcirOutputType::Vrp)
|
||||||
|
.count() as u32;
|
||||||
|
let local_aspa_count = local_outputs
|
||||||
|
.iter()
|
||||||
|
.filter(|output| output.output_type == VcirOutputType::Aspa)
|
||||||
|
.count() as u32;
|
||||||
|
let local_router_key_count = local_outputs
|
||||||
|
.iter()
|
||||||
|
.filter(|output| output.output_type == VcirOutputType::RouterKey)
|
||||||
|
.count() as u32;
|
||||||
let accepted_object_count = related_artifacts
|
let accepted_object_count = related_artifacts
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|artifact| artifact.validation_status == VcirArtifactValidationStatus::Accepted)
|
.filter(|artifact| artifact.validation_status == VcirArtifactValidationStatus::Accepted)
|
||||||
@ -2768,21 +2746,12 @@ fn build_vcir_from_fresh_result_with_timing(
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
child_entries,
|
child_entries,
|
||||||
local_outputs: local_outputs.clone(),
|
local_outputs,
|
||||||
related_artifacts: related_artifacts.clone(),
|
related_artifacts,
|
||||||
summary: VcirSummary {
|
summary: VcirSummary {
|
||||||
local_vrp_count: local_outputs
|
local_vrp_count,
|
||||||
.iter()
|
local_aspa_count,
|
||||||
.filter(|output| output.output_type == VcirOutputType::Vrp)
|
local_router_key_count,
|
||||||
.count() as u32,
|
|
||||||
local_aspa_count: local_outputs
|
|
||||||
.iter()
|
|
||||||
.filter(|output| output.output_type == VcirOutputType::Aspa)
|
|
||||||
.count() as u32,
|
|
||||||
local_router_key_count: local_outputs
|
|
||||||
.iter()
|
|
||||||
.filter(|output| output.output_type == VcirOutputType::RouterKey)
|
|
||||||
.count() as u32,
|
|
||||||
child_count: discovered_children.len() as u32,
|
child_count: discovered_children.len() as u32,
|
||||||
accepted_object_count,
|
accepted_object_count,
|
||||||
rejected_object_count,
|
rejected_object_count,
|
||||||
@ -2799,6 +2768,17 @@ fn build_vcir_from_fresh_result_with_timing(
|
|||||||
Ok((vcir, timing))
|
Ok((vcir, timing))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn take_or_build_vcir_local_outputs(
|
||||||
|
ca: &CaInstanceHandle,
|
||||||
|
pack: &PublicationPointSnapshot,
|
||||||
|
objects: &mut crate::validation::objects::ObjectsOutput,
|
||||||
|
) -> Result<Vec<VcirLocalOutput>, String> {
|
||||||
|
if !objects.local_outputs_cache.is_empty() {
|
||||||
|
return Ok(std::mem::take(&mut objects.local_outputs_cache));
|
||||||
|
}
|
||||||
|
build_vcir_local_outputs(ca, pack, objects)
|
||||||
|
}
|
||||||
|
|
||||||
fn build_vcir_ccr_manifest_projection_from_fresh(
|
fn build_vcir_ccr_manifest_projection_from_fresh(
|
||||||
ca: &CaInstanceHandle,
|
ca: &CaInstanceHandle,
|
||||||
pack: &PublicationPointSnapshot,
|
pack: &PublicationPointSnapshot,
|
||||||
@ -2892,7 +2872,7 @@ fn select_manifest_current_crl_from_snapshot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_vcir_local_outputs(
|
fn build_vcir_local_outputs(
|
||||||
ca: &CaInstanceHandle,
|
_ca: &CaInstanceHandle,
|
||||||
pack: &PublicationPointSnapshot,
|
pack: &PublicationPointSnapshot,
|
||||||
objects: &crate::validation::objects::ObjectsOutput,
|
objects: &crate::validation::objects::ObjectsOutput,
|
||||||
) -> Result<Vec<VcirLocalOutput>, String> {
|
) -> Result<Vec<VcirLocalOutput>, String> {
|
||||||
@ -2930,12 +2910,6 @@ fn build_vcir_local_outputs(
|
|||||||
PackTime::from_utc_offset_datetime(ee.resource_cert.tbs.validity_not_after);
|
PackTime::from_utc_offset_datetime(ee.resource_cert.tbs.validity_not_after);
|
||||||
for vrp in roa_to_vrps_for_vcir(&roa) {
|
for vrp in roa_to_vrps_for_vcir(&roa) {
|
||||||
let prefix = vrp_prefix_to_string(&vrp);
|
let prefix = vrp_prefix_to_string(&vrp);
|
||||||
let payload_json = json!({
|
|
||||||
"asn": vrp.asn,
|
|
||||||
"prefix": prefix,
|
|
||||||
"max_length": vrp.max_length,
|
|
||||||
})
|
|
||||||
.to_string();
|
|
||||||
let rule_hash = sha256_hex(
|
let rule_hash = sha256_hex(
|
||||||
format!(
|
format!(
|
||||||
"roa-rule:{}:{}:{}:{}",
|
"roa-rule:{}:{}:{}:{}",
|
||||||
@ -2944,20 +2918,20 @@ fn build_vcir_local_outputs(
|
|||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
);
|
);
|
||||||
out.push(VcirLocalOutput {
|
out.push(VcirLocalOutput {
|
||||||
output_id: rule_hash.clone(),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
output_type: VcirOutputType::Vrp,
|
||||||
item_effective_until: item_effective_until.clone(),
|
item_effective_until: item_effective_until.clone(),
|
||||||
source_object_uri: file.rsync_uri.clone(),
|
source_object_uri: file.rsync_uri.clone(),
|
||||||
source_object_type: "roa".to_string(),
|
source_object_type: VcirSourceObjectType::Roa,
|
||||||
source_object_hash: source_object_hash.clone(),
|
source_object_hash: file.sha256,
|
||||||
source_ee_cert_hash: source_ee_cert_hash.clone(),
|
source_ee_cert_hash: sha256_hex_to_32(&source_ee_cert_hash),
|
||||||
payload_json,
|
payload: VcirLocalOutputPayload::Vrp {
|
||||||
rule_hash,
|
asn: vrp.asn,
|
||||||
validation_path_hint: vec![
|
afi: vrp.prefix.afi,
|
||||||
ca.manifest_rsync_uri.clone(),
|
prefix_len: vrp.prefix.prefix_len,
|
||||||
file.rsync_uri.clone(),
|
addr: vrp.prefix.addr,
|
||||||
source_object_hash.clone(),
|
max_length: vrp.max_length,
|
||||||
],
|
},
|
||||||
|
rule_hash: sha256_hex_to_32(&rule_hash),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if accepted_aspa_uris.contains(file.rsync_uri.as_str()) {
|
} else if accepted_aspa_uris.contains(file.rsync_uri.as_str()) {
|
||||||
@ -2970,11 +2944,6 @@ fn build_vcir_local_outputs(
|
|||||||
let source_ee_cert_hash = sha256_hex(ee.raw_der.as_slice());
|
let source_ee_cert_hash = sha256_hex(ee.raw_der.as_slice());
|
||||||
let item_effective_until =
|
let item_effective_until =
|
||||||
PackTime::from_utc_offset_datetime(ee.resource_cert.tbs.validity_not_after);
|
PackTime::from_utc_offset_datetime(ee.resource_cert.tbs.validity_not_after);
|
||||||
let payload_json = json!({
|
|
||||||
"customer_as_id": aspa.aspa.customer_as_id,
|
|
||||||
"provider_as_ids": aspa.aspa.provider_as_ids,
|
|
||||||
})
|
|
||||||
.to_string();
|
|
||||||
let providers = aspa
|
let providers = aspa
|
||||||
.aspa
|
.aspa
|
||||||
.provider_as_ids
|
.provider_as_ids
|
||||||
@ -2990,20 +2959,17 @@ fn build_vcir_local_outputs(
|
|||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
);
|
);
|
||||||
out.push(VcirLocalOutput {
|
out.push(VcirLocalOutput {
|
||||||
output_id: rule_hash.clone(),
|
|
||||||
output_type: VcirOutputType::Aspa,
|
output_type: VcirOutputType::Aspa,
|
||||||
item_effective_until,
|
item_effective_until,
|
||||||
source_object_uri: file.rsync_uri.clone(),
|
source_object_uri: file.rsync_uri.clone(),
|
||||||
source_object_type: "aspa".to_string(),
|
source_object_type: VcirSourceObjectType::Aspa,
|
||||||
source_object_hash: source_object_hash.clone(),
|
source_object_hash: file.sha256,
|
||||||
source_ee_cert_hash,
|
source_ee_cert_hash: sha256_hex_to_32(&source_ee_cert_hash),
|
||||||
payload_json,
|
payload: VcirLocalOutputPayload::Aspa {
|
||||||
rule_hash,
|
customer_as_id: aspa.aspa.customer_as_id,
|
||||||
validation_path_hint: vec![
|
provider_as_ids: aspa.aspa.provider_as_ids.clone(),
|
||||||
ca.manifest_rsync_uri.clone(),
|
},
|
||||||
file.rsync_uri.clone(),
|
rule_hash: sha256_hex_to_32(&rule_hash),
|
||||||
source_object_hash,
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3011,7 +2977,7 @@ fn build_vcir_local_outputs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build_router_key_local_outputs(
|
pub(crate) fn build_router_key_local_outputs(
|
||||||
ca: &CaInstanceHandle,
|
_ca: &CaInstanceHandle,
|
||||||
router_keys: &[RouterKeyPayload],
|
router_keys: &[RouterKeyPayload],
|
||||||
) -> Vec<VcirLocalOutput> {
|
) -> Vec<VcirLocalOutput> {
|
||||||
router_keys
|
router_keys
|
||||||
@ -3028,25 +2994,18 @@ pub(crate) fn build_router_key_local_outputs(
|
|||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
);
|
);
|
||||||
VcirLocalOutput {
|
VcirLocalOutput {
|
||||||
output_id: rule_hash.clone(),
|
|
||||||
output_type: VcirOutputType::RouterKey,
|
output_type: VcirOutputType::RouterKey,
|
||||||
item_effective_until: router_key.item_effective_until.clone(),
|
item_effective_until: router_key.item_effective_until.clone(),
|
||||||
source_object_uri: router_key.source_object_uri.clone(),
|
source_object_uri: router_key.source_object_uri.clone(),
|
||||||
source_object_type: "router_key".to_string(),
|
source_object_type: VcirSourceObjectType::RouterKey,
|
||||||
source_object_hash: router_key.source_object_hash.clone(),
|
source_object_hash: sha256_hex_to_32(&router_key.source_object_hash),
|
||||||
source_ee_cert_hash: router_key.source_ee_cert_hash.clone(),
|
source_ee_cert_hash: sha256_hex_to_32(&router_key.source_ee_cert_hash),
|
||||||
payload_json: json!({
|
payload: VcirLocalOutputPayload::RouterKey {
|
||||||
"as_id": router_key.as_id,
|
as_id: router_key.as_id,
|
||||||
"ski_hex": ski_hex,
|
ski: router_key.ski.clone(),
|
||||||
"spki_der_base64": spki_der_base64,
|
spki_der: router_key.spki_der.clone(),
|
||||||
})
|
},
|
||||||
.to_string(),
|
rule_hash: sha256_hex_to_32(&rule_hash),
|
||||||
rule_hash,
|
|
||||||
validation_path_hint: vec![
|
|
||||||
ca.manifest_rsync_uri.clone(),
|
|
||||||
router_key.source_object_uri.clone(),
|
|
||||||
router_key.source_object_hash.clone(),
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::data_model::rc::ResourceCertificate;
|
use crate::data_model::rc::ResourceCertificate;
|
||||||
|
use crate::data_model::roa::RoaAfi;
|
||||||
use crate::fetch::rsync::LocalDirRsyncFetcher;
|
use crate::fetch::rsync::LocalDirRsyncFetcher;
|
||||||
use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
|
use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
|
||||||
use crate::storage::{
|
use crate::storage::{
|
||||||
PackFile, PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult,
|
PackFile, PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult,
|
||||||
ValidatedManifestMeta, VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus,
|
ValidatedManifestMeta, VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus,
|
||||||
VcirAuditSummary, VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirOutputType,
|
VcirAuditSummary, VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirLocalOutputPayload,
|
||||||
VcirRelatedArtifact, VcirSummary,
|
VcirOutputType, VcirRelatedArtifact, VcirSourceObjectType, VcirSummary,
|
||||||
};
|
};
|
||||||
use crate::sync::rrdp::Fetcher;
|
use crate::sync::rrdp::Fetcher;
|
||||||
use crate::validation::publication_point::PublicationPointSnapshot;
|
use crate::validation::publication_point::PublicationPointSnapshot;
|
||||||
@ -16,6 +17,16 @@ use std::process::Command;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
fn sha256_32(input: &[u8]) -> [u8; 32] {
|
||||||
|
sha256_hex_to_32(&sha256_hex(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ipv4_addr(octets: [u8; 4]) -> [u8; 16] {
|
||||||
|
let mut addr = [0u8; 16];
|
||||||
|
addr[..4].copy_from_slice(&octets);
|
||||||
|
addr
|
||||||
|
}
|
||||||
|
|
||||||
struct NeverHttpFetcher;
|
struct NeverHttpFetcher;
|
||||||
impl Fetcher for NeverHttpFetcher {
|
impl Fetcher for NeverHttpFetcher {
|
||||||
fn fetch(&self, _uri: &str) -> Result<Vec<u8>, String> {
|
fn fetch(&self, _uri: &str) -> Result<Vec<u8>, String> {
|
||||||
@ -534,138 +545,147 @@ fn sample_vcir_for_projection(
|
|||||||
subordinate_skis: vec![vec![0x33; 20]],
|
subordinate_skis: vec![vec![0x33; 20]],
|
||||||
};
|
};
|
||||||
ValidatedCaInstanceResult {
|
ValidatedCaInstanceResult {
|
||||||
manifest_rsync_uri: manifest_uri.clone(),
|
manifest_rsync_uri: manifest_uri.clone(),
|
||||||
parent_manifest_rsync_uri: None,
|
parent_manifest_rsync_uri: None,
|
||||||
tal_id: "test-tal".to_string(),
|
tal_id: "test-tal".to_string(),
|
||||||
ca_subject_name: "CN=Issuer".to_string(),
|
ca_subject_name: "CN=Issuer".to_string(),
|
||||||
ca_ski: "11".repeat(20),
|
ca_ski: "11".repeat(20),
|
||||||
issuer_ski: "22".repeat(20),
|
issuer_ski: "22".repeat(20),
|
||||||
last_successful_validation_time: PackTime::from_utc_offset_datetime(now),
|
last_successful_validation_time: PackTime::from_utc_offset_datetime(now),
|
||||||
current_manifest_rsync_uri: manifest_uri.clone(),
|
current_manifest_rsync_uri: manifest_uri.clone(),
|
||||||
current_crl_rsync_uri: current_crl_uri.clone(),
|
current_crl_rsync_uri: current_crl_uri.clone(),
|
||||||
validated_manifest_meta: ValidatedManifestMeta {
|
validated_manifest_meta: ValidatedManifestMeta {
|
||||||
validated_manifest_number: vec![1],
|
validated_manifest_number: vec![1],
|
||||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(now),
|
validated_manifest_this_update: PackTime::from_utc_offset_datetime(now),
|
||||||
validated_manifest_next_update: gate_until.clone(),
|
validated_manifest_next_update: gate_until.clone(),
|
||||||
|
},
|
||||||
|
ccr_manifest_projection,
|
||||||
|
instance_gate: VcirInstanceGate {
|
||||||
|
manifest_next_update: gate_until.clone(),
|
||||||
|
current_crl_next_update: gate_until.clone(),
|
||||||
|
self_ca_not_after: PackTime::from_utc_offset_datetime(now + time::Duration::hours(2)),
|
||||||
|
instance_effective_until: gate_until.clone(),
|
||||||
|
},
|
||||||
|
child_entries: vec![VcirChildEntry {
|
||||||
|
child_manifest_rsync_uri: child_manifest_uri,
|
||||||
|
child_cert_rsync_uri: child_cert_uri.clone(),
|
||||||
|
child_cert_hash: child_cert_hash.to_string(),
|
||||||
|
child_ski: "33".repeat(20),
|
||||||
|
child_rsync_base_uri: "rsync://example.test/repo/child/".to_string(),
|
||||||
|
child_publication_point_rsync_uri: "rsync://example.test/repo/child/".to_string(),
|
||||||
|
child_rrdp_notification_uri: Some("https://example.test/child-notify.xml".to_string()),
|
||||||
|
child_effective_ip_resources: None,
|
||||||
|
child_effective_as_resources: None,
|
||||||
|
accepted_at_validation_time: PackTime::from_utc_offset_datetime(now),
|
||||||
|
}],
|
||||||
|
local_outputs: vec![
|
||||||
|
VcirLocalOutput {
|
||||||
|
output_type: VcirOutputType::Vrp,
|
||||||
|
item_effective_until: PackTime::from_utc_offset_datetime(
|
||||||
|
now + time::Duration::minutes(30),
|
||||||
|
),
|
||||||
|
source_object_uri: roa_uri.clone(),
|
||||||
|
source_object_type: VcirSourceObjectType::Roa,
|
||||||
|
source_object_hash: sha256_hex_to_32(&roa_hash),
|
||||||
|
source_ee_cert_hash: sha256_hex_to_32(&ee_hash),
|
||||||
|
payload: VcirLocalOutputPayload::Vrp {
|
||||||
|
asn: 64496,
|
||||||
|
afi: RoaAfi::Ipv4,
|
||||||
|
prefix_len: 24,
|
||||||
|
addr: ipv4_addr([203, 0, 113, 0]),
|
||||||
|
max_length: 24,
|
||||||
|
},
|
||||||
|
rule_hash: sha256_32(b"roa-rule"),
|
||||||
},
|
},
|
||||||
ccr_manifest_projection,
|
VcirLocalOutput {
|
||||||
instance_gate: VcirInstanceGate {
|
output_type: VcirOutputType::Aspa,
|
||||||
manifest_next_update: gate_until.clone(),
|
item_effective_until: PackTime::from_utc_offset_datetime(
|
||||||
current_crl_next_update: gate_until.clone(),
|
now + time::Duration::minutes(30),
|
||||||
self_ca_not_after: PackTime::from_utc_offset_datetime(now + time::Duration::hours(2)),
|
),
|
||||||
instance_effective_until: gate_until.clone(),
|
source_object_uri: aspa_uri.clone(),
|
||||||
|
source_object_type: VcirSourceObjectType::Aspa,
|
||||||
|
source_object_hash: sha256_hex_to_32(&aspa_hash),
|
||||||
|
source_ee_cert_hash: sha256_hex_to_32(&ee_hash),
|
||||||
|
payload: VcirLocalOutputPayload::Aspa {
|
||||||
|
customer_as_id: 64496,
|
||||||
|
provider_as_ids: vec![64497, 64498],
|
||||||
|
},
|
||||||
|
rule_hash: sha256_32(b"aspa-rule"),
|
||||||
},
|
},
|
||||||
child_entries: vec![VcirChildEntry {
|
VcirLocalOutput {
|
||||||
child_manifest_rsync_uri: child_manifest_uri,
|
output_type: VcirOutputType::RouterKey,
|
||||||
child_cert_rsync_uri: child_cert_uri.clone(),
|
item_effective_until: PackTime::from_utc_offset_datetime(
|
||||||
child_cert_hash: child_cert_hash.to_string(),
|
now + time::Duration::minutes(30),
|
||||||
child_ski: "33".repeat(20),
|
),
|
||||||
child_rsync_base_uri: "rsync://example.test/repo/child/".to_string(),
|
source_object_uri: router_uri.clone(),
|
||||||
child_publication_point_rsync_uri: "rsync://example.test/repo/child/".to_string(),
|
source_object_type: VcirSourceObjectType::RouterKey,
|
||||||
child_rrdp_notification_uri: Some("https://example.test/child-notify.xml".to_string()),
|
source_object_hash: sha256_hex_to_32(&router_hash),
|
||||||
child_effective_ip_resources: None,
|
source_ee_cert_hash: sha256_hex_to_32(&router_hash),
|
||||||
child_effective_as_resources: None,
|
payload: VcirLocalOutputPayload::RouterKey {
|
||||||
accepted_at_validation_time: PackTime::from_utc_offset_datetime(now),
|
as_id: 64496,
|
||||||
}],
|
ski: vec![0x11; 20],
|
||||||
local_outputs: vec![
|
spki_der: vec![0x30, 0x00],
|
||||||
VcirLocalOutput {
|
|
||||||
output_id: sha256_hex(b"vrp-out"),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(30)),
|
|
||||||
source_object_uri: roa_uri.clone(),
|
|
||||||
source_object_type: "roa".to_string(),
|
|
||||||
source_object_hash: roa_hash.clone(),
|
|
||||||
source_ee_cert_hash: ee_hash.clone(),
|
|
||||||
payload_json: serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24}).to_string(),
|
|
||||||
rule_hash: sha256_hex(b"roa-rule"),
|
|
||||||
validation_path_hint: vec![manifest_uri.clone(), roa_uri.clone()],
|
|
||||||
},
|
},
|
||||||
VcirLocalOutput {
|
rule_hash: sha256_32(b"router-key-rule"),
|
||||||
output_id: sha256_hex(b"aspa-out"),
|
|
||||||
output_type: VcirOutputType::Aspa,
|
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(30)),
|
|
||||||
source_object_uri: aspa_uri.clone(),
|
|
||||||
source_object_type: "aspa".to_string(),
|
|
||||||
source_object_hash: aspa_hash.clone(),
|
|
||||||
source_ee_cert_hash: ee_hash,
|
|
||||||
payload_json: serde_json::json!({"customer_as_id": 64496, "provider_as_ids": [64497, 64498]}).to_string(),
|
|
||||||
rule_hash: sha256_hex(b"aspa-rule"),
|
|
||||||
validation_path_hint: vec![manifest_uri.clone(), aspa_uri.clone()],
|
|
||||||
},
|
|
||||||
VcirLocalOutput {
|
|
||||||
output_id: sha256_hex(b"router-key-out"),
|
|
||||||
output_type: VcirOutputType::RouterKey,
|
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(30)),
|
|
||||||
source_object_uri: router_uri.clone(),
|
|
||||||
source_object_type: "router_key".to_string(),
|
|
||||||
source_object_hash: router_hash.clone(),
|
|
||||||
source_ee_cert_hash: router_hash.clone(),
|
|
||||||
payload_json: serde_json::json!({
|
|
||||||
"as_id": 64496,
|
|
||||||
"ski_hex": "11".repeat(20),
|
|
||||||
"spki_der_base64": base64::engine::general_purpose::STANDARD.encode([0x30u8, 0x00]),
|
|
||||||
}).to_string(),
|
|
||||||
rule_hash: sha256_hex(b"router-key-rule"),
|
|
||||||
validation_path_hint: vec![manifest_uri.clone(), router_uri.clone()],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
related_artifacts: vec![
|
|
||||||
VcirRelatedArtifact {
|
|
||||||
artifact_role: VcirArtifactRole::Manifest,
|
|
||||||
artifact_kind: VcirArtifactKind::Mft,
|
|
||||||
uri: Some(manifest_uri.clone()),
|
|
||||||
sha256: manifest_hash,
|
|
||||||
object_type: Some("mft".to_string()),
|
|
||||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
||||||
},
|
|
||||||
VcirRelatedArtifact {
|
|
||||||
artifact_role: VcirArtifactRole::CurrentCrl,
|
|
||||||
artifact_kind: VcirArtifactKind::Crl,
|
|
||||||
uri: Some(current_crl_uri),
|
|
||||||
sha256: current_crl_hash,
|
|
||||||
object_type: Some("crl".to_string()),
|
|
||||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
||||||
},
|
|
||||||
VcirRelatedArtifact {
|
|
||||||
artifact_role: VcirArtifactRole::ChildCaCert,
|
|
||||||
artifact_kind: VcirArtifactKind::Cer,
|
|
||||||
uri: Some(child_cert_uri),
|
|
||||||
sha256: child_cert_hash.to_string(),
|
|
||||||
object_type: Some("cer".to_string()),
|
|
||||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
||||||
},
|
|
||||||
VcirRelatedArtifact {
|
|
||||||
artifact_role: VcirArtifactRole::SignedObject,
|
|
||||||
artifact_kind: VcirArtifactKind::Roa,
|
|
||||||
uri: Some(roa_uri),
|
|
||||||
sha256: roa_hash,
|
|
||||||
object_type: Some("roa".to_string()),
|
|
||||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
||||||
},
|
|
||||||
VcirRelatedArtifact {
|
|
||||||
artifact_role: VcirArtifactRole::SignedObject,
|
|
||||||
artifact_kind: VcirArtifactKind::Aspa,
|
|
||||||
uri: Some(aspa_uri),
|
|
||||||
sha256: aspa_hash,
|
|
||||||
object_type: Some("aspa".to_string()),
|
|
||||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
summary: VcirSummary {
|
|
||||||
local_vrp_count: 1,
|
|
||||||
local_aspa_count: 1,
|
|
||||||
local_router_key_count: 1,
|
|
||||||
child_count: 1,
|
|
||||||
accepted_object_count: 4,
|
|
||||||
rejected_object_count: 0,
|
|
||||||
},
|
},
|
||||||
audit_summary: VcirAuditSummary {
|
],
|
||||||
failed_fetch_eligible: true,
|
related_artifacts: vec![
|
||||||
last_failed_fetch_reason: None,
|
VcirRelatedArtifact {
|
||||||
warning_count: 0,
|
artifact_role: VcirArtifactRole::Manifest,
|
||||||
audit_flags: Vec::new(),
|
artifact_kind: VcirArtifactKind::Mft,
|
||||||
|
uri: Some(manifest_uri.clone()),
|
||||||
|
sha256: manifest_hash,
|
||||||
|
object_type: Some("mft".to_string()),
|
||||||
|
validation_status: VcirArtifactValidationStatus::Accepted,
|
||||||
},
|
},
|
||||||
}
|
VcirRelatedArtifact {
|
||||||
|
artifact_role: VcirArtifactRole::CurrentCrl,
|
||||||
|
artifact_kind: VcirArtifactKind::Crl,
|
||||||
|
uri: Some(current_crl_uri),
|
||||||
|
sha256: current_crl_hash,
|
||||||
|
object_type: Some("crl".to_string()),
|
||||||
|
validation_status: VcirArtifactValidationStatus::Accepted,
|
||||||
|
},
|
||||||
|
VcirRelatedArtifact {
|
||||||
|
artifact_role: VcirArtifactRole::ChildCaCert,
|
||||||
|
artifact_kind: VcirArtifactKind::Cer,
|
||||||
|
uri: Some(child_cert_uri),
|
||||||
|
sha256: child_cert_hash.to_string(),
|
||||||
|
object_type: Some("cer".to_string()),
|
||||||
|
validation_status: VcirArtifactValidationStatus::Accepted,
|
||||||
|
},
|
||||||
|
VcirRelatedArtifact {
|
||||||
|
artifact_role: VcirArtifactRole::SignedObject,
|
||||||
|
artifact_kind: VcirArtifactKind::Roa,
|
||||||
|
uri: Some(roa_uri),
|
||||||
|
sha256: roa_hash,
|
||||||
|
object_type: Some("roa".to_string()),
|
||||||
|
validation_status: VcirArtifactValidationStatus::Accepted,
|
||||||
|
},
|
||||||
|
VcirRelatedArtifact {
|
||||||
|
artifact_role: VcirArtifactRole::SignedObject,
|
||||||
|
artifact_kind: VcirArtifactKind::Aspa,
|
||||||
|
uri: Some(aspa_uri),
|
||||||
|
sha256: aspa_hash,
|
||||||
|
object_type: Some("aspa".to_string()),
|
||||||
|
validation_status: VcirArtifactValidationStatus::Accepted,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
summary: VcirSummary {
|
||||||
|
local_vrp_count: 1,
|
||||||
|
local_aspa_count: 1,
|
||||||
|
local_router_key_count: 1,
|
||||||
|
child_count: 1,
|
||||||
|
accepted_object_count: 4,
|
||||||
|
rejected_object_count: 0,
|
||||||
|
},
|
||||||
|
audit_summary: VcirAuditSummary {
|
||||||
|
failed_fetch_eligible: true,
|
||||||
|
last_failed_fetch_reason: None,
|
||||||
|
warning_count: 0,
|
||||||
|
audit_flags: Vec::new(),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -716,16 +736,20 @@ fn build_vcir_local_outputs_prefers_cached_outputs() {
|
|||||||
rrdp_notification_uri: None,
|
rrdp_notification_uri: None,
|
||||||
};
|
};
|
||||||
let cached = vec![VcirLocalOutput {
|
let cached = vec![VcirLocalOutput {
|
||||||
output_id: "cached-output".to_string(),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
output_type: VcirOutputType::Vrp,
|
||||||
item_effective_until: pack.next_update.clone(),
|
item_effective_until: pack.next_update.clone(),
|
||||||
source_object_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
|
source_object_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
|
||||||
source_object_type: "roa".to_string(),
|
source_object_type: VcirSourceObjectType::Roa,
|
||||||
source_object_hash: sha256_hex(b"cached-roa"),
|
source_object_hash: sha256_32(b"cached-roa"),
|
||||||
source_ee_cert_hash: sha256_hex(b"cached-ee"),
|
source_ee_cert_hash: sha256_32(b"cached-ee"),
|
||||||
payload_json: "{\"asn\":64500}".to_string(),
|
payload: VcirLocalOutputPayload::Vrp {
|
||||||
rule_hash: sha256_hex(b"cached-rule"),
|
asn: 64500,
|
||||||
validation_path_hint: vec![pack.manifest_rsync_uri.clone()],
|
afi: RoaAfi::Ipv4,
|
||||||
|
prefix_len: 24,
|
||||||
|
addr: ipv4_addr([203, 0, 113, 0]),
|
||||||
|
max_length: 24,
|
||||||
|
},
|
||||||
|
rule_hash: sha256_32(b"cached-rule"),
|
||||||
}];
|
}];
|
||||||
let outputs = build_vcir_local_outputs(
|
let outputs = build_vcir_local_outputs(
|
||||||
&ca,
|
&ca,
|
||||||
@ -801,7 +825,7 @@ fn persist_vcir_non_repository_evidence_stores_current_ca_cert_only() {
|
|||||||
.expect("first local output");
|
.expect("first local output");
|
||||||
assert!(
|
assert!(
|
||||||
store
|
store
|
||||||
.get_raw_by_hash_entry(&first_output.source_ee_cert_hash)
|
.get_raw_by_hash_entry(&first_output.source_ee_cert_hash_hex())
|
||||||
.expect("load source ee raw")
|
.expect("load source ee raw")
|
||||||
.is_none()
|
.is_none()
|
||||||
);
|
);
|
||||||
@ -838,8 +862,11 @@ fn build_router_key_local_outputs_encodes_router_key_payloads() {
|
|||||||
);
|
);
|
||||||
assert_eq!(outputs.len(), 1);
|
assert_eq!(outputs.len(), 1);
|
||||||
assert_eq!(outputs[0].output_type, VcirOutputType::RouterKey);
|
assert_eq!(outputs[0].output_type, VcirOutputType::RouterKey);
|
||||||
assert_eq!(outputs[0].source_object_type, "router_key");
|
assert_eq!(
|
||||||
assert!(outputs[0].payload_json.contains("spki_der_base64"));
|
outputs[0].source_object_type,
|
||||||
|
VcirSourceObjectType::RouterKey
|
||||||
|
);
|
||||||
|
assert!(outputs[0].payload_json().contains("spki_der_base64"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -996,7 +1023,7 @@ fn finalize_fresh_publication_point_releases_local_outputs_cache_after_persist()
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn persist_vcir_for_fresh_result_stores_vcir_and_audit_indexes_for_real_snapshot() {
|
fn persist_vcir_for_fresh_result_stores_vcir_and_replay_meta_for_real_snapshot() {
|
||||||
let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
|
let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
|
||||||
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
|
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
|
||||||
let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer(
|
let objects = crate::validation::objects::process_publication_point_snapshot_for_issuer(
|
||||||
@ -1029,8 +1056,18 @@ fn persist_vcir_for_fresh_result_stores_vcir_and_audit_indexes_for_real_snapshot
|
|||||||
rrdp_notification_uri: None,
|
rrdp_notification_uri: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
persist_vcir_for_fresh_result(&store, &ca, &pack, &objects, &[], &[], &[], validation_time)
|
let mut objects = objects;
|
||||||
.expect("persist vcir for fresh result");
|
persist_vcir_for_fresh_result(
|
||||||
|
&store,
|
||||||
|
&ca,
|
||||||
|
&pack,
|
||||||
|
&mut objects,
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
validation_time,
|
||||||
|
)
|
||||||
|
.expect("persist vcir for fresh result");
|
||||||
|
|
||||||
let vcir = store
|
let vcir = store
|
||||||
.get_vcir(&pack.manifest_rsync_uri)
|
.get_vcir(&pack.manifest_rsync_uri)
|
||||||
@ -1054,12 +1091,15 @@ fn persist_vcir_for_fresh_result_stores_vcir_and_audit_indexes_for_real_snapshot
|
|||||||
vcir.ccr_manifest_projection.manifest_size,
|
vcir.ccr_manifest_projection.manifest_size,
|
||||||
pack.manifest_bytes.len() as u64
|
pack.manifest_bytes.len() as u64
|
||||||
);
|
);
|
||||||
let first_output = vcir.local_outputs.first().expect("local outputs stored");
|
assert!(vcir.local_outputs.first().is_some(), "local outputs stored");
|
||||||
assert!(
|
let replay_meta = store
|
||||||
store
|
.get_manifest_replay_meta(&pack.manifest_rsync_uri)
|
||||||
.get_audit_rule_index_entry(crate::storage::AuditRuleKind::Roa, &first_output.rule_hash)
|
.expect("get replay meta")
|
||||||
.expect("get audit rule index entry")
|
.expect("replay meta exists");
|
||||||
.is_some()
|
assert_eq!(replay_meta.manifest_rsync_uri, pack.manifest_rsync_uri);
|
||||||
|
assert_eq!(
|
||||||
|
replay_meta.manifest_sha256,
|
||||||
|
sha2::Sha256::digest(&pack.manifest_bytes).to_vec()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1531,26 +1571,16 @@ fn runner_offline_rsync_fixture_produces_pack_and_warnings() {
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|output| output.output_type == crate::storage::VcirOutputType::Vrp)
|
.find(|output| output.output_type == crate::storage::VcirOutputType::Vrp)
|
||||||
.expect("first VCIR VRP output");
|
.expect("first VCIR VRP output");
|
||||||
let audit_rule = store
|
assert!(!first_vrp.rule_hash_hex().is_empty());
|
||||||
.get_audit_rule_index_entry(crate::storage::AuditRuleKind::Roa, &first_vrp.rule_hash)
|
assert!(!first_vrp.output_id().is_empty());
|
||||||
.expect("get audit rule index")
|
let replay_meta = store
|
||||||
.expect("audit rule index exists");
|
.get_manifest_replay_meta(&manifest_rsync_uri)
|
||||||
assert_eq!(audit_rule.manifest_rsync_uri, manifest_rsync_uri);
|
.expect("get replay meta")
|
||||||
assert_eq!(audit_rule.output_id, first_vrp.output_id);
|
.expect("replay meta exists");
|
||||||
let trace = crate::audit_trace::trace_rule_to_root(
|
|
||||||
&store,
|
|
||||||
crate::storage::AuditRuleKind::Roa,
|
|
||||||
&first_vrp.rule_hash,
|
|
||||||
)
|
|
||||||
.expect("trace rule")
|
|
||||||
.expect("trace exists");
|
|
||||||
assert_eq!(trace.chain_leaf_to_root.len(), 1);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
trace.chain_leaf_to_root[0].manifest_rsync_uri,
|
replay_meta.manifest_rsync_uri,
|
||||||
manifest_rsync_uri
|
manifest_rsync_uri
|
||||||
);
|
);
|
||||||
assert!(trace.source_object_raw.raw_present);
|
|
||||||
assert!(trace.source_ee_cert_raw.raw_present);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -2791,10 +2821,11 @@ fn fresh_and_reuse_paths_produce_equivalent_ccr_manifest_projection() {
|
|||||||
let child_discovery =
|
let child_discovery =
|
||||||
discover_children_from_fresh_snapshot_with_audit(&ca, &pack, validation_time, None)
|
discover_children_from_fresh_snapshot_with_audit(&ca, &pack, validation_time, None)
|
||||||
.expect("discover children");
|
.expect("discover children");
|
||||||
|
let mut objects = empty_objects_output();
|
||||||
let fresh_vcir = build_vcir_from_fresh_result(
|
let fresh_vcir = build_vcir_from_fresh_result(
|
||||||
&ca,
|
&ca,
|
||||||
&pack,
|
&pack,
|
||||||
&empty_objects_output(),
|
&mut objects,
|
||||||
&[],
|
&[],
|
||||||
&child_discovery.audits,
|
&child_discovery.audits,
|
||||||
&child_discovery.children,
|
&child_discovery.children,
|
||||||
@ -2913,70 +2944,77 @@ fn build_objects_output_from_vcir_tracks_expired_and_invalid_cached_outputs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vcir.local_outputs.push(VcirLocalOutput {
|
vcir.local_outputs.push(VcirLocalOutput {
|
||||||
output_id: sha256_hex(b"bad-time"),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
output_type: VcirOutputType::Vrp,
|
||||||
item_effective_until: PackTime {
|
item_effective_until: PackTime {
|
||||||
rfc3339_utc: "bad-time-value".to_string(),
|
rfc3339_utc: "bad-time-value".to_string(),
|
||||||
},
|
},
|
||||||
source_object_uri: bad_time_uri.clone(),
|
source_object_uri: bad_time_uri.clone(),
|
||||||
source_object_type: "roa".to_string(),
|
source_object_type: VcirSourceObjectType::Roa,
|
||||||
source_object_hash: sha256_hex(b"bad-time-src"),
|
source_object_hash: sha256_32(b"bad-time-src"),
|
||||||
source_ee_cert_hash: sha256_hex(b"bad-time-ee"),
|
source_ee_cert_hash: sha256_32(b"bad-time-ee"),
|
||||||
payload_json:
|
payload: VcirLocalOutputPayload::Vrp {
|
||||||
serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24})
|
asn: 64496,
|
||||||
.to_string(),
|
afi: RoaAfi::Ipv4,
|
||||||
rule_hash: sha256_hex(b"bad-time-rule"),
|
prefix_len: 24,
|
||||||
validation_path_hint: vec![vcir.manifest_rsync_uri.clone(), bad_time_uri.clone()],
|
addr: ipv4_addr([203, 0, 113, 0]),
|
||||||
|
max_length: 24,
|
||||||
|
},
|
||||||
|
rule_hash: sha256_32(b"bad-time-rule"),
|
||||||
});
|
});
|
||||||
vcir.local_outputs.push(VcirLocalOutput {
|
vcir.local_outputs.push(VcirLocalOutput {
|
||||||
output_id: sha256_hex(b"expired"),
|
|
||||||
output_type: VcirOutputType::Aspa,
|
output_type: VcirOutputType::Aspa,
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(now - time::Duration::minutes(1)),
|
item_effective_until: PackTime::from_utc_offset_datetime(now - time::Duration::minutes(1)),
|
||||||
source_object_uri: expired_uri.clone(),
|
source_object_uri: expired_uri.clone(),
|
||||||
source_object_type: "aspa".to_string(),
|
source_object_type: VcirSourceObjectType::Aspa,
|
||||||
source_object_hash: sha256_hex(b"expired-src"),
|
source_object_hash: sha256_32(b"expired-src"),
|
||||||
source_ee_cert_hash: sha256_hex(b"expired-ee"),
|
source_ee_cert_hash: sha256_32(b"expired-ee"),
|
||||||
payload_json: serde_json::json!({"customer_as_id": 64500, "provider_as_ids": [64501]})
|
payload: VcirLocalOutputPayload::Aspa {
|
||||||
.to_string(),
|
customer_as_id: 64500,
|
||||||
rule_hash: sha256_hex(b"expired-rule"),
|
provider_as_ids: vec![64501],
|
||||||
validation_path_hint: vec![vcir.manifest_rsync_uri.clone(), expired_uri.clone()],
|
},
|
||||||
|
rule_hash: sha256_32(b"expired-rule"),
|
||||||
});
|
});
|
||||||
vcir.local_outputs.push(VcirLocalOutput {
|
vcir.local_outputs.push(VcirLocalOutput {
|
||||||
output_id: sha256_hex(b"bad-json"),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
output_type: VcirOutputType::Vrp,
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(5)),
|
item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(5)),
|
||||||
source_object_uri: bad_json_uri.clone(),
|
source_object_uri: bad_json_uri.clone(),
|
||||||
source_object_type: "roa".to_string(),
|
source_object_type: VcirSourceObjectType::Roa,
|
||||||
source_object_hash: sha256_hex(b"bad-json-src"),
|
source_object_hash: sha256_32(b"bad-json-src"),
|
||||||
source_ee_cert_hash: sha256_hex(b"bad-json-ee"),
|
source_ee_cert_hash: sha256_32(b"bad-json-ee"),
|
||||||
payload_json: "{not-json".to_string(),
|
payload: VcirLocalOutputPayload::Aspa {
|
||||||
rule_hash: sha256_hex(b"bad-json-rule"),
|
customer_as_id: 64510,
|
||||||
validation_path_hint: vec![vcir.manifest_rsync_uri.clone(), bad_json_uri.clone()],
|
provider_as_ids: vec![64511],
|
||||||
|
},
|
||||||
|
rule_hash: sha256_32(b"bad-json-rule"),
|
||||||
});
|
});
|
||||||
vcir.local_outputs.push(VcirLocalOutput {
|
vcir.local_outputs.push(VcirLocalOutput {
|
||||||
output_id: sha256_hex(b"bad-prefix"),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
output_type: VcirOutputType::Vrp,
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(5)),
|
item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(5)),
|
||||||
source_object_uri: bad_prefix_uri.clone(),
|
source_object_uri: bad_prefix_uri.clone(),
|
||||||
source_object_type: "roa".to_string(),
|
source_object_type: VcirSourceObjectType::Roa,
|
||||||
source_object_hash: sha256_hex(b"bad-prefix-src"),
|
source_object_hash: sha256_32(b"bad-prefix-src"),
|
||||||
source_ee_cert_hash: sha256_hex(b"bad-prefix-ee"),
|
source_ee_cert_hash: sha256_32(b"bad-prefix-ee"),
|
||||||
payload_json: serde_json::json!({"asn": 64510, "prefix": "203.0.113.0", "max_length": 24})
|
payload: VcirLocalOutputPayload::Aspa {
|
||||||
.to_string(),
|
customer_as_id: 64512,
|
||||||
rule_hash: sha256_hex(b"bad-prefix-rule"),
|
provider_as_ids: vec![64513],
|
||||||
validation_path_hint: vec![vcir.manifest_rsync_uri.clone(), bad_prefix_uri.clone()],
|
},
|
||||||
|
rule_hash: sha256_32(b"bad-prefix-rule"),
|
||||||
});
|
});
|
||||||
vcir.local_outputs.push(VcirLocalOutput {
|
vcir.local_outputs.push(VcirLocalOutput {
|
||||||
output_id: sha256_hex(b"bad-aspa"),
|
|
||||||
output_type: VcirOutputType::Aspa,
|
output_type: VcirOutputType::Aspa,
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(5)),
|
item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(5)),
|
||||||
source_object_uri: bad_aspa_uri.clone(),
|
source_object_uri: bad_aspa_uri.clone(),
|
||||||
source_object_type: "aspa".to_string(),
|
source_object_type: VcirSourceObjectType::Aspa,
|
||||||
source_object_hash: sha256_hex(b"bad-aspa-src"),
|
source_object_hash: sha256_32(b"bad-aspa-src"),
|
||||||
source_ee_cert_hash: sha256_hex(b"bad-aspa-ee"),
|
source_ee_cert_hash: sha256_32(b"bad-aspa-ee"),
|
||||||
payload_json: serde_json::json!({"customer_as_id": 64520}).to_string(),
|
payload: VcirLocalOutputPayload::Vrp {
|
||||||
rule_hash: sha256_hex(b"bad-aspa-rule"),
|
asn: 64520,
|
||||||
validation_path_hint: vec![vcir.manifest_rsync_uri.clone(), bad_aspa_uri.clone()],
|
afi: RoaAfi::Ipv4,
|
||||||
|
prefix_len: 24,
|
||||||
|
addr: ipv4_addr([198, 51, 100, 0]),
|
||||||
|
max_length: 24,
|
||||||
|
},
|
||||||
|
rule_hash: sha256_32(b"bad-aspa-rule"),
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
|
|||||||
@ -596,7 +596,7 @@ fn process_snapshot_for_issuer_populates_local_outputs_cache_from_real_cernet_fi
|
|||||||
assert!(
|
assert!(
|
||||||
out.local_outputs_cache
|
out.local_outputs_cache
|
||||||
.iter()
|
.iter()
|
||||||
.all(|entry| entry.source_object_type == "roa")
|
.all(|entry| entry.source_object_type == rpki::storage::VcirSourceObjectType::Roa)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user