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