statbtrfs 0.28__tar.gz → 0.30__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.
- {statbtrfs-0.28 → statbtrfs-0.30}/PKG-INFO +2 -2
- {statbtrfs-0.28 → statbtrfs-0.30}/setup.py +1 -1
- {statbtrfs-0.28 → statbtrfs-0.30}/statbtrfs.egg-info/PKG-INFO +2 -2
- {statbtrfs-0.28 → statbtrfs-0.30}/statbtrfs.egg-info/requires.txt +1 -1
- {statbtrfs-0.28 → statbtrfs-0.30}/statbtrfs.py +81 -16
- {statbtrfs-0.28 → statbtrfs-0.30}/README.txt +0 -0
- {statbtrfs-0.28 → statbtrfs-0.30}/setup.cfg +0 -0
- {statbtrfs-0.28 → statbtrfs-0.30}/statbtrfs.egg-info/SOURCES.txt +0 -0
- {statbtrfs-0.28 → statbtrfs-0.30}/statbtrfs.egg-info/dependency_links.txt +0 -0
- {statbtrfs-0.28 → statbtrfs-0.30}/statbtrfs.egg-info/entry_points.txt +0 -0
- {statbtrfs-0.28 → statbtrfs-0.30}/statbtrfs.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: statbtrfs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.30
|
|
4
4
|
Summary: Query Btrfs filesystem status
|
|
5
5
|
Home-page: https://github.com/yufei-pan/statbtrfs
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -11,7 +11,7 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
|
|
|
11
11
|
Classifier: Operating System :: POSIX :: Linux
|
|
12
12
|
Requires-Python: >=3.6
|
|
13
13
|
Description-Content-Type: text/plain
|
|
14
|
-
Requires-Dist: multiCMD>=1.
|
|
14
|
+
Requires-Dist: multiCMD>=1.47
|
|
15
15
|
Requires-Dist: argparse
|
|
16
16
|
Provides-Extra: completion
|
|
17
17
|
Requires-Dist: argcomplete; extra == "completion"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: statbtrfs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.30
|
|
4
4
|
Summary: Query Btrfs filesystem status
|
|
5
5
|
Home-page: https://github.com/yufei-pan/statbtrfs
|
|
6
6
|
Author: Yufei Pan
|
|
@@ -11,7 +11,7 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
|
|
|
11
11
|
Classifier: Operating System :: POSIX :: Linux
|
|
12
12
|
Requires-Python: >=3.6
|
|
13
13
|
Description-Content-Type: text/plain
|
|
14
|
-
Requires-Dist: multiCMD>=1.
|
|
14
|
+
Requires-Dist: multiCMD>=1.47
|
|
15
15
|
Requires-Dist: argparse
|
|
16
16
|
Provides-Extra: completion
|
|
17
17
|
Requires-Dist: argcomplete; extra == "completion"
|
|
@@ -5,10 +5,11 @@ import time
|
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
7
|
import fnmatch
|
|
8
|
+
from datetime import datetime
|
|
8
9
|
try:
|
|
9
10
|
import multiCMD # type: ignore
|
|
10
|
-
assert
|
|
11
|
-
except (ImportError, AssertionError):
|
|
11
|
+
assert tuple(int(part) for part in multiCMD.version.split('.')[:2]) >= (1, 47)
|
|
12
|
+
except (ImportError, AssertionError, ValueError):
|
|
12
13
|
import sys,types,base64,lzma # noqa: E401
|
|
13
14
|
multiCMD = types.ModuleType("multiCMD")
|
|
14
15
|
sys.modules["multiCMD"] = multiCMD
|
|
@@ -92,7 +93,7 @@ J%qnMk3G%Ki=s>n[1@,^@>j\8FEhN0`om\)D0qE:R`HqPJ4V2sGJQF,mfou/XJaY?rc[UV'\!tti]K/e
|
|
|
92
93
|
3o1Z^c;DqS!6qW$2SYp!!/0?QbEJ#:7ZhqZ,C;P!WW3#!!HG.'''
|
|
93
94
|
exec(lzma.decompress(base64.a85decode(_SRC_B85)).decode("utf-8"), multiCMD.__dict__)
|
|
94
95
|
|
|
95
|
-
version='0.
|
|
96
|
+
version='0.30'
|
|
96
97
|
__version__ = version
|
|
97
98
|
|
|
98
99
|
# Colors
|
|
@@ -137,7 +138,7 @@ def find_btrfs_mounts(filters):
|
|
|
137
138
|
|
|
138
139
|
def parse_show_info(info):
|
|
139
140
|
fs_used = None
|
|
140
|
-
|
|
141
|
+
device_paths = []
|
|
141
142
|
uuid = None
|
|
142
143
|
for line in info:
|
|
143
144
|
if "uuid:" in line:
|
|
@@ -145,7 +146,8 @@ def parse_show_info(info):
|
|
|
145
146
|
if "Total devices" in line:
|
|
146
147
|
fs_used = line.split("FS bytes used")[1].strip()
|
|
147
148
|
if "path" in line:
|
|
148
|
-
|
|
149
|
+
device_paths.append(line.split("path")[1].strip())
|
|
150
|
+
device_path = ", ".join(device_paths) if device_paths else None
|
|
149
151
|
if fs_used is None or device_path is None or uuid is None:
|
|
150
152
|
print(f"{RED}Could not parse btrfs show info.{RESET}")
|
|
151
153
|
print(f"Output from btrfs filesystem show was:\n{info}")
|
|
@@ -154,7 +156,10 @@ def parse_show_info(info):
|
|
|
154
156
|
def parse_device_stats(stats):
|
|
155
157
|
err_dict = {}
|
|
156
158
|
for line in stats:
|
|
157
|
-
|
|
159
|
+
parts = line.split()
|
|
160
|
+
if len(parts) != 2:
|
|
161
|
+
continue
|
|
162
|
+
key, val = parts
|
|
158
163
|
err_dict[key.split(".")[-1]] = val
|
|
159
164
|
return err_dict
|
|
160
165
|
|
|
@@ -187,15 +192,15 @@ def parse_scrub_status(output):
|
|
|
187
192
|
}
|
|
188
193
|
for line in lines:
|
|
189
194
|
if "Status:" in line:
|
|
190
|
-
scrub_data["scrub_status"] = line.split(":")[1].strip()
|
|
195
|
+
scrub_data["scrub_status"] = line.split(":", 1)[1].strip()
|
|
191
196
|
elif "Scrub started:" in line:
|
|
192
|
-
scrub_data["scrub_time"] = line.split(":")[1].strip()
|
|
197
|
+
scrub_data["scrub_time"] = line.split(":", 1)[1].strip()
|
|
193
198
|
elif "Rate:" in line:
|
|
194
|
-
scrub_data["scrub_rate"] = line.split(":")[1].strip()
|
|
199
|
+
scrub_data["scrub_rate"] = line.split(":", 1)[1].strip()
|
|
195
200
|
elif "Total to scrub:" in line and scrub_data.get("scrub_status") != "running":
|
|
196
|
-
scrub_data["scrubbed_data"] = line.split(":")[1].strip()
|
|
201
|
+
scrub_data["scrubbed_data"] = line.split(":", 1)[1].strip()
|
|
197
202
|
elif "Bytes scrubbed:" in line and scrub_data.get("scrub_status") == "running":
|
|
198
|
-
scrub_data["scrubbed_data"] = line.split(":")[1].strip()
|
|
203
|
+
scrub_data["scrubbed_data"] = line.split(":", 1)[1].strip()
|
|
199
204
|
elif "Error summary:" in line:
|
|
200
205
|
err_idx = lines.index(line)
|
|
201
206
|
error_text = "\n".join(lines[err_idx:])
|
|
@@ -206,6 +211,46 @@ def parse_scrub_status(output):
|
|
|
206
211
|
|
|
207
212
|
return scrub_data
|
|
208
213
|
|
|
214
|
+
def parse_scrub_time_to_epoch(scrub_time):
|
|
215
|
+
# btrfs commonly prints values like: "Thu May 2 09:56:55 2024"
|
|
216
|
+
time_formats = [
|
|
217
|
+
"%a %b %d %H:%M:%S %Y",
|
|
218
|
+
"%a %b %d %H:%M:%S %Y %Z",
|
|
219
|
+
"%Y-%m-%d %H:%M:%S"
|
|
220
|
+
]
|
|
221
|
+
for time_format in time_formats:
|
|
222
|
+
try:
|
|
223
|
+
return datetime.strptime(scrub_time, time_format).timestamp()
|
|
224
|
+
except ValueError:
|
|
225
|
+
continue
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
def filter_recently_scrubbed_mounts(btrfs_mounts, exclude_recent_days):
|
|
229
|
+
if exclude_recent_days <= 0:
|
|
230
|
+
return btrfs_mounts, []
|
|
231
|
+
|
|
232
|
+
now = time.time()
|
|
233
|
+
cutoff_seconds = exclude_recent_days * 24 * 3600
|
|
234
|
+
filtered_mounts = set()
|
|
235
|
+
skipped_mounts = []
|
|
236
|
+
|
|
237
|
+
for mount in btrfs_mounts:
|
|
238
|
+
scrub_output = execute_command(["btrfs", "scrub", "status", mount])
|
|
239
|
+
scrub_data = parse_scrub_status(scrub_output or [])
|
|
240
|
+
scrub_status = scrub_data.get("scrub_status", "").strip().lower()
|
|
241
|
+
scrub_time_epoch = parse_scrub_time_to_epoch(scrub_data["scrub_time"])
|
|
242
|
+
|
|
243
|
+
if scrub_status != "finished" or scrub_time_epoch is None:
|
|
244
|
+
filtered_mounts.add(mount)
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
if now - scrub_time_epoch < cutoff_seconds:
|
|
248
|
+
skipped_mounts.append(mount)
|
|
249
|
+
else:
|
|
250
|
+
filtered_mounts.add(mount)
|
|
251
|
+
|
|
252
|
+
return filtered_mounts, skipped_mounts
|
|
253
|
+
|
|
209
254
|
def issue_scrub_command(mounts):
|
|
210
255
|
# print(f"{YELLOW}Issuing scrub on {mount}{RESET}")
|
|
211
256
|
# execute_command(['bash','-c',f"btrfs scrub start {mount}"],wait=False)
|
|
@@ -225,8 +270,15 @@ def main():
|
|
|
225
270
|
parser.add_argument("-s", "--scrub", help="Issue scrub to all pools", action="store_true")
|
|
226
271
|
parser.add_argument("-i", "--interval", help="Interval for status check in seconds", default=2, type=int)
|
|
227
272
|
parser.add_argument("-m", "--max_scrub_count", help="Maximum number of scrubs to issue at the same time", default=32, type=int)
|
|
273
|
+
parser.add_argument(
|
|
274
|
+
"-d",
|
|
275
|
+
"--exclude-recent-scrubbed-days",
|
|
276
|
+
help="Exclude mounts scrubbed within this many days",
|
|
277
|
+
default=0,
|
|
278
|
+
type=int
|
|
279
|
+
)
|
|
228
280
|
parser.add_argument("--scrub_command_lockout", help="Lockout for scrub command. Used to block two commands sent quickly.", default=10, type=int)
|
|
229
|
-
parser.add_argument('pattern',nargs='*',help='Patterns to filter btrfs
|
|
281
|
+
parser.add_argument('pattern',nargs='*',help='Patterns to filter btrfs mounts. Default="*"')
|
|
230
282
|
parser.add_argument('-V', '--version', action='version', version=f"%(prog)s {version} stat btrfs by pan@zopyr.us")
|
|
231
283
|
try:
|
|
232
284
|
import argcomplete
|
|
@@ -236,6 +288,19 @@ def main():
|
|
|
236
288
|
args = parser.parse_args()
|
|
237
289
|
|
|
238
290
|
btrfs_mounts = set(find_btrfs_mounts(args.pattern))
|
|
291
|
+
btrfs_mounts, skipped_mounts = filter_recently_scrubbed_mounts(
|
|
292
|
+
btrfs_mounts,
|
|
293
|
+
args.exclude_recent_scrubbed_days
|
|
294
|
+
)
|
|
295
|
+
if skipped_mounts:
|
|
296
|
+
print(
|
|
297
|
+
f"{YELLOW}Skipping mounts scrubbed within the last "
|
|
298
|
+
f"{args.exclude_recent_scrubbed_days}d: {', '.join(sorted(skipped_mounts))}{RESET}"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if not btrfs_mounts:
|
|
302
|
+
print(f"{RED}No Btrfs mounts found.{RESET}")
|
|
303
|
+
return
|
|
239
304
|
|
|
240
305
|
# If the scrub flag is enabled, issue the scrub command to all mounts
|
|
241
306
|
if args.scrub:
|
|
@@ -289,10 +354,10 @@ def main():
|
|
|
289
354
|
mount_dir_name,
|
|
290
355
|
device_path,
|
|
291
356
|
fs_used,
|
|
292
|
-
color_error_count(err_dict
|
|
293
|
-
color_error_count(err_dict
|
|
294
|
-
color_error_count(err_dict
|
|
295
|
-
color_error_count(err_dict
|
|
357
|
+
color_error_count(err_dict.get('write_io_errs', 0)),
|
|
358
|
+
color_error_count(err_dict.get('read_io_errs', 0)),
|
|
359
|
+
color_error_count(err_dict.get('corruption_errs', 0)),
|
|
360
|
+
color_error_count(err_dict.get('generation_errs', 0)),
|
|
296
361
|
scrub_data["scrub_status"],
|
|
297
362
|
scrub_data["scrub_time"],
|
|
298
363
|
scrub_data["scrub_rate"],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|