easymanet 0.1.0__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.
- easymanet/__init__.py +3 -0
- easymanet/disks/__init__.py +73 -0
- easymanet/disks/_common.py +88 -0
- easymanet/disks/core.py +139 -0
- easymanet/disks/linux.py +306 -0
- easymanet/disks/macos.py +343 -0
- easymanet/download.py +563 -0
- easymanet/format.py +13 -0
- easymanet/image.py +323 -0
- easymanet/inject.py +322 -0
- easymanet/manifest.py +91 -0
- easymanet/platform.py +25 -0
- easymanet/privileges.py +39 -0
- easymanet/render.py +60 -0
- easymanet/validate.py +338 -0
- easymanet/workspace.py +132 -0
- easymanet-0.1.0.data/data/share/easymanet/images/openmanet/provisioning/extra-packages.txt +6 -0
- easymanet-0.1.0.data/data/share/easymanet/images/openmanet/provisioning/openwrt-overlay/README.md +18 -0
- easymanet-0.1.0.data/data/share/easymanet/images/openmanet/provisioning/openwrt-overlay/etc/easymanet/provision.json +3 -0
- easymanet-0.1.0.data/data/share/easymanet/images/openmanet/provisioning/openwrt-overlay/etc/init.d/easymanet-boot-report +13 -0
- easymanet-0.1.0.data/data/share/easymanet/images/openmanet/provisioning/openwrt-overlay/etc/init.d/easymanet-management-lan +22 -0
- easymanet-0.1.0.data/data/share/easymanet/images/openmanet/provisioning/openwrt-overlay/etc/sysctl.d/99-easymanet.conf +18 -0
- easymanet-0.1.0.data/data/share/easymanet/images/openmanet/provisioning/openwrt-overlay/etc/uci-defaults/97-easymanet-management-lan +7 -0
- easymanet-0.1.0.data/data/share/easymanet/images/openmanet/provisioning/openwrt-overlay/etc/uci-defaults/98-easymanet-boot-report +8 -0
- easymanet-0.1.0.data/data/share/easymanet/images/openmanet/provisioning/openwrt-overlay/etc/uci-defaults/99-easymanet +24 -0
- easymanet-0.1.0.data/data/share/easymanet/images/openmanet/provisioning/openwrt-overlay/usr/lib/easymanet/boot-report.sh +125 -0
- easymanet-0.1.0.data/data/share/easymanet/images/openmanet/provisioning/openwrt-overlay/usr/lib/easymanet/network.sh +86 -0
- easymanet-0.1.0.data/data/share/easymanet/images/openmanet/provisioning/openwrt-overlay/usr/lib/easymanet/provision-lib.sh +75 -0
- easymanet-0.1.0.data/data/share/easymanet/images/openmanet/provisioning/openwrt-overlay/usr/lib/easymanet/provision.sh +532 -0
- easymanet-0.1.0.dist-info/METADATA +59 -0
- easymanet-0.1.0.dist-info/RECORD +42 -0
- easymanet-0.1.0.dist-info/WHEEL +5 -0
- easymanet-0.1.0.dist-info/entry_points.txt +2 -0
- easymanet-0.1.0.dist-info/top_level.txt +3 -0
- easymanet_cli/__init__.py +1 -0
- easymanet_cli/app.py +162 -0
- easymanet_cli/common.py +57 -0
- easymanet_cli/flash.py +437 -0
- easymanet_image/__init__.py +1 -0
- easymanet_image/build.py +410 -0
- easymanet_image/cli.py +240 -0
- easymanet_image/release.py +105 -0
easymanet/__init__.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Disk detection and listing for macOS and Linux."""
|
|
2
|
+
|
|
3
|
+
import glob
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
from ..platform import is_linux, is_macos
|
|
8
|
+
from ._common import (
|
|
9
|
+
DISK_PARSE_ERRORS,
|
|
10
|
+
DISK_SUSPICIOUS_SIZE_GB,
|
|
11
|
+
DISK_WARN_THRESHOLD_GB,
|
|
12
|
+
OVERLAY_WIPE_BLOCK_MIB,
|
|
13
|
+
OVERLAY_WIPE_BLOCKS,
|
|
14
|
+
SYS_MOUNT_POINTS,
|
|
15
|
+
DiskInfo,
|
|
16
|
+
debug_note,
|
|
17
|
+
)
|
|
18
|
+
from .core import (
|
|
19
|
+
assert_flash_allowed,
|
|
20
|
+
eject_disk,
|
|
21
|
+
find_disk,
|
|
22
|
+
get_partition2_wipe_range,
|
|
23
|
+
list_disks,
|
|
24
|
+
lookup_device,
|
|
25
|
+
unmount_disk,
|
|
26
|
+
)
|
|
27
|
+
from .linux import (
|
|
28
|
+
_check_linux_system_disk,
|
|
29
|
+
_findmnt_source,
|
|
30
|
+
_linux_base_block_device,
|
|
31
|
+
_linux_disk_from_lsblk,
|
|
32
|
+
_linux_lsblk_pkname,
|
|
33
|
+
_linux_partition2_wipe_range,
|
|
34
|
+
_linux_partitions_for_device,
|
|
35
|
+
_linux_resolve_findmnt_source,
|
|
36
|
+
_linux_root_block_devices,
|
|
37
|
+
_linux_should_list_default,
|
|
38
|
+
list_disks_linux,
|
|
39
|
+
lookup_device_linux,
|
|
40
|
+
unmount_disk_linux,
|
|
41
|
+
)
|
|
42
|
+
from .macos import (
|
|
43
|
+
_macos_partition2_wipe_range,
|
|
44
|
+
get_macos_partitions,
|
|
45
|
+
list_disks_macos,
|
|
46
|
+
lookup_device_macos,
|
|
47
|
+
unmount_disk_macos,
|
|
48
|
+
)
|
|
49
|
+
from .core import _is_block_device
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"DISK_PARSE_ERRORS",
|
|
53
|
+
"DISK_SUSPICIOUS_SIZE_GB",
|
|
54
|
+
"DISK_WARN_THRESHOLD_GB",
|
|
55
|
+
"DiskInfo",
|
|
56
|
+
"OVERLAY_WIPE_BLOCK_MIB",
|
|
57
|
+
"OVERLAY_WIPE_BLOCKS",
|
|
58
|
+
"SYS_MOUNT_POINTS",
|
|
59
|
+
"assert_flash_allowed",
|
|
60
|
+
"debug_note",
|
|
61
|
+
"eject_disk",
|
|
62
|
+
"find_disk",
|
|
63
|
+
"get_macos_partitions",
|
|
64
|
+
"get_partition2_wipe_range",
|
|
65
|
+
"glob",
|
|
66
|
+
"is_linux",
|
|
67
|
+
"is_macos",
|
|
68
|
+
"list_disks",
|
|
69
|
+
"lookup_device",
|
|
70
|
+
"os",
|
|
71
|
+
"subprocess",
|
|
72
|
+
"unmount_disk",
|
|
73
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Shared disk types and helpers."""
|
|
2
|
+
|
|
3
|
+
import plistlib
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
_SUBPROCESS_ERRORS = (
|
|
9
|
+
subprocess.CalledProcessError,
|
|
10
|
+
subprocess.TimeoutExpired,
|
|
11
|
+
FileNotFoundError,
|
|
12
|
+
)
|
|
13
|
+
DISK_PARSE_ERRORS = (
|
|
14
|
+
*_SUBPROCESS_ERRORS,
|
|
15
|
+
OSError,
|
|
16
|
+
ValueError,
|
|
17
|
+
UnicodeDecodeError,
|
|
18
|
+
plistlib.InvalidFileException,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
DISK_WARN_THRESHOLD_GB = 128
|
|
22
|
+
DISK_SUSPICIOUS_SIZE_GB = 256
|
|
23
|
+
|
|
24
|
+
SYS_MOUNT_POINTS = frozenset({"/", "/boot", "/boot/efi", "/home", "/var", "/usr"})
|
|
25
|
+
|
|
26
|
+
OVERLAY_WIPE_BLOCK_MIB = 16
|
|
27
|
+
OVERLAY_WIPE_BLOCKS = 288
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def debug_note(message: str) -> None:
|
|
31
|
+
print(f"easymanet: {message}", file=sys.stderr)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DiskInfo:
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
device: str,
|
|
38
|
+
size_bytes: int = 0,
|
|
39
|
+
model: str = "",
|
|
40
|
+
removable: bool = False,
|
|
41
|
+
mounted: Optional[List[str]] = None,
|
|
42
|
+
is_system: bool = False,
|
|
43
|
+
not_in_default_list: bool = False,
|
|
44
|
+
):
|
|
45
|
+
self.device = device
|
|
46
|
+
self.size_bytes = size_bytes
|
|
47
|
+
self.model = model
|
|
48
|
+
self.removable = removable
|
|
49
|
+
self.mounted = mounted or []
|
|
50
|
+
self.is_system = is_system
|
|
51
|
+
self.not_in_default_list = not_in_default_list
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def size_gb(self) -> float:
|
|
55
|
+
return self.size_bytes / (1024 ** 3)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def size_human(self) -> str:
|
|
59
|
+
gb = self.size_gb
|
|
60
|
+
if gb < 1:
|
|
61
|
+
mb = self.size_bytes / (1024 ** 2)
|
|
62
|
+
return f"{mb:.1f} MB"
|
|
63
|
+
return f"{gb:.1f} GB"
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def blocking_warnings(self) -> List[str]:
|
|
67
|
+
w: List[str] = []
|
|
68
|
+
if self.is_system:
|
|
69
|
+
w.append(
|
|
70
|
+
"WARNING: This appears to be a system disk — use --force to override"
|
|
71
|
+
)
|
|
72
|
+
elif not self.removable and self.size_gb > DISK_WARN_THRESHOLD_GB:
|
|
73
|
+
w.append(
|
|
74
|
+
"WARNING: Large fixed disk — use --force to proceed"
|
|
75
|
+
)
|
|
76
|
+
elif self.size_gb > DISK_SUSPICIOUS_SIZE_GB:
|
|
77
|
+
w.append(
|
|
78
|
+
"WARNING: Suspiciously large device — use --force to proceed"
|
|
79
|
+
)
|
|
80
|
+
if self.not_in_default_list:
|
|
81
|
+
w.append(
|
|
82
|
+
"WARNING: Device not in default disk list — use --force to proceed"
|
|
83
|
+
)
|
|
84
|
+
return w
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def warnings(self) -> List[str]:
|
|
88
|
+
return self.blocking_warnings
|
easymanet/disks/core.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Cross-platform disk listing and flash safety."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import stat
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from typing import List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from ._common import (
|
|
10
|
+
OVERLAY_WIPE_BLOCK_MIB,
|
|
11
|
+
OVERLAY_WIPE_BLOCKS,
|
|
12
|
+
DiskInfo,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _disks_module():
|
|
17
|
+
return sys.modules[__package__]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_block_device(path: str) -> bool:
|
|
21
|
+
if not os.path.exists(path):
|
|
22
|
+
return False
|
|
23
|
+
try:
|
|
24
|
+
return stat.S_ISBLK(os.stat(path).st_mode)
|
|
25
|
+
except OSError:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def list_disks(include_all: bool = False) -> List[DiskInfo]:
|
|
30
|
+
disks_mod = _disks_module()
|
|
31
|
+
if disks_mod.is_macos():
|
|
32
|
+
disks = disks_mod.list_disks_macos(include_all=include_all)
|
|
33
|
+
elif disks_mod.is_linux():
|
|
34
|
+
disks = disks_mod.list_disks_linux(include_all=include_all)
|
|
35
|
+
else:
|
|
36
|
+
return []
|
|
37
|
+
return sorted(disks, key=lambda d: d.size_bytes, reverse=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def lookup_device(
|
|
41
|
+
device: str,
|
|
42
|
+
default_disks: Optional[List[DiskInfo]] = None,
|
|
43
|
+
) -> Optional[DiskInfo]:
|
|
44
|
+
disks_mod = _disks_module()
|
|
45
|
+
if disks_mod.is_macos():
|
|
46
|
+
disk = disks_mod.lookup_device_macos(device)
|
|
47
|
+
elif disks_mod.is_linux():
|
|
48
|
+
disk = disks_mod.lookup_device_linux(device)
|
|
49
|
+
else:
|
|
50
|
+
disk = None
|
|
51
|
+
|
|
52
|
+
if disk is None:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
disks = (
|
|
56
|
+
default_disks
|
|
57
|
+
if default_disks is not None
|
|
58
|
+
else disks_mod.list_disks(include_all=False)
|
|
59
|
+
)
|
|
60
|
+
if not any(d.device == device for d in disks):
|
|
61
|
+
disk.not_in_default_list = True
|
|
62
|
+
return disk
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def find_disk(device: str) -> Optional[DiskInfo]:
|
|
66
|
+
disks = list_disks()
|
|
67
|
+
for disk in disks:
|
|
68
|
+
if disk.device == device:
|
|
69
|
+
return disk
|
|
70
|
+
return lookup_device(device, default_disks=disks)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def assert_flash_allowed(device: str, force: bool = False) -> DiskInfo:
|
|
74
|
+
if not _disks_module()._is_block_device(device):
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Device {device} does not exist or is not a block device."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
disk = _disks_module().lookup_device(device)
|
|
80
|
+
if disk is None:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Could not read disk information for {device}."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
blocking = disk.blocking_warnings
|
|
86
|
+
if blocking and not force:
|
|
87
|
+
lines = "\n".join(f" {w}" for w in blocking)
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"Refusing to flash {device}:\n{lines}\n"
|
|
90
|
+
f" Model: {disk.model}\n"
|
|
91
|
+
f" Size: {disk.size_human}\n"
|
|
92
|
+
f" Mounted: {', '.join(disk.mounted) if disk.mounted else 'none'}\n"
|
|
93
|
+
f"Use --force to override."
|
|
94
|
+
)
|
|
95
|
+
return disk
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def unmount_disk(device: str) -> None:
|
|
99
|
+
disks_mod = _disks_module()
|
|
100
|
+
if disks_mod.is_macos():
|
|
101
|
+
disks_mod.unmount_disk_macos(device)
|
|
102
|
+
elif disks_mod.is_linux():
|
|
103
|
+
disks_mod.unmount_disk_linux(device)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_partition2_wipe_range(device: str) -> Optional[Tuple[int, int]]:
|
|
107
|
+
"""Return (start_byte_offset, wipe_bytes) for partition 2 stale-overlay tail wipe."""
|
|
108
|
+
max_wipe = OVERLAY_WIPE_BLOCK_MIB * OVERLAY_WIPE_BLOCKS * 1024 * 1024
|
|
109
|
+
disks_mod = _disks_module()
|
|
110
|
+
|
|
111
|
+
if disks_mod.is_linux():
|
|
112
|
+
return disks_mod._linux_partition2_wipe_range(device, max_wipe)
|
|
113
|
+
if disks_mod.is_macos():
|
|
114
|
+
return disks_mod._macos_partition2_wipe_range(device, max_wipe)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def eject_disk(device: str) -> None:
|
|
119
|
+
disks_mod = _disks_module()
|
|
120
|
+
if disks_mod.is_macos():
|
|
121
|
+
result = subprocess.run(
|
|
122
|
+
["diskutil", "eject", device],
|
|
123
|
+
capture_output=True,
|
|
124
|
+
text=True,
|
|
125
|
+
timeout=30,
|
|
126
|
+
)
|
|
127
|
+
elif disks_mod.is_linux():
|
|
128
|
+
result = subprocess.run(
|
|
129
|
+
["eject", device],
|
|
130
|
+
capture_output=True,
|
|
131
|
+
text=True,
|
|
132
|
+
timeout=30,
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
return
|
|
136
|
+
if result.returncode != 0:
|
|
137
|
+
detail = (result.stderr or result.stdout or "").strip()
|
|
138
|
+
suffix = f": {detail}" if detail else ""
|
|
139
|
+
raise RuntimeError(f"Failed to eject {device}{suffix}")
|
easymanet/disks/linux.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Linux disk discovery and safety helpers."""
|
|
2
|
+
|
|
3
|
+
import glob
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from typing import List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from ._common import DISK_PARSE_ERRORS, DiskInfo, debug_note
|
|
12
|
+
|
|
13
|
+
_FINDMNT_PARENT_RESOLUTION_MAX_DEPTH = 8
|
|
14
|
+
_LINUX_DEFAULT_LOGICAL_SECTOR_BYTES = 512
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _disks_module():
|
|
18
|
+
return sys.modules[__package__]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_lsblk_size(size_str: str) -> int:
|
|
22
|
+
try:
|
|
23
|
+
return int(size_str)
|
|
24
|
+
except ValueError:
|
|
25
|
+
suffixes = {"K": 1024, "M": 1024**2, "G": 1024**3, "T": 1024**4}
|
|
26
|
+
try:
|
|
27
|
+
num = float(size_str[:-1])
|
|
28
|
+
suffix = size_str[-1].upper()
|
|
29
|
+
return int(num * suffixes.get(suffix, 1))
|
|
30
|
+
except (ValueError, IndexError):
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_linux_mounts(dev: dict) -> List[str]:
|
|
35
|
+
mounts = []
|
|
36
|
+
children = dev.get("children", [])
|
|
37
|
+
for child in children:
|
|
38
|
+
mp = child.get("mountpoint")
|
|
39
|
+
if mp:
|
|
40
|
+
mounts.append(mp)
|
|
41
|
+
return mounts
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _linux_disk_from_lsblk(dev: dict) -> DiskInfo:
|
|
45
|
+
dev_name = dev.get("name", "")
|
|
46
|
+
dev_path = f"/dev/{dev_name}"
|
|
47
|
+
model = (dev.get("model") or "").strip() or dev_name
|
|
48
|
+
removable = dev.get("rm", "0") == "1"
|
|
49
|
+
tran = (dev.get("tran") or "").lower()
|
|
50
|
+
if tran in ("usb", "mmc"):
|
|
51
|
+
removable = True
|
|
52
|
+
size_bytes = _parse_lsblk_size(dev.get("size", "0"))
|
|
53
|
+
mounted = _get_linux_mounts(dev)
|
|
54
|
+
is_system = _check_linux_system_disk(dev_path, mounted)
|
|
55
|
+
return DiskInfo(
|
|
56
|
+
device=dev_path,
|
|
57
|
+
size_bytes=size_bytes,
|
|
58
|
+
model=model,
|
|
59
|
+
removable=removable,
|
|
60
|
+
mounted=mounted,
|
|
61
|
+
is_system=is_system,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _linux_should_list_default(dev: dict) -> bool:
|
|
66
|
+
if dev.get("type") != "disk":
|
|
67
|
+
return False
|
|
68
|
+
if dev.get("rm", "0") == "1":
|
|
69
|
+
return True
|
|
70
|
+
tran = (dev.get("tran") or "").lower()
|
|
71
|
+
return tran in ("usb", "mmc")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _linux_lsblk_data(device: Optional[str] = None) -> Optional[dict]:
|
|
75
|
+
cmd = [
|
|
76
|
+
"lsblk",
|
|
77
|
+
"-J",
|
|
78
|
+
"-o",
|
|
79
|
+
"NAME,SIZE,TYPE,MOUNTPOINT,MODEL,RM,ROTA,TRAN",
|
|
80
|
+
]
|
|
81
|
+
if device:
|
|
82
|
+
cmd.extend(["-n", device])
|
|
83
|
+
try:
|
|
84
|
+
output = subprocess.check_output(cmd, timeout=10).decode()
|
|
85
|
+
return json.loads(output)
|
|
86
|
+
except (
|
|
87
|
+
subprocess.CalledProcessError,
|
|
88
|
+
FileNotFoundError,
|
|
89
|
+
json.JSONDecodeError,
|
|
90
|
+
subprocess.TimeoutExpired,
|
|
91
|
+
):
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def list_disks_linux(include_all: bool = False) -> List[DiskInfo]:
|
|
96
|
+
data = _linux_lsblk_data()
|
|
97
|
+
if not data:
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
disks: List[DiskInfo] = []
|
|
101
|
+
for dev in data.get("blockdevices", []):
|
|
102
|
+
if dev.get("type") != "disk":
|
|
103
|
+
continue
|
|
104
|
+
if not include_all and not _linux_should_list_default(dev):
|
|
105
|
+
continue
|
|
106
|
+
disks.append(_linux_disk_from_lsblk(dev))
|
|
107
|
+
|
|
108
|
+
return sorted(disks, key=lambda d: d.size_bytes, reverse=True)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def lookup_device_linux(device: str) -> Optional[DiskInfo]:
|
|
112
|
+
data = _linux_lsblk_data(device)
|
|
113
|
+
if not data:
|
|
114
|
+
return None
|
|
115
|
+
blockdevs = data.get("blockdevices", [])
|
|
116
|
+
if not blockdevs:
|
|
117
|
+
return None
|
|
118
|
+
dev = blockdevs[0]
|
|
119
|
+
if dev.get("type") != "disk":
|
|
120
|
+
return None
|
|
121
|
+
return _linux_disk_from_lsblk(dev)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _findmnt_source(mount_point: str) -> Optional[str]:
|
|
125
|
+
try:
|
|
126
|
+
output = subprocess.check_output(
|
|
127
|
+
["findmnt", "-n", "-o", "SOURCE", mount_point],
|
|
128
|
+
stderr=subprocess.DEVNULL,
|
|
129
|
+
timeout=5,
|
|
130
|
+
).decode()
|
|
131
|
+
return output.strip() or None
|
|
132
|
+
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _linux_base_block_device(source: str) -> Optional[str]:
|
|
137
|
+
if not source.startswith("/dev/"):
|
|
138
|
+
return None
|
|
139
|
+
match = re.match(r"^(?P<base>/dev/(?:mmcblk\d+|nvme\d+n\d+))p\d+$", source)
|
|
140
|
+
if match:
|
|
141
|
+
return match.group("base")
|
|
142
|
+
match = re.match(r"^(?P<base>/dev/[a-z]+)\d+$", source)
|
|
143
|
+
if match:
|
|
144
|
+
return match.group("base")
|
|
145
|
+
if re.match(r"^/dev/(?:mmcblk\d+|nvme\d+n\d+|[a-z]+)$", source):
|
|
146
|
+
return source
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _linux_lsblk_pkname(device: str) -> Optional[str]:
|
|
151
|
+
try:
|
|
152
|
+
output = subprocess.check_output(
|
|
153
|
+
["lsblk", "-no", "PKNAME", device],
|
|
154
|
+
stderr=subprocess.DEVNULL,
|
|
155
|
+
timeout=5,
|
|
156
|
+
).decode()
|
|
157
|
+
name = output.strip()
|
|
158
|
+
if name:
|
|
159
|
+
return f"/dev/{name}"
|
|
160
|
+
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
|
|
161
|
+
return None
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _linux_resolve_findmnt_source(source: str) -> Optional[str]:
|
|
166
|
+
disks_mod = _disks_module()
|
|
167
|
+
device_path: Optional[str] = None
|
|
168
|
+
if source.startswith("UUID="):
|
|
169
|
+
uuid = source.split("=", 1)[1]
|
|
170
|
+
by_uuid = f"/dev/disk/by-uuid/{uuid}"
|
|
171
|
+
if disks_mod.os.path.exists(by_uuid):
|
|
172
|
+
device_path = disks_mod.os.path.realpath(by_uuid)
|
|
173
|
+
elif source.startswith("PARTUUID="):
|
|
174
|
+
partuuid = source.split("=", 1)[1]
|
|
175
|
+
by_partuuid = f"/dev/disk/by-partuuid/{partuuid}"
|
|
176
|
+
if disks_mod.os.path.exists(by_partuuid):
|
|
177
|
+
device_path = disks_mod.os.path.realpath(by_partuuid)
|
|
178
|
+
elif source.startswith("/dev/"):
|
|
179
|
+
device_path = disks_mod.os.path.realpath(source)
|
|
180
|
+
else:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
if not device_path:
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
current = device_path
|
|
187
|
+
for _ in range(_FINDMNT_PARENT_RESOLUTION_MAX_DEPTH):
|
|
188
|
+
base = disks_mod._linux_base_block_device(current)
|
|
189
|
+
if base:
|
|
190
|
+
return base
|
|
191
|
+
parent = disks_mod._linux_lsblk_pkname(current)
|
|
192
|
+
if not parent or parent == current:
|
|
193
|
+
break
|
|
194
|
+
current = parent
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _linux_partitions_for_device(device: str) -> List[str]:
|
|
199
|
+
partitions = set()
|
|
200
|
+
for pattern in (f"{device}[0-9]*", f"{device}p[0-9]*"):
|
|
201
|
+
partitions.update(_disks_module().glob.glob(pattern))
|
|
202
|
+
partitions.discard(device)
|
|
203
|
+
return sorted(partitions)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _linux_root_block_devices() -> set:
|
|
207
|
+
disks_mod = _disks_module()
|
|
208
|
+
related: set = set()
|
|
209
|
+
for mount_point in sorted(disks_mod.SYS_MOUNT_POINTS):
|
|
210
|
+
source = disks_mod._findmnt_source(mount_point)
|
|
211
|
+
if not source:
|
|
212
|
+
continue
|
|
213
|
+
base = disks_mod._linux_resolve_findmnt_source(source)
|
|
214
|
+
if not base:
|
|
215
|
+
continue
|
|
216
|
+
related.add(base)
|
|
217
|
+
related.update(disks_mod._linux_partitions_for_device(base))
|
|
218
|
+
if source.startswith("/dev/"):
|
|
219
|
+
resolved = disks_mod.os.path.realpath(source)
|
|
220
|
+
if resolved.startswith("/dev/"):
|
|
221
|
+
related.add(resolved)
|
|
222
|
+
return related
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _check_linux_system_disk(dev_path: str, mounts: List[str]) -> bool:
|
|
226
|
+
disks_mod = _disks_module()
|
|
227
|
+
if any(mp in disks_mod.SYS_MOUNT_POINTS for mp in mounts):
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
root_related = disks_mod._linux_root_block_devices()
|
|
231
|
+
if not root_related:
|
|
232
|
+
debug_note(
|
|
233
|
+
"could not resolve Linux root block devices; treating candidate as a system disk"
|
|
234
|
+
)
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
if dev_path in root_related:
|
|
238
|
+
return True
|
|
239
|
+
if set(disks_mod._linux_partitions_for_device(dev_path)) & root_related:
|
|
240
|
+
return True
|
|
241
|
+
for entry in root_related:
|
|
242
|
+
if disks_mod._linux_base_block_device(entry) == dev_path:
|
|
243
|
+
return True
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _linux_partition2_wipe_range(device: str, max_wipe: int) -> Optional[Tuple[int, int]]:
|
|
248
|
+
cmd = ["lsblk", "-J", "-b", "-o", "NAME,START,SIZE,TYPE,LOG-SEC", "-n", device]
|
|
249
|
+
try:
|
|
250
|
+
output = subprocess.check_output(cmd, timeout=10).decode()
|
|
251
|
+
data = json.loads(output)
|
|
252
|
+
except (
|
|
253
|
+
subprocess.CalledProcessError,
|
|
254
|
+
FileNotFoundError,
|
|
255
|
+
json.JSONDecodeError,
|
|
256
|
+
subprocess.TimeoutExpired,
|
|
257
|
+
):
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
blockdevs = data.get("blockdevices", [])
|
|
261
|
+
if not blockdevs:
|
|
262
|
+
return None
|
|
263
|
+
children = blockdevs[0].get("children", [])
|
|
264
|
+
parts = [c for c in children if c.get("type") == "part"]
|
|
265
|
+
if len(parts) < 2:
|
|
266
|
+
return None
|
|
267
|
+
part2 = sorted(parts, key=lambda p: int(p.get("start", 0) or 0))[1]
|
|
268
|
+
start = int(part2.get("start", 0) or 0)
|
|
269
|
+
size = int(part2.get("size", 0) or 0)
|
|
270
|
+
if start <= 0 or size <= 0:
|
|
271
|
+
return None
|
|
272
|
+
sector_bytes = _linux_logical_sector_bytes(part2, blockdevs[0])
|
|
273
|
+
start_bytes = start * sector_bytes
|
|
274
|
+
wipe_bytes = min(size, max_wipe)
|
|
275
|
+
tail_start = start_bytes + size - wipe_bytes
|
|
276
|
+
return (tail_start, wipe_bytes)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _linux_logical_sector_bytes(*devices: dict) -> int:
|
|
280
|
+
for dev in devices:
|
|
281
|
+
for key in ("log-sec", "log_sec", "LOG-SEC"):
|
|
282
|
+
try:
|
|
283
|
+
value = int(dev.get(key, 0) or 0)
|
|
284
|
+
except (TypeError, ValueError):
|
|
285
|
+
continue
|
|
286
|
+
if value > 0:
|
|
287
|
+
return value
|
|
288
|
+
return _LINUX_DEFAULT_LOGICAL_SECTOR_BYTES
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def unmount_disk_linux(device: str) -> None:
|
|
292
|
+
targets = _linux_partitions_for_device(device) or [device]
|
|
293
|
+
for target in targets:
|
|
294
|
+
result = subprocess.run(
|
|
295
|
+
["umount", target],
|
|
296
|
+
capture_output=True,
|
|
297
|
+
text=True,
|
|
298
|
+
timeout=60,
|
|
299
|
+
)
|
|
300
|
+
if result.returncode == 0:
|
|
301
|
+
continue
|
|
302
|
+
detail = (result.stderr or result.stdout or "").strip()
|
|
303
|
+
if "not mounted" in detail.lower():
|
|
304
|
+
continue
|
|
305
|
+
suffix = f": {detail}" if detail else ""
|
|
306
|
+
raise RuntimeError(f"Failed to unmount {target}{suffix}")
|