| #!/usr/bin/env python3 | |
| # | |
| # Script to compare machine type compatible properties (include/hw/core/boards.h). | |
| # compat_props are applied to the driver during initialization to change | |
| # default values, for instance, to maintain compatibility. | |
| # This script constructs table with machines and values of their compat_props | |
| # to compare and to find places for improvements or places with bugs. If | |
| # during the comparison, some machine type doesn't have a property (it is in | |
| # the comparison table because another machine type has it), then the | |
| # appropriate method will be used to obtain the default value of this driver | |
| # property via qmp command (e.g. query-cpu-model-expansion for x86_64-cpu). | |
| # These methods are defined below in qemu_property_methods. | |
| # | |
| # Copyright (c) Yandex Technologies LLC, 2023 | |
| # | |
| # This program is free software; you can redistribute it and/or modify | |
| # it under the terms of the GNU General Public License as published by | |
| # the Free Software Foundation; either version 2 of the License, or | |
| # (at your option) any later version. | |
| # | |
| # This program is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| # GNU General Public License for more details. | |
| # | |
| # You should have received a copy of the GNU General Public License | |
| # along with this program; if not, see <http://www.gnu.org/licenses/>. | |
| import sys | |
| from argparse import ArgumentParser, RawTextHelpFormatter, Namespace | |
| import pandas as pd | |
| from contextlib import ExitStack | |
| from typing import Optional, List, Dict, Generator, Tuple, Union, Any, Set | |
| try: | |
| from qemu.machine import QEMUMachine | |
| except ModuleNotFoundError as exc: | |
| print(f"Module '{exc.name}' not found.", file=sys.stderr) | |
| print(f"Try $builddir/run {' '.join(sys.argv)}", file=sys.stderr) | |
| sys.exit(1) | |
| default_qemu_args = '-enable-kvm -machine none' | |
| default_qemu_binary = 'build/qemu-system-x86_64' | |
| # Methods for gettig the right values of drivers properties | |
| # | |
| # Use these methods as a 'whitelist' and add entries only if necessary. It's | |
| # important to be stable and predictable in analysis and tests. | |
| # Be careful: | |
| # * Class must be inherited from 'QEMUObject' and used in new_driver() | |
| # * Class has to implement get_prop method in order to get values | |
| # * Specialization always wins (with the given classes for 'device' and | |
| # 'x86_64-cpu', method of 'x86_64-cpu' will be used for '486-x86_64-cpu') | |
| class Driver(): | |
| def __init__(self, vm: QEMUMachine, name: str, abstract: bool) -> None: | |
| self.vm = vm | |
| self.name = name | |
| self.abstract = abstract | |
| self.parent: Optional[Driver] = None | |
| self.property_getter: Optional[Driver] = None | |
| def get_prop(self, driver: str, prop: str) -> str: | |
| if self.property_getter: | |
| return self.property_getter.get_prop(driver, prop) | |
| else: | |
| return 'Unavailable method' | |
| def is_child_of(self, parent: 'Driver') -> bool: | |
| """Checks whether self is (recursive) child of @parent""" | |
| cur_parent = self.parent | |
| while cur_parent: | |
| if cur_parent is parent: | |
| return True | |
| cur_parent = cur_parent.parent | |
| return False | |
| def set_implementations(self, implementations: List['Driver']) -> None: | |
| self.implementations = implementations | |
| class QEMUObject(Driver): | |
| def __init__(self, vm: QEMUMachine, name: str) -> None: | |
| super().__init__(vm, name, True) | |
| def set_implementations(self, implementations: List[Driver]) -> None: | |
| self.implementations = implementations | |
| # each implementation of the abstract driver has to use property getter | |
| # of this abstract driver unless it has specialization. (e.g. having | |
| # 'device' and 'x86_64-cpu', property getter of 'x86_64-cpu' will be | |
| # used for '486-x86_64-cpu') | |
| for impl in implementations: | |
| if not impl.property_getter or\ | |
| self.is_child_of(impl.property_getter): | |
| impl.property_getter = self | |
| class QEMUDevice(QEMUObject): | |
| def __init__(self, vm: QEMUMachine) -> None: | |
| super().__init__(vm, 'device') | |
| self.cached: Dict[str, List[Dict[str, Any]]] = {} | |
| def get_prop(self, driver: str, prop_name: str) -> str: | |
| if driver not in self.cached: | |
| self.cached[driver] = self.vm.cmd('device-list-properties', | |
| typename=driver) | |
| for prop in self.cached[driver]: | |
| if prop['name'] == prop_name: | |
| return str(prop.get('default-value', 'No default value')) | |
| return 'Unknown property' | |
| class QEMUx86CPU(QEMUObject): | |
| def __init__(self, vm: QEMUMachine) -> None: | |
| super().__init__(vm, 'x86_64-cpu') | |
| self.cached: Dict[str, Dict[str, Any]] = {} | |
| def get_prop(self, driver: str, prop_name: str) -> str: | |
| if not driver.endswith('-x86_64-cpu'): | |
| return 'Wrong x86_64-cpu name' | |
| # crop last 11 chars '-x86_64-cpu' | |
| name = driver[:-11] | |
| if name not in self.cached: | |
| self.cached[name] = self.vm.cmd( | |
| 'query-cpu-model-expansion', type='full', | |
| model={'name': name})['model']['props'] | |
| return str(self.cached[name].get(prop_name, 'Unknown property')) | |
| # Now it's stub, because all memory_backend types don't have default values | |
| # but this behaviour can be changed | |
| class QEMUMemoryBackend(QEMUObject): | |
| def __init__(self, vm: QEMUMachine) -> None: | |
| super().__init__(vm, 'memory-backend') | |
| self.cached: Dict[str, List[Dict[str, Any]]] = {} | |
| def get_prop(self, driver: str, prop_name: str) -> str: | |
| if driver not in self.cached: | |
| self.cached[driver] = self.vm.cmd('qom-list-properties', | |
| typename=driver) | |
| for prop in self.cached[driver]: | |
| if prop['name'] == prop_name: | |
| return str(prop.get('default-value', 'No default value')) | |
| return 'Unknown property' | |
| def new_driver(vm: QEMUMachine, name: str, is_abstr: bool) -> Driver: | |
| if name == 'object': | |
| return QEMUObject(vm, 'object') | |
| elif name == 'device': | |
| return QEMUDevice(vm) | |
| elif name == 'x86_64-cpu': | |
| return QEMUx86CPU(vm) | |
| elif name == 'memory-backend': | |
| return QEMUMemoryBackend(vm) | |
| else: | |
| return Driver(vm, name, is_abstr) | |
| # End of methods definition | |
| class VMPropertyGetter: | |
| """It implements the relationship between drivers and how to get their | |
| properties""" | |
| def __init__(self, vm: QEMUMachine) -> None: | |
| self.drivers: Dict[str, Driver] = {} | |
| qom_all_types = vm.cmd('qom-list-types', abstract=True) | |
| self.drivers = {t['name']: new_driver(vm, t['name'], | |
| t.get('abstract', False)) | |
| for t in qom_all_types} | |
| for t in qom_all_types: | |
| drv = self.drivers[t['name']] | |
| if 'parent' in t: | |
| drv.parent = self.drivers[t['parent']] | |
| for drv in self.drivers.values(): | |
| imps = vm.cmd('qom-list-types', implements=drv.name) | |
| # only implementations inherit property getter | |
| drv.set_implementations([self.drivers[imp['name']] | |
| for imp in imps]) | |
| def get_prop(self, driver: str, prop: str) -> str: | |
| # wrong driver name or disabled in config driver | |
| try: | |
| drv = self.drivers[driver] | |
| except KeyError: | |
| return 'Unavailable driver' | |
| assert not drv.abstract | |
| return drv.get_prop(driver, prop) | |
| def get_implementations(self, driver: str) -> List[str]: | |
| return [impl.name for impl in self.drivers[driver].implementations] | |
| class Machine: | |
| """A short QEMU machine type description. It contains only processed | |
| compat_props (properties of abstract classes are applied to its | |
| implementations) | |
| """ | |
| # raw_mt_dict - dict produced by `query-machines` | |
| def __init__(self, raw_mt_dict: Dict[str, Any], | |
| qemu_drivers: VMPropertyGetter) -> None: | |
| self.name = raw_mt_dict['name'] | |
| self.compat_props: Dict[str, Any] = {} | |
| # properties are applied sequentially and can rewrite values like in | |
| # QEMU. Also it has to resolve class relationships to apply appropriate | |
| # values from abstract class to all implementations | |
| for prop in raw_mt_dict['compat-props']: | |
| driver = prop['qom-type'] | |
| try: | |
| # implementation adds only itself, abstract class adds | |
| # lementation (abstract classes are uninterestiong) | |
| impls = qemu_drivers.get_implementations(driver) | |
| for impl in impls: | |
| if impl not in self.compat_props: | |
| self.compat_props[impl] = {} | |
| self.compat_props[impl][prop['property']] = prop['value'] | |
| except KeyError: | |
| # QEMU doesn't know this driver thus it has to be saved | |
| if driver not in self.compat_props: | |
| self.compat_props[driver] = {} | |
| self.compat_props[driver][prop['property']] = prop['value'] | |
| class Configuration(): | |
| """Class contains all necessary components to generate table and is used | |
| to compare different binaries""" | |
| def __init__(self, vm: QEMUMachine, | |
| req_mt: List[str], all_mt: bool) -> None: | |
| self._vm = vm | |
| self._binary = vm.binary | |
| self._qemu_args = args.qemu_args.split(' ') | |
| self._qemu_drivers = VMPropertyGetter(vm) | |
| self.req_mt = get_req_mt(self._qemu_drivers, vm, req_mt, all_mt) | |
| def get_implementations(self, driver_name: str) -> List[str]: | |
| return self._qemu_drivers.get_implementations(driver_name) | |
| def get_table(self, req_props: List[Tuple[str, str]]) -> pd.DataFrame: | |
| table: List[pd.DataFrame] = [] | |
| for mt in self.req_mt: | |
| name = f'{self._binary}\n{mt.name}' | |
| column = [] | |
| for driver, prop in req_props: | |
| try: | |
| # values from QEMU machine type definitions | |
| column.append(mt.compat_props[driver][prop]) | |
| except KeyError: | |
| # values from QEMU type definitions | |
| column.append(self._qemu_drivers.get_prop(driver, prop)) | |
| table.append(pd.DataFrame({name: column})) | |
| return pd.concat(table, axis=1) | |
| script_desc = """Script to compare machine types (their compat_props). | |
| Examples: | |
| * save info about all machines: ./scripts/compare-machine-types.py --all \ | |
| --format csv --raw > table.csv | |
| * compare machines: ./scripts/compare-machine-types.py --mt pc-q35-2.12 \ | |
| pc-q35-3.0 | |
| * compare binaries and machines: ./scripts/compare-machine-types.py \ | |
| --mt pc-q35-6.2 pc-q35-7.0 --qemu-binary build/qemu-system-x86_64 \ | |
| build/qemu-exp | |
| ╒════════════╤══════════════════════════╤════════════════════════════\ | |
| ╤════════════════════════════╤══════════════════╤══════════════════╕ | |
| │ Driver │ Property │ build/qemu-system-x86_64 \ | |
| │ build/qemu-system-x86_64 │ build/qemu-exp │ build/qemu-exp │ | |
| │ │ │ pc-q35-6.2 \ | |
| │ pc-q35-7.0 │ pc-q35-6.2 │ pc-q35-7.0 │ | |
| ╞════════════╪══════════════════════════╪════════════════════════════\ | |
| ╪════════════════════════════╪══════════════════╪══════════════════╡ | |
| │ PIIX4_PM │ x-not-migrate-acpi-index │ True \ | |
| │ False │ False │ False │ | |
| ├────────────┼──────────────────────────┼────────────────────────────\ | |
| ┼────────────────────────────┼──────────────────┼──────────────────┤ | |
| │ virtio-mem │ unplugged-inaccessible │ False \ | |
| │ auto │ False │ auto │ | |
| ╘════════════╧══════════════════════════╧════════════════════════════\ | |
| ╧════════════════════════════╧══════════════════╧══════════════════╛ | |
| If a property from QEMU machine defintion applies to an abstract class (e.g. \ | |
| x86_64-cpu) this script will compare all implementations of this class. | |
| "Unavailable method" - means that this script doesn't know how to get \ | |
| default values of the driver. To add method use the construction described \ | |
| at the top of the script. | |
| "Unavailable driver" - means that this script doesn't know this driver. \ | |
| For instance, this can happen if you configure QEMU without this device or \ | |
| if machine type definition has error. | |
| "No default value" - means that the appropriate method can't get the default \ | |
| value and most likely that this property doesn't have it. | |
| "Unknown property" - means that the appropriate method can't find property \ | |
| with this name.""" | |
| def parse_args() -> Namespace: | |
| parser = ArgumentParser(formatter_class=RawTextHelpFormatter, | |
| description=script_desc) | |
| parser.add_argument('--format', choices=['human-readable', 'json', 'csv'], | |
| default='human-readable', | |
| help='returns table in json format') | |
| parser.add_argument('--raw', action='store_true', | |
| help='prints ALL defined properties without value ' | |
| 'transformation. By default, only rows ' | |
| 'with different values will be printed and ' | |
| 'values will be transformed(e.g. "on" -> True)') | |
| parser.add_argument('--qemu-args', default=default_qemu_args, | |
| help='command line to start qemu. ' | |
| f'Default: "{default_qemu_args}"') | |
| parser.add_argument('--qemu-binary', nargs="*", type=str, | |
| default=[default_qemu_binary], | |
| help='list of qemu binaries that will be compared. ' | |
| f'Deafult: {default_qemu_binary}') | |
| mt_args_group = parser.add_mutually_exclusive_group() | |
| mt_args_group.add_argument('--all', action='store_true', | |
| help='prints all available machine types (list ' | |
| 'of machine types will be ignored)') | |
| mt_args_group.add_argument('--mt', nargs="*", type=str, | |
| help='list of Machine Types ' | |
| 'that will be compared') | |
| return parser.parse_args() | |
| def mt_comp(mt: Machine) -> Tuple[str, int, int, int]: | |
| """Function to compare and sort machine by names. | |
| It returns socket_name, major version, minor version, revision""" | |
| # none, microvm, x-remote and etc. | |
| if '-' not in mt.name or '.' not in mt.name: | |
| return mt.name, 0, 0, 0 | |
| socket, ver = mt.name.rsplit('-', 1) | |
| ver_list = list(map(int, ver.split('.', 2))) | |
| ver_list += [0] * (3 - len(ver_list)) | |
| return socket, ver_list[0], ver_list[1], ver_list[2] | |
| def get_mt_definitions(qemu_drivers: VMPropertyGetter, | |
| vm: QEMUMachine) -> List[Machine]: | |
| """Constructs list of machine definitions (primarily compat_props) via | |
| info from QEMU""" | |
| raw_mt_defs = vm.cmd('query-machines', compat_props=True) | |
| mt_defs = [] | |
| for raw_mt in raw_mt_defs: | |
| mt_defs.append(Machine(raw_mt, qemu_drivers)) | |
| mt_defs.sort(key=mt_comp) | |
| return mt_defs | |
| def get_req_mt(qemu_drivers: VMPropertyGetter, vm: QEMUMachine, | |
| req_mt: Optional[List[str]], all_mt: bool) -> List[Machine]: | |
| """Returns list of requested by user machines""" | |
| mt_defs = get_mt_definitions(qemu_drivers, vm) | |
| if all_mt: | |
| return mt_defs | |
| if req_mt is None: | |
| print('Enter machine types for comparision') | |
| exit(0) | |
| matched_mt = [] | |
| for mt in mt_defs: | |
| if mt.name in req_mt: | |
| matched_mt.append(mt) | |
| return matched_mt | |
| def get_affected_props(configs: List[Configuration]) -> Generator[Tuple[str, | |
| str], | |
| None, None]: | |
| """Helps to go through all affected in machine definitions drivers | |
| and properties""" | |
| driver_props: Dict[str, Set[Any]] = {} | |
| for config in configs: | |
| for mt in config.req_mt: | |
| compat_props = mt.compat_props | |
| for driver, prop in compat_props.items(): | |
| if driver not in driver_props: | |
| driver_props[driver] = set() | |
| driver_props[driver].update(prop.keys()) | |
| for driver, props in sorted(driver_props.items()): | |
| for prop in sorted(props): | |
| yield driver, prop | |
| def transform_value(value: str) -> Union[str, bool]: | |
| true_list = ['true', 'on'] | |
| false_list = ['false', 'off'] | |
| out = value.lower() | |
| if out in true_list: | |
| return True | |
| if out in false_list: | |
| return False | |
| return value | |
| def simplify_table(table: pd.DataFrame) -> pd.DataFrame: | |
| """transforms values to make it easier to compare it and drops rows | |
| with the same values for all columns""" | |
| table = table.map(transform_value) | |
| return table[~table.iloc[:, 3:].eq(table.iloc[:, 2], axis=0).all(axis=1)] | |
| # constructs table in the format: | |
| # | |
| # Driver | Property | binary1 | binary1 | ... | |
| # | | machine1 | machine2 | ... | |
| # ------------------------------------------------------ ... | |
| # driver1 | property1 | value1 | value2 | ... | |
| # driver1 | property2 | value3 | value4 | ... | |
| # driver2 | property3 | value5 | value6 | ... | |
| # ... | ... | ... | ... | ... | |
| # | |
| def fill_prop_table(configs: List[Configuration], | |
| is_raw: bool) -> pd.DataFrame: | |
| req_props = list(get_affected_props(configs)) | |
| if not req_props: | |
| print('No drivers to compare. Check machine names') | |
| exit(0) | |
| driver_col, prop_col = tuple(zip(*req_props)) | |
| table = [pd.DataFrame({'Driver': driver_col}), | |
| pd.DataFrame({'Property': prop_col})] | |
| table.extend([config.get_table(req_props) for config in configs]) | |
| df_table = pd.concat(table, axis=1) | |
| if is_raw: | |
| return df_table | |
| return simplify_table(df_table) | |
| def print_table(table: pd.DataFrame, table_format: str) -> None: | |
| if table_format == 'json': | |
| print(comp_table.to_json()) | |
| elif table_format == 'csv': | |
| print(comp_table.to_csv()) | |
| else: | |
| print(comp_table.to_markdown(index=False, stralign='center', | |
| colalign=('center',), headers='keys', | |
| tablefmt='fancy_grid', | |
| disable_numparse=True)) | |
| if __name__ == '__main__': | |
| args = parse_args() | |
| with ExitStack() as stack: | |
| vms = [stack.enter_context(QEMUMachine(binary=binary, qmp_timer=15, | |
| args=args.qemu_args.split(' '))) for binary in args.qemu_binary] | |
| configurations = [] | |
| for vm in vms: | |
| vm.launch() | |
| configurations.append(Configuration(vm, args.mt, args.all)) | |
| comp_table = fill_prop_table(configurations, args.raw) | |
| if not comp_table.empty: | |
| print_table(comp_table, args.format) |