refactor(trex): Startup configuration 42/38842/12
authorpmikus <peter.mikus@protonmail.ch>
Thu, 18 May 2023 05:59:39 +0000 (05:59 +0000)
committerPeter Mikus <peter.mikus@protonmail.ch>
Wed, 24 May 2023 05:08:08 +0000 (05:08 +0000)
Signed-off-by: pmikus <peter.mikus@protonmail.ch>
Change-Id: I16defefa5edd01638bc382be4f5e8cbca4fe9453

resources/libraries/python/CpuUtils.py
resources/libraries/python/TRexConfigGenerator.py [new file with mode: 0644]
resources/libraries/python/TrafficGenerator.py

index 5805ba7..1e306f0 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (c) 2021 Cisco and/or its affiliates.
+# Copyright (c) 2023 Cisco and/or its affiliates.
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at:
@@ -388,25 +388,23 @@ class CpuUtils:
 
     @staticmethod
     def get_affinity_trex(
-            node, if1_pci, if2_pci, tg_mtc=1, tg_dtc=1, tg_ltc=1):
+            node, if_key, tg_mtc=1, tg_dtc=1, tg_ltc=1):
         """Get affinity for T-Rex. Result will be used to pin T-Rex threads.
 
         :param node: TG node.
-        :param if1_pci: TG first interface.
-        :param if2_pci: TG second interface.
+        :param if_key: TG first interface.
         :param tg_mtc: TG main thread count.
         :param tg_dtc: TG dataplane thread count.
         :param tg_ltc: TG latency thread count.
         :type node: dict
-        :type if1_pci: str
-        :type if2_pci: str
+        :type if_key: str
         :type tg_mtc: int
         :type tg_dtc: int
         :type tg_ltc: int
         :returns: List of CPUs allocated to T-Rex including numa node.
         :rtype: int, int, int, list
         """
