| Jonathan Doman | 94c94bf | 2020-10-05 23:25:45 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python3 | 
 | 2 |  | 
 | 3 | # This tool runs on the host CPU and gathers all SST related configuration from | 
 | 4 | # the BMC (Redfish) and from the linux driver, and compares them to catch any | 
 | 5 | # errors or disagreement. Only required arguments are the details to start a | 
 | 6 | # Redfish session. | 
 | 7 | # | 
 | 8 | # This was tested running on a live Arch Linux ISO environment. Any Linux | 
 | 9 | # installation should work, but best to get the latest tools and kernel driver. | 
 | 10 | # | 
 | 11 | # Required dependencies: | 
 | 12 | # * DMTF's redfish python library. This is available in pip. | 
 | 13 | # * intel-speed-select tool from the kernel source tree | 
 | 14 | #   (tools/power/x86/intel-speed-select), and available in the PATH. | 
 | 15 |  | 
 | 16 | import redfish | 
 | 17 |  | 
 | 18 | import argparse | 
 | 19 | import json | 
 | 20 | import re | 
 | 21 | import subprocess | 
 | 22 | import sys | 
 | 23 |  | 
 | 24 | linux_cpu_map = dict() | 
 | 25 | success = True | 
 | 26 |  | 
 | 27 | def get_linux_output(): | 
 | 28 |     cmd = "/usr/bin/env intel-speed-select --debug --format json perf-profile info".split() | 
 | 29 |     process = subprocess.run(cmd, capture_output=True, text=True) | 
 | 30 |     process.check_returncode() | 
 | 31 |     result = json.loads(process.stderr) | 
 | 32 |  | 
 | 33 |     global linux_cpu_map | 
 | 34 |     linux_cpu_map = dict() | 
 | 35 |     for line in process.stdout.split('\n'): | 
 | 36 |         match = re.search("logical_cpu:(\d+).*punit_core:(\d+)", line) | 
 | 37 |         if not match: | 
 | 38 |             continue | 
 | 39 |         logical_thread = int(match.group(1)) | 
 | 40 |         physical_core = int(match.group(2)) | 
 | 41 |         linux_cpu_map[logical_thread] = physical_core | 
 | 42 |  | 
 | 43 |     cmd = "/usr/bin/env intel-speed-select --format json perf-profile get-config-current-level".split() | 
 | 44 |     process = subprocess.run(cmd, capture_output=True, text=True) | 
 | 45 |     current_level = json.loads(process.stderr) | 
 | 46 |  | 
 | 47 |     for proc, data in current_level.items(): | 
 | 48 |         result[proc].update(data) | 
 | 49 |  | 
 | 50 |     return result | 
 | 51 |  | 
 | 52 |  | 
 | 53 | def compare(redfish_val, linux_val, description): | 
 | 54 |     err = "" | 
| Jonathan Doman | a30229e | 2022-05-13 13:19:02 -0700 | [diff] [blame] | 55 |     if None in (redfish_val, linux_val): | 
 | 56 |         err = "MISSING VALUE" | 
 | 57 |     elif redfish_val != linux_val: | 
