xref: /dpdk/dts/framework/testbed_model/linux_session.py (revision e3ab9dd5cd5d5e7cb117507ba9580dae9706c1f5)
1840b1e01SJuraj Linkeš# SPDX-License-Identifier: BSD-3-Clause
2840b1e01SJuraj Linkeš# Copyright(c) 2023 PANTHEON.tech s.r.o.
3840b1e01SJuraj Linkeš# Copyright(c) 2023 University of New Hampshire
4840b1e01SJuraj Linkeš
56ef07151SJuraj Linkeš"""Linux OS translator.
66ef07151SJuraj Linkeš
76ef07151SJuraj LinkešTranslate OS-unaware calls into Linux calls/utilities. Most of Linux distributions are mostly
86ef07151SJuraj Linkešcompliant with POSIX standards, so this module only implements the parts that aren't.
96ef07151SJuraj LinkešThis intermediate module implements the common parts of mostly POSIX compliant distributions.
106ef07151SJuraj Linkeš"""
116ef07151SJuraj Linkeš
12840b1e01SJuraj Linkešimport json
13*e3ab9dd5SPaul Szczepanekfrom typing import TypedDict
14840b1e01SJuraj Linkeš
15840b1e01SJuraj Linkešfrom typing_extensions import NotRequired
16840b1e01SJuraj Linkeš
17e5307b25SNicholas Prattefrom framework.exception import ConfigurationError, RemoteCommandExecutionError
18840b1e01SJuraj Linkešfrom framework.utils import expand_range
19840b1e01SJuraj Linkeš
20840b1e01SJuraj Linkešfrom .cpu import LogicalCore
21840b1e01SJuraj Linkešfrom .port import Port
22840b1e01SJuraj Linkešfrom .posix_session import PosixSession
23840b1e01SJuraj Linkeš
24840b1e01SJuraj Linkeš
25840b1e01SJuraj Linkešclass LshwConfigurationOutput(TypedDict):
266ef07151SJuraj Linkeš    """The relevant parts of ``lshw``'s ``configuration`` section."""
276ef07151SJuraj Linkeš
286ef07151SJuraj Linkeš    #:
29840b1e01SJuraj Linkeš    link: str
30840b1e01SJuraj Linkeš
31840b1e01SJuraj Linkeš
32840b1e01SJuraj Linkešclass LshwOutput(TypedDict):
336ef07151SJuraj Linkeš    """A model of the relevant information from ``lshw``'s json output.
346ef07151SJuraj Linkeš
356ef07151SJuraj Linkeš    Example:
366ef07151SJuraj Linkeš        ::
376ef07151SJuraj Linkeš
38840b1e01SJuraj Linkeš            {
39840b1e01SJuraj Linkeš            ...
40840b1e01SJuraj Linkeš            "businfo" : "pci@0000:08:00.0",
41840b1e01SJuraj Linkeš            "logicalname" : "enp8s0",
42840b1e01SJuraj Linkeš            "version" : "00",
43840b1e01SJuraj Linkeš            "serial" : "52:54:00:59:e1:ac",
44840b1e01SJuraj Linkeš            ...
45840b1e01SJuraj Linkeš            "configuration" : {
46840b1e01SJuraj Linkeš              ...
47840b1e01SJuraj Linkeš              "link" : "yes",
48840b1e01SJuraj Linkeš              ...
49840b1e01SJuraj Linkeš            },
50840b1e01SJuraj Linkeš            ...
51840b1e01SJuraj Linkeš    """
52840b1e01SJuraj Linkeš
536ef07151SJuraj Linkeš    #:
54840b1e01SJuraj Linkeš    businfo: str
556ef07151SJuraj Linkeš    #:
56840b1e01SJuraj Linkeš    logicalname: NotRequired[str]
576ef07151SJuraj Linkeš    #:
58840b1e01SJuraj Linkeš    serial: NotRequired[str]
596ef07151SJuraj Linkeš    #:
60840b1e01SJuraj Linkeš    configuration: LshwConfigurationOutput
61840b1e01SJuraj Linkeš
62840b1e01SJuraj Linkeš
63840b1e01SJuraj Linkešclass LinuxSession(PosixSession):
646ef07151SJuraj Linkeš    """The implementation of non-Posix compliant parts of Linux."""
65840b1e01SJuraj Linkeš
66840b1e01SJuraj Linkeš    @staticmethod
67840b1e01SJuraj Linkeš    def _get_privileged_command(command: str) -> str:
68840b1e01SJuraj Linkeš        return f"sudo -- sh -c '{command}'"
69840b1e01SJuraj Linkeš
70840b1e01SJuraj Linkeš    def get_remote_cpus(self, use_first_core: bool) -> list[LogicalCore]:
716ef07151SJuraj Linkeš        """Overrides :meth:`~.os_session.OSSession.get_remote_cpus`."""
72840b1e01SJuraj Linkeš        cpu_info = self.send_command("lscpu -p=CPU,CORE,SOCKET,NODE|grep -v \\#").stdout
73840b1e01SJuraj Linkeš        lcores = []
74840b1e01SJuraj Linkeš        for cpu_line in cpu_info.splitlines():
75840b1e01SJuraj Linkeš            lcore, core, socket, node = map(int, cpu_line.split(","))
76840b1e01SJuraj Linkeš            if core == 0 and socket == 0 and not use_first_core:
77840b1e01SJuraj Linkeš                self._logger.info("Not using the first physical core.")
78840b1e01SJuraj Linkeš                continue
79840b1e01SJuraj Linkeš            lcores.append(LogicalCore(lcore, core, socket, node))
80840b1e01SJuraj Linkeš        return lcores
81840b1e01SJuraj Linkeš
82840b1e01SJuraj Linkeš    def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str:
836ef07151SJuraj Linkeš        """Overrides :meth:`~.os_session.OSSession.get_dpdk_file_prefix`."""
84840b1e01SJuraj Linkeš        return dpdk_prefix
85840b1e01SJuraj Linkeš
86c0dd39deSNicholas Pratte    def setup_hugepages(self, number_of: int, hugepage_size: int, force_first_numa: bool) -> None:
876ef07151SJuraj Linkeš        """Overrides :meth:`~.os_session.OSSession.setup_hugepages`."""
88840b1e01SJuraj Linkeš        self._logger.info("Getting Hugepage information.")
89e5307b25SNicholas Pratte        hugepages_total = self._get_hugepages_total(hugepage_size)
90e5307b25SNicholas Pratte        if (
91e5307b25SNicholas Pratte            f"hugepages-{hugepage_size}kB"
92e5307b25SNicholas Pratte            not in self.send_command("ls /sys/kernel/mm/hugepages").stdout
93e5307b25SNicholas Pratte        ):
94e5307b25SNicholas Pratte            raise ConfigurationError("hugepage size not supported by operating system")
95840b1e01SJuraj Linkeš        self._numa_nodes = self._get_numa_nodes()
96840b1e01SJuraj Linkeš
97c0dd39deSNicholas Pratte        if force_first_numa or hugepages_total < number_of:
98840b1e01SJuraj Linkeš            # when forcing numa, we need to clear existing hugepages regardless
99840b1e01SJuraj Linkeš            # of size, so they can be moved to the first numa node
100c0dd39deSNicholas Pratte            self._configure_huge_pages(number_of, hugepage_size, force_first_numa)
101840b1e01SJuraj Linkeš        else:
102840b1e01SJuraj Linkeš            self._logger.info("Hugepages already configured.")
103840b1e01SJuraj Linkeš        self._mount_huge_pages()
104840b1e01SJuraj Linkeš
105e5307b25SNicholas Pratte    def _get_hugepages_total(self, hugepage_size: int) -> int:
106840b1e01SJuraj Linkeš        hugepages_total = self.send_command(
107e5307b25SNicholas Pratte            f"cat /sys/kernel/mm/hugepages/hugepages-{hugepage_size}kB/nr_hugepages"
108840b1e01SJuraj Linkeš        ).stdout
109840b1e01SJuraj Linkeš        return int(hugepages_total)
110840b1e01SJuraj Linkeš
111840b1e01SJuraj Linkeš    def _get_numa_nodes(self) -> list[int]:
112840b1e01SJuraj Linkeš        try:
113840b1e01SJuraj Linkeš            numa_count = self.send_command(
114840b1e01SJuraj Linkeš                "cat /sys/devices/system/node/online", verify=True
115840b1e01SJuraj Linkeš            ).stdout
116840b1e01SJuraj Linkeš            numa_range = expand_range(numa_count)
117840b1e01SJuraj Linkeš        except RemoteCommandExecutionError:
118840b1e01SJuraj Linkeš            # the file doesn't exist, meaning the node doesn't support numa
119840b1e01SJuraj Linkeš            numa_range = []
120840b1e01SJuraj Linkeš        return numa_range
121840b1e01SJuraj Linkeš
122840b1e01SJuraj Linkeš    def _mount_huge_pages(self) -> None:
123840b1e01SJuraj Linkeš        self._logger.info("Re-mounting Hugepages.")
124840b1e01SJuraj Linkeš        hugapge_fs_cmd = "awk '/hugetlbfs/ { print $2 }' /proc/mounts"
125224c1f0eSJeremy Spewock        self.send_command(f"umount $({hugapge_fs_cmd})", privileged=True)
126840b1e01SJuraj Linkeš        result = self.send_command(hugapge_fs_cmd)
127840b1e01SJuraj Linkeš        if result.stdout == "":
128840b1e01SJuraj Linkeš            remote_mount_path = "/mnt/huge"
129224c1f0eSJeremy Spewock            self.send_command(f"mkdir -p {remote_mount_path}", privileged=True)
130224c1f0eSJeremy Spewock            self.send_command(f"mount -t hugetlbfs nodev {remote_mount_path}", privileged=True)
131840b1e01SJuraj Linkeš
132840b1e01SJuraj Linkeš    def _supports_numa(self) -> bool:
133840b1e01SJuraj Linkeš        # the system supports numa if self._numa_nodes is non-empty and there are more
134840b1e01SJuraj Linkeš        # than one numa node (in the latter case it may actually support numa, but
135840b1e01SJuraj Linkeš        # there's no reason to do any numa specific configuration)
136840b1e01SJuraj Linkeš        return len(self._numa_nodes) > 1
137840b1e01SJuraj Linkeš
138c0dd39deSNicholas Pratte    def _configure_huge_pages(self, number_of: int, size: int, force_first_numa: bool) -> None:
139840b1e01SJuraj Linkeš        self._logger.info("Configuring Hugepages.")
140840b1e01SJuraj Linkeš        hugepage_config_path = f"/sys/kernel/mm/hugepages/hugepages-{size}kB/nr_hugepages"
141840b1e01SJuraj Linkeš        if force_first_numa and self._supports_numa():
142840b1e01SJuraj Linkeš            # clear non-numa hugepages
143840b1e01SJuraj Linkeš            self.send_command(f"echo 0 | tee {hugepage_config_path}", privileged=True)
144840b1e01SJuraj Linkeš            hugepage_config_path = (
145840b1e01SJuraj Linkeš                f"/sys/devices/system/node/node{self._numa_nodes[0]}/hugepages"
146840b1e01SJuraj Linkeš                f"/hugepages-{size}kB/nr_hugepages"
147840b1e01SJuraj Linkeš            )
148840b1e01SJuraj Linkeš
149c0dd39deSNicholas Pratte        self.send_command(f"echo {number_of} | tee {hugepage_config_path}", privileged=True)
150840b1e01SJuraj Linkeš
151840b1e01SJuraj Linkeš    def update_ports(self, ports: list[Port]) -> None:
1526ef07151SJuraj Linkeš        """Overrides :meth:`~.os_session.OSSession.update_ports`."""
153840b1e01SJuraj Linkeš        self._logger.debug("Gathering port info.")
154840b1e01SJuraj Linkeš        for port in ports:
155840b1e01SJuraj Linkeš            assert port.node == self.name, "Attempted to gather port info on the wrong node"
156840b1e01SJuraj Linkeš
157840b1e01SJuraj Linkeš        port_info_list = self._get_lshw_info()
158840b1e01SJuraj Linkeš        for port in ports:
159840b1e01SJuraj Linkeš            for port_info in port_info_list:
160840b1e01SJuraj Linkeš                if f"pci@{port.pci}" == port_info.get("businfo"):
161840b1e01SJuraj Linkeš                    self._update_port_attr(port, port_info.get("logicalname"), "logical_name")
162840b1e01SJuraj Linkeš                    self._update_port_attr(port, port_info.get("serial"), "mac_address")
163840b1e01SJuraj Linkeš                    port_info_list.remove(port_info)
164840b1e01SJuraj Linkeš                    break
165840b1e01SJuraj Linkeš            else:
166840b1e01SJuraj Linkeš                self._logger.warning(f"No port at pci address {port.pci} found.")
167840b1e01SJuraj Linkeš
168840b1e01SJuraj Linkeš    def _get_lshw_info(self) -> list[LshwOutput]:
169840b1e01SJuraj Linkeš        output = self.send_command("lshw -quiet -json -C network", verify=True)
170840b1e01SJuraj Linkeš        return json.loads(output.stdout)
171840b1e01SJuraj Linkeš
172840b1e01SJuraj Linkeš    def _update_port_attr(self, port: Port, attr_value: str | None, attr_name: str) -> None:
173840b1e01SJuraj Linkeš        if attr_value:
174840b1e01SJuraj Linkeš            setattr(port, attr_name, attr_value)
175840b1e01SJuraj Linkeš            self._logger.debug(f"Found '{attr_name}' of port {port.pci}: '{attr_value}'.")
176840b1e01SJuraj Linkeš        else:
177840b1e01SJuraj Linkeš            self._logger.warning(
178840b1e01SJuraj Linkeš                f"Attempted to get '{attr_name}' of port {port.pci}, but it doesn't exist."
179840b1e01SJuraj Linkeš            )
180840b1e01SJuraj Linkeš
181ace78563SJeremy Spewock    def configure_port_mtu(self, mtu: int, port: Port) -> None:
182ace78563SJeremy Spewock        """Overrides :meth:`~.os_session.OSSession.configure_port_mtu`."""
183ace78563SJeremy Spewock        self.send_command(
184ace78563SJeremy Spewock            f"ip link set dev {port.logical_name} mtu {mtu}",
185ace78563SJeremy Spewock            privileged=True,
186ace78563SJeremy Spewock            verify=True,
187ace78563SJeremy Spewock        )
188ace78563SJeremy Spewock
189840b1e01SJuraj Linkeš    def configure_ipv4_forwarding(self, enable: bool) -> None:
1906ef07151SJuraj Linkeš        """Overrides :meth:`~.os_session.OSSession.configure_ipv4_forwarding`."""
191840b1e01SJuraj Linkeš        state = 1 if enable else 0
192840b1e01SJuraj Linkeš        self.send_command(f"sysctl -w net.ipv4.ip_forward={state}", privileged=True)
193