Skip to content

Commit a2b7e81

Browse files
Storage mangment API: disks infos
1 parent 088d12d commit a2b7e81

File tree

6 files changed

+141
-0
lines changed

6 files changed

+141
-0
lines changed

debian/control

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Depends: ${python3:Depends}, ${misc:Depends}
1515
, python3-miniupnpc, python3-dbus, python3-jinja2
1616
, python3-toml, python3-packaging, python3-publicsuffix2
1717
, python3-ldap, python3-zeroconf (>= 0.36), python3-lexicon,
18+
, python3-pyudev
1819
, python-is-python3
1920
, nginx, nginx-extras (>=1.18)
2021
, apt, apt-transport-https, apt-utils, aptitude, dirmngr

share/actionsmap.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,3 +2082,18 @@ diagnosis:
20822082
help: Remove a filter (it should be an existing filter as listed with "ignore --list")
20832083
nargs: "*"
20842084
metavar: CRITERIA
2085+
2086+
2087+
#############################
2088+
# Storage #
2089+
#############################
2090+
storage:
2091+
category_help: Manage hard-drives, filesystem, pools
2092+
subcategories:
2093+
disk:
2094+
subcategory_help: Manage et get infos about hard-drives
2095+
actions:
2096+
# storage_disks_list
2097+
infos:
2098+
action_help: Gets infos about hard-drives currently attached to this system
2099+
api: GET /storage/disk/infos

src/disks.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from collections import OrderedDict
2+
import dataclasses
3+
from glob import glob
4+
from typing import Optional
5+
6+
import pyudev
7+
import psutil
8+
9+
from moulinette.utils.log import getActionLogger
10+
11+
12+
from yunohost.utils.disks import filter_device
13+
14+
15+
logger = getActionLogger("yunohost.storage")
16+
17+
18+
@dataclasses.dataclass
19+
class DiskParts:
20+
devname: str
21+
filesystem: str
22+
encrypted: bool
23+
mountpoint: str
24+
25+
@staticmethod
26+
def from_parent_device(device: pyudev.Device, partitions):
27+
result = OrderedDict()
28+
for child_dev in sorted(
29+
filter(filter_device, device.children), key=lambda it: it.device_node
30+
):
31+
encrypted_provider = glob(f"/sys/block/dm-*/slaves/{child_dev.sys_name}")
32+
if encrypted_provider:
33+
# retrive the dm-x part
34+
dm = encrypted_provider[0].split("/")[3]
35+
enc_dev = pyudev.Devices.from_name(device.context, "block", dm)
36+
# This work for LUKS, what about other partition mecanisms?
37+
partname = f"/dev/mapper/{enc_dev.properties['DM_NAME']}"
38+
encrypted = True
39+
else:
40+
partname = child_dev.device_node
41+
encrypted = False
42+
43+
if partname not in partitions:
44+
logger.warning(
45+
f"{child_dev.device_node} not found by 'psutil.disk_partitions'"
46+
)
47+
continue
48+
49+
result[child_dev.sys_name] = DiskParts(
50+
devname=device.device_node,
51+
filesystem=partitions[partname].fstype,
52+
encrypted=encrypted,
53+
mountpoint=partitions[partname].mountpoint,
54+
)
55+
56+
return result
57+
58+
59+
@dataclasses.dataclass
60+
class DiskInfos:
61+
devname: str
62+
model: str
63+
serial: str
64+
size: int
65+
links: list[str]
66+
partitions: Optional[list[DiskParts]]
67+
68+
@staticmethod
69+
def from_device(device, partitions):
70+
try:
71+
dev_size = device.attributes.asint("size")
72+
except (AttributeError, UnicodeError, ValueError):
73+
dev_size = None
74+
75+
dev_links = list(sorted(it for it in device.device_links))
76+
child_parts = DiskParts.from_parent_device(device, partitions)
77+
78+
return DiskInfos(
79+
devname=device.device_node,
80+
model=device.get("ID_MODEL", None),
81+
serial=device.get("ID_SERIAL_SHORT", None),
82+
size=dev_size,
83+
links=dev_links,
84+
partitions=child_parts or None,
85+
)
86+
87+
88+
def infos():
89+
context = pyudev.Context()
90+
partitions = {it.device: it for it in psutil.disk_partitions()}
91+
result = OrderedDict()
92+
93+
for it in sorted(
94+
filter(filter_device, context.list_devices(subsystem="block", DEVTYPE="disk")),
95+
key=lambda it: it.device_node,
96+
):
97+
result[it.sys_name] = dataclasses.asdict(
98+
DiskInfos.from_device(it, partitions), dict_factory=OrderedDict
99+
)
100+
101+
return result

src/storage.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def storage_disk_infos():
2+
from yunohost.disks import infos
3+
4+
return infos()

src/tests/test_storage.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
def setup_function(function):
2+
...
3+
4+
def teardown_function(function):
5+
...
6+
7+
def test_storage_disks_infos():
8+
...

src/utils/disks.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import re
2+
3+
IGNORE_DISKS = "sr", "md", "dm-", "loop", "zd", "pmem"
4+
# regex: ^((sr)|(md)|...)
5+
IGNORE_DISK_RE = re.compile(rf"""^({"|".join([f'({it})' for it in IGNORE_DISKS])})""")
6+
7+
8+
def filter_device(device):
9+
"""
10+
Returns True if device has parents (e.g. USB device) and its name is not amongst
11+
"""
12+
return device.parent is not None and not IGNORE_DISK_RE.match(device.sys_name)

0 commit comments

Comments
 (0)