Modify OF plugin Python script to run in Opendaylight
[integration/test.git] / test / tools / OF_Test / odl_tests.py
1 #!/usr/bin/python2.7
2
3 import os
4 import sys
5 import time
6 import logging
7 import argparse
8 import unittest
9 import requests
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
14
15 import mininet.node
16 import mininet.topo
17 import mininet.net
18 import mininet.util
19
20 from mininet.node import RemoteController
21 from mininet.node import OVSKernelSwitch
22
23 def create_network(controller_ip, controller_port):
24     """Create topology and mininet network."""
25     topo = mininet.topo.Topo()
26
27     topo.addSwitch('s1')
28     topo.addHost('h1')
29     topo.addHost('h2')
30
31     topo.addLink('h1', 's1')
32     topo.addLink('h2', 's1')
33
34     switch=mininet.util.customConstructor(
35         {'ovsk':OVSKernelSwitch}, 'ovsk,protocols=OpenFlow13')
36
37     controller=mininet.util.customConstructor(
38         {'remote': RemoteController}, 'remote,ip=%s:%s' % (controller_ip,
39                                                            controller_port))
40
41
42     net = mininet.net.Mininet(topo=topo, switch=switch, controller=controller)
43
44     return net
45
46
47 def get_flows(net):
48     """Get list of flows from network's first switch.
49
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.
53     Example:
54
55         {
56             'actions': 'drop',
57             'cookie': '0xa,',
58             'duration': '3.434s,',
59             'hard_timeout': '12,',
60             'idle_timeout': '34,',
61             'matches': {
62                 'ip': None,
63                 'nw_dst': '10.0.0.0/24'
64             },
65             'n_bytes': '0,',
66             'n_packets': '0,',
67             'priority': '2',
68             'table': '1,'
69         }
70
71     """
72     log = logging.getLogger(__name__)
73     def parse_matches(flow, matches):
74         flow['matches'] = {}
75
76         for match in matches:
77             split_match = match.split('=', 1)
78             if len(split_match) == 1:
79                 flow['matches'][split_match[0]] = None
80             else:
81                 flow['matches'][split_match[0]] = split_match[1].rstrip(',')
82
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)
88
89     log.debug('switch flow table: {}'.format(output))
90
91     flows = []
92
93     for line in output.splitlines()[1:]:
94         flow = {}
95         for word in line.split():
96             word.rstrip(',')
97             try:
98                 key, value = word.split('=', 1)
99             except ValueError:
100                 #TODO: need to figure out what to do here?
101                 continue
102
103             if key == 'priority':
104                 values = value.split(',')
105                 flow[key] = values[0]
106                 parse_matches(flow, values[1:])
107             else:
108                 flow[key] = value.rstrip(',')
109
110         flows.append(flow)
111
112     # sort by duration 
113     return sorted(flows, key=lambda x: x['duration'].rstrip('s'))
114
115
116 def translate_to_flow(flow, name, dictionary):
117     switch_flow_name = dictionary[name]
118
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
123
124
125 def get_text_value(element):
126     return element.childNodes[0].nodeValue
127
128
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:
132             continue
133       
134         comparator = comparators.get(child.nodeName, default)
135         comparator(child, actual_match, kw)
136
137
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
142
143     name = translate_to_flow(switch_flow, xml_element.nodeName, kw)
144
145     actual = switch_flow[name]
146     expected = xml_element.childNodes[0].nodeValue
147
148     data = xml_element.toxml(), name, actual
149     # print 'fallback_comparator: data', data
150
151     assert expected == actual, 'xml part: %s && switch %s=%s' % data
152
153
154 def default_comparator(xml_element, switch_flow):
155     fallback_comparator(xml_element, switch_flow, keywords)
156
157
158 def integer_comparator(expected, actual, kw, base):
159     expected_value = int(expected.childNodes[0].data)
160
161     name = kw.get(expected.nodeName)
162     actual_value = int(actual[name], base)
163
164     data = expected.toxml(), name, actual
165     assert expected_value == actual_value, \
166         'xml value: %s && actual value %s=%s' % data
167
168
169 def cookie_comparator(cookie, switch_flow):
170     integer_comparator(cookie, switch_flow, keywords, 16)
171
172
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)]
176
177     data = child.toxml(), kw.get(child.nodeName), actual_address
178
179     assert lower(expected_address) == lower(actual_address), \
180         'xml address: %s && actual address %s=%s' % data
181
182
183 def vlan_match_comparator(expected_match, actual_match, kw):
184
185     def compare_vlan_pcp(expected_match, actual_match, kw):
186         integer_comparator(expected_match, actual_match, kw, 10)
187
188     def compare_vlan_id(expected_match, actual_match, kw):
189         integer_comparator(expected_match.getElementsByTagName('vlan-id')[0], \
190                            actual_match, kw, 10)
191
192     VLAN_COMPARATORS = {
193         'vlan-pcp': compare_vlan_pcp, 
194         'vlan-id': compare_vlan_id,
195     }    
196
197     # print 'ethernet_match_comparator-expected_match:', expected_match.toxml()
198     # print 'ethernet_match_comparator-actual_match:', actual_match
199
200     compare_elements(expected_match, actual_match, kw, \
201                      VLAN_COMPARATORS, fallback_comparator)
202
203
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
209
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
216  
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
220
221         else:
222             actual_etype = int(actual_match[name], 16)
223
224             assert expected_etype == actual_etype, \
225                 'xml etype: %s && actual etype %s=%s' % data
226
227
228     ETH_COMPARATORS = {
229         'ethernet-type': compare_etype, 
230         'ethernet-source': ethernet_address_comparator,
231         'ethernet-destination': ethernet_address_comparator,
232     }    
233
234     # print 'ethernet_match_comparator-expected_match:', expected_match.toxml()
235     # print 'ethernet_match_comparator-actual_match:', actual_match
236
237     compare_elements(expected_match, actual_match, kw, \
238                      ETH_COMPARATORS, fallback_comparator)
239             
240
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
244
245     expected_value = expected_match.childNodes[0].data
246     actual_value = actual_match[kw.get(expected_match.nodeName)]
247
248     data = expected_match.toxml(), kw.get(expected_match.nodeName), actual_value
249
250     assert IPNetwork(expected_value) == IPNetwork(actual_value),\
251         'xml part: %s && address %s=%s' % data
252
253
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)
258
259         name = child.nodeName
260         data = expected_match.toxml(), name, actual_match
261
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
265
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
269
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
273
274         else:
275             fallback_comparator(child, actual_match, kw)
276
277
278     def compare_dscp(child, actual_match, kw):
279         # print 'compare_dscp:', child.toxml(), actual_match
280
281         expected_dscp = int(child.childNodes[0].data)
282         name = kw.get(child.nodeName)
283         actual_dscp = int(actual_match[name])
284         
285         data = child.toxml(), name, actual_match
286
287         assert (expected_dscp * 4) == actual_dscp, 'dscp: expected %s, actual %s=%s' % data
288
289
290     IP_MATCH_COMPARATORS = {
291         'ip-protocol': compare_proto, 
292         'ip-dscp': compare_dscp,
293     }    
294
295     # print 'ip_match_comparator:', expected_match.toxml(), actual_match
296     compare_elements(expected_match, actual_match, kw, \
297                      IP_MATCH_COMPARATORS, fallback_comparator)
298
299
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,
309     }
310
311     actual_match = switch_flow['matches']
312
313     # print 'match_comparator-expected_match:', expected_match.toxml()
314     # print 'match_comparator-actual_match:', actual_match
315     # print 'match_comparator: keywords', keywords
316
317     compare_elements(expected_match, actual_match, match_keywords, \
318                      MATCH_COMPARATORS, fallback_comparator)
319
320
321 def actions_comparator(actions, switch_flow):
322     # print 'actions_comparator:', actions, switch_flow
323
324     actual_actions = switch_flow['actions'].split(",")
325     # print 'actions_comparator:', actual_actions
326
327     for action in actions.childNodes:
328         if action.nodeType is actions.TEXT_NODE:
329             continue
330
331         action_name = action.childNodes[3].nodeName
332         expected_action = action_keywords.get(action_name)
333
334         data = action.toxml(), expected_action
335         # print 'actions_comparator:', data
336
337         assert expected_action in actual_actions, 'xml part:\n%s\n expected action: %s' % data
338
339
340 def null_comparator(element, switch_flow):
341     pass
342
343
344 def instructions_comparator(instructions_element, switch_flow):
345     INSTRUCTION_COMPARATORS = {
346         'apply-actions': actions_comparator,
347         'default': null_comparator,
348     }
349     # print 'instructions_comparator:', instructions_element, switch_flow
350
351     instructions = instructions_element.childNodes
352
353     for instruction in instructions_element.childNodes:
354         if instruction.nodeType is instructions_element.TEXT_NODE:
355             continue
356         
357         for itype in instruction.childNodes:
358             if itype.nodeType is itype.TEXT_NODE:
359                 continue
360
361             comparator = INSTRUCTION_COMPARATORS.get(itype.nodeName,
362                                                      INSTRUCTION_COMPARATORS['default'])
363             comparator(itype, switch_flow)
364
365
366 COMPARATORS = {
367     'cookie': cookie_comparator,
368     'instructions': instructions_comparator,
369     'match': match_comparator,
370     'default': default_comparator,
371 }
372
373 def all_nodes(xml_root):
374     """
375     Generates every non-text nodes.
376     """
377     current_nodes = [xml_root]
378     next_nodes = []
379
380     while len(current_nodes) > 0:
381         for node in current_nodes:
382             if node.nodeType != xml_root.TEXT_NODE:
383                 yield node
384                 next_nodes.extend(node.childNodes)
385
386         current_nodes, next_nodes = next_nodes, []
387
388
389 def check_elements(xmlstr, keywords):
390     # namespace = 'urn:opendaylight:flow:inventory'
391     tree = md.parseString(xmlstr)
392
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'
397             continue
398
399         yield element
400
401     raise StopIteration()
402
403
404 class TestOpenFlowXMLs(unittest.TestCase):
405     @classmethod
406     def setUpClass(cls):
407         cls.net = create_network(cls.host, cls.mn_port)
408         cls.net.start()
409         time.sleep(15)
410
411     @classmethod
412     def tearDownClass(cls):
413         cls.net.stop()
414
415
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
421     return result
422
423
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):
427         xml_string = ''
428         with open(path_to_xml) as f:
429             xml_string = f.read()
430
431         tree = md.parseString(xml_string)
432         ids = get_values(tree.documentElement, 'table_id', 'id')
433
434         def new_test(self):
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
440             headers = {
441                 'Content-Type': 'application/xml',
442                 'Accept': 'application/xml',
443             }
444             log.info('sending request to url: {}'.format(url))
445             rsp = requests.put(url, auth=('admin', 'admin'), data=xml_string,
446                                headers=headers)
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
451
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)
460
461             # collect flow table state on switch
462             switch_flows = get_flows(self.net)
463             assert len(switch_flows) > 0
464
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'])
470
471                 comparator(important_element, switch_flows[0])
472
473         return new_test
474
475     # generate list of available xml requests
476     xmlfiles = None
477     if xmls is not None:
478         xmlfiles = ('f%d.xml' % fid for fid in xmls)
479     else:
480         xmlfiles = (xml for xml in os.listdir(path) if xml.endswith('.xml'))
481
482     # define key getter for sorting
483     def get_test_number(test_name):
484         return int(test_name[1:-4])
485
486     for xmlfile in xmlfiles:
487         test_name = 'test_xml_%04d' % get_test_number(xmlfile)
488         setattr(TestOpenFlowXMLs,
489                 test_name,
490                 generate_test(os.path.join(path, xmlfile)))
491
492
493 if __name__ == '__main__':
494     # set up logging
495     logging.basicConfig(level=logging.DEBUG)
496
497     # parse cmdline arguments
498     parser = argparse.ArgumentParser(description='Run switch <-> ODL tests '
499                                      'defined by xmls.')
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()
509
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
514
515     keywords = None
516     with open('keywords.csv') as f:
517         keywords = dict(line.strip().split(';') for line in f
518                         if not line.startswith('#'))
519
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('#'))
524
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('#'))
529
530     # fix arguments for unittest
531     del sys.argv[1:]
532
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)
537     else:
538         generate_tests_from_xmls('xmls')
539
540     # run all tests
541     unittest.main()