testenv.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. # TestEnv class to manage test environment variables.
  2. #
  3. # Copyright (c) 2020-2021 Virtuozzo International GmbH
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation; either version 2 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. #
  18. import os
  19. import sys
  20. import tempfile
  21. from pathlib import Path
  22. import shutil
  23. import collections
  24. import random
  25. import subprocess
  26. import glob
  27. from typing import List, Dict, Any, Optional
  28. if sys.version_info >= (3, 9):
  29. from contextlib import AbstractContextManager as ContextManager
  30. else:
  31. from typing import ContextManager
  32. DEF_GDB_OPTIONS = 'localhost:12345'
  33. def isxfile(path: str) -> bool:
  34. return os.path.isfile(path) and os.access(path, os.X_OK)
  35. def get_default_machine(qemu_prog: str) -> str:
  36. outp = subprocess.run([qemu_prog, '-machine', 'help'], check=True,
  37. universal_newlines=True,
  38. stdout=subprocess.PIPE).stdout
  39. machines = outp.split('\n')
  40. try:
  41. default_machine = next(m for m in machines if ' (default)' in m)
  42. except StopIteration:
  43. return ''
  44. default_machine = default_machine.split(' ', 1)[0]
  45. alias_suf = ' (alias of {})'.format(default_machine)
  46. alias = next((m for m in machines if m.endswith(alias_suf)), None)
  47. if alias is not None:
  48. default_machine = alias.split(' ', 1)[0]
  49. return default_machine
  50. class TestEnv(ContextManager['TestEnv']):
  51. """
  52. Manage system environment for running tests
  53. The following variables are supported/provided. They are represented by
  54. lower-cased TestEnv attributes.
  55. """
  56. # We store environment variables as instance attributes, and there are a
  57. # lot of them. Silence pylint:
  58. # pylint: disable=too-many-instance-attributes
  59. env_variables = ['PYTHONPATH', 'TEST_DIR', 'SOCK_DIR', 'SAMPLE_IMG_DIR',
  60. 'PYTHON', 'QEMU_PROG', 'QEMU_IMG_PROG',
  61. 'QEMU_IO_PROG', 'QEMU_NBD_PROG', 'QSD_PROG',
  62. 'QEMU_OPTIONS', 'QEMU_IMG_OPTIONS',
  63. 'QEMU_IO_OPTIONS', 'QEMU_IO_OPTIONS_NO_FMT',
  64. 'QEMU_NBD_OPTIONS', 'IMGOPTS', 'IMGFMT', 'IMGPROTO',
  65. 'AIOMODE', 'CACHEMODE', 'VALGRIND_QEMU',
  66. 'CACHEMODE_IS_DEFAULT', 'IMGFMT_GENERIC', 'IMGOPTSSYNTAX',
  67. 'IMGKEYSECRET', 'QEMU_DEFAULT_MACHINE', 'MALLOC_PERTURB_',
  68. 'GDB_OPTIONS', 'PRINT_QEMU']
  69. def prepare_subprocess(self, args: List[str]) -> Dict[str, str]:
  70. if self.debug:
  71. args.append('-d')
  72. with open(args[0], encoding="utf-8") as f:
  73. try:
  74. if f.readline().rstrip() == '#!/usr/bin/env python3':
  75. args.insert(0, self.python)
  76. except UnicodeDecodeError: # binary test? for future.
  77. pass
  78. os_env = os.environ.copy()
  79. os_env.update(self.get_env())
  80. return os_env
  81. def get_env(self) -> Dict[str, str]:
  82. env = {}
  83. for v in self.env_variables:
  84. val = getattr(self, v.lower(), None)
  85. if val is not None:
  86. env[v] = val
  87. return env
  88. def init_directories(self) -> None:
  89. """Init directory variables:
  90. PYTHONPATH
  91. TEST_DIR
  92. SOCK_DIR
  93. SAMPLE_IMG_DIR
  94. """
  95. # Path where qemu goodies live in this source tree.
  96. qemu_srctree_path = Path(__file__, '../../../python').resolve()
  97. self.pythonpath = os.pathsep.join(filter(None, (
  98. self.source_iotests,
  99. str(qemu_srctree_path),
  100. os.getenv('PYTHONPATH'),
  101. )))
  102. self.test_dir = os.getenv('TEST_DIR',
  103. os.path.join(os.getcwd(), 'scratch'))
  104. Path(self.test_dir).mkdir(parents=True, exist_ok=True)
  105. try:
  106. self.sock_dir = os.environ['SOCK_DIR']
  107. self.tmp_sock_dir = False
  108. Path(self.sock_dir).mkdir(parents=True, exist_ok=True)
  109. except KeyError:
  110. self.sock_dir = tempfile.mkdtemp(prefix="qemu-iotests-")
  111. self.tmp_sock_dir = True
  112. self.sample_img_dir = os.getenv('SAMPLE_IMG_DIR',
  113. os.path.join(self.source_iotests,
  114. 'sample_images'))
  115. def init_binaries(self) -> None:
  116. """Init binary path variables:
  117. PYTHON (for bash tests)
  118. QEMU_PROG, QEMU_IMG_PROG, QEMU_IO_PROG, QEMU_NBD_PROG, QSD_PROG
  119. """
  120. self.python = sys.executable
  121. def root(*names: str) -> str:
  122. return os.path.join(self.build_root, *names)
  123. arch = os.uname().machine
  124. if 'ppc64' in arch:
  125. arch = 'ppc64'
  126. self.qemu_prog = os.getenv('QEMU_PROG', root(f'qemu-system-{arch}'))
  127. if not os.path.exists(self.qemu_prog):
  128. pattern = root('qemu-system-*')
  129. try:
  130. progs = sorted(glob.iglob(pattern))
  131. self.qemu_prog = next(p for p in progs if isxfile(p))
  132. except StopIteration:
  133. sys.exit("Not found any Qemu executable binary by pattern "
  134. f"'{pattern}'")
  135. self.qemu_img_prog = os.getenv('QEMU_IMG_PROG', root('qemu-img'))
  136. self.qemu_io_prog = os.getenv('QEMU_IO_PROG', root('qemu-io'))
  137. self.qemu_nbd_prog = os.getenv('QEMU_NBD_PROG', root('qemu-nbd'))
  138. self.qsd_prog = os.getenv('QSD_PROG', root('storage-daemon',
  139. 'qemu-storage-daemon'))
  140. for b in [self.qemu_img_prog, self.qemu_io_prog, self.qemu_nbd_prog,
  141. self.qemu_prog, self.qsd_prog]:
  142. if not os.path.exists(b):
  143. sys.exit('No such file: ' + b)
  144. if not isxfile(b):
  145. sys.exit('Not executable: ' + b)
  146. def __init__(self, source_dir: str, build_dir: str,
  147. imgfmt: str, imgproto: str, aiomode: str,
  148. cachemode: Optional[str] = None,
  149. imgopts: Optional[str] = None,
  150. misalign: bool = False,
  151. debug: bool = False,
  152. valgrind: bool = False,
  153. gdb: bool = False,
  154. qprint: bool = False,
  155. dry_run: bool = False) -> None:
  156. self.imgfmt = imgfmt
  157. self.imgproto = imgproto
  158. self.aiomode = aiomode
  159. self.imgopts = imgopts
  160. self.misalign = misalign
  161. self.debug = debug
  162. if qprint:
  163. self.print_qemu = 'y'
  164. if gdb:
  165. self.gdb_options = os.getenv('GDB_OPTIONS', DEF_GDB_OPTIONS)
  166. if not self.gdb_options:
  167. # cover the case 'export GDB_OPTIONS='
  168. self.gdb_options = DEF_GDB_OPTIONS
  169. elif 'GDB_OPTIONS' in os.environ:
  170. # to not propagate it in prepare_subprocess()
  171. del os.environ['GDB_OPTIONS']
  172. if valgrind:
  173. self.valgrind_qemu = 'y'
  174. if cachemode is None:
  175. self.cachemode_is_default = 'true'
  176. self.cachemode = 'writeback'
  177. else:
  178. self.cachemode_is_default = 'false'
  179. self.cachemode = cachemode
  180. # Initialize generic paths: build_root, build_iotests, source_iotests,
  181. # which are needed to initialize some environment variables. They are
  182. # used by init_*() functions as well.
  183. self.source_iotests = source_dir
  184. self.build_iotests = build_dir
  185. self.build_root = Path(self.build_iotests).parent.parent
  186. self.init_directories()
  187. if dry_run:
  188. return
  189. self.init_binaries()
  190. self.malloc_perturb_ = os.getenv('MALLOC_PERTURB_',
  191. str(random.randrange(1, 255)))
  192. # QEMU_OPTIONS
  193. self.qemu_options = '-nodefaults -display none -accel qtest'
  194. machine_map = (
  195. ('arm', 'virt'),
  196. ('aarch64', 'virt'),
  197. ('avr', 'mega2560'),
  198. ('m68k', 'virt'),
  199. ('or1k', 'virt'),
  200. ('riscv32', 'virt'),
  201. ('riscv64', 'virt'),
  202. ('rx', 'gdbsim-r5f562n8'),
  203. ('sh4', 'r2d'),
  204. ('sh4eb', 'r2d'),
  205. ('tricore', 'tricore_testboard')
  206. )
  207. for suffix, machine in machine_map:
  208. if self.qemu_prog.endswith(f'qemu-system-{suffix}'):
  209. self.qemu_options += f' -machine {machine}'
  210. # QEMU_DEFAULT_MACHINE
  211. self.qemu_default_machine = get_default_machine(self.qemu_prog)
  212. self.qemu_img_options = os.getenv('QEMU_IMG_OPTIONS')
  213. self.qemu_nbd_options = os.getenv('QEMU_NBD_OPTIONS')
  214. is_generic = self.imgfmt not in ['bochs', 'cloop', 'dmg', 'vvfat']
  215. self.imgfmt_generic = 'true' if is_generic else 'false'
  216. self.qemu_io_options = f'--cache {self.cachemode} --aio {self.aiomode}'
  217. if self.misalign:
  218. self.qemu_io_options += ' --misalign'
  219. self.qemu_io_options_no_fmt = self.qemu_io_options
  220. if self.imgfmt == 'luks':
  221. self.imgoptssyntax = 'true'
  222. self.imgkeysecret = '123456'
  223. if not self.imgopts:
  224. self.imgopts = 'iter-time=10'
  225. elif 'iter-time=' not in self.imgopts:
  226. self.imgopts += ',iter-time=10'
  227. else:
  228. self.imgoptssyntax = 'false'
  229. self.qemu_io_options += ' -f ' + self.imgfmt
  230. if self.imgfmt == 'vmdk':
  231. if not self.imgopts:
  232. self.imgopts = 'zeroed_grain=on'
  233. elif 'zeroed_grain=' not in self.imgopts:
  234. self.imgopts += ',zeroed_grain=on'
  235. def close(self) -> None:
  236. if self.tmp_sock_dir:
  237. shutil.rmtree(self.sock_dir)
  238. def __enter__(self) -> 'TestEnv':
  239. return self
  240. def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
  241. self.close()
  242. def print_env(self, prefix: str = '') -> None:
  243. template = """\
  244. {prefix}QEMU -- "{QEMU_PROG}" {QEMU_OPTIONS}
  245. {prefix}QEMU_IMG -- "{QEMU_IMG_PROG}" {QEMU_IMG_OPTIONS}
  246. {prefix}QEMU_IO -- "{QEMU_IO_PROG}" {QEMU_IO_OPTIONS}
  247. {prefix}QEMU_NBD -- "{QEMU_NBD_PROG}" {QEMU_NBD_OPTIONS}
  248. {prefix}IMGFMT -- {IMGFMT}{imgopts}
  249. {prefix}IMGPROTO -- {IMGPROTO}
  250. {prefix}PLATFORM -- {platform}
  251. {prefix}TEST_DIR -- {TEST_DIR}
  252. {prefix}SOCK_DIR -- {SOCK_DIR}
  253. {prefix}GDB_OPTIONS -- {GDB_OPTIONS}
  254. {prefix}VALGRIND_QEMU -- {VALGRIND_QEMU}
  255. {prefix}PRINT_QEMU_OUTPUT -- {PRINT_QEMU}
  256. {prefix}"""
  257. args = collections.defaultdict(str, self.get_env())
  258. if 'IMGOPTS' in args:
  259. args['imgopts'] = f" ({args['IMGOPTS']})"
  260. u = os.uname()
  261. args['platform'] = f'{u.sysname}/{u.machine} {u.nodename} {u.release}'
  262. args['prefix'] = prefix
  263. print(template.format_map(args))