1#!/usr/bin/env python3 2# 3# Copyright (C) 2021-2022 Intel Corporation. 4# 5# SPDX-License-Identifier: BSD-3-Clause 6# 7 8import sys, os 9import re 10import logging 11import tempfile 12import subprocess # nosec 13import lxml.etree 14from defusedxml.lxml import parse 15import argparse 16from tqdm import tqdm 17from collections import namedtuple 18from importlib import import_module 19 20script_dir = os.path.dirname(os.path.realpath(__file__)) 21sys.path.append(os.path.join(script_dir)) 22 23from cpuparser import parse_cpuid, get_online_cpu_ids, get_offline_cpu_ids 24from inspectorlib import external_tools, validator 25 26class AddLLCCATAction(argparse.Action): 27 CATInfo = namedtuple("CATInfo", ["capacity_mask_length", "clos_number", "has_CDP"]) 28 29 def __call__(self, parser, namespace, values, option_string=None): 30 pattern = re.compile("([0-9]+),([0-9]+),(true|false|y|n|yes|no)") 31 if option_string: 32 m = pattern.match(values.lower()) 33 if not m: 34 parser.error(f"{values} is ill-formed. The expected format is: <capacity_mask_length:int>,<clos_number:int>,<has_CDP:bool>") 35 v = self.CATInfo(int(m.group(1)), int(m.group(2)), m.group(3) in ["true", "y", "yes"]) 36 else: 37 v = None 38 setattr(namespace, self.dest, v) 39 40def check_deps(): 41 # Check that the required tools are installed on the system 42 had_error = not external_tools.locate_tools(["cpuid", "rdmsr", "lspci", "dmidecode", "blkid", "stty", "modprobe"]) 43 44 try: 45 cpuid_min_ver = 20170122 46 res = external_tools.run("cpuid -v") 47 line = res.stdout.readline().decode("ascii") 48 version = line.split()[2] 49 if int(version) < cpuid_min_ver: 50 logger.critical("This tool requires CPUID version >= {}. Try updating and upgrading the OS" \ 51 "on this system and reruning the Board Inspector. If that fails, install a newer CPUID tool" \ 52 "from https://github.com/tycho/cpuid.".format(cpuid_min_ver)) 53 had_error = True 54 except external_tools.ExecutableNotFound: 55 pass 56 57 if had_error: 58 sys.exit(1) 59 60 # Try updating pci.ids for latest PCI device descriptions 61 external_tools.locate_tools(["update-pciids"]) 62 try: 63 logger.info("Updating pci.ids for latest PCI device descriptions.") 64 res = external_tools.run("update-pciids -q", stderr=subprocess.DEVNULL) 65 if res.wait(timeout=40) != 0: 66 logger.warning(f"Failed to invoke update-pciids. No functional impact is foreseen, but descriptions of PCI devices may be inaccurate.") 67 except Exception as e: 68 logger.warning(f"Failed to invoke update-pciids: {e}. No functional impact is foreseen, but descriptions of PCI devices may be unavailable.") 69 70def native_check(): 71 cpu_ids = get_online_cpu_ids() 72 cpu_id = cpu_ids.pop(0) 73 leaf_1 = parse_cpuid(1, 0, cpu_id) 74 if leaf_1.hypervisor != 0: 75 logger.error("Board inspector is running inside an unsupported Virtual Machine (VM). " \ 76 "Only KVM or QEMU is supported. Unexpected results may occur.") 77 78def check_pci_domains(): 79 root_buses = os.listdir("/sys/bus/pci/devices/") 80 domain_ids = set(map(lambda x: x.split(":")[0], root_buses)) 81 if len(domain_ids) > 1: 82 logger.fatal(f"ACRN does not support platforms with multiple PCI domains {domain_ids}. " \ 83 "Check if the BIOS has any configuration that consolidates those domains into one. " \ 84 "Known causes of multiple PCI domains include: VMD (Volume Management Device) being enabled.") 85 sys.exit(1) 86 87def bring_up_cores(): 88 cpu_ids = get_offline_cpu_ids() 89 for id in cpu_ids: 90 try: 91 with open("/sys/devices/system/cpu/cpu{}/online".format(id), "w") as f: 92 f.write("1") 93 except : 94 logger.warning("Cannot bring up core with cpu id {}.".format(id)) 95 96def summary_loginfo(board_xml): 97 length = 120 98 warning_list = [] 99 error_list = [] 100 critical_list = [] 101 log_line = open(str(tmpfile.name), "r", encoding='UTF-8') 102 for line in log_line: 103 if "WARNING" in line: 104 warning_list.append(line) 105 elif "ERROR" in line: 106 error_list.append(line) 107 elif "CRITICAL" in line: 108 critical_list.append(line) 109 110 if len(warning_list) != 0: 111 print("="*length) 112 print("\033[1;37mWARNING\033[0m") 113 print("These issues affect optional features. You can ignore them if they don't apply to you.\n") 114 for warning in warning_list: 115 print("\033[1;33m{0}\033[0m".format(warning.strip('\n'))) 116 117 if len(error_list) != 0: 118 print("="*length) 119 print("\033[1;37mERROR\033[0m") 120 print("You must resolve these issues to generate a VALID board configuration file for building and boot ACRN.\n") 121 for error in error_list: 122 print("\033[1;31m{0}\033[0m".format(error.strip('\n'))) 123 124 if len(critical_list) != 0: 125 print("="*length) 126 print("\033[1;37mCRITICAL\033[0m") 127 print("You must resolve these issues to generate a board configuration file.\n") 128 for critical in critical_list: 129 print("\033[1;31m{0}\033[0m".format(critical.strip('\n'))) 130 131 print("="*length) 132 if len(critical_list) == 0: 133 print( 134 f"\033[1;32mSUCCESS: Board configuration file {board_xml} generated successfully and saved to {os.path.dirname(os.path.abspath(board_xml))}\033[0m\n") 135 if len(error_list) != 0: 136 print("\033[1;36mNOTE: Board configuration file lacks important features, which will cause ACRN to fail build or boot. Resolve ERROR messages then run the tool again.\033[0m") 137 tmpfile.close() 138 139def main(board_name, board_xml, args): 140 print(f"Generating board XML {board_name}. This may take a few minutes...") 141 142 with tqdm(total=100) as pbar: 143 # Check that the dependencies are met 144 check_deps() 145 pbar.update(10) 146 147 # Check if this is native os 148 native_check() 149 150 # Check if there exists multiple PCI domains (which is not supported) 151 check_pci_domains() 152 153 # Bring up all cores 154 bring_up_cores() 155 156 try: 157 # First invoke the legacy board parser to create the board XML ... 158 legacy_parser = os.path.join(script_dir, "legacy", "board_parser.py") 159 env = { "PYTHONPATH": script_dir, "PATH": os.environ["PATH"] } 160 subprocess.run([sys.executable, legacy_parser, args.board_name, "--out", board_xml], check=True, env=env) 161 # ... then load the created board XML and append it with additional data by invoking the extractors. 162 board_etree = parse(board_xml) 163 root_node = board_etree.getroot() 164 165 # Clear the whitespaces between adjacent children under the root node 166 root_node.text = None 167 for elem in root_node: 168 elem.tail = None 169 170 171 # Create nodes for each kind of resource 172 root_node.append(lxml.etree.Element("processors")) 173 root_node.append(lxml.etree.Element("caches")) 174 root_node.append(lxml.etree.Element("memory")) 175 root_node.append(lxml.etree.Element("ioapics")) 176 root_node.append(lxml.etree.Element("devices")) 177 root_node.append(lxml.etree.Element("device-classes")) 178 pbar.update(10) 179 180 extractors_path = os.path.join(script_dir, "extractors") 181 extractors = [f for f in os.listdir(extractors_path) if f[:2].isdigit()] 182 for extractor in sorted(extractors): 183 module_name = os.path.splitext(extractor)[0] 184 module = import_module(f"extractors.{module_name}") 185 if args.basic and getattr(module, "advanced", False): 186 continue 187 module.extract(args, board_etree) 188 if "50-acpi-namespace.py" in module_name: 189 pbar.update(30) 190 else: 191 pbar.update(10) 192 193 # Validate the XML against XSD assertions 194 count = validator.validate_board(os.path.join(script_dir, 'schema', 'boardchecks.xsd'), board_etree) 195 if count == 0: 196 logger.info("All board checks passed.") 197 198 # Finally overwrite the output with the updated XML 199 board_etree.write(board_xml, pretty_print=True) 200 201 #Format and out put the log info 202 summary_loginfo(board_xml) 203 pbar.update(10) 204 205 except subprocess.CalledProcessError as e: 206 logger.critical(e) 207 sys.exit(1) 208 209if __name__ == "__main__": 210 parser = argparse.ArgumentParser() 211 parser.add_argument("board_name", help="the name of the board that runs the ACRN hypervisor") 212 parser.add_argument("--out", help="the name of board info file") 213 parser.add_argument("--basic", action="store_true", default=False, help="do not extract advanced information such as ACPI namespace") 214 parser.add_argument("--loglevel", default="warning", help="choose log level, e.g. debug, info, warning, error or critical") 215 parser.add_argument("--check-device-status", action="store_true", default=False, help="filter out devices whose _STA object evaluates to 0") 216 parser.add_argument("--add-llc-cat", default=None, action=AddLLCCATAction, 217 metavar="<capacity_mask_length:int>,<clos_number:int>,<has_CDP:bool>", help="manually set the Cache Allocation Technology capability of the last level cache") 218 args = parser.parse_args() 219 try: 220 tmpfile = tempfile.NamedTemporaryFile(delete=True) 221 logger = logging.getLogger() 222 logger.setLevel(args.loglevel.upper()) 223 formatter = logging.Formatter('%(asctime)s-%(name)s-%(levelname)s:-%(message)s', datefmt='%Y-%m-%d %H:%M:%S') 224 fh = logging.FileHandler(str(tmpfile.name)) 225 fh.setLevel(args.loglevel.upper()) 226 fh.setFormatter(formatter) 227 228 sh = logging.StreamHandler() 229 sh.setLevel(args.loglevel.upper()) 230 231 sh.setFormatter(formatter) 232 logger.addHandler(fh) 233 logger.addHandler(sh) 234 235 except ValueError: 236 print(f"{args.loglevel} is not a valid log level") 237 print(f"Valid log levels (non case-sensitive): critical, error, warning, info, debug") 238 sys.exit(1) 239 240 board_xml = args.out if args.out else f"{args.board_name}.xml" 241 main(args.board_name, board_xml, args) 242