diff --git a/README.md b/README.md index 0e8b1a3..7c53405 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ Constructor: * `is_package_installed_via_rpm(self, package: str, cwd: "Path | str | None" = None) -> bool` - verify if package installed using rpm * `is_package_installed_via_dpkg(self, package: str, cwd: "Path | str | None" = None) -> bool`- verify if package installed using dpkg * `install_package_via_dnf(self, package: str, cwd: "Path | str | None" = None) -> "ConnectionCompletedProcess":` - install package using dnf +* `install_rdma_drivers(self, build_path: "Path | str") -> None:` - install RDMA drivers from build path. #### ESXiPackageManager: * `get_driver_info(self, interface_name: str) -> DriverInfo` - Get driver info (name, version). @@ -143,6 +144,7 @@ Constructor: * `_get_inf_device_section_name(self, build: str, section_dictionary: Dict[str, List[str]], component_id: str, client_os: bool) -> str` get the section name from the parsed section dictionary corresponding to the specified component id * `_get_server_or_client_section(self, section_dictionary: Dict[str, List[str]], client_os: bool) -> List[str]` get all the component ids from the parsed section dictionary for either the server or client OS section * `_get_default_vals_from_inf(self, device_section_name: str, section_dictionary: Dict[str, List[str]]) -> List[Tuple[str, str]]` get all advanced feature names and the default value for each feature +* `install_rdma_drivers(self, build_path: "Path | str") -> None:` - install RDMA drivers from build path. #### BSDPackageManager: * API from Unix Package Manager diff --git a/examples/linux_example.py b/examples/linux_example.py index 1563d76..1e6cd6b 100644 --- a/examples/linux_example.py +++ b/examples/linux_example.py @@ -51,3 +51,6 @@ package_manager.is_package_installed_via_rpm(package="make", cwd="/home/user") package_manager.is_package_installed_via_dpkg(package="docker") package_manager.install_package_via_dnf("1.2.3", cwd="/home") + +# install rdma drivers from build path +package_manager.install_rdma_drivers("/home/rdma_drivers/build") diff --git a/examples/windows_example.py b/examples/windows_example.py index dbc140a..c86e913 100644 --- a/examples/windows_example.py +++ b/examples/windows_example.py @@ -31,3 +31,6 @@ package_manager.install_build(r"C:\Users\admin\Downloads\drivers", device_id) package_manager.install_build(r"C:\Users\admin\Downloads\drivers") # will install on all devices + +# install rdma drivers from build path +package_manager.install_rdma_drivers(r"C:\Users\admin\Downloads\drivers") \ No newline at end of file diff --git a/mfd_package_manager/base.py b/mfd_package_manager/base.py index 2435cb6..86cae99 100644 --- a/mfd_package_manager/base.py +++ b/mfd_package_manager/base.py @@ -327,3 +327,11 @@ def _verify_pip_parameters( level=log_levels.MODULE_DEBUG, msg="Connection interpreter is not available for other connection than Python (RPyC and Local)", ) + + def install_rdma_drivers(self) -> None: + """ + Install RDMA drivers if not installed. + + :raises PackageManagerModuleException: on failure + """ + raise NotImplementedError("Method install_rdma_drivers is not implemented for base PackageManager class.") diff --git a/mfd_package_manager/linux.py b/mfd_package_manager/linux.py index 9fb0f22..a1f0609 100644 --- a/mfd_package_manager/linux.py +++ b/mfd_package_manager/linux.py @@ -741,3 +741,53 @@ def install_package_via_zypper(self, package: str, cwd: "Path | str | None" = No stderr_to_stdout=True, cwd=cwd, ) + + def install_rdma_drivers(self, build_path: "Path | str") -> None: + """ + Install RDMA drivers. + + tar -zxvf irdma-X.X.X.tgz + cd irdma-X.X.X + ./build.sh + ./build_core.sh + ./install_core.sh + + :param build_path: Path to build + """ + if isinstance(build_path, str): + build_path = self._controller_connection.path(build_path) + if not build_path.exists(): + raise PackageManagerNotFoundException(f"Build path {build_path} does not exist.") + build_path = build_path / "RDMA" / "Linux" + irdma_tar_files = list(build_path.glob("irdma-*.tgz")) + if not irdma_tar_files: + raise PackageManagerNotFoundException("No irdma tar files found in build path.") + if len(irdma_tar_files) > 1: + raise PackageManagerModuleException("Multiple irdma tar files found in build path.") + irdma_tar_file = irdma_tar_files[0] + remote_irdma_tar = self._connection.path(f"/tmp/{irdma_tar_file.name}") + copy( + src_conn=self._controller_connection, + dst_conn=self._connection, + source=irdma_tar_file, + target=remote_irdma_tar, + ) + self._connection.execute_command( + f"tar -zxvf {remote_irdma_tar} -C /tmp/ --no-same-owner", expected_return_codes={0}, shell=True + ) + irdma_dir_name = irdma_tar_file.name.replace(".tgz", "") + remote_irdma_dir = self._connection.path(f"/tmp/{irdma_dir_name}") + logger.log(level=log_levels.MODULE_DEBUG, msg="Building and installing RDMA drivers.") + self._connection.execute_command("./build.sh", expected_return_codes={0}, shell=True, cwd=remote_irdma_dir) + logger.log(level=log_levels.MODULE_DEBUG, msg="Building RDMA core drivers.") + self._connection.execute_command( + f"sh -c '{remote_irdma_dir}/build_core.sh -y'", + expected_return_codes={0}, + shell=True, + stderr_to_stdout=True, + ) + logger.log(level=log_levels.MODULE_DEBUG, msg="Installing RDMA core drivers.") + self._connection.execute_command( + "./install_core.sh", expected_return_codes={0}, shell=True, cwd=remote_irdma_dir + ) + logger.log(level=log_levels.MODULE_DEBUG, msg="RDMA drivers installed successfully.") diff --git a/mfd_package_manager/windows.py b/mfd_package_manager/windows.py index fc3c807..f154314 100644 --- a/mfd_package_manager/windows.py +++ b/mfd_package_manager/windows.py @@ -55,6 +55,14 @@ class WindowsPackageManager(PackageManager): "WS2025": range(26000, 26999), } + RDMA_FOLDER_OS_VERSION_MATCH = { + "ws2019": range(17600, 19042 + 1), + "W10": range(19044, 19044 + 1), + "ws2022": list(chain(range(20348, 22000), [23598], range(25000, 25999))), + "W11": range(22000, 22999), + "ws2025": range(26000, 26999), + } + def _prepare_map(self) -> Dict[str, Set]: """Update mapping for Windows driver names.""" driver_map = cpy.copy(DRIVER_DEVICE_ID_MAP) @@ -542,6 +550,19 @@ def _get_folder_for_os_version(self, os_version: int) -> str: return folder raise PackageManagerConnectedOSNotSupported(f"Windows in version {os_version} is not supported by module.") + def _get_rdma_folder_for_os_version(self, os_version: int) -> str: + """ + Return correct Windows OS directory required for RDMA driver searching. + + :param os_version: Version of Windows kernel. + :return: Directory correct for version. + :raises PackageManagerConnectedOSNotSupported: When version is not supported. + """ + for folder, version in self.RDMA_FOLDER_OS_VERSION_MATCH.items(): + if os_version in version: + return folder + raise PackageManagerConnectedOSNotSupported(f"Windows in version {os_version} is not supported by module.") + def find_drivers(self, build_path: Union[str, Path], device_id: "DeviceID") -> List[Path]: """ Find drivers in build path for given driver_details. @@ -902,3 +923,47 @@ def _get_default_vals_from_inf( except KeyError: logger.log(level=logging.WARNING, msg="Unable to find the default feature values") return [] + + def install_rdma_drivers(self, build_path: "Path | str") -> None: + """ + Install RDMA drivers from build path. + + :param build_path: Path to build + :raises PackageManagerNotFoundException: when not found something in flow + """ + if isinstance(build_path, str): + build_path = self._controller_connection.path(build_path) + build_path = build_path / "RDMA" / "Windows" + if not build_path.exists(): + raise PackageManagerNotFoundException(f"Build path {build_path} does not exist.") + kernel_version = int(self._connection.get_system_info().kernel_version) + windows_version_folder = self._get_rdma_folder_for_os_version(kernel_version) + logger.log( + level=log_levels.MODULE_DEBUG, + msg=f"Going to use {windows_version_folder} windows version directory for {kernel_version}.", + ) + specific_rdma_on_controller_path = build_path / windows_version_folder + + if not specific_rdma_on_controller_path.exists(): + raise PackageManagerNotFoundException( + f"RDMA path {specific_rdma_on_controller_path} does not exist on controller." + ) + rdma_remote_path = self._connection.path(self.DRIVERS_REMOTE_LOCATION, "RDMA") + copy( + src_conn=self._controller_connection, + dst_conn=self._connection, + source=specific_rdma_on_controller_path / "*", + target=rdma_remote_path, + ) + inf_file_path = rdma_remote_path / "indv2.inf" + if not inf_file_path.exists(): + raise PackageManagerNotFoundException(f"RDMA inf file path {inf_file_path} does not exist.") + logger.log(level=log_levels.MODULE_DEBUG, msg=f"Installing RDMA driver from {inf_file_path}") + self.install_certificates_from_driver(str(inf_file_path)) + self.install_inf_driver_for_matching_devices(str(inf_file_path)) + driver_list = self.get_driver_files() + for driver in driver_list: + if "indv2.inf" in driver.original_name.casefold(): + logger.log(level=log_levels.MODULE_DEBUG, msg="RDMA driver installed successfully.") + return + raise PackageManagerModuleException("RDMA driver installation failed.") diff --git a/tests/unit/test_mfd_package_manager/test_base.py b/tests/unit/test_mfd_package_manager/test_base.py index 238ea48..16ef59d 100644 --- a/tests/unit/test_mfd_package_manager/test_base.py +++ b/tests/unit/test_mfd_package_manager/test_base.py @@ -139,7 +139,8 @@ def test_controller_connection_not_supported(self, mocker): conn2 = mocker.create_autospec(RPyCConnection) conn2.get_os_name.return_value = OSName.ESXI with pytest.raises( - PackageManagerConnectedOSNotSupported, match="Not supported OS for controller PackageManager: [OSName.ESXI|VMKernel]" + PackageManagerConnectedOSNotSupported, + match="Not supported OS for controller PackageManager: [OSName.ESXI|VMKernel]", ): PackageManager(connection=conn, controller_connection=conn2) diff --git a/tests/unit/test_mfd_package_manager/test_linux.py b/tests/unit/test_mfd_package_manager/test_linux.py index 74c609b..3a3c4a2 100644 --- a/tests/unit/test_mfd_package_manager/test_linux.py +++ b/tests/unit/test_mfd_package_manager/test_linux.py @@ -708,3 +708,126 @@ def test_ethtool(self, manager, mocker): assert manager._LinuxPackageManager__ethtool is None manager._ethtool assert manager._LinuxPackageManager__ethtool is not None + + def test_install_rdma_drivers_success(self, manager, mocker): + # prepare controller and remote paths + manager._controller_connection = mocker.create_autospec(LocalConnection) + controller_build_path = Path("/home/user/build") + manager._controller_connection.path.return_value = controller_build_path + + # pretend all Paths exist + mocker.patch("mfd_package_manager.linux.Path.exists", return_value=True) + + # glob returns exactly one irdma tar file located under RDMA/Linux + irdma_file = controller_build_path / "RDMA" / "Linux" / "irdma-1.2.3.tgz" + mocker.patch("mfd_package_manager.linux.Path.glob", return_value=[irdma_file]) + + # make connection.path return Path objects for remote paths + manager._connection.path.side_effect = lambda p: Path(p) + + copy_mock = mocker.patch("mfd_package_manager.linux.copy") + + # ensure execute_command returns success for all commands + manager._connection.execute_command.return_value = ConnectionCompletedProcess(args="", stdout="", return_code=0) + + # call the method + manager.install_rdma_drivers(str(controller_build_path)) + + # copy should be called with controller src and connection dst + copy_mock.assert_called_once_with( + src_conn=manager._controller_connection, + dst_conn=manager._connection, + source=irdma_file, + target=Path(f"/tmp/{irdma_file.name}"), + ) + + # verify build and install commands were invoked (cwd equals remote irdma dir) + remote_dir = Path(f"/tmp/{irdma_file.name.replace('.tgz','')}") + manager._connection.execute_command.assert_any_call( + "./build.sh", expected_return_codes={0}, shell=True, cwd=remote_dir + ) + manager._connection.execute_command.assert_any_call( + "./install_core.sh", expected_return_codes={0}, shell=True, cwd=remote_dir + ) + + def test_install_rdma_drivers_no_tar(self, manager, mocker): + manager._controller_connection = mocker.create_autospec(LocalConnection) + controller_build_path = Path("/home/user/build") + manager._controller_connection.path.return_value = controller_build_path + + mocker.patch("mfd_package_manager.linux.Path.exists", return_value=True) + mocker.patch("mfd_package_manager.linux.Path.glob", return_value=[]) + + with pytest.raises(PackageManagerNotFoundException, match="No irdma tar files found in build path."): + manager.install_rdma_drivers(str(controller_build_path)) + + def test_install_rdma_drivers_multiple_tar(self, manager, mocker): + manager._controller_connection = mocker.create_autospec(LocalConnection) + controller_build_path = Path("/home/user/build") + manager._controller_connection.path.return_value = controller_build_path + + mocker.patch("mfd_package_manager.linux.Path.exists", return_value=True) + tf1 = controller_build_path / "RDMA" / "Linux" / "irdma-1.2.3.tgz" + tf2 = controller_build_path / "RDMA" / "Linux" / "irdma-2.0.0.tgz" + mocker.patch("mfd_package_manager.linux.Path.glob", return_value=[tf1, tf2]) + + with pytest.raises(PackageManagerModuleException, match="Multiple irdma tar files found in build path."): + manager.install_rdma_drivers(str(controller_build_path)) + + def test_install_rdma_drivers_runs_build_core(self, manager, mocker): + # Ensure the sh -c build_core command path is executed + manager._controller_connection = mocker.create_autospec(LocalConnection) + controller_build_path = Path("/home/user/build") + manager._controller_connection.path.return_value = controller_build_path + + mocker.patch("mfd_package_manager.linux.Path.exists", return_value=True) + irdma_file = controller_build_path / "RDMA" / "Linux" / "irdma-1.2.3.tgz" + mocker.patch("mfd_package_manager.linux.Path.glob", return_value=[irdma_file]) + + manager._connection.path.side_effect = lambda p: Path(p) + manager._connection.execute_command.return_value = ConnectionCompletedProcess(args="", stdout="", return_code=0) + + manager.install_rdma_drivers(str(controller_build_path)) + + remote_dir = Path(f"/tmp/{irdma_file.name.replace('.tgz','')}") + expected = f"sh -c '{remote_dir}/build_core.sh -y'" + manager._connection.execute_command.assert_any_call( + expected, expected_return_codes={0}, shell=True, stderr_to_stdout=True + ) + + def test_install_rdma_drivers_tar_extraction(self, manager, mocker): + # Ensure tar extraction command is executed with expected args + manager._controller_connection = mocker.create_autospec(LocalConnection) + controller_build_path = Path("/home/user/build") + manager._controller_connection.path.return_value = controller_build_path + + mocker.patch("mfd_package_manager.linux.Path.exists", return_value=True) + irdma_file = controller_build_path / "RDMA" / "Linux" / "irdma-1.2.3.tgz" + mocker.patch("mfd_package_manager.linux.Path.glob", return_value=[irdma_file]) + + # make remote path resolver return pathlib.Path + manager._connection.path.side_effect = lambda p: Path(p) + # stub execute_command to success + manager._connection.execute_command.return_value = ConnectionCompletedProcess( + args="", stdout="", return_code=0 + ) + + manager.install_rdma_drivers(str(controller_build_path)) + + remote_irdma_tar = Path(f"/tmp/{irdma_file.name}") + manager._connection.execute_command.assert_any_call( + f"tar -zxvf {remote_irdma_tar} -C /tmp/ --no-same-owner", + expected_return_codes={0}, + shell=True, + ) + + def test_install_rdma_drivers_missing_build(self, manager, mocker): + manager._controller_connection = mocker.create_autospec(LocalConnection) + controller_build_path = Path("/home/user/build") + manager._controller_connection.path.return_value = controller_build_path + + mocker.patch("mfd_package_manager.linux.Path.exists", return_value=False) + with pytest.raises( + PackageManagerNotFoundException, match=re.escape(f"Build path {controller_build_path} does not exist.") + ): + manager.install_rdma_drivers(str(controller_build_path)) \ No newline at end of file diff --git a/tests/unit/test_mfd_package_manager/test_windows.py b/tests/unit/test_mfd_package_manager/test_windows.py index d06159c..26f01b6 100644 --- a/tests/unit/test_mfd_package_manager/test_windows.py +++ b/tests/unit/test_mfd_package_manager/test_windows.py @@ -1106,3 +1106,98 @@ def test_FOLDER_OS_VERSION_MATCH(self, manager): assert 23598 in manager.FOLDER_OS_VERSION_MATCH["WS2022"] assert 20348 in manager.FOLDER_OS_VERSION_MATCH["WS2022"] assert 9200 in manager.FOLDER_OS_VERSION_MATCH["NDIS64"] + + def test_install_rdma_drivers_success(self, manager, mocker): + # prepare controller and remote paths + manager._controller_connection = mocker.create_autospec(LocalConnection) + controller_build_path = Path("C:/build") + manager._controller_connection.path.return_value = controller_build_path + + # ensure controller rdma Windows path exists + mocker.patch("mfd_package_manager.windows.Path.exists", return_value=True) + + # make manager._connection.path return a remote RDMA path + remote_rdma = Path(r"C:\drivers_under_test\RDMA") + manager._connection.path.return_value = remote_rdma + + # system info kernel version that maps to W10 + manager._connection.get_system_info.return_value.kernel_version = "19044" + + # stub certificate and inf installation calls + manager.install_certificates_from_driver = mocker.create_autospec(manager.install_certificates_from_driver) + manager.install_inf_driver_for_matching_devices = mocker.create_autospec( + manager.install_inf_driver_for_matching_devices + ) + + # simulate driver store containing indv2.inf + from mfd_package_manager.data_structures import WindowsStoreDriver + + driver = WindowsStoreDriver( + published_name="oemX.inf", + original_name="indv2.inf", + provider_name="Intel", + class_name="Network adapters", + class_guid="{4d36e972-e325-11ce-bfc1-08002be10318}", + driver_version="1.2.3", + signer_name="Microsoft", + ) + manager.get_driver_files = mocker.Mock(return_value=[driver]) + + # call the method - should not raise + manager.install_rdma_drivers(str(controller_build_path)) + + # verify copy/installation helpers were called + manager.install_certificates_from_driver.assert_called_once() + manager.install_inf_driver_for_matching_devices.assert_called_once() + + def test_install_rdma_drivers_controller_path_missing(self, manager, mocker): + manager._controller_connection = mocker.create_autospec(LocalConnection) + controller_build_path = Path("C:/build") + manager._controller_connection.path.return_value = controller_build_path + + # simulate controller RDMA/Windows path missing + mocker.patch("mfd_package_manager.windows.Path.exists", return_value=False) + + # If INF is missing on remote, installation will not find driver in store + # and should raise PackageManagerModuleException + with pytest.raises(PackageManagerModuleException): + manager.install_rdma_drivers(str(controller_build_path)) + + def test_install_rdma_drivers_missing_windows_directory(self, manager, mocker): + manager._controller_connection = mocker.create_autospec(LocalConnection) + controller_build_path = Path("C:/build") + manager._controller_connection.path.return_value = controller_build_path + + # simulate controller RDMA/Windows path missing + mocker.patch("mfd_package_manager.windows.Path.exists", return_value=False) + + with pytest.raises(PackageManagerModuleException): + manager.install_rdma_drivers(str(controller_build_path)) + + def test_install_rdma_drivers_no_driver_in_store(self, manager, mocker): + manager._controller_connection = mocker.create_autospec(LocalConnection) + controller_build_path = Path("C:/build") + manager._controller_connection.path.return_value = controller_build_path + mocker.patch("mfd_package_manager.windows.Path.exists", return_value=True) + manager._connection.path.return_value = Path(r"C:\drivers_under_test\RDMA") + manager._connection.get_system_info.return_value.kernel_version = "19044" + + # return drivers not containing indv2.inf + manager.get_driver_files = mocker.Mock(return_value=[ + WindowsStoreDriver( + published_name="oem1.inf", + original_name="other.inf", + provider_name="Intel", + class_name="Network adapters", + class_guid="{4d36e972-e325-11ce-bfc1-08002be10318}", + driver_version="1.2.3", + signer_name="Microsoft", + ) + ]) + + with pytest.raises(PackageManagerModuleException): + manager.install_rdma_drivers(str(controller_build_path)) + + def test__get_rdma_folder_for_os_version_failure(self, manager): + with pytest.raises(PackageManagerModuleException): + manager._get_rdma_folder_for_os_version("99999") \ No newline at end of file