#!/usr/bin/env python3 """Submit Lua TestBot jobs through existing authenticated Trinity SOAP commands.""" import argparse import base64 import json import os from pathlib import Path import time import urllib.error import urllib.request import xml.etree.ElementTree as ET def command(endpoint, username, password, text, timeout=20): envelope = ET.Element('{http://schemas.xmlsoap.org/soap/envelope/}Envelope') body = ET.SubElement(envelope, '{http://schemas.xmlsoap.org/soap/envelope/}Body') execute = ET.SubElement(body, '{urn:TC}executeCommand') ET.SubElement(execute, 'command').text = text auth = base64.b64encode((username + ':' + password).encode()).decode() request = urllib.request.Request(endpoint, ET.tostring(envelope), headers={ 'Content-Type': 'text/xml; charset=utf-8', 'SOAPAction': 'urn:TC#executeCommand', 'Authorization': 'Basic ' + auth, }) with urllib.request.urlopen(request, timeout=timeout) as response: root = ET.fromstring(response.read()) for node in root.iter(): if node.tag.rsplit('}', 1)[-1] == 'Fault': raise RuntimeError('SOAP fault: ' + ''.join(node.itertext())) for node in root.iter(): if node.tag.rsplit('}', 1)[-1] in ('result', 'return'): return ''.join(node.itertext()).strip() raise RuntimeError('SOAP response contains no command result') def run(endpoint, username, password, run_id, script, deadline=330): if not run_id or len(run_id) > 80 or any(c not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-' for c in run_id): raise ValueError('Invalid run ID') content = Path(script).read_bytes() if not content or len(content) > 65536: raise ValueError('Script must contain 1..65536 bytes') reply = command(endpoint, username, password, f'clientless test submit {run_id} ' + base64.b64encode(content).decode()) if f'Testbot run {run_id}: queued' not in reply: raise RuntimeError(reply) end = time.monotonic() + deadline while time.monotonic() < end: reply = command(endpoint, username, password, f'clientless test result {run_id}') try: result = json.loads(reply) except json.JSONDecodeError: if 'not available yet' not in reply: raise RuntimeError(reply) else: if result['run_id'] != run_id: raise RuntimeError('Result belongs to another run') if result['state'] != 'running': return result time.sleep(1) raise TimeoutError(f'Client deadline exceeded; retrieve/cancel run {run_id} on server') def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('run_id') parser.add_argument('script', type=Path) parser.add_argument('--endpoint', default='http://127.0.0.1:7878/') parser.add_argument('--user', required=True) parser.add_argument('--output', type=Path) parser.add_argument('--timeout', type=float, default=330, help='client wait deadline in seconds') args = parser.parse_args() try: result = run(args.endpoint, args.user, os.environ['TESTBOT_PASSWORD'], args.run_id, args.script, deadline=args.timeout) text = json.dumps(result, ensure_ascii=False, indent=2) if args.output: args.output.write_text(text + '\n') print(text) return 0 if result['state'] == 'passed' else 1 except (OSError, ValueError, RuntimeError, TimeoutError, KeyError, ET.ParseError) as error: print(json.dumps({'client_error': str(error)}, ensure_ascii=False)) return 2 if __name__ == '__main__': raise SystemExit(main())