Add: Use containers for shared TG
[csit.git] / resources / libraries / python / TrafficGenerator.py
index 2e59459..8c3abfb 100644 (file)
@@ -106,6 +106,7 @@ class TGDropRateSearchImpl(DropRateSearch):
             logger.trace("comparing: {los} < {acc} {typ}".format(
                 los=loss, acc=loss_acceptance, typ=loss_acceptance_type))
             return float(loss) <= float(loss_acceptance)
+        return False
 
     def get_latency(self):
         """Returns min/avg/max latency.
@@ -147,6 +148,10 @@ class TrafficGenerator(AbstractMeasurer):
         self.frame_size = None
         self.traffic_profile = None
         self.warmup_time = None
+        self.traffic_directions = None
+        # Transient data needed for async measurements.
+        self._xstats = (None, None)
+        # TODO: Rename "xstats" to something opaque, so TRex is not privileged?
 
     @property
     def node(self):
@@ -323,18 +328,12 @@ class TrafficGenerator(AbstractMeasurer):
                 self._node, cmd, sudo=True,
                 message='Unbind PCI ports from driver failed!')
 
-            cmd = ("sh -c 'cd {dir}/scripts/ && ./trex-cfg "
-                   "--unbind-unused-ports'"
-                   .format(dir=Constants.TREX_INSTALL_DIR))
-            exec_cmd_no_error(
-                self._node, cmd, sudo=True, message='Config TRex failed!')
-
             # Start TRex.
             cmd = ("sh -c 'cd {dir}/scripts/ && "
-                   "nohup ./t-rex-64 {mode} -i -c 7 > "
-                   "/tmp/trex.log 2>&1 &' > /dev/null"
+                   "nohup ./t-rex-64 --hdrh{mode} --prefix $(hostname)"
+                   " -i -c 7 > /tmp/trex.log 2>&1 &' > /dev/null"
                    .format(dir=Constants.TREX_INSTALL_DIR,
-                           mode='--astf' if osi_layer == 'L7' else ''))
+                           mode=' --astf' if osi_layer == 'L7' else ''))
             try:
                 exec_cmd_no_error(self._node, cmd, sudo=True)
             except RuntimeError:
@@ -389,7 +388,7 @@ class TrafficGenerator(AbstractMeasurer):
                 sudo=False, message='pkill t-rex failed')
 
     def _parse_traffic_results(self, stdout):
-        """Parse stdout of scripts into fieds of self.
+        """Parse stdout of scripts into fields of self.
 
         Block of code to reuse, by sync start, or stop after async.
         TODO: Is the output TG subtype dependent?
@@ -401,37 +400,45 @@ class TrafficGenerator(AbstractMeasurer):
         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._received = self._result.split(', ')[1].split('=', 1)[1]
+        self._sent = self._result.split(', ')[2].split('=', 1)[1]
+        self._loss = self._result.split(', ')[3].split('=', 1)[1]
         self._latency = []
-        self._latency.append(self._result.split(', ')[4].split('=')[1])
-        self._latency.append(self._result.split(', ')[5].split('=')[1])
+        self._latency.append(self._result.split(', ')[4].split('=', 1)[1])
+        self._latency.append(self._result.split(', ')[5].split('=', 1)[1])
 
     def trex_stl_stop_remote_exec(self, node):
         """Execute script on remote node over ssh to stop running traffic.
 
-        Internal state is updated with results.
+        Internal state is updated with measurement results.
 
         :param node: TRex generator node.
         :type node: dict
-        :returns: Nothing
         :raises RuntimeError: If stop traffic script fails.
         """
         # No need to check subtype, we know it is TREX.
+        x_args = ""
+        for index, value in enumerate(self._xstats):
+            if value is not None:
+                # Nested quoting is fun.
+                value = value.replace("'", "\"")
+                x_args += " --xstat{i}='\"'\"'{v}'\"'\"'".format(
+                    i=index, v=value)
         stdout, _ = exec_cmd_no_error(
-            node,
-            "sh -c '{}/resources/tools/trex/"
-            "trex_stateless_stop.py'".format(Constants.REMOTE_FW_DIR),
+            node, "sh -c '{d}/resources/tools/trex/trex_stateless_stop.py{a}'"\
+            .format(d=Constants.REMOTE_FW_DIR, a=x_args),
             message='TRex stateless runtime error')
         self._parse_traffic_results(stdout)
 
     def trex_stl_start_remote_exec(
             self, duration, rate, frame_size, traffic_profile, async_call=False,
-            latency=True, warmup_time=5.0, unidirection=False, tx_port=0,
+            latency=True, warmup_time=5.0, traffic_directions=2, tx_port=0,
             rx_port=1):
         """Execute script on remote node over ssh to start traffic.
 
