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 = []
76 def make_request(self, session, urls):
78 Makes a request for a resource at a random URL selected from a list of URLs passed as input parameter
79 :param session: Session to system under test
80 :param urls: List of resource URLs
81 :return: Status code from the resource request call
83 url_index = randrange(0, len(urls))
84 r_url = urls[url_index]
85 self.url_counters[url_index].increment()
88 r = session.get(r_url, headers=self.headers, stream=False)
90 r = session.get(r_url, headers=self.headers, stream=False, auth=('admin', 'admin'))
93 def worker(self, tid, urls):
95 Worker thread function. Connects to system-under-test and makes 'self.requests' requests for
96 resources to URLs randomly selected from 'urls'
97 :param tid: Worker thread ID
98 :param urls: List of resource URLs
103 s = requests.Session()
105 with self.print_lock:
106 print ' Thread %d: Performing %d requests' % (tid, self.requests)
109 for r in range(self.requests):
110 sts = self.make_request(s, urls)
116 ok_rate = res[200] / t.secs
117 total_rate = sum(res.values()) / t.secs
119 with self.print_lock:
120 print 'Thread %d done:' % tid
121 print ' Time: %.2f,' % t.secs
122 print ' Success rate: %.2f, Total rate: %.2f' % (ok_rate, total_rate)
123 print ' Per-thread stats: ',
125 self.threads_done += 1
126 self.total_rate += total_rate
131 self.cond.notifyAll()
133 def run_test(self, urls):
135 Runs the performance test. Starts 'self.threads' worker threads, waits for all of them to finish and
137 :param urls: List of urls from which to request resources
144 # Initialize url counters
145 del self.url_counters[:]
146 for i in range(len(urls)):
147 self.url_counters.append(Counter(0))
149 # Start all worker threads
150 for i in range(self.threads):
151 t = threading.Thread(target=self.worker, args=(i, urls))
155 # Wait for all threads to finish and measure the execution time
157 while self.threads_done < self.threads:
161 # Print summary results. Each worker prints its owns results too.
162 print '\nSummary Results:'
163 print ' Requests/sec (total_sum): %.2f' % ((self.threads * self.requests) / t.secs)
164 print ' Requests/sec (measured): %.2f' % ((self.threads * self.requests) / t.secs)
165 print ' Time: %.2f' % t.secs
166 self.threads_done = 0
169 print ' Per URL Counts: ',
170 for i in range(len(urls)):
171 print '%d' % self.url_counters[i].value,
175 class TestUrlGenerator(object):
177 Base abstract class to generate test URLs for ShardPerformanceTester. First, an entire subtree representing
178 a shard or a set of resources is retrieved, then a set of URLS to access small data stanzas is created. This
179 class only defines the framework, the methods that create URL sets are defined in derived classes.
182 def __init__(self, host, port, auth):
185 :param host: Controller's IP address
186 :param port: Controller's RESTCONF port
187 :param auth: Indicates whether to use authentication with default user/password (admin/admin)
193 self.resource_string = ''
195 def url_generator(self, data):
197 Abstract URL generator. Must be overridden in a derived class
198 :param data: Bulk resource data (JSON) from which to generate the URLs
199 :return: List of generated Resources
201 print "Abstract class '%s' should never be used standalone" % self.__class__.__name__
206 Drives the generation of test URLs. First, it gets a 'bulk' resource (e.g. the entire inventory
207 or the entire topology) from the controller specified during int() and then invokes a resource-specific
208 URL generator to create a set of resource-specific URLs.
210 t_url = 'http://' + self.host + ":" + self.port + '/' + self.resource_string
211 headers = {'Accept': 'application/json'}
215 r = requests.get(t_url, headers=headers, stream=False)
217 r = requests.get(t_url, headers=headers, stream=False, auth=('admin', 'admin'))
219 if r.status_code != 200:
220 print "Failed to get HTTP response from '%s', code %d" % (t_url, r.status_code)
223 r_url = self.url_generator(json.loads(r.content))
225 print "Failed to get json from '%s'. Please make sure you are connected to mininet." % r_url
230 class TopoUrlGenerator(TestUrlGenerator):
232 Class to generate test URLs from the topology shard.
233 :return: List of generated Resources
235 def __init__(self, host, port, auth):
236 TestUrlGenerator.__init__(self, host, port, auth)
237 self.resource_string = 'restconf/operational/network-topology:network-topology/topology/flow:1'
239 def url_generator(self, topo_data):
242 nodes = topo_data['topology'][0]['node']
244 tpoints = node['termination-point']
245 for tpoint in tpoints:
246 t_url = 'http://' + self.host + ":" + self.port + \
247 '/restconf/operational/network-topology:network-topology/topology/flow:1/node/' + \
248 node['node-id'] + '/termination-point/' + tpoint['tp-id']
249 url_list.append(t_url)
252 print 'Error parsing topology json'
256 class InvUrlGenerator(TestUrlGenerator):
258 Class to generate test URLs from the inventory shard.
261 def __init__(self, host, port, auth):
262 TestUrlGenerator.__init__(self, host, port, auth)
263 self.resource_string = 'restconf/operational/opendaylight-inventory:nodes'
265 def url_generator(self, inv_data):
268 nodes = inv_data['nodes']['node']
270 nconns = node['node-connector']
272 i_url = 'http://' + self.host + ":" + self.port + \
273 '/restconf/operational/opendaylight-inventory:nodes/node/' + \
274 node['id'] + '/node-connector/' + nconn['id'] + \
275 '/opendaylight-port-statistics:flow-capable-node-connector-statistics'
276 url_list.append(i_url)
279 print 'Error parsing inventory json'
283 if __name__ == "__main__":
284 parser = argparse.ArgumentParser(description='Flow programming performance test: First adds and then deletes flows '
285 'into the config tree, as specified by optional parameters.')
287 parser.add_argument('--host', default='127.0.0.1',
288 help='Host where odl controller is running (default is 127.0.0.1)')
289 parser.add_argument('--port', default='8181',
290 help='Port on which odl\'s RESTCONF is listening (default is 8181)')
291 parser.add_argument('--auth', dest='auth', action='store_true', default=False,
292 help="Use the ODL default username/password 'admin'/'admin' to authenticate access to REST; "
293 'default: no authentication')
294 parser.add_argument('--threads', type=int, default=1,
295 help='Number of request worker threads to start in each cycle; default=1. ')
296 parser.add_argument('--requests', type=int, default=100,
297 help='Number of requests each worker thread will send to the controller; default=100.')
298 parser.add_argument('--resource', choices=['inv', 'topo', 'topo+inv', 'all'], default='both',
299 help='Which resource to test: inventory, topology, or both; default both')
300 parser.add_argument('--plevel', type=int, default=0,
301 help='Print level: controls output verbosity. 0-lowest, 1-highest; default 0')
302 in_args = parser.parse_args()
307 # If required, get topology resource URLs
308 if in_args.resource != 'inventory':
309 tg = TopoUrlGenerator(in_args.host, in_args.port, in_args.auth)
310 topo_urls += tg.generate()
311 if len(topo_urls) == 0:
312 print 'Failed to generate topology URLs'
315 # If required, get inventory resource URLs
316 if in_args.resource != 'topology':
317 ig = InvUrlGenerator(in_args.host, in_args.port, in_args.auth)
318 inv_urls += ig.generate()
319 if len(inv_urls) == 0:
320 print 'Failed to generate inventory URLs'
323 if in_args.resource == 'topo+inv' or in_args.resource == 'all':
324 # To have balanced test results, the number of URLs for topology and inventory must be the same
325 if len(topo_urls) != len(inv_urls):
326 print "The number of topology and inventory URLs don't match"
329 st = ShardPerformanceTester(in_args.host, in_args.port, in_args.auth, in_args.threads, in_args.requests,
332 if in_args.resource == 'all' or in_args.resource == 'topo':
333 print '==================================='
334 print 'Testing topology shard performance:'
335 print '==================================='
336 st.run_test(topo_urls)
338 if in_args.resource == 'all' or in_args.resource == 'inv':
339 print '===================================='
340 print 'Testing inventory shard performance:'
341 print '===================================='
342 st.run_test(inv_urls)
344 if in_args.resource == 'topo+inv' or in_args.resource == 'all':
345 print '==============================================='
346 print 'Testing combined shards (topo+inv) performance:'
347 print '==============================================='
348 st.run_test(topo_urls + inv_urls)