Step 1: Move vm scripts to the right place
[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
24 def create_network(controller_ip, controller_port):
25     """Create topology and mininet network."""
26     topo = mininet.topo.Topo()
27
28     topo.addSwitch('s1')
29     topo.addHost('h1')
30     topo.addHost('h2')
31
32     topo.addLink('h1', 's1')
33     topo.addLink('h2', 's1')
34
35     switch = mininet.util.customConstructor(
36         {'ovsk': OVSKernelSwitch}, 'ovsk,protocols=OpenFlow13')
37
38     controller = mininet.util.customConstructor(
39         {'remote': RemoteController}, 'remote,ip=%s:%s' % (controller_ip,
40                                                            controller_port))
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
74     def parse_matches(flow, matches):
75         flow['matches'] = {}
76
77         for match in matches:
78             split_match = match.split('=', 1)
79             if len(split_match) == 1:
80                 flow['matches'][split_match[0]] = None
81             else:
82                 flow['matches'][split_match[0]] = split_match[1].rstrip(',')
83
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)
89
90     log.debug('switch flow table: {}'.format(output))
91
92     flows = []
93
94     for line in output.splitlines()[1:]:
95         flow = {}
96         for word in line.split():
97             word.rstrip(',')
98             try:
99                 key, value = word.split('=', 1)
100             except ValueError:
101                 # TODO: need to figure out what to do here?
102                 continue
103
104             if key == 'priority':
105                 values = value.split(',')
106                 flow[key] = values[0]
107                 parse_matches(flow, values[1:])
108             else:
109                 flow[key] = value.rstrip(',')
110
111         flows.append(flow)
112
113     # sort by duration
114     return sorted(flows, key=lambda x: x['duration'].rstrip('s'))
115
116
117 def translate_to_flow(flow, name, dictionary):
118     switch_flow_name = dictionary[name]
119
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
124
125
126 def get_text_value(element):
127     return element.childNodes[0].nodeValue
128
129
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:
133             continue
134
135         comparator = comparators.get(child.nodeName, default)
136         comparator(child, actual_match, kw)
137
138
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
143
144     name = translate_to_flow(switch_flow, xml_element.nodeName, kw)
145
146     actual = switch_flow[name]
147     expected = xml_element.childNodes[0].nodeValue
148
149     data = xml_element.toxml(), name, actual
150     # print 'fallback_comparator: data', data
151
152     assert expected == actual, 'xml part: %s && switch %s=%s' % data
153
154
155 def default_comparator(xml_element, switch_flow):
156     fallback_comparator(xml_element, switch_flow, keywords)
157
158
159 def integer_comparator(expected, actual, kw, base):
160     expected_value = int(expected.childNodes[0].data)
161
162     name = kw.get(expected.nodeName)
163     actual_value = int(actual[name], base)
164
165     data = expected.toxml(), name, actual
166     assert expected_value == actual_value, \
167         'xml value: %s && actual value %s=%s' % data
168
169
170 def cookie_comparator(cookie, switch_flow):
171     integer_comparator(cookie, switch_flow, keywords, 16)
172
173
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)]
177
178     data = child.toxml(), kw.get(child.nodeName), actual_address
179
180     assert lower(expected_address) == lower(actual_address), \
181         'xml address: %s && actual address %s=%s' % data
182
183
184 def vlan_match_comparator(expected_match, actual_match, kw):
185
186     def compare_vlan_pcp(expected_match, actual_match, kw):
187         integer_comparator(expected_match, actual_match, kw, 10)
188
189     def compare_vlan_id(expected_match, actual_match, kw):
190         integer_comparator(expected_match.getElementsByTagName('vlan-id')[0],
191                            actual_match, kw, 10)
192
193     VLAN_COMPARATORS = {
194         'vlan-pcp': compare_vlan_pcp,
195         'vlan-id': compare_vlan_id,
196     }
197
198     # print 'ethernet_match_comparator-expected_match:', expected_match.toxml()
199     # print 'ethernet_match_comparator-actual_match:', actual_match
200
201     compare_elements(expected_match, actual_match, kw,
202                      VLAN_COMPARATORS, fallback_comparator)
203
204
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
210
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
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, 'xml etype: %s && actual etype %s=%s' % data
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, 'ip protocol type: expected %s, actual %s=%s' % data  # noqa
262
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
265
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
268
269         else:
270             fallback_comparator(child, actual_match, kw)
271
272     def compare_dscp(child, actual_match, kw):
273         # print 'compare_dscp:', child.toxml(), actual_match
274
275         expected_dscp = int(child.childNodes[0].data)
276         name = kw.get(child.nodeName)
277         actual_dscp = int(actual_match[name])
278
279         data = child.toxml(), name, actual_match
280
281         assert (expected_dscp * 4) == actual_dscp, 'dscp: expected %s, actual %s=%s' % data
282
283     IP_MATCH_COMPARATORS = {
284         'ip-protocol': compare_proto,
285         'ip-dscp': compare_dscp,
286     }
287
288     # print 'ip_match_comparator:', expected_match.toxml(), actual_match
289     compare_elements(expected_match, actual_match, kw,
290                      IP_MATCH_COMPARATORS, fallback_comparator)
291
292
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,
302     }
303
304     actual_match = switch_flow['matches']
305
306     # print 'match_comparator-expected_match:', expected_match.toxml()
307     # print 'match_comparator-actual_match:', actual_match
308     # print 'match_comparator: keywords', keywords
309
310     compare_elements(expected_match, actual_match, match_keywords,
311                      MATCH_COMPARATORS, fallback_comparator)
312
313
314 def actions_comparator(actions, switch_flow):
315     # print 'actions_comparator:', actions, switch_flow
316
317     actual_actions = switch_flow['actions'].split(",")
318     # print 'actions_comparator:', actual_actions
319
320     for action in actions.childNodes:
321         if action.nodeType is actions.TEXT_NODE:
322             continue
323
324         action_name = action.childNodes[3].nodeName
325         expected_action = action_keywords.get(action_name)
326
327         data = action.toxml(), expected_action
328         # print 'actions_comparator:', data
329
330         assert expected_action in actual_actions, 'xml part:\n%s\n expected action: %s' % data
331
332
333 def null_comparator(element, switch_flow):
334     pass
335
336
337 def instructions_comparator(instructions_element, switch_flow):
338     INSTRUCTION_COMPARATORS = {
339         'apply-actions': actions_comparator,
340         'default': null_comparator,
341     }
342     # print 'instructions_comparator:', instructions_element, switch_flow
343
344     instructions = instructions_element.childNodes  # noqa
345
346     for instruction in instructions_element.childNodes:
347         if instruction.nodeType is instructions_element.TEXT_NODE:
348             continue
349
350         for itype in instruction.childNodes:
351             if itype.nodeType is itype.TEXT_NODE:
352                 continue
353
354             comparator = INSTRUCTION_COMPARATORS.get(itype.nodeName,
355                                                      INSTRUCTION_COMPARATORS['default'])
356             comparator(itype, switch_flow)
357
358
359 COMPARATORS = {
360     'cookie': cookie_comparator,
361     'instructions': instructions_comparator,
362     'match': match_comparator,
363     'default': default_comparator,
364 }
365
366
367 def all_nodes(xml_root):
368     """
369     Generates every non-text nodes.
370     """
371     current_nodes = [xml_root]
372     next_nodes = []
373
374     while len(current_nodes) > 0:
375         for node in current_nodes:
376             if node.nodeType != xml_root.TEXT_NODE:
377                 yield node
378                 next_nodes.extend(node.childNodes)
379
380         current_nodes, next_nodes = next_nodes, []
381
382
383 def check_elements(xmlstr, keywords):
384     # namespace = 'urn:opendaylight:flow:inventory'
385     tree = md.parseString(xmlstr)
386
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'
391             continue
392
393         yield element
394
395     raise StopIteration()
396
397
398 class TestOpenFlowXMLs(unittest.TestCase):
399     @classmethod
400     def setUpClass(cls):
401         cls.net = create_network(cls.host, cls.mn_port)
402         cls.net.start()
403         time.sleep(15)
404
405     @classmethod
406     def tearDownClass(cls):
407         cls.net.stop()
408
409
410 def get_values(node, *tags):
411     result = dict((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
415     return result
416
417
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):
421         xml_string = ''
422         with open(path_to_xml) as f:
423             xml_string = f.read()
424
425         tree = md.parseString(xml_string)
426         ids = get_values(tree.documentElement, 'table_id', 'id')
427
428         def new_test(self):
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
434             headers = {
435                 'Content-Type': 'application/xml',
436                 'Accept': 'application/xml',
437             }
438             log.info('sending request to url: {}'.format(url))
439             rsp = requests.put(url, auth=('admin', 'admin'), data=xml_string,
440                                headers=headers)
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
445
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)
454
455             # collect flow table state on switch
456             switch_flows = get_flows(self.net)
457             assert len(switch_flows) > 0
458
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'])
464
465                 comparator(important_element, switch_flows[0])
466
467         return new_test
468
469     # generate list of available xml requests
470     xmlfiles = None
471     if xmls is not None:
472         xmlfiles = ('f%d.xml' % fid for fid in xmls)
473     else:
474         xmlfiles = (xml for xml in os.listdir(path) if xml.endswith('.xml'))
475
476     # define key getter for sorting
477     def get_test_number(test_name):
478         return int(test_name[1:-4])
479
480     for xmlfile in xmlfiles:
481         test_name = 'test_xml_%04d' % get_test_number(xmlfile)
482         setattr(TestOpenFlowXMLs,
483                 test_name,
484                 generate_test(os.path.join(path, xmlfile)))
485
486
487 if __name__ == '__main__':
488     # set up logging
489     logging.basicConfig(level=logging.DEBUG)
490
491     # parse cmdline arguments
492     parser = argparse.ArgumentParser(description='Run switch <-> ODL tests '
493                                      'defined by xmls.')
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()
503
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
508
509     keywords = None
510     with open('keywords.csv') as f:
511         keywords = dict(line.strip().split(';') for line in f
512                         if not line.startswith('#'))
513
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('#'))
518
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('#'))
523
524     # fix arguments for unittest
525     del sys.argv[1:]
526
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)
531     else:
532         generate_tests_from_xmls('xmls')
533
534     # run all tests
535     unittest.main()