Aktualizr
C++ SOTA Client
All Classes Namespaces Files Functions Variables Enumerations Enumerator Pages
test_io_failure.py
1 #!/usr/bin/env python3
2 
3 import argparse
4 import contextlib
5 import functools
6 import multiprocessing
7 import os
8 import shutil
9 import subprocess
10 import sys
11 import tempfile
12 import time
13 
14 from os import path
15 from subprocess import run
16 
17 from fake_http_server.fake_test_server import FakeTestServerBackground
18 
19 
20 """
21 Run aktualizr provisioning and update cycles with random IO failures,
22 then retry the procedure and make sure the second attempt succeeds.
23 
24 Right now, the executable should be aktualizr-cycle-simple which has
25 been fine-tuned for this test, but others can be adapted on the same
26 model.
27 
28 On a sample run, this test calls ~3000/4000 io syscalls, so it's recommended
29 to run with pf = 1/4000 = 0.00025, so that there should be around one error
30 per run, for around 4000*log(4000) ~= 50000 runs
31 (see https://en.wikipedia.org/wiki/Coupon_collector%27s_problem)
32 """
33 
34 
35 @contextlib.contextmanager
36 def uptane_repo(uptane_gen):
37  with tempfile.TemporaryDirectory() as repo_path:
38  arepo = [uptane_gen, '--keytype', 'ed25519']
39  run([*arepo, 'generate', '--path', repo_path])
40  fw_path = path.join(repo_path, 'images/firmware.txt')
41  os.makedirs(path.join(repo_path, 'images'))
42  with open(fw_path, 'wb') as f:
43  f.write(b'fw')
44  run([*arepo, 'image', '--path', repo_path, '--filename', fw_path,
45  '--targetname', 'firmware.txt'])
46  run([*arepo, 'addtarget', '--path', repo_path, '--targetname',
47  'firmware.txt', '--hwid', 'primary_hw', '--serial', 'CA:FE:A6:D2:84:9D'])
48  run([*arepo, 'signtargets', '--path', repo_path])
49  yield repo_path
50 
51 
52 def run_test(akt_test, server, srcdir, pf, k):
53  print(f'Running test {k}')
54  with tempfile.TemporaryDirectory() as storage_dir:
55  # run once with (maybe) a fault, trying to update
56  cp_err = run(['fiu-run', '-x', '-c', f'enable_random name=posix/io/*,probability={pf}',
57  akt_test, storage_dir, server],
58  stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=srcdir)
59  if cp_err.returncode == 0:
60  return True
61 
62  print('update failed, checking state')
63  cp = run([akt_test, storage_dir, server],
64  stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=srcdir)
65  if cp.returncode != 0:
66  error_state_dir = f'fail_state.{path.basename(storage_dir)}'
67  print(f'Error detected, see {error_state_dir}')
68  shutil.copytree(storage_dir, error_state_dir)
69  with open(path.join(error_state_dir, 'output1.log'), 'wb') as f:
70  f.write(cp_err.stdout)
71  with open(path.join(error_state_dir, 'output2.log'), 'wb') as f:
72  f.write(cp.stdout)
73  return False
74  return True
75 
76 
77 def main():
78  parser = argparse.ArgumentParser(description='Run io failure tests')
79  parser.add_argument('-n', '--n-tests', type=int, default=100,
80  help='number of random tests to run')
81  parser.add_argument('-j', '--jobs', type=int, default=1,
82  help='number of parallel tests')
83  parser.add_argument('-pf', '--probability-failure', type=float, default=0.00025,
84  help='probability of syscall failure')
85  parser.add_argument('--akt-srcdir', help='path to the aktualizr source directory')
86  parser.add_argument('--uptane-gen', help='path to uptane-generator executable')
87  parser.add_argument('--akt-test', help='path to aktualizr cycle test')
88  parser.add_argument('--serve-only', action='store_true',
89  help='only serve metadata, do not run tests')
90  args = parser.parse_args()
91 
92  srcdir = path.abspath(args.akt_srcdir) if args.akt_srcdir is not None else os.getcwd()
93 
94  with uptane_repo(args.uptane_gen) as repo_dir, \
95  FakeTestServerBackground(repo_dir, srcdir=srcdir) as uptane_server, \
96  multiprocessing.Pool(args.jobs) as pool:
97 
98  server = f'http://localhost:{uptane_server.port}'
99  print(f'Running tests on {server} (repo directory: {repo_dir})')
100 
101  if args.serve_only:
102  while True:
103  time.sleep(1)
104  return 0
105 
106  fk = functools.partial(run_test, path.abspath(args.akt_test),
107  server, srcdir, args.probability_failure)
108  for r in pool.imap(fk, range(1, args.n_tests+1)):
109  if not r:
110  print('error found!')
111  pool.close()
112  break
113 
114  return 0
115 
116 
117 if __name__ == '__main__':
118  sys.exit(main())