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