CSIT-102: Add latency measurement to performance testing
[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.03"
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-IPv6"]:
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} -6 "
353                 "--p{4}_src_start_ip 2001:1::2 "
354                 "--p{4}_src_end_ip 2001:1::FE "
355                 "--p{4}_dst_start_ip 2001:2::2 "
356                 "--p{5}_src_start_ip 2001:2::2 "
357                 "--p{5}_src_end_ip 2001:2::FE "
358                 "--p{5}_dst_start_ip 2001:1::2 "
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         else:
365             raise NotImplementedError('Unsupported traffic type')
366
367         logger.trace(ret)
368         logger.trace(stdout)
369         logger.trace(stderr)
370
371         if int(ret) != 0:
372             raise RuntimeError('T-rex stateless runtime error')
373         elif async_call:
374             #no result
375             self._received = None
376             self._sent = None
377             self._loss = None
378             self._latency = None
379         else:
380             # last line from console output
381             line = stdout.splitlines()[-1]
382
383             self._result = line
384             logger.info('TrafficGen result: {0}'.format(self._result))
385
386             self._received = self._result.split(', ')[1].split('=')[1]
387             self._sent = self._result.split(', ')[2].split('=')[1]
388             self._loss = self._result.split(', ')[3].split('=')[1]
389             self._latency = []
390             self._latency.append(self._result.split(', ')[4].split('=')[1])
391             self._latency.append(self._result.split(', ')[5].split('=')[1])
392
393     def stop_traffic_on_tg(self):
394         """Stop all traffic on TG
395
396         :return: Nothing
397         """
398         if self._node is None:
399             raise RuntimeError("TG is not set")
400         if self._node['subtype'] == NodeSubTypeTG.TREX:
401             self.trex_stl_stop_remote_exec(self._node)
402
403     def send_traffic_on_tg(self, duration, rate, framesize,
404                            traffic_type, warmup_time=5, async_call=False,
405                            latency=True):
406         """Send traffic from all configured interfaces on TG.
407
408         :param duration: Duration of test traffic generation in seconds.
409         :param rate: Offered load per interface (e.g. 1%, 3gbps, 4mpps, ...).
410         :param framesize: Frame size (L2) in Bytes.
411         :param traffic_type: Traffic profile.
412         :param latency: With latency measurement.
413         :type duration: str
414         :type rate: str
415         :type framesize: str
416         :type traffic_type: str
417         :type latency: bool
418         :return: TG output.
419         :rtype: str
420         """
421
422         node = self._node
423         if node is None:
424             raise RuntimeError("TG is not set")
425
426         if node['type'] != NodeType.TG:
427             raise Exception('Node type is not a TG')
428
429         if node['subtype'] is None:
430             raise Exception('TG subtype not defined')
431         elif node['subtype'] == NodeSubTypeTG.TREX:
432             self.trex_stl_start_remote_exec(duration, rate, framesize,
433                                             traffic_type, async_call, latency,
434                                             warmup_time=warmup_time)
435         else:
436             raise NotImplementedError("TG subtype not supported")
437
438         return self._result
439
440     def no_traffic_loss_occurred(self):
441         """Fail if loss occurred in traffic run.
442
443         :return: nothing
444         """
445         if self._loss is None:
446             raise Exception('The traffic generation has not been issued')
447         if self._loss != '0':
448             raise Exception('Traffic loss occurred: {0}'.format(self._loss))
449
450     def partial_traffic_loss_accepted(self, loss_acceptance,
451                                       loss_acceptance_type):
452         """Fail if loss is higher then accepted in traffic run.
453
454         :return: nothing
455         """
456         if self._loss is None:
457             raise Exception('The traffic generation has not been issued')
458
459         if loss_acceptance_type == 'percentage':
460             loss = (float(self._loss) / float(self._sent)) * 100
461         elif loss_acceptance_type == 'frames':
462             loss = float(self._loss)
463         else:
464             raise Exception('Loss acceptance type not supported')
465
466         if loss > float(loss_acceptance):
467             raise Exception("Traffic loss {} above loss acceptance: {}".format(
468                 loss, loss_acceptance))