NFVBENCH-158 Allow multiple UDP ports in traffic generation
[nfvbench.git] / nfvbench / traffic_client.py
index 7591062..8ef0403 100755 (executable)
@@ -15,6 +15,7 @@
 """Interface to the traffic generator clients including NDR/PDR binary search."""
 
 from datetime import datetime
+from math import gcd
 import socket
 import struct
 import time
@@ -26,23 +27,22 @@ from netaddr import IPNetwork
 from trex.stl.api import Ether
 from trex.stl.api import STLError
 from trex.stl.api import UDP
+# pylint: disable=wrong-import-order
+from scapy.contrib.mpls import MPLS  # flake8: noqa
+# pylint: enable=wrong-import-order
 # pylint: enable=import-error
 
-from log import LOG
-from packet_stats import InterfaceStats
-from packet_stats import PacketPathStats
-from stats_collector import IntervalCollector
-from stats_collector import IterationCollector
-import traffic_gen.traffic_utils as utils
-from utils import cast_integer
-
+from .log import LOG
+from .packet_stats import InterfaceStats
+from .packet_stats import PacketPathStats
+from .stats_collector import IntervalCollector
+from .stats_collector import IterationCollector
+from .traffic_gen import traffic_utils as utils
+from .utils import cast_integer
 
 class TrafficClientException(Exception):
     """Generic traffic client exception."""
 
-    pass
-
-
 class TrafficRunner(object):
     """Serialize various steps required to run traffic."""
 
@@ -127,16 +127,25 @@ class IpBlock(object):
             raise IndexError('Index out of bounds: %d (max=%d)' % (index, self.max_available))
         return Device.int_to_ip(self.base_ip_int + index * self.step)
 
-    def reserve_ip_range(self, count):
-        """Reserve a range of count consecutive IP addresses spaced by step."""
-        if self.next_free + count > self.max_available:
+    def reserve_ip_range(self, count, force_ip_reservation=False):
+        """Reserve a range of count consecutive IP addresses spaced by step.
+        force_ip_reservation parameter allows to continue the calculation of IPs when
+        the 2 sides (ports) have different size and the flow is greater than
+        the size as well.
+        """
+        if self.next_free + count > self.max_available and force_ip_reservation is False:
             raise IndexError('No more IP addresses next free=%d max_available=%d requested=%d' %
                              (self.next_free,
                               self.max_available,
                               count))
-        first_ip = self.get_ip(self.next_free)
-        last_ip = self.get_ip(self.next_free + count - 1)
-        self.next_free += count
+        if self.next_free + count > self.max_available and force_ip_reservation is True:
+            first_ip = self.get_ip(self.next_free)
+            last_ip = self.get_ip(self.next_free + self.max_available - 1)
+            self.next_free += self.max_available
+        else:
+            first_ip = self.get_ip(self.next_free)
+            last_ip = self.get_ip(self.next_free + count - 1)
+            self.next_free += count
         return (first_ip, last_ip)
 
     def reset_reservation(self):
@@ -144,6 +153,17 @@ class IpBlock(object):
         self.next_free = 0
 
 
