Skip to content

SCSI Debug

This section documents the SCSI debug device functionality, which provides virtual SCSI devices for testing.

sts.scsi_debug

SCSI debug device management.

This module provides functionality for managing SCSI debug devices: - Module loading/unloading - Device discovery - Failure injection

The SCSI debug driver (scsi_debug) creates virtual SCSI devices for testing: - Simulates SCSI disk behavior - Allows failure injection - Supports multipath configurations - Useful for testing without real hardware

ScsiDebugDevice dataclass

Bases: StorageDevice

SCSI debug device.

The scsi_debug module creates virtual SCSI devices that can: - Simulate various disk sizes and configurations - Inject failures on command - Test error handling and recovery - Verify multipath functionality

Parameters:

Name Type Description Default
path Path | str | None

Device path (optional, defaults to /dev/)

None
size int | None

Device size in bytes (optional, discovered from device)

None
model str | None

Device model (optional, defaults to 'SCSI Debug')

None
Example
device = ScsiDebugDevice()  # Discovers first available device
device = ScsiDebugDevice.create(size=1024 * 1024 * 1024)  # Creates new device
Source code in sts_libs/src/sts/scsi_debug.py
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
@dataclass
class ScsiDebugDevice(StorageDevice):
    """SCSI debug device.

    The scsi_debug module creates virtual SCSI devices that can:
    - Simulate various disk sizes and configurations
    - Inject failures on command
    - Test error handling and recovery
    - Verify multipath functionality

    Args:
        path: Device path (optional, defaults to /dev/<name>)
        size: Device size in bytes (optional, discovered from device)
        model: Device model (optional, defaults to 'SCSI Debug')

    Example:
        ```python
        device = ScsiDebugDevice()  # Discovers first available device
        device = ScsiDebugDevice.create(size=1024 * 1024 * 1024)  # Creates new device
        ```
    """

    # Optional parameters from parent classes
    path: Path | str | None = None
    size: int | None = None
    model: str | None = None

    # Internal fields
    module: ModuleManager = field(init=False, default_factory=ModuleManager)
    multipath: MultipathService = field(init=False, default_factory=MultipathService)

    # Sysfs path for module parameters
    SYSFS_PATH: ClassVar[Path] = Path('/sys/bus/pseudo/drivers/scsi_debug')

    def __post_init__(self) -> None:
        """Initialize device.

        Sets default model name if not provided.
        """
        # Set model if not provided
        if not self.model:
            self.model = 'SCSI Debug'

        # Initialize parent class
        super().__post_init__()

    @staticmethod
    def get_scsi_name_by_vendor(vendor: str) -> list[str] | None:
        """Get SCSI device names by vendor.

        Uses lsscsi to find devices with matching vendor string.
        For scsi_debug devices, vendor is typically 'Linux'.

        Args:
            vendor: Device vendor (e.g. 'Linux' for scsi_debug)

        Returns:
            List of device names (e.g. ['sda', 'sdb']) or None if not found
        """
        result = run('lsscsi -s')
        if result.failed:
            return None

        devices = []
        for line in result.stdout.splitlines():
            if vendor in line:
                # Parse line like: [0:0:0:0] disk Linux SCSI disk 1.0 /dev/sda 1024M
                parts = line.split()
                if len(parts) >= 6:
                    devices.append(parts[5].split('/')[-1])

        return devices or None

    @classmethod
    def create(cls, *, size: int | None = None, options: str | None = None) -> ScsiDebugDevice | None:
        """Create SCSI debug device.

        Creates a new virtual SCSI device by loading the scsi_debug module.
        Key module parameters:
        - dev_size_mb: Device size in megabytes
        - num_tgts: Number of targets (default: 1)
        - max_luns: Maximum LUNs per target (default: 1)

        Args:
            size: Device size in bytes (minimum 1MB)
            options: Additional module options (e.g. 'num_tgts=2 max_luns=4')

        Returns:
            ScsiDebugDevice instance or None if creation failed

        Example:
            ```python
            device = ScsiDebugDevice.create(size=1024 * 1024 * 1024)
            device.exists
            True
            ```
        """
        # Convert size to megabytes for module parameter
        if size:
            size_mb = size // (1024 * 1024)
            size_mb = max(size_mb, 1)  # Minimum 1MB
        else:
            size_mb = 8  # Default 8MB

        # Build module options
        module_options = f'dev_size_mb={size_mb}'
        if options:
            module_options = f'{module_options} {options}'

        # Load scsi_debug module with options
        module = ModuleManager()
        if not module.load('scsi_debug', module_options):
            return None

        # Get created device names
        devices = cls.get_devices()
        if not devices:
            return None

        # Return first device
        return cls(path=devices[0], size=size)

    def remove(self) -> bool:
        """Remove SCSI debug device.

        Cleanup process:
        1. Remove any multipath devices using this device
        2. Unload the scsi_debug module
        3. Device nodes are automatically removed

        Returns:
            True if successful, False otherwise

        Example:
            ```python
            device = ScsiDebugDevice.create(size=1024 * 1024 * 1024)
            device.remove()
            True
            ```
        """
        # Remove multipath devices if active
        if self.multipath.is_running():
            for _mpath in self.multipath.get_paths('Linux'):
                if not self.multipath.flush():
                    return False

        # Unload scsi_debug module
        return self.module.unload('scsi_debug')

    def set_param(self, param_name: str, value: str | int) -> bool:
        """Set device parameter.

        Sets module parameters through sysfs:
        - Parameters control device behavior
        - Changes take effect immediately
        - Some parameters are read-only

        Args:
            param_name: Parameter name (e.g. 'every_nth', 'opts')
            value: Parameter value

        Returns:
            True if successful, False otherwise

        Example:
            ```python
            device.set_param('every_nth', 1)
            True
            ```
        """
        param = self.SYSFS_PATH / param_name
        try:
            param.write_text(str(value))
        except OSError:
            logging.exception(f'Failed to set parameter: {param_name}={value}')
            return False

        return True

    def inject_failure(self, every_nth: int = 0, opts: int = 0) -> bool:
        """Inject device failures.

        Controls failure injection behavior:
        - every_nth: Frequency of failures
        - opts: Type and behavior of failures

        Args:
            every_nth: How often to inject failure (0 = disabled)
            opts: Failure options (bitmask):
                1 - "noisy": Log detailed error messages
                2 - "medium error": Report media errors
                4 - ignore "nth": Always inject failures
                8 - cause "nth" read/write to yield RECOVERED_ERROR
                16 - cause "nth" read/write to yield ABORTED_COMMAND

        Returns:
            True if successful, False otherwise

        Example:
            ```python
            # Inject media errors on every operation
            device.inject_failure(every_nth=1, opts=2)
            True
            ```
        """
        if not self.set_param('every_nth', every_nth):
            return False
        return self.set_param('opts', opts)

    @classmethod
    def get_devices(cls) -> list[str] | None:
        """Get SCSI debug devices.

        Finds devices in this order:
        1. Check if scsi_debug module is loaded
        2. Look for multipath devices using scsi_debug
        3. Look for direct SCSI devices from scsi_debug

        Returns:
            List of device names or None if no devices

        Example:
            ```python
            ScsiDebugDevice.get_devices()
            ['sda', 'sdb']
            ```
        """
        # Check if module is loaded
        if not ModuleInfo.from_name('scsi_debug'):
            return None

        # Check multipath devices first
        multipath = MultipathService()
        if multipath.is_running() and (devices := multipath.get_paths('Linux')):
            return [path['dev'] for path in devices]

        # Fall back to direct SCSI devices
        return cls.get_scsi_name_by_vendor('Linux')

