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