ENV TREX_VER "v2.61"
 ENV VM_IMAGE_VER "0.11"
+ENV PYTHONIOENCODING "utf8"
 
 # Note: do not clone with --depth 1 as it will cause pbr to fail extracting the nfvbench version
 # from the git tag
 
+RUN apt-get update && apt-get install -y software-properties-common
+
+RUN add-apt-repository -y ppa:deadsnakes/ppa
+
 RUN apt-get update && apt-get install -y \
        git \
        kmod \
        pciutils \
-       python \
-       python-pip \
+       python3.6 \
        vim \
        wget \
        net-tools \
        iproute2 \
        libelf1 \
+       && ln -s /usr/bin/python3.6 /usr/local/bin/python3 \
        && mkdir -p /opt/trex \
        && mkdir /var/log/nfvbench \
        && wget --no-cache https://trex-tgn.cisco.com/trex/release/$TREX_VER.tar.gz \
        && tar xzf $TREX_VER.tar.gz -C /opt/trex \
        && rm -f /$TREX_VER.tar.gz \
        && rm -f /opt/trex/$TREX_VER/trex_client_$TREX_VER.tar.gz \
-       && cp -a /opt/trex/$TREX_VER/automation/trex_control_plane/interactive/trex /usr/local/lib/python2.7/dist-packages/ \
+       && cp -a /opt/trex/$TREX_VER/automation/trex_control_plane/interactive/trex /usr/local/lib/python3.6/dist-packages/ \
        && rm -rf /opt/trex/$TREX_VER/automation/trex_control_plane/interactive/trex \
-       && sed -i -e "s/2048 /512 /" -e "s/2048\"/512\"/" /opt/trex/$TREX_VER/trex-cfg \
-       && apt-get remove -y python-pip \
        && wget https://bootstrap.pypa.io/get-pip.py \
-       && python get-pip.py \
-       && pip install -U pbr \
-       && pip install -U setuptools \
+       && python3 get-pip.py \
+       && pip3 install -U pbr \
+       && pip3 install -U setuptools \
        && cd / \
        && git clone https://gerrit.opnfv.org/gerrit/nfvbench \
-       && cd /nfvbench && pip install -e . \
+       && cd /nfvbench && pip3 install -e . \
        && wget -O nfvbenchvm-$VM_IMAGE_VER.qcow2 http://artifacts.opnfv.org/nfvbench/images/nfvbenchvm_centos-$VM_IMAGE_VER.qcow2 \
-       && python ./docker/cleanup_generators.py \
+       && python3 ./docker/cleanup_generators.py \
        && rm -rf /nfvbench/.git \
        && apt-get remove -y wget git \
        && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
 
             else:
                 os.remove(f)
         except OSError:
-            print "Skipped file:"
-            print f
+            print("Skipped file:")
+            print(f)
             continue
 
 
     versions = os.listdir(TREX_OPT)
     for version in versions:
         trex_path = os.path.join(TREX_OPT, version)
-        print 'Cleaning TRex', version
+        print('Cleaning TRex', version)
         try:
             size_before = get_dir_size(trex_path)
             remove_unused_libs(trex_path, TREX_UNUSED)
             size_after = get_dir_size(trex_path)
-            print '==== Saved Space ===='
-            print size_before - size_after
+            print('==== Saved Space ====')
+            print(size_before - size_after)
         except OSError:
             import traceback
-            print traceback.print_exc()
-            print 'Cleanup was not finished.'
+            print(traceback.print_exc())
+            print('Cleanup was not finished.')
 
 
 
 PVP L3 Router Internal Chain
---------------
+----------------------------
 
 NFVbench can measure the performance of 1 L3 service chain that are setup by NFVbench (VMs, routers and networks).
 
 - generate packets with the proper VLAN ID and measure traffic.
 
 
-Please note: ``l3_router`` option is also compatible with external routers. In this case NFVBench will use ``EXT`` chain.
\ No newline at end of file
+Please note: ``l3_router`` option is also compatible with external routers. In this case NFVBench will use ``EXT`` chain.
 
 on any NFVi system viewed as a black box (NFVi Full Stack).
 An NFVi full stack exposes the following interfaces:
 - an OpenStack API for those NFVi platforms based on OpenStack
-- an interface to send and receive packets on the data plane (typically through top of rack switches
-  while simpler direct wiring to a looping device would also work)
+- an interface to send and receive packets on the data plane (typically through top of rack switches while simpler direct wiring to a looping device would also work)
 
 The NFVi full stack can be any functional OpenStack system that provides the above interfaces.
 NFVbench can also be used without OpenStack on any networking device that can handle L2 forwarding or L3 routing.
 
 # https://trex-tgn.cisco.com/trex/doc/trex_stateless.html#_tutorial_field_engine_significantly_improve_performance
 # If cache_size = 0 (or empty): no cache will be used by TRex (default)
 # If cache_size < 0: cache_size will be set to flow count value
