| 1 | #!/usr/bin/env python3 |
| 2 | """Publish a clean, signed-in sandbox Windows template with the built guest agent.""" |
| 3 | import argparse |
| 4 | import base64 |
| 5 | import json |
| 6 | from pathlib import Path |
| 7 | import time |
| 8 | import vms |
| 9 | |
| 10 | parser = argparse.ArgumentParser(description=__doc__) |
| 11 | parser.add_argument('name', help='Running disposable template with the sandbox administrator signed in') |
| 12 | parser.add_argument('id', help='New immutable preset ID') |
| 13 | parser.add_argument('--supersedes', required=True) |
| 14 | args = parser.parse_args() |
| 15 | for name in [args.name, args.id, args.supersedes]: |
| 16 | vms.ensure_name(name) |
| 17 | if (vms.PRESETS / args.id).exists(): |
| 18 | raise ValueError('A preset already has this name.') |
| 19 | root = Path(__file__).resolve().parent.parent |
| 20 | preset = vms.read_preset(args.supersedes) |
| 21 | if preset['platform'] != 'windows' or preset['arch'] != 'x86_64': |
| 22 | raise ValueError('Select a Windows preset.') |
| 23 | files = [(root / 'guest/install.ps1', 'install-guest.ps1'), |
| 24 | (root / 'guest/windows-provision.ps1', 'windows-provision.ps1'), |
| 25 | (root / 'guest/build/snowglobe-guest-x86_64.exe', 'snowglobe-guest.exe')] |
| 26 | contents = [(path.read_bytes(), name) for path, name in files] |
| 27 | |
| 28 | def qga(operation, **arguments): |
| 29 | command = {'execute': operation} |
| 30 | if arguments: |
| 31 | command['arguments'] = arguments |
| 32 | return json.loads(vms.virsh('qemu-agent-command', args.name, json.dumps(command), '--timeout', '15'))['return'] |
| 33 | |
| 34 | def wait(pid): |
| 35 | deadline = time.monotonic() + 180 |
| 36 | while True: |
| 37 | result = qga('guest-exec-status', pid=pid) |
| 38 | if result.get('exited'): |
| 39 | if result.get('exitcode') != 0: |
| 40 | raise RuntimeError(base64.b64decode(result.get('err-data', '')).decode(errors='replace')) |
| 41 | return |
| 42 | if time.monotonic() > deadline: |
| 43 | raise TimeoutError('Template preparation did not finish; inspect the existing process before retrying.') |
| 44 | time.sleep(.25) |
| 45 | |
| 46 | pid = qga('guest-exec', path='powershell.exe', arg=['-NoProfile', '-NonInteractive', '-Command', |
| 47 | "New-Item C:\\Snowglobe -ItemType Directory -Force | Out-Null; Stop-ScheduledTask -TaskName SnowglobeProvision -ErrorAction SilentlyContinue"], **{'capture-output': True})['pid'] |
| 48 | wait(pid) |
| 49 | for content, name in contents: |
| 50 | handle = qga('guest-file-open', path='C:\\Snowglobe\\' + name, mode='w') |
| 51 | try: |
| 52 | qga('guest-file-write', handle=handle, **{'buf-b64': base64.b64encode(content).decode()}) |
| 53 | finally: |
| 54 | qga('guest-file-close', handle=handle) |
| 55 | pid = qga('guest-exec', path='powershell.exe', arg=['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', |
| 56 | '-File', 'C:\\Snowglobe\\windows-provision.ps1', '-Prepare'], **{'capture-output': True})['pid'] |
| 57 | wait(pid) |
| 58 | vms.virsh('shutdown', args.name, '--mode', 'agent') |
| 59 | deadline = time.monotonic() + 120 |
| 60 | while vms.virsh('domstate', args.name).strip() != 'shut off': |
| 61 | if time.monotonic() > deadline: |
| 62 | raise TimeoutError('The template did not shut down.') |
| 63 | time.sleep(.5) |
| 64 | vms.save_preset({'name': args.name, 'id': args.id, 'os': preset['os'], |
| 65 | 'description': 'Prepared desktop with cursor integration', 'setup': 'windows-v3', 'supersedes': args.supersedes}) |
| 66 | (vms.PRESETS / args.id / 'disk.qcow2').chmod(0o440) |
| 67 | print(args.id) |