Merge "Add source and destination seed value in IXIA RFC2544"
[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             srcseed = traffic_param['outer_l3']['srcseed']
481             dstseed = traffic_param['outer_l3']['dstseed']
482             srcmask = traffic_param['outer_l3']['srcmask'] or IP_VERSION_4_MASK
483             dstmask = traffic_param['outer_l3']['dstmask'] or IP_VERSION_4_MASK
484
485             self._update_ipv4_address(
486                 self._get_stack_item(fg_id, PROTO_IPV4)[0],
487                 'srcIp', srcip, srcseed, srcmask, count)
488             self._update_ipv4_address(
489                 self._get_stack_item(fg_id, PROTO_IPV4)[0],
490                 'dstIp', dstip, dstseed, dstmask, count)
491
492     def update_l4(self, traffic):
493         """Update the L4 headers
494
495         NOTE: Only UDP is currently supported
496         :param traffic: list of traffic elements; each traffic element contains
497                         the injection parameter for each flow group
498         """
499         for traffic_param in traffic.values():
500             fg_id = str(traffic_param['id'])
501             if not self._get_config_element_by_flow_group_name(fg_id):
502                 raise exceptions.IxNetworkFlowNotPresent(flow_group=fg_id)
503
504             proto = traffic_param['outer_l3']['proto']
505             if proto not in SUPPORTED_PROTO:
506                 raise exceptions.IXIAUnsupportedProtocol(protocol=proto)
507
508             count = traffic_param['outer_l4']['count']
509             seed = traffic_param['outer_l4']['seed']
510
511             srcport = traffic_param['outer_l4']['srcport']
512             srcmask = traffic_param['outer_l4']['srcportmask']
513
514             dstport = traffic_param['outer_l4']['dstport']
515             dstmask = traffic_param['outer_l4']['dstportmask']
516
517             if proto in SUPPORTED_PROTO:
518                 self._update_udp_port(self._get_stack_item(fg_id, proto)[0],
519                                       'srcPort', srcport, seed, srcmask, count)
520
521                 self._update_udp_port(self._get_stack_item(fg_id, proto)[0],
522                                       'dstPort', dstport, seed, dstmask, count)
523
524     def _update_udp_port(self, descriptor, field, value,
525                          seed=1, mask=0, count=1):
526         """Set the UDP port in a config element stack UDP field
527
528         :param udp_descriptor: (str) UDP descriptor, e.g.:
529             /traffic/trafficItem:1/configElement:1/stack:"udp-3"
530         :param field: (str) field name, e.g.: scrPort, dstPort
531         :param value: (int) UDP port fixed bits
532         :param seed: (int) seed length
533         :param mask: (int) UDP port mask
534         :param count: (int) number of random ports to generate
535         """
536         field_descriptor = self._get_field_in_stack_item(descriptor, field)
537
538         if mask == 0:
539             seed = count = 1
540
541         self.ixnet.setMultiAttribute(field_descriptor,
542                                      '-auto', 'false',
543                                      '-seed', seed,
544                                      '-fixedBits', value,
545                                      '-randomMask', mask,
546                                      '-valueType', 'random',
547                                      '-countValue', count)
548
549         self.ixnet.commit()
550
551     def _build_stats_map(self, view_obj, name_map):
552         return {data_yardstick: self.ixnet.execute(
553             'getColumnValues', view_obj, data_ixia)
554             for data_yardstick, data_ixia in name_map.items()}
555
556     def get_statistics(self):
557         """Retrieve port and flow statistics
558
559         "Port Statistics" parameters are stored in self.PORT_STATS_NAME_MAP.
560         "Flow Statistics" parameters are stored in self.LATENCY_NAME_MAP.
561
562         :return: dictionary with the statistics; the keys of this dictionary
563                  are PORT_STATS_NAME_MAP and LATENCY_NAME_MAP keys.
564         """
565         port_statistics = '::ixNet::OBJ-/statistics/view:"Port Statistics"'
566         flow_statistics = '::ixNet::OBJ-/statistics/view:"Flow Statistics"'
567         stats = self._build_stats_map(port_statistics,
568                                       self.PORT_STATS_NAME_MAP)
569         stats.update(self._build_stats_map(flow_statistics,
570                                           self.LATENCY_NAME_MAP))
571         return stats
572
573     def start_traffic(self):
574         """Start the traffic injection in the traffic item
575
576         By configuration, there is only one traffic item. This function returns
577         when the traffic state is TRAFFIC_STATUS_STARTED.
578         """
579         traffic_items = self.ixnet.getList('/traffic', 'trafficItem')
580         if self.is_traffic_running():
581             self.ixnet.execute('stop', '/traffic')
582             # pylint: disable=unnecessary-lambda
583             utils.wait_until_true(lambda: self.is_traffic_stopped())
584
585         self.ixnet.execute('generate', traffic_items)
586         self.ixnet.execute('apply', '/traffic')
587         self.ixnet.execute('start', '/traffic')
588         # pylint: disable=unnecessary-lambda
589         utils.wait_until_true(lambda: self.is_traffic_running())