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 16import argparse 17import json 18import re 19import subprocess 20import sys 21 22import redfish 23 24linux_cpu_map = dict() 25success = True 26 27 28def filter_powerdomains(sst_data): 29 # For TPMI-based CPUs, we only care about powerdomain-0 30 cpus = list(sst_data.keys()) 31 for proc in cpus: 32 match = re.search("powerdomain-(\\d+)", proc) 33 if not match or match.group(1) == "0": 34 continue 35 del sst_data[proc] 36 37 38def get_linux_output(): 39 cmd = [ 40 "/usr/bin/env", 41 "intel-speed-select", 42 "--debug", 43 "--format=json", 44 "perf-profile", 45 "info", 46 ] 47 process = subprocess.run(cmd, capture_output=True, text=True) 48 process.check_returncode() 49 result = json.loads(process.stderr) 50 filter_powerdomains(result) 51 52 global linux_cpu_map 53 linux_cpu_map = dict() 54 for line in process.stdout.split("\n"): 55 match = re.search("logical_cpu:(\\d+).*punit_core:(\\d+)", line) 56 if not match: 57 continue 58 logical_thread = int(match.group(1)) 59 physical_core = int(match.group(2)) 60 linux_cpu_map[logical_thread] = physical_core 61 62 cmd = [ 63 "/usr/bin/env", 64 "intel-speed-select", 65 "--format=json", 66 "perf-profile", 67 "get-config-current-level", 68 ] 69 process = subprocess.run(cmd, capture_output=True, text=True) 70 current_level = json.loads(process.stderr) 71 filter_powerdomains(current_level) 72 73 for proc, data in current_level.items(): 74 result[proc].update(data) 75 76 return result 77 78 79def compare(redfish_val, linux_val, description): 80 err = "" 81 if None in (redfish_val, linux_val): 82 err = "MISSING VALUE" 83 elif redfish_val != linux_val: 84 err = "!! MISMATCH !!" 85 global success 86 success = False 87 print(f"{description}: {err}") 88 print(f" Redfish: {redfish_val}") 89 print(f" Linux: {linux_val}") 90 91 92def get_linux_package(linux_data, redfish_id): 93 match = re.match("cpu(\\d+)", redfish_id) 94 if not match: 95 raise RuntimeError(f"Redfish CPU name is unexpected: {redfish_id}") 96 num = match.group(1) 97 matching_keys = [] 98 for key in linux_data.keys(): 99 if re.match(f"^package-{num}:.*", key): 100 matching_keys.append(key) 101 if len(matching_keys) != 1: 102 raise RuntimeError( 103 f"Unexpected number of matching linux objects for {redfish_id}" 104 ) 105 return linux_data[matching_keys[0]] 106 107 108def compare_config(redfish_config, linux_config): 109 print(f"--Checking {redfish_config['Id']}--") 110 compare( 111 redfish_config["BaseSpeedMHz"], 112 int(linux_config["base-frequency(MHz)"]), 113 "Base Speed", 114 ) 115 116 actual_hp_p1 = actual_lp_p1 = 0 117 actual_hp_cores = set() 118 for bf in redfish_config["BaseSpeedPrioritySettings"]: 119 if not actual_hp_p1 or bf["BaseSpeedMHz"] > actual_hp_p1: 120 actual_hp_p1 = bf["BaseSpeedMHz"] 121 actual_hp_cores = set(bf["CoreIDs"]) 122 if not actual_lp_p1 or bf["BaseSpeedMHz"] < actual_lp_p1: 123 actual_lp_p1 = bf["BaseSpeedMHz"] 124 125 exp_hp_p1 = exp_lp_p1 = 0 126 exp_hp_cores = set() 127 if "speed-select-base-freq-properties" in linux_config: 128 exp_bf_props = linux_config["speed-select-base-freq-properties"] 129 exp_hp_p1 = int(exp_bf_props["high-priority-base-frequency(MHz)"]) 130 exp_hp_cores = set( 131 map( 132 lambda x: linux_cpu_map[x], 133 map(int, exp_bf_props["high-priority-cpu-list"].split(",")), 134 ) 135 ) 136 exp_lp_p1 = int(exp_bf_props["low-priority-base-frequency(MHz)"]) 137 138 compare(actual_hp_p1, exp_hp_p1, "SST-BF High Priority P1 Freq") 139 compare(actual_hp_cores, exp_hp_cores, "SST-BF High Priority Core List") 140 compare(actual_lp_p1, exp_lp_p1, "SST-BF Low Priority P1 Freq") 141 142 compare( 143 redfish_config["MaxJunctionTemperatureCelsius"], 144 int(linux_config["tjunction-max(C)"]), 145 "Junction Temperature", 146 ) 147 # There is no equivalent value in linux for the per-level P0_1 freq. 148 compare(redfish_config["MaxSpeedMHz"], None, "SSE Max Turbo Speed") 149 compare( 150 redfish_config["TDPWatts"], 151 int(linux_config["thermal-design-power(W)"]), 152 "TDP", 153 ) 154 compare( 155 redfish_config["TotalAvailableCoreCount"], 156 int(linux_config["enable-cpu-count"]) // 2, 157 "Enabled Core Count", 158 ) 159 160 actual_turbo = [ 161 (x["ActiveCoreCount"], x["MaxSpeedMHz"]) 162 for x in redfish_config["TurboProfile"] 163 ] 164 linux_turbo = ( 165 linux_config.get("turbo-ratio-limits-sse") 166 or linux_config["turbo-ratio-limits-level-0"] 167 ) 168 exp_turbo = [] 169 for bucket_key in sorted(linux_turbo.keys()): 170 bucket = linux_turbo[bucket_key] 171 exp_turbo.append( 172 ( 173 int(bucket["core-count"]), 174 int(bucket["max-turbo-frequency(MHz)"]), 175 ) 176 ) 177 compare(actual_turbo, exp_turbo, "SSE Turbo Profile") 178 179 180def get_level_from_config_id(config_id): 181 match = re.match("config(\\d+)", config_id) 182 if not match: 183 raise RuntimeError(f"Invalid config name {config_id}") 184 return match.group(1) 185 186 187def main(): 188 parser = argparse.ArgumentParser( 189 description="Compare Redfish SST properties against Linux tools" 190 ) 191 parser.add_argument("hostname") 192 parser.add_argument("--username", "-u", default="root") 193 parser.add_argument("--password", "-p", default="0penBmc") 194 args = parser.parse_args() 195 196 linux_data = get_linux_output() 197 198 bmc = redfish.redfish_client( 199 base_url=f"https://{args.hostname}", 200 username=args.username, 201 password=args.password, 202 ) 203 bmc.login(auth="session") 204 205 # Load the ProcessorCollection 206 resp = json.loads(bmc.get("/redfish/v1/Systems/system/Processors").text) 207 for proc_member in resp["Members"]: 208 proc_resp = json.loads(bmc.get(proc_member["@odata.id"]).text) 209 proc_id = proc_resp["Id"] 210 print() 211 print(f"----Checking Processor {proc_id}----") 212 213 if proc_resp["Status"]["State"] == "Absent": 214 print("Not populated") 215 continue 216 217 # Get subset of intel-speed-select data which applies to this CPU 218 pkg_data = get_linux_package(linux_data, proc_id) 219 220 # Check currently applied config 221 applied_config = proc_resp["AppliedOperatingConfig"][ 222 "@odata.id" 223 ].split("/")[-1] 224 current_level = get_level_from_config_id(applied_config) 225 compare( 226 current_level, 227 pkg_data["get-config-current_level"], 228 "Applied Config", 229 ) 230 231 exp_cur_level_data = pkg_data[f"perf-profile-level-{current_level}"] 232 233 # Check whether SST-BF is enabled 234 bf_enabled = proc_resp["BaseSpeedPriorityState"].lower() 235 exp_bf_enabled = exp_cur_level_data["speed-select-base-freq"] 236 if exp_bf_enabled == "unsupported": 237 exp_bf_enabled = "disabled" 238 compare(bf_enabled, exp_bf_enabled, "SST-BF Enabled?") 239 240 # Check high speed core list 241 hscores = set(proc_resp["HighSpeedCoreIDs"]) 242 exp_hscores = set() 243 if "speed-select-base-freq-properties" in exp_cur_level_data: 244 exp_hscores = exp_cur_level_data[ 245 "speed-select-base-freq-properties" 246 ]["high-priority-cpu-list"] 247 exp_hscores = set( 248 [linux_cpu_map[int(x)] for x in exp_hscores.split(",")] 249 ) 250 compare(hscores, exp_hscores, "High Speed Core List") 251 252 # Load the OperatingConfigCollection 253 resp = json.loads( 254 bmc.get(proc_resp["OperatingConfigs"]["@odata.id"]).text 255 ) 256 257 # Check number of available configs 258 profile_keys = list( 259 filter( 260 lambda x: x.startswith("perf-profile-level"), pkg_data.keys() 261 ) 262 ) 263 compare( 264 resp["Members@odata.count"], 265 int(len(profile_keys)), 266 "Number of profiles", 267 ) 268 269 for config_member in resp["Members"]: 270 # Load each OperatingConfig and compare all its contents 271 config_resp = json.loads(bmc.get(config_member["@odata.id"]).text) 272 level = get_level_from_config_id(config_resp["Id"]) 273 exp_level_data = pkg_data[f"perf-profile-level-{level}"] 274 compare_config(config_resp, exp_level_data) 275 276 print() 277 if success: 278 print("Everything matched! :)") 279 return 0 280 else: 281 print("There were mismatches, please check output :(") 282 return 1 283 284 285if __name__ == "__main__": 286 sys.exit(main()) 287