8f3698cb95f8f00dee10c7f9bd3ade22be30e88b
[yardstick.git] / yardstick / network_services / vnf_generic / vnf / tg_rfc2544_ixia.py
1 # Copyright (c) 2016-2019 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 ipaddress
16 import logging
17 import six
18
19 from yardstick.common import utils
20 from yardstick.common import exceptions
21 from yardstick.network_services.libs.ixia_libs.ixnet import ixnet_api
22 from yardstick.network_services.vnf_generic.vnf.sample_vnf import SampleVNFTrafficGen
23 from yardstick.network_services.vnf_generic.vnf.sample_vnf import ClientResourceHelper
24 from yardstick.network_services.vnf_generic.vnf.sample_vnf import Rfc2544ResourceHelper
25
26
27 LOG = logging.getLogger(__name__)
28
29 WAIT_AFTER_CFG_LOAD = 10
30 WAIT_FOR_TRAFFIC = 30
31 WAIT_PROTOCOLS_STARTED = 360
32
33
34 class IxiaBasicScenario(object):
35     """Ixia Basic scenario for flow from port to port"""
36
37     def __init__(self, client, context_cfg, ixia_cfg):
38
39         self.client = client
40         self.context_cfg = context_cfg
41         self.ixia_cfg = ixia_cfg
42
43         self._uplink_vports = None
44         self._downlink_vports = None
45
46     def apply_config(self):
47         pass
48
49     def run_protocols(self):
50         pass
51
52     def stop_protocols(self):
53         pass
54
55     def create_traffic_model(self, traffic_profile=None):
56         # pylint: disable=unused-argument
57         vports = self.client.get_vports()
58         self._uplink_vports = vports[::2]
59         self._downlink_vports = vports[1::2]
60         self.client.create_traffic_model(self._uplink_vports,
61                                          self._downlink_vports)
62
63
64 class IxiaL3Scenario(IxiaBasicScenario):
65     """Ixia scenario for L3 flow between static ip's"""
66
67     def _add_static_ips(self):
68         vports = self.client.get_vports()
69         uplink_intf_vport = [(self.client.get_static_interface(vport), vport)
70                              for vport in vports[::2]]
71         downlink_intf_vport = [(self.client.get_static_interface(vport), vport)
72                                for vport in vports[1::2]]
73
74         for index in range(len(uplink_intf_vport)):
75             intf, vport = uplink_intf_vport[index]
76             try:
77                 iprange = self.ixia_cfg['flow'].get('src_ip')[index]
78                 start_ip = utils.get_ip_range_start(iprange)
79                 count = utils.get_ip_range_count(iprange)
80                 self.client.add_static_ipv4(intf, vport, start_ip, count, '32')
81             except IndexError:
82                 raise exceptions.IncorrectFlowOption(
83                     option="src_ip", link="uplink_{}".format(index))
84
85             intf, vport = downlink_intf_vport[index]
86             try:
87                 iprange = self.ixia_cfg['flow'].get('dst_ip')[index]
88                 start_ip = utils.get_ip_range_start(iprange)
89                 count = utils.get_ip_range_count(iprange)
90                 self.client.add_static_ipv4(intf, vport, start_ip, count, '32')
91             except IndexError:
92                 raise exceptions.IncorrectFlowOption(
93                     option="dst_ip", link="downlink_{}".format(index))
94
95     def _add_interfaces(self):
96         vports = self.client.get_vports()
97         uplink_vports = (vport for vport in vports[::2])
98         downlink_vports = (vport for vport in vports[1::2])
99
100         ix_node = next(node for _, node in self.context_cfg['nodes'].items()
101                        if node['role'] == 'IxNet')
102
103         for intf in ix_node['interfaces'].values():
104             ip = intf.get('local_ip')
105             mac = intf.get('local_mac')
106             gateway = None
107             try:
108                 gateway = next(route.get('gateway')
109                                for route in ix_node.get('routing_table')
110                                if route.get('if') == intf.get('ifname'))
111             except StopIteration:
112                 LOG.debug("Gateway not provided")
113
114             if 'uplink' in intf.get('vld_id'):
115                 self.client.add_interface(next(uplink_vports),
116                                           ip, mac, gateway)
117             else:
118                 self.client.add_interface(next(downlink_vports),
119                                           ip, mac, gateway)
120
121     def apply_config(self):
122         self._add_interfaces()
123         self._add_static_ips()
124
125     def create_traffic_model(self, traffic_profile=None):
126         # pylint: disable=unused-argument
127         vports = self.client.get_vports()
128         self._uplink_vports = vports[::2]
129         self._downlink_vports = vports[1::2]
130
131         uplink_endpoints = [port + '/protocols/static'
132                             for port in self._uplink_vports]
133         downlink_endpoints = [port + '/protocols/static'
134                               for port in self._downlink_vports]
135
136         self.client.create_ipv4_traffic_model(uplink_endpoints,
137                                               downlink_endpoints)
138
139
140 class IxiaPppoeClientScenario(object):
141     def __init__(self, client, context_cfg, ixia_cfg):
142
143         self.client = client
144
145         self._uplink_vports = None
146         self._downlink_vports = None
147
148         self._access_topologies = []
149         self._core_topologies = []
150
151         self._context_cfg = context_cfg
152         self._ixia_cfg = ixia_cfg
153         self.protocols = []
154         self.device_groups = []
155
156     def apply_config(self):
157         vports = self.client.get_vports()
158         self._uplink_vports = vports[::2]
159         self._downlink_vports = vports[1::2]
160         self._fill_ixia_config()
161         self._apply_access_network_config()
162         self._apply_core_network_config()
163
164     def create_traffic_model(self, traffic_profile):
165         endpoints_id_pairs = self._get_endpoints_src_dst_id_pairs(
166             traffic_profile.full_profile)
167         endpoints_obj_pairs = \
168             self._get_endpoints_src_dst_obj_pairs(endpoints_id_pairs)
169         uplink_endpoints = endpoints_obj_pairs[::2]
170         downlink_endpoints = endpoints_obj_pairs[1::2]
171         self.client.create_ipv4_traffic_model(uplink_endpoints,
172                                               downlink_endpoints)
173
174     def run_protocols(self):
175         LOG.info('PPPoE Scenario - Start Protocols')
176         self.client.start_protocols()
177         utils.wait_until_true(
178             lambda: self.client.is_protocols_running(self.protocols),
179             timeout=WAIT_PROTOCOLS_STARTED, sleep=2)
180
181     def stop_protocols(self):
182         LOG.info('PPPoE Scenario - Stop Protocols')
183         self.client.stop_protocols()
184
185     def _get_intf_addr(self, intf):
186         """Retrieve interface IP address and mask
187
188         :param intf: could be the string which represents IP address
189         with mask (e.g 192.168.10.2/24) or a dictionary with the host
190         name and the port (e.g. {'tg__0': 'xe1'})
191         :return: (tuple) pair of ip address and mask
192         """
193         if isinstance(intf, six.string_types):
194             ip, mask = tuple(intf.split('/'))
195             return ip, int(mask)
196
197         node_name, intf_name = next(iter(intf.items()))
198         node = self._context_cfg["nodes"].get(node_name, {})
199         interface = node.get("interfaces", {})[intf_name]
200         ip = interface["local_ip"]
201         mask = interface["netmask"]
202         ipaddr = ipaddress.ip_network(six.text_type('{}/{}'.format(ip, mask)),
203                                       strict=False)
204         return ip, ipaddr.prefixlen
205
206     @staticmethod
207     def _get_endpoints_src_dst_id_pairs(flows_params):
208         """Get list of flows src/dst port pairs
209
210         Create list of flows src/dst port pairs based on traffic profile
211         flows data. Each uplink/downlink pair in traffic profile represents
212         specific flows between the pair of ports.
213
214         Example ('port' key represents port on which flow will be created):
215
216         Input flows data:
217         uplink_0:
218           ipv4:
219             id: 1
220             port: xe0
221         downlink_0:
222           ipv4:
223             id: 2
224             port: xe1
225         uplink_1:
226           ipv4:
227             id: 3
228             port: xe2
229         downlink_1:
230           ipv4:
231             id: 4
232             port: xe3
233
234         Result list: ['xe0', 'xe1', 'xe2', 'xe3']
235
236         Result list means that the following flows pairs will be created:
237         - uplink 0: port xe0 <-> port xe1
238         - downlink 0: port xe1 <-> port xe0
239         - uplink 1: port xe2 <-> port xe3
240         - downlink 1: port xe3 <-> port xe2
241
242         :param flows_params: ordered dict of traffic profile flows params
243         :return: (list) list of flows src/dst ports
244         """
245         if len(flows_params) % 2:
246             raise RuntimeError('Number of uplink/downlink pairs'
247                                ' in traffic profile is not equal')
248         endpoint_pairs = []
249         for flow in flows_params:
250             port = flows_params[flow]['ipv4'].get('port')
251             if port is None:
252                 continue
253             endpoint_pairs.append(port)
254         return endpoint_pairs
255
256     def _get_endpoints_src_dst_obj_pairs(self, endpoints_id_pairs):
257         """Create list of uplink/downlink device groups pairs
258
259         Based on traffic profile options, create list of uplink/downlink
260         device groups pairs between which flow groups will be created:
261
262         1. In case uplink/downlink flows in traffic profile doesn't have
263            specified 'port' key, flows will be created between each device
264            group on access port and device group on corresponding core port.
265            E.g.:
266            Device groups created on access port xe0: dg1, dg2, dg3
267            Device groups created on core port xe1: dg4
268            Flows will be created between:
269            dg1 -> dg4
270            dg4 -> dg1
271            dg2 -> dg4
272            dg4 -> dg2
273            dg3 -> dg4
274            dg4 -> dg3
275
276         2. In case uplink/downlink flows in traffic profile have specified
277            'port' key, flows will be created between device groups on this
278            port.
279            E.g., for the following traffic profile
280            uplink_0:
281              port: xe0
282            downlink_0:
283              port: xe1
284            uplink_1:
285              port: xe0
286            downlink_0:
287              port: xe3
288            Flows will be created between:
289            Port xe0 (dg1) -> Port xe1 (dg1)
290            Port xe1 (dg1) -> Port xe0 (dg1)
291            Port xe0 (dg2) -> Port xe3 (dg1)
292            Port xe3 (dg3) -> Port xe0 (dg1)
293
294         :param endpoints_id_pairs: (list) List of uplink/downlink flows ports
295          pairs
296         :return: (list) list of uplink/downlink device groups descriptors pairs
297         """
298         pppoe = self._ixia_cfg['pppoe_client']
299         sessions_per_port = pppoe['sessions_per_port']
300         sessions_per_svlan = pppoe['sessions_per_svlan']
301         svlan_count = int(sessions_per_port / sessions_per_svlan)
302
303         uplink_ports = [p['tg__0'] for p in self._ixia_cfg['flow']['src_ip']]
304         downlink_ports = [p['tg__0'] for p in self._ixia_cfg['flow']['dst_ip']]
305         uplink_port_topology_map = zip(uplink_ports, self._access_topologies)
306         downlink_port_topology_map = zip(downlink_ports, self._core_topologies)
307
308         port_to_dev_group_mapping = {}
309         for port, topology in uplink_port_topology_map:
310             topology_dgs = self.client.get_topology_device_groups(topology)
311             port_to_dev_group_mapping[port] = topology_dgs
312         for port, topology in downlink_port_topology_map:
313             topology_dgs = self.client.get_topology_device_groups(topology)
314             port_to_dev_group_mapping[port] = topology_dgs
315
316         uplink_endpoints = endpoints_id_pairs[::2]
317         downlink_endpoints = endpoints_id_pairs[1::2]
318
319         uplink_dev_groups = []
320         group_up = [uplink_endpoints[i:i + svlan_count]
321                     for i in range(0, len(uplink_endpoints), svlan_count)]
322
323         for group in group_up:
324             for i, port in enumerate(group):
325                 uplink_dev_groups.append(port_to_dev_group_mapping[port][i])
326
327         downlink_dev_groups = []
328         for port in downlink_endpoints:
329             downlink_dev_groups.append(port_to_dev_group_mapping[port][0])
330
331         endpoint_obj_pairs = []
332         [endpoint_obj_pairs.extend([up, down])
333          for up, down in zip(uplink_dev_groups, downlink_dev_groups)]
334
335         if not endpoint_obj_pairs:
336             for up, down in zip(uplink_ports, downlink_ports):
337                 uplink_dev_groups = port_to_dev_group_mapping[up]
338                 downlink_dev_groups = \
339                     port_to_dev_group_mapping[down] * len(uplink_dev_groups)
340                 [endpoint_obj_pairs.extend(list(i))
341                  for i in zip(uplink_dev_groups, downlink_dev_groups)]
342         return endpoint_obj_pairs
343
344     def _fill_ixia_config(self):
345         pppoe = self._ixia_cfg["pppoe_client"]
346         ipv4 = self._ixia_cfg["ipv4_client"]
347
348         _ip = [self._get_intf_addr(intf)[0] for intf in pppoe["ip"]]
349         self._ixia_cfg["pppoe_client"]["ip"] = _ip
350
351         _ip = [self._get_intf_addr(intf)[0] for intf in ipv4["gateway_ip"]]
352         self._ixia_cfg["ipv4_client"]["gateway_ip"] = _ip
353
354         addrs = [self._get_intf_addr(intf) for intf in ipv4["ip"]]
355         _ip = [addr[0] for addr in addrs]
356         _prefix = [addr[1] for addr in addrs]
357
358         self._ixia_cfg["ipv4_client"]["ip"] = _ip
359         self._ixia_cfg["ipv4_client"]["prefix"] = _prefix
360
361     def _apply_access_network_config(self):
362         pppoe = self._ixia_cfg["pppoe_client"]
363         sessions_per_port = pppoe['sessions_per_port']
364         sessions_per_svlan = pppoe['sessions_per_svlan']
365         svlan_count = int(sessions_per_port / sessions_per_svlan)
366
367         # add topology per uplink port (access network)
368         for access_tp_id, vport in enumerate(self._uplink_vports):
369             name = 'Topology access {}'.format(access_tp_id)
370             tp = self.client.add_topology(name, vport)
371             self._access_topologies.append(tp)
372             # add device group per svlan
373             for dg_id in range(svlan_count):
374                 s_vlan_id = int(pppoe['s_vlan']) + dg_id + access_tp_id * svlan_count
375                 s_vlan = ixnet_api.Vlan(vlan_id=s_vlan_id)
376                 c_vlan = ixnet_api.Vlan(vlan_id=pppoe['c_vlan'], vlan_id_step=1)
377                 name = 'SVLAN {}'.format(s_vlan_id)
378                 dg = self.client.add_device_group(tp, name, sessions_per_svlan)
379                 self.device_groups.append(dg)
380                 # add ethernet layer to device group
381                 ethernet = self.client.add_ethernet(dg, 'Ethernet')
382                 self.protocols.append(ethernet)
383                 self.client.add_vlans(ethernet, [s_vlan, c_vlan])
384                 # add ppp over ethernet
385                 if 'pap_user' in pppoe:
386                     ppp = self.client.add_pppox_client(ethernet, 'pap',
387                                                        pppoe['pap_user'],
388                                                        pppoe['pap_password'])
389                 else:
390                     ppp = self.client.add_pppox_client(ethernet, 'chap',
391                                                        pppoe['chap_user'],
392                                                        pppoe['chap_password'])
393                 self.protocols.append(ppp)
394
395     def _apply_core_network_config(self):
396         ipv4 = self._ixia_cfg["ipv4_client"]
397         sessions_per_port = ipv4['sessions_per_port']
398         sessions_per_vlan = ipv4['sessions_per_vlan']
399         vlan_count = int(sessions_per_port / sessions_per_vlan)
400
401         # add topology per downlink port (core network)
402         for core_tp_id, vport in enumerate(self._downlink_vports):
403             name = 'Topology core {}'.format(core_tp_id)
404             tp = self.client.add_topology(name, vport)
405             self._core_topologies.append(tp)
406             # add device group per vlan
407             for dg_id in range(vlan_count):
408                 name = 'Core port {}'.format(core_tp_id)
409                 dg = self.client.add_device_group(tp, name, sessions_per_vlan)
410                 self.device_groups.append(dg)
411                 # add ethernet layer to device group
412                 ethernet = self.client.add_ethernet(dg, 'Ethernet')
413                 self.protocols.append(ethernet)
414                 if 'vlan' in ipv4:
415                     vlan_id = int(ipv4['vlan']) + dg_id + core_tp_id * vlan_count
416                     vlan = ixnet_api.Vlan(vlan_id=vlan_id)
417                     self.client.add_vlans(ethernet, [vlan])
418                 # add ipv4 layer
419                 gw_ip = ipv4['gateway_ip'][core_tp_id]
420                 # use gw addr to generate ip addr from the same network
421                 ip_addr = ipaddress.IPv4Address(gw_ip) + 1
422                 ipv4_obj = self.client.add_ipv4(ethernet, name='ipv4',
423                                                 addr=ip_addr,
424                                                 addr_step='0.0.0.1',
425                                                 prefix=ipv4['prefix'][core_tp_id],
426                                                 gateway=gw_ip)
427                 self.protocols.append(ipv4_obj)
428                 if ipv4.get("bgp"):
429                     bgp_peer_obj = self.client.add_bgp(ipv4_obj,
430                                                        dut_ip=ipv4["bgp"]["dut_ip"],
431                                                        local_as=ipv4["bgp"]["as_number"],
432                                                        bgp_type=ipv4["bgp"].get("bgp_type"))
433                     self.protocols.append(bgp_peer_obj)
434
435
436 class IxiaRfc2544Helper(Rfc2544ResourceHelper):
437
438     def is_done(self):
439         return self.latency and self.iteration.value > 10
440
441
442 class IxiaResourceHelper(ClientResourceHelper):
443
444     LATENCY_TIME_SLEEP = 120
445
446     def __init__(self, setup_helper, rfc_helper_type=None):
447         super(IxiaResourceHelper, self).__init__(setup_helper)
448         self.scenario_helper = setup_helper.scenario_helper
449
450         self._ixia_scenarios = {
451             "IxiaBasic": IxiaBasicScenario,
452             "IxiaL3": IxiaL3Scenario,
453             "IxiaPppoeClient": IxiaPppoeClientScenario,
454         }
455
456         self.client = ixnet_api.IxNextgen()
457
458         if rfc_helper_type is None:
459             rfc_helper_type = IxiaRfc2544Helper
460
461         self.rfc_helper = rfc_helper_type(self.scenario_helper)
462         self.uplink_ports = None
463         self.downlink_ports = None
464         self.context_cfg = None
465         self._ix_scenario = None
466         self._connect()
467
468     def _connect(self, client=None):
469         self.client.connect(self.vnfd_helper)
470
471     def get_stats(self, *args, **kwargs):
472         return self.client.get_statistics()
473
474     def setup(self):
475         super(IxiaResourceHelper, self).setup()
476         self._init_ix_scenario()
477
478     def stop_collect(self):
479         self._ix_scenario.stop_protocols()
480         self._terminated.value = 1
481
482     def generate_samples(self, ports, duration):
483         stats = self.get_stats()
484
485         samples = {}
486         # this is not DPDK port num, but this is whatever number we gave
487         # when we selected ports and programmed the profile
488         for port_num in ports:
489             try:
490                 # reverse lookup port name from port_num so the stats dict is descriptive
491                 intf = self.vnfd_helper.find_interface_by_port(port_num)
492                 port_name = intf['name']
493                 avg_latency = stats['Store-Forward_Avg_latency_ns'][port_num]
494                 min_latency = stats['Store-Forward_Min_latency_ns'][port_num]
495                 max_latency = stats['Store-Forward_Max_latency_ns'][port_num]
496                 samples[port_name] = {
497                     'rx_throughput_kps': float(stats['Rx_Rate_Kbps'][port_num]),
498                     'tx_throughput_kps': float(stats['Tx_Rate_Kbps'][port_num]),
499                     'rx_throughput_mbps': float(stats['Rx_Rate_Mbps'][port_num]),
500                     'tx_throughput_mbps': float(stats['Tx_Rate_Mbps'][port_num]),
501                     'in_packets': int(stats['Valid_Frames_Rx'][port_num]),
502                     'out_packets': int(stats['Frames_Tx'][port_num]),
503                     'RxThroughput': float(stats['Valid_Frames_Rx'][port_num]) / duration,
504                     'TxThroughput': float(stats['Frames_Tx'][port_num]) / duration,
505                     'Store-Forward_Avg_latency_ns': utils.safe_cast(avg_latency, int, 0),
506                     'Store-Forward_Min_latency_ns': utils.safe_cast(min_latency, int, 0),
507                     'Store-Forward_Max_latency_ns': utils.safe_cast(max_latency, int, 0)
508                 }
509             except IndexError:
510                 pass
511
512         return samples
513
514     def _init_ix_scenario(self):
515         ixia_config = self.scenario_helper.scenario_cfg.get('ixia_config', 'IxiaBasic')
516
517         if ixia_config in self._ixia_scenarios:
518             scenario_type = self._ixia_scenarios[ixia_config]
519
520             self._ix_scenario = scenario_type(self.client, self.context_cfg,
521                                               self.scenario_helper.scenario_cfg['options'])
522         else:
523             raise RuntimeError(
524                 "IXIA config type '{}' not supported".format(ixia_config))
525
526     def _initialize_client(self, traffic_profile):
527         """Initialize the IXIA IxNetwork client and configure the server"""
528         self.client.clear_config()
529         self.client.assign_ports()
530         self._ix_scenario.apply_config()
531         self._ix_scenario.create_traffic_model(traffic_profile)
532
533     def run_traffic(self, traffic_profile):
534         if self._terminated.value:
535             return
536
537         min_tol = self.rfc_helper.tolerance_low
538         max_tol = self.rfc_helper.tolerance_high
539         precision = self.rfc_helper.tolerance_precision
540         default = "00:00:00:00:00:00"
541
542         self._build_ports()
543         traffic_profile.update_traffic_profile(self)
544         self._initialize_client(traffic_profile)
545
546         mac = {}
547         for port_name in self.vnfd_helper.port_pairs.all_ports:
548             intf = self.vnfd_helper.find_interface(name=port_name)
549             virt_intf = intf["virtual-interface"]
550             # we only know static traffic id by reading the json
551             # this is used by _get_ixia_trafficrofile
552             port_num = self.vnfd_helper.port_num(intf)
553             mac["src_mac_{}".format(port_num)] = virt_intf.get("local_mac", default)
554             mac["dst_mac_{}".format(port_num)] = virt_intf.get("dst_mac", default)
555
556         self._ix_scenario.run_protocols()
557
558         try:
559             while not self._terminated.value:
560                 first_run = traffic_profile.execute_traffic(
561                     self, self.client, mac)
562                 self.client_started.value = 1
563                 # pylint: disable=unnecessary-lambda
564                 utils.wait_until_true(lambda: self.client.is_traffic_stopped(),
565                                       timeout=traffic_profile.config.duration * 2)
566                 samples = self.generate_samples(traffic_profile.ports,
567                                                 traffic_profile.config.duration)
568
569                 completed, samples = traffic_profile.get_drop_percentage(
570                     samples, min_tol, max_tol, precision, first_run=first_run)
571                 self._queue.put(samples)
572
573                 if completed:
574                     self._terminated.value = 1
575
576         except Exception:  # pylint: disable=broad-except
577             LOG.exception('Run Traffic terminated')
578
579         self._ix_scenario.stop_protocols()
580         self._terminated.value = 1
581
582     def collect_kpi(self):
583         self.rfc_helper.iteration.value += 1
584         return super(IxiaResourceHelper, self).collect_kpi()
585
586
587 class IxiaTrafficGen(SampleVNFTrafficGen):
588
589     APP_NAME = 'Ixia'
590
591     def __init__(self, name, vnfd, setup_env_helper_type=None, resource_helper_type=None):
592         if resource_helper_type is None:
593             resource_helper_type = IxiaResourceHelper
594
595         super(IxiaTrafficGen, self).__init__(name, vnfd, setup_env_helper_type,
596                                              resource_helper_type)
597         self._ixia_traffic_gen = None
598         self.ixia_file_name = ''
599         self.vnf_port_pairs = []
600
601     def _check_status(self):
602         pass
603
604     def terminate(self):
605         self.resource_helper.stop_collect()
606         super(IxiaTrafficGen, self).terminate()