remap-badblocks 0.8__py3-none-any.whl → 0.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- remap_badblocks/__init__.py +1 -1
- remap_badblocks/cli/commands/update.py +8 -6
- remap_badblocks/src/badblocks/_remap_badblocks.py +23 -23
- remap_badblocks/src/mapping.py +5 -1
- remap_badblocks-0.9.dist-info/METADATA +107 -0
- remap_badblocks-0.9.dist-info/RECORD +36 -0
- {remap_badblocks-0.8.dist-info → remap_badblocks-0.9.dist-info}/WHEEL +1 -1
- __init__.py +0 -0
- cli/__init__.py +0 -0
- cli/__main__.py +0 -239
- cli/commands/__init__.py +0 -8
- cli/commands/add.py +0 -91
- cli/commands/apply.py +0 -81
- cli/commands/get.py +0 -28
- cli/commands/remove.py +0 -45
- cli/commands/update.py +0 -208
- cli/commands/version.py +0 -15
- remap_badblocks-0.8.dist-info/METADATA +0 -130
- remap_badblocks-0.8.dist-info/RECORD +0 -66
- src/badblocks/_compute_good_ranges.py +0 -43
- src/badblocks/_find_badblocks.py +0 -76
- src/badblocks/_mapping_generation.py +0 -12
- src/badblocks/_remap_badblocks.py +0 -114
- src/badblocks/badblocks.py +0 -40
- src/devices/__init__.py +0 -0
- src/devices/device_config.py +0 -62
- src/devices/devices_config.py +0 -300
- src/devices/exceptions.py +0 -22
- src/devices_config_constants.py +0 -3
- src/mapping.py +0 -109
- src/remappers/_check_applied_devices.py +0 -10
- src/remappers/_generate_dm_table.py +0 -27
- src/test_utils.py +0 -18
- src/utils/__init__.py +0 -0
- src/utils/_get_device_info.py +0 -43
- src/utils/_iterable_bytes_converter.py +0 -19
- src/utils/_parse_inputs.py +0 -84
- src/utils/_run_command.py +0 -76
- src/utils/_sort_devices.py +0 -26
- {remap_badblocks-0.8.dist-info → remap_badblocks-0.9.dist-info}/entry_points.txt +0 -0
- {remap_badblocks-0.8.dist-info → remap_badblocks-0.9.dist-info}/licenses/LICENSE +0 -0
- {remap_badblocks-0.8.dist-info → remap_badblocks-0.9.dist-info}/top_level.txt +0 -0
src/badblocks/_find_badblocks.py
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
from itertools import chain
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from typing import Iterable, Optional, Union
|
|
4
|
-
|
|
5
|
-
from remap_badblocks.src.utils._run_command import run_command_realtime
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def parse_badblocks(lines: Iterable[str]) -> Iterable[int]:
|
|
9
|
-
"""Parse badblocks output and return a set of bad sector numbers."""
|
|
10
|
-
lines = map(lambda line: line.strip(), lines)
|
|
11
|
-
lines = filter(lambda line: line.isdigit(), lines)
|
|
12
|
-
return map(int, lines)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def read_known_badblocks(known_badblocks_file: Union[Path, str]) -> Iterable[int]:
|
|
16
|
-
"""Merge badblocks with known badblocks from a file."""
|
|
17
|
-
with open(known_badblocks_file, "r") as f:
|
|
18
|
-
known_badblocks = parse_badblocks(f.readlines())
|
|
19
|
-
return known_badblocks
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def build_badblocks_command(
|
|
23
|
-
device: Union[Path, str],
|
|
24
|
-
sector_size: int,
|
|
25
|
-
mode: str,
|
|
26
|
-
known_badblocks_file: Optional[Union[Path, str]],
|
|
27
|
-
blocks_range: Optional[tuple[int, int]],
|
|
28
|
-
) -> list[str]:
|
|
29
|
-
"""
|
|
30
|
-
Build the badblocks command based on the mode and optional known badblocks file.
|
|
31
|
-
blocks_range is a right-open interval
|
|
32
|
-
"""
|
|
33
|
-
if mode == "read":
|
|
34
|
-
cmd = ["badblocks", "-sv"]
|
|
35
|
-
elif mode == "write":
|
|
36
|
-
cmd = ["badblocks", "-wsv"]
|
|
37
|
-
else:
|
|
38
|
-
raise ValueError("Invalid mode. Use 'read' or 'write'.")
|
|
39
|
-
|
|
40
|
-
cmd += ["-b", str(sector_size)]
|
|
41
|
-
|
|
42
|
-
if known_badblocks_file:
|
|
43
|
-
cmd += ["-i", str(known_badblocks_file)]
|
|
44
|
-
|
|
45
|
-
cmd += [str(device)]
|
|
46
|
-
|
|
47
|
-
if blocks_range:
|
|
48
|
-
start_block, end_block = blocks_range
|
|
49
|
-
cmd += [str(end_block), str(start_block)]
|
|
50
|
-
|
|
51
|
-
return cmd
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def get_all_badblocks(
|
|
55
|
-
device: Path,
|
|
56
|
-
sector_size: int,
|
|
57
|
-
mode: str = "read",
|
|
58
|
-
known_badblocks_file: Optional[Path] = None,
|
|
59
|
-
block_range: Optional[tuple[int, int]] = None,
|
|
60
|
-
) -> Iterable[int]:
|
|
61
|
-
"""
|
|
62
|
-
Run badblocks on a device and return the results as a set of bad sectors.
|
|
63
|
-
block_range is a right-open interval
|
|
64
|
-
"""
|
|
65
|
-
badblocks: Iterable[int] = set()
|
|
66
|
-
|
|
67
|
-
if known_badblocks_file:
|
|
68
|
-
badblocks = read_known_badblocks(known_badblocks_file)
|
|
69
|
-
|
|
70
|
-
cmd = build_badblocks_command(
|
|
71
|
-
device, sector_size, mode, known_badblocks_file, block_range
|
|
72
|
-
)
|
|
73
|
-
badblocks_lines = run_command_realtime(cmd)
|
|
74
|
-
badblocks = chain(badblocks, parse_badblocks(badblocks_lines))
|
|
75
|
-
|
|
76
|
-
return badblocks
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from typing import Generator, Iterable
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def generate_mapping(
|
|
5
|
-
ranges: Iterable[tuple[int, int]],
|
|
6
|
-
) -> Generator[tuple[int, int, int], None, None]:
|
|
7
|
-
"""Generate a mapping from good ranges."""
|
|
8
|
-
offset = 0
|
|
9
|
-
for start, end in ranges:
|
|
10
|
-
length = end - start
|
|
11
|
-
yield (offset, start, length) # (start_id_virtual, start_id_real, length)
|
|
12
|
-
offset += length
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import itertools
|
|
2
|
-
from typing import Iterable, Iterator, Optional, Union
|
|
3
|
-
|
|
4
|
-
from remap_badblocks.src.devices.devices_config import Mapping
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def remap_badblocks(
|
|
8
|
-
mapping: Iterable[tuple[int, int, int]],
|
|
9
|
-
badblocks: Iterable[int],
|
|
10
|
-
spare_sectors: Iterator[int],
|
|
11
|
-
) -> Iterable[tuple[int, int, int]]:
|
|
12
|
-
sorted_badblocks = sorted(badblocks)
|
|
13
|
-
|
|
14
|
-
for start_virtual, start_real, length in mapping:
|
|
15
|
-
end_real = start_real + length - 1
|
|
16
|
-
sorted_to_remap: list[int] = []
|
|
17
|
-
sorted_to_remap_virtual: list[int] = []
|
|
18
|
-
for badblock in sorted_badblocks:
|
|
19
|
-
if start_real <= badblock <= end_real:
|
|
20
|
-
sorted_to_remap.append(badblock)
|
|
21
|
-
if not sorted_to_remap:
|
|
22
|
-
# nothing to remap
|
|
23
|
-
yield start_virtual, start_real, length
|
|
24
|
-
else:
|
|
25
|
-
current_start_virtual = start_virtual
|
|
26
|
-
current_start_real = start_real
|
|
27
|
-
for badblock in sorted_to_remap:
|
|
28
|
-
if current_start_real < badblock:
|
|
29
|
-
# pieces to keep
|
|
30
|
-
yield current_start_virtual, current_start_real, badblock - current_start_real
|
|
31
|
-
step = badblock - current_start_real + 1
|
|
32
|
-
current_start_virtual += step
|
|
33
|
-
current_start_real += step
|
|
34
|
-
sorted_to_remap_virtual.append(current_start_virtual - 1)
|
|
35
|
-
if current_start_real < end_real:
|
|
36
|
-
# pieces to keep
|
|
37
|
-
yield current_start_virtual, current_start_real, end_real - current_start_real + 1
|
|
38
|
-
for virt_to_remap in sorted_to_remap_virtual:
|
|
39
|
-
try:
|
|
40
|
-
while (spare_sector := next(spare_sectors)) in badblocks:
|
|
41
|
-
pass
|
|
42
|
-
except StopIteration:
|
|
43
|
-
raise RuntimeError
|
|
44
|
-
# remap
|
|
45
|
-
yield virt_to_remap, spare_sector, 1
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def identify_simplifiable_couple_in_mapping(
|
|
49
|
-
mapping: list[tuple[int, int, int]],
|
|
50
|
-
) -> Optional[tuple[int, int]]:
|
|
51
|
-
for i, (start_virtual_0, start_real_0, length_0) in enumerate(mapping[:-1]):
|
|
52
|
-
end_virtual_0 = start_virtual_0 + length_0
|
|
53
|
-
end_real_0 = start_real_0 + length_0
|
|
54
|
-
for _j, (start_virtual_1, start_real_1, length_1) in enumerate(
|
|
55
|
-
mapping[i + 1 :]
|
|
56
|
-
):
|
|
57
|
-
j = _j + i + 1
|
|
58
|
-
end_virtual_1 = start_virtual_1 + length_1
|
|
59
|
-
end_real_1 = start_real_1 + length_1
|
|
60
|
-
if (start_virtual_0 == end_virtual_1) and (start_real_0 == end_real_1):
|
|
61
|
-
return j, i
|
|
62
|
-
if (start_virtual_1 == end_virtual_0) and (start_real_1 == end_real_0):
|
|
63
|
-
return i, j
|
|
64
|
-
return None
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def simplify_mapping(
|
|
68
|
-
mapping: list[tuple[int, int, int]],
|
|
69
|
-
) -> Iterable[tuple[int, int, int]]:
|
|
70
|
-
while (to_simplify := identify_simplifiable_couple_in_mapping(mapping)) is not None:
|
|
71
|
-
i, j = to_simplify
|
|
72
|
-
element_i = mapping[i]
|
|
73
|
-
element_j = mapping[j]
|
|
74
|
-
|
|
75
|
-
simplified_element = element_i[0], element_i[1], element_i[2] + element_j[2]
|
|
76
|
-
|
|
77
|
-
_i = min(i, j)
|
|
78
|
-
_j = max(i, j)
|
|
79
|
-
|
|
80
|
-
mapping = (
|
|
81
|
-
mapping[:_i]
|
|
82
|
-
+ mapping[_i + 1 : _j]
|
|
83
|
-
+ mapping[_j + 1 :]
|
|
84
|
-
+ [simplified_element]
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
return mapping
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def iter_all_spare_sectors(
|
|
91
|
-
n_spare_sectors: int, first_spare_sector: int
|
|
92
|
-
) -> Iterator[int]:
|
|
93
|
-
return iter(range(first_spare_sector, first_spare_sector + n_spare_sectors))
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def iter_free_spare_sectors(
|
|
97
|
-
spare_sectors: Iterable[int], n_used_spare_sectors: int
|
|
98
|
-
) -> Iterator[int]:
|
|
99
|
-
return itertools.islice(spare_sectors, n_used_spare_sectors, None)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def count_used_spare_sectors(
|
|
103
|
-
mapping: Union[Iterable[tuple[int, int, int]], Mapping],
|
|
104
|
-
spare_sectors: Iterable[int],
|
|
105
|
-
) -> int:
|
|
106
|
-
spare_sectors = set(spare_sectors)
|
|
107
|
-
used = 0
|
|
108
|
-
for _, start, length in mapping:
|
|
109
|
-
end = start + length
|
|
110
|
-
for s in spare_sectors:
|
|
111
|
-
if start <= s < end:
|
|
112
|
-
used += 1
|
|
113
|
-
|
|
114
|
-
return used
|
src/badblocks/badblocks.py
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from typing import Iterable, Iterator
|
|
3
|
-
|
|
4
|
-
from remap_badblocks.src.utils._iterable_bytes_converter import (
|
|
5
|
-
iterable_from_bytes, iterable_to_bytes)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@dataclass
|
|
9
|
-
class Badblocks(Iterable[int]):
|
|
10
|
-
badblocks: set[int]
|
|
11
|
-
|
|
12
|
-
INT_LENGTH: int = 8
|
|
13
|
-
|
|
14
|
-
def __init__(self, badblocks: Iterable[int]):
|
|
15
|
-
self.badblocks = set(badblocks)
|
|
16
|
-
|
|
17
|
-
def __bytes__(self) -> bytes:
|
|
18
|
-
"""
|
|
19
|
-
Convert the Badblocks to bytes for storage.
|
|
20
|
-
"""
|
|
21
|
-
return iterable_to_bytes(self.badblocks, length=self.INT_LENGTH)
|
|
22
|
-
|
|
23
|
-
@classmethod
|
|
24
|
-
def from_bytes(cls, data: bytes) -> "Badblocks":
|
|
25
|
-
"""
|
|
26
|
-
Create a Badblocks instance from bytes.
|
|
27
|
-
"""
|
|
28
|
-
return cls(badblocks=set(iterable_from_bytes(data, length=cls.INT_LENGTH)))
|
|
29
|
-
|
|
30
|
-
def __len__(self) -> int:
|
|
31
|
-
"""
|
|
32
|
-
Get the number of bad blocks.
|
|
33
|
-
"""
|
|
34
|
-
return len(self.badblocks)
|
|
35
|
-
|
|
36
|
-
def __iter__(self) -> Iterator[int]:
|
|
37
|
-
"""
|
|
38
|
-
Iterate over the bad blocks.
|
|
39
|
-
"""
|
|
40
|
-
return iter(self.badblocks)
|
src/devices/__init__.py
DELETED
|
File without changes
|
src/devices/device_config.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from typing import Iterable, Optional, Union
|
|
4
|
-
|
|
5
|
-
from remap_badblocks.src.badblocks.badblocks import Badblocks
|
|
6
|
-
from remap_badblocks.src.mapping import Mapping
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@dataclass
|
|
10
|
-
class DeviceConfig:
|
|
11
|
-
id: int
|
|
12
|
-
path: Path
|
|
13
|
-
name: str
|
|
14
|
-
sector_size: int
|
|
15
|
-
badblocks: Badblocks
|
|
16
|
-
depends_on: set[int]
|
|
17
|
-
logical_range: tuple[int, int]
|
|
18
|
-
apply_at_startup: bool = False
|
|
19
|
-
spare_sectors: Optional[int] = None
|
|
20
|
-
mapping: Optional[Mapping] = None
|
|
21
|
-
|
|
22
|
-
def __init__(
|
|
23
|
-
self,
|
|
24
|
-
id: int,
|
|
25
|
-
path: Union[Path, str],
|
|
26
|
-
name: str,
|
|
27
|
-
sector_size: int,
|
|
28
|
-
badblocks: Iterable[int],
|
|
29
|
-
logical_range: tuple[int, int],
|
|
30
|
-
apply_at_startup: bool = False,
|
|
31
|
-
depends_on: Iterable[int] = set(),
|
|
32
|
-
spare_sectors: Optional[int] = None,
|
|
33
|
-
mapping: Optional[Mapping] = None,
|
|
34
|
-
):
|
|
35
|
-
if isinstance(path, str):
|
|
36
|
-
path = Path(path)
|
|
37
|
-
self.id = id
|
|
38
|
-
self.path = path
|
|
39
|
-
self.name = name
|
|
40
|
-
self.sector_size = sector_size
|
|
41
|
-
self.badblocks = Badblocks(badblocks)
|
|
42
|
-
self.mapping = mapping if mapping is not None else None
|
|
43
|
-
self.spare_sectors = spare_sectors
|
|
44
|
-
self.depends_on = set(depends_on)
|
|
45
|
-
self.logical_range = logical_range
|
|
46
|
-
self.apply_at_startup = apply_at_startup
|
|
47
|
-
|
|
48
|
-
def __str__(self) -> str:
|
|
49
|
-
return (
|
|
50
|
-
"DeviceConfig("
|
|
51
|
-
f" id={self.id}, path={self.path}, name={self.name},"
|
|
52
|
-
f" sector_size={self.sector_size}, badblocks={len(self.badblocks)}, spare_sectors={self.spare_sectors},"
|
|
53
|
-
f" mapping={'Undefined' if self.mapping is None else 'Defined'},"
|
|
54
|
-
f" depends_on={self.depends_on}, logical_range={self.logical_range}"
|
|
55
|
-
")"
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
def get_applied_path(self) -> Path:
|
|
59
|
-
return Path("/dev/mapper/", self.name)
|
|
60
|
-
|
|
61
|
-
def __hash__(self) -> int:
|
|
62
|
-
return self.id
|
src/devices/devices_config.py
DELETED
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
import sqlite3 as sqlite
|
|
4
|
-
from collections import OrderedDict
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import (Any, Collection, Iterable, Literal, Optional, Union,
|
|
7
|
-
overload)
|
|
8
|
-
|
|
9
|
-
from remap_badblocks.src.badblocks.badblocks import Badblocks
|
|
10
|
-
from remap_badblocks.src.mapping import Mapping
|
|
11
|
-
from remap_badblocks.src.utils._iterable_bytes_converter import (
|
|
12
|
-
iterable_from_bytes, iterable_to_bytes)
|
|
13
|
-
|
|
14
|
-
from .device_config import DeviceConfig
|
|
15
|
-
from .exceptions import DeviceNotFoundError
|
|
16
|
-
|
|
17
|
-
DEFAULT_INT_LENGTH = 8
|
|
18
|
-
DEVICE_INFO_COLUMNS_TYPES = OrderedDict(
|
|
19
|
-
{
|
|
20
|
-
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
|
|
21
|
-
"path": "TEXT NOT NULL",
|
|
22
|
-
"name": "TEXT NOT NULL",
|
|
23
|
-
"sector_size": "INTEGER NOT NULL",
|
|
24
|
-
"badblocks": "BLOB NOT NULL",
|
|
25
|
-
"spare_sectors": "INTEGER",
|
|
26
|
-
"mapping": "BLOB",
|
|
27
|
-
"depends_on": "BLOB NOT NULL",
|
|
28
|
-
"logical_range_start": "INTEGER NOT NULL",
|
|
29
|
-
"logical_range_end": "INTEGER NOT NULL",
|
|
30
|
-
"apply_at_startup": "BOOLEAN DEFAULT FALSE NOT NULL",
|
|
31
|
-
}
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class DevicesConfig:
|
|
36
|
-
_path: Path
|
|
37
|
-
|
|
38
|
-
def __init__(self, path: Union[Path, str]):
|
|
39
|
-
if isinstance(path, str):
|
|
40
|
-
path = Path(path)
|
|
41
|
-
assert not path.is_dir(), "The provided path must not be a directory."
|
|
42
|
-
self._path = path
|
|
43
|
-
self.__init_table()
|
|
44
|
-
|
|
45
|
-
def __init_table(self) -> None:
|
|
46
|
-
"""
|
|
47
|
-
Initialize the devices table in the database.
|
|
48
|
-
"""
|
|
49
|
-
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
|
50
|
-
with sqlite.connect(self._path) as conn:
|
|
51
|
-
cursor = conn.cursor()
|
|
52
|
-
cursor.execute(
|
|
53
|
-
"""
|
|
54
|
-
CREATE TABLE IF NOT EXISTS devices (
|
|
55
|
-
"""
|
|
56
|
-
+ ",".join(
|
|
57
|
-
(
|
|
58
|
-
col_name + " " + col_type
|
|
59
|
-
for col_name, col_type in DEVICE_INFO_COLUMNS_TYPES.items()
|
|
60
|
-
)
|
|
61
|
-
)
|
|
62
|
-
+ """
|
|
63
|
-
)
|
|
64
|
-
"""
|
|
65
|
-
)
|
|
66
|
-
conn.commit()
|
|
67
|
-
|
|
68
|
-
def __check_device_in_db(self, conn: sqlite.Connection, id: int) -> bool:
|
|
69
|
-
"""
|
|
70
|
-
Check if a device with the given ID exists in the database.
|
|
71
|
-
"""
|
|
72
|
-
cursor = conn.cursor()
|
|
73
|
-
cursor.execute("SELECT id FROM devices WHERE id = ? LIMIT 1", (id,))
|
|
74
|
-
val = cursor.fetchone()
|
|
75
|
-
return val is not None
|
|
76
|
-
|
|
77
|
-
def _assert_depends_on(
|
|
78
|
-
self, conn: sqlite.Connection, depends_on: Collection[int]
|
|
79
|
-
) -> None:
|
|
80
|
-
missing: set[int] = set()
|
|
81
|
-
for _id in depends_on:
|
|
82
|
-
if not self.__check_device_in_db(conn, id=_id):
|
|
83
|
-
missing.add(_id)
|
|
84
|
-
if missing:
|
|
85
|
-
raise DeviceNotFoundError(device_id=missing)
|
|
86
|
-
|
|
87
|
-
def __parse_device_from_row(
|
|
88
|
-
self,
|
|
89
|
-
id: int,
|
|
90
|
-
path: Union[Path, str],
|
|
91
|
-
name: str,
|
|
92
|
-
sector_size: int,
|
|
93
|
-
badblocks: bytes,
|
|
94
|
-
spare_sectors: Optional[int],
|
|
95
|
-
mapping: Optional[bytes],
|
|
96
|
-
depends_on: bytes,
|
|
97
|
-
logical_range_start: int,
|
|
98
|
-
logical_range_end: int,
|
|
99
|
-
apply_at_startup: bool,
|
|
100
|
-
) -> DeviceConfig:
|
|
101
|
-
"""
|
|
102
|
-
Parse a device configuration from a database row.
|
|
103
|
-
"""
|
|
104
|
-
return DeviceConfig(
|
|
105
|
-
id=id,
|
|
106
|
-
path=Path(path),
|
|
107
|
-
name=name,
|
|
108
|
-
sector_size=sector_size,
|
|
109
|
-
badblocks=Badblocks.from_bytes(badblocks),
|
|
110
|
-
spare_sectors=spare_sectors,
|
|
111
|
-
mapping=Mapping.from_bytes(mapping) if mapping is not None else None,
|
|
112
|
-
depends_on=set(iterable_from_bytes(depends_on, length=DEFAULT_INT_LENGTH)),
|
|
113
|
-
logical_range=(logical_range_start, logical_range_end),
|
|
114
|
-
apply_at_startup=apply_at_startup,
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
def get_devices(self) -> Iterable[DeviceConfig]:
|
|
118
|
-
"""
|
|
119
|
-
Get the devices configuration as a DataFrame.
|
|
120
|
-
"""
|
|
121
|
-
with sqlite.connect(self._path) as conn:
|
|
122
|
-
data = conn.execute(
|
|
123
|
-
"""SELECT """
|
|
124
|
-
+ ",".join(DEVICE_INFO_COLUMNS_TYPES.keys())
|
|
125
|
-
+ """ FROM devices"""
|
|
126
|
-
).fetchall()
|
|
127
|
-
|
|
128
|
-
data = [
|
|
129
|
-
self.__parse_device_from_row(
|
|
130
|
-
**dict(zip(DEVICE_INFO_COLUMNS_TYPES.keys(), row))
|
|
131
|
-
)
|
|
132
|
-
for row in data
|
|
133
|
-
]
|
|
134
|
-
return data
|
|
135
|
-
|
|
136
|
-
@overload
|
|
137
|
-
def get_device(self, *, id: int, name: Literal[None] = None) -> DeviceConfig: ...
|
|
138
|
-
@overload
|
|
139
|
-
def get_device(self, *, id: Literal[None] = None, name: str) -> DeviceConfig: ...
|
|
140
|
-
|
|
141
|
-
def get_device(
|
|
142
|
-
self, *, id: Optional[int] = None, name: Optional[str] = None
|
|
143
|
-
) -> DeviceConfig:
|
|
144
|
-
if id is not None and name is not None:
|
|
145
|
-
raise ValueError("Only one of id or name can be provided to get a device.")
|
|
146
|
-
|
|
147
|
-
with sqlite.connect(self._path) as conn:
|
|
148
|
-
cursor = conn.cursor()
|
|
149
|
-
QUERY = (
|
|
150
|
-
"""SELECT """
|
|
151
|
-
+ ",".join(DEVICE_INFO_COLUMNS_TYPES.keys())
|
|
152
|
-
+ """ FROM devices"""
|
|
153
|
-
)
|
|
154
|
-
if id is not None:
|
|
155
|
-
cursor.execute(QUERY + " WHERE id = ?", (id,))
|
|
156
|
-
elif name is not None:
|
|
157
|
-
cursor.execute(QUERY + " WHERE name = ?", (name,))
|
|
158
|
-
else:
|
|
159
|
-
raise ValueError("Either id or name must be provided to get a device.")
|
|
160
|
-
row = cursor.fetchone()
|
|
161
|
-
if row is None:
|
|
162
|
-
raise DeviceNotFoundError(device_id=id, device_name=name)
|
|
163
|
-
|
|
164
|
-
return self.__parse_device_from_row(
|
|
165
|
-
**dict(zip(DEVICE_INFO_COLUMNS_TYPES.keys(), row))
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
def __insert_into_devices_table(self, cursor: sqlite.Cursor, row: dict[str, Any]):
|
|
169
|
-
cols = list(row.keys())
|
|
170
|
-
cursor.execute(
|
|
171
|
-
"""INSERT INTO devices
|
|
172
|
-
("""
|
|
173
|
-
+ ",".join(cols)
|
|
174
|
-
+ """)
|
|
175
|
-
VALUES ("""
|
|
176
|
-
+ ",".join(("?",) * len(cols))
|
|
177
|
-
+ """)""",
|
|
178
|
-
tuple(row[col] for col in cols),
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
def add_device(
|
|
182
|
-
self,
|
|
183
|
-
path: Union[Path, str],
|
|
184
|
-
name: str,
|
|
185
|
-
logical_range: tuple[int, int],
|
|
186
|
-
*,
|
|
187
|
-
sector_size: Optional[int] = None,
|
|
188
|
-
badblocks: Union[set[int], Badblocks] = set(),
|
|
189
|
-
depends_on: Collection[int] = set(),
|
|
190
|
-
) -> None:
|
|
191
|
-
_path: Path = Path(path)
|
|
192
|
-
assert _path.exists(), f"The provided device {_path} does not exist."
|
|
193
|
-
|
|
194
|
-
assert (
|
|
195
|
-
re.fullmatch(r"[0-9A-Za-z#+\-\.:=@_]+", name) is not None
|
|
196
|
-
), "Name does not match requirements"
|
|
197
|
-
|
|
198
|
-
try:
|
|
199
|
-
existing_dev = self.get_device(name=name)
|
|
200
|
-
raise ValueError(f"Name {name} is used by device {existing_dev.id}.")
|
|
201
|
-
except DeviceNotFoundError:
|
|
202
|
-
pass
|
|
203
|
-
|
|
204
|
-
badblocks = Badblocks(badblocks)
|
|
205
|
-
|
|
206
|
-
with sqlite.connect(self._path) as conn:
|
|
207
|
-
self._assert_depends_on(conn, depends_on)
|
|
208
|
-
|
|
209
|
-
cursor = conn.cursor()
|
|
210
|
-
self.__insert_into_devices_table(
|
|
211
|
-
cursor,
|
|
212
|
-
{
|
|
213
|
-
"path": str(_path),
|
|
214
|
-
"name": name,
|
|
215
|
-
"sector_size": sector_size,
|
|
216
|
-
"badblocks": sqlite.Binary(bytes(badblocks)),
|
|
217
|
-
"spare_sectors": None,
|
|
218
|
-
"mapping": None,
|
|
219
|
-
"depends_on": iterable_to_bytes(
|
|
220
|
-
depends_on, length=DEFAULT_INT_LENGTH
|
|
221
|
-
),
|
|
222
|
-
"logical_range_start": logical_range[0],
|
|
223
|
-
"logical_range_end": logical_range[1],
|
|
224
|
-
},
|
|
225
|
-
)
|
|
226
|
-
conn.commit()
|
|
227
|
-
|
|
228
|
-
def remove_device(self, id: int) -> None:
|
|
229
|
-
with sqlite.connect(self._path) as conn:
|
|
230
|
-
if not self.__check_device_in_db(conn, id=id):
|
|
231
|
-
raise DeviceNotFoundError(device_id=id)
|
|
232
|
-
|
|
233
|
-
for dev in self.get_devices():
|
|
234
|
-
if id in dev.depends_on:
|
|
235
|
-
raise KeyError(
|
|
236
|
-
f"Device {dev.id} depends on device {id}, can't remove the latter."
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
cursor = conn.cursor()
|
|
240
|
-
cursor.execute("DELETE FROM devices WHERE id = ?", (id,))
|
|
241
|
-
conn.commit()
|
|
242
|
-
|
|
243
|
-
def update_device(
|
|
244
|
-
self,
|
|
245
|
-
id: int,
|
|
246
|
-
*,
|
|
247
|
-
path: Optional[Union[Path, str]] = None,
|
|
248
|
-
name: Optional[str] = None,
|
|
249
|
-
badblocks: Optional[Iterable[int]] = None,
|
|
250
|
-
mapping: Optional[Mapping] = None,
|
|
251
|
-
spare_sectors: Optional[int] = None,
|
|
252
|
-
depends_on: Optional[Collection[int]] = None,
|
|
253
|
-
) -> None:
|
|
254
|
-
with sqlite.connect(self._path) as conn:
|
|
255
|
-
if not self.__check_device_in_db(conn, id=id):
|
|
256
|
-
raise DeviceNotFoundError(device_id=id)
|
|
257
|
-
|
|
258
|
-
cursor = conn.cursor()
|
|
259
|
-
if path is not None:
|
|
260
|
-
cursor.execute(
|
|
261
|
-
"UPDATE devices SET path = ? WHERE id = ?", (str(path), id)
|
|
262
|
-
)
|
|
263
|
-
if name is not None:
|
|
264
|
-
cursor.execute("UPDATE devices SET name = ? WHERE id = ?", (name, id))
|
|
265
|
-
if badblocks is not None:
|
|
266
|
-
badblocks = Badblocks(badblocks)
|
|
267
|
-
cursor.execute(
|
|
268
|
-
"UPDATE devices SET badblocks = ? WHERE id = ?",
|
|
269
|
-
(sqlite.Binary(bytes(badblocks)), id),
|
|
270
|
-
)
|
|
271
|
-
if mapping is not None:
|
|
272
|
-
cursor.execute(
|
|
273
|
-
"UPDATE devices SET mapping = ? WHERE id = ?",
|
|
274
|
-
(sqlite.Binary(bytes(mapping)), id),
|
|
275
|
-
)
|
|
276
|
-
if spare_sectors is not None:
|
|
277
|
-
cursor.execute(
|
|
278
|
-
"UPDATE devices SET spare_sectors = ? WHERE id = ?",
|
|
279
|
-
(spare_sectors, id),
|
|
280
|
-
)
|
|
281
|
-
if depends_on is not None:
|
|
282
|
-
self._assert_depends_on(conn, depends_on)
|
|
283
|
-
cursor.execute(
|
|
284
|
-
"UPDATE devices SET depends_on = ? WHERE id = ?",
|
|
285
|
-
(
|
|
286
|
-
iterable_to_bytes(depends_on, length=DEFAULT_INT_LENGTH),
|
|
287
|
-
id,
|
|
288
|
-
),
|
|
289
|
-
)
|
|
290
|
-
conn.commit()
|
|
291
|
-
|
|
292
|
-
def __len__(self) -> int:
|
|
293
|
-
"""
|
|
294
|
-
Get the number of devices in the database.
|
|
295
|
-
"""
|
|
296
|
-
with sqlite.connect(self._path) as conn:
|
|
297
|
-
cursor = conn.cursor()
|
|
298
|
-
cursor.execute("SELECT COUNT(*) FROM devices")
|
|
299
|
-
count = cursor.fetchone()[0]
|
|
300
|
-
return count
|
src/devices/exceptions.py
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
from typing import Iterable, Optional, Union
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class DeviceNotFoundError(KeyError):
|
|
5
|
-
def __init__(
|
|
6
|
-
self,
|
|
7
|
-
*,
|
|
8
|
-
device_id: Optional[Union[int, Iterable[int]]] = None,
|
|
9
|
-
device_name: Optional[Union[str, Iterable[str]]] = None,
|
|
10
|
-
) -> None:
|
|
11
|
-
if device_id is not None and device_name is not None:
|
|
12
|
-
raise ValueError("Only one of device_id or device_name can be provided.")
|
|
13
|
-
if device_id is not None:
|
|
14
|
-
message = f"Device(s) with id(s) {device_id} does not exist."
|
|
15
|
-
args = (message,)
|
|
16
|
-
elif device_name is not None:
|
|
17
|
-
message = f"Device(s) with name(s) '{device_name}' does not exist."
|
|
18
|
-
args = (message,)
|
|
19
|
-
else:
|
|
20
|
-
message = "Device(s) not found."
|
|
21
|
-
args = (message,)
|
|
22
|
-
super().__init__(*args)
|
src/devices_config_constants.py
DELETED