__post_init__()

Initialize device.

Sets default model name if not provided.

Source code in sts_libs/src/sts/scsi_debug.py
64
65
66
67
68
69
70
71
72
73
74
def __post_init__(self) -> None:
    """Initialize device.

    Sets default model name if not provided.
    """
    # Set model if not provided
    if not self.model:
        self.model = 'SCSI Debug'

    # Initialize parent class
    super().__post_init__()

create(*, size=None, options=None) classmethod

Create SCSI debug device.

Creates a new virtual SCSI device by loading the scsi_debug module. Key module parameters: - dev_size_mb: Device size in megabytes - num_tgts: Number of targets (default: 1) - max_luns: Maximum LUNs per target (default: 1)

Parameters:

Name Type Description Default
size int | None

Device size in bytes (minimum 1MB)

None
options str | None

Additional module options (e.g. 'num_tgts=2 max_luns=4')

None

Returns:

Type Description
ScsiDebugDevice | None

ScsiDebugDevice instance or None if creation failed

Example
device = ScsiDebugDevice.create(size=1024 * 1024 * 1024)
device.exists
True
Source code in sts_libs/src/sts/scsi_debug.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
@classmethod
def create(cls, *, size: int | None = None, options: str | None = None) -> ScsiDebugDevice | None:
    """Create SCSI debug device.

    Creates a new virtual SCSI device by loading the scsi_debug module.
    Key module parameters:
    - dev_size_mb: Device size in megabytes
    - num_tgts: Number of targets (default: 1)
    - max_luns: Maximum LUNs per target (default: 1)

    Args:
        size: Device size in bytes (minimum 1MB)
        options: Additional module options (e.g. 'num_tgts=2 max_luns=4')

    Returns:
        ScsiDebugDevice instance or None if creation failed

    Example:
        ```python
        device = ScsiDebugDevice.create(size=1024 * 1024 * 1024)
        device.exists
        True
        ```
    """
    # Convert size to megabytes for module parameter
    if size:
        size_mb = size // (1024 * 1024)
        size_mb = max(size_mb, 1)  # Minimum 1MB
    else:
        size_mb = 8  # Default 8MB

    # Build module options
    module_options = f'dev_size_mb={size_mb}'
    if options:
        module_options = f'{module_options} {options}'

    # Load scsi_debug module with options
    module = ModuleManager()
    if not module.load('scsi_debug', module_options):
        return None

    # Get created device names
    devices = cls.get_devices()
    if not devices:
        return None

    # Return first device
    return cls(path=devices[0], size=size)