| Jonathan Doman | 94c94bf | 2020-10-05 23:25:45 -0700 | [diff] [blame] | 58 |         err = "!! MISMATCH !!" | 
 | 59 |         global success | 
 | 60 |         success = False | 
 | 61 |     print(f"{description}: {err}") | 
 | 62 |     print(f"  Redfish: {redfish_val}") | 
 | 63 |     print(f"  Linux: {linux_val}") | 
 | 64 |  | 
 | 65 |  | 
 | 66 | def get_linux_package(linux_data, redfish_id): | 
 | 67 |     match = re.match("cpu(\d+)", redfish_id) | 
 | 68 |     if not match: | 
 | 69 |         raise RuntimeError(f"Redfish CPU name is unexpected: {redfish_id}") | 
 | 70 |     num = match.group(1) | 
 | 71 |     matching_keys = [] | 
 | 72 |     for key in linux_data.keys(): | 
 | 73 |         if re.match(f"^package-{num}:.*", key): | 
 | 74 |             matching_keys.append(key) | 
 | 75 |     if len(matching_keys) != 1: | 
 | 76 |         raise RuntimeError(f"Unexpected number of matching linux objects for {redfish_id}") | 
 | 77 |     return linux_data[matching_keys[0]] | 
 | 78 |  | 
 | 79 |  | 
 | 80 | def compare_config(redfish_config, linux_config): | 
 | 81 |     print(f"--Checking {redfish_config['Id']}--") | 
 | 82 |     compare(redfish_config["BaseSpeedMHz"], int(linux_config["base-frequency(MHz)"]), "Base Speed") | 
 | 83 |  | 
 | 84 |     actual_hp_p1 = actual_lp_p1 = 0 | 
 | 85 |     actual_hp_cores = set() | 
 | 86 |     for bf in redfish_config["BaseSpeedPrioritySettings"]: | 
 | 87 |         if not actual_hp_p1 or bf["BaseSpeedMHz"] > actual_hp_p1: | 
 | 88 |             actual_hp_p1 = bf["BaseSpeedMHz"] | 
 | 89 |             actual_hp_cores = set(bf["CoreIDs"]) | 
 | 90 |         if not actual_lp_p1 or bf["BaseSpeedMHz"] < actual_lp_p1: | 
 | 91 |             actual_lp_p1 = bf["BaseSpeedMHz"] | 
 | 92 |  | 
 | 93 |     exp_hp_p1 = exp_lp_p1 = 0 | 
 | 94 |     exp_hp_cores = set() | 
 | 95 |     if "speed-select-base-freq-properties" in linux_config: | 
 | 96 |         exp_bf_props = linux_config["speed-select-base-freq-properties"] | 
 | 97 |         exp_hp_p1 = int(exp_bf_props["high-priority-base-frequency(MHz)"]) | 
 | 98 |         exp_hp_cores = set(map(lambda x: linux_cpu_map[x], | 
 | 99 |                               map(int, exp_bf_props["high-priority-cpu-list"].split(",")))) | 
 | 100 |         exp_lp_p1 = int(exp_bf_props["low-priority-base-frequency(MHz)"]) | 
 | 101 |  | 
 | 102 |     compare(actual_hp_p1, exp_hp_p1, "SST-BF High Priority P1 Freq") | 
 | 103 |     compare(actual_hp_cores, exp_hp_cores, "SST-BF High Priority Core List") | 
 | 104 |     compare(actual_lp_p1, exp_lp_p1, "SST-BF Low Priority P1 Freq") | 
 | 105 |  | 
 | 106 |  | 
 | 107 |     compare(redfish_config["MaxJunctionTemperatureCelsius"], | 
 | 108 |             int(linux_config["tjunction-max(C)"]), | 
 | 109 |             "Junction Temperature") | 
