PLRsearch: Initial implementation and suites
[csit.git] / resources / libraries / python / TrafficGenerator.py
index 698b67e..6091511 100644 (file)
 from robot.api import logger
 from robot.libraries.BuiltIn import BuiltIn
 
-from resources.libraries.python.constants import Constants
-from resources.libraries.python.ssh import SSH
-from resources.libraries.python.topology import NodeType
-from resources.libraries.python.topology import NodeSubTypeTG
-from resources.libraries.python.topology import Topology
-from resources.libraries.python.DropRateSearch import DropRateSearch
+from .DropRateSearch import DropRateSearch
+from .constants import Constants
+from .ssh import SSH
+from .topology import NodeType
+from .topology import NodeSubTypeTG
+from .topology import Topology
+from .MLRsearch.AbstractMeasurer import AbstractMeasurer
+from .MLRsearch.MultipleLossRatioSearch import MultipleLossRatioSearch
+from .MLRsearch.ReceiveRateMeasurement import ReceiveRateMeasurement
+from .PLRsearch.PLRsearch import PLRsearch
 
-__all__ = ['TrafficGenerator', 'TGDropRateSearchImpl']
+__all__ = ['TGDropRateSearchImpl', 'TrafficGenerator', 'OptimizedSearch']
 
 
 class TGDropRateSearchImpl(DropRateSearch):
@@ -33,23 +37,26 @@ class TGDropRateSearchImpl(DropRateSearch):
         super(TGDropRateSearchImpl, self).__init__()
 
     def measure_loss(self, rate, frame_size, loss_acceptance,
-                     loss_acceptance_type, traffic_type):
+                     loss_acceptance_type, traffic_type, skip_warmup=False):
         """Runs the traffic and evaluate the measured results.
 
         :param rate: Offered traffic load.
         :param frame_size: Size of frame.
         :param loss_acceptance: Permitted drop ratio or frames count.
         :param loss_acceptance_type: Type of permitted loss.
-        :param traffic_type: Traffic profile ([2,3]-node-L[2,3], ...).
-        :type rate: int
+        :param traffic_type: Module name as a traffic type identifier.
+            See resources/traffic_profiles/trex for implemented modules.
+        :param skip_warmup: Start TRex without warmup traffic if true.
+        :type rate: float
         :type frame_size: str
         :type loss_acceptance: float
         :type loss_acceptance_type: LossAcceptanceType
         :type traffic_type: str
+        :type skip_warmup: bool
         :returns: Drop threshold exceeded? (True/False)
         :rtype: bool
-        :raises: NotImplementedError if TG is not supported.
-        :raises: RuntimeError if TG is not specified.
+        :raises NotImplementedError: If TG is not supported.
+        :raises RuntimeError: If TG is not specified.
         """
         # we need instance of TrafficGenerator instantiated by Robot Framework
         # to be able to use trex_stl-*()
@@ -60,9 +67,15 @@ class TGDropRateSearchImpl(DropRateSearch):
             raise RuntimeError('TG subtype not defined')
         elif tg_instance.node['subtype'] == NodeSubTypeTG.TREX:
             unit_rate = str(rate) + self.get_rate_type_str()
-            tg_instance.trex_stl_start_remote_exec(self.get_duration(),
-                                                   unit_rate, frame_size,
-                                                   traffic_type)
+            if skip_warmup:
+                tg_instance.trex_stl_start_remote_exec(self.get_duration(),
+                                                       unit_rate, frame_size,
+                                                       traffic_type,
+                                                       warmup_time=0.0)
+            else:
+                tg_instance.trex_stl_start_remote_exec(self.get_duration(),
+                                                       unit_rate, frame_size,
+                                                       traffic_type)
             loss = tg_instance.get_loss()
             sent = tg_instance.get_sent()
             if self.loss_acceptance_type_is_percentage():
@@ -89,10 +102,15 @@ class TGDropRateSearchImpl(DropRateSearch):
         return tg_instance.get_latency_int()
 
 