get_devices() classmethod

Get SCSI debug devices.

Finds devices in this order: 1. Check if scsi_debug module is loaded 2. Look for multipath devices using scsi_debug 3. Look for direct SCSI devices from scsi_debug

Returns:

Type Description
list[str] | None

List of device names or None if no devices

Example
ScsiDebugDevice.get_devices()
['sda', 'sdb']
Source code in sts_libs/src/sts/scsi_debug.py
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
@classmethod
def get_devices(cls) -> list[str] | None:
    """Get SCSI debug devices.

    Finds devices in this order:
    1. Check if scsi_debug module is loaded
    2. Look for multipath devices using scsi_debug
    3. Look for direct SCSI devices from scsi_debug

    Returns:
        List of device names or None if no devices

    Example:
        ```python
        ScsiDebugDevice.get_devices()
        ['sda', 'sdb']
        ```
    """
    # Check if module is loaded
    if not ModuleInfo.from_name('scsi_debug'):
        return None

    # Check multipath devices first
    multipath = MultipathService()
    if multipath.is_running() and (devices := multipath.get_paths('Linux')):
        return [path['dev'] for path in devices]

    # Fall back to direct SCSI devices
    return cls.get_scsi_name_by_vendor('Linux')

get_scsi_name_by_vendor(vendor) staticmethod

Get SCSI device names by vendor.

Uses lsscsi to find devices with matching vendor string. For scsi_debug devices, vendor is typically 'Linux'.

Parameters:

Name Type Description Default
vendor str

Device vendor (e.g. 'Linux' for scsi_debug)

required

Returns:

Type Description
list[str] | None

List of device names (e.g. ['sda', 'sdb']) or None if not found

Source code in sts_libs/src/sts/scsi_debug.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
@staticmethod
def get_scsi_name_by_vendor(vendor: str) -> list[str] | None:
    """Get SCSI device names by vendor.

    Uses lsscsi to find devices with matching vendor string.
    For scsi_debug devices, vendor is typically 'Linux'.

    Args:
        vendor: Device vendor (e.g. 'Linux' for scsi_debug)

    Returns:
        List of device names (e.g. ['sda', 'sdb']) or None if not found
    """
    result = run('lsscsi -s')
    if result.failed:
        return None

    devices = []
    for line in result.stdout.splitlines():
        if vendor in line:
            # Parse line like: [0:0:0:0] disk Linux SCSI disk 1.0 /dev/sda 1024M
            parts = line.split()
            if len(parts) >= 6:
                devices.append(parts[5].split('/')[-1])

    return devices or None

