487 lines
20 KiB
Python
487 lines
20 KiB
Python
|
#!/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)
|