2 __author__ = "Jan Medved"
3 __copyright__ = "Copyright(c) 2014, Cisco Systems, Inc."
4 __license__ = "New-style BSD"
5 __email__ = "jmedved@cisco.com"
7 from random import randrange
16 class Counter(object):
17 def __init__(self, start=0):
18 self.lock = threading.Lock()
21 def increment(self, value=1):
32 def __init__(self, verbose=False):
33 self.verbose = verbose
36 self.start = time.time()
39 def __exit__(self, *args):
40 self.end = time.time()
41 self.secs = self.end - self.start
42 self.msecs = self.secs * 1000 # millisecs
44 print ("elapsed time: %f ms" % self.msecs)
47 class ShardPerformanceTester(object):
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.
56 headers = {'Accept': 'application/json'}
58 def __init__(self, host, port, auth, threads, nrequests, plevel):
64 self.requests = nrequests
65 self.threads = threads
68 self.print_lock = threading.Lock()
69 self.cond = threading.Condition()
73 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'))
95 def worker(self, tid, urls):
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
105 s = requests.Session()
107 with self.print_lock:
108 print ' Thread %d: Performing %d requests' % (tid, self.requests)
111 for r in range(self.requests):
112 sts = self.make_request(s, urls)
118 ok_rate = res[200] / t.secs
119 total_rate = sum(res.values()) / t.secs
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: ',
127 self.threads_done += 1
128 self.total_rate += total_rate
133 self.cond.notifyAll()
136 def run_test(self, urls):
138 Runs the performance test. Starts 'self.threads' worker threads, waits for all of them to finish and
140 :param urls: List of urls from which to request resources
147 # Initialize url counters
148 del self.url_counters[:]
149 for i in range(len(urls)):
150 self.url_counters.append(Counter(0))
152 # Start all worker threads
153 for i in range(self.threads):
154 t = threading.Thread(target=self.worker, args=(i, urls))
158 # Wait for all threads to finish and measure the execution time
160 while self.threads_done < self.threads:
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
172 print ' Per URL Counts: ',
173 for i in range(len(urls)):
174 print '%d' % self.url_counters[i].value,
178 class TestUrlGenerator(object):
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.
185 def __init__(self, host, port, auth):
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)
196 self.resource_string = ''
198 def url_generator(self, data):
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
204 print "Abstract class '%s' should never be used standalone" % self.__class__.__name__
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.
213 t_url = 'http://' + self.host + ":" + self.port + '/' + self.resource_string
214 headers = {'Accept': 'application/json'}
218 r = requests.get(t_url, headers=headers, stream=False)
220 r = requests.get(t_url, headers=headers, stream=False, auth=('admin', 'admin'))
222 if r.status_code != 200:
223 print "Failed to get HTTP response from '%s', code %d" % (t_url, r.status_code)
226 r_url = self.url_generator(json.loads(r.content))
228 print "Failed to get json from '%s'. Please make sure you are connected to mininet." % r_url
233 class TopoUrlGenerator(TestUrlGenerator):
235 Class to generate test URLs from the topology shard.
236 :return: List of generated Resources
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'
242 def url_generator(self, topo_data):
245 nodes = topo_data['topology'][0]['node']
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)
255 print 'Error parsing topology json'
259 class InvUrlGenerator(TestUrlGenerator):
261 Class to generate test URLs from the inventory shard.
264 def __init__(self, host, port, auth):
265 TestUrlGenerator.__init__(self, host, port, auth)
266 self.resource_string = 'restconf/operational/opendaylight-inventory:nodes'
268 def url_generator(self, inv_data):
271 nodes = inv_data['nodes']['node']
273 nconns = node['node-connector']
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)
282 print 'Error parsing inventory json'
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.')
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()
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'
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'
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"
332 st = ShardPerformanceTester(in_args.host, in_args.port, in_args.auth, in_args.threads, in_args.requests,
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)
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)
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)