compare-machine-types.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. #!/usr/bin/env python3
  2. #
  3. # Script to compare machine type compatible properties (include/hw/boards.h).
  4. # compat_props are applied to the driver during initialization to change
  5. # default values, for instance, to maintain compatibility.
  6. # This script constructs table with machines and values of their compat_props
  7. # to compare and to find places for improvements or places with bugs. If
  8. # during the comparison, some machine type doesn't have a property (it is in
  9. # the comparison table because another machine type has it), then the
  10. # appropriate method will be used to obtain the default value of this driver
  11. # property via qmp command (e.g. query-cpu-model-expansion for x86_64-cpu).
  12. # These methods are defined below in qemu_property_methods.
  13. #
  14. # Copyright (c) Yandex Technologies LLC, 2023
  15. #
  16. # This program is free software; you can redistribute it and/or modify
  17. # it under the terms of the GNU General Public License as published by
  18. # the Free Software Foundation; either version 2 of the License, or
  19. # (at your option) any later version.
  20. #
  21. # This program is distributed in the hope that it will be useful,
  22. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  23. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  24. # GNU General Public License for more details.
  25. #
  26. # You should have received a copy of the GNU General Public License
  27. # along with this program; if not, see <http://www.gnu.org/licenses/>.
  28. import sys
  29. from os import path
  30. from argparse import ArgumentParser, RawTextHelpFormatter, Namespace
  31. import pandas as pd
  32. from contextlib import ExitStack
  33. from typing import Optional, List, Dict, Generator, Tuple, Union, Any, Set
  34. try:
  35. qemu_dir = path.abspath(path.dirname(path.dirname(__file__)))
  36. sys.path.append(path.join(qemu_dir, 'python'))
  37. from qemu.machine import QEMUMachine
  38. except ModuleNotFoundError as exc:
  39. print(f"Module '{exc.name}' not found.")
  40. print("Try export PYTHONPATH=top-qemu-dir/python or run from top-qemu-dir")
  41. sys.exit(1)
  42. default_qemu_args = '-enable-kvm -machine none'
  43. default_qemu_binary = 'build/qemu-system-x86_64'
  44. # Methods for gettig the right values of drivers properties
  45. #
  46. # Use these methods as a 'whitelist' and add entries only if necessary. It's
  47. # important to be stable and predictable in analysis and tests.
  48. # Be careful:
  49. # * Class must be inherited from 'QEMUObject' and used in new_driver()
  50. # * Class has to implement get_prop method in order to get values
  51. # * Specialization always wins (with the given classes for 'device' and
  52. # 'x86_64-cpu', method of 'x86_64-cpu' will be used for '486-x86_64-cpu')
  53. class Driver():
  54. def __init__(self, vm: QEMUMachine, name: str, abstract: bool) -> None:
  55. self.vm = vm
  56. self.name = name
  57. self.abstract = abstract
  58. self.parent: Optional[Driver] = None
  59. self.property_getter: Optional[Driver] = None
  60. def get_prop(self, driver: str, prop: str) -> str:
  61. if self.property_getter:
  62. return self.property_getter.get_prop(driver, prop)
  63. else:
  64. return 'Unavailable method'
  65. def is_child_of(self, parent: 'Driver') -> bool:
  66. """Checks whether self is (recursive) child of @parent"""
  67. cur_parent = self.parent
  68. while cur_parent:
  69. if cur_parent is parent:
  70. return True
  71. cur_parent = cur_parent.parent
  72. return False
  73. def set_implementations(self, implementations: List['Driver']) -> None:
  74. self.implementations = implementations
  75. class QEMUObject(Driver):
  76. def __init__(self, vm: QEMUMachine, name: str) -> None:
  77. super().__init__(vm, name, True)
  78. def set_implementations(self, implementations: List[Driver]) -> None:
  79. self.implementations = implementations
  80. # each implementation of the abstract driver has to use property getter
  81. # of this abstract driver unless it has specialization. (e.g. having
  82. # 'device' and 'x86_64-cpu', property getter of 'x86_64-cpu' will be
  83. # used for '486-x86_64-cpu')
  84. for impl in implementations:
  85. if not impl.property_getter or\
  86. self.is_child_of(impl.property_getter):
  87. impl.property_getter = self
  88. class QEMUDevice(QEMUObject):
  89. def __init__(self, vm: QEMUMachine) -> None:
  90. super().__init__(vm, 'device')
  91. self.cached: Dict[str, List[Dict[str, Any]]] = {}
  92. def get_prop(self, driver: str, prop_name: str) -> str:
  93. if driver not in self.cached:
  94. self.cached[driver] = self.vm.cmd('device-list-properties',
  95. typename=driver)
  96. for prop in self.cached[driver]:
  97. if prop['name'] == prop_name:
  98. return str(prop.get('default-value', 'No default value'))
  99. return 'Unknown property'
  100. class QEMUx86CPU(QEMUObject):
  101. def __init__(self, vm: QEMUMachine) -> None:
  102. super().__init__(vm, 'x86_64-cpu')
  103. self.cached: Dict[str, Dict[str, Any]] = {}
  104. def get_prop(self, driver: str, prop_name: str) -> str:
  105. if not driver.endswith('-x86_64-cpu'):
  106. return 'Wrong x86_64-cpu name'
  107. # crop last 11 chars '-x86_64-cpu'
  108. name = driver[:-11]
  109. if name not in self.cached:
  110. self.cached[name] = self.vm.cmd(
  111. 'query-cpu-model-expansion', type='full',
  112. model={'name': name})['model']['props']
  113. return str(self.cached[name].get(prop_name, 'Unknown property'))
  114. # Now it's stub, because all memory_backend types don't have default values
  115. # but this behaviour can be changed
  116. class QEMUMemoryBackend(QEMUObject):
  117. def __init__(self, vm: QEMUMachine) -> None:
  118. super().__init__(vm, 'memory-backend')
  119. self.cached: Dict[str, List[Dict[str, Any]]] = {}
  120. def get_prop(self, driver: str, prop_name: str) -> str:
  121. if driver not in self.cached:
  122. self.cached[driver] = self.vm.cmd('qom-list-properties',
  123. typename=driver)
  124. for prop in self.cached[driver]:
  125. if prop['name'] == prop_name:
  126. return str(prop.get('default-value', 'No default value'))
  127. return 'Unknown property'
  128. def new_driver(vm: QEMUMachine, name: str, is_abstr: bool) -> Driver:
  129. if name == 'object':
  130. return QEMUObject(vm, 'object')
  131. elif name == 'device':
  132. return QEMUDevice(vm)
  133. elif name == 'x86_64-cpu':
  134. return QEMUx86CPU(vm)
  135. elif name == 'memory-backend':
  136. return QEMUMemoryBackend(vm)
  137. else:
  138. return Driver(vm, name, is_abstr)
  139. # End of methods definition
  140. class VMPropertyGetter:
  141. """It implements the relationship between drivers and how to get their
  142. properties"""
  143. def __init__(self, vm: QEMUMachine) -> None:
  144. self.drivers: Dict[str, Driver] = {}
  145. qom_all_types = vm.cmd('qom-list-types', abstract=True)
  146. self.drivers = {t['name']: new_driver(vm, t['name'],
  147. t.get('abstract', False))
  148. for t in qom_all_types}
  149. for t in qom_all_types:
  150. drv = self.drivers[t['name']]
  151. if 'parent' in t:
  152. drv.parent = self.drivers[t['parent']]
  153. for drv in self.drivers.values():
  154. imps = vm.cmd('qom-list-types', implements=drv.name)
  155. # only implementations inherit property getter
  156. drv.set_implementations([self.drivers[imp['name']]
  157. for imp in imps])
  158. def get_prop(self, driver: str, prop: str) -> str:
  159. # wrong driver name or disabled in config driver
  160. try:
  161. drv = self.drivers[driver]
  162. except KeyError:
  163. return 'Unavailable driver'
  164. assert not drv.abstract
  165. return drv.get_prop(driver, prop)
  166. def get_implementations(self, driver: str) -> List[str]:
  167. return [impl.name for impl in self.drivers[driver].implementations]
  168. class Machine:
  169. """A short QEMU machine type description. It contains only processed
  170. compat_props (properties of abstract classes are applied to its
  171. implementations)
  172. """
  173. # raw_mt_dict - dict produced by `query-machines`
  174. def __init__(self, raw_mt_dict: Dict[str, Any],
  175. qemu_drivers: VMPropertyGetter) -> None:
  176. self.name = raw_mt_dict['name']
  177. self.compat_props: Dict[str, Any] = {}
  178. # properties are applied sequentially and can rewrite values like in
  179. # QEMU. Also it has to resolve class relationships to apply appropriate
  180. # values from abstract class to all implementations
  181. for prop in raw_mt_dict['compat-props']:
  182. driver = prop['qom-type']
  183. try:
  184. # implementation adds only itself, abstract class adds
  185. # lementation (abstract classes are uninterestiong)
  186. impls = qemu_drivers.get_implementations(driver)
  187. for impl in impls:
  188. if impl not in self.compat_props:
  189. self.compat_props[impl] = {}
  190. self.compat_props[impl][prop['property']] = prop['value']
  191. except KeyError:
  192. # QEMU doesn't know this driver thus it has to be saved
  193. if driver not in self.compat_props:
  194. self.compat_props[driver] = {}
  195. self.compat_props[driver][prop['property']] = prop['value']
  196. class Configuration():
  197. """Class contains all necessary components to generate table and is used
  198. to compare different binaries"""
  199. def __init__(self, vm: QEMUMachine,
  200. req_mt: List[str], all_mt: bool) -> None:
  201. self._vm = vm
  202. self._binary = vm.binary
  203. self._qemu_args = args.qemu_args.split(' ')
  204. self._qemu_drivers = VMPropertyGetter(vm)
  205. self.req_mt = get_req_mt(self._qemu_drivers, vm, req_mt, all_mt)
  206. def get_implementations(self, driver_name: str) -> List[str]:
  207. return self._qemu_drivers.get_implementations(driver_name)
  208. def get_table(self, req_props: List[Tuple[str, str]]) -> pd.DataFrame:
  209. table: List[pd.DataFrame] = []
  210. for mt in self.req_mt:
  211. name = f'{self._binary}\n{mt.name}'
  212. column = []
  213. for driver, prop in req_props:
  214. try:
  215. # values from QEMU machine type definitions
  216. column.append(mt.compat_props[driver][prop])
  217. except KeyError:
  218. # values from QEMU type definitions
  219. column.append(self._qemu_drivers.get_prop(driver, prop))
  220. table.append(pd.DataFrame({name: column}))
  221. return pd.concat(table, axis=1)
  222. script_desc = """Script to compare machine types (their compat_props).
  223. Examples:
  224. * save info about all machines: ./scripts/compare-machine-types.py --all \
  225. --format csv --raw > table.csv
  226. * compare machines: ./scripts/compare-machine-types.py --mt pc-q35-2.12 \
  227. pc-q35-3.0
  228. * compare binaries and machines: ./scripts/compare-machine-types.py \
  229. --mt pc-q35-6.2 pc-q35-7.0 --qemu-binary build/qemu-system-x86_64 \
  230. build/qemu-exp
  231. ╒════════════╤══════════════════════════╤════════════════════════════\
  232. ╤════════════════════════════╤══════════════════╤══════════════════╕
  233. │ Driver │ Property │ build/qemu-system-x86_64 \
  234. │ build/qemu-system-x86_64 │ build/qemu-exp │ build/qemu-exp │
  235. │ │ │ pc-q35-6.2 \
  236. │ pc-q35-7.0 │ pc-q35-6.2 │ pc-q35-7.0 │
  237. ╞════════════╪══════════════════════════╪════════════════════════════\
  238. ╪════════════════════════════╪══════════════════╪══════════════════╡
  239. │ PIIX4_PM │ x-not-migrate-acpi-index │ True \
  240. │ False │ False │ False │
  241. ├────────────┼──────────────────────────┼────────────────────────────\
  242. ┼────────────────────────────┼──────────────────┼──────────────────┤
  243. │ virtio-mem │ unplugged-inaccessible │ False \
  244. │ auto │ False │ auto │
  245. ╘════════════╧══════════════════════════╧════════════════════════════\
  246. ╧════════════════════════════╧══════════════════╧══════════════════╛
  247. If a property from QEMU machine defintion applies to an abstract class (e.g. \
  248. x86_64-cpu) this script will compare all implementations of this class.
  249. "Unavailable method" - means that this script doesn't know how to get \
  250. default values of the driver. To add method use the construction described \
  251. at the top of the script.
  252. "Unavailable driver" - means that this script doesn't know this driver. \
  253. For instance, this can happen if you configure QEMU without this device or \
  254. if machine type definition has error.
  255. "No default value" - means that the appropriate method can't get the default \
  256. value and most likely that this property doesn't have it.
  257. "Unknown property" - means that the appropriate method can't find property \
  258. with this name."""
  259. def parse_args() -> Namespace:
  260. parser = ArgumentParser(formatter_class=RawTextHelpFormatter,
  261. description=script_desc)
  262. parser.add_argument('--format', choices=['human-readable', 'json', 'csv'],
  263. default='human-readable',
  264. help='returns table in json format')
  265. parser.add_argument('--raw', action='store_true',
  266. help='prints ALL defined properties without value '
  267. 'transformation. By default, only rows '
  268. 'with different values will be printed and '
  269. 'values will be transformed(e.g. "on" -> True)')
  270. parser.add_argument('--qemu-args', default=default_qemu_args,
  271. help='command line to start qemu. '
  272. f'Default: "{default_qemu_args}"')
  273. parser.add_argument('--qemu-binary', nargs="*", type=str,
  274. default=[default_qemu_binary],
  275. help='list of qemu binaries that will be compared. '
  276. f'Deafult: {default_qemu_binary}')
  277. mt_args_group = parser.add_mutually_exclusive_group()
  278. mt_args_group.add_argument('--all', action='store_true',
  279. help='prints all available machine types (list '
  280. 'of machine types will be ignored)')
  281. mt_args_group.add_argument('--mt', nargs="*", type=str,
  282. help='list of Machine Types '
  283. 'that will be compared')
  284. return parser.parse_args()
  285. def mt_comp(mt: Machine) -> Tuple[str, int, int, int]:
  286. """Function to compare and sort machine by names.
  287. It returns socket_name, major version, minor version, revision"""
  288. # none, microvm, x-remote and etc.
  289. if '-' not in mt.name or '.' not in mt.name:
  290. return mt.name, 0, 0, 0
  291. socket, ver = mt.name.rsplit('-', 1)
  292. ver_list = list(map(int, ver.split('.', 2)))
  293. ver_list += [0] * (3 - len(ver_list))
  294. return socket, ver_list[0], ver_list[1], ver_list[2]
  295. def get_mt_definitions(qemu_drivers: VMPropertyGetter,
  296. vm: QEMUMachine) -> List[Machine]:
  297. """Constructs list of machine definitions (primarily compat_props) via
  298. info from QEMU"""
  299. raw_mt_defs = vm.cmd('query-machines', compat_props=True)
  300. mt_defs = []
  301. for raw_mt in raw_mt_defs:
  302. mt_defs.append(Machine(raw_mt, qemu_drivers))
  303. mt_defs.sort(key=mt_comp)
  304. return mt_defs
  305. def get_req_mt(qemu_drivers: VMPropertyGetter, vm: QEMUMachine,
  306. req_mt: Optional[List[str]], all_mt: bool) -> List[Machine]:
  307. """Returns list of requested by user machines"""
  308. mt_defs = get_mt_definitions(qemu_drivers, vm)
  309. if all_mt:
  310. return mt_defs
  311. if req_mt is None:
  312. print('Enter machine types for comparision')
  313. exit(0)
  314. matched_mt = []
  315. for mt in mt_defs:
  316. if mt.name in req_mt:
  317. matched_mt.append(mt)
  318. return matched_mt
  319. def get_affected_props(configs: List[Configuration]) -> Generator[Tuple[str,
  320. str],
  321. None, None]:
  322. """Helps to go through all affected in machine definitions drivers
  323. and properties"""
  324. driver_props: Dict[str, Set[Any]] = {}
  325. for config in configs:
  326. for mt in config.req_mt:
  327. compat_props = mt.compat_props
  328. for driver, prop in compat_props.items():
  329. if driver not in driver_props:
  330. driver_props[driver] = set()
  331. driver_props[driver].update(prop.keys())
  332. for driver, props in sorted(driver_props.items()):
  333. for prop in sorted(props):
  334. yield driver, prop
  335. def transform_value(value: str) -> Union[str, bool]:
  336. true_list = ['true', 'on']
  337. false_list = ['false', 'off']
  338. out = value.lower()
  339. if out in true_list:
  340. return True
  341. if out in false_list:
  342. return False
  343. return value
  344. def simplify_table(table: pd.DataFrame) -> pd.DataFrame:
  345. """transforms values to make it easier to compare it and drops rows
  346. with the same values for all columns"""
  347. table = table.map(transform_value)
  348. return table[~table.iloc[:, 3:].eq(table.iloc[:, 2], axis=0).all(axis=1)]
  349. # constructs table in the format:
  350. #
  351. # Driver | Property | binary1 | binary1 | ...
  352. # | | machine1 | machine2 | ...
  353. # ------------------------------------------------------ ...
  354. # driver1 | property1 | value1 | value2 | ...
  355. # driver1 | property2 | value3 | value4 | ...
  356. # driver2 | property3 | value5 | value6 | ...
  357. # ... | ... | ... | ... | ...
  358. #
  359. def fill_prop_table(configs: List[Configuration],
  360. is_raw: bool) -> pd.DataFrame:
  361. req_props = list(get_affected_props(configs))
  362. if not req_props:
  363. print('No drivers to compare. Check machine names')
  364. exit(0)
  365. driver_col, prop_col = tuple(zip(*req_props))
  366. table = [pd.DataFrame({'Driver': driver_col}),
  367. pd.DataFrame({'Property': prop_col})]
  368. table.extend([config.get_table(req_props) for config in configs])
  369. df_table = pd.concat(table, axis=1)
  370. if is_raw:
  371. return df_table
  372. return simplify_table(df_table)
  373. def print_table(table: pd.DataFrame, table_format: str) -> None:
  374. if table_format == 'json':
  375. print(comp_table.to_json())
  376. elif table_format == 'csv':
  377. print(comp_table.to_csv())
  378. else:
  379. print(comp_table.to_markdown(index=False, stralign='center',
  380. colalign=('center',), headers='keys',
  381. tablefmt='fancy_grid',
  382. disable_numparse=True))
  383. if __name__ == '__main__':
  384. args = parse_args()
  385. with ExitStack() as stack:
  386. vms = [stack.enter_context(QEMUMachine(binary=binary, qmp_timer=15,
  387. args=args.qemu_args.split(' '))) for binary in args.qemu_binary]
  388. configurations = []
  389. for vm in vms:
  390. vm.launch()
  391. configurations.append(Configuration(vm, args.mt, args.all))
  392. comp_table = fill_prop_table(configurations, args.raw)
  393. if not comp_table.empty:
  394. print_table(comp_table, args.format)