8 import xml.dom.minidom as md
9 from xml.etree import ElementTree as ET
10 from netaddr import IPNetwork
11 from string import lower
18 from mininet.node import RemoteController
19 from mininet.node import OVSKernelSwitch
21 def create_network(controller_ip, controller_port):
22 """Create topology and mininet network."""
23 topo = mininet.topo.Topo()
29 topo.addLink('h1', 's1')
30 topo.addLink('h2', 's1')
32 switch=mininet.util.customConstructor(
33 {'ovsk':OVSKernelSwitch}, 'ovsk,protocols=OpenFlow13')
35 controller=mininet.util.customConstructor(
36 {'remote': RemoteController}, 'remote,ip=%s:%s' % (controller_ip,
40 net = mininet.net.Mininet(topo=topo, switch=switch, controller=controller)
46 """Get list of flows from network's first switch.
48 Return list of all flows on switch, sorted by duration (newest first)
49 One flow is a dictionary with all flow's attribute:value pairs. Matches
50 are stored under 'matches' key as another dictionary.
56 'duration': '3.434s,',
57 'hard_timeout': '12,',
58 'idle_timeout': '34,',
61 'nw_dst': '10.0.0.0/24'
70 log = logging.getLogger(__name__)
71 def parse_matches(flow, matches):
75 split_match = match.split('=', 1)
76 if len(split_match) == 1:
77 flow['matches'][split_match[0]] = None
79 flow['matches'][split_match[0]] = split_match[1].rstrip(',')
81 switch = net.switches[0]
82 output = switch.cmdPrint(
83 'ovs-ofctl -O OpenFlow13 dump-flows %s' % switch.name)
84 # output = switch.cmdPrint(
85 # 'ovs-ofctl -F openflow10 dump-flows %s' % switch.name)
87 log.debug('switch flow table: {}'.format(output))
91 for line in output.splitlines()[1:]:
93 for word in line.split():
96 key, value = word.split('=', 1)
98 #TODO: need to figure out what to do here?
101 if key == 'priority':
102 values = value.split(',')
103 flow[key] = values[0]
104 parse_matches(flow, values[1:])
106 flow[key] = value.rstrip(',')
111 return sorted(flows, key=lambda x: x['duration'].rstrip('s'))
114 def translate_to_flow(flow, name, dictionary):
115 switch_flow_name = dictionary[name]
117 key_err = '{} needs to be present in flow definition. Flow definition ' \
118 'was: {}.'.format(switch_flow_name, flow)
119 assert switch_flow_name in flow, key_err
120 return switch_flow_name
123 def get_text_value(element):
124 return element.childNodes[0].nodeValue
127 def compare_elements(expected_match, actual_match, kw, comparators, default):
128 for child in expected_match.childNodes:
129 if child.nodeType is expected_match.TEXT_NODE:
132 comparator = comparators.get(child.nodeName, default)
133 comparator(child, actual_match, kw)
136 def fallback_comparator(xml_element, switch_flow, kw):
137 # print 'fallback_comparator-xml_element', xml_element.toxml()
138 # print 'fallback_comparator: switch_flow', switch_flow
139 # print 'fallback_comparator: kw', kws
141 name = translate_to_flow(switch_flow, xml_element.nodeName, kw)
143 actual = switch_flow[name]
144 expected = xml_element.childNodes[0].nodeValue
146 data = xml_element.toxml(), name, actual
147 # print 'fallback_comparator: data', data
149 assert expected == actual, 'xml part: %s && switch %s=%s' % data
152 def default_comparator(xml_element, switch_flow):
153 fallback_comparator(xml_element, switch_flow, keywords)
156 def integer_comparator(expected, actual, kw, base):
157 expected_value = int(expected.childNodes[0].data)
159 name = kw.get(expected.nodeName)
160 actual_value = int(actual[name], base)
162 data = expected.toxml(), name, actual
163 assert expected_value == actual_value, \
164 'xml value: %s && actual value %s=%s' % data
167 def cookie_comparator(cookie, switch_flow):
168 integer_comparator(cookie, switch_flow, keywords, 16)
171 def ethernet_address_comparator(child, actual_match, kw):
172 expected_address = child.getElementsByTagName("address")[0].childNodes[0].data
173 actual_address = actual_match[kw.get(child.nodeName)]
175 data = child.toxml(), kw.get(child.nodeName), actual_address
177 assert lower(expected_address) == lower(actual_address), \
178 'xml address: %s && actual address %s=%s' % data
181 def vlan_match_comparator(expected_match, actual_match, kw):
183 def compare_vlan_pcp(expected_match, actual_match, kw):
184 integer_comparator(expected_match, actual_match, kw, 10)
186 def compare_vlan_id(expected_match, actual_match, kw):
187 integer_comparator(expected_match.getElementsByTagName('vlan-id')[0], \
188 actual_match, kw, 10)
191 'vlan-pcp': compare_vlan_pcp,
192 'vlan-id': compare_vlan_id,
195 # print 'ethernet_match_comparator-expected_match:', expected_match.toxml()
196 # print 'ethernet_match_comparator-actual_match:', actual_match
198 compare_elements(expected_match, actual_match, kw, \
199 VLAN_COMPARATORS, fallback_comparator)
202 def ethernet_match_comparator(expected_match, actual_match, kw):
203 def compare_etype(child, actual_match, kw):
204 expected_etype = int(child.getElementsByTagName("type")[0].childNodes[0].data)
205 name = kw.get(child.nodeName)
206 data = child.toxml(), name, actual_match
208 if expected_etype == 2048: # IP
209 assert ((actual_match.get('ip', 'IP Not-present') is None) or \
210 (actual_match.get('tcp', 'TCP Not-present') is None) or \
211 (actual_match.get('sctp', 'SCTP Not-present') is None) or \
212 (actual_match.get('udp', 'UDP Not-present') is None)), \
213 'Expected etype %s && actual etype %s=%s' % data
215 elif expected_etype == 2054: #ARP
216 assert actual_match.get('arp', 'ARP Not-present') is None, \
217 'Expected etype %s && actual etype %s=%s' % data
220 actual_etype = int(actual_match[name], 16)
222 assert expected_etype == actual_etype, \
223 'xml etype: %s && actual etype %s=%s' % data
227 'ethernet-type': compare_etype,
228 'ethernet-source': ethernet_address_comparator,
229 'ethernet-destination': ethernet_address_comparator,
232 # print 'ethernet_match_comparator-expected_match:', expected_match.toxml()
233 # print 'ethernet_match_comparator-actual_match:', actual_match
235 compare_elements(expected_match, actual_match, kw, \
236 ETH_COMPARATORS, fallback_comparator)
239 def ipv4_comparator(expected_match, actual_match, kw):
240 # print 'ip_v4_comparator:', expected_match.toxml(), actual_match
241 # print 'ip_v4_comparator-actual_match:', actual_match
243 expected_value = expected_match.childNodes[0].data
244 actual_value = actual_match[kw.get(expected_match.nodeName)]
246 data = expected_match.toxml(), kw.get(expected_match.nodeName), actual_value
248 assert IPNetwork(expected_value) == IPNetwork(actual_value),\
249 'xml part: %s && address %s=%s' % data
252 def ip_match_comparator(expected_match, actual_match, kw):
253 def compare_proto(child, actual_match, kw):
254 print 'compare_proto:', child.toxml(), actual_match
255 expected_proto = int(child.childNodes[0].data)
257 name = child.nodeName
258 data = expected_match.toxml(), name, actual_match
260 if expected_proto == 6: # TCP
261 assert actual_match.get('tcp', 'TCP Not-present') is None, \
262 'ip protocol type: expected %s, actual %s=%s' % data
264 elif expected_proto == 17: #UDP
265 assert actual_match.get('udp', 'UDP Not-present') is None, \
266 'ip protocol type: expected %s, actual %s=%s' % data
268 elif expected_proto == 132: #SCTP
269 assert actual_match.get('sctp', 'SCTP Not-present') is None, \
270 'ip protocol type: expected %s, actual %s=%s' % data
273 fallback_comparator(child, actual_match, kw)
276 def compare_dscp(child, actual_match, kw):
277 # print 'compare_dscp:', child.toxml(), actual_match
279 expected_dscp = int(child.childNodes[0].data)
280 name = kw.get(child.nodeName)
281 actual_dscp = int(actual_match[name])
283 data = child.toxml(), name, actual_match
285 assert (expected_dscp * 4) == actual_dscp, 'dscp: expected %s, actual %s=%s' % data
288 IP_MATCH_COMPARATORS = {
289 'ip-protocol': compare_proto,
290 'ip-dscp': compare_dscp,
293 # print 'ip_match_comparator:', expected_match.toxml(), actual_match
294 compare_elements(expected_match, actual_match, kw, \
295 IP_MATCH_COMPARATORS, fallback_comparator)
298 def match_comparator(expected_match, switch_flow):
299 MATCH_COMPARATORS = {
300 'arp-source-hardware-address': ethernet_address_comparator,
301 'arp-target-hardware-address': ethernet_address_comparator,
302 'vlan-match': vlan_match_comparator,
303 'ethernet-match': ethernet_match_comparator,
304 'ip-match': ip_match_comparator,
305 'ipv4-destination': ipv4_comparator,
306 'ipv4-source': ipv4_comparator,
309 actual_match = switch_flow['matches']
311 # print 'match_comparator-expected_match:', expected_match.toxml()
312 # print 'match_comparator-actual_match:', actual_match
313 # print 'match_comparator: keywords', keywords
315 compare_elements(expected_match, actual_match, match_keywords, \
316 MATCH_COMPARATORS, fallback_comparator)
319 def actions_comparator(actions, switch_flow):
320 # print 'actions_comparator:', actions, switch_flow
322 actual_actions = switch_flow['actions'].split(",")
323 # print 'actions_comparator:', actual_actions
325 for action in actions.childNodes:
326 if action.nodeType is actions.TEXT_NODE:
329 action_name = action.childNodes[3].nodeName
330 expected_action = action_keywords.get(action_name)
332 data = action.toxml(), expected_action
333 # print 'actions_comparator:', data
335 assert expected_action in actual_actions, 'xml part:\n%s\n expected action: %s' % data
338 def null_comparator(element, switch_flow):
342 def instructions_comparator(instructions_element, switch_flow):
343 INSTRUCTION_COMPARATORS = {
344 'apply-actions': actions_comparator,
345 'default': null_comparator,
347 # print 'instructions_comparator:', instructions_element, switch_flow
349 instructions = instructions_element.childNodes
351 for instruction in instructions_element.childNodes:
352 if instruction.nodeType is instructions_element.TEXT_NODE:
355 for itype in instruction.childNodes:
356 if itype.nodeType is itype.TEXT_NODE:
359 comparator = INSTRUCTION_COMPARATORS.get(itype.nodeName,
360 INSTRUCTION_COMPARATORS['default'])
361 comparator(itype, switch_flow)
365 'cookie': cookie_comparator,
366 'instructions': instructions_comparator,
367 'match': match_comparator,
368 'default': default_comparator,
371 def all_nodes(xml_root):
373 Generates every non-text nodes.
375 current_nodes = [xml_root]
378 while len(current_nodes) > 0:
379 for node in current_nodes:
380 if node.nodeType != xml_root.TEXT_NODE:
382 next_nodes.extend(node.childNodes)
384 current_nodes, next_nodes = next_nodes, []
387 def check_elements(xmlstr, keywords):
388 # namespace = 'urn:opendaylight:flow:inventory'
389 tree = md.parseString(xmlstr)
391 for element in all_nodes(tree.documentElement):
392 # switch flow object contains only some data from xml
393 if element.nodeName not in keywords:
394 # print 'check_elements: element.nodeName', element.nodeName, 'NOT in keywords'
399 raise StopIteration()
402 class TestOpenFlowXMLs(unittest.TestCase):
405 cls.net = create_network(cls.host, cls.mn_port)
410 def tearDownClass(cls):
414 def get_values(node, *tags):
415 result = {tag: None for tag in tags}
416 for node in all_nodes(node):
417 if node.nodeName in result and len(node.childNodes) > 0:
418 result[node.nodeName] = node.childNodes[0].nodeValue
422 def generate_tests_from_xmls(path, xmls=None):
423 # generate test function from path to request xml
424 def generate_test(path_to_xml):
426 with open(path_to_xml) as f:
427 xml_string = f.read()
429 tree = md.parseString(xml_string)
430 ids = get_values(tree.documentElement, 'table_id', 'id')
433 log = logging.getLogger(__name__)
434 # send request throught RESTCONF
435 data = (self.host, self.port, ids['table_id'], ids['id'])
436 url = 'http://%s:%d/restconf/config/opendaylight-inventory:nodes' \
437 '/node/openflow:1/table/%s/flow/%s' % data
439 'Content-Type': 'application/xml',
440 'Accept': 'application/xml',
442 log.info('sending request to url: {}'.format(url))
443 rsp = requests.put(url, auth=('admin', 'admin'), data=xml_string,
445 log.info('received status code: {}'.format(rsp.status_code))
446 log.debug('received content: {}'.format(rsp.text))
447 assert rsp.status_code == 204 or rsp.status_code == 200, 'Status' \
448 ' code returned %d' % rsp.status_code
450 # check request content against restconf's datastore
451 response = requests.get(url, auth=('admin', 'admin'),
452 headers={'Accept': 'application/xml'})
453 assert response.status_code == 200
454 req = ET.tostring(ET.fromstring(xml_string))
455 res = ET.tostring(ET.fromstring(response.text))
456 assert req == res, 'uploaded and stored xml, are not the same\n' \
457 'uploaded: %s\nstored:%s' % (req, res)
459 # collect flow table state on switch
460 switch_flows = get_flows(self.net)
461 assert len(switch_flows) > 0
463 # compare requested object and flow table state
464 for important_element in check_elements(xml_string, keywords):
465 # log.info('important element: {}'.format(important_element.nodeName))
466 comparator = COMPARATORS.get(important_element.nodeName,
467 COMPARATORS['default'])
469 comparator(important_element, switch_flows[0])
473 # generate list of available xml requests
476 xmlfiles = ('f%d.xml' % fid for fid in xmls)
478 xmlfiles = (xml for xml in os.listdir(path) if xml.endswith('.xml'))
480 # define key getter for sorting
481 def get_test_number(test_name):
482 return int(test_name[1:-4])
484 for xmlfile in xmlfiles:
485 test_name = 'test_xml_%04d' % get_test_number(xmlfile)
486 setattr(TestOpenFlowXMLs,
488 generate_test(os.path.join(path, xmlfile)))
491 if __name__ == '__main__':
493 logging.basicConfig(level=logging.DEBUG)
495 # parse cmdline arguments
496 parser = argparse.ArgumentParser(description='Run switch <-> ODL tests '
498 parser.add_argument('--odlhost', default='127.0.0.1', help='host where '
499 'odl controller is running')
500 parser.add_argument('--odlport', type=int, default=8080, help='port on '
501 'which odl\'s RESTCONF is listening')
502 parser.add_argument('--mnport', type=int, default=6653, help='port on '
503 'which odl\'s controller is listening')
504 parser.add_argument('--xmls', default=None, help='generete tests only '
505 'from some xmls (i.e. 1,3,34) ')
506 args = parser.parse_args()
508 # set host and port of ODL controller for test cases
509 TestOpenFlowXMLs.port = args.odlport
510 TestOpenFlowXMLs.host = args.odlhost
511 TestOpenFlowXMLs.mn_port = args.mnport
514 with open('keywords.csv') as f:
515 keywords = dict(line.strip().split(';') for line in f
516 if not line.startswith('#'))
518 match_keywords = None
519 with open('match-keywords.csv') as f:
520 match_keywords = dict(line.strip().split(';') for line in f
521 if not line.startswith('#'))
523 action_keywords = None
524 with open('action-keywords.csv') as f:
525 action_keywords = dict(line.strip().split(';') for line in f
526 if not line.startswith('#'))
528 # fix arguments for unittest
531 # generate tests for TestOpenFlowXMLs
532 if args.xmls is not None:
533 xmls = map(int, args.xmls.split(','))
534 generate_tests_from_xmls('xmls', xmls)
536 generate_tests_from_xmls('xmls')