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