statblk 1.1__tar.gz → 1.21__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.1 → statblk-1.21}/PKG-INFO +3 -1
- {statblk-1.1 → statblk-1.21}/setup.py +3 -0
- {statblk-1.1 → statblk-1.21}/statblk.egg-info/PKG-INFO +3 -1
- {statblk-1.1 → statblk-1.21}/statblk.egg-info/SOURCES.txt +1 -0
- statblk-1.21/statblk.egg-info/requires.txt +1 -0
- statblk-1.21/statblk.py +661 -0
- statblk-1.1/statblk.py +0 -580
- {statblk-1.1 → statblk-1.21}/README.txt +0 -0
- {statblk-1.1 → statblk-1.21}/setup.cfg +0 -0
- {statblk-1.1 → statblk-1.21}/statblk.egg-info/dependency_links.txt +0 -0
- {statblk-1.1 → statblk-1.21}/statblk.egg-info/entry_points.txt +0 -0
- {statblk-1.1 → statblk-1.21}/statblk.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: statblk
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.21
|
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
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: statblk
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.21
|
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
|
|
@@ -0,0 +1 @@
|
|
1
|
+
multiCMD>=1.35
|
statblk-1.21/statblk.py
ADDED
@@ -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:
|
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.21'
|
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
|
File without changes
|
File without changes
|
File without changes
|