Merge "Update PROX_NSB_DEVGUIDE for F Release"
[yardstick.git] / yardstick / network_services / traffic_profile / rfc2544.py
index c6facc9..c24e2f6 100644 (file)
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-""" RFC2544 Throughput implemenation """
 
-from __future__ import absolute_import
 import logging
 
-from yardstick.network_services.traffic_profile.traffic_profile \
-    import TrexProfile
+from trex_stl_lib import api as Pkt
+from trex_stl_lib import trex_stl_client
+from trex_stl_lib import trex_stl_packet_builder_scapy
+from trex_stl_lib import trex_stl_streams
+
+from yardstick.network_services.traffic_profile import trex_traffic_profile
+
 
 LOGGING = logging.getLogger(__name__)
+SRC_PORT = 'sport'
+DST_PORT = 'dport'
+
+
+class PortPgIDMap(object):
+    """Port and pg_id mapping class
+
+    "pg_id" is the identification STL library gives to each stream. In the
+    RFC2544Profile class, the traffic has a STLProfile per port, which contains
+    one or several streams, one per packet size defined in the IMIX test case
+    description.
+
+    Example of port <-> pg_id map:
+        self._port_pg_id_map = {
+            0: [1, 2, 3, 4],
+            1: [5, 6, 7, 8]
+        }
+    """
+
+    def __init__(self):
+        self._pg_id = 0
+        self._last_port = None
+        self._port_pg_id_map = {}
+
+    def add_port(self, port):
+        self._last_port = port
+        self._port_pg_id_map[port] = []
+
+    def get_pg_ids(self, port):
+        return self._port_pg_id_map.get(port)
+
+    def increase_pg_id(self, port=None):
+        port = self._last_port if not port else port
+        if port is None:
+            return
+        pg_id_list = self._port_pg_id_map.get(port)
+        if not pg_id_list:
+            self.add_port(port)
+            pg_id_list = self._port_pg_id_map[port]
+        self._pg_id += 1
+        pg_id_list.append(self._pg_id)
+        return self._pg_id
+
 
+class RFC2544Profile(trex_traffic_profile.TrexProfile):
+    """TRex RFC2544 traffic profile"""
 
-class RFC2544Profile(TrexProfile):
-    """ This class handles rfc2544 implemenation. """
+    TOLERANCE_LIMIT = 0.05
 
     def __init__(self, traffic_generator):
         super(RFC2544Profile, self).__init__(traffic_generator)
-        self.max_rate = None
-        self.min_rate = None
-        self.rate = 100
-        self.tmp_drop = None
-        self.tmp_throughput = None
-        self.profile_data = None
-
-    def execute(self, traffic_generator):
-        ''' Generate the stream and run traffic on the given ports '''
-        if self.first_run:
-            self.profile_data = self.params.get('private', '')
-            ports = [traffic_generator.my_ports[0]]
-            traffic_generator.client.add_streams(self.get_streams(),
-                                                 ports=ports[0])
-            profile_data = self.params.get('public', '')
-            if profile_data:
-                self.profile_data = profile_data
-                ports.append(traffic_generator.my_ports[1])
-                traffic_generator.client.add_streams(self.get_streams(),
-                                                     ports=ports[1])
+        self.generator = None
+        self.rate = self.config.frame_rate
+        self.max_rate = self.config.frame_rate
+        self.min_rate = 0
+        self.drop_percent_max = 0
 
