#!/home/claudio/lib/perlbrew/perls/perl-5.12.3/bin/perl

#########################################################
###                                                   ###
### Program to change passwords on selected machines. ###
### Send bugs to Claudio Ramirez, nxadm@cpan.org      ###
###						      ###
#########################################################

### Pragmas and Modules ###
use strict;
use warnings;
use feature ':5.10';
use Getopt::Long;
use File::Basename qw(basename);
use Parallel::ForkManager;

### Variables ###
our $VERSION = '0.3';
our $AUTHOR  = 'Claudio Ramirez <nxadm@cpan.org>';

my @ssh     = ( 'ssh', '-t' );    # ssh will be found in the PATH
my $sim     = 5;                  # default
my $timeout = 20;                 # default
my $runs    = 0;                  # default

### Main ###
### Command Line Interface ###
my ( %cli, @servers );

GetOptions(
    \%cli,          'help|?',       'version|v',   'user|u=s',
    'password|p=s', 'base|b=s',     'date|d=i',    'generate_only|g',
    'sim|s=i',      'ssh_args|a=s', 'timeout|t=i', 'reruns|r=i',
    'debug',
  )
  or die "\nInvalid switch. Please read the help page: "
  . basename $0
  . " -?\n\n";
my @gen_only_options = qw/generate_only base date sim/;

if ( $cli{help}
    or ( !@ARGV and !( grep { defined $_ } values %cli ) ) )
{
    help(0);
    exit 0;
}
if ( $cli{version} ) { # Faster help status
    help(1);
    exit 0;
}
if ( !check_params() ) {
    exit 1;
}

### Remote connections ###

my $conn;
if ( !$cli{generate_only} ) {
    require App::Unix::RPasswd::Connection;
    $conn = App::Unix::RPasswd::Connection->new(
        user     => $cli{user},
        ssh_args => \@ssh,
    );
}

my $salt_password;
if ( $cli{base} ) {
    require App::Unix::RPasswd::SaltedPasswd;
    $salt_password =
      App::Unix::RPasswd::SaltedPasswd->new( salt => $cli{base}, );
}

my @errors = pexec(@servers);
if (@errors) {
    say "The following servers had errors: @errors";
    say 'Please review the output (--debug enables debug information).';
}
else {
    say "All targets run successfully.";
}
say '_' x 80 . "\n";

### Subroutines ###

sub pexec {
    my @servers = @_;
    state $run++;
    my @errors;
    my $pfm = Parallel::ForkManager->new( $cli{sim} );
    $pfm->run_on_finish(
        sub {
            my ( undef, undef, undef, undef, undef, $dataref ) = @_;
            if ( defined $dataref and defined $$dataref ) {
                push @errors, $$dataref;
            }
        }
    );
    for my $server (@servers) {
        $pfm->start() and next;
        my $error;
        $cli{password} //=
          $salt_password->generate( $server . $cli{date} . $server )
          ;    #/
        if ( $cli{debug} and $cli{base} ) {
            say 'Generated password: ' . $cli{password} . " ($server).";
        }

        if ( $cli{generate_only} ) { say $cli{password} . " ($server)" }
        else {
            say "Running on $server...";
            my $status = eval {
                local $SIG{ALRM} = sub {
                    say "Timeout exceeded for $server.";
                    die "Timeout exceeded\n";    # NB: \n required
                };
                alarm $cli{timeout};
                return $conn->run( $server, $cli{password}, $cli{debug} );
                alarm 0;
            };
            if ( !$status ) { $error = $server; }
        }
        $pfm->finish( 0, \$error );
    }
    @errors = pexec(@errors) unless ( $run > $cli{reruns} );    # Recursive
    $pfm->wait_all_children;
    say "\nRun $run done.\n" . '_' x 80 . "\n";
    return @errors;
}

