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
24 def create_network(controller_ip, controller_port):
25 """Create topology and mininet network."""
26 topo = mininet.topo.Topo()
32 topo.addLink('h1', 's1')
33 topo.addLink('h2', 's1')
35 switch = mininet.util.customConstructor(
36 {'ovsk': OVSKernelSwitch}, 'ovsk,protocols=OpenFlow13')
38 controller = mininet.util.customConstructor(
39 {'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__)
74 def parse_matches(flow, matches):
78 split_match = match.split('=', 1)
79 if len(split_match) == 1:
80 flow['matches'][split_match[0]] = None
82 flow['matches'][split_match[0]] = split_match[1].rstrip(',')
84 switch = net.switches[0]
85 output = switch.cmdPrint(
86 'ovs-ofctl -O OpenFlow13 dump-flows %s' % switch.name)
87 # output = switch.cmdPrint(
88 # 'ovs-ofctl -F openflow10 dump-flows %s' % switch.name)
90 log.debug('switch flow table: {}'.format(output))
94 for line in output.splitlines()[1:]:
96 for word in line.split():
99 key, value = word.split('=', 1)
101 # TODO: need to figure out what to do here?
104 if key == 'priority':
105 values = value.split(',')
106 flow[key] = values[0]
107 parse_matches(flow, values[1:])
109 flow[key] = value.rstrip(',')
114 return sorted(flows, key=lambda x: x['duration'].rstrip('s'))
117 def translate_to_flow(flow, name, dictionary):
118 switch_flow_name = dictionary[name]
120 key_err = '{} needs to be present in flow definition. Flow definition ' \
121 'was: {}.'.format(switch_flow_name, flow)
122 assert switch_flow_name in flow, key_err
123 return switch_flow_name
126 def get_text_value(element):
127 return element.childNodes[0].nodeValue
130 def compare_elements(expected_match, actual_match, kw, comparators, default):
131 for child in expected_match.childNodes:
132 if child.nodeType is expected_match.TEXT_NODE:
135 comparator = comparators.get(child.nodeName, default)
136 comparator(child, actual_match, kw)
139 def fallback_comparator(xml_element, switch_flow, kw):
140 # print 'fallback_comparator-xml_element', xml_element.toxml()
141 # print 'fallback_comparator: switch_flow', switch_flow
142 # print 'fallback_comparator: kw', kws
144 name = translate_to_flow(switch_flow, xml_element.nodeName, kw)
146 actual = switch_flow[name]
147 expected = xml_element.childNodes[0].nodeValue
149 data = xml_element.toxml(), name, actual
150 # print 'fallback_comparator: data', data
152 assert expected == actual, 'xml part: %s && switch %s=%s' % data
155 def default_comparator(xml_element, switch_flow):
156 fallback_comparator(xml_element, switch_flow, keywords)
159 def integer_comparator(expected, actual, kw, base):
160 expected_value = int(expected.childNodes[0].data)
162 name = kw.get(expected.nodeName)
163 actual_value = int(actual[name], base)
165 data = expected.toxml(), name, actual
166 assert expected_value == actual_value, \
167 'xml value: %s && actual value %s=%s' % data
170 def cookie_comparator(cookie, switch_flow):
171 integer_comparator(cookie, switch_flow, keywords, 16)
174 def ethernet_address_comparator(child, actual_match, kw):
175 expected_address = child.getElementsByTagName("address")[0].childNodes[0].data
176 actual_address = actual_match[kw.get(child.nodeName)]
178 data = child.toxml(), kw.get(child.nodeName), actual_address
180 assert lower(expected_address) == lower(actual_address), \
181 'xml address: %s && actual address %s=%s' % data
184 def vlan_match_comparator(expected_match, actual_match, kw):
186 def compare_vlan_pcp(expected_match, actual_match, kw):
187 integer_comparator(expected_match, actual_match, kw, 10)
189 def compare_vlan_id(expected_match, actual_match, kw):
190 integer_comparator(expected_match.getElementsByTagName('vlan-id')[0],
191 actual_match, kw, 10)
194 'vlan-pcp': compare_vlan_pcp,
195 'vlan-id': compare_vlan_id,
198 # print 'ethernet_match_comparator-expected_match:', expected_match.toxml()
199 # print 'ethernet_match_comparator-actual_match:', actual_match
201 compare_elements(expected_match, actual_match, kw,
202 VLAN_COMPARATORS, fallback_comparator)
205 def ethernet_match_comparator(expected_match, actual_match, kw):
206 def compare_etype(child, actual_match, kw):
207 expected_etype = int(child.getElementsByTagName("type")[0].childNodes[0].data)
208 name = kw.get(child.nodeName)
209 data = child.toxml(), name, actual_match
211 if expected_etype == 2048: # IP
212 assert ((actual_match.get('ip', 'IP Not-present') is None) or
213 (actual_match.get('tcp', 'TCP Not-present') is None) or
214 (actual_match.get('sctp', 'SCTP Not-present') is None) or
215 (actual_match.get('udp', 'UDP Not-present') is None)), 'Expected etype %s && actual etype %s=%s' % data # noqa
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, '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, 'ip protocol type: expected %s, actual %s=%s' % data # noqa
263 elif expected_proto == 17: # UDP
264 assert actual_match.get('udp', 'UDP Not-present') is None, 'ip protocol type: expected %s, actual %s=%s' % data # noqa
266 elif expected_proto == 132: # SCTP
267 assert actual_match.get('sctp', 'SCTP Not-present') is None, 'ip protocol type: expected %s, actual %s=%s' % data # noqa
270 fallback_comparator(child, actual_match, kw)
272 def compare_dscp(child, actual_match, kw):
273 # print 'compare_dscp:', child.toxml(), actual_match
275 expected_dscp = int(child.childNodes[0].data)
276 name = kw.get(child.nodeName)
277 actual_dscp = int(actual_match[name])
279 data = child.toxml(), name, actual_match
281 assert (expected_dscp * 4) == actual_dscp, 'dscp: expected %s, actual %s=%s' % data
283 IP_MATCH_COMPARATORS = {
284 'ip-protocol': compare_proto,
285 'ip-dscp': compare_dscp,
288 # print 'ip_match_comparator:', expected_match.toxml(), actual_match
289 compare_elements(expected_match, actual_match, kw,
290 IP_MATCH_COMPARATORS, fallback_comparator)
293 def match_comparator(expected_match, switch_flow):
294 MATCH_COMPARATORS = {
295 'arp-source-hardware-address': ethernet_address_comparator,
296 'arp-target-hardware-address': ethernet_address_comparator,
297 'vlan-match': vlan_match_comparator,
298 'ethernet-match': ethernet_match_comparator,
299 'ip-match': ip_match_comparator,
300 'ipv4-destination': ipv4_comparator,
301 'ipv4-source': ipv4_comparator,
304 actual_match = switch_flow['matches']
306 # print 'match_comparator-expected_match:', expected_match.toxml()
307 # print 'match_comparator-actual_match:', actual_match
308 # print 'match_comparator: keywords', keywords
310 compare_elements(expected_match, actual_match, match_keywords,
311 MATCH_COMPARATORS, fallback_comparator)
314 def actions_comparator(actions, switch_flow):
315 # print 'actions_comparator:', actions, switch_flow
317 actual_actions = switch_flow['actions'].split(",")
318 # print 'actions_comparator:', actual_actions
320 for action in actions.childNodes:
321 if action.nodeType is actions.TEXT_NODE:
324 action_name = action.childNodes[3].nodeName
325 expected_action = action_keywords.get(action_name)
327 data = action.toxml(), expected_action
328 # print 'actions_comparator:', data
330 assert expected_action in actual_actions, 'xml part:\n%s\n expected action: %s' % data
333 def null_comparator(element, switch_flow):
337 def instructions_comparator(instructions_element, switch_flow):
338 INSTRUCTION_COMPARATORS = {
339 'apply-actions': actions_comparator,
340 'default': null_comparator,
342 # print 'instructions_comparator:', instructions_element, switch_flow
344 instructions = instructions_element.childNodes # noqa
346 for instruction in instructions_element.childNodes:
347 if instruction.nodeType is instructions_element.TEXT_NODE:
350 for itype in instruction.childNodes:
351 if itype.nodeType is itype.TEXT_NODE:
354 comparator = INSTRUCTION_COMPARATORS.get(itype.nodeName,
355 INSTRUCTION_COMPARATORS['default'])
356 comparator(itype, switch_flow)
360 'cookie': cookie_comparator,
361 'instructions': instructions_comparator,
362 'match': match_comparator,
363 'default': default_comparator,
367 def all_nodes(xml_root):
369 Generates every non-text nodes.
371 current_nodes = [xml_root]
374 while len(current_nodes) > 0:
375 for node in current_nodes:
376 if node.nodeType != xml_root.TEXT_NODE:
378 next_nodes.extend(node.childNodes)
380 current_nodes, next_nodes = next_nodes, []
383 def check_elements(xmlstr, keywords):
384 # namespace = 'urn:opendaylight:flow:inventory'
385 tree = md.parseString(xmlstr)
387 for element in all_nodes(tree.documentElement):
388 # switch flow object contains only some data from xml
389 if element.nodeName not in keywords:
390 # print 'check_elements: element.nodeName', element.nodeName, 'NOT in keywords'
395 raise StopIteration()
398 class TestOpenFlowXMLs(unittest.TestCase):
401 cls.net = create_network(cls.host, cls.mn_port)
406 def tearDownClass(cls):
410 def get_values(node, *tags):
411 result = {tag: None for tag in tags}
412 for node in all_nodes(node):
413 if node.nodeName in result and len(node.childNodes) > 0:
414 result[node.nodeName] = node.childNodes[0].nodeValue
418 def generate_tests_from_xmls(path, xmls=None):
419 # generate test function from path to request xml
420 def generate_test(path_to_xml):
422 with open(path_to_xml) as f:
423 xml_string = f.read()
425 tree = md.parseString(xml_string)
426 ids = get_values(tree.documentElement, 'table_id', 'id')
429 log = logging.getLogger(__name__)
430 # send request throught RESTCONF
431 data = (self.host, self.port, ids['table_id'], ids['id'])
432 url = 'http://%s:%d/restconf/config/opendaylight-inventory:nodes' \
433 '/node/openflow:1/table/%s/flow/%s' % data
435 'Content-Type': 'application/xml',
436 'Accept': 'application/xml',
438 log.info('sending request to url: {}'.format(url))
439 rsp = requests.put(url, auth=('admin', 'admin'), data=xml_string,
441 log.info('received status code: {}'.format(rsp.status_code))
442 log.debug('received content: {}'.format(rsp.text))
443 assert rsp.status_code == 204 or rsp.status_code == 200, 'Status' \
444 ' code returned %d' % rsp.status_code
446 # check request content against restconf's datastore
447 response = requests.get(url, auth=('admin', 'admin'),
448 headers={'Accept': 'application/xml'})
449 assert response.status_code == 200
450 req = ET.tostring(ET.fromstring(xml_string))
451 res = ET.tostring(ET.fromstring(response.text))
452 assert req == res, 'uploaded and stored xml, are not the same\n' \
453 'uploaded: %s\nstored:%s' % (req, res)
455 # collect flow table state on switch
456 switch_flows = get_flows(self.net)
457 assert len(switch_flows) > 0
459 # compare requested object and flow table state
460 for important_element in check_elements(xml_string, keywords):
461 # log.info('important element: {}'.format(important_element.nodeName))
462 comparator = COMPARATORS.get(important_element.nodeName,
463 COMPARATORS['default'])
465 comparator(important_element, switch_flows[0])
469 # generate list of available xml requests
472 xmlfiles = ('f%d.xml' % fid for fid in xmls)
474 xmlfiles = (xml for xml in os.listdir(path) if xml.endswith('.xml'))
476 # define key getter for sorting
477 def get_test_number(test_name):
478 return int(test_name[1:-4])
480 for xmlfile in xmlfiles:
481 test_name = 'test_xml_%04d' % get_test_number(xmlfile)
482 setattr(TestOpenFlowXMLs,
484 generate_test(os.path.join(path, xmlfile)))
487 if __name__ == '__main__':
489 logging.basicConfig(level=logging.DEBUG)
491 # parse cmdline arguments
492 parser = argparse.ArgumentParser(description='Run switch <-> ODL tests '
494 parser.add_argument('--odlhost', default='127.0.0.1', help='host where '
495 'odl controller is running')
496 parser.add_argument('--odlport', type=int, default=8080, help='port on '
497 'which odl\'s RESTCONF is listening')
498 parser.add_argument('--mnport', type=int, default=6653, help='port on '
499 'which odl\'s controller is listening')
500 parser.add_argument('--xmls', default=None, help='generete tests only '
501 'from some xmls (i.e. 1,3,34) ')
502 args = parser.parse_args()
504 # set host and port of ODL controller for test cases
505 TestOpenFlowXMLs.port = args.odlport
506 TestOpenFlowXMLs.host = args.odlhost
507 TestOpenFlowXMLs.mn_port = args.mnport
510 with open('keywords.csv') as f:
511 keywords = dict(line.strip().split(';') for line in f
512 if not line.startswith('#'))
514 match_keywords = None
515 with open('match-keywords.csv') as f:
516 match_keywords = dict(line.strip().split(';') for line in f
517 if not line.startswith('#'))
519 action_keywords = None
520 with open('action-keywords.csv') as f:
521 action_keywords = dict(line.strip().split(';') for line in f
522 if not line.startswith('#'))
524 # fix arguments for unittest
527 # generate tests for TestOpenFlowXMLs
528 if args.xmls is not None:
529 xmls = map(int, args.xmls.split(','))
530 generate_tests_from_xmls('xmls', xmls)
532 generate_tests_from_xmls('xmls')