Merge "Improve SampleVNF hugepages setup"
[yardstick.git] / yardstick / network_services / traffic_profile / traffic_profile.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 struct
16 import socket
17 import logging
18 from random import SystemRandom
19 import ipaddress
20
21 import six
22
23 from yardstick.common import exceptions as y_exc
24 from yardstick.network_services.traffic_profile.base import TrafficProfile
25 from trex_stl_lib.trex_stl_client import STLStream
26 from trex_stl_lib.trex_stl_streams import STLFlowLatencyStats
27 from trex_stl_lib.trex_stl_streams import STLTXCont
28 from trex_stl_lib.trex_stl_streams import STLProfile
29 from trex_stl_lib.trex_stl_packet_builder_scapy import STLVmWrFlowVar
30 from trex_stl_lib.trex_stl_packet_builder_scapy import STLVmFlowVarRepeatableRandom
31 from trex_stl_lib.trex_stl_packet_builder_scapy import STLVmFlowVar
32 from trex_stl_lib.trex_stl_packet_builder_scapy import STLPktBuilder
33 from trex_stl_lib.trex_stl_packet_builder_scapy import STLScVmRaw
34 from trex_stl_lib.trex_stl_packet_builder_scapy import STLVmFixIpv4
35 from trex_stl_lib import api as Pkt
36
37 SRC = 'src'
38 DST = 'dst'
39 ETHERNET = 'Ethernet'
40 IP = 'IP'
41 IPv6 = 'IPv6'
42 UDP = 'UDP'
43 DSCP = 'DSCP'
44 SRC_PORT = 'sport'
45 DST_PORT = 'dport'
46 TYPE_OF_SERVICE = 'tos'
47
48 LOG = logging.getLogger(__name__)
49
50
51 class TrexProfile(TrafficProfile):
52     """ This class handles Trex Traffic profile generation and execution """
53
54     PROTO_MAP = {
55         ETHERNET: ('ether_packet', Pkt.Ether),
56         IP: ('ip_packet', Pkt.IP),
57         IPv6: ('ip6_packet', Pkt.IPv6),
58         UDP: ('udp_packet', Pkt.UDP),
59     }
60
61     def _general_single_action_partial(self, protocol):
62         def f(field):
63             def partial(value):
64                 kwargs = {
65                     field: value
66                 }
67                 self._set_proto_fields(protocol, **kwargs)
68             return partial
69         return f
70
71     def _ethernet_range_action_partial(self, direction, _):
72         def partial(min_value, max_value, count):
73             # pylint: disable=unused-argument
74             stl_vm_flow_var = STLVmFlowVar(name="mac_{}".format(direction),
75                                            min_value=1,
76                                            max_value=30,
77                                            size=4,
78                                            op='inc',
79                                            step=1)
80             self.vm_flow_vars.append(stl_vm_flow_var)
81             stl_vm_wr_flow_var = STLVmWrFlowVar(
82                 fv_name='mac_{}'.format(direction),
83                 pkt_offset='Ether.{}'.format(direction))
84             self.vm_flow_vars.append(stl_vm_wr_flow_var)
85         return partial
86
87     def _ip_range_action_partial(self, direction, count=1):
88         # pylint: disable=unused-argument
89         def partial(min_value, max_value, count):
90             _, _, actual_count = self._count_ip(min_value, max_value)
91             if not actual_count:
92                 count = 1
93             elif actual_count < int(count):
94                 count = actual_count
95
96             stl_vm_flow_var = STLVmFlowVarRepeatableRandom(
97                 name="ip4_{}".format(direction),
98                 min_value=min_value,
99                 max_value=max_value,
100                 size=4,
101                 limit=int(count),
102                 seed=0x1235)
103             self.vm_flow_vars.append(stl_vm_flow_var)
104             stl_vm_wr_flow_var = STLVmWrFlowVar(
105                 fv_name='ip4_{}'.format(direction),
106                 pkt_offset='IP.{}'.format(direction))
107             self.vm_flow_vars.append(stl_vm_wr_flow_var)
108             stl_vm_fix_ipv4 = STLVmFixIpv4(offset="IP")
109             self.vm_flow_vars.append(stl_vm_fix_ipv4)
110         return partial
111
112     def _ip6_range_action_partial(self, direction, _):
113         def partial(min_value, max_value, count):
114             # pylint: disable=unused-argument
115             min_value, max_value, _ = self._count_ip(min_value, max_value)
116             stl_vm_flow_var = STLVmFlowVar(name="ip6_{}".format(direction),
117                                            min_value=min_value,
118                                            max_value=max_value,
119                                            size=8,
120                                            op='random',
121                                            step=1)
122             self.vm_flow_vars.append(stl_vm_flow_var)
123             stl_vm_wr_flow_var = STLVmWrFlowVar(
124                 fv_name='ip6_{}'.format(direction),
125                 pkt_offset='IPv6.{}'.format(direction),
126                 offset_fixup=8)
127             self.vm_flow_vars.append(stl_vm_wr_flow_var)
128         return partial
129
130     def _dscp_range_action_partial(self, *_):
131         def partial(min_value, max_value, count):
132             # pylint: disable=unused-argument
133             stl_vm_flow_var = STLVmFlowVar(name="dscp",
134                                            min_value=min_value,
135                                            max_value=max_value,
136                                            size=2,
137                                            op='inc',
138                                            step=8)
139             self.vm_flow_vars.append(stl_vm_flow_var)
140             stl_vm_wr_flow_var = STLVmWrFlowVar(fv_name='dscp',
141                                                 pkt_offset='IP.tos')
142             self.vm_flow_vars.append(stl_vm_wr_flow_var)
143         return partial
144
145     def _udp_range_action_partial(self, field, count=1):
146         # pylint: disable=unused-argument
147         def partial(min_value, max_value, count):
148             actual_count = int(max_value) - int(min_value)
149             if not actual_count:
150                 count = 1
151             elif int(count) > actual_count:
152                 count = actual_count
153
154             stl_vm_flow_var = STLVmFlowVarRepeatableRandom(
155                 name="port_{}".format(field),
156                 min_value=min_value,
157                 max_value=max_value,
158                 size=2,
159                 limit=int(count),
160                 seed=0x1235)
161             self.vm_flow_vars.append(stl_vm_flow_var)
162             stl_vm_wr_flow_var = STLVmWrFlowVar(
163                 fv_name='port_{}'.format(field),
164                 pkt_offset=self.udp[field])
165             self.vm_flow_vars.append(stl_vm_wr_flow_var)
166         return partial
167
168     def __init__(self, yaml_data):
169         super(TrexProfile, self).__init__(yaml_data)
170         self.flows = 100
171         self.pps = 100
172         self.pg_id = 0
173         self.first_run = True
174         self.streams = 1
175         self.profile_data = []
176         self.profile = None
177         self.base_pkt = None
178         self.fsize = None
179         self.trex_vm = None
180         self.vms = []
181         self.rate = None
182         self.ether_packet = None
183         self.ip_packet = None
184         self.ip6_packet = None
185         self.udp_packet = None
186         self.udp = {
187             SRC_PORT: '',
188             DST_PORT: '',
189         }
190         self.qinq_packet = None
191         self.qinq = False
192         self.vm_flow_vars = []
193         self.packets = []
194
195         self._map_proto_actions = {
196             # the tuple is (single value function, range value function, if the values should be
197             # converted to integer).
198             ETHERNET: (self._general_single_action_partial(ETHERNET),
199                        self._ethernet_range_action_partial,
200                        False,
201                        ),
202             IP: (self._general_single_action_partial(IP),
203                  self._ip_range_action_partial,
204                  False,
205                  ),
206             IPv6: (self._general_single_action_partial(IPv6),
207                    self._ip6_range_action_partial,
208                    False,
209                    ),
210             DSCP: (self._general_single_action_partial(IP),
211                    self._dscp_range_action_partial,
212                    True,
213                    ),
214             UDP: (self._general_single_action_partial(UDP),
215                   self._udp_range_action_partial,
216                   True,
217                   ),
218         }
219
220     def execute_traffic(self, traffic_generator):
221         """ Generate the stream and run traffic on the given ports """
222         raise NotImplementedError()
223
224     def _call_on_range(self, range, single_action, range_action, count=1, to_int=False):
225         def convert_to_int(val):
226             return int(val) if to_int else val
227
228         range_iter = iter(str(range).split('-'))
229         min_value = convert_to_int(next(range_iter))
230         try:
231             max_value = convert_to_int(next(range_iter))
232         except StopIteration:
233             single_action(min_value)
234         else:
235             range_action(min_value=min_value, max_value=max_value, count=count)
236
237     def _set_proto_addr(self, protocol, field, address, count=1):
238         single_action, range_action, to_int = self._map_proto_actions[protocol]
239         self._call_on_range(address,
240                             single_action(field),
241                             range_action(field, count),
242                             count=count,
243                             to_int=to_int,
244                             )
245
246     def _set_proto_fields(self, protocol, **kwargs):
247         _attr_name, _class = self.PROTO_MAP[protocol]
248
249         if not getattr(self, _attr_name):
250             setattr(self, _attr_name, _class())
251
252         _attr = getattr(self, _attr_name)
253         for key, value in six.iteritems(kwargs):
254             setattr(_attr, key, value)
255
256     def set_svlan_cvlan(self, svlan, cvlan):
257         """ set svlan & cvlan """
258         self.qinq = True
259         ether_params = {'type': 0x8100}
260         self._set_proto_fields(ETHERNET, **ether_params)
261         svlans = str(svlan['id']).split('-')
262         svlan_min = int(svlans[0])
263         svlan_max = int(svlans[1]) if len(svlans) == 2 else int(svlans[0])
264         if len(svlans) == 2:
265             svlan = self._get_random_value(svlan_min, svlan_max)
266         else:
267             svlan = svlan_min
268         cvlans = str(cvlan['id']).split('-')
269         cvlan_min = int(cvlans[0])
270         cvlan_max = int(cvlans[1]) if len(cvlans) == 2 else int(cvlans[0])
271         if len(cvlans) == 2:
272             cvlan = self._get_random_value(cvlan_min, cvlan_max)
273         else:
274             cvlan = cvlan_min
275         self.qinq_packet = Pkt.Dot1Q(vlan=svlan) / Pkt.Dot1Q(vlan=cvlan)
276
277     def set_qinq(self, qinq):
278         """ set qinq in packet """
279         self.set_svlan_cvlan(qinq['S-VLAN'], qinq['C-VLAN'])
280
281     def _set_outer_l2_fields(self, outer_l2):
282         """ setup outer l2 fields from traffic profile """
283         ether_params = {'type': 0x800}
284         self._set_proto_fields(ETHERNET, **ether_params)
285         if 'srcmac' in outer_l2:
286             self._set_proto_addr(ETHERNET, SRC, outer_l2['srcmac'])
287         if 'dstmac' in outer_l2:
288             self._set_proto_addr(ETHERNET, DST, outer_l2['dstmac'])
289         if 'QinQ' in outer_l2:
290             self.set_qinq(outer_l2['QinQ'])
291
292     def _set_outer_l3v4_fields(self, outer_l3v4):
293         """ setup outer l3v4 fields from traffic profile """
294         ip_params = {}
295         if 'proto' in outer_l3v4:
296             ip_params['proto'] = socket.getprotobyname(outer_l3v4['proto'])
297             if outer_l3v4['proto'] == 'tcp':
298                 self.udp_packet = Pkt.TCP()
299                 self.udp[DST_PORT] = 'TCP.dport'
300                 self.udp[SRC_PORT] = 'TCP.sport'
301                 tcp_params = {'flags': '', 'window': 0}
302                 self._set_proto_fields(UDP, **tcp_params)
303         if 'ttl' in outer_l3v4:
304             ip_params['ttl'] = outer_l3v4['ttl']
305         self._set_proto_fields(IP, **ip_params)
306         if 'dscp' in outer_l3v4:
307             self._set_proto_addr(DSCP, TYPE_OF_SERVICE, outer_l3v4['dscp'])
308         if 'srcip4' in outer_l3v4:
309             self._set_proto_addr(IP, SRC, outer_l3v4['srcip4'], outer_l3v4['count'])
310         if 'dstip4' in outer_l3v4:
311             self._set_proto_addr(IP, DST, outer_l3v4['dstip4'], outer_l3v4['count'])
312
313     def _set_outer_l3v6_fields(self, outer_l3v6):
314         """ setup outer l3v6 fields from traffic profile """
315         ether_params = {'type': 0x86dd}
316         self._set_proto_fields(ETHERNET, **ether_params)
317         ip6_params = {}
318         if 'proto' in outer_l3v6:
319             ip6_params['proto'] = outer_l3v6['proto']
320             if outer_l3v6['proto'] == 'tcp':
321                 self.udp_packet = Pkt.TCP()
322                 self.udp[DST_PORT] = 'TCP.dport'
323                 self.udp[SRC_PORT] = 'TCP.sport'
324                 tcp_params = {'flags': '', 'window': 0}
325                 self._set_proto_fields(UDP, **tcp_params)
326         if 'ttl' in outer_l3v6:
327             ip6_params['ttl'] = outer_l3v6['ttl']
328         if 'tc' in outer_l3v6:
329             ip6_params['tc'] = outer_l3v6['tc']
330         if 'hlim' in outer_l3v6:
331             ip6_params['hlim'] = outer_l3v6['hlim']
332         self._set_proto_fields(IPv6, **ip6_params)
333         if 'srcip6' in outer_l3v6:
334             self._set_proto_addr(IPv6, SRC, outer_l3v6['srcip6'])
335         if 'dstip6' in outer_l3v6:
336             self._set_proto_addr(IPv6, DST, outer_l3v6['dstip6'])
337
338     def _set_outer_l4_fields(self, outer_l4):
339         """ setup outer l4 fields from traffic profile """
340         if 'srcport' in outer_l4:
341             self._set_proto_addr(UDP, SRC_PORT, outer_l4['srcport'], outer_l4['count'])
342         if 'dstport' in outer_l4:
343             self._set_proto_addr(UDP, DST_PORT, outer_l4['dstport'], outer_l4['count'])
344
345     def generate_imix_data(self, packet_definition):
346         """ generate packet size for a given traffic profile """
347         imix_count = {}
348         imix_data = {}
349         if not packet_definition:
350             return imix_count
351         imix = packet_definition.get('framesize')
352         if imix:
353             for size in imix:
354                 data = imix[size]
355                 imix_data[int(size[:-1])] = int(data)
356             imix_sum = sum(imix_data.values())
357             if imix_sum > 100:
358                 raise SystemExit("Error in IMIX data")
359             elif imix_sum < 100:
360                 imix_data[64] = imix_data.get(64, 0) + (100 - imix_sum)
361
362             avg_size = 0.0
363             for size in imix_data:
364                 count = int(imix_data[size])
365                 if count:
366                     avg_size += round(size * count / 100, 2)
367                     pps = round(self.pps * count / 100, 0)
368                     imix_count[size] = pps
369             self.rate = round(1342177280 / avg_size, 0) * 2
370             logging.debug("Imax: %s rate: %s", imix_count, self.rate)
371         return imix_count
372
373     def get_streams(self, profile_data):
374         """ generate trex stream
375         :param profile_data:
376         :type profile_data:
377         """
378         self.streams = []
379         self.pps = self.params['traffic_profile'].get('frame_rate', 100)
380         for packet_name in profile_data:
381             outer_l2 = profile_data[packet_name].get('outer_l2')
382             imix_data = self.generate_imix_data(outer_l2)
383             if not imix_data:
384                 imix_data = {64: self.pps}
385             self.generate_vm(profile_data[packet_name])
386             for size in imix_data:
387                 self._generate_streams(size, imix_data[size])
388         self._generate_profile()
389         return self.profile
390
391     def generate_vm(self, packet_definition):
392         """ generate  trex vm with flows setup """
393         self.ether_packet = Pkt.Ether()
394         self.ip_packet = Pkt.IP()
395         self.ip6_packet = None
396         self.udp_packet = Pkt.UDP()
397         self.udp[DST_PORT] = 'UDP.dport'
398         self.udp[SRC_PORT] = 'UDP.sport'
399         self.qinq = False
400         self.vm_flow_vars = []
401         outer_l2 = packet_definition.get('outer_l2', None)
402         outer_l3v4 = packet_definition.get('outer_l3v4', None)
403         outer_l3v6 = packet_definition.get('outer_l3v6', None)
404         outer_l4 = packet_definition.get('outer_l4', None)
405         if outer_l2:
406             self._set_outer_l2_fields(outer_l2)
407         if outer_l3v4:
408             self._set_outer_l3v4_fields(outer_l3v4)
409         if outer_l3v6:
410             self._set_outer_l3v6_fields(outer_l3v6)
411         if outer_l4:
412             self._set_outer_l4_fields(outer_l4)
413         self.trex_vm = STLScVmRaw(self.vm_flow_vars)
414
415     def generate_packets(self):
416         """ generate packets from trex TG """
417         base_pkt = self.base_pkt
418         size = self.fsize - 4
419         pad = max(0, size - len(base_pkt)) * 'x'
420         self.packets = [STLPktBuilder(pkt=base_pkt / pad,
421                                       vm=vm) for vm in self.vms]
422
423     def _create_single_packet(self, size=64):
424         size = size - 4
425         ether_packet = self.ether_packet
426         ip_packet = self.ip6_packet if self.ip6_packet else self.ip_packet
427         udp_packet = self.udp_packet
428         if self.qinq:
429             qinq_packet = self.qinq_packet
430             base_pkt = ether_packet / qinq_packet / ip_packet / udp_packet
431         else:
432             base_pkt = ether_packet / ip_packet / udp_packet
433         pad = max(0, size - len(base_pkt)) * 'x'
434         packet = STLPktBuilder(pkt=base_pkt / pad, vm=self.trex_vm)
435         return packet
436
437     def _create_single_stream(self, packet_size, pps, isg=0):
438         packet = self._create_single_packet(packet_size)
439         if self.pg_id:
440             self.pg_id += 1
441             stl_flow = STLFlowLatencyStats(pg_id=self.pg_id)
442             stream = STLStream(isg=isg, packet=packet, mode=STLTXCont(pps=pps),
443                                flow_stats=stl_flow)
444         else:
445             stream = STLStream(isg=isg, packet=packet, mode=STLTXCont(pps=pps))
446         return stream
447
448     def _generate_streams(self, packet_size, pps):
449         self.streams.append(self._create_single_stream(packet_size, pps))
450
451     def _generate_profile(self):
452         self.profile = STLProfile(self.streams)
453
454     @classmethod
455     def _count_ip(cls, start_ip, end_ip):
456         start = ipaddress.ip_address(six.u(start_ip))
457         end = ipaddress.ip_address(six.u(end_ip))
458         if start.version == 4:
459             return start, end, int(end) - int(start)
460         elif start.version == 6:
461             if int(start) > int(end):
462                 raise y_exc.IPv6RangeError(start_ip=str(start),
463                                            end_ip=str(end))
464             _, lo1 = struct.unpack('!QQ', start.packed)
465             _, lo2 = struct.unpack('!QQ', end.packed)
466             return lo1, lo2, lo2 - lo1
467
468     @classmethod
469     def _get_random_value(cls, min_port, max_port):
470         cryptogen = SystemRandom()
471         return cryptogen.randrange(min_port, max_port)