Skip to content

Device Mapper

This section documents the Device Mapper functionality, which provides interfaces for working with Linux's device-mapper framework.

Device Mapper Core

sts.dm

Device Mapper device management.

This package provides functionality for managing Device Mapper devices.

CacheDevice dataclass

Bases: DmDevice

Cache target.

Caches data from a slow origin device on a fast cache device. Improves I/O performance by keeping frequently accessed data on SSD.

Args format: <#features> [features] <#policy_args> [policy_args]

Example
# Writeback cache with default policy
target = CacheDevice(0, 1000000, '253:0 253:1 253:2 512 1 writeback default 0')
str(target)
'0 1000000 cache 253:0 253:1 253:2 512 1 writeback default 0'
Source code in sts_libs/src/sts/dm/cache.py
 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
@dataclass
class CacheDevice(DmDevice):
    """Cache target.

    Caches data from a slow origin device on a fast cache device.
    Improves I/O performance by keeping frequently accessed data on SSD.

    Args format: <metadata> <cache> <origin> <block_size> <#features> [features] <policy> <#policy_args> [policy_args]

    Example:
        ```python
        # Writeback cache with default policy
        target = CacheDevice(0, 1000000, '253:0 253:1 253:2 512 1 writeback default 0')
        str(target)
        '0 1000000 cache 253:0 253:1 253:2 512 1 writeback default 0'
        ```
    """

    def __post_init__(self) -> None:
        """Set target type and initialize."""
        self.target_type = 'cache'
        super().__post_init__()

    @classmethod
    def from_block_devices(
        cls,
        metadata_device: BlockDevice,
        cache_device: BlockDevice,
        origin_device: BlockDevice,
        block_size_sectors: int = 512,
        policy: str = 'default',
        policy_args: dict[str, str] | None = None,
        start: int = 0,
        size_sectors: int | None = None,
        *,
        writethrough: bool = False,
        passthrough: bool = False,
        metadata2: bool = False,
        no_discard_passdown: bool = False,
    ) -> CacheDevice:
        """Create CacheDevice from BlockDevices.

        Args:
            metadata_device: Device for storing cache metadata
            cache_device: Fast device for cached data (e.g., SSD)
            origin_device: Slow device with original data (e.g., HDD)
            block_size_sectors: Cache block size in sectors (default: 512 = 256KB)
                Must be between 64 (32KB) and 2097152 (1GB), multiple of 64
            writethrough: Use writethrough mode (writes go to both devices)
            passthrough: Use passthrough mode (all I/O to origin)
            metadata2: Use version 2 metadata format
            no_discard_passdown: Don't pass discards to origin
            policy: Cache policy name (default: 'default')
            policy_args: Policy-specific key/value arguments
            start: Start sector in virtual device (default: 0)
            size_sectors: Size in sectors (default: origin device size)

        Returns:
            CacheDevice instance

        Example:
            ```python
            metadata = BlockDevice('/dev/sdb1')  # Small metadata device
            cache = BlockDevice('/dev/nvme0n1')  # Fast SSD cache
            origin = BlockDevice('/dev/sda')  # Slow HDD origin

            cache_dev = CacheDevice.from_block_devices(
                metadata_device=metadata,
                cache_device=cache,
                origin_device=origin,
                block_size_sectors=1024,  # 512KB blocks
                policy='smq',
            )
            ```
        """
        if size_sectors is None and origin_device.size is not None:
            size_sectors = origin_device.size // origin_device.sector_size

        metadata_id = cls._get_device_identifier(metadata_device)
        cache_id = cls._get_device_identifier(cache_device)
        origin_id = cls._get_device_identifier(origin_device)

        # Build feature list
        features: list[str] = []
        if writethrough:
            features.append('writethrough')
        if passthrough:
            features.append('passthrough')
        if metadata2:
            features.append('metadata2')
        if no_discard_passdown:
            features.append('no_discard_passdown')

        # Build policy args list
        policy_args_list: list[str] = []
        if policy_args:
            for key, value in policy_args.items():
                policy_args_list.extend([key, str(value)])

        # Build args: <metadata> <cache> <origin> <block_size> <#features> [features] <policy> <#policy_args> [args]
        args_parts = [
            metadata_id,
            cache_id,
            origin_id,
            str(block_size_sectors),
            str(len(features)),
        ]
        args_parts.extend(features)
        args_parts.append(policy)
        args_parts.append(str(len(policy_args_list)))
        args_parts.extend(policy_args_list)

        args = ' '.join(args_parts)
        if size_sectors is None:
            raise ValueError('size_sectors must be provided or origin_device.size must be available')
        return cls(start=start, size_sectors=size_sectors, args=args)

    @staticmethod
    def parse_status(status: str) -> dict[str, str | int]:
        """Parse cache device status string.

        Raw status format (from dmsetup status):
            <start> <size> cache <metadata_block_size> <used_meta>/<total_meta>
            <cache_block_size> <used_cache>/<total_cache> <read_hits> <read_misses>
            <write_hits> <write_misses> <demotions> <promotions> <dirty>
            <#features> [features] <#core_args> [core_args] <policy>
            <#policy_args> [policy_args] <mode> <needs_check>

        Example:
            0 524288 cache 8 13/65536 512 0/1024 0 20 0 0 0 0 0 1 writeback 2 migration_threshold 2048 smq 0 rw -

        Args:
            status: Raw status string from dmsetup status

        Returns:
            Dictionary with parsed status fields
        """
        parts = status.split()
        result: dict[str, str | int] = {}

        # Status includes table header: <start> <size> cache <actual status...>
        # Skip first 3 fields to get to actual cache status
        if len(parts) >= 14 and parts[2] == 'cache':
            offset = 3  # Skip: start, size, "cache"
            result['metadata_block_size'] = int(parts[offset])
            if '/' in parts[offset + 1]:
                used, total = parts[offset + 1].split('/')
                result['used_metadata_blocks'] = int(used)
                result['total_metadata_blocks'] = int(total)
            result['cache_block_size'] = int(parts[offset + 2])
            if '/' in parts[offset + 3]:
                used, total = parts[offset + 3].split('/')
                result['used_cache_blocks'] = int(used)
                result['total_cache_blocks'] = int(total)
            result['read_hits'] = int(parts[offset + 4])
            result['read_misses'] = int(parts[offset + 5])
            result['write_hits'] = int(parts[offset + 6])
            result['write_misses'] = int(parts[offset + 7])
            result['demotions'] = int(parts[offset + 8])
            result['promotions'] = int(parts[offset + 9])
            result['dirty'] = int(parts[offset + 10])

        return result

__post_init__()

Set target type and initialize.

Source code in sts_libs/src/sts/dm/cache.py
58
59
60
61
def __post_init__(self) -> None:
    """Set target type and initialize."""
    self.target_type = 'cache'
    super().__post_init__()

from_block_devices(metadata_device, cache_device, origin_device, block_size_sectors=512, policy='default', policy_args=None, start=0, size_sectors=None, *, writethrough=False, passthrough=False, metadata2=False, no_discard_passdown=False) classmethod

Create CacheDevice from BlockDevices.

Parameters:

Name Type Description Default
metadata_device BlockDevice

Device for storing cache metadata

required
cache_device BlockDevice

Fast device for cached data (e.g., SSD)

required
origin_device BlockDevice

Slow device with original data (e.g., HDD)

required
block_size_sectors int

Cache block size in sectors (default: 512 = 256KB) Must be between 64 (32KB) and 2097152 (1GB), multiple of 64

512
writethrough bool

Use writethrough mode (writes go to both devices)

False
passthrough bool

Use passthrough mode (all I/O to origin)

False
metadata2 bool

Use version 2 metadata format

False
no_discard_passdown bool

Don't pass discards to origin

False
policy str

Cache policy name (default: 'default')

'default'
policy_args dict[str, str] | None

Policy-specific key/value arguments

None
start int

Start sector in virtual device (default: 0)

0
size_sectors int | None

Size in sectors (default: origin device size)

None

Returns:

Type Description
CacheDevice

CacheDevice instance

Example
metadata = BlockDevice('/dev/sdb1')  # Small metadata device
cache = BlockDevice('/dev/nvme0n1')  # Fast SSD cache
origin = BlockDevice('/dev/sda')  # Slow HDD origin

cache_dev = CacheDevice.from_block_devices(
    metadata_device=metadata,
    cache_device=cache,
    origin_device=origin,
    block_size_sectors=1024,  # 512KB blocks
    policy='smq',
)
Source code in sts_libs/src/sts/dm/cache.py
 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
@classmethod
def from_block_devices(
    cls,
    metadata_device: BlockDevice,
    cache_device: BlockDevice,
    origin_device: BlockDevice,
    block_size_sectors: int = 512,
    policy: str = 'default',
    policy_args: dict[str, str] | None = None,
    start: int = 0,
    size_sectors: int | None = None,
    *,
    writethrough: bool = False,
    passthrough: bool = False,
    metadata2: bool = False,
    no_discard_passdown: bool = False,
) -> CacheDevice:
    """Create CacheDevice from BlockDevices.

    Args:
        metadata_device: Device for storing cache metadata
        cache_device: Fast device for cached data (e.g., SSD)
        origin_device: Slow device with original data (e.g., HDD)
        block_size_sectors: Cache block size in sectors (default: 512 = 256KB)
            Must be between 64 (32KB) and 2097152 (1GB), multiple of 64
        writethrough: Use writethrough mode (writes go to both devices)
        passthrough: Use passthrough mode (all I/O to origin)
        metadata2: Use version 2 metadata format
        no_discard_passdown: Don't pass discards to origin
        policy: Cache policy name (default: 'default')
        policy_args: Policy-specific key/value arguments
        start: Start sector in virtual device (default: 0)
        size_sectors: Size in sectors (default: origin device size)

    Returns:
        CacheDevice instance

    Example:
        ```python
        metadata = BlockDevice('/dev/sdb1')  # Small metadata device
        cache = BlockDevice('/dev/nvme0n1')  # Fast SSD cache
        origin = BlockDevice('/dev/sda')  # Slow HDD origin

        cache_dev = CacheDevice.from_block_devices(
            metadata_device=metadata,
            cache_device=cache,
            origin_device=origin,
            block_size_sectors=1024,  # 512KB blocks
            policy='smq',
        )
        ```
    """
    if size_sectors is None and origin_device.size is not None:
        size_sectors = origin_device.size // origin_device.sector_size

    metadata_id = cls._get_device_identifier(metadata_device)
    cache_id = cls._get_device_identifier(cache_device)
    origin_id = cls._get_device_identifier(origin_device)

    # Build feature list
    features: list[str] = []
    if writethrough:
        features.append('writethrough')
    if passthrough:
        features.append('passthrough')
    if metadata2:
        features.append('metadata2')
    if no_discard_passdown:
        features.append('no_discard_passdown')

    # Build policy args list
    policy_args_list: list[str] = []
    if policy_args:
        for key, value in policy_args.items():
            policy_args_list.extend([key, str(value)])

    # Build args: <metadata> <cache> <origin> <block_size> <#features> [features] <policy> <#policy_args> [args]
    args_parts = [
        metadata_id,
        cache_id,
        origin_id,
        str(block_size_sectors),
        str(len(features)),
    ]
    args_parts.extend(features)
    args_parts.append(policy)
    args_parts.append(str(len(policy_args_list)))
    args_parts.extend(policy_args_list)

    args = ' '.join(args_parts)
    if size_sectors is None:
        raise ValueError('size_sectors must be provided or origin_device.size must be available')
    return cls(start=start, size_sectors=size_sectors, args=args)

parse_status(status) staticmethod

Parse cache device status string.

Raw status format (from dmsetup status): cache / / <#features> [features] <#core_args> [core_args] <#policy_args> [policy_args]

Example

0 524288 cache 8 13/65536 512 0/1024 0 20 0 0 0 0 0 1 writeback 2 migration_threshold 2048 smq 0 rw -

Parameters:

Name Type Description Default
status str

Raw status string from dmsetup status

required

Returns:

Type Description
dict[str, str | int]

Dictionary with parsed status fields

Source code in sts_libs/src/sts/dm/cache.py
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
@staticmethod
def parse_status(status: str) -> dict[str, str | int]:
    """Parse cache device status string.

    Raw status format (from dmsetup status):
        <start> <size> cache <metadata_block_size> <used_meta>/<total_meta>
        <cache_block_size> <used_cache>/<total_cache> <read_hits> <read_misses>
        <write_hits> <write_misses> <demotions> <promotions> <dirty>
        <#features> [features] <#core_args> [core_args] <policy>
        <#policy_args> [policy_args] <mode> <needs_check>

    Example:
        0 524288 cache 8 13/65536 512 0/1024 0 20 0 0 0 0 0 1 writeback 2 migration_threshold 2048 smq 0 rw -

    Args:
        status: Raw status string from dmsetup status

    Returns:
        Dictionary with parsed status fields
    """
    parts = status.split()
    result: dict[str, str | int] = {}

    # Status includes table header: <start> <size> cache <actual status...>
    # Skip first 3 fields to get to actual cache status
    if len(parts) >= 14 and parts[2] == 'cache':
        offset = 3  # Skip: start, size, "cache"
        result['metadata_block_size'] = int(parts[offset])
        if '/' in parts[offset + 1]:
            used, total = parts[offset + 1].split('/')
            result['used_metadata_blocks'] = int(used)
            result['total_metadata_blocks'] = int(total)
        result['cache_block_size'] = int(parts[offset + 2])
        if '/' in parts[offset + 3]:
            used, total = parts[offset + 3].split('/')
            result['used_cache_blocks'] = int(used)
            result['total_cache_blocks'] = int(total)
        result['read_hits'] = int(parts[offset + 4])
        result['read_misses'] = int(parts[offset + 5])
        result['write_hits'] = int(parts[offset + 6])
        result['write_misses'] = int(parts[offset + 7])
        result['demotions'] = int(parts[offset + 8])
        result['promotions'] = int(parts[offset + 9])
        result['dirty'] = int(parts[offset + 10])

    return result

DelayDevice dataclass

Bases: DmDevice

Delay target.

Delays reads and/or writes and/or flushes and optionally maps them to different devices. Useful for testing how applications handle slow devices.

Table line has to either have 3, 6 or 9 arguments:

  • 3 args: Apply offset and delay to read, write and flush operations on device

  • 6 args: Apply offset and delay to device for reads, also apply write_offset and write_delay to write and flush operations on optionally different write_device

  • 9 args: Same as 6 arguments plus define flush_offset and flush_delay explicitly on/with optionally different flush_device/flush_offset

Offsets are specified in sectors. Delays are specified in milliseconds.

Attributes:

Name Type Description
read_device str | None

Device for read operations (parsed from args)

read_offset int | None

Offset in read device sectors (parsed from args)

read_delay_ms int | None

Delay for read operations in milliseconds (parsed from args)

write_device str | None

Device for write operations (parsed from args, 6+ arg format)

write_offset int | None

Offset in write device sectors (parsed from args, 6+ arg format)

write_delay_ms int | None

Delay for write operations in milliseconds (parsed from args, 6+ arg format)

flush_device str | None

Device for flush operations (parsed from args, 9 arg format)

flush_offset int | None

Offset in flush device sectors (parsed from args, 9 arg format)

flush_delay_ms int | None

Delay for flush operations in milliseconds (parsed from args, 9 arg format)

arg_format str | None

Format indicator ('3', '6', or '9')

Example
# 3 args: 100ms delay for all operations
target = DelayTarget(0, 1000000, '8:16 0 100')
str(target)
'0 1000000 delay 8:16 0 100'
target.read_delay_ms
100

# 6 args: no read delay, 400ms write/flush delay
target = DelayTarget(0, 1000000, '8:16 0 0 8:32 0 400')
str(target)
'0 1000000 delay 8:16 0 0 8:32 0 400'
target.write_delay_ms
400

# 9 args: 50ms read, 100ms write, 333ms flush
target = DelayTarget(0, 1000000, '8:16 0 50 8:16 0 100 8:16 0 333')
str(target)
'0 1000000 delay 8:16 0 50 8:16 0 100 8:16 0 333'
target.flush_delay_ms
333
Source code in sts_libs/src/sts/dm/delay.py
 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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
@dataclass
class DelayDevice(DmDevice):
    """Delay target.

    Delays reads and/or writes and/or flushes and optionally maps them
    to different devices. Useful for testing how applications handle slow devices.

    Table line has to either have 3, 6 or 9 arguments:

    - 3 args: <device> <offset> <delay>
        Apply offset and delay to read, write and flush operations on device

    - 6 args: <device> <offset> <delay> <write_device> <write_offset> <write_delay>
        Apply offset and delay to device for reads, also apply write_offset and
        write_delay to write and flush operations on optionally different write_device

    - 9 args: <device> <offset> <delay> <write_device> <write_offset> <write_delay>
              <flush_device> <flush_offset> <flush_delay>
        Same as 6 arguments plus define flush_offset and flush_delay explicitly
        on/with optionally different flush_device/flush_offset

    Offsets are specified in sectors. Delays are specified in milliseconds.

    Attributes:
        read_device: Device for read operations (parsed from args)
        read_offset: Offset in read device sectors (parsed from args)
        read_delay_ms: Delay for read operations in milliseconds (parsed from args)
        write_device: Device for write operations (parsed from args, 6+ arg format)
        write_offset: Offset in write device sectors (parsed from args, 6+ arg format)
        write_delay_ms: Delay for write operations in milliseconds (parsed from args, 6+ arg format)
        flush_device: Device for flush operations (parsed from args, 9 arg format)
        flush_offset: Offset in flush device sectors (parsed from args, 9 arg format)
        flush_delay_ms: Delay for flush operations in milliseconds (parsed from args, 9 arg format)
        arg_format: Format indicator ('3', '6', or '9')

    Example:
        ```python
        # 3 args: 100ms delay for all operations
        target = DelayTarget(0, 1000000, '8:16 0 100')
        str(target)
        '0 1000000 delay 8:16 0 100'
        target.read_delay_ms
        100

        # 6 args: no read delay, 400ms write/flush delay
        target = DelayTarget(0, 1000000, '8:16 0 0 8:32 0 400')
        str(target)
        '0 1000000 delay 8:16 0 0 8:32 0 400'
        target.write_delay_ms
        400

        # 9 args: 50ms read, 100ms write, 333ms flush
        target = DelayTarget(0, 1000000, '8:16 0 50 8:16 0 100 8:16 0 333')
        str(target)
        '0 1000000 delay 8:16 0 50 8:16 0 100 8:16 0 333'
        target.flush_delay_ms
        333
        ```
    """

    # Parsed attributes (populated by refresh)
    read_device: str | None = field(init=False, default=None)
    read_offset: int | None = field(init=False, default=None)
    read_delay_ms: int | None = field(init=False, default=None)
    write_device: str | None = field(init=False, default=None)
    write_offset: int | None = field(init=False, default=None)
    write_delay_ms: int | None = field(init=False, default=None)
    flush_device: str | None = field(init=False, default=None)
    flush_offset: int | None = field(init=False, default=None)
    flush_delay_ms: int | None = field(init=False, default=None)
    arg_format: str | None = field(init=False, default=None)

    def __post_init__(self) -> None:
        """Set target type and initialize."""
        self.target_type = 'delay'
        super().__post_init__()

    @classmethod
    def from_block_device(
        cls,
        device: BlockDevice,
        delay_ms: int,
        start: int = 0,
        size_sectors: int | None = None,
        offset: int = 0,
    ) -> DelayDevice:
        """Create DelayDevice from BlockDevice (3 argument format).

        This applies the same delay to read, write, and flush operations.

        Args:
            device: Source block device
            delay_ms: Delay in milliseconds for all operations
            start: Start sector in virtual device (default: 0)
            size_sectors: Size in sectors (default: device size in sectors)
            offset: Offset in source device sectors (default: 0)

        Returns:
            DelayDevice instance

        Example:
            ```python
            device = BlockDevice('/dev/sdb')
            delay = DelayDevice.from_block_device(device, delay_ms=100, size_sectors=1000000)
            delay.create('my-delay')
            ```
        """
        if size_sectors is None and device.size is not None:
            size_sectors = device.size // device.sector_size

        device_id = cls._get_device_identifier(device)
        args = f'{device_id} {offset} {delay_ms}'

        if size_sectors is None:
            raise ValueError('size_sectors must be provided or device.size must be available')
        dev = cls(start=start, size_sectors=size_sectors, args=args)
        # Set attributes directly - we know the values
        dev.read_device = device_id
        dev.read_offset = offset
        dev.read_delay_ms = delay_ms
        dev.arg_format = '3'
        return dev

    @classmethod
    def from_block_devices_rw(
        cls,
        read_device: BlockDevice,
        read_offset: int,
        read_delay_ms: int,
        write_device: BlockDevice,
        write_offset: int,
        write_delay_ms: int,
        start: int = 0,
        size: int | None = None,
    ) -> DelayDevice:
        """Create DelayTarget with separate read and write/flush devices (6 argument format).

        This allows different devices, offsets, and delays for read operations
        versus write and flush operations.

        Args:
            read_device: Device for read operations
            read_offset: Offset in read device sectors
            read_delay_ms: Delay for read operations in milliseconds
            write_device: Device for write and flush operations
            write_offset: Offset in write device sectors
            write_delay_ms: Delay for write and flush operations in milliseconds
            start: Start sector in virtual device (default: 0)
            size: Size in sectors (default: read device size in sectors)

        Returns:
            DelayTarget instance

        Example:
            ```python
            read_dev = BlockDevice('/dev/sdb')
            write_dev = BlockDevice('/dev/sdc')
            target = DelayTarget.from_block_devices_rw(
                read_dev, read_offset=2048, read_delay_ms=0,
                write_dev, write_offset=4096, write_delay_ms=400,
                size=1000000
            )
            str(target)
            '0 1000000 delay 8:16 2048 0 8:32 4096 400'
            ```
        """
        if size is None and read_device.size is not None:
            size = read_device.size // read_device.sector_size

        read_device_id = cls._get_device_identifier(read_device)
        write_device_id = cls._get_device_identifier(write_device)

        args = f'{read_device_id} {read_offset} {read_delay_ms} {write_device_id} {write_offset} {write_delay_ms}'

        if size is None:
            raise ValueError('size must be provided or read_device.size must be available')
        target = cls(start=start, size_sectors=size, args=args)
        # Set attributes directly - we know the values
        target.read_device = read_device_id
        target.read_offset = read_offset
        target.read_delay_ms = read_delay_ms
        target.write_device = write_device_id
        target.write_offset = write_offset
        target.write_delay_ms = write_delay_ms
        target.arg_format = '6'
        return target

    @classmethod
    def from_block_devices_rwf(
        cls,
        read_device: BlockDevice,
        read_offset: int,
        read_delay_ms: int,
        write_device: BlockDevice,
        write_offset: int,
        write_delay_ms: int,
        flush_device: BlockDevice,
        flush_offset: int,
        flush_delay_ms: int,
        start: int = 0,
        size: int | None = None,
    ) -> DelayDevice:
        """Create DelayTarget with separate read, write, and flush devices (9 argument format).

        This allows different devices, offsets, and delays for read, write,
        and flush operations separately.

        Args:
            read_device: Device for read operations
            read_offset: Offset in read device sectors
            read_delay_ms: Delay for read operations in milliseconds
            write_device: Device for write operations
            write_offset: Offset in write device sectors
            write_delay_ms: Delay for write operations in milliseconds
            flush_device: Device for flush operations
            flush_offset: Offset in flush device sectors
            flush_delay_ms: Delay for flush operations in milliseconds
            start: Start sector in virtual device (default: 0)
            size: Size in sectors (default: read device size in sectors)

        Returns:
            DelayTarget instance

        Example:
            ```python
            device = BlockDevice('/dev/sdb')
            target = DelayTarget.from_block_devices_rwf(
                device, read_offset=0, read_delay_ms=50,
                device, write_offset=0, write_delay_ms=100,
                device, flush_offset=0, flush_delay_ms=333,
                size=1000000
            )
            str(target)
            '0 1000000 delay 8:16 0 50 8:16 0 100 8:16 0 333'
            ```
        """
        if size is None and read_device.size is not None:
            size = read_device.size // read_device.sector_size

        read_device_id = cls._get_device_identifier(read_device)
        write_device_id = cls._get_device_identifier(write_device)
        flush_device_id = cls._get_device_identifier(flush_device)

        args = (
            f'{read_device_id} {read_offset} {read_delay_ms} '
            f'{write_device_id} {write_offset} {write_delay_ms} '
            f'{flush_device_id} {flush_offset} {flush_delay_ms}'
        )

        if size is None:
            raise ValueError('size must be provided or read_device.size must be available')
        target = cls(start=start, size_sectors=size, args=args)
        # Set attributes directly - we know the values
        target.read_device = read_device_id
        target.read_offset = read_offset
        target.read_delay_ms = read_delay_ms
        target.write_device = write_device_id
        target.write_offset = write_offset
        target.write_delay_ms = write_delay_ms
        target.flush_device = flush_device_id
        target.flush_offset = flush_offset
        target.flush_delay_ms = flush_delay_ms
        target.arg_format = '9'
        return target

    @classmethod
    def create_positional(
        cls,
        device_path: str,
        offset: int,
        delay_ms: int,
        start: int = 0,
        size: int | None = None,
        write_device_path: str | None = None,
        write_offset: int | None = None,
        write_delay_ms: int | None = None,
        flush_device_path: str | None = None,
        flush_offset: int | None = None,
        flush_delay_ms: int | None = None,
    ) -> DelayDevice:
        """Create DelayTarget with positional arguments from device mapper documentation.

        Supports 3, 6, or 9 argument formats depending on which parameters are provided.

        Args:
            device_path: Read device path (e.g., '/dev/sdb' or '8:16')
            offset: Read offset in source device sectors
            delay_ms: Read delay in milliseconds
            start: Start sector in virtual device (default: 0)
            size: Size in sectors (required)
            write_device_path: Write device path (enables 6-arg format)
            write_offset: Write offset in sectors (required if write_device_path set)
            write_delay_ms: Write delay in milliseconds (required if write_device_path set)
            flush_device_path: Flush device path (enables 9-arg format)
            flush_offset: Flush offset in sectors (required if flush_device_path set)
            flush_delay_ms: Flush delay in milliseconds (required if flush_device_path set)

        Returns:
            DelayTarget instance

        Raises:
            ValueError: If size is not specified or if incomplete write/flush parameters

        Example:
            ```python
            # 3 argument format
            target = DelayTarget.create_positional('/dev/sdb', offset=0, delay_ms=100, size=2097152)
            str(target)
            '0 2097152 delay /dev/sdb 0 100'

            # 6 argument format
            target = DelayTarget.create_positional(
                '/dev/sdb',
                offset=2048,
                delay_ms=0,
                size=2097152,
                write_device_path='/dev/sdc',
                write_offset=4096,
                write_delay_ms=400,
            )
            str(target)
            '0 2097152 delay /dev/sdb 2048 0 /dev/sdc 4096 400'

            # 9 argument format
            target = DelayTarget.create_positional(
                '/dev/sdb',
                offset=0,
                delay_ms=50,
                size=2097152,
                write_device_path='/dev/sdb',
                write_offset=0,
                write_delay_ms=100,
                flush_device_path='/dev/sdb',
                flush_offset=0,
                flush_delay_ms=333,
            )
            str(target)
            '0 2097152 delay /dev/sdb 0 50 /dev/sdb 0 100 /dev/sdb 0 333'
            ```
        """
        if size is None:
            raise ValueError('Size must be specified for delay targets')

        # Build args based on provided parameters
        args = f'{device_path} {offset} {delay_ms}'
        arg_format = '3'

        # Check for 6-argument format
        if write_device_path is not None:
            if write_offset is None or write_delay_ms is None:
                raise ValueError('write_offset and write_delay_ms required when write_device_path is specified')
            args = f'{args} {write_device_path} {write_offset} {write_delay_ms}'
            arg_format = '6'

            # Check for 9-argument format
            if flush_device_path is not None:
                if flush_offset is None or flush_delay_ms is None:
                    raise ValueError('flush_offset and flush_delay_ms required when flush_device_path is specified')
                args = f'{args} {flush_device_path} {flush_offset} {flush_delay_ms}'
                arg_format = '9'
        elif flush_device_path is not None:
            raise ValueError('flush_device_path requires write_device_path to be specified first')

        target = cls(start=start, size_sectors=size, args=args)
        # Set attributes directly - we know the values
        target.read_device = device_path
        target.read_offset = offset
        target.read_delay_ms = delay_ms
        target.arg_format = arg_format

        if write_device_path is not None:
            target.write_device = write_device_path
            target.write_offset = write_offset
            target.write_delay_ms = write_delay_ms

        if flush_device_path is not None:
            target.flush_device = flush_device_path
            target.flush_offset = flush_offset
            target.flush_delay_ms = flush_delay_ms

        return target

    def refresh(self) -> None:
        """Parse args and update instance attributes.

        Extracts all delay parameters from the args string based on
        the number of arguments (3, 6, or 9) and updates instance attributes.

        Updates:
            - read_device, read_offset, read_delay_ms (always)
            - write_device, write_offset, write_delay_ms (6+ args)
            - flush_device, flush_offset, flush_delay_ms (9 args)
            - arg_format: '3', '6', or '9' indicating format used

        Example:
            ```python
            target = DelayTarget(0, 1000000, '8:16 0 50 8:16 0 100 8:16 0 333')
            target.read_delay_ms  # 50
            target.write_delay_ms  # 100
            target.flush_delay_ms  # 333
            target.arg_format  # '9'
            ```
        """
        parts = self.args.split()

        if len(parts) < 3:
            return

        # Parse read device params (always present)
        self.read_device = parts[0]
        self.read_offset = int(parts[1])
        self.read_delay_ms = int(parts[2])

        if len(parts) == 3:
            self.arg_format = '3'
        elif len(parts) >= 6:
            # Parse write device params
            self.write_device = parts[3]
            self.write_offset = int(parts[4])
            self.write_delay_ms = int(parts[5])

            if len(parts) == 6:
                self.arg_format = '6'
            elif len(parts) >= 9:
                # Parse flush device params
                self.flush_device = parts[6]
                self.flush_offset = int(parts[7])
                self.flush_delay_ms = int(parts[8])
                self.arg_format = '9'

    @classmethod
    def from_table_line(cls, table_line: str) -> DelayDevice | None:
        """Create DelayTarget from a dmsetup table line.

        Parses a full table line (including start, size, and type) and
        creates a DelayTarget instance. Useful for reconstructing targets
        from existing devices.

        Args:
            table_line: Full table line from 'dmsetup table' output

        Returns:
            DelayTarget instance or None if parsing fails

        Example:
            ```python
            line = '0 2097152 delay 8:16 0 100'
            target = DelayTarget.from_table_line(line)
            target.size  # 2097152
            ```
        """
        parts = table_line.strip().split(None, 3)
        if len(parts) < 4:
            logging.warning(f'Invalid table line: {table_line}')
            return None

        start = int(parts[0])
        size = int(parts[1])
        target_type = parts[2]
        args = parts[3]

        if target_type != 'delay':
            logging.warning(f'Not a delay target: {target_type}')
            return None

        target = cls(start=start, size_sectors=size, args=args)
        target.refresh()  # Parse args to populate attributes
        return target

