2 from random import randrange
11 __author__ = "Jan Medved"
12 __copyright__ = "Copyright(c) 2014, Cisco Systems, Inc."
13 __license__ = "New-style BSD"
14 __email__ = "jmedved@cisco.com"
17 class Counter(object):
18 def __init__(self, start=0):
19 self.lock = threading.Lock()
22 def increment(self, value=1):
33 def __init__(self, verbose=False):
34 self.verbose = verbose
37 self.start = time.time()
40 def __exit__(self, *args):
41 self.end = time.time()
42 self.secs = self.end - self.start
43 self.msecs = self.secs * 1000 # millisecs
45 print("elapsed time: %f ms" % self.msecs)
48 class ShardPerformanceTester(object):
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.
57 headers = {'Accept': 'application/json'}
59 def __init__(self, host, port, auth, threads, nrequests, plevel):
65 self.requests = nrequests
66 self.threads = threads
69 self.print_lock = threading.Lock()
70 self.cond = threading.Condition()
74 self.url_counters = []
77 def make_request(self, session, urls):
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
84 url_index = randrange(0, len(urls))
85 r_url = urls[url_index]
86 self.url_counters[url_index].increment()
89 r = session.get(r_url, headers=self.headers, stream=False)
91 r = session.get(r_url, headers=self.headers, stream=False, auth=('admin', 'admin'))
94 def worker(self, tid, urls):
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
104 s = requests.Session()
106 with self.print_lock:
107 print(' Thread %d: Performing %d requests' % (tid, self.requests))
110 for r in range(self.requests):
111 sts = self.make_request(s, urls)
117 ok_rate = res[200] / t.secs
118 total_rate = sum(res.values()) / t.secs
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: ',)
126 self.threads_done += 1
127 self.total_rate += total_rate
132 self.cond.notifyAll()
134 def run_test(self, urls):
136 Runs the performance test. Starts 'self.threads' worker threads, waits for all of them to finish and
138 :param urls: List of urls from which to request resources
145 # Initialize url counters
146 del self.url_counters[:]
147 for i in range(len(urls)):
148 self.url_counters.append(Counter(0))
150 # Start all worker threads
151 for i in range(self.threads):
152 t = threading.Thread(target=self.worker, args=(i, urls))
156 # Wait for all threads to finish and measure the execution time
158 while self.threads_done < self.threads:
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
170 print(' Per URL Counts: ',)
171 for i in range(len(urls)):
172 print('%d' % self.url_counters[i].value)
176 class TestUrlGenerator(object):
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.
183 def __init__(self, host, port, auth):
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)
194 self.resource_string = ''
196 def url_generator(self, data):
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
202 print("Abstract class '%s' should never be used standalone" % (self.__class__.__name__))
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.
211 t_url = 'http://' + self.host + ":" + self.port + '/' + self.resource_string
212 headers = {'Accept': 'application/json'}
216 r = requests.get(t_url, headers=headers, stream=False)
218 r = requests.get(t_url, headers=headers, stream=False, auth=('admin', 'admin'))
220 if r.status_code != 200:
221 print("Failed to get HTTP response from '%s', code %d" % ((t_url, r.status_code)))
224 r_url = self.url_generator(json.loads(r.content))
226 print("Failed to get json from '%s'. Please make sure you are connected to mininet." % (r_url))
231 class TopoUrlGenerator(TestUrlGenerator):
233 Class to generate test URLs from the topology shard.
234 :return: List of generated Resources
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'
241 def url_generator(self, topo_data):
244 nodes = topo_data['topology'][0]['node']
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)
254 print('Error parsing topology json')
258 class InvUrlGenerator(TestUrlGenerator):
260 Class to generate test URLs from the inventory shard.
263 def __init__(self, host, port, auth):
264 TestUrlGenerator.__init__(self, host, port, auth)
265 self.resource_string = 'restconf/operational/opendaylight-inventory:nodes'
267 def url_generator(self, inv_data):
270 nodes = inv_data['nodes']['node']
272 nconns = node['node-connector']
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)
281 print('Error parsing inventory json')
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.')
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()
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')
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')
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")
331 st = ShardPerformanceTester(in_args.host, in_args.port, in_args.auth, in_args.threads, in_args.requests,
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)
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)
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)