#!/usr/bin/perl
#
# volcreate-logs -- Create and grant quota to log volumes in AFS.
#
# Written by Russ Allbery <rra@stanford.edu>
# Copyright 2002, 2004, 2011, 2012, 2013
#     The Board of Trustees of the Leland Stanford Junior University
#
# Updated by Adam H. Lewenberg <adamhl@stanford.edu> in 2025, 2026

# This program is free software; you may redistribute it and/or modify it
# under the same terms as Perl itself.

## no critic (ProhibitParensWithBuiltins, RequireCarping)
## no critic (RequireNoMatchVarsWithUseEnglish, ProhibitDoubleSigils)

##############################################################################
# Modules and declarations
##############################################################################

use 5.24.0;
use strict;
use warnings;
use autodie qw(:all !mkdir);

use English;
use Getopt::Long qw(GetOptions);
use IPC::Run qw/ run/ ;
use Readonly;
use YAML::Tiny;

##############################################################################
# Site configuration
##############################################################################

my $VERSION = '3.0 (2026-03-02)';

my $JUSTPRINT;

### Useful constants
###
# The number of KB in one MB.
Readonly my $KB_IN_MB => 1_000;

# The number of KB in 10MB.
Readonly my $KB_IN_10MB => 10_000;

# The number of MB in one GB.
Readonly my $MB_IN_GB => 1_000;

# Once a volume has reached this percent of its quota, increase the quota.
# By what percent we increase the quota when we get close to the limit.
# See also the function "new_quota_kb" below.
Readonly my $QUOTA_PCT_MAX      => 90;
Readonly my $PCT_INCREASE_QUOTA => 0.10;

# We also increase by a fixed amount in units of 10MB.
Readonly my $FIXED_INCREASE_10MB => 1.0;

# No quotas can be larger than this number of MBs.
Readonly my $MAX_QUOTA_MBS => 50_000;

###
### More global configuration

# Set to 1 to show progress messages.
my $VERBOSE = 0;

# The path to the config file specifying what to do.
my $LOGS_CONFIG = '/etc/afs-admin-tools/log-volumes.yaml';

# The path to volcreate, used to create volumes.
my $VOLCREATE = 'volcreate';

# The full path to fs and vos.
my $FS  = '/usr/bin/fs';
my $VOS = '/usr/bin/vos';

my %LOG_CONFIGS;

##############################################################################
# Overrides
##############################################################################

sub exit_with_error {
    my ($msg) = @_;

    print "error: $msg\n";
    exit 1;
}

# Use: pass in an array, returns ($stdout, $stderr, $exit_value)
sub run_command {
    my (@command) = @_ ;

    # (From Russ)
    my ($out, $err) ;
    IPC::Run::run(\@command, q{>}, \$out, q{2>}, \$err) ;

    ## no critic (ProhibitMagicNumbers)
    return ($out, $err, $CHILD_ERROR >> 8) ;
}

sub print_command {
    my (@command) = @_ ;

    print "dry-run: ";
    print join(q{ }, @command);
    print "\n";
    return;
}

##############################################################################
# Reporting
##############################################################################

# Show progress messages if the global VERBOSE message is true.
sub progress {
    my ($msg, $prefix) = @_;

    if ($VERBOSE) {
        # Split into individual lines.
        my @lines = split(/\n/xsm, $msg) ;

        my $header = q{};
        if ($prefix) {
            $header = "[progress/$prefix]";
        } else {
            $header = '[progress]';
        }
        for my $line (@lines) {
            print "$header $line\n";
        }
    }
    return;
}

# Report an action.
sub report {
    my (@parameters) = @_;

    print "report @parameters\n";
    return;
}

##############################################################################
# Volume quota checking
##############################################################################

