Change URI to specific ovsdb node
[integration/test.git] / tools / OF_Test / odl_tests.py.backup
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 fallback_comparator(xml_element, switch_flow, kw):
128     # print 'fallback_comparator-xml_element', xml_element.toxml()
129     # print 'fallback_comparator: switch_flow', switch_flow
130     # print 'fallback_comparator: kw', kws
131
132     name = translate_to_flow(switch_flow, xml_element.nodeName, kw)
133
134     actual = switch_flow[name]
135     expected = xml_element.childNodes[0].nodeValue
136
137     data = xml_element.toxml(), name, actual
138     # print 'fallback_comparator: data', data
139
140     assert expected == actual, 'xml part: %s && switch %s=%s' % data
141
142
143 def default_comparator(xml_element, switch_flow):
144     fallback_comparator(xml_element, switch_flow, keywords)
145
146
147 def cookie_comparator(cookie, switch_flow):
148     name = translate_to_flow(switch_flow, cookie.nodeName, keywords)
149
150     actual = int(switch_flow[name], 0)
151     expected = int(cookie.childNodes[0].nodeValue)
152     data = cookie.toxml(), name, actual
153
154     assert expected == actual, 'xml part: %s && switch %s=%s' % data
155
156
157 def ethernet_address_comparator(child, actual_match, kw):
158     expected_address = child.getElementsByTagName("address")[0].childNodes[0].data
159     actual_address = actual_match[kw.get(child.nodeName)]
160
161     data = child.toxml(), kw.get(child.nodeName), actual_address
162
163     assert lower(expected_address) == lower(actual_address), \
164         'xml address: %s && actual address %s=%s' % data
165
166
167 def ethernet_match_comparator(expected_match, actual_match, kw):
168     def compare_etype(child, actual_match, kw):
169         expected_etype = int(child.getElementsByTagName("type")[0].childNodes[0].data)
170         name = kw.get(child.nodeName)
171         data = child.toxml(), name, actual_match
172
173         if expected_etype == 2048: # IP
174             assert ((actual_match.get('ip', 'IP Not-present') is None) or \
175                     (actual_match.get('tcp', 'TCP Not-present') is None) or \
176                     (actual_match.get('sctp', 'SCTP Not-present') is None) or \
177                     (actual_match.get('udp', 'UDP Not-present') is None)), \
178                      'Expected etype %s && actual etype %s=%s' % data
179  
180         elif expected_etype == 2054: #ARP
181             assert actual_match.get('arp', 'ARP Not-present') is None, \
182                      'Expected etype %s && actual etype %s=%s' % data
183
184         else:
185             actual_etype = int(actual_match[name], 16)
186
187             assert expected_etype == actual_etype, 'xml etype: %s && actual etype %s=%s' % data
188
189
190     ETH_COMPARATORS = {
191         'ethernet-type': compare_etype, 
192         'ethernet-source': ethernet_address_comparator,
193         'ethernet-destination': ethernet_address_comparator,
194     }    
195
196     # print 'ethernet_match_comparator-expected_match:', expected_match.toxml()
197     # print 'ethernet_match_comparator-actual_match:', actual_match
198     # print 'ethernet_match_comparator-keywords:', keywords
199
200     for child in expected_match.childNodes:
201         if child.nodeType is expected_match.TEXT_NODE:
202             continue
203       
204         comparator = ETH_COMPARATORS.get(child.nodeName)
205         comparator(child, actual_match, kw)
206             
207
208 def ip_v4_comparator(expected_match, actual_match, kw):
209     # print 'ip_v4_comparator:', expected_match.toxml(), actual_match
210     # print 'ip_v4_comparator-actual_match:', actual_match
211
212     expected_value = expected_match.childNodes[0].data
213     actual_value = actual_match[kw.get(expected_match.nodeName)]
214
215     data = expected_match.toxml(), kw.get(expected_match.nodeName), actual_value
216
217     assert IPNetwork(expected_value) == IPNetwork(actual_value), 'xml part: %s && address %s=%s' % data
218
219
220 def ip_match_comparator(expected_match, actual_match, kw):
221     def compare_proto(child, actual_match, kw):
222         print 'compare_proto:', child.toxml(), actual_match
223         expected_proto = int(child.childNodes[0].data)
224
225         name = child.nodeName
226         data = expected_match.toxml(), name, actual_match
227
228         if expected_proto == 6: # TCP
229             assert actual_match.get('tcp', 'TCP Not-present') is None, \
230                    'ip protocol type: expected %s, actual %s=%s' % data
231
232         elif expected_proto == 17: #UDP
233             assert actual_match.get('udp', 'UDP Not-present') is None, \
234                    'ip protocol type: expected %s, actual %s=%s' % data
235
236         elif expected_proto == 132: #SCTP
237             assert actual_match.get('sctp', 'SCTP Not-present') is None, \
238                    'ip protocol type: expected %s, actual %s=%s' % data
239
240         else:
241             fallback_comparator(child, actual_match, kw)
242
243
244     def compare_dscp(child, actual_match, kw):
245         # print 'compare_dscp:', child.toxml(), actual_match
246
247         expected_dscp = int(child.childNodes[0].data)
248         name = kw.get(child.nodeName)
249         actual_dscp = int(actual_match[name])
250         
251         data = child.toxml(), name, actual_match
252
253         assert (expected_dscp * 4) == actual_dscp, 'dscp: expected %s, actual %s=%s' % data
254
255
256     IP_MATCH_COMPARATORS = {
257         'ip-protocol': compare_proto, 
258         'ip-dscp': compare_dscp,
259         'ip-ecn': fallback_comparator,
260     }    
261
262     # print 'ip_match_comparator:', expected_match.toxml(), actual_match
263
264     for child in expected_match.childNodes:
265         if child.nodeType is expected_match.TEXT_NODE:
266             continue
267       
268         comparator = IP_MATCH_COMPARATORS.get(child.nodeName)
269         comparator(child, actual_match, kw)
270
271
272 def match_comparator(expected_match, switch_flow):
273     MATCH_COMPARATORS = {
274         'arp-source-hardware-address': ethernet_address_comparator,
275         'arp-target-hardware-address': ethernet_address_comparator,
276         'ethernet-match': ethernet_match_comparator,
277         'ip-match': ip_match_comparator,
278         'ipv4-destination': ip_v4_comparator,
279         'ipv4-source': ip_v4_comparator,
280         'default': fallback_comparator,
281     }
282
283     actual_match = switch_flow['matches']
284
285     # print 'match_comparator-expected_match:', expected_match.toxml()
286     # print 'match_comparator-actual_match:', actual_match
287     # print 'match_comparator: keywords', keywords
288
289     for child in expected_match.childNodes:
290         if child.nodeType is expected_match.TEXT_NODE:
291             continue
292         
293         comparator = MATCH_COMPARATORS.get(child.nodeName,
294                                            MATCH_COMPARATORS['default'])
295         comparator(child, actual_match, match_keywords)
296
297
298 def actions_comparator(actions, switch_flow):
299     # print 'actions_comparator:', actions, switch_flow
300
301     actual_actions = switch_flow['actions'].split(",")
302     # print 'actions_comparator:', actual_actions
303
304     for action in actions.childNodes:
305         if action.nodeType is actions.TEXT_NODE:
306             continue
307
308         action_name = action.childNodes[3].nodeName
309         expected_action = action_keywords.get(action_name)
310
311         data = action.toxml(), expected_action
312         # print 'actions_comparator:', data
313
314         assert expected_action in actual_actions, 'xml part:\n%s\n expected action: %s' % data
315
316
317 def null_comparator(element, switch_flow):
318     pass
319
320
321 def instructions_comparator(instructions_element, switch_flow):
322     INSTRUCTION_COMPARATORS = {
323         'apply-actions': actions_comparator,
324         'default': null_comparator,
325     }
326     # print 'instructions_comparator:', instructions_element, switch_flow
327
328     instructions = instructions_element.childNodes
329
330     for instruction in instructions_element.childNodes:
331         if instruction.nodeType is instructions_element.TEXT_NODE:
332             continue
333         
334         for itype in instruction.childNodes:
335             if itype.nodeType is itype.TEXT_NODE:
336                 continue
337
338             comparator = INSTRUCTION_COMPARATORS.get(itype.nodeName,
339                                                      INSTRUCTION_COMPARATORS['default'])
340             comparator(itype, switch_flow)
341
342
343 COMPARATORS = {
344     'cookie': cookie_comparator,
345     'instructions': instructions_comparator,
346     'match': match_comparator,
347     'default': default_comparator,
348 }
349
350 def all_nodes(xml_root):
351     """
352     Generates every non-text nodes.
353     """
354     current_nodes = [xml_root]
355     next_nodes = []
356
357     while len(current_nodes) > 0:
358         for node in current_nodes:
359             if node.nodeType != xml_root.TEXT_NODE:
360                 yield node
361                 next_nodes.extend(node.childNodes)
362
363         current_nodes, next_nodes = next_nodes, []
364
365
366 def check_elements(xmlstr, keywords):
367     # namespace = 'urn:opendaylight:flow:inventory'
368     tree = md.parseString(xmlstr)
369
370     for element in all_nodes(tree.documentElement):
371         # switch flow object contains only some data from xml
372         if element.nodeName not in keywords:
373             # print 'check_elements: element.nodeName', element.nodeName, 'NOT in keywords'
374             continue
375
376         yield element
377
378     raise StopIteration()
379
380
381 class TestOpenFlowXMLs(unittest.TestCase):
382     @classmethod
383     def setUpClass(cls):
384         cls.net = create_network(cls.host, cls.mn_port)
385         cls.net.start()
386         time.sleep(15)
387
388     @classmethod
389     def tearDownClass(cls):
390         cls.net.stop()
391
392
393 def get_values(node, *tags):
394     result = {tag: None for tag in tags}
395     for node in all_nodes(node):
396         if node.nodeName in result and len(node.childNodes) > 0:
397             result[node.nodeName] = node.childNodes[0].nodeValue
398     return result
399
400
401 def generate_tests_from_xmls(path, xmls=None):
402     # generate test function from path to request xml
403     def generate_test(path_to_xml):
404         xml_string = ''
405         with open(path_to_xml) as f:
406             xml_string = f.read()
407
408         tree = md.parseString(xml_string)
409         ids = get_values(tree.documentElement, 'table_id', 'id')
410
411         def new_test(self):
412             log = logging.getLogger(__name__)
413             # send request throught RESTCONF
414             data = (self.host, self.port, ids['table_id'], ids['id'])
415             url = 'http://%s:%d/restconf/config/opendaylight-inventory:nodes' \
416                   '/node/openflow:1/table/%s/flow/%s' % data
417             headers = {
418                 'Content-Type': 'application/xml',
419                 'Accept': 'application/xml',
420             }
421             log.info('sending request to url: {}'.format(url))
422             rsp = requests.put(url, auth=('admin', 'admin'), data=xml_string,
423                                headers=headers)
424             log.info('received status code: {}'.format(rsp.status_code))
425             log.debug('received content: {}'.format(rsp.text))
426             assert rsp.status_code == 204 or rsp.status_code == 200, 'Status' \
427                     ' code returned %d' % rsp.status_code
428
429             # check request content against restconf's datastore
430             response = requests.get(url, auth=('admin', 'admin'),
431                                     headers={'Accept': 'application/xml'})
432             assert response.status_code == 200
433             req = ET.tostring(ET.fromstring(xml_string))
434             res = ET.tostring(ET.fromstring(response.text))
435             assert req == res, 'uploaded and stored xml, are not the same\n' \
436                 'uploaded: %s\nstored:%s' % (req, res)
437
438             # collect flow table state on switch
439             switch_flows = get_flows(self.net)
440             assert len(switch_flows) > 0
441
442             # compare requested object and flow table state
443             for important_element in check_elements(xml_string, keywords):
444                 # log.info('important element: {}'.format(important_element.nodeName))
445                 comparator = COMPARATORS.get(important_element.nodeName,
446                                              COMPARATORS['default'])
447
448                 comparator(important_element, switch_flows[0])
449
450         return new_test
451
452     # generate list of available xml requests
453     xmlfiles = None
454     if xmls is not None:
455         xmlfiles = ('f%d.xml' % fid for fid in xmls)
456     else:
457         xmlfiles = (xml for xml in os.listdir(path) if xml.endswith('.xml'))
458
459     # define key getter for sorting
460     def get_test_number(test_name):
461         return int(test_name[1:-4])
462
463     for xmlfile in xmlfiles:
464         test_name = 'test_xml_%04d' % get_test_number(xmlfile)
465         setattr(TestOpenFlowXMLs,
466                 test_name,
467                 generate_test(os.path.join(path, xmlfile)))
468
469
470 if __name__ == '__main__':
471     # set up logging
472     logging.basicConfig(level=logging.DEBUG)
473
474     # parse cmdline arguments
475     parser = argparse.ArgumentParser(description='Run switch <-> ODL tests '
476                                      'defined by xmls.')
477     parser.add_argument('--odlhost', default='127.0.0.1', help='host where '
478                         'odl controller is running')
479     parser.add_argument('--odlport', type=int, default=8080, help='port on '
480                         'which odl\'s RESTCONF is listening')
481     parser.add_argument('--mnport', type=int, default=6653, help='port on '
482                         'which odl\'s controller is listening')
483     parser.add_argument('--xmls', default=None, help='generete tests only '
484                         'from some xmls (i.e. 1,3,34) ')
485     args = parser.parse_args()
486
487     # set host and port of ODL controller for test cases
488     TestOpenFlowXMLs.port = args.odlport
489     TestOpenFlowXMLs.host = args.odlhost
490     TestOpenFlowXMLs.mn_port = args.mnport
491
492     keywords = None
493     with open('keywords.csv') as f:
494         keywords = dict(line.strip().split(';') for line in f
495                         if not line.startswith('#'))
496
497     match_keywords = None
498     with open('match-keywords.csv') as f:
499         match_keywords = dict(line.strip().split(';') for line in f
500                               if not line.startswith('#'))
501
502     action_keywords = None
503     with open('action-keywords.csv') as f:
504         action_keywords = dict(line.strip().split(';') for line in f
505                                     if not line.startswith('#'))
506
507     # fix arguments for unittest
508     del sys.argv[1:]
509
510     # generate tests for TestOpenFlowXMLs
511     if args.xmls is not None:
512         xmls = map(int, args.xmls.split(','))
513         generate_tests_from_xmls('xmls', xmls)
514     else:
515         generate_tests_from_xmls('xmls')
516
517     # run all tests
518     unittest.main()