package Provision::Unix::User::FreeBSD;

use warnings;
use strict;

our $VERSION = '0.06';

use English qw( -no_match_vars );
use Carp;
use Params::Validate qw( :all );

use lib 'lib';

my ($prov, $user, $util);

sub new {

    my $class = shift;

    my %p = validate(
        @_,
        {   prov  => { type => HASHREF },
            user  => { type => HASHREF },
            debug => { type => BOOLEAN, optional => 1, default => 1 },
            fatal => { type => BOOLEAN, optional => 1, default => 1 },
        }
    );

    my $self = {
        prov  => $p{prov},
        user  => $p{user},
        debug => $p{debug},
        fatal => $p{fatal},
    };
    bless( $self, $class );

    $prov = $p{prov};
    $user = $p{user};
    $prov->audit("loaded User/FreeBSD");

    require Provision::Unix::Utility;
    $util = Provision::Unix::Utility->new( prov=> $prov );
    return $self;
}

sub create {

    my $self = shift;

    my %p = validate(
        @_,
        {   'username' => { type => SCALAR },
            'uid'      => { type => SCALAR },
            'gid'      => { type => SCALAR },
            'shell'    => { type => SCALAR | UNDEF, optional => 1 },
            'password' => { type => SCALAR | UNDEF, optional => 1 },
            'homedir'  => { type => SCALAR | UNDEF, optional => 1 },
            'gecos'    => { type => SCALAR | UNDEF, optional => 1 },
            'domain'   => { type => SCALAR | UNDEF, optional => 1 },
            'expire'   => { type => SCALAR | UNDEF, optional => 1 },
            'quota'    => { type => SCALAR | UNDEF, optional => 1 },
            'debug'    => { type => SCALAR, optional => 1, default => 1 },
            'test_mode'=> { type => SCALAR, optional => 1 },
        }
    );

    my $debug = $p{'debug'};
    $prov->audit("creating FreeBSD user");

    my $username = $p{'username'};
    my $shell    = $p{'shell'} || "/sbin/nologin";
    my $homedir  = $p{'homedir'};
    my $uid      = $p{'uid'};
    my $gid      = $p{'gid'};

    $user->{username} = $username;
    $user->_is_valid_username() or return;

    my $bak = $util->file_archive(
        file  => "/etc/master.passwd",
        fatal => 0,
        debug => 0,
    );
    $prov->audit("user->create: backed up master.passwd to $bak");

    # pw creates accounts using defaults from /etc/pw.conf
    my $pw = $util->find_bin( bin => "pw", debug => 0 );
    my $pwcmd = "$pw useradd -n $username ";

    $pwcmd .= "-d $homedir "                    if $homedir;
    $pwcmd .= "-u $uid "                        if $uid;
    $pwcmd .= "-g $gid "                        if $gid;
    $pwcmd .= "-c $p{'gecos'} "                 if $p{'gecos'};
    $pwcmd .= "-u 89 -g 89 -c Vpopmail-Master " if ( $username eq "vpopmail" );
    $pwcmd .= "-n $username -d /nonexistent -c Clam-AntiVirus "
        if ( $username eq "clamav" );
    $pwcmd .= "-s $shell ";
    $pwcmd .= "-m ";

    $prov->audit("pw command is: $pwcmd");
    return 1 if $p{test_mode};

    if ( $p{password} ) {
        $prov->audit("pw command is: $pwcmd -h 0 (****)");

        ## no critic
        my $FH;
        unless ( open $FH, "| $pwcmd -h 0" ) {
            return $prov->error(
                message => "user_add: opening pw failed for $username" );
        }
        print $FH "$p{password}\n";
        close $FH;
        ## use critic
    }
    else {
        $prov->audit( "pw command is: $pwcmd -h-");
        $util->syscmd( cmd => "$pwcmd -h-", debug => 0 );
    }

### TODO
    # call verify_master_passwd
    # set up user quotas

    $user->exists($username)
        ? return $prov->progress( num=>10, desc=>'created successfully')
        : return $prov->error( message=>'create user failed' );
}

sub create_group {

    my $self = shift;

    my %p = validate(
        @_,
        {   'group'  => { type => SCALAR },
            'gid'    => { type => SCALAR,  optional => 1, },
            'debug'  => { type => SCALAR,  optional => 1, default => 1 },
            'fatal'  => { type => BOOLEAN, optional => 1, default => 1 },
        }
    );

    # see if the group exists
    my $r = getgrnam( $p{group} );

    if ($r) {
        $prov->audit( "create_group: $p{group} exists" );
        return 2;
    };

    $prov->audit("create_group: installing $p{group} on $OSNAME");

    # use the pw tool to add the user
    my $pw = $util->find_bin( bin => "pw", debug => $p{debug} );

    my $cmd = "$pw groupadd -n $p{group}";
    $cmd .= " -g $p{gid}" if $p{gid};

    return $util->syscmd( cmd => $cmd, debug => $p{debug} );
}

