0e1dbd592d524afd55a36bdb37eba79cc4f01b06
[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.network_services.traffic_profile import trex_traffic_profile
23
24
25 LOGGING = logging.getLogger(__name__)
26 SRC_PORT = 'sport'
27 DST_PORT = 'dport'
28
29
30 class PortPgIDMap(object):
31     """Port and pg_id mapping class
32
33     "pg_id" is the identification STL library gives to each stream. In the
34     RFC2544Profile class, the traffic has a STLProfile per port, which contains
35     one or several streams, one per packet size defined in the IMIX test case
36     description.
37
38     Example of port <-> pg_id map:
39         self._port_pg_id_map = {
40             0: [1, 2, 3, 4],
41             1: [5, 6, 7, 8]
42         }
43     """
44
45     def __init__(self):
46         self._pg_id = 0
47         self._last_port = None
48         self._port_pg_id_map = {}
49
50     def add_port(self, port):
51         self._last_port = port
52         self._port_pg_id_map[port] = []
53
54     def get_pg_ids(self, port):
55         return self._port_pg_id_map.get(port, [])
56
57     def increase_pg_id(self, port=None):
58         port = self._last_port if not port else port
59         if port is None:
60             return
61         pg_id_list = self._port_pg_id_map.get(port)
62         if not pg_id_list:
63             self.add_port(port)
64             pg_id_list = self._port_pg_id_map[port]
65         self._pg_id += 1
66         pg_id_list.append(self._pg_id)
67         return self._pg_id
68
69
70 class RFC2544Profile(trex_traffic_profile.TrexProfile):
71     """TRex RFC2544 traffic profile"""
72
73     TOLERANCE_LIMIT = 0.05
74
75     def __init__(self, traffic_generator):
76         super(RFC2544Profile, self).__init__(traffic_generator)
77         self.generator = None
78         self.rate = self.config.frame_rate
79         self.max_rate = self.config.frame_rate
80         self.min_rate = 0
81         self.drop_percent_max = 0
82
83     def register_generator(self, generator):
84         self.generator = generator
85
86     def stop_traffic(self, traffic_generator=None):
87         """"Stop traffic injection, reset counters and remove streams"""
88         if traffic_generator is not None and self.generator is None:
89             self.generator = traffic_generator
90
91         self.generator.client.stop()
92         self.generator.client.reset()
93         self.generator.client.remove_all_streams()
94
95     def execute_traffic(self, traffic_generator=None):
96         """Generate the stream and run traffic on the given ports
97
98         :param traffic_generator: (TrexTrafficGenRFC) traffic generator
99         :return ports: (list of int) indexes of ports
100                 port_pg_id: (dict) port indexes and pg_id [1] map
101         [1] https://trex-tgn.cisco.com/trex/doc/cp_stl_docs/api/
102             profile_code.html#stlstream-modes
103         """
104         if traffic_generator is not None and self.generator is None:
105             self.generator = traffic_generator
106
107         port_pg_id = PortPgIDMap()
108         ports = []
109         for vld_id, intfs in sorted(self.generator.networks.items()):
110             profile_data = self.params.get(vld_id)
111             if not profile_data:
112                 continue
113             if (vld_id.startswith(self.DOWNLINK) and
114                     self.generator.rfc2544_helper.correlated_traffic):
115                 continue
116             for intf in intfs:
117                 port_num = int(self.generator.port_num(intf))
118                 ports.append(port_num)
119                 port_pg_id.add_port(port_num)
120                 profile = self._create_profile(profile_data,
121                                                self.rate, port_pg_id)
122                 self.generator.client.add_streams(profile, ports=[port_num])
123
124         self.generator.client.start(ports=ports,
125                                     duration=self.config.duration,
126                                     force=True)
127         return ports, port_pg_id
128
129     def _create_profile(self, profile_data, rate, port_pg_id):
130         """Create a STL profile (list of streams) for a port"""
131         streams = []
132         for packet_name in profile_data:
133             imix = (profile_data[packet_name].
134                     get('outer_l2', {}).get('framesize'))
135             imix_data = self._create_imix_data(imix)
136             self._create_vm(profile_data[packet_name])
137             _streams = self._create_streams(imix_data, rate, port_pg_id)
138             streams.extend(_streams)
139         return trex_stl_streams.STLProfile(streams)
140
141     def _create_imix_data(self, imix):
142         """Generate the IMIX distribution for a STL profile
143
144         The input information is the framesize dictionary in a test case
145         traffic profile definition. E.g.:
146           downlink_0:
147             ipv4:
148               id: 2
149                 outer_l2:
150                   framesize:
151                     64B: 10
152                     128B: 20
153                     ...
154
155         This function normalizes the sum of framesize weights to 100 and
156         returns a dictionary of frame sizes in bytes and weight in percentage.
157         E.g.:
158           imix_count = {64: 25, 128: 75}
159
160         :param imix: (dict) IMIX size and weight
161         """
162         imix_count = {}
163         if not imix:
164             return imix_count
165
166         imix_count = {size.upper().replace('B', ''): int(weight)
167                       for size, weight in imix.items()}
168         imix_sum = sum(imix_count.values())
169         if imix_sum <= 0:
170             imix_count = {64: 100}
171             imix_sum = 100
172
173         weight_normalize = float(imix_sum) / 100
174         return {size: float(weight) / weight_normalize
175                 for size, weight in imix_count.items()}
176
177     def _create_vm(self, packet_definition):
178         """Create the STL Raw instructions"""
179         self.ether_packet = Pkt.Ether()
180         self.ip_packet = Pkt.IP()
181         self.ip6_packet = None
182         self.udp_packet = Pkt.UDP()
183         self.udp[DST_PORT] = 'UDP.dport'
184         self.udp[SRC_PORT] = 'UDP.sport'
185         self.qinq = False
186         self.vm_flow_vars = []
187         outer_l2 = packet_definition.get('outer_l2')
188         outer_l3v4 = packet_definition.get('outer_l3v4')
189         outer_l3v6 = packet_definition.get('outer_l3v6')
190         outer_l4 = packet_definition.get('outer_l4')
191         if outer_l2:
192             self._set_outer_l2_fields(outer_l2)
193         if outer_l3v4:
194             self._set_outer_l3v4_fields(outer_l3v4)
195         if outer_l3v6:
196             self._set_outer_l3v6_fields(outer_l3v6)
197         if outer_l4:
198             self._set_outer_l4_fields(outer_l4)
199         self.trex_vm = trex_stl_packet_builder_scapy.STLScVmRaw(
200             self.vm_flow_vars)
201
202     def _create_single_packet(self, size=64):
203         size -= 4
204         ether_packet = self.ether_packet
205         ip_packet = self.ip6_packet if self.ip6_packet else self.ip_packet
206         udp_packet = self.udp_packet
207         if self.qinq:
208             qinq_packet = self.qinq_packet
209             base_pkt = ether_packet / qinq_packet / ip_packet / udp_packet
210         else:
211             base_pkt = ether_packet / ip_packet / udp_packet
212         pad = max(0, size - len(base_pkt)) * 'x'
213         return trex_stl_packet_builder_scapy.STLPktBuilder(
214             pkt=base_pkt / pad, vm=self.trex_vm)
215
216     def _create_streams(self, imix_data, rate, port_pg_id):
217         """Create a list of streams per packet size
218
219         The STL TX mode speed of the generated streams will depend on the frame
220         weight and the frame rate. Both the frame weight and the total frame
221         rate are normalized to 100. The STL TX mode speed, defined in
222         percentage, is the combitation of both percentages. E.g.:
223           frame weight = 100
224           rate = 90
225             --> STLTXmode percentage = 10 (%)
226
227           frame weight = 80
228           rate = 50
229             --> STLTXmode percentage = 40 (%)
230
231         :param imix_data: (dict) IMIX size and weight
232         :param rate: (float) normalized [0..100] total weight
233         :param pg_id: (PortPgIDMap) port / pg_id (list) map
234         """
235         streams = []
236         for size, weight in ((int(size), float(weight)) for (size, weight)
237                              in imix_data.items() if float(weight) > 0):
238             packet = self._create_single_packet(size)
239             pg_id = port_pg_id.increase_pg_id()
240             stl_flow = trex_stl_streams.STLFlowLatencyStats(pg_id=pg_id)
241             mode = trex_stl_streams.STLTXCont(percentage=weight * rate / 100)
242             streams.append(trex_stl_client.STLStream(
243                 packet=packet, flow_stats=stl_flow, mode=mode))
244         return streams
245
246     def get_drop_percentage(self, samples, tol_low, tol_high,
247                             correlated_traffic):
248         """Calculate the drop percentage and run the traffic"""
249         tx_rate_fps = 0
250         rx_rate_fps = 0
251         for sample in samples:
252             tx_rate_fps += sum(
253                 port['tx_throughput_fps'] for port in sample.values())
254             rx_rate_fps += sum(
255                 port['rx_throughput_fps'] for port in sample.values())
256         tx_rate_fps = round(float(tx_rate_fps) / len(samples), 2)
257         rx_rate_fps = round(float(rx_rate_fps) / len(samples), 2)
258
259         # TODO(esm): RFC2544 doesn't tolerate packet loss, why do we?
260         out_packets = sum(port['out_packets'] for port in samples[-1].values())
261         in_packets = sum(port['in_packets'] for port in samples[-1].values())
262         drop_percent = 100.0
263
264         # https://tools.ietf.org/html/rfc2544#section-26.3
265         if out_packets:
266             drop_percent = round(
267                 (float(abs(out_packets - in_packets)) / out_packets) * 100, 5)
268
269         tol_high = tol_high if tol_high > self.TOLERANCE_LIMIT else tol_high
270         tol_low = tol_low if tol_low > self.TOLERANCE_LIMIT else tol_low
271         if drop_percent > tol_high:
272             self.max_rate = self.rate
273         elif drop_percent < tol_low:
274             self.min_rate = self.rate
275         # else:
276             # NOTE(ralonsoh): the test should finish here
277             # pass
278         last_rate = self.rate
279         self.rate = round(float(self.max_rate + self.min_rate) / 2.0, 5)
280
281         throughput = rx_rate_fps * 2 if correlated_traffic else rx_rate_fps
282
283         if drop_percent > self.drop_percent_max:
284             self.drop_percent_max = drop_percent
285
286         latency = {port_num: value['latency']
287                    for port_num, value in samples[-1].items()}
288
289         output = {
290             'TxThroughput': tx_rate_fps,
291             'RxThroughput': rx_rate_fps,
292             'CurrentDropPercentage': drop_percent,
293             'Throughput': throughput,
294             'DropPercentage': self.drop_percent_max,
295             'Rate': last_rate,
296             'Latency': latency
297         }
298         return output