From aa54983f716754df477b3d4d106be2cfe825c49f Mon Sep 17 00:00:00 2001 From: SrikanthMyakam Date: Sat, 3 Jan 2026 01:07:37 +0530 Subject: [PATCH 1/3] New testcase 'verify_smb_linux' New testcase 'verify_smb_linux' A test to verify CIFS module and SMB share functionality between two Linux VMs. SMB server and client tools are added which are useful in validating samba file share with Linux OS and CIFS module in kernel. --- lisa/microsoft/testsuites/core/storage.py | 208 +++++++++++++++++++++- lisa/tools/__init__.py | 3 + lisa/tools/mkfs.py | 1 + lisa/tools/smb.py | 199 +++++++++++++++++++++ 4 files changed, 409 insertions(+), 2 deletions(-) create mode 100644 lisa/tools/smb.py diff --git a/lisa/microsoft/testsuites/core/storage.py b/lisa/microsoft/testsuites/core/storage.py index 279493b5f4..707aad77a2 100644 --- a/lisa/microsoft/testsuites/core/storage.py +++ b/lisa/microsoft/testsuites/core/storage.py @@ -32,7 +32,7 @@ SecurityProfileType, ) from lisa.node import Node -from lisa.operating_system import BSD, Fedora, Posix, Windows +from lisa.operating_system import BSD, Debian, Fedora, Posix, Suse, Ubuntu, Windows from lisa.schema import DiskControllerType, DiskOptionSettings, DiskType from lisa.sut_orchestrator import AZURE, HYPERV from lisa.sut_orchestrator.azure.features import ( @@ -42,7 +42,21 @@ FileShareProtocol, ) from lisa.sut_orchestrator.azure.tools import Waagent -from lisa.tools import Blkid, Cat, Dmesg, Echo, Lsblk, Mount, NFSClient, Swap, Sysctl +from lisa.tools import ( + Blkid, + Cat, + Dmesg, + Echo, + Ls, + Lsblk, + Mount, + NFSClient, + Rm, + SmbClient, + SmbServer, + Swap, + Sysctl, +) from lisa.tools.blkid import PartitionInfo from lisa.tools.journalctl import Journalctl from lisa.tools.kernel_config import KernelConfig @@ -659,6 +673,196 @@ def after_case(self, log: Logger, **kwargs: Any) -> None: except Exception: raise BadEnvironmentStateException + @TestCaseMetadata( + description=""" + A comprehensive test to verify CIFS module and SMB share functionality between + two Linux VMs. + This test case will + 1. Create 2 VMs in Azure + 2. Check if CONFIG_CIFS is enabled in KCONFIG + 3. Configure one VM as SMB server and create a share + 4. Mount the other VM to the SMB share + 5. Verify mount is successful + 6. Write a test file to the SMB share and read it back to verify IO + 7. Clean up the SMB share and unmount + 8. repeat steps 4-7 for SMB versions ["2.0", "2.1", "3.0", "3.1.1"] + """, + timeout=TIME_OUT, + requirement=simple_requirement( + min_count=2, + supported_os=[Debian, Ubuntu], + ), + priority=1, + ) + def verify_smb_linux( + self, log: Logger, node: Node, environment: Environment + ) -> None: + # Assign server and client roles to the 2 VMs + server_node = cast(RemoteNode, environment.nodes[0]) + client_node = cast(RemoteNode, environment.nodes[1]) + + # Check if CONFIG_CIFS is enabled in KCONFIG on both nodes + for role_node in (server_node, client_node): + if not role_node.tools[KernelConfig].is_enabled("CONFIG_CIFS"): + raise LisaException("CIFS module must be present for SMB testing") + # Install and setup SMB tools on both nodes + smb_server = server_node.tools[SmbServer] + smb_client = client_node.tools[SmbClient] + + # SMB versions to test + smb_versions = ["3.0", "3.1.1", "2.1", "2.0"] + + # Test configuration + share_name = "testshare" + share_path = f"/tmp/{share_name}" + mount_point = f"/mnt/{share_name}" + + failed_versions: List[str] = [] + try: + # Step 3: Configure SMB server and create a share + smb_server.create_share(share_name, share_path) + + # Step 8: Repeat for different SMB versions + for smb_version in smb_versions: + log.info(f"Testing SMB version {smb_version}") + try: + # Step 4: Mount the SMB share on client + smb_client.mount_share( + server_node.internal_address, + share_name, + mount_point, + smb_version, + ) + + # Step 5 & 6: Verify mount is successful + self._verify_smb_mount( + client_node, + mount_point, + server_node, + share_path, + log, + ) + except Exception as e: + log.info(f"SMB version {smb_version} failed: {e}") + failed_versions.append(smb_version) + finally: + # Step 7: Cleanup between version tests + try: + smb_client.unmount_share(mount_point) + except Exception: + pass + finally: + # Cleanup + self._cleanup_smb_test( + server_node, client_node, share_path, mount_point, log + ) + + if failed_versions: + raise LisaException( + f"SMB test failed for versions: {', '.join(failed_versions)}" + ) + + def _verify_smb_mount( + self, + client_node: RemoteNode, + mount_point: str, + server_node: RemoteNode, + share_path: str, + log: Logger, + ) -> None: + """ + Verify SMB mount is working by creating and reading a file from + both client and server. + """ + test_file = "smb_test.txt" + test_content = "SMB test content" + mount = client_node.tools[Mount] + + # Verify mount point exists and is mounted + mount_point_exists = mount.check_mount_point_exist(mount_point) + if not mount_point_exists: + raise LisaException( + f"Mount point {mount_point} does not exist or is not mounted" + ) + + # Create test file on mounted share from client + test_file_path = f"{mount_point}/{test_file}" + echo = client_node.tools[Echo] + echo.write_to_file( + test_content, + client_node.get_pure_path(test_file_path), + sudo=True, + ignore_error=False, + ) + + # Read and verify file content from client side + file_content_client = client_node.tools[Cat].read( + test_file_path, sudo=True, force_run=True + ) + + assert_that(file_content_client).described_as( + "SMB file content should match written content on client" + ).is_equal_to(test_content) + log.info(f"Successfully verified file content on client: '{test_content}'") + + # Read and verify file content from server side + # Verify content from server VM + server_file_path = f"{share_path}/{test_file}" + + # Check if file exists on server + if not server_node.tools[Ls].path_exists(server_file_path, sudo=True): + raise LisaException(f"Test file {server_file_path} not found on server VM") + + # Read file content directly from server VM + file_content_server = server_node.tools[Cat].read( + server_file_path, sudo=True, force_run=True + ) + + assert_that(file_content_server).described_as( + "SMB file content should match on server VM" + ).is_equal_to(test_content) + + log.info( + f"Successfully verified file content on both client and server: " + f"'{test_content}'" + ) + # Clean up test file from client (will also remove from server via SMB) + client_node.tools[Rm].remove_file(test_file_path, sudo=True) + + def _cleanup_smb_test( + self, + server_node: RemoteNode, + client_node: RemoteNode, + share_path: str, + mount_point: str, + log: Logger, + ) -> None: + """Clean up SMB test resources.""" + # Cleanup on client + try: + smb_client = client_node.tools[SmbClient] + if smb_client.is_mounted(mount_point): + smb_client.unmount_share(mount_point) + smb_client.cleanup_mount_point(mount_point) + except Exception as e: + log.info( + f"Failed to cleanup SMB client mount point {mount_point}: " + f"{e}. Continuing cleanup..." + ) + client_node.mark_dirty() + + # Cleanup on server + try: + smb_server = server_node.tools[SmbServer] + smb_server.stop() + smb_server.remove_share(share_path) + except Exception as e: + log.info( + f"Failed to remove share {share_path} from SMB server: " + f"{e}. Finishing cleanup..." + ) + server_node.mark_dirty() + @TestCaseMetadata( description=""" This test case will diff --git a/lisa/tools/__init__.py b/lisa/tools/__init__.py index 1bb424c3d6..b46696bed3 100644 --- a/lisa/tools/__init__.py +++ b/lisa/tools/__init__.py @@ -124,6 +124,7 @@ from .resize_partition import ResizePartition from .rm import Rm from .sar import Sar +from .smb import SmbClient, SmbServer from .sockperf import Sockperf from .ss import Ss from .ssh import Ssh @@ -292,6 +293,8 @@ "Sed", "Service", "ServiceInternal", + "SmbClient", + "SmbServer", "Sockperf", "Ss", "Ssh", diff --git a/lisa/tools/mkfs.py b/lisa/tools/mkfs.py index 6881cf00d4..8cbb05e9d0 100644 --- a/lisa/tools/mkfs.py +++ b/lisa/tools/mkfs.py @@ -12,6 +12,7 @@ "mkfs", [ "xfs", + "cifs", "ext2", "ext3", "ext4", diff --git a/lisa/tools/smb.py b/lisa/tools/smb.py new file mode 100644 index 0000000000..4cc1cc58c7 --- /dev/null +++ b/lisa/tools/smb.py @@ -0,0 +1,199 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +from pathlib import PurePosixPath +from typing import Any, List, Optional + +from lisa.executable import Tool +from lisa.operating_system import ( + Alpine, + CBLMariner, + CoreOs, + Debian, + Fedora, + Oracle, + Redhat, + Suse, + Ubuntu, +) +from lisa.tools import Chmod, Echo, Mkdir, Mount, Rm, Service +from lisa.tools.firewall import Firewall +from lisa.tools.mkfs import FileSystem +from lisa.util import UnsupportedDistroException + + +class SmbServer(Tool): + SMB_CONF_FILE = "/etc/samba/smb.conf" + + def _initialize(self, *args: Any, **kwargs: Any) -> None: + # Set service names based on distribution + if isinstance(self.node.os, (CBLMariner, Redhat, Fedora, Oracle, Suse)): + self._smb_service = "smb" + self._nmb_service = "nmb" + elif isinstance(self.node.os, Alpine): + self._smb_service = "samba" + self._nmb_service = "nmbd" + else: + # Default fallback + self._smb_service = "smbd" + self._nmb_service = "nmbd" + + @property + def command(self) -> str: + return "" + + @property + def can_install(self) -> bool: + return True + + def _install(self) -> bool: + # Install samba server and client utilities + if isinstance(self.node.os, Ubuntu): + self.node.os.install_packages(["samba", "samba-common-bin", "cifs-utils"]) + elif isinstance(self.node.os, Alpine): + self.node.os.install_packages(["samba", "samba-client"]) + elif isinstance(self.node.os, (Debian, CoreOs, Fedora, Oracle, Redhat, Suse)): + self.node.os.install_packages(["samba", "cifs-utils"]) + else: + raise UnsupportedDistroException(self.node.os) + + return self._check_exists() + + def _check_exists(self) -> bool: + # Check if samba services exist + return self.command_exists("smbd")[0] and self.command_exists("nmbd")[0] + + def create_share( + self, + share_name: str, + share_path: str, + workgroup: str = "WORKGROUP", + server_string: str = "LISA SMB Test Server", + ) -> None: + """Configure SMB server and create a share.""" + # Create share directory + self.node.tools[Mkdir].create_directory(share_path, sudo=True) + + # Set permissions for the share directory. Guest connections are mapped + # to the unprivileged samba "guest account" (typically `nobody`), which + # is neither the owner nor in the owning group of a freshly-created + # /tmp share. Without world-writable bits the guest cannot create files + # on the share and writes fail with EACCES at the server. + self.node.tools[Chmod].chmod(share_path, "0777", sudo=True) + # Create SMB configuration + smb_config = f""" +[global] + workgroup = {workgroup} + server string = {server_string} + security = user + map to guest = bad user + dns proxy = no + +[{share_name}] + path = {share_path} + browsable = yes + writable = yes + guest ok = yes + guest only = yes + read only = no + create mask = 0666 + directory mask = 0777 +""" + + # Write SMB configuration + self.node.tools[Echo].write_to_file( + smb_config, PurePosixPath(self.SMB_CONF_FILE), sudo=True + ) + + # Start SMB services + self.start() + + def start(self) -> None: + """Start SMB services.""" + service = self.node.tools[Service] + service.restart_service(self._smb_service) + service.restart_service(self._nmb_service) + # stop firewall to allow SMB traffic + self.node.tools[Firewall].stop() + + def stop(self) -> None: + """Stop SMB services.""" + service = self.node.tools[Service] + service.stop_service(self._smb_service) + service.stop_service(self._nmb_service) + + def is_running(self) -> bool: + """Check if SMB services are running.""" + service = self.node.tools[Service] + return service.is_service_running( + self._smb_service + ) and service.is_service_running(self._nmb_service) + + def remove_share(self, share_path: str) -> None: + """Remove a SMB share and its directory.""" + self.node.tools[Rm].remove_directory(share_path, sudo=True) + + +class SmbClient(Tool): + @property + def command(self) -> str: + return "mount.cifs" + + @property + def can_install(self) -> bool: + return True + + def _install(self) -> bool: + # Install client utilities + if isinstance( + self.node.os, + (Ubuntu, Debian, CBLMariner, CoreOs, Fedora, Oracle, Redhat, Suse, Alpine), + ): + self.node.os.install_packages(["cifs-utils"]) + else: + raise UnsupportedDistroException(self.node.os) + return self._check_exists() + + def mount_share( + self, + server_address: str, + share_name: str, + mount_point: str, + smb_version: str = "3.0", + options: Optional[List[str]] = None, + ) -> None: + """Mount SMB share on client node with specified SMB version.""" + # Create mount point + self.node.tools[Mkdir].create_directory(mount_point, sudo=True) + + # Build mount options + mount_options = [ + f"vers={smb_version}", + "file_mode=0755", + "dir_mode=0755", + "guest", + ] + + if options: + mount_options.extend(options) + + # Mount SMB share + self.node.tools[Mount].mount( + point=mount_point, + name=f"//{server_address}/{share_name}", + fs_type=FileSystem.cifs, + options=",".join(mount_options), + format_=False, + ) + + def unmount_share(self, mount_point: str) -> None: + """Unmount SMB share.""" + self.node.tools[Mount].umount(point=mount_point, disk_name="", erase=False) + + def is_mounted(self, mount_point: str) -> bool: + """Check if mount point exists and is mounted.""" + return self.node.tools[Mount].check_mount_point_exist(mount_point) + + def cleanup_mount_point(self, mount_point: str) -> None: + """Remove mount point directory.""" + self.node.tools[Rm].remove_directory(mount_point, sudo=True) From 7d03a8dcb697cea0316c6896ab0ee3ed0ef815e7 Mon Sep 17 00:00:00 2001 From: Srikanth Myakam <374767+SRIKKANTH@users.noreply.github.com> Date: Sat, 30 May 2026 11:24:11 +0530 Subject: [PATCH 2/3] flake issue fix --- lisa/microsoft/testsuites/core/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lisa/microsoft/testsuites/core/storage.py b/lisa/microsoft/testsuites/core/storage.py index 707aad77a2..11d8addbb1 100644 --- a/lisa/microsoft/testsuites/core/storage.py +++ b/lisa/microsoft/testsuites/core/storage.py @@ -32,7 +32,7 @@ SecurityProfileType, ) from lisa.node import Node -from lisa.operating_system import BSD, Debian, Fedora, Posix, Suse, Ubuntu, Windows +from lisa.operating_system import BSD, Debian, Fedora, Posix, Ubuntu, Windows from lisa.schema import DiskControllerType, DiskOptionSettings, DiskType from lisa.sut_orchestrator import AZURE, HYPERV from lisa.sut_orchestrator.azure.features import ( From a01601ef9cfd38bf742484e4ceaf8ccace3fb5cc Mon Sep 17 00:00:00 2001 From: Srikanth Myakam <374767+SRIKKANTH@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:33:07 +0530 Subject: [PATCH 3/3] comments addressed --- lisa/microsoft/testsuites/core/storage.py | 27 +++++++++++++++-------- lisa/tools/smb.py | 1 + 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lisa/microsoft/testsuites/core/storage.py b/lisa/microsoft/testsuites/core/storage.py index 11d8addbb1..a004e72e4d 100644 --- a/lisa/microsoft/testsuites/core/storage.py +++ b/lisa/microsoft/testsuites/core/storage.py @@ -32,7 +32,7 @@ SecurityProfileType, ) from lisa.node import Node -from lisa.operating_system import BSD, Debian, Fedora, Posix, Ubuntu, Windows +from lisa.operating_system import BSD, Debian, Fedora, Posix, Windows from lisa.schema import DiskControllerType, DiskOptionSettings, DiskType from lisa.sut_orchestrator import AZURE, HYPERV from lisa.sut_orchestrator.azure.features import ( @@ -690,7 +690,7 @@ def after_case(self, log: Logger, **kwargs: Any) -> None: timeout=TIME_OUT, requirement=simple_requirement( min_count=2, - supported_os=[Debian, Ubuntu], + supported_os=[Debian], ), priority=1, ) @@ -742,15 +742,24 @@ def verify_smb_linux( share_path, log, ) - except Exception as e: + except (LisaException, AssertionError) as e: log.info(f"SMB version {smb_version} failed: {e}") failed_versions.append(smb_version) finally: # Step 7: Cleanup between version tests try: smb_client.unmount_share(mount_point) - except Exception: - pass + except LisaException as e: + log.info( + f"Failed to unmount SMB share for version " + f"{smb_version}: {e}" + ) + client_node.mark_dirty() + except (LisaException, AssertionError): + log.info( + "SMB Linux test failed due to unexpected error. See logs for details." + ) + client_node.mark_dirty() finally: # Cleanup self._cleanup_smb_test( @@ -798,7 +807,7 @@ def _verify_smb_mount( # Read and verify file content from client side file_content_client = client_node.tools[Cat].read( test_file_path, sudo=True, force_run=True - ) + ).rstrip("\n") assert_that(file_content_client).described_as( "SMB file content should match written content on client" @@ -816,7 +825,7 @@ def _verify_smb_mount( # Read file content directly from server VM file_content_server = server_node.tools[Cat].read( server_file_path, sudo=True, force_run=True - ) + ).rstrip("\n") assert_that(file_content_server).described_as( "SMB file content should match on server VM" @@ -844,7 +853,7 @@ def _cleanup_smb_test( if smb_client.is_mounted(mount_point): smb_client.unmount_share(mount_point) smb_client.cleanup_mount_point(mount_point) - except Exception as e: + except LisaException as e: log.info( f"Failed to cleanup SMB client mount point {mount_point}: " f"{e}. Continuing cleanup..." @@ -856,7 +865,7 @@ def _cleanup_smb_test( smb_server = server_node.tools[SmbServer] smb_server.stop() smb_server.remove_share(share_path) - except Exception as e: + except LisaException as e: log.info( f"Failed to remove share {share_path} from SMB server: " f"{e}. Finishing cleanup..." diff --git a/lisa/tools/smb.py b/lisa/tools/smb.py index 4cc1cc58c7..b0134b9b84 100644 --- a/lisa/tools/smb.py +++ b/lisa/tools/smb.py @@ -115,6 +115,7 @@ def start(self) -> None: service.restart_service(self._nmb_service) # stop firewall to allow SMB traffic self.node.tools[Firewall].stop() + self.node.mark_dirty() def stop(self) -> None: """Stop SMB services."""