__post_init__()

Set target type and initialize.

Source code in sts_libs/src/sts/dm/delay.py
120
121
122
123
def __post_init__(self) -> None:
    """Set target type and initialize."""
    self.target_type = 'delay'
    super().__post_init__()

create_positional(device_path, offset, delay_ms, start=0, size=None, write_device_path=None, write_offset=None, write_delay_ms=None, flush_device_path=None, flush_offset=None, flush_delay_ms=None) classmethod

Create DelayTarget with positional arguments from device mapper documentation.

Supports 3, 6, or 9 argument formats depending on which parameters are provided.

Parameters:

Name Type Description Default
device_path str

Read device path (e.g., '/dev/sdb' or '8:16')

required
offset int

Read offset in source device sectors

required
delay_ms int

Read delay in milliseconds

required
start int

Start sector in virtual device (default: 0)

0
size int | None

Size in sectors (required)

None
write_device_path str | None

Write device path (enables 6-arg format)

None
write_offset int | None

Write offset in sectors (required if write_device_path set)

None
write_delay_ms int | None

Write delay in milliseconds (required if write_device_path set)

None
flush_device_path str | None

Flush device path (enables 9-arg format)

None
flush_offset int | None

Flush offset in sectors (required if flush_device_path set)

None
flush_delay_ms int | None

Flush delay in milliseconds (required if flush_device_path set)

None

Returns:

Type Description
DelayDevice

DelayTarget instance

Raises:

Type Description
ValueError

If size is not specified or if incomplete write/flush parameters

Example
# 3 argument format
target = DelayTarget.create_positional('/dev/sdb', offset=0, delay_ms=100, size=2097152)
str(target)
'0 2097152 delay /dev/sdb 0 100'

# 6 argument format
target = DelayTarget.create_positional(
    '/dev/sdb',
    offset=2048,
    delay_ms=0,
    size=2097152,
    write_device_path='/dev/sdc',
    write_offset=4096,
    write_delay_ms=400,
)
str(target)
'0 2097152 delay /dev/sdb 2048 0 /dev/sdc 4096 400'

# 9 argument format
target = DelayTarget.create_positional(
    '/dev/sdb',
    offset=0,
    delay_ms=50,
    size=2097152,
    write_device_path='/dev/sdb',
    write_offset=0,
    write_delay_ms=100,
    flush_device_path='/dev/sdb',
    flush_offset=0,
    flush_delay_ms=333,
)
str(target)
'0 2097152 delay /dev/sdb 0 50 /dev/sdb 0 100 /dev/sdb 0 333'
Source code in sts_libs/src/sts/dm/delay.py
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
@classmethod
def create_positional(
    cls,
    device_path: str,
    offset: int,
    delay_ms: int,
    start: int = 0,
    size: int | None = None,
    write_device_path: str | None = None,
    write_offset: int | None = None,
    write_delay_ms: int | None = None,
    flush_device_path: str | None = None,
    flush_offset: int | None = None,
    flush_delay_ms: int | None = None,
) -> DelayDevice:
    """Create DelayTarget with positional arguments from device mapper documentation.

    Supports 3, 6, or 9 argument formats depending on which parameters are provided.

    Args:
        device_path: Read device path (e.g., '/dev/sdb' or '8:16')
        offset: Read offset in source device sectors
        delay_ms: Read delay in milliseconds
        start: Start sector in virtual device (default: 0)
        size: Size in sectors (required)
        write_device_path: Write device path (enables 6-arg format)
        write_offset: Write offset in sectors (required if write_device_path set)
        write_delay_ms: Write delay in milliseconds (required if write_device_path set)
        flush_device_path: Flush device path (enables 9-arg format)
        flush_offset: Flush offset in sectors (required if flush_device_path set)
        flush_delay_ms: Flush delay in milliseconds (required if flush_device_path set)

    Returns:
        DelayTarget instance

    Raises:
        ValueError: If size is not specified or if incomplete write/flush parameters

    Example:
        ```python
        # 3 argument format
        target = DelayTarget.create_positional('/dev/sdb', offset=0, delay_ms=100, size=2097152)
        str(target)
        '0 2097152 delay /dev/sdb 0 100'

        # 6 argument format
        target = DelayTarget.create_positional(
            '/dev/sdb',
            offset=2048,
            delay_ms=0,
            size=2097152,
            write_device_path='/dev/sdc',
            write_offset=4096,
            write_delay_ms=400,
        )
        str(target)
        '0 2097152 delay /dev/sdb 2048 0 /dev/sdc 4096 400'

        # 9 argument format
        target = DelayTarget.create_positional(
            '/dev/sdb',
            offset=0,
            delay_ms=50,
            size=2097152,
            write_device_path='/dev/sdb',
            write_offset=0,
            write_delay_ms=100,
            flush_device_path='/dev/sdb',
            flush_offset=0,
            flush_delay_ms=333,
        )
        str(target)
        '0 2097152 delay /dev/sdb 0 50 /dev/sdb 0 100 /dev/sdb 0 333'
        ```
    """
    if size is None:
        raise ValueError('Size must be specified for delay targets')

    # Build args based on provided parameters
    args = f'{device_path} {offset} {delay_ms}'
    arg_format = '3'

    # Check for 6-argument format
    if write_device_path is not None:
        if write_offset is None or write_delay_ms is None:
            raise ValueError('write_offset and write_delay_ms required when write_device_path is specified')
        args = f'{args} {write_device_path} {write_offset} {write_delay_ms}'
        arg_format = '6'

        # Check for 9-argument format
        if flush_device_path is not None:
            if flush_offset is None or flush_delay_ms is None:
                raise ValueError('flush_offset and flush_delay_ms required when flush_device_path is specified')
            args = f'{args} {flush_device_path} {flush_offset} {flush_delay_ms}'
            arg_format = '9'
    elif flush_device_path is not None:
        raise ValueError('flush_device_path requires write_device_path to be specified first')

    target = cls(start=start, size_sectors=size, args=args)
    # Set attributes directly - we know the values
    target.read_device = device_path
    target.read_offset = offset
    target.read_delay_ms = delay_ms
    target.arg_format = arg_format

    if write_device_path is not None:
        target.write_device = write_device_path
        target.write_offset = write_offset
        target.write_delay_ms = write_delay_ms

    if flush_device_path is not None:
        target.flush_device = flush_device_path
        target.flush_offset = flush_offset
        target.flush_delay_ms = flush_delay_ms

    return target

from_block_device(device, delay_ms, start=0, size_sectors=None, offset=0) classmethod

Create DelayDevice from BlockDevice (3 argument format).

This applies the same delay to read, write, and flush operations.

Parameters:

Name Type Description Default
device BlockDevice

Source block device

required
delay_ms int

Delay in milliseconds for all operations

required
start int

Start sector in virtual device (default: 0)

0
size_sectors int | None

Size in sectors (default: device size in sectors)

None
offset int

Offset in source device sectors (default: 0)

0

Returns:

Type Description
DelayDevice

DelayDevice instance

Example
device = BlockDevice('/dev/sdb')
delay = DelayDevice.from_block_device(device, delay_ms=100, size_sectors=1000000)
delay.create('my-delay')
Source code in sts_libs/src/sts/dm/delay.py
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
@classmethod
def from_block_device(
    cls,
    device: BlockDevice,
    delay_ms: int,
    start: int = 0,
    size_sectors: int | None = None,
    offset: int = 0,
) -> DelayDevice:
    """Create DelayDevice from BlockDevice (3 argument format).

    This applies the same delay to read, write, and flush operations.

    Args:
        device: Source block device
        delay_ms: Delay in milliseconds for all operations
        start: Start sector in virtual device (default: 0)
        size_sectors: Size in sectors (default: device size in sectors)
        offset: Offset in source device sectors (default: 0)

    Returns:
        DelayDevice instance

    Example:
        ```python
        device = BlockDevice('/dev/sdb')
        delay = DelayDevice.from_block_device(device, delay_ms=100, size_sectors=1000000)
        delay.create('my-delay')
        ```
    """
    if size_sectors is None and device.size is not None:
        size_sectors = device.size // device.sector_size

    device_id = cls._get_device_identifier(device)
    args = f'{device_id} {offset} {delay_ms}'

    if size_sectors is None:
        raise ValueError('size_sectors must be provided or device.size must be available')
    dev = cls(start=start, size_sectors=size_sectors, args=args)
    # Set attributes directly - we know the values
    dev.read_device = device_id
    dev.read_offset = offset
    dev.read_delay_ms = delay_ms
    dev.arg_format = '3'
    return dev

from_block_devices_rw(read_device, read_offset, read_delay_ms, write_device, write_offset, write_delay_ms, start=0, size=None) classmethod

Create DelayTarget with separate read and write/flush devices (6 argument format).

This allows different devices, offsets, and delays for read operations versus write and flush operations.

Parameters:

Name Type Description Default
read_device BlockDevice

Device for read operations

required
read_offset int

Offset in read device sectors

required
read_delay_ms int

Delay for read operations in milliseconds

required
write_device BlockDevice

Device for write and flush operations

required
write_offset int

Offset in write device sectors

required
write_delay_ms int

Delay for write and flush operations in milliseconds

required
start int

Start sector in virtual device (default: 0)

0
size int | None

Size in sectors (default: read device size in sectors)

None

Returns:

Type Description
DelayDevice

DelayTarget instance

Example
read_dev = BlockDevice('/dev/sdb')
write_dev = BlockDevice('/dev/sdc')
target = DelayTarget.from_block_devices_rw(
    read_dev, read_offset=2048, read_delay_ms=0,
    write_dev, write_offset=4096, write_delay_ms=400,
    size=1000000
)
str(target)
'0 1000000 delay 8:16 2048 0 8:32 4096 400'
Source code in sts_libs/src/sts/dm/delay.py
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
@classmethod
def from_block_devices_rw(
    cls,
    read_device: BlockDevice,
    read_offset: int,
    read_delay_ms: int,
    write_device: BlockDevice,
    write_offset: int,
    write_delay_ms: int,
    start: int = 0,
    size: int | None = None,
) -> DelayDevice:
    """Create DelayTarget with separate read and write/flush devices (6 argument format).

    This allows different devices, offsets, and delays for read operations
    versus write and flush operations.

    Args:
        read_device: Device for read operations
        read_offset: Offset in read device sectors
        read_delay_ms: Delay for read operations in milliseconds
        write_device: Device for write and flush operations
        write_offset: Offset in write device sectors
        write_delay_ms: Delay for write and flush operations in milliseconds
        start: Start sector in virtual device (default: 0)
        size: Size in sectors (default: read device size in sectors)

    Returns:
        DelayTarget instance

    Example:
        ```python
        read_dev = BlockDevice('/dev/sdb')
        write_dev = BlockDevice('/dev/sdc')
        target = DelayTarget.from_block_devices_rw(
            read_dev, read_offset=2048, read_delay_ms=0,
            write_dev, write_offset=4096, write_delay_ms=400,
            size=1000000
        )
        str(target)
        '0 1000000 delay 8:16 2048 0 8:32 4096 400'
        ```
    """
    if size is None and read_device.size is not None:
        size = read_device.size // read_device.sector_size

    read_device_id = cls._get_device_identifier(read_device)
    write_device_id = cls._get_device_identifier(write_device)

    args = f'{read_device_id} {read_offset} {read_delay_ms} {write_device_id} {write_offset} {write_delay_ms}'

    if size is None:
        raise ValueError('size must be provided or read_device.size must be available')
    target = cls(start=start, size_sectors=size, args=args)
    # Set attributes directly - we know the values
    target.read_device = read_device_id
    target.read_offset = read_offset
    target.read_delay_ms = read_delay_ms
    target.write_device = write_device_id
    target.write_offset = write_offset
    target.write_delay_ms = write_delay_ms
    target.arg_format = '6'
    return target

from_block_devices_rwf(read_device, read_offset, read_delay_ms, write_device, write_offset, write_delay_ms, flush_device, flush_offset, flush_delay_ms, start=0, size=None) classmethod

Create DelayTarget with separate read, write, and flush devices (9 argument format).

This allows different devices, offsets, and delays for read, write, and flush operations separately.

Parameters:

Name Type Description Default
read_device BlockDevice

Device for read operations

required
read_offset int

Offset in read device sectors

required
read_delay_ms int

Delay for read operations in milliseconds

required
write_device BlockDevice

Device for write operations

required
write_offset int

Offset in write device sectors

required
write_delay_ms int

Delay for write operations in milliseconds

required
flush_device BlockDevice

Device for flush operations

required
flush_offset int

Offset in flush device sectors

required
flush_delay_ms int

Delay for flush operations in milliseconds

required
start int

Start sector in virtual device (default: 0)

0
size int | None

Size in sectors (default: read device size in sectors)

None

Returns:

Type Description
DelayDevice

DelayTarget instance

Example
device = BlockDevice('/dev/sdb')
target = DelayTarget.from_block_devices_rwf(
    device, read_offset=0, read_delay_ms=50,
    device, write_offset=0, write_delay_ms=100,
    device, flush_offset=0, flush_delay_ms=333,
    size=1000000
)
str(target)
'0 1000000 delay 8:16 0 50 8:16 0 100 8:16 0 333'
Source code in sts_libs/src/sts/dm/delay.py
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
@classmethod
def from_block_devices_rwf(
    cls,
    read_device: BlockDevice,
    read_offset: int,
    read_delay_ms: int,
    write_device: BlockDevice,
    write_offset: int,
    write_delay_ms: int,
    flush_device: BlockDevice,
    flush_offset: int,
    flush_delay_ms: int,
    start: int = 0,
    size: int | None = None,
) -> DelayDevice:
    """Create DelayTarget with separate read, write, and flush devices (9 argument format).

    This allows different devices, offsets, and delays for read, write,
    and flush operations separately.

    Args:
        read_device: Device for read operations
        read_offset: Offset in read device sectors
        read_delay_ms: Delay for read operations in milliseconds
        write_device: Device for write operations
        write_offset: Offset in write device sectors
        write_delay_ms: Delay for write operations in milliseconds
        flush_device: Device for flush operations
        flush_offset: Offset in flush device sectors
        flush_delay_ms: Delay for flush operations in milliseconds
        start: Start sector in virtual device (default: 0)
        size: Size in sectors (default: read device size in sectors)

    Returns:
        DelayTarget instance

    Example:
        ```python
        device = BlockDevice('/dev/sdb')
        target = DelayTarget.from_block_devices_rwf(
            device, read_offset=0, read_delay_ms=50,
            device, write_offset=0, write_delay_ms=100,
            device, flush_offset=0, flush_delay_ms=333,
            size=1000000
        )
        str(target)
        '0 1000000 delay 8:16 0 50 8:16 0 100 8:16 0 333'
        ```
    """
    if size is None and read_device.size is not None:
        size = read_device.size // read_device.sector_size

    read_device_id = cls._get_device_identifier(read_device)
    write_device_id = cls._get_device_identifier(write_device)
    flush_device_id = cls._get_device_identifier(flush_device)

    args = (
        f'{read_device_id} {read_offset} {read_delay_ms} '
        f'{write_device_id} {write_offset} {write_delay_ms} '
        f'{flush_device_id} {flush_offset} {flush_delay_ms}'
    )

    if size is None:
        raise ValueError('size must be provided or read_device.size must be available')
    target = cls(start=start, size_sectors=size, args=args)
    # Set attributes directly - we know the values
    target.read_device = read_device_id
    target.read_offset = read_offset
    target.read_delay_ms = read_delay_ms
    target.write_device = write_device_id
    target.write_offset = write_offset
    target.write_delay_ms = write_delay_ms
    target.flush_device = flush_device_id
    target.flush_offset = flush_offset
    target.flush_delay_ms = flush_delay_ms
    target.arg_format = '9'
    return target

from_table_line(table_line) classmethod

Create DelayTarget from a dmsetup table line.

Parses a full table line (including start, size, and type) and creates a DelayTarget instance. Useful for reconstructing targets from existing devices.

Parameters:

Name Type Description Default
table_line str

Full table line from 'dmsetup table' output

required

Returns:

Type Description
DelayDevice | None

DelayTarget instance or None if parsing fails

Example
line = '0 2097152 delay 8:16 0 100'
target = DelayTarget.from_table_line(line)
target.size  # 2097152
Source code in sts_libs/src/sts/dm/delay.py
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
@classmethod
def from_table_line(cls, table_line: str) -> DelayDevice | None:
    """Create DelayTarget from a dmsetup table line.

    Parses a full table line (including start, size, and type) and
    creates a DelayTarget instance. Useful for reconstructing targets
    from existing devices.

    Args:
        table_line: Full table line from 'dmsetup table' output

    Returns:
        DelayTarget instance or None if parsing fails

    Example:
        ```python
        line = '0 2097152 delay 8:16 0 100'
        target = DelayTarget.from_table_line(line)
        target.size  # 2097152
        ```
    """
    parts = table_line.strip().split(None, 3)
    if len(parts) < 4:
        logging.warning(f'Invalid table line: {table_line}')
        return None

    start = int(parts[0])
    size = int(parts[1])
    target_type = parts[2]
    args = parts[3]

    if target_type != 'delay':
        logging.warning(f'Not a delay target: {target_type}')
        return None

    target = cls(start=start, size_sectors=size, args=args)
    target.refresh()  # Parse args to populate attributes
    return target

refresh()

Parse args and update instance attributes.

Extracts all delay parameters from the args string based on the number of arguments (3, 6, or 9) and updates instance attributes.

Updates
  • read_device, read_offset, read_delay_ms (always)
  • write_device, write_offset, write_delay_ms (6+ args)
  • flush_device, flush_offset, flush_delay_ms (9 args)
  • arg_format: '3', '6', or '9' indicating format used
Example
target = DelayTarget(0, 1000000, '8:16 0 50 8:16 0 100 8:16 0 333')
target.read_delay_ms  # 50
target.write_delay_ms  # 100
target.flush_delay_ms  # 333
target.arg_format  # '9'
Source code in sts_libs/src/sts/dm/delay.py
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
def refresh(self) -> None:
    """Parse args and update instance attributes.

    Extracts all delay parameters from the args string based on
    the number of arguments (3, 6, or 9) and updates instance attributes.

    Updates:
        - read_device, read_offset, read_delay_ms (always)
        - write_device, write_offset, write_delay_ms (6+ args)
        - flush_device, flush_offset, flush_delay_ms (9 args)
        - arg_format: '3', '6', or '9' indicating format used

    Example:
        ```python
        target = DelayTarget(0, 1000000, '8:16 0 50 8:16 0 100 8:16 0 333')
        target.read_delay_ms  # 50
        target.write_delay_ms  # 100
        target.flush_delay_ms  # 333
        target.arg_format  # '9'
        ```
    """
    parts = self.args.split()

    if len(parts) < 3:
        return

    # Parse read device params (always present)
    self.read_device = parts[0]
    self.read_offset = int(parts[1])
    self.read_delay_ms = int(parts[2])

    if len(parts) == 3:
        self.arg_format = '3'
    elif len(parts) >= 6:
        # Parse write device params
        self.write_device = parts[3]
        self.write_offset = int(parts[4])
        self.write_delay_ms = int(parts[5])

        if len(parts) == 6:
            self.arg_format = '6'
        elif len(parts) >= 9:
            # Parse flush device params
            self.flush_device = parts[6]
            self.flush_offset = int(parts[7])
            self.flush_delay_ms = int(parts[8])
            self.arg_format = '9'

DmDevice dataclass

Bases: BlockDevice

Base class for all Device Mapper devices.

A DM device can be in two states: 1. Configuration: has target config (start, size, args) but not yet created on system 2. Active: device exists on system, has path, dm_name, table, etc.

This class unifies target configuration and device management. Before calling create(), the device is just a configuration. After create(), it becomes a full block device with all BlockDevice functionality.

Parameters:

Name Type Description Default
start int

Start sector (where this target begins)

0
size int | None

Size in sectors (length of this target)

None
args str

Target-specific arguments (e.g. device paths, options)

''
dm_name str | None

Device Mapper name (set after creation or when loading existing device)

None
name str | None

Kernel device name like 'dm-0' (set after creation)

None
path Path | str | None

Device path like '/dev/dm-0' (set after creation)

None
Example
# Create configuration
device = LinearDevice.from_block_device(backing_dev, size=1000000)

# Create on system
device.create('my-linear')

