added stress test script
[openflowplugin.git] / test-scripts / stress_test.py
1 import unittest
2 import os
3 import re
4 import sys
5 import logging
6 import time
7 import argparse
8 import threading
9 import requests
10
11 from multiprocessing import Process
12 import xml.dom.minidom as md
13 from xml.etree import ElementTree as ET
14
15 from odl_tests_new import MininetTools, ParseTools
16
17 FLOW_ID_TEMPLATE = 'FLOW_ID_TEMPLATE'
18 COOKIE_TEMPLATE = 'COOKIE_TEMPLATE'
19 HARD_TO_TEMPLATE = 'HARD_TO_TEMPLATE'
20 FLOW_NAME_TEMPLATE = 'FLOW_NAME_TEMPLATE'
21 IPV4DST_TEMPLATE = 'IPV4DST_TEMPLATE'
22 PRIORITY_TEMPLATE = 'PRIORITY_TEMPLATE'
23
24 xml_template = '<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>' \
25 '<flow xmlns=\"urn:opendaylight:flow:inventory\">' \
26     '<strict>false</strict>' \
27     '<instructions><instruction><order>0</order><apply-actions><action><order>0</order><dec-nw-ttl/>' \
28     '</action></apply-actions></instruction></instructions><table_id>2</table_id>' \
29     '<id>'+FLOW_ID_TEMPLATE+'</id><cookie_mask>255</cookie_mask><installHw>false</installHw>' \
30     '<match><ethernet-match><ethernet-type><type>2048</type></ethernet-type></ethernet-match>' \
31     '<ipv4-destination>'+IPV4DST_TEMPLATE+'</ipv4-destination></match><hard-timeout>'+HARD_TO_TEMPLATE+'</hard-timeout>' \
32     '<flags>FlowModFlags [_cHECKOVERLAP=false, _rESETCOUNTS=false, _nOPKTCOUNTS=false, _nOBYTCOUNTS=false, _sENDFLOWREM=false]</flags>' \
33     '<cookie>'+COOKIE_TEMPLATE+'</cookie><idle-timeout>34</idle-timeout><flow-name>'+FLOW_NAME_TEMPLATE+'</flow-name><priority>'+PRIORITY_TEMPLATE+'</priority>' \
34     '<barrier>false</barrier></flow>'
35
36
37 class Tool():
38
39     @staticmethod
40     def get_flows_string(net=None):
41         if net is None:
42             return []
43
44         switch = net.switches[0]
45         output = switch.cmdPrint(
46         'ovs-ofctl -O OpenFlow13 dump-flows %s' % switch.name)
47
48         return output.splitlines()[1:]
49
50
51 class MultiTest(unittest.TestCase):
52
53     log = logging.getLogger('MultiTest')
54     total_errors = 0
55     total_flows = 0
56
57     def setUp(self):
58         MultiTest.log.info('setUp')
59         self.threads_count = 50
60         self.thread_pool = list()
61
62         self.__start_MN()
63         self.__setup_threads()
64         self.__run_threads()
65
66     def tearDown(self):
67         MultiTest.log.info('tearDown')
68         self.net.stop()
69
70     @staticmethod
71     def inc_error(value=1):
72         MultiTest.total_errors += value
73
74     @staticmethod
75     def inc_flow(value=1):
76         MultiTest.total_flows += 1
77
78     def __start_MN(self):
79         wait_time = 15
80
81         self.net = MininetTools.create_network(self.host, self.mn_port)
82         self.net.start()
83         MultiTest.log.info('mininet stared')
84         MultiTest.log.info('waiting {} seconds...'.format(wait_time))
85         time.sleep(wait_time)
86
87
88     def __setup_threads(self):
89         if args.threads is not None:
90             self.threads_count = int(args.threads)
91
92         for i in range(0, self.threads_count):
93             #thread will have predetermined flows id to avoid using shared resource
94             t = FlowAdderThread(i, self.host, self.port, self.net, flows_ids_from=i*MultiTest.flows + 1, flows_ids_to=(i+1)*MultiTest.flows + 1)
95
96             self.thread_pool.append(t)
97
98     def __run_threads(self):
99         # start threads
100         for t in self.thread_pool:
101             t.start()
102
103         # wait for them to finish
104         for t in self.thread_pool:
105             t.join()
106
107         # collect results
108         #for t in self.thread_pool:
109         #    MultiTest.inc_flow(t.flows)
110         #    MultiTest.inc_error(t.errors)
111
112     def test(self):
113
114         switch_flows = 0
115
116         assert MultiTest.total_flows > 0, 'Stored flows should be greater than 0, actual is {}'.format(MultiTest.total_flows)
117
118         MultiTest.log.info('\n\n---------- preparation finished, running test ----------')
119         # check config
120         url = 'http://%s:%d/restconf/config/opendaylight-inventory:nodes' \
121             '/node/openflow:1/table/2/' % (self.host, self.port)
122         MultiTest.log.info('checking flows in controller - sending request to url: {}'.format(url))
123         response = requests.get(url, auth=('admin', 'admin'),
124                                 headers={'Accept': 'application/xml'})
125         assert response.status_code == 200
126
127         tree = ET.ElementTree(ET.fromstring(response.text))
128         flows_on_controller = len(tree.getroot())
129
130         # check operational
131         url = 'http://%s:%d/restconf/operational/opendaylight-inventory:nodes' \
132             '/node/openflow:1/table/2/' % (self.host, self.port)
133         MultiTest.log.info('checking flows in controller - sending request to url: {}'.format(url))
134         response = requests.get(url, auth=('admin', 'admin'),
135                                 headers={'Accept': 'application/xml'})
136         #assert response.status_code == 200
137         MultiTest.log.info('got resposnse: {}'.format(response.status_code))
138         MultiTest.log.info('operational dump:\n{}'.format(response.text))
139
140         MultiTest.log.info('{} flows are stored by results from threads, {} errors'.format(MultiTest.total_flows, MultiTest.total_errors))
141         MultiTest.log.info('{} flows are stored in controller config'.format(flows_on_controller))
142
143         switch_flows_list = Tool.get_flows_string(self.net)
144         switch_flows = len(switch_flows_list)
145         MultiTest.log.info('{} flows are stored on switch'.format(switch_flows))
146         MultiTest.log.debug('switch flow-dump:\n{}'.format(switch_flows_list))
147
148
149         assert MultiTest.total_flows == switch_flows, 'Added amount of flows to switch should be equal to successfully added flows to controller {} <> {}'.format(switch_flows,MultiTest.total_flows)
150
151
152 class FlowAdderThread(threading.Thread):
153
154     def __init__(self, thread_id, host, port, net, flows_ids_from=0, flows_ids_to=1):
155         threading.Thread.__init__(self)
156         self.thread_id = thread_id
157         self.flows = 0
158         self.errors = 0
159         self.flows_ids_from = flows_ids_from
160         self.flows_ids_to = flows_ids_to
161
162         self.net = net
163         self.host = host
164         self.port = port
165
166         self.log = logging.getLogger('FlowAdderThread: ' + str(thread_id))
167         self.log.propagate = False
168         self.channel = logging.StreamHandler()
169         self.log.addHandler(self.channel)
170         formatter = logging.Formatter('THREAD {}: %(levelname)s: %(message)s'.format(self.thread_id))
171         self.channel.setFormatter(formatter)
172         #self.log.setLevel(logging.INFO)
173
174         self.log.info('created new FlowAdderThread->id:{}, flows id: {} -> {}'.format(self.thread_id, self.flows_ids_from, self.flows_ids_to))
175
176     def make_ipv4_address(self, number, octet_count=4, octet_size=255):
177         mask = 24
178         ip = ['10', '0', '0', '0']
179
180         if number < (255**3):
181             for o in range(1, octet_count):
182                 ip[octet_count - 1 - o] = str(number % octet_size)
183                 number = number / octet_size
184                 #mask -= 8
185                 if number == 0:
186                     break
187
188         return '.'.join(ip) + '/{}'.format(mask)
189
190     def __add_flows(self, act_flow_id):
191         try:
192             self.log.info('adding flow id: {}'.format(act_flow_id))
193             self.log.debug('flow ip address from id: {}'.format(self.make_ipv4_address(act_flow_id)))
194
195             xml_string = str(xml_template).replace(FLOW_ID_TEMPLATE, str(act_flow_id)).replace(COOKIE_TEMPLATE, str(act_flow_id))\
196             .replace(HARD_TO_TEMPLATE, '12').replace(FLOW_NAME_TEMPLATE,'FooXf{}'.format(act_flow_id))\
197             .replace(IPV4DST_TEMPLATE,self.make_ipv4_address(act_flow_id)).replace(PRIORITY_TEMPLATE,str(act_flow_id))
198
199             #TestRestartMininet.log.info('loaded xml: {}'.format(''.join(xml_string.split())))
200             tree = md.parseString(xml_string)
201             ids = ParseTools.get_values(tree.documentElement, 'table_id', 'id')
202             data = (self.host, self.port, ids['table_id'], ids['id'])
203
204             url = 'http://%s:%d/restconf/config/opendaylight-inventory:nodes' \
205                   '/node/openflow:1/table/%s/flow/%s' % data
206             # send request via RESTCONF
207             headers = {
208                 'Content-Type': 'application/xml',
209                 'Accept': 'application/xml',
210             }
211             self.log.debug('sending request to url: {}'.format(url))
212             rsp = requests.put(url, auth=('admin', 'admin'), data=xml_string,
213                                headers=headers)
214             self.log.debug('received status code: {}'.format(rsp.status_code))
215             self.log.debug('received content: {}'.format(rsp.text))
216             assert rsp.status_code == 204 or rsp.status_code == 200, 'Status' \
217                             ' code returned %d' % rsp.status_code
218
219             # check request content against restconf's datastore
220             #response = requests.get(url, auth=('admin', 'admin'),
221             #                        headers={'Accept': 'application/xml'})
222             #assert response.status_code == 200, 'Conifg response should be 200, is {}'.format(response.status_code)
223
224             #switch_flows = Tool.get_flows_string(self.net)
225             #assert len(switch_flows) > 0, 'Flows stored on switch shoul be greater than 0'
226
227             # we expect that controller doesn't fail to store flow on switch
228             self.flows += 1
229             MultiTest.inc_flow()
230             # store last used table id which got flows for later checkup
231             #self.log.debug('{} successfully stored flows - {} flows are on switch'.format(self.flows, len(switch_flows)))
232         except AssertionError as e:
233             self.errors += 1
234             self.log.error('AssertionError storing flow id:{}, reason: {}'.format(act_flow_id, str(e)))
235             MultiTest.inc_error()
236         except Exception as e:
237             self.errors += 1
238             self.log.error('Error storing flow id:{}, reason: {}'.format(act_flow_id, str(e)))
239             MultiTest.inc_error()
240
241     def run(self):
242         self.log.info('started... adding flows {} to {}'.format(self.flows_ids_from, self.flows_ids_to))
243         for i in range(self.flows_ids_from, self.flows_ids_to):
244             self.__add_flows(i)
245
246         self.log.info('finished, successfully added {} flows, {} errors'.format(self.flows,self.errors))
247
248
249 if __name__ == '__main__':
250     logging.basicConfig(level=logging.INFO)
251
252     # parse cmdline arguments
253     parser = argparse.ArgumentParser(description='Test for flow addition to'
254                         ' switch after the switch has been restarted')
255     parser.add_argument('--odlhost', default='127.0.0.1', help='host where '
256                         'odl controller is running')
257     parser.add_argument('--odlport', type=int, default=8080, help='port on '
258                         'which odl\'s RESTCONF is listening')
259     parser.add_argument('--mnport', type=int, default=6653, help='port on '
260                         'which odl\'s controller is listening')
261     parser.add_argument('--xmls', default=None, help='generete tests only '
262                         'from some xmls (i.e. 1,3,34) ')
263     parser.add_argument('--threads', default=50, help='how many threads '
264                         'should be used')
265     parser.add_argument('--flows', default=20, help='how many flows will add'
266                         ' one thread')
267     args = parser.parse_args()
268
269     # set host and port of ODL controller for test cases
270     MultiTest.port = args.odlport
271     MultiTest.host = args.odlhost
272     MultiTest.mn_port = args.mnport
273     MultiTest.threads = int(args.threads)
274     MultiTest.flows = int(args.flows)
275
276     del sys.argv[1:]
277     unittest.main()