1#!/usr/bin/env python3 2# 3# Copyright (C) 2022 Intel Corporation. 4# 5# SPDX-License-Identifier: BSD-3-Clause 6# 7 8import re 9 10import os 11import sys 12 13import copy 14import shlex 15import argparse 16 17import logging 18 19from defusedxml.lxml import parse 20 21 22def eval_xpath(element, xpath, default_value=None): 23 return next(iter(element.xpath(xpath)), default_value) 24 25 26def eval_xpath_all(element, xpath): 27 return element.xpath(xpath) 28 29 30class LaunchScript: 31 script_template_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "launch_script_template.sh") 32 33 class VirtualBDFAllocator: 34 def __init__(self): 35 # Reserved slots: 36 # 0 - For (virtual) hostbridge 37 # 1 - For (virtual) LPC 38 # 2 - For passthrough integarted GPU (either PF or VF) 39 # 31 - For LPC bridge needed by integrated GPU 40 self._free_slots = list(range(3, 30)) 41 42 def get_virtual_bdf(self, device_etree=None, options=None): 43 if device_etree is not None: 44 bus = eval_xpath(device_etree, "../@address") 45 vendor_id = eval_xpath(device_etree, "vendor/text()") 46 class_code = eval_xpath(device_etree, "class/text()") 47 48 # VGA-compatible controller, either integrated or discrete GPU 49 if class_code == "0x030000": 50 return 2 51 52 if options: 53 if "igd" in options: 54 return 2 55 56 next_vbdf = self._free_slots.pop(0) 57 return next_vbdf 58 59 def remove_virtual_bdf(self, slot): 60 if slot in self._free_slots: 61 self._free_slots.remove(slot) 62 63 class PassThruDeviceOptions: 64 passthru_device_options = { 65 "0x0200": [".//PTM[text()='y']", "enable_ptm"], # Ethernet controller, added if PTM is enabled for the VM 66 "0x0c0330": [".//os_type[text()='Windows OS']", "d3hot_reset"], 67 } 68 69 def __init__(self, vm_scenario_etree): 70 self._options = copy.copy(self.passthru_device_options) 71 72 def get_option(self, device_etree, vm_scenario_etree): 73 passthru_options = [] 74 if device_etree is not None: 75 class_code = eval_xpath(device_etree, "class/text()", "") 76 for k, v in self._options.items(): 77 if class_code.startswith(k) and vm_scenario_etree.xpath(v[0]): 78 passthru_options.extend(v[1:]) 79 return ",".join(passthru_options) 80 81 def __init__(self, board_etree, vm_name, vm_scenario_etree): 82 self._board_etree = board_etree 83 self._vm_scenario_etree = vm_scenario_etree 84 85 self._vm_name = vm_name 86 self._vm_descriptors = {} 87 self._init_commands = [] 88 self._cpu_dict = {} 89 self._dm_parameters = [] 90 self._deinit_commands = [] 91 92 self._vbdf_allocator = self.VirtualBDFAllocator() 93 self._passthru_options = self.PassThruDeviceOptions(vm_scenario_etree) 94 95 def add_vm_descriptor(self, name, value): 96 self._vm_descriptors[name] = value 97 98 def add_sos_cpu_dict(self, cpu_and_lapic_id_list): 99 self._cpu_dict = dict(cpu_and_lapic_id_list) 100 101 def add_init_command(self, command): 102 if command not in self._init_commands: 103 self._init_commands.append(command) 104 105 def add_deinit_command(self, command): 106 if command not in self._deinit_commands: 107 self._deinit_commands.append(command) 108 109 def add_plain_dm_parameter(self, opt): 110 full_opt = f"{opt}" 111 if full_opt not in self._dm_parameters: 112 self._dm_parameters.append(full_opt) 113 114 def add_dynamic_dm_parameter(self, cmd, opt=""): 115 full_cmd = f"{cmd:40s} {opt}".strip() 116 full_opt = f"`{full_cmd}`" 117 if full_opt not in self._dm_parameters: 118 self._dm_parameters.append(full_opt) 119 120 def to_string(self): 121 s = "" 122 123 with open(self.script_template_path, "r") as f: 124 s += f.read(99) 125 126 s += "# Launch script for VM name: " 127 s += f"{self._vm_name}\n" 128 s += "\n" 129 130 with open(self.script_template_path, "r") as f: 131 f.seek(99,0) 132 s += f.read() 133 134 s += """ 135### 136# The followings are generated by launch_cfg_gen.py 137### 138""" 139 s += "\n" 140 141 s += "# Defining variables that describe VM types\n" 142 for name, value in self._vm_descriptors.items(): 143 s += f"{name}={value}\n" 144 s += "\n" 145 146 s += "# Initializing\n" 147 for command in self._init_commands: 148 s += f"{command}\n" 149 s += "\n" 150 151 s += """ 152# Note for developers: The number of available logical CPUs depends on the 153# number of enabled cores and whether Hyperthreading is enabled in the BIOS 154# settings. CPU IDs are assigned to each logical CPU but are not the same ID 155# value throughout the system: 156# 157# Native CPU_ID: 158# ID enumerated by the Linux Kernel and shown in the 159# ACRN Configurator's CPU Affinity option (used in the scenario.xml) 160# Service VM CPU_ID: 161# ID assigned by the Service VM at runtime 162# APIC_ID: 163# Advanced Programmable Interrupt Controller's unique ID as 164# enumerated by the board inspector (used in this launch script) 165# 166# This table shows equivalent CPU IDs for this scenario and board: 167# 168""" 169 s += "\n" 170 171 s += "# Native CPU_ID Service VM CPU_ID APIC_ID\n" 172 s += "# ------------- ----------------- -------\n" 173 vcpu_id = 0 174 for cpu_info in self._cpu_dict: 175 s += "# " 176 s += f"{cpu_info:3d}{'':17s}" 177 s += f"{vcpu_id:3d}{'':17s}" 178 s += f"{self._cpu_dict[cpu_info]:3d}\n" 179 vcpu_id += 1 180 s += "\n" 181 s += "# Invoking ACRN device model\n" 182 s += "dm_params=(\n" 183 for param in self._dm_parameters: 184 s += f" {param}\n" 185 s += ")\n\n" 186 187 s += "echo \"Launch device model with parameters: ${dm_params[@]}\"\n" 188 s += "acrn-dm \"${dm_params[@]}\"\n\n" 189 190 s += "# Deinitializing\n" 191 for command in self._deinit_commands: 192 s += f"{command}\n" 193 194 return s 195 196 def write_to_file(self, path): 197 with open(path, "w") as f: 198 f.write(self.to_string()) 199 logging.info(f"Successfully generated launch script {path} for VM '{self._vm_name}'.") 200 201 def add_virtual_device(self, kind, vbdf=None, options=""): 202 if "virtio" in kind and eval_xpath(self._vm_scenario_etree, ".//vm_type/text()") == "RTVM": 203 self.add_plain_dm_parameter("--virtio_poll 1000000") 204 205 if vbdf is None: 206 vbdf = self._vbdf_allocator.get_virtual_bdf() 207 else: 208 self._vbdf_allocator.remove_virtual_bdf(vbdf) 209 self.add_dynamic_dm_parameter("add_virtual_device", f"{vbdf} {kind} {options}") 210 211 def add_passthru_device(self, bus, dev, fun, options=""): 212 device_etree = eval_xpath( 213 self._board_etree, 214 f"//bus[@type='pci' and @address='0x{bus:x}']/device[@address='0x{(dev << 16) | fun:x}']" 215 ) 216 if not options: 217 options = self._passthru_options.get_option(device_etree, self._vm_scenario_etree) 218 219 vbdf = self._vbdf_allocator.get_virtual_bdf(device_etree, options) 220 self.add_dynamic_dm_parameter("add_passthrough_device", f"{vbdf} 0000:{bus:02x}:{dev:02x}.{fun} {options}") 221 222 # Enable interrupt storm monitoring if the VM has any passthrough device other than the integrated GPU (whose 223 # vBDF is fixed to 2) 224 if vbdf != 2: 225 self.add_dynamic_dm_parameter("add_interrupt_storm_monitor", "10000 10 1 100") 226 227 def has_dm_parameter(self, fn): 228 try: 229 next(filter(fn, self._dm_parameters)) 230 return True 231 except StopIteration: 232 return False 233 234 235def cpu_id_to_lapic_id(board_etree, vm_name, cpu): 236 237 lapic_id = eval_xpath(board_etree, f"//processors//thread[cpu_id='{cpu}']/apic_id/text()", None) 238 if lapic_id is not None: 239 return int(lapic_id, 16) 240 else: 241 logging.warning(f"CPU {cpu} is not defined in the board XML, so it can't be available to VM {vm_name}") 242 return None 243 244def get_slot_by_vbdf(vbdf): 245 if vbdf is not None: 246 return int((vbdf.split(":")[1].split(".")[0]), 16) 247 else: 248 return None 249 250def generate_for_one_vm(board_etree, hv_scenario_etree, vm_scenario_etree, vm_id): 251 vm_name = eval_xpath(vm_scenario_etree, "./name/text()", f"ACRN Post-Launched VM") 252 script = LaunchScript(board_etree, vm_name, vm_scenario_etree) 253 254 script.add_init_command("probe_modules") 255 256 ### 257 # VM types and guest OSes 258 ### 259 260 if eval_xpath(vm_scenario_etree, ".//os_type/text()") == "Windows OS": 261 script.add_plain_dm_parameter("--windows") 262 script.add_vm_descriptor("vm_type", f"'{eval_xpath(vm_scenario_etree, './/vm_type/text()', 'STANDARD_VM')}'") 263 script.add_vm_descriptor("scheduler", f"'{eval_xpath(hv_scenario_etree, './/SCHEDULER/text()')}'") 264 script.add_vm_descriptor("own_pcpu", f"'{eval_xpath(vm_scenario_etree, './/own_pcpu/text()')}'") 265 266 ### 267 # CPU and memory resources 268 ### 269 cpus = set(eval_xpath_all(vm_scenario_etree, ".//cpu_affinity//pcpu_id[text() != '']/text()")) 270 lapic_ids = [x for x in [cpu_id_to_lapic_id(board_etree, vm_name, cpu_id) for cpu_id in cpus] if x != None] 271 if lapic_ids: 272 script.add_dynamic_dm_parameter("add_cpus", f"{' '.join([str(x) for x in sorted(lapic_ids)])}") 273 274 script.add_plain_dm_parameter(f"-m {eval_xpath(vm_scenario_etree, './/memory/size/text()')}M") 275 276 if eval_xpath(vm_scenario_etree, "//SSRAM_ENABLED") == "y" and \ 277 eval_xpath(vm_scenario_etree, ".//vm_type/text()") == "RTVM": 278 script.add_plain_dm_parameter("--ssram") 279 280 ### 281 # Guest BIOS 282 ### 283 if eval_xpath(vm_scenario_etree, ".//vbootloader/text()") == "y": 284 script.add_plain_dm_parameter("--ovmf /usr/share/acrn/bios/OVMF.fd") 285 286 ### 287 # Devices 288 ### 289 290 # Emulated platform devices 291 if eval_xpath(vm_scenario_etree, ".//vm_type/text()") != "RTVM": 292 script.add_virtual_device("lpc", vbdf="1:0") 293 294 if eval_xpath(vm_scenario_etree, ".//vuart0/text()") == "y": 295 script.add_plain_dm_parameter("-l com1,stdio") 296 297 # Emulated PCI devices 298 script.add_virtual_device("hostbridge", vbdf="0:0") 299 300 #ivshmem and vuart must be the first virtual devices generated before the others except hostbridge and LPC 301 #ivshmem and vuart own reserved slots which setting by user 302 303 for ivshmem in eval_xpath_all(vm_scenario_etree, f"//IVSHMEM_REGION[PROVIDED_BY = 'Device Model' and .//VM_NAME = '{vm_name}']"): 304 vbdf = eval_xpath(ivshmem, f".//VBDF/text()") 305 slot = get_slot_by_vbdf(vbdf) 306 if ivshmem.find('IVSHMEM_REGION_ID') is not None: 307 script.add_virtual_device("ivshmem", slot, options=f"dm:/{ivshmem.find('NAME').text},{ivshmem.find('IVSHMEM_SIZE').text},{ivshmem.find('IVSHMEM_REGION_ID').text}") 308 else: 309 script.add_virtual_device("ivshmem", slot, options=f"dm:/{ivshmem.find('NAME').text},{ivshmem.find('IVSHMEM_SIZE').text},{0}") 310 311 for ivshmem in eval_xpath_all(vm_scenario_etree, f"//IVSHMEM_REGION[PROVIDED_BY = 'Hypervisor' and .//VM_NAME = '{vm_name}']"): 312 vbdf = eval_xpath(ivshmem, f".//VBDF/text()") 313 slot = get_slot_by_vbdf(vbdf) 314 if ivshmem.find('IVSHMEM_REGION_ID') is not None: 315 script.add_virtual_device("ivshmem", slot, options=f"hv:/{ivshmem.find('NAME').text},{ivshmem.find('IVSHMEM_SIZE').text},{ivshmem.find('IVSHMEM_REGION_ID').text}") 316 else: 317 script.add_virtual_device("ivshmem", slot, options=f"hv:/{ivshmem.find('NAME').text},{ivshmem.find('IVSHMEM_SIZE').text},{0}") 318 319 if eval_xpath(vm_scenario_etree, ".//console_vuart/text()") == "PCI": 320 script.add_virtual_device("uart", options="vuart_idx:0") 321 322 for idx, conn in enumerate(eval_xpath_all(hv_scenario_etree, f"//vuart_connection[endpoint/vm_name/text() = '{vm_name}']"), start=1): 323 if eval_xpath(conn, f"./type/text()") == "pci": 324 vbdf = eval_xpath(conn, f"./endpoint[vm_name/text() = '{vm_name}']/vbdf/text()") 325 slot = get_slot_by_vbdf(vbdf) 326 script.add_virtual_device("uart", slot, options=f"vuart_idx:{idx}") 327 328 xhci_params = [] 329 # Mediated PCI devices, including virtio 330 for usb_xhci in eval_xpath_all(vm_scenario_etree, ".//usb_xhci/usb_dev[text() != '']/text()"): 331 bus_port = usb_xhci.split(' ')[0] 332 xhci_params.append(bus_port) 333 if xhci_params: 334 script.add_virtual_device("xhci", options=",".join(xhci_params)) 335 336 for virtio_input_etree in eval_xpath_all(vm_scenario_etree, ".//virtio_devices/input"): 337 backend_device_file = eval_xpath(virtio_input_etree, "./backend_device_file[text() != '']/text()") 338 unique_identifier = eval_xpath(virtio_input_etree, "./id[text() != '']/text()") 339 if backend_device_file is not None and unique_identifier is not None: 340 script.add_virtual_device("virtio-input", options=f"{backend_device_file},id:{unique_identifier}") 341 elif backend_device_file is not None: 342 script.add_virtual_device("virtio-input", options=backend_device_file) 343 344 for virtio_console_etree in eval_xpath_all(vm_scenario_etree, ".//virtio_devices/console"): 345 preceding_mask = "" 346 use_type = eval_xpath(virtio_console_etree, "./use_type/text()") 347 backend_type = eval_xpath(virtio_console_etree, "./backend_type/text()") 348 if use_type == "Virtio console": 349 preceding_mask = "@" 350 351 if backend_type == "file": 352 output_file_path = eval_xpath(virtio_console_etree, "./output_file_path/text()") 353 script.add_virtual_device("virtio-console", options=f"{preceding_mask}file:file_port={output_file_path}") 354 elif backend_type == "tty": 355 tty_file_path = eval_xpath(virtio_console_etree, "./tty_device_path/text()") 356 script.add_virtual_device("virtio-console", options=f"{preceding_mask}tty:tty_port={tty_file_path}") 357 elif backend_type == "sock server" or backend_type == "sock client": 358 sock_file_path = eval_xpath(virtio_console_etree, "./sock_file_path/text()") 359 script.add_virtual_device("virtio-console", options=f"socket:{os.path.basename(sock_file_path).split('.')[0]}={sock_file_path}:{backend_type.replace('sock ', '')}") 360 elif backend_type == "pty" or backend_type == "stdio": 361 script.add_virtual_device("virtio-console", options=f"{preceding_mask}{backend_type}:{backend_type}_port") 362 363 for virtio_network_etree in eval_xpath_all(vm_scenario_etree, ".//virtio_devices/network"): 364 virtio_framework = eval_xpath(virtio_network_etree, "./virtio_framework/text()") 365 interface_name = eval_xpath(virtio_network_etree, "./interface_name/text()") 366 params = interface_name.split(",", maxsplit=1) 367 tap_conf = f"tap={params[0]}" 368 params = [tap_conf] + params[1:] 369 if virtio_framework == "Kernel based (Virtual Host)": 370 params.append("vhost") 371 script.add_init_command(f"mac=$(cat /sys/class/net/e*/address)") 372 params.append(f"mac_seed=${{mac:0:17}}-{vm_name}") 373 script.add_virtual_device("virtio-net", options=",".join(params)) 374 375 for virtio_block in eval_xpath_all(vm_scenario_etree, ".//virtio_devices/block[text() != '']/text()"): 376 params = virtio_block.split(":", maxsplit=1) 377 if len(params) == 1: 378 script.add_virtual_device("virtio-blk", options=virtio_block) 379 else: 380 block_device = params[0] 381 rootfs_img = params[1] 382 var = f"dir_{os.path.basename(block_device)}" 383 script.add_init_command(f"{var}=`mount_partition {block_device}`") 384 script.add_virtual_device("virtio-blk", options=os.path.join(f"${{{var}}}", rootfs_img)) 385 script.add_deinit_command(f"unmount_partition ${{{var}}}") 386 387 for gpu_etree in eval_xpath_all(vm_scenario_etree, ".//virtio_devices/gpu[./display_type]"): 388 display_type = eval_xpath(gpu_etree, "./display_type[text() != '']/text()") 389 params = list() 390 for display_etree in eval_xpath_all(gpu_etree, "./displays/display"): 391 if display_type == "Window": 392 window_resolutions = eval_xpath(display_etree, "./window_resolutions/text()") 393 horizontal_offset = eval_xpath(display_etree, "./horizontal_offset/text()") 394 vertical_offset = eval_xpath(display_etree, "./vertical_offset/text()") 395 params.append(f"geometry={window_resolutions}+{horizontal_offset}+{vertical_offset}") 396 if display_type == "Full screen": 397 monitor_id = eval_xpath(display_etree, "./monitor_id/text()") 398 params.append(f"geometry=fullscreen:{monitor_id}") 399 script.add_virtual_device("virtio-gpu", options=",".join(params)) 400 401 for vsock in eval_xpath_all(vm_scenario_etree, ".//virtio_devices/vsock[text() != '']/text()"): 402 script.add_virtual_device("vhost-vsock", options="cid="+vsock) 403 404 # Passthrough PCI devices 405 bdf_regex = re.compile("([0-9a-f]{2}):([0-1][0-9a-f]).([0-7])") 406 for passthru_device in eval_xpath_all(vm_scenario_etree, ".//pci_devs/*/text()"): 407 m = bdf_regex.match(passthru_device) 408 if not m: 409 continue 410 bus = int(m.group(1), 16) 411 dev = int(m.group(2), 16) 412 func = int(m.group(3), 16) 413 script.add_passthru_device(bus, dev, func) 414 415 ### 416 # Miscellaneous 417 ### 418 if eval_xpath(vm_scenario_etree, ".//vm_type/text()") == "RTVM": 419 script.add_plain_dm_parameter("--rtvm") 420 if eval_xpath(vm_scenario_etree, ".//lapic_passthrough/text()") == "y": 421 script.add_plain_dm_parameter("--lapic_pt") 422 script.add_dynamic_dm_parameter("add_logger_settings", "console=4 kmsg=3 disk=5") 423 424 ### 425 # Lastly, conclude the device model parameters with the VM name 426 ### 427 customized_parameters = eval_xpath(vm_scenario_etree, ".//customized_parameters/text()") 428 if customized_parameters is not None: 429 customized_parameters = shlex.quote(customized_parameters) 430 script.add_plain_dm_parameter(f"{customized_parameters}") 431 script.add_plain_dm_parameter(f"{vm_name}") 432 433 return script 434 435 436def main(board_xml, scenario_xml, user_vm_id, out_dir): 437 board_etree = parse(board_xml) 438 scenario_etree = parse(scenario_xml) 439 440 service_vm_id = eval_xpath(scenario_etree, "//vm[load_order = 'SERVICE_VM']/@id") 441 service_vm_name = eval_xpath(scenario_etree, "//vm[load_order = 'SERVICE_VM']/name/text()") 442 443 hv_scenario_etree = eval_xpath(scenario_etree, "//hv") 444 post_vms = eval_xpath_all(scenario_etree, "//vm[load_order = 'POST_LAUNCHED_VM']") 445 if service_vm_id is None and len(post_vms) > 0: 446 logging.error("The scenario does not define a service VM so no launch scripts will be generated for the post-launched VMs in the scenario.") 447 return 1 448 service_vm_id = int(service_vm_id) 449 450 # Service VM CPU list 451 pre_all_cpus = eval_xpath_all(scenario_etree, "//vm[load_order = 'PRE_LAUNCHED_VM']/cpu_affinity//pcpu_id/text()") 452 cpus_for_sos = sorted([int(x) for x in eval_xpath_all(board_etree, "//processors//thread//cpu_id/text()") if x not in pre_all_cpus]) 453 454 try: 455 os.mkdir(out_dir) 456 except FileExistsError: 457 if os.path.isfile(out_dir): 458 logging.error(f"Cannot create output directory {out_dir}: File exists") 459 return 1 460 except Exception as e: 461 logging.error(f"Cannot create output directory: {e}") 462 return 1 463 464 if user_vm_id == 0: 465 post_vm_ids = [int(vm_scenario_etree.get("id")) for vm_scenario_etree in post_vms] 466 else: 467 post_vm_ids = [user_vm_id + service_vm_id] 468 469 for post_vm in post_vms: 470 post_vm_id = int(post_vm.get("id")) 471 if post_vm_id not in post_vm_ids: 472 continue 473 474 script = generate_for_one_vm(board_etree, hv_scenario_etree, post_vm, post_vm_id) 475 script.add_sos_cpu_dict([(x, cpu_id_to_lapic_id(board_etree, service_vm_name, x)) for x in cpus_for_sos]) 476 script.write_to_file(os.path.join(out_dir, f"launch_user_vm_id{post_vm_id - service_vm_id}.sh")) 477 478 return 0 479 480 481if __name__ == "__main__": 482 parser = argparse.ArgumentParser() 483 parser.add_argument("--board", help="the XML file summarizing characteristics of the target board") 484 parser.add_argument("--scenario", help="the XML file specifying the scenario to be set up") 485 parser.add_argument("--launch", default=None, help="(obsoleted. DO NOT USE)") 486 parser.add_argument("--user_vmid", type=int, default=0, 487 help="the post-launched VM ID (as is specified in the launch XML) whose launch script is to be generated, or 0 if all post-launched VMs shall be processed") 488 parser.add_argument("--out", default="output", help="path to the directory where generated scripts are placed") 489 args = parser.parse_args() 490 491 logging.basicConfig(level="INFO") 492 493 sys.exit(main(args.board, args.scenario, args.user_vmid, args.out)) 494