# Now it's a full device
device.dm_device_path  # '/dev/mapper/my-linear'
device.suspend()
device.resume()
device.remove()
Source code in sts_libs/src/sts/dm/base.py
  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
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
@dataclass
class DmDevice(BlockDevice):
    """Base class for all Device Mapper devices.

    A DM device can be in two states:
    1. Configuration: has target config (start, size, args) but not yet created on system
    2. Active: device exists on system, has path, dm_name, table, etc.

    This class unifies target configuration and device management. Before calling
    create(), the device is just a configuration. After create(), it becomes a
    full block device with all BlockDevice functionality.

    Args:
        start: Start sector (where this target begins)
        size: Size in sectors (length of this target)
        args: Target-specific arguments (e.g. device paths, options)
        dm_name: Device Mapper name (set after creation or when loading existing device)
        name: Kernel device name like 'dm-0' (set after creation)
        path: Device path like '/dev/dm-0' (set after creation)

    Example:
        ```python
        # Create configuration
        device = LinearDevice.from_block_device(backing_dev, size=1000000)

        # Create on system
        device.create('my-linear')

        # Now it's a full device
        device.dm_device_path  # '/dev/mapper/my-linear'
        device.suspend()
        device.resume()
        device.remove()
        ```
    """

    # Target configuration (always available)
    start: int = 0
    size_sectors: int = 0  # Size in sectors (renamed to avoid conflict with BlockDevice.size)
    args: str = ''

    # Device Mapper name (set after creation or when loading existing device)
    dm_name: str | None = None

    # Target type - override in subclasses
    target_type: str = field(init=False, default='')

    # Internal state
    table: str | None = field(init=False, default=None, repr=False)
    is_created: bool = field(init=False, default=False, repr=False)

    # Class-level paths
    DM_PATH: ClassVar[Path] = Path('/sys/class/block')
    DM_DEV_PATH: ClassVar[Path] = Path('/dev/mapper')

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

        If dm_name or path is provided, assumes device exists and initializes
        BlockDevice properties. Otherwise, stays in configuration-only state.
        """
        # Check if this is an existing device (has dm_name or path)
        if self.dm_name or self.path:
            # Set path based on dm_name if not provided
            if not self.path and self.dm_name:
                self.path = f'/dev/mapper/{self.dm_name}'
            elif not self.path and self.name:
                self.path = f'/dev/{self.name}'

            # Initialize BlockDevice (queries system)
            super().__post_init__()

            # Get device mapper name if not provided
            if not self.dm_name and self.name:
                result = run(f'dmsetup info -c --noheadings -o name {self.name}')
                if result.succeeded:
                    self.dm_name = result.stdout.strip()

            # Load table from system
            if self.dm_name:
                result = run(f'dmsetup table {self.dm_name}')
                if result.succeeded:
                    self.table = result.stdout.strip()
                    self._parse_table()

            self.is_created = True

    @property
    def type(self) -> str:
        """Get target type.

        Returns:
            Target type string (e.g. 'linear', 'delay', 'vdo')
        """
        if self.target_type:
            return self.target_type
        # Fallback: Remove 'Device' suffix from class name and convert to lowercase
        return self.__class__.__name__.lower().removesuffix('device')

    def __str__(self) -> str:
        """Return target table entry.

        Format: <start> <size> <type> <args>
        Used in dmsetup table commands.
        """
        return f'{self.start} {self.size_sectors} {self.type} {self.args}'

    @property
    def device_path(self) -> Path:
        """Get path to device in sysfs.

        Returns:
            Path to device directory

        Raises:
            DeviceNotFoundError: If device does not exist
        """
        if not self.is_created or not self.name:
            msg = 'Device not created yet'
            raise DeviceNotFoundError(msg)

        path = self.DM_PATH / self.name
        if not path.exists():
            msg = f'Device {self.name} not found'
            raise DeviceNotFoundError(msg)
        return path

    @property
    def device_root_dir(self) -> str:
        """Get device mapper root directory.

        Returns:
            Device mapper root directory path (/dev/mapper)
        """
        return str(self.DM_DEV_PATH)

    @property
    def dm_device_path(self) -> str | None:
        """Get full device mapper device path.

        Returns:
            Full device path (e.g. '/dev/mapper/my-device') or None if not available
        """
        if not self.dm_name:
            return None
        return f'{self.device_root_dir}/{self.dm_name}'

    def create(
        self,
        dm_name: str,
        *,
        uuid: str | None = None,
        readonly: bool = False,
        notable: bool = False,
        readahead: str | int | None = None,
        addnodeoncreate: bool = False,
        addnodeonresume: bool = False,
    ) -> bool:
        """Create the device on the system.

        After successful creation, this device becomes a full BlockDevice
        with all properties populated from the system.

        Args:
            dm_name: Name for the device mapper device
            uuid: Optional UUID for the device (can be used in place of name in commands)
            readonly: Set the table being loaded as read-only
            notable: Create device without loading any table
            readahead: Read ahead size in sectors, or 'auto', 'none', or '+N' for minimum
            addnodeoncreate: Ensure /dev/mapper node exists after create
            addnodeonresume: Ensure /dev/mapper node exists after resume (default with udev)

        Returns:
            True if successful, False otherwise

        Example:
            ```python
            device = DelayDevice.from_block_device(backing, delay_ms=100)
            if device.create('my-delay', uuid='my-uuid-123'):
                print(f'Created: {device.dm_device_path}')
            ```
        """
        if self.is_created:
            logging.warning(f'Device {self.dm_name} already created')
            return True

        cmd = f'dmsetup create {dm_name}'

        if uuid:
            cmd += f' --uuid {uuid}'
        if readonly:
            cmd += ' --readonly'
        if addnodeoncreate:
            cmd += ' --addnodeoncreate'
        if addnodeonresume:
            cmd += ' --addnodeonresume'
        if readahead is not None:
            cmd += f' --readahead {readahead}'

        if notable:
            cmd += ' --notable'
        else:
            # Build table from this device's configuration
            table = str(self)
            cmd += f' --table "{table}"'
            logging.info(f'Creating device {dm_name} with table: {table}')

        result = run(cmd)

        if result.failed:
            logging.error(f'Failed to create device {dm_name}: {result.stderr}')
            return False

        logging.info(f'Successfully created device mapper device: {dm_name}')

        # Set dm_name and reinitialize as existing device
        self.dm_name = dm_name
        self.path = f'/dev/mapper/{dm_name}'

        # Initialize BlockDevice properties from system
        super().__post_init__()

        # Get kernel device name
        if not self.name:
            result = run(f'dmsetup info -c --noheadings -o name,blkdevname {dm_name}')
            if result.succeeded:
                parts = result.stdout.strip().split()
                if len(parts) >= 2:
                    self.name = parts[1]

        # Load table from system
        result = run(f'dmsetup table {dm_name}')
        if result.succeeded:
            self.table = result.stdout.strip()
            self._parse_table()

        self.is_created = True
        return True

    def _parse_table(self) -> None:
        """Parse table and update target configuration from system.

        Subclasses should override this to parse target-specific attributes.
        """
        if not self.table:
            return

        parts = self.table.strip().split(None, 3)
        if len(parts) >= 4:
            self.start = int(parts[0])
            self.size_sectors = int(parts[1])
            # parts[2] is the target type
            self.args = parts[3]

            # Call refresh if available (subclasses implement this for target-specific parsing)
            if hasattr(self, 'refresh') and callable(getattr(self, 'refresh', None)):
                self.refresh()

    def refresh(self) -> None:
        """Refresh device state from system.

        Re-reads the device table and status from the system.
        Subclasses should override to parse target-specific attributes.

        Returns:
            True if successful, False otherwise
        """
        if not self.is_created or not self.dm_name:
            return

        # Refresh table from system
        result = run(f'dmsetup table {self.dm_name}')
        if result.succeeded:
            self.table = result.stdout.strip()
            self._parse_table()

    def get_status(
        self,
        *,
        target_type: str | None = None,
        noflush: bool = False,
    ) -> str | None:
        """Get device status from dmsetup.

        Outputs status information for each of the device's targets.

        Args:
            target_type: Only display information for the specified target type
            noflush: Do not commit thin-pool metadata before reporting statistics

        Returns:
            Status string or None if not available
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return None

        cmd = f'dmsetup status {self.dm_name}'
        if target_type:
            cmd += f' --target {target_type}'
        if noflush:
            cmd += ' --noflush'

        result = run(cmd)
        if result.failed:
            logging.error(f'Failed to get status for {self.dm_name}')
            return None

        return result.stdout.strip()

    def suspend(
        self,
        *,
        nolockfs: bool = False,
        noflush: bool = False,
    ) -> bool:
        """Suspend device.

        Suspends I/O to the device. Any I/O that has already been mapped by the device
        but has not yet completed will be flushed. Any further I/O to that device will
        be postponed for as long as the device is suspended.

        Args:
            nolockfs: Do not attempt to synchronize filesystem (skip fsfreeze)
            noflush: Let outstanding I/O remain unflushed (requires target support)

        Returns:
            True if successful, False otherwise
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return False

        cmd = f'dmsetup suspend {self.dm_name}'
        if nolockfs:
            cmd += ' --nolockfs'
        if noflush:
            cmd += ' --noflush'

        result = run(cmd)
        if result.failed:
            logging.error('Failed to suspend device')
            return False
        return True

    def resume(
        self,
        *,
        addnodeoncreate: bool = False,
        addnodeonresume: bool = False,
        noflush: bool = False,
        nolockfs: bool = False,
        readahead: str | int | None = None,
    ) -> bool:
        """Resume device.

        Un-suspends a device. If an inactive table has been loaded, it becomes live.
        Postponed I/O then gets re-queued for processing.

        Args:
            addnodeoncreate: Ensure /dev/mapper node exists after create
            addnodeonresume: Ensure /dev/mapper node exists after resume (default with udev)
            noflush: Do not flush outstanding I/O
            nolockfs: Do not attempt to synchronize filesystem
            readahead: Read ahead size in sectors, or 'auto', 'none', or '+N' for minimum

        Returns:
            True if successful, False otherwise
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return False

        cmd = f'dmsetup resume {self.dm_name}'
        if addnodeoncreate:
            cmd += ' --addnodeoncreate'
        if addnodeonresume:
            cmd += ' --addnodeonresume'
        if noflush:
            cmd += ' --noflush'
        if nolockfs:
            cmd += ' --nolockfs'
        if readahead is not None:
            cmd += f' --readahead {readahead}'

        result = run(cmd)
        if result.failed:
            logging.error('Failed to resume device')
            return False
        return True

    def remove(
        self,
        *,
        force: bool = False,
        retry: bool = False,
        deferred: bool = False,
    ) -> bool:
        """Remove device.

        Removes the device mapping. The underlying devices are unaffected.
        Open devices cannot be removed, but --force will replace the table with
        one that fails all I/O. --deferred enables deferred removal when the device
        is in use.

        Args:
            force: Replace the table with one that fails all I/O
            retry: Retry removal for a few seconds if it fails (e.g., udev race)
            deferred: Enable deferred removal - device removed when last user closes it

        Returns:
            True if successful, False otherwise

        Note:
            Do NOT combine --force and --udevcookie as this may cause
            nondeterministic results.
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return False

        cmd = f'dmsetup remove {self.dm_name}'
        if force:
            cmd += ' --force'
        if retry:
            cmd += ' --retry'
        if deferred:
            cmd += ' --deferred'

        result = run(cmd)
        if result.failed:
            logging.error('Failed to remove device')
            return False

        self.is_created = False
        return True

    def clear(self) -> bool:
        """Clear the inactive table slot.

        Destroys the table in the inactive table slot for the device.
        This is useful when you want to abort a table load before resuming.

        Returns:
            True if successful, False otherwise
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return False

        result = run(f'dmsetup clear {self.dm_name}')
        if result.failed:
            logging.error(f'Failed to clear inactive table for {self.dm_name}')
            return False
        return True

    def deps(self, *, output_format: str = 'devno') -> list[str]:
        """Get device dependencies.

        Outputs a list of devices referenced by the live table for this device.

        Args:
            output_format: Output format for device names:
                - 'devno': Major and minor pair (default)
                - 'blkdevname': Block device name
                - 'devname': Map name for DM devices, blkdevname otherwise

        Returns:
            List of device identifiers (format depends on output_format)
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return []

        result = run(f'dmsetup deps -o {output_format} {self.dm_name}')
        if result.failed:
            logging.error(f'Failed to get dependencies for {self.dm_name}')
            return []

        # Parse output: "1 dependencies  : (major, minor) ..."
        output = result.stdout.strip()
        deps = []
        if ':' in output:
            deps_part = output.split(':', 1)[1].strip()
            # Extract device identifiers from the output
            if output_format == 'devno':
                # Format: (major, minor) or (major:minor)
                matches = re.findall(r'\((\d+[,:]\s*\d+)\)', deps_part)
                deps = [m.replace(' ', '').replace(',', ':') for m in matches]
            else:
                # Format: (device_name)
                matches = re.findall(r'\(([^)]+)\)', deps_part)
                deps = matches

        return deps

    def info(
        self,
        *,
        columns: bool = False,
        noheadings: bool = False,
        fields: str | None = None,
        separator: str | None = None,
        sort_fields: str | None = None,
        nameprefixes: bool = False,
    ) -> dict[str, str] | str | None:
        """Get device information.

        Outputs information about the device in various formats.

        Args:
            columns: Display output in columns rather than Field: Value lines
            noheadings: Suppress the headings line when using columnar output
            fields: Comma-separated list of fields to display
                   (name, major, minor, attr, open, segments, events, uuid)
            separator: Separator for columnar output
            sort_fields: Fields to sort by (prefix with '-' for reverse)
            nameprefixes: Add DM_ prefix plus field name to output

        Returns:
            If columns=False: Dictionary of field names to values
            If columns=True: Raw output string
            None if device not found or error
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return None

        cmd = f'dmsetup info {self.dm_name}'
        if columns:
            cmd += ' --columns'
            if noheadings:
                cmd += ' --noheadings'
            if fields:
                cmd += f' -o {fields}'
            if separator:
                cmd += f' --separator "{separator}"'
            if sort_fields:
                cmd += f' --sort {sort_fields}'
            if nameprefixes:
                cmd += ' --nameprefixes'

        result = run(cmd)
        if result.failed:
            logging.error(f'Failed to get info for {self.dm_name}')
            return None

        output = result.stdout.strip()

        if columns:
            return output

        # Parse Field: Value format into dictionary
        info_dict: dict[str, str] = {}
        for line in output.splitlines():
            if ':' in line:
                key, value = line.split(':', 1)
                info_dict[key.strip()] = value.strip()

        return info_dict

    def message(self, sector: int, message: str) -> bool:
        """Send message to device target.

        Send a message to the target at the specified sector.
        Use sector 0 if the sector is not relevant to the target.

        Args:
            sector: Sector number (use 0 if not needed)
            message: Message string to send to the target

        Returns:
            True if successful, False otherwise
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return False

        result = run(f'dmsetup message {self.dm_name} {sector} {message}')
        if result.failed:
            logging.error(f'Failed to send message to {self.dm_name}: {result.stderr}')
            return False
        return True

    def rename(self, new_name: str) -> bool:
        """Rename the device.

        Args:
            new_name: New name for the device

        Returns:
            True if successful, False otherwise
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return False

        result = run(f'dmsetup rename {self.dm_name} {new_name}')
        if result.failed:
            logging.error(f'Failed to rename {self.dm_name} to {new_name}')
            return False

        # Update internal state
        old_name = self.dm_name
        self.dm_name = new_name
        self.path = f'/dev/mapper/{new_name}'
        logging.info(f'Successfully renamed device from {old_name} to {new_name}')
        return True

    def set_uuid(self, uuid: str) -> bool:
        """Set the UUID of a device that was created without one.

        After a UUID has been set it cannot be changed.

        Args:
            uuid: UUID to set for the device

        Returns:
            True if successful, False otherwise
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return False

        result = run(f'dmsetup rename {self.dm_name} --setuuid {uuid}')
        if result.failed:
            logging.error(f'Failed to set UUID for {self.dm_name}: {result.stderr}')
            return False

        logging.info(f'Successfully set UUID {uuid} for device {self.dm_name}')
        return True

    def reload_table(self, new_table: str) -> bool:
        """Reload device table.

        Suspends the device, loads a new table, and resumes.

        Args:
            new_table: New table string to load

        Returns:
            True if successful, False otherwise
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return False

        # Suspend device
        if not self.suspend():
            return False

        # Load new table
        result = run(f'dmsetup load {self.dm_name} "{new_table}"')
        if result.failed:
            logging.error(f'Failed to load new table: {result.stderr}')
            self.resume()
            return False

        # Resume with new table
        if not self.resume():
            return False

        # Refresh from system
        self.refresh()
        logging.info(f'Successfully reloaded table for {self.dm_name}')
        return True

    def load(
        self,
        table: str | None = None,
        table_file: str | None = None,
    ) -> bool:
        """Load table into the inactive table slot.

        Loads a table into the inactive table slot for the device.
        Use resume() to make the inactive table live.

        Args:
            table: Table string to load
            table_file: Path to file containing the table

        Returns:
            True if successful, False otherwise

        Note:
            Either table or table_file must be provided, but not both.
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return False

        if table and table_file:
            raise ValueError('Cannot specify both table and table_file')

        if table:
            result = run(f'dmsetup load {self.dm_name} --table "{table}"')
        elif table_file:
            result = run(f'dmsetup load {self.dm_name} {table_file}')
        else:
            raise ValueError('Either table or table_file must be provided')

        if result.failed:
            logging.error(f'Failed to load table for {self.dm_name}: {result.stderr}')
            return False
        return True

    def get_table(
        self,
        *,
        concise: bool = False,
        target_type: str | None = None,
        showkeys: bool = False,
    ) -> str | None:
        """Get the current device table.

        Outputs the current table for the device in a format that can be fed
        back using create or load commands.

        Args:
            concise: Output in concise format (name,uuid,minor,flags,table)
            target_type: Only show information for the specified target type
            showkeys: Show real encryption keys (for crypt/integrity targets)

        Returns:
            Table string or None if error
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return None

        cmd = f'dmsetup table {self.dm_name}'
        if concise:
            cmd += ' --concise'
        if target_type:
            cmd += f' --target {target_type}'
        if showkeys:
            cmd += ' --showkeys'

        result = run(cmd)
        if result.failed:
            logging.error(f'Failed to get table for {self.dm_name}')
            return None

        return result.stdout.strip()

    def wipe_table(
        self,
        *,
        force: bool = False,
        noflush: bool = False,
        nolockfs: bool = False,
    ) -> bool:
        """Wipe device table.

        Wait for any I/O in-flight through the device to complete, then replace
        the table with a new table that fails any new I/O sent to the device.
        If successful, this should release any devices held open by the device's table(s).

        Args:
            force: Force the operation
            noflush: Do not flush outstanding I/O
            nolockfs: Do not attempt to synchronize filesystem

        Returns:
            True if successful, False otherwise
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return False

        cmd = f'dmsetup wipe_table {self.dm_name}'
        if force:
            cmd += ' --force'
        if noflush:
            cmd += ' --noflush'
        if nolockfs:
            cmd += ' --nolockfs'

        result = run(cmd)
        if result.failed:
            logging.error(f'Failed to wipe table for {self.dm_name}: {result.stderr}')
            return False
        return True

    def mknodes(self) -> bool:
        """Ensure /dev/mapper node is correct.

        Ensures that the node in /dev/mapper for this device is correct.

        Returns:
            True if successful, False otherwise
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return False

        result = run(f'dmsetup mknodes {self.dm_name}')
        if result.failed:
            logging.error(f'Failed to mknodes for {self.dm_name}')
            return False
        return True

    def setgeometry(
        self,
        cylinders: int,
        heads: int,
        sectors: int,
        start: int,
    ) -> bool:
        """Set the device geometry.

        Sets the device geometry to C/H/S format.

        Args:
            cylinders: Number of cylinders
            heads: Number of heads
            sectors: Sectors per track
            start: Start sector

        Returns:
            True if successful, False otherwise
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return False

        result = run(f'dmsetup setgeometry {self.dm_name} {cylinders} {heads} {sectors} {start}')
        if result.failed:
            logging.error(f'Failed to set geometry for {self.dm_name}: {result.stderr}')
            return False
        return True

    def wait(
        self,
        event_nr: int | None = None,
        *,
        noflush: bool = False,
    ) -> int | None:
        """Wait for device event.

        Sleeps until the event counter for the device exceeds event_nr.

        Args:
            event_nr: Event number to wait for (waits until counter exceeds this)
            noflush: Do not commit thin-pool metadata before reporting

        Returns:
            Current event number after waiting, or None on error
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return None

        cmd = f'dmsetup wait {self.dm_name}'
        if noflush:
            cmd += ' --noflush'
        if event_nr is not None:
            cmd += f' {event_nr}'

        result = run(cmd)
        if result.failed:
            logging.error(f'Failed to wait for {self.dm_name}')
            return None

        # Parse event number from output
        try:
            return int(result.stdout.strip())
        except ValueError:
            return None

    def measure(self) -> str | None:
        """Show IMA measurement data.

        Shows the data that the device would report to the IMA subsystem
        if a measurement was triggered. This is for debugging and does not
        actually trigger a measurement.

        Returns:
            Measurement data string or None on error
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return None

        result = run(f'dmsetup measure {self.dm_name}')
        if result.failed:
            logging.error(f'Failed to measure {self.dm_name}')
            return None

        return result.stdout.strip()

    def mangle(self) -> bool:
        """Mangle device name.

        Ensures the device name and UUID contains only whitelisted characters
        (supported by udev) and renames if necessary.

        Returns:
            True if successful, False otherwise
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return False

        result = run(f'dmsetup mangle {self.dm_name}')
        if result.failed:
            logging.error(f'Failed to mangle {self.dm_name}')
            return False
        return True

    def splitname(self, subsystem: str = 'LVM') -> dict[str, str] | None:
        """Split device name into subsystem constituents.

        Splits the device name into its subsystem components.
        LVM generates device names by concatenating Volume Group, Logical Volume,
        and any internal Layer with a hyphen separator.

        Args:
            subsystem: Subsystem to use for splitting (default: LVM)

        Returns:
            Dictionary with split name components or None on error
        """
        if not self.is_created or not self.dm_name:
            logging.error('Device not created or dm_name not available')
            return None

        result = run(f'dmsetup splitname {self.dm_name} {subsystem}')
        if result.failed:
            logging.error(f'Failed to split name {self.dm_name}')
            return None

        # Parse output into dictionary
        output = result.stdout.strip()
        name_dict: dict[str, str] = {}
        for line in output.splitlines():
            if ':' in line:
                key, value = line.split(':', 1)
                name_dict[key.strip()] = value.strip()

        return name_dict

    @staticmethod
    def version() -> dict[str, str] | None:
        """Get dmsetup and driver version information.

        Returns:
            Dictionary with 'library' and 'driver' version strings, or None on error
        """
        result = run('dmsetup version')
        if result.failed:
            logging.error('Failed to get dmsetup version')
            return None

        output = result.stdout.strip()
        version_dict: dict[str, str] = {}
        for line in output.splitlines():
            if ':' in line:
                key, value = line.split(':', 1)
                version_dict[key.strip().lower().replace(' ', '_')] = value.strip()

        return version_dict

    @staticmethod
    def targets() -> list[dict[str, str]]:
        """Get list of available device mapper targets.

        Displays the names and versions of currently-loaded targets.

        Returns:
            List of dictionaries with 'name' and 'version' keys
        """
        result = run('dmsetup targets')
        if result.failed:
            logging.error('Failed to get targets')
            return []

        targets_list: list[dict[str, str]] = []
        for line in result.stdout.strip().splitlines():
            parts = line.strip().split()
            if len(parts) >= 2:
                targets_list.append(
                    {
                        'name': parts[0],
                        'version': parts[1],
                    }
                )

        return targets_list

    @staticmethod
    def udevcreatecookie() -> str | None:
        """Create a new udev synchronization cookie.

        Creates a new cookie to synchronize actions with udev processing.
        The cookie can be used with --udevcookie option or exported as
        DM_UDEV_COOKIE environment variable.

        Returns:
            Cookie value string or None on error

        Note:
            The cookie is a system-wide semaphore that needs to be cleaned up
            explicitly by calling udevreleasecookie().
        """
        result = run('dmsetup udevcreatecookie')
        if result.failed:
            logging.error('Failed to create udev cookie')
            return None

        return result.stdout.strip()

    @staticmethod
    def udevreleasecookie(cookie: str | None = None) -> bool:
        """Release a udev synchronization cookie.

        Waits for all pending udev processing bound to the cookie value
        and cleans up the cookie with underlying semaphore.

        Args:
            cookie: Cookie value to release. If not provided, uses DM_UDEV_COOKIE
                   environment variable.

        Returns:
            True if successful, False otherwise
        """
        cmd = 'dmsetup udevreleasecookie'
        if cookie:
            cmd += f' {cookie}'

        result = run(cmd)
        if result.failed:
            logging.error('Failed to release udev cookie')
            return False
        return True

    @staticmethod
    def udevcomplete(cookie: str) -> bool:
        """Signal udev processing completion.

        Wakes any processes that are waiting for udev to complete processing
        the specified cookie.

        Args:
            cookie: Cookie value to signal completion for

        Returns:
            True if successful, False otherwise
        """
        result = run(f'dmsetup udevcomplete {cookie}')
        if result.failed:
            logging.error(f'Failed to complete udev cookie {cookie}')
            return False
        return True

    @staticmethod
    def udevcomplete_all(age_in_minutes: int | None = None) -> bool:
        """Remove all old udev cookies.

        Removes all cookies older than the specified number of minutes.
        Any process waiting on a cookie will be resumed immediately.

        Args:
            age_in_minutes: Remove cookies older than this many minutes

        Returns:
            True if successful, False otherwise
        """
        cmd = 'dmsetup udevcomplete_all'
        if age_in_minutes is not None:
            cmd += f' {age_in_minutes}'

        result = run(cmd)
        if result.failed:
            logging.error('Failed to complete all udev cookies')
            return False
        return True

    @staticmethod
    def udevcookie() -> list[str]:
        """List all existing udev cookies.

        Cookies are system-wide semaphores with keys prefixed by two
        predefined bytes (0x0D4D).

        Returns:
            List of cookie values
        """
        result = run('dmsetup udevcookie')
        if result.failed:
            logging.error('Failed to list udev cookies')
            return []

        return result.stdout.strip().splitlines()

    @staticmethod
    def udevflags(cookie: str) -> dict[str, str]:
        """Parse udev control flags from cookie.

        Parses the cookie value and extracts any udev control flags encoded.
        The output is in environment key format suitable for use in udev rules.

        Args:
            cookie: Cookie value to parse

        Returns:
            Dictionary of flag names to values (typically '1')
        """
        result = run(f'dmsetup udevflags {cookie}')
        if result.failed:
            logging.error(f'Failed to get udev flags for cookie {cookie}')
            return {}

        flags: dict[str, str] = {}
        for line in result.stdout.strip().splitlines():
            if '=' in line:
                key, value = line.split('=', 1)
                flags[key.strip()] = value.strip().strip('\'"')

        return flags

    @classmethod
    def remove_all(cls, *, force: bool = False, deferred: bool = False) -> bool:
        """Remove all device mapper devices.

        Attempts to remove all device definitions (reset the driver).
        This also runs mknodes afterwards. Use with care!

        Args:
            force: Replace tables with one that fails all I/O before removing
            deferred: Enable deferred removal for open devices

        Returns:
            True if successful, False otherwise

        Warning:
            Open devices cannot be removed unless force or deferred is used.
        """
        cmd = 'dmsetup remove_all'
        if force:
            cmd += ' --force'
        if deferred:
            cmd += ' --deferred'

        result = run(cmd)
        if result.failed:
            logging.error('Failed to remove all devices')
            return False
        return True

    @classmethod
    def mknodes_all(cls) -> bool:
        """Ensure all /dev/mapper nodes are correct.

        Ensures that all nodes in /dev/mapper correspond to mapped devices
        currently loaded by the device-mapper kernel driver, adding, changing,
        or removing nodes as necessary.

        Returns:
            True if successful, False otherwise
        """
        result = run('dmsetup mknodes')
        if result.failed:
            logging.error('Failed to run mknodes')
            return False
        return True

    @classmethod
    def ls(
        cls,
        *,
        target_type: str | None = None,
        output_format: str = 'devno',
        tree: bool = False,
        tree_options: str | None = None,
        exec_cmd: str | None = None,
    ) -> list[str] | str:
        """List device mapper devices.

        Lists device names with optional filtering and formatting.

        Args:
            target_type: Only list devices with at least one target of this type
            output_format: Output format for device names:
                - 'devno': Major and minor pair (default)
                - 'blkdevname': Block device name
                - 'devname': Map name for DM devices
            tree: Display dependencies between devices as a tree
            tree_options: Comma-separated tree display options:
                - device/nodevice, blkdevname, active, open, rw, uuid
                - ascii, utf, vt100, compact, inverted, notrunc
            exec_cmd: Execute command for each device (device name appended)

        Returns:
            If tree=True: Tree output as string
            Otherwise: List of device names
        """
        cmd = 'dmsetup ls'
        if target_type:
            cmd += f' --target {target_type}'
        if output_format != 'devno':
            cmd += f' -o {output_format}'
        if tree:
            cmd += ' --tree'
            if tree_options:
                cmd += f' -o {tree_options}'
        if exec_cmd:
            cmd += f' --exec "{exec_cmd}"'

        result = run(cmd)
        if result.failed:
            logging.warning('No Device Mapper devices found')
            return [] if not tree else ''

        output = result.stdout.strip()

        if tree or exec_cmd:
            return output

        # Parse device names from output
        devices = []
        for line in output.splitlines():
            if line.strip():
                parts = line.strip().split()
                if parts:
                    devices.append(parts[0])

        return devices

    @classmethod
    def get_all(cls) -> Sequence[DmDevice]:
        """Get list of all Device Mapper devices.

        Returns:
            List of DmDevice instances
        """
        devices = []
        result = run('dmsetup ls')
        if result.failed:
            logging.warning('No Device Mapper devices found')
            return []

        for line in result.stdout.splitlines():
            try:
                stripped_line = line.strip()
                if not stripped_line:
                    continue

                parts = stripped_line.split()
                if len(parts) < 2:
                    continue

                dm_name = parts[0]
                dev_id = parts[1].strip('()')
                major, minor = dev_id.split(':')

                # Get kernel device name (dm-N)
                result = run(f'ls -l /dev/dm-* | grep "{major}, *{minor}"')
                if result.failed:
                    continue
                name = result.stdout.split('/')[-1].strip()

                devices.append(cls(dm_name=dm_name, name=name, path=f'/dev/{name}'))
            except (ValueError, DeviceError):
                logging.exception('Failed to parse device info')
                continue

        return devices

    @classmethod
    def create_concise(cls, concise_spec: str) -> bool:
        """Create one or more devices from concise device specification.

        Each device is specified by a comma-separated list:
        name,uuid,minor,flags,table[,table]...

        Multiple devices are separated by semicolons.

        Args:
            concise_spec: Concise device specification string
                Format: <name>,<uuid>,<minor>,<flags>,<table>[,<table>+][;<next device>...]
                - name: Device name
                - uuid: Device UUID (optional, can be empty)
                - minor: Minor number (optional, can be empty for auto-assignment)
                - flags: 'ro' for read-only or 'rw' for read-write (default)
                - table: One or more table lines (comma-separated if multiple)

        Returns:
            True if successful, False otherwise

        Example:
            ```python
            # Single linear device
            DmDevice.create_concise('test-linear,,,rw,0 2097152 linear /dev/loop0 0')

            # Two devices at once
            DmDevice.create_concise('dev1,,,,0 2097152 linear /dev/loop0 0;dev2,,,,0 2097152 linear /dev/loop1 0')
            ```

        Note:
            Use backslash to escape commas or semicolons in names or tables.
        """
        result = run(f'dmsetup create --concise "{concise_spec}"')
        if result.failed:
            logging.error(f'Failed to create devices from concise spec: {result.stderr}')
            return False
        return True

    @staticmethod
    def help(*, columns: bool = False) -> str | None:
        """Get dmsetup command help.

        Outputs a summary of the commands available.

        Args:
            columns: Include the list of report fields in the output

        Returns:
            Help text or None on error
        """
        cmd = 'dmsetup help'
        if columns:
            cmd += ' --columns'

        result = run(cmd)
        # Note: dmsetup help returns exit code 0 but writes to stderr
        output = result.stdout.strip() or result.stderr.strip()
        return output if output else None

    @classmethod
    def get_by_name(cls, dm_name: str) -> DmDevice | None:
        """Get Device Mapper device by name.

        Args:
            dm_name: Device Mapper name (e.g. 'my-device')

        Returns:
            DmDevice instance or None if not found
        """
        if not dm_name:
            raise ValueError('Device Mapper name required')

        # Check if device exists
        result = run(f'dmsetup info {dm_name}')
        if result.failed:
            return None

        try:
            return cls(dm_name=dm_name)
        except DeviceError:
            return None

    @staticmethod
    def _get_device_identifier(device: BlockDevice) -> str:
        """Get device identifier for device mapper.

        Args:
            device: BlockDevice instance

        Returns:
            Device identifier (major:minor format)

        Raises:
            DeviceError: If device identifier cannot be determined
        """
        device_id = device.device_id
        if device_id:
            return device_id
        raise DeviceError(f'No device ID available for {device.path}')