sub new_quota_kb {
    # Take the current quota in KB and return a new quota that is 10% bigger plus 10MB.

    my ($current_quota_kb) = @_;

    # Convert $current_quota_kb to tens of megabytes
    my $current_quota_10mb = $current_quota_kb / $KB_IN_10MB;

    # Increase this by 10%.
    my $current_quota_10mb_bigger = (1 + $PCT_INCREASE_QUOTA) * $current_quota_10mb ;

    # Increase this by 9MB.
    my $current_quota_10mb_bigger2 = $current_quota_10mb_bigger + $FIXED_INCREASE_10MB;

    # Convert back to kb.
    my $newquota_kb = int($current_quota_10mb_bigger2) * $KB_IN_10MB;

    return $newquota_kb;
}

# Takes the volume name, the full path to a mount point for a volume, and the
# minimum quota that that volume should have.  Increases the quota of the
# volume to at least the minimum and by 10% if the volume is within 90% of its
# quota.
sub check_quota {
    my ($volume, $mountpoint, $minimum_mb) = @_;

    my $msg;
    my @cmd;
    my ($stdout, $stderr, $rc);

    my $minimum_kb = $minimum_mb * $KB_IN_MB;

    # The first line is either an error or headers.  Check to see if it is
    # an error, and if not, ignore it.  The second line is our quota
    # information, in the form volume name, quota, used amount, used
    # percentage, and partition percentage.

    @cmd = ($FS, 'listquota', $mountpoint);

    ($stdout, $stderr, $rc) = run_command(@cmd);

    my @output_lines = split(/\n/xsm, $stdout);

    if ($rc != 0) {
        warn $output_lines[0];
        warn "Unable to get quota information for $volume\n";
        return;
    }

    my ($volname, $quota_kb, $used, $percent) = split (q{ }, $output_lines[1]);
    if ($percent !~ s/%(?:<\<)?$//xsm) {
        warn "Unable to parse: $output_lines[1]";
        warn "Unable to get quota information for $volume\n";
        return;
    } else {
        $msg = "current quota of $mountpoint is $quota_kb at ${percent}% used";
        progress($msg);
    }

    # Figure out if we need to do anything.
    my $newquota_kb = $quota_kb;
    if ($percent > $QUOTA_PCT_MAX) {
        # The volume has grown too close to its quota, so increase
        # the quote by 10% plus an extra 9MB.
        $newquota_kb = new_quota_kb($quota_kb);
        $msg = "volume '$volname' too close to quota; new quota is $newquota_kb";
        progress($msg);
    }

    if ($newquota_kb < $minimum_kb) {
        $newquota_kb = $minimum_kb;
        $msg = "new quota is too small, setting it to the minimum of $minimum_kb KB";
        progress($msg);
    }

    progress("old quota: $quota_kb KB");
    progress("new quota: $newquota_kb KB");

    if ($newquota_kb != $quota_kb) {
        my $quota_mb = $newquota_kb / $KB_IN_MB;
        $msg = "Setting quota to $quota_mb MB for $volume";
        report("$msg\n");
        progress($msg);

        @cmd = ($FS, 'setquota', $mountpoint, $newquota_kb);

        if ($JUSTPRINT) {
            print_command(@cmd);
        } else {
            ($stdout, $stderr, $rc) = run_command(@cmd);
            if ($rc != 0) {
                warn "$FS setquota $mountpoint $newquota_kb exited with status $rc\n";
                warn "Unable to set quota for $volume\n";
            } else {
                $msg = "set quota of $mountpoint to $newquota_kb KB";
                progress($msg);
            }
        }
    } else {
        $msg = 'nothing to do: quota is already set to new quota';
        progress($msg);
    }

    return;
}

##############################################################################
# Finding volumes
##############################################################################

# Given a path, run fs lsmount on it and return the volume name, or undef if
# this path isn't a mount point.
sub lsmount {
    my ($path) = @_;

    my @cmd = ($FS, 'lsmount', $path);
    my ($stdout, $stderr, $rc) = run_command(@cmd);

    if ($stdout =~ /is[ ]a[ ]mount[ ]point[ ]for[ ]volume[ ]\'\#([^\']+)\'/xsm) {
        return $1;
    } else {
        return;
    }
}