+class UdpPorts(object):
+
+    def __init__(self, src_min, src_max, dst_min, dst_max, step):
+
+        self.src_min = src_min
+        self.src_max = src_max
+        self.dst_min = dst_min
+        self.dst_max = dst_max
+        self.step = step
+
+
 class Device(object):
     """Represent a port device and all information associated to it.
 
@@ -155,12 +175,19 @@ class Device(object):
         """Create a new device for a given port."""
         self.generator_config = generator_config
         self.chain_count = generator_config.service_chain_count
-        self.flow_count = generator_config.flow_count / 2
+        if generator_config.bidirectional:
+            self.flow_count = generator_config.flow_count / 2
+        else:
+            self.flow_count = generator_config.flow_count
+
         self.port = port
         self.switch_port = generator_config.interfaces[port].get('switch_port', None)
         self.vtep_vlan = None
         self.vtep_src_mac = None
         self.vxlan = False
+        self.mpls = False
+        self.inner_labels = None
+        self.outer_labels = None
         self.pci = generator_config.interfaces[port].pci
         self.mac = None
         self.dest_macs = None
@@ -173,10 +200,58 @@ class Device(object):
         self.vnis = None
         self.vlans = None
         self.ip_addrs = generator_config.ip_addrs[port]
-        subnet = IPNetwork(self.ip_addrs)
-        self.ip = subnet.ip.format()
+        self.ip_src_static = generator_config.ip_src_static
         self.ip_addrs_step = generator_config.ip_addrs_step
-        self.ip_block = IpBlock(self.ip, self.ip_addrs_step, self.flow_count)
+        if self.ip_addrs_step == 'random':
+            # Set step to 1 to calculate the IP range size (see check_ip_size below)
+            step = '0.0.0.1'
+        else:
+            step = self.ip_addrs_step
+        self.ip_size = self.check_ipsize(IPNetwork(self.ip_addrs).size, Device.ip_to_int(step))
+        self.ip = str(IPNetwork(self.ip_addrs).network)
+        ip_addrs_left = generator_config.ip_addrs[0]
+        ip_addrs_right = generator_config.ip_addrs[1]
+        self.ip_addrs_size = {
+            'left': self.check_ipsize(IPNetwork(ip_addrs_left).size, Device.ip_to_int(step)),
+            'right': self.check_ipsize(IPNetwork(ip_addrs_right).size, Device.ip_to_int(step))}
+        udp_src_port = generator_config.gen_config.udp_src_port
+        if udp_src_port is None:
+            udp_src_port = 53
+        udp_dst_port = generator_config.gen_config.udp_dst_port
+        if udp_dst_port is None:
+            udp_dst_port = 53
+        src_max, src_min = self.define_udp_range(udp_src_port, 'udp_src_port')
+        dst_max, dst_min = self.define_udp_range(udp_dst_port, 'udp_dst_port')
+        udp_src_range = int(src_max) - int(src_min) + 1
+        udp_dst_range = int(dst_max) - int(dst_min) + 1
+        lcm_port = self.lcm(udp_src_range, udp_dst_range)
+        if self.ip_src_static is True:
+            lcm_ip = self.lcm(1, min(self.ip_addrs_size['left'], self.ip_addrs_size['right']))
+        else:
+            lcm_ip = self.lcm(self.ip_addrs_size['left'], self.ip_addrs_size['right'])
+        flow_max = self.lcm(lcm_port, lcm_ip)
+        if self.flow_count > flow_max:
+            raise TrafficClientException('Trying to set unachievable traffic (%d > %d)' %
+                                         (self.flow_count, flow_max))
+
+        # manage udp range regarding flow count value
+        # UDP dst range is greater than FC => range will be limited to min + FC
+        if self.flow_count <= udp_dst_range:
+            dst_max = int(dst_min) + self.flow_count - 1
+        # UDP src range is greater than FC => range will be limited to min + FC
+        if self.flow_count <= udp_src_range:
+            src_max = int(src_min) + self.flow_count - 1
+        # Define IP block limit regarding flow count
+        if self.flow_count <= self.ip_size:
+            self.ip_block = IpBlock(self.ip, step, self.flow_count)
+        else:
+            self.ip_block = IpBlock(self.ip, step, self.ip_size)
+
+        if generator_config.gen_config.udp_port_step == 'random':
+            step = 1
+        else:
+            step = generator_config.gen_config.udp_port_step
+        self.udp_ports = UdpPorts(src_min, src_max, dst_min, dst_max, step)
         self.gw_ip_block = IpBlock(generator_config.gateway_ips[port],
                                    generator_config.gateway_ip_addrs_step,
                                    self.chain_count)
@@ -184,8 +259,40 @@ class Device(object):
         self.tg_gw_ip_block = IpBlock(self.tg_gateway_ip_addrs,
                                       generator_config.tg_gateway_ip_addrs_step,
                                       self.chain_count)
-        self.udp_src_port = generator_config.udp_src_port
-        self.udp_dst_port = generator_config.udp_dst_port
+
+    @staticmethod
+    def define_udp_range(udp_port, property_name):
+        if isinstance(udp_port, int):
+            min = udp_port
+            max = min
+        elif isinstance(udp_port, tuple):
+            min = udp_port[0]
+            max = udp_port[1]
+        else:
+            raise TrafficClientException('Invalid %s property value (53 or [\'53\',\'1024\'])'
+                                         % property_name)
+        return max, min
+
+    @staticmethod
+    def lcm(a, b):
+        """Calculate the maximum possible value for both IP and ports,
+        eventually for maximum possible flux."""
+        if a != 0 and b != 0:
+            lcm_value = a * b // gcd(a, b)
+            return lcm_value
+        raise TypeError(" IP size or port range can't be zero !")
+
+    @staticmethod
+    def check_ipsize(ip_size, step):
+        """Check and set the available IPs, considering the step."""
+        try:
+            if ip_size % step == 0:
+                value = int(ip_size / step)
+            else:
+                value = int((ip_size / step)) + 1
+            return value
+        except ZeroDivisionError:
+            raise ZeroDivisionError("step can't be zero !")
 
     def set_mac(self, mac):
         """Set the local MAC for this port device."""
@@ -204,7 +311,7 @@ class Device(object):
         - VM macs discovered using openstack API
         - dest MACs provisioned in config file
         """