sub check_params {
    my $status = 1;
    use List::MoreUtils ('uniq');
    @servers = uniq map { s/\s//g; $_ } @ARGV;           # Cleanup serverlist

    # all modes
    if ( scalar @servers == 0 ) {
        $status = 0;
        say 'You need at least one server.';
    }

    # gen only mode
    if ( $cli{generate_only} ) {
        for my $key ( grep { !( $_ ~~ @gen_only_options ) } keys %cli ) {
            if ( defined $cli{$key} ) {
                $status = 0;
                say "Parameter $key is invalid in this mode.";
            }
        }
        if ( !exists $cli{base} ) {
            $status = 0;
            say "Parameter base is required in this mode.";
        }
        $cli{reruns} = $runs;
        return $status if !$status;
    }
    else {

        # conn mode
        if ( !$cli{user} ) { $status = 0; say 'Parameter user is mandatory.'; }
        if ( !defined $cli{password} and !defined $cli{base} ) {
            $status = 0;
            say 'You need to specify password or base.';
        }
        elsif ( defined $cli{password} and defined $cli{base} ) {
            $status = 0;
            say 'You need to specify password or base, not both.';
        }
        elsif ( defined $cli{password} ) {
            if ( $cli{date} ) {
                $status = 0;
                say 'Date is only valid in combination with base.';
            }
            elsif ( $status == 1 and $cli{password} eq '-' ) {
                require App::Unix::RPasswd;
                my $rpasswd = App::Unix::RPasswd->new();
                $cli{password} = $rpasswd->ask_key('password');
            }
        }
        if ( defined $cli{ssh_args} ) {
            @ssh = ( @ssh, split( /\s/, $cli{ssh_args} ) );
        }
        if ( !defined $cli{timeout} ) { $cli{timeout} = $timeout; }
        if ( defined $cli{reruns} ) {
            if ( $cli{reruns} > 98 ) {
                $status = 0;
                say 'Less than 100 retries allowed. Let be raisonable.';
            }
            else { $cli{reruns} = $runs }
        }
    }

    if ( $status == 1 and defined $cli{base} ) {
        require App::Unix::RPasswd;
        my $rpasswd = App::Unix::RPasswd->new();
        if ( $cli{date} and $cli{date} !~ /^\d{8}$/ ) {
            $status = 0;
            say 'Supply parameter date in a YYYYMMDD format (e.g. 20101123).';
        }
        elsif ( !$cli{date} ) {
            $cli{date} = $rpasswd->date;
        }
        if ( $cli{base} eq '-' ) {
            $cli{base} = $rpasswd->ask_key('base salt');
        }
        while ( $cli{base} eq '' ) {
            say 'Base salt can not be empty.';
            $cli{base} = $rpasswd->ask_key('base salt');
        }
        
    }
    return $status if $status == 0;
    return $status;
}

sub help {
    my $version_bool = shift;
    require File::Basename;
    my $program = File::Basename::basename($0);
    say $program . ', version ' . $VERSION . '.';
    return if $version_bool;
    my $reruns = $runs + 1;
    say <<"EOL";

Change passwords on UNIX and UNIX-like servers on a simple, fast (in parallel)
and secure (SSH) way. A salt-based retrievable "random" password generator, 
tied to the supplied server names and date, is included.

Usage:
\t$program -u <user> -p <password> <server(s)>
\t$program -g -b <base salt> -date <YYYYMMDD> <server(s)>

Options:
\t--generate_only|-g:\t(re-)generate the salted password.
\t--user|-u:\t\tremote user name.
\t--password|-p:\t\tnew password for remote user.
\t--base|-b:\t\tbase salt for encryption.
\t--date|-d:\t\tdate in YYYYMMDD format (defaults to today)*.
\t--ssh_args|-a:\t\tsettings for the ssh client (man ssh)*.
\t--reruns|-r:\t\treruns for failed targets (defaults to 0)*.
\t--sessions|-s:\t\tsimultaneous sessions (defaults to 5)*.
\t--timeout|-t:\t\tsession timeout (defaults to 20 seconds)*.
\t--debug:\t\tprints debug output*.
\t--help|-h:\t\tprints this help screen.
\t--version|-v:\t\tprints the version number.

\t*: optional

The program has two modes. The default mode connects to remote targets and 
changes the password (optional) of the specified user (mandatory) on the 
supplied servers (mandatory). Optional valid parameters for this mode are 
sessions, ssh_args, reruns, timeout and debug. The built-in salted password 
generator can be used to create unique 'random' passwords for each server on 
the fly. In this case date (optional) and base (mandatory) are valid parameters 
for this mode.

The "generate_only" mode is used to (re-) generate salted passwords. In this 
mode only date (optional), base (mandatory), sim (optional) and one of more 
servers (mandatory) are valid parameters.

From a security point of view, it is strongly advised to supply '-' as the base
salt or password on the command line. The program will then ask interactively 
for the base salt or password. 
Bugs to $AUTHOR.
EOL
}

__END__

=pod

=head1 NAME

App::Unix::RPasswd - Change passwords on UNIX and UNIX-like servers on a simple, 
fast (in parallel) and secure (SSH) way.

=head1 VERSION

Version 0.3

=head1 SYNOPSIS

App::Unix::RPasswd is an application for changing passwords on UNIX and 
UNIX-like servers on a simple, fast (in parallel) and secure (SSH) way. 
A salt-based retrievable "random" password generator, tied to the supplied 
server names and date, is included. 
This generated passwords, unique for each server, can be generated and
automatically remotely applied. Because the salt is secret and the correct date
string is required, the password for an specific server can only be regenerated 
by authorized personnel.

Perl 5.10 or higher is required.

The program has two modes. The default mode connects to remote targets and 
changes the password (optional) of the specified user (mandatory) on the 
supplied servers (mandatory). Optional valid parameters for this mode are 
sessions, ssh_args, reruns, timeout and debug. The built-in salted password 
generator can be used to create unique 'random' passwords for each server on 
the fly. In this case date (optional) and base (mandatory) are valid parameters 
for this mode.

The "generate_only" mode is used to (re-) generate salted passwords. In this 
mode only date (optional), base (mandatory), sim (optional) and one of more 
servers (mandatory) are valid parameters.

From a security point of view, it is strongly advised to supply '-' as the base
salt or password on the command line. The program will then ask interactively 
for the base salt or password. 

Usage:
    rpasswd -u <user> -p <password> <server(s)>
    rpasswd -g -b <base salt> -date <YYYYMMDD> <server(s)>

Options:
    --generate_only|-g: (re-)generate the salted password.
    --user|-u:          remote user name.
    --password|-p:      new password for remote user.
    --base|-b:          base salt for encryption.
    --date|-d:          date in YYYYMMDD format (defaults to today)*.
    --ssh_args|-a:      settings for the ssh client (man ssh)*.
    --reruns|-r:        reruns for failed targets (defaults to 0)*.
    --sessions|-s:      simultaneous sessions (defaults to 5)*.
    --timeout|-t:       session timeout (defaults to 20 seconds)*.
    --debug:            prints debug output*.
    --help|-h:          print this help screen.
    --version|-v:       prints the version number.

    *: optional

=head1 PARAMETERS

=head2 generate_only | g

This parameter enables the (re-)generation of salted passwords.

=head2 user | u

This parameter sets the remote user name that will receive a new password.

=head2 password | p

This parameter sets the new password for the remote user. When "-" is supplied 
as argument, the program asks interactively for the password.

=head2 base | b

This parameter sets the base salt for encryption. When "-" is supplied as 
argument, the program asks interactively for the base salt. The salt can be 
between 1 and 8 characters. Longer salts are truncated.

=head2 date | d

This optional parameters sets the date string in a YYYYMMDD format (defaults to 
today).

=head2 ssh_args | a

This optional parameter sets additional settings for the ssh client (man ssh).
Quote the argument string (e.g. --ssh_args "-l root").

=head2 reruns | r

This optional parameterre sets the reruns for failed targets (defaults to 0).

=head2 sessions | s

This optional parameter sets the simultaneous sessions (defaults to 5).

=head2 timeout | t

This optional parameter sets the session timeout in seconds (defaults to 20 
seconds). While OpenSSH has the ConnectTimeout (passed as --ssh_args "-OConnectTimeout=<value>")
that provides a similar funcionality, its for on Solaris, SunSSH, has not. This
is a generic implementation that work on both ssh families.

=head2 debug

This parameter prints debug output.

=head2 help | h

This parameter prints this help screen.

=head2 version | v

This parameter prints the version number.


=head1 AUTHOR

Claudio Ramirez, C<< <nxadm at cpan.org> >>

=head1 BUGS

Please report any bugs or feature requests to C<bug-App-Unix-RPasswd at rt.cpan.org>, or through
the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=App-Unix-RPasswd>.  I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.

This distribution has been tested on GNU/Linux (Debian 6.0 and Ubuntu 10.10) 
running OpenSSH and Solaris 10 running SunSSH.


=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc App-Unix-RPasswd


You can also look for information at:

=over 4

=item * RT: CPAN's request tracker

L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=App-Unix-RPasswd>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/App-Unix-RPasswd>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/App-Unix-RPasswd>

=item * Search CPAN

L<http://search.cpan.org/dist/App-Unix-RPasswd/>

=back

=head1 ACKNOWLEDGEMENTS

The following non-core modules were used:

=over 2

=item * Crypt::PasswdMD5
L<http://search.cpan.org/dist/Crypt-PasswdMD5/>

=item * DateTime
L<http://search.cpan.org/dist/DateTime/>

=item * List::MoreUtils
L<http://search.cpan.org/dist/List-MoreUtils/>

=item * Moose
L<http://search.cpan.org/dist/Moose/>

=item * namespace::autoclean
L<http://search.cpan.org/dist/namespace-autoclean/>

=back

=head1 AUTHOR

Claudio Ramirez <nxadm@cpan.org>

=head1 LICENSE AND COPYRIGHT

Copyright 2010 Claudio Ramirez.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

=cut
