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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statbtrfs
3
- Version: 0.28
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.40
14
+ Requires-Dist: multiCMD>=1.47
15
15
  Requires-Dist: argparse
16
16
  Provides-Extra: completion
17
17
  Requires-Dist: argcomplete; extra == "completion"
@@ -17,7 +17,7 @@ setup(
17
17
  ],
18
18
  },
19
19
  install_requires=[
20
- 'multiCMD>=1.40',
20
+ 'multiCMD>=1.47',
21
21
  'argparse',
22
22
  ],
23
23
  extras_require={
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statbtrfs
3
- Version: 0.28
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.40
14
+ Requires-Dist: multiCMD>=1.47
15
15
  Requires-Dist: argparse
16
16
  Provides-Extra: completion
17
17
  Requires-Dist: argcomplete; extra == "completion"
@@ -1,4 +1,4 @@
1
- multiCMD>=1.40
1
+ multiCMD>=1.47
2
2
  argparse
3
3
 
4
4
  [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 float(multiCMD.version) >= 1.47
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.28'
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
- device_path = None
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
- device_path = line.split("path")[1].strip()
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
- key, val = line.split()
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 moutns. Default="*"')
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['write_io_errs']),
293
- color_error_count(err_dict['read_io_errs']),
294
- color_error_count(err_dict['corruption_errs']),
295
- color_error_count(err_dict['generation_errs']),
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