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