67 lines
3.0 KiB
Python
67 lines
3.0 KiB
Python
#!/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())
|