Add ability to use vfsv1 for newly created Linux quota files
This PR adds ability to use the `vfsv1` quota format for newly created user and group quota files when supported by quota-tools 4 or newer. This removes the `vfsv0` range limitation while preserving existing quota files and configured journal formats. Quota creation remains scoped to the requested user or group type, safely normalizes configured `quotacheck` flags, and retains `vfsv0` and legacy fallbacks for compatibility. Addresses issue report in: https://forum.virtualmin.com/t/quota-out-of-range/137331?u=ilia
This commit is contained in:
+24
-1
@@ -1924,7 +1924,30 @@ elsif ($_[0] =~ /^ext\d+$/) {
|
||||
($u, $g) = ("usrjquota", "grpjquota");
|
||||
$jufile ||= "aquota.user";
|
||||
$jgfile ||= "aquota.group";
|
||||
$options{"jqfmt"} = "vfsv0";
|
||||
if (!$options{"jqfmt"}) {
|
||||
# Keep the format of existing external quota files when
|
||||
# switching them to journaled quotas.
|
||||
my $jqfmt;
|
||||
foreach my $qfile ($jufile, $jgfile) {
|
||||
next if (!-s "$_[2]/$qfile");
|
||||
if (open(my $qfh, "<", "$_[2]/$qfile")) {
|
||||
my $header;
|
||||
if (read($qfh, $header, 8) == 8) {
|
||||
my (undef, $version) = unpack("V2", $header);
|
||||
$jqfmt = $version == 0 ? "vfsv0" :
|
||||
$version == 1 ? "vfsv1" : undef;
|
||||
}
|
||||
close($qfh);
|
||||
}
|
||||
last if ($jqfmt);
|
||||
}
|
||||
if (!$jqfmt) {
|
||||
my $qver = &backquote_command("quota -V 2>&1");
|
||||
$jqfmt = $qver =~ /\s(\d+)\.\d+/ && $1 >= 4 ?
|
||||
"vfsv1" : "vfsv0";
|
||||
}
|
||||
$options{"jqfmt"} = $jqfmt;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$jufile = "";
|
||||
|
||||
+55
-25
@@ -295,15 +295,22 @@ if ($out =~ /\s(\d+\.\d+)/) {
|
||||
# Force load of quota kernel modules
|
||||
&system_logged("modprobe quota_v2 >/dev/null 2>&1");
|
||||
|
||||
local $fmt = $version >= 2 ? "vfsv0" : "vfsold";
|
||||
# Quota tools 4 and later support 64-bit limits in vfsv1 files
|
||||
local $fmt = $version >= 4 ? "vfsv1" :
|
||||
$version >= 2 ? "vfsv0" : "vfsold";
|
||||
local $hidden = &hidden_ext_quota_mode($_[0]);
|
||||
if ($_[1]%2 == 1) {
|
||||
# turn on user quotas
|
||||
local $qf = $version >= 2 ? "aquota.user" : "quota.user";
|
||||
if (!-s "$_[0]/$qf" && !($hidden & 1)) {
|
||||
local $legacy = $version >= 4 && !($hidden & 1) &&
|
||||
$qf ne "quota.user" &&
|
||||
!-s "$_[0]/$qf" &&
|
||||
-s "$_[0]/quota.user";
|
||||
if (!-s "$_[0]/$qf" && !$legacy && !($hidden & 1)) {
|
||||
# Setting up for the first time
|
||||
local $ok = 0;
|
||||
if (&has_command("convertquota") && $version >= 2) {
|
||||
if (&has_command("convertquota") && $version >= 2 &&
|
||||
$version < 4) {
|
||||
# Try creating a quota.user file and converting it
|
||||
&open_tempfile(QUOTAFILE, ">>$_[0]/quota.user", 0, 1);
|
||||
&close_tempfile(QUOTAFILE);
|
||||
@@ -321,24 +328,32 @@ if ($_[1]%2 == 1) {
|
||||
&set_ownership_permissions(undef, undef, 0600,
|
||||
"$_[0]/$qf");
|
||||
}
|
||||
&run_quotacheck($_[0]) ||
|
||||
&run_quotacheck($_[0], "-u -f") ||
|
||||
&run_quotacheck($_[0], "-u -f -m") ||
|
||||
&run_quotacheck($_[0], "-u -f -m -c") ||
|
||||
&run_quotacheck($_[0], "-u -f -m -c -F $fmt");
|
||||
local $fflag = $fmt eq "vfsv1" ? " -F $fmt" : "";
|
||||
$ok = &run_quotacheck($_[0], "-u$fflag") ||
|
||||
&run_quotacheck($_[0], "-u -f$fflag") ||
|
||||
&run_quotacheck($_[0], "-u -f -m$fflag") ||
|
||||
&run_quotacheck($_[0], "-u -f -m -c$fflag");
|
||||
&run_quotacheck($_[0], "-u -f -m -c -F ".
|
||||
($fmt eq "vfsv1" ? "vfsv0" : $fmt)) if (!$ok);
|
||||
}
|
||||
}
|
||||
$out = &backquote_logged(
|
||||
"$config{'user_quotaon_command'} ".quotemeta($_[0])." 2>&1");
|
||||
local $fflag = $legacy ? " -F vfsold" : "";
|
||||
$out = &backquote_logged("$config{'user_quotaon_command'}$fflag ".
|
||||
quotemeta($_[0])." 2>&1");
|
||||
if ($?) { return $out; }
|
||||
}
|
||||
if ($_[1] > 1) {
|
||||
# turn on group quotas
|
||||
local $qf = $version >= 2 ? "aquota.group" : "quota.group";
|
||||
if (!-s "$_[0]/$qf" && !($hidden & 2)) {
|
||||
local $legacy = $version >= 4 && !($hidden & 2) &&
|
||||
$qf ne "quota.group" &&
|
||||
!-s "$_[0]/$qf" &&
|
||||
-s "$_[0]/quota.group";
|
||||
if (!-s "$_[0]/$qf" && !$legacy && !($hidden & 2)) {
|
||||
# Setting up for the first time
|
||||
local $ok = 0;
|
||||
if (!$ok && &has_command("convertquota") && $version >= 2) {
|
||||
if (!$ok && &has_command("convertquota") && $version >= 2 &&
|
||||
$version < 4) {
|
||||
# Try creating a quota.group file and converting it
|
||||
&open_tempfile(QUOTAFILE, ">>$_[0]/quota.group", 0, 1);
|
||||
&close_tempfile(QUOTAFILE);
|
||||
@@ -356,15 +371,18 @@ if ($_[1] > 1) {
|
||||
&set_ownership_permissions(undef, undef, 0600,
|
||||
"$_[0]/$qf");
|
||||
}
|
||||
&run_quotacheck($_[0]) ||
|
||||
&run_quotacheck($_[0], "-g -f") ||
|
||||
&run_quotacheck($_[0], "-g -f -m") ||
|
||||
&run_quotacheck($_[0], "-g -f -m -c") ||
|
||||
&run_quotacheck($_[0], "-g -f -m -c -F $fmt");
|
||||
local $fflag = $fmt eq "vfsv1" ? " -F $fmt" : "";
|
||||
$ok = &run_quotacheck($_[0], "-g$fflag") ||
|
||||
&run_quotacheck($_[0], "-g -f$fflag") ||
|
||||
&run_quotacheck($_[0], "-g -f -m$fflag") ||
|
||||
&run_quotacheck($_[0], "-g -f -m -c$fflag");
|
||||
&run_quotacheck($_[0], "-g -f -m -c -F ".
|
||||
($fmt eq "vfsv1" ? "vfsv0" : $fmt)) if (!$ok);
|
||||
}
|
||||
}
|
||||
$out = &backquote_logged(
|
||||
"$config{'group_quotaon_command'} ".quotemeta($_[0])." 2>&1");
|
||||
local $fflag = $legacy ? " -F vfsold" : "";
|
||||
$out = &backquote_logged("$config{'group_quotaon_command'}$fflag ".
|
||||
quotemeta($_[0])." 2>&1");
|
||||
if ($?) { return $out; }
|
||||
}
|
||||
return undef;
|
||||
@@ -379,8 +397,10 @@ Runs the quotacheck command on some filesystem, and returns 1 on success or
|
||||
sub run_quotacheck
|
||||
{
|
||||
&clean_language();
|
||||
local $cmd = $config{'quotacheck_command'};
|
||||
$cmd =~ s/\s+-[ug]+(?=\s|$)//g;
|
||||
local $out = &backquote_logged(
|
||||
"$config{'quotacheck_command'} $_[1] ".quotemeta($_[0])." 2>&1");
|
||||
"$cmd $_[1] ".quotemeta($_[0])." 2>&1");
|
||||
&reset_environment();
|
||||
return $? || $out =~ /cannot guess|cannot remount|cannot find|please stop/i ? 0 : 1;
|
||||
}
|
||||
@@ -773,18 +793,28 @@ if ($_[1] == 0 || $_[1] == 2) {
|
||||
&unlink_file("$_[0]/aquota.group.new");
|
||||
}
|
||||
local $cmd = $config{'quotacheck_command'};
|
||||
$cmd =~ s/\s+-[ug]//g;
|
||||
$cmd =~ s/\s+-[ug]+(?=\s|$)//g;
|
||||
local $flag = $_[1] == 1 ? "-u" : $_[1] == 2 ? "-g" : "-u -g";
|
||||
$out = &backquote_logged("$cmd $flag ".quotemeta($_[0])." 2>&1");
|
||||
local $new = $_[1] == 1 ?
|
||||
!-s "$_[0]/aquota.user" && !-s "$_[0]/quota.user" :
|
||||
$_[1] == 2 ?
|
||||
!-s "$_[0]/aquota.group" && !-s "$_[0]/quota.group" :
|
||||
!-s "$_[0]/aquota.user" && !-s "$_[0]/quota.user" &&
|
||||
!-s "$_[0]/aquota.group" && !-s "$_[0]/quota.group";
|
||||
local $qver = $new ? &backquote_command("quota -V 2>&1") : "";
|
||||
local $fmt = $new && $qver =~ /\s(\d+)\.\d+/ && $1 >= 4 ? "vfsv1" : undef;
|
||||
local $fflag = $fmt ? " -F $fmt" : "";
|
||||
$out = &backquote_logged("$cmd $flag$fflag ".quotemeta($_[0])." 2>&1");
|
||||
if ($?) {
|
||||
# Try with the -f and -m options
|
||||
$out = &backquote_logged(
|
||||
"$cmd $flag -f -m ".quotemeta($_[0])." 2>&1");
|
||||
"$cmd $flag -f -m$fflag ".quotemeta($_[0])." 2>&1");
|
||||
if ($?) {
|
||||
# Try with the -F option
|
||||
foreach my $fmt ("vfsv1", "vfsv0", "vfsold") {
|
||||
foreach my $tryfmt ($fmt ? ("vfsv0", "vfsold") :
|
||||
("vfsv1", "vfsv0", "vfsold")) {
|
||||
$out = &backquote_logged(
|
||||
"$cmd $flag -f -m -F $fmt ".quotemeta($_[0])." 2>&1");
|
||||
"$cmd $flag -f -m -F $tryfmt ".quotemeta($_[0])." 2>&1");
|
||||
last if (!$?);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ return $_[0] eq "btrfs" ? "/usr/bin/btrfs" : undef;
|
||||
|
||||
sub clean_language { }
|
||||
sub reset_environment { }
|
||||
sub is_readonly_mode { return 0; }
|
||||
sub system_logged { return 0; }
|
||||
sub unlink_file { return unlink($_[0]); }
|
||||
|
||||
sub is_under_directory
|
||||
{
|
||||
@@ -73,6 +76,134 @@ return @{$main::mounted[0]};
|
||||
|
||||
do "$root/quota/linux-lib.pl" or die "linux-lib.pl: $@ $!";
|
||||
|
||||
$main::config{'quotacheck_command'} = "quotacheck -ug";
|
||||
$main::config{'user_quotaon_command'} = "quotaon -u";
|
||||
$main::config{'group_quotaon_command'} = "quotaon -g";
|
||||
|
||||
my $newquota = tempdir(CLEANUP => 1);
|
||||
@commands = ( );
|
||||
@responses = (
|
||||
{ 'out' => "Quota utilities version 4.06.\n", 'status' => 0 },
|
||||
{ 'out' => "", 'status' => 0 },
|
||||
);
|
||||
is(main::quotacheck($newquota, 1), undef,
|
||||
"new user quota file can be checked");
|
||||
like($commands[1], qr/quotacheck -u -F vfsv1 /,
|
||||
"combined configured flags are replaced and new files prefer vfsv1");
|
||||
unlike($commands[1], qr/ -g(?: |$)/,
|
||||
"user quota check does not also create group quotas");
|
||||
|
||||
my $oldquota = tempdir(CLEANUP => 1);
|
||||
open(my $oldfh, '>', "$oldquota/aquota.user") or die $!;
|
||||
print {$oldfh} "existing\n";
|
||||
close($oldfh);
|
||||
@commands = ( );
|
||||
@responses = ({ 'out' => "", 'status' => 0 });
|
||||
is(main::quotacheck($oldquota, 1), undef,
|
||||
"existing user quota file can be checked");
|
||||
is(scalar(@commands), 1,
|
||||
"existing quota check does not probe the quota tools version");
|
||||
unlike($commands[0], qr/ -F /,
|
||||
"existing quota file format is auto-detected");
|
||||
|
||||
my $groupquota = tempdir(CLEANUP => 1);
|
||||
@commands = ( );
|
||||
@responses = (
|
||||
{ 'out' => "Quota utilities version 4.06.\n", 'status' => 0 },
|
||||
{ 'out' => "", 'status' => 0 },
|
||||
);
|
||||
is(main::quotacheck($groupquota, 2), undef,
|
||||
"new group quota file can be checked");
|
||||
like($commands[1], qr/quotacheck -g -F vfsv1 /,
|
||||
"group-only creation also prefers vfsv1");
|
||||
|
||||
my $legacyquota = tempdir(CLEANUP => 1);
|
||||
@commands = ( );
|
||||
@responses = (
|
||||
{ 'out' => "Quota utilities version 3.17.\n", 'status' => 0 },
|
||||
{ 'out' => "", 'status' => 0 },
|
||||
);
|
||||
is(main::quotacheck($legacyquota, 1), undef,
|
||||
"legacy quota tools can create quota files");
|
||||
unlike($commands[1], qr/ -F vfsv1 /,
|
||||
"legacy quota tools retain their default format");
|
||||
|
||||
my $fallbackquota = tempdir(CLEANUP => 1);
|
||||
@commands = ( );
|
||||
@responses = (
|
||||
{ 'out' => "Quota utilities version 4.06.\n", 'status' => 0 },
|
||||
{ 'out' => "failed\n", 'status' => 1 },
|
||||
{ 'out' => "failed\n", 'status' => 1 },
|
||||
{ 'out' => "", 'status' => 0 },
|
||||
);
|
||||
is(main::quotacheck($fallbackquota, 1), undef,
|
||||
"quota check falls back when vfsv1 creation fails");
|
||||
like($commands[3], qr/ -F vfsv0 /,
|
||||
"vfsv0 is the first creation fallback");
|
||||
|
||||
my $activatequota = tempdir(CLEANUP => 1);
|
||||
@commands = ( );
|
||||
@responses = (
|
||||
{ 'out' => "Quota utilities version 4.06.\n", 'status' => 0 },
|
||||
{ 'out' => "", 'status' => 0 },
|
||||
{ 'out' => "", 'status' => 0 },
|
||||
);
|
||||
is(main::quotaon($activatequota, 1), undef,
|
||||
"new user quotas can be activated");
|
||||
like($commands[1], qr/quotacheck -u -F vfsv1 /,
|
||||
"quota activation creates vfsv1 files");
|
||||
unlike($commands[1], qr/ -g(?: |$)/,
|
||||
"user quota activation does not also create group quotas");
|
||||
|
||||
my $legacyfile = tempdir(CLEANUP => 1);
|
||||
open(my $legacyfh, '>', "$legacyfile/quota.user") or die $!;
|
||||
print {$legacyfh} "existing legacy quotas\n";
|
||||
close($legacyfh);
|
||||
@commands = ( );
|
||||
@responses = (
|
||||
{ 'out' => "Quota utilities version 4.06.\n", 'status' => 0 },
|
||||
{ 'out' => "", 'status' => 0 },
|
||||
);
|
||||
is(main::quotaon($legacyfile, 1), undef,
|
||||
"legacy user quota files can be activated");
|
||||
like($commands[1], qr/^quotaon -u -F vfsold /,
|
||||
"legacy user quota files are activated without conversion");
|
||||
ok(-s "$legacyfile/quota.user",
|
||||
"legacy user quota files are preserved");
|
||||
|
||||
my $legacygroup = tempdir(CLEANUP => 1);
|
||||
open(my $legacygfh, '>', "$legacygroup/quota.group") or die $!;
|
||||
print {$legacygfh} "existing legacy quotas\n";
|
||||
close($legacygfh);
|
||||
@commands = ( );
|
||||
@responses = (
|
||||
{ 'out' => "Quota utilities version 4.06.\n", 'status' => 0 },
|
||||
{ 'out' => "", 'status' => 0 },
|
||||
);
|
||||
is(main::quotaon($legacygroup, 2), undef,
|
||||
"legacy group quota files can be activated");
|
||||
like($commands[1], qr/^quotaon -g -F vfsold /,
|
||||
"legacy group quota files are activated without conversion");
|
||||
ok(-s "$legacygroup/quota.group",
|
||||
"legacy group quota files are preserved");
|
||||
|
||||
my $mixedquota = tempdir(CLEANUP => 1);
|
||||
foreach my $file (qw(aquota.user quota.user aquota.group quota.group)) {
|
||||
open(my $mixedfh, '>', "$mixedquota/$file") or die $!;
|
||||
print {$mixedfh} "existing quotas\n";
|
||||
close($mixedfh);
|
||||
}
|
||||
@commands = ( );
|
||||
@responses = (
|
||||
{ 'out' => "Quota utilities version 4.06.\n", 'status' => 0 },
|
||||
{ 'out' => "", 'status' => 0 },
|
||||
{ 'out' => "", 'status' => 0 },
|
||||
);
|
||||
is(main::quotaon($mixedquota, 3), undef,
|
||||
"modern quota files take precedence over stale legacy files");
|
||||
unlike(join("\n", @commands), qr/ -F vfsold /,
|
||||
"stale legacy files do not override modern quota formats");
|
||||
|
||||
# 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");
|
||||
|
||||
Reference in New Issue
Block a user