Migrate Get Requests invocations(libraries)
[integration/test.git] / tools / odl-mdsal-clustering-tests / clustering-performance-test / flow_config_blaster.py
1 #!/usr/bin/python
2
3 from random import randrange
4 import json
5 import argparse
6 import time
7 import threading
8 import re
9 import copy
10
11 import requests
12 import netaddr
13
14
15 __author__ = "Jan Medved"
16 __copyright__ = "Copyright(c) 2014, Cisco Systems, Inc."
17 __license__ = "New-style BSD"
18 __email__ = "jmedved@cisco.com"
19
20
21 class Counter(object):
22     def __init__(self, start=0):
23         self.lock = threading.Lock()
24         self.value = start
25
26     def increment(self, value=1):
27         self.lock.acquire()
28         val = self.value
29         try:
30             self.value += value
31         finally:
32             self.lock.release()
33         return val
34
35
36 class Timer(object):
37     def __init__(self, verbose=False):
38         self.verbose = verbose
39
40     def __enter__(self):
41         self.start = time.time()
42         return self
43
44     def __exit__(self, *args):
45         self.end = time.time()
46         self.secs = self.end - self.start
47         self.msecs = self.secs * 1000  # millisecs
48         if self.verbose:
49             print("elapsed time: %f ms" % self.msecs)
50
51
52 class FlowConfigBlaster(object):
53     putheaders = {"content-type": "application/json"}
54     getheaders = {"Accept": "application/json"}
55
56     FLWURL = (
57         "restconf/config/opendaylight-inventory:nodes/node/openflow:%d/table/0/flow/%d"
58     )
59     TBLURL = "restconf/config/opendaylight-inventory:nodes/node/openflow:%d/table/0"
60     INVURL = "restconf/operational/opendaylight-inventory:nodes"
61     TIMEOUT = 10
62
63     flows = {}
64
65     # The "built-in" flow template
66     flow_mode_template = {
67         "flow": [
68             {
69                 "hard-timeout": 65000,
70                 "idle-timeout": 65000,
71                 "cookie_mask": 4294967295,
72                 "flow-name": "FLOW-NAME-TEMPLATE",
73                 "priority": 2,
74                 "strict": False,
75                 "cookie": 0,
76                 "table_id": 0,
77                 "installHw": False,
78                 "id": "FLOW-ID-TEMPLATE",
79                 "match": {
80                     "ipv4-destination": "0.0.0.0/32",
81                     "ethernet-match": {"ethernet-type": {"type": 2048}},
82                 },
83                 "instructions": {
84                     "instruction": [
85                         {
86                             "order": 0,
87                             "apply-actions": {
88                                 "action": [{"drop-action": {}, "order": 0}]
89                             },
90                         }
91                     ]
92                 },
93             }
94         ]
95     }
96
97     class FcbStats(object):
98         """
99         FlowConfigBlaster Statistics: a class that stores and further processes
100         statistics collected by Blaster worker threads during their execution.
101         """
102
103         def __init__(self):
104             self.ok_rqst_rate = Counter(0.0)
105             self.total_rqst_rate = Counter(0.0)
106             self.ok_flow_rate = Counter(0.0)
107             self.total_flow_rate = Counter(0.0)
108             self.ok_rqsts = Counter(0)
109             self.total_rqsts = Counter(0)
110             self.ok_flows = Counter(0)
111             self.total_flows = Counter(0)
112
113         def process_stats(self, rqst_stats, flow_stats, elapsed_time):
114             """
115             Calculates the stats for RESTCONF request and flow programming
116             throughput, and aggregates statistics across all Blaster threads.
117
118             Args:
119                 rqst_stats: Request statistics dictionary
120                 flow_stats: Flow statistcis dictionary
121                 elapsed_time: Elapsed time for the test
122
123             Returns: Rates (requests/sec) for successfully finished requests,
124                      the total number of requests, sucessfully installed flow and
125                      the total number of flows
126             """
127             ok_rqsts = rqst_stats[200] + rqst_stats[204]
128             total_rqsts = sum(rqst_stats.values())
129             ok_flows = flow_stats[200] + flow_stats[204]
130             total_flows = sum(flow_stats.values())
131
132             ok_rqst_rate = ok_rqsts / elapsed_time
133             total_rqst_rate = total_rqsts / elapsed_time
134             ok_flow_rate = ok_flows / elapsed_time
135             total_flow_rate = total_flows / elapsed_time
136
137             self.ok_rqsts.increment(ok_rqsts)
138             self.total_rqsts.increment(total_rqsts)
139             self.ok_flows.increment(ok_flows)
140             self.total_flows.increment(total_flows)
141
142             self.ok_rqst_rate.increment(ok_rqst_rate)
143             self.total_rqst_rate.increment(total_rqst_rate)
144             self.ok_flow_rate.increment(ok_flow_rate)
145             self.total_flow_rate.increment(total_flow_rate)
146
147             return ok_rqst_rate, total_rqst_rate, ok_flow_rate, total_flow_rate
148
149         def get_ok_rqst_rate(self):
150             return self.ok_rqst_rate.value
151
152         def get_total_rqst_rate(self):
153             return self.total_rqst_rate.value
154
155         def get_ok_flow_rate(self):
156             return self.ok_flow_rate.value
157
158         def get_total_flow_rate(self):
159             return self.total_flow_rate.value
160
161         def get_ok_rqsts(self):
162             return self.ok_rqsts.value
163
164         def get_total_rqsts(self):
165             return self.total_rqsts.value
166
167         def get_ok_flows(self):
168             return self.ok_flows.value
169
170         def get_total_flows(self):
171             return self.total_flows.value
172
173     def __init__(
174         self,
175         host,
176         port,
177         ncycles,
178         nthreads,
179         fpr,
180         nnodes,
181         nflows,
182         startflow,
183         auth,
184         flow_mod_template=None,
185     ):
186         self.host = host
187         self.port = port
188         self.ncycles = ncycles
189         self.nthreads = nthreads
190         self.fpr = fpr
191         self.nnodes = nnodes
192         self.nflows = nflows
193         self.startflow = startflow
194         self.auth = auth
195
196         if flow_mod_template:
197             self.flow_mode_template = flow_mod_template
198
199         self.post_url_template = "http://%s:" + self.port + "/" + self.TBLURL
200         self.del_url_template = "http://%s:" + self.port + "/" + self.FLWURL
201
202         self.stats = self.FcbStats()
203         self.total_ok_flows = 0
204         self.total_ok_rqsts = 0
205
206         self.ip_addr = Counter(int(netaddr.IPAddress("10.0.0.1")) + startflow)
207
208         self.print_lock = threading.Lock()
209         self.cond = threading.Condition()
210         self.threads_done = 0
211
212         for i in range(self.nthreads):
213             self.flows[i] = {}
214
215     def get_num_nodes(self, session):
216         """
217         Determines the number of OF nodes in the connected mininet network. If
218         mininet is not connected, the default number of flows is set to 16.
219         :param session: 'requests' session which to use to query the controller
220                         for openflow nodes
221         :return: None
222         """
223         hosts = self.host.split(",")
224         host = hosts[0]
225         inventory_url = "http://" + host + ":" + self.port + "/" + self.INVURL
226         nodes = self.nnodes
227
228         if not self.auth:
229             r = session.get(
230                 inventory_url,
231                 headers=self.getheaders,
232                 stream=False,
233                 timeout=self.TIMEOUT,
234             )
235         else:
236             r = session.get(
237                 inventory_url,
238                 headers=self.getheaders,
239                 stream=False,
240                 auth=("admin", "admin"),
241                 timeout=self.TIMEOUT,
242             )
243
244         if r.status_code == 200:
245             try:
246                 inv = json.loads(r.content)["nodes"]["node"]
247                 nn = 0
248                 for n in range(len(inv)):
249                     if re.search("openflow", inv[n]["id"]) is not None:
250                         nn += 1
251                 if nn != 0:
252                     nodes = nn
253             except KeyError:
254                 pass
255
256         return nodes
257
258     def create_flow_from_template(self, flow_id, ipaddr, node_id):
259         """
260         Create a new flow instance from the flow template specified during
261         FlowConfigBlaster instantiation. Flow templates are json-compatible
262         dictionaries that MUST contain elements for flow cookie, flow name,
263         flow id and the destination IPv4 address in the flow match field.
264
265         Args:
266             flow_id: Id for the new flow to create
267             ipaddr: IP Address to put into the flow's match
268             node_id: ID of the node where to create the flow
269
270         Returns: The flow that gas been created from the template
271
272         """
273         flow = copy.deepcopy(self.flow_mode_template["flow"][0])
274         flow["cookie"] = flow_id
275         flow["flow-name"] = self.create_flow_name(flow_id)
276         flow["id"] = str(flow_id)
277         flow["match"]["ipv4-destination"] = "%s/32" % str(netaddr.IPAddress(ipaddr))
278         return flow
279
280     def post_flows(self, session, node, flow_list, flow_count):
281         """
282         Performs a RESTCONF post of flows passed in the 'flow_list' parameters
283         :param session: 'requests' session on which to perform the POST
284         :param node: The ID of the openflow node to which to post the flows
285         :param flow_list: List of flows (in dictionary form) to POST
286         :param flow_count: Flow counter for round-robin host load balancing
287
288         :return: status code from the POST operation
289         """
290         flow_data = self.convert_to_json(flow_list, node)
291
292         hosts = self.host.split(",")
293         host = hosts[flow_count % len(hosts)]
294         flow_url = self.assemble_post_url(host, node)
295
296         if not self.auth:
297             r = session.post(
298                 flow_url,
299                 data=flow_data,
300                 headers=self.putheaders,
301                 stream=False,
302                 timeout=self.TIMEOUT,
303             )
304         else:
305             r = session.post(
306                 flow_url,
307                 data=flow_data,
308                 headers=self.putheaders,
309                 stream=False,
310                 auth=("admin", "admin"),
311                 timeout=self.TIMEOUT,
312             )
313
314         return r.status_code
315
316     def assemble_post_url(self, host, node):
317         """
318         Creates url pointing to config dataStore: /nodes/node/<node-id>/table/<table-id>
319         :param host: ip address or host name pointing to controller
320         :param node: id of node (without protocol prefix and colon)
321         :return: url suitable for sending a flow to controller via POST method
322         """
323         return self.post_url_template % (host, node)
324
325     def convert_to_json(self, flow_list, node_id=None):
326         """
327         Dumps flows to json form.
328         :param flow_list: list of flows in json friendly structure
329         :param node_id: node identifier of corresponding node
330         :return: string containing plain json
331         """
332         fmod = dict(self.flow_mode_template)
333         fmod["flow"] = flow_list
334         flow_data = json.dumps(fmod)
335         return flow_data
336
337     def add_flows(self, start_flow_id, tid):
338         """
339         Adds flows into the ODL config data store. This function is executed by
340         a worker thread (the Blaster thread). The number of flows created and
341         the batch size (i.e. how many flows will be put into a RESTCONF request)
342         are determined by control parameters initialized when FlowConfigBlaster
343         is created.
344         :param start_flow_id - the ID of the first flow. Each Blaster thread
345                                programs a different set of flows
346         :param tid: Thread ID - used to id the Blaster thread when statistics
347                                 for the thread are printed out
348         :return: None
349         """
350         rqst_stats = {200: 0, 204: 0}
351         flow_stats = {200: 0, 204: 0}
352
353         s = requests.Session()
354
355         n_nodes = self.get_num_nodes(s)
356
357         with self.print_lock:
358             print(
359                 "    Thread %d:\n        Adding %d flows on %d nodes"
360                 % (tid, self.nflows, n_nodes)
361             )
362
363         nflows = 0
364         nb_actions = []
365         while nflows < self.nflows:
366             node_id = randrange(1, n_nodes + 1)
367             flow_list = []
368             for i in range(self.fpr):
369                 flow_id = (
370                     tid * (self.ncycles * self.nflows)
371                     + nflows
372                     + start_flow_id
373                     + self.startflow
374                 )
375                 self.flows[tid][flow_id] = node_id
376                 flow_list.append(
377                     self.create_flow_from_template(
378                         flow_id, self.ip_addr.increment(), node_id
379                     )
380                 )
381                 nflows += 1
382                 if nflows >= self.nflows:
383                     break
384             nb_actions.append((s, node_id, flow_list, nflows))
385
386         with Timer() as t:
387             for nb_action in nb_actions:
388                 sts = self.post_flows(*nb_action)
389                 try:
390                     rqst_stats[sts] += 1
391                     flow_stats[sts] += len(nb_action[2])
392                 except KeyError:
393                     rqst_stats[sts] = 1
394                     flow_stats[sts] = len(nb_action[2])
395
396         ok_rps, total_rps, ok_fps, total_fps = self.stats.process_stats(
397             rqst_stats, flow_stats, t.secs
398         )
399
400         with self.print_lock:
401             print("\n    Thread %d results (ADD): " % tid)
402             print("        Elapsed time: %.2fs," % t.secs)
403             print("        Requests/s: %.2f OK, %.2f Total" % (ok_rps, total_rps))
404             print("        Flows/s:    %.2f OK, %.2f Total" % (ok_fps, total_fps))
405             print("        Stats ({Requests}, {Flows}): ")
406             print(rqst_stats)
407             print(flow_stats)
408             self.threads_done += 1
409
410         s.close()
411
412         with self.cond:
413             self.cond.notifyAll()
414
415     def delete_flow(self, session, node, flow_id, flow_count):
416         """
417         Deletes a single flow from the ODL config data store using RESTCONF
418         Args:
419             session: 'requests' session on which to perform the POST
420             node:  Id of the openflow node from which to delete the flow
421             flow_id: ID of the to-be-deleted flow
422             flow_count: Index of the flow being processed (for round-robin LB)
423
424         Returns: status code from the DELETE operation
425
426         """
427
428         hosts = self.host.split(",")
429         host = hosts[flow_count % len(hosts)]
430         flow_url = self.del_url_template % (host, node, flow_id)
431
432         if not self.auth:
433             r = session.delete(flow_url, headers=self.getheaders, timeout=self.TIMEOUT)
434         else:
435             r = session.delete(
436                 flow_url,
437                 headers=self.getheaders,
438                 auth=("admin", "admin"),
439                 timeout=self.TIMEOUT,
440             )
441
442         return r.status_code
443
444     def delete_flows(self, start_flow, tid):
445         """
446         Deletes flow from the ODL config space that have been added using the
447         'add_flows()' function. This function is executed by a worker thread
448         :param start_flow - the ID of the first flow. Each Blaster thread
449                                deletes a different set of flows
450         :param tid: Thread ID - used to id the Blaster thread when statistics
451                                 for the thread are printed out
452         :return:
453         """
454
455         rqst_stats = {200: 0, 204: 0}
456
457         s = requests.Session()
458         n_nodes = self.get_num_nodes(s)
459
460         with self.print_lock:
461             print(
462                 "Thread %d: Deleting %d flows on %d nodes" % (tid, self.nflows, n_nodes)
463             )
464
465         with Timer() as t:
466             for flow in range(self.nflows):
467                 flow_id = (
468                     tid * (self.ncycles * self.nflows)
469                     + flow
470                     + start_flow
471                     + self.startflow
472                 )
473                 sts = self.delete_flow(s, self.flows[tid][flow_id], flow_id, flow)
474                 try:
475                     rqst_stats[sts] += 1
476                 except KeyError:
477                     rqst_stats[sts] = 1
478
479         ok_rps, total_rps, ok_fps, total_fps = self.stats.process_stats(
480             rqst_stats, rqst_stats, t.secs
481         )
482
483         with self.print_lock:
484             print("\n    Thread %d results (DELETE): " % tid)
485             print("        Elapsed time: %.2fs," % t.secs)
486             print("        Requests/s:  %.2f OK,  %.2f Total" % (ok_rps, total_rps))
487             print("        Flows/s:     %.2f OK,  %.2f Total" % (ok_fps, total_fps))
488             print("        Stats ({Requests})")
489             print(rqst_stats)
490             self.threads_done += 1
491
492         s.close()
493
494         with self.cond:
495             self.cond.notifyAll()
496
497     def run_cycle(self, function):
498         """
499         Runs a flow-add or flow-delete test cycle. Each test consists of a
500         <cycles> test cycles, where <threads> worker (Blaster) threads are
501         started in each test cycle. Each Blaster thread programs <flows>
502         OpenFlow flows into the controller using the controller's RESTCONF API.
503         :param function: Add or delete, determines what test will be executed.
504         :return: None
505         """
506         self.total_ok_flows = 0
507         self.total_ok_rqsts = 0
508
509         for c in range(self.ncycles):
510             self.stats = self.FcbStats()
511             with self.print_lock:
512                 print("\nCycle %d:" % c)
513
514             threads = []
515             for i in range(self.nthreads):
516                 t = threading.Thread(target=function, args=(c * self.nflows, i))
517                 threads.append(t)
518                 t.start()
519
520             # Wait for all threads to finish and measure the execution time
521             with Timer() as t:
522                 for thread in threads:
523                     thread.join()
524
525             with self.print_lock:
526                 print("\n*** Test summary:")
527                 print("    Elapsed time:    %.2fs" % t.secs)
528                 print(
529                     "    Peak requests/s: %.2f OK, %.2f Total"
530                     % (self.stats.get_ok_rqst_rate(), self.stats.get_total_rqst_rate())
531                 )
532                 print(
533                     "    Peak flows/s:    %.2f OK, %.2f Total"
534                     % (self.stats.get_ok_flow_rate(), self.stats.get_total_flow_rate())
535                 )
536                 print(
537                     "    Avg. requests/s: %.2f OK, %.2f Total (%.2f%% of peak total)"
538                     % (
539                         self.stats.get_ok_rqsts() / t.secs,
540                         self.stats.get_total_rqsts() / t.secs,
541                         (self.stats.get_total_rqsts() / t.secs * 100)
542                         / self.stats.get_total_rqst_rate(),
543                     )
544                 )
545                 print(
546                     "    Avg. flows/s:    %.2f OK, %.2f Total (%.2f%% of peak total)"
547                     % (
548                         self.stats.get_ok_flows() / t.secs,
549                         self.stats.get_total_flows() / t.secs,
550                         (self.stats.get_total_flows() / t.secs * 100)
551                         / self.stats.get_total_flow_rate(),
552                     )
553                 )
554
555                 self.total_ok_flows += self.stats.get_ok_flows()
556                 self.total_ok_rqsts += self.stats.get_ok_rqsts()
557                 self.threads_done = 0
558
559     def add_blaster(self):
560         self.run_cycle(self.add_flows)
561
562     def delete_blaster(self):
563         self.run_cycle(self.delete_flows)
564
565     def get_ok_flows(self):
566         return self.total_ok_flows
567
568     def get_ok_rqsts(self):
569         return self.total_ok_rqsts
570
571     def create_flow_name(self, flow_id):
572         return "TestFlow-%d" % flow_id
573
574
575 def get_json_from_file(filename):
576     """
577     Get a flow programming template from a file
578     :param filename: File from which to get the template
579     :return: The json flow template (string)
580     """
581     with open(filename, "r") as f:
582         try:
583             ft = json.load(f)
584             keys = ft["flow"][0].keys()
585             if (
586                 ("cookie" in keys)
587                 and ("flow-name" in keys)
588                 and ("id" in keys)
589                 and ("match" in keys)
590             ):
591                 if "ipv4-destination" in ft["flow"][0]["match"].keys():
592                     print('File "%s" ok to use as flow template' % filename)
593                     return ft
594         except ValueError:
595             print("JSON parsing of file %s failed" % filename)
596             pass
597
598     return None
599
600
601 ###############################################################################
602 # This is an example of what the content of a JSON flow mode template should
603 # look like. Cut & paste to create a custom template. "id" and "ipv4-destination"
604 # MUST be unique if multiple flows will be programmed in the same test. It's
605 # also beneficial to have unique "cookie" and "flow-name" attributes for easier
606 # identification of the flow.
607 ###############################################################################
608 example_flow_mod_json = """{
609     "flow": [
610         {
611             "id": "38",
612             "cookie": 38,
613             "instructions": {
614                 "instruction": [
615                     {
616                         "order": 0,
617                         "apply-actions": {
618                             "action": [
619                                 {
620                                     "order": 0,
621                                     "drop-action": { }
622                                 }
623                             ]
624                         }
625                     }
626                 ]
627             },
628             "hard-timeout": 65000,
629             "match": {
630                 "ethernet-match": {
631                     "ethernet-type": {
632                         "type": 2048
633                     }
634                 },
635                 "ipv4-destination": "10.0.0.38/32"
636             },
637             "flow-name": "TestFlow-8",
638             "strict": false,
639             "cookie_mask": 4294967295,
640             "priority": 2,
641             "table_id": 0,
642             "idle-timeout": 65000,
643             "installHw": false
644         }
645
646     ]
647 }"""
648
649
650 def create_arguments_parser():
651     """
652     Shorthand to arg parser on library level in order to access and eventually enhance in ancestors.
653     :return: argument parser supporting config blaster arguments and parameters
654     """
655     my_parser = argparse.ArgumentParser(
656         description="Flow programming performance test: First adds and then"
657         " deletes flows into the config tree, as specified by"
658         " optional parameters."
659     )
660
661     my_parser.add_argument(
662         "--host",
663         default="127.0.0.1",
664         help="Host where odl controller is running (default is 127.0.0.1).  "
665         "Specify a comma-separated list of hosts to perform round-robin load-balancing.",
666     )
667     my_parser.add_argument(
668         "--port",
669         default="8181",
670         help="Port on which odl's RESTCONF is listening (default is 8181)",
671     )
672     my_parser.add_argument(
673         "--cycles",
674         type=int,
675         default=1,
676         help="Number of flow add/delete cycles; default 1. Both Flow Adds and Flow Deletes are "
677         "performed in cycles. <THREADS> worker threads are started in each cycle and the cycle "
678         "ends when all threads finish. Another cycle is started when the previous cycle "
679         "finished.",
680     )
681     my_parser.add_argument(
682         "--threads",
683         type=int,
684         default=1,
685         help="Number of request worker threads to start in each cycle; default=1. "
686         "Each thread will add/delete <FLOWS> flows.",
687     )
688     my_parser.add_argument(
689         "--flows",
690         type=int,
691         default=10,
692         help="Number of flows that will be added/deleted by each worker thread in each cycle; "
693         "default 10",
694     )
695     my_parser.add_argument(
696         "--fpr",
697         type=int,
698         default=1,
699         help="Flows-per-Request - number of flows (batch size) sent in each HTTP request; "
700         "default 1",
701     )
702     my_parser.add_argument(
703         "--nodes",
704         type=int,
705         default=16,
706         help="Number of nodes if mininet is not connected; default=16. If mininet is connected, "
707         "flows will be evenly distributed (programmed) into connected nodes.",
708     )
709     my_parser.add_argument(
710         "--delay",
711         type=int,
712         default=0,
713         help="Time (in seconds) to wait between the add and delete cycles; default=0",
714     )
715     my_parser.add_argument(
716         "--delete",
717         dest="delete",
718         action="store_true",
719         default=True,
720         help="Delete all added flows one by one, benchmark delete " "performance.",
721     )
722     my_parser.add_argument(
723         "--no-delete",
724         dest="delete",
725         action="store_false",
726         help="Do not perform the delete cycle.",
727     )
728     my_parser.add_argument(
729         "--auth",
730         dest="auth",
731         action="store_true",
732         default=False,
733         help="Use the ODL default username/password 'admin'/'admin' to authenticate access to REST; "
734         "default: no authentication",
735     )
736     my_parser.add_argument(
737         "--startflow", type=int, default=0, help="The starting Flow ID; default=0"
738     )
739     my_parser.add_argument(
740         "--file",
741         default="",
742         help="File from which to read the JSON flow template; default: no file, use a built in "
743         "template.",
744     )
745     return my_parser
746
747
748 if __name__ == "__main__":
749     ############################################################################
750     # This program executes the base performance test. The test adds flows into
751     # the controller's config space. This function is basically the CLI frontend
752     # to the FlowConfigBlaster class and drives its main functions: adding and
753     # deleting flows from the controller's config data store
754     ############################################################################
755
756     parser = create_arguments_parser()
757     in_args = parser.parse_args()
758
759     if in_args.file != "":
760         flow_template = get_json_from_file(in_args.file)
761     else:
762         flow_template = None
763
764     fct = FlowConfigBlaster(
765         in_args.host,
766         in_args.port,
767         in_args.cycles,
768         in_args.threads,
769         in_args.fpr,
770         in_args.nodes,
771         in_args.flows,
772         in_args.startflow,
773         in_args.auth,
774     )
775
776     # Run through <cycles>, where <threads> are started in each cycle and
777     # <flows> are added from each thread
778     fct.add_blaster()
779
780     print("\n*** Total flows added: %s" % fct.get_ok_flows())
781     print("    HTTP[OK] results:  %d\n" % fct.get_ok_rqsts())
782
783     if in_args.delay > 0:
784         print(
785             "*** Waiting for %d seconds before the delete cycle ***\n" % in_args.delay
786         )
787         time.sleep(in_args.delay)
788
789     # Run through <cycles>, where <threads> are started in each cycle and
790     # <flows> previously added in an add cycle are deleted in each thread
791     if in_args.delete:
792         fct.delete_blaster()
793         print("\n*** Total flows deleted: %s" % fct.get_ok_flows())
794         print("    HTTP[OK] results:    %d\n" % fct.get_ok_rqsts())