Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lisa/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
from .resize_partition import ResizePartition
from .rm import Rm
from .sar import Sar
from .smb import SmbClient, SmbServer
from .sockperf import Sockperf
from .ssh import Ssh
from .sshpass import Sshpass
Expand Down Expand Up @@ -281,6 +282,8 @@
"Sed",
"Service",
"ServiceInternal",
"SmbClient",
"SmbServer",
"Sockperf",
"Ssh",
"Sshpass",
Expand Down
1 change: 1 addition & 0 deletions lisa/tools/mkfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"mkfs",
[
"xfs",
"cifs",
"ext2",
"ext3",
"ext4",
Expand Down
196 changes: 196 additions & 0 deletions lisa/tools/smb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
# 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
service = self.node.tools[Service]
return service.check_service_exists(
self._smb_service
) and service.check_service_exists(self._nmb_service)
Comment on lines +63 to +67
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _check_exists method should verify that the samba service binaries exist on the system, not check if the service exists in systemd/service manager. Looking at similar tools in the codebase (like NFSServer), _check_exists typically checks if the installed packages exist rather than if services are registered. Consider checking for the installed packages or binary commands instead.

Suggested change
# Check if samba services exist
service = self.node.tools[Service]
return service.check_service_exists(
self._smb_service
) and service.check_service_exists(self._nmb_service)
# Verify that the Samba server binaries exist on the system.
# This follows the pattern used by other tools (e.g., NFSServer),
# where _check_exists validates the presence of required commands
# rather than relying on service-manager registration.
result = self.node.execute(
"command -v smbd && command -v nmbd",
sudo=True,
shell=True,
)
return result.exit_code == 0

Copilot uses AI. Check for mistakes.

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
self.node.tools[Chmod].chmod(share_path, "777", sudo=True)
Comment thread
SRIKKANTH marked this conversation as resolved.
# 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
read only = no
create mask = 0755
"""

# 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()
Comment thread
adityagesh marked this conversation as resolved.

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)
Comment on lines +108 to +131
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The start, stop, is_running, and remove_share methods lack docstrings. Public methods should have docstrings explaining their purpose and behavior.

Copilot uses AI. Check for mistakes.


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=0777",
"dir_mode=0777",
Comment thread
SRIKKANTH marked this conversation as resolved.
"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."""
Comment on lines +187 to +195
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unmount_share, is_mounted, and cleanup_mount_point methods lack docstrings. Public methods should have docstrings explaining their purpose, parameters, and behavior.

Suggested change
"""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."""
"""Unmount an SMB share from the specified mount point.
This method detaches the SMB share from the filesystem but does not
remove the mount point directory or erase any data on the underlying
storage.
:param mount_point: The path where the SMB share is currently mounted.
"""
self.node.tools[Mount].umount(point=mount_point, disk_name="", erase=False)
def is_mounted(self, mount_point: str) -> bool:
"""Check whether the given mount point is currently mounted.
:param mount_point: The filesystem path to check.
:return: ``True`` if the mount point exists and has an active mount,
otherwise ``False``.
"""
return self.node.tools[Mount].check_mount_point_exist(mount_point)
def cleanup_mount_point(self, mount_point: str) -> None:
"""Remove the directory used as the mount point.
This should typically be called after :meth:`unmount_share` to clean up
the now-unused mount point directory.
:param mount_point: The filesystem path of the mount point directory
to remove.
"""

Copilot uses AI. Check for mistakes.
self.node.tools[Rm].remove_directory(mount_point, sudo=True)
Loading