# Given the base path and the current month and year, find the full path to
# the volume associated with that month and year.  Returns the full path and
# the volume name in a list.
sub find_volume_month {
    my ($base, $month, $year) = @_;
    $month = sprintf ('%02d', $month);
    my ($volume, $path);
    if (-d "$base/$year/$month") {
        $volume = lsmount ("$base/$year/$month");
        if (defined $volume) {
            $path = "$base/$year/$month";
        }
    }
    return ($volume, $path);
}

# Given the base path and the current year, find the full path to the volume
# associated with that year for logs where we only create one volume per year.
# Returns the full path and the volume name in a list.
sub find_volume_year {
    my ($base, $year) = @_;
    my ($volume, $path);
    if (-d "$base/$year") {
        $volume = lsmount("$base/$year");
        if (defined $volume) {
            $path = "$base/$year";
        }
    }
    return ($volume, $path);
}

##############################################################################
# Creating volumes
##############################################################################

# Given a volume name, mount point, quota (in MB), and flag saying whether to be
# quiet, create that volume.  Performs some basic consistency checks to make
# sure that the quota isn't too large and that the volume or mount point don't
# already exist.
sub create_volume {
    my ($volume, $mountpoint, $quota_mb, $quiet) = @_;

    my ($stdout, $stderr, $rc);
    my @cmd;

    progress("creating log volume $volume at $mountpoint");

    if ($quota_mb > $MAX_QUOTA_MBS) {
        my $max_quota_gb = int($MAX_QUOTA_MBS / $MB_IN_GB);
        warn "$volume not created: ${quota_mb}MB exceeds maximum of ${max_quota_gb}GB\n";
        return;
    }
    $mountpoint =~ s{/afs/ir}{/afs/.ir}xsm;
    if (-d "$mountpoint") {
        warn "$volume not created: $mountpoint already exists\n";
        return;
    }

    @cmd = ($VOS, 'examine', $volume);
    ($stdout, $stderr, $rc) = run_command(@cmd);
    if ($rc == 0) {
        warn "$volume not created: volume already exists\n";
        return;
    }

    # Create the volume. Remember that volcreate expects the quota to be given
    # in megabytes.
    @cmd = ($VOLCREATE, '-t', 'logs', $volume, $quota_mb, $mountpoint);
    if ($quiet) {
        unshift (@cmd, '-q');
    }

    if ($JUSTPRINT) {
        print_command(@cmd);
    } else {
        ($stdout, $stderr, $rc) = run_command(@cmd);

        if ($rc != 0) {
            my $cmd_fmted = join(q{ }, @cmd);
            warn "$cmd_fmted exited with status $rc\n";
            warn "$volume not fully created, please check\n";
        } else {
            report("Created $volume with quota ${quota_mb}MB\n");
        }
    }

    return;
}

# For a yearly log volume, create all of the month subdirectories.  Takes the
# base directory of the new log volume.
sub create_month_dirs {
    my ($base) = @_;
    ## no critic (ProhibitMagicNumbers);
    for (1..12) {
        my $month = sprintf ('%02d', $_);
        if ($JUSTPRINT) {
            print "mkdir $base/$month\n";
        } else {
            mkdir("$base/$month", 0755)
                or warn "could not create $base/$month: $ERRNO\n";
        }
    }

    return;
}

