#!/usr/bin/perl -w
=head1 NAME

afs-vol-paths - list full paths to volumes

=head1 SYNOPSIS

afs-vol-paths [options] [mounts ...]

  Options:
    --volume  volume name
    --rw      list paths which contain a -rw mount point
    --vldb    vos listvldb output
    --cell    cellname
    --help    brief help message

=head1 DESCRIPTION

List the absolute paths to volumes from volscan output from
each fileserver.

Give a list of file names which contain the output from volscan for
each fileserver.  Use the following options to volscan:

    volscan \
      -find mounts \
      -output host partid vname vid vtype mtype mcell mvol path \
      -noheader

Specify - (dash) to read the volscan data from stdin.

Cross cell mount points are ignored by this program. Specify
the --cell option your cell includes cellular mount points.

=head1 OPTIONS

=over 8

=item B<--volume>

Volume name to resolve. Default is to list all volumes. Unless
the --vldb option is given, afs-vol-paths will run 'vos listvldb'
to retrieve the list of volumes.

=item B<--rw>

Only list paths which contain at least one -rw mount.

=item B<--vldb>

The output of a previous 'vos listvldb', which gives the volume names
to be listed by afs-vol-paths.

=item B<--cell>

The cell name to be used when finding the volume paths.

=item B<--help>

Print a brief help message and exits.

=back

=cut

use Pod::Usage;
use Getopt::Long;
use Data::Dumper;

my %g_parents;  # volume to parent volume hash; where parent is a rw volume
                #   containing at least one mount point to the child volume
my %g_in_ro;    # volume to parent hash for ro volumes not seen in g_parents.
                #   this is used to warn for mounts in unreleased ro volumes.

my $opt_volume = "";
my $opt_rw = 0;
my $opt_vldb = "";
my $opt_cell = "";
my $opt_debug = 0;
my $opt_help = 0;

GetOptions(
  "volume=s" => \$opt_volume,
  "rw" => \$opt_rw,
  "vldb=s" => \$opt_vldb,
  "cell=s" => \$opt_cell,
  "debug" => \$opt_debug,
  "help" => \$opt_help) || pod2usage(2);

if ($opt_help) {
    pod2usage(2);
}
main();

#----------------------------------------------------------------
# Main routine
#
sub main {
    my $mc = 0;
    foreach my $mounts (@ARGV) {
        read_mounts($mounts);
        $mc++;
    }
    if ($mc == 0) {
        read_mounts("-");
    }

    check_unreleased_mounts();

    if ($opt_volume) {
        print_paths($opt_volume);
    }
    else {
        foreach my $v (get_volumes()) {
            print_paths($v);
        }
    }
}

#----------------------------------------------------------------
# Read the files generated by volscan
#
# The volscan output files contain the mount point information used
# to determine the paths.
#
sub read_mounts {
    my $mounts = shift;
    open(MOUNTS, "$mounts") || die "Cant open mount point data file: $mounts\n";
    while (<MOUNTS>) {
        chomp;
        my ($host, $partid, $volume, $vid, $vtype, $mtype, $mcell, $mvol, $path) = split;
        my $rw = ($mtype eq '%' ? 1 : 0);

        next if $vtype eq "BK"; # skip backups

        if ($mcell ne "--") {
            if ($opt_cell && $mcell ne $opt_cell) {
                next; # skip cross cell
            }
        }

        if ($volume eq $mvol) {
            if ($opt_debug) {
                warn("Skipping volume $volume mounted to self on path $path\n");
            }
            next;  # skip self-parent
        }

        if ($vtype eq "RW") {
            unless ($g_parents{$mvol}) {
                $g_parents{$mvol} = [];
            }
            push(@{$g_parents{$mvol}}, {"path"=>$path, "volume"=>$volume, "vid"=>$vid, "rw"=>$rw});
            next;
        }

        if ($vtype eq "RO") {
            # If this mount point has not been seen in a read/write volume, it may only
            # be present in an unreleased read-only volume. Save it here and reconcile
            # after reading all of the files.
            unless ($g_parents{$mvol}) {
                unless ($g_in_ro{$mvol}) {
                    push(@{$g_in_ro{$mvol}}, {"path"=>$path, "volume"=>$volume, "vid"=>$vid, "rw"=>$rw});
                }
            }
            next;
        }
    }
    close MOUNTS;
}

#----------------------------------------------------------------
# Find any mount points detected in read-only volumes which
# are not present in the read/write space.
#
sub check_unreleased_mounts
{
    foreach my $mvol (keys %{%g_in_ro}) {
        my $parents = $g_in_ro{$mvol};
        unless ($g_parents{$mvol}) {
            foreach my $p (@{$parents}) {
                warn("Unreleased $p->{volume} ($p->{vid}) has only mount point to $mvol\n");
            }
        }
    }
}

#----------------------------------------------------------------
# Find and print the paths to a volume.
#
sub print_paths {
    my $volume = shift;
    my $paths = find_parent_paths([], [$volume], '', 0);

    if (scalar @{$paths} == 0) {
        warn("No paths found for volume: $volume\n");
        return;
    }
    print "$volume\n";
    foreach (@{$paths}) {
        print "\t$_\n";
    }
    print "\n";
}

#----------------------------------------------------------------
# Resursively lookup the paths to a volume.
#
sub find_parent_paths {
    my ($paths, $vols_ref, $path, $rwcount) = @_;
    my @vols = @{$vols_ref};

    my $volume = $vols[0];
    my $parents = $g_parents{$volume};
    if (!$parents) {
        if ($volume eq 'root.afs') {
            if ($rwcount || !$opt_rw) {
                push(@{$paths}, "/afs" . $path);
            }
        }
        else {
            if ($opt_debug) {
                warn("\nnot mounted: ". join(" <- ", @vols) ."\n");
            }
        }
    }
    else {
        foreach my $p (@{$parents}) {
            if (grep { $p->{volume} eq $_ } @vols) {
                if ($opt_debug) {
                    warn("\ncycle detected: ". join(", ", @vols) ."\n");
                }
            }
            else {
                $paths = find_parent_paths(
                    $paths,
                    [$p->{volume}, @vols],
                    $p->{path} . $path,
                    $rwcount + $p->{rw});
            }
        }
    }
    return $paths;
}

#----------------------------------------------------------------
# Get list of volumes from vos listvldb.
#
sub get_volumes
{
    my @volumes = ();
    if (!$opt_vldb) {
        my $vos = "vos listvldb";
        $vos .= " -cell $opt_cell" if $opt_cell;
        open(VOS, "$vos |") or die "could not run '$vos': $?";
    }
    else {
        open(VOS, "$opt_vldb") or die "could not read open vldb listing: $opt_vldb: $?";
    }
    while (<VOS>) {
        next if /^VLDB entries/;
        next if /^Total entries/;
        if (/^(\S+)/) {
            push(@volumes, $1);
        }
    }
    close VOS;
    return @volumes;
}