sub destroy {

    my $self = shift;

    my %p = validate(   
        @_, 
        {   'username'  => { type => SCALAR, },
            'homedir'   => { type => SCALAR,  optional => 1, },
            'archive'   => { type => BOOLEAN, optional => 1, default => 0 },
            'prompt'    => { type => BOOLEAN, optional => 1, default => 0 },
            'test_mode' => { type => BOOLEAN, optional => 1, default => 0 },
            'fatal'     => { type => SCALAR,  optional => 1, default => 1 },
            'debug'     => { type => SCALAR,  optional => 1, default => 1 },
        },
    );

    $prov->audit("destroy user $p{username} on FreeBSD");

    $prov->progress( num => 1, desc => 'validating' );

    return 1 if $p{test_mode};

    # make sure user exists
    if ( !$user->exists( $p{username} ) ) {
        return $prov->progress(
            num  => 10,
            desc => 'error',
            err  => 'no such user'
        );
    }

### TODO
    # this would be a good time to archive the user if desired.

    my $bak = $util->file_archive(
        file  => "/etc/master.passwd",
        fatal => $p{fatal},
        debug => $p{debug},
    );
    $prov->progress( num => 2, desc => "backed up master.passwd.");

    my $pw = $util->find_bin( bin => 'pw', debug => 0 );
    my $cmd = "$pw userdel -n $p{username} -r";

    my $r = $util->syscmd( cmd => $cmd, debug => $p{debug}, fatal=>$p{fatal} );
    $prov->progress( num => 3, desc => "deleted user" );

## TODO
    # verify_master_passwd

    # validate that the user was removed
    if ( !$user->exists( $p{username} ) ) {
        return $prov->progress( num => 10, desc => 'completed' );
    }

    return $prov->progress(
        num    => 10,
        desc   => 'error',
        errmsg => $prov->{errors}->[-1]->{errmsg},
    );
}

sub destroy_group {

    my $self = shift;

    my %p = validate(   
        @_, 
        {   'group'     => { type => SCALAR, },
            'gid'       => { type => SCALAR,  optional => 1 },
            'test_mode' => { type => BOOLEAN, optional => 1, default => 0 },
            'fatal'     => { type => SCALAR,  optional => 1, default => 1 },
            'debug'     => { type => SCALAR,  optional => 1, default => 1 },
        },
    );

    $prov->audit("destroy group $p{group} on FreeBSD");

    $prov->progress( num => 1, desc => 'validating' );

    # make sure group exists
    if ( !$user->exists_group( $p{group} ) ) {
        return $prov->progress(
            num  => 10,
            desc => 'error',
            err  => 'no such group'
        );
    }

    my $bak = $util->file_archive(
        file  => "/etc/group",
        fatal => $p{fatal},
        debug => $p{debug},
    );
    $prov->progress( num => 2, desc => "backed up /etc/group");

    my $pw = $util->find_bin( bin => 'pw', debug => 0 );
    my $cmd = "$pw groupdel -n $p{group}";

    my $r = $util->syscmd( cmd => $cmd, debug => $p{debug} );
    $prov->progress( num => 3, desc => "deleted group");

    # validate that the group was removed
    if ( !$user->exists_group( $p{group} ) ) {
        return $prov->progress( num => 10, desc => 'completed' );
    }

    return $prov->progress(
        num    => 10,
        desc   => 'error',
        errmsg => $prov->{errors}->[-1]->{errmsg},
    );
}