-cache_size:
+cache_size: 0
 # The cache size is actually limited by the number of 64B mbufs configured in the trex platform configuration (see Trex manual 6.2.2. Memory section configuration)
 # Trex will use 1 x 64B mbuf per pre-built cached packet, assuming 1 pre-built cached packet per flow, it means for very large number of flows, the number of configured mbuf_64 will need to be set accordingly.
 mbuf_64:
 
 from netaddr import IPAddress
 from netaddr import IPNetwork
 
-from log import LOG
+from .log import LOG
 
 
 class ChainException(Exception):
     """Exception while operating the chains."""
 
-    pass
-
-
 class ChainRouter(object):
     """Could be a shared router across all chains or a chain private router."""
 
 
 
 from collections import OrderedDict
 
-from chaining import ChainManager
-from log import LOG
-from specs import ChainType
-from stats_manager import StatsManager
-from traffic_client import TrafficClient
+from .chaining import ChainManager
+from .log import LOG
+from .specs import ChainType
+from .stats_manager import StatsManager
+from .traffic_client import TrafficClient
 
 
 class ChainRunner(object):
 
         Specialized workers can insert their own interface stats inside each existing packet path
         stats for every chain.
         """
-        pass
 
     def update_interface_stats(self, diff=False):
         """Update all interface stats.
         Make sure that the interface stats inserted in insert_interface_stats() are updated
         with proper values
         """
-        pass
 
 from novaclient.client import Client
 
 from attrdict import AttrDict
-from chain_router import ChainRouter
-import compute
-from log import LOG
-from specs import ChainType
+from .chain_router import ChainRouter
+from . import compute
+from .log import LOG
+from .specs import ChainType
 # Left and right index for network and port lists
 LEFT = 0
 RIGHT = 1
 class ChainException(Exception):
     """Exception while operating the chains."""
 
-    pass
-
-
 class NetworkEncaps(object):
     """Network encapsulation."""
 
                     # here we MUST wait until this instance is resolved otherwise subsequent
                     # VNF creation can be placed in other hypervisors!
                     config = self.manager.config
-                    max_retries = (config.check_traffic_time_sec +
-                                   config.generic_poll_sec - 1) / config.generic_poll_sec
+                    max_retries = int((config.check_traffic_time_sec +
+                                       config.generic_poll_sec - 1) / config.generic_poll_sec)
                     retry = 0
                     for retry in range(max_retries):
                         status = self.get_status()
         return: the hypervisor where the matching port runs or None if not found
         """
         # _existing_ports is a dict of list of ports indexed by network id
-        for port_list in self.get_existing_ports().values():
+        for port_list in list(self.get_existing_ports().values()):
             for port in port_list:
                 try:
                     if port['mac_address'] == mac:
 
 from novaclient.exceptions import NotFound
 from tabulate import tabulate
 
-import credentials as credentials
-from log import LOG
+from . import credentials
+from .log import LOG
 
 
 class ComputeCleaner(object):
         LOG.info("NFVbench will delete resources shown...")
         clean_options = None
         if prompt:
-            answer = raw_input("Do you want to delete all ressources? (y/n) ")
+            answer = input("Do you want to delete all ressources? (y/n) ")
             if answer.lower() != 'y':
-                print "What kind of resources do you want to delete?"
+                print("What kind of resources do you want to delete?")
                 all_option = ""
                 all_option_codes = []
                 for cleaner in self.cleaners:
                     code = cleaner.get_cleaner_code()
-                    print "%s: %s" % (code[0], code)
+                    print(("%s: %s" % (code[0], code)))
                     all_option += code[0]
                     all_option_codes.append(code)
-                print "a: all resources - a shortcut for '%s'" % all_option
+                print(("a: all resources - a shortcut for '%s'" % all_option))
                 all_option_codes.append("all resources")
-                print "q: quit"
-                answer_res = raw_input(":").lower()
+                print("q: quit")
+                answer_res = input(":").lower()
                 # Check only first character because answer_res can be "flavor" and it is != all
                 if answer_res[0] == "a":
                     clean_options = all_option
 
 import keystoneauth1
 import novaclient
 
-from log import LOG
+from .log import LOG
 
 
 class Compute(object):
         retry = 0
         try:
             # check image is file/url based.
