1"""
2This file contains the FactoryLoader class that is used to dynamically
3create Runstat Table and View objects from a <dict> of data.  The <dict> can
4originate from any kind of source: YAML, JSON, program.  For examples of YAML
5refer to the .yml files in this jnpr.junos.op directory.
6"""
7# stdlib
8from copy import deepcopy
9import re
10
11from jinja2 import Environment
12
13# locally
14from jnpr.junos.factory.factory_cls import *
15from jnpr.junos.factory.viewfields import *
16
17__all__ = ["FactoryLoader"]
18
19# internally used shortcuts
20
21_VIEW = FactoryView
22_CMDVIEW = FactoryCMDView
23_FIELDS = ViewFields
24_GET = FactoryOpTable
25_TABLE = FactoryTable
26_CFGTBL = FactoryCfgTable
27_CMDTBL = FactoryCMDTable
28_CMDCHILDTBL = FactoryCMDChildTable
29
30
31class FactoryLoader(object):
32
33    """
34    Used to load a <dict> of data that contains Table and View definitions.
35
36    The primary method is :load(): which will return a <dict> of item-name and
37    item-class definitions.
38
39    If you want to import these definitions directly into your namespace,
40    (like a module) you would do the following:
41
42      loader = FactoryLoader()
43      catalog = loader.load( <catalog_dict> )
44      globals().update( catalog )
45
46    If you did not want to do this, you can access the items as the catalog.
47    For example, if your <catalog_dict> contained a Table called MyTable, then
48    you could do something like:
49
50      MyTable = catalog['MyTable']
51      table = MyTable(dev)
52      table.get()
53      ...
54    """
55
56    def __init__(self):
57        self._catalog_dict = None  # YAML data
58
59        self._item_optables = []  # list of the get/op-tables
60        self._item_cfgtables = []  # list of get/cfg-tables
61        self._item_cmdtables = []  # list of commands with unstructured data o/p
62        self._item_views = []  # list of views to build
63        self._item_tables = []  # list of tables to build
64
65        self.catalog = {}  # catalog of built classes
66
67    # -----------------------------------------------------------------------
68    # Create a View class from YAML definition
69    # -----------------------------------------------------------------------
70
71    def _fieldfunc_True(self, value_rhs):
72        def true_test(x):
73            if value_rhs.startswith("regex("):
74                return True if bool(re.search(value_rhs.strip("regex()"), x)) else False
75            return x == value_rhs
76
77        return true_test
78
79    def _fieldfunc_False(self, value_rhs):
80        def false_test(x):
81            if value_rhs.startswith("regex("):
82                return False if bool(re.search(value_rhs.strip("regex()"), x)) else True
83            return x != value_rhs
84
85        return false_test
86
87    def _fieldfunc_Search(self, regex_pattern):
88        def search_field(field_text):
89            """ Returns the first occurrence of regex_pattern within given field_text."""
90            match = re.search(regex_pattern, field_text)
91            if match:
92                return match.groups()[0]
93            else:
94                return None
95
96        return search_field
97
98    def _add_dictfield(self, fields, f_name, f_dict, kvargs):
99        """ add a field based on its associated dictionary """
100        # at present if a field is a <dict> then there is **one
101        # item** - { the xpath value : the option control }.  typically
102        # the option would be a bultin class type like 'int'
103        # however, as this framework expands in capability, this
104        # will be enhaced, yo!
105
106        xpath, opt = list(f_dict.items())[0]  # get first/only key,value
107
108        if opt == "group":
109            fields.group(f_name, xpath)
110            return
111
112        if "flag" == opt:
113            opt = "bool"  # flag is alias for bool
114
115        # first check to see if the option is a built-in Python
116        # type, most commonly would be 'int' for numbers, like counters
117        if isinstance(opt, dict):
118            kvargs.update(opt)
119            fields.str(f_name, xpath, **kvargs)
120            return
121
122        astype = __builtins__.get(opt) or globals().get(opt)
123        if astype is not None:
124            kvargs["astype"] = astype
125            fields.astype(f_name, xpath, **kvargs)
126            return
127
128        # next check to see if this is a "field-function"
129        # operator in the form "func=value", like "True=enabled"
130
131        if isinstance(opt, str) and opt.find("=") > 0:
132            field_cmd, value_rhs = opt.split("=")
133            fn_field = "_fieldfunc_" + field_cmd
134            if not hasattr(self, fn_field):
135                raise ValueError("Unknown field-func: '%'" % field_cmd)
136            kvargs["astype"] = getattr(self, fn_field)(value_rhs)
137            fields.astype(f_name, xpath, **kvargs)
138            return
139
140        raise RuntimeError("Dont know what to do with field: '%s'" % f_name)
141
142    # ---[ END: _add_dictfield ] ---------------------------------------------
143
144    def _add_view_fields(self, view_dict, fields_name, fields):
145        """ add a group of fields to the view """
146        fields_dict = view_dict[fields_name]
147        try:
148            # see if this is a 'fields_<group>' collection, and if so
149            # then we automatically setup using the group mechanism
150            mark = fields_name.index("_")
151            group = {"group": fields_name[mark + 1 :]}
152        except:
153            # otherwise, no group, just standard 'fields'
154            group = {}
155
156        for f_name, f_data in fields_dict.items():
157            # each field could have its own unique set of properties
158            # so create a kvargs <dict> each time.  but copy in the
159            # groups <dict> (single item) generically.
160            kvargs = {}
161            kvargs.update(group)
162
163            if isinstance(f_data, dict):
164                self._add_dictfield(fields, f_name, f_data, kvargs)
165                continue
166
167            if f_data in self._catalog_dict:
168                # f_data is the table name
169                cls_tbl = self.catalog.get(f_data, self._build_table(f_data))
170                fields.table(f_name, cls_tbl)
171                continue
172
173            # if we are here, then it means that the field is a string value
174            xpath = f_name if f_data is True else f_data
175            fields.str(f_name, xpath, **kvargs)
176
177    def _add_cmd_view_fields(self, view_dict, fields_name, fields):
178        """ add a group of fields to the view """
179        fields_dict = view_dict[fields_name]
180        for f_name, f_data in fields_dict.items():
181            if f_data in self._catalog_dict:
182                cls_tbl = self.catalog.get(f_data, self._build_cmdtable(f_data))
183                fields.table(f_name, cls_tbl)
184                continue
185
186            # if we are here, it means we need to filter fields from textfsm
187            fields._fields.update({f_name: f_data})
188
189    # -------------------------------------------------------------------------
190
191    def _build_view(self, view_name):
192        """ build a new View definition """
193        if view_name in self.catalog:
194            return self.catalog[view_name]
195
196        view_dict = self._catalog_dict[view_name]
197        kvargs = {"view_name": view_name}
198
199        # if there are field groups, then get that now.
200        if "groups" in view_dict:
201            kvargs["groups"] = view_dict["groups"]
202
203        # if there are eval, then get that now.
204        if "eval" in view_dict:
205            kvargs["eval"] = {}
206            for key, exp in view_dict["eval"].items():
207                env = Environment()
208                kvargs["eval"][key] = env.parse(exp)
209
210        # if this view extends another ...
211        if "extends" in view_dict:
212            base_cls = self.catalog.get(view_dict["extends"])
213            # @@@ should check for base_cls is None!
214            kvargs["extends"] = base_cls
215
216        fields = _FIELDS()
217        fg_list = [name for name in view_dict if name.startswith("fields")]
218        for fg_name in fg_list:
219            self._add_view_fields(view_dict, fg_name, fields)
220
221        cls = _VIEW(fields.end, **kvargs)
222        self.catalog[view_name] = cls
223        return cls
224
225    # -------------------------------------------------------------------------
226
227    def _build_cmdview(self, view_name):
228        """ build a new View definition """
229        if view_name in self.catalog:
230            return self.catalog[view_name]
231
232        view_dict = self._catalog_dict[view_name]
233        kvargs = {"view_name": view_name}
234
235        if "columns" in view_dict:
236            kvargs["columns"] = view_dict["columns"]
237        elif "title" in view_dict:
238            kvargs["title"] = view_dict["title"]
239        if "regex" in view_dict:
240            kvargs["regex"] = view_dict["regex"]
241        if "exists" in view_dict:
242            kvargs["exists"] = view_dict["exists"]
243        if "filters" in view_dict:
244            kvargs["filters"] = view_dict["filters"]
245        if "eval" in view_dict:
246            kvargs["eval"] = {}
247            for key, exp in view_dict["eval"].items():
248                env = Environment()
249                kvargs["eval"][key] = env.parse(exp)
250        fields = _FIELDS()
251        fg_list = [name for name in view_dict if name.startswith("fields")]
252        for fg_name in fg_list:
253            self._add_cmd_view_fields(view_dict, fg_name, fields)
254
255        cls = _CMDVIEW(fields.end, **kvargs)
256        self.catalog[view_name] = cls
257        return cls
258
259    # -----------------------------------------------------------------------
260    # Create a Get-Table from YAML definition
261    # -----------------------------------------------------------------------
262
263    def _build_optable(self, table_name):
264        """ build a new Get-Table definition """
265        if table_name in self.catalog:
266            return self.catalog[table_name]
267
268        tbl_dict = self._catalog_dict[table_name]
269        kvargs = deepcopy(tbl_dict)
270
271        rpc = kvargs.pop("rpc")
272        kvargs["table_name"] = table_name
273
274        if "view" in tbl_dict:
275            view_name = tbl_dict["view"]
276            cls_view = self.catalog.get(view_name, self._build_view(view_name))
277            kvargs["view"] = cls_view
278
279        cls = _GET(rpc, **kvargs)
280        self.catalog[table_name] = cls
281        return cls
282
283    # -----------------------------------------------------------------------
284    # Create a Get-Table from YAML definition
285    # -----------------------------------------------------------------------
286
287    def _build_cmdtable(self, table_name):
288        """ build a new command-Table definition """
289        if table_name in self.catalog:
290            return self.catalog[table_name]
291
292        tbl_dict = self._catalog_dict[table_name]
293        kvargs = deepcopy(tbl_dict)
294
295        if "command" in kvargs:
296            cmd = kvargs.pop("command")
297            kvargs["table_name"] = table_name
298
299            if "view" in tbl_dict:
300                view_name = tbl_dict["view"]
301                cls_view = self.catalog.get(view_name, self._build_cmdview(view_name))
302                kvargs["view"] = cls_view
303
304            cls = _CMDTBL(cmd, **kvargs)
305            self.catalog[table_name] = cls
306            return cls
307        elif "title" in kvargs:
308            cmd = kvargs.pop("title")
309            kvargs["table_name"] = table_name
310
311            if "view" in tbl_dict:
312                view_name = tbl_dict["view"]
313                cls_view = self.catalog.get(view_name, self._build_cmdview(view_name))
314                kvargs["view"] = cls_view
315
316            cls = _CMDCHILDTBL(cmd, **kvargs)
317            self.catalog[table_name] = cls
318            return cls
319        else:
320            kvargs["table_name"] = table_name
321
322            if "view" in tbl_dict:
323                view_name = tbl_dict["view"]
324                cls_view = self.catalog.get(view_name, self._build_cmdview(view_name))
325                kvargs["view"] = cls_view
326
327            cls = _CMDCHILDTBL(**kvargs)
328            self.catalog[table_name] = cls
329            return cls
330
331    # -----------------------------------------------------------------------
332    # Create a Table class from YAML definition
333    # -----------------------------------------------------------------------
334
335    def _build_table(self, table_name):
336        """ build a new Table definition """
337        if table_name in self.catalog:
338            return self.catalog[table_name]
339
340        tbl_dict = self._catalog_dict[table_name]
341
342        table_item = tbl_dict.pop("item")
343        kvargs = deepcopy(tbl_dict)
344        kvargs["table_name"] = table_name
345
346        if "view" in tbl_dict:
347            view_name = tbl_dict["view"]
348            cls_view = self.catalog.get(view_name, self._build_view(view_name))
349            kvargs["view"] = cls_view
350
351        cls = _TABLE(table_item, **kvargs)
352        self.catalog[table_name] = cls
353        return cls
354
355    def _build_cfgtable(self, table_name):
356        """ build a new Config-Table definition """
357        if table_name in self.catalog:
358            return self.catalog[table_name]
359        tbl_dict = deepcopy(self._catalog_dict[table_name])
360
361        if "view" in tbl_dict:
362            # transpose name to class
363            view_name = tbl_dict["view"]
364            tbl_dict["view"] = self.catalog.get(view_name, self._build_view(view_name))
365
366        cls = _CFGTBL(table_name, tbl_dict)
367        self.catalog[table_name] = cls
368        return cls
369
370    # -----------------------------------------------------------------------
371    # Primary builders ...
372    # -----------------------------------------------------------------------
373
374    def _sortitems(self):
375        for k, v in self._catalog_dict.items():
376            if "rpc" in v:
377                self._item_optables.append(k)
378            elif "get" in v:
379                self._item_cfgtables.append(k)
380            elif "set" in v:
381                self._item_cfgtables.append(k)
382            elif "command" in v or "title" in v:
383                self._item_cmdtables.append(k)
384            elif "view" in v and "item" in v and v["item"] == "*":
385                self._item_cmdtables.append(k)
386            elif "view" in v:
387                self._item_tables.append(k)
388            else:
389                self._item_views.append(k)
390
391    def load(self, catalog_dict, envrion={}):
392
393        # load the yaml data and extract the item names.  these names will
394        # become the new class definitions
395
396        self._catalog_dict = catalog_dict
397        self._sortitems()
398
399        list(map(self._build_optable, self._item_optables))
400        list(map(self._build_cfgtable, self._item_cfgtables))
401        list(map(self._build_cmdtable, self._item_cmdtables))
402        list(map(self._build_table, self._item_tables))
403        list(map(self._build_view, self._item_views))
404
405        return self.catalog
406