device_path: Path property

Get path to device in sysfs.

Returns:

Type Description
Path

Path to device directory

Raises:

Type Description
DeviceNotFoundError

If device does not exist

device_root_dir: str property

Get device mapper root directory.

Returns:

Type Description
str

Device mapper root directory path (/dev/mapper)

dm_device_path: str | None property

Get full device mapper device path.

Returns:

Type Description
str | None

Full device path (e.g. '/dev/mapper/my-device') or None if not available

type: str property

Get target type.

Returns:

Type Description
str

Target type string (e.g. 'linear', 'delay', 'vdo')

__post_init__()

Initialize Device Mapper device.

If dm_name or path is provided, assumes device exists and initializes BlockDevice properties. Otherwise, stays in configuration-only state.

Source code in sts_libs/src/sts/dm/base.py
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
def __post_init__(self) -> None:
    """Initialize Device Mapper device.

    If dm_name or path is provided, assumes device exists and initializes
    BlockDevice properties. Otherwise, stays in configuration-only state.
    """
    # Check if this is an existing device (has dm_name or path)
    if self.dm_name or self.path:
        # Set path based on dm_name if not provided
        if not self.path and self.dm_name:
            self.path = f'/dev/mapper/{self.dm_name}'
        elif not self.path and self.name:
            self.path = f'/dev/{self.name}'

        # Initialize BlockDevice (queries system)
        super().__post_init__()

        # Get device mapper name if not provided
        if not self.dm_name and self.name:
            result = run(f'dmsetup info -c --noheadings -o name {self.name}')
            if result.succeeded:
                self.dm_name = result.stdout.strip()

        # Load table from system
        if self.dm_name:
            result = run(f'dmsetup table {self.dm_name}')
            if result.succeeded:
                self.table = result.stdout.strip()
                self._parse_table()

        self.is_created = True

__str__()

Return target table entry.

Format: Used in dmsetup table commands.

Source code in sts_libs/src/sts/dm/base.py
154
155
156
157
158
159
160
def __str__(self) -> str:
    """Return target table entry.

    Format: <start> <size> <type> <args>
    Used in dmsetup table commands.
    """
    return f'{self.start} {self.size_sectors} {self.type} {self.args}'

clear()

Clear the inactive table slot.

Destroys the table in the inactive table slot for the device. This is useful when you want to abort a table load before resuming.

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
def clear(self) -> bool:
    """Clear the inactive table slot.

    Destroys the table in the inactive table slot for the device.
    This is useful when you want to abort a table load before resuming.

    Returns:
        True if successful, False otherwise
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return False

    result = run(f'dmsetup clear {self.dm_name}')
    if result.failed:
        logging.error(f'Failed to clear inactive table for {self.dm_name}')
        return False
    return True

create(dm_name, *, uuid=None, readonly=False, notable=False, readahead=None, addnodeoncreate=False, addnodeonresume=False)

Create the device on the system.

After successful creation, this device becomes a full BlockDevice with all properties populated from the system.

Parameters:

Name Type Description Default
dm_name str

Name for the device mapper device

required
uuid str | None

Optional UUID for the device (can be used in place of name in commands)

None
readonly bool

Set the table being loaded as read-only

False
notable bool

Create device without loading any table

False
readahead str | int | None

Read ahead size in sectors, or 'auto', 'none', or '+N' for minimum

None
addnodeoncreate bool

Ensure /dev/mapper node exists after create

False
addnodeonresume bool

Ensure /dev/mapper node exists after resume (default with udev)

False

Returns:

Type Description
bool

True if successful, False otherwise

Example
device = DelayDevice.from_block_device(backing, delay_ms=100)
if device.create('my-delay', uuid='my-uuid-123'):
    print(f'Created: {device.dm_device_path}')
Source code in sts_libs/src/sts/dm/base.py
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
def create(
    self,
    dm_name: str,
    *,
    uuid: str | None = None,
    readonly: bool = False,
    notable: bool = False,
    readahead: str | int | None = None,
    addnodeoncreate: bool = False,
    addnodeonresume: bool = False,
) -> bool:
    """Create the device on the system.

    After successful creation, this device becomes a full BlockDevice
    with all properties populated from the system.

    Args:
        dm_name: Name for the device mapper device
        uuid: Optional UUID for the device (can be used in place of name in commands)
        readonly: Set the table being loaded as read-only
        notable: Create device without loading any table
        readahead: Read ahead size in sectors, or 'auto', 'none', or '+N' for minimum
        addnodeoncreate: Ensure /dev/mapper node exists after create
        addnodeonresume: Ensure /dev/mapper node exists after resume (default with udev)

    Returns:
        True if successful, False otherwise

    Example:
        ```python
        device = DelayDevice.from_block_device(backing, delay_ms=100)
        if device.create('my-delay', uuid='my-uuid-123'):
            print(f'Created: {device.dm_device_path}')
        ```
    """
    if self.is_created:
        logging.warning(f'Device {self.dm_name} already created')
        return True

    cmd = f'dmsetup create {dm_name}'

    if uuid:
        cmd += f' --uuid {uuid}'
    if readonly:
        cmd += ' --readonly'
    if addnodeoncreate:
        cmd += ' --addnodeoncreate'
    if addnodeonresume:
        cmd += ' --addnodeonresume'
    if readahead is not None:
        cmd += f' --readahead {readahead}'

    if notable:
        cmd += ' --notable'
    else:
        # Build table from this device's configuration
        table = str(self)
        cmd += f' --table "{table}"'
        logging.info(f'Creating device {dm_name} with table: {table}')

    result = run(cmd)

    if result.failed:
        logging.error(f'Failed to create device {dm_name}: {result.stderr}')
        return False

    logging.info(f'Successfully created device mapper device: {dm_name}')

    # Set dm_name and reinitialize as existing device
    self.dm_name = dm_name
    self.path = f'/dev/mapper/{dm_name}'

    # Initialize BlockDevice properties from system
    super().__post_init__()

    # Get kernel device name
    if not self.name:
        result = run(f'dmsetup info -c --noheadings -o name,blkdevname {dm_name}')
        if result.succeeded:
            parts = result.stdout.strip().split()
            if len(parts) >= 2:
                self.name = parts[1]

    # Load table from system
    result = run(f'dmsetup table {dm_name}')
    if result.succeeded:
        self.table = result.stdout.strip()
        self._parse_table()

    self.is_created = True
    return True

create_concise(concise_spec) classmethod

Create one or more devices from concise device specification.

Each device is specified by a comma-separated list: name,uuid,minor,flags,table[,table]...

Multiple devices are separated by semicolons.

Parameters:

Name Type Description Default
concise_spec str

Concise device specification string Format: ,,,,

[,
+][;...] - name: Device name - uuid: Device UUID (optional, can be empty) - minor: Minor number (optional, can be empty for auto-assignment) - flags: 'ro' for read-only or 'rw' for read-write (default) - table: One or more table lines (comma-separated if multiple)

required

Returns:

Type Description
bool

True if successful, False otherwise

Example
# Single linear device
DmDevice.create_concise('test-linear,,,rw,0 2097152 linear /dev/loop0 0')

# Two devices at once
DmDevice.create_concise('dev1,,,,0 2097152 linear /dev/loop0 0;dev2,,,,0 2097152 linear /dev/loop1 0')
Note

Use backslash to escape commas or semicolons in names or tables.

Source code in sts_libs/src/sts/dm/base.py
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
@classmethod
def create_concise(cls, concise_spec: str) -> bool:
    """Create one or more devices from concise device specification.

    Each device is specified by a comma-separated list:
    name,uuid,minor,flags,table[,table]...

    Multiple devices are separated by semicolons.

    Args:
        concise_spec: Concise device specification string
            Format: <name>,<uuid>,<minor>,<flags>,<table>[,<table>+][;<next device>...]
            - name: Device name
            - uuid: Device UUID (optional, can be empty)
            - minor: Minor number (optional, can be empty for auto-assignment)
            - flags: 'ro' for read-only or 'rw' for read-write (default)
            - table: One or more table lines (comma-separated if multiple)

    Returns:
        True if successful, False otherwise

    Example:
        ```python
        # Single linear device
        DmDevice.create_concise('test-linear,,,rw,0 2097152 linear /dev/loop0 0')

        # Two devices at once
        DmDevice.create_concise('dev1,,,,0 2097152 linear /dev/loop0 0;dev2,,,,0 2097152 linear /dev/loop1 0')
        ```

    Note:
        Use backslash to escape commas or semicolons in names or tables.
    """
    result = run(f'dmsetup create --concise "{concise_spec}"')
    if result.failed:
        logging.error(f'Failed to create devices from concise spec: {result.stderr}')
        return False
    return True

deps(*, output_format='devno')

Get device dependencies.

Outputs a list of devices referenced by the live table for this device.

Parameters:

Name Type Description Default
output_format str

Output format for device names: - 'devno': Major and minor pair (default) - 'blkdevname': Block device name - 'devname': Map name for DM devices, blkdevname otherwise

'devno'

Returns:

Type Description
list[str]

List of device identifiers (format depends on output_format)

Source code in sts_libs/src/sts/dm/base.py
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
def deps(self, *, output_format: str = 'devno') -> list[str]:
    """Get device dependencies.

    Outputs a list of devices referenced by the live table for this device.

    Args:
        output_format: Output format for device names:
            - 'devno': Major and minor pair (default)
            - 'blkdevname': Block device name
            - 'devname': Map name for DM devices, blkdevname otherwise

    Returns:
        List of device identifiers (format depends on output_format)
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return []

    result = run(f'dmsetup deps -o {output_format} {self.dm_name}')
    if result.failed:
        logging.error(f'Failed to get dependencies for {self.dm_name}')
        return []

    # Parse output: "1 dependencies  : (major, minor) ..."
    output = result.stdout.strip()
    deps = []
    if ':' in output:
        deps_part = output.split(':', 1)[1].strip()
        # Extract device identifiers from the output
        if output_format == 'devno':
            # Format: (major, minor) or (major:minor)
            matches = re.findall(r'\((\d+[,:]\s*\d+)\)', deps_part)
            deps = [m.replace(' ', '').replace(',', ':') for m in matches]
        else:
            # Format: (device_name)
            matches = re.findall(r'\(([^)]+)\)', deps_part)
            deps = matches

    return deps

get_all() classmethod

Get list of all Device Mapper devices.

Returns:

Type Description
Sequence[DmDevice]

List of DmDevice instances

Source code in sts_libs/src/sts/dm/base.py
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
@classmethod
def get_all(cls) -> Sequence[DmDevice]:
    """Get list of all Device Mapper devices.

    Returns:
        List of DmDevice instances
    """
    devices = []
    result = run('dmsetup ls')
    if result.failed:
        logging.warning('No Device Mapper devices found')
        return []

    for line in result.stdout.splitlines():
        try:
            stripped_line = line.strip()
            if not stripped_line:
                continue

            parts = stripped_line.split()
            if len(parts) < 2:
                continue

            dm_name = parts[0]
            dev_id = parts[1].strip('()')
            major, minor = dev_id.split(':')

            # Get kernel device name (dm-N)
            result = run(f'ls -l /dev/dm-* | grep "{major}, *{minor}"')
            if result.failed:
                continue
            name = result.stdout.split('/')[-1].strip()

            devices.append(cls(dm_name=dm_name, name=name, path=f'/dev/{name}'))
        except (ValueError, DeviceError):
            logging.exception('Failed to parse device info')
            continue

    return devices

get_by_name(dm_name) classmethod

Get Device Mapper device by name.

Parameters:

Name Type Description Default
dm_name str

Device Mapper name (e.g. 'my-device')

required

Returns:

Type Description
DmDevice | None

DmDevice instance or None if not found

Source code in sts_libs/src/sts/dm/base.py
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
@classmethod
def get_by_name(cls, dm_name: str) -> DmDevice | None:
    """Get Device Mapper device by name.

    Args:
        dm_name: Device Mapper name (e.g. 'my-device')

    Returns:
        DmDevice instance or None if not found
    """
    if not dm_name:
        raise ValueError('Device Mapper name required')

    # Check if device exists
    result = run(f'dmsetup info {dm_name}')
    if result.failed:
        return None

    try:
        return cls(dm_name=dm_name)
    except DeviceError:
        return None

get_status(*, target_type=None, noflush=False)

Get device status from dmsetup.

Outputs status information for each of the device's targets.

Parameters:

Name Type Description Default
target_type str | None

Only display information for the specified target type

None
noflush bool

Do not commit thin-pool metadata before reporting statistics

False

Returns:

Type Description
str | None

Status string or None if not available

Source code in sts_libs/src/sts/dm/base.py
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
def get_status(
    self,
    *,
    target_type: str | None = None,
    noflush: bool = False,
) -> str | None:
    """Get device status from dmsetup.

    Outputs status information for each of the device's targets.

    Args:
        target_type: Only display information for the specified target type
        noflush: Do not commit thin-pool metadata before reporting statistics

    Returns:
        Status string or None if not available
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return None

    cmd = f'dmsetup status {self.dm_name}'
    if target_type:
        cmd += f' --target {target_type}'
    if noflush:
        cmd += ' --noflush'

    result = run(cmd)
    if result.failed:
        logging.error(f'Failed to get status for {self.dm_name}')
        return None

    return result.stdout.strip()

get_table(*, concise=False, target_type=None, showkeys=False)

Get the current device table.

Outputs the current table for the device in a format that can be fed back using create or load commands.

Parameters:

Name Type Description Default
concise bool

Output in concise format (name,uuid,minor,flags,table)

False
target_type str | None

Only show information for the specified target type

None
showkeys bool

Show real encryption keys (for crypt/integrity targets)

False

Returns:

Type Description
str | None

Table string or None if error

Source code in sts_libs/src/sts/dm/base.py
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
def get_table(
    self,
    *,
    concise: bool = False,
    target_type: str | None = None,
    showkeys: bool = False,
) -> str | None:
    """Get the current device table.

    Outputs the current table for the device in a format that can be fed
    back using create or load commands.

    Args:
        concise: Output in concise format (name,uuid,minor,flags,table)
        target_type: Only show information for the specified target type
        showkeys: Show real encryption keys (for crypt/integrity targets)

    Returns:
        Table string or None if error
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return None

    cmd = f'dmsetup table {self.dm_name}'
    if concise:
        cmd += ' --concise'
    if target_type:
        cmd += f' --target {target_type}'
    if showkeys:
        cmd += ' --showkeys'

    result = run(cmd)
    if result.failed:
        logging.error(f'Failed to get table for {self.dm_name}')
        return None

    return result.stdout.strip()

help(*, columns=False) staticmethod

Get dmsetup command help.

Outputs a summary of the commands available.

Parameters:

Name Type Description Default
columns bool

Include the list of report fields in the output

False

Returns:

Type Description
str | None

Help text or None on error

Source code in sts_libs/src/sts/dm/base.py
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
@staticmethod
def help(*, columns: bool = False) -> str | None:
    """Get dmsetup command help.

    Outputs a summary of the commands available.

    Args:
        columns: Include the list of report fields in the output

    Returns:
        Help text or None on error
    """
    cmd = 'dmsetup help'
    if columns:
        cmd += ' --columns'

    result = run(cmd)
    # Note: dmsetup help returns exit code 0 but writes to stderr
    output = result.stdout.strip() or result.stderr.strip()
    return output if output else None

info(*, columns=False, noheadings=False, fields=None, separator=None, sort_fields=None, nameprefixes=False)

Get device information.

Outputs information about the device in various formats.

Parameters:

Name Type Description Default
columns bool

Display output in columns rather than Field: Value lines

False
noheadings bool

Suppress the headings line when using columnar output

False
fields str | None

Comma-separated list of fields to display (name, major, minor, attr, open, segments, events, uuid)

None
separator str | None

Separator for columnar output

None
sort_fields str | None

Fields to sort by (prefix with '-' for reverse)

None
nameprefixes bool

Add DM_ prefix plus field name to output

False

Returns:

Type Description
dict[str, str] | str | None

If columns=False: Dictionary of field names to values

dict[str, str] | str | None

If columns=True: Raw output string

dict[str, str] | str | None

None if device not found or error

Source code in sts_libs/src/sts/dm/base.py
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
def info(
    self,
    *,
    columns: bool = False,
    noheadings: bool = False,
    fields: str | None = None,
    separator: str | None = None,
    sort_fields: str | None = None,
    nameprefixes: bool = False,
) -> dict[str, str] | str | None:
    """Get device information.

    Outputs information about the device in various formats.

    Args:
        columns: Display output in columns rather than Field: Value lines
        noheadings: Suppress the headings line when using columnar output
        fields: Comma-separated list of fields to display
               (name, major, minor, attr, open, segments, events, uuid)
        separator: Separator for columnar output
        sort_fields: Fields to sort by (prefix with '-' for reverse)
        nameprefixes: Add DM_ prefix plus field name to output

    Returns:
        If columns=False: Dictionary of field names to values
        If columns=True: Raw output string
        None if device not found or error
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return None

    cmd = f'dmsetup info {self.dm_name}'
    if columns:
        cmd += ' --columns'
        if noheadings:
            cmd += ' --noheadings'
        if fields:
            cmd += f' -o {fields}'
        if separator:
            cmd += f' --separator "{separator}"'
        if sort_fields:
            cmd += f' --sort {sort_fields}'
        if nameprefixes:
            cmd += ' --nameprefixes'

    result = run(cmd)
    if result.failed:
        logging.error(f'Failed to get info for {self.dm_name}')
        return None

    output = result.stdout.strip()

    if columns:
        return output

    # Parse Field: Value format into dictionary
    info_dict: dict[str, str] = {}
    for line in output.splitlines():
        if ':' in line:
            key, value = line.split(':', 1)
            info_dict[key.strip()] = value.strip()

    return info_dict

load(table=None, table_file=None)

Load table into the inactive table slot.

Loads a table into the inactive table slot for the device. Use resume() to make the inactive table live.

Parameters:

Name Type Description Default
table str | None

Table string to load

None
table_file str | None

Path to file containing the table

None

Returns:

Type Description
bool

True if successful, False otherwise

Note

Either table or table_file must be provided, but not both.

Source code in sts_libs/src/sts/dm/base.py
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
def load(
    self,
    table: str | None = None,
    table_file: str | None = None,
) -> bool:
    """Load table into the inactive table slot.

    Loads a table into the inactive table slot for the device.
    Use resume() to make the inactive table live.

    Args:
        table: Table string to load
        table_file: Path to file containing the table

    Returns:
        True if successful, False otherwise

    Note:
        Either table or table_file must be provided, but not both.
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return False

    if table and table_file:
        raise ValueError('Cannot specify both table and table_file')

    if table:
        result = run(f'dmsetup load {self.dm_name} --table "{table}"')
    elif table_file:
        result = run(f'dmsetup load {self.dm_name} {table_file}')
    else:
        raise ValueError('Either table or table_file must be provided')

    if result.failed:
        logging.error(f'Failed to load table for {self.dm_name}: {result.stderr}')
        return False
    return True

ls(*, target_type=None, output_format='devno', tree=False, tree_options=None, exec_cmd=None) classmethod

List device mapper devices.

Lists device names with optional filtering and formatting.

Parameters:

Name Type Description Default
target_type str | None

Only list devices with at least one target of this type

None
output_format str

Output format for device names: - 'devno': Major and minor pair (default) - 'blkdevname': Block device name - 'devname': Map name for DM devices

'devno'
tree bool

Display dependencies between devices as a tree

False
tree_options str | None

Comma-separated tree display options: - device/nodevice, blkdevname, active, open, rw, uuid - ascii, utf, vt100, compact, inverted, notrunc

None
exec_cmd str | None

Execute command for each device (device name appended)

None

Returns:

Name Type Description
list[str] | str

If tree=True: Tree output as string

Otherwise list[str] | str

List of device names

Source code in sts_libs/src/sts/dm/base.py
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
@classmethod
def ls(
    cls,
    *,
    target_type: str | None = None,
    output_format: str = 'devno',
    tree: bool = False,
    tree_options: str | None = None,
    exec_cmd: str | None = None,
) -> list[str] | str:
    """List device mapper devices.

    Lists device names with optional filtering and formatting.

    Args:
        target_type: Only list devices with at least one target of this type
        output_format: Output format for device names:
            - 'devno': Major and minor pair (default)
            - 'blkdevname': Block device name
            - 'devname': Map name for DM devices
        tree: Display dependencies between devices as a tree
        tree_options: Comma-separated tree display options:
            - device/nodevice, blkdevname, active, open, rw, uuid
            - ascii, utf, vt100, compact, inverted, notrunc
        exec_cmd: Execute command for each device (device name appended)

    Returns:
        If tree=True: Tree output as string
        Otherwise: List of device names
    """
    cmd = 'dmsetup ls'
    if target_type:
        cmd += f' --target {target_type}'
    if output_format != 'devno':
        cmd += f' -o {output_format}'
    if tree:
        cmd += ' --tree'
        if tree_options:
            cmd += f' -o {tree_options}'
    if exec_cmd:
        cmd += f' --exec "{exec_cmd}"'

    result = run(cmd)
    if result.failed:
        logging.warning('No Device Mapper devices found')
        return [] if not tree else ''

    output = result.stdout.strip()

    if tree or exec_cmd:
        return output

    # Parse device names from output
    devices = []
    for line in output.splitlines():
        if line.strip():
            parts = line.strip().split()
            if parts:
                devices.append(parts[0])

    return devices

mangle()

Mangle device name.

Ensures the device name and UUID contains only whitelisted characters (supported by udev) and renames if necessary.

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
def mangle(self) -> bool:
    """Mangle device name.

    Ensures the device name and UUID contains only whitelisted characters
    (supported by udev) and renames if necessary.

    Returns:
        True if successful, False otherwise
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return False

    result = run(f'dmsetup mangle {self.dm_name}')
    if result.failed:
        logging.error(f'Failed to mangle {self.dm_name}')
        return False
    return True

measure()

Show IMA measurement data.

Shows the data that the device would report to the IMA subsystem if a measurement was triggered. This is for debugging and does not actually trigger a measurement.

Returns:

Type Description
str | None

Measurement data string or None on error

Source code in sts_libs/src/sts/dm/base.py
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
def measure(self) -> str | None:
    """Show IMA measurement data.

    Shows the data that the device would report to the IMA subsystem
    if a measurement was triggered. This is for debugging and does not
    actually trigger a measurement.

    Returns:
        Measurement data string or None on error
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return None

    result = run(f'dmsetup measure {self.dm_name}')
    if result.failed:
        logging.error(f'Failed to measure {self.dm_name}')
        return None

    return result.stdout.strip()

message(sector, message)

Send message to device target.

Send a message to the target at the specified sector. Use sector 0 if the sector is not relevant to the target.

Parameters:

Name Type Description Default
sector int

Sector number (use 0 if not needed)

required
message str

Message string to send to the target

required

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
def message(self, sector: int, message: str) -> bool:
    """Send message to device target.

    Send a message to the target at the specified sector.
    Use sector 0 if the sector is not relevant to the target.

    Args:
        sector: Sector number (use 0 if not needed)
        message: Message string to send to the target

    Returns:
        True if successful, False otherwise
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return False

    result = run(f'dmsetup message {self.dm_name} {sector} {message}')
    if result.failed:
        logging.error(f'Failed to send message to {self.dm_name}: {result.stderr}')
        return False
    return True

mknodes()

Ensure /dev/mapper node is correct.

Ensures that the node in /dev/mapper for this device is correct.

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
def mknodes(self) -> bool:
    """Ensure /dev/mapper node is correct.

    Ensures that the node in /dev/mapper for this device is correct.

    Returns:
        True if successful, False otherwise
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return False

    result = run(f'dmsetup mknodes {self.dm_name}')
    if result.failed:
        logging.error(f'Failed to mknodes for {self.dm_name}')
        return False
    return True

mknodes_all() classmethod

Ensure all /dev/mapper nodes are correct.

Ensures that all nodes in /dev/mapper correspond to mapped devices currently loaded by the device-mapper kernel driver, adding, changing, or removing nodes as necessary.

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
@classmethod
def mknodes_all(cls) -> bool:
    """Ensure all /dev/mapper nodes are correct.

    Ensures that all nodes in /dev/mapper correspond to mapped devices
    currently loaded by the device-mapper kernel driver, adding, changing,
    or removing nodes as necessary.

    Returns:
        True if successful, False otherwise
    """
    result = run('dmsetup mknodes')
    if result.failed:
        logging.error('Failed to run mknodes')
        return False
    return True

refresh()

Refresh device state from system.

Re-reads the device table and status from the system. Subclasses should override to parse target-specific attributes.

Returns:

Type Description
None

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
def refresh(self) -> None:
    """Refresh device state from system.

    Re-reads the device table and status from the system.
    Subclasses should override to parse target-specific attributes.

    Returns:
        True if successful, False otherwise
    """
    if not self.is_created or not self.dm_name:
        return

    # Refresh table from system
    result = run(f'dmsetup table {self.dm_name}')
    if result.succeeded:
        self.table = result.stdout.strip()
        self._parse_table()

reload_table(new_table)

Reload device table.

Suspends the device, loads a new table, and resumes.

Parameters:

Name Type Description Default
new_table str

New table string to load

required

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
def reload_table(self, new_table: str) -> bool:
    """Reload device table.

    Suspends the device, loads a new table, and resumes.

    Args:
        new_table: New table string to load

    Returns:
        True if successful, False otherwise
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return False

    # Suspend device
    if not self.suspend():
        return False

    # Load new table
    result = run(f'dmsetup load {self.dm_name} "{new_table}"')
    if result.failed:
        logging.error(f'Failed to load new table: {result.stderr}')
        self.resume()
        return False

    # Resume with new table
    if not self.resume():
        return False

    # Refresh from system
    self.refresh()
    logging.info(f'Successfully reloaded table for {self.dm_name}')
    return True

remove(*, force=False, retry=False, deferred=False)

Remove device.

Removes the device mapping. The underlying devices are unaffected. Open devices cannot be removed, but --force will replace the table with one that fails all I/O. --deferred enables deferred removal when the device is in use.

Parameters:

Name Type Description Default
force bool

Replace the table with one that fails all I/O

False
retry bool

Retry removal for a few seconds if it fails (e.g., udev race)

False
deferred bool

Enable deferred removal - device removed when last user closes it

False

Returns:

Type Description
bool

True if successful, False otherwise

Note

Do NOT combine --force and --udevcookie as this may cause nondeterministic results.

Source code in sts_libs/src/sts/dm/base.py
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
def remove(
    self,
    *,
    force: bool = False,
    retry: bool = False,
    deferred: bool = False,
) -> bool:
    """Remove device.

    Removes the device mapping. The underlying devices are unaffected.
    Open devices cannot be removed, but --force will replace the table with
    one that fails all I/O. --deferred enables deferred removal when the device
    is in use.

    Args:
        force: Replace the table with one that fails all I/O
        retry: Retry removal for a few seconds if it fails (e.g., udev race)
        deferred: Enable deferred removal - device removed when last user closes it

    Returns:
        True if successful, False otherwise

    Note:
        Do NOT combine --force and --udevcookie as this may cause
        nondeterministic results.
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return False

    cmd = f'dmsetup remove {self.dm_name}'
    if force:
        cmd += ' --force'
    if retry:
        cmd += ' --retry'
    if deferred:
        cmd += ' --deferred'

    result = run(cmd)
    if result.failed:
        logging.error('Failed to remove device')
        return False

    self.is_created = False
    return True

remove_all(*, force=False, deferred=False) classmethod

Remove all device mapper devices.

Attempts to remove all device definitions (reset the driver). This also runs mknodes afterwards. Use with care!

