statblk 1.1__py3-none-any.whl → 1.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {statblk-1.1.dist-info → statblk-1.20.dist-info}/METADATA +3 -1
- statblk-1.20.dist-info/RECORD +6 -0
- statblk.py +545 -464
- statblk-1.1.dist-info/RECORD +0 -6
- {statblk-1.1.dist-info → statblk-1.20.dist-info}/WHEEL +0 -0
- {statblk-1.1.dist-info → statblk-1.20.dist-info}/entry_points.txt +0 -0
- {statblk-1.1.dist-info → statblk-1.20.dist-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.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
|
|
@@ -0,0 +1,6 @@
|
|
1
|
+
statblk.py,sha256=uvXs6TVqmLZ0-xi0w6XpNZE5yvpSS6zTTe6YPN0gqEU,26815
|
2
|
+
statblk-1.20.dist-info/METADATA,sha256=hANM-8XXekJgSHz9jRn4EhqD3WXKpAkgu7Oe40iZhTg,1465
|
3
|
+
statblk-1.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
4
|
+
statblk-1.20.dist-info/entry_points.txt,sha256=JDz-sa6FIdaOckmlz9NbnhZXQaB5Yle-cTKgUQmAV40,41
|
5
|
+
statblk-1.20.dist-info/top_level.txt,sha256=dBdU6_PD4tG_7uquWEs6YremqudiePASv3u3G59scf4,8
|
6
|
+
statblk-1.20.dist-info/RECORD,,
|
statblk.py
CHANGED
@@ -7,16 +7,279 @@
|
|
7
7
|
import os
|
8
8
|
import re
|
9
9
|
import stat
|
10
|
-
from collections import defaultdict
|
10
|
+
from collections import defaultdict, namedtuple
|
11
11
|
import argparse
|
12
12
|
import shutil
|
13
13
|
import subprocess
|
14
|
-
|
15
|
-
|
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'
|
16
280
|
VERSION = version
|
17
281
|
__version__ = version
|
18
|
-
COMMIT_DATE = '2025-
|
19
|
-
|
282
|
+
COMMIT_DATE = '2025-09-10'
|
20
283
|
|
21
284
|
SMARTCTL_PATH = shutil.which("smartctl")
|
22
285
|
|
@@ -27,7 +290,6 @@ def read_text(path):
|
|
27
290
|
except Exception:
|
28
291
|
return None
|
29
292
|
|
30
|
-
|
31
293
|
def read_int(path):
|
32
294
|
s = read_text(path)
|
33
295
|
if s is None:
|
@@ -37,8 +299,7 @@ def read_int(path):
|
|
37
299
|
except Exception:
|
38
300
|
return None
|
39
301
|
|
40
|
-
|
41
|
-
def build_symlink_map(dir_path):
|
302
|
+
def build_symlink_dict(dir_path):
|
42
303
|
"""
|
43
304
|
Build map: devname -> token (uuid or label string) using symlinks under
|
44
305
|
/dev/disk/by-uuid or /dev/disk/by-label.
|
@@ -52,177 +313,89 @@ def build_symlink_map(dir_path):
|
|
52
313
|
try:
|
53
314
|
if os.path.islink(p):
|
54
315
|
tgt = os.path.realpath(p)
|
55
|
-
|
56
|
-
mapping.setdefault(devname, entry)
|
316
|
+
mapping.setdefault(tgt, entry)
|
57
317
|
except Exception:
|
58
318
|
continue
|
59
319
|
except Exception:
|
60
320
|
pass
|
61
321
|
return mapping
|
62
322
|
|
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):
|
323
|
+
def get_statvfs_use_size(mountpoint):
|
137
324
|
try:
|
138
325
|
st = os.statvfs(mountpoint)
|
139
|
-
if st.
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
143
331
|
except Exception:
|
144
|
-
return
|
332
|
+
return 0, 0
|
145
333
|
|
146
|
-
|
147
|
-
def read_discard_support(
|
148
|
-
if not
|
149
|
-
return
|
150
|
-
dmbytes = read_int(os.path.join(
|
151
|
-
dgran = read_int(os.path.join(sys_block_path, "queue", "discard_granularity"))
|
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"))
|
152
339
|
try:
|
153
|
-
|
340
|
+
if (dmbytes or 0) > 0:
|
341
|
+
return 'Yes'
|
342
|
+
else:
|
343
|
+
return 'No'
|
154
344
|
except Exception:
|
155
|
-
return
|
345
|
+
return 'N/A'
|
156
346
|
|
157
|
-
|
158
|
-
def get_parent_device_sysfs(
|
347
|
+
@cache_decorator
|
348
|
+
def get_parent_device_sysfs(sysfs_block_path):
|
159
349
|
"""
|
160
350
|
Return the sysfs 'device' directory for this block node (resolves partition
|
161
351
|
to its parent device as well).
|
162
352
|
"""
|
163
|
-
dev_link = os.path.join(
|
353
|
+
dev_link = os.path.join(sysfs_block_path, "device")
|
164
354
|
try:
|
165
355
|
return os.path.realpath(dev_link)
|
166
356
|
except Exception:
|
167
357
|
return dev_link
|
168
358
|
|
169
|
-
|
170
|
-
def read_model_and_serial(
|
171
|
-
if not
|
172
|
-
return
|
173
|
-
device_path = get_parent_device_sysfs(
|
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)
|
174
364
|
model = read_text(os.path.join(device_path, "model"))
|
175
365
|
serial = read_text(os.path.join(device_path, "serial"))
|
176
366
|
if serial is None:
|
177
367
|
serial = read_text(os.path.join(device_path, "wwid"))
|
178
368
|
if model:
|
179
369
|
model = " ".join(model.split())
|
370
|
+
else:
|
371
|
+
model = ''
|
180
372
|
if serial:
|
181
373
|
serial = " ".join(serial.split())
|
374
|
+
else:
|
375
|
+
serial = ''
|
182
376
|
return model, serial
|
183
377
|
|
184
|
-
|
185
|
-
def
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
return
|
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
|
-
|
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
|
226
399
|
def is_block_device(devpath):
|
227
400
|
try:
|
228
401
|
st_mode = os.stat(devpath).st_mode
|
@@ -230,329 +403,224 @@ def is_block_device(devpath):
|
|
230
403
|
except Exception:
|
231
404
|
return False
|
232
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"))
|
233
410
|
|
234
|
-
|
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
|
+
@cache_decorator
|
411
412
|
def get_partition_parent_name(name):
|
412
|
-
|
413
|
-
part_file = os.path.join(real, "partition")
|
414
|
-
if not os.path.exists(part_file):
|
413
|
+
if not name:
|
415
414
|
return None
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
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
|
435
445
|
try:
|
436
|
-
|
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)
|
437
459
|
except Exception:
|
438
|
-
|
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
|
460
|
+
yield 0, 0
|
461
|
+
while True:
|
454
462
|
try:
|
455
|
-
|
456
|
-
|
457
|
-
|
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)
|
458
491
|
except Exception:
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
if
|
493
|
-
|
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
|
494
531
|
try:
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
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:
|
499
563
|
line = line.lower()
|
500
564
|
if "health" in line:
|
501
565
|
smartinfo = line.rpartition(':')[2].strip().upper()
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
if
|
508
|
-
|
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, [])
|
509
588
|
if best_only:
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
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:
|
522
610
|
continue
|
523
|
-
|
524
|
-
|
525
|
-
|
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:
|
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:
|
545
614
|
continue
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
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
|
553
623
|
|
554
|
-
results.sort(key=lambda x: x["NAME"]) # type: ignore
|
555
|
-
return results
|
556
624
|
|
557
625
|
def main():
|
558
626
|
parser = argparse.ArgumentParser(description="Gather disk and partition info for block devices.")
|
@@ -561,19 +629,32 @@ def main():
|
|
561
629
|
parser.add_argument('-H','--si', help="Use powers of 1000 not 1024 for SIZE column", action="store_true")
|
562
630
|
parser.add_argument('-F','-fo','--formated_only', help="Show only formated filesystems", action="store_true")
|
563
631
|
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
|
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")
|
565
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")
|
566
637
|
parser.add_argument('-V', '--version', action='version', version=f"%(prog)s {version} @ {COMMIT_DATE} stat drives by pan@zopyr.us")
|
567
638
|
|
568
639
|
args = parser.parse_args()
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
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
|
577
658
|
|
578
659
|
|
579
660
|
if __name__ == "__main__":
|
statblk-1.1.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
statblk.py,sha256=De3O7pzb2zDInQm0XGb-wnuyZuOyTHWtayohSBK-E_0,16384
|
2
|
-
statblk-1.1.dist-info/METADATA,sha256=k-ZmPI5eIRMc6TBefitRBEUsXlBFkONYeDUgImZZTqw,1411
|
3
|
-
statblk-1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
4
|
-
statblk-1.1.dist-info/entry_points.txt,sha256=JDz-sa6FIdaOckmlz9NbnhZXQaB5Yle-cTKgUQmAV40,41
|
5
|
-
statblk-1.1.dist-info/top_level.txt,sha256=dBdU6_PD4tG_7uquWEs6YremqudiePASv3u3G59scf4,8
|
6
|
-
statblk-1.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|