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 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
77def 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                            "TimerConf{static_cast<std::chrono::seconds>(" +
129                            str(eActions[p]['delay']) + "), " +
130                            "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
152def 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'] = "TimerType::repeating"
261    event['timer'] = timer
262
263    return event
264
265
266def 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'] = "TimerType::repeating"
413    precond['pctime'] = timer
414
415    return precond
416
417
418def 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
444def 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
478def 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
505def 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
581if __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