CSIT-152: Detect if the T-rex is responding to API after start
[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, False)
50             loss = tg_instance.get_loss()
51             sent = tg_instance.get_sent()
52             if self.loss_acceptance_type_is_percentage():
53                 loss = (float(loss) / float(sent)) * 100
54
55             # TODO: getters for tg_instance
56             logger.trace("comparing: {} < {} {}".format(loss,
57                                                         loss_acceptance,
58                                                         loss_acceptance_type))
59             if float(loss) > float(loss_acceptance):
60                 return False
61             else:
62                 return True
63         else:
64             raise NotImplementedError("TG subtype not supported")
65
66
67 class TrafficGenerator(object):
68     """Traffic Generator."""
69
70     # use one instance of TrafficGenerator for all tests in test suite
71     ROBOT_LIBRARY_SCOPE = 'TEST SUITE'
72
73     def __init__(self):
74         self._result = None
75         self._loss = None
76         self._sent = None
77         self._received = None
78         self._node = None
79         # T-REX interface order mapping
80         self._ifaces_reordered = 0
81
82     def get_loss(self):
83         """Return number of lost packets.
84
85         :return: Number of lost packets.
86         :rtype: str
87         """
88         return self._loss
89
90     def get_sent(self):
91         """Return number of sent packets.
92
93         :return: Number of sent packets.
94         :rtype: str
95         """
96         return self._sent
97
98     def get_received(self):
99         """Return number of received packets.
100
101         :return: Number of received packets.
102         :rtype: str
103         """
104         return self._received
105
106     #pylint: disable=too-many-arguments, too-many-locals
107     def initialize_traffic_generator(self, tg_node, tg_if1, tg_if2,
108                                      dut1_node, dut1_if1, dut1_if2,
109                                      dut2_node, dut2_if1, dut2_if2,
110                                      test_type):
111         """TG initialization.
112
113         :param tg_node: Traffic generator node.
114         :param tg_if1: TG - name of first interface.
115         :param tg_if2: TG - name of second interface.
116         :param dut1_node: DUT1 node.
117         :param dut1_if1: DUT1 - name of first interface.
118         :param dut1_if2: DUT1 - name of second interface.
119         :param dut2_node: DUT2 node.
120         :param dut2_if1: DUT2 - name of first interface.
121         :param dut2_if2: DUT2 - name of second interface.
122         :test_type: 'L2' or 'L3' - src/dst MAC address.
123         :type tg_node: dict
124         :type tg_if1: str
125         :type tg_if2: str
126         :type dut1_node: dict
127         :type dut1_if1: str
128         :type dut1_if2: str
129         :type dut2_node: dict
130         :type dut2_if1: str
131         :type dut2_if2: str
132         :type test_type: str
133         :return: nothing
134         """
135
136         topo = Topology()
137
138         if tg_node['type'] != NodeType.TG:
139             raise Exception('Node type is not a TG')
140         self._node = tg_node
141
142         if tg_node['subtype'] == NodeSubTypeTG.TREX:
143             trex_path = "/opt/trex-core-2.03"
144
145             ssh = SSH()
146             ssh.connect(tg_node)
147
148             (ret, stdout, stderr) = ssh.exec_command(
149                 "sudo sh -c '{}/resources/tools/t-rex/"
150                 "t-rex-installer.sh'".format(Constants.REMOTE_FW_DIR),
151                 timeout=1800)
152             if int(ret) != 0:
153                 logger.error('trex installation failed: {0}'.format(
154                     stdout + stderr))
155                 raise RuntimeError('Installation of TG failed')
156
157             if1_pci = topo.get_interface_pci_addr(tg_node, tg_if1)
158             if2_pci = topo.get_interface_pci_addr(tg_node, tg_if2)
159             if1_mac = topo.get_interface_mac(tg_node, tg_if1)
160             if2_mac = topo.get_interface_mac(tg_node, tg_if2)
161
162             if test_type == 'L2':
163                 if1_adj_mac = if2_mac
164                 if2_adj_mac = if1_mac
165             elif test_type == 'L3':
166                 if1_adj_mac = topo.get_interface_mac(dut1_node, dut1_if1)
167                 if2_adj_mac = topo.get_interface_mac(dut2_node, dut2_if2)
168             else:
169                 raise Exception("test_type unknown")
170
171             if min(if1_pci, if2_pci) != if1_pci:
172                 if1_mac, if2_mac = if2_mac, if1_mac
173                 if1_pci, if2_pci = if2_pci, if1_pci
174                 if1_adj_mac, if2_adj_mac = if2_adj_mac, if1_adj_mac
175                 self._ifaces_reordered = 1
176
177             if1_mac_hex = "0x"+if1_mac.replace(":", ",0x")
178             if2_mac_hex = "0x"+if2_mac.replace(":", ",0x")
179             if1_adj_mac_hex = "0x"+if1_adj_mac.replace(":", ",0x")
180             if2_adj_mac_hex = "0x"+if2_adj_mac.replace(":", ",0x")
181
182             (ret, stdout, stderr) = ssh.exec_command(
183                 "sudo sh -c 'cat << EOF > /etc/trex_cfg.yaml\n"
184                 "- port_limit      : 2\n"
185                 "  version         : 2\n"
186                 "  interfaces      : [\"{}\",\"{}\"]\n"
187                 "  port_bandwidth_gb : 10\n"
188                 "  port_info       :\n"
189                 "          - dest_mac        :   [{}]\n"
190                 "            src_mac         :   [{}]\n"
191                 "          - dest_mac        :   [{}]\n"
192                 "            src_mac         :   [{}]\n"
193                 "EOF'"\
194                 .format(if1_pci, if2_pci,
195                         if1_adj_mac_hex, if1_mac_hex,
196                         if2_adj_mac_hex, if2_mac_hex))
197             if int(ret) != 0:
198                 logger.error("failed to create t-rex config: {}"\
199                 .format(stdout + stderr))
200                 raise RuntimeError('trex config generation error')
201
202             (ret, stdout, stderr) = ssh.exec_command(
203                 "sh -c 'cd {0}/scripts/ && sudo ./trex-cfg'".format(trex_path))
204             if int(ret) != 0:
205                 logger.error('trex-cfg failed: {0}'.format(stdout + stderr))
206                 raise RuntimeError('trex-cfg failed')
207
208             max_startup_retries = 3
209             while max_startup_retries > 0:
210                 # kill T-rex only if it is already running
211                 (ret, _, _) = ssh.exec_command(
212                     "sh -c 'pgrep t-rex && sudo pkill t-rex'")
213
214                 # start T-rex
215                 (ret, _, _) = ssh.exec_command(
216                     "sh -c 'cd {0}/scripts/ && "
217                     "sudo nohup ./t-rex-64 -i -c 7 --iom 0 > /dev/null 2>&1 &'"
218                     "> /dev/null"\
219                     .format(trex_path))
220                 if int(ret) != 0:
221                     raise RuntimeError('t-rex-64 startup failed')
222
223                 # get T-rex server info
224                 (ret, _, _) = ssh.exec_command(
225                     "sh -c '{0}/resources/tools/t-rex/t-rex-server-info.py'"\
226                     .format(Constants.REMOTE_FW_DIR),
227                     timeout=120)
228                 if int(ret) == 0:
229                     # If we get info T-rex is running
230                     return
231                 # try again
232                 max_startup_retries -= 1
233             # after max retries T-rex is still not responding to API
234             # critical error occured
235             raise RuntimeError('t-rex-64 startup failed')
236
237
238     @staticmethod
239     def teardown_traffic_generator(node):
240         """TG teardown.
241
242         :param node: Traffic generator node.
243         :type node: dict
244         :return: nothing
245         """
246         if node['type'] != NodeType.TG:
247             raise Exception('Node type is not a TG')
248         if node['subtype'] == NodeSubTypeTG.TREX:
249             ssh = SSH()
250             ssh.connect(node)
251             (ret, stdout, stderr) = ssh.exec_command(
252                 "sh -c 'sudo pkill t-rex'")
253             if int(ret) != 0:
254                 logger.error('pkill t-rex failed: {0}'.format(stdout + stderr))
255                 raise RuntimeError('pkill t-rex failed')
256
257     @staticmethod
258     def trex_stl_stop_remote_exec(node):
259         """Execute script on remote node over ssh to stop running traffic.
260
261         :param node: T-REX generator node.
262         :type node: dict
263         :return: Nothing
264         """
265         ssh = SSH()
266         ssh.connect(node)
267
268         (ret, stdout, stderr) = ssh.exec_command(
269             "sh -c '{}/resources/tools/t-rex/"
270             "t-rex-stateless-stop.py'".format(Constants.REMOTE_FW_DIR))
271         logger.trace(ret)
272         logger.trace(stdout)
273         logger.trace(stderr)
274
275         if int(ret) != 0:
276             raise RuntimeError('T-rex stateless runtime error')
277
278     def trex_stl_start_remote_exec(self, duration, rate, framesize,
279                                    traffic_type, async_call, warmup_time=5):
280         """Execute script on remote node over ssh to start traffic.
281
282         :param duration: Time expresed in seconds for how long to send traffic.
283         :param rate: Traffic rate expressed with units (pps, %)
284         :param framesize: L2 frame size to send (without padding and IPG).
285         :param traffic_type: Traffic profile.
286         :param async_call: If enabled then don't wait for all incomming trafic.
287         :param warmup_time: Warmup time period.
288         :type duration: int
289         :type rate: str
290         :type framesize: int
291         :type traffic_type: str
292         :type async_call: bool
293         :type warmup_time: int
294         :return: Nothing
295         """
296         ssh = SSH()
297         ssh.connect(self._node)
298
299         _p0 = 1
300         _p1 = 2
301         _async = ""
302
303         if async_call:
304             _async = "--async"
305         if self._ifaces_reordered != 0:
306             _p0, _p1 = _p1, _p0
307
308         if traffic_type in ["3-node-xconnect", "3-node-bridge"]:
309             (ret, stdout, stderr) = ssh.exec_command(
310                 "sh -c '{0}/resources/tools/t-rex/t-rex-stateless.py "
311                 "--duration={1} -r {2} -s {3} "
312                 "--p{4}_src_start_ip 10.10.10.1 "
313                 "--p{4}_src_end_ip 10.10.10.254 "
314                 "--p{4}_dst_start_ip 20.20.20.1 "
315                 "--p{5}_src_start_ip 20.20.20.1 "
316                 "--p{5}_src_end_ip 20.20.20.254 "
317                 "--p{5}_dst_start_ip 10.10.10.1 "
318                 "{6} --warmup_time={7}'".format(Constants.REMOTE_FW_DIR,
319                                                 duration, rate, framesize, _p0,
320                                                 _p1, _async, warmup_time),
321                 timeout=int(duration)+60)
322         elif traffic_type in ["3-node-IPv4"]:
323             (ret, stdout, stderr) = ssh.exec_command(
324                 "sh -c '{0}/resources/tools/t-rex/t-rex-stateless.py "
325                 "--duration={1} -r {2} -s {3} "
326                 "--p{4}_src_start_ip 10.10.10.2 "
327                 "--p{4}_src_end_ip 10.10.10.254 "
328                 "--p{4}_dst_start_ip 20.20.20.2 "
329                 "--p{5}_src_start_ip 20.20.20.2 "
330                 "--p{5}_src_end_ip 20.20.20.254 "
331                 "--p{5}_dst_start_ip 10.10.10.2 "
332                 "{6} --warmup_time={7}'".format(Constants.REMOTE_FW_DIR,
333                                                 duration, rate, framesize, _p0,
334                                                 _p1, _async, warmup_time),
335                 timeout=int(duration)+60)
336         elif traffic_type in ["3-node-IPv6"]:
337             (ret, stdout, stderr) = ssh.exec_command(
338                 "sh -c '{0}/resources/tools/t-rex/t-rex-stateless.py "
339                 "--duration={1} -r {2} -s {3} -6 "
340                 "--p{4}_src_start_ip 2001:1::2 "
341                 "--p{4}_src_end_ip 2001:1::FE "
342                 "--p{4}_dst_start_ip 2001:2::2 "
343                 "--p{5}_src_start_ip 2001:2::2 "
344                 "--p{5}_src_end_ip 2001:2::FE "
345                 "--p{5}_dst_start_ip 2001:1::2 "
346                 "{6} --warmup_time={7}'".format(Constants.REMOTE_FW_DIR,
347                                                 duration, rate, framesize, _p0,
348                                                 _p1, _async, warmup_time),
349                 timeout=int(duration)+60)
350         else:
351             raise NotImplementedError('Unsupported traffic type')
352
353         logger.trace(ret)
354         logger.trace(stdout)
355         logger.trace(stderr)
356
357         if int(ret) != 0:
358             raise RuntimeError('T-rex stateless runtime error')
359         elif async_call:
360             #no result
361             self._received = None
362             self._sent = None
363             self._loss = None
364         else:
365             # last line from console output
366             line = stdout.splitlines()[-1]
367
368             self._result = line
369             logger.info('TrafficGen result: {0}'.format(self._result))
370
371             self._received = self._result.split(', ')[1].split('=')[1]
372             self._sent = self._result.split(', ')[2].split('=')[1]
373             self._loss = self._result.split(', ')[3].split('=')[1]
374
375     def stop_traffic_on_tg(self):
376         """Stop all traffic on TG
377
378         :return: Nothing
379         """
380         if self._node is None:
381             raise RuntimeError("TG is not set")
382         if self._node['subtype'] == NodeSubTypeTG.TREX:
383             self.trex_stl_stop_remote_exec(self._node)
384
385     def send_traffic_on_tg(self, duration, rate, framesize,
386                            traffic_type, warmup_time=5, async_call=False):
387         """Send traffic from all configured interfaces on TG.
388
389         :param duration: Duration of test traffic generation in seconds.
390         :param rate: Offered load per interface (e.g. 1%, 3gbps, 4mpps, ...).
391         :param framesize: Frame size (L2) in Bytes.
392         :param traffic_type: Traffic profile.
393         :type duration: str
394         :type rate: str
395         :type framesize: str
396         :type traffic_type: str
397         :return: TG output.
398         :rtype: str
399         """
400
401         node = self._node
402         if node is None:
403             raise RuntimeError("TG is not set")
404
405         if node['type'] != NodeType.TG:
406             raise Exception('Node type is not a TG')
407
408         if node['subtype'] is None:
409             raise Exception('TG subtype not defined')
410         elif node['subtype'] == NodeSubTypeTG.TREX:
411             self.trex_stl_start_remote_exec(duration, rate, framesize,
412                                             traffic_type, async_call,
413                                             warmup_time=warmup_time)
414         else:
415             raise NotImplementedError("TG subtype not supported")
416
417         return self._result
418
419     def no_traffic_loss_occurred(self):
420         """Fail if loss occurred in traffic run.
421
422         :return: nothing
423         """
424         if self._loss is None:
425             raise Exception('The traffic generation has not been issued')
426         if self._loss != '0':
427             raise Exception('Traffic loss occurred: {0}'.format(self._loss))
428
429     def partial_traffic_loss_accepted(self, loss_acceptance,
430                                       loss_acceptance_type):
431         """Fail if loss is higher then accepted in traffic run.
432
433         :return: nothing
434         """
435         if self._loss is None:
436             raise Exception('The traffic generation has not been issued')
437
438         if loss_acceptance_type == 'percentage':
439             loss = (float(self._loss) / float(self._sent)) * 100
440         elif loss_acceptance_type == 'frames':
441             loss = float(self._loss)
442         else:
443             raise Exception('Loss acceptance type not supported')
444
445         if loss > float(loss_acceptance):
446             raise Exception("Traffic loss {} above loss acceptance: {}".format(
447                 loss, loss_acceptance))