# This script is used as extension of sonic_yang class. It has methods of # class sonic_yang. A separate file is used to avoid a single large file. from __future__ import print_function import yang as ly import re import syslog from json import dump, dumps, loads from xmltodict import parse from glob import glob """ This is the Exception thrown out of all public function of this class. """ class SonicYangException(Exception): pass # class sonic_yang methods, use mixin to extend sonic_yang class SonicYangExtMixin: """ load all YANG models, create JSON of yang models. (Public function) """ def loadYangModel(self): try: # get all files self.yangFiles = glob(self.yang_dir +"/*.yang") # load yang modules for file in self.yangFiles: m = self._load_schema_module(file) if m is not None: self.sysLog(msg="module: {} is loaded successfully".format(m.name())) else: raise(Exception("Could not load module {}".format(file))) # keep only modules name in self.yangFiles self.yangFiles = [f.split('/')[-1] for f in self.yangFiles] self.yangFiles = [f.split('.')[0] for f in self.yangFiles] print('Loaded below Yang Models') print(self.yangFiles) # load json for each yang model self._loadJsonYangModel() # create a map from config DB table to yang container self._createDBTableToModuleMap() except Exception as e: print("Yang Models Load failed") raise SonicYangException("Yang Models Load failed\n{}".format(str(e))) return True """ load JSON schema format from yang models """ def _loadJsonYangModel(self): try: for f in self.yangFiles: m = self.ctx.get_module(f) if m is not None: xml = m.print_mem(ly.LYD_JSON, ly.LYP_FORMAT) self.yJson.append(parse(xml)) self.sysLog(msg="Parsed Json for {}".format(m.name())) except Exception as e: raise e return """ Create a map from config DB tables to container in yang model This module name and topLevelContainer are fetched considering YANG models are written using below Guidelines: https://github.com/Azure/SONiC/blob/master/doc/mgmt/SONiC_YANG_Model_Guidelines.md. """ def _createDBTableToModuleMap(self): for j in self.yJson: # get module name moduleName = j['module']['@name'] # get top level container topLevelContainer = j['module'].get('container') # if top level container is none, this is common yang files, which may # have definitions. Store module. if topLevelContainer is None: self.confDbYangMap[moduleName] = j['module'] continue # top level container must exist for rest of the yang files and it should # have same name as module name. assert topLevelContainer['@name'] == moduleName # Each container inside topLevelContainer maps to a sonic config table. container = topLevelContainer['container'] # container is a list if isinstance(container, list): for c in container: self.confDbYangMap[c['@name']] = { "module" : moduleName, "topLevelContainer": topLevelContainer['@name'], "container": c } # container is a dict else: self.confDbYangMap[container['@name']] = { "module" : moduleName, "topLevelContainer": topLevelContainer['@name'], "container": container } return """ Get module, topLevelContainer(TLC) and json container for a config DB table """ def _getModuleTLCcontainer(self, table): cmap = self.confDbYangMap m = cmap[table]['module'] t = cmap[table]['topLevelContainer'] c = cmap[table]['container'] return m, t, c """ Crop config as per yang models, This Function crops from config only those TABLEs, for which yang models is provided. The Tables without YANG models are stored in self.tablesWithOutYangModels. """ def _cropConfigDB(self, croppedFile=None): tables = list(self.jIn.keys()) for table in tables: if table not in self.confDbYangMap: # store in tablesWithOutYang self.tablesWithOutYang[table] = self.jIn[table] del self.jIn[table] if len(self.tablesWithOutYang): print("Note: Below table(s) have no YANG models:") for table in self.tablesWithOutYang.keys(): print(str(table), end=", ") print() if croppedFile: with open(croppedFile, 'w') as f: dump(self.jIn, f, indent=4) return """ Extract keys from table entry in Config DB and return in a dict Input: tableKey: Config DB Primary Key, Example tableKey = "Vlan111|2a04:5555:45:6709::1/64" keys: key string from YANG list, i.e. 'vlan_name ip-prefix'. regex: A regex to extract keys from tableKeys, good to have it as accurate as possible. Return: KeyDict = {"vlan_name": "Vlan111", "ip-prefix": "2a04:5555:45:6709::1/64"} """ def _extractKey(self, tableKey, keys, regex): keyList = keys.split() # get the value groups value = re.match(regex, tableKey) # create the keyDict i = 1 keyDict = dict() for k in keyList: if value.group(i): keyDict[k] = value.group(i) else: raise Exception("Value not found for {} in {}".format(k, tableKey)) i = i + 1 return keyDict """ Fill the dict based on leaf as a list or dict @model yang model object """ def _fillLeafDict(self, leafs, leafDict, isleafList=False): if leafs is None: return # fill default values def _fillSteps(leaf): leaf['__isleafList'] = isleafList leafDict[leaf['@name']] = leaf return if isinstance(leafs, list): for leaf in leafs: #print("{}:{}".format(leaf['@name'], leaf)) _fillSteps(leaf) else: #print("{}:{}".format(leaf['@name'], leaf)) _fillSteps(leafs) return """ create a dict to map each key under primary key with a dict yang model. This is done to improve performance of mapping from values of TABLEs in config DB to leaf in YANG LIST. """ def _createLeafDict(self, model): leafDict = dict() #Iterate over leaf, choices and leaf-list. self._fillLeafDict(model.get('leaf'), leafDict) #choices, this is tricky, since leafs are under cases in tree. choices = model.get('choice') if choices: for choice in choices: cases = choice['case'] for case in cases: self._fillLeafDict(case.get('leaf'), leafDict) # leaf-lists self._fillLeafDict(model.get('leaf-list'), leafDict, True) return leafDict """ Convert a string from Config DB value to Yang Value based on type of the key in Yang model. @model : A List of Leafs in Yang model list """ def _findYangTypedValue(self, key, value, leafDict): # convert config DB string to yang Type def _yangConvert(val): # Convert everything to string val = str(val) # find type of this key from yang leaf type = leafDict[key]['type']['@name'] if 'uint' in type: vValue = int(val, 10) # TODO: find type of leafref from schema node elif 'leafref' in type: vValue = val #TODO: find type in sonic-head, as of now, all are enumeration elif 'head:' in type: vValue = val else: vValue = val return vValue # if it is a leaf-list do it for each element if leafDict[key]['__isleafList']: vValue = list() for v in value: vValue.append(_yangConvert(v)) else: vValue = _yangConvert(value) return vValue """ Xlate a list This function will xlate from a dict in config DB to a Yang JSON list using yang model. Output will be go in self.xlateJson """ def _xlateList(self, model, yang, config, table): #create a dict to map each key under primary key with a dict yang model. #This is done to improve performance of mapping from values of TABLEs in #config DB to leaf in YANG LIST. leafDict = self._createLeafDict(model) # fetch regex from YANG models. keyRegEx = model['ext:key-regex-configdb-to-yang']['@value'] # seperator `|` has special meaning in regex, so change it appropriately. keyRegEx = re.sub(r'\|', r'\\|', keyRegEx) # get keys from YANG model list itself listKeys = model['key']['@value'] self.sysLog(msg="xlateList regex:{} keyList:{}".\ format(keyRegEx, listKeys)) primaryKeys = list(config.keys()) for pkey in primaryKeys: try: vKey = None self.sysLog(syslog.LOG_DEBUG, "xlateList Extract pkey:{}".\ format(pkey)) # Find and extracts key from each dict in config keyDict = self._extractKey(pkey, listKeys, keyRegEx) # fill rest of the values in keyDict for vKey in config[pkey]: self.sysLog(syslog.LOG_DEBUG, "xlateList vkey {}".format(vKey)) keyDict[vKey] = self._findYangTypedValue(vKey, \ config[pkey][vKey], leafDict) yang.append(keyDict) # delete pkey from config, done to match one key with one list del config[pkey] except Exception as e: # log debug, because this exception may occur with multilists self.sysLog(syslog.LOG_DEBUG, "xlateList Exception {}".format(e)) # with multilist, we continue matching other keys. continue return """ Process list inside a Container. This function will call xlateList based on list(s) present in Container. """ def _xlateListInContainer(self, model, yang, configC, table): clist = model #print(clist['@name']) yang[clist['@name']] = list() self.sysLog(msg="xlateProcessListOfContainer: {}".format(clist['@name'])) self._xlateList(clist, yang[clist['@name']], configC, table) # clean empty lists if len(yang[clist['@name']]) == 0: del yang[clist['@name']] return """ Process container inside a Container. This function will call xlateContainer based on Container(s) present in outer Container. """ def _xlateContainerInContainer(self, model, yang, configC, table): ccontainer = model #print(ccontainer['@name']) yang[ccontainer['@name']] = dict() if not configC.get(ccontainer['@name']): return self.sysLog(msg="xlateProcessListOfContainer: {}".format(ccontainer['@name'])) self._xlateContainer(ccontainer, yang[ccontainer['@name']], \ configC[ccontainer['@name']], table) # clean empty container if len(yang[ccontainer['@name']]) == 0: del yang[ccontainer['@name']] # remove copy after processing del configC[ccontainer['@name']] return """ Xlate a container This function will xlate from a dict in config DB to a Yang JSON container using yang model. Output will be stored in self.xlateJson """ def _xlateContainer(self, model, yang, config, table): # To Handle multiple Lists, Make a copy of config, because we delete keys # from config after each match. This is done to match one pkey with one list. configC = config.copy() clist = model.get('list') # If single list exists in container, if clist and isinstance(clist, dict) and \ clist['@name'] == model['@name']+"_LIST" and bool(configC): self._xlateListInContainer(clist, yang, configC, table) # If multi-list exists in container, elif clist and isinstance(clist, list) and bool(configC): for modelList in clist: self._xlateListInContainer(modelList, yang, configC, table) # Handle container(s) in container ccontainer = model.get('container') # If single list exists in container, if ccontainer and isinstance(ccontainer, dict) and bool(configC): self._xlateContainerInContainer(ccontainer, yang, configC, table) # If multi-list exists in container, elif ccontainer and isinstance(ccontainer, list) and bool(configC): for modelContainer in ccontainer: self._xlateContainerInContainer(modelContainer, yang, configC, table) ## Handle other leaves in container, leafDict = self._createLeafDict(model) vKeys = list(configC.keys()) for vKey in vKeys: #vkey must be a leaf\leaf-list\choice in container if leafDict.get(vKey): self.sysLog(syslog.LOG_DEBUG, "xlateContainer vkey {}".format(vKey)) yang[vKey] = self._findYangTypedValue(vKey, configC[vKey], leafDict) # delete entry from copy of config del configC[vKey] # All entries in copy of config must have been parsed. if len(configC): self.sysLog(syslog.LOG_ERR, "Alert: Remaining keys in Config") raise(Exception("All Keys are not parsed in {}\n{}".format(table, \ configC.keys()))) return """ xlate ConfigDB json to Yang json """ def _xlateConfigDBtoYang(self, jIn, yangJ): # find top level container for each table, and run the xlate_container. for table in jIn.keys(): cmap = self.confDbYangMap[table] # create top level containers key = cmap['module']+":"+cmap['topLevelContainer'] subkey = cmap['topLevelContainer']+":"+cmap['container']['@name'] # Add new top level container for first table in this container yangJ[key] = dict() if yangJ.get(key) is None else yangJ[key] yangJ[key][subkey] = dict() self.sysLog(msg="xlateConfigDBtoYang {}:{}".format(key, subkey)) self._xlateContainer(cmap['container'], yangJ[key][subkey], \ jIn[table], table) return """ Read config file and crop it as per yang models """ def _xlateConfigDB(self, xlateFile=None): jIn= self.jIn yangJ = self.xlateJson # xlation is written in self.xlateJson self._xlateConfigDBtoYang(jIn, yangJ) if xlateFile: with open(xlateFile, 'w') as f: dump(self.xlateJson, f, indent=4) return """ create config DB table key from entry in yang JSON """ def _createKey(self, entry, regex): keyDict = dict() keyV = regex # get the keys from regex of key extractor keyList = re.findall(r'<(.*?)>', regex) for key in keyList: val = entry.get(key) if val: #print("pair: {} {}".format(key, val)) keyDict[key] = sval = str(val) keyV = re.sub(r'<'+key+'>', sval, keyV) #print("VAL: {} {}".format(regex, keyV)) else: raise Exception("key {} not found in entry".format(key)) #print("kDict {}".format(keyDict)) return keyV, keyDict """ Convert a string from Config DB value to Yang Value based on type of the key in Yang model. @model : A List of Leafs in Yang model list """ def _revFindYangTypedValue(self, key, value, leafDict): # convert yang Type to config DB string def _revYangConvert(val): # config DB has only strings, thank god for that :), wait not yet!!! return str(val) # if it is a leaf-list do it for each element if leafDict[key]['__isleafList']: vValue = list() for v in value: vValue.append(_revYangConvert(v)) else: vValue = _revYangConvert(value) return vValue """ Rev xlate from