-        self.vtep_dst_mac = map(str, dest_macs)
+        self.vtep_dst_mac = list(map(str, dest_macs))
 
     def set_dest_macs(self, dest_macs):
         """Set the list of dest MACs indexed by the chain id.
@@ -213,7 +320,7 @@ class Device(object):
         - VM macs discovered using openstack API
         - dest MACs provisioned in config file
         """
-        self.dest_macs = map(str, dest_macs)
+        self.dest_macs = list(map(str, dest_macs))
 
     def get_dest_macs(self):
         """Get the list of dest macs for this device.
@@ -244,10 +351,25 @@ class Device(object):
         LOG.info("Port %d: src_vtep %s, dst_vtep %s", self.port,
                  self.vtep_src_ip, self.vtep_dst_ip)
 
+    def set_mpls_peers(self, src_ip, dst_ip):
+        self.mpls = True
+        self.vtep_dst_ip = dst_ip
+        self.vtep_src_ip = src_ip
+        LOG.info("Port %d: src_mpls_vtep %s, mpls_peer_ip %s", self.port,
+                 self.vtep_src_ip, self.vtep_dst_ip)
+
     def set_vxlans(self, vnis):
         self.vnis = vnis
         LOG.info("Port %d: VNIs %s", self.port, self.vnis)
 
+    def set_mpls_inner_labels(self, labels):
+        self.inner_labels = labels
+        LOG.info("Port %d: MPLS Inner Labels %s", self.port, self.inner_labels)
+
+    def set_mpls_outer_labels(self, labels):
+        self.outer_labels = labels
+        LOG.info("Port %d: MPLS Outer Labels %s", self.port, self.outer_labels)
+
     def set_gw_ip(self, gateway_ip):
         self.gw_ip_block = IpBlock(gateway_ip,
                                    self.generator_config.gateway_ip_addrs_step,
@@ -269,16 +391,29 @@ class Device(object):
         #   calculated as (total_flows + chain_count - 1) / chain_count
         # - the first chain will have the remainder
         # example 11 flows and 3 chains => 3, 4, 4
-        flows_per_chain = (self.flow_count + self.chain_count - 1) / self.chain_count
-        cur_chain_flow_count = self.flow_count - flows_per_chain * (self.chain_count - 1)
+        flows_per_chain = int((self.flow_count + self.chain_count - 1) / self.chain_count)
+        cur_chain_flow_count = int(self.flow_count - flows_per_chain * (self.chain_count - 1))
+        force_ip_reservation = False
+        # use case example of this parameter:
+        # - static IP addresses (source & destination), netmask = /30
+        # - 4 varying UDP source ports | 1 UDP destination port
+        # - Flow count = 8
+        # --> parameter 'reserve_ip_range' should have flag 'force_ip_reservation'
+        # in order to assign the maximum available IP on each iteration
+        if self.ip_size < cur_chain_flow_count \
+                or self.ip_addrs_size['left'] != self.ip_addrs_size['right']:
+            force_ip_reservation = True
+
         peer = self.get_peer_device()
         self.ip_block.reset_reservation()
         peer.ip_block.reset_reservation()
         dest_macs = self.get_dest_macs()
 
-        for chain_idx in xrange(self.chain_count):
-            src_ip_first, src_ip_last = self.ip_block.reserve_ip_range(cur_chain_flow_count)
-            dst_ip_first, dst_ip_last = peer.ip_block.reserve_ip_range(cur_chain_flow_count)
+        for chain_idx in range(self.chain_count):
+            src_ip_first, src_ip_last = self.ip_block.reserve_ip_range\
+            (cur_chain_flow_count, force_ip_reservation)
+            dst_ip_first, dst_ip_last = peer.ip_block.reserve_ip_range\
+            (cur_chain_flow_count, force_ip_reservation)
 
             configs.append({
                 'count': cur_chain_flow_count,
@@ -291,19 +426,29 @@ class Device(object):
                 'ip_dst_addr_max': dst_ip_last,
                 'ip_dst_count': cur_chain_flow_count,
                 'ip_addrs_step': self.ip_addrs_step,
-                'udp_src_port': self.udp_src_port,
-                'udp_dst_port': self.udp_dst_port,
+                'ip_src_static': self.ip_src_static,
+                'udp_src_port': self.udp_ports.src_min,
+                'udp_src_port_max': self.udp_ports.src_max,
+                'udp_src_count': int(self.udp_ports.src_max) - int(self.udp_ports.src_min) + 1,
+                'udp_dst_port': self.udp_ports.dst_min,
+                'udp_dst_port_max': self.udp_ports.dst_max,
+                'udp_dst_count': int(self.udp_ports.dst_max) - int(self.udp_ports.dst_min) + 1,
+                'udp_port_step': self.udp_ports.step,
                 'mac_discovery_gw': self.get_gw_ip(chain_idx),
                 'ip_src_tg_gw': self.tg_gw_ip_block.get_ip(chain_idx),
                 'ip_dst_tg_gw': peer.tg_gw_ip_block.get_ip(chain_idx),
                 'vlan_tag': self.vlans[chain_idx] if self.vlans else None,
                 'vxlan': self.vxlan,
                 'vtep_vlan': self.vtep_vlan if self.vtep_vlan else None,
-                'vtep_src_mac': self.mac if self.vxlan is True else None,
-                'vtep_dst_mac': self.vtep_dst_mac if self.vxlan is True else None,
+                'vtep_src_mac': self.mac if (self.vxlan or self.mpls) else None,
+                'vtep_dst_mac': self.vtep_dst_mac if (self.vxlan or self.mpls) else None,
                 'vtep_dst_ip': self.vtep_dst_ip if self.vxlan is True else None,
                 'vtep_src_ip': self.vtep_src_ip if self.vxlan is True else None,
-                'net_vni': self.vnis[chain_idx] if self.vxlan is True else None
+                'net_vni': self.vnis[chain_idx] if self.vxlan is True else None,
+                'mpls': self.mpls,
+                'mpls_outer_label': self.outer_labels[chain_idx] if self.mpls is True else None,
+                'mpls_inner_label': self.inner_labels[chain_idx] if self.mpls is True else None
+
             })
             # after first chain, fall back to the flow count for all other chains
             cur_chain_flow_count = flows_per_chain
@@ -317,7 +462,7 @@ class Device(object):
     @staticmethod
     def int_to_ip(nvalue):
         """Convert an IP address from numeric to string."""
-        return socket.inet_ntoa(struct.pack("!I", nvalue))
+        return socket.inet_ntoa(struct.pack("!I", int(nvalue)))
 
 
 class GeneratorConfig(object):
@@ -366,7 +511,7 @@ class GeneratorConfig(object):
         self.service_chain_count = config.service_chain_count
         self.flow_count = config.flow_count
         self.host_name = gen_config.host_name
-
+        self.bidirectional = config.traffic.bidirectional
         self.tg_gateway_ip_addrs = gen_config.tg_gateway_ip_addrs
         self.ip_addrs = gen_config.ip_addrs
         self.ip_addrs_step = gen_config.ip_addrs_step or self.DEFAULT_SRC_DST_IP_STEP
@@ -374,8 +519,7 @@ class GeneratorConfig(object):
             gen_config.tg_gateway_ip_addrs_step or self.DEFAULT_IP_STEP
         self.gateway_ip_addrs_step = gen_config.gateway_ip_addrs_step or self.DEFAULT_IP_STEP
         self.gateway_ips = gen_config.gateway_ip_addrs
-        self.udp_src_port = gen_config.udp_src_port
-        self.udp_dst_port = gen_config.udp_dst_port
+        self.ip_src_static = gen_config.ip_src_static
         self.vteps = gen_config.get('vteps')
         self.devices = [Device(port, self) for port in [0, 1]]
         # This should normally always be [0, 1]
@@ -420,7 +564,7 @@ class GeneratorConfig(object):
             raise TrafficClientException('Dest MAC list %s must have %d entries' %
                                          (dest_macs, self.config.service_chain_count))
         self.devices[port_index].set_vtep_dst_mac(dest_macs)
-        LOG.info('Port %d: vtep dst MAC %s', port_index, set([str(mac) for mac in dest_macs]))
+        LOG.info('Port %d: vtep dst MAC %s', port_index, {str(mac) for mac in dest_macs})
 
     def get_dest_macs(self):
         """Return the list of dest macs indexed by port."""
@@ -448,6 +592,28 @@ class GeneratorConfig(object):
                                          (vxlans, self.config.service_chain_count))
         self.devices[port_index].set_vxlans(vxlans)
 
+    def set_mpls_inner_labels(self, port_index, labels):
+        """Set the list of MPLS Labels to use indexed by the chain id on given port.
+
+        port_index: the port for which Labels must be set
+        Labels: a list of Labels lists indexed by chain id
+        """
+        if len(labels) != self.config.service_chain_count:
+            raise TrafficClientException('Inner MPLS list %s must have %d entries' %
+                                         (labels, self.config.service_chain_count))
+        self.devices[port_index].set_mpls_inner_labels(labels)
+
+    def set_mpls_outer_labels(self, port_index, labels):
+        """Set the list of MPLS Labels to use indexed by the chain id on given port.
+
+        port_index: the port for which Labels must be set
+        Labels: a list of Labels lists indexed by chain id
+        """
+        if len(labels) != self.config.service_chain_count:
+            raise TrafficClientException('Outer MPLS list %s must have %d entries' %
+                                         (labels, self.config.service_chain_count))
+        self.devices[port_index].set_mpls_outer_labels(labels)
+
     def set_vtep_vlan(self, port_index, vlan):
         """Set the vtep vlan to use indexed by the chain id on given port.
         port_index: the port for which VLAN must be set