-class TrafficGenerator(object):
-    """Traffic Generator."""
+class TrafficGenerator(AbstractMeasurer):
+    """Traffic Generator.
+
+    FIXME: Describe API."""
+
+    # TODO: Decrease friction between various search and rate provider APIs.
+    # TODO: Remove "trex" from lines which could work with other TGs.
 
-    # use one instance of TrafficGenerator for all tests in test suite
+    # Use one instance of TrafficGenerator for all tests in test suite
     ROBOT_LIBRARY_SCOPE = 'TEST SUITE'
 
     def __init__(self):
@@ -104,6 +122,10 @@ class TrafficGenerator(object):
         self._node = None
         # T-REX interface order mapping
         self._ifaces_reordered = False
+        # Parameters not given by measure().
+        self.frame_size = None
+        self.traffic_type = None
+        self.warmup_time = None
 
     @property
     def node(self):
@@ -174,7 +196,7 @@ class TrafficGenerator(object):
         :type tg_if1_dst_mac: str
         :type tg_if2_dst_mac: str
         :returns: nothing
-        :raises: RuntimeError in case of issue during initialization.
+        :raises RuntimeError: In case of issue during initialization.
         """
         if tg_node['type'] != NodeType.TG:
             raise RuntimeError('Node type is not a TG')
@@ -307,6 +329,24 @@ class TrafficGenerator(object):
             # critical error occurred
             raise RuntimeError('t-rex-64 startup failed')
 
+    @staticmethod
+    def is_trex_running(node):
+        """Check if TRex is running using pidof.
+
+        :param node: Traffic generator node.
+        :type node: dict
+        :returns: True if TRex is running otherwise False.
+        :rtype: bool
+        :raises RuntimeError: If node type is not a TG.
+        """
+        if node['type'] != NodeType.TG:
+            raise RuntimeError('Node type is not a TG')
+
+        ssh = SSH()
+        ssh.connect(node)
+        ret, _, _ = ssh.exec_command_sudo("pidof t-rex")
+        return bool(int(ret) == 0)
+
     @staticmethod
     def teardown_traffic_generator(node):
         """TG teardown.
@@ -314,8 +354,8 @@ class TrafficGenerator(object):
         :param node: Traffic generator node.
         :type node: dict
         :returns: nothing
-        :raises: RuntimeError if TRex teardown failed.
-        :raises: RuntimeError if node type is not a TG.
+        :raises RuntimeError: If node type is not a TG,
+            or if TRex teardown fails.
         """
         if node['type'] != NodeType.TG:
             raise RuntimeError('Node type is not a TG')
@@ -334,7 +374,7 @@ class TrafficGenerator(object):
         :param node: TRex generator node.
         :type node: dict
         :returns: Nothing
-        :raises: RuntimeError if stop traffic script fails.
+        :raises RuntimeError: If stop traffic script fails.
         """
         ssh = SSH()
         ssh.connect(node)
@@ -348,25 +388,26 @@ class TrafficGenerator(object):
 
     def trex_stl_start_remote_exec(self, duration, rate, framesize,
                                    traffic_type, async_call=False,
-                                   latency=True, warmup_time=5):
+                                   latency=True, warmup_time=5.0):
         """Execute script on remote node over ssh to start traffic.
 
         :param duration: Time expresed in seconds for how long to send traffic.
         :param rate: Traffic rate expressed with units (pps, %)
         :param framesize: L2 frame size to send (without padding and IPG).
-        :param traffic_type: Traffic profile.
+        :param traffic_type: Module name as a traffic type identifier.
+            See resources/traffic_profiles/trex for implemented modules.
         :param async_call: If enabled then don't wait for all incomming trafic.
         :param latency: With latency measurement.
         :param warmup_time: Warmup time period.
-        :type duration: int
+        :type duration: float
         :type rate: str
         :type framesize: str
         :type traffic_type: str
         :type async_call: bool
         :type latency: bool
-        :type warmup_time: int
+        :type warmup_time: float
         :returns: Nothing
-        :raises: RuntimeError in case of TG driver issue.
+        :raises RuntimeError: In case of TG driver issue.
         """
         ssh = SSH()
         ssh.connect(self._node)
