7 import xml.dom.minidom as md
8 from xml.etree import ElementTree as ET
9 from string import lower
12 from netaddr import IPNetwork
17 from mininet.node import RemoteController
18 from mininet.node import OVSKernelSwitch
22 def create_network(controller_ip, controller_port):
23 """Create topology and mininet network."""
24 topo = mininet.topo.Topo()
30 topo.addLink('h1', 's1')
31 topo.addLink('h2', 's1')
33 switch=mininet.util.customConstructor(
34 {'ovsk':OVSKernelSwitch}, 'ovsk,protocols=OpenFlow13')
36 controller=mininet.util.customConstructor(
37 {'remote': RemoteController}, 'remote,ip=%s,port=%s' % (controller_ip,
41 net = mininet.net.Mininet(topo=topo, switch=switch, controller=controller)
47 """Get list of flows from network's first switch.
49 Return list of all flows on switch, sorted by duration (newest first)
50 One flow is a dictionary with all flow's attribute:value pairs. Matches
51 are stored under 'matches' key as another dictionary.
57 'duration': '3.434s,',
58 'hard_timeout': '12,',
59 'idle_timeout': '34,',
62 'nw_dst': '10.0.0.0/24'
71 log = logging.getLogger(__name__)
72 def parse_matches(flow, matches):
76 split_match = match.split('=', 1)
77 if len(split_match) == 1:
78 flow['matches'][split_match[0]] = None
80 flow['matches'][split_match[0]] = split_match[1].rstrip(',')
82 switch = net.switches[0]
83 output = switch.cmdPrint(
84 'ovs-ofctl -O OpenFlow13 dump-flows %s' % switch.name)
85 # output = switch.cmdPrint(
86 # 'ovs-ofctl -F openflow10 dump-flows %s' % switch.name)
88 log.debug('switch flow table: {}'.format(output))
92 for line in output.splitlines()[1:]:
94 for word in line.split():
97 key, value = word.split('=', 1)
99 #TODO: need to figure out what to do here?
102 if key == 'priority':
103 values = value.split(',')
104 flow[key] = values[0]
105 parse_matches(flow, values[1:])
107 flow[key] = value.rstrip(',')
112 return sorted(flows, key=lambda x: x['duration'].rstrip('s'))
115 def translate_to_flow(flow, name, dictionary):
116 switch_flow_name = dictionary[name]
118 key_err = '{} needs to be present in flow definition. Flow definition ' \
119 'was: {}.'.format(switch_flow_name, flow)
120 assert switch_flow_name in flow, key_err
121 return switch_flow_name
124 def get_text_value(element):
125 return element.childNodes[0].nodeValue
128 def compare_elements(expected_match, actual_match, kw, comparators, default):
129 for child in expected_match.childNodes:
130 if child.nodeType is expected_match.TEXT_NODE:
133 comparator = comparators.get(child.nodeName, default)
134 comparator(child, actual_match, kw)
137 def fallback_comparator(xml_element, switch_flow, kw):
138 # print 'fallback_comparator-xml_element', xml_element.toxml()
139 # print 'fallback_comparator: switch_flow', switch_flow
140 # print 'fallback_comparator: kw', kws
142 name = translate_to_flow(switch_flow, xml_element.nodeName, kw)
144 actual = switch_flow[name]
145 expected = xml_element.childNodes[0].nodeValue
147 data = xml_element.toxml(), name, actual
148 # print 'fallback_comparator: data', data
150 assert expected == actual, 'xml part: %s && switch %s=%s' % data
153 def default_comparator(xml_element, switch_flow):
154 fallback_comparator(xml_element, switch_flow, keywords)
157 def integer_comparator(expected, actual, kw, base):
158 expected_value = int(expected.childNodes[0].data)
160 name = kw.get(expected.nodeName)
161 actual_value = int(actual[name], base)
163 data = expected.toxml(), name, actual
164 assert expected_value == actual_value, \
165 'xml value: %s && actual value %s=%s' % data
168 def cookie_comparator(cookie, switch_flow):
169 integer_comparator(cookie, switch_flow, keywords, 16)
172 def ethernet_address_comparator(child, actual_match, kw):
173 expected_address = child.getElementsByTagName("address")[0].childNodes[0].data
174 actual_address = actual_match[kw.get(child.nodeName)]
176 data = child.toxml(), kw.get(child.nodeName), actual_address
178 assert lower(expected_address) == lower(actual_address), \
179 'xml address: %s && actual address %s=%s' % data
182 def masked_value_hex_comparator(child, actual_match, kw, vname, kname):
183 print 'masked_value_hex_comparator', child.toxml(), actual_match, \
184 vname, kname, child.nodeName
186 emd = int(child.getElementsByTagName(vname)[0].childNodes[0].data)
189 data = child.toxml(), name, actual_match
190 print 'masked_value_hex_comparator', name
192 amd = int(actual_match[name], 16)
194 emasks = child.getElementsByTagName(kname)
196 print 'masked_value_hex_comparator - mask present:', \
197 emasks[0].childNodes[0].data
199 assert emd == amd, 'metadata: expected %s && actual %s=%s' % data
203 def proto_match_comparator(expected_match, actual_match, kw):
205 def compare_base10_integer(expected_match, actual_match, kw):
206 integer_comparator(expected_match, actual_match, kw, 10)
208 def compare_vlan_id(expected_match, actual_match, kw):
209 integer_comparator(expected_match.getElementsByTagName('vlan-id')[0], \
210 actual_match, kw, 10)
212 def compare_pbb(expected, actual, kw):
213 masked_value_hex_comparator(expected, actual, kw, \
214 'pbb-isid', 'pbb-mask')
216 PROTO_COMPARATORS = {
217 'vlan-id': compare_vlan_id,
221 # print 'ethernet_match_comparator-expected_match:', expected_match.toxml()
222 # print 'ethernet_match_comparator-actual_match:', actual_match
224 compare_elements(expected_match, actual_match, kw, \
225 PROTO_COMPARATORS, compare_base10_integer)
228 #def masked_value_hex_comparator(child, actual_match, kw):
229 # emd = int(child.getElementsByTagName("metadata")[0].childNodes[0].data)
231 # name = kw.get(child.nodeName)
232 # data = child.toxml(), name, actual_match
234 # amd = int(actual_match[kw.get(name)], 16)
236 # emasks = child.getElementsByTagName("metadata-mask")
237 # if len(emasks) != 0:
238 # print 'mask present'
240 # assert emd == amd, 'metadata: expected %s && actual %s=%s' % data
244 def ethernet_match_comparator(expected_match, actual_match, kw):
245 def compare_etype(child, actual_match, kw):
247 int(child.getElementsByTagName("type")[0].childNodes[0].data)
248 name = kw.get(child.nodeName)
249 data = child.toxml(), name, actual_match
251 if expected_etype == 2048: # IPv4
252 assert((actual_match.get('ip', 'IP Not-present') is None) or \
253 (actual_match.get('tcp', 'TCP Not-present') is None) or \
254 (actual_match.get('icmp', 'ICMP Not-present') is None) or \
255 (actual_match.get('sctp', 'SCTP Not-present') is None) or \
256 (actual_match.get('udp', 'UDP Not-present') is None)), \
257 'Expected etype %s && actual etype %s=%s' % data
259 elif expected_etype == 2054: # ARP
260 assert actual_match.get('arp', 'ARP Not-present') is None, \
261 'Expected etype %s && actual etype %s=%s' % data
263 elif expected_etype == 34887: # MPLS
264 assert actual_match.get('mpls', 'MPLS Not-present') is None, \
265 'Expected etype %s && actual etype %s=%s' % data
267 elif expected_etype == 34525: # IPv6
268 assert((actual_match.get('ipv6', 'IPv6 Not-present') is None) or \
269 (actual_match.get('tcp6', 'TCP6 Not-present') is None) or \
270 (actual_match.get('icmp6', 'ICMP6 Not-present') is None) or \
271 (actual_match.get('sctp6', 'SCTP6 Not-present') is None) or \
272 (actual_match.get('udp6', 'UDP6 Not-present') is None)), \
273 'Expected etype %s && actual etype %s=%s' % data
276 actual_etype = int(actual_match[name], 16)
278 assert expected_etype == actual_etype, \
279 'xml etype: %s && actual etype %s=%s' % data
283 'ethernet-type': compare_etype,
284 'ethernet-source': ethernet_address_comparator,
285 'ethernet-destination': ethernet_address_comparator,
288 # print 'ethernet_match_comparator-expected_match:', expected_match.toxml()
289 # print 'ethernet_match_comparator-actual_match:', actual_match
291 compare_elements(expected_match, actual_match, kw, \
292 ETH_COMPARATORS, fallback_comparator)
295 def ip_subnet_comparator(expected_match, actual_match, kw):
296 # print 'ip_comparator:', expected_match.toxml(), actual_match
297 # print 'ip_comparator-actual_match:', actual_match
299 expected_value = expected_match.childNodes[0].data
300 actual_value = actual_match[kw.get(expected_match.nodeName)]
302 data = expected_match.toxml(), kw.get(expected_match.nodeName), actual_value
304 assert IPNetwork(expected_value) == IPNetwork(actual_value),\
305 'xml part: %s && address %s=%s' % data
308 def ip_match_comparator(expected_match, actual_match, kw):
309 def compare_proto(child, actual_match, kw):
310 print 'compare_proto:', child.toxml(), actual_match
311 expected_proto = int(child.childNodes[0].data)
313 name = child.nodeName
314 data = expected_match.toxml(), name, actual_match
316 if expected_proto == 1: # ICMP
317 assert ((actual_match.get('icmp', 'ICMP Not-present') is None) or \
318 (actual_match.get('icmp6', 'ICMP6 Not-present') is None)), \
319 'ip protocol type: expected %s, actual %s=%s' % data
321 elif expected_proto == 6: # TCP
322 assert ((actual_match.get('tcp', 'TCP Not-present') is None) or \
323 (actual_match.get('tcp6', 'TCP6 Not-present') is None)), \
324 'ip protocol type: expected %s, actual %s=%s' % data
326 elif expected_proto == 17: #UDP
327 assert ((actual_match.get('udp', 'UDP Not-present') is None) or \
328 (actual_match.get('udp6', 'UDP6 Not-present') is None)), \
329 'ip protocol type: expected %s, actual %s=%s' % data
331 elif expected_proto == 58: # ICMP
332 assert actual_match.get('icmp6', 'ICMP6 Not-present') is None, \
333 'ip protocol type: expected %s, actual %s=%s' % data
335 elif expected_proto == 132: #SCTP
336 assert ((actual_match.get('sctp', 'SCTP Not-present') is None) or \
337 (actual_match.get('sctp6', 'SCTP6 Not-present') is None)), \
338 'ip protocol type: expected %s, actual %s=%s' % data
341 fallback_comparator(child, actual_match, kw)
344 def compare_dscp(child, actual_match, kw):
345 # print 'compare_dscp:', child.toxml(), actual_match
347 expected_dscp = int(child.childNodes[0].data)
348 name = kw.get(child.nodeName)
349 actual_dscp = int(actual_match[name])
351 data = child.toxml(), name, actual_match
353 assert (expected_dscp * 4) == actual_dscp, \
354 'dscp: expected %s, actual %s=%s' % data
357 IP_MATCH_COMPARATORS = {
358 'ip-protocol': compare_proto,
359 'ip-dscp': compare_dscp,
362 # print 'ip_match_comparator:', expected_match.toxml(), actual_match
363 compare_elements(expected_match, actual_match, kw, \
364 IP_MATCH_COMPARATORS, fallback_comparator)
367 def match_comparator(expected_match, switch_flow):
369 def compare_metadata(expected, actual, kw):
370 masked_value_hex_comparator(expected, actual, kw, \
371 'metadata', 'metadata-mask')
373 def compare_ipv6_label(expected, actual, kw):
374 print 'compare_ipv6_label', expected.toxml(), actual
375 masked_value_hex_comparator(expected, actual, kw, \
376 'ipv6-flabel', 'flabel-mask')
379 def compare_tunnel_id(expected, actual, kw):
380 masked_value_hex_comparator(expected, actual, kw, \
381 'tunnel-id', 'tunnel-mask')
384 def compare_ipv6_ext_header(expected, actual, kw):
385 masked_value_hex_comparator(expected, actual, kw, \
386 'ipv6-exthdr', 'ipv6-exthdr-mask')
389 MATCH_COMPARATORS = {
390 'arp-source-hardware-address': ethernet_address_comparator,
391 'arp-target-hardware-address': ethernet_address_comparator,
392 'metadata': compare_metadata,
393 'ipv6-label': compare_ipv6_label,
394 'ipv6-ext-header': compare_ipv6_ext_header,
395 'tunnel': compare_tunnel_id,
396 'protocol-match-fields': proto_match_comparator,
397 'vlan-match': proto_match_comparator,
398 'ethernet-match': ethernet_match_comparator,
399 'ip-match': ip_match_comparator,
400 'icmpv4-match': ip_match_comparator,
401 'icmpv6-match': ip_match_comparator,
402 'ipv4-destination': ip_subnet_comparator,
403 'ipv4-source': ip_subnet_comparator,
404 'ipv6-destination': ip_subnet_comparator,
405 'ipv6-source': ip_subnet_comparator,
408 actual_match = switch_flow['matches']
410 # print 'match_comparator-expected_match:', expected_match.toxml()
411 # print 'match_comparator-actual_match:', actual_match
412 # print 'match_comparator: keywords', keywords
414 compare_elements(expected_match, actual_match, match_keywords, \
415 MATCH_COMPARATORS, fallback_comparator)
418 def actions_comparator(actions, switch_flow):
419 # print 'actions_comparator:', actions, switch_flow
421 actual_actions = switch_flow['actions'].split(",")
422 # print 'actions_comparator:', actual_actions
424 for action in actions.childNodes:
425 if action.nodeType is actions.TEXT_NODE:
428 action_name = action.childNodes[3].nodeName
429 expected_action = action_keywords.get(action_name)
431 data = action.toxml(), expected_action
432 # print 'actions_comparator:', data
434 assert expected_action in actual_actions, \
435 'xml part:\n%s\n expected action: %s' % data
438 def null_comparator(element, switch_flow):
442 def instructions_comparator(instructions_element, switch_flow):
443 INSTRUCTION_COMPARATORS = {
444 'apply-actions': actions_comparator,
445 'default': null_comparator,
447 # print 'instructions_comparator:', instructions_element, switch_flow
449 instructions = instructions_element.childNodes
451 for instruction in instructions_element.childNodes:
452 if instruction.nodeType is instructions_element.TEXT_NODE:
455 for itype in instruction.childNodes:
456 if itype.nodeType is itype.TEXT_NODE:
459 comparator = INSTRUCTION_COMPARATORS.get(itype.nodeName,
460 INSTRUCTION_COMPARATORS['default'])
461 comparator(itype, switch_flow)
465 'cookie': cookie_comparator,
466 'instructions': instructions_comparator,
467 'match': match_comparator,
468 'default': default_comparator,
471 def all_nodes(xml_root):
473 Generates every non-text nodes.
475 current_nodes = [xml_root]
478 while len(current_nodes) > 0:
479 for node in current_nodes:
480 if node.nodeType != xml_root.TEXT_NODE:
482 next_nodes.extend(node.childNodes)
484 current_nodes, next_nodes = next_nodes, []
487 def check_elements(xmlstr, keywords):
488 # namespace = 'urn:opendaylight:flow:inventory'
489 tree = md.parseString(xmlstr)
491 for element in all_nodes(tree.documentElement):
492 # switch flow object contains only some data from xml
493 if element.nodeName not in keywords:
494 # print 'check_elements: element.nodeName', element.nodeName, 'NOT in keywords'
499 raise StopIteration()
502 class TestOpenFlowXMLs(unittest.TestCase):
505 cls.net = create_network(cls.host, cls.mn_port)
510 def tearDownClass(cls):
514 def get_values(node, *tags):
515 result = {tag: None for tag in tags}
516 for node in all_nodes(node):
517 if node.nodeName in result and len(node.childNodes) > 0:
518 result[node.nodeName] = node.childNodes[0].nodeValue
522 class BadResponseCodeError(Exception):
523 def __init__(self, value):
526 return repr('BadResponseCodeError: %s' % self.value)
529 def generate_tests_from_xmls(path, xmls=None):
530 # generate test function from path to request xml
531 def generate_test(path_to_xml):
533 with open(path_to_xml) as f:
534 xml_string = f.read()
536 tree = md.parseString(xml_string)
537 ids = get_values(tree.documentElement, 'table_id', 'id')
540 log = logging.getLogger(__name__)
541 # send request throught RESTCONF
542 data = (self.host, self.port, ids['table_id'], ids['id'])
543 url = 'http://%s:%d/restconf/config/opendaylight-inventory:nodes' \
544 '/node/openflow:1/table/%s/flow/%s' % data
546 'Content-Type': 'application/xml',
547 'Accept': 'application/xml',
549 log.info('sending request to url: {}'.format(url))
550 rsp = requests.put(url, auth=('admin', 'admin'), data=xml_string,
552 log.info('received status code: {}'.format(rsp.status_code))
553 log.debug('received content: {}'.format(rsp.text))
554 assert rsp.status_code == 204 or rsp.status_code == 200, 'Status' \
555 ' code returned %d' % rsp.status_code
558 # check request content against restconf's datastore
559 response = requests.get(url, auth=('admin', 'admin'),
560 headers={'Accept': 'application/xml'})
561 if response.status_code != 200:
562 raise BadResponseCodeError('response: {}'.format(response))
564 req = xmltodict.parse(ET.tostring(ET.fromstring(xml_string)))
565 res = xmltodict.parse(ET.tostring(ET.fromstring(response.text)))
566 assert req == res, 'uploaded and stored xml, are not the same\n' \
567 'uploaded: %s\nstored:%s' % (req, res)
569 # collect flow table state on switch
570 switch_flows = get_flows(self.net)
571 assert len(switch_flows) > 0
573 # compare requested object and flow table state
574 for important_element in check_elements(xml_string, keywords):
575 # log.info('important element: {}'.format(important_element.nodeName))
576 comparator = COMPARATORS.get(important_element.nodeName,
577 COMPARATORS['default'])
579 comparator(important_element, switch_flows[0])
581 response = requests.delete(url, auth=('admin', 'admin'),
582 headers={'Accept': 'application/xml'})
583 assert response.status_code == 200
588 # generate list of available xml requests
591 xmlfiles = ('f%d.xml' % fid for fid in xmls)
593 xmlfiles = (xml for xml in os.listdir(path) if xml.endswith('.xml'))
595 # define key getter for sorting
596 def get_test_number(test_name):
597 return int(test_name[1:-4])
599 for xmlfile in xmlfiles:
600 test_name = 'test_xml_%04d' % get_test_number(xmlfile)
601 setattr(TestOpenFlowXMLs,
603 generate_test(os.path.join(path, xmlfile)))
606 if __name__ == '__main__':
608 logging.basicConfig(level=logging.DEBUG)
610 # parse cmdline arguments
611 parser = argparse.ArgumentParser(description='Run switch <-> ODL tests '
613 parser.add_argument('--odlhost', default='127.0.0.1', help='host where '
614 'odl controller is running')
615 parser.add_argument('--odlport', type=int, default=8080, help='port on '
616 'which odl\'s RESTCONF is listening')
617 parser.add_argument('--mnport', type=int, default=6653, help='port on '
618 'which odl\'s controller is listening')
619 parser.add_argument('--xmls', default=None, help='generete tests only '
620 'from some xmls (i.e. 1,3,34) ')
621 args = parser.parse_args()
623 # set host and port of ODL controller for test cases
624 TestOpenFlowXMLs.port = args.odlport
625 TestOpenFlowXMLs.host = args.odlhost
626 TestOpenFlowXMLs.mn_port = args.mnport
629 with open('keywords.csv') as f:
630 keywords = dict(line.strip().split(';') for line in f
631 if not line.startswith('#'))
633 match_keywords = None
634 with open('match-keywords.csv') as f:
635 match_keywords = dict(line.strip().split(';') for line in f
636 if not line.startswith('#'))
638 action_keywords = None
639 with open('action-keywords.csv') as f:
640 action_keywords = dict(line.strip().split(';') for line in f
641 if not line.startswith('#'))
643 # fix arguments for unittest
646 # generate tests for TestOpenFlowXMLs
647 if args.xmls is not None:
648 xmls = map(int, args.xmls.split(','))
649 generate_tests_from_xmls('xmls', xmls)
651 generate_tests_from_xmls('xmls')