#!/usr/bin/env python3 """Prepare, inspect or attend the fixed email-connect sender transition.""" from __future__ import annotations import argparse import json import shlex import subprocess import sys from pathlib import Path import sender_release as release from sender_transition import Refused, require ROOT = Path(__file__).resolve().parent FOUNDATION = Path("/home/worsch/railiance-platform/openbao/email-connect-platform-smtp/foundation.json") # This bootstrap is itself bound by the launcher's source hash. No archive # extraction, arbitrary filenames, shell interpolation of data or persistent code. BOOTSTRAP = r''' import hashlib,json,os,sys,tempfile from pathlib import Path sys.dont_write_bytecode=True request=json.load(sys.stdin) sources=request['sources'] expected=request['source_hashes'] assert __debug__ and set(sources)==set(expected) assert all('/' not in n and '\\' not in n and n.endswith('.py') for n in sources) assert sum(len(v.encode()) for v in sources.values())<=2*1024**2 assert {n:hashlib.sha256(v.encode()).hexdigest() for n,v in sources.items()}==expected with tempfile.TemporaryDirectory(prefix='email-sender-capsule-') as directory: for name,source in sources.items(): fd=os.open(Path(directory)/name,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600) with os.fdopen(fd,'w') as stream:stream.write(source) sys.path.insert(0,directory) import sender_release as release try: operation=request['operation'] if operation=='prepare': result=release.prepare(sources,request['foundation'],request['foundation_receipt']) else: packet=request['packet'] assert packet['sources']==sources release.validate(packet) if operation=='inspect':result=release.inspect(packet) elif operation in ('execute','recover'):result=release.execute(packet,recover=operation=='recover') else:raise ValueError('unknown_operation') print(json.dumps(result)) except Exception as error: print(json.dumps({'status':'sender_release_requires_inspection' if request['operation'] in ('execute','recover') else 'sender_release_refused','failure_type':type(error).__name__, 'retry_automatically':False})) sys.exit(1) ''' def remote(request): try: response = subprocess.run([ "ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "railiance01", "sudo -n python3 -I -c " + shlex.quote(BOOTSTRAP)], input=json.dumps(request).encode(), capture_output=True, timeout=900, check=False) result = json.loads(response.stdout) except (subprocess.TimeoutExpired, ValueError): # A timeout/lost SSH reply says nothing about whether the node stopped. return {"status": "remote_outcome_unconfirmed", "retry_automatically": False}, 1 return result, int(response.returncode != 0) def load_packet(path): release.private(path) require(path.stat().st_size <= 4 * 1024**2, "capsule_too_large") packet = json.loads(path.read_text()) release.validate(packet) current = {name: (ROOT / name).read_text() for name in release.FILES} require(current == packet["sources"], "reviewed_source_changed_restore_capsule_sources") return packet def read_foundation_receipt(path): release.private(path) require(path.stat().st_size <= 2 * 1024**2, "foundation_journal_too_large") data = path.read_bytes() require(data.endswith(b"\n"), "incomplete_foundation_journal") entries = [json.loads(line) for line in data.splitlines()] require(bool(entries), "foundation_receipt_missing") release.foundation_receipt(entries[-1]) return entries[-1] def main(argv=None): parser = argparse.ArgumentParser(description=__doc__) actions = parser.add_mutually_exclusive_group(required=True) for action in ("prepare", "show", "inspect", "execute", "recover"): actions.add_argument("--" + action, action="store_true") parser.add_argument("--packet", type=Path, required=True, help="Private new output for preparation; retained sealed packet otherwise") parser.add_argument("--foundation-receipt", type=Path) parser.add_argument("--expect-digest") args = parser.parse_args(argv) require(__debug__, "optimized_python_refused") if args.prepare: require(args.foundation_receipt is not None and not args.packet.exists(), "fresh_packet_and_receipt_required") release.private(args.packet.parent, directory=True) release.owner_return("http://127.0.0.1:8000") receipt = read_foundation_receipt(args.foundation_receipt) sources = {name: (ROOT / name).read_text() for name in release.FILES} request = {"operation": "prepare", "sources": sources, "source_hashes": release.source_hashes(sources), "foundation": json.loads(FOUNDATION.read_text()), "foundation_receipt": receipt} packet, code = remote(request) if code: print(json.dumps(packet)) return code release.validate(packet) require(packet["sources"] == sources, "returned_sources_changed") release.write(args.packet, packet) print(json.dumps({"status": "sender_proposal_prepared", "proposal_digest": packet["proposal_digest"], "native_server_dry_runs": 5, "executed": False})) return 0 require(args.foundation_receipt is None, "receipt_only_for_preparation") packet = load_packet(args.packet) value = packet["proposal_digest"] if args.expect_digest: require(args.expect_digest == value, "reviewed_digest_changed") if args.show: print(json.dumps({"proposal_digest": value, "source_files": len(packet["sources"]), "old_sender": packet["proposal"]["before"]["config_data"]["EMAIL_CONNECT_SENDER"], "new_sender": packet["proposal"]["after"]["config_data"]["EMAIL_CONNECT_SENDER"], "ledger_restore": False, "mail_send": False, "executed": False})) return 0 if args.execute or args.recover: require(args.expect_digest == value and sys.stdin.isatty(), "founder_terminal_and_exact_digest_required") if args.execute: release.owner_return("http://127.0.0.1:8000") verb = "RECOVER" if args.recover else "DEPLOY" print("Recover original configuration using the current ledger." if args.recover else "Switch email-connect to the admitted platform sender; retain the current ledger.") require(input("Type " + verb + " followed by the full digest: ").strip() == verb + " " + value, "not_confirmed") request = {"operation": "recover" if args.recover else "execute" if args.execute else "inspect", "packet": packet, "sources": packet["sources"], "source_hashes": packet["proposal"]["sources"]} result, code = remote(request) print(json.dumps(result)) return code if __name__ == "__main__": try: raise SystemExit(main()) except Refused as error: print(json.dumps({"status": "sender_release_refused", "reason": str(error), "retry_automatically": False})) raise SystemExit(1) from None except (OSError, ValueError, KeyError, TypeError) as error: print(json.dumps({"status": "sender_release_refused", "failure_type": type(error).__name__, "retry_automatically": False})) raise SystemExit(1) from None