+        In sync mode, measurement results are stored internally.
+        In async mode, initial data including xstats are stored internally.
+
         :param duration: Time expresed in seconds for how long to send traffic.
         :param rate: Traffic rate expressed with units (pps, %)
         :param frame_size: L2 frame size to send (without padding and IPG).
@@ -440,7 +447,8 @@ class TrafficGenerator(AbstractMeasurer):
         :param async_call: If enabled then don't wait for all incomming trafic.
         :param latency: With latency measurement.
         :param warmup_time: Warmup time period.
-        :param unidirection: Traffic is unidirectional. Default: False
+        :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
+            Default: 2
         :param tx_port: Traffic generator transmit port for first flow.
             Default: 0
         :param rx_port: Traffic generator receive port for first flow.
@@ -452,7 +460,7 @@ class TrafficGenerator(AbstractMeasurer):
         :type async_call: bool
         :type latency: bool
         :type warmup_time: float
-        :type unidirection: bool
+        :type traffic_directions: int
         :type tx_port: int
         :type rx_port: int
         :raises RuntimeError: In case of TG driver issue.
@@ -472,31 +480,40 @@ class TrafficGenerator(AbstractMeasurer):
             "sh -c '{tool}/resources/tools/trex/trex_stateless_profile.py"
             " --profile {prof}/resources/traffic_profiles/trex/{traffic}.py"
             " --duration {duration!r} --frame_size {frame_size} --rate {rate!r}"
-            " --warmup_time {warmup!r} --port_0 {p_0} --port_1 {p_1}").format(
+            " --warmup_time {warmup!r} --port_0 {p_0} --port_1 {p_1}"
+            " --traffic_directions {dirs}").format(
                 tool=Constants.REMOTE_FW_DIR, prof=Constants.REMOTE_FW_DIR,
                 traffic=traffic_profile, duration=duration,
                 frame_size=frame_size, rate=rate, warmup=warmup_time, p_0=p_0,
-                p_1=p_1)
+                p_1=p_1, dirs=traffic_directions)
         if async_call:
-            command += " --async"
+            command += " --async_start"
         if latency:
             command += " --latency"
-        if unidirection:
-            command += " --unidirection"
         command += "'"
 
         stdout, _ = exec_cmd_no_error(
             self._node, command, timeout=float(duration) + 60,
             message='TRex stateless runtime error')
 
+        self.traffic_directions = traffic_directions
         if async_call:
             #no result
             self._start_time = time.time()
-            self._rate = float(rate[:-3]) if "pps" in rate else rate
+            self._rate = float(rate[:-3]) if "pps" in rate else float(rate)
             self._received = None
             self._sent = None
             self._loss = None
             self._latency = None
+            xstats = [None, None]
+            index = 0
+            for line in stdout.splitlines():
+                if "Xstats snapshot {i}: ".format(i=index) in line:
+                    xstats[index] = line[19:]
+                    index += 1
+                if index == 2:
+                    break
+            self._xstats = tuple(xstats)
         else:
             self._parse_traffic_results(stdout)
             self._start_time = None
@@ -505,19 +522,26 @@ class TrafficGenerator(AbstractMeasurer):
     def stop_traffic_on_tg(self):
         """Stop all traffic on TG.
 
-        :returns: Nothing
+        :returns: Structure containing the result of the measurement.
+        :rtype: ReceiveRateMeasurement
         :raises RuntimeError: If TG is not set.
         """
         subtype = check_subtype(self._node)
         if subtype == NodeSubTypeTG.TREX:
             self.trex_stl_stop_remote_exec(self._node)
