Establish pre-production record disposition and add the migration tool
AUDIT-WP-0005-T04. Disposition: there are no pre-production records. No audit-core SQLite store on this host, no mock-file-backend output, and no audit-core pod, deployment or PVC on railiance01 - the only audit-* PVC there is OpenBao's own audit device. Consistent with the history: WP-0003-T03 was cancelled before the receiver was ever deployed, so every SQLite store that has existed was a test fixture. Nothing is being discarded because nothing was ever accepted outside tests. The tool is built anyway because the SQLite path stays reachable - the entrypoint falls back to it when AUDIT_CORE_DATABASE_URL is unset. If that fallback is ever used in anger the records are audit records, and writing the migration afterwards under pressure is the wrong time. audit_core.migrate_store and `python -m audit_core migrate-store` transfer events, dead letters and secret-finding counters. Records keep their original event_id, payload_hash and accepted_at, which is why this bypasses accept(): that stamps acceptance with the current time, and a migration that rewrote acceptance times would destroy the evidence it exists to preserve. Idempotent, and verification reads back from the destination rather than trusting the write path. A destination record with a differing payload hash is reported as a conflict and left untouched - silently overwriting a stored audit record is the same class of failure as losing it. Conflicts and failed verification exit non-zero; a partial migration is not a success. Tests 71 -> 77. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
bd274f6269
commit
88d16847ff
5 changed files with 455 additions and 2 deletions
|
|
@ -69,9 +69,53 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
cleanup_parser.add_argument("--retention-days", type=int, default=None)
|
||||
cleanup_parser.set_defaults(func=cleanup)
|
||||
|
||||
migrate_parser = sub.add_parser(
|
||||
"migrate-store", help="Move audit records from a SQLite store into PostgreSQL."
|
||||
)
|
||||
migrate_parser.add_argument("--from-sqlite", required=True, dest="source")
|
||||
migrate_parser.add_argument(
|
||||
"--to-postgres", dest="destination", default=None,
|
||||
help="DSN; defaults to AUDIT_CORE_DATABASE_URL",
|
||||
)
|
||||
migrate_parser.add_argument("--schema", default="audit_core")
|
||||
migrate_parser.add_argument(
|
||||
"--dry-run", action="store_true",
|
||||
help="Report what would move without writing anything.",
|
||||
)
|
||||
migrate_parser.set_defaults(func=migrate_store)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def migrate_store(args: argparse.Namespace) -> int:
|
||||
import os
|
||||
|
||||
from audit_core.migrate_store import migrate
|
||||
|
||||
if args.dry_run:
|
||||
import sqlite3
|
||||
|
||||
with sqlite3.connect(f"file:{args.source}?mode=ro", uri=True) as source:
|
||||
count = source.execute("SELECT count(*) FROM events").fetchone()[0]
|
||||
print(json.dumps({"dry_run": True, "events_in_source": count}, sort_keys=True))
|
||||
return 0
|
||||
|
||||
from audit_core.postgres_backend import PostgresAuditBackend
|
||||
|
||||
dsn = args.destination or os.environ.get("AUDIT_CORE_DATABASE_URL")
|
||||
if not dsn:
|
||||
raise SystemExit("--to-postgres or AUDIT_CORE_DATABASE_URL is required")
|
||||
|
||||
backend = PostgresAuditBackend(dsn, schema=args.schema)
|
||||
try:
|
||||
report = migrate(args.source, backend)
|
||||
finally:
|
||||
backend.close()
|
||||
print(report.summary())
|
||||
# A conflict or a failed verification is not a partial success.
|
||||
return 0 if report.ok else 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue