Merge "Prox L2FWD multiflow test fix" into stable/gambia
[yardstick.git] / yardstick / network_services / traffic_profile / rfc2544.py
1 # Copyright (c) 2016-2017 Intel Corporation
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #      http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import logging
16
17 from trex_stl_lib import api as Pkt
18 from trex_stl_lib import trex_stl_client
19 from trex_stl_lib import trex_stl_packet_builder_scapy
20 from trex_stl_lib import trex_stl_streams
21
22 from yardstick.common import constants
23 from yardstick.network_services.traffic_profile import trex_traffic_profile
24
25
26 LOGGING = logging.getLogger(__name__)
27 SRC_PORT = 'sport'
28 DST_PORT = 'dport'
29
30
31 class PortPgIDMap(object):
32     """Port and pg_id mapping class
33
34     "pg_id" is the identification STL library gives to each stream. In the
35     RFC2544Profile class, the traffic has a STLProfile per port, which contains
36     one or several streams, one per packet size defined in the IMIX test case
37     description.
38
39     Example of port <-> pg_id map:
40         self._port_pg_id_map = {
41             0: [1, 2, 3, 4],
42             1: [5, 6, 7, 8]
43         }
44     """
45
46     def __init__(self):
47         self._pg_id = 0
48         self._last_port = None
49         self._port_pg_id_map = {}
50
51     def add_port(self, port):
52         self._last_port = port
53         self._port_pg_id_map[port] = []
54
55     def get_pg_ids(self, port):
56         return self._port_pg_id_map.get(port, [])
57
58     def increase_pg_id(self, port=None):
59         port = self._last_port if not port else port
60         if port is None:
61             return
62         pg_id_list = self._port_pg_id_map.get(port)
63         if not pg_id_list:
64             self.add_port(port)
65             pg_id_list = self._port_pg_id_map[port]
66         self._pg_id += 1
67         pg_id_list.append(self._pg_id)
68         return self._pg_id
69
70
71 class RFC2544Profile(trex_traffic_profile.TrexProfile):
72     """TRex RFC2544 traffic profile"""
73
74     TOLERANCE_LIMIT = 0.01
75
76     def __init__(self, traffic_generator):
77         super(RFC2544Profile, self).__init__(traffic_generator)
78         self.generator = None
79         self.rate = self.config.frame_rate
80         self.max_rate = self.config.frame_rate
81         self.min_rate = 0
82         self.drop_percent_max = 0
83
84     def register_generator(self, generator):
85         self.generator = generator
86
87     def stop_traffic(self, traffic_generator=None):
88         """"Stop traffic injection, reset counters and remove streams"""
89         if traffic_generator is not None and self.generator is None:
90             self.generator = traffic_generator
91
92         self.generator.client.stop()
93         self.generator.client.reset()
94         self.generator.client.remove_all_streams()
95
96     def execute_traffic(self, traffic_generator=None):
97         """Generate the stream and run traffic on the given ports
98
99         :param traffic_generator: (TrexTrafficGenRFC) traffic generator
100         :return ports: (list of int) indexes of ports
101                 port_pg_id: (dict) port indexes and pg_id [1] map
102         [1] https://trex-tgn.cisco.com/trex/doc/cp_stl_docs/api/
103             profile_code.html#stlstream-modes
104         """
105         if traffic_generator is not None and self.generator is None:
106             self.generator = traffic_generator
107
108         port_pg_id = PortPgIDMap()
109         ports = []
110         for vld_id, intfs in sorted(self.generator.networks.items()):
111             profile_data = self.params.get(vld_id)
112             if not profile_data:
113                 continue
114             if (vld_id.startswith(self.DOWNLINK) and
115                     self.generator.rfc2544_helper.correlated_traffic):
116                 continue
117             for intf in intfs:
118                 port_num = int(self.generator.port_num(intf))
119                 ports.append(port_num)
120                 port_pg_id.add_port(port_num)
121                 profile = self._create_profile(profile_data,
122                                                self.rate, port_pg_id,
123                                                self.config.enable_latency)
124                 self.generator.client.add_streams(profile, ports=[port_num])
125
126         self.generator.client.start(ports=ports,
127                                     duration=self.config.duration,
128                                     force=True)
129         return ports, port_pg_id
130
131     def _create_profile(self, profile_data, rate, port_pg_id, enable_latency):
132         """Create a STL profile (list of streams) for a port"""
133         streams = []
134         for packet_name in profile_data:
135             imix = (profile_data[packet_name].
136                     get('outer_l2', {}).get('framesize'))
137             imix_data = self._create_imix_data(imix)
138             self._create_vm(profile_data[packet_name])
139             _streams = self._create_streams(imix_data, rate, port_pg_id,
140                                             enable_latency)
141             streams.extend(_streams)
142         return trex_stl_streams.STLProfile(streams)
143
144     def _create_imix_data(self, imix,
145                           weight_mode=constants.DISTRIBUTION_IN_PACKETS):
146         """Generate the IMIX distribution for a STL profile
147
148         The input information is the framesize dictionary in a test case
149         traffic profile definition. E.g.:
150           downlink_0:
151             ipv4:
152               id: 2
153                 outer_l2:
154                   framesize:
155                     64B: 10
156                     128B: 20
157                     ...
158
159         This function normalizes the sum of framesize weights to 100 and
160         returns a dictionary of frame sizes in bytes and weight in percentage.
161         E.g.:
162           imix_count = {64: 25, 128: 75}
163
164         The weight mode is described in [1]. There are two ways to describe the
165         weight of the packets:
166           - Distribution in packets: the weight defines the percentage of
167             packets sent per packet size. IXIA uses this definition.
168           - Distribution in bytes: the weight defines the percentage of bytes
169             sent per packet size.
170
171         Packet size  # packets  D. in packets  Bytes  D. in bytes
172         40           7          58.33%         280    7%
173         576          4          33.33%         2304   56%
174         1500         1          8.33%          1500   37%
175
176         [1] https://en.wikipedia.org/wiki/Internet_Mix
177
178         :param imix: (dict) IMIX size and weight
179         """
180         imix_count = {}
181         if not imix:
182             return imix_count
183
184         imix_count = {size.upper().replace('B', ''): int(weight)
185                       for size, weight in imix.items()}
186         imix_sum = sum(imix_count.values())
187         if imix_sum <= 0:
188             imix_count = {64: 100}
189             imix_sum = 100
190
191         weight_normalize = float(imix_sum) / 100
192         imix_dip = {size: float(weight) / weight_normalize
193                     for size, weight in imix_count.items()}
194
195         if weight_mode == constants.DISTRIBUTION_IN_BYTES:
196             return imix_dip
197
198         byte_total = sum([int(size) * weight
199                           for size, weight in imix_dip.items()])
200         return {size: (int(size) * weight * 100) / byte_total
201                 for size, weight in imix_dip.items()}
202
203     def _create_vm(self, packet_definition):
204         """Create the STL Raw instructions"""
205         self.ether_packet = Pkt.Ether()
206         self.ip_packet = Pkt.IP()
207         self.ip6_packet = None
208         self.udp_packet = Pkt.UDP()
209         self.udp[DST_PORT] = 'UDP.dport'
210         self.udp[SRC_PORT] = 'UDP.sport'
211         self.qinq = False
212         self.vm_flow_vars = []
213         outer_l2 = packet_definition.get('outer_l2')
214         outer_l3v4 = packet_definition.get('outer_l3v4')
215         outer_l3v6 = packet_definition.get('outer_l3v6')
216         outer_l4 = packet_definition.get('outer_l4')
217         if outer_l2:
218             self._set_outer_l2_fields(outer_l2)
219         if outer_l3v4:
220             self._set_outer_l3v4_fields(outer_l3v4)
221         if outer_l3v6:
222             self._set_outer_l3v6_fields(outer_l3v6)
223         if outer_l4:
224             self._set_outer_l4_fields(outer_l4)
225         self.trex_vm = trex_stl_packet_builder_scapy.STLScVmRaw(
226             self.vm_flow_vars)
227
228     def _create_single_packet(self, size=64):
229         size -= 4
230         ether_packet = self.ether_packet
231         ip_packet = self.ip6_packet if self.ip6_packet else self.ip_packet
232         udp_packet = self.udp_packet
233         if self.qinq:
234             qinq_packet = self.qinq_packet
235             base_pkt = ether_packet / qinq_packet / ip_packet / udp_packet
236         else:
237             base_pkt = ether_packet / ip_packet / udp_packet
238         pad = max(0, size - len(base_pkt)) * 'x'
239         return trex_stl_packet_builder_scapy.STLPktBuilder(
240             pkt=base_pkt / pad, vm=self.trex_vm)
241
242     def _create_streams(self, imix_data, rate, port_pg_id, enable_latency):
243         """Create a list of streams per packet size
244
245         The STL TX mode speed of the generated streams will depend on the frame
246         weight and the frame rate. Both the frame weight and the total frame
247         rate are normalized to 100. The STL TX mode speed, defined in
248         percentage, is the combitation of both percentages. E.g.:
249           frame weight = 100
250           rate = 90
251             --> STLTXmode percentage = 10 (%)
252
253           frame weight = 80
254           rate = 50
255             --> STLTXmode percentage = 40 (%)
256
257         :param imix_data: (dict) IMIX size and weight
258         :param rate: (float) normalized [0..100] total weight
259         :param pg_id: (PortPgIDMap) port / pg_id (list) map
260         """
261         streams = []
262         for size, weight in ((int(size), float(weight)) for (size, weight)
263                              in imix_data.items() if float(weight) > 0):
264             packet = self._create_single_packet(size)
265             pg_id = port_pg_id.increase_pg_id()
266             stl_flow = (trex_stl_streams.STLFlowLatencyStats(pg_id=pg_id) if
267                         enable_latency else None)
268             mode = trex_stl_streams.STLTXCont(percentage=weight * rate / 100)
269             streams.append(trex_stl_client.STLStream(
270                 packet=packet, flow_stats=stl_flow, mode=mode))
271         return streams
272
273     def get_drop_percentage(self, samples, tol_low, tol_high,
274                             correlated_traffic):
275         """Calculate the drop percentage and run the traffic"""
276         completed = False
277         out_pkt_end = sum(port['out_packets'] for port in samples[-1].values())
278         in_pkt_end = sum(port['in_packets'] for port in samples[-1].values())
279         out_pkt_ini = sum(port['out_packets'] for port in samples[0].values())
280         in_pkt_ini = sum(port['in_packets'] for port in samples[0].values())
281         time_diff = (list(samples[-1].values())[0]['timestamp'] -
282                      list(samples[0].values())[0]['timestamp']).total_seconds()
283         out_packets = out_pkt_end - out_pkt_ini
284         in_packets = in_pkt_end - in_pkt_ini
285         tx_rate_fps = float(out_packets) / time_diff
286         rx_rate_fps = float(in_packets) / time_diff
287         drop_percent = 100.0
288
289         # https://tools.ietf.org/html/rfc2544#section-26.3
290         if out_packets:
291             drop_percent = round(
292                 (float(abs(out_packets - in_packets)) / out_packets) * 100, 5)
293
294         tol_high = max(tol_high, self.TOLERANCE_LIMIT)
295         tol_low = min(tol_low, self.TOLERANCE_LIMIT)
296         if drop_percent > tol_high:
297             self.max_rate = self.rate
298         elif drop_percent < tol_low:
299             self.min_rate = self.rate
300         else:
301             completed = True
302
303         last_rate = self.rate
304         self.rate = round(float(self.max_rate + self.min_rate) / 2.0, 5)
305
306         throughput = rx_rate_fps * 2 if correlated_traffic else rx_rate_fps
307
308         if drop_percent > self.drop_percent_max:
309             self.drop_percent_max = drop_percent
310
311         latency = {port_num: value['latency']
312                    for port_num, value in samples[-1].items()}
313
314         output = {
315             'TxThroughput': tx_rate_fps,
316             'RxThroughput': rx_rate_fps,
317             'CurrentDropPercentage': drop_percent,
318             'Throughput': throughput,
319             'DropPercentage': self.drop_percent_max,
320             'Rate': last_rate,
321             'Latency': latency
322         }
323         return completed, output