@@ -457,6 +623,9 @@ class GeneratorConfig(object):
     def set_vxlan_endpoints(self, port_index, src_ip, dst_ip):
         self.devices[port_index].set_vxlan_endpoints(src_ip, dst_ip)
 
+    def set_mpls_peers(self, port_index, src_ip, dst_ip):
+        self.devices[port_index].set_mpls_peers(src_ip, dst_ip)
+
     @staticmethod
     def __match_generator_profile(traffic_generator, generator_profile):
         gen_config = AttrDict(traffic_generator)
@@ -512,10 +681,10 @@ class TrafficClient(object):
     def _get_generator(self):
         tool = self.tool.lower()
         if tool == 'trex':
-            from traffic_gen import trex_gen
+            from .traffic_gen import trex_gen
             return trex_gen.TRex(self)
         if tool == 'dummy':
-            from traffic_gen import dummy
+            from .traffic_gen import dummy
             return dummy.DummyTG(self)
         raise TrafficClientException('Unsupported generator tool name:' + self.tool)
 
@@ -533,7 +702,7 @@ class TrafficClient(object):
         if len(matching_profiles) > 1:
             raise TrafficClientException('Multiple traffic profiles with name: ' +
                                          traffic_profile_name)
-        elif not matching_profiles:
+        if not matching_profiles:
             raise TrafficClientException('Cannot find traffic profile: ' + traffic_profile_name)
         return matching_profiles[0].l2frame_size
 
