a640c1d80abd9020c9d9bd8aa285e3c61361805d
[csit.git] / resources / libraries / python / TrafficGenerator.py
1 # Copyright (c) 2016 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Performance testing traffic generator library."""
15
16 from robot.api import logger
17 from robot.libraries.BuiltIn import BuiltIn
18
19 from resources.libraries.python.constants import Constants
20 from resources.libraries.python.ssh import SSH
21 from resources.libraries.python.topology import NodeType
22 from resources.libraries.python.topology import NodeSubTypeTG
23 from resources.libraries.python.topology import Topology
24 from resources.libraries.python.DropRateSearch import DropRateSearch
25
26 __all__ = ['TrafficGenerator', 'TGDropRateSearchImpl']
27
28
29 class TGDropRateSearchImpl(DropRateSearch):
30     """Drop Rate Search implementation."""
31
32     def __init__(self):
33         super(TGDropRateSearchImpl, self).__init__()
34
35     def measure_loss(self, rate, frame_size, loss_acceptance,
36                      loss_acceptance_type, traffic_type):
37
38         # we need instance of TrafficGenerator instantiated by Robot Framework
39         # to be able to use trex_stl-*()
40         tg_instance = BuiltIn().get_library_instance(
41             'resources.libraries.python.TrafficGenerator')
42
43         if tg_instance._node['subtype'] is None:
44             raise Exception('TG subtype not defined')
45         elif tg_instance._node['subtype'] == NodeSubTypeTG.TREX:
46             unit_rate = str(rate) + self.get_rate_type_str()
47             tg_instance.trex_stl_start_remote_exec(self.get_duration(),
48                                                    unit_rate, frame_size,
49                                                    traffic_type)
50             # Get latency stats from stream
51             self._latency_stats = tg_instance.get_latency()
52
53             loss = tg_instance.get_loss()
54             sent = tg_instance.get_sent()
55             if self.loss_acceptance_type_is_percentage():
56                 loss = (float(loss) / float(sent)) * 100
57
58             # TODO: getters for tg_instance
59             logger.trace("comparing: {} < {} {}".format(loss,
60                                                         loss_acceptance,
61                                                         loss_acceptance_type))
62             if float(loss) > float(loss_acceptance):
63                 return False
64             else:
65                 return True
66         else:
67             raise NotImplementedError("TG subtype not supported")
68
69
70 class TrafficGenerator(object):
71     """Traffic Generator."""
72
73     # use one instance of TrafficGenerator for all tests in test suite
74     ROBOT_LIBRARY_SCOPE = 'TEST SUITE'
75
76     def __init__(self):
77         self._result = None
78         self._loss = None
79         self._sent = None
80         self._latency = None
81         self._received = None
82         self._node = None
83         # T-REX interface order mapping
84         self._ifaces_reordered = 0
85
86     def get_loss(self):
87         """Return number of lost packets.
88
89         :return: Number of lost packets.
90         :rtype: str
91         """
92         return self._loss
93
94     def get_sent(self):
95         """Return number of sent packets.
96
97         :return: Number of sent packets.
98         :rtype: str
99         """
100         return self._sent
101
102     def get_received(self):
103         """Return number of received packets.
104
105         :return: Number of received packets.
106         :rtype: str
107         """
108         return self._received
109
110     def get_latency(self):
111         """Return min/avg/max latency.
112
113         :return: Latency stats.
114         :rtype: list
115         """
116         return self._latency
117
118     #pylint: disable=too-many-arguments, too-many-locals
119     def initialize_traffic_generator(self, tg_node, tg_if1, tg_if2,
120                                      tg_if1_adj_node, tg_if1_adj_if,
121                                      tg_if2_adj_node, tg_if2_adj_if,
122                                      test_type):
123         """TG initialization.
124
125         :param tg_node: Traffic generator node.
126         :param tg_if1: TG - name of first interface.
127         :param tg_if2: TG - name of second interface.
128         :param tg_if1_adj_node: TG if1 adjecent node.
129         :param tg_if1_adj_if: TG if1 adjecent interface.
130         :param tg_if2_adj_node: TG if2 adjecent node.
131         :param tg_if2_adj_if: TG if2 adjecent interface.
132         :test_type: 'L2' or 'L3' - src/dst MAC address.
133         :type tg_node: dict
134         :type tg_if1: str
135         :type tg_if2: str
136         :type tg_if1_adj_node: dict
137         :type tg_if1_adj_if: str
138         :type tg_if2_adj_node: dict
139         :type tg_if2_adj_if: str
140         :type test_type: str
141         :return: nothing
142         """
143
144         topo = Topology()
145
146         if tg_node['type'] != NodeType.TG:
147             raise Exception('Node type is not a TG')
148         self._node = tg_node
149
150         if tg_node['subtype'] == NodeSubTypeTG.TREX:
151             trex_path = "/opt/trex-core-2.06"
152
153             ssh = SSH()
154             ssh.connect(tg_node)
155
156             (ret, stdout, stderr) = ssh.exec_command(
157                 "sudo sh -c '{}/resources/tools/t-rex/"
158                 "t-rex-installer.sh'".format(Constants.REMOTE_FW_DIR),
159                 timeout=1800)
160             if int(ret) != 0:
161                 logger.error('trex installation failed: {0}'.format(
162                     stdout + stderr))
163                 raise RuntimeError('Installation of TG failed')
164
165             if1_pci = topo.get_interface_pci_addr(tg_node, tg_if1)
166             if2_pci = topo.get_interface_pci_addr(tg_node, tg_if2)
167             if1_mac = topo.get_interface_mac(tg_node, tg_if1)
168             if2_mac = topo.get_interface_mac(tg_node, tg_if2)
169
170             if test_type == 'L2':
171                 if1_adj_mac = if2_mac
172                 if2_adj_mac = if1_mac
173             elif test_type == 'L3':
174                 if1_adj_mac = topo.get_interface_mac(tg_if1_adj_node,
175                                                      tg_if1_adj_if)
176                 if2_adj_mac = topo.get_interface_mac(tg_if2_adj_node,
177                                                      tg_if2_adj_if)
178             else:
179                 raise Exception("test_type unknown")
180
181             if min(if1_pci, if2_pci) != if1_pci:
182                 if1_mac, if2_mac = if2_mac, if1_mac
183                 if1_pci, if2_pci = if2_pci, if1_pci
184                 if1_adj_mac, if2_adj_mac = if2_adj_mac, if1_adj_mac
185                 self._ifaces_reordered = 1
186
187             if1_mac_hex = "0x"+if1_mac.replace(":", ",0x")
188             if2_mac_hex = "0x"+if2_mac.replace(":", ",0x")
189             if1_adj_mac_hex = "0x"+if1_adj_mac.replace(":", ",0x")
190             if2_adj_mac_hex = "0x"+if2_adj_mac.replace(":", ",0x")
191
192             (ret, stdout, stderr) = ssh.exec_command(
193                 "sudo sh -c 'cat << EOF > /etc/trex_cfg.yaml\n"
194                 "- port_limit      : 2\n"
195                 "  version         : 2\n"
196                 "  interfaces      : [\"{}\",\"{}\"]\n"
197                 "  port_info       :\n"
198                 "          - dest_mac        :   [{}]\n"
199                 "            src_mac         :   [{}]\n"
200                 "          - dest_mac        :   [{}]\n"
201                 "            src_mac         :   [{}]\n"
202                 "EOF'"\
203                 .format(if1_pci, if2_pci,
204                         if1_adj_mac_hex, if1_mac_hex,
205                         if2_adj_mac_hex, if2_mac_hex))
206             if int(ret) != 0:
207                 logger.error("failed to create t-rex config: {}"\
208                 .format(stdout + stderr))
209                 raise RuntimeError('trex config generation error')
210
211             (ret, stdout, stderr) = ssh.exec_command(
212                 "sh -c 'cd {0}/scripts/ && sudo ./trex-cfg'".format(trex_path))
213             if int(ret) != 0:
214                 logger.error('trex-cfg failed: {0}'.format(stdout + stderr))
215                 raise RuntimeError('trex-cfg failed')
216
217             max_startup_retries = 3
218             while max_startup_retries > 0:
219                 # kill T-rex only if it is already running
220                 (ret, _, _) = ssh.exec_command(
221                     "sh -c 'pgrep t-rex && sudo pkill t-rex'")
222
223                 # start T-rex
224                 (ret, _, _) = ssh.exec_command(
225                     "sh -c 'cd {0}/scripts/ && "
226                     "sudo nohup ./t-rex-64 -i -c 7 --iom 0 > /dev/null 2>&1 &'"
227                     "> /dev/null"\
228                     .format(trex_path))
229                 if int(ret) != 0:
230                     raise RuntimeError('t-rex-64 startup failed')
231
232                 # get T-rex server info
233                 (ret, _, _) = ssh.exec_command(
234                     "sh -c '{0}/resources/tools/t-rex/t-rex-server-info.py'"\
235                     .format(Constants.REMOTE_FW_DIR),
236                     timeout=120)
237                 if int(ret) == 0:
238                     # If we get info T-rex is running
239                     return
240                 # try again
241                 max_startup_retries -= 1
242             # after max retries T-rex is still not responding to API
243             # critical error occured
244             raise RuntimeError('t-rex-64 startup failed')
245
246
247     @staticmethod
248     def teardown_traffic_generator(node):
249         """TG teardown.
250
251         :param node: Traffic generator node.
252         :type node: dict
253         :return: nothing
254         """
255         if node['type'] != NodeType.TG:
256             raise Exception('Node type is not a TG')
257         if node['subtype'] == NodeSubTypeTG.TREX:
258             ssh = SSH()
259             ssh.connect(node)
260             (ret, stdout, stderr) = ssh.exec_command(
261                 "sh -c 'sudo pkill t-rex'")
262             if int(ret) != 0:
263                 logger.error('pkill t-rex failed: {0}'.format(stdout + stderr))
264                 raise RuntimeError('pkill t-rex failed')
265
266     @staticmethod
267     def trex_stl_stop_remote_exec(node):
268         """Execute script on remote node over ssh to stop running traffic.
269
270         :param node: T-REX generator node.
271         :type node: dict
272         :return: Nothing
273         """
274         ssh = SSH()
275         ssh.connect(node)
276
277         (ret, stdout, stderr) = ssh.exec_command(
278             "sh -c '{}/resources/tools/t-rex/"
279             "t-rex-stateless-stop.py'".format(Constants.REMOTE_FW_DIR))
280         logger.trace(ret)
281         logger.trace(stdout)
282         logger.trace(stderr)
283
284         if int(ret) != 0:
285             raise RuntimeError('T-rex stateless runtime error')
286
287     def trex_stl_start_remote_exec(self, duration, rate, framesize,
288                                    traffic_type, async_call=False,
289                                    latency=True, warmup_time=5):
290         """Execute script on remote node over ssh to start traffic.
291
292         :param duration: Time expresed in seconds for how long to send traffic.
293         :param rate: Traffic rate expressed with units (pps, %)
294         :param framesize: L2 frame size to send (without padding and IPG).
295         :param traffic_type: Traffic profile.
296         :param async_call: If enabled then don't wait for all incomming trafic.
297         :param latency: With latency measurement.
298         :param warmup_time: Warmup time period.
299         :type duration: int
300         :type rate: str
301         :type framesize: int
302         :type traffic_type: str
303         :type async_call: bool
304         :type latency: bool
305         :type warmup_time: int
306         :return: Nothing
307         """
308         ssh = SSH()
309         ssh.connect(self._node)
310
311         _p0 = 1
312         _p1 = 2
313         _async = "--async" if async_call else ""
314         _latency = "--latency" if latency else ""
315
316         if self._ifaces_reordered != 0:
317             _p0, _p1 = _p1, _p0
318
319         if traffic_type in ["3-node-xconnect", "3-node-bridge"]:
320             (ret, stdout, stderr) = ssh.exec_command(
321                 "sh -c '{0}/resources/tools/t-rex/t-rex-stateless.py "
322                 "--duration={1} -r {2} -s {3} "
323                 "--p{4}_src_start_ip 10.10.10.1 "
324                 "--p{4}_src_end_ip 10.10.10.254 "
325                 "--p{4}_dst_start_ip 20.20.20.1 "
326                 "--p{5}_src_start_ip 20.20.20.1 "
327                 "--p{5}_src_end_ip 20.20.20.254 "
328                 "--p{5}_dst_start_ip 10.10.10.1 "
329                 "{6} {7} --warmup_time={8}'".format(Constants.REMOTE_FW_DIR,
330                                                     duration, rate, framesize,
331                                                     _p0, _p1, _async, _latency,
332                                                     warmup_time),
333                 timeout=int(duration)+60)
334         elif traffic_type in ["3-node-IPv4"]:
335             (ret, stdout, stderr) = ssh.exec_command(
336                 "sh -c '{0}/resources/tools/t-rex/t-rex-stateless.py "
337                 "--duration={1} -r {2} -s {3} "
338                 "--p{4}_src_start_ip 10.10.10.2 "
339                 "--p{4}_src_end_ip 10.10.10.254 "
340                 "--p{4}_dst_start_ip 20.20.20.2 "
341                 "--p{5}_src_start_ip 20.20.20.2 "
342                 "--p{5}_src_end_ip 20.20.20.254 "
343                 "--p{5}_dst_start_ip 10.10.10.2 "
344                 "{6} {7} --warmup_time={8}'".format(Constants.REMOTE_FW_DIR,
345                                                     duration, rate, framesize,
346                                                     _p0, _p1, _async, _latency,
347                                                     warmup_time),
348                 timeout=int(duration)+60)
349         elif traffic_type in ["3-node-IPv4-dst-10000"]:
350             (ret, stdout, stderr) = ssh.exec_command(
351                 "sh -c '{0}/resources/tools/t-rex/t-rex-stateless.py "
352                 "--duration={1} -r {2} -s {3} "
353                 "--p{4}_src_start_ip 10.0.0.1 "
354                 "--p{4}_dst_start_ip 20.0.0.0 "
355                 "--p{4}_dst_end_ip 20.0.39.15 "
356                 "--p{5}_src_start_ip 20.0.0.1 "
357                 "--p{5}_dst_start_ip 10.0.0.0 "
358                 "--p{5}_dst_end_ip 10.0.39.15 "
359                 "{6} {7} --warmup_time={8}'".format(Constants.REMOTE_FW_DIR,
360                                                     duration, rate, framesize,
361                                                     _p0, _p1, _async, _latency,
362                                                     warmup_time),
363                 timeout=int(duration)+60)
364         elif traffic_type in ["3-node-IPv4-dst-100000"]:
365             (ret, stdout, stderr) = ssh.exec_command(
366                 "sh -c '{0}/resources/tools/t-rex/t-rex-stateless.py "
367                 "--duration={1} -r {2} -s {3} "
368                 "--p{4}_src_start_ip 10.0.0.1 "
369                 "--p{4}_dst_start_ip 20.0.0.0 "
370                 "--p{4}_dst_end_ip 20.1.134.159 "
371                 "--p{5}_src_start_ip 20.0.0.1 "
372                 "--p{5}_dst_start_ip 10.0.0.0 "
373                 "--p{5}_dst_end_ip 10.1.134.159 "
374                 "{6} {7} --warmup_time={8}'".format(Constants.REMOTE_FW_DIR,
375                                                     duration, rate, framesize,
376                                                     _p0, _p1, _async, _latency,
377                                                     warmup_time),
378                 timeout=int(duration)+60)
379         elif traffic_type in ["3-node-IPv4-dst-1000000"]:
380             (ret, stdout, stderr) = ssh.exec_command(
381                 "sh -c '{0}/resources/tools/t-rex/t-rex-stateless.py "
382                 "--duration={1} -r {2} -s {3} "
383                 "--p{4}_src_start_ip 10.0.0.1 "
384                 "--p{4}_dst_start_ip 20.0.0.0 "
385                 "--p{4}_dst_end_ip 20.15.66.63 "
386                 "--p{5}_src_start_ip 20.0.0.1 "
387                 "--p{5}_dst_start_ip 10.0.0.0 "
388                 "--p{5}_dst_end_ip 10.15.66.63 "
389                 "{6} {7} --warmup_time={8}'".format(Constants.REMOTE_FW_DIR,
390                                                     duration, rate, framesize,
391                                                     _p0, _p1, _async, _latency,
392                                                     warmup_time),
393                 timeout=int(duration)+60)
394         elif traffic_type in ["3-node-IPv6"]:
395             (ret, stdout, stderr) = ssh.exec_command(
396                 "sh -c '{0}/resources/tools/t-rex/t-rex-stateless.py "
397                 "--duration={1} -r {2} -s {3} -6 "
398                 "--p{4}_src_start_ip 2001:1::2 "
399                 "--p{4}_src_end_ip 2001:1::FE "
400                 "--p{4}_dst_start_ip 2001:2::2 "
401                 "--p{5}_src_start_ip 2001:2::2 "
402                 "--p{5}_src_end_ip 2001:2::FE "
403                 "--p{5}_dst_start_ip 2001:1::2 "
404                 "{6} {7} --warmup_time={8}'".format(Constants.REMOTE_FW_DIR,
405                                                     duration, rate, framesize,
406                                                     _p0, _p1, _async, _latency,
407                                                     warmup_time),
408                 timeout=int(duration)+60)
409         else:
410             raise NotImplementedError('Unsupported traffic type')
411
412         logger.trace(ret)
413         logger.trace(stdout)
414         logger.trace(stderr)
415
416         if int(ret) != 0:
417             raise RuntimeError('T-rex stateless runtime error')
418         elif async_call:
419             #no result
420             self._received = None
421             self._sent = None
422             self._loss = None
423             self._latency = None
424         else:
425             # last line from console output
426             line = stdout.splitlines()[-1]
427
428             self._result = line
429             logger.info('TrafficGen result: {0}'.format(self._result))
430
431             self._received = self._result.split(', ')[1].split('=')[1]
432             self._sent = self._result.split(', ')[2].split('=')[1]
433             self._loss = self._result.split(', ')[3].split('=')[1]
434             self._latency = []
435             self._latency.append(self._result.split(', ')[4].split('=')[1])
436             self._latency.append(self._result.split(', ')[5].split('=')[1])
437
438     def stop_traffic_on_tg(self):
439         """Stop all traffic on TG
440
441         :return: Nothing
442         """
443         if self._node is None:
444             raise RuntimeError("TG is not set")
445         if self._node['subtype'] == NodeSubTypeTG.TREX:
446             self.trex_stl_stop_remote_exec(self._node)
447
448     def send_traffic_on_tg(self, duration, rate, framesize,
449                            traffic_type, warmup_time=5, async_call=False,
450                            latency=True):
451         """Send traffic from all configured interfaces on TG.
452
453         :param duration: Duration of test traffic generation in seconds.
454         :param rate: Offered load per interface (e.g. 1%, 3gbps, 4mpps, ...).
455         :param framesize: Frame size (L2) in Bytes.
456         :param traffic_type: Traffic profile.
457         :param latency: With latency measurement.
458         :type duration: str
459         :type rate: str
460         :type framesize: str
461         :type traffic_type: str
462         :type latency: bool
463         :return: TG output.
464         :rtype: str
465         """
466
467         node = self._node
468         if node is None:
469             raise RuntimeError("TG is not set")
470
471         if node['type'] != NodeType.TG:
472             raise Exception('Node type is not a TG')
473
474         if node['subtype'] is None:
475             raise Exception('TG subtype not defined')
476         elif node['subtype'] == NodeSubTypeTG.TREX:
477             self.trex_stl_start_remote_exec(duration, rate, framesize,
478                                             traffic_type, async_call, latency,
479                                             warmup_time=warmup_time)
480         else:
481             raise NotImplementedError("TG subtype not supported")
482
483         return self._result
484
485     def no_traffic_loss_occurred(self):
486         """Fail if loss occurred in traffic run.
487
488         :return: nothing
489         """
490         if self._loss is None:
491             raise Exception('The traffic generation has not been issued')
492         if self._loss != '0':
493             raise Exception('Traffic loss occurred: {0}'.format(self._loss))
494
495     def partial_traffic_loss_accepted(self, loss_acceptance,
496                                       loss_acceptance_type):
497         """Fail if loss is higher then accepted in traffic run.
498
499         :return: nothing
500         """
501         if self._loss is None:
502             raise Exception('The traffic generation has not been issued')
503
504         if loss_acceptance_type == 'percentage':
505             loss = (float(self._loss) / float(self._sent)) * 100
506         elif loss_acceptance_type == 'frames':
507             loss = float(self._loss)
508         else:
509             raise Exception('Loss acceptance type not supported')
510
511         if loss > float(loss_acceptance):
512             raise Exception("Traffic loss {} above loss acceptance: {}".format(
513                 loss, loss_acceptance))