-            with open(image_file) as f_image:
+            with open(image_file, 'rb') as f_image:
                 img = self.glance_client.images.create(name=str(final_image_name),
                                                        disk_format="qcow2",
                                                        container_format="bare",
 
 from attrdict import AttrDict
 import yaml
 
-from log import LOG
+from .log import LOG
 
 def config_load(file_name, from_cfg=None, whitelist_keys=None):
     """Load a yaml file into a config dict, merge with from_cfg if not None
 def _validate_config(subset, superset, whitelist_keys):
     def get_err_config(subset, superset):
         result = {}
-        for k, v in subset.items():
+        for k, v in list(subset.items()):
             if k not in whitelist_keys:
                 if k not in superset:
                     result.update({k: v})
 
 This module is used to override the configuration with platform specific constraints and extensions
 """
 import abc
-import specs
+from . import specs
 
 
-class ConfigPluginBase(object):
+class ConfigPluginBase(object, metaclass=abc.ABCMeta):
     """Base class for config plugins."""
 
-    __metaclass__ = abc.ABCMeta
-
     class InitializationFailure(Exception):
         """Used in case of any init failure."""
 
-        pass
-
     def __init__(self, config):
         """Save configuration."""
         if not config:
 
     def validate_config(self, config, openstack_spec):
         """Nothing to validate by default."""
-        pass
 
     def prepare_results_config(self, cfg):
         """Nothing to add the results by default."""
 
 from keystoneauth1.identity import v2
 from keystoneauth1.identity import v3
 from keystoneauth1 import session
-from log import LOG
+from .log import LOG
 
 
 class Credentials(object):
 
 #
 """Factory for creating worker and config plugin instances."""
 
-import chain_workers as workers
-from config_plugin import ConfigPlugin
+from . import chain_workers as workers
+from .config_plugin import ConfigPlugin
 
 
 class BasicFactory(object):
 
     def __get_highest_level(self):
         if self.__error_counter > 0:
             return logging.ERROR
-        elif self.__warning_counter > 0:
+        if self.__warning_counter > 0:
             return logging.WARNING
         return logging.INFO
 
         highest_level = self.__get_highest_level()
         if highest_level == logging.INFO:
             return "GOOD RUN"
-        elif highest_level == logging.WARNING:
+        if highest_level == logging.WARNING:
             return "RUN WITH WARNINGS"
         return "RUN WITH ERRORS"
 
 
 import pbr.version
 from pkg_resources import resource_string
 
-from __init__ import __version__
-from chain_runner import ChainRunner
-from cleanup import Cleaner
-from config import config_load
-from config import config_loads
-import credentials as credentials
-from fluentd import FluentLogHandler
-import log
-from log import LOG
-from nfvbenchd import WebServer
-from specs import ChainType
-from specs import Specs
-from summarizer import NFVBenchSummarizer
-import utils
+from .__init__ import __version__
+from .chain_runner import ChainRunner
+from .cleanup import Cleaner
+from .config import config_load
+from .config import config_loads
+from . import credentials
+from .fluentd import FluentLogHandler
+from . import log
+from .log import LOG
+from .nfvbenchd import WebServer
+from .specs import ChainType
+from .specs import Specs
+from .summarizer import NFVBenchSummarizer
+from . import utils
 
 fluent_logger = None
 
         try:
             # recalc the running config based on the base config and options for this run
             self._update_config(opts)
-            if self.config.cache_size < 0:
+            if int(self.config.cache_size) < 0:
                 self.config.cache_size = self.config.flow_count
             # check that an empty openrc file (no OpenStack) is only allowed
             # with EXT chain
         log.set_level(debug=opts.debug)
 
         if opts.version:
-            print pbr.version.VersionInfo('nfvbench').version_string_with_vcs()
+            print((pbr.version.VersionInfo('nfvbench').version_string_with_vcs()))
             sys.exit(0)
 
         if opts.summary:
                 result = json.load(json_data)
                 if opts.user_label:
                     result['config']['user_label'] = opts.user_label
-                print NFVBenchSummarizer(result, fluent_logger)
+                print((NFVBenchSummarizer(result, fluent_logger)))
             sys.exit(0)
 
         # show default config in text/yaml format
         if opts.show_default_config:
-            print default_cfg
+            print((default_cfg.decode("utf-8")))
             sys.exit(0)
 
         config.name = ''
 
         # show running config in json format
         if opts.show_config:
-            print json.dumps(config, sort_keys=True, indent=4)
+            print((json.dumps(config, sort_keys=True, indent=4)))
             sys.exit(0)
 
         # update the config in the config plugin as it might have changed
                     raise Exception(err_msg)
 
                 # remove unfilled values
-                opts = {k: v for k, v in vars(opts).iteritems() if v is not None}
+                opts = {k: v for k, v in list(vars(opts).items()) if v is not None}
                 # get CLI args
                 params = ' '.join(str(e) for e in sys.argv[1:])
                 result = nfvbench_instance.run(opts, params)
             'status': NFVBench.STATUS_ERROR,
             'error_message': traceback.format_exc()
         })
-        print str(exc)
+        print((str(exc)))
     finally:
         if fluent_logger:
             # only send a summary record if there was an actual nfvbench run or
 
 #
 
 import json
