xref: /openbmc/phosphor-fan-presence/control/gen-fan-zone-defs.py (revision 702c4a55a69915c3ab3506ee552acc5c21e06e2a)
1 #!/usr/bin/env python
2 
3 """
4 This script reads in fan definition and zone definition YAML
5 files and generates a set of structures for use by the fan control code.
6 """
7 
8 import os
9 import sys
10 import yaml
11 from argparse import ArgumentParser
12 from mako.template import Template
13 from mako.lookup import TemplateLookup
14 
15 
16 def convertToMap(listOfDict):
17     """
18     Converts a list of dictionary entries to a std::map initialization list.
19     """
20     listOfDict = listOfDict.replace('\'', '\"')
21     listOfDict = listOfDict.replace('[', '{')
22     listOfDict = listOfDict.replace(']', '}')
23     listOfDict = listOfDict.replace(':', ',')
24     return listOfDict
25 
26 
27 def getGroups(zNum, zCond, edata, events):
28     """
29     Extract and construct the groups for the given event.
30     """
31     groups = []
32     for eGroups in edata['groups']:
33         if ('zone_conditions' in eGroups) and \
34            (eGroups['zone_conditions'] is not None):
35             # Zone conditions are optional in the events yaml but skip
36             # if this event's condition is not in this zone's conditions
37             if all('name' in z and z['name'] is not None and
38                    not any(c['name'] == z['name'] for c in zCond)
39                    for z in eGroups['zone_conditions']):
40                 continue
41 
42             # Zone numbers are optional in the events yaml but skip if this
43             # zone's zone number is not in the event's zone numbers
44             if all('zones' in z and z['zones'] is not None and
45                    zNum not in z['zones']
46                    for z in eGroups['zone_conditions']):
47                 continue
48 
49         eGroup = next(g for g in events['groups']
50                       if g['name'] == eGroups['name'])
51 
52         group = {}
53         members = []
54         group['name'] = eGroup['name']
55         for m in eGroup['members']:
56             member = {}
57             member['path'] = eGroup['type']
58             member['object'] = (eGroup['type'] + m)
59             member['interface'] = eGroups['interface']
60             member['property'] = eGroups['property']['name']
61             member['type'] = eGroups['property']['type']
62             # Add expected group member's property value if given
63             if ('value' in eGroups['property']) and \
64                (eGroups['property']['value'] is not None):
65                     if isinstance(eGroups['property']['value'], str) or \
66                             "string" in str(member['type']).lower():
67                         member['value'] = (
68                             "\"" + eGroups['property']['value'] + "\"")
69                     else:
70                         member['value'] = eGroups['property']['value']
71             members.append(member)
72         group['members'] = members
73         groups.append(group)
74     return groups
75 
76 
77 def getActions(edata, actions, events):
78     """
79     Extracts and constructs the make_action function call for
80     all the actions within the given event.
81     """
82     action = []
83     for eActions in actions['actions']:
84         actions = {}
85         eAction = next(a for a in events['actions']
86                        if a['name'] == eActions['name'])
87         actions['name'] = eAction['name']
88         params = []
89         if ('parameters' in eAction) and \
90            (eAction['parameters'] is not None):
91             for p in eAction['parameters']:
92                 param = "static_cast<"
93                 if type(eActions[p]) is not dict:
94                     if p == 'actions':
95                         param = "std::vector<Action>{"
96                         pActs = getActions(edata, eActions, events)
97                         for a in pActs:
98                             if (len(a['parameters']) != 0):
99                                 param += (
100                                     "make_action(action::" +
101                                     a['name'] +
102                                     "(\n")
103                                 for i, ap in enumerate(a['parameters']):
104                                     if (i+1) != len(a['parameters']):
105                                         param += (ap + ",")
106                                     else:
107                                         param += (ap + ")")
108                             else:
109                                 param += ("make_action(action::" + a['name'])
110                             param += "),"
111                         param += "}"
112                     elif p == 'property':
113                         if isinstance(eActions[p], str) or \
114                            "string" in str(eActions[p]['type']).lower():
115                             param += (
116                                 str(eActions[p]['type']).lower() +
117                                 ">(\"" + str(eActions[p]) + "\")")
118                         else:
119                             param += (
120                                 str(eActions[p]['type']).lower() +
121                                 ">(" + str(eActions[p]['value']).lower() + ")")
122                     else:
123                         # Default type to 'size_t' when not given
124                         param += ("size_t>(" + str(eActions[p]).lower() + ")")
125                 else:
126                     if p == 'timer':
127                         param = (
128                             "Timer{static_cast<std::chrono::seconds>(" +
129                             str(eActions[p]['delay']) + "), " +
130                             "util::Timer::TimerType::" +
131                             str(eActions[p]['type']) + "}")
132                     else:
133                         param += (str(eActions[p]['type']).lower() + ">(")
134                         if p != 'map':
135                             if isinstance(eActions[p]['value'], str) or \
136                                "string" in str(eActions[p]['type']).lower():
137                                 param += \
138                                     "\"" + str(eActions[p]['value']) + "\")"
139                             else:
140                                 param += \
141                                     str(eActions[p]['value']).lower() + ")"
142                         else:
143                             param += (
144                                 str(eActions[p]['type']).lower() +
145                                 convertToMap(str(eActions[p]['value'])) + ")")
146                 params.append(param)
147         actions['parameters'] = params
148         action.append(actions)
149     return action
150 
151 
152 def getEvent(zone_num, zone_conditions, e, events_data):
153     """
154     Parses the sections of an event and populates the properties
155     that construct an event within the generated source.
156     """
157     event = {}
158 
159     # Add set speed event groups
160     grps = getGroups(zone_num, zone_conditions, e, events_data)
161     if not grps:
162         return
163     event['groups'] = grps
164 
165     # Add optional set speed actions and function parameters
166     event['action'] = []
167     if ('actions' in e) and \
168        (e['actions'] is not None):
169         event['action'] = getActions(e, e, events_data)
170 
171     # Add signal handlers
172     signals = []
173     for group in event['groups']:
174         for member in group['members']:
175             for eMatches in e['matches']:
176                 signal = {}
177                 eMatch = next(m for m in events_data['matches']
178                               if m['name'] == eMatches['name'])
179                 signal['match'] = eMatch['name']
180                 params = []
181                 if ('parameters' in eMatch) and \
182                    (eMatch['parameters'] is not None):
183                     for p in eMatch['parameters']:
184                         params.append(member[str(p)])
185                 signal['mparams'] = params
186                 if ('parameters' in eMatch['signal']) and \
187                    (eMatch['signal']['parameters'] is not None):
188                     eSignal = eMatch['signal']
189                 else:
190                     eSignal = next(s for s in events_data['signals']
191                                    if s['name'] == eMatch['signal'])
192                 signal['signal'] = eSignal['name']
193                 sparams = {}
194                 if ('parameters' in eSignal) and \
195                    (eSignal['parameters'] is not None):
196                     splist = []
197                     for p in eSignal['parameters']:
198                         sp = str(p)
199                         if (sp != 'type'):
200                             splist.append(sp)
201                             if (sp != 'group'):
202                                 sparams[sp] = "\"" + member[sp] + "\""
203                             else:
204                                 sparams[sp] = "Group{\n"
205                                 for m in group['members']:
206                                     sparams[sp] += (
207                                         "{\n" +
208                                         "\"" + str(m['object']) + "\",\n" +
209                                         "{\"" + str(m['interface']) + "\"," +
210                                         "\"" + str(m['property']) + "\"}\n" +
211                                         "},\n")
212                                 sparams[sp] += "}"
213                         else:
214                             sparams[sp] = member[sp]
215                     sparams['params'] = splist
216                 signal['sparams'] = sparams
217                 # Add signal handler
218                 eHandler = next(h for h in events_data['handlers']
219                                 if h['name'] == eSignal['handler'])
220                 signal['handler'] = eHandler['name']
221                 hparams = {}
222                 if ('parameters' in eHandler) and \
223                    (eHandler['parameters'] is not None):
224                     hplist = []
225                     for p in eHandler['parameters']:
226                         hp = str(p)
227                         if (hp != 'type'):
228                             hplist.append(hp)
229                             if (hp != 'group'):
230                                 hparams[hp] = "\"" + member[hp] + "\""
231                             else:
232                                 hparams[hp] = "Group{\n"
233                                 for m in group['members']:
234                                     hparams[hp] += (
235                                         "{\n" +
236                                         "\"" + str(m['object']) + "\",\n" +
237                                         "{\"" + str(m['interface']) + "\"," +
238                                         "\"" + str(m['property']) + "\"}\n" +
239                                         "},\n")
240                                 hparams[hp] += "}"
241                         else:
242                             hparams[hp] = member[hp]
243                     hparams['params'] = hplist
244                 signal['hparams'] = hparams
245                 signals.append(signal)
246     event['signals'] = signals
247 
248     # Add optional action call timer
249     timer = {}
250     interval = "static_cast<std::chrono::seconds>"
251     if ('timer' in e) and \
252        (e['timer'] is not None):
253         timer['interval'] = (interval +
254                              "(" +
255                              str(e['timer']['interval']) +
256                              ")")
257     else:
258         timer['interval'] = (interval +
259                              "(" + str(0) + ")")
260     timer['type'] = "util::Timer::TimerType::repeating"
261     event['timer'] = timer
262 
263     return event
264 
265 
266 def addPrecondition(zNum, zCond, event, events_data):
267     """
268     Parses the precondition section of an event and populates the necessary
269     structures to generate a precondition for a set speed event.
270     """
271     precond = {}
272     # Add set speed event precondition group
273     grps = getGroups(zNum, zCond, event['precondition'], events_data)
274     if not grps:
275         return
276     precond['pcgrps'] = grps
277 
278     # Add set speed event precondition actions
279     pc = []
280     pcs = {}
281     pcs['name'] = event['precondition']['name']
282     epc = next(p for p in events_data['preconditions']
283                if p['name'] == event['precondition']['name'])
284     params = []
285     for p in epc['parameters']:
286         param = {}
287         if p == 'groups':
288             param['type'] = "std::vector<PrecondGroup>"
289             param['open'] = "{"
290             param['close'] = "}"
291             values = []
292             for group in precond['pcgrps']:
293                 for pcgrp in group['members']:
294                     value = {}
295                     value['value'] = (
296                         "PrecondGroup{\"" +
297                         str(pcgrp['object']) + "\",\"" +
298                         str(pcgrp['interface']) + "\",\"" +
299                         str(pcgrp['property']) + "\"," +
300                         "static_cast<" +
301                         str(pcgrp['type']).lower() + ">")
302                     if isinstance(pcgrp['value'], str) or \
303                        "string" in str(pcgrp['type']).lower():
304                         value['value'] += ("(" + str(pcgrp['value']) + ")}")
305                     else:
306                         value['value'] += \
307                             ("(" + str(pcgrp['value']).lower() + ")}")
308                     values.append(value)
309             param['values'] = values
310         params.append(param)
311     pcs['params'] = params
312     pc.append(pcs)
313     precond['pcact'] = pc
314 
315     pcevents = []
316     for pce in event['precondition']['events']:
317         pcevent = getEvent(zNum, zCond, pce, events_data)
318         if not pcevent:
319             continue
320         pcevents.append(pcevent)
321     precond['pcevts'] = pcevents
322 
323     # Add precondition signal handlers
324     signals = []
325     for group in precond['pcgrps']:
326         for member in group['members']:
327             for eMatches in event['precondition']['matches']:
328                 signal = {}
329                 eMatch = next(m for m in events_data['matches']
330                               if m['name'] == eMatches['name'])
331                 signal['match'] = eMatch['name']
332                 params = []
333                 if ('parameters' in eMatch) and \
334                    (eMatch['parameters'] is not None):
335                     for p in eMatch['parameters']:
336                         params.append(member[str(p)])
337                 signal['mparams'] = params
338                 if ('parameters' in eMatch['signal']) and \
339                    (eMatch['signal']['parameters'] is not None):
340                     eSignal = eMatch['signal']
341                 else:
342                     eSignal = next(s for s in events_data['signals']
343                                    if s['name'] == eMatch['signal'])
344                 signal['signal'] = eSignal['name']
345                 sparams = {}
346                 if ('parameters' in eSignal) and \
347                    (eSignal['parameters'] is not None):
348                     splist = []
349                     for p in eSignal['parameters']:
350                         sp = str(p)
351                         if (sp != 'type'):
352                             splist.append(sp)
353                             if (sp != 'group'):
354                                 sparams[sp] = "\"" + member[sp] + "\""
355                             else:
356                                 sparams[sp] = "Group{\n"
357                                 for m in group:
358                                     sparams[sp] += (
359                                         "{\n" +
360                                         "\"" + str(m['object']) + "\",\n" +
361                                         "{\"" + str(m['interface']) + "\"," +
362                                         "\"" + str(m['property']) + "\"}\n" +
363                                         "},\n")
364                                 sparams[sp] += "}"
365                         else:
366                             sparams[sp] = member[sp]
367                     sparams['params'] = splist
368                 signal['sparams'] = sparams
369                 # Add signal handler
370                 eHandler = next(h for h in events_data['handlers']
371                                 if h['name'] == eSignal['handler'])
372                 signal['handler'] = eHandler['name']
373                 hparams = {}
374                 if ('parameters' in eHandler) and \
375                    (eHandler['parameters'] is not None):
376                     hplist = []
377                     for p in eHandler['parameters']:
378                         hp = str(p)
379                         if (hp != 'type'):
380                             hplist.append(hp)
381                             if (hp != 'group'):
382                                 hparams[hp] = "\"" + member[hp] + "\""
383                             else:
384                                 hparams[hp] = "Group{\n"
385                                 for m in group:
386                                     hparams[hp] += (
387                                         "{\n" +
388                                         "\"" + str(m['object']) + "\",\n" +
389                                         "{\"" + str(m['interface']) + "\"," +
390                                         "\"" + str(m['property']) + "\"}\n" +
391                                         "},\n")
392                                 hparams[hp] += "}"
393                         else:
394                             hparams[hp] = member[hp]
395                     hparams['params'] = hplist
396                 signal['hparams'] = hparams
397                 signals.append(signal)
398     precond['pcsigs'] = signals
399 
400     # Add optional action call timer
401     timer = {}
402     interval = "static_cast<std::chrono::seconds>"
403     if ('timer' in event['precondition']) and \
404        (event['precondition']['timer'] is not None):
405         timer['interval'] = (interval +
406                              "(" +
407                              str(event['precondition']['timer']['interval']) +
408                              ")")
409     else:
410         timer['interval'] = (interval +
411                              "(" + str(0) + ")")
412     timer['type'] = "util::Timer::TimerType::repeating"
413     precond['pctime'] = timer
414 
415     return precond
416 
417 
418 def getEventsInZone(zone_num, zone_conditions, events_data):
419     """
420     Constructs the event entries defined for each zone using the events yaml
421     provided.
422     """
423     events = []
424 
425     if 'events' in events_data:
426         for e in events_data['events']:
427             event = {}
428             # Add precondition if given
429             if ('precondition' in e) and \
430                (e['precondition'] is not None):
431                 event['pc'] = addPrecondition(zone_num,
432                                               zone_conditions,
433                                               e,
434                                               events_data)
435             else:
436                 event = getEvent(zone_num, zone_conditions, e, events_data)
437                 if not event:
438                     continue
439             events.append(event)
440 
441     return events
442 
443 
444 def getFansInZone(zone_num, profiles, fan_data):
445     """
446     Parses the fan definition YAML files to find the fans
447     that match both the zone passed in and one of the
448     cooling profiles.
449     """
450 
451     fans = []
452 
453     for f in fan_data['fans']:
454 
455         if zone_num != f['cooling_zone']:
456             continue
457 
458         # 'cooling_profile' is optional (use 'all' instead)
459         if f.get('cooling_profile') is None:
460             profile = "all"
461         else:
462             profile = f['cooling_profile']
463 
464         if profile not in profiles:
465             continue
466 
467         fan = {}
468         fan['name'] = f['inventory']
469         fan['sensors'] = f['sensors']
470         fan['target_interface'] = f.get(
471             'target_interface',
472             'xyz.openbmc_project.Control.FanSpeed')
473         fans.append(fan)
474 
475     return fans
476 
477 
478 def getConditionInZoneConditions(zone_condition, zone_conditions_data):
479     """
480     Parses the zone conditions definition YAML files to find the condition
481     that match both the zone condition passed in.
482     """
483 
484     condition = {}
485 
486     for c in zone_conditions_data['conditions']:
487 
488         if zone_condition != c['name']:
489             continue
490         condition['type'] = c['type']
491         properties = []
492         for p in c['properties']:
493             property = {}
494             property['property'] = p['property']
495             property['interface'] = p['interface']
496             property['path'] = p['path']
497             property['type'] = p['type'].lower()
498             property['value'] = str(p['value']).lower()
499             properties.append(property)
500         condition['properties'] = properties
501 
502         return condition
503 
504 
505 def buildZoneData(zone_data, fan_data, events_data, zone_conditions_data):
506     """
507     Combines the zone definition YAML and fan
508     definition YAML to create a data structure defining
509     the fan cooling zones.
510     """
511 
512     zone_groups = []
513 
514     for group in zone_data:
515         conditions = []
516         # zone conditions are optional
517         if 'zone_conditions' in group and group['zone_conditions'] is not None:
518             for c in group['zone_conditions']:
519 
520                 if not zone_conditions_data:
521                     sys.exit("No zone_conditions YAML file but " +
522                              "zone_conditions used in zone YAML")
523 
524                 condition = getConditionInZoneConditions(c['name'],
525                                                          zone_conditions_data)
526 
527                 if not condition:
528                     sys.exit("Missing zone condition " + c['name'])
529 
530                 conditions.append(condition)
531 
532         zone_group = {}
533         zone_group['conditions'] = conditions
534 
535         zones = []
536         for z in group['zones']:
537             zone = {}
538 
539             # 'zone' is required
540             if ('zone' not in z) or (z['zone'] is None):
541                 sys.exit("Missing fan zone number in " + zone_yaml)
542 
543             zone['num'] = z['zone']
544 
545             zone['full_speed'] = z['full_speed']
546 
547             zone['default_floor'] = z['default_floor']
548 
549             # 'increase_delay' is optional (use 0 by default)
550             key = 'increase_delay'
551             zone[key] = z.setdefault(key, 0)
552 
553             # 'decrease_interval' is optional (use 0 by default)
554             key = 'decrease_interval'
555             zone[key] = z.setdefault(key, 0)
556 
557             # 'cooling_profiles' is optional (use 'all' instead)
558             if ('cooling_profiles' not in z) or \
559                     (z['cooling_profiles'] is None):
560                 profiles = ["all"]
561             else:
562                 profiles = z['cooling_profiles']
563 
564             fans = getFansInZone(z['zone'], profiles, fan_data)
565             events = getEventsInZone(z['zone'], group['zone_conditions'],
566                                      events_data)
567 
568             if len(fans) == 0:
569                 sys.exit("Didn't find any fans in zone " + str(zone['num']))
570 
571             zone['fans'] = fans
572             zone['events'] = events
573             zones.append(zone)
574 
575         zone_group['zones'] = zones
576         zone_groups.append(zone_group)
577 
578     return zone_groups
579 
580 
581 if __name__ == '__main__':
582     parser = ArgumentParser(
583         description="Phosphor fan zone definition parser")
584 
585     parser.add_argument('-z', '--zone_yaml', dest='zone_yaml',
586                         default="example/zones.yaml",
587                         help='fan zone definitional yaml')
588     parser.add_argument('-f', '--fan_yaml', dest='fan_yaml',
589                         default="example/fans.yaml",
590                         help='fan definitional yaml')
591     parser.add_argument('-e', '--events_yaml', dest='events_yaml',
592                         help='events to set speeds yaml')
593     parser.add_argument('-c', '--zone_conditions_yaml',
594                         dest='zone_conditions_yaml',
595                         help='conditions to determine zone yaml')
596     parser.add_argument('-o', '--output_dir', dest='output_dir',
597                         default=".",
598                         help='output directory')
599     args = parser.parse_args()
600 
601     if not args.zone_yaml or not args.fan_yaml:
602         parser.print_usage()
603         sys.exit(-1)
604 
605     with open(args.zone_yaml, 'r') as zone_input:
606         zone_data = yaml.safe_load(zone_input) or {}
607 
608     with open(args.fan_yaml, 'r') as fan_input:
609         fan_data = yaml.safe_load(fan_input) or {}
610 
611     events_data = {}
612     if args.events_yaml:
613         with open(args.events_yaml, 'r') as events_input:
614             events_data = yaml.safe_load(events_input) or {}
615 
616     zone_conditions_data = {}
617     if args.zone_conditions_yaml:
618         with open(args.zone_conditions_yaml, 'r') as zone_conditions_input:
619             zone_conditions_data = yaml.safe_load(zone_conditions_input) or {}
620 
621     zone_config = buildZoneData(zone_data.get('zone_configuration', {}),
622                                 fan_data, events_data, zone_conditions_data)
623 
624     manager_config = zone_data.get('manager_configuration', {})
625 
626     if manager_config.get('power_on_delay') is None:
627         manager_config['power_on_delay'] = 0
628 
629     tmpls_dir = os.path.join(
630         os.path.dirname(os.path.realpath(__file__)),
631         "templates")
632     output_file = os.path.join(args.output_dir, "fan_zone_defs.cpp")
633     if sys.version_info < (3, 0):
634         lkup = TemplateLookup(
635             directories=tmpls_dir.split(),
636             disable_unicode=True)
637     else:
638         lkup = TemplateLookup(
639             directories=tmpls_dir.split())
640     tmpl = lkup.get_template('fan_zone_defs.mako.cpp')
641     with open(output_file, 'w') as output:
642         output.write(tmpl.render(zones=zone_config,
643                                  mgr_data=manager_config))
644