1#!/usr/bin/env python3
2"""Run the embedded transport message-loss matrix on an owned Samba VM."""
3import argparse
4import hashlib
5import json
6import os
7from pathlib import Path
8import shutil
9import signal
10import subprocess
11import time
12
13from native_runner import ROOT
14import linux_vm
15
16
17def run(output, server):
18 if linux_vm.instance_path(server).exists():
19 raise ValueError('Choose a new Linux VM name.')
20 output = output.resolve()
21 output.mkdir(parents=True, exist_ok=False)
22 sources = ['tools/smb_faults.py', 'tools/smb-proxy.py', 'Cargo.lock',
23 'crates/notebook/src/smb/mod.rs', 'crates/notebook/src/smb/tests.rs',
24 'crates/notebook/src/smb/tests/faults.rs', 'crates/onestore/src/commit.rs',
25 'crates/onestore/src/snapshot.rs']
26 for name in sources:
27 target = output / 'harness' / name
28 target.parent.mkdir(parents=True, exist_ok=True)
29 shutil.copyfile(ROOT / name, target)
30 (output / 'run.json').write_text(json.dumps({'server': server,
31 'source_sha256': {name: hashlib.sha256((output / 'harness' / name).read_bytes()).hexdigest() for name in sources}}, indent=2))
32 process = None
33 try:
34 linux_vm.create_instance(server)
35 linux_vm.launch(server)
36 linux_vm.wait_instance(server, 600)
37 config = linux_vm.load_instance(server)
38 (output / 'linux.json').write_text(json.dumps(config, indent=2))
39 with (output / 'test.log').open('w') as log:
40 process = subprocess.Popen(['cargo', 'test', '-p', 'notebook', '--features', 'smb', 'live_message_loss', '--', '--ignored', '--nocapture'],
41 cwd=ROOT, stdout=log, stderr=subprocess.STDOUT, start_new_session=True,
42 env={**os.environ, 'ONESTORE_SMB_LAB': f'127.0.0.1:{config["samba_port"]}',
43 'ONESTORE_SMB_EVIDENCE': str(output / 'cases')})
44 if process.wait(timeout=900) != 0: raise RuntimeError('The message-loss matrix failed; inspect test.log.')
45 hashes = linux_vm.run_ssh(server, 'sha256sum /srv/agent/fault-*.one', timeout=30)
46 hashes.check_returncode()
47 (output / 'server-sha256.txt').write_text(hashes.stdout)
48 hashes = {Path(path).name: digest for digest, path in (line.split() for line in hashes.stdout.splitlines())}
49 results = json.loads((output / 'cases/results.json').read_text())
50 for result in results:
51 captured = output / 'cases/recovered' / (result['case'] + '.one')
52 assert hashlib.sha256(captured.read_bytes()).hexdigest() == hashes[result['path']], 'Recovered bytes differ from the independent server read'
53 (output / 'verification.json').write_text(json.dumps({'cases': len(results), 'server_hashes_match': True}, indent=2))
54 finally:
55 if process is not None and process.poll() is None:
56 os.killpg(process.pid, signal.SIGTERM)
57 process.wait(timeout=30)
58 if linux_vm.instance_path(server).exists():
59 if linux_vm.running(server):
60 try:
61 with (output / 'server.tar').open('wb') as archive:
62 subprocess.run(linux_vm.ssh_argv(server, 'tar cf - -C /srv/agent .'), stdout=archive, check=True, timeout=30)
63 result = linux_vm.run_ssh(server, 'sudo smbstatus --byterange --json', timeout=10)
64 (output / 'server-locks.json').write_text(result.stdout)
65 finally:
66 try: linux_vm.shutdown(server, 60)
67 finally:
68 if linux_vm.running(server): linux_vm.qmp(server, 'quit')
69 deadline = time.monotonic() + 10
70 while linux_vm.running(server) and time.monotonic() < deadline: time.sleep(.1)
71 linux_vm.delete_instance(server)
72 (output / 'teardown.json').write_text(json.dumps({'linux_absent': not linux_vm.instance_path(server).exists()}, indent=2))
73
74
75if __name__ == '__main__':
76 parser = argparse.ArgumentParser(description=__doc__)
77 parser.add_argument('output', type=Path)
78 parser.add_argument('--linux', required=True)
79 args = parser.parse_args()
80 def interrupted(_signal, _frame): raise KeyboardInterrupt
81 signal.signal(signal.SIGTERM, interrupted)
82 run(args.output, args.linux)