| #!/usr/bin/env python3 |
| |
| # This tool runs on the host CPU and gathers all SST related configuration from |
| # the BMC (Redfish) and from the linux driver, and compares them to catch any |
| # errors or disagreement. Only required arguments are the details to start a |
| # Redfish session. |
| # |
| # This was tested running on a live Arch Linux ISO environment. Any Linux |
| # installation should work, but best to get the latest tools and kernel driver. |
| # |
| # Required dependencies: |
| # * DMTF's redfish python library. This is available in pip. |
| # * intel-speed-select tool from the kernel source tree |
| # (tools/power/x86/intel-speed-select), and available in the PATH. |
| |
| import argparse |
| import json |
| import re |
| import subprocess |
| import sys |
| |
| import redfish |
| |
| linux_cpu_map = dict() |
| success = True |
| |
| |
| def filter_powerdomains(sst_data): |
| # For TPMI-based CPUs, we only care about powerdomain-0 |
| cpus = list(sst_data.keys()) |
| for proc in cpus: |
| match = re.search("powerdomain-(\\d+)", proc) |
| if not match or match.group(1) == "0": |
| continue |
| del sst_data[proc] |
| |
| |
| def get_linux_output(): |
| cmd = [ |
| "/usr/bin/env", |
| "intel-speed-select", |
| "--debug", |
| "--format=json", |
| "perf-profile", |
| "info", |
| ] |
| process = subprocess.run(cmd, capture_output=True, text=True) |
| process.check_returncode() |
| result = json.loads(process.stderr) |
| filter_powerdomains(result) |
| |
| global linux_cpu_map |
| linux_cpu_map = dict() |
| for line in process.stdout.split("\n"): |
| match = re.search("logical_cpu:(\\d+).*punit_core:(\\d+)", line) |
| if not match: |
| continue |
| logical_thread = int(match.group(1)) |
| physical_core = int(match.group(2)) |
| linux_cpu_map[logical_thread] = physical_core |
| |
| cmd = [ |
| "/usr/bin/env", |
| "intel-speed-select", |
| "--format=json", |
| "perf-profile", |
| "get-config-current-level", |
| ] |
| process = subprocess.run(cmd, capture_output=True, text=True) |
| current_level = json.loads(process.stderr) |
| filter_powerdomains(current_level) |
| |
| for proc, data in current_level.items(): |
| result[proc].update(data) |
| |
| return result |
| |
| |
| def compare(redfish_val, linux_val, description): |
| err = "" |
| if None in (redfish_val, linux_val): |
| err = "MISSING VALUE" |
| elif redfish_val != linux_val: |
| err = "!! MISMATCH !!" |
| global success |
| success = False |
| print(f"{description}: {err}") |
| print(f" Redfish: {redfish_val}") |
| print(f" Linux: {linux_val}") |
| |
| |
| def get_linux_package(linux_data, redfish_id): |
| match = re.match("cpu(\\d+)", redfish_id) |
| if not match: |
| raise RuntimeError(f"Redfish CPU name is unexpected: {redfish_id}") |
| num = match.group(1) |
| matching_keys = [] |
| for key in linux_data.keys(): |
| if re.match(f"^package-{num}:.*", key): |
| matching_keys.append(key) |
| if len(matching_keys) != 1: |
| raise RuntimeError( |
| f"Unexpected number of matching linux objects for {redfish_id}" |
| ) |
| return linux_data[matching_keys[0]] |
| |
| |
| def compare_config(redfish_config, linux_config): |
| print(f"--Checking {redfish_config['Id']}--") |
| compare( |
| redfish_config["BaseSpeedMHz"], |
| int(linux_config["base-frequency(MHz)"]), |
| "Base Speed", |
| ) |
| |
| actual_hp_p1 = actual_lp_p1 = 0 |
| actual_hp_cores = set() |
| for bf in redfish_config["BaseSpeedPrioritySettings"]: |
| if not actual_hp_p1 or bf["BaseSpeedMHz"] > actual_hp_p1: |
| actual_hp_p1 = bf["BaseSpeedMHz"] |
| actual_hp_cores = set(bf["CoreIDs"]) |
| if not actual_lp_p1 or bf["BaseSpeedMHz"] < actual_lp_p1: |
| actual_lp_p1 = bf["BaseSpeedMHz"] |
| |
| exp_hp_p1 = exp_lp_p1 = 0 |
| exp_hp_cores = set() |
| if "speed-select-base-freq-properties" in linux_config: |
| exp_bf_props = linux_config["speed-select-base-freq-properties"] |
| exp_hp_p1 = int(exp_bf_props["high-priority-base-frequency(MHz)"]) |
| exp_hp_cores = set( |
| map( |
| lambda x: linux_cpu_map[x], |
| map(int, exp_bf_props["high-priority-cpu-list"].split(",")), |
| ) |
| ) |
| exp_lp_p1 = int(exp_bf_props["low-priority-base-frequency(MHz)"]) |
| |
| compare(actual_hp_p1, exp_hp_p1, "SST-BF High Priority P1 Freq") |
| compare(actual_hp_cores, exp_hp_cores, "SST-BF High Priority Core List") |
| compare(actual_lp_p1, exp_lp_p1, "SST-BF Low Priority P1 Freq") |
| |
| compare( |
| redfish_config["MaxJunctionTemperatureCelsius"], |
| int(linux_config["tjunction-max(C)"]), |
| "Junction Temperature", |
| ) |
| # There is no equivalent value in linux for the per-level P0_1 freq. |
| compare(redfish_config["MaxSpeedMHz"], None, "SSE Max Turbo Speed") |
| compare( |
| redfish_config["TDPWatts"], |
| int(linux_config["thermal-design-power(W)"]), |
| "TDP", |
| ) |
| compare( |
| redfish_config["TotalAvailableCoreCount"], |
| int(linux_config["enable-cpu-count"]) // 2, |
| "Enabled Core Count", |
| ) |
| |
| actual_turbo = [ |
| (x["ActiveCoreCount"], x["MaxSpeedMHz"]) |
| for x in redfish_config["TurboProfile"] |
| ] |
| linux_turbo = ( |
| linux_config.get("turbo-ratio-limits-sse") |
| or linux_config["turbo-ratio-limits-level-0"] |
| ) |
| exp_turbo = [] |
| for bucket_key in sorted(linux_turbo.keys()): |
| bucket = linux_turbo[bucket_key] |
| exp_turbo.append( |
| ( |
| int(bucket["core-count"]), |
| int(bucket["max-turbo-frequency(MHz)"]), |
| ) |
| ) |
| compare(actual_turbo, exp_turbo, "SSE Turbo Profile") |
| |
| |
| def get_level_from_config_id(config_id): |
| match = re.match("config(\\d+)", config_id) |
| if not match: |
| raise RuntimeError(f"Invalid config name {config_id}") |
| return match.group(1) |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Compare Redfish SST properties against Linux tools" |
| ) |
| parser.add_argument("hostname") |
| parser.add_argument("--username", "-u", default="root") |
| parser.add_argument("--password", "-p", default="0penBmc") |
| args = parser.parse_args() |
| |
| linux_data = get_linux_output() |
| |
| bmc = redfish.redfish_client( |
| base_url=f"https://{args.hostname}", |
| username=args.username, |
| password=args.password, |
| ) |
| bmc.login(auth="session") |
| |
| # Load the ProcessorCollection |
| resp = json.loads(bmc.get("/redfish/v1/Systems/system/Processors").text) |
| for proc_member in resp["Members"]: |
| proc_resp = json.loads(bmc.get(proc_member["@odata.id"]).text) |
| proc_id = proc_resp["Id"] |
| print() |
| print(f"----Checking Processor {proc_id}----") |
| |
| if proc_resp["Status"]["State"] == "Absent": |
| print("Not populated") |
| continue |
| |
| # Get subset of intel-speed-select data which applies to this CPU |
| pkg_data = get_linux_package(linux_data, proc_id) |
| |
| # Check currently applied config |
| applied_config = proc_resp["AppliedOperatingConfig"][ |
| "@odata.id" |
| ].split("/")[-1] |
| current_level = get_level_from_config_id(applied_config) |
| compare( |
| current_level, |
| pkg_data["get-config-current_level"], |
| "Applied Config", |
| ) |
| |
| exp_cur_level_data = pkg_data[f"perf-profile-level-{current_level}"] |
| |
| # Check whether SST-BF is enabled |
| bf_enabled = proc_resp["BaseSpeedPriorityState"].lower() |
| exp_bf_enabled = exp_cur_level_data["speed-select-base-freq"] |
| if exp_bf_enabled == "unsupported": |
| exp_bf_enabled = "disabled" |
| compare(bf_enabled, exp_bf_enabled, "SST-BF Enabled?") |
| |
| # Check high speed core list |
| hscores = set(proc_resp["HighSpeedCoreIDs"]) |
| exp_hscores = set() |
| if "speed-select-base-freq-properties" in exp_cur_level_data: |
| exp_hscores = exp_cur_level_data[ |
| "speed-select-base-freq-properties" |
| ]["high-priority-cpu-list"] |
| exp_hscores = set( |
| [linux_cpu_map[int(x)] for x in exp_hscores.split(",")] |
| ) |
| compare(hscores, exp_hscores, "High Speed Core List") |
| |
| # Load the OperatingConfigCollection |
| resp = json.loads( |
| bmc.get(proc_resp["OperatingConfigs"]["@odata.id"]).text |
| ) |
| |
| # Check number of available configs |
| profile_keys = list( |
| filter( |
| lambda x: x.startswith("perf-profile-level"), pkg_data.keys() |
| ) |
| ) |
| compare( |
| resp["Members@odata.count"], |
| int(len(profile_keys)), |
| "Number of profiles", |
| ) |
| |
| for config_member in resp["Members"]: |
| # Load each OperatingConfig and compare all its contents |
| config_resp = json.loads(bmc.get(config_member["@odata.id"]).text) |
| level = get_level_from_config_id(config_resp["Id"]) |
| exp_level_data = pkg_data[f"perf-profile-level-{level}"] |
| compare_config(config_resp, exp_level_data) |
| |
| print() |
| if success: |
| print("Everything matched! :)") |
| return 0 |
| else: |
| print("There were mismatches, please check output :(") |
| return 1 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |