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
18 class Counter(object):
19 def __init__(self, start=0):
20 self.lock = threading.Lock()
23 def increment(self, value=1):
34 def __init__(self, verbose=False):
35 self.verbose = verbose
38 self.start = time.time()
41 def __exit__(self, *args):
42 self.end = time.time()
43 self.secs = self.end - self.start
44 self.msecs = self.secs * 1000 # millisecs
46 print ("elapsed time: %f ms" % self.msecs)
49 class FlowConfigBlaster(object):
50 putheaders = {'content-type': 'application/json'}
51 getheaders = {'Accept': 'application/json'}
53 FLWURL = "restconf/config/opendaylight-inventory:nodes/node/openflow:%d/table/0/flow/%d"
54 INVURL = 'restconf/operational/opendaylight-inventory:nodes'
60 def __init__(self, host, port, ncycles, nthreads, nnodes, nflows, startflow, auth, json_template):
63 self.ncycles = ncycles
64 self.nthreads = nthreads
67 self.startflow = startflow
70 self.json_template = json_template
71 self.url_template = 'http://' + self.host + ":" + self.port + '/' + self.FLWURL
73 self.ok_rate = Counter(0.0)
74 self.total_rate = Counter(0.0)
76 self.ip_addr = Counter(int(netaddr.IPAddress('10.0.0.1')) + startflow)
78 self.print_lock = threading.Lock()
79 self.cond = threading.Condition()
82 for i in range(self.nthreads):
86 def get_num_nodes(self, session):
88 Determines the number of OF nodes in the connected mininet network. If mininet is not connected, the default
91 inventory_url = 'http://' + self.host + ":" + self.port + '/' + self.INVURL
95 r = session.get(inventory_url, headers=self.getheaders, stream=False)
97 r = session.get(inventory_url, headers=self.getheaders, stream=False, auth=('admin', 'admin'))
99 if r.status_code == 200:
101 inv = json.loads(r.content)['nodes']['node']
103 for n in range(len(inv)):
104 if re.search('openflow', inv[n]['id']) is not None:
114 def add_flow(self, session, node, flow_id, ipaddr):
116 Adds a single flow to the config data store via REST
118 flow_data = self.json_template % (flow_id, 'TestFlow-%d' % flow_id, 65000, str(flow_id), 65000,
119 str(netaddr.IPAddress(ipaddr)))
121 flow_url = self.url_template % (node, flow_id)
125 r = session.put(flow_url, data=flow_data, headers=self.putheaders, stream=False)
127 r = session.put(flow_url, data=flow_data, headers=self.putheaders, stream=False, auth=('admin', 'admin'))
132 def add_flows(self, start_flow, tid):
134 Adds flows into the ODL config space. This function is executed by a worker thread
139 s = requests.Session()
141 n_nodes = self.get_num_nodes(s)
143 with self.print_lock:
144 print ' Thread %d:\n Adding %d flows on %d nodes' % (tid, self.nflows, n_nodes)
147 for flow in range(self.nflows):
148 node_id = randrange(1, n_nodes + 1)
149 flow_id = tid * (self.ncycles * self.nflows) + flow + start_flow + self.startflow
150 self.flows[tid][flow_id] = node_id
151 sts = self.add_flow(s, node_id, flow_id, self.ip_addr.increment())
158 add_ok_rate = add_res[200] / add_time
159 add_total_rate = sum(add_res.values()) / add_time
161 self.ok_rate.increment(add_ok_rate)
162 self.total_rate.increment(add_total_rate)
164 with self.print_lock:
165 print ' Thread %d: ' % tid
166 print ' Add time: %.2f,' % add_time
167 print ' Add success rate: %.2f, Add total rate: %.2f' % (add_ok_rate, add_total_rate)
168 print ' Add Results: ',
170 self.ok_total += add_res[200]
171 self.threads_done += 1
176 self.cond.notifyAll()
179 def delete_flow(self, session, node, flow_id):
181 Deletes a single flow from the ODL config data store via REST
183 flow_url = self.url_template % (node, flow_id)
186 r = session.delete(flow_url, headers=self.getheaders)
188 r = session.delete(flow_url, headers=self.getheaders, auth=('admin', 'admin'))
193 def delete_flows(self, start_flow, tid):
195 Deletes flow from the ODL config space that have been added using the 'add_flows()' function. This function is
196 executed by a worker thread
200 s = requests.Session()
201 n_nodes = self.get_num_nodes(s)
203 with self.print_lock:
204 print 'Thread %d: Deleting %d flows on %d nodes' % (tid, self.nflows, n_nodes)
207 for flow in range(self.nflows):
208 flow_id = tid * (self.ncycles * self.nflows) + flow + start_flow + self.startflow
209 sts = self.delete_flow(s, self.flows[tid][flow_id], flow_id)
217 del_ok_rate = del_res[200] / del_time
218 del_total_rate = sum(del_res.values()) / del_time
220 self.ok_rate.increment(del_ok_rate)
221 self.total_rate.increment(del_total_rate)
223 with self.print_lock:
224 print ' Thread %d: ' % tid
225 print ' Delete time: %.2f,' % del_time
226 print ' Delete success rate: %.2f, Delete total rate: %.2f' % (del_ok_rate, del_total_rate)
227 print ' Delete Results: ',
229 self.threads_done += 1
234 self.cond.notifyAll()
237 def run_cycle(self, function):
239 Runs an add or delete cycle. Starts a number of worker threads that each add a bunch of flows. Work is done
240 in context of the worker threads.
243 for c in range(self.ncycles):
244 with self.print_lock:
245 print '\nCycle %d:' % c
248 for i in range(self.nthreads):
249 t = threading.Thread(target=function, args=(c * self.nflows, i))
253 # Wait for all threads to finish and measure the execution time
255 while self.threads_done < self.nthreads:
259 with self.print_lock:
260 print ' Total success rate: %.2f, Total rate: %.2f' % (
261 self.ok_rate.value, self.total_rate.value)
262 measured_rate = (self.nthreads * self.nflows) / t.secs
263 print ' Measured rate: %.2f (%.2f%% of Total success rate)' % \
264 (measured_rate, measured_rate / self.total_rate.value * 100)
265 print ' Measured time: %.2fs' % t.secs
266 self.threads_done = 0
268 self.ok_rate.value = 0
269 self.total_rate.value = 0
272 def add_blaster(self):
273 self.run_cycle(self.add_flows)
275 def delete_blaster(self):
276 self.run_cycle(self.delete_flows)
278 def get_total_flows(self):
279 return sum(len(self.flows[key]) for key in self.flows.keys())
281 def get_ok_flows(self):
285 def get_json_from_file(filename):
287 Get a flow programming template from a file
288 :param filename: File from which to get the template
289 :return: The json flow template (string)
291 with open(filename, 'r') as f:
296 if __name__ == "__main__":
297 JSON_FLOW_MOD1 = '''{
298 "flow-node-inventory:flow": [
300 "flow-node-inventory:cookie": %d,
301 "flow-node-inventory:cookie_mask": 4294967295,
302 "flow-node-inventory:flow-name": "%s",
303 "flow-node-inventory:hard-timeout": %d,
304 "flow-node-inventory:id": "%s",
305 "flow-node-inventory:idle-timeout": %d,
306 "flow-node-inventory:installHw": false,
307 "flow-node-inventory:instructions": {
308 "flow-node-inventory:instruction": [
310 "flow-node-inventory:apply-actions": {
311 "flow-node-inventory:action": [
313 "flow-node-inventory:drop-action": {},
314 "flow-node-inventory:order": 0
318 "flow-node-inventory:order": 0
322 "flow-node-inventory:match": {
323 "flow-node-inventory:ipv4-destination": "%s/32",
324 "flow-node-inventory:ethernet-match": {
325 "flow-node-inventory:ethernet-type": {
326 "flow-node-inventory:type": 2048
330 "flow-node-inventory:priority": 2,
331 "flow-node-inventory:strict": false,
332 "flow-node-inventory:table_id": 0
337 parser = argparse.ArgumentParser(description='Flow programming performance test: First adds and then deletes flows '
338 'into the config tree, as specified by optional parameters.')
340 parser.add_argument('--host', default='127.0.0.1',
341 help='Host where odl controller is running (default is 127.0.0.1)')
342 parser.add_argument('--port', default='8181',
343 help='Port on which odl\'s RESTCONF is listening (default is 8181)')
344 parser.add_argument('--cycles', type=int, default=1,
345 help='Number of flow add/delete cycles; default 1. Both Flow Adds and Flow Deletes are '
346 'performed in cycles. <THREADS> worker threads are started in each cycle and the cycle '
347 'ends when all threads finish. Another cycle is started when the previous cycle finished.')
348 parser.add_argument('--threads', type=int, default=1,
349 help='Number of request worker threads to start in each cycle; default=1. '
350 'Each thread will add/delete <FLOWS> flows.')
351 parser.add_argument('--flows', type=int, default=10,
352 help='Number of flows that will be added/deleted by each worker thread in each cycle; '
354 parser.add_argument('--nodes', type=int, default=16,
355 help='Number of nodes if mininet is not connected; default=16. If mininet is connected, '
356 'flows will be evenly distributed (programmed) into connected nodes.')
357 parser.add_argument('--delay', type=int, default=0,
358 help='Time (in seconds) to wait between the add and delete cycles; default=0')
359 parser.add_argument('--delete', dest='delete', action='store_true', default=True,
360 help='Delete all added flows one by one, benchmark delete '
362 parser.add_argument('--no-delete', dest='delete', action='store_false',
363 help='Do not perform the delete cycle.')
364 parser.add_argument('--auth', dest='auth', action='store_true', default=False,
365 help="Use the ODL default username/password 'admin'/'admin' to authenticate access to REST; "
366 'default: no authentication')
367 parser.add_argument('--startflow', type=int, default=0,
368 help='The starting Flow ID; default=0')
369 parser.add_argument('--file', default='',
370 help='File from which to read the JSON flow template; default: no file, use a built in '
373 in_args = parser.parse_args()
375 if in_args.file != '':
376 flow_template = get_json_from_file(in_args.file)
378 flow_template = JSON_FLOW_MOD1
380 fct = FlowConfigBlaster(in_args.host, in_args.port, in_args.cycles, in_args.threads, in_args.nodes,
381 in_args.flows, in_args.startflow, in_args.auth, flow_template)
383 # Run through <cycles>, where <threads> are started in each cycle and <flows> are added from each thread
386 print '\n*** Total flows added: %s' % fct.get_total_flows()
387 print ' HTTP[OK] results: %d\n' % fct.get_ok_flows()
389 if in_args.delay > 0:
390 print '*** Waiting for %d seconds before the delete cycle ***\n' % in_args.delay
391 time.sleep(in_args.delay)
393 # Run through <cycles>, where <threads> are started in each cycle and <flows> previously added in an add cycle are
394 # deleted in each thread