@@ -392,7 +433,7 @@ class TrafficGenerator(object):
             "{9}'".  # --latency
             format(Constants.REMOTE_FW_DIR, profile_path, duration, framesize,
                    rate, warmup_time, _p0 - 1, _p1 - 1, _async, _latency),
-            timeout=int(duration) + 60)
+            timeout=float(duration) + 60)
 
         if int(ret) != 0:
             raise RuntimeError('TRex stateless runtime error')
@@ -417,26 +458,109 @@ class TrafficGenerator(object):
             self._latency.append(self._result.split(', ')[4].split('=')[1])
             self._latency.append(self._result.split(', ')[5].split('=')[1])
 
+    def trex_stl_start_unidirection(self, duration, rate, framesize,
+                                    traffic_type, tx_port=0, rx_port=1,
+                                    async_call=False, latency=False,
+                                    warmup_time=5.0):
+        """Execute script on remote node over ssh to start unidirection traffic.
+        The purpose of this function is to support performance test that need to
+        measure unidirectional traffic, e.g. Load balancer maglev mode and l3dsr
+        mode test.
+
+        :param duration: Time expresed in seconds for how long to send traffic.
+        :param rate: Traffic rate expressed with units (pps, %)
+        :param framesize: L2 frame size to send (without padding and IPG).
+        :param traffic_type: Module name as a traffic type identifier.
+            See resources/traffic_profiles/trex for implemented modules.
+        :param tx_port: Traffic generator transmit port.
+        :param rx_port: Traffic generator receive port.
+        :param latency: With latency measurement.
+        :param async_call: If enabled then don't wait for all incomming trafic.
+        :param warmup_time: Warmup time period.
+        :type duration: float
+        :type rate: str
+        :type framesize: str
+        :type traffic_type: str
+        :type tx_port: integer
+        :type rx_port: integer
+        :type latency: bool
+        :type async_call: bool
+        :type warmup_time: float
+        :returns: Nothing
+        :raises RuntimeError: In case of TG driver issue.
+        """
+        ssh = SSH()
+        ssh.connect(self._node)
+
+        _latency = "--latency" if latency else ""
+        _async = "--async" if async_call else ""
+
+        profile_path = ("{0}/resources/traffic_profiles/trex/"
+                        "{1}.py".format(Constants.REMOTE_FW_DIR,
+                                        traffic_type))
+        (ret, stdout, _) = ssh.exec_command(
+            "sh -c "
+            "'{0}/resources/tools/trex/trex_stateless_profile.py "
+            "--profile {1} "
+            "--duration {2} "
+            "--frame_size {3} "
+            "--rate {4} "
+            "--warmup_time {5} "
+            "--port_0 {6} "
+            "--port_1 {7} "
+            "{8} "  # --async
+            "{9} "  # --latency
+            "{10}'".  # --unidirection
+            format(Constants.REMOTE_FW_DIR, profile_path, duration, framesize,
+                   rate, warmup_time, tx_port, rx_port, _async, _latency,
+                   "--unidirection"),
+            timeout=float(duration) + 60)
+
+        if int(ret) != 0:
+            raise RuntimeError('TRex unidirection runtime error')
+        elif async_call:
+            #no result
+            self._received = None
+            self._sent = None
+            self._loss = None
+            self._latency = None
+        else:
+            # last line from console output
+            line = stdout.splitlines()[-1]
+
+            self._result = line
+            logger.info('TrafficGen result: {0}'.format(self._result))
+
+            self._received = self._result.split(', ')[1].split('=')[1]
+            self._sent = self._result.split(', ')[2].split('=')[1]
+            self._loss = self._result.split(', ')[3].split('=')[1]
+            self._latency = []
+            self._latency.append(self._result.split(', ')[4].split('=')[1])
+
     def stop_traffic_on_tg(self):
         """Stop all traffic on TG.
 
         :returns: Nothing
-        :raises: RuntimeError if TG is not set.
+        :raises RuntimeError: If TG is not set.
         """
         if self._node is None:
             raise RuntimeError("TG is not set")
         if self._node['subtype'] == NodeSubTypeTG.TREX:
             self.trex_stl_stop_remote_exec(self._node)
 
