master
py 483 lines 20 KB
Raw
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)