10 import xml.dom.minidom as md
11 from xml.etree import ElementTree as ET
12 from netaddr import IPNetwork
13 from string import lower
20 from mininet.node import RemoteController
21 from mininet.node import OVSKernelSwitch
23 def create_network(controller_ip, controller_port):
24 """Create topology and mininet network."""
25 topo = mininet.topo.Topo()
31 topo.addLink('h1', 's1')
32 topo.addLink('h2', 's1')
34 switch=mininet.util.customConstructor(
35 {'ovsk':OVSKernelSwitch}, 'ovsk,protocols=OpenFlow13')
37 controller=mininet.util.customConstructor(
38 {'remote': RemoteController}, 'remote,ip=%s:%s' % (controller_ip,
42 net = mininet.net.Mininet(topo=topo, switch=switch, controller=controller)
48 """Get list of flows from network's first switch.
50 Return list of all flows on switch, sorted by duration (newest first)
51 One flow is a dictionary with all flow's attribute:value pairs. Matches
52 are stored under 'matches' key as another dictionary.
58 'duration': '3.434s,',
59 'hard_timeout': '12,',
60 'idle_timeout': '34,',
63 'nw_dst': '10.0.0.0/24'
72 log = logging.getLogger(__name__)
73 def parse_matches(flow, matches):
77 split_match = match.split('=', 1)
78 if len(split_match) == 1:
79 flow['matches'][split_match[0]] = None
81 flow['matches'][split_match[0]] = split_match[1].rstrip(',')
83 switch = net.switches[0]
84 output = switch.cmdPrint(
85 'ovs-ofctl -O OpenFlow13 dump-flows %s' % switch.name)
86 # output = switch.cmdPrint(
87 # 'ovs-ofctl -F openflow10 dump-flows %s' % switch.name)
89 log.debug('switch flow table: {}'.format(output))
93 for line in output.splitlines()[1:]:
95 for word in line.split():
98 key, value = word.split('=', 1)
100 #TODO: need to figure out what to do here?
103 if key == 'priority':
104 values = value.split(',')
105 flow[key] = values[0]
106 parse_matches(flow, values[1:])
108 flow[key] = value.rstrip(',')
113 return sorted(flows, key=lambda x: x['duration'].rstrip('s'))
116 def translate_to_flow(flow, name, dictionary):
117 switch_flow_name = dictionary[name]
119 key_err = '{} needs to be present in flow definition. Flow definition ' \
120 'was: {}.'.format(switch_flow_name, flow)
121 assert switch_flow_name in flow, key_err
122 return switch_flow_name
125 def get_text_value(element):
126 return element.childNodes[0].nodeValue
129 def compare_elements(expected_match, actual_match, kw, comparators, default):
130 for child in expected_match.childNodes:
131 if child.nodeType is expected_match.TEXT_NODE:
134 comparator = comparators.get(child.nodeName, default)
135 comparator(child, actual_match, kw)
138 def fallback_comparator(xml_element, switch_flow, kw):
139 # print 'fallback_comparator-xml_element', xml_element.toxml()
140 # print 'fallback_comparator: switch_flow', switch_flow
141 # print 'fallback_comparator: kw', kws
143 name = translate_to_flow(switch_flow, xml_element.nodeName, kw)
145 actual = switch_flow[name]
146 expected = xml_element.childNodes[0].nodeValue
148 data = xml_element.toxml(), name, actual
149 # print 'fallback_comparator: data', data
151 assert expected == actual, 'xml part: %s && switch %s=%s' % data
154 def default_comparator(xml_element, switch_flow):
155 fallback_comparator(xml_element, switch_flow, keywords)
158 def integer_comparator(expected, actual, kw, base):
159 expected_value = int(expected.childNodes[0].data)
161 name = kw.get(expected.nodeName)
162 actual_value = int(actual[name], base)
164 data = expected.toxml(), name, actual
165 assert expected_value == actual_value, \
166 'xml value: %s && actual value %s=%s' % data
169 def cookie_comparator(cookie, switch_flow):
170 integer_comparator(cookie, switch_flow, keywords, 16)
173 def ethernet_address_comparator(child, actual_match, kw):
174 expected_address = child.getElementsByTagName("address")[0].childNodes[0].data
175 actual_address = actual_match[kw.get(child.nodeName)]
177 data = child.toxml(), kw.get(child.nodeName), actual_address
179 assert lower(expected_address) == lower(actual_address), \
180 'xml address: %s && actual address %s=%s' % data
183 def vlan_match_comparator(expected_match, actual_match, kw):
185 def compare_vlan_pcp(expected_match, actual_match, kw):
186 integer_comparator(expected_match, actual_match, kw, 10)
188 def compare_vlan_id(expected_match, actual_match, kw):
189 integer_comparator(expected_match.getElementsByTagName('vlan-id')[0], \
190 actual_match, kw, 10)
193 'vlan-pcp': compare_vlan_pcp,
194 'vlan-id': compare_vlan_id,
197 # print 'ethernet_match_comparator-expected_match:', expected_match.toxml()
198 # print 'ethernet_match_comparator-actual_match:', actual_match
200 compare_elements(expected_match, actual_match, kw, \
201 VLAN_COMPARATORS, fallback_comparator)
204 def ethernet_match_comparator(expected_match, actual_match, kw):
205 def compare_etype(child, actual_match, kw):
206 expected_etype = int(child.getElementsByTagName("type")[0].childNodes[0].data)
207 name = kw.get(child.nodeName)
208 data = child.toxml(), name, actual_match
210 if expected_etype == 2048: # IP
211 assert ((actual_match.get('ip', 'IP Not-present') is None) or \
212 (actual_match.get('tcp', 'TCP Not-present') is None) or \
213 (actual_match.get('sctp', 'SCTP Not-present') is None) or \
214 (actual_match.get('udp', 'UDP Not-present') is None)), \
215 'Expected etype %s && actual etype %s=%s' % data
217 elif expected_etype == 2054: #ARP
218 assert actual_match.get('arp', 'ARP Not-present') is None, \
219 'Expected etype %s && actual etype %s=%s' % data
222 actual_etype = int(actual_match[name], 16)
224 assert expected_etype == actual_etype, \
225 'xml etype: %s && actual etype %s=%s' % data
229 'ethernet-type': compare_etype,
230 'ethernet-source': ethernet_address_comparator,
231 'ethernet-destination': ethernet_address_comparator,
234 # print 'ethernet_match_comparator-expected_match:', expected_match.toxml()
235 # print 'ethernet_match_comparator-actual_match:', actual_match
237 compare_elements(expected_match, actual_match, kw, \
238 ETH_COMPARATORS, fallback_comparator)
241 def ipv4_comparator(expected_match, actual_match, kw):
242 # print 'ip_v4_comparator:', expected_match.toxml(), actual_match
243 # print 'ip_v4_comparator-actual_match:', actual_match
245 expected_value = expected_match.childNodes[0].data
246 actual_value = actual_match[kw.get(expected_match.nodeName)]
248 data = expected_match.toxml(), kw.get(expected_match.nodeName), actual_value
250 assert IPNetwork(expected_value) == IPNetwork(actual_value),\
251 'xml part: %s && address %s=%s' % data
254 def ip_match_comparator(expected_match, actual_match, kw):
255 def compare_proto(child, actual_match, kw):
256 print 'compare_proto:', child.toxml(), actual_match
257 expected_proto = int(child.childNodes[0].data)
259 name = child.nodeName
260 data = expected_match.toxml(), name, actual_match
262 if expected_proto == 6: # TCP
263 assert actual_match.get('tcp', 'TCP Not-present') is None, \
264 'ip protocol type: expected %s, actual %s=%s' % data
266 elif expected_proto == 17: #UDP
267 assert actual_match.get('udp', 'UDP Not-present') is None, \
268 'ip protocol type: expected %s, actual %s=%s' % data
270 elif expected_proto == 132: #SCTP
271 assert actual_match.get('sctp', 'SCTP Not-present') is None, \
272 'ip protocol type: expected %s, actual %s=%s' % data
275 fallback_comparator(child, actual_match, kw)
278 def compare_dscp(child, actual_match, kw):
279 # print 'compare_dscp:', child.toxml(), actual_match
281 expected_dscp = int(child.childNodes[0].data)
282 name = kw.get(child.nodeName)
283 actual_dscp = int(actual_match[name])
285 data = child.toxml(), name, actual_match
287 assert (expected_dscp * 4) == actual_dscp, 'dscp: expected %s, actual %s=%s' % data
290 IP_MATCH_COMPARATORS = {
291 'ip-protocol': compare_proto,
292 'ip-dscp': compare_dscp,
295 # print 'ip_match_comparator:', expected_match.toxml(), actual_match
296 compare_elements(expected_match, actual_match, kw, \
297 IP_MATCH_COMPARATORS, fallback_comparator)
300 def match_comparator(expected_match, switch_flow):
301 MATCH_COMPARATORS = {
302 'arp-source-hardware-address': ethernet_address_comparator,
303 'arp-target-hardware-address': ethernet_address_comparator,
304 'vlan-match': vlan_match_comparator,
305 'ethernet-match': ethernet_match_comparator,
306 'ip-match': ip_match_comparator,
307 'ipv4-destination': ipv4_comparator,
308 'ipv4-source': ipv4_comparator,
311 actual_match = switch_flow['matches']
313 # print 'match_comparator-expected_match:', expected_match.toxml()
314 # print 'match_comparator-actual_match:', actual_match
315 # print 'match_comparator: keywords', keywords
317 compare_elements(expected_match, actual_match, match_keywords, \
318 MATCH_COMPARATORS, fallback_comparator)
321 def actions_comparator(actions, switch_flow):
322 # print 'actions_comparator:', actions, switch_flow
324 actual_actions = switch_flow['actions'].split(",")
325 # print 'actions_comparator:', actual_actions
327 for action in actions.childNodes:
328 if action.nodeType is actions.TEXT_NODE:
331 action_name = action.childNodes[3].nodeName
332 expected_action = action_keywords.get(action_name)
334 data = action.toxml(), expected_action
335 # print 'actions_comparator:', data
337 assert expected_action in actual_actions, 'xml part:\n%s\n expected action: %s' % data
340 def null_comparator(element, switch_flow):
344 def instructions_comparator(instructions_element, switch_flow):
345 INSTRUCTION_COMPARATORS = {
346 'apply-actions': actions_comparator,
347 'default': null_comparator,
349 # print 'instructions_comparator:', instructions_element, switch_flow
351 instructions = instructions_element.childNodes
353 for instruction in instructions_element.childNodes:
354 if instruction.nodeType is instructions_element.TEXT_NODE:
357 for itype in instruction.childNodes:
358 if itype.nodeType is itype.TEXT_NODE:
361 comparator = INSTRUCTION_COMPARATORS.get(itype.nodeName,
362 INSTRUCTION_COMPARATORS['default'])
363 comparator(itype, switch_flow)
367 'cookie': cookie_comparator,
368 'instructions': instructions_comparator,
369 'match': match_comparator,
370 'default': default_comparator,
373 def all_nodes(xml_root):
375 Generates every non-text nodes.
377 current_nodes = [xml_root]
380 while len(current_nodes) > 0:
381 for node in current_nodes:
382 if node.nodeType != xml_root.TEXT_NODE:
384 next_nodes.extend(node.childNodes)
386 current_nodes, next_nodes = next_nodes, []
389 def check_elements(xmlstr, keywords):
390 # namespace = 'urn:opendaylight:flow:inventory'
391 tree = md.parseString(xmlstr)
393 for element in all_nodes(tree.documentElement):
394 # switch flow object contains only some data from xml
395 if element.nodeName not in keywords:
396 # print 'check_elements: element.nodeName', element.nodeName, 'NOT in keywords'
401 raise StopIteration()
404 class TestOpenFlowXMLs(unittest.TestCase):
407 cls.net = create_network(cls.host, cls.mn_port)
412 def tearDownClass(cls):
416 def get_values(node, *tags):
417 result = {tag: None for tag in tags}
418 for node in all_nodes(node):
419 if node.nodeName in result and len(node.childNodes) > 0:
420 result[node.nodeName] = node.childNodes[0].nodeValue
424 def generate_tests_from_xmls(path, xmls=None):
425 # generate test function from path to request xml
426 def generate_test(path_to_xml):
428 with open(path_to_xml) as f:
429 xml_string = f.read()
431 tree = md.parseString(xml_string)
432 ids = get_values(tree.documentElement, 'table_id', 'id')
435 log = logging.getLogger(__name__)
436 # send request throught RESTCONF
437 data = (self.host, self.port, ids['table_id'], ids['id'])
438 url = 'http://%s:%d/restconf/config/opendaylight-inventory:nodes' \
439 '/node/openflow:1/table/%s/flow/%s' % data
441 'Content-Type': 'application/xml',
442 'Accept': 'application/xml',
444 log.info('sending request to url: {}'.format(url))
445 rsp = requests.put(url, auth=('admin', 'admin'), data=xml_string,
447 log.info('received status code: {}'.format(rsp.status_code))
448 log.debug('received content: {}'.format(rsp.text))
449 assert rsp.status_code == 204 or rsp.status_code == 200, 'Status' \
450 ' code returned %d' % rsp.status_code
452 # check request content against restconf's datastore
453 response = requests.get(url, auth=('admin', 'admin'),
454 headers={'Accept': 'application/xml'})
455 assert response.status_code == 200
456 req = ET.tostring(ET.fromstring(xml_string))
457 res = ET.tostring(ET.fromstring(response.text))
458 assert req == res, 'uploaded and stored xml, are not the same\n' \
459 'uploaded: %s\nstored:%s' % (req, res)
461 # collect flow table state on switch
462 switch_flows = get_flows(self.net)
463 assert len(switch_flows) > 0
465 # compare requested object and flow table state
466 for important_element in check_elements(xml_string, keywords):
467 # log.info('important element: {}'.format(important_element.nodeName))
468 comparator = COMPARATORS.get(important_element.nodeName,
469 COMPARATORS['default'])
471 comparator(important_element, switch_flows[0])
475 # generate list of available xml requests
478 xmlfiles = ('f%d.xml' % fid for fid in xmls)
480 xmlfiles = (xml for xml in os.listdir(path) if xml.endswith('.xml'))
482 # define key getter for sorting
483 def get_test_number(test_name):
484 return int(test_name[1:-4])
486 for xmlfile in xmlfiles:
487 test_name = 'test_xml_%04d' % get_test_number(xmlfile)
488 setattr(TestOpenFlowXMLs,
490 generate_test(os.path.join(path, xmlfile)))
493 if __name__ == '__main__':
495 logging.basicConfig(level=logging.DEBUG)
497 # parse cmdline arguments
498 parser = argparse.ArgumentParser(description='Run switch <-> ODL tests '
500 parser.add_argument('--odlhost', default='127.0.0.1', help='host where '
501 'odl controller is running')
502 parser.add_argument('--odlport', type=int, default=8080, help='port on '
503 'which odl\'s RESTCONF is listening')
504 parser.add_argument('--mnport', type=int, default=6653, help='port on '
505 'which odl\'s controller is listening')
506 parser.add_argument('--xmls', default=None, help='generete tests only '
507 'from some xmls (i.e. 1,3,34) ')
508 args = parser.parse_args()
510 # set host and port of ODL controller for test cases
511 TestOpenFlowXMLs.port = args.odlport
512 TestOpenFlowXMLs.host = args.odlhost
513 TestOpenFlowXMLs.mn_port = args.mnport
516 with open('keywords.csv') as f:
517 keywords = dict(line.strip().split(';') for line in f
518 if not line.startswith('#'))
520 match_keywords = None
521 with open('match-keywords.csv') as f:
522 match_keywords = dict(line.strip().split(';') for line in f
523 if not line.startswith('#'))
525 action_keywords = None
526 with open('action-keywords.csv') as f:
527 action_keywords = dict(line.strip().split(';') for line in f
528 if not line.startswith('#'))
530 # fix arguments for unittest
533 # generate tests for TestOpenFlowXMLs
534 if args.xmls is not None:
535 xmls = map(int, args.xmls.split(','))
536 generate_tests_from_xmls('xmls', xmls)
538 generate_tests_from_xmls('xmls')