statblk 1.0__tar.gz
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.
- statblk-1.0/PKG-INFO +43 -0
- statblk-1.0/README.txt +21 -0
- statblk-1.0/setup.cfg +4 -0
- statblk-1.0/setup.py +25 -0
- statblk-1.0/statblk.egg-info/PKG-INFO +43 -0
- statblk-1.0/statblk.egg-info/SOURCES.txt +8 -0
- statblk-1.0/statblk.egg-info/dependency_links.txt +1 -0
- statblk-1.0/statblk.egg-info/entry_points.txt +2 -0
- statblk-1.0/statblk.egg-info/top_level.txt +1 -0
- statblk-1.0/statblk.py +580 -0
statblk-1.0/PKG-INFO
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: statblk
|
3
|
+
Version: 1.0
|
4
|
+
Summary: Gather essential disk and partition info for block devices and print it in a nice table
|
5
|
+
Home-page: https://github.com/yufei-pan/statblk
|
6
|
+
Author: Yufei Pan
|
7
|
+
Author-email: pan@zopyr.us
|
8
|
+
License: GPLv3+
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
11
|
+
Requires-Python: >=3.6
|
12
|
+
Description-Content-Type: text/plain
|
13
|
+
Dynamic: author
|
14
|
+
Dynamic: author-email
|
15
|
+
Dynamic: classifier
|
16
|
+
Dynamic: description
|
17
|
+
Dynamic: description-content-type
|
18
|
+
Dynamic: home-page
|
19
|
+
Dynamic: license
|
20
|
+
Dynamic: requires-python
|
21
|
+
Dynamic: summary
|
22
|
+
|
23
|
+
usage: statblk.py [-h] [-j] [-b] [-H] [-F] [-M] [-B] [--show_zero_size_devices] [-V]
|
24
|
+
|
25
|
+
Gather disk and partition info for block devices.
|
26
|
+
|
27
|
+
options:
|
28
|
+
-h, --help show this help message and exit
|
29
|
+
-j, --json Produce JSON output
|
30
|
+
-b, --bytes Print the SIZE column in bytes rather than in a human-readable format
|
31
|
+
-H, --si Use powers of 1000 not 1024 for SIZE column
|
32
|
+
-F, -fo, --formated_only
|
33
|
+
Show only formated filesystems
|
34
|
+
-M, -mo, --mounted_only
|
35
|
+
Show only mounted filesystems
|
36
|
+
-B, -bo, --best_only Show only best mount for each device
|
37
|
+
--show_zero_size_devices
|
38
|
+
Show devices with zero size
|
39
|
+
-V, --version show program's version number and exit
|
40
|
+
|
41
|
+
|
42
|
+
Install:
|
43
|
+
sudo pip install statblk
|
statblk-1.0/README.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
usage: statblk.py [-h] [-j] [-b] [-H] [-F] [-M] [-B] [--show_zero_size_devices] [-V]
|
2
|
+
|
3
|
+
Gather disk and partition info for block devices.
|
4
|
+
|
5
|
+
options:
|
6
|
+
-h, --help show this help message and exit
|
7
|
+
-j, --json Produce JSON output
|
8
|
+
-b, --bytes Print the SIZE column in bytes rather than in a human-readable format
|
9
|
+
-H, --si Use powers of 1000 not 1024 for SIZE column
|
10
|
+
-F, -fo, --formated_only
|
11
|
+
Show only formated filesystems
|
12
|
+
-M, -mo, --mounted_only
|
13
|
+
Show only mounted filesystems
|
14
|
+
-B, -bo, --best_only Show only best mount for each device
|
15
|
+
--show_zero_size_devices
|
16
|
+
Show devices with zero size
|
17
|
+
-V, --version show program's version number and exit
|
18
|
+
|
19
|
+
|
20
|
+
Install:
|
21
|
+
sudo pip install statblk
|
statblk-1.0/setup.cfg
ADDED
statblk-1.0/setup.py
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
from setuptools import setup
|
2
|
+
from statblk import version
|
3
|
+
|
4
|
+
setup(
|
5
|
+
name='statblk',
|
6
|
+
version=version,
|
7
|
+
description='Gather essential disk and partition info for block devices and print it in a nice table',
|
8
|
+
long_description=open('README.txt').read(),
|
9
|
+
long_description_content_type='text/plain',
|
10
|
+
author='Yufei Pan',
|
11
|
+
author_email='pan@zopyr.us',
|
12
|
+
url='https://github.com/yufei-pan/statblk',
|
13
|
+
py_modules=['statblk'],
|
14
|
+
entry_points={
|
15
|
+
'console_scripts': [
|
16
|
+
'statblk=statblk:main',
|
17
|
+
],
|
18
|
+
},
|
19
|
+
classifiers=[
|
20
|
+
'Programming Language :: Python :: 3',
|
21
|
+
'Operating System :: POSIX :: Linux',
|
22
|
+
],
|
23
|
+
python_requires='>=3.6',
|
24
|
+
license='GPLv3+',
|
25
|
+
)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: statblk
|
3
|
+
Version: 1.0
|
4
|
+
Summary: Gather essential disk and partition info for block devices and print it in a nice table
|
5
|
+
Home-page: https://github.com/yufei-pan/statblk
|
6
|
+
Author: Yufei Pan
|
7
|
+
Author-email: pan@zopyr.us
|
8
|
+
License: GPLv3+
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
11
|
+
Requires-Python: >=3.6
|
12
|
+
Description-Content-Type: text/plain
|
13
|
+
Dynamic: author
|
14
|
+
Dynamic: author-email
|
15
|
+
Dynamic: classifier
|
16
|
+
Dynamic: description
|
17
|
+
Dynamic: description-content-type
|
18
|
+
Dynamic: home-page
|
19
|
+
Dynamic: license
|
20
|
+
Dynamic: requires-python
|
21
|
+
Dynamic: summary
|
22
|
+
|
23
|
+
usage: statblk.py [-h] [-j] [-b] [-H] [-F] [-M] [-B] [--show_zero_size_devices] [-V]
|
24
|
+
|
25
|
+
Gather disk and partition info for block devices.
|
26
|
+
|
27
|
+
options:
|
28
|
+
-h, --help show this help message and exit
|
29
|
+
-j, --json Produce JSON output
|
30
|
+
-b, --bytes Print the SIZE column in bytes rather than in a human-readable format
|
31
|
+
-H, --si Use powers of 1000 not 1024 for SIZE column
|
32
|
+
-F, -fo, --formated_only
|
33
|
+
Show only formated filesystems
|
34
|
+
-M, -mo, --mounted_only
|
35
|
+
Show only mounted filesystems
|
36
|
+
-B, -bo, --best_only Show only best mount for each device
|
37
|
+
--show_zero_size_devices
|
38
|
+
Show devices with zero size
|
39
|
+
-V, --version show program's version number and exit
|
40
|
+
|
41
|
+
|
42
|
+
Install:
|
43
|
+
sudo pip install statblk
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
statblk
|
statblk-1.0/statblk.py
ADDED
@@ -0,0 +1,580 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# requires-python = ">=3.6"
|
3
|
+
# -*- coding: utf-8 -*-
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
import os
|
8
|
+
import re
|
9
|
+
import stat
|
10
|
+
from collections import defaultdict
|
11
|
+
import argparse
|
12
|
+
import shutil
|
13
|
+
import subprocess
|
14
|
+
|
15
|
+
version = '1.0'
|
16
|
+
VERSION = version
|
17
|
+
__version__ = version
|
18
|
+
COMMIT_DATE = '2025-08-26'
|
19
|
+
|
20
|
+
|
21
|
+
SMARTCTL_PATH = shutil.which("smartctl")
|
22
|
+
|
23
|
+
def read_text(path):
|
24
|
+
try:
|
25
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
26
|
+
return f.read().strip()
|
27
|
+
except Exception:
|
28
|
+
return None
|
29
|
+
|
30
|
+
|
31
|
+
def read_int(path):
|
32
|
+
s = read_text(path)
|
33
|
+
if s is None:
|
34
|
+
return None
|
35
|
+
try:
|
36
|
+
return int(s)
|
37
|
+
except Exception:
|
38
|
+
return None
|
39
|
+
|
40
|
+
|
41
|
+
def build_symlink_map(dir_path):
|
42
|
+
"""
|
43
|
+
Build map: devname -> token (uuid or label string) using symlinks under
|
44
|
+
/dev/disk/by-uuid or /dev/disk/by-label.
|
45
|
+
"""
|
46
|
+
mapping = {}
|
47
|
+
if not os.path.isdir(dir_path):
|
48
|
+
return mapping
|
49
|
+
try:
|
50
|
+
for entry in os.listdir(dir_path):
|
51
|
+
p = os.path.join(dir_path, entry)
|
52
|
+
try:
|
53
|
+
if os.path.islink(p):
|
54
|
+
tgt = os.path.realpath(p)
|
55
|
+
devname = os.path.basename(tgt)
|
56
|
+
mapping.setdefault(devname, entry)
|
57
|
+
except Exception:
|
58
|
+
continue
|
59
|
+
except Exception:
|
60
|
+
pass
|
61
|
+
return mapping
|
62
|
+
|
63
|
+
|
64
|
+
def parse_mountinfo_enhanced(uuid_rev, label_rev):
|
65
|
+
"""
|
66
|
+
Parse /proc/self/mountinfo.
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
- by_majmin: dict "major:minor" -> list of mounts
|
70
|
+
- by_devname: dict devname -> list of mounts (resolved from source)
|
71
|
+
- all_mounts: list of mount dicts
|
72
|
+
|
73
|
+
Each mount dict: {mountpoint, fstype, source, majmin}
|
74
|
+
"""
|
75
|
+
by_majmin = defaultdict(list)
|
76
|
+
by_devname = defaultdict(list)
|
77
|
+
all_mounts = []
|
78
|
+
|
79
|
+
def resolve_source_to_devnames(src):
|
80
|
+
# Returns list of candidate devnames for a given source string
|
81
|
+
if not src:
|
82
|
+
return []
|
83
|
+
cands = []
|
84
|
+
try:
|
85
|
+
if src.startswith("/dev/"):
|
86
|
+
real = os.path.realpath(src)
|
87
|
+
dn = os.path.basename(real)
|
88
|
+
if dn:
|
89
|
+
cands.append(dn)
|
90
|
+
elif src.startswith("UUID="):
|
91
|
+
u = src[5:]
|
92
|
+
dn = uuid_rev.get(u)
|
93
|
+
if dn:
|
94
|
+
cands.append(dn)
|
95
|
+
elif src.startswith("LABEL="):
|
96
|
+
l = src[6:]
|
97
|
+
dn = label_rev.get(l)
|
98
|
+
if dn:
|
99
|
+
cands.append(dn)
|
100
|
+
except Exception:
|
101
|
+
pass
|
102
|
+
return cands
|
103
|
+
|
104
|
+
try:
|
105
|
+
with open("/proc/self/mountinfo", "r", encoding="utf-8") as f:
|
106
|
+
for line in f:
|
107
|
+
line = line.strip()
|
108
|
+
if not line:
|
109
|
+
continue
|
110
|
+
parts = line.split()
|
111
|
+
try:
|
112
|
+
majmin = parts[2]
|
113
|
+
mnt_point = parts[4]
|
114
|
+
dash_idx = parts.index("-")
|
115
|
+
fstype = parts[dash_idx + 1]
|
116
|
+
source = parts[dash_idx + 2] if len(parts) > dash_idx + 2 else ""
|
117
|
+
rec = {
|
118
|
+
"MOUNTPOINT": mnt_point,
|
119
|
+
"FSTYPE": fstype,
|
120
|
+
"SOURCE": source,
|
121
|
+
"MAJMIN": majmin,
|
122
|
+
}
|
123
|
+
all_mounts.append(rec)
|
124
|
+
by_majmin[majmin].append(rec)
|
125
|
+
# Build secondary index by devname from source
|
126
|
+
for dn in resolve_source_to_devnames(source):
|
127
|
+
by_devname[dn].append(rec)
|
128
|
+
except Exception:
|
129
|
+
continue
|
130
|
+
except Exception:
|
131
|
+
pass
|
132
|
+
|
133
|
+
return by_majmin, by_devname, all_mounts
|
134
|
+
|
135
|
+
|
136
|
+
def get_statvfs_use_percent(mountpoint):
|
137
|
+
try:
|
138
|
+
st = os.statvfs(mountpoint)
|
139
|
+
if st.f_blocks == 0:
|
140
|
+
return None
|
141
|
+
used_pct = 100.0 * (1.0 - (st.f_bavail / float(st.f_blocks)))
|
142
|
+
return int(round(used_pct))
|
143
|
+
except Exception:
|
144
|
+
return None
|
145
|
+
|
146
|
+
|
147
|
+
def read_discard_support(sys_block_path):
|
148
|
+
if not sys_block_path or not os.path.isdir(sys_block_path):
|
149
|
+
return False
|
150
|
+
dmbytes = read_int(os.path.join(sys_block_path, "queue", "discard_max_bytes"))
|
151
|
+
dgran = read_int(os.path.join(sys_block_path, "queue", "discard_granularity"))
|
152
|
+
try:
|
153
|
+
return (dmbytes or 0) > 0 or (dgran or 0) > 0
|
154
|
+
except Exception:
|
155
|
+
return False
|
156
|
+
|
157
|
+
|
158
|
+
def get_parent_device_sysfs(block_sysfs_path):
|
159
|
+
"""
|
160
|
+
Return the sysfs 'device' directory for this block node (resolves partition
|
161
|
+
to its parent device as well).
|
162
|
+
"""
|
163
|
+
dev_link = os.path.join(block_sysfs_path, "device")
|
164
|
+
try:
|
165
|
+
return os.path.realpath(dev_link)
|
166
|
+
except Exception:
|
167
|
+
return dev_link
|
168
|
+
|
169
|
+
|
170
|
+
def read_model_and_serial(block_sysfs_path):
|
171
|
+
if not block_sysfs_path or not os.path.isdir(block_sysfs_path):
|
172
|
+
return None, None
|
173
|
+
device_path = get_parent_device_sysfs(block_sysfs_path)
|
174
|
+
model = read_text(os.path.join(device_path, "model"))
|
175
|
+
serial = read_text(os.path.join(device_path, "serial"))
|
176
|
+
if serial is None:
|
177
|
+
serial = read_text(os.path.join(device_path, "wwid"))
|
178
|
+
if model:
|
179
|
+
model = " ".join(model.split())
|
180
|
+
if serial:
|
181
|
+
serial = " ".join(serial.split())
|
182
|
+
return model, serial
|
183
|
+
|
184
|
+
|
185
|
+
def get_udev_props(major, minor):
|
186
|
+
"""
|
187
|
+
Read udev properties for a block device from /run/udev/data/bMAJ:MIN
|
188
|
+
Returns keys like ID_FS_TYPE, ID_FS_LABEL, ID_FS_UUID when available.
|
189
|
+
"""
|
190
|
+
props = {}
|
191
|
+
path = f"/run/udev/data/b{major}:{minor}"
|
192
|
+
try:
|
193
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
194
|
+
for line in f:
|
195
|
+
line = line.strip()
|
196
|
+
if not line or "=" not in line or line.startswith("#"):
|
197
|
+
continue
|
198
|
+
k, v = line.split("=", 1)
|
199
|
+
props[k] = v
|
200
|
+
except Exception:
|
201
|
+
pass
|
202
|
+
return props
|
203
|
+
|
204
|
+
|
205
|
+
def choose_mount_for_dev(devname, mounts):
|
206
|
+
"""
|
207
|
+
Choose the most relevant mount for a device:
|
208
|
+
- Prefer mounts whose source resolves to /dev/<devname>.
|
209
|
+
- If multiple, prefer '/' then shortest mountpoint path.
|
210
|
+
- Otherwise return the first entry.
|
211
|
+
"""
|
212
|
+
if not mounts:
|
213
|
+
return None
|
214
|
+
|
215
|
+
def score(m):
|
216
|
+
mp = m.get("MOUNTPOINT") or "~"
|
217
|
+
s = m.get("SOURCE") or ""
|
218
|
+
exact = 1 if (s.startswith("/dev/") and os.path.basename(os.path.realpath(s)) == devname) else 0
|
219
|
+
root = 1 if mp == "/" else 0
|
220
|
+
return (exact, root, -len(mp))
|
221
|
+
|
222
|
+
best = sorted(mounts, key=score, reverse=True)[0]
|
223
|
+
return best
|
224
|
+
|
225
|
+
|
226
|
+
def is_block_device(devpath):
|
227
|
+
try:
|
228
|
+
st_mode = os.stat(devpath).st_mode
|
229
|
+
return stat.S_ISBLK(st_mode)
|
230
|
+
except Exception:
|
231
|
+
return False
|
232
|
+
|
233
|
+
|
234
|
+
def pretty_format_table(data, delimiter="\t", header=None):
|
235
|
+
version = 1.11
|
236
|
+
_ = version
|
237
|
+
if not data:
|
238
|
+
return ""
|
239
|
+
if isinstance(data, str):
|
240
|
+
data = data.strip("\n").split("\n")
|
241
|
+
data = [line.split(delimiter) for line in data]
|
242
|
+
elif isinstance(data, dict):
|
243
|
+
if isinstance(next(iter(data.values())), dict):
|
244
|
+
tempData = [["key"] + list(next(iter(data.values())).keys())]
|
245
|
+
tempData.extend(
|
246
|
+
[[key] + list(value.values()) for key, value in data.items()]
|
247
|
+
)
|
248
|
+
data = tempData
|
249
|
+
else:
|
250
|
+
data = [[key] + list(value) for key, value in data.items()]
|
251
|
+
elif not isinstance(data, list):
|
252
|
+
data = list(data)
|
253
|
+
if isinstance(data[0], dict):
|
254
|
+
tempData = [data[0].keys()]
|
255
|
+
tempData.extend([list(item.values()) for item in data])
|
256
|
+
data = tempData
|
257
|
+
data = [[str(item) for item in row] for row in data]
|
258
|
+
num_cols = len(data[0])
|
259
|
+
col_widths = [0] * num_cols
|
260
|
+
for c in range(num_cols):
|
261
|
+
col_widths[c] = max(
|
262
|
+
len(re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", row[c])) for row in data
|
263
|
+
)
|
264
|
+
if header:
|
265
|
+
header_widths = [
|
266
|
+
len(re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", col)) for col in header
|
267
|
+
]
|
268
|
+
col_widths = [max(col_widths[i], header_widths[i]) for i in range(num_cols)]
|
269
|
+
row_format = " | ".join("{{:<{}}}".format(width) for width in col_widths)
|
270
|
+
if not header:
|
271
|
+
header = data[0]
|
272
|
+
outTable = []
|
273
|
+
outTable.append(row_format.format(*header))
|
274
|
+
outTable.append("-+-".join("-" * width for width in col_widths))
|
275
|
+
for row in data[1:]:
|
276
|
+
if not any(row):
|
277
|
+
outTable.append("-+-".join("-" * width for width in col_widths))
|
278
|
+
else:
|
279
|
+
outTable.append(row_format.format(*row))
|
280
|
+
else:
|
281
|
+
if isinstance(header, str):
|
282
|
+
header = header.split(delimiter)
|
283
|
+
if len(header) < num_cols:
|
284
|
+
header += [""] * (num_cols - len(header))
|
285
|
+
elif len(header) > num_cols:
|
286
|
+
header = header[:num_cols]
|
287
|
+
outTable = []
|
288
|
+
outTable.append(row_format.format(*header))
|
289
|
+
outTable.append("-+-".join("-" * width for width in col_widths))
|
290
|
+
for row in data:
|
291
|
+
if not any(row):
|
292
|
+
outTable.append("-+-".join("-" * width for width in col_widths))
|
293
|
+
else:
|
294
|
+
outTable.append(row_format.format(*row))
|
295
|
+
return "\n".join(outTable) + "\n"
|
296
|
+
|
297
|
+
|
298
|
+
def format_bytes(
|
299
|
+
size, use_1024_bytes=None, to_int=False, to_str=False, str_format=".2f"
|
300
|
+
):
|
301
|
+
if to_int or isinstance(size, str):
|
302
|
+
if isinstance(size, int):
|
303
|
+
return size
|
304
|
+
elif isinstance(size, str):
|
305
|
+
match = re.match(r"(\d+(\.\d+)?)\s*([a-zA-Z]*)", size)
|
306
|
+
if not match:
|
307
|
+
if to_str:
|
308
|
+
return size
|
309
|
+
print(
|
310
|
+
"Invalid size format. Expected format: 'number [unit]', "
|
311
|
+
"e.g., '1.5 GiB' or '1.5GiB'"
|
312
|
+
)
|
313
|
+
print(f"Got: {size}")
|
314
|
+
return 0
|
315
|
+
number, _, unit = match.groups()
|
316
|
+
number = float(number)
|
317
|
+
unit = unit.strip().lower().rstrip("b")
|
318
|
+
if unit.endswith("i"):
|
319
|
+
use_1024_bytes = True
|
320
|
+
elif use_1024_bytes is None:
|
321
|
+
use_1024_bytes = False
|
322
|
+
unit = unit.rstrip("i")
|
323
|
+
if use_1024_bytes:
|
324
|
+
power = 2**10
|
325
|
+
else:
|
326
|
+
power = 10**3
|
327
|
+
unit_labels = {
|
328
|
+
"": 0,
|
329
|
+
"k": 1,
|
330
|
+
"m": 2,
|
331
|
+
"g": 3,
|
332
|
+
"t": 4,
|
333
|
+
"p": 5,
|
334
|
+
"e": 6,
|
335
|
+
"z": 7,
|
336
|
+
"y": 8,
|
337
|
+
}
|
338
|
+
if unit not in unit_labels:
|
339
|
+
if to_str:
|
340
|
+
return size
|
341
|
+
else:
|
342
|
+
if to_str:
|
343
|
+
return format_bytes(
|
344
|
+
size=int(number * (power**unit_labels[unit])),
|
345
|
+
use_1024_bytes=use_1024_bytes,
|
346
|
+
to_str=True,
|
347
|
+
str_format=str_format,
|
348
|
+
)
|
349
|
+
return int(number * (power**unit_labels[unit]))
|
350
|
+
else:
|
351
|
+
try:
|
352
|
+
return int(size)
|
353
|
+
except Exception:
|
354
|
+
return 0
|
355
|
+
elif to_str or isinstance(size, int) or isinstance(size, float):
|
356
|
+
if isinstance(size, str):
|
357
|
+
try:
|
358
|
+
size = size.rstrip("B").rstrip("b")
|
359
|
+
size = float(size.lower().strip())
|
360
|
+
except Exception:
|
361
|
+
return size
|
362
|
+
if use_1024_bytes or use_1024_bytes is None:
|
363
|
+
power = 2**10
|
364
|
+
n = 0
|
365
|
+
power_labels = {
|
366
|
+
0: "",
|
367
|
+
1: "Ki",
|
368
|
+
2: "Mi",
|
369
|
+
3: "Gi",
|
370
|
+
4: "Ti",
|
371
|
+
5: "Pi",
|
372
|
+
6: "Ei",
|
373
|
+
7: "Zi",
|
374
|
+
8: "Yi",
|
375
|
+
}
|
376
|
+
while size > power:
|
377
|
+
size /= power
|
378
|
+
n += 1
|
379
|
+
return f"{size:{str_format}} {' '}{power_labels[n]}".replace(" ", " ")
|
380
|
+
else:
|
381
|
+
power = 10**3
|
382
|
+
n = 0
|
383
|
+
power_labels = {
|
384
|
+
0: "",
|
385
|
+
1: "K",
|
386
|
+
2: "M",
|
387
|
+
3: "G",
|
388
|
+
4: "T",
|
389
|
+
5: "P",
|
390
|
+
6: "E",
|
391
|
+
7: "Z",
|
392
|
+
8: "Y",
|
393
|
+
}
|
394
|
+
while size > power:
|
395
|
+
size /= power
|
396
|
+
n += 1
|
397
|
+
return f"{size:{str_format}} {' '}{power_labels[n]}".replace(" ", " ")
|
398
|
+
else:
|
399
|
+
try:
|
400
|
+
return format_bytes(float(size), use_1024_bytes)
|
401
|
+
except Exception:
|
402
|
+
pass
|
403
|
+
return 0
|
404
|
+
|
405
|
+
|
406
|
+
def is_partition(name):
|
407
|
+
real = os.path.realpath(os.path.join("/sys/class/block", name))
|
408
|
+
return os.path.exists(os.path.join(real, "partition"))
|
409
|
+
|
410
|
+
|
411
|
+
def get_partition_parent_name(name):
|
412
|
+
real = os.path.realpath(os.path.join("/sys/class/block", name))
|
413
|
+
part_file = os.path.join(real, "partition")
|
414
|
+
if not os.path.exists(part_file):
|
415
|
+
return None
|
416
|
+
parent = os.path.basename(os.path.dirname(real))
|
417
|
+
return parent if parent and parent != name else None
|
418
|
+
|
419
|
+
def get_drives_info(print_bytes = False, use_1024 = False, mounted_only=False, best_only=False, formated_only=False, show_zero_size_devices=False):
|
420
|
+
# Build UUID/LABEL maps and reverse maps for resolving mount "source"
|
421
|
+
uuid_map = build_symlink_map("/dev/disk/by-uuid")
|
422
|
+
label_map = build_symlink_map("/dev/disk/by-label")
|
423
|
+
uuid_rev = {v: k for k, v in uuid_map.items()}
|
424
|
+
label_rev = {v: k for k, v in label_map.items()}
|
425
|
+
|
426
|
+
# Mount info maps
|
427
|
+
by_majmin, by_devname, _ = parse_mountinfo_enhanced(uuid_rev, label_rev)
|
428
|
+
|
429
|
+
results = []
|
430
|
+
results_by_name = {}
|
431
|
+
df_stats_by_name = {} # name -> (blocks, bavail)
|
432
|
+
parent_to_children = defaultdict(list)
|
433
|
+
|
434
|
+
sys_class_block = "/sys/class/block"
|
435
|
+
try:
|
436
|
+
entries = sorted(os.listdir(sys_class_block))
|
437
|
+
except Exception:
|
438
|
+
entries = []
|
439
|
+
|
440
|
+
for name in entries:
|
441
|
+
block_path = os.path.join(sys_class_block, name)
|
442
|
+
|
443
|
+
devnode = os.path.join("/dev", name)
|
444
|
+
if not is_block_device(devnode):
|
445
|
+
pass
|
446
|
+
|
447
|
+
parent_name = get_partition_parent_name(name)
|
448
|
+
if parent_name:
|
449
|
+
parent_to_children[parent_name].append(name)
|
450
|
+
|
451
|
+
majmin_str = read_text(os.path.join(block_path, "dev"))
|
452
|
+
if not majmin_str or ":" not in majmin_str:
|
453
|
+
continue
|
454
|
+
try:
|
455
|
+
major, minor = majmin_str.split(":", 1)
|
456
|
+
major = int(major)
|
457
|
+
minor = int(minor)
|
458
|
+
except Exception:
|
459
|
+
continue
|
460
|
+
|
461
|
+
sectors = read_int(os.path.join(block_path, "size")) or 0
|
462
|
+
lb_size = (
|
463
|
+
read_int(os.path.join(block_path, "queue", "logical_block_size")) or 512
|
464
|
+
)
|
465
|
+
size_bytes = sectors * lb_size
|
466
|
+
if not show_zero_size_devices and size_bytes == 0:
|
467
|
+
continue
|
468
|
+
|
469
|
+
parent_block_path = os.path.join(sys_class_block, parent_name) if parent_name else None
|
470
|
+
model, serial = read_model_and_serial(block_path)
|
471
|
+
parent_model, parent_serial = read_model_and_serial(parent_block_path)
|
472
|
+
|
473
|
+
# UUID/Label from by-uuid/by-label symlinks
|
474
|
+
|
475
|
+
props = get_udev_props(major, minor=minor)
|
476
|
+
# Fallback to udev props
|
477
|
+
rec = {
|
478
|
+
"NAME": name,
|
479
|
+
"FSTYPE": props.get("ID_FS_TYPE"),
|
480
|
+
"LABEL": label_map.get(name, props.get("ID_FS_LABEL")),
|
481
|
+
"UUID": uuid_map.get(name, props.get("ID_FS_UUID")),
|
482
|
+
"MOUNTPOINT": None,
|
483
|
+
"MODEL": model if model else parent_model,
|
484
|
+
"SERIAL": serial if serial else parent_serial,
|
485
|
+
"DISCARD": bool(read_discard_support(block_path) or read_discard_support(parent_block_path)),
|
486
|
+
"SIZE": f"{format_bytes(size_bytes, to_int=print_bytes, use_1024_bytes=use_1024)}B",
|
487
|
+
"FSUSE%": None,
|
488
|
+
"SMART": 'N/A',
|
489
|
+
}
|
490
|
+
if SMARTCTL_PATH:
|
491
|
+
# if do not have read permission on the denode, set SMART to 'DENIED'
|
492
|
+
if not os.access(devnode, os.R_OK):
|
493
|
+
rec["SMART"] = 'DENIED'
|
494
|
+
try:
|
495
|
+
# run smartctl -H <device>
|
496
|
+
outputLines = subprocess.check_output([SMARTCTL_PATH, "-H", devnode], stderr=subprocess.STDOUT).decode().splitlines()
|
497
|
+
# Parse output for SMART status
|
498
|
+
for line in outputLines:
|
499
|
+
line = line.lower()
|
500
|
+
if "health" in line:
|
501
|
+
smartinfo = line.rpartition(':')[2].strip().upper()
|
502
|
+
rec["SMART"] = smartinfo.replace('PASSED', 'OK')
|
503
|
+
except:
|
504
|
+
pass
|
505
|
+
# Mount and fstype: try maj:min first, then by resolved devname
|
506
|
+
mounts = by_majmin.get(majmin_str, [])
|
507
|
+
if not mounts:
|
508
|
+
mounts = by_devname.get(name, [])
|
509
|
+
if best_only:
|
510
|
+
mounts = [choose_mount_for_dev(name, mounts)] if mounts else []
|
511
|
+
if not mounted_only and not mounts:
|
512
|
+
if formated_only and not rec.get("FSTYPE"):
|
513
|
+
continue
|
514
|
+
results.append(rec)
|
515
|
+
results_by_name[name] = rec
|
516
|
+
for mount in mounts:
|
517
|
+
rec = rec.copy()
|
518
|
+
mountpoint = mount.get("MOUNTPOINT")
|
519
|
+
if mount.get("FSTYPE"):
|
520
|
+
rec["FSTYPE"] = mount.get("FSTYPE")
|
521
|
+
if formated_only and not rec.get("FSTYPE"):
|
522
|
+
continue
|
523
|
+
# Use% via statvfs and collect raw stats for aggregation
|
524
|
+
if mountpoint:
|
525
|
+
try:
|
526
|
+
st = os.statvfs(mountpoint)
|
527
|
+
if st.f_blocks > 0:
|
528
|
+
use_pct = 100.0 * (1.0 - (st.f_bavail / float(st.f_blocks)))
|
529
|
+
rec["FSUSE%"] = f'{use_pct:.1f}%'
|
530
|
+
df_stats_by_name[name] = (st.f_blocks, st.f_bavail)
|
531
|
+
except Exception:
|
532
|
+
pass
|
533
|
+
rec["MOUNTPOINT"] = mountpoint
|
534
|
+
results.append(rec)
|
535
|
+
results_by_name[name] = rec
|
536
|
+
|
537
|
+
# Aggregate use% for parent devices with partitions:
|
538
|
+
# parent's use% = 1 - sum(bavail)/sum(blocks) over mounted partitions
|
539
|
+
for parent, children in parent_to_children.items():
|
540
|
+
sum_blocks = 0
|
541
|
+
sum_bavail = 0
|
542
|
+
for ch in children:
|
543
|
+
vals = df_stats_by_name.get(ch)
|
544
|
+
if not vals:
|
545
|
+
continue
|
546
|
+
b, ba = vals
|
547
|
+
if b and b > 0:
|
548
|
+
sum_blocks += b
|
549
|
+
sum_bavail += ba if ba is not None else 0
|
550
|
+
if sum_blocks > 0 and parent in results_by_name:
|
551
|
+
pct = 100.0 * (1.0 - (sum_bavail / float(sum_blocks)))
|
552
|
+
results_by_name[parent]["FSUSE%"] = f'{pct:.1f}%'
|
553
|
+
|
554
|
+
results.sort(key=lambda x: x["NAME"]) # type: ignore
|
555
|
+
return results
|
556
|
+
|
557
|
+
def main():
|
558
|
+
parser = argparse.ArgumentParser(description="Gather disk and partition info for block devices.")
|
559
|
+
parser.add_argument('-j','--json', help="Produce JSON output", action="store_true")
|
560
|
+
parser.add_argument('-b','--bytes', help="Print the SIZE column in bytes rather than in a human-readable format", action="store_true")
|
561
|
+
parser.add_argument('-H','--si', help="Use powers of 1000 not 1024 for SIZE column", action="store_true")
|
562
|
+
parser.add_argument('-F','-fo','--formated_only', help="Show only formated filesystems", action="store_true")
|
563
|
+
parser.add_argument('-M','-mo','--mounted_only', help="Show only mounted filesystems", action="store_true")
|
564
|
+
parser.add_argument('-B','-bo','--best_only', help="Show only best mount for each device", action="store_true")
|
565
|
+
parser.add_argument('--show_zero_size_devices', help="Show devices with zero size", action="store_true")
|
566
|
+
parser.add_argument('-V', '--version', action='version', version=f"%(prog)s {version} @ {COMMIT_DATE} stat drives by pan@zopyr.us")
|
567
|
+
|
568
|
+
args = parser.parse_args()
|
569
|
+
results = get_drives_info(print_bytes = args.bytes, use_1024 = not args.si,
|
570
|
+
mounted_only=args.mounted_only, best_only=args.best_only,
|
571
|
+
formated_only=args.formated_only, show_zero_size_devices=args.show_zero_size_devices)
|
572
|
+
if args.json:
|
573
|
+
import json
|
574
|
+
print(json.dumps(results, indent=4))
|
575
|
+
else:
|
576
|
+
print(pretty_format_table(results))
|
577
|
+
|
578
|
+
|
579
|
+
if __name__ == "__main__":
|
580
|
+
main()
|