-import Queue
+import queue
 from threading import Thread
 import uuid
 
 from flask import jsonify
 from flask import request
 
-from summarizer import NFVBenchSummarizer
+from .summarizer import NFVBenchSummarizer
 
-from log import LOG
-from utils import byteify
-from utils import RunLock
+from .log import LOG
+from .utils import byteify
+from .utils import RunLock
 
-from __init__ import __version__
+from .__init__ import __version__
 
 STATUS_OK = 'OK'
 STATUS_ERROR = 'ERROR'
 
 class Ctx(object):
     MAXLEN = 5
-    run_queue = Queue.Queue()
+    run_queue = queue.Queue()
     busy = False
     result = None
     results = {}
                 res = Ctx.results[request_id]
             except KeyError:
                 return None
-
+            # pylint: disable=unsubscriptable-object
             if Ctx.result and request_id == Ctx.result['request_id']:
                 Ctx.result = None
-
-            return res
-        else:
-            res = Ctx.result
-            if res:
-                Ctx.result = None
             return res
+            # pylint: enable=unsubscriptable-object
+        res = Ctx.result
+        if res:
+            Ctx.result = None
+        return res
 
     @staticmethod
     def is_busy():
                 return jsonify(res)
             # result for given request_id not found
             return jsonify(result_json(STATUS_NOT_FOUND, not_found_msg, request_id))
-        else:
-            if Ctx.is_busy():
-                # task still pending, return with request_id
-                return jsonify(result_json(STATUS_PENDING,
-                                           pending_msg,
-                                           Ctx.get_current_request_id()))
-
-            res = Ctx.get_result()
-            if res:
-                return jsonify(res)
-            return jsonify(not_busy_json)
+        if Ctx.is_busy():
+            # task still pending, return with request_id
+            return jsonify(result_json(STATUS_PENDING,
+                                       pending_msg,
+                                       Ctx.get_current_request_id()))
 
-    return app
+        res = Ctx.get_result()
+        if res:
+            return jsonify(res)
+        return jsonify(not_busy_json)
 
+    return app
 
 class WebServer(object):
     """This class takes care of the web server. Caller should simply create an instance
             # print config
             try:
                 # remove unfilled values as we do not want them to override default values with None
-                config = {k: v for k, v in config.items() if v is not None}
+                config = {k: v for k, v in list(config.items()) if v is not None}
                 with RunLock():
                     if self.fluent_logger:
                         self.fluent_logger.start_new_run()
 
 
 import copy
 
-from traffic_gen.traffic_base import Latency
+from .traffic_gen.traffic_base import Latency
 
 class InterfaceStats(object):
     """A class to hold the RX and TX counters for a virtual or physical interface.
             chains['total'] = agg_pps.get_stats(reverse)
 
         for index, pps in enumerate(self.pps_list):
