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