#!/usr/bin/env python3 """Run reviewed, pinned site-specific commands for batch.py without a shell. Missing integration is a waiting gate, never an assumed successful operation. Handlers receive paths to the frozen request, not passwords on the command line. """ import argparse import hashlib import json from pathlib import Path import subprocess import sys def main(): p = argparse.ArgumentParser(description=__doc__) for field in ('plan', 'node', 'phase', 'action', 'state-dir'): p.add_argument('--' + field, required=True) a = p.parse_args() node = json.loads(Path(a.node).read_text()) plan = json.loads(Path(a.plan).read_text()) identity = {'key': node['key'], 'phase': a.phase, 'request_sha256': node['request_sha256']} try: profile = plan['command_profile'] # Every runnable local script/config must be pinned; pinning the interpreter # alone does not pin scripts, remote code or the commands' dependencies. files = profile['files'] if not files: raise ValueError('No pinned handler files') for filename, expected in files.items(): if not Path(filename).is_absolute(): raise ValueError('Absolute handler paths required') if hashlib.sha256(Path(filename).read_bytes()).hexdigest() != expected: raise ValueError('Handler changed') command = profile['phases'].get(a.phase, {}).get(a.action) if command is None: result = dict(identity, status='waiting', reason='handler_not_configured') else: if not isinstance(command, list) or not command or not all(isinstance(x, str) for x in command): raise ValueError('Command must be argv') values = dict(plan=a.plan, node=a.node, state_dir=a.state_dir, key=node['key'], phase=a.phase, action=a.action) argv = [x.format_map(values) for x in command] if not Path(argv[0]).is_absolute(): raise ValueError('Absolute executable required') completed = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) if completed.returncode or len(completed.stdout) > 65536: raise ValueError('Handler failed') result = json.loads(completed.stdout) if any(result.get(k) != v for k, v in identity.items()): raise ValueError('Wrong handler receipt') if result.get('status') not in ('ready', 'absent', 'waiting', 'unknown', 'conflict'): raise ValueError('Wrong handler status') # Only structured nonsecret receipt fields are allowed through to the runner. safe = {**identity, 'status': result['status']} if result.get('retry_safe') is True: safe['retry_safe'] = True print(json.dumps(safe)) return 0 except Exception: print(json.dumps(dict(identity, status='unknown'))) return 2 if __name__ == '__main__': sys.exit(main())