-
-
Notifications
You must be signed in to change notification settings - Fork 58
/
shrink_btrfs.py
executable file
·105 lines (80 loc) · 3.17 KB
/
shrink_btrfs.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#!/usr/bin/env python3
import os
import subprocess
import sys
import tempfile
from pathlib import Path
import click
# btrfs cannot shrink smaller than 256 MiB
SMALLEST_SIZE = 256 * 1024 * 1024
@click.command()
@click.argument(
'btrfs_img',
type=click.Path(exists=True, dir_okay=False, file_okay=True, path_type=Path),
)
def cli(btrfs_img: Path):
"""
Shrinks a Btrfs image
// I cannot believe that Btrfs is over 15 years old,
// yet there is no resize2fs tool which can shrink a disk image
// to minimum size.
// It cannot even tell you how much should be the right size,
// it just randomly fails after which you have to umount and mount again.
// So we have to make a loop which tries to shrink it until it fails.
// Also, WONTFIX bugs like how instead of telling you that
// minimum fs size is 256 MB, it says "ERROR: unable to resize - Invalid argument"
// https://bugzilla.kernel.org/show_bug.cgi?id=118111
"""
if os.geteuid() != 0:
sys.exit(' needs sudo')
current_dir = Path.cwd()
mnt_dir = Path(tempfile.mkdtemp(dir=current_dir, prefix='tmp_shrink_'))
subprocess.run(['mount', '-t', 'btrfs', btrfs_img, mnt_dir], check=True)
# shink until max. 10 MB left or reached SMALLEST_SIZE or failure
while True:
# needs to start with a balancing
# https://btrfs.readthedocs.io/en/latest/Balance.html
# https://marc.merlins.org/perso/btrfs/post_2014-05-04_Fixing-Btrfs-Filesystem-Full-Problems.html
do_balancing(mnt_dir)
free_bytes = get_usage(mnt_dir, 'Device unallocated')
device_size = get_usage(mnt_dir, 'Device size')
shrink_idea = free_bytes * 0.7
# workaround for the SMALLEST_SIZE limit
if device_size - free_bytes < SMALLEST_SIZE:
shrink_idea = (device_size - SMALLEST_SIZE) * 0.7
# stop if 10 MB left
if shrink_idea < 10_000_000:
break
# stop if process error
if not do_shrink(mnt_dir, shrink_idea):
break
total_size = get_usage(mnt_dir, 'Device size')
subprocess.run(['umount', mnt_dir])
mnt_dir.rmdir()
subprocess.run(['truncate', '-s', str(total_size), btrfs_img])
print(f'Truncated {btrfs_img} to {total_size//1_000_000} MB size')
print('shrink_btrfs.py DONE')
def get_usage(mnt: Path, key: str):
p = subprocess.run(
['btrfs', 'filesystem', 'usage', '-b', mnt], text=True, capture_output=True, check=True
)
for line in p.stdout.splitlines():
if f'{key}:' not in line:
continue
free = int(line.split(':')[1])
return free
def do_shrink(mnt: Path, delta_size: float):
delta_size = int(delta_size)
print(f'Trying to shrink by {delta_size//1_000_000} MB')
p = subprocess.run(['btrfs', 'filesystem', 'resize', str(-delta_size), mnt])
return p.returncode == 0
def do_balancing(mnt: Path):
print('Starting btrfs balancing')
p = subprocess.run(
['btrfs', 'balance', 'start', '-dusage=100', mnt], capture_output=True, text=True
)
if p.returncode:
print(f'Balance error: {p.stdout} {p.stderr}')
print('Balancing done')
if __name__ == '__main__':
cli()