-            chains[index] = pps.get_stats(reverse)
+            chains[str(index)] = pps.get_stats(reverse)
         return {'interfaces': self._get_if_agg_name(reverse),
                 'chains': chains}
 
             'Forward': {
                 'interfaces': ['Port0', 'vhost0', 'Port1'],
                 'chains': {
-                    0: {'packets': [2000054, 1999996, 1999996],
+                    '0': {'packets': [2000054, 1999996, 1999996],
                         'min_usec': 10,
                         'max_usec': 187,
                         'avg_usec': 45},
-                    1: {...},
+                    '1': {...},
                     'total': {...}
                 }
             },
 
 #
 import time
 
-from log import LOG
-from packet_stats import PacketPathStatsManager
-from stats_collector import IntervalCollector
+from .log import LOG
+from .packet_stats import PacketPathStatsManager
+from .stats_collector import IntervalCollector
 
 
 class StatsManager(object):
 
     In the case of shared net, some columns in packets array can have ''.
     Some columns cab also be None which means the data is not available.
     """
-    for stats in chain_stats.values():
+    for stats in list(chain_stats.values()):
         packets = stats['packets']
         count = len(packets)
         if count > 1:
     def standard(data):
         if isinstance(data, int):
             return Formatter.int(data)
-        elif isinstance(data, float):
+        if isinstance(data, float):
             return Formatter.float(4)(data)
         return Formatter.fixed(data)
 
     def percentage(data):
         if data is None:
             return ''
-        elif math.isnan(data):
+        if math.isnan(data):
             return '-'
         return Formatter.suffix('%')(Formatter.float(4)(data))
 
     """ASCII readable table class."""
 
     def __init__(self, header):
-        header_row, self.formatters = zip(*header)
+        header_row, self.formatters = list(zip(*header))
         self.data = [header_row]
         self.columns = len(header_row)
 
 
     def _put_dict(self, data):
         with self._create_block(False):
-            for key, value in data.iteritems():
+            for key, value in list(data.items()):
                 if isinstance(value, dict):
                     self._put(key + ':')
                     self._put_dict(value)
                     if network_benchmark['versions']:
                         self._put('Versions:')
                         with self._create_block():
-                            for component, version in network_benchmark['versions'].iteritems():
+                            for component, version in list(network_benchmark['versions'].items()):
                                 self._put(component + ':', version)
 
                 if self.config['ndr_run'] or self.config['pdr_run']:
                         if self.config['pdr_run']:
                             self._put('PDR:', self.config['measurement']['PDR'])
                 self._put('Service chain:')
-                for result in network_benchmark['service_chain'].iteritems():
+                for result in list(network_benchmark['service_chain'].items()):
                     with self._create_block():
                         self.__chain_summarize(*result)
 
         self._put('Bidirectional:', traffic_benchmark['bidirectional'])
         self._put('Flow count:', traffic_benchmark['flow_count'])
         self._put('Service chains count:', traffic_benchmark['service_chain_count'])
-        self._put('Compute nodes:', traffic_benchmark['compute_nodes'].keys())
+        self._put('Compute nodes:', list(traffic_benchmark['compute_nodes'].keys()))
 
         self.__record_header_put('profile', traffic_benchmark['profile'])
         self.__record_header_put('bidirectional', traffic_benchmark['bidirectional'])
         self.__record_header_put('flow_count', traffic_benchmark['flow_count'])
         self.__record_header_put('sc_count', traffic_benchmark['service_chain_count'])
-        self.__record_header_put('compute_nodes', traffic_benchmark['compute_nodes'].keys())
+        self.__record_header_put('compute_nodes', list(traffic_benchmark['compute_nodes'].keys()))
         with self._create_block(False):
             self._put()
             if not self.config['no_traffic']:
                     except KeyError:
                         pass
 
-            for entry in traffic_benchmark['result'].iteritems():
+            for entry in list(traffic_benchmark['result'].items()):
                 if 'warning' in entry:
                     continue
                 self.__chain_analysis_summarize(*entry)
             summary_table = Table(self.ndr_pdr_header)
 
         if self.config['ndr_run']:
-            for frame_size, analysis in traffic_result.iteritems():
+            for frame_size, analysis in list(traffic_result.items()):
                 if frame_size == 'warning':
                     continue
                 summary_table.add_row([
                     'max_delay_usec': analysis['ndr']['stats']['overall']['max_delay_usec']
                 }})
         if self.config['pdr_run']:
-            for frame_size, analysis in traffic_result.iteritems():
+            for frame_size, analysis in list(traffic_result.items()):
                 if frame_size == 'warning':
                     continue
                 summary_table.add_row([
                     'max_delay_usec': analysis['pdr']['stats']['overall']['max_delay_usec']
                 }})
         if self.config['single_run']:
-            for frame_size, analysis in traffic_result.iteritems():
+            for frame_size, analysis in list(traffic_result.items()):
                 summary_table.add_row([
                     frame_size,
                     analysis['stats']['overall']['drop_rate_percent'],
         chain_stats: {
              'interfaces': ['Port0', 'drop %'', 'vhost0', 'Port1'],
              'chains': {
-                 0: {'packets': [2000054, '-0.023%', 1999996, 1999996],
+                 '0': {'packets': [2000054, '-0.023%', 1999996, 1999996],
                      'lat_min_usec': 10,
                      'lat_max_usec': 187,
                      'lat_avg_usec': 45},
-                 1: {...},
+                 '1': {...},
                  'total': {...}
              }
         }
         lat_map = {'lat_avg_usec': 'Avg lat.',
                    'lat_min_usec': 'Min lat.',
                    'lat_max_usec': 'Max lat.'}
-        if 'lat_avg_usec' in chains[0]:
+        if 'lat_avg_usec' in chains['0']:
             lat_keys = ['lat_avg_usec', 'lat_min_usec', 'lat_max_usec']
             for key in lat_keys:
                 header.append((lat_map[key], Formatter.standard))
 
         table = Table(header)
-        for chain in sorted(chains.keys()):
+        for chain in sorted(list(chains.keys()), key=str):
             row = [chain] + chains[chain]['packets']
             for lat_key in lat_keys:
                 row.append('{:,} usec'.format(chains[chain][lat_key]))
 
 from trex.stl.api import UDP
 # 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."""
 
         - 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.
         - 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.
         #   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))
         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):
+        for chain_idx in range(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)
 
     @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):
             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."""
     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)
 
         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
 
         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
             get_mac_id = lambda packet: packet['binary'][56:62]
         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()
             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]
         """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:
 
     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'],
         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()
         # 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
         # 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}),
         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
 
 #    under the License.
 
 from nfvbench.log import LOG
