''' Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. This program and the accompanying materials are made available under the terms of the Eclipse Public License v1.0 which accompanies this distribution, and is available at http://www.eclipse.org/legal/epl-v10.html Created on May 21, 2014 @author: Vaclav Demcak ''' from xml.dom.minidom import Element import ipaddr import xml.dom.minidom as md import copy KEY_NOT_FOUND = '' # KeyNotFound for dictDiff class XMLtoDictParserTools(): @staticmethod def parseTreeToDict(node, returnedDict=None, ignoreList=[]): """ Return Dictionary representation of the xml Tree DOM Element. Repeated tags are put to the array sorted by key (id or order) otherwise is the value represented by tag key name. @param node: DOM Element @param returnedDict : dictionary (default value None) @param ignereList : list of ignored tags for the xml Tree DOM Element (default value is empty list) @return: dict representation for the input DOM Element """ returnedDict = {} if returnedDict is None else returnedDict if (node.nodeType == Element.ELEMENT_NODE): nodeKey = (node.localName).encode('utf-8', 'ignore') if nodeKey not in ignoreList: if node.childNodes is not None: childDict = {} for child in node.childNodes: if child.nodeType == Element.TEXT_NODE: nodeValue = (child.nodeValue).encode('utf-8', 'ignore') if (len(nodeValue.strip(' \t\n\r'))) > 0: XMLtoDictParserTools.addDictValue(returnedDict, nodeKey, nodeValue) nodeKey = None break elif child.nodeType == Element.ELEMENT_NODE: childDict = XMLtoDictParserTools.parseTreeToDict(child, childDict, ignoreList) XMLtoDictParserTools.addDictValue(returnedDict, nodeKey, childDict) return returnedDict @staticmethod def addDictValue(m_dict, key, value): def _allign_address(value): """unifies output""" n = ipaddr.IPNetwork(value) return '{0}/{1}'.format(n.network.exploded, n.prefixlen) def _convert_numbers(value): if value.startswith("0x"): return str(long(value, 16)) return str(long(value)) if key is not None: if (isinstance(value, str)): # we need to predict possible differences # for same value in upper or lower case value = value.lower() if key not in m_dict: # lets add mask for ips withot mask if key in ['ipv4-destination', 'ipv4-source', 'ipv6-destination', 'ipv6-source', 'ipv6-nd-target']: nvalue = _allign_address(value) m_dict[key] = nvalue elif key in ['tunnel-mask', 'type', 'metadata-mask', 'out_port', 'out_group']: nvalue = _convert_numbers(value) m_dict[key] = nvalue else: m_dict[key] = value else: exist_value = m_dict.get(key) if (type(exist_value) is dict): list_values = [exist_value, value] key_for_sort = XMLtoDictParserTools.searchKey(exist_value) if key_for_sort is not None: list_values = sorted(list_values, key=lambda k: k[key_for_sort]) m_dict[key] = list_values elif (isinstance(exist_value, list)): exist_value.append(value) list_values = exist_value key_for_sort = XMLtoDictParserTools.searchKey(value) if key_for_sort is not None: list_values = sorted(list_values, key=lambda k: k[key_for_sort]) m_dict[key] = list_values else: m_dict[key] += value @staticmethod def searchKey(dictionary): """ Return an order key for the array ordering. OF_13 allows only two possible kind of the order keys 'order' or '*-id' @param dictionary: dictionary with data @return: the array order key """ subKeyStr = ['-id', 'order'] for substr in subKeyStr: for key in dictionary: if key == substr: return key elif key.endswith(substr): return key return None @staticmethod def getDifferenceDict(original_dict, responded_dict): """ Return a dict of keys that differ with another config object. If a value is not found in one fo the configs, it will be represented by KEY_NOT_FOUND. @param original_dict: Fist dictionary to diff. @param responded_dict: Second dictionary to diff. @return diff: Dict of Key => (original_dict.val, responded_dict.val) Dict of Key => (original_key, KEY_NOT_FOUND) Dict of Key => (KEY_NOT_FOUNE, original_key) """ diff = {} # Check all keys in original_dict dict for key in original_dict.keys(): if key not in responded_dict: # missing key in responded dict diff[key] = (key, KEY_NOT_FOUND) # check values of the dictionaries elif (original_dict[key] != responded_dict[key]): # values are not the same # orig_dict_val = original_dict[key] resp_dict_val = responded_dict[key] # check value is instance of dictionary if isinstance(orig_dict_val, dict) and isinstance(resp_dict_val, dict): sub_dif = XMLtoDictParserTools.getDifferenceDict(orig_dict_val, resp_dict_val) if sub_dif: diff[key] = sub_dif # check value is instance of list # TODO - > change a basic comparator to compare by id or order elif isinstance(orig_dict_val, list) and isinstance(resp_dict_val, list): sub_list_diff = {} # the list lengths orig_i, resp_i = len(orig_dict_val), len(resp_dict_val) # define a max iteration length (less from both) min_index = orig_i if orig_i < resp_i else resp_i for index in range(0, min_index, 1): if (orig_dict_val[index] != resp_dict_val[index]): sub_list_diff[index] = (orig_dict_val[index], resp_dict_val[index]) if (orig_i > min_index): # original is longer as responded dict for index in range(min_index, orig_i, 1): sub_list_diff[index] = (orig_dict_val[index], None) elif (resp_i > min_index): # responded dict is longer as original for index in range(min_index, resp_i, 1): sub_list_diff[index] = (None, resp_dict_val[index]) if sub_list_diff: diff[key] = sub_list_diff else: diff[key] = (original_dict[key], responded_dict[key]) # Check all keys in responded_dict dict to find missing for key in responded_dict.keys(): if key not in original_dict: diff[key] = (KEY_NOT_FOUND, key) return diff IGNORED_TAGS_FOR_OPERATIONAL_COMPARISON = ['id', 'flow-name', 'barrier', 'cookie_mask', 'installHw', 'flags', 'strict', 'byte-count', 'duration', 'packet-count', 'in-port', 'vlan-id-present', 'out_group', 'out_port', 'hard-timeout', 'idle-timeout', 'flow-statistics', 'cookie', 'clear-actions'] # noqa IGNORED_PATHS_FOR_OC = [(['flow', 'instructions', 'instruction', 'apply-actions', 'action', 'controller-action'], True), # noqa (['flow', 'instructions', 'instruction', 'clear-actions', 'action'], False), (['flow', 'instructions', 'instruction', 'apply-actions', 'action', 'push-vlan-action', 'vlan-id'], False), # noqa (['flow', 'instructions', 'instruction', 'apply-actions', 'action', 'drop-action'], True), (['flow', 'instructions', 'instruction', 'apply-actions', 'action', 'flood-action'], True), ] TAGS_TO_ADD_FOR_OC = [(['flow', 'instructions', 'instruction', 'apply-actions', 'action', 'output-action'], 'max-length', '0'), # noqa ] TAGS_TO_MODIFY_FOR_OC = [(['flow', 'match', 'metadata'], 'metadata', 'metadata-mask'), (['flow', 'match', 'tunnel'], 'tunnel-id', 'tunnel-mask'), ] class XmlComparator: def is_flow_configured(self, requested_flow, configured_flows): orig_tree = md.parseString(requested_flow) xml_resp_stream = configured_flows.encode('utf-8', 'ignore') xml_resp_tree = md.parseString(xml_resp_stream) nodeListOperFlows = xml_resp_tree.getElementsByTagNameNS("*", 'flow') origDict = XMLtoDictParserTools.parseTreeToDict(orig_tree._get_documentElement()) reportDict = {} index = 0 for node in nodeListOperFlows: nodeDict = XMLtoDictParserTools.parseTreeToDict(node) XMLtoDictParserTools.addDictValue(reportDict, index, nodeDict) index += 1 # print nodeDict # print origDict if nodeDict == origDict: return True, '' if nodeDict['flow']['priority'] == origDict['flow']['priority']: return False, 'Flow found with diferences {0}'.format( XMLtoDictParserTools.getDifferenceDict(nodeDict, origDict)) return False, '' def is_flow_operational2(self, requested_flow, oper_resp): def _rem_unimplemented_tags(tagpath, recurs, tdict): # print "_rem_unimplemented_tags", tagpath, tdict if len(tagpath) > 1 and tagpath[0] in tdict: _rem_unimplemented_tags(tagpath[1:], recurs, tdict[tagpath[0]]) # when not to delete anything if len(tagpath) == 1 and tagpath[0] not in tdict: return if len(tagpath) == 0: return # when to delete if len(tagpath) == 1 and tagpath[0] in tdict: del tdict[tagpath[0]] if len(tagpath) > 1 and recurs is True and tagpath[0] in tdict and tdict[tagpath[0]] == {}: del tdict[tagpath[0]] if tdict.keys() == ['order']: del tdict['order'] # print "leaving", tdict def _add_tags(tagpath, newtag, value, tdict): '''if whole tagpath exists and the tag is not present, it is added with given value''' # print "_add_tags", tagpath, newtag, value, tdict if len(tagpath) > 0 and tagpath[0] in tdict: _add_tags(tagpath[1:], newtag, value, tdict[tagpath[0]]) elif len(tagpath) == 0 and newtag not in tdict: tdict[newtag] = value def _to_be_modified_tags(tagpath, tag, related_tag, tdict): '''if whole tagpath exists and the tag is not present, it is added with given value''' # print "_to_be_modified_tags", tagpath, tag, related_tag, tdict if len(tagpath) > 0 and tagpath[0] in tdict: _to_be_modified_tags(tagpath[1:], tag, related_tag, tdict[tagpath[0]]) elif len(tagpath) == 0 and tag in tdict and related_tag in tdict: tdict[tag] = str(long(tdict[tag]) & long(tdict[related_tag])) orig_tree = md.parseString(requested_flow) xml_resp_stream = oper_resp.encode('utf-8', 'ignore') xml_resp_tree = md.parseString(xml_resp_stream) nodeListOperFlows = xml_resp_tree.getElementsByTagNameNS("*", 'flow') origDict = XMLtoDictParserTools.parseTreeToDict( orig_tree._get_documentElement(), ignoreList=IGNORED_TAGS_FOR_OPERATIONAL_COMPARISON) # origDict['flow-statistics'] = origDict.pop( 'flow' ) reportDict = {} index = 0 for node in nodeListOperFlows: nodeDict = XMLtoDictParserTools.parseTreeToDict( node, ignoreList=IGNORED_TAGS_FOR_OPERATIONAL_COMPARISON) XMLtoDictParserTools.addDictValue(reportDict, index, nodeDict) index += 1 # print nodeDict # print origDict # print reportDict if nodeDict == origDict: return True, '' if nodeDict['flow']['priority'] == origDict['flow']['priority']: for p in IGNORED_PATHS_FOR_OC: td = copy.copy(origDict) _rem_unimplemented_tags(p[0], p[1], td) for (p, t, v) in TAGS_TO_ADD_FOR_OC: _add_tags(p, t, v, td) for (p, t, rt) in TAGS_TO_MODIFY_FOR_OC: _to_be_modified_tags(p, t, rt, td) # print "comparing1", nodeDict # print "comparing2", td if nodeDict == td: return True, '' if nodeDict == origDict: return True, '' return False, 'Flow found with diferences {0}'.format( XMLtoDictParserTools.getDifferenceDict(nodeDict, origDict)) return False, '' def get_data_for_flow_put_update(self, xml): # action only for yet xml_dom_input = md.parseString(xml) actionList = xml_dom_input.getElementsByTagName('action') if actionList is not None and len(actionList) > 0: action = actionList[0] for child in action.childNodes: if child.nodeType == Element.ELEMENT_NODE: nodeKey = (child.localName).encode('utf-8', 'ignore') if nodeKey != 'order': if nodeKey != 'drop-action': new_act = child.ownerDocument.createElement('drop-action') else: new_act = child.ownerDocument.createElement('output-action') onc = child.ownerDocument.createElement('output-node-connector') onc_content = child.ownerDocument.createTextNode('TABLE') onc.appendChild(onc_content) new_act.appendChild(onc) ml = child.ownerDocument.createElement('max-length') ml_content = child.ownerDocument.createTextNode('60') ml.appendChild(ml_content) new_act.appendChild(ml) child.parentNode.replaceChild(new_act, child) return xml_dom_input.toxml(encoding='utf-8') def get_flow_content(self, tid=1, fid=1, priority=1): """Returns an xml flow content identified by given details. Args: :param tid: table id :param fid: flow id :param priority: flow priority """ flow_template = ''' false 0 0 %s %s 4294967295 false 2048 10.0.0.1/32 %s %s %s false ''' flow_data = flow_template % (tid, fid, fid, 'TestFlow-{0}'.format(fid), priority) return flow_data