statblk 1.1__tar.gz → 1.20__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statblk
3
- Version: 1.1
3
+ Version: 1.20
4
4
  Summary: Gather essential disk and partition info for block devices and print it in a nice table
5
5
  Home-page: https://github.com/yufei-pan/statblk
6
6
  Author: Yufei Pan
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: POSIX :: Linux
11
11
  Requires-Python: >=3.6
12
12
  Description-Content-Type: text/plain
13
+ Requires-Dist: multiCMD>=1.35
13
14
  Dynamic: author
14
15
  Dynamic: author-email
15
16
  Dynamic: classifier
@@ -17,6 +18,7 @@ Dynamic: description
17
18
  Dynamic: description-content-type
18
19
  Dynamic: home-page
19
20
  Dynamic: license
21
+ Dynamic: requires-dist
20
22
  Dynamic: requires-python
21
23
  Dynamic: summary
22
24
 
@@ -16,6 +16,9 @@ setup(
16
16
  'statblk=statblk:main',
17
17
  ],
18
18
  },
19
+ install_requires=[
20
+ 'multiCMD>=1.35',
21
+ ],
19
22
  classifiers=[
20
23
  'Programming Language :: Python :: 3',
21
24
  'Operating System :: POSIX :: Linux',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statblk
3
- Version: 1.1
3
+ Version: 1.20
4
4
  Summary: Gather essential disk and partition info for block devices and print it in a nice table
5
5
  Home-page: https://github.com/yufei-pan/statblk
6
6
  Author: Yufei Pan
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: POSIX :: Linux
11
11
  Requires-Python: >=3.6
12
12
  Description-Content-Type: text/plain
13
+ Requires-Dist: multiCMD>=1.35
13
14
  Dynamic: author
14
15
  Dynamic: author-email
15
16
  Dynamic: classifier
@@ -17,6 +18,7 @@ Dynamic: description
17
18
  Dynamic: description-content-type
18
19
  Dynamic: home-page
19
20
  Dynamic: license
21
+ Dynamic: requires-dist
20
22
  Dynamic: requires-python
21
23
  Dynamic: summary
22
24
 
@@ -5,4 +5,5 @@ statblk.egg-info/PKG-INFO
5
5
  statblk.egg-info/SOURCES.txt
6
6
  statblk.egg-info/dependency_links.txt
7
7
  statblk.egg-info/entry_points.txt
8
+ statblk.egg-info/requires.txt
8
9
  statblk.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ multiCMD>=1.35
@@ -0,0 +1,661 @@
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, namedtuple
11
+ import argparse
12
+ import shutil
13
+ import subprocess
14
+ try:
15
+ import multiCMD
16
+ assert float(multiCMD.version) > 1.35
17
+ except ImportError:
18
+ import time,threading,io,sys,subprocess,select,string,re,itertools,signal
19
+ class multiCMD:
20
+ version='1.35_min_statblk'
21
+ __version__=version
22
+ COMMIT_DATE='2025-09-10'
23
+ __running_threads=set()
24
+ __variables={}
25
+ _BRACKET_RX=re.compile('\\[([^\\]]+)\\]')
26
+ _ALPHANUM=string.digits+string.ascii_letters
27
+ _ALPHA_IDX={B:A for(A,B)in enumerate(_ALPHANUM)}
28
+ class Task:
29
+ def __init__(A,command):A.command=command;A.returncode=None;A.stdout=[];A.stderr=[];A.thread=None;A.stop=False
30
+ def __iter__(A):return zip(['command','returncode','stdout','stderr'],[A.command,A.returncode,A.stdout,A.stderr])
31
+ def __repr__(A):return f"Task(command={A.command}, returncode={A.returncode}, stdout={A.stdout}, stderr={A.stderr}, stop={A.stop})"
32
+ def __str__(A):return str(dict(A))
33
+ def is_alive(A):
34
+ if A.thread is not None:return A.thread.is_alive()
35
+ return False
36
+ def _expand_piece(piece,vars_):
37
+ D=vars_;C=piece;C=C.strip()
38
+ if':'in C:E,F,G=C.partition(':');D[E]=G;return
39
+ if'-'in C:
40
+ A,F,B=(A.strip()for A in C.partition('-'));A=D.get(A,A);B=D.get(B,B)
41
+ if A.isdigit()and B.isdigit():H=max(len(A),len(B));return[f"{A:0{H}d}"for A in range(int(A),int(B)+1)]
42
+ if all(A in string.hexdigits for A in A+B):return[format(A,'x')for A in range(int(A,16),int(B,16)+1)]
43
+ try:return[multiCMD._ALPHANUM[A]for A in range(multiCMD._ALPHA_IDX[A],multiCMD._ALPHA_IDX[B]+1)]
44
+ except KeyError:pass
45
+ return[D.get(C,C)]
46
+ def _expand_ranges_fast(inStr):
47
+ D=inStr;A=[];B=0
48
+ for C in multiCMD._BRACKET_RX.finditer(D):
49
+ if C.start()>B:A.append([D[B:C.start()]])
50
+ E=[]
51
+ for G in C.group(1).split(','):
52
+ F=multiCMD._expand_piece(G,multiCMD.__variables)
53
+ if F:E.extend(F)
54
+ A.append(E or['']);B=C.end()
55
+ A.append([D[B:]]);return[''.join(A)for A in itertools.product(*A)]
56
+ def __handle_stream(stream,target,pre='',post='',quiet=False):
57
+ E=quiet;C=target
58
+ def D(current_line,target,keepLastLine=True):
59
+ A=target
60
+ if not keepLastLine:
61
+ if not E:sys.stdout.write('\r')
62
+ A.pop()
63
+ elif not E:sys.stdout.write('\n')
64
+ B=current_line.decode('utf-8',errors='backslashreplace');A.append(B)
65
+ if not E:sys.stdout.write(pre+B+post);sys.stdout.flush()
66
+ A=bytearray();B=True
67
+ for F in iter(lambda:stream.read(1),b''):
68
+ if F==b'\n':
69
+ if not B and A:D(A,C,keepLastLine=False)
70
+ elif B:D(A,C,keepLastLine=True)
71
+ A=bytearray();B=True
72
+ elif F==b'\r':D(A,C,keepLastLine=B);A=bytearray();B=False
73
+ else:A.extend(F)
74
+ if A:D(A,C,keepLastLine=B)
75
+ def int_to_color(n,brightness_threshold=500):
76
+ B=brightness_threshold;A=hash(str(n));C=A>>16&255;D=A>>8&255;E=A&255
77
+ if C+D+E<B:return multiCMD.int_to_color(A,B)
78
+ return C,D,E
79
+ def __run_command(task,sem,timeout=60,quiet=False,dry_run=False,with_stdErr=False,identity=None):
80
+ I=timeout;F=identity;E=quiet;A=task;C='';D=''
81
+ with sem:
82
+ try:
83
+ if F is not None:
84
+ if F==...:F=threading.get_ident()
85
+ P,Q,R=multiCMD.int_to_color(F);C=f"[38;2;{P};{Q};{R}m";D='\x1b[0m'
86
+ if not E:print(C+'Running command: '+' '.join(A.command)+D);print(C+'-'*100+D)
87
+ if dry_run:return A.stdout+A.stderr
88
+ B=subprocess.Popen(A.command,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE);J=threading.Thread(target=multiCMD.__handle_stream,args=(B.stdout,A.stdout,C,D,E),daemon=True);J.start();K=threading.Thread(target=multiCMD.__handle_stream,args=(B.stderr,A.stderr,C,D,E),daemon=True);K.start();L=time.time();M=len(A.stdout)+len(A.stderr);time.sleep(0);H=1e-07
89
+ while B.poll()is None:
90
+ if A.stop:B.send_signal(signal.SIGINT);time.sleep(.01);B.terminate();break
91
+ if I>0:
92
+ if len(A.stdout)+len(A.stderr)!=M:L=time.time();M=len(A.stdout)+len(A.stderr)
93
+ elif time.time()-L>I:A.stderr.append('Timeout!');B.send_signal(signal.SIGINT);time.sleep(.01);B.terminate();break
94
+ time.sleep(H)
95
+ if H<.001:H*=2
96
+ A.returncode=B.poll();J.join(timeout=1);K.join(timeout=1);N,O=B.communicate()
97
+ if N:multiCMD.__handle_stream(io.BytesIO(N),A.stdout,A)
98
+ if O:multiCMD.__handle_stream(io.BytesIO(O),A.stderr,A)
99
+ if A.returncode is None:
100
+ if A.stderr and A.stderr[-1].strip().startswith('Timeout!'):A.returncode=124
101
+ elif A.stderr and A.stderr[-1].strip().startswith('Ctrl C detected, Emergency Stop!'):A.returncode=137
102
+ else:A.returncode=-1
103
+ except FileNotFoundError as G:print(f"Command / path not found: {A.command[0]}",file=sys.stderr,flush=True);A.stderr.append(str(G));A.returncode=127
104
+ except Exception as G:import traceback as S;print(f"Error running command: {A.command}",file=sys.stderr,flush=True);print(str(G).split('\n'));A.stderr.extend(str(G).split('\n'));A.stderr.extend(S.format_exc().split('\n'));A.returncode=-1
105
+ if not E:print(C+'\n'+'-'*100+D);print(C+f"Process exited with return code {A.returncode}"+D)
106
+ if with_stdErr:return A.stdout+A.stderr
107
+ else:return A.stdout
108
+ def __format_command(command,expand=False):
109
+ D=expand;A=command
110
+ if isinstance(A,str):
111
+ if D:B=multiCMD._expand_ranges_fast(A)
112
+ else:B=[A]
113
+ return[A.split()for A in B]
114
+ elif hasattr(A,'__iter__'):
115
+ C=[]
116
+ for E in A:
117
+ if isinstance(E,str):C.append(E)
118
+ else:C.append(repr(E))
119
+ if not D:return[C]
120
+ F=[multiCMD._expand_ranges_fast(A)for A in C];B=list(itertools.product(*F));return[list(A)for A in B]
121
+ else:return multiCMD.__format_command(str(A),expand=D)
122
+ def run_command(command,timeout=0,max_threads=1,quiet=False,dry_run=False,with_stdErr=False,return_code_only=False,return_object=False,wait_for_return=True,sem=None):return multiCMD.run_commands(commands=[command],timeout=timeout,max_threads=max_threads,quiet=quiet,dry_run=dry_run,with_stdErr=with_stdErr,return_code_only=return_code_only,return_object=return_object,parse=False,wait_for_return=wait_for_return,sem=sem)[0]
123
+ def run_commands(commands,timeout=0,max_threads=1,quiet=False,dry_run=False,with_stdErr=False,return_code_only=False,return_object=False,parse=False,wait_for_return=True,sem=None):
124
+ K=wait_for_return;J=dry_run;I=quiet;H=timeout;C=max_threads;B=sem;E=[]
125
+ for L in commands:E.extend(multiCMD.__format_command(L,expand=parse))
126
+ A=[multiCMD.Task(A)for A in E]
127
+ if C<1:C=len(E)
128
+ if C>1 or not K:
129
+ if not B:B=threading.Semaphore(C)
130
+ F=[threading.Thread(target=multiCMD.__run_command,args=(A,B,H,I,J,...),daemon=True)for A in A]
131
+ for(D,G)in zip(F,A):G.thread=D;D.start()
132
+ if K:
133
+ for D in F:D.join()
134
+ else:multiCMD.__running_threads.update(F)
135
+ else:
136
+ B=threading.Semaphore(1)
137
+ for G in A:multiCMD.__run_command(G,B,H,I,J,identity=None)
138
+ if return_code_only:return[A.returncode for A in A]
139
+ elif return_object:return A
140
+ elif with_stdErr:return[A.stdout+A.stderr for A in A]
141
+ else:return[A.stdout for A in A]
142
+ def pretty_format_table(data,delimiter='\t',header=None,full=False):
143
+ O=delimiter;B=header;A=data;import re;S=1.12;Z=S
144
+ def J(s):return len(re.sub('\\x1b\\[[0-?]*[ -/]*[@-~]','',s))
145
+ def L(col_widths,sep_len):A=col_widths;return sum(A)+sep_len*(len(A)-1)
146
+ def T(s,width):
147
+ A=width
148
+ if J(s)<=A:return s
149
+ if A<=0:return''
150
+ return s[:max(A-2,0)]+'..'
151
+ if not A:return''
152
+ if isinstance(A,str):A=A.strip('\n').split('\n');A=[A.split(O)for A in A]
153
+ elif isinstance(A,dict):
154
+ if isinstance(next(iter(A.values())),dict):H=[['key']+list(next(iter(A.values())).keys())];H.extend([[A]+list(B.values())for(A,B)in A.items()]);A=H
155
+ else:A=[[A]+list(B)for(A,B)in A.items()]
156
+ elif not isinstance(A,list):A=list(A)
157
+ if isinstance(A[0],dict):H=[list(A[0].keys())];H.extend([list(A.values())for A in A]);A=H
158
+ A=[[str(A)for A in A]for A in A];C=len(A[0]);U=B is not None
159
+ if not U:B=A[0];E=A[1:]
160
+ else:
161
+ if isinstance(B,str):B=B.split(O)
162
+ if len(B)<C:B=B+['']*(C-len(B))
163
+ elif len(B)>C:B=B[:C]
164
+ E=A
165
+ def V(hdr,rows_):
166
+ B=hdr;C=[0]*len(B)
167
+ for A in range(len(B)):C[A]=max(J(B[A]),*(J(B[A])for B in rows_ if A<len(B)))
168
+ return C
169
+ P=[]
170
+ for F in E:
171
+ if len(F)<C:F=F+['']*(C-len(F))
172
+ elif len(F)>C:F=F[:C]
173
+ P.append(F)
174
+ E=P;D=V(B,E);G=' | ';I='-+-';M=multiCMD.get_terminal_size()[0]
175
+ def K(hdr,rows,col_w,sep_str,hsep_str):
176
+ D=hsep_str;C=col_w;E=sep_str.join('{{:<{}}}'.format(A)for A in C);A=[];A.append(E.format(*hdr));A.append(D.join('-'*A for A in C))
177
+ for B in rows:
178
+ if not any(B):A.append(D.join('-'*A for A in C))
179
+ else:B=[T(B[A],C[A])for A in range(len(B))];A.append(E.format(*B))
180
+ return'\n'.join(A)+'\n'
181
+ if full:return K(B,E,D,G,I)
182
+ if L(D,len(G))<=M:return K(B,E,D,G,I)
183
+ G='|';I='+'
184
+ if L(D,len(G))<=M:return K(B,E,D,G,I)
185
+ W=[J(A)for A in B];X=[max(D[A]-W[A],0)for A in range(C)];N=L(D,len(G))-M
186
+ for(Y,Q)in sorted(enumerate(X),key=lambda x:-x[1]):
187
+ if N<=0:break
188
+ if Q<=0:continue
189
+ R=min(Q,N);D[Y]-=R;N-=R
190
+ return K(B,E,D,G,I)
191
+ def get_terminal_size():
192
+ try:import os;A=os.get_terminal_size()
193
+ except:
194
+ try:import fcntl,termios as C,struct as B;D=fcntl.ioctl(0,C.TIOCGWINSZ,B.pack('HHHH',0,0,0,0));A=B.unpack('HHHH',D)[:2]
195
+ except:import shutil as E;A=E.get_terminal_size(fallback=(120,30))
196
+ return A
197
+ def format_bytes(size,use_1024_bytes=None,to_int=False,to_str=False,str_format='.2f'):
198
+ H=str_format;F=to_str;C=use_1024_bytes;A=size
199
+ if to_int or isinstance(A,str):
200
+ if isinstance(A,int):return A
201
+ elif isinstance(A,str):
202
+ K=re.match('(\\d+(\\.\\d+)?)\\s*([a-zA-Z]*)',A)
203
+ if not K:
204
+ if F:return A
205
+ print("Invalid size format. Expected format: 'number [unit]', e.g., '1.5 GiB' or '1.5GiB'");print(f"Got: {A}");return 0
206
+ G,L,D=K.groups();G=float(G);D=D.strip().lower().rstrip('b')
207
+ if D.endswith('i'):C=True
208
+ elif C is None:C=False
209
+ D=D.rstrip('i')
210
+ if C:B=2**10
211
+ else:B=10**3
212
+ I={'':0,'k':1,'m':2,'g':3,'t':4,'p':5,'e':6,'z':7,'y':8}
213
+ if D not in I:
214
+ if F:return A
215
+ else:
216
+ if F:return multiCMD.format_bytes(size=int(G*B**I[D]),use_1024_bytes=C,to_str=True,str_format=H)
217
+ return int(G*B**I[D])
218
+ else:
219
+ try:return int(A)
220
+ except Exception:return 0
221
+ elif F or isinstance(A,int)or isinstance(A,float):
222
+ if isinstance(A,str):
223
+ try:A=A.rstrip('B').rstrip('b');A=float(A.lower().strip())
224
+ except Exception:return A
225
+ if C or C is None:
226
+ B=2**10;E=0;J={0:'',1:'Ki',2:'Mi',3:'Gi',4:'Ti',5:'Pi',6:'Ei',7:'Zi',8:'Yi'}
227
+ while A>B:A/=B;E+=1
228
+ return f"{A:{H}} {' '}{J[E]}".replace(' ',' ')
229
+ else:
230
+ B=10**3;E=0;J={0:'',1:'K',2:'M',3:'G',4:'T',5:'P',6:'E',7:'Z',8:'Y'}
231
+ while A>B:A/=B;E+=1
232
+ return f"{A:{H}} {' '}{J[E]}".replace(' ',' ')
233
+ else:
234
+ try:return multiCMD.format_bytes(float(A),C)
235
+ except Exception:pass
236
+ return 0
237
+ import time
238
+ try:
239
+ import functools
240
+ import typing
241
+ # Check if functiools.cache is available
242
+ # cache_decorator = functools.cache
243
+ def cache_decorator(user_function):
244
+ def _make_hashable(item):
245
+ if isinstance(item, typing.Mapping):
246
+ # Sort items so that {'a':1, 'b':2} and {'b':2, 'a':1} hash the same
247
+ return tuple(
248
+ ( _make_hashable(k), _make_hashable(v) )
249
+ for k, v in sorted(item.items(), key=lambda item: item[0])
250
+ )
251
+ if isinstance(item, (list, set, tuple)):
252
+ return tuple(_make_hashable(e) for e in item)
253
+ # Fallback: assume item is already hashable
254
+ return item
255
+ def decorating_function(user_function):
256
+ # Create the real cached function
257
+ cached_func = functools.lru_cache(maxsize=None)(user_function)
258
+ @functools.wraps(user_function)
259
+ def wrapper(*args, **kwargs):
260
+ # Convert all args/kwargs to hashable equivalents
261
+ hashable_args = tuple(_make_hashable(a) for a in args)
262
+ hashable_kwargs = {
263
+ k: _make_hashable(v) for k, v in kwargs.items()
264
+ }
265
+ # Call the lru-cached version
266
+ return cached_func(*hashable_args, **hashable_kwargs)
267
+ # Expose cache statistics and clear method
268
+ wrapper.cache_info = cached_func.cache_info
269
+ wrapper.cache_clear = cached_func.cache_clear
270
+ return wrapper
271
+ return decorating_function(user_function)
272
+ except :
273
+ import sys
274
+ # If lrucache is not available, use a dummy decorator
275
+ print('Warning: functools.lru_cache is not available, multiSSH3 will run slower without cache.',file=sys.stderr)
276
+ def cache_decorator(func):
277
+ return func
278
+
279
+ version = '1.20'
280
+ VERSION = version
281
+ __version__ = version
282
+ COMMIT_DATE = '2025-09-10'
283
+
284
+ SMARTCTL_PATH = shutil.which("smartctl")
285
+
286
+ def read_text(path):
287
+ try:
288
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
289
+ return f.read().strip()
290
+ except Exception:
291
+ return None
292
+
293
+ def read_int(path):
294
+ s = read_text(path)
295
+ if s is None:
296
+ return None
297
+ try:
298
+ return int(s)
299
+ except Exception:
300
+ return None
301
+
302
+ def build_symlink_dict(dir_path):
303
+ """
304
+ Build map: devname -> token (uuid or label string) using symlinks under
305
+ /dev/disk/by-uuid or /dev/disk/by-label.
306
+ """
307
+ mapping = {}
308
+ if not os.path.isdir(dir_path):
309
+ return mapping
310
+ try:
311
+ for entry in os.listdir(dir_path):
312
+ p = os.path.join(dir_path, entry)
313
+ try:
314
+ if os.path.islink(p):
315
+ tgt = os.path.realpath(p)
316
+ mapping.setdefault(tgt, entry)
317
+ except Exception:
318
+ continue
319
+ except Exception:
320
+ pass
321
+ return mapping
322
+
323
+ def get_statvfs_use_size(mountpoint):
324
+ try:
325
+ st = os.statvfs(mountpoint)
326
+ block_size = st.f_frsize if st.f_frsize > 0 else st.f_bsize
327
+ total = st.f_blocks * block_size
328
+ avail = st.f_bavail * block_size
329
+ used = total - avail
330
+ return total, used
331
+ except Exception:
332
+ return 0, 0
333
+
334
+ @cache_decorator
335
+ def read_discard_support(sysfs_block_path):
336
+ if not sysfs_block_path or not os.path.isdir(sysfs_block_path):
337
+ return 'N/A'
338
+ dmbytes = read_int(os.path.join(sysfs_block_path, "queue", "discard_max_bytes"))
339
+ try:
340
+ if (dmbytes or 0) > 0:
341
+ return 'Yes'
342
+ else:
343
+ return 'No'
344
+ except Exception:
345
+ return 'N/A'
346
+
347
+ @cache_decorator
348
+ def get_parent_device_sysfs(sysfs_block_path):
349
+ """
350
+ Return the sysfs 'device' directory for this block node (resolves partition
351
+ to its parent device as well).
352
+ """
353
+ dev_link = os.path.join(sysfs_block_path, "device")
354
+ try:
355
+ return os.path.realpath(dev_link)
356
+ except Exception:
357
+ return dev_link
358
+
359
+ @cache_decorator
360
+ def read_model_and_serial(sysfs_block_path):
361
+ if not sysfs_block_path or not os.path.isdir(sysfs_block_path):
362
+ return '', ''
363
+ device_path = get_parent_device_sysfs(sysfs_block_path)
364
+ model = read_text(os.path.join(device_path, "model"))
365
+ serial = read_text(os.path.join(device_path, "serial"))
366
+ if serial is None:
367
+ serial = read_text(os.path.join(device_path, "wwid"))
368
+ if model:
369
+ model = " ".join(model.split())
370
+ else:
371
+ model = ''
372
+ if serial:
373
+ serial = " ".join(serial.split())
374
+ else:
375
+ serial = ''
376
+ return model, serial
377
+
378
+ MountEntry = namedtuple("MountEntry", ["MOUNTPOINT", "FSTYPE", "OPTIONS"])
379
+ def parseMount():
380
+ rtn = multiCMD.run_command('mount',timeout=1,quiet=True)
381
+ mount_table = defaultdict(list)
382
+ for line in rtn:
383
+ device_name, _, line = line.partition(' on ')
384
+ mount_point, _, line = line.partition(' type ')
385
+ fstype, _ , options = line.partition(' (')
386
+ options = options.rstrip(')').split(',')
387
+ mount_table[device_name].append(MountEntry(mount_point, fstype, options))
388
+ return mount_table
389
+
390
+ def get_blocks():
391
+ # get entries in /sys/class/block
392
+ block_devices = []
393
+ for entry in os.listdir("/sys/class/block"):
394
+ if os.path.isdir(os.path.join("/sys/class/block", entry)):
395
+ block_devices.append(f'/dev/{entry}')
396
+ return block_devices
397
+
398
+ @cache_decorator
399
+ def is_block_device(devpath):
400
+ try:
401
+ st_mode = os.stat(devpath).st_mode
402
+ return stat.S_ISBLK(st_mode)
403
+ except Exception:
404
+ return False
405
+
406
+ def is_partition(sysfs_block_path):
407
+ if not sysfs_block_path or not os.path.isdir(sysfs_block_path):
408
+ return False
409
+ return os.path.exists(os.path.join(sysfs_block_path, "partition"))
410
+
411
+ @cache_decorator
412
+ def get_partition_parent_name(name):
413
+ if not name:
414
+ return None
415
+ name = os.path.basename(name)
416
+ sysfs_block_path = os.path.realpath(os.path.join('/sys/class/block', name))
417
+ if not sysfs_block_path or not os.path.isdir(sysfs_block_path):
418
+ return None
419
+ part_file = os.path.join(sysfs_block_path, "partition")
420
+ if not os.path.exists(part_file):
421
+ return os.path.join('/dev', name) if is_block_device(os.path.join('/dev', name)) else None
422
+ parent = os.path.basename(os.path.dirname(sysfs_block_path))
423
+ return os.path.join('/dev', parent) if parent and parent != name else None
424
+
425
+ @cache_decorator
426
+ def get_sector_size(sysfs_block_path):
427
+ if not sysfs_block_path or not os.path.isdir(sysfs_block_path):
428
+ return 512
429
+ if get_partition_parent_name(sysfs_block_path):
430
+ sysfs_block_path = os.path.join('/sys/class/block', os.path.basename(get_partition_parent_name(sysfs_block_path)))
431
+ sector_size = read_int(os.path.join(sysfs_block_path, "queue", "hw_sector_size"))
432
+ if sector_size is None:
433
+ sector_size = read_int(os.path.join(sysfs_block_path, "queue", "logical_block_size"))
434
+ return sector_size if sector_size else 512
435
+
436
+ def get_read_write_rate_throughput_iter(sysfs_block_path):
437
+ if not sysfs_block_path or not os.path.isdir(sysfs_block_path):
438
+ while True:
439
+ yield 0, 0
440
+ rx_path = os.path.join(sysfs_block_path, "stat")
441
+ start_time = time.monotonic()
442
+ sector_size = get_sector_size(sysfs_block_path)
443
+ previous_bytes_read = 0
444
+ previous_bytes_written = 0
445
+ try:
446
+ with open(rx_path, "r", encoding="utf-8", errors="ignore") as f:
447
+ fields = f.read().strip().split()
448
+ if len(fields) < 7:
449
+ yield 0, 0
450
+ sectors_read = int(fields[2])
451
+ read_time = int(fields[3]) / 1000.0
452
+ sectors_written = int(fields[6])
453
+ write_time = int(fields[7]) / 1000.0
454
+ read_throughput = (sectors_read * sector_size) / read_time if read_time > 0 else 0
455
+ write_throughput = (sectors_written * sector_size) / write_time if write_time > 0 else 0
456
+ previous_bytes_read = sectors_read * sector_size
457
+ previous_bytes_written = sectors_written * sector_size
458
+ yield int(read_throughput), int(write_throughput)
459
+ except Exception:
460
+ yield 0, 0
461
+ while True:
462
+ try:
463
+ with open(rx_path, "r", encoding="utf-8", errors="ignore") as f:
464
+ fields = f.read().strip().split()
465
+ if len(fields) < 7:
466
+ yield 0, 0
467
+ # fields: https://www.kernel.org/doc/html/latest/block/stat.html
468
+ # 0 - reads completed successfully
469
+ # 1 - reads merged
470
+ # 2 - sectors read
471
+ # 3 - time spent reading (ms)
472
+ # 4 - writes completed
473
+ # 5 - writes merged
474
+ # 6 - sectors written
475
+ # 7 - time spent writing (ms)
476
+ # 8 - I/Os currently in progress
477
+ # 9 - time spent doing I/Os (ms)
478
+ # 10 - weighted time spent doing I/Os (ms)
479
+ sectors_read = int(fields[2])
480
+ sectors_written = int(fields[6])
481
+ bytes_read = sectors_read * sector_size
482
+ bytes_written = sectors_written * sector_size
483
+ end_time = time.monotonic()
484
+ elapsed_time = end_time - start_time
485
+ start_time = end_time
486
+ read_throughput = (bytes_read - previous_bytes_read) / elapsed_time if elapsed_time > 0 else 0
487
+ write_throughput = (bytes_written - previous_bytes_written) / elapsed_time if elapsed_time > 0 else 0
488
+ previous_bytes_read = bytes_read
489
+ previous_bytes_written = bytes_written
490
+ yield int(read_throughput), int(write_throughput)
491
+ except Exception:
492
+ yield 0, 0
493
+
494
+ # DRIVE_INFO = namedtuple("DRIVE_INFO",
495
+ # ["NAME", "FSTYPE", "SIZE", "FSUSEPCT", "MOUNTPOINT", "SMART","RTPT",'WTPT', "LABEL", "UUID", "MODEL", "SERIAL", "DISCARD"])
496
+ def get_drives_info(print_bytes = False, use_1024 = False, mounted_only=False, best_only=False, formated_only=False, show_zero_size_devices=False,pseudo=False,tptDict = {},full=False):
497
+ lsblk_result = multiCMD.run_command(f'lsblk -brnp -o NAME,SIZE,FSTYPE,UUID,LABEL',timeout=2,quiet=True,wait_for_return=False,return_object=True)
498
+ block_devices = get_blocks()
499
+ smart_infos = {}
500
+ for block_device in block_devices:
501
+ parent_name = get_partition_parent_name(block_device)
502
+ if parent_name:
503
+ if parent_name not in smart_infos:
504
+ smart_infos[parent_name] = multiCMD.run_command(f'{SMARTCTL_PATH} -H {parent_name}',timeout=2,quiet=True,wait_for_return=False,return_object=True)
505
+ if block_device not in tptDict:
506
+ sysfs_block_path = os.path.realpath(os.path.join('/sys/class/block', os.path.basename(block_device)))
507
+ tptDict[block_device] = get_read_write_rate_throughput_iter(sysfs_block_path)
508
+ mount_table = parseMount()
509
+ target_devices = set(block_devices)
510
+ if pseudo:
511
+ target_devices.update(mount_table.keys())
512
+ target_devices = sorted(target_devices)
513
+ uuid_dict = build_symlink_dict("/dev/disk/by-uuid")
514
+ label_dict = build_symlink_dict("/dev/disk/by-label")
515
+ fstype_dict = {}
516
+ size_dict = {}
517
+ lsblk_result.thread.join()
518
+ if lsblk_result.returncode == 0:
519
+ for line in lsblk_result.stdout:
520
+ lsblk_name, lsblk_size, lsblk_fstype, lsblk_uuid, lsblk_label = line.split(' ', 4)
521
+ # the label can be \x escaped, we need to decode it
522
+ lsblk_uuid = bytes(lsblk_uuid, "utf-8").decode("unicode_escape")
523
+ lsblk_fstype = bytes(lsblk_fstype, "utf-8").decode("unicode_escape")
524
+ lsblk_label = bytes(lsblk_label, "utf-8").decode("unicode_escape")
525
+ if lsblk_uuid:
526
+ uuid_dict[lsblk_name] = lsblk_uuid
527
+ if lsblk_fstype:
528
+ fstype_dict[lsblk_name] = lsblk_fstype
529
+ if lsblk_label:
530
+ label_dict[lsblk_name] = lsblk_label
531
+ try:
532
+ size_dict[lsblk_name] = int(lsblk_size)
533
+ except Exception:
534
+ pass
535
+ output = [["NAME", "FSTYPE", "SIZE", "FSUSE%", "MOUNTPOINT", "SMART", "LABEL", "UUID", "MODEL", "SERIAL", "DISCARD","RTPT",'WTPT']]
536
+ for device_name in target_devices:
537
+ if mounted_only and device_name not in mount_table:
538
+ continue
539
+ fstype = ''
540
+ size = ''
541
+ fsusepct = ''
542
+ mountpoint = ''
543
+ smart = ''
544
+ label = ''
545
+ uuid = ''
546
+ model = ''
547
+ serial = ''
548
+ discard = ''
549
+ rtpt = ''
550
+ wtpt = ''
551
+ # fstype, size, fsuse%, mountpoint, rtpt, wtpt, lable, uuid are partition specific
552
+ # smart, model, serial, discard are device specific, and only for block devices
553
+ # fstype, size, fsuse%, mountpoint does not require block device and can have multiple values per device
554
+ if is_block_device(device_name):
555
+ parent_name = get_partition_parent_name(device_name)
556
+ parent_sysfs_path = os.path.realpath(os.path.join('/sys/class/block', os.path.basename(parent_name))) if parent_name else None
557
+ model, serial = read_model_and_serial(parent_sysfs_path)
558
+ discard = read_discard_support(parent_sysfs_path)
559
+ if parent_name in smart_infos and SMARTCTL_PATH:
560
+ smart_info_obj = smart_infos[parent_name]
561
+ smart_info_obj.thread.join()
562
+ for line in smart_info_obj.stdout:
563
+ line = line.lower()
564
+ if "health" in line:
565
+ smartinfo = line.rpartition(':')[2].strip().upper()
566
+ smart = smartinfo.replace('PASSED', 'OK')
567
+ break
568
+ elif "denied" in line:
569
+ smart = 'DENIED'
570
+ break
571
+ if device_name in tptDict:
572
+ try:
573
+ rtpt, wtpt = next(tptDict[device_name])
574
+ if print_bytes:
575
+ rtpt = str(rtpt)
576
+ wtpt = str(wtpt)
577
+ else:
578
+ rtpt = multiCMD.format_bytes(rtpt, use_1024_bytes=use_1024, to_str=True,str_format='.1f') + 'B/s'
579
+ wtpt = multiCMD.format_bytes(wtpt, use_1024_bytes=use_1024, to_str=True,str_format='.1f') + 'B/s'
580
+ except Exception:
581
+ rtpt = ''
582
+ wtpt = ''
583
+ if device_name in label_dict:
584
+ label = label_dict[device_name]
585
+ if device_name in uuid_dict:
586
+ uuid = uuid_dict[device_name]
587
+ mount_points = mount_table.get(device_name, [])
588
+ if best_only:
589
+ if mount_points:
590
+ mount_points = [sorted(mount_points, key=lambda x: len(x.MOUNTPOINT))[0]]
591
+ if mount_points:
592
+ for mount_entry in mount_points:
593
+ fstype = mount_entry.FSTYPE
594
+ if formated_only and not fstype:
595
+ continue
596
+ mountpoint = mount_entry.MOUNTPOINT
597
+ size_bytes, used_bytes = get_statvfs_use_size(mountpoint)
598
+ if size_bytes == 0 and not show_zero_size_devices:
599
+ continue
600
+ fsusepct = f"{int(round(100.0 * used_bytes / size_bytes))}%" if size_bytes > 0 else "N/A"
601
+ if print_bytes:
602
+ size = str(size_bytes)
603
+ else:
604
+ size = multiCMD.format_bytes(size_bytes, use_1024_bytes=use_1024, to_str=True) + 'B'
605
+ if not full:
606
+ device_name = device_name.lstrip('/dev/')
607
+ output.append([device_name, fstype, size, fsusepct, mountpoint, smart, label, uuid, model, serial, discard, rtpt, wtpt])
608
+ else:
609
+ if formated_only and device_name not in fstype_dict:
610
+ continue
611
+ fstype = fstype_dict.get(device_name, '')
612
+ size_bytes = size_dict.get(device_name, 0)
613
+ if size_bytes == 0 and not show_zero_size_devices:
614
+ continue
615
+ if print_bytes:
616
+ size = str(size_bytes)
617
+ else:
618
+ size = multiCMD.format_bytes(size_bytes, use_1024_bytes=use_1024, to_str=True) + 'B'
619
+ if not full:
620
+ device_name = device_name.lstrip('/dev/')
621
+ output.append([device_name, fstype, size, fsusepct, mountpoint, smart, label, uuid, model, serial, discard, rtpt, wtpt])
622
+ return output
623
+
624
+
625
+ def main():
626
+ parser = argparse.ArgumentParser(description="Gather disk and partition info for block devices.")
627
+ parser.add_argument('-j','--json', help="Produce JSON output", action="store_true")
628
+ parser.add_argument('-b','--bytes', help="Print the SIZE column in bytes rather than in a human-readable format", action="store_true")
629
+ parser.add_argument('-H','--si', help="Use powers of 1000 not 1024 for SIZE column", action="store_true")
630
+ parser.add_argument('-F','-fo','--formated_only', help="Show only formated filesystems", action="store_true")
631
+ parser.add_argument('-M','-mo','--mounted_only', help="Show only mounted filesystems", action="store_true")
632
+ parser.add_argument('-B','-bo','--best_only', help="Show only shortest mount point for each device", action="store_true")
633
+ parser.add_argument('-a','--full', help="Show full device information, do not collapse drive info when length > console length", action="store_true")
634
+ parser.add_argument('-P','--pseudo', help="Include pseudo file systems as well (tmpfs / nfs / cifs etc.)", action="store_true")
635
+ parser.add_argument('--show_zero_size_devices', help="Show devices with zero size", action="store_true")
636
+ parser.add_argument('print_period', nargs='?', default=0, type=int, help="If specified as a number, repeat the output every N seconds")
637
+ parser.add_argument('-V', '--version', action='version', version=f"%(prog)s {version} @ {COMMIT_DATE} stat drives by pan@zopyr.us")
638
+
639
+ args = parser.parse_args()
640
+ tptDict = {}
641
+ while True:
642
+ results = get_drives_info(print_bytes = args.bytes, use_1024 = not args.si,
643
+ mounted_only=args.mounted_only, best_only=args.best_only,
644
+ formated_only=args.formated_only, show_zero_size_devices=args.show_zero_size_devices,
645
+ pseudo=args.pseudo,tptDict=tptDict,full=args.full)
646
+ if args.json:
647
+ import json
648
+ print(json.dumps(results, indent=1))
649
+ else:
650
+ print(multiCMD.pretty_format_table(results,full=args.full))
651
+ if args.print_period > 0:
652
+ try:
653
+ time.sleep(args.print_period)
654
+ except KeyboardInterrupt:
655
+ break
656
+ else:
657
+ break
658
+
659
+
660
+ if __name__ == "__main__":
661
+ main()
statblk-1.1/statblk.py DELETED
@@ -1,580 +0,0 @@
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.01'
16
- VERSION = version
17
- __version__ = version
18
- COMMIT_DATE = '2025-08-27'
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=1))
575
- else:
576
- print(pretty_format_table(results))
577
-
578
-
579
- if __name__ == "__main__":
580
- main()
File without changes
File without changes