-from traffic_base import AbstractTrafficGenerator
-import traffic_utils as utils
+from .traffic_base import AbstractTrafficGenerator
+from . import traffic_utils as utils
 
 
 class DummyTG(AbstractTrafficGenerator):
     def fetch_capture_packets(self):
         def _get_packet_capture(mac):
             # convert text to binary
-            src_mac = mac.replace(':', '').decode('hex')
-            return {'binary': 'SSSSSS' + src_mac}
+            src_mac = bytearray.fromhex(mac.replace(':', '')).decode()
+            return {'binary': bytes('SSSSSS' + src_mac, 'ascii')}
 
         # for packet capture, generate 2*scc random packets
         # normally we should generate packets coming from the right dest macs
 
 import sys
 
 from nfvbench.log import LOG
-import traffic_utils
+from . import traffic_utils
 
 
 class Latency(object):
 
         latency_list: aggregate all latency values from list if not None
         """
-        self.min_usec = sys.maxint
+        self.min_usec = sys.maxsize
         self.max_usec = 0
         self.avg_usec = 0
         self.hdrh = None
 
     def available(self):
         """Return True if latency information is available."""
-        return self.min_usec != sys.maxint
+        return self.min_usec != sys.maxsize
 
 
 class TrafficGeneratorException(Exception):
     """Exception for traffic generator."""
 
-    pass
-
-
 class AbstractTrafficGenerator(object):
 
     def __init__(self, traffic_client):
 
     def clear_streamblock(self):
         """Clear all streams from the traffic generator."""
-        pass
 
     @abc.abstractmethod
     def resolve_arp(self):
                 else a dict of list of dest macs indexed by port#
                 the dest macs in the list are indexed by the chain id
         """
-        pass
 
     @abc.abstractmethod
     def get_macs(self):
 
         return: a list of MAC addresses indexed by the port#
         """
-        pass
 
     @abc.abstractmethod
     def get_port_speed_gbps(self):
 
         return: a list of speed in Gbps indexed by the port#
         """
-        pass
 
         pps = bps_to_pps(bps, avg_packet_size)
     else:
         raise Exception('Traffic config needs to have a rate type key')
-
     return {
         'initial_rate_type': initial_rate_type,
-        'rate_pps': int(pps),
+        'rate_pps': int(float(pps)),
         'rate_percent': load,
-        'rate_bps': int(bps)
+        'rate_bps': int(float(bps))
     }
 
 
             rate_pps = rate_pps[:-1]
         except KeyError:
             multiplier = 1
-        rate_pps = int(rate_pps.strip()) * multiplier
+        rate_pps = int(float(rate_pps.strip()) * multiplier)
         if rate_pps <= 0:
             raise Exception('%s is out of valid range' % rate_str)
         return {'rate_pps': str(rate_pps)}
-    elif rate_str.endswith('ps'):
+    if rate_str.endswith('ps'):
         rate = rate_str.replace('ps', '').strip()
         bit_rate = bitmath.parse_string(rate).bits
         if bit_rate <= 0:
             raise Exception('%s is out of valid range' % rate_str)
         return {'rate_bps': str(int(bit_rate))}
-    elif rate_str.endswith('%'):
+    if rate_str.endswith('%'):
         rate_percent = float(rate_str.replace('%', '').strip())
         if rate_percent <= 0 or rate_percent > 100.0:
             raise Exception('%s is out of valid range (must be 1-100%%)' % rate_str)
         return {'rate_percent': str(rate_percent)}