# Given a year, month, configuration hash for a log volume, and flag saying
# whether to be quiet, make sure that a volume for that year and month already
# exists.  If it doesn't, create it.
sub create_log_volume {
    my ($year, $month, $log_config_href, $quiet) = @_;

    my ($path, $volume);

    my $volume_name = $log_config_href->{'volume_name'};
    my $frequency   = $log_config_href->{'frequency'};
    my $base_path   = $log_config_href->{'base_path'};
    my $quota_mb    = $log_config_href->{'quota_mb'};

    if ($frequency eq 'monthly') {
        $path = sprintf ("$base_path/%04d/%02d", $year, $month);
        $volume = sprintf ('%s.%04d%02d', $volume_name, $year, $month);
        if (!-d "$base_path/$year") {
            progress("making directory $base_path/$year");
            if ($JUSTPRINT) {
                print "mkdir $base_path/$year\n";
            } elsif (!mkdir ("$base_path/$year", 0755)) {
                warn "Could not create $base_path/$year: $ERRNO\n";
                warn "$volume not created\n";
                return;
            }
        }
    } elsif ($frequency eq 'yearly') {
        $path = sprintf ("$base_path/%04d", $year);
        $volume = sprintf ('%s.%04d', $volume_name, $year);
    }
    if (!-d $path) {
        create_volume ($volume, $path, $quota_mb, $quiet);
        if ($frequency eq 'yearly') {
            create_month_dirs($path);
        }
    }

    return;
}

##############################################################################
# Configuration parsing
##############################################################################


# The YAML file should have this format:
#
# logs:
#   - volume_name: logs.afs
#     base_path: /afs/ir/service/afs/logs
#     frequency: yearly
#     quota_mb: 500
#     description: This is for the AFS server.
#   - volume_name: logs.auth
#     base_path: /afs/ir/service/auth/logs
#     frequency: monthly
#     quota_mb: 8000
#
# where <volume_name> is the base name of the volume (.YYYY or .YYYYMM will
# be appended), <base_path> is the root of the log structure (its immediate
# subdirectories must be the year directories), <frequency> is either monthly to
# create a new volume for each month or yearly to create one volume for
# each year of logs, and <quota_mb> is the starting quota for each volume in MB.
#
# Note that the <description> field is optional.
#
# Populates the global %LOG_CONFIGS hash.
sub read_config_yaml {
    my ($config_file_yaml) = @_ ;

    my $yaml = YAML::Tiny->read($config_file_yaml);

    # Get the top element.
    my $config = $yaml->[0];

    # Get the log configurations.
    my @log_configs  = @{ $config->{logs} };

    # Validate.
    for my $log_config_href (@log_configs) {
        my $volume_name = $log_config_href->{'volume_name'};
        my $base_path   = $log_config_href->{'base_path'};
        my $frequency   = $log_config_href->{'frequency'};
        my $quota_mb    = $log_config_href->{'quota_mb'};

        # Check 1: $volume_name.
        if (! $volume_name) {
            my $msg = q{missing required parameter 'volume_name'};
            exit_with_error($msg);
        }

        # Check 2: $base_path.
        if (! $base_path) {
            my $msg = q{missing required parameter 'base_path'};
            exit_with_error($msg);
        }

        if ($base_path !~ m{^/afs/.*$}xsm) {
            my $msg = "base_path '$base_path' is not a a valid AFS path";
            exit_with_error($msg);
        }

        # Check 3: $frequency.
        if (! $frequency) {
            my $msg = q{missing required parameter 'frequency'};
            exit_with_error($msg);
        }

        if ($frequency !~ m{^(monthly|yearly)$}ixsm) {
            my $msg = "frequency '$frequency' not recognized; must be one of 'monthly' or 'yearly'";
            exit_with_error($msg);
        }

        # Check 4: $quota_mb.
        if (! defined($quota_mb)) {
            my $msg = q{missing required parameter 'quota_mb'};
            exit_with_error($msg);
        }

        $quota_mb = $quota_mb + 0;
        if ($quota_mb <= 0) {
            my $msg = "'quota_mb' $quota_mb must be a positive integer";
            exit_with_error($msg);
        }

        # If we get here we have a valid configuration. Add it to the global
        # $LOG_CONFIGS hash.
        $LOG_CONFIGS{$volume_name} = $log_config_href;
    }

    return;
}