| Jonathan Doman | a30229e | 2022-05-13 13:19:02 -0700 | [diff] [blame] | 110 |     # There is no equivalent value in linux for the per-level P0_1 freq. | 
| Jonathan Doman | 94c94bf | 2020-10-05 23:25:45 -0700 | [diff] [blame] | 111 |     compare(redfish_config["MaxSpeedMHz"], | 
| Jonathan Doman | a30229e | 2022-05-13 13:19:02 -0700 | [diff] [blame] | 112 |             None, | 
| Jonathan Doman | 94c94bf | 2020-10-05 23:25:45 -0700 | [diff] [blame] | 113 |             "SSE Max Turbo Speed") | 
 | 114 |     compare(redfish_config["TDPWatts"], | 
 | 115 |             int(linux_config["thermal-design-power(W)"]), | 
 | 116 |             "TDP") | 
 | 117 |     compare(redfish_config["TotalAvailableCoreCount"], | 
 | 118 |             int(linux_config["enable-cpu-count"])//2, | 
 | 119 |             "Enabled Core Count") | 
 | 120 |  | 
 | 121 |     actual_turbo = [(x["ActiveCoreCount"], x["MaxSpeedMHz"]) for x in redfish_config["TurboProfile"]] | 
 | 122 |     linux_turbo = linux_config["turbo-ratio-limits-sse"] | 
 | 123 |     exp_turbo = [] | 
 | 124 |     for bucket_key in sorted(linux_turbo.keys()): | 
 | 125 |         bucket = linux_turbo[bucket_key] | 
 | 126 |         exp_turbo.append((int(bucket["core-count"]), int(bucket["max-turbo-frequency(MHz)"]))) | 
 | 127 |     compare(actual_turbo, exp_turbo, "SSE Turbo Profile") | 
 | 128 |  | 
 | 129 |  | 
 | 130 | def get_level_from_config_id(config_id): | 
 | 131 |     match = re.match("config(\d+)", config_id) | 
 | 132 |     if not match: | 
 | 133 |         raise RuntimeError(f"Invalid config name {config_id}") | 
 | 134 |     return match.group(1) | 
 | 135 |  | 
 | 136 |  | 
 | 137 | def main(): | 
 | 138 |     parser = argparse.ArgumentParser(description="Compare Redfish SST properties against Linux tools") | 
 | 139 |     parser.add_argument("hostname") | 
 | 140 |     parser.add_argument("--username", "-u", default="root") | 
 | 141 |     parser.add_argument("--password", "-p", default="0penBmc") | 
 | 142 |     args = parser.parse_args() | 
 | 143 |  | 
 | 144 |     linux_data = get_linux_output() | 
 | 145 |  | 
 | 146 |     bmc = redfish.redfish_client(base_url=f"https://{args.hostname}", | 
 | 147 |             username=args.username, password=args.password) | 
 | 148 |     bmc.login(auth="session") | 
 | 149 |  | 
 | 150 |     # Load the ProcessorCollection | 
 | 151 |     resp = json.loads(bmc.get("/redfish/v1/Systems/system/Processors").text) | 
 | 152 |     for proc_member in resp["Members"]: | 
 | 153 |         proc_resp = json.loads(bmc.get(proc_member["@odata.id"]).text) | 
 | 154 |         proc_id = proc_resp["Id"] | 
 | 155 |         print() | 
 | 156 |         print(f"----Checking Processor {proc_id}----") | 
 | 157 |  | 
 | 158 |         if proc_resp["Status"]["State"] == "Absent": | 
 | 159 |             print("Not populated") | 
 | 160 |             continue | 
 | 161 |  | 
 | 162 |         # Get subset of intel-speed-select data which applies to this CPU | 
 | 163 |         pkg_data = get_linux_package(linux_data, proc_id) | 
 | 164 |  | 
 | 165 |         # Check currently applied config | 
 | 166 |         applied_config = proc_resp["AppliedOperatingConfig"]["@odata.id"].split('/')[-1] | 
 | 167 |         current_level = get_level_from_config_id(applied_config) | 
 | 168 |         compare(current_level, pkg_data["get-config-current_level"], "Applied Config") | 
 | 169 |  | 
 | 170 |         exp_cur_level_data = pkg_data[f"perf-profile-level-{current_level}"] | 
 | 171 |  | 
 | 172 |         # Check whether SST-BF is enabled | 
 | 173 |         bf_enabled = proc_resp["BaseSpeedPriorityState"].lower() | 
 | 174 |         exp_bf_enabled = exp_cur_level_data["speed-select-base-freq"] | 
 | 175 |         if exp_bf_enabled == "unsupported": | 
 | 176 |             exp_bf_enabled = "disabled" | 
 | 177 |         compare(bf_enabled, exp_bf_enabled, "SST-BF Enabled?") | 
 | 178 |  | 
 | 179 |         # Check high speed core list | 
 | 180 |         hscores = set(proc_resp["HighSpeedCoreIDs"]) | 
 | 181 |         exp_hscores = set() | 
 | 182 |         if "speed-select-base-freq-properties" in exp_cur_level_data: | 
 | 183 |             exp_hscores = exp_cur_level_data["speed-select-base-freq-properties"]["high-priority-cpu-list"] | 
 | 184 |             exp_hscores = set([linux_cpu_map[int(x)] for x in exp_hscores.split(",")]) | 
 | 185 |         compare(hscores, exp_hscores, "High Speed Core List") | 
 | 186 |  | 
 | 187 |         # Load the OperatingConfigCollection | 
 | 188 |         resp = json.loads(bmc.get(proc_resp["OperatingConfigs"]["@odata.id"]).text) | 
 | 189 |  | 
 | 190 |         # Check number of available configs | 
 | 191 |         profile_keys = list(filter(lambda x: x.startswith("perf-profile-level"), pkg_data.keys())) | 
 | 192 |         compare(resp["Members@odata.count"], int(len(profile_keys)), "Number of profiles") | 
 | 193 |  | 
 | 194 |         for config_member in resp["Members"]: | 
 | 195 |             # Load each OperatingConfig and compare all its contents | 
 | 196 |             config_resp = json.loads(bmc.get(config_member["@odata.id"]).text) | 
 | 197 |             level = get_level_from_config_id(config_resp["Id"]) | 
 | 198 |             exp_level_data = pkg_data[f"perf-profile-level-{level}"] | 
 | 199 |             compare_config(config_resp, exp_level_data) | 
 | 200 |  | 
 | 201 |     print() | 
 | 202 |     if success: | 
 | 203 |         print("Everything matched! :)") | 
 | 204 |         return 0 | 
 | 205 |     else: | 
 | 206 |         print("There were mismatches, please check output :(") | 
 | 207 |         return 1 | 
 | 208 |  | 
 | 209 | if __name__ == "__main__": | 
 | 210 |     sys.exit(main()) |