1
1
#!/usr/bin/env python3
2
2
3
- # List BTFS subvolume space use information similar to ` df -h`
3
+ # List BTRFS subvolume space use information similar to df -h (with snapshot paths)
4
4
#
5
- # Note: this requires the "hurry.filesize" module. Install it with:
5
+ # Btrfsprogs is able to list and sort snapshots on a volume, but it only prints their
6
+ # id, not their path. This script wraps `btrfs qgroup show` to add filesystem paths
7
+ # to the generated table.
6
8
#
7
- # pip install hurry.filesize
9
+ # For this to work on a BTRFS volume, you first need to enable quotas on the volume:
8
10
#
9
- # This is rewritten from a shell script that was too slow:
11
+ # btrfs quota enable /mnt/some-volume
12
+ #
13
+ # Note that the current version of this script does not allow sorting by path, as it
14
+ # passes all arugments through to btrfsprogs. If you need that and don't mind being
15
+ # limited to only sorting by path, see this previous version:
16
+ #
17
+ # https://gist.github.com/stecman/3fd04a36111874f67c484c74e15ef311/6690edbd6a88380a1712024bb4115969b2545509
18
+ #
19
+ # This is based on a shell script that was too slow:
10
20
# https://github.com/agronick/btrfs-size
11
21
12
22
from __future__ import print_function
13
23
14
- from hurry .filesize import size
15
-
16
- import argparse
17
24
import subprocess
18
25
import sys
19
26
import os
20
27
import re
21
28
22
- parser = argparse .ArgumentParser (description = "List subvolume and snapshot sizes on BTRFS volumes" )
23
- parser .add_argument ("path" , help = "Path or UUID of the BTRFS volume to inspect" )
24
- parser .add_argument (
25
- "-s" ,
26
- dest = "sort" ,
27
- choices = [
28
- "exclusive" ,
29
- "total"
30
- ],
31
- help = "Sort output instead of using the order from btrfs"
32
- )
33
-
34
-
35
- class BtrfsQuota :
36
- def __init__ (self , raw_id , total_bytes , exclusive_bytes ):
37
- self .id = raw_id .split ("/" )[1 ]
38
- self .total_bytes = int (total_bytes )
39
- self .exclusive_bytes = int (exclusive_bytes )
40
-
41
- def btrfs_subvols_get (path ):
29
+ def get_btrfs_subvols (path ):
42
30
"""Return a dictionary of subvolume names indexed by their subvolume ID"""
43
31
try :
44
32
raw = subprocess .check_output (["btrfs" , "subvolume" , "list" , path ])
@@ -52,64 +40,90 @@ def btrfs_subvols_get(path):
52
40
print ("Is '%s' really a BTRFS volume?" % path )
53
41
sys .exit (1 )
54
42
55
- def btrfs_quotas_get ( path ):
56
- """Return a list of BtrfsQuota objects for path """
43
+ def get_data_raw ( args ):
44
+ """Return lines of output from a call to 'btrfs qgroup show' with args appended """
57
45
try :
58
46
# Get the lines of output, ignoring the two header lines
59
- raw = subprocess .check_output (["btrfs" , "qgroup" , "show" , "--raw" , path ])
60
- lines = raw .decode ("utf8" ).split ("\n " )[2 :]
61
-
62
- return [BtrfsQuota (* line .split ()) for line in lines if line != '' ]
47
+ raw = subprocess .check_output (["btrfs" , "qgroup" , "show" ] + args )
48
+ return raw .decode ("utf8" ).split ("\n " )
63
49
64
50
except subprocess .CalledProcessError as e :
65
51
if e .returncode != 0 :
66
52
print ("\n Failed to get subvolume quotas. Have you enabled quotas on this volume?" )
67
- print ("(You can do so with: sudo btrfs quota enable '%s')" % path )
53
+ print ("(You can do so with: sudo btrfs quota enable <path-to-volume>)" )
68
54
sys .exit (1 )
69
55
70
- def print_table (subvols , quotas ):
71
- # Get the column size right for the path
72
- max_path_length = 0
73
- for path in subvols .values ():
74
- max_path_length = max (len (path ), max_path_length )
56
+ def get_qgroup_id (line ):
57
+ """Extract qgroup id from a line of btrfs qgroup show output
58
+ Returns None if the line wasn't valid
59
+ """
60
+ id_match = re .match (r"\d+/(\d+)" , line )
61
+
62
+ if not id_match :
63
+ return None
75
64
76
- template = "{path:<{path_len}}{bytes:>16}{exclusive:>16}"
65
+ return id_match . group ( 1 )
77
66
78
- # Print table header
79
- header = template . format (
80
- path = "Subvolume" ,
81
- bytes = "Total size" ,
82
- exclusive = "Exclusive size" ,
83
- path_len = max_path_length
84
- )
67
+ def guess_path_argument ( argv ):
68
+ """Return an argument most likely to be the <path> arg for 'btrfs qgroup show'
69
+ This is a cheap way to pass through to btrfsprogs without duplicating the options here.
70
+ Currently only easier than duplication because the option/argument list is simple.
71
+ """
72
+ # Path can't be the first argument (program)
73
+ args = argv [ 1 :]
85
74
86
- print (header )
87
- print ("-" * len (header ))
75
+ # Filter out arguments to options
76
+ # Only the sort option currently takes an argument
77
+ option_follows = [
78
+ "--sort"
79
+ ]
88
80
89
- # Print table rows
90
- for quota in quotas :
91
- if quota .id in subvols :
92
- print (template .format (
93
- path_len = max_path_length ,
94
- path = subvols [quota .id ],
95
- bytes = size (quota .total_bytes ),
96
- exclusive = size (quota .exclusive_bytes )
97
- ))
81
+ for text in option_follows :
82
+ try :
83
+ position = args .index (text )
84
+ del args [position + 1 ]
85
+ except :
86
+ pass
87
+
88
+ # Ignore options
89
+ args = [arg for arg in args if re .match (r"^-" , arg ) is None ]
90
+
91
+ # Prefer the item at the end of the list as this is the suggested argument order
92
+ return args [- 1 ]
98
93
99
94
100
95
# Re-run the script as root if started with a non-priveleged account
101
96
if os .getuid () != 0 :
102
97
cmd = 'sudo "' + '" "' .join (sys .argv ) + '"'
103
98
sys .exit (subprocess .call (cmd , shell = True ))
104
99
105
- args = parser .parse_args ()
106
100
107
- subvols = btrfs_subvols_get (args .path )
108
- quotas = btrfs_quotas_get (args .path )
101
+ # Fetch command output to work with
102
+ output = get_data_raw (sys .argv [1 :])
103
+ subvols = get_btrfs_subvols (guess_path_argument (sys .argv ))
104
+
105
+ # Data for the new column
106
+ path_column = [
107
+ "path" ,
108
+ "----"
109
+ ]
110
+
111
+ # Iterate through all lines except for the table header
112
+ for index ,line in enumerate (output ):
113
+ # Ignore header rows
114
+ if index < 1 :
115
+ continue
116
+
117
+ groupid = get_qgroup_id (line )
118
+
119
+ if groupid in subvols :
120
+ path_column .append (subvols [groupid ])
121
+ else :
122
+ path_column .append ("" )
109
123
110
- if args .sort == "exclusive" :
111
- quotas .sort (key = lambda q : q .exclusive_bytes , reverse = True )
112
- elif args .sort == "bytes" :
113
- quotas .sort (key = lambda q : q .total_bytes , reverse = True )
124
+ # Find the required width for the new column
125
+ column_width = len (max (path_column , key = len )) + 2
114
126
115
- print_table (subvols , quotas )
127
+ # Output data with extra column for path
128
+ for index ,line in enumerate (output ):
129
+ print (path_column [index ].ljust (column_width ) + output [index ])
0 commit comments