+        return self.get_measurement_result()
 
     def send_traffic_on_tg(
             self, duration, rate, frame_size, traffic_profile, warmup_time=5,
-            async_call=False, latency=True, unidirection=False, tx_port=0,
+            async_call=False, latency=True, traffic_directions=2, tx_port=0,
             rx_port=1):
         """Send traffic from all configured interfaces on TG.
 
+        In async mode, xstats is stored internally,
+        to enable getting correct result when stopping the traffic.
+        In both modes, stdout is returned,
+        but _parse_traffic_results only works in sync output.
+
         Note that bidirectional traffic also contains flows
         transmitted from rx_port and received in tx_port.
         But some tests use asymmetric traffic, so those arguments are relevant.
@@ -540,7 +564,8 @@ class TrafficGenerator(AbstractMeasurer):
         :param warmup_time: Warmup phase in seconds.
         :param async_call: Async mode.
         :param latency: With latency measurement.
-        :param unidirection: Traffic is unidirectional. Default: False
+        :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
+            Default: 2
         :param tx_port: Traffic generator transmit port for first flow.
             Default: 0
         :param rx_port: Traffic generator receive port for first flow.
@@ -552,7 +577,7 @@ class TrafficGenerator(AbstractMeasurer):
         :type warmup_time: float
         :type async_call: bool
         :type latency: bool
-        :type unidirection: bool
+        :type traffic_directions: int
         :type tx_port: int
         :type rx_port: int
         :returns: TG output.
@@ -565,7 +590,7 @@ class TrafficGenerator(AbstractMeasurer):
         if subtype == NodeSubTypeTG.TREX:
             self.trex_stl_start_remote_exec(
                 duration, rate, frame_size, traffic_profile, async_call,
-                latency, warmup_time, unidirection, tx_port, rx_port)
+                latency, warmup_time, traffic_directions, tx_port, rx_port)
 
         return self._result
 
@@ -617,20 +642,24 @@ class TrafficGenerator(AbstractMeasurer):
                 loss, loss_acceptance))
 
     def set_rate_provider_defaults(self, frame_size, traffic_profile,
-                                   warmup_time=0.0):
+                                   warmup_time=0.0, traffic_directions=2):
         """Store values accessed by measure().
 
         :param frame_size: Frame size identifier or value [B].
         :param traffic_profile: Module name as a traffic profile identifier.
             See resources/traffic_profiles/trex for implemented modules.
         :param warmup_time: Traffic duration before measurement starts [s].
+        :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
+            Default: 2
         :type frame_size: str or int
         :type traffic_profile: str
         :type warmup_time: float
+        :type traffic_directions: int
         """
         self.frame_size = frame_size
         self.traffic_profile = str(traffic_profile)
         self.warmup_time = float(warmup_time)
+        self.traffic_directions = traffic_directions
 
     def get_measurement_result(self, duration=None, transmit_rate=None):
         """Return the result of last measurement as ReceiveRateMeasurement.
@@ -653,8 +682,7 @@ class TrafficGenerator(AbstractMeasurer):
             duration = time.time() - self._start_time
             self._start_time = None
         if transmit_rate is None:
-            # Assuming bi-directional traffic here.
-            transmit_rate = self._rate * 2.0
+            transmit_rate = self._rate * self.traffic_directions
         transmit_count = int(self.get_sent())
         loss_count = int(self.get_loss())
         measurement = ReceiveRateMeasurement(
@@ -663,10 +691,12 @@ class TrafficGenerator(AbstractMeasurer):
         return measurement
 
     def measure(self, duration, transmit_rate):
-        """Run bi-directional measurement, parse and return results.
+        """Run trial measurement, parse and return aggregate results.
+
+        Aggregate means sum over traffic directions.
 
         :param duration: Trial duration [s].
-        :param transmit_rate: Target bidirectional transmit rate [pps].
+        :param transmit_rate: Target aggregate transmit rate [pps].
         :type duration: float
         :type transmit_rate: float
         :returns: Structure containing the result of the measurement.
@@ -678,15 +708,21 @@ class TrafficGenerator(AbstractMeasurer):
         duration = float(duration)
         transmit_rate = float(transmit_rate)
         # TG needs target Tr per stream, but reports aggregate Tx and Dx.
-        unit_rate = str(transmit_rate / 2.0) + "pps"
+        unit_rate_int = transmit_rate / float(self.traffic_directions)
+        unit_rate_str = str(unit_rate_int) + "pps"
         self.send_traffic_on_tg(
-            duration, unit_rate, self.frame_size, self.traffic_profile,
-            warmup_time=self.warmup_time, latency=True)
+            duration, unit_rate_str, self.frame_size, self.traffic_profile,
+            warmup_time=self.warmup_time, latency=True,
+            traffic_directions=self.traffic_directions)
         return self.get_measurement_result(duration, transmit_rate)
 
 
 class OptimizedSearch(object):
