├── .github └── workflows │ └── check.yml ├── README.md └── btrfs-list /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push,pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: install prerequisites 13 | run: sudo apt-get install -y perl libperl-critic-perl perltidy 14 | - name: perl check 15 | run: perl -c btrfs-list 16 | - name: perlcritic 17 | run: perlcritic btrfs-list 18 | - name: perltidy 19 | run: | 20 | perltidy -b -csc -iscl -nolc -nbbc -pt=2 -sbt=2 -bt=2 -l=120 -msc=1 btrfs-list 21 | if ! test -e btrfs-list.tdy; then 22 | echo "OK: perltidy didn't find any change to make" 23 | else 24 | echo "KO: perltidy found changes to make:" 25 | diff -u btrfs-list btrfs-list.tdy 26 | exit 1 27 | fi 28 | - name: perlcritic 29 | run: | 30 | perlcritic -3 --statistics --exclude 'ValuesAndExpressions::ProhibitConstantPragma|Variables::RequireLocalizedPunctuationVars|Subroutines::RequireArgUnpacking|InputOutput::RequireBriefOpen|RegularExpressions::RequireExtendedFormatting|Subroutines::ProhibitExcessComplexity|ControlStructures::ProhibitCascadingIfElse|RegularExpressions::ProhibitUnusedCapture|ValuesAndExpressions::ProhibitMismatchedOperators|Modules::ProhibitExcessMainComplexity|ErrorHandling::RequireCarping' btrfs-list 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Btrfs is a great filesystem, but its userland tools are not very user-friendly yet. 4 | As a long-time user, I've developed `btrfs-list` as a wrapper to make sense out of the `btrfs sub list` and `btrfs qgroup show` commands. 5 | 6 | You need `btrfs-list` if either: 7 | - You'd like to have a nice tree-style overview of your subvolumes/snapshots 8 | - You've already used ZFS before and you're missing the _zfs list_ command 9 | - You're looking for exactly which snapshot to destroy to regain some space 10 | - You're using `btrfs-progs` < v5.7 and you're looking for a more accurate estimation of how much space is remaining on your FS for all btrfs supported data profiles, 11 | as corner cases of raid1/raid10 are not handled well by older versions of `btrfs-progs`, and raid5/raid6 were not implemented at all 12 | 13 | Basically it turns this: 14 | ![btrfs_sub_list](https://user-images.githubusercontent.com/218502/53362053-99564e00-3939-11e9-9072-1d9ef617971f.PNG) 15 | into this: 16 | ![btrfs_list](https://user-images.githubusercontent.com/218502/53362048-965b5d80-3939-11e9-8e2f-8f92c7db79e4.PNG) 17 | 18 | # Prerequisites 19 | - `btrfs-progs` v3.18 at least (Dec 2014) 20 | - The _quota_ feature enabled on your Btrfs filesystems (optional, to get space usage for subvolumes and snapshots) 21 | 22 | # Usage 23 | 24 | ``` 25 | Usage: btrfs-list [options] [mountpoint1 [mountpoint2 [...]]] 26 | 27 | If no mountpoints are specified, display info for all btrfs filesystems. 28 | 29 | -h, --help display this message 30 | --debug enable debug output 31 | -q, --quiet silence quota disabled & quota rescan warnings, 32 | repeat to silence all other warnings. 33 | --version display version info 34 | --color WHEN colorize the output; WHEN can be 'never', 35 | 'always', or 'auto' (default is: 36 | colorize if STDOUT is a term) 37 | -n, --no-color synonym of --color=never 38 | --bright use bright colors (better for dark terminals) 39 | -H, --no-header hide header from output 40 | -r, --raw show raw numbers instead of human-readable 41 | --btrfs-binary BIN path to the btrfs binary to use instead of using 42 | the first binary found in the PATH 43 | --ignore-version-check try to continue even if btrfs-progs seems too old 44 | --ignore-root-check try to continue even if we are not root 45 | 46 | -s, --hide-snap hide all snapshots 47 | -S, --snap-only only show snapshots 48 | -d, --deleted show deleted parents of orphaned snapshots 49 | --snap-min-excl SIZE hide snapshots whose exclusively allocated extents 50 | take up less space than SIZE 51 | --snap-max-excl SIZE hide snapshots whose exclusively allocated extents 52 | take up more space than SIZE 53 | -f, --free-space only show free space on the filesystem 54 | -u, --used display used space instead of free space 55 | 56 | -p, --profile PROFILE override data profile detection and consider it 57 | as 'dup', 'single', 'raid0', 'raid1', 58 | 'raid1c3', 'raid1c4', 'raid10', 'raid5' or 59 | 'raid6' for free space calculation 60 | 61 | -a, --show-all show all information for each item 62 | --show-gen show generation of each item 63 | --show-cgen show generation at creation of each item 64 | --show-id show id of each item 65 | --show-parent show parent id of each item 66 | --show-toplevel show top level of each item 67 | --show-uuid show uuid of each item 68 | --show-puuid show parent uuid of each item 69 | --show-ruuid show received uuid of each item 70 | --show-otime show snap creation time 71 | 72 | -w, --wide don't truncate uuids on output (this is the 73 | default if STDOUT is NOT a term) 74 | --no-wide always truncate uuids on output (useful to 75 | override above default) 76 | --max-name-len LEN trim long subvol names to LEN. 0 means never trim. 77 | Defaults to 80 if STDOUT is a term, 0 otherwise. 78 | --indent LEN number of spaces to indent the tree, default: 3. 79 | 80 | SIZE can be a number (in bytes), or a number followed by k, M, G, T or P. 81 | ``` 82 | 83 | # Examples 84 | 85 | ## Quick view 86 | 87 | ``` 88 | root@nas:~# btrfs-list /git 89 | NAME TYPE REFER EXCL MOUNTPOINT 90 | git fs - 58.96G (single, 17.27G free) 91 | [main] mainvol 60.61G 292.00k /git 92 | .beeshome subvol 56.64M 56.64M 93 | .snaps/20220103_005415_daily.0 rosnap 60.54G 144.00k 94 | .snaps/20220101_004815_daily.2 rosnap 60.42G 160.00k 95 | .snaps/20211229_004815_daily.5 rosnap 60.42G 160.00k 96 | .snaps/20220102_005203_daily.1 rosnap 60.42G 160.00k 97 | .snaps/20220103_114214_hourly.6 rosnap 60.54G 144.00k 98 | .snaps/20220103_154814_hourly.2 rosnap 60.61G 144.00k 99 | .snaps/20220103_175114_hourly.0 rosnap 60.61G 144.00k 100 | .snaps/20211230_004815_daily.4 rosnap 60.42G 160.00k 101 | .snaps/20211231_004815_daily.3 rosnap 60.42G 160.00k 102 | .snaps/20220103_144814_hourly.3 rosnap 60.61G 144.00k 103 | .snaps/20220103_164815_hourly.1 rosnap 60.61G 144.00k 104 | .snaps/20220103_124215_hourly.5 rosnap 60.54G 2.23M 105 | .snaps/20220103_134556_hourly.4 rosnap 60.60G 480.00k 106 | .snaps/20220103_104215_hourly.7 rosnap 60.54G 144.00k 107 | ``` 108 | 109 | ## Detailed view 110 | 111 | ``` 112 | root@nas:/mnt/b# btrfs-list -qad . 113 | NAME ID PARENT TOPLVL GEN CGEN UUID PARENTUUID RCVD_UUID OTIME TYPE EXCL MOUNTPOINT 114 | 7cb8325f - - - - - - - - - fs 157.00M (raid5, 3.59G free, 2.74G unallocatable) 115 | [main] 5 - - - - - - - - mainvol - /mnt/b 116 | sub1 258 5 5 21 8 ae9c..6cae - - - subvol - 117 | sub1/.snap1 259 258 258 9 9 2d50..2094 ae9c..6cae - 2022-01-03 18:39:48 snap - 118 | sub1/.snap2 260 258 258 10 10 a2e9..1431 ae9c..6cae - 2022-01-03 18:39:48 snap - 119 | sub1/.snap3 270 258 258 20 20 b054..6ce2 ae9c..6cae - 2022-01-03 18:41:26 snap - 120 | sub1/.snap4 271 258 258 21 21 ae07..cc69 ae9c..6cae - 2022-01-03 18:41:27 snap - 121 | sub1/subsub1 261 258 258 14 11 bdf1..e7fe - - - subvol - 122 | sub1/subsub1/.snapA1 262 261 261 12 12 ab9b..e6df bdf1..e7fe - 2022-01-03 18:40:09 snap - 123 | sub1/subsub1/.snapA2 263 261 261 13 13 407c..1a14 bdf1..e7fe - 2022-01-03 18:40:09 snap - 124 | sub2 265 5 5 19 15 bc35..8104 - - - subvol - 125 | sub3 266 5 5 17 16 eb52..da06 - - - subvol - 126 | sub3/subsub3 267 266 266 32 17 eb81..00af - - - subvol - 127 | sub3-snaps/.snapK 280 279 279 30 30 41b9..442a eb81..00af - 2022-01-03 18:43:25 snap - 128 | sub3-snaps/.snapL 281 279 279 31 31 3308..f953 eb81..00af - 2022-01-03 18:43:25 snap - 129 | sub2/subsub2 268 265 265 28 18 2189..08b4 - - - subvol - 130 | sub2/subsub2/.snapB 269 268 268 19 19 259d..0afd 2189..08b4 - 2022-01-03 18:41:15 snap - 131 | sub2/subsub2/.snapC 272 268 268 24 22 8ae4..1313 2189..08b4 - 2022-01-03 18:41:49 snap - 132 | sub2/subsub2/.snapB-backup 294 268 268 24 24 fe27..2169 259d..0afd - 2022-01-03 18:42:05 snap - 133 | sub2/subsub2/.snapC-backup 274 268 268 24 24 da86..1964 8ae4..1313 - 2022-01-03 18:42:03 snap - 134 | sub2/subsub2/.snapD 273 268 268 23 23 713f..6a28 2189..08b4 - 2022-01-03 18:41:50 snap - 135 | sub2-snaps/.snapX 276 275 275 26 26 97ef..7187 2189..08b4 - 2022-01-03 18:43:04 snap - 136 | sub2-snaps/.snapZ 278 275 275 28 28 4c08..f7fb 2189..08b4 - 2022-01-03 18:43:06 snap - 137 | sub2-snaps 275 5 5 28 25 3ce4..2ae3 - - - subvol - 138 | sub3-snaps 279 5 5 32 29 05c9..0620 - - - subvol - 139 | sub4-snaps 284 5 5 37 34 2366..b451 - - - subvol - 140 | (deleted) - - - - - 05eb..d578 - - - deleted - 141 | sub4-snaps/sub4-bkp1 285 284 284 35 35 0134..77cb 05eb..d578 - 2022-01-03 18:45:11 snap - 142 | sub4-snaps/sub4-bkp2 286 284 284 36 36 587b..e3e0 05eb..d578 - 2022-01-03 18:45:11 snap - 143 | sub4-snaps/sub4-bkp3 287 284 284 37 37 55eb..e32e 05eb..d578 - 2022-01-03 18:45:12 snap - 144 | ``` 145 | 146 | Note that the hierarchy here is the hierarchy between the subvolumes and snapshots, not the folder hierarchy. 147 | This is why for example `sub3-snaps/.snapK` is under `sub3/subsub3`, because it is a snapshot of this subvolume, 148 | even if in the folder hierarchy, it is under `sub3-snaps`. 149 | 150 | Same goes for `.snapD` and `.snapX`, these are at a different spot in the folder hierarchy, but both are snapshots 151 | of the `sub2/subsub2` subvolume, hence are placed under it. 152 | 153 | We also have 3 snapshots of a `(deleted)` subvolume, these ghosts subvolumes are shown with the option `-d`. 154 | 155 | ## View free space of all btrfs filesystems at a glance 156 | 157 | ``` 158 | root@nas:/tmp/md5# btrfs-list -f 159 | NAME TYPE EXCL MOUNTPOINT 160 | var fs 18.09G (single, 5.62G free) 161 | root fs 950.39M (single, 36.61M free) 162 | newtank fs 15.61T (raid1, 764.30G free) 163 | git fs 58.96G (single, 17.27G free) 164 | opt fs 1.11G (single, 668.23M free) 165 | incoming fs 26.18T (single, 1.07T free) 166 | 7cb8325f fs 157.00M (raid5, 3.59G free, 2.74G unallocatable) 167 | home fs 13.64G (single, 5.99G free) 168 | slash fs 19.80G (single, 11.70G free) 169 | varlog fs 12.26G (single, 3.96G free) 170 | ``` 171 | 172 | ## Display heavy snapshots only 173 | 174 | ``` 175 | root@nas:~# btrfs-list --snap-min-excl 4G --snap-only /tank 176 | NAME TYPE REFER EXCL MOUNTPOINT 177 | backups/.snaps/skyline/20130213_231649_lastskyline rosnap 22.52G 19.58G 178 | backups/.snaps/box/20171231_221207_monthly.12 rosnap 88.73G 4.96G 179 | backups/.snaps/box/20180130_221209_monthly.11 rosnap 91.25G 4.90G 180 | backups/.snaps/box/20180307_154215_monthly.10 rosnap 96.28G 10.72G 181 | backups/.snaps/box/20190120_193004_weekly.3 rosnap 56.45G 4.25G 182 | backups/.snaps/nasroot/20180122_091325_monthly.12 rosnap 34.65G 10.79G 183 | backups/.snaps/nasroot/20180221_092311_monthly.11 rosnap 31.96G 4.98G 184 | backups/.snaps/nasroot/20180323_092734_monthly.10 rosnap 33.69G 7.05G 185 | backups/.snaps/nasroot/20180820_205559_monthly.5 rosnap 31.74G 5.37G 186 | .syncthing-bkp rosnap 40.48G 8.15G 187 | ``` 188 | 189 | ## Get accurate free space amount 190 | 191 | Note: this is fixed with recent versions of `btrfs-progs` (v5.7 onwards), but we'll keep this feature to continue 192 | supporting older releases of `btrfs-progs` if for some reason you're stuck with older versions. 193 | 194 | For RAID5/6 setups, old versions of `btrfs filesystem usage` always display 0 bytes in the *Free (estimated)* section, 195 | and you have no way to know the free space of your filesystem. `btrfs-list` handles this transparently by doing 196 | the calculations needed to report the proper amount of free space, even in RAID5/6 setups. 197 | -------------------------------------------------------------------------------- /btrfs-list: -------------------------------------------------------------------------------- 1 | #! /usr/bin/perl 2 | # vim: et:ts=4:sw=4:sts=4: 3 | # 4 | # SPDX-License-Identifier: GPL-2.0-only 5 | # 6 | # btrfs-list: a wrapper to btrfs-progs to show a nice tree-style overview 7 | # of your btrfs subvolumes and snapshots, a la 'zfs list' 8 | # 9 | # Check for the latest version at: 10 | # https://github.com/speed47/btrfs-list 11 | # git clone https://github.com/speed47/btrfs-list.git 12 | # or wget https://raw.githubusercontent.com/speed47/btrfs-list/master/btrfs-list -O btrfs-list 13 | # or curl -L https://raw.githubusercontent.com/speed47/btrfs-list/master/btrfs-list -o btrfs-list 14 | # 15 | # perltidy -b -csc -iscl -nolc -nbbc -pt=2 -sbt=2 -bt=2 -l=120 -msc=1 btrfs-list 16 | # 17 | # Stephane Lesimple 18 | # 19 | use strict; 20 | use warnings; 21 | use version; 22 | use File::Basename; 23 | use IPC::Open3; 24 | use Symbol 'gensym'; 25 | use Getopt::Long qw{ :config gnu_getopt no_ignore_case }; 26 | use Data::Dumper; 27 | use Term::ANSIColor; 28 | 29 | my $VERSION = "2.4"; 30 | 31 | $Data::Dumper::Sortkeys = 1; 32 | $Data::Dumper::Terse = 1; 33 | use constant KiB => 1024**1; 34 | use constant MiB => 1024**2; 35 | use constant GiB => 1024**3; 36 | use constant TiB => 1024**4; 37 | use constant PiB => 1024**5; 38 | 39 | use constant PARENT_UUID_DF => '*'; 40 | use constant PARENT_UUID_NONE_MAINVOL => '+'; 41 | use constant PARENT_UUID_NONE => '-'; 42 | 43 | use constant FAKE_ID_DF => -1; 44 | use constant FAKE_ID_GHOST => -2; 45 | 46 | sub help { 47 | print <<"EOF"; 48 | Usage: $0 [options] [mountpoint1 [mountpoint2 [...]]] 49 | 50 | If no mountpoints are specified, display info for all btrfs filesystems. 51 | 52 | -h, --help display this message 53 | --debug enable debug output 54 | -q, --quiet silence quota disabled & quota rescan warnings, 55 | repeat to silence all other warnings. 56 | --version display version info 57 | --color WHEN colorize the output; WHEN can be 'never', 58 | 'always', or 'auto' (default is: 59 | colorize if STDOUT is a term) 60 | -n, --no-color synonym of --color=never 61 | --bright use bright colors (better for dark terminals) 62 | -H, --no-header hide header from output 63 | -r, --raw show raw numbers instead of human-readable 64 | --btrfs-binary BIN path to the btrfs binary to use instead of using 65 | the first binary found in the PATH 66 | --ignore-version-check try to continue even if btrfs-progs seems too old 67 | --ignore-root-check try to continue even if we are not root 68 | 69 | -s, --hide-snap hide all snapshots 70 | -S, --snap-only only show snapshots 71 | -d, --deleted show deleted parents of orphaned snapshots 72 | --snap-min-excl SIZE hide snapshots whose exclusively allocated extents 73 | take up less space than SIZE 74 | --snap-max-excl SIZE hide snapshots whose exclusively allocated extents 75 | take up more space than SIZE 76 | -f, --free-space only show free space on the filesystem 77 | -u, --used display used space instead of free space 78 | 79 | -p, --profile PROFILE override data profile detection and consider it 80 | as 'dup', 'single', 'raid0', 'raid1', 81 | 'raid1c3', 'raid1c4', 'raid10', 'raid5' or 82 | 'raid6' for free space calculation 83 | 84 | -a, --show-all show all information for each item 85 | --show-gen show generation of each item 86 | --show-cgen show generation at creation of each item 87 | --show-id show id of each item 88 | --show-parent show parent id of each item 89 | --show-toplevel show top level of each item 90 | --show-uuid show uuid of each item 91 | --show-puuid show parent uuid of each item 92 | --show-ruuid show received uuid of each item 93 | --show-otime show snap creation time 94 | 95 | -w, --wide don't truncate uuids on output (this is the 96 | default if STDOUT is NOT a term) 97 | --no-wide always truncate uuids on output (useful to 98 | override above default) 99 | --max-name-len LEN trim long subvol names to LEN. 0 means never trim. 100 | Defaults to 80 if STDOUT is a term, 0 otherwise. 101 | --indent LEN number of spaces to indent the tree, default: 3. 102 | 103 | SIZE can be a number (in bytes), or a number followed by k, M, G, T or P. 104 | 105 | EOF 106 | exit 0; 107 | } ## end sub help 108 | 109 | GetOptions( 110 | 'debug' => \my $opt_debug, 111 | 'version' => \my $opt_version, 112 | 'ignore-version-check' => \my $opt_ignore_version_check, 113 | 'ignore-root-check' => \my $opt_ignore_root_check, 114 | 'q|quiet+' => \my $opt_quiet, 115 | 's|hide-snap' => \my $opt_hide_snapshots, 116 | 'S|snap-only' => \my $opt_only_snapshots, 117 | 'f|free-space' => \my $opt_free_space, 118 | 'a|show-all' => \my $opt_show_all, 119 | 'H|no-header' => \my $opt_no_header, 120 | 'show-gen' => \my $opt_show_gen, 121 | 'show-cgen' => \my $opt_show_cgen, 122 | 'show-id' => \my $opt_show_id, 123 | 'show-parent' => \my $opt_show_parent, 124 | 'show-toplevel' => \my $opt_show_toplevel, 125 | 'show-uuid' => \my $opt_show_uuid, 126 | 'show-puuid' => \my $opt_show_puuid, 127 | 'show-ruuid' => \my $opt_show_ruuid, 128 | 'show-otime' => \my $opt_show_otime, 129 | 'wide|w' => \my $opt_wide, 130 | 'no-wide' => \my $opt_no_wide, 131 | 'max-name-len=i' => \my $opt_max_name_len, 132 | 'indent=i' => \my $opt_indent, 133 | 'snap-min-used|snap-min-excl=s' => \my $opt_snap_min_used, 134 | 'snap-max-used|snap-max-excl=s' => \my $opt_snap_max_used, 135 | 'n|no-color' => \my $opt_no_color, 136 | 'color=s' => \my $opt_color, 137 | 'bright' => \my $opt_bright, 138 | 'h|help|usage' => \my $opt_help, 139 | 'p|profile=s' => \my $opt_profile, 140 | 'r|raw' => \my $opt_raw, 141 | 'btrfs-binary=s' => \my $opt_btrfs_binary, 142 | 'd|deleted' => \my $opt_deleted, 143 | 'u|used' => \my $opt_used, 144 | ) or die "FATAL: Error parsing arguments, aborting\n"; 145 | 146 | $opt_quiet ||= 0; 147 | 148 | sub debug { 149 | return if !$opt_debug; 150 | print STDERR $_ . "\n" for @_; 151 | return; 152 | } 153 | 154 | sub warning { 155 | my ($level, @lines) = @_; 156 | return if ($level <= $opt_quiet); 157 | print STDERR "WARNING: $_\n" for @lines; 158 | return; 159 | } ## end sub warning 160 | 161 | sub run_cmd { 162 | my %params = @_; 163 | my $cmd = $params{'cmd'}; 164 | my $silent_stderr = $params{'silent_stderr'}; 165 | my $fatal = $params{'fatal'}; 166 | 167 | if ($cmd->[0] eq 'btrfs' && $opt_btrfs_binary) { 168 | $cmd->[0] = $opt_btrfs_binary; 169 | } 170 | 171 | my ($_stdin, $_stdout, $_stderr); 172 | $_stderr = gensym; 173 | debug("about to run_cmd ['" . join("','", @$cmd) . "']"); 174 | my $pid = eval { open3($_stdin, $_stdout, $_stderr, @$cmd); }; 175 | if ($@) { 176 | if ($fatal) { 177 | print STDERR "FATAL: failed to run [" . join(' ', @$cmd) . "] ($@)\n"; 178 | exit 1; 179 | } 180 | return {status => -1, stdout => [], stderr => []}; 181 | } ## end if ($@) 182 | debug("waiting for cmd to complete..."); 183 | my @stdout = (); 184 | my @stderr = (); 185 | while (<$_stdout>) { 186 | chomp; 187 | debug("stdout: " . $_); 188 | /WARNING: (.+)/ and warning(2, "btrfs-progs: $1"); 189 | push @stdout, $_; 190 | } ## end while (<$_stdout>) 191 | while (<$_stderr>) { 192 | chomp; 193 | debug("stderr: " . $_); 194 | /WARNING: (RAID56 detected, not implemented)/ and warning(2, "btrfs-progs: $1"); 195 | if (!$silent_stderr) { 196 | print join(' ', @$cmd) . ": stderr: " . $_ . "\n"; 197 | } 198 | push @stderr, $_; 199 | } ## end while (<$_stderr>) 200 | waitpid($pid, 0); 201 | my $child_exit_status = $? >> 8; 202 | debug("cmd return status is $child_exit_status"); 203 | if ($fatal && $child_exit_status != 0) { 204 | print STDERR "FATAL: the command [" . join(' ', @$cmd) . "] returned a non-zero status ($child_exit_status)\n"; 205 | print STDERR "FATAL: stdout: " . $_ . "\n" for @stdout; 206 | print STDERR "FATAL: stderr: " . $_ . "\n" for @stderr; 207 | exit 1; 208 | } ## end if ($fatal && $child_exit_status...) 209 | return {status => $child_exit_status, stdout => \@stdout, stderr => \@stderr}; 210 | } ## end sub run_cmd 211 | 212 | sub link2real { 213 | my $dev = shift; 214 | CORE::state %readlinkcache; 215 | if (defined $readlinkcache{$dev}) { 216 | return $readlinkcache{$dev}; 217 | } 218 | my $cmd = run_cmd(fatal => 1, cmd => [qw{ readlink -f }, $dev]); 219 | if (defined $cmd->{stdout}->[0]) { 220 | $readlinkcache{$dev} = $cmd->{stdout}->[0]; 221 | return $readlinkcache{$dev}; 222 | } 223 | return $dev; 224 | } ## end sub link2real 225 | 226 | # returns a list with 5 items 227 | # item1: color-code before the number 228 | # item2: the string 229 | # item3: color-code after the number and before the multiplier 230 | # item4: the multiplier (1 char) 231 | # item5: color-code after the multiplier 232 | sub pretty_print { 233 | my ($raw, $mode) = @_; 234 | 235 | =comment 236 | debug("pretty_print(@_);"); 237 | my @c = caller(0); 238 | debug(Dumper(\@c)); 239 | =cut 240 | 241 | if ($opt_raw) { 242 | return ('', $raw, '', '', '') if (!$mode || $raw ne 0); 243 | return ('', '-', '', '', ''); 244 | } 245 | elsif ($mode && ($raw eq '-' || $raw == 0)) { 246 | return ('', '-', '', '', '') if $mode == 1; 247 | return ('', '0', '', '', '') if $mode == 2; 248 | } 249 | 250 | my $bright = ($opt_bright ? 'bright_' : ''); 251 | CORE::state($nbcolors, $dark); 252 | if (!defined $nbcolors) { 253 | # try to use tput if we happen to have it 254 | my $cmd = run_cmd(cmd => [qw{ tput colors }], silent_stderr => 1); 255 | if ($cmd->{'status'} == -1 || !@{$cmd->{stdout}}) { 256 | # we don't have tput, get info from env instead 257 | if ($ENV{'TERM'}) { 258 | if ($ENV{'TERM'} =~ /256/) { 259 | $nbcolors = 256; 260 | } 261 | elsif ($ENV{'TERM'} =~ /dumb/) { 262 | $nbcolors = -1; 263 | } 264 | else { 265 | $nbcolors = 8; 266 | } 267 | } ## end if ($ENV{'TERM'}) 268 | else { 269 | $nbcolors = -1; 270 | } 271 | } ## end if ($cmd->{'status'} ==...) 272 | else { 273 | $nbcolors = $cmd->{stdout}->[0]; 274 | chomp $nbcolors; 275 | } 276 | $nbcolors = 8 if !$nbcolors; 277 | debug("nbcolors=$nbcolors"); 278 | $dark = ($nbcolors <= 8 ? "${bright}black" : 'grey9'); 279 | 280 | # terms that don't support colors (except if --color=always) 281 | $ENV{'ANSI_COLORS_DISABLED'} = 1 if ($nbcolors == -1 && $opt_color ne 'always'); 282 | } ## end if (!defined $nbcolors) 283 | my $r = color('reset'); 284 | if ($raw > PiB) { return (color("${bright}magenta"), sprintf('%.2f', $raw / PiB), color($dark), 'P', $r); } 285 | elsif ($raw > TiB) { return (color("${bright}red"), sprintf('%.2f', $raw / TiB), color($dark), 'T', $r); } 286 | elsif ($raw > GiB) { return (color("${bright}yellow"), sprintf('%.2f', $raw / GiB), color($dark), 'G', $r); } 287 | elsif ($raw > MiB) { return (color("${bright}green"), sprintf('%.2f', $raw / MiB), color($dark), 'M', $r); } 288 | elsif ($raw > KiB) { return (color("${bright}blue"), sprintf('%.2f', $raw / KiB), color($dark), 'k', $r); } 289 | else { return ('', sprintf('%.2f', $raw), '', ' ', ''); } 290 | } ## end sub pretty_print 291 | 292 | sub pretty_print_str { 293 | return sprintf("%s%s%s%s%s", pretty_print(@_)); 294 | } 295 | 296 | sub human2raw { 297 | my $human = shift; 298 | return $human if ($human !~ /^((\d+)(\.\d+)?)([kMGTP])/); 299 | if ($4 eq 'P') { return $1 * PiB; } 300 | elsif ($4 eq 'T') { return $1 * TiB; } 301 | elsif ($4 eq 'G') { return $1 * GiB; } 302 | elsif ($4 eq 'M') { return $1 * MiB; } 303 | elsif ($4 eq 'k') { return $1 * KiB; } 304 | return $human; 305 | } ## end sub human2raw 306 | 307 | sub compute_allocatable_for_profile { 308 | my ($profile, $free, $devBytesRef) = @_; 309 | my $unallocFree = 0; 310 | my $sliceSize = TiB; 311 | my %devBytes = %$devBytesRef; 312 | while (1) { 313 | 314 | # reduce sliceSize if needed, note that btrfs never allocates chunks 315 | # smaller than 1 MiB 316 | if ($sliceSize > MiB && grep { $_ < 3 * $sliceSize } values %devBytes) { 317 | $sliceSize /= 2; 318 | next; 319 | } 320 | 321 | # sort device by remaining free space. 322 | # $sk[0] has the most available space, then $sk[1], etc. 323 | my @sk = sort { $devBytes{$b} <=> $devBytes{$a} } keys %devBytes; 324 | 325 | if ($profile eq 'raid1') { 326 | last if ($devBytes{$sk[1]} <= $sliceSize); # out of space 327 | $unallocFree += $sliceSize; 328 | $devBytes{$sk[0]} -= $sliceSize; 329 | $devBytes{$sk[1]} -= $sliceSize; 330 | } ## end if ($profile eq 'raid1') 331 | elsif ($profile eq 'raid1c3') { 332 | last if ($devBytes{$sk[2]} <= $sliceSize); # out of space 333 | $unallocFree += $sliceSize; 334 | $devBytes{$sk[0]} -= $sliceSize; 335 | $devBytes{$sk[1]} -= $sliceSize; 336 | $devBytes{$sk[2]} -= $sliceSize; 337 | } ## end elsif ($profile eq 'raid1c3') 338 | elsif ($profile eq 'raid1c4') { 339 | last if ($devBytes{$sk[3]} <= $sliceSize); # out of space 340 | $unallocFree += $sliceSize; 341 | $devBytes{$sk[0]} -= $sliceSize; 342 | $devBytes{$sk[1]} -= $sliceSize; 343 | $devBytes{$sk[2]} -= $sliceSize; 344 | $devBytes{$sk[3]} -= $sliceSize; 345 | } ## end elsif ($profile eq 'raid1c4') 346 | elsif ($profile eq 'raid10') { 347 | last if ($devBytes{$sk[3]} <= $sliceSize); # out of space 348 | $unallocFree += $sliceSize * 2; 349 | $devBytes{$sk[0]} -= $sliceSize; 350 | $devBytes{$sk[1]} -= $sliceSize; 351 | $devBytes{$sk[2]} -= $sliceSize; 352 | $devBytes{$sk[3]} -= $sliceSize; 353 | } ## end elsif ($profile eq 'raid10') 354 | elsif ($profile eq 'raid5' || $profile eq 'raid6') { 355 | my $parity = ($profile eq 'raid5' ? 1 : 2); 356 | my $nb = grep { $_ > $sliceSize } values %devBytes; 357 | last if $nb < $parity + 1; # out of space 358 | foreach my $dev (keys %devBytes) { 359 | $devBytes{$dev} -= $sliceSize if $devBytes{$dev} > $sliceSize; 360 | } 361 | $unallocFree += ($nb - $parity) * $sliceSize; 362 | } ## end elsif ($profile eq 'raid5'...) 363 | elsif (grep { $profile eq $_ } qw( raid0 single dup )) { 364 | 365 | # those are easy, we just add up every free space of every device 366 | # and call it a day (no need to loop through the allocator) 367 | $unallocFree += $_ for values %devBytes; 368 | $unallocFree /= 2 if $profile eq 'dup'; 369 | %devBytes = (); 370 | last; 371 | } ## end elsif (grep { $profile eq...}) 372 | else { 373 | print "ERROR: Unknown data profile '$profile'!\n"; 374 | exit 1; 375 | } 376 | } ## end while (1) 377 | $free += $unallocFree; 378 | 379 | # if free is < 1 MiB, then consider it as full to the brim, 380 | # because when FS is completely full, it always shows a couple 381 | # kB left (depending on the profile), even if not a single more 382 | # byte can be written. 383 | $free = 0 if $free < MiB; 384 | 385 | # remaining space on each device is unallocatable, don't count space 386 | # below the MiB for a given device for the same reason as above 387 | my $unallocatable = 0; 388 | foreach (values %devBytes) { 389 | $unallocatable += ($_ - MiB) if $_ > MiB; 390 | } 391 | 392 | return {allocatable => $free, unallocatable => $unallocatable}; 393 | } ## end sub compute_allocatable_for_profile 394 | 395 | # MAIN 396 | 397 | if ($opt_version) { 398 | 399 | # if we were git clone'd, adjust VERSION 400 | my $ver = $VERSION; 401 | my $dir = dirname($0); 402 | if (-d "$dir/.git") { 403 | my $cmd = run_cmd(silent_stderr => 1, cmd => [qw{ git -C }, $dir, qw{ describe --tags --dirty }]); 404 | if ($cmd->{status} == 0 && $cmd->{stdout}) { 405 | $ver = $cmd->{stdout}[0]; 406 | $ver =~ s/^v//; 407 | } 408 | } ## end if (-d "$dir/.git") 409 | 410 | # also get btrfs --version 411 | my $btrfsver; 412 | my $cmd = run_cmd(cmd => [qw{ btrfs --version }]); 413 | if ($cmd->{status} == 0) { 414 | ($btrfsver) = $cmd->{stdout}->[0] =~ /v([0-9.]+)/; 415 | } 416 | 417 | print "btrfs-list v$ver using btrfs v$btrfsver\n"; 418 | exit 0; 419 | } ## end if ($opt_version) 420 | 421 | # check opts 422 | 423 | $opt_color = 'never' if $opt_no_color; 424 | $opt_color //= 'auto'; 425 | 426 | if (defined $opt_snap_min_used) { 427 | $opt_snap_min_used = human2raw($opt_snap_min_used); 428 | } 429 | if (defined $opt_snap_max_used) { 430 | $opt_snap_max_used = human2raw($opt_snap_max_used); 431 | } 432 | 433 | if ($opt_color eq 'never' || ($opt_color eq 'auto' && !-t 1)) { ## no critic(InputOutput::ProhibitInteractiveTest) 434 | $ENV{'ANSI_COLORS_DISABLED'} = 1; 435 | } 436 | if (!$opt_wide && !-t 1) { ## no critic(InputOutput::ProhibitInteractiveTest) 437 | 438 | # wide if STDOUT is NOT a term 439 | $opt_wide = 1; 440 | } 441 | if (defined $opt_no_wide) { 442 | 443 | # --no-wide always wins 444 | $opt_wide = 0; 445 | } 446 | 447 | if (!defined $opt_max_name_len) { 448 | # if STDOUT is a term, set to 80, otherwise 0 (no limit) 449 | $opt_max_name_len = (-t 1 ? 80 : 0); ## no critic(InputOutput::ProhibitInteractiveTest) 450 | } 451 | if ($opt_max_name_len > 0 && $opt_max_name_len < 4) { 452 | $opt_max_name_len = 4; 453 | } 454 | 455 | $opt_indent //= 3; 456 | 457 | if (defined $opt_profile && $opt_profile !~ /^(raid([0156]|1c[34]|10)|single|dup)$/) { 458 | print STDERR "FATAL: invalid argument for --profile\n"; 459 | help(); 460 | exit 1; 461 | } 462 | 463 | if ($opt_show_all) { 464 | $opt_show_gen = 1; 465 | $opt_show_cgen = 1; 466 | $opt_show_id = 1; 467 | $opt_show_parent = 1; 468 | $opt_show_toplevel = 1; 469 | $opt_show_uuid = 1; 470 | $opt_show_puuid = 1; 471 | $opt_show_ruuid = 1; 472 | $opt_show_otime = 1; 473 | } ## end if ($opt_show_all) 474 | 475 | if ($opt_btrfs_binary && !-f -x $opt_btrfs_binary) { 476 | print STDERR "FATAL: Specified btrfs binary '$opt_btrfs_binary' doesn't exist or is not executable\n"; 477 | exit 1; 478 | } 479 | 480 | help() if $opt_help; 481 | 482 | # check btrfs-progs version 483 | 484 | my $cmd = run_cmd(fatal => 1, cmd => [qw{ btrfs --version }]); 485 | my ($version_verbatim, $version) = $cmd->{stdout}->[0] =~ /v((\d+\.\d+)\S*)/; 486 | 487 | if (version->declare($version)->numify lt version->declare("3.18")->numify && !$opt_ignore_version_check) { 488 | print STDERR "FATAL: you're using an old version of btrfs-progs, v$version, " 489 | . "we need at least version 3.18 (Dec 2014).\n"; 490 | print STDERR "If you think this is in error, use --ignore-version-check.\n"; 491 | exit 1; 492 | } ## end if (version->declare($version...)) 493 | 494 | if ($version_verbatim eq '6.1' && !$opt_ignore_version_check) { 495 | warning(2, 496 | "the btrfs-progs version you're using, " . "v$version_verbatim, is known to be missing subvolume uuids."); 497 | } 498 | elsif ($version_verbatim eq '5.15' && !$opt_ignore_version_check) { 499 | warning(2, 500 | "the btrfs-progs version you're using, " . "v$version_verbatim, is known to report free fs space incorrectly."); 501 | } 502 | 503 | if ($< != 0 && !$opt_ignore_root_check) { 504 | print STDERR "FATAL: you must be root to use this command\n"; 505 | print STDERR "If you think this is in error, use --ignore-root-check\n"; 506 | exit 1; 507 | } 508 | 509 | # get moutpoint list, we'll need it several times in the script 510 | my @procmounts; 511 | my %mphash; 512 | open(my $procfd, '<', '/proc/mounts') or die("Couldn't open /proc/mounts: $!"); 513 | while (<$procfd>) { 514 | if (m{^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)}) { 515 | push @procmounts, 516 | { 517 | dev => $1, 518 | mp => $2, 519 | fstype => $3, 520 | options => $4, 521 | }; 522 | $mphash{$2} = 1; 523 | } ## end if (m{^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)}) 524 | } ## end while (<$procfd>) 525 | close($procfd); 526 | 527 | # get passed mountpoints 528 | my @mountPoints = @ARGV; 529 | 530 | # ensure these (if any) are mountpoints 531 | foreach my $mp (@mountPoints) { 532 | # canonicalize 533 | $mp = link2real($mp); 534 | 535 | # if not a mp, find a parent that is 536 | while (1) { 537 | $mp ||= '/'; 538 | my $ismp = 0; 539 | foreach (@procmounts) { 540 | next if ($_->{mp} ne $mp); 541 | $ismp = 1; 542 | last; 543 | } 544 | if (!$ismp) { 545 | next if ($mp =~ s{/[^/]+$}{}); 546 | last; 547 | } 548 | last; 549 | } ## end while (1) 550 | 551 | debug("done, mp is: $mp"); 552 | } ## end foreach my $mp (@mountPoints) 553 | 554 | # get filesystems list 555 | 556 | =comment 557 | # btrfs filesystem show 558 | Label: 'beurre' uuid: 010705d8-430f-4f5b-9315-12df40677e97 559 | Total devices 4 FS bytes used 18.23MiB 560 | devid 1 size 250.00MiB used 176.00MiB path /dev/loop1 561 | devid 2 size 250.00MiB used 164.00MiB path /dev/loop2 562 | devid 3 size 250.00MiB used 164.00MiB path /dev/loop3 563 | devid 4 size 250.00MiB used 164.00MiB path /dev/loop4 564 | =cut 565 | 566 | # if no mountpoints specified, use undef to run a 'btrfs fi show' once with no params (all fs) 567 | @mountPoints = (undef) if !@mountPoints; 568 | 569 | my @fishow = (); 570 | foreach my $mp (@mountPoints) { 571 | $cmd = run_cmd(silent_stderr => 1, cmd => [qw{ btrfs filesystem show --raw }, defined $mp ? $mp : ()]); 572 | if (!@{$cmd->{stdout}} || $cmd->{status}) { 573 | $cmd = run_cmd(fatal => 1, cmd => [qw{ btrfs filesystem show }, defined $mp ? $mp : ()]); 574 | } 575 | push @fishow, @{$cmd->{stdout}} if $cmd->{stdout}; 576 | } ## end foreach my $mp (@mountPoints) 577 | 578 | my ($label, $fuuid, %filesystems); 579 | foreach (@fishow) { 580 | if (/^Label:\s+(\S+)\s+uuid:\s+([0-9a-f-]+)/) { 581 | $label = $1; 582 | $fuuid = $2; 583 | 584 | # btrfs-progs v3.14+ enquote the label 585 | if ($label =~ /^'(.+)'$/) { 586 | $label = $1; 587 | } 588 | if ($label eq 'none') { 589 | 590 | # use the beggining of the uuid instead 591 | $label = substr($2, 0, 8); 592 | } 593 | } ## end if (/^Label:\s+(\S+)\s+uuid:\s+([0-9a-f-]+)/) 594 | if (defined $fuuid and m{devid\s+(\d+)\s+size\s+(\S+).+path\s+(\S+)}) { 595 | my ($devid, $size, $dev) = ($1, human2raw($2), $3); 596 | if (not exists $filesystems{$fuuid}) { 597 | $filesystems{$fuuid} = {uuid => $fuuid, label => $label, devices => [], devinfo => {}}; 598 | } 599 | # btrfs-progs v5.10.1 bug workaround: "dm-X" instead of "/dev/dm-X" 600 | if ($dev && $dev =~ m{^dm-}) { 601 | debug("Applying workaround $dev => /dev/$dev"); 602 | $dev = "/dev/$dev"; 603 | } 604 | if (-l $dev) { 605 | $dev = link2real($dev); 606 | } 607 | push @{$filesystems{$fuuid}{'devices'}}, $dev; 608 | $filesystems{$fuuid}{'devinfo'}{$dev} = { 609 | devid => $devid, 610 | size => $size 611 | }; 612 | } ## end if (defined $fuuid and...) 613 | } ## end foreach (@fishow) 614 | debug("FILESYSTEMS HASH DUMP 1:", Dumper \%filesystems); 615 | 616 | if (!%filesystems) { 617 | print "No btrfs filesystem found.\n"; 618 | exit 0; 619 | } 620 | 621 | # now look for the mountpoints 622 | 623 | my %dev2mp; 624 | my %volid2mp; 625 | foreach my $line (@procmounts) { 626 | 627 | # fix for /dev/mapper/stuff being a sylink to ../dm-xxx 628 | next if $line->{fstype} ne 'btrfs'; 629 | my $subvolid = 0; 630 | ($subvolid) = $line->{options} =~ /subvolid=(\d+)/; 631 | debug(">> mounts item [$line->{dev}] subvolid[$subvolid] mounted on $line->{mp}"); 632 | 633 | # ||=: we might have bind mounts and such, just take the first occurence 634 | if ($line->{options} =~ /subvolid=(\d+)/) { 635 | $dev2mp{$line->{dev}} ||= $line->{mp}; 636 | $volid2mp{$line->{dev}}{$subvolid} ||= $line->{mp}; 637 | } 638 | if (-l $line->{dev}) { 639 | my $real = link2real($line->{dev}); 640 | $dev2mp{$real} ||= $line->{mp}; 641 | $volid2mp{$real}{$subvolid} ||= $line->{mp}; 642 | } 643 | } ## end foreach my $line (@procmounts) 644 | 645 | foreach my $fuuid (keys %filesystems) { 646 | foreach my $dev (@{$filesystems{$fuuid}{'devices'} || []}) { 647 | if (exists $dev2mp{$dev}) { 648 | $filesystems{$fuuid}{'mountpoint'} = $dev2mp{$dev}; 649 | $filesystems{$fuuid}{'volmp'} = $volid2mp{$dev}; 650 | last; 651 | } 652 | } ## end foreach my $dev (@{$filesystems...}) 653 | } ## end foreach my $fuuid (keys %filesystems) 654 | 655 | debug("FILESYSTEMS HASH DUMP 2:", Dumper \%filesystems); 656 | 657 | # now, for each filesystem we found, let's dig: 658 | 659 | my %vol; 660 | foreach my $fuuid (keys %filesystems) { 661 | my $mp = $filesystems{$fuuid}{'mountpoint'}; 662 | defined $mp or next; 663 | -d $mp or next; 664 | 665 | $cmd = run_cmd(silent_stderr => 1, cmd => [qw{ btrfs filesystem usage --raw }, $mp]); 666 | if (!@{$cmd->{stdout}} || $cmd->{status}) { 667 | $cmd = run_cmd(fatal => 1, cmd => [qw{ btrfs filesystem usage }, $mp]); 668 | } 669 | my ($seenUnallocated, %devFree, $profile, $mprofile); 670 | my ($total, $fssize, $used, $freeEstimated) = (0, 0, 0, 0); 671 | foreach (@{$cmd->{stdout}}) { 672 | if (/Device\s+size:\s*(\S+)/) { 673 | $fssize = human2raw($1); 674 | } 675 | elsif (/^Data,([^:]+): Size:([^,]+), Used:(\S+)/) { 676 | 677 | #v3.18: Data,RAID1: Size:9.90TiB, Used:9.61TiB 678 | #v3.19+: Data,RAID1: Size:10881302659072, Used:10569277333504 679 | $profile = lc($1); 680 | $total += human2raw($2); 681 | $used += human2raw($3); 682 | } ## end elsif (/^Data,([^:]+): Size:([^,]+), Used:(\S+)/) 683 | elsif (/^Metadata,([^:]+): Size:([^,]+), Used:(\S+)/) { 684 | $mprofile = lc($1); 685 | } 686 | elsif (/Free\s*\(estimated\)\s*:\s*(\S+)/) { 687 | 688 | #Free (estimated): 405441961984 (min: 405441961984) 689 | #Free (estimated): 377.60GiB (min: 377.60GiB) 690 | $freeEstimated = human2raw($1); 691 | } ## end elsif (/Free\s*\(estimated\)\s*:\s*(\S+)/) 692 | 693 | if (m{^Unallocated:}) { 694 | $seenUnallocated = 1; 695 | } 696 | elsif ($seenUnallocated && m{^\s*(/\S+)\s+(\d+)\s*$}) { 697 | $devFree{$1} = human2raw($2) + 0; 698 | } 699 | } ## end foreach (@{$cmd->{stdout}}) 700 | 701 | $vol{$fuuid}{df} = { 702 | id => FAKE_ID_DF, 703 | path => $filesystems{$fuuid}{label}, 704 | gen => 0, 705 | cgen => 0, 706 | parent => '-', 707 | top => '-', # top_level 708 | uuid => $fuuid, 709 | puuid => PARENT_UUID_DF, # parent_uuid 710 | ruuid => '-', # received_uuid 711 | type => 'fs', 712 | mode => 'rw', 713 | rfer => '-', 714 | excl => $used, 715 | free => $total - $used, 716 | fssize => $fssize, 717 | }; 718 | debug( "df for $fuuid (" 719 | . $filesystems{$fuuid}{label} 720 | . "), excl=$used, free=" 721 | . ($total - $used) 722 | . ", fssize=$fssize"); 723 | 724 | # cmdline override 725 | $profile = $opt_profile if defined $opt_profile; 726 | 727 | if (!$profile) { 728 | warning(2, "No profile found, assuming single"); 729 | $profile = "single"; 730 | } 731 | 732 | $vol{$fuuid}{df}{profile} = $profile; 733 | $vol{$fuuid}{df}{mprofile} = $mprofile; 734 | 735 | my $computed = compute_allocatable_for_profile($profile, $vol{$fuuid}{df}{free}, \%devFree); 736 | $vol{$fuuid}{df}{free} = $computed->{allocatable}; 737 | $vol{$fuuid}{df}{unallocatable} = $computed->{unallocatable}; 738 | 739 | # also compute total allocatable size if FS fs empty 740 | my %devSize; 741 | foreach my $dev (@{$filesystems{$fuuid}{devices}}) { 742 | $devSize{$dev} = $filesystems{$fuuid}{devinfo}{$dev}{size}; 743 | } 744 | $computed = compute_allocatable_for_profile($profile, 0, \%devSize); 745 | $vol{$fuuid}{df}{fssize} = $computed->{allocatable}; 746 | 747 | next if $opt_free_space; 748 | 749 | # cvol btrfs sub list 750 | $cmd = run_cmd(silent_stderr => 1, cmd => [qw{ btrfs subvolume list -pacguq }, $mp]); 751 | 752 | # ID 3332 gen 81668 cgen 2039 parent 0 top level 0 parent_uuid 9faf..17d4 uuid 20b7..5b61 path /DELETED 753 | # ID 1911 gen 81668 cgen 929 parent 5 top level 5 parent_uuid - uuid aec0705e-6cae-a941-854c-d95e0a36ba2c path main 754 | foreach (@{$cmd->{stdout}}) { 755 | my $vuuid = undef; 756 | if (/(\s|^)uuid ([0-9a-f-]+)/) { 757 | $vuuid = $2; 758 | if ($vuuid eq '-') { 759 | 760 | # old btrfs kernel, recent btrfsprogs 761 | m{ID (\d+)} and $vuuid = $1; 762 | } 763 | $vol{$fuuid}{$vuuid}{uuid} = $vuuid; 764 | } ## end if (/(\s|^)uuid ([0-9a-f-]+)/) 765 | elsif (/(\s|^)ID (\d+)/) { 766 | 767 | # old btrfsprogs 768 | $vuuid = $2; 769 | $vol{$fuuid}{$vuuid}{uuid} = $vuuid; 770 | } ## end elsif (/(\s|^)ID (\d+)/) 771 | else { 772 | next; 773 | } 774 | 775 | # ID 257 gen 17 cgen 11 parent 5 top level 5 parent_uuid - received_uuid - uuid 9bc4..fd75 path sub1 with spaces 776 | $vol{$fuuid}{$vuuid}{puuid} = PARENT_UUID_NONE; # old btrfsprogs don't have puuid, set a sane default 777 | /(\s|^)ID (\d+)/ and $vol{$fuuid}{$vuuid}{id} = $2; 778 | /(\s|^)gen (\d+)/ and $vol{$fuuid}{$vuuid}{gen} = $2; 779 | /(\s|^)cgen (\d+)/ and $vol{$fuuid}{$vuuid}{cgen} = $2; 780 | /(\s|^)parent (\d+)/ and $vol{$fuuid}{$vuuid}{parent} = $2; 781 | /(\s|^)top level (\d+)/ and $vol{$fuuid}{$vuuid}{top} = $2; 782 | /(\s|^)parent_uuid (\S+)/ and $vol{$fuuid}{$vuuid}{puuid} = $2; 783 | /(\s|^)received_uuid (\S+)/ and $vol{$fuuid}{$vuuid}{ruuid} = $2; 784 | /(\s|^)path (.+)/ and $vol{$fuuid}{$vuuid}{path} = $2; 785 | $vol{$fuuid}{$vuuid}{path} =~ s/^\///; 786 | $vol{$fuuid}{$vuuid}{type} = 'subvol'; # by default, will be overriden below if applicable 787 | $vol{$fuuid}{$vuuid}{mode} = 'rw'; # by default, will be overriden below if applicable 788 | $vol{$fuuid}{$vuuid}{rfer} = 0; 789 | $vol{$fuuid}{$vuuid}{excl} = 0; 790 | $vol{$fuuid}{$vuuid}{mp} = $filesystems{$fuuid}{volmp}{$vol{$fuuid}{$vuuid}{id}}; 791 | } ## end foreach (@{$cmd->{stdout}}) 792 | 793 | # now, list only snapshots, we also get their otime for free 794 | $cmd = run_cmd(cmd => [qw{ btrfs subvolume list -us }, $mp]); 795 | 796 | # ID 694 gen 30002591 cgen 30002589 top level 5 otime 2022-01-02 14:37:14 path test backup2 with spaces 797 | foreach (@{$cmd->{stdout}}) { 798 | my ($found, $otime); 799 | /(\s|^)uuid ([0-9a-f-]+)/ and exists $vol{$fuuid}{$2} and $found = $2; 800 | /(\s|^)ID ([0-9]+)/ and exists $vol{$fuuid}{$2} and $found = $2; 801 | /(\s|^)otime (\S+ \S+)/ and $otime = $2; 802 | if (defined $found) { 803 | if ($opt_hide_snapshots) { 804 | delete $vol{$fuuid}{$found}; 805 | } 806 | else { 807 | $vol{$fuuid}{$found}{type} = 'snap'; 808 | $vol{$fuuid}{$found}{otime} = $otime if $otime; 809 | } 810 | } ## end if (defined $found) 811 | } ## end foreach (@{$cmd->{stdout}}) 812 | 813 | # then, list readonly snapshots 814 | $cmd = run_cmd(cmd => [qw{ btrfs subvolume list -ur }, $mp]); 815 | foreach (@{$cmd->{stdout}}) { 816 | /(\s|^)uuid ([0-9a-f-]+)/ and exists $vol{$fuuid}{$2} and $vol{$fuuid}{$2}{mode} = 'ro'; 817 | /(\s|^)ID ([0-9]+)/ and exists $vol{$fuuid}{$2} and $vol{$fuuid}{$2}{mode} = 'ro'; 818 | } 819 | debug("VOL{FUUID=$fuuid} DUMP:", Dumper \$vol{$fuuid}); 820 | } ## end foreach my $fuuid (keys %filesystems) 821 | 822 | # get quota stuff 823 | 824 | # v3.18 (no --raw) 825 | 826 | =comment 827 | WARNING: Qgroup data inconsistent, rescan recommended 828 | qgroupid rfer excl max_rfer max_excl parent child 829 | -------- ---- ---- -------- -------- ------ ----- 830 | 0/5 7.99MiB 7.99MiB 0.00B 0.00B --- --- 831 | 0/257 10.02MiB 10.01MiB 0.00B 0.00B --- --- 832 | =cut 833 | 834 | # v3.19+ has --raw, and additionally, since v4.1, we get 'none' instead of 0: 835 | 836 | =comment 837 | qgroupid rfer excl max_rfer max_excl parent child 838 | -------- ---- ---- -------- -------- ------ ----- 839 | 0/5 9848498 8015121 none none --- --- 840 | 0/257 10213513 10131212 none none --- --- 841 | =cut 842 | 843 | foreach my $fuuid (keys %filesystems) { 844 | my $mp = $filesystems{$fuuid}{'mountpoint'}; 845 | defined $mp or next; 846 | -d $mp or next; 847 | next if $opt_free_space; 848 | 849 | # let's still fill the info for the main volume 850 | $vol{$fuuid}{5} = { 851 | id => 5, 852 | path => "[main]", 853 | gen => 0, 854 | cgen => 0, 855 | parent => '-', 856 | top => '-', 857 | uuid => '-', # may be filled below 858 | puuid => PARENT_UUID_NONE_MAINVOL, 859 | ruuid => '-', 860 | type => 'mainvol', 861 | mode => 'rw', 862 | mp => $mp, 863 | }; 864 | 865 | # grab the uuid of the main volume, note that sometimes there is none, and UUID is reported as '-' 866 | # also get the current geenration, and the gen at creation (which should always be 0) 867 | $cmd = run_cmd(silent_stderr => 1, cmd => [qw{ btrfs subvolume show -b }, $mp]); 868 | foreach (@{$cmd->{stdout}}) { 869 | /^\s*UUID:\s*([0-9a-f-]+)/ and $vol{$fuuid}{5}{uuid} = $1; 870 | /Generation:\s*(\d+)/ and $vol{$fuuid}{5}{gen} = $1; 871 | /Gen at creation:\s*(\d+)/ and $vol{$fuuid}{5}{cgen} = $1; 872 | } 873 | 874 | $cmd = run_cmd(silent_stderr => 1, cmd => [qw{ btrfs quota rescan -s }, $mp]); 875 | if ($cmd->{stdout}->[0] && $cmd->{stdout}->[0] =~ /operation running|current key/) { 876 | warning(1, "a quota rescan is running, size information is not correct yet"); 877 | } 878 | 879 | $cmd = run_cmd(silent_stderr => 1, cmd => [qw{ btrfs qgroup show -pcre --raw }, $mp]); 880 | if ($cmd->{status} || !@{$cmd->{stdout}}) { 881 | 882 | # btrfs-progs v3.18 doesn't support --raw 883 | $cmd = run_cmd(silent_stderr => 1, cmd => [qw{ btrfs qgroup show -pcre }, $mp]); 884 | if ($cmd->{status} || !@{$cmd->{stdout}}) { 885 | warning(1, "to get refer/excl size information, please enable qgroups (btrfs quota enable $mp)"); 886 | $vol{$fuuid}{df}{noquota} = 1; 887 | } 888 | } ## end if ($cmd->{status} || ...) 889 | 890 | foreach (@{$cmd->{stdout}}) { 891 | if (m{^(\d+)/(\d+)\s+(\S+)\s+(\S+)}) { 892 | my ($qid, $id, $rfer, $excl) = ($1, $2, human2raw($3), human2raw($4)); 893 | next if $qid != 0; # only check level 0 qgroups (leafs) 894 | if ($id < 256) { 895 | if (not exists $vol{$fuuid}{$id}) { 896 | $vol{$fuuid}{$id} = { 897 | id => $id, 898 | path => "[main]", 899 | gen => 0, 900 | cgen => 0, 901 | parent => '-', 902 | top => '-', 903 | puuid => PARENT_UUID_NONE_MAINVOL, 904 | ruuid => '-', 905 | type => 'mainvol', 906 | mode => 'rw', 907 | mp => $filesystems{$fuuid}{volmp}{5}, 908 | }; 909 | } ## end if (not exists $vol{$fuuid...}) 910 | $vol{$fuuid}{$id}{rfer} = $rfer; 911 | $vol{$fuuid}{$id}{excl} = $excl; 912 | next; 913 | } ## end if ($id < 256) 914 | foreach my $vuuid (keys %{$vol{$fuuid}}) { 915 | if ($id eq $vol{$fuuid}{$vuuid}{id}) { 916 | $vol{$fuuid}{$vuuid}{rfer} = $rfer; 917 | $vol{$fuuid}{$vuuid}{excl} = $excl; 918 | last; 919 | } 920 | } ## end foreach my $vuuid (keys %{$vol...}) 921 | } ## end if (m{^(\d+)/(\d+)\s+(\S+)\s+(\S+)}) 922 | } ## end foreach (@{$cmd->{stdout}}) 923 | } ## end foreach my $fuuid (keys %filesystems) 924 | debug("VOL HASH DUMP (filesystem uuid - volume uuid - data):", Dumper \%vol); 925 | 926 | # ok, now, do the magic 927 | 928 | my @ordered = (); 929 | my $maxdepth = 0; 930 | my %seen; 931 | 932 | sub recursive_add_children_of { 933 | my %params = @_; 934 | my $volumes = $params{'volumes'}; 935 | my $depth = $params{'depth'}; 936 | my $parentuuid = $params{'parentuuid'}; 937 | 938 | $depth > $maxdepth and $maxdepth = $depth; 939 | 940 | foreach my $vuuid (sort { $volumes->{$a}{id} <=> $volumes->{$b}{id} } keys %$volumes) { 941 | next if $seen{$vuuid}; # not needed, but just in case 942 | my $vol = $volumes->{$vuuid}; 943 | debug( "..." x ($depth) 944 | . "parent_uuid=$parentuuid, currently working on id " 945 | . $vol->{id} 946 | . " volume_uuid=$vuuid having parent_uuid=" 947 | . $vol->{puuid} 948 | . " and path-type " 949 | . $vol->{path} . "-" 950 | . $vol->{type}); 951 | if ($parentuuid eq $vol->{puuid}) { 952 | $vol->{depth} = $depth; 953 | push @ordered, $vol; 954 | debug("..." x ($depth) . "^^^"); 955 | $seen{$vuuid} = 1; 956 | recursive_add_children_of(volumes => $volumes, depth => $depth + 1, parentuuid => $vuuid); # unless $parentuuid eq '-'; 957 | } ## end if ($parentuuid eq $vol...) 958 | } ## end foreach my $vuuid (sort { $volumes...}) 959 | return; 960 | } ## end sub recursive_add_children_of 961 | 962 | my @orderedAll; 963 | $opt_deleted ||= 0; 964 | foreach my $fuuid (sort keys %filesystems) { 965 | @ordered = (); 966 | %seen = (); 967 | $maxdepth = 0; 968 | my @orphans = (); 969 | 970 | # first, we want the so-called "df" line, which conveniently has a fake specific parent_uuid 971 | debug(">>> order df"); 972 | recursive_add_children_of(volumes => $vol{$fuuid}, depth => 0, parentuuid => PARENT_UUID_DF); 973 | 974 | # then, the builtin main volume (id=5) and all its descendants 975 | debug(">>> order mainvol"); 976 | recursive_add_children_of(volumes => $vol{$fuuid}, depth => 1, parentuuid => PARENT_UUID_NONE_MAINVOL); 977 | 978 | # then, all the other top-level volumes (i.e. that have no parent uuid) 979 | debug(">>> order top level vols"); 980 | recursive_add_children_of(volumes => $vol{$fuuid}, depth => 1, parentuuid => PARENT_UUID_NONE); 981 | 982 | next if !@ordered; 983 | 984 | # then, we might still have unseen volumes, which are orphans (they have a parent_uuid) 985 | # but the parent_uuid no longer exists). get all those in a hash 986 | 987 | ORPHANS: foreach my $vuuid (keys %{$vol{$fuuid}}) { 988 | next if $seen{$vuuid}; 989 | push @orphans, $vuuid; 990 | } 991 | 992 | # those orphans might however have parents/children between themselves, 993 | # so find the first one that has no known parent among the other orphans 994 | foreach my $orphan (sort @orphans) { 995 | my $no_known_parent = 1; 996 | foreach my $potential_parent (@orphans) { 997 | next if $orphan eq $potential_parent; # skip myself 998 | $no_known_parent = 0 if ($potential_parent eq $vol{$fuuid}{$orphan}{puuid}); 999 | } 1000 | debug(">>> orphan loop on $orphan, no known parent: $no_known_parent"); 1001 | if ($no_known_parent == 1) { 1002 | if ($opt_deleted) { 1003 | my $parent_uuid = $vol{$fuuid}{$orphan}{puuid}; 1004 | 1005 | # craft a ghost parent if asked to 1006 | my $ghost = { 1007 | id => FAKE_ID_GHOST, 1008 | type => 'deleted', 1009 | path => "(deleted)", 1010 | uuid => $parent_uuid, 1011 | depth => 1, 1012 | }; 1013 | push @ordered, $ghost; 1014 | $seen{$parent_uuid} = 1; 1015 | debug(">>> added ghost parent $parent_uuid"); 1016 | 1017 | # and all the ghost' children, if any 1018 | debug(">>> adding children of ghost parent $parent_uuid (we should have at least $orphan)"); 1019 | recursive_add_children_of(volumes => $vol{$fuuid}, depth => 2, parentuuid => $parent_uuid); 1020 | } ## end if ($opt_deleted) 1021 | else { 1022 | 1023 | # add the orphan ourselves 1024 | push @ordered, $vol{$fuuid}{$orphan}; 1025 | $seen{$orphan} = 1; 1026 | $vol{$fuuid}{$orphan}{depth} = 1; 1027 | 1028 | # and all the orphans' children, if any 1029 | debug(">>> adding children of orphan $orphan"); 1030 | recursive_add_children_of(volumes => $vol{$fuuid}, depth => 2, parentuuid => $orphan); 1031 | } ## end else [ if ($opt_deleted) ] 1032 | } ## end if ($no_known_parent ==...) 1033 | 1034 | if ($opt_deleted) { 1035 | 1036 | # we have added a new ghost parent, so other orphans might no longer 1037 | # actually be orphans, start again above 1038 | @orphans = (); 1039 | goto ORPHANS; 1040 | } ## end if ($opt_deleted) 1041 | } ## end foreach my $orphan (sort @orphans) 1042 | 1043 | # do we still have unseen volumes? (we shouldn't) 1044 | foreach my $vuuid (keys %{$vol{$fuuid}}) { 1045 | next if $seen{$vuuid}; 1046 | warning(2, "we shouldn't have orphaned volumne $vuuid"); 1047 | push @ordered, $vuuid; 1048 | } 1049 | 1050 | push @orderedAll, @ordered; 1051 | } ## end foreach my $fuuid (sort keys...) 1052 | 1053 | # this sub returns the length of the longest item of a column or @sortedAll 1054 | # and pushes the header name to @headers 1055 | my @header; 1056 | 1057 | sub longest { 1058 | my $headerName = shift; 1059 | my $useDepth = shift; # whether 'depth' should be taken into account 1060 | my $key = shift; 1061 | 1062 | # ensure the header name always fits 1063 | my $longest = ($opt_no_header ? 1 : length($headerName)); 1064 | push @header, $headerName; 1065 | 1066 | # loop through all the items 1067 | foreach my $item (@orderedAll) { 1068 | my $len = ($useDepth ? (($item->{depth} || 0) * $opt_indent) : 0); 1069 | $len += length($item->{$key} || ''); 1070 | $longest = $len if $len > $longest; 1071 | } 1072 | 1073 | return $longest; 1074 | } ## end sub longest 1075 | 1076 | # find the longest path (including leading spaces) 1077 | # note that longest() also pushes the header to @headers 1078 | my $format; 1079 | 1080 | # special case for path: if opt_max_name_len is specified, 1081 | # and it is shorter that longest('NAME', 1, 'path'), use it 1082 | # instead 1083 | my $formatpathlen = longest('NAME', 1, 'path'); 1084 | if ($opt_max_name_len > 0 && $opt_max_name_len < $formatpathlen) { 1085 | $format = "%-" . $opt_max_name_len . "s "; 1086 | } 1087 | else { 1088 | $format = "%-" . $formatpathlen . "s "; 1089 | } 1090 | 1091 | if ($opt_show_id) { 1092 | $format .= "%" . longest('ID', 0, 'id') . "s "; 1093 | } 1094 | if ($opt_show_parent) { 1095 | $format .= "%" . longest('PARENT', 0, 'parent') . "s "; 1096 | } 1097 | if ($opt_show_toplevel) { 1098 | $format .= "%" . longest('TOPLVL', 0, 'top') . "s "; 1099 | } 1100 | if ($opt_show_gen) { 1101 | $format .= "%" . longest('GEN', 0, 'gen') . "s "; 1102 | } 1103 | if ($opt_show_cgen) { 1104 | $format .= "%" . longest('CGEN', 0, 'cgen') . "s "; 1105 | } 1106 | my $uuid_len = ($opt_wide ? 36 : 10); 1107 | if ($opt_show_uuid) { 1108 | $format .= "%${uuid_len}s "; 1109 | push @header, qw{ UUID }; 1110 | } 1111 | if ($opt_show_puuid) { 1112 | $format .= "%${uuid_len}s "; 1113 | push @header, qw{ PARENTUUID }; 1114 | } 1115 | if ($opt_show_ruuid) { 1116 | $format .= "%${uuid_len}s "; 1117 | push @header, qw{ RCVD_UUID }; 1118 | } 1119 | if ($opt_show_otime) { 1120 | $format .= "%20s "; 1121 | push @header, qw{ OTIME }; 1122 | } 1123 | $format .= "%" . longest('TYPE', 0, 'type') . "s "; 1124 | 1125 | my $pretty_print_size = ($opt_raw ? 16 : 7); 1126 | my $noquota = $vol{$fuuid}{df}{noquota} || $opt_free_space; 1127 | if (!$noquota) { 1128 | $format .= "%s%${pretty_print_size}s%s%1s%s "; 1129 | push @header, '', 'REFE', 'R', '', ''; 1130 | push @header, '', 'EXCL', '', '', '', 'MOUNTPOINT'; 1131 | } 1132 | else { 1133 | push @header, '', 'EXC', 'L', '', '', 'MOUNTPOINT'; 1134 | } 1135 | 1136 | $format .= "%s%${pretty_print_size}s%s%1s%s %s\n"; 1137 | 1138 | printf $format, @header if !$opt_no_header; 1139 | 1140 | foreach my $line (@orderedAll) { 1141 | next if ($opt_hide_snapshots and $line->{type} eq 'snap'); 1142 | next if ($opt_only_snapshots and $line->{type} ne 'snap'); 1143 | $line->{rfer} ||= 0; 1144 | $line->{excl} ||= 0; 1145 | my $type = $line->{type}; 1146 | if ($opt_snap_min_used) { 1147 | next if ($type eq 'snap' && $line->{rfer} =~ /^\d+$/ && $line->{excl} < $opt_snap_min_used); 1148 | } 1149 | if ($opt_snap_max_used) { 1150 | next if ($type eq 'snap' && $line->{rfer} =~ /^\d+$/ && $line->{excl} > $opt_snap_max_used); 1151 | } 1152 | $type = "ro$type" if ($line->{mode} && $line->{mode} eq 'ro'); 1153 | my $extra = ''; 1154 | if (exists $line->{free}) { 1155 | my $displayProfile = $line->{profile}; 1156 | $displayProfile .= "/" . $line->{mprofile} if ($line->{profile} ne $line->{mprofile}); 1157 | if (!$opt_used) { 1158 | $extra = sprintf( 1159 | "(%s, %s/%s free, %.02f%%", 1160 | $displayProfile, 1161 | pretty_print_str($line->{free}, 2), 1162 | pretty_print_str($line->{fssize}, 2), 1163 | $line->{free} * 100 / $line->{fssize} 1164 | ); 1165 | } ## end if (!$opt_used) 1166 | else { 1167 | my $used = $line->{fssize} - $line->{free}; 1168 | $extra = sprintf( 1169 | "(%s, %s/%s used, %.02f%%", 1170 | $displayProfile, 1171 | pretty_print_str($used, 2), 1172 | pretty_print_str($line->{fssize}, 2), 1173 | $used * 100 / $line->{fssize} 1174 | ); 1175 | } ## end else [ if (!$opt_used) ] 1176 | if ($line->{unallocatable} && $line->{unallocatable} > MiB) { 1177 | $extra .= sprintf(', %s unallocatable', pretty_print_str($line->{unallocatable}, 2)); 1178 | } 1179 | $extra .= ')'; 1180 | } ## end if (exists $line->{free...}) 1181 | elsif (defined $line->{mp}) { 1182 | $extra = $line->{mp}; 1183 | } 1184 | $line->{depth} ||= 0; 1185 | $line->{id} ||= 0; 1186 | 1187 | if (!$opt_wide) { 1188 | foreach my $key (qw{ uuid puuid ruuid }) { 1189 | next if !$line->{$key}; 1190 | if ($line->{$key} =~ m{^(....).+(....)$}) { 1191 | $line->{$key} = "$1..$2"; 1192 | } 1193 | } ## end foreach my $key (qw{ uuid puuid ruuid }) 1194 | } ## end if (!$opt_wide) 1195 | 1196 | # replace our internal id==-1 by - 1197 | $line->{id} =~ /^\d+$/ or $line->{id} = '-'; 1198 | 1199 | # replace our internal '*' and '+' by '-' 1200 | $line->{puuid} = '-' if ($line->{'puuid'} && length($line->{puuid}) == 1); 1201 | 1202 | # shorten path if --max-name-len is specified 1203 | my $pathprefix = " " x ($line->{depth} * $opt_indent); 1204 | my $pathdisplay = $line->{path}; 1205 | if ($opt_max_name_len > 0 && length($line->{path}) > $opt_max_name_len) { 1206 | my $remaininglen = $opt_max_name_len - length($pathprefix) - 4; 1207 | 1208 | # if we exceed the available space just with the indentation, fallback 1209 | # to limit the indentation and just display '[..]' for the subvol name 1210 | if ($remaininglen <= 4) { 1211 | $pathprefix = " " x ($opt_max_name_len - 4); 1212 | $pathdisplay = '[..]'; 1213 | } 1214 | else { 1215 | $pathdisplay = substr($line->{'path'}, 0, $remaininglen / 2) . '[..]' 1216 | . substr($line->{'path'}, length($line->{'path'}) - $remaininglen / 2); 1217 | } 1218 | } ## end if ($opt_max_name_len ...) 1219 | 1220 | my @fields = $pathprefix . $pathdisplay; 1221 | push @fields, $line->{id} || '-' if $opt_show_id; 1222 | push @fields, $line->{parent} || '-' if $opt_show_parent; 1223 | push @fields, $line->{top} || '-' if $opt_show_toplevel; 1224 | push @fields, $line->{gen} || '-' if $opt_show_gen; 1225 | push @fields, $line->{cgen} || '-' if $opt_show_cgen; 1226 | push @fields, $line->{uuid} || '-' if $opt_show_uuid; 1227 | push @fields, $line->{puuid} || '-' if $opt_show_puuid; 1228 | push @fields, $line->{ruuid} || '-' if $opt_show_ruuid; 1229 | push @fields, $line->{otime} || '-' if $opt_show_otime; 1230 | push @fields, $type; 1231 | push @fields, pretty_print($line->{rfer}, 1) if !$noquota; 1232 | push @fields, pretty_print($line->{excl}, 1), $extra; 1233 | printf $format, @fields; 1234 | } ## end foreach my $line (@orderedAll) 1235 | --------------------------------------------------------------------------------