-            self.max_rate = self.rate
-            self.min_rate = 0
-            traffic_generator.client.start(ports=ports,
-                                           mult=self.get_multiplier(),
-                                           duration=30, force=True)
-            self.tmp_drop = 0
-            self.tmp_throughput = 0
-
-    def get_multiplier(self):
-        ''' Get the rate at which next iternation to run '''
-        self.rate = round((self.max_rate + self.min_rate) / 2.0, 2)
-        multiplier = round(self.rate / self.pps, 2)
-        return str(multiplier)
-
-    def get_drop_percentage(self, traffic_generator,
-                            samples, tol_min, tolerance):
-        ''' Calculate the drop percentage and run the traffic '''
-        in_packets = sum([samples[iface]['in_packets'] for iface in samples])
-        out_packets = sum([samples[iface]['out_packets'] for iface in samples])
-        packet_drop = abs(out_packets - in_packets)
+    def register_generator(self, generator):
+        self.generator = generator
+
+    def stop_traffic(self, traffic_generator=None):
+        """"Stop traffic injection, reset counters and remove streams"""
+        if traffic_generator is not None and self.generator is None:
+            self.generator = traffic_generator
+
+        self.generator.client.stop()
+        self.generator.client.reset()
+        self.generator.client.remove_all_streams()
+
+    def execute_traffic(self, traffic_generator=None):
+        """Generate the stream and run traffic on the given ports
+
+        :param traffic_generator: (TrexTrafficGenRFC) traffic generator
+        :return ports: (list of int) indexes of ports
+                port_pg_id: (dict) port indexes and pg_id [1] map
+        [1] https://trex-tgn.cisco.com/trex/doc/cp_stl_docs/api/
+            profile_code.html#stlstream-modes
+        """
+        if traffic_generator is not None and self.generator is None:
+            self.generator = traffic_generator
+
+        port_pg_id = PortPgIDMap()
+        ports = []
+        for vld_id, intfs in sorted(self.generator.networks.items()):
+            profile_data = self.params.get(vld_id)
+            if not profile_data:
+                continue
+            if (vld_id.startswith(self.DOWNLINK) and
+                    self.generator.rfc2544_helper.correlated_traffic):
+                continue
+            for intf in intfs:
+                port_num = int(self.generator.port_num(intf))
+                ports.append(port_num)
+                port_pg_id.add_port(port_num)
+                profile = self._create_profile(profile_data,
+                                               self.rate, port_pg_id)
+                self.generator.client.add_streams(profile, ports=[port_num])
+
+        self.generator.client.start(ports=ports,
+                                    duration=self.config.duration,
+                                    force=True)
+        return ports, port_pg_id
+
+    def _create_profile(self, profile_data, rate, port_pg_id):
+        """Create a STL profile (list of streams) for a port"""
+        streams = []
+        for packet_name in profile_data:
+            imix = (profile_data[packet_name].
+                    get('outer_l2', {}).get('framesize'))
+            imix_data = self._create_imix_data(imix)
+            self._create_vm(profile_data[packet_name])
+            _streams = self._create_streams(imix_data, rate, port_pg_id)
+            streams.extend(_streams)
+        return trex_stl_streams.STLProfile(streams)
+
+    def _create_imix_data(self, imix):
+        """Generate the IMIX distribution for a STL profile
+
+        The input information is the framesize dictionary in a test case
+        traffic profile definition. E.g.:
+          downlink_0:
+            ipv4:
+              id: 2
+                outer_l2:
+                  framesize:
+                    64B: 10
+                    128B: 20
+                    ...
+
+        This function normalizes the sum of framesize weights to 100 and
+        returns a dictionary of frame sizes in bytes and weight in percentage.
+        E.g.:
+          imix_count = {64: 25, 128: 75}
+
+        :param imix: (dict) IMIX size and weight
+        """
+        imix_count = {}
+        if not imix:
+            return imix_count
+
+        imix_count = {size.upper().replace('B', ''): int(weight)
+                      for size, weight in imix.items()}
+        imix_sum = sum(imix_count.values())
+        if imix_sum <= 0:
+            imix_count = {64: 100}
+            imix_sum = 100
+
+        weight_normalize = float(imix_sum) / 100
+        return {size: float(weight) / weight_normalize
+                for size, weight in imix_count.items()}
+
+    def _create_vm(self, packet_definition):
+        """Create the STL Raw instructions"""
+        self.ether_packet = Pkt.Ether()
+        self.ip_packet = Pkt.IP()
+        self.ip6_packet = None
+        self.udp_packet = Pkt.UDP()
+        self.udp[DST_PORT] = 'UDP.dport'
+        self.udp[SRC_PORT] = 'UDP.sport'
+        self.qinq = False
+        self.vm_flow_vars = []
+        outer_l2 = packet_definition.get('outer_l2')
+        outer_l3v4 = packet_definition.get('outer_l3v4')
+        outer_l3v6 = packet_definition.get('outer_l3v6')
+        outer_l4 = packet_definition.get('outer_l4')
+        if outer_l2:
+            self._set_outer_l2_fields(outer_l2)
+        if outer_l3v4:
+            self._set_outer_l3v4_fields(outer_l3v4)
+        if outer_l3v6:
+            self._set_outer_l3v6_fields(outer_l3v6)
+        if outer_l4:
+            self._set_outer_l4_fields(outer_l4)
+        self.trex_vm = trex_stl_packet_builder_scapy.STLScVmRaw(
+            self.vm_flow_vars)
+
+    def _create_single_packet(self, size=64):
+        size -= 4
+        ether_packet = self.ether_packet
+        ip_packet = self.ip6_packet if self.ip6_packet else self.ip_packet
+        udp_packet = self.udp_packet
+        if self.qinq:
+            qinq_packet = self.qinq_packet
+            base_pkt = ether_packet / qinq_packet / ip_packet / udp_packet
+        else:
+            base_pkt = ether_packet / ip_packet / udp_packet
+        pad = max(0, size - len(base_pkt)) * 'x'
+        return trex_stl_packet_builder_scapy.STLPktBuilder(
+            pkt=base_pkt / pad, vm=self.trex_vm)
+
+    def _create_streams(self, imix_data, rate, port_pg_id):
+        """Create a list of streams per packet size
+
+        The STL TX mode speed of the generated streams will depend on the frame
+        weight and the frame rate. Both the frame weight and the total frame
+        rate are normalized to 100. The STL TX mode speed, defined in
+        percentage, is the combitation of both percentages. E.g.:
+          frame weight = 100
+          rate = 90
+            --> STLTXmode percentage = 10 (%)
+
+          frame weight = 80
+          rate = 50
+            --> STLTXmode percentage = 40 (%)
+
+        :param imix_data: (dict) IMIX size and weight
+        :param rate: (float) normalized [0..100] total weight
+        :param pg_id: (PortPgIDMap) port / pg_id (list) map
+        """
+        streams = []
+        for size, weight in ((int(size), float(weight)) for (size, weight)
+                             in imix_data.items() if float(weight) > 0):
+            packet = self._create_single_packet(size)
+            pg_id = port_pg_id.increase_pg_id()
+            stl_flow = trex_stl_streams.STLFlowLatencyStats(pg_id=pg_id)
+            mode = trex_stl_streams.STLTXCont(percentage=weight * rate / 100)
+            streams.append(trex_stl_client.STLStream(
+                packet=packet, flow_stats=stl_flow, mode=mode))
+        return streams
+
+    def get_drop_percentage(self, samples, tol_low, tol_high,
+                            correlated_traffic):
+        """Calculate the drop percentage and run the traffic"""
+        tx_rate_fps = 0
+        rx_rate_fps = 0
+        for sample in samples:
+            tx_rate_fps += sum(
+                port['tx_throughput_fps'] for port in sample.values())
+            rx_rate_fps += sum(
+                port['rx_throughput_fps'] for port in sample.values())
+        tx_rate_fps = round(float(tx_rate_fps) / len(samples), 2)
+        rx_rate_fps = round(float(rx_rate_fps) / len(samples), 2)
+
+        # TODO(esm): RFC2544 doesn't tolerate packet loss, why do we?
+        out_packets = sum(port['out_packets'] for port in samples[-1].values())
+        in_packets = sum(port['in_packets'] for port in samples[-1].values())
         drop_percent = 100.0
