|  | #!/usr/bin/env python3 | 
|  | # | 
|  | # Script to compare machine type compatible properties (include/hw/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 os import path | 
|  | 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: | 
|  | qemu_dir = path.abspath(path.dirname(path.dirname(__file__))) | 
|  | sys.path.append(path.join(qemu_dir, 'python')) | 
|  | from qemu.machine import QEMUMachine | 
|  | except ModuleNotFoundError as exc: | 
|  | print(f"Module '{exc.name}' not found.") | 
|  | print("Try export PYTHONPATH=top-qemu-dir/python or run from top-qemu-dir") | 
|  | 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) |