-    """Class to be imported as Robot Library, containing a single keyword."""
+    """Class to be imported as Robot Library, containing search keywords.
+
+    Aside of setting up measurer and forwarding arguments,
+    the main business is to translate min/max rate from unidir to aggregate.
+    """
 
     @staticmethod
     def perform_optimized_ndrpdr_search(
@@ -694,15 +730,15 @@ class OptimizedSearch(object):
             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):
+            timeout=720.0, doublings=1, traffic_directions=2):
         """Setup initialized TG, perform optimized search, return intervals.
 
         :param frame_size: Frame size identifier or value [B].
         :param traffic_profile: Module name as a traffic profile identifier.
             See resources/traffic_profiles/trex for implemented modules.
-        :param minimum_transmit_rate: Minimal bidirectional
+        :param minimum_transmit_rate: Minimal uni-directional
             target transmit rate [pps].
-        :param maximum_transmit_rate: Maximal bidirectional
+        :param maximum_transmit_rate: Maximal uni-directional
             target transmit rate [pps].
         :param packet_loss_ratio: Fraction of packets lost, for PDR [1].
         :param final_relative_width: Final lower bound transmit rate
@@ -717,6 +753,8 @@ class OptimizedSearch(object):
         :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.
+        :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
+            Default: 2
         :type frame_size: str or int
         :type traffic_profile: str
         :type minimum_transmit_rate: float
@@ -728,16 +766,20 @@ class OptimizedSearch(object):
         :type number_of_intermediate_phases: int
         :type timeout: float
         :type doublings: int
+        :type traffic_directions: int
         :returns: Structure containing narrowed down NDR and PDR intervals
             and their measurements.
         :rtype: NdrPdrResult
         :raises RuntimeError: If total duration is larger than timeout.
         """
+        minimum_transmit_rate *= traffic_directions
+        maximum_transmit_rate *= traffic_directions
         # 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_profile)
+        tg_instance.set_rate_provider_defaults(
+            frame_size, traffic_profile, traffic_directions=traffic_directions)
         algorithm = MultipleLossRatioSearch(
             measurer=tg_instance, final_trial_duration=final_trial_duration,
             final_relative_width=final_relative_width,
@@ -752,15 +794,16 @@ class OptimizedSearch(object):
     def perform_soak_search(
             frame_size, traffic_profile, minimum_transmit_rate,
             maximum_transmit_rate, plr_target=1e-7, tdpt=0.1,
-            initial_count=50, timeout=1800.0, trace_enabled=False):
+            initial_count=50, timeout=1800.0, trace_enabled=False,
+            traffic_directions=2):
         """Setup initialized TG, perform soak search, return avg and stdev.
 
         :param frame_size: Frame size identifier or value [B].
         :param traffic_profile: Module name as a traffic profile identifier.
             See resources/traffic_profiles/trex for implemented modules.
-        :param minimum_transmit_rate: Minimal bidirectional
+        :param minimum_transmit_rate: Minimal uni-directional
             target transmit rate [pps].
-        :param maximum_transmit_rate: Maximal bidirectional
+        :param maximum_transmit_rate: Maximal uni-directional
             target transmit rate [pps].
         :param plr_target: Fraction of packets lost to achieve [1].
         :param tdpt: Trial duration per trial.
@@ -771,6 +814,8 @@ class OptimizedSearch(object):
             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].
+        :param traffic_directions: Traffic is bi- (2) or uni- (1) directional.
+            Default: 2
         :type frame_size: str or int
         :type traffic_profile: str
         :type minimum_transmit_rate: float
@@ -778,12 +823,16 @@ class OptimizedSearch(object):
         :type plr_target: float
         :type initial_count: int
         :type timeout: float
-        :returns: Average and stdev of estimated bidirectional rate giving PLR.
+        :type traffic_directions: int
+        :returns: Average and stdev of estimated aggregate rate giving PLR.
         :rtype: 2-tuple of float
         """
+        minimum_transmit_rate *= traffic_directions
+        maximum_transmit_rate *= traffic_directions
         tg_instance = BuiltIn().get_library_instance(
             'resources.libraries.python.TrafficGenerator')
-        tg_instance.set_rate_provider_defaults(frame_size, traffic_profile)
+        tg_instance.set_rate_provider_defaults(
+            frame_size, traffic_profile, traffic_directions=traffic_directions)
         algorithm = PLRsearch(
             measurer=tg_instance, trial_duration_per_trial=tdpt,
             packet_loss_ratio_target=plr_target,