-        interface_list = [if1_pci, if2_pci]
+        interface_list = [if_key]
         cpu_node = Topology.get_interfaces_numa_node(node, *interface_list)
 
         master_thread_id = CpuUtils.cpu_slice_of_list_per_node(
diff --git a/resources/libraries/python/TRexConfigGenerator.py b/resources/libraries/python/TRexConfigGenerator.py
new file mode 100644 (file)
index 0000000..2015b09
--- /dev/null
@@ -0,0 +1,287 @@
+# Copyright (c) 2023 Cisco and/or its affiliates.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# 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.
+
+"""TRex Configuration File Generator library."""
+
+import re
+import yaml
+
+from resources.libraries.python.Constants import Constants
+from resources.libraries.python.CpuUtils import CpuUtils
+from resources.libraries.python.ssh import exec_cmd_no_error
+from resources.libraries.python.topology import NodeType, NodeSubTypeTG
+from resources.libraries.python.topology import Topology
+
+
+__all__ = ["TrexConfigGenerator", "TrexInitConfig"]
+
+def pci_dev_check(pci_dev):
+    """Check if provided PCI address is in correct format.
+
+    :param pci_dev: PCI address (expected format: xxxx:xx:xx.x).
+    :type pci_dev: str
+    :returns: True if PCI address is in correct format.
+    :rtype: bool
+    :raises ValueError: If PCI address is in incorrect format.
+    """
+    pattern = re.compile(
+        r"^[0-9A-Fa-f]{4}:[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}\.[0-9A-Fa-f]$"
+    )
+    if not re.match(pattern, pci_dev):
+        raise ValueError(
+            f"PCI address {pci_dev} is not in valid format xxxx:xx:xx.x"
+        )
+    return True
+
+
+class TrexConfigGenerator:
+    """TRex Startup Configuration File Generator."""
+
+    def __init__(self):
+        """Initialize library.
+        """
+        self._node = ""
+        self._node_key = ""
+        self._node_config = dict()
+        self._node_serialized_config = ""
+        self._startup_configuration_path = "/etc/trex_cfg.yaml"
+
+    def set_node(self, node, node_key=None):
+        """Set topology node.
+
+        :param node: Node to store configuration on.
+        :param node_key: Topology node key.
+        :type node: dict
+        :type node_key: str
+        :raises RuntimeError: If Node type is not TG and subtype is not TREX.
+        """
+        if node.get("type") is None:
+            msg = "Node type is not defined!"
+        elif node["type"] != NodeType.TG:
+            msg = f"Node type is {node['type']!r}, not a TG!"
+        elif node.get("subtype") is None:
+            msg = "TG subtype is not defined"
+        elif node["subtype"] != NodeSubTypeTG.TREX:
+            msg = f"TG subtype {node['subtype']!r} is not supported"
+        else:
+            self._node = node
+            self._node_key = node_key
+            return
+        raise RuntimeError(msg)
+
+    def get_serialized_config(self):
+        """Get serialized startup configuration in YAML format.
+
+        :returns: Startup configuration in YAML format.
+        :rtype: str
+        """
+        self.serialize_config(self._node_config)
+        return self._node_serialized_config
+
+    def serialize_config(self, obj):
+        """Serialize the startup configuration in YAML format.
+
+        :param obj: Python Object to print.
+        :type obj: Obj
+        """
+        self._node_serialized_config = yaml.dump([obj], default_style=None)
+
+    def add_config_item(self, config, value, path):
+        """Add startup configuration item.
+
+        :param config: Startup configuration of node.
+        :param value: Value to insert.
+        :param path: Path where to insert item.
+        :type config: dict
+        :type value: str
+        :type path: list
+        """
+        if len(path) == 1:
+            config[path[0]] = value
+            return
+        if path[0] not in config:
+            config[path[0]] = dict()
+        elif isinstance(config[path[0]], str):
+            config[path[0]] = dict() if config[path[0]] == "" \
+                else {config[path[0]]: ""}
+        self.add_config_item(config[path[0]], value, path[1:])
+
+    def add_version(self, value=2):
+        """Add config file version.
+
+        :param value: Version of configuration file.
+        :type value: int
+        """
+        path = ["version"]
+        self.add_config_item(self._node_config, value, path)
+
+    def add_c(self, value):
+        """Add core count.
+
+        :param value: Core count.
+        :type value: int
+        """
+        path = ["c"]
+        self.add_config_item(self._node_config, value, path)
+
+    def add_limit_memory(self, value):
+        """Add memory limit.
+
+        :param value: Memory limit.
+        :type value: str
+        """
+        path = ["limit_memory"]
+        self.add_config_item(self._node_config, value, path)
+
+    def add_interfaces(self, devices):
+        """Add PCI device configuration.
+
+        :param devices: PCI device(s) (format xxxx:xx:xx.x).
+        :type devices: list(str)
+        """
+        for device in devices:
+            pci_dev_check(device)
+
+        path = ["interfaces"]
+        self.add_config_item(self._node_config, devices, path)
+
+    def add_rx_desc(self, value):
+        """Add RX descriptors.
+
+        :param value: RX descriptors count.
+        :type value: int
+        """
+        path = ["rx_desc"]
+        self.add_config_item(self._node_config, value, path)
+
+    def add_tx_desc(self, value):
+        """Add TX descriptors.
+
+        :param value: TX descriptors count.
+        :type value: int
+        """
+        path = ["tx_desc"]
+        self.add_config_item(self._node_config, value, path)
+
+    def add_port_info(self, value):
+        """Add port information configuration.
+
+        :param value: Port information configuration.
+        :type value: list(dict)
+        """
+        path = ["port_info"]
+        self.add_config_item(self._node_config, value, path)
+
+    def add_platform_master_thread_id(self, value):
+        """Add platform master thread ID.
+
+        :param value: Master thread ID.
+        :type value: int
+        """
+        path = ["platform", "master_thread_id"]
+        self.add_config_item(self._node_config, value, path)
+
+    def add_platform_latency_thread_id(self, value):
+        """Add platform latency thread ID.
+
+        :param value: Latency thread ID.
+        :type value: int
+        """
+        path = ["platform", "latency_thread_id"]
+        self.add_config_item(self._node_config, value, path)
+
+    def add_platform_dual_if(self, value):
+        """Add platform dual interface configuration.
+
+        :param value: Dual interface configuration.
+        :type value: list(dict)
+        """
+        path = ["platform", "dual_if"]
+        self.add_config_item(self._node_config, value, path)
+
+    def write_config(self, path=None):
+        """Generate and write TRex startup configuration to file.
+
+        :param path: Override startup configuration path.
+        :type path: str
+        """
+        self.serialize_config(self._node_config)
+
+        if path is None:
+            path = self._startup_configuration_path
+
+        command = f"echo \"{self._node_serialized_config}\" | sudo tee {path}"
+        message = "Writing TRex startup configuration failed!"
+        exec_cmd_no_error(self._node, command, message=message)
+
+
+class TrexInitConfig:
+    """TRex Initial Configuration.
+    """
+    @staticmethod
+    def init_trex_startup_configuration(node, tg_topology):
+        """Apply initial TRex startup configuration.
+
+        :param node: TRex node in the topology.
+        :param tg_topology: Ordered TRex links.
+        :type node: dict
+        :type tg_topology: list(dict)
+        """
+        pci_addresses = list()
+        dual_if = list()
+        port_info = list()
+        master_thread_id = None
+        latency_thread_id = None
+        cores = None
+        limit_memory = f"{Constants.TREX_LIMIT_MEMORY}"
+        sockets = 0
+
+        for link in tg_topology:
+            pci_addresses.append(
+                Topology().get_interface_pci_addr(node, link["interface"])
+            )
+            master_thread_id, latency_thread_id, socket, threads = \
+                CpuUtils.get_affinity_trex(
+                    node, link["interface"], tg_dtc=Constants.TREX_CORE_COUNT
+                )
+            dual_if.append(dict(socket=socket, threads=threads))
+            cores = len(threads)
+            port_info.append(
+                dict(
+                    src_mac=Topology().get_interface_mac(
+                        node, link["interface"]
+                    ),
+                    dst_mac=link["dst_mac"]
+                )
+            )
+            sockets = sockets | socket
+        if sockets:
+            limit_memory = (
+                f"{Constants.TREX_LIMIT_MEMORY},{Constants.TREX_LIMIT_MEMORY}"
+            )
+
+        trex_config = TrexConfigGenerator()
+        trex_config.set_node(node)
+        trex_config.add_version()
+        trex_config.add_interfaces(pci_addresses)
+        trex_config.add_c(cores)
+        trex_config.add_limit_memory(limit_memory)
+        trex_config.add_port_info(port_info)
+        if Constants.TREX_RX_DESCRIPTORS_COUNT != 0:
+            trex_config.add_rx_desc(Constants.TREX_RX_DESCRIPTORS_COUNT)
+        if Constants.TREX_TX_DESCRIPTORS_COUNT != 0:
+            trex_config.add_rx_desc(Constants.TREX_TX_DESCRIPTORS_COUNT)
+        trex_config.add_platform_master_thread_id(int(master_thread_id))
+        trex_config.add_platform_latency_thread_id(int(latency_thread_id))
+        trex_config.add_platform_dual_if(dual_if)
+        trex_config.write_config()
index 2e03b4b..46c8b01 100644 (file)
@@ -20,7 +20,6 @@ from robot.api import logger
 from robot.libraries.BuiltIn import BuiltIn
 
 from .Constants import Constants
-from .CpuUtils import CpuUtils
 from .DropRateSearch import DropRateSearch
 from .MLRsearch.AbstractMeasurer import AbstractMeasurer
 from .MLRsearch.MultipleLossRatioSearch import MultipleLossRatioSearch
@@ -31,6 +30,7 @@ from .ssh import exec_cmd_no_error, exec_cmd
 from .topology import NodeType
 from .topology import NodeSubTypeTG
 from .topology import Topology
+from .TRexConfigGenerator import TrexInitConfig
 from .DUTSetup import DUTSetup as DS
 
 __all__ = [u"TGDropRateSearchImpl", u"TrafficGenerator", u"OptimizedSearch"]
@@ -129,18 +129,13 @@ class TrexMode:
     STL = u"STL"
 
 
-# TODO: Pylint says too-many-instance-attributes.
 class TrafficGenerator(AbstractMeasurer):
     """Traffic Generator."""
 
-    # TODO: Remove "trex" from lines which could work with other TGs.
-
     # Use one instance of TrafficGenerator for all tests in test suite
     ROBOT_LIBRARY_SCOPE = u"TEST SUITE"
 
     def __init__(self):
-        # TODO: Separate into few dataclasses/dicts.
-        #       Pylint dislikes large unstructured state, and it is right.
         self._node = None
         self._mode = None
         # TG interface order mapping
@@ -180,7 +175,6 @@ class TrafficGenerator(AbstractMeasurer):
         self.state_timeout = None
         # Transient data needed for async measurements.
         self._xstats = (None, None)
-        # TODO: Rename "xstats" to something opaque, so T-Rex is not privileged?
 
     @property
     def node(self):
@@ -284,15 +278,12 @@ class TrafficGenerator(AbstractMeasurer):
         else:
             return "none"
 
-    # TODO: pylint disable=too-many-locals.
     def initialize_traffic_generator(
             self, tg_node, tg_if1, tg_if2, tg_if1_adj_node, tg_if1_adj_if,
             tg_if2_adj_node, tg_if2_adj_if, osi_layer, tg_if1_dst_mac=None,
             tg_if2_dst_mac=None):
         """TG initialization.
 
-        TODO: Document why do we need (and how do we use) _ifaces_reordered.
-
         :param tg_node: Traffic generator node.
         :param tg_if1: TG - name of first interface.
         :param tg_if2: TG - name of second interface.
@@ -319,87 +310,32 @@ class TrafficGenerator(AbstractMeasurer):
         subtype = check_subtype(tg_node)
         if subtype == NodeSubTypeTG.TREX:
             self._node = tg_node
-            self._mode = TrexMode.ASTF if osi_layer == u"L7" else TrexMode.STL
-            if1 = dict()
-            if2 = dict()
-            if1[u"pci"] = Topology().get_interface_pci_addr(self._node, tg_if1)
-            if2[u"pci"] = Topology().get_interface_pci_addr(self._node, tg_if2)
-            if1[u"addr"] = Topology().get_interface_mac(self._node, tg_if1)
-            if2[u"addr"] = Topology().get_interface_mac(self._node, tg_if2)
-
-            if osi_layer == u"L2":
-                if1[u"adj_addr"] = if2[u"addr"]
-                if2[u"adj_addr"] = if1[u"addr"]
-            elif osi_layer in (u"L3", u"L7"):
-                if1[u"adj_addr"] = Topology().get_interface_mac(
+            self._mode = TrexMode.ASTF if osi_layer == "L7" else TrexMode.STL
+
+            if osi_layer == "L2":
+                tg_if1_adj_addr = Topology().get_interface_mac(tg_node, tg_if2)
+                tg_if2_adj_addr = Topology().get_interface_mac(tg_node, tg_if1)
+            elif osi_layer in ("L3", "L7"):
+                tg_if1_adj_addr = Topology().get_interface_mac(
                     tg_if1_adj_node, tg_if1_adj_if
                 )
-                if2[u"adj_addr"] = Topology().get_interface_mac(
+                tg_if2_adj_addr = Topology().get_interface_mac(
                     tg_if2_adj_node, tg_if2_adj_if
                 )
             else:
-                raise ValueError(u"Unknown OSI layer!")
-
-            # in case of switched environment we can override MAC addresses
-            if tg_if1_dst_mac is not None and tg_if2_dst_mac is not None:
-                if1[u"adj_addr"] = tg_if1_dst_mac
-                if2[u"adj_addr"] = tg_if2_dst_mac
-
-            if min(if1[u"pci"], if2[u"pci"]) != if1[u"pci"]:
-                if1, if2 = if2, if1
+                raise ValueError("Unknown OSI layer!")
+
+            tg_topology = list()
+            tg_topology.append(dict(interface=tg_if1, dst_mac=tg_if1_adj_addr))
+            tg_topology.append(dict(interface=tg_if2, dst_mac=tg_if2_adj_addr))
+            if1_pci = Topology().get_interface_pci_addr(self._node, tg_if1)
+            if2_pci = Topology().get_interface_pci_addr(self._node, tg_if2)
+            if min(if1_pci, if2_pci) != if1_pci:
                 self._ifaces_reordered = True
+                tg_topology.sort(reverse=True)
 
-            master_thread_id, latency_thread_id, socket, threads = \
-                CpuUtils.get_affinity_trex(
-                    self._node, tg_if1, tg_if2,
-                    tg_dtc=Constants.TREX_CORE_COUNT)
-
-            if osi_layer in (u"L2", u"L3", u"L7"):
-                exec_cmd_no_error(
-                    self._node,
-                    f"sh -c 'cat << EOF > /etc/trex_cfg.yaml\n"
-                    f"- version: 2\n"
-                    f"  c: {len(threads)}\n"
-                    f"  limit_memory: {Constants.TREX_LIMIT_MEMORY}\n"
-                    f"  interfaces: [\"{if1[u'pci']}\",\"{if2[u'pci']}\"]\n"
-                    f"  port_info:\n"
-                    f"      - dest_mac: \'{if1[u'adj_addr']}\'\n"
-                    f"        src_mac: \'{if1[u'addr']}\'\n"
-                    f"      - dest_mac: \'{if2[u'adj_addr']}\'\n"
-                    f"        src_mac: \'{if2[u'addr']}\'\n"
-                    f"  platform :\n"
-                    f"      master_thread_id: {master_thread_id}\n"
-                    f"      latency_thread_id: {latency_thread_id}\n"
-                    f"      dual_if:\n"
-                    f"          - socket: {socket}\n"
-                    f"            threads: {threads}\n"
-                    f"EOF'",
-                    sudo=True, message=u"T-Rex config generation!"
-                )
-
-                if Constants.TREX_RX_DESCRIPTORS_COUNT != 0:
-                    exec_cmd_no_error(
-                        self._node,
-                        f"sh -c 'cat << EOF >> /etc/trex_cfg.yaml\n"
-                        f"  rx_desc: {Constants.TREX_RX_DESCRIPTORS_COUNT}\n"
-                        f"EOF'",
-                        sudo=True, message=u"T-Rex rx_desc modification!"
-                    )
-
-                if Constants.TREX_TX_DESCRIPTORS_COUNT != 0:
-                    exec_cmd_no_error(
-                        self._node,
-                        f"sh -c 'cat << EOF >> /etc/trex_cfg.yaml\n"
-                        f"  tx_desc: {Constants.TREX_TX_DESCRIPTORS_COUNT}\n"
-                        f"EOF'",
-                        sudo=True, message=u"T-Rex tx_desc modification!"
-                    )
-            else:
-                raise ValueError(u"Unknown OSI layer!")
-
-            TrafficGenerator.startup_trex(
-                self._node, osi_layer, subtype=subtype
-            )
+            TrexInitConfig.init_trex_startup_configuration(tg_node, tg_topology)
+            TrafficGenerator.startup_trex(tg_node, osi_layer, subtype=subtype)
 
     @staticmethod
     def startup_trex(tg_node, osi_layer, subtype=None):
@@ -670,8 +606,6 @@ class TrafficGenerator(AbstractMeasurer):
         if not isinstance(duration, (float, int)):
             duration = float(duration)
 
-        # TODO: Refactor the code so duration is computed only once,
-        # and both the initial and the computed durations are logged.
         computed_duration, _ = self._compute_duration(duration, multiplier)
 
         command_line = OptionString().add(u"python3")
@@ -783,8 +717,6 @@ class TrafficGenerator(AbstractMeasurer):
         if not isinstance(duration, (float, int)):
             duration = float(duration)
 
-        # TODO: Refactor the code so duration is computed only once,
-        # and both the initial and the computed durations are logged.
         duration, _ = self._compute_duration(duration=duration, multiplier=rate)
 
         command_line = OptionString().add(u"python3")
@@ -808,7 +740,6 @@ class TrafficGenerator(AbstractMeasurer):
         command_line.add_if(u"force", Constants.TREX_SEND_FORCE)
         command_line.add_with_value(u"delay", Constants.PERF_TRIAL_STL_DELAY)
 
-        # TODO: This is ugly. Handle parsing better.
         self._start_time = time.monotonic()
         self._rate = float(rate[:-3]) if u"pps" in rate else float(rate)
         stdout, _ = exec_cmd_no_error(
@@ -980,7 +911,6 @@ class TrafficGenerator(AbstractMeasurer):
                 )
             elif u"trex-stl" in self.traffic_profile:
                 unit_rate_str = str(rate) + u"pps"
-                # TODO: Suport transaction_scale et al?
                 self.trex_stl_start_remote_exec(
                     duration, unit_rate_str, async_call
                 )
@@ -1027,7 +957,6 @@ class TrafficGenerator(AbstractMeasurer):
         complete = False
         if self.ramp_up_rate:
             # Figure out whether we need to insert a ramp-up trial.
-            # TODO: Give up on async_call=True?
             if ramp_up_only or self.ramp_up_start is None:
                 # We never ramped up yet (at least not in this test case).
                 ramp_up_needed = True
@@ -1099,8 +1028,6 @@ class TrafficGenerator(AbstractMeasurer):
     def fail_if_no_traffic_forwarded(self):
         """Fail if no traffic forwarded.
 
-        TODO: Check number of passed transactions instead.
-
         :returns: nothing
         :raises Exception: If no traffic forwarded.
         """
@@ -1261,8 +1188,6 @@ class TrafficGenerator(AbstractMeasurer):
         depend on the transaction type. Usually they are in transactions
         per second, or aggregated packets per second.
 
-        TODO: Fail on running or already reported measurement.
-
         :returns: Structure containing the result of the measurement.
         :rtype: ReceiveRateMeasurement
         """
@@ -1405,8 +1330,6 @@ class TrafficGenerator(AbstractMeasurer):
         if self.sleep_till_duration:
             sleeptime = time_stop - time.monotonic()
             if sleeptime > 0.0:
-                # TODO: Sometimes we have time to do additional trials here,
-                # adapt PLRsearch to accept all the results.
                 time.sleep(sleeptime)
         return result
 
@@ -1447,7 +1370,6 @@ class TrafficGenerator(AbstractMeasurer):
         :param transaction_type: An identifier specifying which counters
             and formulas to use when computing attempted and failed
             transactions. Default: "packet".
-            TODO: Does this also specify parsing for the measured duration?
         :param duration_limit: Zero or maximum limit for computed (or given)
             duration.
         :param negative_loss: If false, negative loss is reported as zero loss.
@@ -1596,8 +1518,6 @@ class OptimizedSearch:
             u"resources.libraries.python.TrafficGenerator"
         )
         # Overrides for fixed transaction amount.
-        # TODO: Move to robot code? We have two call sites, so this saves space,
-        #       even though this is surprising for log readers.
         if transaction_scale:
             initial_trial_duration = 1.0
             final_trial_duration = 1.0
@@ -1725,11 +1645,7 @@ class OptimizedSearch:
             u"resources.libraries.python.TrafficGenerator"
         )
         # Overrides for fixed transaction amount.
-        # TODO: Move to robot code? We have a single call site
-        #       but MLRsearch has two and we want the two to be used similarly.
         if transaction_scale:
-            # TODO: What is a good value for max scale?
-            # TODO: Scale the timeout with transaction scale.
             timeout = 7200.0
         tg_instance.set_rate_provider_defaults(
             frame_size=frame_size,