-    def send_traffic_on_tg(self, duration, rate, framesize,
-                           traffic_type, warmup_time=5, async_call=False,
-                           latency=True):
+    def send_traffic_on_tg(self, duration, rate, framesize, traffic_type,
+                           unidirection=False, tx_port=0, rx_port=1,
+                           warmup_time=5, async_call=False, latency=True):
         """Send traffic from all configured interfaces on TG.
 
         :param duration: Duration of test traffic generation in seconds.
         :param rate: Offered load per interface (e.g. 1%, 3gbps, 4mpps, ...).
         :param framesize: Frame size (L2) in Bytes.
-        :param traffic_type: Traffic profile.
+        :param traffic_type: Module name as a traffic type identifier.
+            See resources/traffic_profiles/trex for implemented modules.
+        :param unidirection: Traffic is unidirectional.
+        :param tx_port: Traffic generator transmit port.
+        :param rx_port: Traffic generator receive port.
         :param warmup_time: Warmup phase in seconds.
         :param async_call: Async mode.
         :param latency: With latency measurement.
@@ -444,14 +568,17 @@ class TrafficGenerator(object):
         :type rate: str
         :type framesize: str
         :type traffic_type: str
-        :type warmup_time: int
+        :type unidirection: bool
+        :type tx_port: integer
+        :type rx_port: integer
+        :type warmup_time: float
         :type async_call: bool
         :type latency: bool
         :returns: TG output.
         :rtype: str
-        :raises: RuntimeError if TG is not set.
-        :raises: RuntimeError if node is not TG or subtype is not specified.
-        :raises: NotImplementedError if TG is not supported.
+        :raises RuntimeError: If TG is not set, or if node is not TG,
+            or if subtype is not specified.
+        :raises NotImplementedError: If TG is not supported.
         """
 
         node = self._node
@@ -464,9 +591,15 @@ class TrafficGenerator(object):
         if node['subtype'] is None:
             raise RuntimeError('TG subtype not defined')
         elif node['subtype'] == NodeSubTypeTG.TREX:
-            self.trex_stl_start_remote_exec(int(duration), rate, framesize,
-                                            traffic_type, async_call, latency,
-                                            warmup_time=warmup_time)
+            if unidirection:
+                self.trex_stl_start_unidirection(duration, rate, framesize,
+                                                 traffic_type, tx_port,
+                                                 rx_port, async_call, latency,
+                                                 warmup_time)
+            else:
+                self.trex_stl_start_remote_exec(duration, rate, framesize,
+                                                traffic_type, async_call,
+                                                latency, warmup_time)
         else:
             raise NotImplementedError("TG subtype not supported")
 
@@ -476,12 +609,23 @@ class TrafficGenerator(object):
         """Fail if loss occurred in traffic run.
 
         :returns: nothing
-        :raises: Exception if loss occured.
+        :raises Exception: If loss occured.
         """
         if self._loss is None:
-            raise Exception('The traffic generation has not been issued')
+            raise RuntimeError('The traffic generation has not been issued')
         if self._loss != '0':