inject_failure(every_nth=0, opts=0)

Inject device failures.

Controls failure injection behavior: - every_nth: Frequency of failures - opts: Type and behavior of failures

Parameters:

Name Type Description Default
every_nth int

How often to inject failure (0 = disabled)

0
opts int

Failure options (bitmask): 1 - "noisy": Log detailed error messages 2 - "medium error": Report media errors 4 - ignore "nth": Always inject failures 8 - cause "nth" read/write to yield RECOVERED_ERROR 16 - cause "nth" read/write to yield ABORTED_COMMAND

0

Returns:

Type Description
bool

True if successful, False otherwise

Example
# Inject media errors on every operation
device.inject_failure(every_nth=1, opts=2)
True
Source code in sts_libs/src/sts/scsi_debug.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
def inject_failure(self, every_nth: int = 0, opts: int = 0) -> bool:
    """Inject device failures.

    Controls failure injection behavior:
    - every_nth: Frequency of failures
    - opts: Type and behavior of failures

    Args:
        every_nth: How often to inject failure (0 = disabled)
        opts: Failure options (bitmask):
            1 - "noisy": Log detailed error messages
            2 - "medium error": Report media errors
            4 - ignore "nth": Always inject failures
            8 - cause "nth" read/write to yield RECOVERED_ERROR
            16 - cause "nth" read/write to yield ABORTED_COMMAND

    Returns:
        True if successful, False otherwise

    Example:
        ```python
        # Inject media errors on every operation
        device.inject_failure(every_nth=1, opts=2)
        True
        ```
    """
    if not self.set_param('every_nth', every_nth):
        return False
    return self.set_param('opts', opts)

remove()

Remove SCSI debug device.

Cleanup process: 1. Remove any multipath devices using this device 2. Unload the scsi_debug module 3. Device nodes are automatically removed

Returns:

Type Description
bool

True if successful, False otherwise

Example
device = ScsiDebugDevice.create(size=1024 * 1024 * 1024)
device.remove()
True
Source code in sts_libs/src/sts/scsi_debug.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def remove(self) -> bool:
    """Remove SCSI debug device.

    Cleanup process:
    1. Remove any multipath devices using this device
    2. Unload the scsi_debug module
    3. Device nodes are automatically removed

    Returns:
        True if successful, False otherwise

    Example:
        ```python
        device = ScsiDebugDevice.create(size=1024 * 1024 * 1024)
        device.remove()
        True
        ```
    """
    # Remove multipath devices if active
    if self.multipath.is_running():
        for _mpath in self.multipath.get_paths('Linux'):
            if not self.multipath.flush():
                return False

    # Unload scsi_debug module
    return self.module.unload('scsi_debug')

set_param(param_name, value)

Set device parameter.

Sets module parameters through sysfs: - Parameters control device behavior - Changes take effect immediately - Some parameters are read-only

Parameters:

Name Type Description Default
param_name str

Parameter name (e.g. 'every_nth', 'opts')

required
value str | int

Parameter value

required

Returns:

Type Description
bool

True if successful, False otherwise

Example
device.set_param('every_nth', 1)
True
Source code in sts_libs/src/sts/scsi_debug.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def set_param(self, param_name: str, value: str | int) -> bool:
    """Set device parameter.

    Sets module parameters through sysfs:
    - Parameters control device behavior
    - Changes take effect immediately
    - Some parameters are read-only

    Args:
        param_name: Parameter name (e.g. 'every_nth', 'opts')
        value: Parameter value

    Returns:
        True if successful, False otherwise

    Example:
        ```python
        device.set_param('every_nth', 1)
        True
        ```
    """
    param = self.SYSFS_PATH / param_name
    try:
        param.write_text(str(value))
    except OSError:
        logging.exception(f'Failed to set parameter: {param_name}={value}')
        return False

    return True