sub filter_config_yaml {
    my (@names_from_command_line) = @_ ;

    progress('filtering configurations from those provided at the command line');

    # If no names were passed do nothing.
    if (! @names_from_command_line) {
        progress('no names passed on command-line, so no filtering needed');
        return;
    }

    # Create an indicator version of @names_from_command_line
    my %names_from_command_line = ();
    foreach my $name (@names_from_command_line) {
        $names_from_command_line{$name} = 1;
    }

    # Stage 1. Any volumes on command line not in config file means raise an error.
    foreach my $name (@names_from_command_line) {
        if (!exists($LOG_CONFIGS{$name})) {
            my $msg = "volume '$name' provided on command-line not in configuration";
            exit_with_error($msg);
        }
    }

    # Stage 2. Remove from @LOGS_CONFIGS any volume not in
    # @names_from_command_line.
    for my $volume_name (keys %LOG_CONFIGS) {
        if (!$names_from_command_line{$volume_name}) {
            progress("removing '$volume_name' from LOG_CONFIGS");
            delete $LOG_CONFIGS{$volume_name};
        } else {
            progress("found '$volume_name' in LOG_CONFIGS");
        }
    }

    progress('finished filtering configurations from those provided at the command line');

    return;
}

sub process_log_config {
    my ($log_config_href, $quiet) = @_ ;

    my $mpfx = (caller 0)[3] =~ s/.*:://r;

    # ## #    # ## #    # ## #    # ## #    # ## #    # ## #    # ## #
    my $progress_local = sub {
        my ($msg) = @_;
        return progress($msg, $mpfx);
    };
    # ## #    # ## #    # ## #    # ## #    # ## #    # ## #    # ## #

    my $volume_name = $log_config_href->{'volume_name'};
    my $base_path   = $log_config_href->{'base_path'};
    my $frequency   = $log_config_href->{'frequency'};
    my $quota_mb    = $log_config_href->{'quota_mb'};

    $progress_local->("starting to process volume '$volume_name'");

    my ($day, $month, $year) = (localtime)[3..5];
    $month++;
    $year += 1900;


    $progress_local->("updating quota for volume $volume_name");

    my ($volume_found, $path_found);
    if ($frequency eq 'monthly') {
        ($volume_found, $path_found) = find_volume_month($base_path, $month, $year);

        if ($volume_found) {
            my $msg = "found (volume, path) = ($volume_found, $path_found) for base path $base_path (monthly, $month, $year)";
            $progress_local->($msg);
        } else {
            my $msg = "no volume or path currently exists for base path $base_path (monthly, $month, $year)";
            $progress_local->($msg);
        }

    } elsif ($frequency eq 'yearly') {
        ($volume_found, $path_found) = find_volume_year ($base_path, $year);

        if ($volume_found) {
            my $msg = "found (volume, path) = ($volume_found, $path_found) for base path $base_path (yearly, $year)";
            $progress_local->($msg);
        } else {
            my $msg = "no volume or path currently exists for base path $base_path (yearly, $year)";
            $progress_local->($msg);
        }

    } else {
        my $msg = "Unknown frequency '$frequency' volume '$volume_name'";
        exit_with_error($msg);
    }

    # $volume_found and $path_found should both be defined, or both be undefined.
    # If not, this is an error.
    if ($volume_found && !$path_found) {
        my $msg = "for volume $volume_name the volume was found but not the path";
        exit_with_error($msg);
    }

    if (!$volume_found && $path_found) {
        my $msg = "for volume $volume_name the path was found but not the volume";
        exit_with_error($msg);
    }


    if ($path_found) {
        $progress_local->("setting quota on path '$path_found' corresponding to $base_path");
        check_quota ($volume_found, $path_found, $quota_mb);
    } else {
        $progress_local->("did not find a path to $base_path; will create volume");
        create_log_volume ($year, $month, $log_config_href, $quiet);
    }

    if ($day >= 20) {
        my $month1 = $month;
        my $year1 = $year;
        $month1++;
        if ($month1 > 12) {
            $month1 = 1;
            $year1++;
        }
        $progress_local->("since day is >= 20 will create the next month as well");
        create_log_volume ($year1, $month1, $log_config_href, $quiet);
    }
}