Parameters:

Name Type Description Default
force bool

Replace tables with one that fails all I/O before removing

False
deferred bool

Enable deferred removal for open devices

False

Returns:

Type Description
bool

True if successful, False otherwise

Warning

Open devices cannot be removed unless force or deferred is used.

Source code in sts_libs/src/sts/dm/base.py
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
@classmethod
def remove_all(cls, *, force: bool = False, deferred: bool = False) -> bool:
    """Remove all device mapper devices.

    Attempts to remove all device definitions (reset the driver).
    This also runs mknodes afterwards. Use with care!

    Args:
        force: Replace tables with one that fails all I/O before removing
        deferred: Enable deferred removal for open devices

    Returns:
        True if successful, False otherwise

    Warning:
        Open devices cannot be removed unless force or deferred is used.
    """
    cmd = 'dmsetup remove_all'
    if force:
        cmd += ' --force'
    if deferred:
        cmd += ' --deferred'

    result = run(cmd)
    if result.failed:
        logging.error('Failed to remove all devices')
        return False
    return True

rename(new_name)

Rename the device.

Parameters:

Name Type Description Default
new_name str

New name for the device

required

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
def rename(self, new_name: str) -> bool:
    """Rename the device.

    Args:
        new_name: New name for the device

    Returns:
        True if successful, False otherwise
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return False

    result = run(f'dmsetup rename {self.dm_name} {new_name}')
    if result.failed:
        logging.error(f'Failed to rename {self.dm_name} to {new_name}')
        return False

    # Update internal state
    old_name = self.dm_name
    self.dm_name = new_name
    self.path = f'/dev/mapper/{new_name}'
    logging.info(f'Successfully renamed device from {old_name} to {new_name}')
    return True

resume(*, addnodeoncreate=False, addnodeonresume=False, noflush=False, nolockfs=False, readahead=None)

Resume device.

Un-suspends a device. If an inactive table has been loaded, it becomes live. Postponed I/O then gets re-queued for processing.

Parameters:

Name Type Description Default
addnodeoncreate bool

Ensure /dev/mapper node exists after create

False
addnodeonresume bool

Ensure /dev/mapper node exists after resume (default with udev)

False
noflush bool

Do not flush outstanding I/O

False
nolockfs bool

Do not attempt to synchronize filesystem

False
readahead str | int | None

Read ahead size in sectors, or 'auto', 'none', or '+N' for minimum

None

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
def resume(
    self,
    *,
    addnodeoncreate: bool = False,
    addnodeonresume: bool = False,
    noflush: bool = False,
    nolockfs: bool = False,
    readahead: str | int | None = None,
) -> bool:
    """Resume device.

    Un-suspends a device. If an inactive table has been loaded, it becomes live.
    Postponed I/O then gets re-queued for processing.

    Args:
        addnodeoncreate: Ensure /dev/mapper node exists after create
        addnodeonresume: Ensure /dev/mapper node exists after resume (default with udev)
        noflush: Do not flush outstanding I/O
        nolockfs: Do not attempt to synchronize filesystem
        readahead: Read ahead size in sectors, or 'auto', 'none', or '+N' for minimum

    Returns:
        True if successful, False otherwise
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return False

    cmd = f'dmsetup resume {self.dm_name}'
    if addnodeoncreate:
        cmd += ' --addnodeoncreate'
    if addnodeonresume:
        cmd += ' --addnodeonresume'
    if noflush:
        cmd += ' --noflush'
    if nolockfs:
        cmd += ' --nolockfs'
    if readahead is not None:
        cmd += f' --readahead {readahead}'

    result = run(cmd)
    if result.failed:
        logging.error('Failed to resume device')
        return False
    return True

set_uuid(uuid)

Set the UUID of a device that was created without one.

After a UUID has been set it cannot be changed.

Parameters:

Name Type Description Default
uuid str

UUID to set for the device

required

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
def set_uuid(self, uuid: str) -> bool:
    """Set the UUID of a device that was created without one.

    After a UUID has been set it cannot be changed.

    Args:
        uuid: UUID to set for the device

    Returns:
        True if successful, False otherwise
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return False

    result = run(f'dmsetup rename {self.dm_name} --setuuid {uuid}')
    if result.failed:
        logging.error(f'Failed to set UUID for {self.dm_name}: {result.stderr}')
        return False

    logging.info(f'Successfully set UUID {uuid} for device {self.dm_name}')
    return True

setgeometry(cylinders, heads, sectors, start)

Set the device geometry.

Sets the device geometry to C/H/S format.

Parameters:

Name Type Description Default
cylinders int

Number of cylinders

required
heads int

Number of heads

required
sectors int

Sectors per track

required
start int

Start sector

required

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
def setgeometry(
    self,
    cylinders: int,
    heads: int,
    sectors: int,
    start: int,
) -> bool:
    """Set the device geometry.

    Sets the device geometry to C/H/S format.

    Args:
        cylinders: Number of cylinders
        heads: Number of heads
        sectors: Sectors per track
        start: Start sector

    Returns:
        True if successful, False otherwise
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return False

    result = run(f'dmsetup setgeometry {self.dm_name} {cylinders} {heads} {sectors} {start}')
    if result.failed:
        logging.error(f'Failed to set geometry for {self.dm_name}: {result.stderr}')
        return False
    return True

splitname(subsystem='LVM')

Split device name into subsystem constituents.

Splits the device name into its subsystem components. LVM generates device names by concatenating Volume Group, Logical Volume, and any internal Layer with a hyphen separator.

Parameters:

Name Type Description Default
subsystem str

Subsystem to use for splitting (default: LVM)

'LVM'

Returns:

Type Description
dict[str, str] | None

Dictionary with split name components or None on error

Source code in sts_libs/src/sts/dm/base.py
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
def splitname(self, subsystem: str = 'LVM') -> dict[str, str] | None:
    """Split device name into subsystem constituents.

    Splits the device name into its subsystem components.
    LVM generates device names by concatenating Volume Group, Logical Volume,
    and any internal Layer with a hyphen separator.

    Args:
        subsystem: Subsystem to use for splitting (default: LVM)

    Returns:
        Dictionary with split name components or None on error
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return None

    result = run(f'dmsetup splitname {self.dm_name} {subsystem}')
    if result.failed:
        logging.error(f'Failed to split name {self.dm_name}')
        return None

    # Parse output into dictionary
    output = result.stdout.strip()
    name_dict: dict[str, str] = {}
    for line in output.splitlines():
        if ':' in line:
            key, value = line.split(':', 1)
            name_dict[key.strip()] = value.strip()

    return name_dict

suspend(*, nolockfs=False, noflush=False)

Suspend device.

Suspends I/O to the device. Any I/O that has already been mapped by the device but has not yet completed will be flushed. Any further I/O to that device will be postponed for as long as the device is suspended.

Parameters:

Name Type Description Default
nolockfs bool

Do not attempt to synchronize filesystem (skip fsfreeze)

False
noflush bool

Let outstanding I/O remain unflushed (requires target support)

False

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
def suspend(
    self,
    *,
    nolockfs: bool = False,
    noflush: bool = False,
) -> bool:
    """Suspend device.

    Suspends I/O to the device. Any I/O that has already been mapped by the device
    but has not yet completed will be flushed. Any further I/O to that device will
    be postponed for as long as the device is suspended.

    Args:
        nolockfs: Do not attempt to synchronize filesystem (skip fsfreeze)
        noflush: Let outstanding I/O remain unflushed (requires target support)

    Returns:
        True if successful, False otherwise
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return False

    cmd = f'dmsetup suspend {self.dm_name}'
    if nolockfs:
        cmd += ' --nolockfs'
    if noflush:
        cmd += ' --noflush'

    result = run(cmd)
    if result.failed:
        logging.error('Failed to suspend device')
        return False
    return True

targets() staticmethod

Get list of available device mapper targets.

Displays the names and versions of currently-loaded targets.

Returns:

Type Description
list[dict[str, str]]

List of dictionaries with 'name' and 'version' keys

Source code in sts_libs/src/sts/dm/base.py
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
@staticmethod
def targets() -> list[dict[str, str]]:
    """Get list of available device mapper targets.

    Displays the names and versions of currently-loaded targets.

    Returns:
        List of dictionaries with 'name' and 'version' keys
    """
    result = run('dmsetup targets')
    if result.failed:
        logging.error('Failed to get targets')
        return []

    targets_list: list[dict[str, str]] = []
    for line in result.stdout.strip().splitlines():
        parts = line.strip().split()
        if len(parts) >= 2:
            targets_list.append(
                {
                    'name': parts[0],
                    'version': parts[1],
                }
            )

    return targets_list

udevcomplete(cookie) staticmethod

Signal udev processing completion.

Wakes any processes that are waiting for udev to complete processing the specified cookie.

Parameters:

Name Type Description Default
cookie str

Cookie value to signal completion for

required

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
@staticmethod
def udevcomplete(cookie: str) -> bool:
    """Signal udev processing completion.

    Wakes any processes that are waiting for udev to complete processing
    the specified cookie.

    Args:
        cookie: Cookie value to signal completion for

    Returns:
        True if successful, False otherwise
    """
    result = run(f'dmsetup udevcomplete {cookie}')
    if result.failed:
        logging.error(f'Failed to complete udev cookie {cookie}')
        return False
    return True

udevcomplete_all(age_in_minutes=None) staticmethod

Remove all old udev cookies.

Removes all cookies older than the specified number of minutes. Any process waiting on a cookie will be resumed immediately.

Parameters:

Name Type Description Default
age_in_minutes int | None

Remove cookies older than this many minutes

None

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
@staticmethod
def udevcomplete_all(age_in_minutes: int | None = None) -> bool:
    """Remove all old udev cookies.

    Removes all cookies older than the specified number of minutes.
    Any process waiting on a cookie will be resumed immediately.

    Args:
        age_in_minutes: Remove cookies older than this many minutes

    Returns:
        True if successful, False otherwise
    """
    cmd = 'dmsetup udevcomplete_all'
    if age_in_minutes is not None:
        cmd += f' {age_in_minutes}'

    result = run(cmd)
    if result.failed:
        logging.error('Failed to complete all udev cookies')
        return False
    return True

udevcookie() staticmethod

List all existing udev cookies.

Cookies are system-wide semaphores with keys prefixed by two predefined bytes (0x0D4D).

Returns:

Type Description
list[str]

List of cookie values

Source code in sts_libs/src/sts/dm/base.py
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
@staticmethod
def udevcookie() -> list[str]:
    """List all existing udev cookies.

    Cookies are system-wide semaphores with keys prefixed by two
    predefined bytes (0x0D4D).

    Returns:
        List of cookie values
    """
    result = run('dmsetup udevcookie')
    if result.failed:
        logging.error('Failed to list udev cookies')
        return []

    return result.stdout.strip().splitlines()

udevcreatecookie() staticmethod

Create a new udev synchronization cookie.

Creates a new cookie to synchronize actions with udev processing. The cookie can be used with --udevcookie option or exported as DM_UDEV_COOKIE environment variable.

Returns:

Type Description
str | None

Cookie value string or None on error

Note

The cookie is a system-wide semaphore that needs to be cleaned up explicitly by calling udevreleasecookie().

Source code in sts_libs/src/sts/dm/base.py
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
@staticmethod
def udevcreatecookie() -> str | None:
    """Create a new udev synchronization cookie.

    Creates a new cookie to synchronize actions with udev processing.
    The cookie can be used with --udevcookie option or exported as
    DM_UDEV_COOKIE environment variable.

    Returns:
        Cookie value string or None on error

    Note:
        The cookie is a system-wide semaphore that needs to be cleaned up
        explicitly by calling udevreleasecookie().
    """
    result = run('dmsetup udevcreatecookie')
    if result.failed:
        logging.error('Failed to create udev cookie')
        return None

    return result.stdout.strip()

udevflags(cookie) staticmethod

Parse udev control flags from cookie.

Parses the cookie value and extracts any udev control flags encoded. The output is in environment key format suitable for use in udev rules.

Parameters:

Name Type Description Default
cookie str

Cookie value to parse

required

Returns:

Type Description
dict[str, str]

Dictionary of flag names to values (typically '1')

Source code in sts_libs/src/sts/dm/base.py
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
@staticmethod
def udevflags(cookie: str) -> dict[str, str]:
    """Parse udev control flags from cookie.

    Parses the cookie value and extracts any udev control flags encoded.
    The output is in environment key format suitable for use in udev rules.

    Args:
        cookie: Cookie value to parse

    Returns:
        Dictionary of flag names to values (typically '1')
    """
    result = run(f'dmsetup udevflags {cookie}')
    if result.failed:
        logging.error(f'Failed to get udev flags for cookie {cookie}')
        return {}

    flags: dict[str, str] = {}
    for line in result.stdout.strip().splitlines():
        if '=' in line:
            key, value = line.split('=', 1)
            flags[key.strip()] = value.strip().strip('\'"')

    return flags

udevreleasecookie(cookie=None) staticmethod

Release a udev synchronization cookie.

Waits for all pending udev processing bound to the cookie value and cleans up the cookie with underlying semaphore.

Parameters:

Name Type Description Default
cookie str | None

Cookie value to release. If not provided, uses DM_UDEV_COOKIE environment variable.

None

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
@staticmethod
def udevreleasecookie(cookie: str | None = None) -> bool:
    """Release a udev synchronization cookie.

    Waits for all pending udev processing bound to the cookie value
    and cleans up the cookie with underlying semaphore.

    Args:
        cookie: Cookie value to release. If not provided, uses DM_UDEV_COOKIE
               environment variable.

    Returns:
        True if successful, False otherwise
    """
    cmd = 'dmsetup udevreleasecookie'
    if cookie:
        cmd += f' {cookie}'

    result = run(cmd)
    if result.failed:
        logging.error('Failed to release udev cookie')
        return False
    return True

version() staticmethod

Get dmsetup and driver version information.

Returns:

Type Description
dict[str, str] | None

Dictionary with 'library' and 'driver' version strings, or None on error

Source code in sts_libs/src/sts/dm/base.py
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
@staticmethod
def version() -> dict[str, str] | None:
    """Get dmsetup and driver version information.

    Returns:
        Dictionary with 'library' and 'driver' version strings, or None on error
    """
    result = run('dmsetup version')
    if result.failed:
        logging.error('Failed to get dmsetup version')
        return None

    output = result.stdout.strip()
    version_dict: dict[str, str] = {}
    for line in output.splitlines():
        if ':' in line:
            key, value = line.split(':', 1)
            version_dict[key.strip().lower().replace(' ', '_')] = value.strip()

    return version_dict

wait(event_nr=None, *, noflush=False)

Wait for device event.

Sleeps until the event counter for the device exceeds event_nr.

Parameters:

Name Type Description Default
event_nr int | None

Event number to wait for (waits until counter exceeds this)

None
noflush bool

Do not commit thin-pool metadata before reporting

False

Returns:

Type Description
int | None

Current event number after waiting, or None on error

