mirror of
https://github.com/webmin/webmin.git
synced 2026-08-22 06:50:34 +01:00
Add Btrfs subvolume quota management
Add Btrfs subvolume quota support to the Disk Quotas module, including full and simple accounting modes, quota lifecycle actions, and qgroup usage and limit management. Includes ACL enforcement, compatibility with older btrfs-progs versions, destructive-action confirmation, top-level qgroup protection, help, and tests.
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
|
||||
* Update the Authentic theme to the latest version with various improvements:
|
||||
- Fix disallowed entry handling in File Manager [forum.virtualmin.com/t/137654](https://forum.virtualmin.com/t/extra-admin-file-manager-permissions/137654?u=ilia)
|
||||
|
||||
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,15,btrfs_mode
|
||||
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
|
||||
|
||||
36
quota/config_info.pl
Executable file
36
quota/config_info.pl
Executable file
@@ -0,0 +1,36 @@
|
||||
# Build and parse the Btrfs-specific module configuration field.
|
||||
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);
|
||||
}
|
||||
|
||||
# show_btrfs_mode(mode)
|
||||
# Display the accounting mode selector, defaulting unknown values to full mode.
|
||||
sub show_btrfs_mode
|
||||
{
|
||||
my ($mode) = @_;
|
||||
$mode = "full" if ($mode ne "simple");
|
||||
return &ui_radio("btrfs_mode", $mode,
|
||||
[ [ "full", $text{'config_btrfs_full'} ],
|
||||
[ "simple", $text{'config_btrfs_simple'} ] ]);
|
||||
}
|
||||
|
||||
# parse_btrfs_mode()
|
||||
# Store only a supported mode and fall back to full accounting otherwise.
|
||||
sub parse_btrfs_mode
|
||||
{
|
||||
return $in{'btrfs_mode'} eq "simple" ? "simple" : "full";
|
||||
}
|
||||
|
||||
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'});
|
||||
53
quota/help/btrfs.html
Normal file
53
quota/help/btrfs.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<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>
|
||||
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. <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,59 @@ 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
|
||||
|
||||
config_btrfs_full=Full accounting (recommended; tracks shared space and snapshots)
|
||||
config_btrfs_simple=Simple accounting (lower overhead; tracks original ownership only)
|
||||
|
||||
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=Referenced usage includes shared data reachable from the subvolume. Exclusive usage is the space that would be freed with it.
|
||||
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+)(?=.*\[\/)\'') );
|
||||
@@ -1168,6 +1174,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 +1247,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 +1308,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 +1358,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;
|
||||
}
|
||||
@@ -1380,8 +1482,9 @@ return defined($_[0]) && $_[0] =~ /^\d+\/\d+$/ ? 1 : 0;
|
||||
|
||||
Sets the referenced or exclusive byte limit for a Btrfs qgroup. If qgroup is
|
||||
undef, path must be a subvolume and its level-0 qgroup is changed. If bytes
|
||||
is undef, the limit is removed. Returns undef on success or an error message
|
||||
on failure.
|
||||
is undef, the limit is removed. If an exceeded limit blocks the update, quota
|
||||
enforcement is temporarily overridden for one retry. Returns undef on success
|
||||
or an error message on failure.
|
||||
|
||||
=cut
|
||||
sub set_btrfs_qgroup_limit
|
||||
@@ -1398,6 +1501,43 @@ push(@args, defined($bytes) ? $bytes : "none");
|
||||
push(@args, $qgroup) if (defined($qgroup));
|
||||
push(@args, $path);
|
||||
my ($out, $err) = &run_btrfs_command(1, @args);
|
||||
|
||||
# An exceeded qgroup can block the metadata write needed to raise or remove its
|
||||
# own limit. Retry once with the kernel's administrative override, preserving
|
||||
# the previous state and restoring enforcement immediately after the command.
|
||||
if ($err && $err =~ /disk quota exceeded/i) {
|
||||
my $uuid = &btrfs_filesystem_uuid($path);
|
||||
my $sysfs = $btrfs_sysfs_root || "/sys/fs/btrfs";
|
||||
my $override = $uuid ? "$sysfs/$uuid/quota_override" : undef;
|
||||
my $current;
|
||||
if ($override && open(my $fh, "<", $override)) {
|
||||
$current = <$fh>;
|
||||
close($fh);
|
||||
$current =~ s/\s+//g if (defined($current));
|
||||
}
|
||||
|
||||
# Only change a known disabled override; an active or unreadable setting
|
||||
# belongs to the administrator and must not be altered here.
|
||||
if (defined($current) && $current eq "0") {
|
||||
my $enabled;
|
||||
if (open(my $fh, ">", $override)) {
|
||||
my $written = syswrite($fh, "1\n");
|
||||
$enabled = defined($written) && $written == 2;
|
||||
close($fh);
|
||||
}
|
||||
if ($enabled) {
|
||||
($out, $err) = &run_btrfs_command(1, @args);
|
||||
my $restored;
|
||||
if (open(my $fh, ">", $override)) {
|
||||
my $written = syswrite($fh, "0\n");
|
||||
$restored = defined($written) && $written == 2;
|
||||
close($fh);
|
||||
}
|
||||
return "Failed to restore Btrfs quota enforcement"
|
||||
if (!$restored);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $err;
|
||||
}
|
||||
|
||||
|
||||
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, keeping an unrelated existing over-limit setting
|
||||
# from causing Btrfs to reject an otherwise valid update.
|
||||
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"),
|
||||
@@ -132,6 +137,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 +199,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 +287,31 @@ 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");
|
||||
|
||||
# An existing over-limit qgroup must not prevent an administrator from raising
|
||||
# or removing its limit. The kernel override is restored after the retry.
|
||||
my $override_root = tempdir(CLEANUP => 1);
|
||||
my $override_uuid = "abcdef01-2345-6789-abcd-ef0123456789";
|
||||
make_path("$override_root/$override_uuid");
|
||||
open(my $override_fh, '>',
|
||||
"$override_root/$override_uuid/quota_override") or die $!;
|
||||
print {$override_fh} "0\n";
|
||||
close($override_fh);
|
||||
local $main::btrfs_sysfs_root = $override_root;
|
||||
@commands = ( );
|
||||
@responses = (
|
||||
{ 'out' => "ERROR: unable to limit requested quota group: ".
|
||||
"Disk quota exceeded\n", 'status' => 1 },
|
||||
{ 'out' => "Label: none uuid: $override_uuid\n", 'status' => 0 },
|
||||
{ 'out' => "", 'status' => 0 },
|
||||
);
|
||||
is(main::set_btrfs_qgroup_limit("/srv/btrfs", "1/100", 2097152),
|
||||
undef, "over-limit qgroup updates retry with the administrative override");
|
||||
open($override_fh, '<', "$override_root/$override_uuid/quota_override")
|
||||
or die $!;
|
||||
is(<$override_fh>, "0\n", "quota enforcement is restored after the retry");
|
||||
close($override_fh);
|
||||
is(scalar(@commands), 3, "one status lookup and one limit retry are run");
|
||||
|
||||
@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