Skip to content

Instantly share code, notes, and snippets.

@stecman
Last active December 26, 2017 20:47
Show Gist options
  • Save stecman/3fd04a36111874f67c484c74e15ef311 to your computer and use it in GitHub Desktop.
Save stecman/3fd04a36111874f67c484c74e15ef311 to your computer and use it in GitHub Desktop.
List BTRFS subvolume and snapshot sizes like df
#!/usr/bin/env python3
# List BTRFS subvolume space use information similar to df -h (with snapshot paths)
#
# Btrfsprogs is able to list and sort snapshots on a volume, but it only prints their
# id, not their path. This script wraps `btrfs qgroup show` to add filesystem paths
# to the generated table.
#
# For this to work on a BTRFS volume, you first need to enable quotas on the volume:
#
# btrfs quota enable /mnt/some-volume
#
# Note that the current version of this script does not allow sorting by path, as it
# passes all arugments through to btrfsprogs. If you need that and don't mind being
# limited to only sorting by path, see this previous version:
#
# https://gist.github.com/stecman/3fd04a36111874f67c484c74e15ef311/6690edbd6a88380a1712024bb4115969b2545509
#
# This is based on a shell script that was too slow:
# https://github.com/agronick/btrfs-size
from __future__ import print_function
import subprocess
import sys
import os
import re
def get_btrfs_subvols(path):
"""Return a dictionary of subvolume names indexed by their subvolume ID"""
try:
raw = subprocess.check_output(["btrfs", "subvolume", "list", path])
volumes = re.findall(r'^ID (\d+) .* path (.*)$', raw.decode("utf8"), re.MULTILINE)
return dict(volumes)
except subprocess.CalledProcessError as e:
if e.returncode != 0:
print("\nFailed to list subvolumes")
print("Is '%s' really a BTRFS volume?" % path)
sys.exit(1)
def get_data_raw(args):
"""Return lines of output from a call to 'btrfs qgroup show' with args appended"""
try:
# Get the lines of output, ignoring the two header lines
raw = subprocess.check_output(["btrfs", "qgroup", "show"] + args)
return raw.decode("utf8").split("\n")
except subprocess.CalledProcessError as e:
if e.returncode != 0:
print("\nFailed to get subvolume quotas. Have you enabled quotas on this volume?")
print("(You can do so with: sudo btrfs quota enable <path-to-volume>)")
sys.exit(1)
def get_qgroup_id(line):
"""Extract qgroup id from a line of btrfs qgroup show output
Returns None if the line wasn't valid
"""
id_match = re.match(r"\d+/(\d+)", line)
if not id_match:
return None
return id_match.group(1)
def guess_path_argument(argv):
"""Return an argument most likely to be the <path> arg for 'btrfs qgroup show'
This is a cheap way to pass through to btrfsprogs without duplicating the options here.
Currently only easier than duplication because the option/argument list is simple.
"""
# Path can't be the first argument (program)
args = argv[1:]
# Filter out arguments to options
# Only the sort option currently takes an argument
option_follows = [
"--sort"
]
for text in option_follows:
try:
position = args.index(text)
del args[position + 1]
except:
pass
# Ignore options
args = [arg for arg in args if re.match(r"^-", arg) is None]
# Prefer the item at the end of the list as this is the suggested argument order
return args[-1]
# Re-run the script as root if started with a non-priveleged account
if os.getuid() != 0:
cmd = 'sudo "' + '" "'.join(sys.argv) + '"'
sys.exit(subprocess.call(cmd, shell=True))
# Fetch command output to work with
output = get_data_raw(sys.argv[1:])
subvols = get_btrfs_subvols(guess_path_argument(sys.argv))
# Data for the new column
path_column = [
"path",
"----"
]
# Iterate through all lines except for the table header
for index,line in enumerate(output):
# Ignore header rows
if index < 1:
continue
groupid = get_qgroup_id(line)
if groupid in subvols:
path_column.append(subvols[groupid])
else:
path_column.append("")
# Find the required width for the new column
column_width = len(max(path_column, key=len)) + 2
# Output data with extra column for path
for index,line in enumerate(output):
print(path_column[index].ljust(column_width) + output[index])
@MurzNN
Copy link

MurzNN commented Jul 3, 2017

Thanks for so useful script! Will be good to add options for order list via Total size, Exclusive size, and option to force fixed units (for example, Gigabytes). And maybe better to provide full GitHub project (with Issues page) instead of gist page?

@MurzNN
Copy link

MurzNN commented Jul 3, 2017

Seems that arguments already exists:
-s {exclusive,total} Sort output instead of using the order from btrfs
But seems there are bug:
in line 29 we see "total" argument, but in 112 - "bytes".

@stecman
Copy link
Author

stecman commented Jul 3, 2017

@MurzNN thanks, I'll update this to pass through arguments to btrfsprogs and only handle the mapping of volume ids to paths. Must have missed those options when I was looking for a solution to this originally, but it looks like --sort has been in there for a while.

@stecman
Copy link
Author

stecman commented Jul 3, 2017

@MurzNN - updated to pass through to btrfs qgroup show directly, without reinterpreting arguments. This makes the script simpler at the cost of not being able to sort by the path column. The sorting options in btrfsprogs are fairly customisable though:

    --sort=qgroupid,rfer,excl,max_rfer,max_excl
                   list qgroups sorted by specified items
                   you can use '+' or '-' in front of each item.
                   (+:ascending, -:descending, ascending default)

Sorting by path can still be achieved by piping to | sort -k1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment