--- /dev/null
+#!/usr/bin/python
+__author__ = "Jan Medved"
+__copyright__ = "Copyright(c) 2014, Cisco Systems, Inc."
+__license__ = "New-style BSD"
+__email__ = "jmedved@cisco.com"
+
+from random import randrange
+import json
+import argparse
+import time
+import threading
+import sys
+import requests
+
+
+class Counter(object):
+ def __init__(self, start=0):
+ self.lock = threading.Lock()
+ self.value = start
+
+ def increment(self, value=1):
+ self.lock.acquire()
+ val = self.value
+ try:
+ self.value += value
+ finally:
+ self.lock.release()
+ return val
+
+
+class Timer(object):
+ def __init__(self, verbose=False):
+ self.verbose = verbose
+
+ def __enter__(self):
+ self.start = time.time()
+ return self
+
+ def __exit__(self, *args):
+ self.end = time.time()
+ self.secs = self.end - self.start
+ self.msecs = self.secs * 1000 # millisecs
+ if self.verbose:
+ print ("elapsed time: %f ms" % self.msecs)
+
+
+class ShardPerformanceTester(object):
+ """
+ The ShardPerformanceTester class facilitates performance testing of CDS shards. The test starts a number of
+ threads, where each thread issues a specified number of resource retrieval requests to URLs specified at the
+ beginning of a test. A ShardPerformanceTester object gets its connection parameters to the system-under-test
+ and its test parameters when it is instantiated. The set of URLs from where to retrieve resources is
+ specified when a test is started. By passing in the appropriate URLs, the test can be used to test data
+ retrieval performance of different shards or different resources at different granularities, etc.
+ """
+ headers = {'Accept': 'application/json'}
+
+ def __init__(self, host, port, auth, threads, nrequests, plevel):
+ """
+ """
+ self.host = host
+ self.port = port
+ self.auth = auth
+ self.requests = nrequests
+ self.threads = threads
+ self.plevel = plevel
+
+ self.print_lock = threading.Lock()
+ self.cond = threading.Condition()
+ self.threads_done = 0
+
+ self.ok_requests = 0
+ self.url_counters = []
+ self.total_rate = 0
+
+
+ def make_request(self, session, urls):
+ """
+ Makes a request for a resource at a random URL selected from a list of URLs passed as input parameter
+ :param session: Session to system under test
+ :param urls: List of resource URLs
+ :return: Status code from the resource request call
+ """
+ url_index = randrange(0, len(urls))
+ r_url = urls[url_index]
+ self.url_counters[url_index].increment()
+
+ if not self.auth:
+ r = session.get(r_url, headers=self.headers, stream=False)
+ else:
+ r = session.get(r_url, headers=self.headers, stream=False, auth=('admin', 'admin'))
+ return r.status_code
+
+
+ def worker(self, tid, urls):
+ """
+ Worker thread function. Connects to system-under-test and makes 'self.requests' requests for
+ resources to URLs randomly selected from 'urls'
+ :param tid: Worker thread ID
+ :param urls: List of resource URLs
+ :return: None
+ """
+ res = {200: 0}
+
+ s = requests.Session()
+
+ with self.print_lock:
+ print ' Thread %d: Performing %d requests' % (tid, self.requests)
+
+ with Timer() as t:
+ for r in range(self.requests):
+ sts = self.make_request(s, urls)
+ try:
+ res[sts] += 1
+ except KeyError:
+ res[sts] = 1
+
+ ok_rate = res[200] / t.secs
+ total_rate = sum(res.values()) / t.secs
+
+ with self.print_lock:
+ print 'Thread %d done:' % tid
+ print ' Time: %.2f,' % t.secs
+ print ' Success rate: %.2f, Total rate: %.2f' % (ok_rate, total_rate)
+ print ' Per-thread stats: ',
+ print res
+ self.threads_done += 1
+ self.total_rate += total_rate
+
+ s.close()
+
+ with self.cond:
+ self.cond.notifyAll()
+
+
+ def run_test(self, urls):
+ """
+ Runs the performance test. Starts 'self.threads' worker threads, waits for all of them to finish and
+ prints results.
+ :param urls: List of urls from which to request resources
+ :return: None
+ """
+
+ threads = []
+ self.total_rate = 0
+
+ # Initialize url counters
+ del self.url_counters[:]
+ for i in range(len(urls)):
+ self.url_counters.append(Counter(0))
+
+ # Start all worker threads
+ for i in range(self.threads):
+ t = threading.Thread(target=self.worker, args=(i, urls))
+ threads.append(t)
+ t.start()
+
+ # Wait for all threads to finish and measure the execution time
+ with Timer() as t:
+ while self.threads_done < self.threads:
+ with self.cond:
+ self.cond.wait()
+
+ # Print summary results. Each worker prints its owns results too.
+ print '\nSummary Results:'
+ print ' Requests/sec (total_sum): %.2f' % ((self.threads * self.requests) / t.secs)
+ print ' Requests/sec (measured): %.2f' % ((self.threads * self.requests) / t.secs)
+ print ' Time: %.2f' % t.secs
+ self.threads_done = 0
+
+ if self.plevel > 0:
+ print ' Per URL Counts: ',
+ for i in range(len(urls)):
+ print '%d' % self.url_counters[i].value,
+ print '\n'
+
+
+class TestUrlGenerator(object):
+ """
+ Base abstract class to generate test URLs for ShardPerformanceTester. First, an entire subtree representing
+ a shard or a set of resources is retrieved, then a set of URLS to access small data stanzas is created. This
+ class only defines the framework, the methods that create URL sets are defined in derived classes.
+ """
+
+ def __init__(self, host, port, auth):
+ """
+ Initialization
+ :param host: Controller's IP address
+ :param port: Controller's RESTCONF port
+ :param auth: Indicates whether to use authentication with default user/password (admin/admin)
+ :return: None
+ """
+ self.host = host
+ self.port = port
+ self.auth = auth
+ self.resource_string = ''
+
+ def url_generator(self, data):
+ """
+ Abstract URL generator. Must be overridden in a derived class
+ :param data: Bulk resource data (JSON) from which to generate the URLs
+ :return: List of generated Resources
+ """
+ print "Abstract class '%s' should never be used standalone" % self.__class__.__name__
+ return []
+
+ def generate(self):
+ """
+ Drives the generation of test URLs. First, it gets a 'bulk' resource (e.g. the entire inventory
+ or the entire topology) from the controller specified during int() and then invokes a resource-specific
+ URL generator to create a set of resource-specific URLs.
+ """
+ t_url = 'http://' + self.host + ":" + self.port + '/' + self.resource_string
+ headers = {'Accept': 'application/json'}
+ r_url = []
+
+ if not self.auth:
+ r = requests.get(t_url, headers=headers, stream=False)
+ else:
+ r = requests.get(t_url, headers=headers, stream=False, auth=('admin', 'admin'))
+
+ if r.status_code != 200:
+ print "Failed to get HTTP response from '%s', code %d" % (t_url, r.status_code)
+ else:
+ try:
+ r_url = self.url_generator(json.loads(r.content))
+ except:
+ print "Failed to get json from '%s'. Please make sure you are connected to mininet." % r_url
+
+ return r_url
+
+
+class TopoUrlGenerator(TestUrlGenerator):
+ """
+ Class to generate test URLs from the topology shard.
+ :return: List of generated Resources
+ """
+ def __init__(self, host, port, auth):
+ TestUrlGenerator.__init__(self, host, port, auth)
+ self.resource_string = 'restconf/operational/network-topology:network-topology/topology/flow:1'
+
+ def url_generator(self, topo_data):
+ url_list = []
+ try:
+ nodes = topo_data['topology'][0]['node']
+ for node in nodes:
+ tpoints = node['termination-point']
+ for tpoint in tpoints:
+ t_url = 'http://' + self.host + ":" + self.port + \
+ '/restconf/operational/network-topology:network-topology/topology/flow:1/node/' + \
+ node['node-id'] + '/termination-point/' + tpoint['tp-id']
+ url_list.append(t_url)
+ return url_list
+ except KeyError:
+ print 'Error parsing topology json'
+ return []
+
+
+class InvUrlGenerator(TestUrlGenerator):
+ """
+ Class to generate test URLs from the inventory shard.
+ """
+
+ def __init__(self, host, port, auth):
+ TestUrlGenerator.__init__(self, host, port, auth)
+ self.resource_string = 'restconf/operational/opendaylight-inventory:nodes'
+
+ def url_generator(self, inv_data):
+ url_list = []
+ try:
+ nodes = inv_data['nodes']['node']
+ for node in nodes:
+ nconns = node['node-connector']
+ for nconn in nconns:
+ i_url = 'http://' + self.host + ":" + self.port + \
+ '/restconf/operational/opendaylight-inventory:nodes/node/' + \
+ node['id'] + '/node-connector/' + nconn['id'] + \
+ '/opendaylight-port-statistics:flow-capable-node-connector-statistics'
+ url_list.append(i_url)
+ return url_list
+ except KeyError:
+ print 'Error parsing inventory json'
+ return []
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description='Flow programming performance test: First adds and then deletes flows '
+ 'into the config tree, as specified by optional parameters.')
+
+ parser.add_argument('--host', default='127.0.0.1',
+ help='Host where odl controller is running (default is 127.0.0.1)')
+ parser.add_argument('--port', default='8181',
+ help='Port on which odl\'s RESTCONF is listening (default is 8181)')
+ parser.add_argument('--auth', dest='auth', action='store_true', default=False,
+ help="Use the ODL default username/password 'admin'/'admin' to authenticate access to REST; "
+ 'default: no authentication')
+ parser.add_argument('--threads', type=int, default=1,
+ help='Number of request worker threads to start in each cycle; default=1. ')
+ parser.add_argument('--requests', type=int, default=100,
+ help='Number of requests each worker thread will send to the controller; default=100.')
+ parser.add_argument('--resource', choices=['inv', 'topo', 'topo+inv', 'all'], default='both',
+ help='Which resource to test: inventory, topology, or both; default both')
+ parser.add_argument('--plevel', type=int, default=0,
+ help='Print level: controls output verbosity. 0-lowest, 1-highest; default 0')
+ in_args = parser.parse_args()
+
+ topo_urls = []
+ inv_urls = []
+
+ # If required, get topology resource URLs
+ if in_args.resource != 'inventory':
+ tg = TopoUrlGenerator(in_args.host, in_args.port, in_args.auth)
+ topo_urls += tg.generate()
+ if len(topo_urls) == 0:
+ print 'Failed to generate topology URLs'
+ sys.exit(-1)
+
+ # If required, get inventory resource URLs
+ if in_args.resource != 'topology':
+ ig = InvUrlGenerator(in_args.host, in_args.port, in_args.auth)
+ inv_urls += ig.generate()
+ if len(inv_urls) == 0:
+ print 'Failed to generate inventory URLs'
+ sys.exit(-1)
+
+ if in_args.resource == 'topo+inv' or in_args.resource == 'all':
+ # To have balanced test results, the number of URLs for topology and inventory must be the same
+ if len(topo_urls) != len(inv_urls):
+ print "The number of topology and inventory URLs don't match"
+ sys.exit(-1)
+
+ st = ShardPerformanceTester(in_args.host, in_args.port, in_args.auth, in_args.threads, in_args.requests,
+ in_args.plevel)
+
+ if in_args.resource == 'all' or in_args.resource == 'topo':
+ print '==================================='
+ print 'Testing topology shard performance:'
+ print '==================================='
+ st.run_test(topo_urls)
+
+ if in_args.resource == 'all' or in_args.resource == 'inv':
+ print '===================================='
+ print 'Testing inventory shard performance:'
+ print '===================================='
+ st.run_test(inv_urls)
+
+ if in_args.resource == 'topo+inv' or in_args.resource == 'all':
+ print '==============================================='
+ print 'Testing combined shards (topo+inv) performance:'
+ print '==============================================='
+ st.run_test(topo_urls + inv_urls)
+
+
+
+