Source code in sts_libs/src/sts/dm/base.py
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
def wait(
    self,
    event_nr: int | None = None,
    *,
    noflush: bool = False,
) -> int | None:
    """Wait for device event.

    Sleeps until the event counter for the device exceeds event_nr.

    Args:
        event_nr: Event number to wait for (waits until counter exceeds this)
        noflush: Do not commit thin-pool metadata before reporting

    Returns:
        Current event number after waiting, or None on error
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return None

    cmd = f'dmsetup wait {self.dm_name}'
    if noflush:
        cmd += ' --noflush'
    if event_nr is not None:
        cmd += f' {event_nr}'

    result = run(cmd)
    if result.failed:
        logging.error(f'Failed to wait for {self.dm_name}')
        return None

    # Parse event number from output
    try:
        return int(result.stdout.strip())
    except ValueError:
        return None

wipe_table(*, force=False, noflush=False, nolockfs=False)

Wipe device table.

Wait for any I/O in-flight through the device to complete, then replace the table with a new table that fails any new I/O sent to the device. If successful, this should release any devices held open by the device's table(s).

Parameters:

Name Type Description Default
force bool

Force the operation

False
noflush bool

Do not flush outstanding I/O

False
nolockfs bool

Do not attempt to synchronize filesystem

False

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/base.py
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
def wipe_table(
    self,
    *,
    force: bool = False,
    noflush: bool = False,
    nolockfs: bool = False,
) -> bool:
    """Wipe device table.

    Wait for any I/O in-flight through the device to complete, then replace
    the table with a new table that fails any new I/O sent to the device.
    If successful, this should release any devices held open by the device's table(s).

    Args:
        force: Force the operation
        noflush: Do not flush outstanding I/O
        nolockfs: Do not attempt to synchronize filesystem

    Returns:
        True if successful, False otherwise
    """
    if not self.is_created or not self.dm_name:
        logging.error('Device not created or dm_name not available')
        return False

    cmd = f'dmsetup wipe_table {self.dm_name}'
    if force:
        cmd += ' --force'
    if noflush:
        cmd += ' --noflush'
    if nolockfs:
        cmd += ' --nolockfs'

    result = run(cmd)
    if result.failed:
        logging.error(f'Failed to wipe table for {self.dm_name}: {result.stderr}')
        return False
    return True

ErrorDevice dataclass

Bases: DmDevice

Error target.

Returns I/O errors for all operations. Useful for testing error handling.

Args format: (no arguments)

Example
target = ErrorDevice(0, 1000000, '')
str(target)
'0 1000000 error'
Source code in sts_libs/src/sts/dm/error.py
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
@dataclass
class ErrorDevice(DmDevice):
    """Error target.

    Returns I/O errors for all operations.
    Useful for testing error handling.

    Args format: (no arguments)

    Example:
        ```python
        target = ErrorDevice(0, 1000000, '')
        str(target)
        '0 1000000 error'
        ```
    """

    def __post_init__(self) -> None:
        """Set target type and initialize."""
        self.target_type = 'error'
        super().__post_init__()

    @classmethod
    def create_config(
        cls,
        start: int = 0,
        size: int | None = None,
    ) -> ErrorDevice:
        """Create ErrorDevice.

        Args:
            start: Start sector in virtual device (default: 0)
            size: Size in sectors (required)

        Returns:
            ErrorDevice instance

        Example:
            ```python
            target = ErrorDevice.create_config(size=2097152)
            str(target)
            '0 2097152 error'
            ```
        """
        if size is None:
            raise ValueError('Size must be specified for error targets')

        return cls(start=start, size_sectors=size, args='')

__post_init__()

Set target type and initialize.

Source code in sts_libs/src/sts/dm/error.py
60
61
62
63
def __post_init__(self) -> None:
    """Set target type and initialize."""
    self.target_type = 'error'
    super().__post_init__()

create_config(start=0, size=None) classmethod

Create ErrorDevice.

Parameters:

Name Type Description Default
start int

Start sector in virtual device (default: 0)

0
size int | None

Size in sectors (required)

None

Returns:

Type Description
ErrorDevice

ErrorDevice instance

Example
target = ErrorDevice.create_config(size=2097152)
str(target)
'0 2097152 error'
Source code in sts_libs/src/sts/dm/error.py
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
@classmethod
def create_config(
    cls,
    start: int = 0,
    size: int | None = None,
) -> ErrorDevice:
    """Create ErrorDevice.

    Args:
        start: Start sector in virtual device (default: 0)
        size: Size in sectors (required)

    Returns:
        ErrorDevice instance

    Example:
        ```python
        target = ErrorDevice.create_config(size=2097152)
        str(target)
        '0 2097152 error'
        ```
    """
    if size is None:
        raise ValueError('Size must be specified for error targets')

    return cls(start=start, size_sectors=size, args='')

FlakeyDevice dataclass

Bases: DmDevice

Flakey target.

Simulates an unreliable device that periodically fails or corrupts I/O. Useful for testing error handling and recovery.

Args format: [ []]

Example
# Device up for 60s, down for 10s, dropping writes when down
target = FlakeyDevice(0, 1000000, '253:0 0 60 10 1 drop_writes')
str(target)
'0 1000000 flakey 253:0 0 60 10 1 drop_writes'
Source code in sts_libs/src/sts/dm/flakey.py
 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
@dataclass
class FlakeyDevice(DmDevice):
    """Flakey target.

    Simulates an unreliable device that periodically fails or corrupts I/O.
    Useful for testing error handling and recovery.

    Args format: <device> <offset> <up_interval> <down_interval> [<num_features> [<features>]]

    Example:
        ```python
        # Device up for 60s, down for 10s, dropping writes when down
        target = FlakeyDevice(0, 1000000, '253:0 0 60 10 1 drop_writes')
        str(target)
        '0 1000000 flakey 253:0 0 60 10 1 drop_writes'
        ```
    """

    def __post_init__(self) -> None:
        """Set target type and initialize."""
        self.target_type = 'flakey'
        super().__post_init__()

    @classmethod
    def from_block_device(
        cls,
        device: BlockDevice,
        up_interval: int,
        down_interval: int,
        offset: int = 0,
        size_sectors: int | None = None,
        corrupt_bio_byte: tuple[int, str, int, int] | None = None,
        start: int = 0,
        *,
        drop_writes: bool = False,
        error_writes: bool = False,
    ) -> FlakeyDevice:
        """Create FlakeyDevice from BlockDevice.

        Args:
            device: Underlying block device
            up_interval: Seconds device is available (reliable)
            down_interval: Seconds device is unreliable
            offset: Starting sector in underlying device (default: 0)
            size_sectors: Size in sectors (default: full device)
            drop_writes: Silently drop writes when down (default: False)
            error_writes: Error writes when down (default: False)
            corrupt_bio_byte: Tuple of (nth_byte, direction, value, flags) for corruption
            start: Start sector in virtual device (default: 0)

        Returns:
            FlakeyDevice instance

        Example:
            ```python
            # Device available 60s, then drops writes for 10s
            device = BlockDevice('/dev/sdb')
            flakey = FlakeyDevice.from_block_device(device, up_interval=60, down_interval=10, drop_writes=True)
            ```
        """
        if size_sectors is None and device.size is not None:
            size_sectors = device.size // device.sector_size

        device_id = cls._get_device_identifier(device)

        # Build args: <device> <offset> <up_interval> <down_interval> [features]
        args_parts = [
            device_id,
            str(offset),
            str(up_interval),
            str(down_interval),
        ]

        # Build feature list
        features: list[str] = []
        if drop_writes:
            features.append('drop_writes')
        if error_writes:
            features.append('error_writes')
        if corrupt_bio_byte is not None:
            nth_byte, direction, value, flags = corrupt_bio_byte
            features.append(f'corrupt_bio_byte {nth_byte} {direction} {value} {flags}')

        # Add features if any
        if features:
            args_parts.append(str(len(features)))
            args_parts.extend(features)

        args = ' '.join(args_parts)
        if size_sectors is None:
            raise ValueError('size_sectors must be provided or device.size must be available')
        return cls(start=start, size_sectors=size_sectors, args=args)

__post_init__()

Set target type and initialize.

Source code in sts_libs/src/sts/dm/flakey.py
51
52
53
54
def __post_init__(self) -> None:
    """Set target type and initialize."""
    self.target_type = 'flakey'
    super().__post_init__()

from_block_device(device, up_interval, down_interval, offset=0, size_sectors=None, corrupt_bio_byte=None, start=0, *, drop_writes=False, error_writes=False) classmethod

Create FlakeyDevice from BlockDevice.

Parameters:

Name Type Description Default
device BlockDevice

Underlying block device

required
up_interval int

Seconds device is available (reliable)

required
down_interval int

Seconds device is unreliable

required
offset int

Starting sector in underlying device (default: 0)

0
size_sectors int | None

Size in sectors (default: full device)

None
drop_writes bool

Silently drop writes when down (default: False)

False
error_writes bool

Error writes when down (default: False)

False
corrupt_bio_byte tuple[int, str, int, int] | None

Tuple of (nth_byte, direction, value, flags) for corruption

None
start int

Start sector in virtual device (default: 0)

0

Returns:

Type Description
FlakeyDevice

FlakeyDevice instance

Example
# Device available 60s, then drops writes for 10s
device = BlockDevice('/dev/sdb')
flakey = FlakeyDevice.from_block_device(device, up_interval=60, down_interval=10, drop_writes=True)
Source code in sts_libs/src/sts/dm/flakey.py
 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
@classmethod
def from_block_device(
    cls,
    device: BlockDevice,
    up_interval: int,
    down_interval: int,
    offset: int = 0,
    size_sectors: int | None = None,
    corrupt_bio_byte: tuple[int, str, int, int] | None = None,
    start: int = 0,
    *,
    drop_writes: bool = False,
    error_writes: bool = False,
) -> FlakeyDevice:
    """Create FlakeyDevice from BlockDevice.

    Args:
        device: Underlying block device
        up_interval: Seconds device is available (reliable)
        down_interval: Seconds device is unreliable
        offset: Starting sector in underlying device (default: 0)
        size_sectors: Size in sectors (default: full device)
        drop_writes: Silently drop writes when down (default: False)
        error_writes: Error writes when down (default: False)
        corrupt_bio_byte: Tuple of (nth_byte, direction, value, flags) for corruption
        start: Start sector in virtual device (default: 0)

    Returns:
        FlakeyDevice instance

    Example:
        ```python
        # Device available 60s, then drops writes for 10s
        device = BlockDevice('/dev/sdb')
        flakey = FlakeyDevice.from_block_device(device, up_interval=60, down_interval=10, drop_writes=True)
        ```
    """
    if size_sectors is None and device.size is not None:
        size_sectors = device.size // device.sector_size

    device_id = cls._get_device_identifier(device)

    # Build args: <device> <offset> <up_interval> <down_interval> [features]
    args_parts = [
        device_id,
        str(offset),
        str(up_interval),
        str(down_interval),
    ]

    # Build feature list
    features: list[str] = []
    if drop_writes:
        features.append('drop_writes')
    if error_writes:
        features.append('error_writes')
    if corrupt_bio_byte is not None:
        nth_byte, direction, value, flags = corrupt_bio_byte
        features.append(f'corrupt_bio_byte {nth_byte} {direction} {value} {flags}')

    # Add features if any
    if features:
        args_parts.append(str(len(features)))
        args_parts.extend(features)

    args = ' '.join(args_parts)
    if size_sectors is None:
        raise ValueError('size_sectors must be provided or device.size must be available')
    return cls(start=start, size_sectors=size_sectors, args=args)

LinearDevice dataclass

Bases: DmDevice

Linear device mapper device.

The simplest DM device type - maps a linear range of the virtual device directly onto a linear range of another device.

Args format:

Example
device = LinearDevice.from_block_device(backing_dev, size_sectors=1000000)
device.create('my-linear')
str(device)
'0 1000000 linear 8:16 0'
Source code in sts_libs/src/sts/dm/linear.py
 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
@dataclass
class LinearDevice(DmDevice):
    """Linear device mapper device.

    The simplest DM device type - maps a linear range of the virtual device
    directly onto a linear range of another device.

    Args format: <destination device> <sector offset>

    Example:
        ```python
        device = LinearDevice.from_block_device(backing_dev, size_sectors=1000000)
        device.create('my-linear')
        str(device)
        '0 1000000 linear 8:16 0'
        ```
    """

    def __post_init__(self) -> None:
        """Set target type and initialize."""
        self.target_type = 'linear'
        super().__post_init__()

    @classmethod
    def from_block_device(
        cls,
        device: BlockDevice,
        start: int = 0,
        size_sectors: int | None = None,
        offset: int = 0,
    ) -> LinearDevice:
        """Create LinearDevice from BlockDevice.

        Args:
            device: Source block device
            start: Start sector in virtual device (default: 0)
            size_sectors: Size in sectors (default: device size in sectors)
            offset: Offset in source device (default: 0)

        Returns:
            LinearDevice instance

        Example:
            ```python
            device = BlockDevice('/dev/sdb')
            linear = LinearDevice.from_block_device(device, size_sectors=1000000)
            linear.create('my-linear')
            ```
        """
        if size_sectors is None and device.size is not None:
            size_sectors = device.size // device.sector_size

        device_id = cls._get_device_identifier(device)
        args = f'{device_id} {offset}'

        if size_sectors is None:
            raise ValueError('size_sectors must be provided or device.size must be available')
        return cls(start=start, size_sectors=size_sectors, args=args)

    @classmethod
    def create_positional(
        cls,
        device_path: str,
        offset: int = 0,
        start: int = 0,
        size_sectors: int | None = None,
    ) -> LinearDevice:
        """Create LinearDevice with positional arguments.

        Args:
            device_path: Target device path (e.g., '/dev/sdb' or '8:16')
            offset: Offset in source device sectors (default: 0)
            start: Start sector in virtual device (default: 0)
            size_sectors: Size in sectors (required)

        Returns:
            LinearDevice instance

        Example:
            ```python
            linear = LinearDevice.create_positional('/dev/sdb', offset=0, size_sectors=2097152)
            linear.create('my-linear')
            ```
        """
        if size_sectors is None:
            raise ValueError('Size must be specified for linear devices')

        args = f'{device_path} {offset}'
        return cls(start=start, size_sectors=size_sectors, args=args)

__post_init__()

Set target type and initialize.

Source code in sts_libs/src/sts/dm/linear.py
65
66
67
68
def __post_init__(self) -> None:
    """Set target type and initialize."""
    self.target_type = 'linear'
    super().__post_init__()

create_positional(device_path, offset=0, start=0, size_sectors=None) classmethod

Create LinearDevice with positional arguments.

Parameters:

Name Type Description Default
device_path str

Target device path (e.g., '/dev/sdb' or '8:16')

required
offset int

Offset in source device sectors (default: 0)

0
start int

Start sector in virtual device (default: 0)

0
size_sectors int | None

Size in sectors (required)

None

Returns:

Type Description
LinearDevice

LinearDevice instance

Example
linear = LinearDevice.create_positional('/dev/sdb', offset=0, size_sectors=2097152)
linear.create('my-linear')
Source code in sts_libs/src/sts/dm/linear.py
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
@classmethod
def create_positional(
    cls,
    device_path: str,
    offset: int = 0,
    start: int = 0,
    size_sectors: int | None = None,
) -> LinearDevice:
    """Create LinearDevice with positional arguments.

    Args:
        device_path: Target device path (e.g., '/dev/sdb' or '8:16')
        offset: Offset in source device sectors (default: 0)
        start: Start sector in virtual device (default: 0)
        size_sectors: Size in sectors (required)

    Returns:
        LinearDevice instance

    Example:
        ```python
        linear = LinearDevice.create_positional('/dev/sdb', offset=0, size_sectors=2097152)
        linear.create('my-linear')
        ```
    """
    if size_sectors is None:
        raise ValueError('Size must be specified for linear devices')

    args = f'{device_path} {offset}'
    return cls(start=start, size_sectors=size_sectors, args=args)

from_block_device(device, start=0, size_sectors=None, offset=0) classmethod

Create LinearDevice from BlockDevice.

Parameters:

Name Type Description Default
device BlockDevice

Source block device

required
start int

Start sector in virtual device (default: 0)

0
size_sectors int | None

Size in sectors (default: device size in sectors)

None
offset int

Offset in source device (default: 0)

0

Returns:

Type Description
LinearDevice

LinearDevice instance

Example
device = BlockDevice('/dev/sdb')
linear = LinearDevice.from_block_device(device, size_sectors=1000000)
linear.create('my-linear')
Source code in sts_libs/src/sts/dm/linear.py
 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
@classmethod
def from_block_device(
    cls,
    device: BlockDevice,
    start: int = 0,
    size_sectors: int | None = None,
    offset: int = 0,
) -> LinearDevice:
    """Create LinearDevice from BlockDevice.

    Args:
        device: Source block device
        start: Start sector in virtual device (default: 0)
        size_sectors: Size in sectors (default: device size in sectors)
        offset: Offset in source device (default: 0)

    Returns:
        LinearDevice instance

    Example:
        ```python
        device = BlockDevice('/dev/sdb')
        linear = LinearDevice.from_block_device(device, size_sectors=1000000)
        linear.create('my-linear')
        ```
    """
    if size_sectors is None and device.size is not None:
        size_sectors = device.size // device.sector_size

    device_id = cls._get_device_identifier(device)
    args = f'{device_id} {offset}'

    if size_sectors is None:
        raise ValueError('size_sectors must be provided or device.size must be available')
    return cls(start=start, size_sectors=size_sectors, args=args)

ThinDevice dataclass

Bases: DmDevice

Thin target.

A virtual device that allocates space from a thin pool on demand. Enables over-provisioning of storage.

Maintains a reference to its parent pool for proper lifecycle management.

Args format:

Example
# Create via pool (recommended)
thin = pool.create_thin(thin_id=0, size=2097152, dm_name='my-thin')

# Or create manually
thin = ThinDevice.from_thin_pool(pool, thin_id=1, size=2097152)
thin.create('another-thin')
Source code in sts_libs/src/sts/dm/thin.py
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
@dataclass
class ThinDevice(DmDevice):
    """Thin target.

    A virtual device that allocates space from a thin pool on demand.
    Enables over-provisioning of storage.

    Maintains a reference to its parent pool for proper lifecycle management.

    Args format: <pool dev> <dev id>

    Example:
        ```python
        # Create via pool (recommended)
        thin = pool.create_thin(thin_id=0, size=2097152, dm_name='my-thin')

        # Or create manually
        thin = ThinDevice.from_thin_pool(pool, thin_id=1, size=2097152)
        thin.create('another-thin')
        ```
    """

    # Reference to parent pool
    pool: ThinPoolDevice | None = field(init=False, default=None, repr=False)

    # Thin device ID within pool
    thin_id: int | None = field(init=False, default=None, repr=False)

    def __post_init__(self) -> None:
        """Set target type and initialize."""
        self.target_type = 'thin'
        super().__post_init__()

    @classmethod
    def create_from_pool(
        cls,
        pool: ThinPoolDevice,
        thin_id: int,
        size: int,
        start: int = 0,
    ) -> ThinDevice:
        """Create ThinDevice configuration from pool.

        This is used by ThinPoolDevice.create_thin() and create_snapshot()
        to create the device configuration before activation.

        Args:
            pool: The parent thin pool device
            thin_id: Unique ID for this thin volume within the pool
            size: Size in sectors
            start: Start sector in virtual device (default: 0)

        Returns:
            ThinDevice instance (not yet activated)
        """
        pool_id = cls._get_device_identifier(pool)
        args = f'{pool_id} {thin_id}'

        device = cls(start=start, size_sectors=size, args=args)
        device.pool = pool
        device.thin_id = thin_id
        return device

    @classmethod
    def from_thin_pool(
        cls,
        pool_device: DmDevice,
        thin_id: int,
        start: int = 0,
        size: int | None = None,
    ) -> ThinDevice:
        """Create ThinDevice from thin pool device.

        Note: For full parent-child tracking, use ThinPoolDevice.create_thin() instead.

        Args:
            pool_device: The thin pool device
            thin_id: Unique ID for this thin volume within the pool
            start: Start sector in virtual device (default: 0)
            size: Size in sectors (required for thin targets)

        Returns:
            ThinDevice instance

        Raises:
            ValueError: If size is not provided

        Example:
            ```python
            pool = DmDevice(dm_name='pool')
            thin = ThinDevice.from_thin_pool(pool, thin_id=1, size=2097152)
            ```
        """
        if size is None:
            raise ValueError('Size must be specified for thin targets')

        pool_id = cls._get_device_identifier(pool_device)
        args = f'{pool_id} {thin_id}'

        device = cls(start=start, size_sectors=size, args=args)
        device.thin_id = thin_id

        # Set pool reference if it's a ThinPoolDevice
        if isinstance(pool_device, ThinPoolDevice):
            device.pool = pool_device

        return device

__post_init__()

Set target type and initialize.

Source code in sts_libs/src/sts/dm/thin.py
352
353
354
355
def __post_init__(self) -> None:
    """Set target type and initialize."""
    self.target_type = 'thin'
    super().__post_init__()

create_from_pool(pool, thin_id, size, start=0) classmethod

Create ThinDevice configuration from pool.

This is used by ThinPoolDevice.create_thin() and create_snapshot() to create the device configuration before activation.

Parameters:

Name Type Description Default
pool ThinPoolDevice

The parent thin pool device

required
thin_id int

Unique ID for this thin volume within the pool

required
size int

Size in sectors

required
start int

Start sector in virtual device (default: 0)

0

Returns:

Type Description
ThinDevice

ThinDevice instance (not yet activated)

Source code in sts_libs/src/sts/dm/thin.py
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
@classmethod
def create_from_pool(
    cls,
    pool: ThinPoolDevice,
    thin_id: int,
    size: int,
    start: int = 0,
) -> ThinDevice:
    """Create ThinDevice configuration from pool.

    This is used by ThinPoolDevice.create_thin() and create_snapshot()
    to create the device configuration before activation.

    Args:
        pool: The parent thin pool device
        thin_id: Unique ID for this thin volume within the pool
        size: Size in sectors
        start: Start sector in virtual device (default: 0)

    Returns:
        ThinDevice instance (not yet activated)
    """
    pool_id = cls._get_device_identifier(pool)
    args = f'{pool_id} {thin_id}'

    device = cls(start=start, size_sectors=size, args=args)
    device.pool = pool
    device.thin_id = thin_id
    return device

from_thin_pool(pool_device, thin_id, start=0, size=None) classmethod

Create ThinDevice from thin pool device.

Note: For full parent-child tracking, use ThinPoolDevice.create_thin() instead.

Parameters:

Name Type Description Default
pool_device DmDevice

The thin pool device

required
thin_id int

Unique ID for this thin volume within the pool

required
start int

Start sector in virtual device (default: 0)

0
size int | None

Size in sectors (required for thin targets)

None

Returns:

Type Description
ThinDevice

ThinDevice instance

Raises:

Type Description
ValueError

If size is not provided

Example
pool = DmDevice(dm_name='pool')
thin = ThinDevice.from_thin_pool(pool, thin_id=1, size=2097152)
Source code in sts_libs/src/sts/dm/thin.py
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
@classmethod
def from_thin_pool(
    cls,
    pool_device: DmDevice,
    thin_id: int,
    start: int = 0,
    size: int | None = None,
) -> ThinDevice:
    """Create ThinDevice from thin pool device.

    Note: For full parent-child tracking, use ThinPoolDevice.create_thin() instead.

    Args:
        pool_device: The thin pool device
        thin_id: Unique ID for this thin volume within the pool
        start: Start sector in virtual device (default: 0)
        size: Size in sectors (required for thin targets)

    Returns:
        ThinDevice instance

    Raises:
        ValueError: If size is not provided

    Example:
        ```python
        pool = DmDevice(dm_name='pool')
        thin = ThinDevice.from_thin_pool(pool, thin_id=1, size=2097152)
        ```
    """
    if size is None:
        raise ValueError('Size must be specified for thin targets')

    pool_id = cls._get_device_identifier(pool_device)
    args = f'{pool_id} {thin_id}'

    device = cls(start=start, size_sectors=size, args=args)
    device.thin_id = thin_id

    # Set pool reference if it's a ThinPoolDevice
    if isinstance(pool_device, ThinPoolDevice):
        device.pool = pool_device

    return device

ThinPoolDevice dataclass

Bases: DmDevice

Thin pool target.

Manages a pool of storage space from which thin volumes can be allocated. Enables thin provisioning - allocating more virtual space than physical.

Tracks child ThinDevice instances and provides methods for managing them.

Args format:

Example
# Create pool from block devices
pool = ThinPoolDevice.from_block_devices(metadata_dev, data_dev)
pool.create('my-pool')

# Create thin volume from pool
thin = pool.create_thin(thin_id=0, size=2097152, dm_name='my-thin')

# Create snapshot
snap = pool.create_snapshot(origin_id=0, snap_id=1, dm_name='my-snap')

# Cleanup
pool.delete_thin(1)  # Delete snapshot
pool.delete_thin(0)  # Delete origin
pool.remove()
Source code in sts_libs/src/sts/dm/thin.py
 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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
@dataclass
class ThinPoolDevice(DmDevice):
    """Thin pool target.

    Manages a pool of storage space from which thin volumes can be allocated.
    Enables thin provisioning - allocating more virtual space than physical.

    Tracks child ThinDevice instances and provides methods for managing them.

    Args format: <metadata dev> <data dev> <block size> <low water mark> <flags> <args>

    Example:
        ```python
        # Create pool from block devices
        pool = ThinPoolDevice.from_block_devices(metadata_dev, data_dev)
        pool.create('my-pool')

        # Create thin volume from pool
        thin = pool.create_thin(thin_id=0, size=2097152, dm_name='my-thin')

        # Create snapshot
        snap = pool.create_snapshot(origin_id=0, snap_id=1, dm_name='my-snap')

        # Cleanup
        pool.delete_thin(1)  # Delete snapshot
        pool.delete_thin(0)  # Delete origin
        pool.remove()
        ```
    """

    # Track child thin devices: thin_id -> ThinDevice
    thin_devices: dict[int, ThinDevice] = field(init=False, default_factory=dict, repr=False)

    def __post_init__(self) -> None:
        """Set target type and initialize."""
        self.target_type = 'thin-pool'
        super().__post_init__()

    @classmethod
    def from_block_devices(
        cls,
        metadata_device: BlockDevice,
        data_device: BlockDevice,
        block_size_sectors: int = 128,
        low_water_mark: int = 0,
        features: list[str] | None = None,
        start: int = 0,
        size: int | None = None,
    ) -> ThinPoolDevice:
        """Create ThinPoolDevice from BlockDevices.

        Args:
            metadata_device: Device for storing thin pool metadata
            data_device: Device for storing actual data
            block_size_sectors: Block size in sectors (default: 128 = 64KB)
            low_water_mark: Low water mark in sectors (default: 0 = auto)
            features: List of thin pool features (default: ['skip_block_zeroing'])
            start: Start sector in virtual device (default: 0)
            size: Size in sectors (default: data device size)

        Returns:
            ThinPoolDevice instance

        Example:
            ```python
            metadata_dev = BlockDevice('/dev/sdb1')  # Small device for metadata
            data_dev = BlockDevice('/dev/sdb2')  # Large device for data
            pool = ThinPoolDevice.from_block_devices(metadata_dev, data_dev)
            ```
        """
        if size is None and data_device.size is not None:
            # Use data device size, converted to sectors using actual sector size
            size = data_device.size // data_device.sector_size

        if features is None:
            features = ['skip_block_zeroing']

        metadata_id = cls._get_device_identifier(metadata_device)
        data_id = cls._get_device_identifier(data_device)

        # Build args: <metadata dev> <data dev> <block size> <low water mark> <# feature args> <feature args>
        args_parts = [
            metadata_id,
            data_id,
            str(block_size_sectors),
            str(low_water_mark),
            str(len(features)),
            *features,
        ]

        args = ' '.join(args_parts)
        if size is None:
            raise ValueError('size must be provided or data_device.size must be available')
        return cls(start=start, size_sectors=size, args=args)

    def create_thin(
        self,
        thin_id: int,
        size: int,
        dm_name: str,
        *,
        activate: bool = True,
    ) -> ThinDevice | None:
        """Create a new thin volume in the pool.

        Sends 'create_thin' message to the pool and optionally activates the device.

        Args:
            thin_id: Unique ID for this thin volume within the pool
            size: Size in sectors for the thin volume
            dm_name: Device mapper name for the activated device
            activate: Whether to activate the device (default: True)

        Returns:
            ThinDevice instance if successful, None otherwise

        Example:
            ```python
            pool.create('my-pool')
            thin = pool.create_thin(thin_id=0, size=2097152, dm_name='my-thin')
            ```
        """
        if not self.is_created:
            logging.error('Pool must be created before creating thin devices')
            return None

        if thin_id in self.thin_devices:
            logging.error(f'Thin device with ID {thin_id} already exists in pool')
            return None

        # Send create_thin message to pool
        if not self.message(0, f'"create_thin {thin_id}"'):
            logging.error(f'Failed to create thin device {thin_id} in pool')
            return None

        logging.info(f'Created thin device ID {thin_id} in pool {self.dm_name}')

        # Create ThinDevice configuration
        thin_dev = ThinDevice.create_from_pool(
            pool=self,
            thin_id=thin_id,
            size=size,
        )

        if activate and not thin_dev.create(dm_name):
            logging.error(f'Failed to activate thin device {dm_name}')
            # Clean up the thin device from pool
            self.message(0, f'"delete {thin_id}"')
            return None

        # Track the thin device
        self.thin_devices[thin_id] = thin_dev
        return thin_dev

    def delete_thin(self, thin_id: int, *, force: bool = False) -> bool:
        """Delete a thin volume from the pool.

        Removes the thin device mapping and deletes it from the pool.

        Args:
            thin_id: ID of the thin volume to delete
            force: Force removal even if device is in use

        Returns:
            True if successful, False otherwise

        Example:
            ```python
            pool.delete_thin(0)
            ```
        """
        if not self.is_created:
            logging.error('Pool must be created before deleting thin devices')
            return False

        # Remove the DM device if it's tracked and activated
        if thin_id in self.thin_devices:
            thin_dev = self.thin_devices[thin_id]
            if thin_dev.is_created and not thin_dev.remove(force=force):
                logging.error(f'Failed to remove thin device {thin_dev.dm_name}')
                return False

        # Delete thin from pool
        if not self.message(0, f'"delete {thin_id}"'):
            logging.error(f'Failed to delete thin device {thin_id} from pool')
            return False

        # Remove from tracking
        self.thin_devices.pop(thin_id, None)
        logging.info(f'Deleted thin device ID {thin_id} from pool {self.dm_name}')
        return True

    def create_snapshot(
        self,
        origin_id: int,
        snap_id: int,
        dm_name: str,
        *,
        size: int | None = None,
        activate: bool = True,
    ) -> ThinDevice | None:
        """Create a snapshot of an existing thin volume.

        Creates an internal snapshot using 'create_snap' message.
        The origin device should be suspended during snapshot creation.

        Args:
            origin_id: ID of the origin thin volume to snapshot
            snap_id: Unique ID for the new snapshot
            dm_name: Device mapper name for the activated snapshot
            size: Size in sectors (default: same as origin)
            activate: Whether to activate the snapshot device (default: True)

        Returns:
            ThinDevice instance for the snapshot if successful, None otherwise

        Example:
            ```python
            # Suspend origin before snapshot
            origin = pool.thin_devices[0]
            origin.suspend()

            # Create snapshot
            snap = pool.create_snapshot(origin_id=0, snap_id=1, dm_name='my-snap')

            # Resume origin
            origin.resume()
            ```
        """
        if not self.is_created:
            logging.error('Pool must be created before creating snapshots')
            return None

        if snap_id in self.thin_devices:
            logging.error(f'Thin device with ID {snap_id} already exists in pool')
            return None

        # Determine size from origin if not specified
        if size is None:
            if origin_id in self.thin_devices:
                size = self.thin_devices[origin_id].size_sectors
            else:
                logging.error(f'Origin {origin_id} not tracked, size must be specified')
                return None

        # Send create_snap message to pool
        if not self.message(0, f'"create_snap {snap_id} {origin_id}"'):
            logging.error(f'Failed to create snapshot {snap_id} of {origin_id}')
            return None

        logging.info(f'Created snapshot ID {snap_id} of origin {origin_id} in pool {self.dm_name}')

        # Create ThinDevice configuration for snapshot
        snap_dev = ThinDevice.create_from_pool(
            pool=self,
            thin_id=snap_id,
            size=size,
        )

        if activate and not snap_dev.create(dm_name):
            logging.error(f'Failed to activate snapshot device {dm_name}')
            # Clean up the snapshot from pool
            self.message(0, f'"delete {snap_id}"')
            return None

        # Track the snapshot device
        self.thin_devices[snap_id] = snap_dev
        return snap_dev

    def remove(self, *, force: bool = False, retry: bool = False, deferred: bool = False) -> bool:
        """Remove the thin pool device.

        Removes all child thin devices first, then removes the pool.

        Args:
            force: Replace table with one that fails all I/O
            retry: Retry removal for a few seconds if it fails
            deferred: Enable deferred removal when device is in use

        Returns:
            True if successful, False otherwise
        """
        # Remove all tracked thin devices first
        for thin_id in list(self.thin_devices.keys()):
            self.delete_thin(thin_id, force=force)

        # Remove the pool itself
        return super().remove(force=force, retry=retry, deferred=deferred)

__post_init__()

Set target type and initialize.

Source code in sts_libs/src/sts/dm/thin.py
67
68
69
70
def __post_init__(self) -> None:
    """Set target type and initialize."""
    self.target_type = 'thin-pool'
    super().__post_init__()

create_snapshot(origin_id, snap_id, dm_name, *, size=None, activate=True)

Create a snapshot of an existing thin volume.

Creates an internal snapshot using 'create_snap' message. The origin device should be suspended during snapshot creation.

Parameters:

Name Type Description Default
origin_id int

ID of the origin thin volume to snapshot

required
snap_id int

Unique ID for the new snapshot

required
dm_name str

Device mapper name for the activated snapshot

required
size int | None

Size in sectors (default: same as origin)

None
activate bool

Whether to activate the snapshot device (default: True)

True

Returns:

Type Description
ThinDevice | None

ThinDevice instance for the snapshot if successful, None otherwise

Example
# Suspend origin before snapshot
origin = pool.thin_devices[0]
origin.suspend()

# Create snapshot
snap = pool.create_snapshot(origin_id=0, snap_id=1, dm_name='my-snap')

# Resume origin
origin.resume()
Source code in sts_libs/src/sts/dm/thin.py
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
def create_snapshot(
    self,
    origin_id: int,
    snap_id: int,
    dm_name: str,
    *,
    size: int | None = None,
    activate: bool = True,
) -> ThinDevice | None:
    """Create a snapshot of an existing thin volume.

    Creates an internal snapshot using 'create_snap' message.
    The origin device should be suspended during snapshot creation.

    Args:
        origin_id: ID of the origin thin volume to snapshot
        snap_id: Unique ID for the new snapshot
        dm_name: Device mapper name for the activated snapshot
        size: Size in sectors (default: same as origin)
        activate: Whether to activate the snapshot device (default: True)

    Returns:
        ThinDevice instance for the snapshot if successful, None otherwise

    Example:
        ```python
        # Suspend origin before snapshot
        origin = pool.thin_devices[0]
        origin.suspend()

        # Create snapshot
        snap = pool.create_snapshot(origin_id=0, snap_id=1, dm_name='my-snap')

        # Resume origin
        origin.resume()
        ```
    """
    if not self.is_created:
        logging.error('Pool must be created before creating snapshots')
        return None

    if snap_id in self.thin_devices:
        logging.error(f'Thin device with ID {snap_id} already exists in pool')
        return None

    # Determine size from origin if not specified
    if size is None:
        if origin_id in self.thin_devices:
            size = self.thin_devices[origin_id].size_sectors
        else:
            logging.error(f'Origin {origin_id} not tracked, size must be specified')
            return None

    # Send create_snap message to pool
    if not self.message(0, f'"create_snap {snap_id} {origin_id}"'):
        logging.error(f'Failed to create snapshot {snap_id} of {origin_id}')
        return None

    logging.info(f'Created snapshot ID {snap_id} of origin {origin_id} in pool {self.dm_name}')

    # Create ThinDevice configuration for snapshot
    snap_dev = ThinDevice.create_from_pool(
        pool=self,
        thin_id=snap_id,
        size=size,
    )

    if activate and not snap_dev.create(dm_name):
        logging.error(f'Failed to activate snapshot device {dm_name}')
        # Clean up the snapshot from pool
        self.message(0, f'"delete {snap_id}"')
        return None

    # Track the snapshot device
    self.thin_devices[snap_id] = snap_dev
    return snap_dev

create_thin(thin_id, size, dm_name, *, activate=True)

Create a new thin volume in the pool.

Sends 'create_thin' message to the pool and optionally activates the device.

Parameters:

Name Type Description Default
thin_id int

Unique ID for this thin volume within the pool

required
size int

Size in sectors for the thin volume

required
dm_name str

Device mapper name for the activated device

required
activate bool

Whether to activate the device (default: True)

True

Returns:

Type Description
ThinDevice | None

ThinDevice instance if successful, None otherwise

Example
pool.create('my-pool')
thin = pool.create_thin(thin_id=0, size=2097152, dm_name='my-thin')
Source code in sts_libs/src/sts/dm/thin.py
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
def create_thin(
    self,
    thin_id: int,
    size: int,
    dm_name: str,
    *,
    activate: bool = True,
) -> ThinDevice | None:
    """Create a new thin volume in the pool.

    Sends 'create_thin' message to the pool and optionally activates the device.

    Args:
        thin_id: Unique ID for this thin volume within the pool
        size: Size in sectors for the thin volume
        dm_name: Device mapper name for the activated device
        activate: Whether to activate the device (default: True)

    Returns:
        ThinDevice instance if successful, None otherwise

    Example:
        ```python
        pool.create('my-pool')
        thin = pool.create_thin(thin_id=0, size=2097152, dm_name='my-thin')
        ```
    """
    if not self.is_created:
        logging.error('Pool must be created before creating thin devices')
        return None

    if thin_id in self.thin_devices:
        logging.error(f'Thin device with ID {thin_id} already exists in pool')
        return None

    # Send create_thin message to pool
    if not self.message(0, f'"create_thin {thin_id}"'):
        logging.error(f'Failed to create thin device {thin_id} in pool')
        return None

    logging.info(f'Created thin device ID {thin_id} in pool {self.dm_name}')

    # Create ThinDevice configuration
    thin_dev = ThinDevice.create_from_pool(
        pool=self,
        thin_id=thin_id,
        size=size,
    )

    if activate and not thin_dev.create(dm_name):
        logging.error(f'Failed to activate thin device {dm_name}')
        # Clean up the thin device from pool
        self.message(0, f'"delete {thin_id}"')
        return None

    # Track the thin device
    self.thin_devices[thin_id] = thin_dev
    return thin_dev

delete_thin(thin_id, *, force=False)

Delete a thin volume from the pool.

Removes the thin device mapping and deletes it from the pool.

Parameters:

Name Type Description Default
thin_id int

ID of the thin volume to delete

required
force bool

Force removal even if device is in use

False

Returns:

Type Description
bool

True if successful, False otherwise

Example
pool.delete_thin(0)
Source code in sts_libs/src/sts/dm/thin.py
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
def delete_thin(self, thin_id: int, *, force: bool = False) -> bool:
    """Delete a thin volume from the pool.

    Removes the thin device mapping and deletes it from the pool.

    Args:
        thin_id: ID of the thin volume to delete
        force: Force removal even if device is in use

    Returns:
        True if successful, False otherwise

    Example:
        ```python
        pool.delete_thin(0)
        ```
    """
    if not self.is_created:
        logging.error('Pool must be created before deleting thin devices')
        return False

    # Remove the DM device if it's tracked and activated
    if thin_id in self.thin_devices:
        thin_dev = self.thin_devices[thin_id]
        if thin_dev.is_created and not thin_dev.remove(force=force):
            logging.error(f'Failed to remove thin device {thin_dev.dm_name}')
            return False

    # Delete thin from pool
    if not self.message(0, f'"delete {thin_id}"'):
        logging.error(f'Failed to delete thin device {thin_id} from pool')
        return False

    # Remove from tracking
    self.thin_devices.pop(thin_id, None)
    logging.info(f'Deleted thin device ID {thin_id} from pool {self.dm_name}')
    return True

from_block_devices(metadata_device, data_device, block_size_sectors=128, low_water_mark=0, features=None, start=0, size=None) classmethod

Create ThinPoolDevice from BlockDevices.

Parameters:

Name Type Description Default
metadata_device BlockDevice

Device for storing thin pool metadata

required
data_device BlockDevice

Device for storing actual data

required
block_size_sectors int

Block size in sectors (default: 128 = 64KB)

128
low_water_mark int

Low water mark in sectors (default: 0 = auto)

0
features list[str] | None

List of thin pool features (default: ['skip_block_zeroing'])

None
start int

Start sector in virtual device (default: 0)

0
size int | None

Size in sectors (default: data device size)

None

Returns:

Type Description
ThinPoolDevice

ThinPoolDevice instance

Example
metadata_dev = BlockDevice('/dev/sdb1')  # Small device for metadata
data_dev = BlockDevice('/dev/sdb2')  # Large device for data
pool = ThinPoolDevice.from_block_devices(metadata_dev, data_dev)
Source code in sts_libs/src/sts/dm/thin.py
 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
@classmethod
def from_block_devices(
    cls,
    metadata_device: BlockDevice,
    data_device: BlockDevice,
    block_size_sectors: int = 128,
    low_water_mark: int = 0,
    features: list[str] | None = None,
    start: int = 0,
    size: int | None = None,
) -> ThinPoolDevice:
    """Create ThinPoolDevice from BlockDevices.

    Args:
        metadata_device: Device for storing thin pool metadata
        data_device: Device for storing actual data
        block_size_sectors: Block size in sectors (default: 128 = 64KB)
        low_water_mark: Low water mark in sectors (default: 0 = auto)
        features: List of thin pool features (default: ['skip_block_zeroing'])
        start: Start sector in virtual device (default: 0)
        size: Size in sectors (default: data device size)

    Returns:
        ThinPoolDevice instance

    Example:
        ```python
        metadata_dev = BlockDevice('/dev/sdb1')  # Small device for metadata
        data_dev = BlockDevice('/dev/sdb2')  # Large device for data
        pool = ThinPoolDevice.from_block_devices(metadata_dev, data_dev)
        ```
    """
    if size is None and data_device.size is not None:
        # Use data device size, converted to sectors using actual sector size
        size = data_device.size // data_device.sector_size

    if features is None:
        features = ['skip_block_zeroing']

    metadata_id = cls._get_device_identifier(metadata_device)
    data_id = cls._get_device_identifier(data_device)

    # Build args: <metadata dev> <data dev> <block size> <low water mark> <# feature args> <feature args>
    args_parts = [
        metadata_id,
        data_id,
        str(block_size_sectors),
        str(low_water_mark),
        str(len(features)),
        *features,
    ]

    args = ' '.join(args_parts)
    if size is None:
        raise ValueError('size must be provided or data_device.size must be available')
    return cls(start=start, size_sectors=size, args=args)

remove(*, force=False, retry=False, deferred=False)

Remove the thin pool device.

Removes all child thin devices first, then removes the pool.

Parameters:

Name Type Description Default
force bool

Replace table with one that fails all I/O

False
retry bool

Retry removal for a few seconds if it fails

False
deferred bool

Enable deferred removal when device is in use

False

Returns:

Type Description
bool

True if successful, False otherwise

Source code in sts_libs/src/sts/dm/thin.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
def remove(self, *, force: bool = False, retry: bool = False, deferred: bool = False) -> bool:
    """Remove the thin pool device.

    Removes all child thin devices first, then removes the pool.

    Args:
        force: Replace table with one that fails all I/O
        retry: Retry removal for a few seconds if it fails
        deferred: Enable deferred removal when device is in use

    Returns:
        True if successful, False otherwise
    """
    # Remove all tracked thin devices first
    for thin_id in list(self.thin_devices.keys()):
        self.delete_thin(thin_id, force=force)

    # Remove the pool itself
    return super().remove(force=force, retry=retry, deferred=deferred)

VdoDevice dataclass

Bases: DmDevice

VDO (Virtual Data Optimizer) target.

Provides block-level deduplication, compression, and thin provisioning. VDO is a device mapper target that can add these features to the storage stack.

Note: VDO volumes must be formatted with 'vdoformat' before use.

V4

[optional arguments]

Attributes:

Name Type Description
vdo_version str | None

VDO version string (e.g., 'V4')

storage_device str | None

Storage device identifier

storage_size_blocks int | None

Storage device size in 4096-byte blocks

minimum_io_size int | None

Minimum I/O size in bytes

block_map_cache_blocks int | None

Block map cache size in 4096-byte blocks

block_map_period int | None

Block map era length

ack_threads int | None

Number of ack threads

bio_threads int | None

Number of bio threads

bio_rotation int | None

Bio rotation interval

cpu_threads int | None

Number of CPU threads

hash_zone_threads int | None

Number of hash zone threads

logical_threads int | None

Number of logical threads

physical_threads int | None

Number of physical threads

max_discard int | None

Maximum discard size in 4096-byte blocks

deduplication bool | None

Whether deduplication is enabled

compression bool | None

Whether compression is enabled

Example
target = VdoTarget(0, 2097152, 'V4 /dev/sdb 262144 4096 32768 16380')
str(target)
'0 2097152 vdo V4 /dev/sdb 262144 4096 32768 16380'
target.storage_device
'/dev/sdb'
target.compression
False
Source code in sts_libs/src/sts/dm/vdo.py
 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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
@dataclass
class VdoDevice(DmDevice):
    """VDO (Virtual Data Optimizer) target.

    Provides block-level deduplication, compression, and thin provisioning.
    VDO is a device mapper target that can add these features to the storage stack.

    Note: VDO volumes must be formatted with 'vdoformat' before use.

    Args format: V4 <storage device> <storage device size> <minimum I/O size>
                 <block map cache size> <block map era length> [optional arguments]

    Attributes:
        vdo_version: VDO version string (e.g., 'V4')
        storage_device: Storage device identifier
        storage_size_blocks: Storage device size in 4096-byte blocks
        minimum_io_size: Minimum I/O size in bytes
        block_map_cache_blocks: Block map cache size in 4096-byte blocks
        block_map_period: Block map era length
        ack_threads: Number of ack threads
        bio_threads: Number of bio threads
        bio_rotation: Bio rotation interval
        cpu_threads: Number of CPU threads
        hash_zone_threads: Number of hash zone threads
        logical_threads: Number of logical threads
        physical_threads: Number of physical threads
        max_discard: Maximum discard size in 4096-byte blocks
        deduplication: Whether deduplication is enabled
        compression: Whether compression is enabled

    Example:
        ```python
        target = VdoTarget(0, 2097152, 'V4 /dev/sdb 262144 4096 32768 16380')
        str(target)
        '0 2097152 vdo V4 /dev/sdb 262144 4096 32768 16380'
        target.storage_device
        '/dev/sdb'
        target.compression
        False
        ```
    """

    # Parsed attributes (populated by refresh)
    # Names match dmsetup parameter names
    vdo_version: str | None = field(init=False, default=None)
    storage_device: str | None = field(init=False, default=None)
    storage_size_blocks: int | None = field(init=False, default=None)
    minimum_io_size: int | None = field(init=False, default=None)
    block_map_cache_blocks: int | None = field(init=False, default=None)
    block_map_period: int | None = field(init=False, default=None)
    ack: int | None = field(init=False, default=None)
    bio: int | None = field(init=False, default=None)
    bioRotationInterval: int | None = field(init=False, default=None)  # noqa: N815
    cpu: int | None = field(init=False, default=None)
    hash: int | None = field(init=False, default=None)
    logical: int | None = field(init=False, default=None)
    physical: int | None = field(init=False, default=None)
    maxDiscard: int | None = field(init=False, default=None)  # noqa: N815
    deduplication: bool | None = field(init=False, default=None)
    compression: bool | None = field(init=False, default=None)

    def __post_init__(self) -> None:
        """Set target type and initialize."""
        self.target_type = 'vdo'
        super().__post_init__()

    @classmethod
    def from_block_device(
        cls,
        device: BlockDevice,
        logical_size_sectors: int,
        *,
        start: int = 0,
        minimum_io_size: int = 4096,
        block_map_cache_size_mb: int = 128,
        block_map_period: int = 16380,
        **kwargs: int | bool | None,
    ) -> VdoDevice:
        """Create VdoTarget from BlockDevice.

        Args:
            device: Storage block device (must be pre-formatted with vdoformat)
            logical_size_sectors: Logical device size in 512-byte sectors
            start: Start sector in virtual device (default: 0)
            minimum_io_size: Minimum I/O size in bytes, 512 or 4096 (default: 4096)
            block_map_cache_size_mb: Block map cache size in MB (default: 128, min: 128)
            block_map_period: Block map era length (default: 16380, range: 1-16380)
            **kwargs: Optional VDO parameters (use dmsetup parameter names):
                ack: Number of ack threads (default: 1)
                bio: Number of bio threads (default: 4)
                bioRotationInterval: Bio rotation interval (default: 64, range: 1-1024)
                cpu: Number of CPU threads (default: 1)
                hash: Number of hash zone threads (default: 0, range: 0-100)
                logical: Number of logical threads (default: 0, range: 0-60)
                physical: Number of physical threads (default: 0, range: 0-16)
                maxDiscard: Maximum discard size in 4096-byte blocks (default: 1)
                deduplication: Enable deduplication (default: True)
                compression: Enable compression (default: False)

        Returns:
            VdoTarget instance

        Example:
            ```python
            device = BlockDevice('/dev/sdb')  # Pre-formatted with vdoformat
            target = VdoTarget.from_block_device(
                device,
                logical_size_sectors=2097152,  # 1 GB logical
                compression=True,
            )
            ```
        """
        # Get device identifier
        device_id = cls._get_device_identifier(device)

        # Calculate storage device size in 4096-byte blocks
        if device.size is None:
            raise ValueError('Cannot determine size: device size is unknown')
        storage_size_blocks = device.size // 4096

        # Convert block map cache size from MB to 4096-byte blocks
        block_map_cache_blocks = (block_map_cache_size_mb * 1024 * 1024) // 4096

        # Build required arguments
        args_parts = [
            'V4',
            device_id,
            str(storage_size_blocks),
            str(minimum_io_size),
            str(block_map_cache_blocks),
            str(block_map_period),
        ]

        # Build optional arguments as key-value pairs
        # NOTE: dm-vdo kernel constraint: if any of hash/logical/physical threads is
        # non-zero, all three must be specified and non-zero. This is NOT validated here
        # to allow testing negative cases - the kernel will reject invalid configurations.

        # Valid dmsetup parameter names for thread/discard settings
        optional_params = ('ack', 'bio', 'bioRotationInterval', 'cpu', 'hash', 'logical', 'physical', 'maxDiscard')
        optional_args: list[str] = []

        for param_name in optional_params:
            value = kwargs.get(param_name)
            if value is not None:
                optional_args.extend([param_name, str(value)])

        # Handle deduplication (only add if False, as True is default)
        dedup_val = kwargs.get('deduplication', True)
        deduplication_enabled = bool(dedup_val) if dedup_val is not None else True
        if not deduplication_enabled:
            optional_args.extend(['deduplication', 'off'])

        # Handle compression (only add if True, as False is default)
        comp_val = kwargs.get('compression', False)
        compression_enabled = bool(comp_val) if comp_val is not None else False
        if compression_enabled:
            optional_args.extend(['compression', 'on'])

        # Combine all arguments
        if optional_args:
            args_parts.extend(optional_args)

        args = ' '.join(args_parts)
        target = cls(start=start, size_sectors=logical_size_sectors, args=args)

        # Set attributes directly - we know the values
        target.vdo_version = 'V4'
        target.storage_device = device_id
        target.storage_size_blocks = storage_size_blocks
        target.minimum_io_size = minimum_io_size
        target.block_map_cache_blocks = block_map_cache_blocks
        target.block_map_period = block_map_period
        target.deduplication = deduplication_enabled
        target.compression = compression_enabled

        # Set optional parameters from kwargs
        for param_name in optional_params:
            value = kwargs.get(param_name)
            if value is not None:
                setattr(target, param_name, value)

        return target

    def refresh(self) -> None:
        """Parse args and update instance attributes.

        Extracts all VDO parameters from the args string and updates
        instance attributes. Useful after modifying args or when
        reconstructing from dmsetup table output.

        Updates:
            - vdo_version, storage_device, storage_size_blocks (required)
            - minimum_io_size, block_map_cache_blocks, block_map_period (required)
            - ack_threads, bio_threads, bio_rotation, cpu_threads (optional)
            - hash_zone_threads, logical_threads, physical_threads (optional)
            - max_discard, deduplication, compression (optional)

        Example:
            ```python
            target = VdoTarget(0, 2097152, 'V4 8:16 262144 4096 32768 16380 compression on')
            target.storage_device  # '8:16'
            target.compression  # True
            ```
        """
        parts = self.args.split()

        if len(parts) < 6:
            return

        # Parse required positional arguments
        self.vdo_version = parts[0]  # V4
        self.storage_device = parts[1]
        self.storage_size_blocks = int(parts[2])
        self.minimum_io_size = int(parts[3])
        self.block_map_cache_blocks = int(parts[4])
        self.block_map_period = int(parts[5])

        # Parse optional key-value arguments
        i = 6
        while i < len(parts) - 1:
            key = parts[i]
            value = parts[i + 1]

            # dmsetup parameter names used directly as attribute names
            if key == 'ack':
                self.ack = int(value)
            elif key == 'bio':
                self.bio = int(value)
            elif key == 'bioRotationInterval':
                self.bioRotationInterval = int(value)
            elif key == 'cpu':
                self.cpu = int(value)
            elif key == 'hash':
                self.hash = int(value)
            elif key == 'logical':
                self.logical = int(value)
            elif key == 'physical':
                self.physical = int(value)
            elif key == 'maxDiscard':
                self.maxDiscard = int(value)
            elif key == 'deduplication':
                self.deduplication = value == 'on'
            elif key == 'compression':
                self.compression = value == 'on'

            i += 2

    @classmethod
    def from_table_line(cls, table_line: str) -> VdoDevice | None:
        """Create VdoTarget from a dmsetup table line.

        Parses a full table line (including start, size, and type) and
        creates a VdoTarget instance. Useful for reconstructing targets
        from existing devices.

        Args:
            table_line: Full table line from 'dmsetup table' output

        Returns:
            VdoTarget instance or None if parsing fails

        Example:
            ```python
            line = '0 2097152 vdo V4 8:16 262144 4096 32768 16380'
            target = VdoTarget.from_table_line(line)
            target.size  # 2097152
            ```
        """
        parts = table_line.strip().split(None, 3)
        if len(parts) < 4:
            logging.warning(f'Invalid table line: {table_line}')
            return None

        start = int(parts[0])
        size = int(parts[1])
        target_type = parts[2]
        args = parts[3]

        if target_type != 'vdo':
            logging.warning(f'Not a VDO target: {target_type}')
            return None

        target = cls(start=start, size_sectors=size, args=args)
        target.refresh()  # Parse args to populate attributes
        return target

    @staticmethod
    def parse_status(status_line: str) -> dict[str, str | int]:
        """Parse VDO status output into a dictionary.

        Parses the output from 'dmsetup status' for a VDO device.

        Status format from dmsetup:
            <start> <size> vdo <device> <operating mode> <in recovery> <index state>
            <compression state> <physical blocks used> <total physical blocks>

        Args:
            status_line: Status line from 'dmsetup status' output

        Returns:
            Dictionary with parsed status fields:
            - start: Start sector
            - size: Size in sectors
            - device: VDO storage device
            - operating_mode: 'normal', 'recovering', or 'read-only'
            - in_recovery: 'recovering' or '-'
            - index_state: 'closed', 'closing', 'error', 'offline', 'online', 'opening', 'unknown'
            - compression_state: 'offline' or 'online'
            - physical_blocks_used: Number of physical blocks in use
            - total_physical_blocks: Total physical blocks available

        Example:
            ```python
            status = '0 2097152 vdo /dev/loop0 normal - online online 12345 262144'
            parsed = VdoDevice.parse_status(status)
            parsed['operating_mode']  # 'normal'
            parsed['physical_blocks_used']  # 12345
            ```
        """
        parts = status_line.strip().split()
        result: dict[str, str | int] = {}

        # Format: <start> <size> vdo <device> <mode> <recovery> <index> <compression> <used> <total>
        # Minimum 10 parts expected
        if len(parts) < 10:
            logging.warning(f'Invalid VDO status line (expected 10+ parts, got {len(parts)}): {status_line}')
            return result

        try:
            result['start'] = int(parts[0])
            result['size'] = int(parts[1])
        except ValueError:
            logging.warning(f'Failed to parse start/size from status: {status_line}')
            return result

        # parts[2] is 'vdo' (target type)
        result['device'] = parts[3]
        result['operating_mode'] = parts[4]
        result['in_recovery'] = parts[5]
        result['index_state'] = parts[6]
        result['compression_state'] = parts[7]

        try:
            result['physical_blocks_used'] = int(parts[8])
            result['total_physical_blocks'] = int(parts[9])
        except ValueError:
            logging.warning(f'Failed to parse block counts from status: {status_line}')

        return result

    @classmethod
    def create_positional(
        cls,
        device_path: str,
        storage_size_blocks: int,
        logical_size_sectors: int,
        *,
        start: int = 0,
        minimum_io_size: int = 4096,
        block_map_cache_blocks: int = 32768,
        block_map_period: int = 16380,
        **kwargs: int | str | bool,
    ) -> VdoDevice:
        """Create VdoTarget with positional arguments.

        This method allows direct specification of VDO parameters matching
        the dmsetup table format.

        Args:
            device_path: Storage device path (e.g., '/dev/sdb' or '8:16')
            storage_size_blocks: Storage device size in 4096-byte blocks
            logical_size_sectors: Logical device size in 512-byte sectors
            start: Start sector in virtual device (default: 0)
            minimum_io_size: Minimum I/O size in bytes (512 or 4096)
            block_map_cache_blocks: Block map cache in 4096-byte blocks (min: 32768)
            block_map_period: Block map era length (range: 1-16380)
            **kwargs: Optional key-value arguments (ack, bio, cpu, hash,
                     logical, physical, maxDiscard, deduplication, compression)

        Returns:
            VdoTarget instance

        Example:
            ```python
            target = VdoTarget.create_positional(
                '/dev/sdb',
                storage_size_blocks=262144,  # 1 GB
                logical_size_sectors=2097152,  # 1 GB
                compression='on',
            )
            ```
        """
        # Build required arguments
        args_parts = [
            'V4',
            device_path,
            str(storage_size_blocks),
            str(minimum_io_size),
            str(block_map_cache_blocks),
            str(block_map_period),
        ]

        # Process optional arguments
        for key, value in kwargs.items():
            if isinstance(value, bool):
                args_parts.extend([key, 'on' if value else 'off'])
            else:
                args_parts.extend([key, str(value)])

        args = ' '.join(args_parts)
        target = cls(start=start, size_sectors=logical_size_sectors, args=args)

        # Set attributes directly - we know the values
        target.vdo_version = 'V4'
        target.storage_device = device_path
        target.storage_size_blocks = storage_size_blocks
        target.minimum_io_size = minimum_io_size
        target.block_map_cache_blocks = block_map_cache_blocks
        target.block_map_period = block_map_period

        # Set optional parameters from kwargs (dmsetup names used directly)
        for key, value in kwargs.items():
            if isinstance(value, bool):
                setattr(target, key, value)
            elif isinstance(value, str) and value in ('on', 'off'):
                setattr(target, key, value == 'on')
            else:
                setattr(target, key, value)

        return target

__post_init__()

Set target type and initialize.

Source code in sts_libs/src/sts/dm/vdo.py
109
110
111
112
def __post_init__(self) -> None:
    """Set target type and initialize."""
    self.target_type = 'vdo'
    super().__post_init__()

create_positional(device_path, storage_size_blocks, logical_size_sectors, *, start=0, minimum_io_size=4096, block_map_cache_blocks=32768, block_map_period=16380, **kwargs) classmethod

Create VdoTarget with positional arguments.

This method allows direct specification of VDO parameters matching the dmsetup table format.

Parameters:

Name Type Description Default
device_path str

Storage device path (e.g., '/dev/sdb' or '8:16')

required
storage_size_blocks int

Storage device size in 4096-byte blocks

required
logical_size_sectors int

Logical device size in 512-byte sectors

required
start int

Start sector in virtual device (default: 0)

0
minimum_io_size int

Minimum I/O size in bytes (512 or 4096)

4096
block_map_cache_blocks int

Block map cache in 4096-byte blocks (min: 32768)

32768
block_map_period int

Block map era length (range: 1-16380)

16380
**kwargs int | str | bool

Optional key-value arguments (ack, bio, cpu, hash, logical, physical, maxDiscard, deduplication, compression)

{}

Returns:

Type Description
VdoDevice

VdoTarget instance

Example
target = VdoTarget.create_positional(
    '/dev/sdb',
    storage_size_blocks=262144,  # 1 GB
    logical_size_sectors=2097152,  # 1 GB
    compression='on',
)
Source code in sts_libs/src/sts/dm/vdo.py
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
@classmethod
def create_positional(
    cls,
    device_path: str,
    storage_size_blocks: int,
    logical_size_sectors: int,
    *,
    start: int = 0,
    minimum_io_size: int = 4096,
    block_map_cache_blocks: int = 32768,
    block_map_period: int = 16380,
    **kwargs: int | str | bool,
) -> VdoDevice:
    """Create VdoTarget with positional arguments.

    This method allows direct specification of VDO parameters matching
    the dmsetup table format.

    Args:
        device_path: Storage device path (e.g., '/dev/sdb' or '8:16')
        storage_size_blocks: Storage device size in 4096-byte blocks
        logical_size_sectors: Logical device size in 512-byte sectors
        start: Start sector in virtual device (default: 0)
        minimum_io_size: Minimum I/O size in bytes (512 or 4096)
        block_map_cache_blocks: Block map cache in 4096-byte blocks (min: 32768)
        block_map_period: Block map era length (range: 1-16380)
        **kwargs: Optional key-value arguments (ack, bio, cpu, hash,
                 logical, physical, maxDiscard, deduplication, compression)

    Returns:
        VdoTarget instance

    Example:
        ```python
        target = VdoTarget.create_positional(
            '/dev/sdb',
            storage_size_blocks=262144,  # 1 GB
            logical_size_sectors=2097152,  # 1 GB
            compression='on',
        )
        ```
    """
    # Build required arguments
    args_parts = [
        'V4',
        device_path,
        str(storage_size_blocks),
        str(minimum_io_size),
        str(block_map_cache_blocks),
        str(block_map_period),
    ]

    # Process optional arguments
    for key, value in kwargs.items():
        if isinstance(value, bool):
            args_parts.extend([key, 'on' if value else 'off'])
        else:
            args_parts.extend([key, str(value)])

    args = ' '.join(args_parts)
    target = cls(start=start, size_sectors=logical_size_sectors, args=args)

    # Set attributes directly - we know the values
    target.vdo_version = 'V4'
    target.storage_device = device_path
    target.storage_size_blocks = storage_size_blocks
    target.minimum_io_size = minimum_io_size
    target.block_map_cache_blocks = block_map_cache_blocks
    target.block_map_period = block_map_period

    # Set optional parameters from kwargs (dmsetup names used directly)
    for key, value in kwargs.items():
        if isinstance(value, bool):
            setattr(target, key, value)
        elif isinstance(value, str) and value in ('on', 'off'):
            setattr(target, key, value == 'on')
        else:
            setattr(target, key, value)

    return target

from_block_device(device, logical_size_sectors, *, start=0, minimum_io_size=4096, block_map_cache_size_mb=128, block_map_period=16380, **kwargs) classmethod

Create VdoTarget from BlockDevice.

Parameters:

Name Type Description Default
device BlockDevice

Storage block device (must be pre-formatted with vdoformat)

required
logical_size_sectors int

Logical device size in 512-byte sectors

required
start int

Start sector in virtual device (default: 0)

0
minimum_io_size int

Minimum I/O size in bytes, 512 or 4096 (default: 4096)

4096
block_map_cache_size_mb int

Block map cache size in MB (default: 128, min: 128)

128
block_map_period int

Block map era length (default: 16380, range: 1-16380)

16380
**kwargs int | bool | None

Optional VDO parameters (use dmsetup parameter names): ack: Number of ack threads (default: 1) bio: Number of bio threads (default: 4) bioRotationInterval: Bio rotation interval (default: 64, range: 1-1024) cpu: Number of CPU threads (default: 1) hash: Number of hash zone threads (default: 0, range: 0-100) logical: Number of logical threads (default: 0, range: 0-60) physical: Number of physical threads (default: 0, range: 0-16) maxDiscard: Maximum discard size in 4096-byte blocks (default: 1) deduplication: Enable deduplication (default: True) compression: Enable compression (default: False)

{}

Returns:

Type Description
VdoDevice

VdoTarget instance

Example
device = BlockDevice('/dev/sdb')  # Pre-formatted with vdoformat
target = VdoTarget.from_block_device(
    device,
    logical_size_sectors=2097152,  # 1 GB logical
    compression=True,
)
Source code in sts_libs/src/sts/dm/vdo.py
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
@classmethod
def from_block_device(
    cls,
    device: BlockDevice,
    logical_size_sectors: int,
    *,
    start: int = 0,
    minimum_io_size: int = 4096,
    block_map_cache_size_mb: int = 128,
    block_map_period: int = 16380,
    **kwargs: int | bool | None,
) -> VdoDevice:
    """Create VdoTarget from BlockDevice.

    Args:
        device: Storage block device (must be pre-formatted with vdoformat)
        logical_size_sectors: Logical device size in 512-byte sectors
        start: Start sector in virtual device (default: 0)
        minimum_io_size: Minimum I/O size in bytes, 512 or 4096 (default: 4096)
        block_map_cache_size_mb: Block map cache size in MB (default: 128, min: 128)
        block_map_period: Block map era length (default: 16380, range: 1-16380)
        **kwargs: Optional VDO parameters (use dmsetup parameter names):
            ack: Number of ack threads (default: 1)
            bio: Number of bio threads (default: 4)
            bioRotationInterval: Bio rotation interval (default: 64, range: 1-1024)
            cpu: Number of CPU threads (default: 1)
            hash: Number of hash zone threads (default: 0, range: 0-100)
            logical: Number of logical threads (default: 0, range: 0-60)
            physical: Number of physical threads (default: 0, range: 0-16)
            maxDiscard: Maximum discard size in 4096-byte blocks (default: 1)
            deduplication: Enable deduplication (default: True)
            compression: Enable compression (default: False)

    Returns:
        VdoTarget instance

    Example:
        ```python
        device = BlockDevice('/dev/sdb')  # Pre-formatted with vdoformat
        target = VdoTarget.from_block_device(
            device,
            logical_size_sectors=2097152,  # 1 GB logical
            compression=True,
        )
        ```
    """
    # Get device identifier
    device_id = cls._get_device_identifier(device)

    # Calculate storage device size in 4096-byte blocks
    if device.size is None:
        raise ValueError('Cannot determine size: device size is unknown')
    storage_size_blocks = device.size // 4096

    # Convert block map cache size from MB to 4096-byte blocks
    block_map_cache_blocks = (block_map_cache_size_mb * 1024 * 1024) // 4096

    # Build required arguments
    args_parts = [
        'V4',
        device_id,
        str(storage_size_blocks),
        str(minimum_io_size),
        str(block_map_cache_blocks),
        str(block_map_period),
    ]

    # Build optional arguments as key-value pairs
    # NOTE: dm-vdo kernel constraint: if any of hash/logical/physical threads is
    # non-zero, all three must be specified and non-zero. This is NOT validated here
    # to allow testing negative cases - the kernel will reject invalid configurations.

    # Valid dmsetup parameter names for thread/discard settings
    optional_params = ('ack', 'bio', 'bioRotationInterval', 'cpu', 'hash', 'logical', 'physical', 'maxDiscard')
    optional_args: list[str] = []

    for param_name in optional_params:
        value = kwargs.get(param_name)
        if value is not None:
            optional_args.extend([param_name, str(value)])

    # Handle deduplication (only add if False, as True is default)
    dedup_val = kwargs.get('deduplication', True)
    deduplication_enabled = bool(dedup_val) if dedup_val is not None else True
    if not deduplication_enabled:
        optional_args.extend(['deduplication', 'off'])

    # Handle compression (only add if True, as False is default)
    comp_val = kwargs.get('compression', False)
    compression_enabled = bool(comp_val) if comp_val is not None else False
    if compression_enabled:
        optional_args.extend(['compression', 'on'])

    # Combine all arguments
    if optional_args:
        args_parts.extend(optional_args)

    args = ' '.join(args_parts)
    target = cls(start=start, size_sectors=logical_size_sectors, args=args)

    # Set attributes directly - we know the values
    target.vdo_version = 'V4'
    target.storage_device = device_id
    target.storage_size_blocks = storage_size_blocks
    target.minimum_io_size = minimum_io_size
    target.block_map_cache_blocks = block_map_cache_blocks
    target.block_map_period = block_map_period
    target.deduplication = deduplication_enabled
    target.compression = compression_enabled

    # Set optional parameters from kwargs
    for param_name in optional_params:
        value = kwargs.get(param_name)
        if value is not None:
            setattr(target, param_name, value)

    return target

from_table_line(table_line) classmethod

Create VdoTarget from a dmsetup table line.

Parses a full table line (including start, size, and type) and creates a VdoTarget instance. Useful for reconstructing targets from existing devices.

Parameters:

Name Type Description Default
table_line str

Full table line from 'dmsetup table' output

required

Returns:

Type Description
VdoDevice | None

VdoTarget instance or None if parsing fails

Example
line = '0 2097152 vdo V4 8:16 262144 4096 32768 16380'
target = VdoTarget.from_table_line(line)
target.size  # 2097152
Source code in sts_libs/src/sts/dm/vdo.py
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
@classmethod
def from_table_line(cls, table_line: str) -> VdoDevice | None:
    """Create VdoTarget from a dmsetup table line.

    Parses a full table line (including start, size, and type) and
    creates a VdoTarget instance. Useful for reconstructing targets
    from existing devices.

    Args:
        table_line: Full table line from 'dmsetup table' output

    Returns:
        VdoTarget instance or None if parsing fails

    Example:
        ```python
        line = '0 2097152 vdo V4 8:16 262144 4096 32768 16380'
        target = VdoTarget.from_table_line(line)
        target.size  # 2097152
        ```
    """
    parts = table_line.strip().split(None, 3)
    if len(parts) < 4:
        logging.warning(f'Invalid table line: {table_line}')
        return None

    start = int(parts[0])
    size = int(parts[1])
    target_type = parts[2]
    args = parts[3]

    if target_type != 'vdo':
        logging.warning(f'Not a VDO target: {target_type}')
        return None

    target = cls(start=start, size_sectors=size, args=args)
    target.refresh()  # Parse args to populate attributes
    return target

parse_status(status_line) staticmethod

Parse VDO status output into a dictionary.

Parses the output from 'dmsetup status' for a VDO device.

Status format from dmsetup

vdo

Parameters:

Name Type Description Default
status_line str

Status line from 'dmsetup status' output

required

Returns:

Type Description
dict[str, str | int]

Dictionary with parsed status fields:

dict[str, str | int]
  • start: Start sector
dict[str, str | int]
  • size: Size in sectors
dict[str, str | int]
  • device: VDO storage device
dict[str, str | int]
  • operating_mode: 'normal', 'recovering', or 'read-only'
dict[str, str | int]
  • in_recovery: 'recovering' or '-'
dict[str, str | int]
  • index_state: 'closed', 'closing', 'error', 'offline', 'online', 'opening', 'unknown'
dict[str, str | int]
  • compression_state: 'offline' or 'online'
dict[str, str | int]
  • physical_blocks_used: Number of physical blocks in use
dict[str, str | int]
  • total_physical_blocks: Total physical blocks available
Example
status = '0 2097152 vdo /dev/loop0 normal - online online 12345 262144'
parsed = VdoDevice.parse_status(status)
parsed['operating_mode']  # 'normal'
parsed['physical_blocks_used']  # 12345
Source code in sts_libs/src/sts/dm/vdo.py
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
@staticmethod
def parse_status(status_line: str) -> dict[str, str | int]:
    """Parse VDO status output into a dictionary.

    Parses the output from 'dmsetup status' for a VDO device.

    Status format from dmsetup:
        <start> <size> vdo <device> <operating mode> <in recovery> <index state>
        <compression state> <physical blocks used> <total physical blocks>

    Args:
        status_line: Status line from 'dmsetup status' output

    Returns:
        Dictionary with parsed status fields:
        - start: Start sector
        - size: Size in sectors
        - device: VDO storage device
        - operating_mode: 'normal', 'recovering', or 'read-only'
        - in_recovery: 'recovering' or '-'
        - index_state: 'closed', 'closing', 'error', 'offline', 'online', 'opening', 'unknown'
        - compression_state: 'offline' or 'online'
        - physical_blocks_used: Number of physical blocks in use
        - total_physical_blocks: Total physical blocks available

    Example:
        ```python
        status = '0 2097152 vdo /dev/loop0 normal - online online 12345 262144'
        parsed = VdoDevice.parse_status(status)
        parsed['operating_mode']  # 'normal'
        parsed['physical_blocks_used']  # 12345
        ```
    """
    parts = status_line.strip().split()
    result: dict[str, str | int] = {}

    # Format: <start> <size> vdo <device> <mode> <recovery> <index> <compression> <used> <total>
    # Minimum 10 parts expected
    if len(parts) < 10:
        logging.warning(f'Invalid VDO status line (expected 10+ parts, got {len(parts)}): {status_line}')
        return result

    try:
        result['start'] = int(parts[0])
        result['size'] = int(parts[1])
    except ValueError:
        logging.warning(f'Failed to parse start/size from status: {status_line}')
        return result

    # parts[2] is 'vdo' (target type)
    result['device'] = parts[3]
    result['operating_mode'] = parts[4]
    result['in_recovery'] = parts[5]
    result['index_state'] = parts[6]
    result['compression_state'] = parts[7]

    try:
        result['physical_blocks_used'] = int(parts[8])
        result['total_physical_blocks'] = int(parts[9])
    except ValueError:
        logging.warning(f'Failed to parse block counts from status: {status_line}')

    return result

refresh()

Parse args and update instance attributes.

Extracts all VDO parameters from the args string and updates instance attributes. Useful after modifying args or when reconstructing from dmsetup table output.

Updates
  • vdo_version, storage_device, storage_size_blocks (required)
  • minimum_io_size, block_map_cache_blocks, block_map_period (required)
  • ack_threads, bio_threads, bio_rotation, cpu_threads (optional)
  • hash_zone_threads, logical_threads, physical_threads (optional)
  • max_discard, deduplication, compression (optional)
Example
target = VdoTarget(0, 2097152, 'V4 8:16 262144 4096 32768 16380 compression on')
target.storage_device  # '8:16'
target.compression  # True
Source code in sts_libs/src/sts/dm/vdo.py
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
def refresh(self) -> None:
    """Parse args and update instance attributes.

    Extracts all VDO parameters from the args string and updates
    instance attributes. Useful after modifying args or when
    reconstructing from dmsetup table output.

    Updates:
        - vdo_version, storage_device, storage_size_blocks (required)
        - minimum_io_size, block_map_cache_blocks, block_map_period (required)
        - ack_threads, bio_threads, bio_rotation, cpu_threads (optional)
        - hash_zone_threads, logical_threads, physical_threads (optional)
        - max_discard, deduplication, compression (optional)

    Example:
        ```python
        target = VdoTarget(0, 2097152, 'V4 8:16 262144 4096 32768 16380 compression on')
        target.storage_device  # '8:16'
        target.compression  # True
        ```
    """
    parts = self.args.split()

    if len(parts) < 6:
        return

    # Parse required positional arguments
    self.vdo_version = parts[0]  # V4
    self.storage_device = parts[1]
    self.storage_size_blocks = int(parts[2])
    self.minimum_io_size = int(parts[3])
    self.block_map_cache_blocks = int(parts[4])
    self.block_map_period = int(parts[5])

    # Parse optional key-value arguments
    i = 6
    while i < len(parts) - 1:
        key = parts[i]
        value = parts[i + 1]

        # dmsetup parameter names used directly as attribute names
        if key == 'ack':
            self.ack = int(value)
        elif key == 'bio':
            self.bio = int(value)
        elif key == 'bioRotationInterval':
            self.bioRotationInterval = int(value)
        elif key == 'cpu':
            self.cpu = int(value)
        elif key == 'hash':
            self.hash = int(value)
        elif key == 'logical':
            self.logical = int(value)
        elif key == 'physical':
            self.physical = int(value)
        elif key == 'maxDiscard':
            self.maxDiscard = int(value)
        elif key == 'deduplication':
            self.deduplication = value == 'on'
        elif key == 'compression':
            self.compression = value == 'on'

        i += 2

ZeroDevice dataclass

Bases: DmDevice

Zero target.

Returns blocks of zeros when read. Writes are discarded. Useful for creating sparse devices or testing.

Args format: (no arguments)

Example
target = ZeroTarget(0, 1000000, '')
str(target)
'0 1000000 zero'
Source code in sts_libs/src/sts/dm/zero.py
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
@dataclass
class ZeroDevice(DmDevice):
    """Zero target.

    Returns blocks of zeros when read. Writes are discarded.
    Useful for creating sparse devices or testing.

    Args format: (no arguments)

    Example:
        ```python
        target = ZeroTarget(0, 1000000, '')
        str(target)
        '0 1000000 zero'
        ```
    """

    def __post_init__(self) -> None:
        """Set target type and initialize."""
        self.target_type = 'zero'
        super().__post_init__()

    @classmethod
    def create_config(
        cls,
        start: int = 0,
        size: int | None = None,
    ) -> ZeroDevice:
        """Create ZeroTarget.

        Args:
            start: Start sector in virtual device (default: 0)
            size: Size in sectors (required)

        Returns:
            ZeroTarget instance

        Example:
            ```python
            target = ZeroTarget.create(size=2097152)
            str(target)
            '0 2097152 zero'
            ```
        """
        if size is None:
            raise ValueError('Size must be specified for zero targets')

        return cls(start=start, size_sectors=size, args='')

__post_init__()

Set target type and initialize.

Source code in sts_libs/src/sts/dm/zero.py
60
61
62
63
def __post_init__(self) -> None:
    """Set target type and initialize."""
    self.target_type = 'zero'
    super().__post_init__()

create_config(start=0, size=None) classmethod

Create ZeroTarget.

Parameters:

Name Type Description Default
start int

Start sector in virtual device (default: 0)

0
size int | None

Size in sectors (required)

None

Returns:

Type Description
ZeroDevice

ZeroTarget instance

Example
target = ZeroTarget.create(size=2097152)
str(target)
'0 2097152 zero'
Source code in sts_libs/src/sts/dm/zero.py
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
@classmethod
def create_config(
    cls,
    start: int = 0,
    size: int | None = None,
) -> ZeroDevice:
    """Create ZeroTarget.

    Args:
        start: Start sector in virtual device (default: 0)
        size: Size in sectors (required)

    Returns:
        ZeroTarget instance

    Example:
        ```python
        target = ZeroTarget.create(size=2097152)
        str(target)
        '0 2097152 zero'
        ```
    """
    if size is None:
        raise ValueError('Size must be specified for zero targets')

    return cls(start=start, size_sectors=size, args='')

Device Mapper Persistent Data

sts.dmpd

Device Mapper Persistent Data (DMPD) tools management.

This module provides functionality for managing Device Mapper Persistent Data tools. Each tool is a separate command-line utility for managing metadata of device-mapper targets:

Cache Tools: - cache_check: Check cache metadata integrity - cache_dump: Dump cache metadata to file or stdout - cache_repair: Repair corrupted cache metadata - cache_restore: Restore cache metadata from backup - cache_metadata_size: Calculate cache metadata size requirements

Thin Tools: - thin_check: Check thin provisioning metadata integrity - thin_dump: Dump thin metadata to file or stdout - thin_repair: Repair corrupted thin metadata - thin_restore: Restore thin metadata from backup - thin_metadata_size: Calculate thin metadata size requirements - thin_trim: Trim thin metadata

Era Tools: - era_check: Check era metadata integrity - era_dump: Dump era metadata to file or stdout - era_repair: Repair corrupted era metadata - era_restore: Restore era metadata from backup

Each tool operates independently and can work with various device sources: - Block devices (/dev/sdX, /dev/mapper/*, /dev/vg/lv) - Regular files (metadata dumps) - Device mapper devices

CacheCheck

Bases: DmpdTool

Check cache metadata integrity.

Usage: cache_check [options] {device|file}

For available options, see: cache_check --help

Source code in sts_libs/src/sts/dmpd.py
119
120
121
122
123
124
125
126
127
class CacheCheck(DmpdTool):
    """Check cache metadata integrity.

    Usage: cache_check [options] {device|file}

    For available options, see: cache_check --help
    """

    TOOL_NAME = 'cache_check'

CacheDump

Bases: DmpdTool

Dump cache metadata to stdout or file.

Usage: cache_dump [options] {device|file}

For available options, see: cache_dump --help

Source code in sts_libs/src/sts/dmpd.py
130
131
132
133
134
135
136
137
138
class CacheDump(DmpdTool):
    """Dump cache metadata to stdout or file.

    Usage: cache_dump [options] {device|file}

    For available options, see: cache_dump --help
    """

    TOOL_NAME = 'cache_dump'

CacheMetadataSize

Bases: DmpdTool

Calculate cache metadata size requirements.

Usage: cache_metadata_size [options]

For available options, see: cache_metadata_size --help

Source code in sts_libs/src/sts/dmpd.py
163
164
165
166
167
168
169
170
171
class CacheMetadataSize(DmpdTool):
    """Calculate cache metadata size requirements.

    Usage: cache_metadata_size [options]

    For available options, see: cache_metadata_size --help
    """

    TOOL_NAME = 'cache_metadata_size'

CacheRepair

Bases: DmpdTool

Repair corrupted cache metadata.

Usage: cache_repair [options] --input {device|file} --output {device|file}

For available options, see: cache_repair --help

Source code in sts_libs/src/sts/dmpd.py
141
142
143
144
145
146
147
148
149
class CacheRepair(DmpdTool):
    """Repair corrupted cache metadata.

    Usage: cache_repair [options] --input {device|file} --output {device|file}

    For available options, see: cache_repair --help
    """

    TOOL_NAME = 'cache_repair'

CacheRestore

Bases: DmpdTool

Restore cache metadata from backup.

Usage: cache_restore [options] --input --output {device|file}

For available options, see: cache_restore --help

Source code in sts_libs/src/sts/dmpd.py
152
153
154
155
156
157
158
159
160
class CacheRestore(DmpdTool):
    """Restore cache metadata from backup.

    Usage: cache_restore [options] --input <file> --output {device|file}

    For available options, see: cache_restore --help
    """

    TOOL_NAME = 'cache_restore'

DmpdTool

Bases: ABC

Base class for DMPD command-line tools.

Each DMPD tool is a separate command-line utility that can operate on: - Block devices (LVM logical volumes, raw devices) - Regular files (metadata dump files) - Device mapper devices

Source code in sts_libs/src/sts/dmpd.py
 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
class DmpdTool(ABC):
    """Base class for DMPD command-line tools.

    Each DMPD tool is a separate command-line utility that can operate on:
    - Block devices (LVM logical volumes, raw devices)
    - Regular files (metadata dump files)
    - Device mapper devices
    """

    # Tool name (must be set by subclasses)
    TOOL_NAME: ClassVar[str]

    @staticmethod
    def _build_command(cmd: str, device_or_file: str | Path | None = None, **options: CommandOption) -> str:
        """Build DMPD command string with options.

        Args:
            cmd: Command name (e.g., 'cache_check')
            device_or_file: Device path or file path
            **options: Command options

        Returns:
            Complete command string
        """
        command_parts = [cmd]

        # Add device/file path if provided
        if device_or_file:
            command_parts.append(str(device_or_file))

        # Add options
        for option_key, value in options.items():
            if value is not None:
                key = option_key.replace('_', '-')
                if isinstance(value, bool) and value:
                    command_parts.append(f'--{key}')
                else:
                    command_parts.append(f'--{key}={value!s}')

        return ' '.join(command_parts)

    @classmethod
    def run(cls, device_or_file: str | Path | None = None, **options: CommandOption) -> CommandResult:
        """Run the DMPD tool with specified options.

        Args:
            device_or_file: Device path or file path to operate on
            **options: Tool-specific options

        Returns:
            Command result
        """
        cmd = cls._build_command(cls.TOOL_NAME, device_or_file, **options)
        return run(cmd)

    @classmethod
    def help(cls) -> CommandResult:
        """Get help information for the tool."""
        return run(f'{cls.TOOL_NAME} --help')

    @classmethod
    def version(cls) -> CommandResult:
        """Get version information for the tool."""
        return run(f'{cls.TOOL_NAME} --version')

help() classmethod

Get help information for the tool.

Source code in sts_libs/src/sts/dmpd.py
105
106
107
108
@classmethod
def help(cls) -> CommandResult:
    """Get help information for the tool."""
    return run(f'{cls.TOOL_NAME} --help')

run(device_or_file=None, **options) classmethod

Run the DMPD tool with specified options.

Parameters:

Name Type Description Default
device_or_file str | Path | None

Device path or file path to operate on

None
**options CommandOption

Tool-specific options

{}

Returns:

Type Description
CommandResult

Command result

Source code in sts_libs/src/sts/dmpd.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
@classmethod
def run(cls, device_or_file: str | Path | None = None, **options: CommandOption) -> CommandResult:
    """Run the DMPD tool with specified options.

    Args:
        device_or_file: Device path or file path to operate on
        **options: Tool-specific options

    Returns:
        Command result
    """
    cmd = cls._build_command(cls.TOOL_NAME, device_or_file, **options)
    return run(cmd)

version() classmethod

Get version information for the tool.

Source code in sts_libs/src/sts/dmpd.py
110
111
112
113
@classmethod
def version(cls) -> CommandResult:
    """Get version information for the tool."""
    return run(f'{cls.TOOL_NAME} --version')

EraCheck

Bases: DmpdTool

Check era metadata integrity.

Usage: era_check [options] {device|file}

For available options, see: era_check --help

Source code in sts_libs/src/sts/dmpd.py
268
269
270
271
272
273
274
275
276
class EraCheck(DmpdTool):
    """Check era metadata integrity.

    Usage: era_check [options] {device|file}

    For available options, see: era_check --help
    """

    TOOL_NAME = 'era_check'

EraDump

Bases: DmpdTool

Dump era metadata to stdout or file.

Usage: era_dump [options] {device|file}

For available options, see: era_dump --help

Source code in sts_libs/src/sts/dmpd.py
279
280
281
282
283
284
285
286
287
class EraDump(DmpdTool):
    """Dump era metadata to stdout or file.

    Usage: era_dump [options] {device|file}

    For available options, see: era_dump --help
    """

    TOOL_NAME = 'era_dump'

EraRepair

Bases: DmpdTool

Repair corrupted era metadata.

Usage: era_repair [options] --input {device|file} --output {device|file}

For available options, see: era_repair --help

Source code in sts_libs/src/sts/dmpd.py
290
291
292
293
294
295
296
297
298
class EraRepair(DmpdTool):
    """Repair corrupted era metadata.

    Usage: era_repair [options] --input {device|file} --output {device|file}

    For available options, see: era_repair --help
    """

    TOOL_NAME = 'era_repair'

EraRestore

Bases: DmpdTool

Restore era metadata from backup.

Usage: era_restore [options] --input --output {device|file}

For available options, see: era_restore --help

Source code in sts_libs/src/sts/dmpd.py
301
302
303
304
305
306
307
308
309
class EraRestore(DmpdTool):
    """Restore era metadata from backup.

    Usage: era_restore [options] --input <file> --output {device|file}

    For available options, see: era_restore --help
    """

    TOOL_NAME = 'era_restore'

ThinCheck

Bases: DmpdTool

Check thin provisioning metadata integrity.

Usage: thin_check [options] {device|file}

For available options, see: thin_check --help

Source code in sts_libs/src/sts/dmpd.py
177
178
179
180
181
182
183
184
185
class ThinCheck(DmpdTool):
    """Check thin provisioning metadata integrity.

    Usage: thin_check [options] {device|file}

    For available options, see: thin_check --help
    """

    TOOL_NAME = 'thin_check'

ThinDelta

Bases: DmpdTool

Print differences between thin devices.

Usage: thin_delta [options]

For available options, see: thin_delta --help

Source code in sts_libs/src/sts/dmpd.py
254
255
256
257
258
259
260
261
262
class ThinDelta(DmpdTool):
    """Print differences between thin devices.

    Usage: thin_delta [options] <input>

    For available options, see: thin_delta --help
    """

    TOOL_NAME = 'thin_delta'

ThinDump

Bases: DmpdTool

Dump thin metadata to stdout or file.

Usage: thin_dump [options] {device|file}

For available options, see: thin_dump --help

Source code in sts_libs/src/sts/dmpd.py
188
189
190
191
192
193
194
195
196
class ThinDump(DmpdTool):
    """Dump thin metadata to stdout or file.

    Usage: thin_dump [options] {device|file}

    For available options, see: thin_dump --help
    """

    TOOL_NAME = 'thin_dump'

ThinLs

Bases: DmpdTool

List thin devices.

Usage: thin_ls [options] {device|file}

For available options, see: thin_ls --help

Source code in sts_libs/src/sts/dmpd.py
243
244
245
246
247
248
249
250
251
class ThinLs(DmpdTool):
    """List thin devices.

    Usage: thin_ls [options] {device|file}

    For available options, see: thin_ls --help
    """

    TOOL_NAME = 'thin_ls'

ThinMetadataSize

Bases: DmpdTool

Calculate thin metadata size requirements.

Usage: thin_metadata_size [options]

For available options, see: thin_metadata_size --help

Source code in sts_libs/src/sts/dmpd.py
221
222
223
224
225
226
227
228
229
class ThinMetadataSize(DmpdTool):
    """Calculate thin metadata size requirements.

    Usage: thin_metadata_size [options]

    For available options, see: thin_metadata_size --help
    """

    TOOL_NAME = 'thin_metadata_size'

ThinRepair

Bases: DmpdTool

Repair corrupted thin metadata.

Usage: thin_repair [options] --input {device|file} --output {device|file}

For available options, see: thin_repair --help

Source code in sts_libs/src/sts/dmpd.py
199
200
201
202
203
204
205
206
207
class ThinRepair(DmpdTool):
    """Repair corrupted thin metadata.

    Usage: thin_repair [options] --input {device|file} --output {device|file}

    For available options, see: thin_repair --help
    """

    TOOL_NAME = 'thin_repair'

ThinRestore

Bases: DmpdTool

Restore thin metadata from backup.

Usage: thin_restore [options] --input --output {device|file}

For available options, see: thin_restore --help

Source code in sts_libs/src/sts/dmpd.py
210
211
212
213
214
215
216
217
218
class ThinRestore(DmpdTool):
    """Restore thin metadata from backup.

    Usage: thin_restore [options] --input <file> --output {device|file}

    For available options, see: thin_restore --help
    """

    TOOL_NAME = 'thin_restore'

ThinTrim

Bases: DmpdTool

Trim thin metadata.

Usage: thin_trim [options] --data-dev {device} --metadata-dev {device}

For available options, see: thin_trim --help

Source code in sts_libs/src/sts/dmpd.py
232
233
234
235
236
237
238
239
240
class ThinTrim(DmpdTool):
    """Trim thin metadata.

    Usage: thin_trim [options] --data-dev {device} --metadata-dev {device}

    For available options, see: thin_trim --help
    """

    TOOL_NAME = 'thin_trim'

cache_check(device_or_file, **options)

Check cache metadata integrity.

Source code in sts_libs/src/sts/dmpd.py
315
316
317
def cache_check(device_or_file: str | Path, **options: CommandOption) -> CommandResult:
    """Check cache metadata integrity."""
    return CacheCheck.run(device_or_file, **options)

cache_dump(device_or_file, **options)

Dump cache metadata.

Source code in sts_libs/src/sts/dmpd.py
320
321
322
def cache_dump(device_or_file: str | Path, **options: CommandOption) -> CommandResult:
    """Dump cache metadata."""
    return CacheDump.run(device_or_file, **options)

cache_metadata_size(**options)

Calculate cache metadata size.

Source code in sts_libs/src/sts/dmpd.py
335
336
337
def cache_metadata_size(**options: CommandOption) -> CommandResult:
    """Calculate cache metadata size."""
    return CacheMetadataSize.run(None, **options)

cache_repair(**options)

Repair cache metadata.

Source code in sts_libs/src/sts/dmpd.py
325
326
327
def cache_repair(**options: CommandOption) -> CommandResult:
    """Repair cache metadata."""
    return CacheRepair.run(None, **options)

cache_restore(**options)

Restore cache metadata.

Source code in sts_libs/src/sts/dmpd.py
330
331
332
def cache_restore(**options: CommandOption) -> CommandResult:
    """Restore cache metadata."""
    return CacheRestore.run(None, **options)

era_check(device_or_file, **options)

Check era metadata integrity.

Source code in sts_libs/src/sts/dmpd.py
380
381
382
def era_check(device_or_file: str | Path, **options: CommandOption) -> CommandResult:
    """Check era metadata integrity."""
    return EraCheck.run(device_or_file, **options)

era_dump(device_or_file, **options)

Dump era metadata.

Source code in sts_libs/src/sts/dmpd.py
385
386
387
def era_dump(device_or_file: str | Path, **options: CommandOption) -> CommandResult:
    """Dump era metadata."""
    return EraDump.run(device_or_file, **options)

era_repair(**options)

Repair era metadata.

Source code in sts_libs/src/sts/dmpd.py
390
391
392
def era_repair(**options: CommandOption) -> CommandResult:
    """Repair era metadata."""
    return EraRepair.run(None, **options)

era_restore(**options)

Restore era metadata.

Source code in sts_libs/src/sts/dmpd.py
395
396
397
def era_restore(**options: CommandOption) -> CommandResult:
    """Restore era metadata."""
    return EraRestore.run(None, **options)

get_help(tool_name)

Get help for any DMPD tool.

Source code in sts_libs/src/sts/dmpd.py
400
401
402
def get_help(tool_name: str) -> CommandResult:
    """Get help for any DMPD tool."""
    return run(f'{tool_name} --help')

get_version(tool_name)

Get version for any DMPD tool.

Source code in sts_libs/src/sts/dmpd.py
405
406
407
def get_version(tool_name: str) -> CommandResult:
    """Get version for any DMPD tool."""
    return run(f'{tool_name} --version')

thin_check(device_or_file, **options)

Check thin metadata integrity.

Source code in sts_libs/src/sts/dmpd.py
340
341
342
def thin_check(device_or_file: str | Path, **options: CommandOption) -> CommandResult:
    """Check thin metadata integrity."""
    return ThinCheck.run(device_or_file, **options)

thin_delta(device_or_file, **options)

Print differences between thin devices.

Source code in sts_libs/src/sts/dmpd.py
375
376
377
def thin_delta(device_or_file: str | Path, **options: CommandOption) -> CommandResult:
    """Print differences between thin devices."""
    return ThinDelta.run(device_or_file, **options)

thin_dump(device_or_file, **options)

Dump thin metadata.

Source code in sts_libs/src/sts/dmpd.py
345
346
347
def thin_dump(device_or_file: str | Path, **options: CommandOption) -> CommandResult:
    """Dump thin metadata."""
    return ThinDump.run(device_or_file, **options)

thin_ls(device_or_file, **options)

List thin devices.

Source code in sts_libs/src/sts/dmpd.py
370
371
372
def thin_ls(device_or_file: str | Path, **options: CommandOption) -> CommandResult:
    """List thin devices."""
    return ThinLs.run(device_or_file, **options)

thin_metadata_size(**options)

Calculate thin metadata size.

Source code in sts_libs/src/sts/dmpd.py
360
361
362
def thin_metadata_size(**options: CommandOption) -> CommandResult:
    """Calculate thin metadata size."""
    return ThinMetadataSize.run(None, **options)

thin_repair(**options)

Repair thin metadata.

Source code in sts_libs/src/sts/dmpd.py
350
351
352
def thin_repair(**options: CommandOption) -> CommandResult:
    """Repair thin metadata."""
    return ThinRepair.run(None, **options)

thin_restore(**options)

Restore thin metadata.

Source code in sts_libs/src/sts/dmpd.py
355
356
357
def thin_restore(**options: CommandOption) -> CommandResult:
    """Restore thin metadata."""
    return ThinRestore.run(None, **options)

thin_trim(**options)

Trim thin metadata.

Source code in sts_libs/src/sts/dmpd.py
365
366
367
def thin_trim(**options: CommandOption) -> CommandResult:
    """Trim thin metadata."""
    return ThinTrim.run(None, **options)