sub verify_master_passwd {

  #    my $r = $user->verify_master_passwd ($passwd, $change, $debug)
  #    $r->{'error_code'} == 200 ? print "success" : print $r->{'error_desc'};
  #    $passwd is the filename of your master.passwd file.
  #    $change is whether the file should "shrink" or "grow"

    my ( $self, $passwd, $change, $debug ) = @_;
    my %r;

    my $new = ( stat($passwd) )[7];
    my $old = ( stat("$passwd.bak") )[7];

    # do we expect it to change?
    if ($change) {
        if ( $change eq "grow" ) {
            if ( $new > $old ) {

                # yay, it grew. response with a success code
                print
                    "verify_master_passwd: The file grew ($old to $new) bytes.\n"
                    if $debug;
                $r{'error_code'} = 200;
                $r{'error_desc'}
                    = "Success: the file grew from $old to $new bytes.";
                return \%r;
            }
            else {

                # boo, it didn't grow. return a failure code and
                # make an archived copy of it for recovery
                print
                    "verify_master_passwd: WARNING: new $passwd size ($new) is not larger than $old and we expected it to $change.\n"
                    if $debug;
                $util->file_archive( file => "$passwd.bak" );
                $r{'error_code'} = 500;
                $r{'error_desc'}
                    = "new $passwd size ($new) is not larger than $old and we expected it to $change.\n";
                return \%r;
            }
        }
        elsif ( $change eq "shrink" ) {
            if ( $new < $old ) {

                # yay, it shrank. response with a success code
                print
                    "verify_master_passwd: The file shrank ($old to $new) bytes.\n"
                    if $debug;
                $r{'error_code'} = 200;
                $r{'error_desc'}
                    = "The file shrank from $old to $new bytes.\n";
                return \%r;
            }
            else {

                # boo, it didn't shrink. return a failure code and
                # make an archived copy of it for recovery
                print
                    "verify_master_passwd: WARNING: new $passwd size ($new) is not smaller than $old and we expected it to $change.\n"
                    if $debug;
                $r{'error_code'} = 500;
                $r{'error_desc'}
                    = "new $passwd size ($new) is not smaller than $old and we expected it to $change.\n";
                $util->file_archive( file => "$passwd.bak" );
                return \%r;
            }
        }
    }

    # just report
    if ( $new == $old ) {
        print "verify_master_passwd: The files are the same size ($new)!\n"
            if $debug;
    }
    else {
        print
            "verify_master_passwd: The files are different sizes new: $new old: $old!\n"
            if $debug;
    }
}

sub archive {

=head2 archive

Create's a tarball of the users home directory. Typically done right before you rm -rf their home directory as part of a de-provisioning step.

    if ( $prov->user_archive("user") ) 
    {
        print "user archived";
    };

returns a boolean.

=cut

    my ( $self, $user, $debug ) = @_;

    my $tar = $util->find_bin( bin => "tar" );
    my $rm  = $util->find_bin( bin => "rm" );

    if ( !$user->exists($user) ) {
        $prov->error( { message => "user $user does not exist!" } );
    }

    my $homedir = ( getpwnam($user) )[7];
    unless ( -d $homedir ) {
        $prov->error( { message => "The home directory does not exist!" } );
    }

    my ( $path, $userdir ) = $util->path_parse( { dir => $homedir } );

    chdir($path)
        or $prov->error( { message => "couldn't cd to $path: $!\n" } );

    if ( -e "$path/$user.tar.gz" && -d "$path/$user" ) {
        carp "user_archive:\tReplacing old tarfile $path/$user.tar.gz.\n";
        system "$rm $path/$user.tar.gz";
    }

    print "\tArchiving $user\'s files to $path/$user.tar.gz...." if $debug;
    print "$tar -Pzcf $homedir.tar.gz $userdir\n";
    system "$tar -Pzcf $homedir.tar.gz $userdir";

    if ( -e "${homedir}.tar.gz" ) {
        print "done.\n" if $debug;
        return 1;
    }
    else {
        carp "\nFAILED: user_archive couldn't complete $homedir.tar.gz.\n\n";
        return 0;
    }
}


1;

=head1 NAME

Provision::Unix::User::FreeBSD - Provision Unix Accounts on FreeBSD systems

=head1 VERSION

Version 0.06

=head1 SYNOPSIS

Handles provisioning operations (create, modify, destroy) for system users on UNIX based operating systems.

    use Provision::Unix::User::FreeBSD;

    my $user_fbsd = Provision::Unix::User::FreeBSD->new();
    ...

=head1 FUNCTIONS


=head2 verify_master_passwd

Verify that new master.passwd is the right size. I found this necessary on some versions of FreeBSD as a race condition would cause the master.passwd file to get corrupted. Now I verify that after I'm finished making my changes, the new file is a small amount larger (or smaller) than the original.



=head1 AUTHOR

Matt Simerson, C<< <matt at tnpi.net> >>

=head1 BUGS

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



=head1 SUPPORT

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

    perldoc Provision::Unix


You can also look for information at:

=over 4

=item * RT: CPAN's request tracker

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

=item * AnnoCPAN: Annotated CPAN documentation

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

=item * CPAN Ratings

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

=item * Search CPAN

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

=back


=head1 ACKNOWLEDGEMENTS


=head1 COPYRIGHT & LICENSE

Copyright 2008 Matt Simerson, all rights reserved.

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


=cut

