mirror of
https://github.com/webmin/webmin.git
synced 2026-08-23 07:20:28 +01:00
Merge pull request #2807 from webmin/feature/btrfs-subvolume-quotas
Add Btrfs subvolume quota management
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
## Changelog
|
||||
#### 2.654 (August, 2026)
|
||||
* Add incremental ban time options to the Fail2Ban module
|
||||
* Add Btrfs subvolume quota management to the Disk Quotas module, with full and simple accounting modes
|
||||
* Fix to ignore failures when adding IPv6 link-local (fe80::) addresses that may already be configured automatically
|
||||
* Fixed creation of permissions new log files in the System Logs module (thanks to Kevin Carter)
|
||||
* Update the Authentic theme to the latest version with various improvements:
|
||||
|
||||
61
quota/btrfs_action.cgi
Executable file
61
quota/btrfs_action.cgi
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/usr/local/bin/perl
|
||||
# Enable, disable or rescan Btrfs quotas
|
||||
|
||||
require './quota-lib.pl';
|
||||
&ReadParse();
|
||||
$dir = $in{'dir'};
|
||||
|
||||
# Require quota activation access and a valid mounted Btrfs filesystem before
|
||||
# running any command that can change filesystem quota state.
|
||||
&can_edit_btrfs_filesys($dir) && $access{'enable'} && !$access{'ro'} ||
|
||||
&error($text{'btrfs_eenable'});
|
||||
defined(&btrfs_quota_status) && &is_btrfs_fs($dir) ||
|
||||
&error($text{'btrfs_enotbtrfs'});
|
||||
|
||||
# Accept only the three operations implemented by this handler.
|
||||
$in{'action'} =~ /^(enable|disable|rescan)$/ ||
|
||||
&error($text{'btrfs_eaction'});
|
||||
|
||||
# Disabling Btrfs quotas removes every qgroup and limit, so require an explicit
|
||||
# confirmation before performing this destructive operation.
|
||||
if ($in{'action'} eq "disable" && !$in{'confirm'}) {
|
||||
# Mark the filesystem and qgroup terms as literal technical values.
|
||||
my $dir_label = &ui_tag("tt", &html_escape($dir));
|
||||
my $qgroup_label = &ui_tag("tt", "qgroup");
|
||||
chomp($dir_label);
|
||||
chomp($qgroup_label);
|
||||
|
||||
# Display the destructive warning inside the confirmation form.
|
||||
&ui_print_header(undef, $text{'btrfs_disable'}, "", "btrfs");
|
||||
print &ui_confirmation_form(
|
||||
"btrfs_action.cgi",
|
||||
&text('btrfs_disable_confirm', $dir_label),
|
||||
[ [ "dir", $dir ], [ "action", "disable" ] ],
|
||||
[ [ "confirm", $text{'btrfs_disable'} ] ],
|
||||
&ui_alert_box(&text('btrfs_disable_warning', $qgroup_label), "warn",
|
||||
undef, undef, ""));
|
||||
&ui_print_footer("", $text{'index_return'});
|
||||
exit;
|
||||
}
|
||||
|
||||
&error_setup($text{'btrfs_efailed'});
|
||||
# Enable quotas using the accounting mode selected in the module configuration.
|
||||
if ($in{'action'} eq "enable") {
|
||||
$err = &enable_btrfs_quotas($dir,
|
||||
$config{'btrfs_mode'} eq "simple" ? 1 : 0);
|
||||
}
|
||||
# Disable quotas after the confirmation branch above has been completed.
|
||||
elsif ($in{'action'} eq "disable") {
|
||||
$err = &disable_btrfs_quotas($dir);
|
||||
}
|
||||
# The remaining valid action starts a full-accounting quota rescan.
|
||||
else {
|
||||
$err = &rescan_btrfs_quotas($dir, 0);
|
||||
}
|
||||
|
||||
# Report command failures, record successful changes, and return to the most
|
||||
# relevant page for the completed action.
|
||||
&error($err) if ($err);
|
||||
&webmin_log($in{'action'}, "btrfs", $dir, \%in);
|
||||
&redirect($in{'action'} eq "rescan" ?
|
||||
"list_btrfs.cgi?dir=".&urlize($dir) : "");
|
||||
@@ -23,3 +23,4 @@ show_grace=1
|
||||
email_msg=Disk usage for user ${USER} on filesystem ${FS} has reached ${PERCENT}% of the allowed quota. ${USED} of disk space is being used, out of a maximum of ${QUOTA}.
|
||||
pc_show=1
|
||||
hide_uids=0
|
||||
btrfs_mode=full
|
||||
|
||||
@@ -5,6 +5,7 @@ pc_show=Usage percentages to show,1,2-Hard and soft,1-Hard only,0-Soft only
|
||||
sort_mode=Sort users and groups by,1,0-Blocks used,2-Name,1-Order from repquota,3-Hard block quota,4-Soft block quota,5-Percent of hard quota used,6-Percent of soft quota used
|
||||
block_mode=Show quotas in,1,1-Kilobytes (where possible),0-Blocks
|
||||
hide_uids=Show deleted users?,1,0-Yes,1-No
|
||||
btrfs_mode=Btrfs accounting mode when enabling quotas,4,full-Full accounting, recommended for shared space and snapshots,simple-Simple accounting, lower overhead with original ownership tracking
|
||||
line1.1=Quota email messages,11
|
||||
email_msg=Email message for users over quota,9,80,5,\t
|
||||
email_subject=Subject for email message to users,3,Default
|
||||
|
||||
18
quota/config_info.pl
Executable file
18
quota/config_info.pl
Executable file
@@ -0,0 +1,18 @@
|
||||
# Hide Btrfs-specific configuration when it cannot be used.
|
||||
require './quota-lib.pl';
|
||||
|
||||
# config_pre_load(info, [order])
|
||||
# Hide Btrfs-specific settings unless both a mounted Btrfs filesystem and the
|
||||
# command-line tool needed to manage it are available.
|
||||
sub config_pre_load
|
||||
{
|
||||
my ($info, $order) = @_;
|
||||
my @btrfs = &list_btrfs_filesystems();
|
||||
return if (@btrfs && &has_command("btrfs"));
|
||||
|
||||
# Remove the field from both the configuration metadata and display order.
|
||||
delete($info->{'btrfs_mode'});
|
||||
@$order = grep { $_ ne "btrfs_mode" } @$order if ($order);
|
||||
}
|
||||
|
||||
1;
|
||||
54
quota/edit_btrfs.cgi
Executable file
54
quota/edit_btrfs.cgi
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/local/bin/perl
|
||||
# Edit the limits for a Btrfs qgroup
|
||||
|
||||
require './quota-lib.pl';
|
||||
&ReadParse();
|
||||
$dir = $in{'dir'};
|
||||
|
||||
# Limit editing requires write access to an allowed mounted Btrfs filesystem
|
||||
# and a syntactically valid qgroup ID.
|
||||
$access{'ro'} && &error($text{'btrfs_eedit'});
|
||||
&can_edit_btrfs_filesys($dir) || &error($text{'btrfs_eallow'});
|
||||
defined(&btrfs_quota_status) && &is_btrfs_fs($dir) ||
|
||||
&error($text{'btrfs_enotbtrfs'});
|
||||
&valid_btrfs_qgroup_id($in{'qgroup'}) || &error($text{'btrfs_eqgroup'});
|
||||
$in{'qgroup'} eq "0/5" && &error($text{'btrfs_etoplevel'});
|
||||
|
||||
# Load the current qgroups and ensure the requested ID still exists.
|
||||
$qgroups = &list_btrfs_qgroups($dir, 0, \$listerr);
|
||||
&error($listerr) if (!$qgroups);
|
||||
($qgroup) = grep { $_->{'id'} eq $in{'qgroup'} } @$qgroups;
|
||||
$qgroup || &error($text{'btrfs_eqgroup'});
|
||||
|
||||
# Start a form bound to the selected filesystem and qgroup.
|
||||
&ui_print_header(undef, $text{'btrfs_edit_title'}, "", "btrfs");
|
||||
print "<p>$text{'btrfs_edit_info'}</p>\n";
|
||||
print &ui_form_start("save_btrfs.cgi", "post");
|
||||
print &ui_hidden("dir", $dir);
|
||||
print &ui_hidden("qgroup", $qgroup->{'id'});
|
||||
print &ui_table_start(&text('btrfs_edit_header',
|
||||
&html_escape($qgroup->{'id'}), &html_escape($dir)), "width=100%", 2);
|
||||
|
||||
# Show the current path and accounted usage as read-only values.
|
||||
print &ui_table_row($text{'btrfs_path'},
|
||||
$qgroup->{'path'} ne "" ? &html_escape($qgroup->{'path'}) : "-");
|
||||
print &ui_table_row($text{'btrfs_referenced'},
|
||||
&nice_size($qgroup->{'referenced'}));
|
||||
print &ui_table_row($text{'btrfs_exclusive'},
|
||||
&nice_size($qgroup->{'exclusive'}));
|
||||
print &ui_table_hr();
|
||||
|
||||
# Allow referenced and exclusive limits to be changed independently.
|
||||
print &ui_table_row($text{'btrfs_max_referenced'},
|
||||
"a_input("max_referenced",
|
||||
defined($qgroup->{'max_referenced'}) ?
|
||||
$qgroup->{'max_referenced'} : 0, 1));
|
||||
print &ui_table_row($text{'btrfs_max_exclusive'},
|
||||
"a_input("max_exclusive",
|
||||
defined($qgroup->{'max_exclusive'}) ?
|
||||
$qgroup->{'max_exclusive'} : 0, 1));
|
||||
print &ui_table_end();
|
||||
print &ui_form_end([ [ undef, $text{'btrfs_update'} ] ]);
|
||||
|
||||
# Return to the qgroup list for this filesystem.
|
||||
&ui_print_footer("list_btrfs.cgi?dir=".&urlize($dir), $text{'btrfs_title'});
|
||||
57
quota/help/btrfs.html
Normal file
57
quota/help/btrfs.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<header>Btrfs Subvolume Quotas</header>
|
||||
|
||||
<h3>Introduction</h3>
|
||||
Btrfs quotas control disk usage for subvolumes through quota groups, usually
|
||||
called <tt>qgroups</tt>. Unlike traditional Unix quotas, they do not limit an
|
||||
individual user or group and do not provide soft limits, grace periods, or
|
||||
file-count limits. <p>
|
||||
|
||||
Each Btrfs subvolume has a level-0 <tt>qgroup</tt>. The module displays the
|
||||
following usage and limit values for each <tt>qgroup</tt> :
|
||||
<dl>
|
||||
<dt><b>Referenced</b>
|
||||
<dd>All data reachable from the subvolume, including data shared with other
|
||||
subvolumes or snapshots.
|
||||
<dt><b>Exclusive</b>
|
||||
<dd>Data used only by the subvolume, which would be freed if it were deleted.
|
||||
<dt><b>Referenced limit</b>
|
||||
<dd>The maximum referenced space that the <tt>qgroup</tt> may use.
|
||||
<dt><b>Exclusive limit</b>
|
||||
<dd>The maximum exclusive space that the <tt>qgroup</tt> may use.
|
||||
</dl>
|
||||
|
||||
<h3>Accounting Modes</h3>
|
||||
When Btrfs quotas are enabled, the accounting mode configured in the module
|
||||
settings is used :
|
||||
<dl>
|
||||
<dt><b>Full accounting</b>
|
||||
<dd>Tracks shared space between subvolumes and snapshots. This is the
|
||||
recommended mode when accurate referenced and exclusive usage is required.
|
||||
<dt><b>Simple accounting</b>
|
||||
<dd>Tracks original ownership with lower overhead, but does not fully track
|
||||
space shared between subvolumes and snapshots.
|
||||
</dl>
|
||||
In simple accounting mode, both values show space assigned to the subvolume
|
||||
that first wrote the data. <p>
|
||||
Changing the module setting does not convert an already-enabled filesystem.
|
||||
The selected mode is used the next time quotas are enabled. <p>
|
||||
|
||||
<h3>Managing Btrfs Quotas</h3>
|
||||
The main module page shows each mounted Btrfs filesystem, its accounting mode,
|
||||
consistency state, and an action to enable or disable quotas. Click a filesystem
|
||||
path to view its <tt>qgroups</tt>, usage, and limits. Click a <tt>qgroup</tt> ID
|
||||
to edit its referenced and exclusive limits. The top-level <tt>qgroup</tt>
|
||||
<tt>0/5</tt> is shown for information only because limiting it could stop
|
||||
filesystem changes. <p>
|
||||
|
||||
Because quota state and <tt>qgroup</tt> IDs apply to the whole underlying Btrfs
|
||||
filesystem, delegated Webmin users must be allowed to manage all filesystems to
|
||||
access these controls. A permission scoped to one mounted subvolume is not
|
||||
sufficient. <p>
|
||||
|
||||
Full accounting also provides a rescan action for rebuilding <tt>qgroup</tt>
|
||||
accounting in the background. Disabling Btrfs quotas removes all
|
||||
<tt>qgroup</tt> configuration and limits on the filesystem, so the module
|
||||
always requests confirmation first. <p>
|
||||
|
||||
<hr>
|
||||
@@ -1,21 +1,31 @@
|
||||
#!/usr/local/bin/perl
|
||||
# index.cgi
|
||||
# Display a list of all local filesystems, and allow editing of quotas
|
||||
# on those which have quotas turned on. The actual turning on of quotas must
|
||||
# be done in the mount module first.
|
||||
# on those which have quotas turned on. Traditional quota mount options are
|
||||
# configured in the mount module, while Btrfs quotas are managed here.
|
||||
|
||||
require './quota-lib.pl';
|
||||
&ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1, 0,
|
||||
|
||||
# Discover allowed Btrfs mounts independently of the traditional quota tools.
|
||||
@btrfs = grep { &can_edit_btrfs_filesys($_->[0]) } &list_btrfs_filesystems();
|
||||
$err = "as_init();
|
||||
|
||||
# Traditional filesystems are unavailable when quota-tools initialization fails.
|
||||
@list = $err ? ( ) : &list_filesystems();
|
||||
|
||||
# Use focused Btrfs help when it is the only quota model shown on this page.
|
||||
$help = @btrfs && !@list ? "btrfs" : "intro";
|
||||
&ui_print_header(undef, $text{'index_title'}, "", $help, 1, 1, 0,
|
||||
&help_search_link("quota", "man", "howto"));
|
||||
|
||||
$err = "as_init();
|
||||
if ($err) {
|
||||
# Stop only when neither traditional quota tools nor Btrfs tools can provide a
|
||||
# usable filesystem list.
|
||||
if ($err && (!@btrfs || !&has_command("btrfs"))) {
|
||||
print "<p><b>$err</b><p>\n";
|
||||
&ui_print_footer("/", $text{'index_return'});
|
||||
exit;
|
||||
}
|
||||
|
||||
@list = &list_filesystems();
|
||||
if (@list) {
|
||||
print &ui_columns_start([
|
||||
$text{'index_fs'},
|
||||
@@ -97,13 +107,84 @@ if (@list) {
|
||||
}
|
||||
print &ui_columns_end();
|
||||
}
|
||||
else {
|
||||
# Report no support only when neither traditional nor Btrfs filesystems exist.
|
||||
elsif (!@btrfs) {
|
||||
print "<b>$text{'index_nosupport'}</b><p>\n";
|
||||
if (&foreign_available("mount")) {
|
||||
print &text('index_mountmod', "../mount/"),"<p>\n";
|
||||
}
|
||||
}
|
||||
|
||||
# Btrfs subvolume quotas use qgroups instead of Unix users and groups, so they
|
||||
# are shown separately from the traditional quota filesystems above.
|
||||
if (@btrfs) {
|
||||
# Activation controls require both enable permission and write access.
|
||||
$btrfs_canactivate = $access{'enable'} && !$access{'ro'};
|
||||
|
||||
# Start a table with an action column only for users who can change state.
|
||||
print &ui_columns_start([
|
||||
$text{'index_fs'},
|
||||
$text{'index_type'},
|
||||
$text{'index_mount'},
|
||||
$text{'index_status'},
|
||||
$btrfs_canactivate ? ( $text{'index_action'} ) : (),
|
||||
], 100, 0, undef, &hlink($text{'index_btrfs_title'}, "btrfs"));
|
||||
foreach $f (@btrfs) {
|
||||
# Query each mount independently so failures remain visible per row.
|
||||
undef($action);
|
||||
$status = &btrfs_quota_status($f->[0]);
|
||||
|
||||
# The OS library could not identify this path as manageable Btrfs.
|
||||
if (!$status) {
|
||||
$msg = $text{'index_btrfs_unavailable'};
|
||||
}
|
||||
# Surface command or parsing errors without offering a state change.
|
||||
elsif ($status->{'error'}) {
|
||||
$msg = &text('index_btrfs_error',
|
||||
&html_escape($status->{'error'}));
|
||||
}
|
||||
# Disabled filesystems can be enabled using the configured mode.
|
||||
elsif (!$status->{'enabled'}) {
|
||||
$msg = $text{'index_btrfs_disabled'};
|
||||
$action = "enable";
|
||||
}
|
||||
# Enabled filesystems expose their accounting and consistency state.
|
||||
else {
|
||||
$mode = $status->{'mode'} eq "squota" ?
|
||||
$text{'index_btrfs_simple'} :
|
||||
$status->{'mode'} eq "qgroup" ?
|
||||
$text{'index_btrfs_full'} :
|
||||
$text{'index_btrfs_unknown'};
|
||||
$msg = &text('index_btrfs_enabled', $mode);
|
||||
$msg .= ", $text{'index_btrfs_inconsistent'}"
|
||||
if ($status->{'inconsistent'});
|
||||
$action = "disable";
|
||||
}
|
||||
|
||||
# Build the common filesystem, type, source and status columns.
|
||||
local @cols = (
|
||||
&ui_link("list_btrfs.cgi?dir=".&urlize($f->[0]),
|
||||
&html_escape($f->[0])),
|
||||
&foreign_call("mount", "fstype_name", $f->[2]),
|
||||
&foreign_call("mount", "device_name", $f->[1]),
|
||||
$msg,
|
||||
);
|
||||
|
||||
# Add the state-changing link only when the ACL allows it.
|
||||
if ($btrfs_canactivate) {
|
||||
push(@cols, $action ?
|
||||
&ui_link("btrfs_action.cgi?dir=".&urlize($f->[0]).
|
||||
"&action=$action",
|
||||
$action eq "enable" ? $text{'index_enable'} :
|
||||
$text{'index_disable'}) : "-");
|
||||
}
|
||||
print &ui_columns_row(\@cols);
|
||||
}
|
||||
|
||||
# Close the separately titled Btrfs filesystem table.
|
||||
print &ui_columns_end();
|
||||
}
|
||||
|
||||
# Buttons to edit and specific user or group
|
||||
if ($useractive || $groupactive) {
|
||||
print &ui_hr();
|
||||
|
||||
@@ -8,9 +8,15 @@ do 'quota-lib.pl';
|
||||
# For mode 0, returns 1 if installed, 0 if not
|
||||
sub is_installed
|
||||
{
|
||||
# Check the traditional quota-tools dependency when this OS implements it.
|
||||
if (defined("as_init)) {
|
||||
local $err = "as_init();
|
||||
return 0 if ($err);
|
||||
# A usable Btrfs mount and command provide an alternative when the
|
||||
# traditional quota-tools package is not installed.
|
||||
if ($err) {
|
||||
local @btrfs = &list_btrfs_filesystems();
|
||||
return 0 if (!@btrfs || !&has_command("btrfs"));
|
||||
}
|
||||
}
|
||||
return $_[0] ? 2 : 1;
|
||||
}
|
||||
|
||||
@@ -167,6 +167,56 @@ index_egroup=Edit Group Quotas:
|
||||
index_egroupdesc=Enter or select a group, and click this button to view its quotas on all filesystems.
|
||||
index_nosupport=No local filesystems can support quotas.
|
||||
index_mountmod=You can enable quotas for a filesystem in the <a href='$1'>Disk and Network Filesystems</a> module.
|
||||
index_btrfs_title=Btrfs Subvolume Quotas
|
||||
index_btrfs_enabled=Enabled, $1
|
||||
index_btrfs_disabled=Disabled
|
||||
index_btrfs_full=full accounting
|
||||
index_btrfs_simple=simple accounting
|
||||
index_btrfs_unknown=accounting mode unavailable
|
||||
index_btrfs_inconsistent=inconsistent
|
||||
index_btrfs_unavailable=Unavailable
|
||||
index_btrfs_error=Error: $1
|
||||
|
||||
btrfs_title=Btrfs Subvolume Quotas
|
||||
btrfs_return=filesystem list
|
||||
btrfs_status_header=Quota status for $1
|
||||
btrfs_status=Status
|
||||
btrfs_enabled=Enabled
|
||||
btrfs_disabled=Disabled
|
||||
btrfs_mode=Accounting mode
|
||||
btrfs_full=Full accounting
|
||||
btrfs_simple=Simple accounting
|
||||
btrfs_unknown=Unavailable from this version of <tt>btrfs-progs</tt>
|
||||
btrfs_consistency=Accounting state
|
||||
btrfs_consistent=Consistent
|
||||
btrfs_inconsistent=Inconsistent - a rescan is recommended
|
||||
btrfs_qgroups=Subvolume quota groups
|
||||
btrfs_qgroup=<tt>qgroup</tt>
|
||||
btrfs_path=Subvolume path
|
||||
btrfs_referenced=Referenced usage
|
||||
btrfs_exclusive=Exclusive usage
|
||||
btrfs_max_referenced=Referenced limit
|
||||
btrfs_max_exclusive=Exclusive limit
|
||||
btrfs_disable=Disable quotas
|
||||
btrfs_disable_confirm=Disable Btrfs quotas on $1?
|
||||
btrfs_disable_warning=All $1 configuration and limits on this filesystem will be removed!
|
||||
btrfs_rescan=Rescan quotas
|
||||
btrfs_rescan_desc=Rebuild full <tt>qgroup</tt> accounting in the background.
|
||||
btrfs_edit_title=Edit Btrfs Quota
|
||||
btrfs_edit_header=Limits for <tt>qgroup</tt> <tt>$1</tt> on <tt>$2</tt>
|
||||
btrfs_edit_info=Full accounting shows space reachable from the subvolume and space freed by deleting it. Simple accounting shows space assigned to the subvolume that first wrote the data.
|
||||
btrfs_update=Update
|
||||
btrfs_eallow=Btrfs quota management requires permission to manage all filesystems because qgroups apply to the whole underlying filesystem
|
||||
btrfs_eenable=You are not allowed to enable or disable quotas on this filesystem
|
||||
btrfs_eedit=You are not allowed to edit Btrfs quota limits
|
||||
btrfs_enotbtrfs=The selected path is not on a mounted Btrfs filesystem
|
||||
btrfs_eqgroup=The selected Btrfs <tt>qgroup</tt> does not exist
|
||||
btrfs_etoplevel=The top-level Btrfs <tt>qgroup</tt> cannot be limited because doing so can block the entire filesystem
|
||||
btrfs_eaction=Invalid Btrfs quota action
|
||||
btrfs_efailed=Failed to manage Btrfs quotas
|
||||
btrfs_esave=Failed to save the Btrfs quota
|
||||
btrfs_elimit=Quota limits must be positive numbers
|
||||
btrfs_emax=You are not allowed to grant limits above $1
|
||||
|
||||
lgroups_failed=Failed to list groups
|
||||
lgroups_tablist=Group list
|
||||
|
||||
@@ -82,6 +82,12 @@ the following :
|
||||
=cut
|
||||
sub quota_can
|
||||
{
|
||||
my ($mnttab) = @_;
|
||||
|
||||
# The quota-tools commands used by this module cannot reliably manage tmpfs
|
||||
# mounts, even when they expose usrquota or grpquota mount options.
|
||||
return 0 if ($mnttab->[2] eq "tmpfs");
|
||||
|
||||
my %exclude_mounts;
|
||||
if (&has_command("findmnt")) {
|
||||
%exclude_mounts = map { $_ => 1 } split( /\n/m, backquote_command('findmnt -r | grep -oP \'^(\S+)(?=.*\[\/)\'') );
|
||||
@@ -1105,6 +1111,84 @@ foreach my $m (&mount::list_mounted()) {
|
||||
return $best && $best->[2] eq "btrfs" ? 1 : 0;
|
||||
}
|
||||
|
||||
# decode_btrfs_mount_path(path)
|
||||
# Decodes the octal escapes used by /proc/self/mountinfo.
|
||||
sub decode_btrfs_mount_path
|
||||
{
|
||||
my ($path) = @_;
|
||||
$path =~ s/\\([0-7]{3})/chr(oct($1))/eg;
|
||||
return $path;
|
||||
}
|
||||
|
||||
# parse_btrfs_mountinfo(text, path)
|
||||
# Returns the deepest Btrfs mount point containing path and its filesystem root.
|
||||
sub parse_btrfs_mountinfo
|
||||
{
|
||||
my ($text, $path) = @_;
|
||||
my ($best_mount, $best_root);
|
||||
# Parse only Btrfs mountinfo records that can contain the requested path.
|
||||
foreach my $line (split(/\r?\n/, $text)) {
|
||||
my ($left, $right) = split(/\s+-\s+/, $line, 2);
|
||||
next if (!defined($right));
|
||||
my @right = split(/\s+/, $right);
|
||||
next if ($right[0] ne "btrfs");
|
||||
my @left = split(/\s+/, $left);
|
||||
next if (@left < 5);
|
||||
my $root = &decode_btrfs_mount_path($left[3]);
|
||||
my $mount = &decode_btrfs_mount_path($left[4]);
|
||||
next if (!&is_under_directory($mount, $path));
|
||||
# Prefer the deepest match when nested Btrfs subvolumes are mounted.
|
||||
if (!defined($best_mount) || length($mount) > length($best_mount)) {
|
||||
$best_mount = $mount;
|
||||
$best_root = $root;
|
||||
}
|
||||
}
|
||||
return defined($best_mount) ? ($best_mount, $best_root) : ( );
|
||||
}
|
||||
|
||||
=head2 btrfs_mountinfo(path)
|
||||
|
||||
Returns the visible Btrfs mount point containing a path and its filesystem
|
||||
root, or an empty list when no containing Btrfs mount can be found.
|
||||
|
||||
=cut
|
||||
sub btrfs_mountinfo
|
||||
{
|
||||
my ($path) = @_;
|
||||
open(my $fh, "<", "/proc/self/mountinfo") || return ( );
|
||||
local $/ = undef;
|
||||
my $text = <$fh>;
|
||||
close($fh);
|
||||
return &parse_btrfs_mountinfo($text, $path);
|
||||
}
|
||||
|
||||
=head2 btrfs_qgroup_absolute_path(mount, filesystem-root, qgroup-path)
|
||||
|
||||
Converts the filesystem-relative path reported by C<btrfs qgroup show> to a
|
||||
visible absolute path, or returns undef when it is outside the mounted root.
|
||||
|
||||
=cut
|
||||
sub btrfs_qgroup_absolute_path
|
||||
{
|
||||
my ($mount, $root, $path) = @_;
|
||||
return undef if (!defined($path) || $path eq "" || $path =~ /^</);
|
||||
$root ||= "/";
|
||||
$root =~ s/^\/+//;
|
||||
$root =~ s/\/+\z//;
|
||||
$path =~ s/^\/+//;
|
||||
# Strip the mounted subvolume root from the filesystem-relative qgroup path.
|
||||
if ($root ne "") {
|
||||
return undef if ($path ne $root && index($path, "$root/") != 0);
|
||||
$path = substr($path, length($root));
|
||||
$path =~ s/^\/+//;
|
||||
}
|
||||
$mount =~ s/\/+\z// if ($mount ne "/");
|
||||
my $absolute = $path eq "" ? ($mount || "/") :
|
||||
($mount eq "/" ? "/$path" : "$mount/$path");
|
||||
$absolute =~ s{//+}{/}g;
|
||||
return $absolute;
|
||||
}
|
||||
|
||||
# valid_btrfs_path(path)
|
||||
# Returns 1 for an absolute path that is safe to pass to Btrfs tools.
|
||||
sub valid_btrfs_path
|
||||
@@ -1168,6 +1252,53 @@ $rv{'levels'} = \%levels if (%levels);
|
||||
return \%rv;
|
||||
}
|
||||
|
||||
# btrfs_filesystem_uuid(path)
|
||||
# Returns the UUID of the Btrfs filesystem containing a path.
|
||||
sub btrfs_filesystem_uuid
|
||||
{
|
||||
my ($path) = @_;
|
||||
my ($out, $err) = &run_btrfs_command(
|
||||
0, "filesystem", "show", "--raw", $path);
|
||||
return undef if (!defined($out) ||
|
||||
$out !~ /^\s*Label:.*\buuid:\s*([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})\s*$/mi);
|
||||
return lc($1);
|
||||
}
|
||||
|
||||
# btrfs_sysfs_quota_status(path)
|
||||
# Reads quota state exported by the kernel. This preserves accounting mode and
|
||||
# consistency information on btrfs-progs releases older than `quota status`.
|
||||
sub btrfs_sysfs_quota_status
|
||||
{
|
||||
my ($path) = @_;
|
||||
my $uuid = &btrfs_filesystem_uuid($path);
|
||||
return undef if (!$uuid);
|
||||
my $sysfs = $btrfs_sysfs_root || "/sys/fs/btrfs";
|
||||
my $qdir = "$sysfs/$uuid/qgroups";
|
||||
return undef if (!-d $qdir);
|
||||
|
||||
my %rv = ( 'supported' => 1, 'enabled' => 1 );
|
||||
foreach my $field (qw(enabled mode inconsistent)) {
|
||||
my $file = "$qdir/$field";
|
||||
next if (!-r $file);
|
||||
open(my $fh, "<", $file) || next;
|
||||
my $value = <$fh>;
|
||||
close($fh);
|
||||
next if (!defined($value));
|
||||
$value =~ s/^\s+|\s+$//g;
|
||||
if ($field eq "mode" && $value =~ /^(qgroup|squota)$/) {
|
||||
$rv{$field} = $value;
|
||||
}
|
||||
elsif ($field ne "mode" && $value =~ /^([01])$/) {
|
||||
$rv{$field} = int($1);
|
||||
}
|
||||
}
|
||||
|
||||
# Kernels predating simple quotas expose the qgroups directory without a mode
|
||||
# file. Their only possible accounting mode is full qgroups.
|
||||
$rv{'mode'} = "qgroup" if (!defined($rv{'mode'}) && !-e "$qdir/mode");
|
||||
return \%rv;
|
||||
}
|
||||
|
||||
=head2 btrfs_quota_status(path)
|
||||
|
||||
Returns a hash reference describing the Btrfs quota status for a path. The
|
||||
@@ -1194,8 +1325,20 @@ if (defined($out)) {
|
||||
# quotas are enabled, and reports a missing quota root when disabled.
|
||||
my ($qout, $qerr) = &run_btrfs_command(0, "qgroup", "show", "--raw", $path);
|
||||
if (defined($qout)) {
|
||||
return { 'supported' => 1,
|
||||
'enabled' => 1 };
|
||||
my $rv = &btrfs_sysfs_quota_status($path) ||
|
||||
{ 'supported' => 1, 'enabled' => 1 };
|
||||
# Old qgroup-show versions warn on stdout when the counters are
|
||||
# inconsistent. Retain that signal if sysfs did not provide the flag.
|
||||
if (!defined($rv->{'inconsistent'})) {
|
||||
$rv->{'inconsistent'} =
|
||||
$qout =~ /^\s*(?:warning|error):.*qgroup.*inconsistent/mi ?
|
||||
1 : 0;
|
||||
}
|
||||
# A simple-quota space holder is definitive even when sysfs is unavailable.
|
||||
$rv->{'mode'} = "squota"
|
||||
if (!defined($rv->{'mode'}) &&
|
||||
$qout =~ /<squota space holder>/i);
|
||||
return $rv;
|
||||
}
|
||||
elsif ($qerr =~ /(?:quota root does not exist|quotas? (?:are |is )?not enabled)/i) {
|
||||
return { 'supported' => 1,
|
||||
@@ -1243,6 +1386,27 @@ foreach my $line (split(/\r?\n/, $out)) {
|
||||
return \@rv;
|
||||
}
|
||||
|
||||
=head2 parse_btrfs_subvolume_list_output(output)
|
||||
|
||||
Parses raw output from C<btrfs subvolume list> and returns a hash reference
|
||||
mapping numeric subvolume IDs to filesystem-relative paths. This is used to
|
||||
fill qgroup paths on btrfs-progs versions older than 6.0.1.
|
||||
|
||||
=cut
|
||||
sub parse_btrfs_subvolume_list_output
|
||||
{
|
||||
my ($out) = @_;
|
||||
my %rv;
|
||||
foreach my $line (split(/\r?\n/, $out)) {
|
||||
# The default output ends in "path <path relative to top level>".
|
||||
# Keep the final field intact because Btrfs paths may contain spaces.
|
||||
if ($line =~ /^ID\s+(\d+)\s+.*?\s+path\s+(.*)$/) {
|
||||
$rv{int($1)} = $2;
|
||||
}
|
||||
}
|
||||
return \%rv;
|
||||
}
|
||||
|
||||
=head2 list_btrfs_qgroups(path, [sync], [&error])
|
||||
|
||||
Returns an array reference containing all Btrfs qgroups on the filesystem
|
||||
@@ -1272,6 +1436,22 @@ if (!@$rv && $out =~ /\S/) {
|
||||
$$errref = "Unable to parse Btrfs qgroup output" if ($errref);
|
||||
return undef;
|
||||
}
|
||||
# qgroup paths were not printed by default until btrfs-progs 6.0.1. Populate
|
||||
# missing level-0 paths from the long-established subvolume-list output so
|
||||
# callers can keep identifying subvolumes by path on supported older systems.
|
||||
if (grep { $_->{'id'} =~ /^0\/(\d+)$/ && $_->{'path'} eq '' } @$rv) {
|
||||
my ($subvolout) = &run_btrfs_command(
|
||||
0, "subvolume", "list", $path);
|
||||
if (defined($subvolout)) {
|
||||
my $paths = &parse_btrfs_subvolume_list_output($subvolout);
|
||||
foreach my $q (@$rv) {
|
||||
if ($q->{'id'} =~ /^0\/(\d+)$/ && $q->{'path'} eq '' &&
|
||||
defined($paths->{$1})) {
|
||||
$q->{'path'} = $paths->{$1};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$$errref = undef if ($errref);
|
||||
return $rv;
|
||||
}
|
||||
|
||||
91
quota/list_btrfs.cgi
Executable file
91
quota/list_btrfs.cgi
Executable file
@@ -0,0 +1,91 @@
|
||||
#!/usr/local/bin/perl
|
||||
# Display Btrfs quota status and subvolume qgroups
|
||||
|
||||
require './quota-lib.pl';
|
||||
&ReadParse();
|
||||
$dir = $in{'dir'};
|
||||
|
||||
# Restrict the page to allowed paths on mounted Btrfs filesystems.
|
||||
&can_edit_btrfs_filesys($dir) || &error($text{'btrfs_eallow'});
|
||||
defined(&btrfs_quota_status) && &is_btrfs_fs($dir) ||
|
||||
&error($text{'btrfs_enotbtrfs'});
|
||||
&error_setup($text{'btrfs_efailed'});
|
||||
|
||||
# Read quota status before building the status and qgroup tables.
|
||||
$status = &btrfs_quota_status($dir);
|
||||
$status || &error($text{'btrfs_enotbtrfs'});
|
||||
&error($status->{'error'}) if ($status->{'error'});
|
||||
|
||||
&ui_print_header(undef, $text{'btrfs_title'}, "", "btrfs");
|
||||
|
||||
# Map the command's accounting mode to a user-facing label.
|
||||
$mode = $status->{'mode'} eq "squota" ? $text{'btrfs_simple'} :
|
||||
$status->{'mode'} eq "qgroup" ? $text{'btrfs_full'} :
|
||||
$text{'btrfs_unknown'};
|
||||
|
||||
# Display the current enablement, accounting mode and consistency state.
|
||||
print &ui_table_start(&text('btrfs_status_header', &html_escape($dir)),
|
||||
"width=100%", 2);
|
||||
print &ui_table_row($text{'btrfs_status'},
|
||||
$status->{'enabled'} ? $text{'btrfs_enabled'} : $text{'btrfs_disabled'});
|
||||
print &ui_table_row($text{'btrfs_mode'}, $mode) if ($status->{'enabled'});
|
||||
# Show consistency only when btrfs-progs or the kernel reports it.
|
||||
if (defined($status->{'inconsistent'})) {
|
||||
print &ui_table_row($text{'btrfs_consistency'},
|
||||
$status->{'inconsistent'} ? $text{'btrfs_inconsistent'} :
|
||||
$text{'btrfs_consistent'});
|
||||
}
|
||||
print &ui_table_end();
|
||||
|
||||
# A disabled filesystem has no qgroups to list or edit.
|
||||
if (!$status->{'enabled'}) {
|
||||
&ui_print_footer("", $text{'btrfs_return'});
|
||||
exit;
|
||||
}
|
||||
|
||||
# Load all qgroups and start the usage and limit table.
|
||||
$qgroups = &list_btrfs_qgroups($dir, 0, \$listerr);
|
||||
&error($listerr) if (!$qgroups);
|
||||
print &ui_columns_start([
|
||||
$text{'btrfs_qgroup'},
|
||||
$text{'btrfs_path'},
|
||||
$text{'btrfs_referenced'},
|
||||
$text{'btrfs_exclusive'},
|
||||
$text{'btrfs_max_referenced'},
|
||||
$text{'btrfs_max_exclusive'},
|
||||
], 100, 0, undef, $text{'btrfs_qgroups'});
|
||||
foreach $q (@$qgroups) {
|
||||
# Read-only users and the filesystem-wide top-level qgroup get no edit
|
||||
# link. Limiting 0/5 can block Webmin from changing the limit back.
|
||||
$qid = &ui_tag("tt", &html_escape($q->{'id'}));
|
||||
chomp($qid);
|
||||
if (!$access{'ro'} && $q->{'id'} ne "0/5") {
|
||||
$qid = &ui_link("edit_btrfs.cgi?dir=".&urlize($dir).
|
||||
"&qgroup=".&urlize($q->{'id'}), $qid);
|
||||
}
|
||||
|
||||
# Display usage and use the standard unlimited label for missing limits.
|
||||
print &ui_columns_row([
|
||||
$qid,
|
||||
$q->{'path'} ne "" ? &html_escape($q->{'path'}) : "-",
|
||||
&nice_size($q->{'referenced'}),
|
||||
&nice_size($q->{'exclusive'}),
|
||||
defined($q->{'max_referenced'}) ?
|
||||
&nice_size($q->{'max_referenced'}) : $text{'quota_unlimited'},
|
||||
defined($q->{'max_exclusive'}) ?
|
||||
&nice_size($q->{'max_exclusive'}) : $text{'quota_unlimited'},
|
||||
]);
|
||||
}
|
||||
print &ui_columns_end();
|
||||
|
||||
# Full accounting supports rescanning; simple accounting deliberately hides it.
|
||||
if (!$access{'ro'} && $access{'enable'} && $status->{'mode'} ne "squota") {
|
||||
print &ui_hr();
|
||||
print &ui_buttons_start();
|
||||
print &ui_buttons_row("btrfs_action.cgi", $text{'btrfs_rescan'},
|
||||
$text{'btrfs_rescan_desc'},
|
||||
[ [ "dir", $dir ], [ "action", "rescan" ] ]);
|
||||
print &ui_buttons_end();
|
||||
}
|
||||
|
||||
&ui_print_footer("", $text{'btrfs_return'});
|
||||
@@ -3,5 +3,5 @@ category=system
|
||||
os_support=solaris *-linux hpux freebsd unixware openbsd irix netbsd macos
|
||||
desc=Disk Quotas
|
||||
depends=mount
|
||||
longdesc=Setup and edit user or group disk quotas for local filesystems.
|
||||
longdesc=Setup and edit user, group and Btrfs subvolume quotas for local filesystems.
|
||||
readonly=1
|
||||
|
||||
@@ -82,6 +82,32 @@ if (defined("a_possible)) {
|
||||
return grep { $_->[4] || $_->[6] } @mtab;
|
||||
}
|
||||
|
||||
=head2 list_btrfs_filesystems
|
||||
|
||||
Returns one entry for each mounted Btrfs filesystem when the OS library
|
||||
provides the Btrfs quota API. Command availability and quota status are checked
|
||||
separately by callers; on systems without the API, this function returns an
|
||||
empty list.
|
||||
|
||||
=cut
|
||||
sub list_btrfs_filesystems
|
||||
{
|
||||
# The OS-specific library determines whether Btrfs quota operations exist.
|
||||
return ( ) if (!defined(&btrfs_quota_status));
|
||||
|
||||
# Separately mounted subvolumes share quota state, so keep only the first mount
|
||||
# for each underlying source while preserving the original display order.
|
||||
my %seen;
|
||||
my @filesystems;
|
||||
foreach my $fs (&mount::list_mounted()) {
|
||||
next if ($fs->[2] ne "btrfs");
|
||||
(my $source = $fs->[1]) =~ s/\[[^\]]*\]$//;
|
||||
next if ($seen{$source}++);
|
||||
push(@filesystems, $fs);
|
||||
}
|
||||
return @filesystems;
|
||||
}
|
||||
|
||||
=head2 parse_options(type, options)
|
||||
|
||||
Convert an options string for some filesystem into the global hash %options.
|
||||
@@ -412,6 +438,20 @@ foreach $fs (split(/\s+/, $access{'filesys'})) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
=head2 can_edit_btrfs_filesys(filesys)
|
||||
|
||||
Returns 1 if the current Webmin user can manage Btrfs quotas. Btrfs quota state
|
||||
and qgroup IDs belong to the whole underlying filesystem, so a mount-path ACL
|
||||
cannot safely confine access to one separately mounted subvolume.
|
||||
|
||||
=cut
|
||||
sub can_edit_btrfs_filesys
|
||||
{
|
||||
my ($filesys) = @_;
|
||||
return 0 if (!&can_edit_filesys($filesys));
|
||||
return scalar(grep { $_ eq "*" } split(/\s+/, $access{'filesys'})) ? 1 : 0;
|
||||
}
|
||||
|
||||
=head2 can_edit_user(user)
|
||||
|
||||
Returns 1 if the current Webmin user can manage quotas for some Unix user.
|
||||
|
||||
86
quota/save_btrfs.cgi
Executable file
86
quota/save_btrfs.cgi
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/local/bin/perl
|
||||
# Save the limits for a Btrfs qgroup
|
||||
|
||||
require './quota-lib.pl';
|
||||
&ReadParse();
|
||||
$dir = $in{'dir'};
|
||||
|
||||
# Require write access to an allowed mounted Btrfs filesystem and reject
|
||||
# malformed qgroup IDs before parsing or applying limits.
|
||||
$access{'ro'} && &error($text{'btrfs_eedit'});
|
||||
&can_edit_btrfs_filesys($dir) || &error($text{'btrfs_eallow'});
|
||||
defined(&btrfs_quota_status) && &is_btrfs_fs($dir) ||
|
||||
&error($text{'btrfs_enotbtrfs'});
|
||||
&valid_btrfs_qgroup_id($in{'qgroup'}) || &error($text{'btrfs_eqgroup'});
|
||||
$in{'qgroup'} eq "0/5" && &error($text{'btrfs_etoplevel'});
|
||||
&error_setup($text{'btrfs_esave'});
|
||||
|
||||
# parse_limit(name)
|
||||
# Parse one optional byte limit from quota_input and validate its unit factor.
|
||||
sub parse_limit
|
||||
{
|
||||
my ($name) = @_;
|
||||
|
||||
# A selected default means that this limit should be removed.
|
||||
return undef if ($in{$name."_def"});
|
||||
|
||||
# Accept only positive decimal values and units offered by ui_bytesbox.
|
||||
$in{$name} =~ /^\d+(?:\.\d+)?$/ && $in{$name} > 0 ||
|
||||
&error($text{'btrfs_elimit'});
|
||||
local %units = map { $_, 1 } ( 1, 1024, 1024**2, 1024**3,
|
||||
1024**4, 1024**5 );
|
||||
$units{$in{$name."_units"}} || &error($text{'btrfs_elimit'});
|
||||
return int($in{$name} * $in{$name."_units"});
|
||||
}
|
||||
|
||||
# Parse both limits completely before making either filesystem change.
|
||||
$max_referenced = &parse_limit("max_referenced");
|
||||
$max_exclusive = &parse_limit("max_exclusive");
|
||||
|
||||
# Apply the existing ACL ceiling, converting its KiB value to bytes.
|
||||
if ($access{'maxblocks'}) {
|
||||
$maxbytes = $access{'maxblocks'} * 1024;
|
||||
defined($max_referenced) && $max_referenced <= $maxbytes &&
|
||||
defined($max_exclusive) && $max_exclusive <= $maxbytes ||
|
||||
&error(&text('btrfs_emax', &nice_size($maxbytes)));
|
||||
}
|
||||
|
||||
# Refresh the selected qgroup so unchanged limits are not re-applied. This
|
||||
# lookup only needs the stored limit values, so no filesystem sync is needed.
|
||||
$qgroups = &list_btrfs_qgroups($dir, 0, \$listerr);
|
||||
&error($listerr) if (!$qgroups);
|
||||
($qgroup) = grep { $_->{'id'} eq $in{'qgroup'} } @$qgroups;
|
||||
$qgroup || &error($text{'btrfs_eqgroup'});
|
||||
|
||||
# same_limit(first, second)
|
||||
# Returns true when two optional byte limits are identical.
|
||||
sub same_limit
|
||||
{
|
||||
my ($first, $second) = @_;
|
||||
return !defined($first) && !defined($second) ||
|
||||
defined($first) && defined($second) && $first == $second;
|
||||
}
|
||||
|
||||
# Compare the submitted referenced and exclusive limits with their current
|
||||
# values before running either Btrfs command.
|
||||
$same_referenced = &same_limit(
|
||||
$max_referenced, $qgroup->{'max_referenced'});
|
||||
$same_exclusive = &same_limit(
|
||||
$max_exclusive, $qgroup->{'max_exclusive'});
|
||||
|
||||
# Apply only changed limits so each independent setting is left untouched when
|
||||
# the submitted value already matches it.
|
||||
if (!$same_referenced) {
|
||||
$err = &set_btrfs_qgroup_limit(
|
||||
$dir, $in{'qgroup'}, $max_referenced, 0);
|
||||
&error($err) if ($err);
|
||||
}
|
||||
if (!$same_exclusive) {
|
||||
$err = &set_btrfs_qgroup_limit(
|
||||
$dir, $in{'qgroup'}, $max_exclusive, 1);
|
||||
&error($err) if ($err);
|
||||
}
|
||||
|
||||
# Log the completed update and return to the qgroup list.
|
||||
&webmin_log("save", "btrfs", $in{'qgroup'}, \%in);
|
||||
&redirect("list_btrfs.cgi?dir=".&urlize($dir));
|
||||
@@ -5,6 +5,8 @@ no warnings 'once';
|
||||
use Test::More;
|
||||
use Cwd qw(abs_path);
|
||||
use File::Basename qw(dirname);
|
||||
use File::Path qw(make_path);
|
||||
use File::Temp qw(tempdir);
|
||||
|
||||
my $root = abs_path(dirname(__FILE__)."/../..") or die "rootdir: $!";
|
||||
my @commands;
|
||||
@@ -71,6 +73,9 @@ return @{$main::mounted[0]};
|
||||
|
||||
do "$root/quota/linux-lib.pl" or die "linux-lib.pl: $@ $!";
|
||||
|
||||
# Device-less tmpfs quota options must not create unusable filesystem rows.
|
||||
is(main::quota_can([ "/tmp", "tmpfs", "tmpfs", "rw,usrquota" ], undef),
|
||||
0, "tmpfs quota mount options are ignored");
|
||||
ok(main::is_btrfs_fs("/srv/btrfs"),
|
||||
"Btrfs mount point is detected");
|
||||
ok(main::is_btrfs_fs("/srv/btrfs/domain1"),
|
||||
@@ -82,6 +87,23 @@ ok(!main::is_btrfs_fs("/"),
|
||||
ok(!defined(main::btrfs_quota_status("/")),
|
||||
"quota status is unavailable for non-Btrfs paths");
|
||||
|
||||
my $mountinfo = <<'EOF';
|
||||
24 1 0:20 / / rw,relatime - ext4 /dev/root rw
|
||||
31 24 0:42 /@home /home rw,relatime - btrfs /dev/vdb rw,compress=zstd
|
||||
32 31 0:42 /@home/example/homes/bob /srv/bob rw,relatime - btrfs /dev/vdb rw,compress=zstd
|
||||
EOF
|
||||
my ($mount, $fsroot) = main::parse_btrfs_mountinfo(
|
||||
$mountinfo, "/home/example/homes/alice");
|
||||
is($mount, "/home", "containing Btrfs mount is selected");
|
||||
is($fsroot, '/@home', "mounted Btrfs filesystem root is returned");
|
||||
is(main::btrfs_qgroup_absolute_path(
|
||||
$mount, $fsroot, '@home/example/homes/alice'),
|
||||
"/home/example/homes/alice",
|
||||
"qgroup path is translated through a mounted subvolume root");
|
||||
ok(!defined(main::btrfs_qgroup_absolute_path(
|
||||
$mount, $fsroot, '@var/lib/mysql')),
|
||||
"qgroups outside the mounted filesystem root are ignored");
|
||||
|
||||
my $status_text = <<'EOF';
|
||||
Quotas on /srv/btrfs:
|
||||
Enabled: yes
|
||||
@@ -132,6 +154,27 @@ is(scalar(@commands), 1, "successful status does not run fallback");
|
||||
$status = main::btrfs_quota_status("/srv/btrfs");
|
||||
ok($status->{'enabled'}, "legacy qgroup fallback detects enabled quotas");
|
||||
|
||||
my $sysfs = tempdir(CLEANUP => 1);
|
||||
my $fsuuid = "12345678-1234-1234-1234-123456789abc";
|
||||
make_path("$sysfs/$fsuuid/qgroups");
|
||||
foreach my $pair ([ 'enabled', 1 ], [ 'mode', 'squota' ],
|
||||
[ 'inconsistent', 1 ]) {
|
||||
open(my $fh, '>', "$sysfs/$fsuuid/qgroups/$pair->[0]") or die $!;
|
||||
print {$fh} "$pair->[1]\n";
|
||||
close($fh);
|
||||
}
|
||||
local $main::btrfs_sysfs_root = $sysfs;
|
||||
@responses = (
|
||||
{ 'out' => "ERROR: unknown token 'status'\n", 'status' => 1 },
|
||||
{ 'out' => "qgroupid rfer excl\n0/5 16384 16384\n", 'status' => 0 },
|
||||
{ 'out' => "Label: none uuid: $fsuuid\n", 'status' => 0 },
|
||||
);
|
||||
$status = main::btrfs_quota_status("/srv/btrfs");
|
||||
is($status->{'mode'}, 'squota',
|
||||
"legacy fallback reads simple-quota mode from sysfs");
|
||||
ok($status->{'inconsistent'},
|
||||
"legacy fallback reads inconsistent accounting from sysfs");
|
||||
|
||||
@responses = (
|
||||
{ 'out' => "ERROR: unknown token 'status'\n", 'status' => 1 },
|
||||
{ 'out' => "ERROR: quota root does not exist\n", 'status' => 1 },
|
||||
@@ -173,6 +216,27 @@ ok(!defined($list_error), "successful qgroup list clears the error");
|
||||
like($commands[0], qr/qgroup show .*\\-\\-sync .*srv.*btrfs/,
|
||||
"synchronized qgroup listing requests --sync");
|
||||
|
||||
my $legacy_qgroup_text = <<'EOF';
|
||||
Qgroupid Referenced Exclusive Max_referenced Max_exclusive Parent Child
|
||||
0/256 16384 16384 67108864 none 1/100 -
|
||||
0/257 0 0 none 33554432 1/100 -
|
||||
1/100 16384 16384 100663296 none - 0/256,0/257
|
||||
EOF
|
||||
@commands = ( );
|
||||
@responses = (
|
||||
{ 'out' => $legacy_qgroup_text, 'status' => 0 },
|
||||
{ 'out' => "ID 256 gen 10 top level 5 path domain1\n".
|
||||
"ID 257 gen 11 top level 5 path domain path two\n",
|
||||
'status' => 0 },
|
||||
);
|
||||
$qgroups = main::list_btrfs_qgroups("/srv/btrfs", 0, \$list_error);
|
||||
is($qgroups->[0]->{'path'}, "domain1",
|
||||
"legacy qgroup rows gain paths from the subvolume list");
|
||||
is($qgroups->[1]->{'path'}, "domain path two",
|
||||
"legacy subvolume paths containing spaces and path are preserved");
|
||||
like($commands[1], qr/subvolume list .*srv.*btrfs/,
|
||||
"legacy qgroup output triggers one compatibility lookup");
|
||||
|
||||
@responses = ({ 'out' => "ERROR: quotas not enabled\n", 'status' => 1 });
|
||||
$qgroups = main::list_btrfs_qgroups("/srv/btrfs", 0, \$list_error);
|
||||
ok(!defined($qgroups), "failed qgroup listing returns undef");
|
||||
@@ -240,6 +304,13 @@ is(main::set_btrfs_qgroup_limit("/srv/btrfs", "1/100", "1M"),
|
||||
is(main::assign_btrfs_qgroup("/srv/btrfs", "bad", "1/100"),
|
||||
"Invalid child Btrfs qgroup ID", "invalid child assignment is rejected");
|
||||
|
||||
@commands = ( );
|
||||
@responses = ({ 'out' => "ERROR: unable to limit requested quota group: ".
|
||||
"Disk quota exceeded\n", 'status' => 1 });
|
||||
like(main::set_btrfs_qgroup_limit("/srv/btrfs", "1/100", 2097152),
|
||||
qr/Disk quota exceeded/, "qgroup limit errors are returned without retry");
|
||||
is(scalar(@commands), 1, "a failed qgroup limit command is not retried");
|
||||
|
||||
@responses = ({ 'out' => "ERROR: qgroup exists\n", 'status' => 1 });
|
||||
is(main::create_btrfs_qgroup("/srv/btrfs", "1/100"),
|
||||
"ERROR: qgroup exists", "Btrfs command errors are returned to callers");
|
||||
|
||||
Reference in New Issue
Block a user