Added shard performance tests (shard_perf_test.py and shard_multi_test.sh)
[integration/test.git] / test / tools / odl-mdsal-clustering-tests / clustering-performance-test / shard_perf_test.py
1 #!/usr/bin/python
2 __author__ = "Jan Medved"
3 __copyright__ = "Copyright(c) 2014, Cisco Systems, Inc."
4 __license__ = "New-style BSD"
5 __email__ = "jmedved@cisco.com"
6
7 from random import randrange
8 import json
9 import argparse
10 import time
11 import threading
12 import sys
13 import requests
14
15
16 class Counter(object):
17     def __init__(self, start=0):
18         self.lock = threading.Lock()
19         self.value = start
20
21     def increment(self, value=1):
22         self.lock.acquire()
23         val = self.value
24         try:
25             self.value += value
26         finally:
27             self.lock.release()
28         return val
29
30
31 class Timer(object):
32     def __init__(self, verbose=False):
33         self.verbose = verbose
34
35     def __enter__(self):
36         self.start = time.time()
37         return self
38
39     def __exit__(self, *args):
40         self.end = time.time()
41         self.secs = self.end - self.start
42         self.msecs = self.secs * 1000  # millisecs
43         if self.verbose:
44             print ("elapsed time: %f ms" % self.msecs)
45
46
47 class ShardPerformanceTester(object):
48     """
49     The ShardPerformanceTester class facilitates performance testing of CDS shards. The test starts a number of
50     threads, where each thread issues a specified number of resource retrieval requests to URLs specified at the
51     beginning of a test. A ShardPerformanceTester object gets its connection parameters to the system-under-test
52     and its test parameters when it is instantiated.  The set of URLs from where to retrieve resources is
53     specified when a test is started. By passing in the appropriate URLs, the test can be used to test data
54     retrieval performance of different shards or different resources at different granularities, etc.
55     """
56     headers = {'Accept': 'application/json'}
57
58     def __init__(self, host, port, auth, threads, nrequests, plevel):
59         """
60         """
61         self.host = host
62         self.port = port
63         self.auth = auth
64         self.requests = nrequests
65         self.threads = threads
66         self.plevel = plevel
67
68         self.print_lock = threading.Lock()
69         self.cond = threading.Condition()
70         self.threads_done = 0
71
72         self.ok_requests = 0
73         self.url_counters = []
74         self.total_rate = 0
75
76
77     def make_request(self, session, urls):
78         """
79         Makes a request for a resource at a random URL selected from a list of URLs passed as input parameter
80         :param session: Session to system under test
81         :param urls: List of resource URLs
82         :return: Status code from the resource request call
83         """
84         url_index = randrange(0, len(urls))
85         r_url = urls[url_index]
86         self.url_counters[url_index].increment()
87
88         if not self.auth:
89             r = session.get(r_url, headers=self.headers, stream=False)
90         else:
91             r = session.get(r_url, headers=self.headers, stream=False, auth=('admin', 'admin'))
92         return r.status_code
93
94
95     def worker(self, tid, urls):
96         """
97         Worker thread function. Connects to system-under-test and makes 'self.requests' requests for
98         resources to URLs randomly selected from 'urls'
99         :param tid: Worker thread ID
100         :param urls: List of resource URLs
101         :return: None
102         """
103         res = {200: 0}
104
105         s = requests.Session()
106
107         with self.print_lock:
108             print '    Thread %d: Performing %d requests' % (tid, self.requests)
109
110         with Timer() as t:
111             for r in range(self.requests):
112                 sts = self.make_request(s, urls)
113                 try:
114                     res[sts] += 1
115                 except KeyError:
116                     res[sts] = 1
117
118         ok_rate = res[200] / t.secs
119         total_rate = sum(res.values()) / t.secs
120
121         with self.print_lock:
122             print 'Thread %d done:' % tid
123             print '    Time: %.2f,' % t.secs
124             print '    Success rate:  %.2f, Total rate: %.2f' % (ok_rate, total_rate)
125             print '    Per-thread stats: ',
126             print res
127             self.threads_done += 1
128             self.total_rate += total_rate
129
130         s.close()
131
132         with self.cond:
133             self.cond.notifyAll()
134
135
136     def run_test(self, urls):
137         """
138         Runs the performance test. Starts 'self.threads' worker threads, waits for all of them to finish and
139         prints results.
140         :param urls: List of urls from which to request resources
141         :return: None
142         """
143
144         threads = []
145         self.total_rate = 0
146
147         # Initialize url counters
148         del self.url_counters[:]
149         for i in range(len(urls)):
150             self.url_counters.append(Counter(0))
151
152         # Start all worker threads
153         for i in range(self.threads):
154             t = threading.Thread(target=self.worker, args=(i, urls))
155             threads.append(t)
156             t.start()
157
158         # Wait for all threads to finish and measure the execution time
159         with Timer() as t:
160             while self.threads_done < self.threads:
161                 with self.cond:
162                     self.cond.wait()
163
164         # Print summary results. Each worker prints its owns results too.
165         print '\nSummary Results:'
166         print '    Requests/sec (total_sum): %.2f' % ((self.threads * self.requests) / t.secs)
167         print '    Requests/sec (measured):  %.2f' % ((self.threads * self.requests) / t.secs)
168         print '    Time: %.2f' % t.secs
169         self.threads_done = 0
170
171         if self.plevel > 0:
172             print '    Per URL Counts: ',
173             for i in range(len(urls)):
174                 print '%d' % self.url_counters[i].value,
175             print '\n'
176
177
178 class TestUrlGenerator(object):
179     """
180     Base abstract class to generate test URLs for ShardPerformanceTester. First, an entire subtree representing
181     a shard or a set of resources is retrieved, then a set of URLS to access small data stanzas is created. This
182     class only defines the framework, the methods that create URL sets are defined in derived classes.
183     """
184
185     def __init__(self, host, port, auth):
186         """
187         Initialization
188         :param host: Controller's IP address
189         :param port: Controller's RESTCONF port
190         :param auth: Indicates whether to use authentication with default user/password (admin/admin)
191         :return: None
192         """
193         self.host = host
194         self.port = port
195         self.auth = auth
196         self.resource_string = ''
197
198     def url_generator(self, data):
199         """
200         Abstract  URL generator. Must be overridden in a derived class
201         :param data: Bulk resource data (JSON) from which to generate the URLs
202         :return: List of generated Resources
203         """
204         print "Abstract class '%s' should never be used standalone" % self.__class__.__name__
205         return []
206
207     def generate(self):
208         """
209         Drives the generation of test URLs. First, it gets a 'bulk' resource (e.g. the entire inventory
210          or the entire topology) from the controller specified during int()  and then invokes a resource-specific
211          URL generator to create a set of resource-specific URLs.
212         """
213         t_url = 'http://' + self.host + ":" + self.port + '/' + self.resource_string
214         headers = {'Accept': 'application/json'}
215         r_url = []
216
217         if not self.auth:
218             r = requests.get(t_url, headers=headers, stream=False)
219         else:
220             r = requests.get(t_url, headers=headers, stream=False, auth=('admin', 'admin'))
221
222         if r.status_code != 200:
223             print "Failed to get HTTP response from '%s', code %d" % (t_url, r.status_code)
224         else:
225             try:
226                 r_url = self.url_generator(json.loads(r.content))
227             except:
228                 print "Failed to get json from '%s'. Please make sure you are connected to mininet." % r_url
229
230         return r_url
231
232
233 class TopoUrlGenerator(TestUrlGenerator):
234     """
235     Class to generate test URLs from the topology shard.
236     :return: List of generated Resources
237     """
238     def __init__(self, host, port, auth):
239         TestUrlGenerator.__init__(self, host, port, auth)
240         self.resource_string = 'restconf/operational/network-topology:network-topology/topology/flow:1'
241
242     def url_generator(self, topo_data):
243         url_list = []
244         try:
245             nodes = topo_data['topology'][0]['node']
246             for node in nodes:
247                 tpoints = node['termination-point']
248                 for tpoint in tpoints:
249                     t_url = 'http://' + self.host + ":" + self.port + \
250                             '/restconf/operational/network-topology:network-topology/topology/flow:1/node/' + \
251                             node['node-id'] + '/termination-point/' + tpoint['tp-id']
252                     url_list.append(t_url)
253             return url_list
254         except KeyError:
255             print 'Error parsing topology json'
256             return []
257
258
259 class InvUrlGenerator(TestUrlGenerator):
260     """
261     Class to generate test URLs from the inventory shard.
262     """
263
264     def __init__(self, host, port, auth):
265         TestUrlGenerator.__init__(self, host, port, auth)
266         self.resource_string = 'restconf/operational/opendaylight-inventory:nodes'
267
268     def url_generator(self, inv_data):
269         url_list = []
270         try:
271             nodes = inv_data['nodes']['node']
272             for node in nodes:
273                 nconns = node['node-connector']
274                 for nconn in nconns:
275                     i_url = 'http://' + self.host + ":" + self.port + \
276                             '/restconf/operational/opendaylight-inventory:nodes/node/' + \
277                             node['id'] + '/node-connector/' + nconn['id'] + \
278                             '/opendaylight-port-statistics:flow-capable-node-connector-statistics'
279                     url_list.append(i_url)
280             return url_list
281         except KeyError:
282             print 'Error parsing inventory json'
283             return []
284
285
286 if __name__ == "__main__":
287     parser = argparse.ArgumentParser(description='Flow programming performance test: First adds and then deletes flows '
288                                                  'into the config tree, as specified by optional parameters.')
289
290     parser.add_argument('--host', default='127.0.0.1',
291                         help='Host where odl controller is running (default is 127.0.0.1)')
292     parser.add_argument('--port', default='8181',
293                         help='Port on which odl\'s RESTCONF is listening (default is 8181)')
294     parser.add_argument('--auth', dest='auth', action='store_true', default=False,
295                         help="Use the ODL default username/password 'admin'/'admin' to authenticate access to REST; "
296                              'default: no authentication')
297     parser.add_argument('--threads', type=int, default=1,
298                         help='Number of request worker threads to start in each cycle; default=1. ')
299     parser.add_argument('--requests', type=int, default=100,
300                         help='Number of requests each worker thread will send to the controller; default=100.')
301     parser.add_argument('--resource', choices=['inv', 'topo', 'topo+inv', 'all'], default='both',
302                         help='Which resource to test: inventory, topology, or both; default both')
303     parser.add_argument('--plevel', type=int, default=0,
304                         help='Print level: controls output verbosity. 0-lowest, 1-highest; default 0')
305     in_args = parser.parse_args()
306
307     topo_urls = []
308     inv_urls = []
309
310     # If required, get topology resource URLs
311     if in_args.resource != 'inventory':
312         tg = TopoUrlGenerator(in_args.host, in_args.port, in_args.auth)
313         topo_urls += tg.generate()
314         if len(topo_urls) == 0:
315             print 'Failed to generate topology URLs'
316             sys.exit(-1)
317
318     # If required, get inventory resource URLs
319     if in_args.resource != 'topology':
320         ig = InvUrlGenerator(in_args.host, in_args.port, in_args.auth)
321         inv_urls += ig.generate()
322         if len(inv_urls) == 0:
323             print 'Failed to generate inventory URLs'
324             sys.exit(-1)
325
326     if in_args.resource == 'topo+inv' or in_args.resource == 'all':
327         # To have balanced test results, the number of URLs for topology and inventory must be the same
328         if len(topo_urls) != len(inv_urls):
329             print "The number of topology and inventory URLs don't match"
330             sys.exit(-1)
331
332     st = ShardPerformanceTester(in_args.host, in_args.port, in_args.auth, in_args.threads, in_args.requests,
333                                 in_args.plevel)
334
335     if in_args.resource == 'all' or in_args.resource == 'topo':
336         print '==================================='
337         print 'Testing topology shard performance:'
338         print '==================================='
339         st.run_test(topo_urls)
340
341     if in_args.resource == 'all' or in_args.resource == 'inv':
342         print '===================================='
343         print 'Testing inventory shard performance:'
344         print '===================================='
345         st.run_test(inv_urls)
346
347     if in_args.resource == 'topo+inv' or in_args.resource == 'all':
348         print '==============================================='
349         print 'Testing combined shards (topo+inv) performance:'
350         print '==============================================='
351         st.run_test(topo_urls + inv_urls)
352
353
354
355