-            raise Exception('Traffic loss occurred: {0}'.format(self._loss))
+            raise RuntimeError('Traffic loss occurred: {0}'.format(self._loss))
+
+    def fail_if_no_traffic_forwarded(self):
+        """Fail if no traffic forwarded.
+
+        :returns: nothing
+        :raises Exception: If no traffic forwarded.
+        """
+        if self._received is None:
+            raise RuntimeError('The traffic generation has not been issued')
+        if self._received == '0':
+            raise RuntimeError('No traffic forwarded')
 
     def partial_traffic_loss_accepted(self, loss_acceptance,
                                       loss_acceptance_type):
@@ -492,7 +636,7 @@ class TrafficGenerator(object):
         :type loss_acceptance: float
         :type loss_acceptance_type: LossAcceptanceType
         :returns: nothing
-        :raises: Exception if loss is above acceptance criteria.
+        :raises Exception: If loss is above acceptance criteria.
         """
         if self._loss is None:
             raise Exception('The traffic generation has not been issued')
@@ -507,3 +651,153 @@ class TrafficGenerator(object):
         if loss > float(loss_acceptance):
             raise Exception("Traffic loss {} above loss acceptance: {}".format(
                 loss, loss_acceptance))
+
+    def set_rate_provider_defaults(self, frame_size, traffic_type,
+                                   warmup_time=0.0):
+        """Store values accessed by measure().
+
+        :param frame_size: Frame size identifier or value [B].
+        :param traffic_type: Module name as a traffic type identifier.
+            See resources/traffic_profiles/trex for implemented modules.
+        :param warmup_time: Traffic duration before measurement starts [s].
+        :type frame_size: str or int
+        :type traffic_type: str
+        :type warmup_time: float
+        """
+        self.frame_size = frame_size
+        self.traffic_type = str(traffic_type)
+        self.warmup_time = float(warmup_time)
+
+    def measure(self, duration, transmit_rate):
+        """Run bi-directional measurement, parse and return results.
+
+        :param duration: Trial duration [s].
+        :param transmit_rate: Target bidirectional transmit rate [pps].
+        :type duration: float
+        :type transmit_rate: float
+        :returns: Structure containing the result of the measurement.
+        :rtype: ReceiveRateMeasurement
+        :raises RuntimeError: If TG is not set, or if node is not TG,
+            or if subtype is not specified.
+        :raises NotImplementedError: If TG is not supported.
+        """
+        duration = float(duration)
+        transmit_rate = float(transmit_rate)
+        # Trex needs target Tr per stream, but reports aggregate Tx and Dx.
+        unit_rate = str(transmit_rate / 2.0) + "pps"
+        self.send_traffic_on_tg(
+            duration, unit_rate, self.frame_size, self.traffic_type,
+            self.warmup_time, latency=True)
+        transmit_count = int(self.get_sent())
+        loss_count = int(self.get_loss())
+        measurement = ReceiveRateMeasurement(
+            duration, transmit_rate, transmit_count, loss_count)
+        measurement.latency = self.get_latency_int()
+        return measurement
+
+
+class OptimizedSearch(object):
+    """Class to be imported as Robot Library, containing a single keyword."""
+
+    @staticmethod
+    def perform_optimized_ndrpdr_search(
+            frame_size, traffic_type, minimum_transmit_rate,
+            maximum_transmit_rate, packet_loss_ratio=0.005,
+            final_relative_width=0.005, final_trial_duration=30.0,
+            initial_trial_duration=1.0, number_of_intermediate_phases=2,
+            timeout=720.0, doublings=1):
+        """Setup initialized TG, perform optimized search, return intervals.
+
+        :param frame_size: Frame size identifier or value [B].
+        :param traffic_type: Module name as a traffic type identifier.
+            See resources/traffic_profiles/trex for implemented modules.
+        :param minimum_transmit_rate: Minimal bidirectional
+            target transmit rate [pps].
+        :param maximum_transmit_rate: Maximal bidirectional
+            target transmit rate [pps].
+        :param packet_loss_ratio: Fraction of packets lost, for PDR [1].
+        :param final_relative_width: Final lower bound transmit rate
+            cannot be more distant that this multiple of upper bound [1].
+        :param final_trial_duration: Trial duration for the final phase [s].
+        :param initial_trial_duration: Trial duration for the initial phase
+            and also for the first intermediate phase [s].
+        :param number_of_intermediate_phases: Number of intermediate phases
+            to perform before the final phase [1].
+        :param timeout: The search will fail itself when not finished
+            before this overall time [s].
+        :param doublings: How many doublings to do in external search step.
+            Default 1 is suitable for fairly stable tests,
+            less stable tests might get better overal duration with 2 or more.
+        :type frame_size: str or int
+        :type traffic_type: str
+        :type minimum_transmit_rate: float
+        :type maximum_transmit_rate: float
+        :type packet_loss_ratio: float
+        :type final_relative_width: float
+        :type final_trial_duration: float
+        :type initial_trial_duration: float
+        :type number_of_intermediate_phases: int
+        :type timeout: float
+        :type doublings: int
+        :returns: Structure containing narrowed down NDR and PDR intervals
+            and their measurements.
+        :rtype: NdrPdrResult
+        :raises RuntimeError: If total duration is larger than timeout.
+        """
+        # we need instance of TrafficGenerator instantiated by Robot Framework
+        # to be able to use trex_stl-*()
+        tg_instance = BuiltIn().get_library_instance(
+            'resources.libraries.python.TrafficGenerator')
+        tg_instance.set_rate_provider_defaults(frame_size, traffic_type)
+        algorithm = MultipleLossRatioSearch(
+            measurer=tg_instance, final_trial_duration=final_trial_duration,
+            final_relative_width=final_relative_width,
+            number_of_intermediate_phases=number_of_intermediate_phases,
+            initial_trial_duration=initial_trial_duration, timeout=timeout,
+            doublings=doublings)
+        result = algorithm.narrow_down_ndr_and_pdr(
+            minimum_transmit_rate, maximum_transmit_rate, packet_loss_ratio)
+        return result
+
+    @staticmethod
+    def perform_soak_search(
+            frame_size, traffic_type, minimum_transmit_rate,
+            maximum_transmit_rate, plr_target=1e-7, tdpt=0.2,
+            initial_count=50, timeout=1800.0):
+        """Setup initialized TG, perform soak search, return avg and stdev.
+
+        :param frame_size: Frame size identifier or value [B].
+        :param traffic_type: Module name as a traffic type identifier.
+            See resources/traffic_profiles/trex for implemented modules.
+        :param minimum_transmit_rate: Minimal bidirectional
+            target transmit rate [pps].
+        :param maximum_transmit_rate: Maximal bidirectional
+            target transmit rate [pps].
+        :param plr_target: Fraction of packets lost to achieve [1].
+        :param tdpt: Trial duration per trial.
+            The algorithm linearly increases trial duration with trial number,
+            this is the increment between succesive trials, in seconds.
+        :param initial_count: Offset to apply before the first trial.
+            For example initial_count=50 makes first trial to be 51*tdpt long.
+            This is needed because initial "search" phase of integrator
+            takes significant time even without any trial results.
+        :param timeout: The search will stop after this overall time [s].
+        :type frame_size: str or int
+        :type traffic_type: str
+        :type minimum_transmit_rate: float
+        :type maximum_transmit_rate: float
+        :type plr_target: float
+        :type initial_count: int
+        :type timeout: float
+        :returns: Average and stdev of estimated bidirectional rate giving PLR.
+        :rtype: 2-tuple of float
+        """
+        tg_instance = BuiltIn().get_library_instance(
+            'resources.libraries.python.TrafficGenerator')
+        tg_instance.set_rate_provider_defaults(frame_size, traffic_type)
+        algorithm = PLRsearch(
+            measurer=tg_instance, trial_duration_per_trial=tdpt,
+            packet_loss_ratio_target=plr_target,
+            trial_number_offset=initial_count, timeout=timeout)
+        result = algorithm.search(minimum_transmit_rate, maximum_transmit_rate)
+        return result