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