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