-        try:
-            drop_percent = round((packet_drop / float(out_packets)) * 100, 2)
-        except ZeroDivisionError:
-            LOGGING.info('No traffic is flowing')
-        samples['TxThroughput'] = out_packets / 30
-        samples['RxThroughput'] = in_packets / 30
-        samples['CurrentDropPercentage'] = drop_percent
-        samples['Throughput'] = self.tmp_throughput
-        samples['DropPercentage'] = self.tmp_drop
-        if drop_percent > tolerance and self.tmp_throughput == 0:
-            samples['Throughput'] = (in_packets / 30)
-            samples['DropPercentage'] = drop_percent
-        if self.first_run:
-            max_supported_rate = out_packets / 30
-            self.rate = max_supported_rate
-            self.first_run = False
-        if drop_percent > tolerance:
+
+        # https://tools.ietf.org/html/rfc2544#section-26.3
+        if out_packets:
+            drop_percent = round(
+                (float(abs(out_packets - in_packets)) / out_packets) * 100, 5)
+
+        tol_high = tol_high if tol_high > self.TOLERANCE_LIMIT else tol_high
+        tol_low = tol_low if tol_low > self.TOLERANCE_LIMIT else tol_low
+        if drop_percent > tol_high:
             self.max_rate = self.rate
-        elif drop_percent < tol_min:
+        elif drop_percent < tol_low:
             self.min_rate = self.rate
-            if drop_percent >= self.tmp_drop:
-                self.tmp_drop = drop_percent
-                self.tmp_throughput = (in_packets / 30)
-                samples['Throughput'] = (in_packets / 30)
-                samples['DropPercentage'] = drop_percent
-        else:
-            samples['Throughput'] = (in_packets / 30)
-            samples['DropPercentage'] = drop_percent
-
-        traffic_generator.client.clear_stats(ports=traffic_generator.my_ports)
-        traffic_generator.client.start(ports=traffic_generator.my_ports,
-                                       mult=self.get_multiplier(),
-                                       duration=30, force=True)
-        return samples
+        # else:
+            # NOTE(ralonsoh): the test should finish here
+            # pass
+        last_rate = self.rate
+        self.rate = round(float(self.max_rate + self.min_rate) / 2.0, 5)
+
+        throughput = rx_rate_fps * 2 if correlated_traffic else rx_rate_fps
+
+        if drop_percent > self.drop_percent_max:
+            self.drop_percent_max = drop_percent
+
+        latency = {port_num: value['latency']
+                   for port_num, value in samples[-1].items()}
+
+        output = {
+            'TxThroughput': tx_rate_fps,
+            'RxThroughput': rx_rate_fps,
+            'CurrentDropPercentage': drop_percent,
+            'Throughput': throughput,
+            'DropPercentage': self.drop_percent_max,
+            'Rate': last_rate,
+            'Latency': latency
+        }
+        return output