1#!/usr/bin/env python3
2
3import argparse
4from typing import NamedTuple
5
6import yaml
7
8
9class RptSensor(NamedTuple):
10    name: str
11    entityId: int
12    typeId: int
13    evtType: int
14    sensorId: int
15    fru: int
16    targetPath: str
17
18
19sampleDimmTemp = {
20    "bExp": 0,
21    "entityID": 32,
22    "entityInstance": 2,
23    "interfaces": {
24        "xyz.openbmc_project.Sensor.Value": {
25            "Value": {"Offsets": {255: {"type": "int64_t"}}}
26        }
27    },
28    "multiplierM": 1,
29    "mutability": "Mutability::Write|Mutability::Read",
30    "offsetB": -127,
31    "path": "/xyz/openbmc_project/sensors/temperature/dimm0_temp",
32    "rExp": 0,
33    "readingType": "readingData",
34    "scale": -3,
35    "sensorNamePattern": "nameLeaf",
36    "sensorReadingType": 1,
37    "sensorType": 1,
38    "serviceInterface": "org.freedesktop.DBus.Properties",
39    "unit": "xyz.openbmc_project.Sensor.Value.Unit.DegreesC",
40}
41sampleCoreTemp = {
42    "bExp": 0,
43    "entityID": 208,
44    "entityInstance": 2,
45    "interfaces": {
46        "xyz.openbmc_project.Sensor.Value": {
47            "Value": {"Offsets": {255: {"type": "int64_t"}}}
48        }
49    },
50    "multiplierM": 1,
51    "mutability": "Mutability::Write|Mutability::Read",
52    "offsetB": -127,
53    "path": "/xyz/openbmc_project/sensors/temperature/p0_core0_temp",
54    "rExp": 0,
55    "readingType": "readingData",
56    "scale": -3,
57    "sensorNamePattern": "nameLeaf",
58    "sensorReadingType": 1,
59    "sensorType": 1,
60    "serviceInterface": "org.freedesktop.DBus.Properties",
61    "unit": "xyz.openbmc_project.Sensor.Value.Unit.DegreesC",
62}
63samplePower = {
64    "bExp": 0,
65    "entityID": 10,
66    "entityInstance": 13,
67    "interfaces": {
68        "xyz.openbmc_project.Sensor.Value": {
69            "Value": {"Offsets": {255: {"type": "int64_t"}}}
70        }
71    },
72    "multiplierM": 2,
73    "offsetB": 0,
74    "path": "/xyz/openbmc_project/sensors/power/p0_power",
75    "rExp": 0,
76    "readingType": "readingData",
77    "scale": -6,
78    "sensorNamePattern": "nameLeaf",
79    "sensorReadingType": 1,
80    "sensorType": 8,
81    "serviceInterface": "org.freedesktop.DBus.Properties",
82    "unit": "xyz.openbmc_project.Sensor.Value.Unit.Watts",
83}
84
85sampleDcmiSensor = {
86    "instance": 1,
87    "dbus": "/xyz/openbmc_project/sensors/temperature/p0_core0_temp",
88    "record_id": 91,
89}
90
91
92def openYaml(f):
93    return yaml.load(open(f))
94
95
96def saveYaml(y, f, safe=True):
97    if safe:
98        noaliasDumper = yaml.dumper.SafeDumper
99        noaliasDumper.ignore_aliases = lambda self, data: True
100        yaml.dump(
101            y, open(f, "w"), default_flow_style=False, Dumper=noaliasDumper
102        )
103    else:
104        yaml.dump(y, open(f, "w"))
105
106
107def getEntityIdAndNamePattern(p, intfs, m):
108    key = (p, intfs)
109    match = m.get(key, None)
110    if match is None:
111        # Workaround for P8's occ sensors, where the path look like
112        # /org/open_power/control/occ_3_0050
113        if (
114            "/org/open_power/control/occ" in p
115            and "org.open_power.OCC.Status" in intfs
116        ):
117            return (210, "nameLeaf")
118        raise Exception("Unable to find sensor", key, "from map")
119    return (m[key]["entityID"], m[key]["sensorNamePattern"])
120
121
122# Global entity instances
123entityInstances = {}
124
125
126def getEntityInstance(id):
127    instanceId = entityInstances.get(id, 0)
128    instanceId = instanceId + 1
129    entityInstances[id] = instanceId
130    print("EntityId:", id, "InstanceId:", instanceId)
131    return instanceId
132
133
134def loadRpt(rptFile):
135    sensors = []
136    with open(rptFile) as f:
137        next(f)
138        next(f)
139        for line in f:
140            fields = line.strip().split("|")
141            fields = list(map(str.strip, fields))
142            sensor = RptSensor(
143                fields[0],
144                int(fields[2], 16) if fields[2] else None,
145                int(fields[3], 16) if fields[3] else None,
146                int(fields[4], 16) if fields[4] else None,
147                int(fields[5], 16) if fields[5] else None,
148                int(fields[7], 16) if fields[7] else None,
149                fields[9],
150            )
151            # print(sensor)
152            sensors.append(sensor)
153    return sensors
154
155
156def getDimmTempPath(p):
157    # Convert path like: /sys-0/node-0/motherboard-0/dimmconn-0/dimm-0
158    # to: /xyz/openbmc_project/sensors/temperature/dimm0_temp
159    import re
160
161    dimmconn = re.search(r"dimmconn-\d+", p).group()
162    dimmId = re.search(r"\d+", dimmconn).group()
163    return "/xyz/openbmc_project/sensors/temperature/dimm{}_temp".format(
164        dimmId
165    )
166
167
168def getMembufTempPath(name):
169    # Convert names like MEMBUF0_Temp or CENTAUR0_Temp
170    # to: /xyz/openbmc_project/sensors/temperature/membuf0_temp
171    # to: /xyz/openbmc_project/sensors/temperature/centaur0_temp
172    return "/xyz/openbmc_project/sensors/temperature/{}".format(name.lower())
173
174
175def getCoreTempPath(name, p):
176    # For different rpts:
177    # Convert path like:
178    #   /sys-0/node-0/motherboard-0/proc_socket-0/module-0/p9_proc_s/eq0/ex0/core0 (for P9)  # noqa: E501
179    # to: /xyz/openbmc_project/sensors/temperature/p0_core0_temp
180    # or name like: CORE0_Temp (for P8)
181    # to: /xyz/openbmc_project/sensors/temperature/core0_temp (for P8)
182    import re
183
184    if "p9_proc" in p:
185        splitted = p.split("/")
186        socket = re.search(r"\d+", splitted[4]).group()
187        core = re.search(r"\d+", splitted[9]).group()
188        return (
189            "/xyz/openbmc_project/sensors/temperature/p{}_core{}_temp".format(
190                socket, core
191            )
192        )
193    else:
194        core = re.search(r"\d+", name).group()
195        return "/xyz/openbmc_project/sensors/temperature/core{}_temp".format(
196            core
197        )
198
199
200def getPowerPath(name):
201    # Convert name like Proc0_Power
202    # to: /xyz/openbmc_project/sensors/power/p0_power
203    import re
204
205    r = re.search(r"\d+", name)
206    if r:
207        index = r.group()
208    else:
209        # Handle cases like IO_A_Power, Storage_Power_A
210        r = re.search(r"_[A|B|C|D]", name).group()[-1]
211        index = str(ord(r) - ord("A"))
212    prefix = "p"
213    m = None
214    if "memory_proc" in name.lower():
215        prefix = None
216        m = "centaur"
217    elif "pcie_proc" in name.lower():
218        m = "pcie"
219    elif "io" in name.lower():
220        m = "io"
221    elif "fan" in name.lower():
222        m = "fan"
223    elif "storage" in name.lower():
224        m = "disk"
225    elif "total" in name.lower():
226        prefix = None
227        m = "total"
228    elif "proc" in name.lower():
229        # Default
230        pass
231
232    ret = "/xyz/openbmc_project/sensors/power/"
233    if prefix:
234        ret = ret + prefix + index
235    if m:
236        if prefix:
237            ret = ret + "_" + m
238        else:
239            ret = ret + m
240    if prefix is None:
241        ret = ret + index
242    ret = ret + "_power"
243    return ret
244
245
246def getDimmTempConfig(s):
247    r = sampleDimmTemp.copy()
248    r["entityInstance"] = getEntityInstance(r["entityID"])
249    r["path"] = getDimmTempPath(s.targetPath)
250    return r
251
252
253def getMembufTempConfig(s):
254    r = sampleDimmTemp.copy()
255    r["entityID"] = 0xD1
256    r["entityInstance"] = getEntityInstance(r["entityID"])
257    r["path"] = getMembufTempPath(s.name)
258    return r
259
260
261def getCoreTempConfig(s):
262    r = sampleCoreTemp.copy()
263    r["entityInstance"] = getEntityInstance(r["entityID"])
264    r["path"] = getCoreTempPath(s.name, s.targetPath)
265    return r
266
267
268def getPowerConfig(s):
269    r = samplePower.copy()
270    r["entityInstance"] = getEntityInstance(r["entityID"])
271    r["path"] = getPowerPath(s.name)
272    return r
273
274
275def isCoreTemp(p):
276    import re
277
278    m = re.search(r"p\d+_core\d+_temp", p)
279    return m is not None
280
281
282def getDcmiSensor(i, sensor):
283    import re
284
285    path = sensor["path"]
286    name = path.split("/")[-1]
287    m = re.findall(r"\d+", name)
288    socket, core = int(m[0]), int(m[1])
289    instance = socket * 24 + core + 1
290    r = sampleDcmiSensor.copy()
291    r["instance"] = instance
292    r["dbus"] = path
293    r["record_id"] = i
294    return r
295
296
297def saveJson(data, f):
298    import json
299
300    with open(f, "w") as outfile:
301        json.dump(data, outfile, indent=4)
302
303
304def main():
305    parser = argparse.ArgumentParser(
306        description="Yaml tool for updating ipmi sensor yaml config"
307    )
308    parser.add_argument(
309        "-i",
310        "--input",
311        required=True,
312        dest="input",
313        help="The ipmi sensor yaml config",
314    )
315    parser.add_argument(
316        "-o",
317        "--output",
318        required=True,
319        dest="output",
320        help="The output yaml file",
321    )
322    parser.add_argument(
323        "-m",
324        "--map",
325        dest="map",
326        default="sensor_map.yaml",
327        help="The sample map yaml file",
328    )
329    parser.add_argument(
330        "-r", "--rpt", dest="rpt", help="The .rpt file generated by op-build"
331    )
332    parser.add_argument(
333        "-f",
334        "--fix",
335        action="store_true",
336        help="Fix entities and sensorNamePattern",
337    )
338
339    # -g expects output as yaml for mapping of entityID/sensorNamePattern
340    # -d expects output as json config for dcmi sensors
341    # Do not mess the output by enforcing only one argument is passed
342    # TODO: -f and -r could be used together, and they are conflicted with
343    #       -g or -d
344    group = parser.add_mutually_exclusive_group()
345    group.add_argument(
346        "-g",
347        "--generate",
348        action="store_true",
349        help="Generate maps for entityID and sensorNamePattern",
350    )
351    group.add_argument(
352        "-d",
353        "--dcmi",
354        action="store_true",
355        help="Generate dcmi sensors json config",
356    )
357
358    args = parser.parse_args()
359    args = vars(args)
360
361    if args["input"] is None or args["output"] is None:
362        parser.print_help()
363        exit(1)
364
365    y = openYaml(args["input"])
366
367    if args["fix"]:
368        # Fix entities and sensorNamePattern
369        m = openYaml(args["map"])
370
371        for i in y:
372            path = y[i]["path"]
373            intfs = tuple(sorted(list(y[i]["interfaces"].keys())))
374            entityId, namePattern = getEntityIdAndNamePattern(path, intfs, m)
375            y[i]["entityID"] = entityId
376            y[i]["entityInstance"] = getEntityInstance(entityId)
377            y[i]["sensorNamePattern"] = namePattern
378            print(
379                y[i]["path"],
380                "id:",
381                entityId,
382                "instance:",
383                y[i]["entityInstance"],
384            )
385
386    sensorIds = list(y.keys())
387    if args["rpt"]:
388        unhandledSensors = []
389        rptSensors = loadRpt(args["rpt"])
390        for s in rptSensors:
391            if s.sensorId is not None and s.sensorId not in sensorIds:
392                print(
393                    "Sensor ID",
394                    s.sensorId,
395                    "not in yaml:",
396                    s.name,
397                    ", path:",
398                    s.targetPath,
399                )
400                isAdded = False
401                if "temp" in s.name.lower():
402                    if "dimm" in s.targetPath.lower():
403                        y[s.sensorId] = getDimmTempConfig(s)
404                        isAdded = True
405                    elif "core" in s.targetPath.lower():
406                        y[s.sensorId] = getCoreTempConfig(s)
407                        isAdded = True
408                    elif (
409                        "centaur" in s.name.lower()
410                        or "membuf" in s.name.lower()
411                    ):
412                        y[s.sensorId] = getMembufTempConfig(s)
413                        isAdded = True
414                elif s.name.lower().endswith("_power"):
415                    y[s.sensorId] = getPowerConfig(s)
416                    isAdded = True
417
418                if isAdded:
419                    print(
420                        "Added sensor id:",
421                        s.sensorId,
422                        ", path:",
423                        y[s.sensorId]["path"],
424                    )
425                else:
426                    unhandledSensors.append(s)
427
428        print("Unhandled sensors:")
429        for s in unhandledSensors:
430            print(s)
431
432    if args["generate"]:
433        m = {}
434        for i in y:
435            path = y[i]["path"]
436            intfs = tuple(sorted(list(y[i]["interfaces"].keys())))
437            entityId = y[i]["entityID"]
438            sensorNamePattern = y[i]["sensorNamePattern"]
439            m[(path, intfs)] = {
440                "entityID": entityId,
441                "sensorNamePattern": sensorNamePattern,
442            }
443        y = m
444
445    if args["dcmi"]:
446        d = []
447        for i in y:
448            if isCoreTemp(y[i]["path"]):
449                s = getDcmiSensor(i, y[i])
450                d.append(s)
451                print(s)
452        saveJson(d, args["output"])
453        return
454
455    safe = False if args["generate"] else True
456    saveYaml(y, args["output"], safe)
457
458
459if __name__ == "__main__":
460    main()
461