@@ -593,8 +762,8 @@ class TrafficClient(object):
         self.gen.create_traffic('64', [rate_pps, rate_pps], bidirectional=True, latency=False,
                                 e2e=True)
         # ensures enough traffic is coming back
-        retry_count = (self.config.check_traffic_time_sec +
-                       self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
+        retry_count = int((self.config.check_traffic_time_sec +
+                           self.config.generic_poll_sec - 1) / self.config.generic_poll_sec)
 
         # we expect to see packets coming from 2 unique MAC per chain
         # because there can be flooding in the case of shared net
@@ -610,9 +779,12 @@ class TrafficClient(object):
             get_mac_id = lambda packet: packet['binary'][60:66]
         elif self.config.vxlan:
             get_mac_id = lambda packet: packet['binary'][56:62]
+        elif self.config.mpls:
+            get_mac_id = lambda packet: packet['binary'][24:30]
+            # mpls_transport_label = lambda packet: packet['binary'][14:18]
         else:
             get_mac_id = lambda packet: packet['binary'][6:12]
-        for it in xrange(retry_count):
+        for it in range(retry_count):
             self.gen.clear_stats()
             self.gen.start_traffic()
             self.gen.start_capture()
@@ -625,13 +797,20 @@ class TrafficClient(object):
             self.gen.fetch_capture_packets()
             self.gen.stop_capture()
             for packet in self.gen.packet_list:
-                mac_id = get_mac_id(packet)
+                mac_id = get_mac_id(packet).decode('latin-1')
                 src_mac = ':'.join(["%02x" % ord(x) for x in mac_id])
-                if src_mac in mac_map and self.is_udp(packet):
-                    port, chain = mac_map[src_mac]
-                    LOG.info('Received packet from mac: %s (chain=%d, port=%d)',
-                             src_mac, chain, port)
-                    mac_map.pop(src_mac, None)
+                if self.config.mpls:
+                    if src_mac in mac_map and self.is_mpls(packet):
+                        port, chain = mac_map[src_mac]
+                        LOG.info('Received mpls packet from mac: %s (chain=%d, port=%d)',
+                                 src_mac, chain, port)
+                        mac_map.pop(src_mac, None)
+                else:
+                    if src_mac in mac_map and self.is_udp(packet):
+                        port, chain = mac_map[src_mac]
+                        LOG.info('Received udp packet from mac: %s (chain=%d, port=%d)',
+                                 src_mac, chain, port)
+                        mac_map.pop(src_mac, None)
 
                 if not mac_map:
                     LOG.info('End-to-end connectivity established')
@@ -648,12 +827,16 @@ class TrafficClient(object):
         pkt = Ether(packet['binary'])
         return UDP in pkt
 
+    def is_mpls(self, packet):
+        pkt = Ether(packet['binary'])
+        return MPLS in pkt
+
     def ensure_arp_successful(self):
         """Resolve all IP using ARP and throw an exception in case of failure."""
         dest_macs = self.gen.resolve_arp()
         if dest_macs:
             # all dest macs are discovered, saved them into the generator config
-            if self.config.vxlan:
+            if self.config.vxlan or self.config.mpls:
                 self.generator_config.set_vtep_dest_macs(0, dest_macs[0])
                 self.generator_config.set_vtep_dest_macs(1, dest_macs[1])
             else:
@@ -679,7 +862,11 @@ class TrafficClient(object):
                 self.run_config['rates'][idx] = {'rate_pps': self.__convert_rates(rate)['rate_pps']}
 
         self.gen.clear_streamblock()
-        self.gen.create_traffic(frame_size, self.run_config['rates'], bidirectional, latency=True)
+
+        if self.config.no_latency_streams:
+            LOG.info("Latency streams are disabled")
+        self.gen.create_traffic(frame_size, self.run_config['rates'], bidirectional,
+                                latency=not self.config.no_latency_streams)
 
     def _modify_load(self, load):
         self.current_total_rate = {'rate_percent': str(load)}
@@ -738,29 +925,29 @@ class TrafficClient(object):
         """Collect final stats for previous run."""
         stats = self.gen.get_stats()
         retDict = {'total_tx_rate': stats['total_tx_rate']}
-        for port in self.PORTS:
-            retDict[port] = {'tx': {}, 'rx': {}}
 
         tx_keys = ['total_pkts', 'total_pkt_bytes', 'pkt_rate', 'pkt_bit_rate']
         rx_keys = tx_keys + ['dropped_pkts']
 
         for port in self.PORTS:
+            port_stats = {'tx': {}, 'rx': {}}
             for key in tx_keys:
-                retDict[port]['tx'][key] = int(stats[port]['tx'][key])
+                port_stats['tx'][key] = int(stats[port]['tx'][key])
             for key in rx_keys:
                 try:
-                    retDict[port]['rx'][key] = int(stats[port]['rx'][key])
+                    port_stats['rx'][key] = int(stats[port]['rx'][key])
                 except ValueError:
-                    retDict[port]['rx'][key] = 0
-            retDict[port]['rx']['avg_delay_usec'] = cast_integer(
+                    port_stats['rx'][key] = 0
+            port_stats['rx']['avg_delay_usec'] = cast_integer(
                 stats[port]['rx']['avg_delay_usec'])
-            retDict[port]['rx']['min_delay_usec'] = cast_integer(
+            port_stats['rx']['min_delay_usec'] = cast_integer(
                 stats[port]['rx']['min_delay_usec'])
-            retDict[port]['rx']['max_delay_usec'] = cast_integer(
+            port_stats['rx']['max_delay_usec'] = cast_integer(
                 stats[port]['rx']['max_delay_usec'])
-            retDict[port]['drop_rate_percent'] = self.__get_dropped_rate(retDict[port])
+            port_stats['drop_rate_percent'] = self.__get_dropped_rate(port_stats)
+            retDict[str(port)] = port_stats
 
-        ports = sorted(retDict.keys())
+        ports = sorted(list(retDict.keys()), key=str)
         if self.run_config['bidirectional']:
             retDict['overall'] = {'tx': {}, 'rx': {}}
             for key in tx_keys:
@@ -801,6 +988,7 @@ class TrafficClient(object):
 
     def __format_output_stats(self, stats):
         for key in self.PORTS + ['overall']:
+            key = str(key)
             interface = stats[key]
             stats[key] = {
                 'tx_pkts': interface['tx']['total_pkts'],
@@ -815,7 +1003,7 @@ class TrafficClient(object):
         return stats
 
     def __targets_found(self, rate, targets, results):
-        for tag, target in targets.iteritems():
+        for tag, target in list(targets.items()):
             LOG.info('Found %s (%s) load: %s', tag, target, rate)
             self.__ndr_pdr_found(tag, rate)
             results[tag]['timestamp_sec'] = time.time()
@@ -851,7 +1039,7 @@ class TrafficClient(object):
         # Split target dicts based on the avg drop rate
         left_targets = {}
         right_targets = {}
-        for tag, target in targets.iteritems():
+        for tag, target in list(targets.items()):
             if stats['overall']['drop_rate_percent'] <= target:
                 # record the best possible rate found for this target
                 results[tag] = rates
@@ -984,8 +1172,8 @@ class TrafficClient(object):
         # because we want each direction to have the far end RX rates,
         # use the far end index (1-idx) to retrieve the RX rates
         for idx, key in enumerate(["direction-forward", "direction-reverse"]):
-            tx_rate = results["stats"][idx]["tx"]["total_pkts"] / self.config.duration_sec
-            rx_rate = results["stats"][1 - idx]["rx"]["total_pkts"] / self.config.duration_sec
+            tx_rate = results["stats"][str(idx)]["tx"]["total_pkts"] / self.config.duration_sec
+            rx_rate = results["stats"][str(1 - idx)]["rx"]["total_pkts"] / self.config.duration_sec
             r[key] = {
                 "orig": self.__convert_rates(self.run_config['rates'][idx]),
                 "tx": self.__convert_rates({'rate_pps': tx_rate}),
@@ -996,7 +1184,7 @@ class TrafficClient(object):
         for direction in ['orig', 'tx', 'rx']:
             total[direction] = {}
             for unit in ['rate_percent', 'rate_bps', 'rate_pps']:
-                total[direction][unit] = sum([float(x[direction][unit]) for x in r.values()])
+                total[direction][unit] = sum([float(x[direction][unit]) for x in list(r.values())])
 
         r['direction-total'] = total
         return r