-    else:
-        raise Exception('Unknown rate string format %s' % rate_str)
+    raise Exception('Unknown rate string format %s' % rate_str)
 
 def get_load_from_rate(rate_str, avg_frame_size=64, line_rate='10Gbps'):
     '''From any rate string (with unit) return the corresponding load (in % unit)
     if 'rate_pps' in rate:
         pps = rate['rate_pps']
         return '{}pps'.format(pps)
-    elif 'rate_bps' in rate:
+    if 'rate_bps' in rate:
         bps = rate['rate_bps']
         return '{}bps'.format(bps)
-    elif 'rate_percent' in rate:
+    if 'rate_percent' in rate:
         load = rate['rate_percent']
         return '{}%'.format(load)
     assert False
 
 def nan_replace(d):
     """Replaces every occurence of 'N/A' with float nan."""
-    for k, v in d.iteritems():
+    for k, v in d.items():
         if isinstance(v, dict):
             nan_replace(v)
         elif v == 'N/A':
 def int_to_mac(i):
     """Converts integer representation of MAC address to hex string."""
     mac = format(i, 'x').zfill(12)
-    blocks = [mac[x:x + 2] for x in xrange(0, len(mac), 2)]
+    blocks = [mac[x:x + 2] for x in range(0, len(mac), 2)]
     return ':'.join(blocks)
 
 from nfvbench.utils import cast_integer
 from nfvbench.utils import timeout
 from nfvbench.utils import TimeoutError
-from traffic_base import AbstractTrafficGenerator
-from traffic_base import TrafficGeneratorException
-import traffic_utils as utils
-from traffic_utils import IMIX_AVG_L2_FRAME_SIZE
-from traffic_utils import IMIX_L2_SIZES
-from traffic_utils import IMIX_RATIOS
 
 # pylint: disable=import-error
 from trex.common.services.trex_service_arp import ServiceARP
 from trex.stl.api import UDP
 from trex.stl.api import XByteField
 
-
 # pylint: enable=import-error
 
+from .traffic_base import AbstractTrafficGenerator
+from .traffic_base import TrafficGeneratorException
+from . import traffic_utils as utils
+from .traffic_utils import IMIX_AVG_L2_FRAME_SIZE
+from .traffic_utils import IMIX_L2_SIZES
+from .traffic_utils import IMIX_RATIOS
+
 class VXLAN(Packet):
     """VxLAN class."""
 
     def __connect_after_start(self):
         # after start, Trex may take a bit of time to initialize
         # so we need to retry a few times
-        for it in xrange(self.config.generic_retry_count):
+        for it in range(self.config.generic_retry_count):
             try:
                 time.sleep(1)
                 self.client.connect()
             except Exception as ex:
                 if it == (self.config.generic_retry_count - 1):
                     raise
-                LOG.info("Retrying connection to TRex (%s)...", ex.message)
+                LOG.info("Retrying connection to TRex (%s)...", ex.msg)
 
     def connect(self):
         """Connect to the TRex server."""
             if os.path.isfile(logpath):
                 # Wait for TRex to finish writing error message
                 last_size = 0
-                for _ in xrange(self.config.generic_retry_count):
+                for _ in range(self.config.generic_retry_count):
                     size = os.path.getsize(logpath)
                     if size == last_size:
                         # probably not writing anymore
         LOG.info("Restarting TRex ...")
         self.__stop_server()
         # Wait for server stopped
-        for _ in xrange(self.config.generic_retry_count):
+        for _ in range(self.config.generic_retry_count):
             time.sleep(1)
             if not self.client.is_connected():
                 LOG.info("TRex is stopped...")
                     self.client.release(ports=ports)
                 self.client.server_shutdown()
             except STLError as e:
-                LOG.warn('Unable to stop TRex. Error: %s', e)
+                LOG.warning('Unable to stop TRex. Error: %s', e)
         else:
             LOG.info('Using remote TRex. Unable to stop TRex')
 
                     arp_dest_macs[port] = dst_macs
                     LOG.info('ARP resolved successfully for port %s', port)
                     break
-                else:
-                    retry = attempt + 1
-                    LOG.info('Retrying ARP for: %s (retry %d/%d)',
-                             unresolved, retry, self.config.generic_retry_count)
-                    if retry < self.config.generic_retry_count:
-                        time.sleep(self.config.generic_poll_sec)
+
+                retry = attempt + 1
+                LOG.info('Retrying ARP for: %s (retry %d/%d)',
+                         unresolved, retry, self.config.generic_retry_count)
+                if retry < self.config.generic_retry_count:
+                    time.sleep(self.config.generic_poll_sec)
             else:
                 LOG.error('ARP timed out for port %s (resolved %d out of %d)',
                           port,
 
 import subprocess
 import yaml
 
-from log import LOG
+from .log import LOG
 
 
 class TrafficServerException(Exception):
                 try:
                     result = yaml.safe_load(stream)
                 except yaml.YAMLError as exc:
-                    print exc
+                    print(exc)
         return result
 
     def __save_config(self, generator_config, filename):
                     try:
                         threads = ",".join([repr(thread) for thread in core.threads])
                     except TypeError:
-                        LOG.warn("No threads defined for socket %s", core.socket)
+                        LOG.warning("No threads defined for socket %s", core.socket)
                     core_result = """
                   - socket : {socket}
                     threads : [{threads}]""".format(socket=core.socket, threads=threads)
 
 import fcntl
 from functools import wraps
 import json
-from log import LOG
+from .log import LOG
 
 
 class TimeoutError(Exception):
 
 def byteify(data, ignore_dicts=False):
     # if this is a unicode string, return its string representation
-    if isinstance(data, unicode):
+    if isinstance(data, str):
         return data.encode('utf-8')
     # if this is a list of values, return list of byteified values
     if isinstance(data, list):
     # but only if we haven't already byteified it
     if isinstance(data, dict) and not ignore_dicts:
         return {byteify(key, ignore_dicts=ignore_dicts): byteify(value, ignore_dicts=ignore_dicts)
-                for key, value in data.iteritems()}
+                for key, value in list(data.items())}
     # if it's anything else, return it in its original form
     return data
 
 
 # --enable=similarities". If you want to run only the classes checker, but have
 # no Warning level messages displayed, use"--disable=all --enable=classes
 # --disable=W"
-disable=unused-argument,global-statement,too-many-statements,too-many-arguments,too-many-branches,catching-non-exception,relative-import,too-many-locals,invalid-name,broad-except,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,import-star-module-level,raw-checker-failed,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,missing-docstring,redefined-builtin,no-name-in-module,no-self-use,no-member,arguments-differ,attribute-defined-outside-init,non-iterator-returned,eval-used,unexpected-keyword-arg,pointless-string-statement,no-value-for-parameter
+disable=unused-argument,global-statement,too-many-statements,too-many-arguments,too-many-branches,catching-non-exception,relative-import,too-many-locals,invalid-name,broad-except,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,import-star-module-level,raw-checker-failed,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,missing-docstring,redefined-builtin,no-name-in-module,no-self-use,no-member,arguments-differ,attribute-defined-outside-init,non-iterator-returned,eval-used,unexpected-keyword-arg,pointless-string-statement,no-value-for-parameter,useless-object-inheritance,import-outside-toplevel,wrong-import-order
 
 # Enable the message, report, category or checker with the given id(s). You can
 # either give multiple identifier separated by comma (,) or put this option
 
     class STLDummy(Exception):
         """Dummy class."""
 
-        pass
-
     trex_lib_mod = ModuleType('trex')
     sys.modules['trex'] = trex_lib_mod
     stl_lib_mod = ModuleType('trex.stl')
 
 def no_op():
     """Empty function."""
-    pass
 
 #
 """Test Chaining functions."""
 
-from mock_trex import no_op
-
 from mock import MagicMock
 from mock import patch
 import pytest
 
+from .mock_trex import no_op
+
 from nfvbench.chain_runner import ChainRunner
 from nfvbench.chaining import ChainException
 from nfvbench.chaining import ChainVnfPort
 from nfvbench.traffic_gen.traffic_base import Latency
 from nfvbench.traffic_gen.trex_gen import TRex
 
-
 # just to get rid of the unused function warning
 no_op()
 
     runner.close()
 
 def test_pvp_chain_runner_no_admin_no_config_values():
-    """Test PVP chain runner."""
+    """Test PVP/mock chain runner."""
     cred = MagicMock(spec=nfvbench.credentials.Credentials)
     cred.is_admin = False
     for shared_net in [True, False]:
         nfvb = NFVBench(config, openstack_spec, config_plugin, factory)
         res = nfvb.run({}, 'pytest')
         if res['status'] != 'OK':
-            print res
+            print(res)
         assert res['status'] == 'OK'
 
 
 
 #    License for the specific language governing permissions and limitations
 #    under the License.
 #
-from mock_trex import no_op
-
 import json
 import logging
 import sys
 from nfvbench.traffic_client import TrafficClient
 import nfvbench.traffic_gen.traffic_utils as traffic_utils
 
+from .mock_trex import no_op
 
 # just to get rid of the unused function warning
 no_op()
 
 [tox]
 minversion = 1.6
-envlist = py27,pep8,lint,docs,docs-linkcheck
+envlist = py36,pep8,lint,docs,docs-linkcheck
 skipsdist = True
 
 [testenv]
 commands = py.test -q --basetemp={envtmpdir} {posargs}
 
 [testenv:pep8]
+basepython = python3
 commands = flake8 {toxinidir}
 
 [testenv:lint]
+basepython = python3
 commands = pylint --rcfile pylint.rc nfvbench test
 
 [testenv:venv]
+basepython = python3
 commands = {posargs}
 
 [testenv:cover]
+basepython = python3
 commands = python setup.py testr --coverage --testr-args='{posargs}'
 
 [flake8]
 #H233: Python 3.x incompatible use of print operator
 #H236: Python 3.x incompatible __metaclass__, use six.add_metaclass()
 #H302: import only modules.
+#H304: No relative imports
+#H306: imports not in alphabetical order
 #H404: multi line docstring should start without a leading new line
 #H405: multi line docstring summary not separated with an empty line
 #H904: Wrap long lines in parentheses instead of a backslash
-ignore = E123,E125,H803,E302,E303,H104,H233,H236,H302,H404,H405,H904,D102,D100,D107
+ignore = E123,E125,H803,E302,E303,H104,H233,H236,H302,H304,H306,H404,H405,H904,D102,D100,D107
 builtins = _
 exclude=venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,dib-venv
 
 [testenv:docs]
+basepython = python3
 deps = -rdocs/requirements.txt
 commands =
     sphinx-build -b html -n -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/html
 whitelist_externals = echo
 
 [testenv:docs-linkcheck]
+basepython = python3
 deps = -rdocs/requirements.txt
 commands = sphinx-build -b linkcheck -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/linkcheck