##############################################################################
# Main routine
##############################################################################

# Parse command line options.
my ($help, $quiet, $version, $verbose);
Getopt::Long::config ('bundling', 'no_ignore_case');
GetOptions ('n|dry-run|just-print' => \$JUSTPRINT,
            'h|help'               => \$help,
            'q|quiet'              => \$quiet,
            'verbose'              => \$verbose,
            'v|version'            => \$version) or exit 1;
if ($help) {
    print "Feeding myself to perldoc, please wait....\n";
    exec ('perldoc', '-t', $PROGRAM_NAME) or die "Cannot fork: $ERRNO\n";
} elsif ($version) {
    print "volcreate-logs $VERSION\n";
    exit 0;
}

if ($verbose) {
    $VERBOSE = 1;
}

if ($JUSTPRINT) {
    progress("in dry-run mode");
}

# Parse the configuration and get the current time.
read_config_yaml($LOGS_CONFIG);

# If a particular name or set of names were given on the command line, limit
# our actions to just those names.
filter_config_yaml(@ARGV);

# For each log configuration, update the quota of the current volume if
# necessary and then create the new volume if it's past the 20th of the month.
for my $volume_name (keys %LOG_CONFIGS) {
    process_log_config($LOG_CONFIGS{$volume_name}, $quiet);
}

__END__

##############################################################################
# Documentation
##############################################################################

=for stopwords
ACL AFS PAG .YYYY .YYYMM afs-admin-tools aklog fs -hmnqv newsyslog
subdirectories volcreate volcreate's volcreate-logs vos logs.service.YYYYMM
YYYYMM

=head1 NAME

volcreate-logs - Create and grant quota to log volumes in AFS

=head1 SYNOPSIS

B<volcreate-logs> [B<-hmnqv>] [B<--verbose>]

=head1 DESCRIPTION

This program maintains the size and existence of log volumes in AFS,
creating new ones for upcoming months or years as necessary and increasing
their quota as necessary if they're getting too full. It reports all of
its actions, by default to standard out.

The rules for each set of log volumes are defined in the program
configuration file, in F</etc/afs-admin-tools/log-volumes> by default. It
uses B<volcreate> to create all new log volumes, and therefore initial
ACLs for new log volumes can be set using B<volcreate>'s ACL handling
capabilities. See L<volcreate(1)> for more information.

In order to use B<volcreate-logs> to manage a set of volumes, the volumes
must use standard directory structure and naming conventions. This means
that under some root log directory, volumes must be organized like:

   ROOT/<year>/<month>

where <year> is the four-digit year and <month> is the two-digit month
(with zero-padding where necessary). If there is a separate volume for
each month, those volumes are mounted at locations that look like the
above; if there is only a single volume for each year's worth of logs,
that volume is mounted at ROOT/<year> and will have subdirectories for
each month created automatically. The log volumes are named
C<logs.service.YYYYMM> where C<service> is some string representing the
service being logged and C<YYYYMM> is the four-digit year and two-digit
month. If the volume only holds logs for one year, the volume will end
with C<YYYY> instead.

New volumes will be created for the following month on any date after the
twentieth of the month (log volumes will only be created if the
appropriate directory does not already exist). Existing log volumes will
have their quota and usage checked. If the quota is under the configured
size, it will be increased to match. If the usage is over 90%, the volume
quota will be increased by 10%, rounded up to the nearest 10MB.

=head1 OPTIONS

=over 4

=item B<-h>, B<--help>

Print out this documentation (which is done simply by feeding the script
to C<perldoc -t>).

=item B<-n>, B<--dry-run>, B<--just-print>

Don't take any actions that chanage things. Instead, print to
standard out each modifying command.

=item B<-q>, B<--quiet>

