#=============================================================================
# Doloforge::Command::Config : configuration check and setup
#
# Checks the main config file against the actual host setup, and eventually
# implements that configuration, creating system users and directories as
# needed.
#
# Full help :
#   doloforge config --man
#
# Testing this package :
#   fakeroot perl ./src/doloforge --config t/doloforge.yaml config --setup config --check
#=============================================================================


package Doloforge::Command::Config;

use strict;
use warnings;

# This base class implements the constructor and help methods in a generic way.
use base qw(Doloforge::Command);
use Sys::Hostname;


#=============================================================================
=encoding utf8

=head1 NAME

Doloforge::Config - Configuration check and setup

=head1 SYNOPSIS

    # Check the configuration (missing users and directories, etc.)
    doloforge config --check
    # Create the missing stuff on the local host
    doloforge config --setup

    # Review the modifications before actually making them
    doloforge config --update --diff
    doloforge config --update

    # Force modifications, in case sub-templates disturb dependencies
    # --force and --diff can be cumulated
    doloforge config --update --force

=head1 DESCRIPTION

Todo

=cut


#=============================================================================
# Method getOptions
#
# Accepted arguments from the command-line, in the form accepted by Getopt::Long.
# Options with no value that match method names of that package will be called as
# methods, with the decoded options as parameters.

sub getOptions {
    return (
        'check',                # checks the main configuration parameters, and reports on stdout
        'setup',                # implements needed things (main user:group, base directories)
        'update',               # update the actual configuration from client/project descriptions
                                # does this by calling client->update(), which in turn calls project->update()
        'list',                 # similarly to "client --list" and "projkect --list": list the configured stages
        # update parameters :
        'diff',                 # display a diff of every file during update, instead of simply telling how many were changed
        'force',                # force regeneration, regardless of timestamps (works around dependency problems related to sub-templates)
    );
}

#=============================================================================
# Method check
#
# Checks the main configuration parameters : users, dirs, etc.

sub check {
    my $self = shift;

    # Just print the name of the current host, to help with initial configuration
    # hostname is exported by Sys::Hostname
    my $hostname = hostname;
    $self->message("Current host is '$hostname', configured host is '$self->{global}->{host}'");
    $self->message("Current stage is '$self->{global}->{stage}'");

    # Report all roles :
    foreach my $role (qw(user_role account_role nfs_role db_role appserv_role mail_role)) {
        $self->message("Current host has role '$role' : " . ($self->has_role($role) ? 'Yes' : 'No'));
    }
    
    # Call the generic check-and-setup method
    my ($option) = @_;
    return $self->_config(0, $self->{global}->{stage});
}

#=============================================================================
# Method setup
#
# Actually setup the configuration parameters : create users, create dirs, etc.

sub setup {
    my $self = shift;

    $self->error("setup: must be run as root (try su, sudo, etc.)")
        unless ($self->check_uid('root'));

    # Call the generic check-and-setup method
    my ($option) = @_;
    return $self->_config(1, $self->{global}->{stage});
}
 

#=============================================================================
# Private methods
#
sub _config {
    my $self = shift;

    # Actually just check or completely setup things ?
    # This method does both...
    my ($setup, $stage) = @_;

    # FIXME : check things for each roles, and no more...

    # Verify that the doloforge main userid and groupid exist
    my ($user, $userid) = @{$self->{global}->{permissions}->{user}};
    my ($group, $groupid) = @{$self->{global}->{permissions}->{group}};
    my ($name, $uid, $gid);
    ($name, $gid) = (getgrnam($group))[0,2];
    if (not defined($name) or ($name ne $group)) {
        if ($setup) {
            system('/usr/sbin/addgroup', '--system', '--gid', $groupid, $group);
        } else {
            $self->message("Missing system group '$group' GID=$groupid");
        }
    }
    if (defined($gid) and ($gid != $groupid)) {
        $self->message("Wrong $group GID=$gid (expected $groupid) : manual fix only");
    }
    ($name, $uid, $gid) = (getpwnam($user))[0,2,3];
    if (not defined($name) or ($name ne $user)) {
        if ($setup) {
            system('/usr/sbin/adduser',
                '--system', '--disabled-password',
                '--uid', $userid,
                '--gid', $groupid,
                '--gecos', $self->gecos_string('Doloforge system user', 'system', undef, undef, $self->now()),
                $user);
        } else {
            $self->message("Missing system user '$user' UID=$userid");
        }
    }
    if (defined($uid) and (($uid != $userid) or ($gid != $groupid))) {
        $self->message("Wrong $user UID=$uid GID=$gid (expected $userid:$groupid) : manual fix only");
    }

    # Verify that each mandatory directory actually exists and it's
    # permissions and ownership are correct.
    # Ownership is tested numerically, to check that IDs are actually the same
    # on different hosts sharing an NFS filesystem.
    foreach my $dirid (qw(lib skel conf projconf projlib users clients shared varlib varlog varcache)) {
        # Exclude some directories, depending on the host roles
        next if (($dirid eq 'users' and not $self->has_role('user_role'))
            or ($dirid eq 'clients' and not $self->has_role('account_role'))
            or ($dirid eq 'shared' and not $self->has_role('nfs_role'))
            or ($dirid =~ m/^var/ and not $self->has_role('appserv_role'))
        );
        # Get the directory setup from the configuration
        $self->dir_entry($setup, "global $dirid",                           # check or setup, directory description in messages
            $self->{global}->{dir}->{$dirid},                               # directory name
            oct($self->{global}->{permissions}->{$dirid}->[0]),             # $perm, written in octal
            $self->{global}->{permissions}->{$dirid}->[1],                  # $user ID
            $self->{global}->{permissions}->{$dirid}->[2],                  # $group ID
            "$self->{global}->{dir}->{skel}/dir/$dirid",                    # skeleton directory tree, or nothing at all if it doesn't exist
        );
    }
}

#=============================================================================
# Method update
#
# Update the configuration on the host, depending on client/project config files
# This method must be run of each host, and will decide what to do from cluster
# information (the roles of the running host)

sub update {
    my $self = shift;

    $self->error("update: must be run as root (try su, sudo, etc.)")
        unless ($self->check_uid('root'));

    my ($option) = @_;

    # Dynamically create a new instance of the Doloforge::Command::Client class
    my $client = &Doloforge::Command::Factory('client', $self->{global});

    # Call the update method of the client class, which will update all clients by default
    # We just hope that local options will not create unexpected behaviour of the "child" method
    delete $option->{name};
    $client->update($option);
}

#=============================================================================
# Method list
#
# List the configured stages, which is just "the stage we're called with", as
# the main doloforfge commande will call this method for each configured stage.
# To be used to run cron jobs in parallel on every stage a host handles.

sub list {
    my $self = shift;

    my ($option) = @_;

    # Simply output the current stage (cf. Client.pm:sub list)
    print "$self->{global}->{stage}\n";
}



1;
__END__

=head1 AUTHOR

Nicolas Huillard <F<nicolas@huillard.net>>, Dolomède

=head1 SEE ALSO

doloforge(1)

=cut
