"""One-shot, offline Windows logical import into a freshly provisioned container. The caller passes its executing container_runtime module, after container and default-environment preflight and installation of a flag-only SIGTERM handler. SIGINT is temporarily made flag-only as well, including the child launch window. Dispatch action import-snapshot, with required --manifest-sha256 HEX, to import_snapshot(sys.modules[__name__], args.manifest_sha256) BEFORE initialize(). Only /import/{manifest.json,files.tar,database.dump} is read. Nothing is resumed, repaired, removed, or started under a supervisor. A failed import needs review. CLI contracts: postgres-runtime initialize-empty/maintenance-start/maintenance-stop --config PATH; migrate-runtime-safety --config PATH --apply --sources-stopped (never --initialize-base). If needed, the separate, fenced recovery action adds --recover-stale-result-pipeline --max-rows 10000 --max-seconds 3600. READY CLI handoffs are intentional: each CLI takes its own ClusterAuthorityLock. initialize.lock remains held across those handoffs. Lifecycle children are NEVER killed on a deadline/cancellation; uncertain stop retains ownership and retries. Phases: 1 preflight, 2 archive verification, 3 extraction, 4 config, 5 initdb, 6 maintenance start, 7 raw equality, 8 Postman, 9 recovery, 10 migration, 11 final checks, 12 confirmed stop, 13 publication. Codes: 1 rejected/failed, 2 review required (count only), 124 timeout, 130 cancellation. Helpers' diagnostic text is suppressed, including migration SQL diagnostics via temporary defaults on the new Linux role (reset after successful final verification; left suppressed on a failed, stopped, unmarked import). Evidence is exclusive, private, fsynced, and never reused: config/windows-import-manifest.json, windows-import-raw.json, and windows-import-report.json. initialized.json is the LAST publication, contains the Linux system identifier, runtime FORMAT, and manifest_sha256. Safe diagnostics precede cleanup: import-snapshot-diagnostic followed by numeric phase, stage, code, review_count, type_id, importer line (0 if unavailable), attempt. Stages: 1 operation failure, 2 backend construction, 3 stop call, 4 stop result, 5 final probe call, 6 final probe state, 7 backend close, 8 stop CLI, 9 child wait. Stages 5/6 mean the stop result already confirmed completed AND stopped. Type IDs: 0 other, 1 Failure, 2 KeyboardInterrupt, 3 OSError, 4 ValueError, 5 TypeError, 6 RuntimeError (including subclasses, never their names or text). After mutations, the first operation failure and first HOLD are also attempted once as private, exclusive, fsynced config/windows-import-failure.json and windows-import-hold.json. They are not completion proofs and are never replaced. Later HOLD stages remain numeric events; failed diagnostic I/O cannot release ownership or replace the original failure. Ordinary progress lines are unchanged. database.database_bytes supplies the actual physical source size. Older v1 snapshots without it use max(24 GiB, dump * 4), rejecting estimates over 1 TiB, plus all file bytes and a 20 GiB free reserve. This does not measure physical free space on the Windows host's S: drive. Integration still needs the image's native PG16/psycopg/PyYAML and real mount tests. Use /data/config/windows-import.yaml for subsequent container commands; this module neither changes the default profile nor starts the supervisor. database.sequence_states is the exporter's optional {schema: {sequence: {last_value: int, is_called: bool}}} evidence. Partitioned/foreign/non-public tables are refused because v1 cannot prove their complete row inventory. Ordinary initialize-empty failure relies on that CLI's confirmed temporary-child cleanup; abnormal child exit instead enters maintenance-stop's indefinite HOLD. Do not force-kill a retained importer to bypass unconfirmed stop. """ import contextlib import hashlib import json import logging import ntpath import os from pathlib import Path import re import shutil import signal import stat import subprocess import sys import tarfile import time import unicodedata from urllib.parse import unquote, urlsplit IMPORT = Path('/import') BLOCK = 1024 * 1024 GIB = 1024 ** 3 MAX_MANIFEST = 32 * BLOCK MAX_CONFIG = 4 * BLOCK MAX_ESTIMATE = 1024 * GIB COUNT_TIMEOUT = 3600 RESTORE_TIMEOUT = 12 * 3600 MIGRATE_TIMEOUT = 6 * 3600 LIFECYCLE_TIMEOUT = 600 SNAPSHOT_FORMAT = 'truf-windows-snapshot-v1' ACTIVE = {'results', 'queues', 'state', 'keychecks', 'postman_cache', 'result_spool'} REQUIRED_FILES = { 'windows-archive/app/config.yaml', 'config/secrets.yaml', 'config/trufflehog-custom-detectors.yaml', 'runtime-linux/proxy.txt', } PLACEHOLDERS = {'runtime-linux/proxy.txt': b'', 'config/secrets.yaml': b'{}\n'} FENCES = ( 'lease_owner', 'lease_token', 'claim_batch', 'leased_at', 'lease_expires_at', 'current_result_reservation_id', 'claim_event_id', 'resolver_token', ) QUIET_PG_SETTINGS = ( ('log_min_error_statement', 'panic'), ('log_min_messages', 'panic'), ('log_statement', 'none'), ('log_min_duration_statement', '-1'), ) # Fixed evidence only, not a general backup/repair facility. Recovery may append # history and advance its own leases, but cannot rewrite these existing facts. PRESERVED = { 'pipeline_quarantine': ('id', None), 'projection_streams': ('stream_name', None), 'projection_cursors': ('stream_name', None), 'projection_appends': ('id', None), 'projection_append_audit': ('id', None), 'projection_rotations': ('id', None), 'target_scans': ('id', ( 'id', 'scan_event_id', 'scan_event_hash', 'queue_id', 'claim_lease_token', 'target', 'normalized_target', 'result_reservation_id', )), 'result_reservations': ('id', ( 'id', 'reservation_token', 'bundle_id', 'scan_event_id', 'queue_id', 'ready_relative_path', 'producer_instance_id', 'producer_pid', 'producer_creation_time', 'producer_executable', )), 'result_bundles': ('reservation_id', ( 'reservation_id', 'bundle_id', 'scan_event_id', 'scan_event_hash', 'relative_path', 'actual_bytes', )), 'worker_progress_events': ('id', None), 'worker_diagnostics': ('id', None), 'runtime_operations': ('operation_id', None), 'runtime_operations_control': ('id', None), 'runtime_audit_events': ('id', None), } class Failure(Exception): def __init__(self, code=1, count=0, *, uncertain=False): self.code, self.count, self.uncertain = code, count, uncertain super().__init__(code) def _diagnostic(progress, stage, exc, attempt=0): # Only local categories and importer line numbers cross the quiet boundary. try: code, count = (exc.code, exc.count) if isinstance(exc, Failure) else ( 130 if isinstance(exc, KeyboardInterrupt) else 1, 0) if type(code) is not int or code not in (1, 2, 124, 130): code = 1 if type(count) is not int or not 0 <= count <= 2 ** 63 - 1: count = 0 type_id = next((index for index, kind in enumerate( (Failure, KeyboardInterrupt, OSError, ValueError, TypeError, RuntimeError), 1) if isinstance(exc, kind)), 0) line, traceback = 0, exc.__traceback__ while traceback is not None: if traceback.tb_frame.f_code.co_filename == __file__: line = traceback.tb_lineno traceback = traceback.tb_next progress(12, attempt, 0, cleanup=True, diagnostic=(stage, code, count, type_id, line)) except BaseException: pass def _checkpoint(runtime): if runtime._shutdown_requested: raise Failure(130) @contextlib.contextmanager def _quiet(): previous = logging.root.manager.disable with open(os.devnull, 'w', encoding='ascii') as sink: try: logging.disable(sys.maxsize) with contextlib.redirect_stdout(sink), contextlib.redirect_stderr(sink): yield finally: logging.disable(previous) def _integer(value, minimum=0, maximum=2 ** 63 - 1): if type(value) is not int or not minimum <= value <= maximum: raise Failure() return value def _sha(value): if not isinstance(value, str) or not re.fullmatch(r'[0-9a-f]{64}', value): raise Failure() return value def _object(pairs): result = {} for key, value in pairs: if key in result: raise Failure() result[key] = value return result def _json(payload): def invalid(_value): raise Failure() return json.loads(payload, object_pairs_hook=_object, parse_constant=invalid) def _relative(name): if (not isinstance(name, str) or not name or len(name.encode('utf-8')) > 4096 or unicodedata.normalize('NFC', name) != name or '\\' in name or ':' in name or any(ord(c) < 32 or ord(c) == 127 for c in name)): raise Failure() parts = name.split('/') if (len(parts) > 64 or any(not p or p in ('.', '..') or p.endswith((' ', '.')) or len(p.encode('utf-8')) > 255 for p in parts)): raise Failure() return parts def _destination(name): parts = _relative(name) archived = parts[0] == 'windows-archive' and len(parts) > 1 allowed = (archived or name in REQUIRED_FILES or len(parts) > 2 and parts[0] == 'runtime-linux' and parts[1] in ACTIVE or len(parts) > 2 and parts[0] == 'scanner-result-bundles' and parts[1] in {'tmp', 'ready', 'quarantine'}) if not allowed: raise Failure() folded = [p.casefold() for p in parts] if any('.lock' in p or p.endswith('.pid') for p in folded): raise Failure() if not archived and any( p in {'control', 'postgres', 'postgres-linux', 'postgres-data', 'pg_wal', 'pg_xlog', 'pg_version', 'postmaster.opts', 'cluster_identity.json', 'supervisor.instance.json', 'janitor.cursor.json'} or p.startswith('scan_limiter') for p in folded ): raise Failure() return parts def _manifest(payload, expected): if (not isinstance(expected, str) or not re.fullmatch(r'[0-9A-Fa-f]{64}', expected) or not 0 < len(payload) <= MAX_MANIFEST or hashlib.sha256(payload).hexdigest() != expected.lower()): raise Failure() value = _json(payload) if not isinstance(value, dict) or value.get('format') != SNAPSHOT_FORMAT: raise Failure() source, database, archive = value['source'], value['database'], value['archive'] if (source.get('supervisor_stopped') is not True or source.get('postgres_stopped') is not True or ntpath.normcase(source.get('root', '')) != ntpath.normcase(r'D:\truf') or ntpath.normcase(source.get('postgres_data_dir', '')) != ntpath.normcase(r'S:\postgres-data') or ntpath.normcase(database.get('data_directory', '')) != ntpath.normcase(r'S:\postgres-data') or _integer(database['version_num']) // 10000 != 16 or not re.fullmatch(r'[1-9][0-9]{0,19}', database['system_identifier'])): raise Failure() _integer(database['port'], 1, 65535) for name in (database['database_name'], database['user_name']): _identifier(name) for item in (archive, database): _sha(item['sha256']) _integer(item['bytes'], 6, 8 * MAX_ESTIMATE) tables = database['table_counts'] if not isinstance(tables, dict) or not tables: raise Failure() for name, count in tables.items(): _identifier(name) _integer(count) sequences = database.get('sequence_states') sequence_count = 0 if 'sequence_states' in database: if not isinstance(sequences, dict): raise Failure() for schema, states in sequences.items(): _identifier(schema) if schema == 'information_schema' or schema.startswith('pg_') or not isinstance(states, dict): raise Failure() for name, state in states.items(): _identifier(name) if (not isinstance(state, dict) or set(state) != {'last_value', 'is_called'} or type(state['is_called']) is not bool): raise Failure() _integer(state['last_value'], -(2 ** 63)) sequence_count += 1 if 'sequence_count' in database and _integer(database['sequence_count']) != sequence_count: raise Failure() if 'database_bytes' in database: _integer(database['database_bytes'], 1, MAX_ESTIMATE) files, names, parents = {}, {}, {} if not isinstance(value['files'], list): raise Failure() for item in value['files']: if not isinstance(item, dict) or set(item) != {'path', 'size', 'sha256'}: raise Failure() name = item['path'] parts = _destination(name) folded = name.casefold() if folded in names or folded in parents: raise Failure() for index in range(1, len(parts)): parent = '/'.join(parts[:index]) key = parent.casefold() if key in names or key in parents and parents[key] != parent: raise Failure() parents[key] = parent names[folded] = name files[name] = {'size': _integer(item['size'], maximum=8 * MAX_ESTIMATE), 'sha256': _sha(item['sha256'])} if not REQUIRED_FILES.issubset(files): raise Failure() file_bytes = sum(item['size'] for item in files.values()) if file_bytes > archive['bytes'] or files['windows-archive/app/config.yaml']['size'] > MAX_CONFIG: raise Failure() if 'counts' in value: counts = {'files': len(files), 'file_bytes': file_bytes, 'active_files': sum(not n.startswith('windows-archive/') for n in files), 'archival_files': sum(n.startswith('windows-archive/') for n in files), 'public_tables': len(tables)} if sequences is not None: counts['sequences'] = sequence_count if any(_integer(value['counts'][k]) != v for k, v in counts.items()): raise Failure() return value, files def _fingerprint(info): return (info.st_dev, info.st_ino, info.st_mode, info.st_nlink, info.st_size, info.st_mtime_ns, info.st_ctime_ns) def _chain(path): for parent in (*reversed(path.parents), path): info = parent.lstat() if stat.S_ISLNK(info.st_mode) or getattr(info, 'st_file_attributes', 0) & 0x400: raise Failure() def _regular(path, runtime=None): _chain(path) if runtime is not None: runtime.private_path(path) info = path.lstat() if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1: raise Failure() return _fingerprint(info) @contextlib.contextmanager def _input(path, runtime=None): before = _regular(path, runtime) descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | getattr(os, 'O_BINARY', 0)) with os.fdopen(descriptor, 'rb', buffering=0) as handle: _unchanged(path, handle, before, runtime) yield handle, before _unchanged(path, handle, before, runtime) def _unchanged(path, handle, before, runtime=None): if _fingerprint(os.fstat(handle.fileno())) != before or _regular(path, runtime) != before: raise Failure() def _mounts(runtime): _chain(IMPORT) if not stat.S_ISDIR(IMPORT.lstat().st_mode): raise Failure() with open('/proc/self/mountinfo', 'rb') as handle: payload = handle.read(BLOCK + 1) if len(payload) > BLOCK: raise Failure() mounts = {} for line in payload.splitlines(): fields = line.split() if len(fields) < 10 or b'-' not in fields: raise Failure() # Mountinfo escapes cannot disguise an exact fixed mount or a submount. target = re.sub(rb'\\([0-7]{3})', lambda m: bytes([int(m[1], 8)]), fields[4]) if target in mounts: raise Failure() mounts[target] = fields if target.startswith((b'/import/', b'/data/')): raise Failure() incoming, destination = mounts.get(b'/import'), mounts.get(b'/data') if (incoming is None or destination is None or b'ro' not in incoming[5].split(b',') or incoming[0] == destination[0] or incoming[2:4] == destination[2:4] or IMPORT.stat().st_dev == runtime.DATA.stat().st_dev or not os.statvfs(IMPORT).f_flag & os.ST_RDONLY): raise Failure() if set(os.listdir(IMPORT)) != {'manifest.json', 'files.tar', 'database.dump'}: raise Failure() def _placeholder(runtime, name, before=None): path = runtime.DATA / name with _input(path, runtime) as (handle, fingerprint): expected = PLACEHOLDERS[name] if handle.read(len(expected) + 1) != expected or before is not None and fingerprint != before: raise Failure() return fingerprint def _fresh(runtime, files=()): allowed_dirs = set(runtime.DIRECTORIES) allowed_files = {'.provisioned.json', '.provision.lock', 'initialize.lock', 'postgres-password', *PLACEHOLDERS} seen_dirs, seen_files = set(), set() def visit(path): runtime.private_path(path, directory=True) if path.stat().st_dev != runtime.DATA.stat().st_dev: raise Failure() with os.scandir(path) as entries: for entry in entries: child = path / entry.name name = child.relative_to(runtime.DATA).as_posix() if entry.is_dir(follow_symlinks=False): if name not in allowed_dirs: raise Failure() seen_dirs.add(name) visit(child) else: if name not in allowed_files: raise Failure() _regular(child, runtime) seen_files.add(name) visit(runtime.DATA) if seen_dirs != allowed_dirs or seen_files != allowed_files: raise Failure() existing = {name.casefold(): name for name in seen_dirs | seen_files} for name in files: if name.casefold() in existing and name not in PLACEHOLDERS: raise Failure() parts = name.split('/') for index in range(1, len(parts)): parent = '/'.join(parts[:index]) found = existing.get(parent.casefold()) if found is not None and (found != parent or found not in seen_dirs): raise Failure() marker = runtime._read_json(runtime.PROVISIONED) if marker != {'format': runtime.FORMAT, 'uid': 10001, 'gid': 10001}: raise Failure() for name in ('.provision.lock', 'initialize.lock'): if (runtime.DATA / name).stat().st_size != 0: raise Failure() return {name: _placeholder(runtime, name) for name in PLACEHOLDERS} def _space(runtime, manifest): database = manifest['database'] estimate = database.get('database_bytes', max(24 * GIB, database['bytes'] * 4)) _integer(estimate, 1, MAX_ESTIMATE) needed = sum(item['size'] for item in manifest['files']) + estimate + 20 * GIB free = shutil.disk_usage(runtime.DATA).free if free < needed: raise Failure() return {'database_estimate_bytes': estimate, 'required_free_bytes': needed, 'available_free_bytes': free, 'database_estimate_from_metadata': 'database_bytes' in database} def _fsync_dir(path): descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) try: os.fsync(descriptor) finally: os.close(descriptor) @contextlib.contextmanager def _new_file(runtime, path): runtime.private_path(path.parent, directory=True) descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | getattr(os, 'O_BINARY', 0), 0o600) with os.fdopen(descriptor, 'wb') as handle: _regular(path, runtime) if not os.path.samestat(os.fstat(handle.fileno()), path.lstat()): raise Failure() yield handle handle.flush() os.fsync(handle.fileno()) _regular(path, runtime) _fsync_dir(path.parent) def _write(runtime, path, payload): with _new_file(runtime, path) as handle: if handle.write(payload) != len(payload): raise Failure() return hashlib.sha256(payload).hexdigest() def _encoded(value): return (json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(',', ':')) + '\n').encode('ascii') class _TarInfo(tarfile.TarInfo): def _proc_pax(self, archive): if self.type != tarfile.XHDTYPE or not 0 < self.size <= 65536: raise Failure() return super()._proc_pax(archive) def _proc_gnulong(self, archive): raise Failure() def _proc_sparse(self, archive): raise Failure() def _proc_gnusparse_00(self, *args): raise Failure() def _proc_gnusparse_01(self, *args): raise Failure() def _proc_gnusparse_10(self, *args): raise Failure() class _HashReader: def __init__(self, handle, size): self.handle, self.limit = handle, size self.digest, self.size = hashlib.sha256(), 0 def read(self, size): block = self.handle.read(min(size, BLOCK)) self.size += len(block) if self.size > self.limit: raise Failure() self.digest.update(block) return block def _archive(runtime, handle, metadata, files, progress, placeholders=None): handle.seek(0) reader = _HashReader(handle, metadata['bytes']) seen, size, copied = set(), 0, {} with tarfile.open(fileobj=reader, mode='r|', tarinfo=_TarInfo) as archive: for member in archive: _checkpoint(runtime) name = member.name _destination(name) if (not member.isreg() or member.type not in (tarfile.REGTYPE, tarfile.AREGTYPE) or member.linkname or member.sparse is not None or set(member.pax_headers) - {'path', 'size'} or name not in files or name in seen or member.size != files[name]['size']): raise Failure() seen.add(name) destination = runtime.DATA / name replacement = placeholders is not None and name in placeholders output = destination.with_name(destination.name + '.windows-import-partial') if replacement else destination if placeholders is not None: for parent in reversed(destination.parents): if parent == runtime.DATA or runtime.DATA in parent.parents: if not os.path.lexists(parent): runtime.private_path(parent.parent, directory=True) parent.mkdir(mode=0o700) _fsync_dir(parent.parent) runtime.private_path(parent, directory=True) context = _new_file(runtime, output) if placeholders is not None else contextlib.nullcontext(None) with archive.extractfile(member) as source, context as target: digest, received = hashlib.sha256(), 0 while True: _checkpoint(runtime) block = source.read(BLOCK) if not block: break received += len(block) digest.update(block) if target is not None and target.write(block) != len(block): raise Failure() if received != member.size or digest.hexdigest() != files[name]['sha256']: raise Failure() if replacement: _placeholder(runtime, name, placeholders[name]) os.replace(output, destination) _regular(destination, runtime) _fsync_dir(destination.parent) if placeholders is not None: copied[name] = _regular(destination, runtime) size += received if len(seen) % 128 == 0: progress(3 if placeholders is not None else 2, len(seen), size) trailer = archive.offset while reader.read(BLOCK): _checkpoint(runtime) if (seen != set(files) or reader.size != metadata['bytes'] or reader.digest.hexdigest() != metadata['sha256'] or not 1024 <= reader.size - trailer <= 2 * tarfile.RECORDSIZE): raise Failure() # tarfile stops at its first zero header. Do not accept a second hidden tar # or nonzero data in its buffered trailer, even if the whole hash was signed. handle.seek(trailer) if any(handle.read(2 * tarfile.RECORDSIZE + 1)): raise Failure() progress(3 if placeholders is not None else 2, len(seen), size) return copied def _hash_input(runtime, path, handle, before, metadata, private=False, dump=False): _unchanged(path, handle, before, runtime if private else None) handle.seek(0) digest, size, prefix = hashlib.sha256(), 0, b'' while True: _checkpoint(runtime) block = handle.read(BLOCK) if not block: break if not prefix: prefix = block[:5] size += len(block) if size > metadata.get('bytes', metadata.get('size')): raise Failure() digest.update(block) if (size != metadata.get('bytes', metadata.get('size')) or digest.hexdigest() != metadata['sha256'] or dump and prefix != b'PGDMP'): raise Failure() _unchanged(path, handle, before, runtime if private else None) handle.seek(0) def _configuration(runtime): import yaml from container_import_config import translate_windows_config values = [] for path in (runtime.DATA / 'windows-archive/app/config.yaml', runtime.DEFAULT_CONFIG): with _input(path, runtime) as (handle, _before): payload = handle.read(MAX_CONFIG + 1) if len(payload) > MAX_CONFIG: raise Failure() values.append(yaml.safe_load(payload)) translated, adjusted = translate_windows_config(*values) global_config = translated['global'] if global_config.get('database_url') or global_config.get('dashboard_db_url'): raise Failure() path = runtime.DATA / 'config/windows-import.yaml' config_hash = _write(runtime, path, yaml.safe_dump(translated, allow_unicode=False).encode('utf-8')) config = runtime.prepare_environment(path) expected = {key: str(runtime.DATA / 'runtime-linux' / folder) for key, folder in ( ('results_dir', 'results'), ('queue_dir', 'queues'), ('state_dir', 'state'), ('log_dir', 'logs'), ('keycheck_dir', 'keychecks'), ('postman_cache_dir', 'postman_cache'), ('result_spool_dir', 'result_spool'), ('legacy_result_spool_dir', 'result_spool'), ('scan_limiter_db', 'state/scan_limiter.db'), )} expected.update(proxy_file=str(runtime.DATA / 'runtime-linux/proxy.txt'), trufflehog_config=str(runtime.DATA / 'config/trufflehog-custom-detectors.yaml')) if any(config['global'].get(key) != value for key, value in expected.items()): raise Failure() return path, config, adjusted, config_hash def _credentials(runtime): with _input(runtime.PASSWORD, runtime) as (handle, _before): password = handle.read(130).decode('ascii').rstrip('\n') if not re.fullmatch(r'[A-Za-z0-9_-]{32,128}', password): raise Failure() dsn = os.environ['SCANNER_DB_URL'] parsed = urlsplit(dsn) if (parsed.scheme != 'postgresql' or parsed.hostname != '127.0.0.1' or parsed.port != 5432 or unquote(parsed.username or '') != 'truf' or unquote(parsed.path) != '/truf' or unquote(parsed.password or '') != password or parsed.query or parsed.fragment or any(os.environ.get(k) != dsn for k in ('DATABASE_URL', 'TRUF_MANAGED_POSTGRES_DSN'))): raise Failure() return dsn, password def _command(runtime, command, timeout, progress, *, env=None, stdin=None, lifecycle=False, stopping=False): if not stopping: _checkpoint(runtime) process = subprocess.Popen(command, stdin=subprocess.DEVNULL if stdin is None else stdin, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env, close_fds=True, start_new_session=True) exited, failure, held = False, None, 0 try: deadline = time.monotonic() + timeout while True: try: result = process.wait(timeout=1) exited = True break except subprocess.TimeoutExpired: pass except BaseException: failure = failure or Failure(130) if not stopping and runtime._shutdown_requested: failure = failure or Failure(130) if time.monotonic() >= deadline: failure = failure or Failure(124) if failure is not None: if lifecycle: # A CLI can be retaining a not-yet-bookkept postmaster. Its # own positive stop contract, not the parent, owns exit. held += 1 if held == 1 or held % 60 == 0: if held == 1 and not stopping: _diagnostic(progress, 1, failure) _diagnostic(progress, 9, failure, held) else: try: process.kill() except OSError: pass except BaseException as exc: failure = failure or Failure(130 if isinstance(exc, KeyboardInterrupt) else 1) _diagnostic(progress, 9 if stopping else 1, exc) finally: # Even a broken progress pipe or an interrupt outside wait() cannot # abandon a lifecycle child, nor a client with an open restore session. while not exited: if not lifecycle: try: process.kill() except BaseException: pass try: result = process.wait(timeout=1) exited = True except BaseException: pass if lifecycle and result < 0: raise Failure(failure.code if failure is not None else 1, uncertain=True) if failure is not None and not stopping: raise failure if result != 0: raise Failure() if not stopping: _checkpoint(runtime) def _identifier(name): if not isinstance(name, str) or not name or '\x00' in name or len(name.encode('utf-8')) > 63: raise Failure() return '"' + name.replace('"', '""') + '"' @contextlib.contextmanager def _connect(runtime, *, readonly=True): import psycopg from psycopg.rows import dict_row dsn, _password = _credentials(runtime) options = ( f'-c search_path=public -c statement_timeout={COUNT_TIMEOUT * 1000} ' '-c lock_timeout=10000 -c idle_in_transaction_session_timeout=3600000 ' '-c row_security=off -c log_min_error_statement=panic -c log_statement=none ' '-c log_min_messages=panic -c log_min_duration_statement=-1 ' '-c default_transaction_read_only=' + ('on' if readonly else 'off') ) connection = psycopg.connect(dsn, connect_timeout=10, row_factory=dict_row, options=options, application_name='truf-container-import', tcp_user_timeout=60000) try: yield connection finally: connection.close() def _identity(runtime, identity, source): if (identity['pg_major'] != 16 or identity['data_directory'] != str(runtime.DATA / 'postgres-linux') or identity['database'] != 'truf' or identity['user'] != 'truf' or identity['port'] != 5432 or not re.fullmatch(r'[1-9][0-9]{0,19}', identity['system_identifier']) or identity['system_identifier'] == source['system_identifier']): raise Failure() def _online(connection, identity): row = connection.execute("""SELECT current_database() AS database, current_user AS user_name, current_setting('data_directory') AS data_directory, current_setting('port')::int AS port, current_setting('server_version_num')::int AS version_num, pg_is_in_recovery() AS in_recovery, (SELECT system_identifier::text FROM pg_catalog.pg_control_system()) AS system_identifier, (SELECT rolsuper FROM pg_catalog.pg_roles WHERE rolname = current_user) AS superuser, (SELECT count(*) FROM pg_catalog.pg_stat_activity WHERE backend_type = 'client backend' AND pid <> pg_backend_pid()) AS other_clients""").fetchone() expected = {'database': identity['database'], 'user_name': identity['user'], 'data_directory': identity['data_directory'], 'port': identity['port'], 'system_identifier': identity['system_identifier'], 'in_recovery': False, 'superuser': True, 'other_clients': 0} if (not row or any(row.get(k) != v for k, v in expected.items()) or _integer(row['version_num']) // 10000 != 16): raise Failure() return row['version_num'] def _database_equality(runtime, connection, database, identity, progress, *, empty=False): version = _online(connection, identity) if empty: row = connection.execute("""SELECT (SELECT count(*) FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname <> 'information_schema' AND n.nspname !~ '^pg_') + (SELECT count(*) FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname <> 'information_schema' AND n.nspname !~ '^pg_') + (SELECT count(*) FROM pg_catalog.pg_type t JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname <> 'information_schema' AND n.nspname !~ '^pg_') + (SELECT count(*) FROM pg_catalog.pg_namespace WHERE nspname NOT IN ('public', 'information_schema') AND nspname !~ '^pg_') AS count""").fetchone() if row['count'] != 0: raise Failure() return {} tables = connection.execute("""SELECT n.nspname AS schema_name, c.relname AS name, c.relkind AS kind FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN ('r','p','f') AND n.nspname <> 'information_schema' AND n.nspname !~ '^pg_' ORDER BY n.nspname, c.relname""").fetchall() if (any(t['schema_name'] != 'public' or t['kind'] != 'r' for t in tables) or {t['name'] for t in tables} != set(database['table_counts'])): raise Failure() counts = {} for table in tables: _checkpoint(runtime) name = table['name'] count = connection.execute('SELECT count(*) AS count FROM ONLY "public".' + _identifier(name)).fetchone()['count'] if _integer(count) != database['table_counts'][name]: raise Failure() counts[name] = count progress(7, len(counts), 0) sequence_count = 0 if 'sequence_states' in database: sequences = connection.execute("""SELECT n.nspname AS schema_name, c.relname AS name FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'S' AND n.nspname <> 'information_schema' AND n.nspname !~ '^pg_' ORDER BY n.nspname, c.relname""").fetchall() expected = {(schema, name): state for schema, states in database['sequence_states'].items() for name, state in states.items()} if {(row['schema_name'], row['name']) for row in sequences} != set(expected): raise Failure() for (schema, name), state in expected.items(): _checkpoint(runtime) row = connection.execute('SELECT last_value, is_called FROM ' + _identifier(schema) + '.' + _identifier(name)).fetchone() if dict(row) != state: raise Failure() sequence_count += 1 connection.commit() _online(connection, identity) connection.commit() return {'table_counts': counts, 'tables': len(counts), 'rows': sum(counts.values()), 'sequences_verified': sequence_count, 'sequences_provided': 'sequence_states' in database, 'version_num': version} def _postman_target(runtime, target, normalized, files): from target_identity import postman_target_identity if not isinstance(target, str) or len(target.encode('utf-8')) > BLOCK: raise Failure(2, 1) value = _json(target) if not isinstance(value, dict): raise Failure(2, 1) digest = _sha(str(value.get('sha256', '')).strip().lower()) if normalized != 'postman:sha256:' + digest or postman_target_identity(value) != normalized: raise Failure(2, 1) changed, offered = False, False for key in ('cache_path', 'local_path'): if key not in value: continue offered = True path = value[key] if not isinstance(path, str) or not path: raise Failure(2, 1) windows = path.replace('\\', '/') prefix = 'd:/truf/runtime/postman_cache/' linux = str(runtime.DATA / 'runtime-linux/postman_cache').replace('\\', '/') + '/' if windows.casefold().startswith(prefix): relative = windows[len(prefix):] elif path.startswith(linux): relative = path[len(linux):] else: raise Failure(2, 1) _relative(relative) name = 'runtime-linux/postman_cache/' + relative # The manifest rejects casefold collisions. Windows path case may differ # from the preserved filename; never rename the copied artifact itself. entry = files.get(name.casefold()) if entry is None or entry['sha256'] != digest or entry['size'] <= 0: raise Failure(2, 1) for size_key in ('size', 'bytes'): if size_key in value and (type(value[size_key]) not in (int, str) or int(value[size_key]) != entry['size']): raise Failure(2, 1) newpath = runtime.DATA / entry['path'] with _input(newpath, runtime) as (handle, before): _hash_input(runtime, newpath, handle, before, entry, private=True) newvalue = str(newpath) changed |= newvalue != value[key] value[key] = newvalue if not offered or postman_target_identity(value) != normalized: raise Failure(2, 1) return json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(',', ':')) if changed else target def _rebase_postman(runtime, connection, files): from target_identity import postman_target_identity cache = {name.casefold(): dict(entry, path=name) for name, entry in files.items() if name.startswith('runtime-linux/postman_cache/')} adjusted, reviewed, unsafe, after = 0, 0, 0, 0 while True: _checkpoint(runtime) rows = connection.execute("""SELECT * FROM public.target_queue WHERE platform IN ('postman','github_gists','github_archive_files') AND status IN ('pending','deferred','in_progress') AND id > %s ORDER BY id LIMIT 128 FOR UPDATE""", (after,)).fetchall() if not rows: break for row in rows: after = _integer(row['id'], 1) reviewed += 1 try: if (row['status'] not in ('pending', 'deferred') or any(row[name] is not None for name in FENCES) or row['resolver_state'] not in (None, 'resolved')): raise Failure(2, 1) normalized = row['normalized_target'] if row['platform'] != 'postman': # ScannerDB normalizes these platforms as the entire lower- # cased JSON, not the artifact digest. Changing a locator # would change queue identity, even for the same artifact. if (not isinstance(row['target'], str) or len(row['target'].encode('utf-8')) > BLOCK or normalized != row['target'].strip().lower()): raise Failure(2, 1) normalized = postman_target_identity(row['target']) replacement = _postman_target(runtime, row['target'], normalized, cache) if row['platform'] != 'postman' and replacement != row['target']: raise Failure(2, 1) except (Failure, ValueError, TypeError, OSError): _checkpoint(runtime) unsafe += 1 continue if replacement != row['target']: # All rows are locked in this transaction. Only the target JSON # changes: no timestamp, identity, attempt, token, or history edit. result = connection.execute("""UPDATE public.target_queue SET target = %s WHERE id = %s AND target = %s AND normalized_target = %s AND status IN ('pending','deferred') AND current_result_reservation_id IS NULL AND lease_token IS NULL AND claim_event_id IS NULL""", (replacement, row['id'], row['target'], row['normalized_target'])) if result.rowcount != 1: raise Failure() adjusted += 1 if unsafe: connection.rollback() raise Failure(2, unsafe) _checkpoint(runtime) connection.commit() return {'postman_reviewed': reviewed, 'postman_adjusted': adjusted} def _preserved(runtime, connection, previous=None): evidence = {} for table, (key, selected) in PRESERVED.items(): _checkpoint(runtime) if previous is not None and table not in previous: continue old = previous.get(table) if previous is not None else None if old is None: columns = [row['name'] for row in connection.execute("""SELECT a.attname AS name FROM pg_catalog.pg_attribute a JOIN pg_catalog.pg_class c ON c.oid = a.attrelid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public' AND c.relname = %s AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attname""", (table,)).fetchall()] if not columns: continue if key not in columns: raise Failure(2, 1) # Additive migration may introduce a table/column not in this # snapshot. The preservation proof covers every preexisting fact. columns = [name for name in selected if name in columns] if selected else columns cutoff = connection.execute('SELECT max(' + _identifier(key) + ') AS cutoff FROM public.' + _identifier(table)).fetchone()['cutoff'] else: columns, cutoff = old['columns'], old['cutoff'] digest, count = hashlib.sha256(), 0 if cutoff is not None: selection = ','.join(_identifier(column) for column in columns) # Named cursor keeps millions of historical identifiers off both # the client's buffered result set and the diagnostic channels. with connection.cursor(name='windows_import_evidence') as cursor: cursor.execute('SELECT pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(' 'pg_catalog.row_to_json(e)::text, \'UTF8\')), \'hex\') AS digest FROM ' '(SELECT ' + selection + ' FROM public.' + _identifier(table) + ' WHERE ' + _identifier(key) + ' <= %s ORDER BY ' + _identifier(key) + ') e', (cutoff,)) for row in cursor: _checkpoint(runtime) digest.update(_sha(row['digest']).encode('ascii')) count += 1 item = {'columns': columns, 'cutoff': cutoff, 'count': count, 'sha256': digest.hexdigest()} if old is not None and item != old: raise Failure(2, 1) evidence[table] = item connection.commit() return evidence def _pipeline_counts(connection): queries = { 'worker_leases': "SELECT count(*) AS count FROM public.pipeline_leases WHERE state NOT IN ('released','failed')", 'result_reservations': "SELECT count(*) AS count FROM public.result_reservations WHERE state IN ('scanning','ready','ingesting','db_committed')", 'queue_leases': "SELECT count(*) AS count FROM public.target_queue q LEFT JOIN public.result_reservations r ON r.id=q.current_result_reservation_id WHERE q.status='in_progress' OR r.state IN ('scanning','ready','ingesting','db_committed')", 'blob_leases': "SELECT count(*) AS count FROM public.docker_content_blobs WHERE state IN ('leased','submitted') OR lease_reservation_id IS NOT NULL", } counts = {name: _integer(connection.execute(sql).fetchone()['count']) for name, sql in queries.items()} connection.commit() return counts def _projection_files(runtime, connection, files): rows = connection.execute("""SELECT s.stream_name, c.stream_name AS cursor_stream_name, s.base_relative_path, s.current_generation, c.generation, c.committed_offset FROM public.projection_streams s FULL JOIN public.projection_cursors c ON c.stream_name = s.stream_name""").fetchall() scan_streams = {'scan_results': 'scan_results.jsonl', 'found_secrets': 'found_secrets.jsonl', 'scan_errors': 'scan_errors.log'} stream_names = {row.get('stream_name') for row in rows} if not scan_streams.keys() <= stream_names or len(stream_names) != len(rows): raise Failure(2, len(rows)) for row in rows: stream_name = row.get('stream_name') if (not isinstance(stream_name, str) or row.get('cursor_stream_name') != stream_name or _integer(row.get('current_generation')) != _integer(row.get('generation'))): raise Failure(2, 1) offset = _integer(row.get('committed_offset')) relative = row.get('base_relative_path') _relative(relative) root = 'runtime-linux/results/' expected = scan_streams.get(stream_name) status = False if expected is None: match = re.fullmatch(r'keycheck:([a-z0-9][a-z0-9_.-]{0,63}):(results|status)', stream_name) if not match: raise Failure(2, 1) status = match[2] == 'status' suffix = 'Checked.txt' if status else 'Results.jsonl' expected = f'{match[1]}/{match[1]}{suffix}' root = 'runtime-linux/keychecks/' if relative != expected: raise Failure(2, 1) name = root + relative path = runtime.DATA / name metadata = files.get(name) if status: # Replacement status snapshots do not advance historical append cursors. if metadata is None: raise Failure(2, 1) with _input(path, runtime) as (handle, before): _hash_input(runtime, path, handle, before, metadata, private=True) else: if offset != (metadata['size'] if metadata is not None else 0): raise Failure(2, 1) if metadata is not None: if _regular(path, runtime)[4] != metadata['size']: raise Failure() elif os.path.lexists(path): raise Failure(2, 1) connection.commit() def _backend_stop(pg, config, backend, progress): retries = 0 while True: stage = 3 try: result = pg.maintenance_stop(config, backend=backend) stage = 4 if result.completed is not True or result.stopped is not True: raise Failure() stage = 5 probe = backend.probe() stage = 6 if probe.kind != pg.ProbeKind.STOPPED: raise Failure() return except BaseException as exc: retries += 1 _diagnostic(progress, stage, exc, retries) try: time.sleep(2) except BaseException: pass @contextlib.contextmanager def _authority(runtime, config, source, progress, *, stopped=False): import postgres_runtime as pg from runtime_security import ClusterAuthorityLock dsn, _password = _credentials(runtime) lock = ClusterAuthorityLock(config, endpoint_dsn=dsn) lock.acquire() backend = None try: backend = pg.PostgresBackend(config) if stopped: _backend_stop(pg, config, backend, progress) elif backend.probe().kind != pg.ProbeKind.READY: raise Failure() identity = pg.verify_cluster_identity(config) _identity(runtime, identity, source) yield identity if not stopped and backend.probe().kind != pg.ProbeKind.READY: raise Failure() except BaseException as exc: _diagnostic(progress, 1, exc) retries = 0 while backend is None: try: backend = pg.PostgresBackend(config) except BaseException as stop_exc: retries += 1 _diagnostic(progress, 2, stop_exc, retries) try: time.sleep(2) except BaseException: pass _backend_stop(pg, config, backend, progress) raise finally: if backend is not None: retries = 0 while True: try: backend.close() break except BaseException as exc: retries += 1 _diagnostic(progress, 7, exc, retries) _backend_stop(pg, config, backend, progress) lock.release() def _stop_cli(runtime, path, progress): retries = 0 while True: try: _command(runtime, runtime._bootstrap_command('postgres-runtime', 'maintenance-stop', '--config', str(path)), LIFECYCLE_TIMEOUT, progress, lifecycle=True, stopping=True) return except BaseException as exc: retries += 1 _diagnostic(progress, 8, exc, retries) try: time.sleep(2) except BaseException: pass def _restore(runtime, path, config, manifest, files, dump, fingerprint, progress, report): try: _command(runtime, runtime._bootstrap_command('postgres-runtime', 'initialize-empty', '--config', str(path)), LIFECYCLE_TIMEOUT, progress, lifecycle=True) except Failure as exc: _diagnostic(progress, 1, exc) if exc.uncertain: # A killed init CLI did not fulfill its ownership contract. Without # an identity the stop CLI must HOLD, not adopt or release this data. _stop_cli(runtime, path, progress) report['maintenance_stopped'] = True raise # initialize-empty itself retains its temporary postmaster until stopped, # including failure. A failed unbound initdb is not adopted by maintenance. started = False try: _checkpoint(runtime) progress(6) started = True _command(runtime, runtime._bootstrap_command('postgres-runtime', 'maintenance-start', '--config', str(path)), LIFECYCLE_TIMEOUT, progress, lifecycle=True) with _authority(runtime, config, manifest['database'], progress) as identity: report['linux_system_identifier'] = identity['system_identifier'] progress(7) with _connect(runtime) as connection: _database_equality(runtime, connection, manifest['database'], identity, progress, empty=True) _hash_input(runtime, IMPORT / 'database.dump', dump, fingerprint, manifest['database'], dump=True) _dsn, password = _credentials(runtime) from runtime_security import require_trusted_native_executable restore_binary = require_trusted_native_executable('/usr/lib/postgresql/16/bin/pg_restore') environment = {k: v for k, v in os.environ.items() if not k.upper().startswith('PG')} environment.update( PGHOST='127.0.0.1', PGHOSTADDR='127.0.0.1', PGPORT='5432', PGUSER='truf', PGDATABASE='truf', PGPASSWORD=password, PGPASSFILE=os.devnull, PGSERVICEFILE=os.devnull, PGSSLMODE='disable', PGGSSENCMODE='disable', PGCONNECT_TIMEOUT='10', PGAPPNAME='truf-container-import-restore', PGCLIENTENCODING='UTF8', LC_ALL='C', LANG='C', PGOPTIONS=f'-c statement_timeout={RESTORE_TIMEOUT * 1000} -c lock_timeout=60000 ' '-c idle_in_transaction_session_timeout=3600000 -c row_security=off ' '-c log_min_error_statement=panic -c log_min_messages=panic ' '-c log_statement=none -c log_min_duration_statement=-1', ) _command(runtime, [restore_binary, '--dbname=truf', '--no-password', '--single-transaction', '--exit-on-error', '--no-owner', '--no-acl', '--no-tablespaces'], RESTORE_TIMEOUT, progress, env=environment, stdin=dump) _hash_input(runtime, IMPORT / 'database.dump', dump, fingerprint, manifest['database'], dump=True) with _connect(runtime) as connection: raw = _database_equality(runtime, connection, manifest['database'], identity, progress) report['raw'] = raw preserved = _preserved(runtime, connection) _projection_files(runtime, connection, files) report['pipeline_before'] = _pipeline_counts(connection) report['raw_evidence_sha256'] = _write(runtime, runtime.DATA / 'config/windows-import-raw.json', _encoded({'manifest_sha256': report['manifest_sha256'], **raw})) progress(8) with _connect(runtime, readonly=False) as connection: report.update(_rebase_postman(runtime, connection, files)) # The existing migration CLI clears PGOPTIONS. Database-scoped # defaults on this freshly generated Linux role also suppress # server log DETAIL/STATEMENT values, not just child stderr. for setting, value in QUIET_PG_SETTINGS: connection.execute('ALTER ROLE "truf" IN DATABASE "truf" SET ' + setting + " TO '" + value + "'") connection.commit() report['recovery_applied'] = any(report['pipeline_before'].values()) if report['recovery_applied']: progress(9) _command(runtime, runtime._bootstrap_command( 'migrate-runtime-safety', '--config', str(path), '--apply', '--sources-stopped', '--recover-stale-result-pipeline', '--max-rows', '10000', '--max-seconds', '3600', ), COUNT_TIMEOUT + 600, progress) # Bundle ingestion can insert derived_postman_targets carrying the # original Windows locators. Recheck after that intentional handoff. with _authority(runtime, config, manifest['database'], progress): with _connect(runtime, readonly=False) as connection: _online(connection, identity) for name, count in _rebase_postman(runtime, connection, files).items(): # Reviewed counts are visits; adjusted counts are writes. report[name] += count # Both CLIs own their own authority; no importer SQL sessions survive # the handoff (the migration explicitly rejects other client sessions). progress(10) _command(runtime, runtime._bootstrap_command( 'migrate-runtime-safety', '--config', str(path), '--apply', '--sources-stopped', ), MIGRATE_TIMEOUT, progress) progress(11) with _authority(runtime, config, manifest['database'], progress): with _connect(runtime) as connection: _online(connection, identity) _preserved(runtime, connection, preserved) report['preserved'] = {name: {k: item[k] for k in ('count', 'sha256')} for name, item in preserved.items()} report['pipeline_after'] = _pipeline_counts(connection) if any(report['pipeline_after'].values()): raise Failure(2, sum(report['pipeline_after'].values())) from scanner_db import ScannerDB db = ScannerDB(db_url=_credentials(runtime)[0], initialize=False) try: if not db.enabled or not db.conn.is_postgres: raise Failure() db.conn.execute('SET default_transaction_read_only = on') db.conn.execute(f'SET statement_timeout = {COUNT_TIMEOUT * 1000}') db.conn.commit() db.require_runtime_safety_schema() db.require_final_cutover() cutover = db.final_cutover_status() report['cutover_sha256'] = _sha(cutover['evidence_sha256']) report['migration_rows'] = _integer(db.conn.execute( 'SELECT count(*) AS count FROM runtime_schema_migrations').fetchone()['count']) db.conn.commit() finally: db.close() with _connect(runtime, readonly=False) as connection: for setting, _value in QUIET_PG_SETTINGS: connection.execute('ALTER ROLE "truf" IN DATABASE "truf" RESET ' + setting) connection.commit() return identity except BaseException as exc: _diagnostic(progress, 1, exc) raise finally: if started: try: progress(12, cleanup=True) except BaseException: pass _stop_cli(runtime, path, progress) report['maintenance_stopped'] = True def import_snapshot(runtime, manifest_sha256) -> int: """Import once, returning only a safe code; never let diagnostics escape.""" output, errors = sys.stdout, sys.stderr phase, report, mutations = 1, {}, False def progress(number, count=0, size=0, *, cleanup=False, diagnostic=None): nonlocal phase if not cleanup: _checkpoint(runtime) phase = number if diagnostic is not None: stage, failure_code, review_count, type_id, line = diagnostic event = dict(phase=phase, stage=stage, code=failure_code, review_count=review_count, type_id=type_id, line=line, attempt=count) key = 'failure' if stage == 1 else 'hold' first = key not in report if first: report[key] = event if first or stage != 1: try: print('import-snapshot-diagnostic', *event.values(), file=errors, flush=True) except BaseException: pass if first and mutations: try: _write(runtime, runtime.DATA / ('config/windows-import-' + key + '.json'), _encoded(event)) except BaseException: pass try: print('import-snapshot', number, count, size, file=output, flush=True) except BaseException: if not cleanup: raise code, review_count, signal_installed = 1, 0, False try: previous_interrupt = signal.signal(signal.SIGINT, lambda *_: setattr(runtime, '_shutdown_requested', True)) signal_installed = True with _quiet(): from runtime_security import PrivateFileLock, write_private_json_exclusive _checkpoint(runtime) lock_path = runtime.INITIALIZE_LOCK runtime.private_path(lock_path.parent, directory=True) if os.path.lexists(lock_path): _regular(lock_path, runtime) with PrivateFileLock(str(lock_path)), contextlib.ExitStack() as stack: try: progress(1) _mounts(runtime) opened = {name: stack.enter_context(_input(IMPORT / name)) for name in ('manifest.json', 'files.tar', 'database.dump')} manifest_handle, manifest_fingerprint = opened['manifest.json'] payload = manifest_handle.read(MAX_MANIFEST + 1) manifest, files = _manifest(payload, manifest_sha256) placeholders = _fresh(runtime, files) report = {'manifest_sha256': manifest_sha256.lower(), 'archive_sha256': manifest['archive']['sha256'], 'database_sha256': manifest['database']['sha256'], 'files': len(files), 'file_bytes': sum(f['size'] for f in files.values()), **_space(runtime, manifest)} archive, archive_fingerprint = opened['files.tar'] dump, dump_fingerprint = opened['database.dump'] progress(2) _archive(runtime, archive, manifest['archive'], files, progress) _unchanged(IMPORT / 'files.tar', archive, archive_fingerprint) _hash_input(runtime, IMPORT / 'database.dump', dump, dump_fingerprint, manifest['database'], dump=True) _unchanged(IMPORT / 'manifest.json', manifest_handle, manifest_fingerprint) if _fresh(runtime, files) != placeholders: raise Failure() _space(runtime, manifest) progress(3) mutations = True _write(runtime, runtime.DATA / 'config/windows-import-manifest.json', payload) copied = _archive(runtime, archive, manifest['archive'], files, progress, placeholders) _unchanged(IMPORT / 'files.tar', archive, archive_fingerprint) progress(4) path, config, adjusted, config_hash = _configuration(runtime) report.update(adjusted_keys=adjusted, config_sha256=config_hash) os.environ.update(TRUF_DB_STATEMENT_TIMEOUT_MS=str(COUNT_TIMEOUT * 1000), TRUF_DB_LOCK_TIMEOUT_MS='10000', TRUF_DB_IDLE_TRANSACTION_TIMEOUT_MS='3600000') progress(5) identity = _restore(runtime, path, config, manifest, files, dump, dump_fingerprint, progress, report) # Reacquire the stopped endpoint and retain it through the # last publication, rather than treating a CLI exit as a marker. with _authority(runtime, config, manifest['database'], progress, stopped=True) as stopped_identity: if identity != stopped_identity: raise Failure() checked = 0 for name, before in copied.items(): _checkpoint(runtime) # Only the explicitly permitted fenced recovery can # consume/move incoming tmp/ready bundle artifacts. if (report.get('recovery_applied') and name.startswith( ('scanner-result-bundles/tmp/', 'scanner-result-bundles/ready/'))): continue destination = runtime.DATA / name if _regular(destination, runtime) != before: with _input(destination, runtime) as (handle, current): _hash_input(runtime, destination, handle, current, files[name], private=True) checked += 1 if checked % 1024 == 0: progress(11, checked, 0) report['preserved_files'] = checked report['remaining_free_bytes'] = shutil.disk_usage(runtime.DATA).free if report['remaining_free_bytes'] < 20 * GIB: raise Failure() for name, (handle, before) in opened.items(): _unchanged(IMPORT / name, handle, before) stack.close() progress(13) report.update(status='verified-stopped', maintenance_stopped=True) report_hash = _write(runtime, runtime.DATA / 'config/windows-import-report.json', _encoded(report)) _checkpoint(runtime) write_private_json_exclusive(str(runtime.INITIALIZED), { 'format': runtime.FORMAT, 'system_identifier': identity['system_identifier'], 'pg_major': identity['pg_major'], 'manifest_sha256': manifest_sha256.lower(), 'import_report_sha256': report_hash, }) code = 0 except BaseException as exc: _diagnostic(progress, 1, exc) failure = report.get('failure', {}) code, review_count = failure.get('code', 1), failure.get('review_count', 0) if mutations and not os.path.lexists(runtime.DATA / 'config/windows-import-report.json'): report.update(status='failed-unmarked', phase=phase, code=code, review_count=review_count) _write(runtime, runtime.DATA / 'config/windows-import-report.json', _encoded(report)) raise except BaseException as exc: _diagnostic(progress, 1, exc) failure = report.get('failure', {}) code, review_count = failure.get('code', 1), failure.get('review_count', 0) finally: if signal_installed: signal.signal(signal.SIGINT, previous_interrupt) if code: print('import-snapshot', phase, code, review_count, file=errors, flush=True) else: summary = {name: report[name] for name in ( 'manifest_sha256', 'archive_sha256', 'database_sha256', 'files', 'file_bytes', 'config_sha256', 'adjusted_keys', 'cutover_sha256', 'migration_rows', 'postman_reviewed', 'postman_adjusted', )} summary.update({key: report['raw'][key] for key in ('tables', 'rows', 'sequences_verified')}) print(json.dumps(summary, ensure_ascii=True, sort_keys=True), file=output, flush=True) return code