Fix warnings reported by gen_doc.sh
[csit.git] / resources / libraries / python / TrafficGenerator.py
1 # Copyright (c) 2018 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, skip_warmup=False):
37         """Runs the traffic and evaluate the measured results.
38
39         :param rate: Offered traffic load.
40         :param frame_size: Size of frame.
41         :param loss_acceptance: Permitted drop ratio or frames count.
42         :param loss_acceptance_type: Type of permitted loss.
43         :param traffic_type: Traffic profile ([2,3]-node-L[2,3], ...).
44         :param skip_warmup: Start TRex without warmup traffic if true.
45         :type rate: int
46         :type frame_size: str
47         :type loss_acceptance: float
48         :type loss_acceptance_type: LossAcceptanceType
49         :type traffic_type: str
50         :type skip_warmup: bool
51         :returns: Drop threshold exceeded? (True/False)
52         :rtype: bool
53         :raises NotImplementedError: If TG is not supported.
54         :raises RuntimeError: If TG is not specified.
55         """
56         # we need instance of TrafficGenerator instantiated by Robot Framework
57         # to be able to use trex_stl-*()
58         tg_instance = BuiltIn().get_library_instance(
59             'resources.libraries.python.TrafficGenerator')
60
61         if tg_instance.node['subtype'] is None:
62             raise RuntimeError('TG subtype not defined')
63         elif tg_instance.node['subtype'] == NodeSubTypeTG.TREX:
64             unit_rate = str(rate) + self.get_rate_type_str()
65             if skip_warmup:
66                 tg_instance.trex_stl_start_remote_exec(self.get_duration(),
67                                                        unit_rate, frame_size,
68                                                        traffic_type,
69                                                        warmup_time=0)
70             else:
71                 tg_instance.trex_stl_start_remote_exec(self.get_duration(),
72                                                        unit_rate, frame_size,
73                                                        traffic_type)
74             loss = tg_instance.get_loss()
75             sent = tg_instance.get_sent()
76             if self.loss_acceptance_type_is_percentage():
77                 loss = (float(loss) / float(sent)) * 100
78
79             logger.trace("comparing: {} < {} {}".format(loss,
80                                                         loss_acceptance,
81                                                         loss_acceptance_type))
82             if float(loss) > float(loss_acceptance):
83                 return False
84             else:
85                 return True
86         else:
87             raise NotImplementedError("TG subtype not supported")
88
89     def get_latency(self):
90         """Returns min/avg/max latency.
91
92         :returns: Latency stats.
93         :rtype: list
94         """
95         tg_instance = BuiltIn().get_library_instance(
96             'resources.libraries.python.TrafficGenerator')
97         return tg_instance.get_latency_int()
98
99
100 class TrafficGenerator(object):
101     """Traffic Generator."""
102
103     # use one instance of TrafficGenerator for all tests in test suite
104     ROBOT_LIBRARY_SCOPE = 'TEST SUITE'
105
106     def __init__(self):
107         self._result = None
108         self._loss = None
109         self._sent = None
110         self._latency = None
111         self._received = None
112         self._node = None
113         # T-REX interface order mapping
114         self._ifaces_reordered = False
115
116     @property
117     def node(self):
118         """Getter.
119
120         :returns: Traffic generator node.
121         :rtype: dict
122         """
123         return self._node
124
125     def get_loss(self):
126         """Return number of lost packets.
127
128         :returns: Number of lost packets.
129         :rtype: str
130         """
131         return self._loss
132
133     def get_sent(self):
134         """Return number of sent packets.
135
136         :returns: Number of sent packets.
137         :rtype: str
138         """
139         return self._sent
140
141     def get_received(self):
142         """Return number of received packets.
143
144         :returns: Number of received packets.
145         :rtype: str
146         """
147         return self._received
148
149     def get_latency_int(self):
150         """Return rounded min/avg/max latency.
151
152         :returns: Latency stats.
153         :rtype: list
154         """
155         return self._latency
156
157     def initialize_traffic_generator(self, tg_node, tg_if1, tg_if2,
158                                      tg_if1_adj_node, tg_if1_adj_if,
159                                      tg_if2_adj_node, tg_if2_adj_if,
160                                      test_type,
161                                      tg_if1_dst_mac=None, tg_if2_dst_mac=None):
162         """TG initialization.
163
164         :param tg_node: Traffic generator node.
165         :param tg_if1: TG - name of first interface.
166         :param tg_if2: TG - name of second interface.
167         :param tg_if1_adj_node: TG if1 adjecent node.
168         :param tg_if1_adj_if: TG if1 adjecent interface.
169         :param tg_if2_adj_node: TG if2 adjecent node.
170         :param tg_if2_adj_if: TG if2 adjecent interface.
171         :param test_type: 'L2', 'L3' or 'L7' - OSI Layer testing type.
172         :param tg_if1_dst_mac: Interface 1 destination MAC address.
173         :param tg_if2_dst_mac: Interface 2 destination MAC address.
174         :type tg_node: dict
175         :type tg_if1: str
176         :type tg_if2: str
177         :type tg_if1_adj_node: dict
178         :type tg_if1_adj_if: str
179         :type tg_if2_adj_node: dict
180         :type tg_if2_adj_if: str
181         :type test_type: str
182         :type tg_if1_dst_mac: str
183         :type tg_if2_dst_mac: str
184         :returns: nothing
185         :raises RuntimeError: In case of issue during initialization.
186         """
187         if tg_node['type'] != NodeType.TG:
188             raise RuntimeError('Node type is not a TG')
189         self._node = tg_node
190
191         if tg_node['subtype'] == NodeSubTypeTG.TREX:
192             ssh = SSH()
193             ssh.connect(tg_node)
194
195             (ret, _, _) = ssh.exec_command(
196                 "sudo -E sh -c '{0}/resources/tools/trex/"
197                 "trex_installer.sh {1}'".format(Constants.REMOTE_FW_DIR,
198                                                 Constants.TREX_INSTALL_VERSION),
199                 timeout=1800)
200             if int(ret) != 0:
201                 raise RuntimeError('TRex installation failed.')
202
203             if1_pci = Topology().get_interface_pci_addr(tg_node, tg_if1)
204             if2_pci = Topology().get_interface_pci_addr(tg_node, tg_if2)
205             if1_addr = Topology().get_interface_mac(tg_node, tg_if1)
206             if2_addr = Topology().get_interface_mac(tg_node, tg_if2)
207
208             if test_type == 'L2':
209                 if1_adj_addr = if2_addr
210                 if2_adj_addr = if1_addr
211             elif test_type == 'L3':
212                 if1_adj_addr = Topology().get_interface_mac(tg_if1_adj_node,
213                                                             tg_if1_adj_if)
214                 if2_adj_addr = Topology().get_interface_mac(tg_if2_adj_node,
215                                                             tg_if2_adj_if)
216             elif test_type == 'L7':
217                 if1_addr = Topology().get_interface_ip4(tg_node, tg_if1)
218                 if2_addr = Topology().get_interface_ip4(tg_node, tg_if2)
219                 if1_adj_addr = Topology().get_interface_ip4(tg_if1_adj_node,
220                                                             tg_if1_adj_if)
221                 if2_adj_addr = Topology().get_interface_ip4(tg_if2_adj_node,
222                                                             tg_if2_adj_if)
223             else:
224                 raise ValueError("Unknown Test Type")
225
226             # in case of switched environment we can override MAC addresses
227             if tg_if1_dst_mac is not None and tg_if2_dst_mac is not None:
228                 if1_adj_addr = tg_if1_dst_mac
229                 if2_adj_addr = tg_if2_dst_mac
230
231             if min(if1_pci, if2_pci) != if1_pci:
232                 if1_pci, if2_pci = if2_pci, if1_pci
233                 if1_addr, if2_addr = if2_addr, if1_addr
234                 if1_adj_addr, if2_adj_addr = if2_adj_addr, if1_adj_addr
235                 self._ifaces_reordered = True
236
237             if test_type == 'L2' or test_type == 'L3':
238                 (ret, _, _) = ssh.exec_command(
239                     "sudo sh -c 'cat << EOF > /etc/trex_cfg.yaml\n"
240                     "- port_limit: 2\n"
241                     "  version: 2\n"
242                     "  interfaces: [\"{0}\",\"{1}\"]\n"
243                     "  port_info:\n"
244                     "      - dest_mac: [{2}]\n"
245                     "        src_mac: [{3}]\n"
246                     "      - dest_mac: [{4}]\n"
247                     "        src_mac: [{5}]\n"
248                     "EOF'"\
249                     .format(if1_pci, if2_pci,
250                             "0x"+if1_adj_addr.replace(":", ",0x"),
251                             "0x"+if1_addr.replace(":", ",0x"),
252                             "0x"+if2_adj_addr.replace(":", ",0x"),
253                             "0x"+if2_addr.replace(":", ",0x")))
254             elif test_type == 'L7':
255                 (ret, _, _) = ssh.exec_command(
256                     "sudo sh -c 'cat << EOF > /etc/trex_cfg.yaml\n"
257                     "- port_limit: 2\n"
258                     "  version: 2\n"
259                     "  interfaces: [\"{0}\",\"{1}\"]\n"
260                     "  port_info:\n"
261                     "      - ip: [{2}]\n"
262                     "        default_gw: [{3}]\n"
263                     "      - ip: [{4}]\n"
264                     "        default_gw: [{5}]\n"
265                     "EOF'"\
266                     .format(if1_pci, if2_pci,
267                             if1_addr, if1_adj_addr,
268                             if2_addr, if2_adj_addr))
269             else:
270                 raise ValueError("Unknown Test Type")
271             if int(ret) != 0:
272                 raise RuntimeError('TRex config generation error')
273
274             for _ in range(0, 3):
275                 # kill TRex only if it is already running
276                 ssh.exec_command(
277                     "sh -c 'pgrep t-rex && sudo pkill t-rex && sleep 3'")
278
279                 # configure TRex
280                 (ret, _, _) = ssh.exec_command(
281                     "sh -c 'cd {0}/scripts/ && sudo ./trex-cfg'"\
282                     .format(Constants.TREX_INSTALL_DIR))
283                 if int(ret) != 0:
284                     raise RuntimeError('trex-cfg failed')
285
286                 # start TRex
287                 if test_type == 'L2' or test_type == 'L3':
288                     (ret, _, _) = ssh.exec_command(
289                         "sh -c 'cd {0}/scripts/ && "
290                         "sudo nohup ./t-rex-64 -i -c 7 --iom 0 > /tmp/trex.log "
291                         "2>&1 &' > /dev/null"\
292                         .format(Constants.TREX_INSTALL_DIR))
293                 elif test_type == 'L7':
294                     (ret, _, _) = ssh.exec_command(
295                         "sh -c 'cd {0}/scripts/ && "
296                         "sudo nohup ./t-rex-64 --astf -i -c 7 --iom 0 > "
297                         "/tmp/trex.log 2>&1 &' > /dev/null"\
298                         .format(Constants.TREX_INSTALL_DIR))
299                 else:
300                     raise ValueError("Unknown Test Type")
301                 if int(ret) != 0:
302                     ssh.exec_command("sh -c 'cat /tmp/trex.log'")
303                     raise RuntimeError('t-rex-64 startup failed')
304
305                 # get TRex server info
306                 (ret, _, _) = ssh.exec_command(
307                     "sh -c 'sleep 3; "
308                     "{0}/resources/tools/trex/trex_server_info.py'"\
309                     .format(Constants.REMOTE_FW_DIR),
310                     timeout=120)
311                 if int(ret) == 0:
312                     # If we get info TRex is running
313                     return
314             # after max retries TRex is still not responding to API
315             # critical error occurred
316             raise RuntimeError('t-rex-64 startup failed')
317
318     @staticmethod
319     def is_trex_running(node):
320         """Check if TRex is running using pidof.
321
322         :param node: Traffic generator node.
323         :type node: dict
324         :returns: True if TRex is running otherwise False.
325         :rtype: bool
326         :raises RuntimeError: If node type is not a TG.
327         """
328         if node['type'] != NodeType.TG:
329             raise RuntimeError('Node type is not a TG')
330
331         ssh = SSH()
332         ssh.connect(node)
333         ret, _, _ = ssh.exec_command_sudo("pidof t-rex")
334         return bool(int(ret) == 0)
335
336     @staticmethod
337     def teardown_traffic_generator(node):
338         """TG teardown.
339
340         :param node: Traffic generator node.
341         :type node: dict
342         :returns: nothing
343         :raises RuntimeError: If node type is not a TG,
344             or if TRex teardown fails.
345         """
346         if node['type'] != NodeType.TG:
347             raise RuntimeError('Node type is not a TG')
348         if node['subtype'] == NodeSubTypeTG.TREX:
349             ssh = SSH()
350             ssh.connect(node)
351             (ret, _, _) = ssh.exec_command(
352                 "sh -c 'sudo pkill t-rex && sleep 3'")
353             if int(ret) != 0:
354                 raise RuntimeError('pkill t-rex failed')
355
356     @staticmethod
357     def trex_stl_stop_remote_exec(node):
358         """Execute script on remote node over ssh to stop running traffic.
359
360         :param node: TRex generator node.
361         :type node: dict
362         :returns: Nothing
363         :raises RuntimeError: If stop traffic script fails.
364         """
365         ssh = SSH()
366         ssh.connect(node)
367
368         (ret, _, _) = ssh.exec_command(
369             "sh -c '{}/resources/tools/trex/"
370             "trex_stateless_stop.py'".format(Constants.REMOTE_FW_DIR))
371
372         if int(ret) != 0:
373             raise RuntimeError('TRex stateless runtime error')
374
375     def trex_stl_start_remote_exec(self, duration, rate, framesize,
376                                    traffic_type, async_call=False,
377                                    latency=True, warmup_time=5):
378         """Execute script on remote node over ssh to start traffic.
379
380         :param duration: Time expresed in seconds for how long to send traffic.
381         :param rate: Traffic rate expressed with units (pps, %)
382         :param framesize: L2 frame size to send (without padding and IPG).
383         :param traffic_type: Traffic profile.
384         :param async_call: If enabled then don't wait for all incomming trafic.
385         :param latency: With latency measurement.
386         :param warmup_time: Warmup time period.
387         :type duration: int
388         :type rate: str
389         :type framesize: str
390         :type traffic_type: str
391         :type async_call: bool
392         :type latency: bool
393         :type warmup_time: int
394         :returns: Nothing
395         :raises RuntimeError: In case of TG driver issue.
396         """
397         ssh = SSH()
398         ssh.connect(self._node)
399
400         _async = "--async" if async_call else ""
401         _latency = "--latency" if latency else ""
402         _p0, _p1 = (2, 1) if self._ifaces_reordered else (1, 2)
403
404         profile_path = ("{0}/resources/traffic_profiles/trex/"
405                         "{1}.py".format(Constants.REMOTE_FW_DIR,
406                                         traffic_type))
407         (ret, stdout, _) = ssh.exec_command(
408             "sh -c "
409             "'{0}/resources/tools/trex/trex_stateless_profile.py "
410             "--profile {1} "
411             "--duration {2} "
412             "--frame_size {3} "
413             "--rate {4} "
414             "--warmup_time {5} "
415             "--port_0 {6} "
416             "--port_1 {7} "
417             "{8} "   # --async
418             "{9}'".  # --latency
419             format(Constants.REMOTE_FW_DIR, profile_path, duration, framesize,
420                    rate, warmup_time, _p0 - 1, _p1 - 1, _async, _latency),
421             timeout=int(duration) + 60)
422
423         if int(ret) != 0:
424             raise RuntimeError('TRex stateless runtime error')
425         elif async_call:
426             #no result
427             self._received = None
428             self._sent = None
429             self._loss = None
430             self._latency = None
431         else:
432             # last line from console output
433             line = stdout.splitlines()[-1]
434
435             self._result = line
436             logger.info('TrafficGen result: {0}'.format(self._result))
437
438             self._received = self._result.split(', ')[1].split('=')[1]
439             self._sent = self._result.split(', ')[2].split('=')[1]
440             self._loss = self._result.split(', ')[3].split('=')[1]
441
442             self._latency = []
443             self._latency.append(self._result.split(', ')[4].split('=')[1])
444             self._latency.append(self._result.split(', ')[5].split('=')[1])
445
446     def stop_traffic_on_tg(self):
447         """Stop all traffic on TG.
448
449         :returns: Nothing
450         :raises RuntimeError: If TG is not set.
451         """
452         if self._node is None:
453             raise RuntimeError("TG is not set")
454         if self._node['subtype'] == NodeSubTypeTG.TREX:
455             self.trex_stl_stop_remote_exec(self._node)
456
457     def send_traffic_on_tg(self, duration, rate, framesize,
458                            traffic_type, warmup_time=5, async_call=False,
459                            latency=True):
460         """Send traffic from all configured interfaces on TG.
461
462         :param duration: Duration of test traffic generation in seconds.
463         :param rate: Offered load per interface (e.g. 1%, 3gbps, 4mpps, ...).
464         :param framesize: Frame size (L2) in Bytes.
465         :param traffic_type: Traffic profile.
466         :param warmup_time: Warmup phase in seconds.
467         :param async_call: Async mode.
468         :param latency: With latency measurement.
469         :type duration: str
470         :type rate: str
471         :type framesize: str
472         :type traffic_type: str
473         :type warmup_time: int
474         :type async_call: bool
475         :type latency: bool
476         :returns: TG output.
477         :rtype: str
478         :raises RuntimeError: If TG is not set, or if node is not TG,
479             or if subtype is not specified.
480         :raises NotImplementedError: If TG is not supported.
481         """
482
483         node = self._node
484         if node is None:
485             raise RuntimeError("TG is not set")
486
487         if node['type'] != NodeType.TG:
488             raise RuntimeError('Node type is not a TG')
489
490         if node['subtype'] is None:
491             raise RuntimeError('TG subtype not defined')
492         elif node['subtype'] == NodeSubTypeTG.TREX:
493             self.trex_stl_start_remote_exec(int(duration), rate, framesize,
494                                             traffic_type, async_call, latency,
495                                             warmup_time=warmup_time)
496         else:
497             raise NotImplementedError("TG subtype not supported")
498
499         return self._result
500
501     def no_traffic_loss_occurred(self):
502         """Fail if loss occurred in traffic run.
503
504         :returns: nothing
505         :raises Exception: If loss occured.
506         """
507         if self._loss is None:
508             raise Exception('The traffic generation has not been issued')
509         if self._loss != '0':
510             raise Exception('Traffic loss occurred: {0}'.format(self._loss))
511
512     def partial_traffic_loss_accepted(self, loss_acceptance,
513                                       loss_acceptance_type):
514         """Fail if loss is higher then accepted in traffic run.
515
516         :param loss_acceptance: Permitted drop ratio or frames count.
517         :param loss_acceptance_type: Type of permitted loss.
518         :type loss_acceptance: float
519         :type loss_acceptance_type: LossAcceptanceType
520         :returns: nothing
521         :raises Exception: If loss is above acceptance criteria.
522         """
523         if self._loss is None:
524             raise Exception('The traffic generation has not been issued')
525
526         if loss_acceptance_type == 'percentage':
527             loss = (float(self._loss) / float(self._sent)) * 100
528         elif loss_acceptance_type == 'frames':
529             loss = float(self._loss)
530         else:
531             raise Exception('Loss acceptance type not supported')
532
533         if loss > float(loss_acceptance):
534             raise Exception("Traffic loss {} above loss acceptance: {}".format(
535                 loss, loss_acceptance))