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