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