Suppress any additional output other than the one line per action taken
(done mostly by adding the B<-q> option to B<volcreate>).

=item  B<-v>, B<--version>

Print out the version of B<volcreate-logs> and exit.

=item B<--verbose>

Run in verbose mode.

=back

=head1 CONFIGURATION

=head2 General Settings

B<volcreate-logs> loads configuration settings from
F</etc/afs-admin-tools/config> if that file exists. If it exists, it must
be Perl code suitable for loading with C<require>. This means that each
line of the configuration file should be of the form:

    our $VARIABLE = VALUE;

where C<$VARIABLE> is the configuration variable being set and C<VALUE> is
the value to set it to (which should be enclosed in quotes if it's not a
number). The file should end with:

    1;

so that Perl knows the file was loaded correctly.

The supported configuration variables are:

=over 4

=item $LOGS_CONFIG

The configuration file specifying what log volumes to manage. See L<Log
Volumes> below for the syntax. The default path is
F</etc/afs-admin-tools/log-volumes.yaml>.


=item $FS

The full path to the AFS B<fs> utility. If this variable is not set,
B<volcreate-logs> defaults to looking for B<fs> on the user's PATH.

=item $VOLCREATE

The full path to the B<volcreate> utility. If this variable is not set,
B<volcreate-logs> defaults to looking for B<volcreate> on the user's PATH.

=item $VOS

The full path to the AFS B<vos> utility. If this variable is not set,
B<volcreate-logs> defaults to F</usr/local/sbin/vos> or F</usr/sbin/vos>
if they exist, and otherwise looks for B<vos> on the user's PATH.

=back

=head2 Log Volumes

The file set by the $LOGS_CONFIG configuration variable, defaulting to
F</etc/afs-admin-tools/log-volumes.yaml>, specifies the collections of log
volumes managed by this program. The configuration will be used to
increase quota or create new log volumes as needed. The file is in YAML
format and should look like this:

    logs:
      - volume_name: logs.afs
        base_path: /afs/ir/service/afs/logs
        frequency: yearly
        quota_mb: 500
        description: This is for the AFS server.
      - volume_name: logs.auth
        base_path: /afs/ir/service/auth/logs
        frequency: monthly
        quota_mb: 8000

where <volume_name> is the base name of the volume (C<.YYYY> or C<.YYYYMM>
will be appended), <base_path> is the root of the log structure (its
immediate subdirectories must be the year directories), <frequency> is either
"monthly" to create a new volume for each month or "yearly" to create one
volume for each year of logs, and <quota_mb> is the starting quota for each
volume in MB. The <description> field is optional.

Blank lines and lines beginning with C<#> are ignored.

=head1 EXAMPLES

Process all of the current rules, reporting actions to standard out.

    volcreate-logs

Check to see what needs to be done, but don't actually do it. Instead,
just print the commands that would be executed to standard out:

    volcreate-logs -n

Only check and reset quota or create new log volumes for the config file
entries with a name value of C<logs.cgi> or C<logs.www>:

    volcreate-logs logs.cgi logs.www

=head1 FILES

=over 4

=item F</etc/afs-admin-tools/log-volumes.yaml>

The default configuration file specifying the collections of log volumes
managed by this program. The path to this file may be overridden by
setting the $LOGS_CONFIG configuration variable.

=back

=head1 AUTHOR

Russ Allbery <rra@stanford.edu>

Updated in 2026 by Adam H. Lewenberg <adamhl@stanford.edu>

=head1 COPYRIGHT AND LICENSE

Copyright 2002, 2004, 2011, 2012, 2026 The Board of Trustees of the Leland Stanford
Junior University.

This program is free software; you may redistribute it and/or modify it
under the same terms as Perl itself.

=head1 SEE ALSO

L<volcreate(1)>

This script was forked from the afs-admin-tools package managed at
L<http://www.eyrie.org/~eagle/software/afs-admin-tools/>.

=cut
