Merge "Add QinQ support for IXIA traffic profile"
[yardstick.git] / yardstick / network_services / libs / ixia_libs / ixnet / ixnet_api.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 ipaddress
16 import logging
17
18 import IxNetwork
19
20 from yardstick.common import exceptions
21 from yardstick.common import utils
22 from yardstick.network_services.traffic_profile import base as tp_base
23
24
25 log = logging.getLogger(__name__)
26
27 IP_VERSION_4 = 4
28 IP_VERSION_6 = 6
29
30 PROTO_ETHERNET = 'ethernet'
31 PROTO_IPV4 = 'ipv4'
32 PROTO_IPV6 = 'ipv6'
33 PROTO_UDP = 'udp'
34 PROTO_TCP = 'tcp'
35 PROTO_VLAN = 'vlan'
36
37 SINGLE_VALUE = "singleValue"
38
39 S_VLAN = 0
40 C_VLAN = 1
41
42 ETHER_TYPE_802_1ad = '0x88a8'
43
44 IP_VERSION_4_MASK = 24
45 IP_VERSION_6_MASK = 64
46
47 TRAFFIC_STATUS_STARTED = 'started'
48 TRAFFIC_STATUS_STOPPED = 'stopped'
49
50 SUPPORTED_PROTO = [PROTO_UDP]
51
52
53 # NOTE(ralonsoh): this pragma will be removed in the last patch of this series
54 class IxNextgen(object):  # pragma: no cover
55
56     PORT_STATS_NAME_MAP = {
57         "stat_name": 'Stat Name',
58         "Frames_Tx": 'Frames Tx.',
59         "Valid_Frames_Rx": 'Valid Frames Rx.',
60         "Frames_Tx_Rate": 'Frames Tx. Rate',
61         "Valid_Frames_Rx_Rate": 'Valid Frames Rx. Rate',
62         "Tx_Rate_Kbps": 'Tx. Rate (Kbps)',
63         "Rx_Rate_Kbps": 'Rx. Rate (Kbps)',
64         "Tx_Rate_Mbps": 'Tx. Rate (Mbps)',
65         "Rx_Rate_Mbps": 'Rx. Rate (Mbps)',
66     }
67
68     LATENCY_NAME_MAP = {
69         "Store-Forward_Avg_latency_ns": 'Store-Forward Avg Latency (ns)',
70         "Store-Forward_Min_latency_ns": 'Store-Forward Min Latency (ns)',
71         "Store-Forward_Max_latency_ns": 'Store-Forward Max Latency (ns)',
72     }
73
74     @staticmethod
75     def get_config(tg_cfg):
76         card = []
77         port = []
78         external_interface = tg_cfg["vdu"][0]["external-interface"]
79         for intf in external_interface:
80             card_port0 = intf["virtual-interface"]["vpci"]
81             card0, port0 = card_port0.split(':')[:2]
82             card.append(card0)
83             port.append(port0)
84
85         cfg = {
86             'machine': tg_cfg["mgmt-interface"]["ip"],
87             'port': tg_cfg["mgmt-interface"]["tg-config"]["tcl_port"],
88             'chassis': tg_cfg["mgmt-interface"]["tg-config"]["ixchassis"],
89             'cards': card,
90             'ports': port,
91             'output_dir': tg_cfg["mgmt-interface"]["tg-config"]["dut_result_dir"],
92             'version': tg_cfg["mgmt-interface"]["tg-config"]["version"],
93             'bidir': True,
94         }
95
96         return cfg
97
98     def __init__(self):  # pragma: no cover
99         self._ixnet = None
100         self._cfg = None
101         self._params = None
102         self._bidir = None
103
104     @property
105     def ixnet(self):  # pragma: no cover
106         if self._ixnet:
107             return self._ixnet
108         raise exceptions.IxNetworkClientNotConnected()
109
110     def _get_config_element_by_flow_group_name(self, flow_group_name):
111         """Get a config element using the flow group name
112
113         Each named flow group contains one config element (by configuration).
114         According to the documentation, "configElements" is a list and "each
115         item in this list is aligned to the sequential order of your endpoint
116         list".
117
118         :param flow_group_name: (str) flow group name; this parameter is
119                                 always a number (converted to string) starting
120                                 from "1".
121         :return: (str) config element reference ID or None.
122         """
123         traffic_item = self.ixnet.getList(self.ixnet.getRoot() + '/traffic',
124                                           'trafficItem')[0]
125         flow_groups = self.ixnet.getList(traffic_item, 'endpointSet')
126         for flow_group in flow_groups:
127             if (str(self.ixnet.getAttribute(flow_group, '-name')) ==
128                     flow_group_name):
129                 return traffic_item + '/configElement:' + flow_group_name
130
131     def _get_stack_item(self, flow_group_name, protocol_name):
132         """Return the stack item given the flow group name and the proto name
133
134         :param flow_group_name: (str) flow group name
135         :param protocol_name: (str) protocol name, referred to PROTO_*
136                               constants
137         :return: list of stack item descriptors
138         """
139         celement = self._get_config_element_by_flow_group_name(flow_group_name)
140         if not celement:
141             raise exceptions.IxNetworkFlowNotPresent(
142                 flow_group=flow_group_name)
143         stack_items = self.ixnet.getList(celement, 'stack')
144         return [s_i for s_i in stack_items if protocol_name in s_i]
145
146     def _get_field_in_stack_item(self, stack_item, field_name):
147         """Return the field in a stack item given the name
148
149         :param stack_item: (str) stack item descriptor
150         :param field_name: (str) field name
151         :return: (str) field descriptor
152         """
153         fields = self.ixnet.getList(stack_item, 'field')
154         for field in (field for field in fields if field_name in field):
155             return field
156         raise exceptions.IxNetworkFieldNotPresentInStackItem(
157             field_name=field_name, stack_item=stack_item)
158
159     def _get_traffic_state(self):
160         """Get traffic state"""
161         return self.ixnet.getAttribute(self.ixnet.getRoot() + 'traffic',
162                                        '-state')
163
164     def is_traffic_running(self):
165         """Returns true if traffic state == TRAFFIC_STATUS_STARTED"""
166         return self._get_traffic_state() == TRAFFIC_STATUS_STARTED
167
168     def is_traffic_stopped(self):
169         """Returns true if traffic state == TRAFFIC_STATUS_STOPPED"""
170         return self._get_traffic_state() == TRAFFIC_STATUS_STOPPED
171
172     @staticmethod
173     def _parse_framesize(framesize):
174         """Parse "framesize" config param. to return a list of weighted pairs
175
176         :param framesize: dictionary of frame sizes and weights
177         :return: list of paired frame sizes and weights
178         """
179         weighted_range_pairs = []
180         for size, weight in ((s, w) for (s, w) in framesize.items()
181                              if int(w) != 0):
182             size = int(size.upper().replace('B', ''))
183             weighted_range_pairs.append([size, size, int(weight)])
184         return weighted_range_pairs
185
186     def iter_over_get_lists(self, x1, x2, y2, offset=0):
187         for x in self.ixnet.getList(x1, x2):
188             y_list = self.ixnet.getList(x, y2)
189             for i, y in enumerate(y_list, offset):
190                 yield x, y, i
191
192     def connect(self, tg_cfg):
193         self._cfg = self.get_config(tg_cfg)
194         self._ixnet = IxNetwork.IxNet()
195
196         machine = self._cfg['machine']
197         port = str(self._cfg['port'])
198         version = str(self._cfg['version'])
199         return self.ixnet.connect(machine, '-port', port,
200                                   '-version', version)
201
202     def clear_config(self):
203         """Wipe out any possible configuration present in the client"""
204         self.ixnet.execute('newConfig')
205
206     def assign_ports(self):
207         """Create and assign vports for each physical port defined in config
208
209         This configuration is present in the IXIA profile file. E.g.:
210             name: trafficgen_1
211             role: IxNet
212             interfaces:
213                 xe0:
214                     vpci: "2:15" # Card:port
215                     driver: "none"
216                     dpdk_port_num: 0
217                     local_ip: "152.16.100.20"
218                     netmask: "255.255.0.0"
219                     local_mac: "00:98:10:64:14:00"
220                 xe1:
221                     ...
222         """
223         chassis_ip = self._cfg['chassis']
224         ports = [(chassis_ip, card, port) for card, port in
225                  zip(self._cfg['cards'], self._cfg['ports'])]
226
227         log.info('Create and assign vports: %s', ports)
228         for port in ports:
229             vport = self.ixnet.add(self.ixnet.getRoot(), 'vport')
230             self.ixnet.commit()
231             self.ixnet.execute('assignPorts', [port], [], [vport], True)
232             self.ixnet.commit()
233             if self.ixnet.getAttribute(vport, '-state') != 'up':
234                 log.warning('Port %s is down', vport)
235
236     def _create_traffic_item(self):
237         """Create the traffic item to hold the flow groups
238
239         The traffic item tracking by "Traffic Item" is enabled to retrieve the
240         latency statistics.
241         """
242         log.info('Create the traffic item "RFC2544"')
243         traffic_item = self.ixnet.add(self.ixnet.getRoot() + '/traffic',
244                                       'trafficItem')
245         self.ixnet.setMultiAttribute(traffic_item, '-name', 'RFC2544',
246                                      '-trafficType', 'raw')
247         self.ixnet.commit()
248
249         traffic_item_id = self.ixnet.remapIds(traffic_item)[0]
250         self.ixnet.setAttribute(traffic_item_id + '/tracking',
251                                 '-trackBy', 'trafficGroupId0')
252         self.ixnet.commit()
253
254     def _create_flow_groups(self):
255         """Create the flow groups between the assigned ports"""
256         traffic_item_id = self.ixnet.getList(self.ixnet.getRoot() + 'traffic',
257                                              'trafficItem')[0]
258         log.info('Create the flow groups')
259         vports = self.ixnet.getList(self.ixnet.getRoot(), 'vport')
260         uplink_ports = vports[::2]
261         downlink_ports = vports[1::2]
262         index = 0
263         for up, down in zip(uplink_ports, downlink_ports):
264             log.info('FGs: %s <--> %s', up, down)
265             endpoint_set_1 = self.ixnet.add(traffic_item_id, 'endpointSet')
266             endpoint_set_2 = self.ixnet.add(traffic_item_id, 'endpointSet')
267             self.ixnet.setMultiAttribute(
268                 endpoint_set_1, '-name', str(index + 1),
269                 '-sources', [up + '/protocols'],
270                 '-destinations', [down + '/protocols'])
271             self.ixnet.setMultiAttribute(
272                 endpoint_set_2, '-name', str(index + 2),
273                 '-sources', [down + '/protocols'],
274                 '-destinations', [up + '/protocols'])
275             self.ixnet.commit()
276             index += 2
277
278     def _append_procotol_to_stack(self, protocol_name, previous_element):
279         """Append a new element in the packet definition stack"""
280         protocol = (self.ixnet.getRoot() +
281                     '/traffic/protocolTemplate:"{}"'.format(protocol_name))
282         self.ixnet.execute('append', previous_element, protocol)
283
284     def _setup_config_elements(self):
285         """Setup the config elements
286
287         The traffic item is configured to allow individual configurations per
288         config element. The default frame configuration is applied:
289             Ethernet II: added by default
290             IPv4: element to add
291             UDP: element to add
292             Payload: added by default
293             Ethernet II (Trailer): added by default
294         :return:
295         """
296         traffic_item_id = self.ixnet.getList(self.ixnet.getRoot() + 'traffic',
297                                              'trafficItem')[0]
298         log.info('Split the frame rate distribution per config element')
299         config_elements = self.ixnet.getList(traffic_item_id, 'configElement')
300         for config_element in config_elements:
301             self.ixnet.setAttribute(config_element + '/frameRateDistribution',
302                                     '-portDistribution', 'splitRateEvenly')
303             self.ixnet.setAttribute(config_element + '/frameRateDistribution',
304                                     '-streamDistribution', 'splitRateEvenly')
305             self.ixnet.commit()
306             self._append_procotol_to_stack(
307                 PROTO_UDP, config_element + '/stack:"ethernet-1"')
308             self._append_procotol_to_stack(
309                 PROTO_IPV4, config_element + '/stack:"ethernet-1"')
310
311     def create_traffic_model(self):
312         """Create a traffic item and the needed flow groups
313
314         Each flow group inside the traffic item (only one is present)
315         represents the traffic between two ports:
316                         (uplink)    (downlink)
317             FlowGroup1: port1    -> port2
318             FlowGroup2: port1    <- port2
319             FlowGroup3: port3    -> port4
320             FlowGroup4: port3    <- port4
321         """
322         self._create_traffic_item()
323         self._create_flow_groups()
324         self._setup_config_elements()
325
326     def _update_frame_mac(self, ethernet_descriptor, field, mac_address):
327         """Set the MAC address in a config element stack Ethernet field
328
329         :param ethernet_descriptor: (str) ethernet descriptor, e.g.:
330             /traffic/trafficItem:1/configElement:1/stack:"ethernet-1"
331         :param field: (str) field name, e.g.: destinationAddress
332         :param mac_address: (str) MAC address
333         """
334         field_descriptor = self._get_field_in_stack_item(ethernet_descriptor,
335                                                          field)
336         self.ixnet.setMultiAttribute(field_descriptor,
337                                      '-singleValue', mac_address,
338                                      '-fieldValue', mac_address,
339                                      '-valueType', 'singleValue')
340         self.ixnet.commit()
341
342     def update_frame(self, traffic, duration):
343         """Update the L2 frame
344
345         This function updates the L2 frame options:
346         - Traffic type: "continuous", "fixedDuration".
347         - Duration: in case of traffic_type="fixedDuration", amount of seconds
348                     to inject traffic.
349         - Rate: in frames per seconds or percentage.
350         - Type of rate: "framesPerSecond" or "percentLineRate" ("bitsPerSecond"
351                          no used)
352         - Frame size: custom IMIX [1] definition; a list of packet size in
353                       bytes and the weight. E.g.:
354                       [[64, 64, 10], [128, 128, 15], [512, 512, 5]]
355
356         [1] https://en.wikipedia.org/wiki/Internet_Mix
357
358         :param traffic: list of traffic elements; each traffic element contains
359                         the injection parameter for each flow group.
360         :param duration: (int) injection time in seconds.
361         """
362         for traffic_param in traffic.values():
363             fg_id = str(traffic_param['id'])
364             config_element = self._get_config_element_by_flow_group_name(fg_id)
365             if not config_element:
366                 raise exceptions.IxNetworkFlowNotPresent(flow_group=fg_id)
367
368             type = traffic_param.get('traffic_type', 'fixedDuration')
369             rate = traffic_param['rate']
370             rate_unit = (
371                 'framesPerSecond' if traffic_param['rate_unit'] ==
372                 tp_base.TrafficProfileConfig.RATE_FPS else 'percentLineRate')
373             weighted_range_pairs = self._parse_framesize(
374                 traffic_param['outer_l2']['framesize'])
375             srcmac = str(traffic_param.get('srcmac', '00:00:00:00:00:01'))
376             dstmac = str(traffic_param.get('dstmac', '00:00:00:00:00:02'))
377
378             if traffic_param['outer_l2']['QinQ']:
379                 s_vlan = traffic_param['outer_l2']['QinQ']['S-VLAN']
380                 c_vlan = traffic_param['outer_l2']['QinQ']['C-VLAN']
381
382                 field_descriptor = self._get_field_in_stack_item(
383                     self._get_stack_item(fg_id, PROTO_ETHERNET)[0],
384                     'etherType')
385
386                 self.ixnet.setMultiAttribute(field_descriptor,
387                                              '-auto', 'false',
388                                              '-singleValue', ETHER_TYPE_802_1ad,
389                                              '-fieldValue', ETHER_TYPE_802_1ad,
390                                              '-valueType', SINGLE_VALUE)
391
392                 self._append_procotol_to_stack(
393                     PROTO_VLAN, config_element + '/stack:"ethernet-1"')
394                 self._append_procotol_to_stack(
395                     PROTO_VLAN, config_element + '/stack:"ethernet-1"')
396
397                 self._update_vlan_tag(fg_id, s_vlan, S_VLAN)
398                 self._update_vlan_tag(fg_id, c_vlan, C_VLAN)
399
400             self.ixnet.setMultiAttribute(
401                 config_element + '/transmissionControl',
402                 '-type', type, '-duration', duration)
403             self.ixnet.setMultiAttribute(
404                 config_element + '/frameRate',
405                 '-rate', rate, '-type', rate_unit)
406             self.ixnet.setMultiAttribute(
407                 config_element + '/frameSize',
408                 '-type', 'weightedPairs',
409                 '-weightedRangePairs', weighted_range_pairs)
410             self.ixnet.commit()
411
412             self._update_frame_mac(
413                 self._get_stack_item(fg_id, PROTO_ETHERNET)[0],
414                 'destinationAddress', dstmac)
415             self._update_frame_mac(
416                 self._get_stack_item(fg_id, PROTO_ETHERNET)[0],
417                 'sourceAddress', srcmac)
418
419     def _update_vlan_tag(self, fg_id, params, vlan=0):
420         field_to_param_map = {
421             'vlanUserPriority': 'priority',
422             'cfi': 'cfi',
423             'vlanID': 'id'
424         }
425         for field, param in field_to_param_map.items():
426             value = params.get(param)
427             if value:
428                 field_descriptor = self._get_field_in_stack_item(
429                     self._get_stack_item(fg_id, PROTO_VLAN)[vlan],
430                     field)
431
432                 self.ixnet.setMultiAttribute(field_descriptor,
433                                              '-auto', 'false',
434                                              '-singleValue', value,
435                                              '-fieldValue', value,
436                                              '-valueType', SINGLE_VALUE)
437
438         self.ixnet.commit()
439
440     def _update_ipv4_address(self, ip_descriptor, field, ip_address, seed,
441                              mask, count):
442         """Set the IPv4 address in a config element stack IP field
443
444         :param ip_descriptor: (str) IP descriptor, e.g.:
445             /traffic/trafficItem:1/configElement:1/stack:"ipv4-2"
446         :param field: (str) field name, e.g.: scrIp, dstIp
447         :param ip_address: (str) IP address
448         :param seed: (int) seed length
449         :param mask: (int) IP address mask length
450         :param count: (int) number of random IPs to generate
451         """
452         field_descriptor = self._get_field_in_stack_item(ip_descriptor,
453                                                          field)
454         random_mask = str(ipaddress.IPv4Address(
455             2**(ipaddress.IPV4LENGTH - mask) - 1).compressed)
456         self.ixnet.setMultiAttribute(field_descriptor,
457                                      '-seed', seed,
458                                      '-fixedBits', ip_address,
459                                      '-randomMask', random_mask,
460                                      '-valueType', 'random',
461                                      '-countValue', count)
462         self.ixnet.commit()
463
464     def update_ip_packet(self, traffic):
465         """Update the IP packet
466
467         NOTE: Only IPv4 is currently supported.
468         :param traffic: list of traffic elements; each traffic element contains
469                         the injection parameter for each flow group.
470         """
471         # NOTE(ralonsoh): L4 configuration is not set.
472         for traffic_param in traffic.values():
473             fg_id = str(traffic_param['id'])
474             if not self._get_config_element_by_flow_group_name(fg_id):
475                 raise exceptions.IxNetworkFlowNotPresent(flow_group=fg_id)
476
477             count = traffic_param['outer_l3']['count']
478             srcip = str(traffic_param['outer_l3']['srcip'])
479             dstip = str(traffic_param['outer_l3']['dstip'])
480             seed = traffic_param['outer_l3']['seed']
481             srcmask = traffic_param['outer_l3']['srcmask'] or IP_VERSION_4_MASK
482             dstmask = traffic_param['outer_l3']['dstmask'] or IP_VERSION_4_MASK
483
484             self._update_ipv4_address(
485                 self._get_stack_item(fg_id, PROTO_IPV4)[0],
486                 'srcIp', srcip, seed, srcmask, count)
487             self._update_ipv4_address(
488                 self._get_stack_item(fg_id, PROTO_IPV4)[0],
489                 'dstIp', dstip, seed, dstmask, count)
490
491     def update_l4(self, traffic):
492         """Update the L4 headers
493
494         NOTE: Only UDP is currently supported
495         :param traffic: list of traffic elements; each traffic element contains
496                         the injection parameter for each flow group
497         """
498         for traffic_param in traffic.values():
499             fg_id = str(traffic_param['id'])
500             if not self._get_config_element_by_flow_group_name(fg_id):
501                 raise exceptions.IxNetworkFlowNotPresent(flow_group=fg_id)
502
503             proto = traffic_param['outer_l3']['proto']
504             if proto not in SUPPORTED_PROTO:
505                 raise exceptions.IXIAUnsupportedProtocol(protocol=proto)
506
507             count = traffic_param['outer_l4']['count']
508             seed = traffic_param['outer_l4']['seed']
509
510             srcport = traffic_param['outer_l4']['srcport']
511             srcmask = traffic_param['outer_l4']['srcportmask']
512
513             dstport = traffic_param['outer_l4']['dstport']
514             dstmask = traffic_param['outer_l4']['dstportmask']
515
516             if proto in SUPPORTED_PROTO:
517                 self._update_udp_port(self._get_stack_item(fg_id, proto)[0],
518                                       'srcPort', srcport, seed, srcmask, count)
519
520                 self._update_udp_port(self._get_stack_item(fg_id, proto)[0],
521                                       'dstPort', dstport, seed, dstmask, count)
522
523     def _update_udp_port(self, descriptor, field, value,
524                          seed=1, mask=0, count=1):
525         """Set the UDP port in a config element stack UDP field
526
527         :param udp_descriptor: (str) UDP descriptor, e.g.:
528             /traffic/trafficItem:1/configElement:1/stack:"udp-3"
529         :param field: (str) field name, e.g.: scrPort, dstPort
530         :param value: (int) UDP port fixed bits
531         :param seed: (int) seed length
532         :param mask: (int) UDP port mask
533         :param count: (int) number of random ports to generate
534         """
535         field_descriptor = self._get_field_in_stack_item(descriptor, field)
536
537         if mask == 0:
538             seed = count = 1
539
540         self.ixnet.setMultiAttribute(field_descriptor,
541                                      '-auto', 'false',
542                                      '-seed', seed,
543                                      '-fixedBits', value,
544                                      '-randomMask', mask,
545                                      '-valueType', 'random',
546                                      '-countValue', count)
547
548         self.ixnet.commit()
549
550     def _build_stats_map(self, view_obj, name_map):
551         return {data_yardstick: self.ixnet.execute(
552             'getColumnValues', view_obj, data_ixia)
553             for data_yardstick, data_ixia in name_map.items()}
554
555     def get_statistics(self):
556         """Retrieve port and flow statistics
557
558         "Port Statistics" parameters are stored in self.PORT_STATS_NAME_MAP.
559         "Flow Statistics" parameters are stored in self.LATENCY_NAME_MAP.
560
561         :return: dictionary with the statistics; the keys of this dictionary
562                  are PORT_STATS_NAME_MAP and LATENCY_NAME_MAP keys.
563         """
564         port_statistics = '::ixNet::OBJ-/statistics/view:"Port Statistics"'
565         flow_statistics = '::ixNet::OBJ-/statistics/view:"Flow Statistics"'
566         stats = self._build_stats_map(port_statistics,
567                                       self.PORT_STATS_NAME_MAP)
568         stats.update(self._build_stats_map(flow_statistics,
569                                           self.LATENCY_NAME_MAP))
570         return stats
571
572     def start_traffic(self):
573         """Start the traffic injection in the traffic item
574
575         By configuration, there is only one traffic item. This function returns
576         when the traffic state is TRAFFIC_STATUS_STARTED.
577         """
578         traffic_items = self.ixnet.getList('/traffic', 'trafficItem')
579         if self.is_traffic_running():
580             self.ixnet.execute('stop', '/traffic')
581             # pylint: disable=unnecessary-lambda
582             utils.wait_until_true(lambda: self.is_traffic_stopped())
583
584         self.ixnet.execute('generate', traffic_items)
585         self.ixnet.execute('apply', '/traffic')
586         self.ixnet.execute('start', '/traffic')
587         # pylint: disable=unnecessary-lambda
588         utils.wait_until_true(lambda: self.is_traffic_running())