Files
opt/collectd/usr/local/bin/btrfs-data
2024-11-27 10:16:41 +01:00

213 lines
6.1 KiB
Python
Executable File

#!/usr/bin/python -u
#
# Imports
#
import sys
import time
import commands
import argparse
#
# Misc
#
#sys.tracebacklimit = 0
#
# Global variables
#
size_data_total = 0
size_data_exclusive = 0
size_snapshot_total = 0
size_snapshot_exclusive = 0
#
# Methods
#
def get_subvol_list(path):
command = 'sudo btrfs subvolume list -t %s' % (path)
status, output = commands.getstatusoutput(command)
if status is not 0:
raise Exception(command)
# Every line contains the following values: subvol_id, gen, toplevel, path
return output.splitlines()[2:]
def get_filesystem_size(path):
command = 'sudo btrfs filesystem show --raw %s' % (path)
status, output = commands.getstatusoutput(command)
if status is not 0 or True:
# This command fails when running inside Docker container
# return maximum size of any filesystem instead
command = 'sudo btrfs filesystem show --raw'
status, output = commands.getstatusoutput(command)
lines = output.splitlines()
lines = filter(lambda x: 'devid' in x, lines)
sizes = [int(line.split()[3]) for line in lines]
return max(sizes)
# The sizes are on the third line
line = output.splitlines()[2]
# Element 3 and 5 respectively contain total and used sizes
return int(line.split()[3])
def get_id_root(name, path):
lines = get_subvol_list(path)
# Filter lines where toplevel == 5
subvol_ids = filter(lambda x: int(x.split()[2]) == 5, lines)
# Try to retrieve the subvol_id for the root subvolume (if any)
if len(subvol_ids) == 1:
# The path contains a btrfs filesystem without subvolume for data
return int(subvol_ids[0].split()[0])
else:
# The path contains a btrfs filesystem with multiple subvolumes for data
try:
return int(filter(lambda x: x.split()[3] == name, subvol_ids)[0].split()[0])
except IndexError:
pass
# Volume not found, root is probably the btrfs default (5)
return 5
def get_id_subvolumes(path, subvol_id):
lines = get_subvol_list(path)
lines = filter(lambda x: int(x.split()[2]) == subvol_id, lines)
return list(map(lambda x: int(x.split()[0]), lines))
def get_disk_usage(name, path):
id_root = get_id_root(name, path)
id_subvolumes = get_id_subvolumes(path, id_root)
size_filesystem = get_filesystem_size(path)
# Get disk usage from quota
command = 'sudo btrfs qgroup show --raw %s' % (path)
status, output = commands.getstatusoutput(command)
if status is not 0:
raise Exception(command)
lines = output.splitlines()[2:]
# Global variables
global size_data_total
global size_data_exclusive
global size_snapshot_total
global size_snapshot_exclusive
# Total data volume in subvolume
size_data_total = 0
# Total data volume in snapshots
# -> this variable is useless
size_snapshot_total = 0
# Data exclusively in subvolume
# -> data that is not (yet) incorporated in a snapshot
size_data_exclusive = 0
# Data exclusively available in snapshots
# -> data that was removed from volume
size_snapshot_exclusive = 0
for line in lines:
split = line.split()
subvol_id = 0
size_total = 0
size_exclusive = 0
try:
subvol_id = int(split[0].split('/')[1])
size_total = float(split[1])
size_exclusive = float(split[2])
except IndexError:
# ignore 'WARNING: Quota disabled'
pass
# size_exclusive is incorrect when snapshot is
# removed and qgroups are not updated yet,
# ignore the value when it seems unrealistic
if size_exclusive > size_filesystem:
size_exclusive = 0
if subvol_id == id_root:
size_data_total = size_total
size_data_exclusive = size_exclusive
elif subvol_id in id_subvolumes:
size_snapshot_total += size_total
size_snapshot_exclusive += size_exclusive
def rescan_quota(path):
command = 'sudo btrfs quota rescan %s' % (path)
status, output = commands.getstatusoutput(command)
if status is not 0:
Exception(command)
def print_human_readable(name):
global size_data_total
global size_data_exclusive
global size_snapshot_exclusive
size_data_total = size_data_total / (1024*1e6)
size_data_exclusive = size_data_exclusive / (1024*1e6)
size_snapshot_exclusive = size_snapshot_exclusive / (1024*1e6)
print '%10s: %6.1f Gb, %6.1f Gb, %6.1f Gb' % (name, size_data_total, size_data_exclusive, size_snapshot_exclusive)
def print_rrd(name):
timestamp = int(time.time())
print('PUTVAL {}/exec-btrfs_{}/gauge-data_total {}:{:.1f}'.format(hostname, name, timestamp, size_data_total))
print('PUTVAL {}/exec-btrfs_{}/gauge-data_exclusive {}:{:.1f}'.format(hostname, name, timestamp, size_data_exclusive))
print('PUTVAL {}/exec-btrfs_{}/gauge-snapshot_total {}:{:.1f}'.format(hostname, name, timestamp, size_snapshot_total))
print('PUTVAL {}/exec-btrfs_{}/gauge-snapshot_exclusive {}:{:.1f}'.format(hostname, name, timestamp, size_snapshot_exclusive))
#
# Volumes to scan
#
hostname = 'server'
interval = 10
volumes = [
['mezzo-scratch', '/mnt/mezzo/scratch'],
['mezzo-sync', '/mnt/mezzo/sync'],
['helium-personal', '/mnt/yotta/helium/personal'],
['helium-shared', '/mnt/yotta/helium/shared'],
['neon', '/mnt/yotta/neon'],
['krypton', '/mnt/yotta/krypton'],
['xenon-borg', '/mnt/yotta/xenon/borg'],
['xenon-rsnapshot', '/mnt/yotta/xenon/rsnapshot']
]
#
# Command line arguments
#
parser = argparse.ArgumentParser(description='Get BTRFS disk usage')
parser.add_argument('-s', action='store_true', help='print in human readable format')
args = parser.parse_args()
human_readable = args.s
#
# Main
#
if human_readable:
for name, path in volumes:
get_disk_usage(name, path)
print_human_readable(name)
else:
# RRD mode
while True:
for name, path in volumes:
get_disk_usage(name, path)
print_rrd(name)
time.sleep(interval)
rescan_quota(path)