1f24d7749SMatthew Barth#!/usr/bin/env python3
25593560bSBrad Bishop
3*0f2588f2SPatrick Williams"""
45593560bSBrad BishopPhosphor Fan Presence (PFP) YAML parser and code generator.
55593560bSBrad Bishop
65593560bSBrad BishopParse the provided PFP configuration file and generate C++ code.
75593560bSBrad Bishop
85593560bSBrad BishopThe parser workflow is broken down as follows:
95593560bSBrad Bishop  1 - Import the YAML configuration file as native python type(s)
105593560bSBrad Bishop        instance(s).
115593560bSBrad Bishop  2 - Create an instance of the Everything class from the
125593560bSBrad Bishop        native python type instance(s) with the Everything.load
135593560bSBrad Bishop        method.
145593560bSBrad Bishop  3 - The Everything class constructor orchestrates conversion of the
155593560bSBrad Bishop        native python type(s) instances(s) to render helper types.
165593560bSBrad Bishop        Each render helper type constructor imports its attributes
175593560bSBrad Bishop        from the native python type(s) instances(s).
185593560bSBrad Bishop  4 - Present the converted YAML to the command processing method
195593560bSBrad Bishop        requested by the script user.
20*0f2588f2SPatrick Williams"""
215593560bSBrad Bishop
225593560bSBrad Bishopimport os
235593560bSBrad Bishopimport sys
245593560bSBrad Bishopfrom argparse import ArgumentParser
25*0f2588f2SPatrick Williams
265593560bSBrad Bishopimport mako.lookup
27*0f2588f2SPatrick Williamsimport yaml
285593560bSBrad Bishopfrom sdbusplus.namedelement import NamedElement
29*0f2588f2SPatrick Williamsfrom sdbusplus.renderer import Renderer
305593560bSBrad Bishop
315593560bSBrad Bishop
32*0f2588f2SPatrick Williamsclass InvalidConfigError(Exception):
33*0f2588f2SPatrick Williams    """General purpose config file parsing error."""
345593560bSBrad Bishop
355593560bSBrad Bishop    def __init__(self, path, msg):
36*0f2588f2SPatrick Williams        """Display configuration file with the syntax
37*0f2588f2SPatrick Williams        error and the error message."""
385593560bSBrad Bishop
395593560bSBrad Bishop        self.config = path
405593560bSBrad Bishop        self.msg = msg
415593560bSBrad Bishop
425593560bSBrad Bishop
435593560bSBrad Bishopclass NotUniqueError(InvalidConfigError):
44*0f2588f2SPatrick Williams    """Within a config file names must be unique.
45*0f2588f2SPatrick Williams    Display the duplicate item."""
465593560bSBrad Bishop
475593560bSBrad Bishop    def __init__(self, path, cls, *names):
485593560bSBrad Bishop        fmt = 'Duplicate {0}: "{1}"'
495593560bSBrad Bishop        super(NotUniqueError, self).__init__(
50*0f2588f2SPatrick Williams            path, fmt.format(cls, " ".join(names))
51*0f2588f2SPatrick Williams        )
525593560bSBrad Bishop
535593560bSBrad Bishop
545593560bSBrad Bishopdef get_index(objs, cls, name):
55*0f2588f2SPatrick Williams    """Items are usually rendered as C++ arrays and as
565593560bSBrad Bishop    such are stored in python lists.  Given an item name
57*0f2588f2SPatrick Williams    its class, find the item index."""
585593560bSBrad Bishop
595593560bSBrad Bishop    for i, x in enumerate(objs.get(cls, [])):
605593560bSBrad Bishop        if x.name != name:
615593560bSBrad Bishop            continue
625593560bSBrad Bishop
635593560bSBrad Bishop        return i
645593560bSBrad Bishop    raise InvalidConfigError('Could not find name: "{0}"'.format(name))
655593560bSBrad Bishop
665593560bSBrad Bishop
675593560bSBrad Bishopdef exists(objs, cls, name):
68*0f2588f2SPatrick Williams    """Check to see if an item already exists in a list given
69*0f2588f2SPatrick Williams    the item name."""
705593560bSBrad Bishop
715593560bSBrad Bishop    try:
725593560bSBrad Bishop        get_index(objs, cls, name)
73*0f2588f2SPatrick Williams    except Exception:
745593560bSBrad Bishop        return False
755593560bSBrad Bishop
765593560bSBrad Bishop    return True
775593560bSBrad Bishop
785593560bSBrad Bishop
795593560bSBrad Bishopdef add_unique(obj, *a, **kw):
80*0f2588f2SPatrick Williams    """Add an item to one or more lists unless already present."""
815593560bSBrad Bishop
825593560bSBrad Bishop    for container in a:
835593560bSBrad Bishop        if not exists(container, obj.cls, obj.name):
845593560bSBrad Bishop            container.setdefault(obj.cls, []).append(obj)
855593560bSBrad Bishop
865593560bSBrad Bishop
875593560bSBrad Bishopclass Indent(object):
88*0f2588f2SPatrick Williams    """Help templates be depth agnostic."""
895593560bSBrad Bishop
905593560bSBrad Bishop    def __init__(self, depth=0):
915593560bSBrad Bishop        self.depth = depth
925593560bSBrad Bishop
935593560bSBrad Bishop    def __add__(self, depth):
945593560bSBrad Bishop        return Indent(self.depth + depth)
955593560bSBrad Bishop
965593560bSBrad Bishop    def __call__(self, depth):
97*0f2588f2SPatrick Williams        """Render an indent at the current depth plus depth."""
98*0f2588f2SPatrick Williams        return 4 * " " * (depth + self.depth)
995593560bSBrad Bishop
1005593560bSBrad Bishop
1015593560bSBrad Bishopclass ConfigEntry(NamedElement):
102*0f2588f2SPatrick Williams    """Base interface for rendered items."""
1035593560bSBrad Bishop
1045593560bSBrad Bishop    def __init__(self, *a, **kw):
105*0f2588f2SPatrick Williams        """Pop the class keyword."""
1065593560bSBrad Bishop
107*0f2588f2SPatrick Williams        self.cls = kw.pop("class")
1085593560bSBrad Bishop        super(ConfigEntry, self).__init__(**kw)
1095593560bSBrad Bishop
1105593560bSBrad Bishop    def factory(self, objs):
111*0f2588f2SPatrick Williams        """Optional factory interface for subclasses to add
112*0f2588f2SPatrick Williams        additional items to be rendered."""
1135593560bSBrad Bishop
1145593560bSBrad Bishop        pass
1155593560bSBrad Bishop
1165593560bSBrad Bishop    def setup(self, objs):
117*0f2588f2SPatrick Williams        """Optional setup interface for subclasses, invoked
118*0f2588f2SPatrick Williams        after all factory methods have been run."""
1195593560bSBrad Bishop
1205593560bSBrad Bishop        pass
1215593560bSBrad Bishop
1225593560bSBrad Bishop
1235593560bSBrad Bishopclass Sensor(ConfigEntry):
124*0f2588f2SPatrick Williams    """Convenience type for config file method:type handlers."""
1255593560bSBrad Bishop
1265593560bSBrad Bishop    def __init__(self, *a, **kw):
127*0f2588f2SPatrick Williams        kw["class"] = "sensor"
128*0f2588f2SPatrick Williams        kw.pop("type")
129*0f2588f2SPatrick Williams        self.policy = kw.pop("policy")
1305593560bSBrad Bishop        super(Sensor, self).__init__(**kw)
1315593560bSBrad Bishop
1325593560bSBrad Bishop    def setup(self, objs):
133*0f2588f2SPatrick Williams        """All sensors have an associated policy.  Get the policy index."""
1345593560bSBrad Bishop
135*0f2588f2SPatrick Williams        self.policy = get_index(objs, "policy", self.policy)
1365593560bSBrad Bishop
1375593560bSBrad Bishop
1385593560bSBrad Bishopclass Gpio(Sensor, Renderer):
139*0f2588f2SPatrick Williams    """Handler for method:type:gpio."""
1405593560bSBrad Bishop
1415593560bSBrad Bishop    def __init__(self, *a, **kw):
142*0f2588f2SPatrick Williams        self.key = kw.pop("key")
143*0f2588f2SPatrick Williams        self.physpath = kw.pop("physpath")
144*0f2588f2SPatrick Williams        self.devpath = kw.pop("devpath")
145*0f2588f2SPatrick Williams        kw["name"] = "gpio-{}".format(self.key)
1465593560bSBrad Bishop        super(Gpio, self).__init__(**kw)
1475593560bSBrad Bishop
1485593560bSBrad Bishop    def construct(self, loader, indent):
149*0f2588f2SPatrick Williams        return self.render(loader, "gpio.mako.hpp", g=self, indent=indent)
1505593560bSBrad Bishop
1515593560bSBrad Bishop    def setup(self, objs):
1525593560bSBrad Bishop        super(Gpio, self).setup(objs)
1535593560bSBrad Bishop
1545593560bSBrad Bishop
1555593560bSBrad Bishopclass Tach(Sensor, Renderer):
156*0f2588f2SPatrick Williams    """Handler for method:type:tach."""
1575593560bSBrad Bishop
1585593560bSBrad Bishop    def __init__(self, *a, **kw):
159*0f2588f2SPatrick Williams        self.sensors = kw.pop("sensors")
160*0f2588f2SPatrick Williams        kw["name"] = "tach-{}".format("-".join(self.sensors))
1615593560bSBrad Bishop        super(Tach, self).__init__(**kw)
1625593560bSBrad Bishop
1635593560bSBrad Bishop    def construct(self, loader, indent):
164*0f2588f2SPatrick Williams        return self.render(loader, "tach.mako.hpp", t=self, indent=indent)
1655593560bSBrad Bishop
1665593560bSBrad Bishop    def setup(self, objs):
1675593560bSBrad Bishop        super(Tach, self).setup(objs)
1685593560bSBrad Bishop
1695593560bSBrad Bishop
1705593560bSBrad Bishopclass Rpolicy(ConfigEntry):
171*0f2588f2SPatrick Williams    """Convenience type for config file rpolicy:type handlers."""
1725593560bSBrad Bishop
1735593560bSBrad Bishop    def __init__(self, *a, **kw):
174*0f2588f2SPatrick Williams        kw.pop("type", None)
175*0f2588f2SPatrick Williams        self.fan = kw.pop("fan")
1765593560bSBrad Bishop        self.sensors = []
177*0f2588f2SPatrick Williams        kw["class"] = "policy"
1785593560bSBrad Bishop        super(Rpolicy, self).__init__(**kw)
1795593560bSBrad Bishop
1805593560bSBrad Bishop    def setup(self, objs):
181*0f2588f2SPatrick Williams        """All policies have an associated fan and methods.
182*0f2588f2SPatrick Williams        Resolve the indices."""
1835593560bSBrad Bishop
1845593560bSBrad Bishop        sensors = []
1855593560bSBrad Bishop        for s in self.sensors:
186*0f2588f2SPatrick Williams            sensors.append(get_index(objs, "sensor", s))
1875593560bSBrad Bishop
1885593560bSBrad Bishop        self.sensors = sensors
189*0f2588f2SPatrick Williams        self.fan = get_index(objs, "fan", self.fan)
1905593560bSBrad Bishop
1915593560bSBrad Bishop
192fcbedca0SBrad Bishopclass AnyOf(Rpolicy, Renderer):
193*0f2588f2SPatrick Williams    """Default policy handler (policy:type:anyof)."""
194fcbedca0SBrad Bishop
195fcbedca0SBrad Bishop    def __init__(self, *a, **kw):
196*0f2588f2SPatrick Williams        kw["name"] = "anyof-{}".format(kw["fan"])
197fcbedca0SBrad Bishop        super(AnyOf, self).__init__(**kw)
198fcbedca0SBrad Bishop
199fcbedca0SBrad Bishop    def setup(self, objs):
200fcbedca0SBrad Bishop        super(AnyOf, self).setup(objs)
201fcbedca0SBrad Bishop
202fcbedca0SBrad Bishop    def construct(self, loader, indent):
203*0f2588f2SPatrick Williams        return self.render(loader, "anyof.mako.hpp", f=self, indent=indent)
204fcbedca0SBrad Bishop
205fcbedca0SBrad Bishop
2065593560bSBrad Bishopclass Fallback(Rpolicy, Renderer):
207*0f2588f2SPatrick Williams    """Fallback policy handler (policy:type:fallback)."""
2085593560bSBrad Bishop
2095593560bSBrad Bishop    def __init__(self, *a, **kw):
210*0f2588f2SPatrick Williams        kw["name"] = "fallback-{}".format(kw["fan"])
2115593560bSBrad Bishop        super(Fallback, self).__init__(**kw)
2125593560bSBrad Bishop
2135593560bSBrad Bishop    def setup(self, objs):
2145593560bSBrad Bishop        super(Fallback, self).setup(objs)
2155593560bSBrad Bishop
2165593560bSBrad Bishop    def construct(self, loader, indent):
217*0f2588f2SPatrick Williams        return self.render(loader, "fallback.mako.hpp", f=self, indent=indent)
2185593560bSBrad Bishop
2195593560bSBrad Bishop
2205593560bSBrad Bishopclass Fan(ConfigEntry):
221*0f2588f2SPatrick Williams    """Fan directive handler.  Fans entries consist of an inventory path,
222*0f2588f2SPatrick Williams    optional redundancy policy and associated sensors."""
2235593560bSBrad Bishop
2245593560bSBrad Bishop    def __init__(self, *a, **kw):
225*0f2588f2SPatrick Williams        self.path = kw.pop("path")
226*0f2588f2SPatrick Williams        self.methods = kw.pop("methods")
227*0f2588f2SPatrick Williams        self.rpolicy = kw.pop("rpolicy", None)
2285593560bSBrad Bishop        super(Fan, self).__init__(**kw)
2295593560bSBrad Bishop
2305593560bSBrad Bishop    def factory(self, objs):
231*0f2588f2SPatrick Williams        """Create rpolicy and sensor(s) objects."""
2325593560bSBrad Bishop
2335593560bSBrad Bishop        if self.rpolicy:
234*0f2588f2SPatrick Williams            self.rpolicy["fan"] = self.name
235*0f2588f2SPatrick Williams            factory = Everything.classmap(self.rpolicy["type"])
2365593560bSBrad Bishop            rpolicy = factory(**self.rpolicy)
2375593560bSBrad Bishop        else:
238fcbedca0SBrad Bishop            rpolicy = AnyOf(fan=self.name)
2395593560bSBrad Bishop
2405593560bSBrad Bishop        for m in self.methods:
241*0f2588f2SPatrick Williams            m["policy"] = rpolicy.name
242*0f2588f2SPatrick Williams            factory = Everything.classmap(m["type"])
2435593560bSBrad Bishop            sensor = factory(**m)
2445593560bSBrad Bishop            rpolicy.sensors.append(sensor.name)
2455593560bSBrad Bishop            add_unique(sensor, objs)
2465593560bSBrad Bishop
2475593560bSBrad Bishop        add_unique(rpolicy, objs)
2485593560bSBrad Bishop        super(Fan, self).factory(objs)
2495593560bSBrad Bishop
2505593560bSBrad Bishop
2515593560bSBrad Bishopclass Everything(Renderer):
252*0f2588f2SPatrick Williams    """Parse/render entry point."""
2535593560bSBrad Bishop
2545593560bSBrad Bishop    @staticmethod
2555593560bSBrad Bishop    def classmap(cls):
256*0f2588f2SPatrick Williams        """Map render item class entries to the appropriate
257*0f2588f2SPatrick Williams        handler methods."""
2585593560bSBrad Bishop
2595593560bSBrad Bishop        class_map = {
260*0f2588f2SPatrick Williams            "anyof": AnyOf,
261*0f2588f2SPatrick Williams            "fan": Fan,
262*0f2588f2SPatrick Williams            "fallback": Fallback,
263*0f2588f2SPatrick Williams            "gpio": Gpio,
264*0f2588f2SPatrick Williams            "tach": Tach,
2655593560bSBrad Bishop        }
2665593560bSBrad Bishop
2675593560bSBrad Bishop        if cls not in class_map:
2685593560bSBrad Bishop            raise NotImplementedError('Unknown class: "{0}"'.format(cls))
2695593560bSBrad Bishop
2705593560bSBrad Bishop        return class_map[cls]
2715593560bSBrad Bishop
2725593560bSBrad Bishop    @staticmethod
2735593560bSBrad Bishop    def load(args):
274*0f2588f2SPatrick Williams        """Load the configuration file.  Parsing occurs in three phases.
2755593560bSBrad Bishop        In the first phase a factory method associated with each
2765593560bSBrad Bishop        configuration file directive is invoked.  These factory
2775593560bSBrad Bishop        methods generate more factory methods.  In the second
2785593560bSBrad Bishop        phase the factory methods created in the first phase
2795593560bSBrad Bishop        are invoked.  In the last phase a callback is invoked on
2805593560bSBrad Bishop        each object created in phase two.  Typically the callback
281*0f2588f2SPatrick Williams        resolves references to other configuration file directives."""
2825593560bSBrad Bishop
2835593560bSBrad Bishop        factory_objs = {}
2845593560bSBrad Bishop        objs = {}
285*0f2588f2SPatrick Williams        with open(args.input, "r") as fd:
2865593560bSBrad Bishop            for x in yaml.safe_load(fd.read()) or {}:
2875593560bSBrad Bishop                # The top level elements all represent fans.
288*0f2588f2SPatrick Williams                x["class"] = "fan"
2895593560bSBrad Bishop                # Create factory object for this config file directive.
290*0f2588f2SPatrick Williams                factory = Everything.classmap(x["class"])
2915593560bSBrad Bishop                obj = factory(**x)
2925593560bSBrad Bishop
2935593560bSBrad Bishop                # For a given class of directive, validate the file
2945593560bSBrad Bishop                # doesn't have any duplicate names.
2955593560bSBrad Bishop                if exists(factory_objs, obj.cls, obj.name):
296*0f2588f2SPatrick Williams                    raise NotUniqueError(args.input, "fan", obj.name)
2975593560bSBrad Bishop
298*0f2588f2SPatrick Williams                factory_objs.setdefault("fan", []).append(obj)
299*0f2588f2SPatrick Williams                objs.setdefault("fan", []).append(obj)
3005593560bSBrad Bishop
3019dc3e0dfSMatthew Barth            for cls, items in list(factory_objs.items()):
3025593560bSBrad Bishop                for obj in items:
3035593560bSBrad Bishop                    # Add objects for template consumption.
3045593560bSBrad Bishop                    obj.factory(objs)
3055593560bSBrad Bishop
3065593560bSBrad Bishop            # Configuration file directives reference each other via
3075593560bSBrad Bishop            # the name attribute; however, when rendered the reference
3085593560bSBrad Bishop            # is just an array index.
3095593560bSBrad Bishop            #
3105593560bSBrad Bishop            # At this point all objects have been created but references
3119a7688a4SGunnar Mills            # have not been resolved to array indices.  Instruct objects
3125593560bSBrad Bishop            # to do that now.
3139dc3e0dfSMatthew Barth            for cls, items in list(objs.items()):
3145593560bSBrad Bishop                for obj in items:
3155593560bSBrad Bishop                    obj.setup(objs)
3165593560bSBrad Bishop
3175593560bSBrad Bishop        return Everything(**objs)
3185593560bSBrad Bishop
3195593560bSBrad Bishop    def __init__(self, *a, **kw):
320*0f2588f2SPatrick Williams        self.fans = kw.pop("fan", [])
321*0f2588f2SPatrick Williams        self.policies = kw.pop("policy", [])
322*0f2588f2SPatrick Williams        self.sensors = kw.pop("sensor", [])
3235593560bSBrad Bishop        super(Everything, self).__init__(**kw)
3245593560bSBrad Bishop
3255593560bSBrad Bishop    def generate_cpp(self, loader):
326*0f2588f2SPatrick Williams        """Render the template with the provided data."""
3275593560bSBrad Bishop        sys.stdout.write(
3285593560bSBrad Bishop            self.render(
3295593560bSBrad Bishop                loader,
3305593560bSBrad Bishop                args.template,
3315593560bSBrad Bishop                fans=self.fans,
3325593560bSBrad Bishop                sensors=self.sensors,
3335593560bSBrad Bishop                policies=self.policies,
334*0f2588f2SPatrick Williams                indent=Indent(),
335*0f2588f2SPatrick Williams            )
336*0f2588f2SPatrick Williams        )
3375593560bSBrad Bishop
338*0f2588f2SPatrick Williams
339*0f2588f2SPatrick Williamsif __name__ == "__main__":
3405593560bSBrad Bishop    script_dir = os.path.dirname(os.path.realpath(__file__))
3415593560bSBrad Bishop    valid_commands = {
342*0f2588f2SPatrick Williams        "generate-cpp": "generate_cpp",
3435593560bSBrad Bishop    }
3445593560bSBrad Bishop
3455593560bSBrad Bishop    parser = ArgumentParser(
346*0f2588f2SPatrick Williams        description=(
347*0f2588f2SPatrick Williams            "Phosphor Fan Presence (PFP) YAML scanner and code generator."
348*0f2588f2SPatrick Williams        )
349*0f2588f2SPatrick Williams    )
3505593560bSBrad Bishop
3515593560bSBrad Bishop    parser.add_argument(
352*0f2588f2SPatrick Williams        "-i",
353*0f2588f2SPatrick Williams        "--input",
354*0f2588f2SPatrick Williams        dest="input",
355*0f2588f2SPatrick Williams        default=os.path.join(script_dir, "example", "example.yaml"),
356*0f2588f2SPatrick Williams        help="Location of config file to process.",
357*0f2588f2SPatrick Williams    )
3585593560bSBrad Bishop    parser.add_argument(
359*0f2588f2SPatrick Williams        "-t",
360*0f2588f2SPatrick Williams        "--template",
361*0f2588f2SPatrick Williams        dest="template",
362*0f2588f2SPatrick Williams        default="generated.mako.hpp",
363*0f2588f2SPatrick Williams        help="The top level template to render.",
364*0f2588f2SPatrick Williams    )
3655593560bSBrad Bishop    parser.add_argument(
366*0f2588f2SPatrick Williams        "-p",
367*0f2588f2SPatrick Williams        "--template-path",
368*0f2588f2SPatrick Williams        dest="template_search",
369*0f2588f2SPatrick Williams        default=os.path.join(script_dir, "templates"),
370*0f2588f2SPatrick Williams        help="The space delimited mako template search path.",
371*0f2588f2SPatrick Williams    )
3725593560bSBrad Bishop    parser.add_argument(
373*0f2588f2SPatrick Williams        "command",
374*0f2588f2SPatrick Williams        metavar="COMMAND",
375*0f2588f2SPatrick Williams        type=str,
3769dc3e0dfSMatthew Barth        choices=list(valid_commands.keys()),
377*0f2588f2SPatrick Williams        help="%s." % " | ".join(list(valid_commands.keys())),
378*0f2588f2SPatrick Williams    )
3795593560bSBrad Bishop
3805593560bSBrad Bishop    args = parser.parse_args()
3815593560bSBrad Bishop
3825593560bSBrad Bishop    if sys.version_info < (3, 0):
3835593560bSBrad Bishop        lookup = mako.lookup.TemplateLookup(
384*0f2588f2SPatrick Williams            directories=args.template_search.split(), disable_unicode=True
385*0f2588f2SPatrick Williams        )
3865593560bSBrad Bishop    else:
3875593560bSBrad Bishop        lookup = mako.lookup.TemplateLookup(
388*0f2588f2SPatrick Williams            directories=args.template_search.split()
389*0f2588f2SPatrick Williams        )
3905593560bSBrad Bishop    try:
391*0f2588f2SPatrick Williams        function = getattr(Everything.load(args), valid_commands[args.command])
3925593560bSBrad Bishop        function(lookup)
3935593560bSBrad Bishop    except InvalidConfigError as e:
394*0f2588f2SPatrick Williams        sys.stderr.write("{0}: {1}\n\n".format(e.config, e.msg))
3955593560bSBrad Bishop        raise
396