#!/usr/bin/perl

#=============================================================================
# This command-line tool (shell ?) calls the various Doloforge managements functions
#
# Full help:
#   doloforge --help
#   doloforge --man
#   perldoc doloforge
#
# Test with:
#   perl ./src/doloforge --config ./t/doloforge.yaml test --check
#   perl ./src/doloforge --config ./t/doloforge.yaml config --check
#   sudo perl ./src/doloforge --config ./t/doloforge.yaml config --update
#=============================================================================


use strict;
use warnings;

use Getopt::Long;
use Pod::Usage;
use Config::YAML;
use Data::Dumper;
use Sys::Hostname;


# Default Doloforge configuration. Will be overriden by the config file options,
# which are overriden by command-line options...
# Doloforge does not have default values : every value must be given in the main
# config file, for administrator review. This hash should remain empty.
# Must not have sub-hashes, which will be erased from there upon reaching the
# corresponding section in the file, thus removing every sub-options before
# even reading them from file.
my $config = {};


#=============================================================================
# Startup options, read even before doing anything, used to fetch available
# commands and their options.
# Parses "[options]" in : doloforge [options] command [arguments] ...
# No variable defined below will be available in the ouside, except for $config
#
{
    my $option = {
        config => '/etc/doloforge/doloforge.yaml',
    };

    # Read command-line options : help, config file path, etc. until the first
    # non-option argument, which is the command to run, and will be parsed later.
    Getopt::Long::Configure('require_order');
    GetOptions($option,
        'help|?',           # simple generic help, made from the POD
        'man',              # complete man, from the POD
        'config|c=s',       # main config file location (doloforge.yaml)
        'host|h=s',         # fake hostname we're running on, that must match a configured host
        'stage|s=s',        # stage we're running in, when the current host can handle many
        'directory=s',      # change directory before doing anything (useful for cron jobs started in /root)
        'verbose|v+',       # add more -v to increase verbosity (up to 2)
        'quiet|q',          # -q will reduce verbosity to 0
        'debug|d',          # -d will increase verbosity to the max (4)
    # Parsing errors show SYNOPSIS from the POD in the current source file
    ) or pod2usage(-exitstatus => 2);

    # General help and man messages (not specific help, parsed later)
    pod2usage(-exitstatus => 1) if ($option->{help});
    pod2usage(-exitstatus => 0, -verbose => 2) if ($option->{man});
    pod2usage(-exitstatus => 2, -message => "Missing 'command' parameter: try adding '--help'.") unless ($#ARGV);

    # --quiet/--debug reset the --verbose count whatever it may be at the end of argument parsing
    $option->{verbose} = 0 if $option->{quiet};
    $option->{verbose} = 4 if $option->{debug};

    # Read full config file and setup the running environment.
    # Default keys are defined in the $config hash ref. They are overridden by the file contents.
    $config = Config::YAML->new(
        config => $option->{config},
        %$config,
    );

    # Read the include'd files defined in the main configuration under the "include:" array key
    foreach my $file (@{$config->{include}}) {
        # Read the file in a new hash object
        my $include = Config::YAML->new(config => "$config->{dir}->{conf}/$file");

        # The included configuration keys take precedence over all previously defined configuration keys.
        # Ignore the Config::YAML system specific keys (starting with "_" : "_infile", "_outfile", etc.)
        # FIXME : effectuer un Merge comme sur le dolodrome
        map {$config->{$_} = $include->{$_} unless ($_ =~ m/^_/)} keys(%$include);
    }

    # The command-line arguments take precedence over config-file arguments, within the
    # same $config hash reference.
    # After that merge, $option must be ignored
    map {$config->{$_} = $option->{$_}} keys(%$option);

    # If the configuration was not manually reviewed, stop there and warn the administrator
    pod2usage(-exitstatus => 2, -message => "Please review the '$option->{config}' config file, then set 'configured' before continuing.")
        unless ($config->{configured});

    # A little hack in the configuration : "host:" is set with the hostname,
    # if not already set in the config file. This allows to set a fake hostname
    # for testing purposes.
    # hostname is exported by Sys::Hostname
    $config->{host} = hostname unless (exists($config->{host}));
    pod2usage(-exitstatus => 2, -message => "The defined host '$config->{host}' does not exist in the cluster configuration 'cluster:$config->{host}:'.")
        unless exists($config->{cluster}->{$config->{host}});

    # If the --stage parameter does not match one of the stages defined in the config file, stop there and warn
    pod2usage(-exitstatus => 2, -message => "The --stage '$option->{stage}' parameter does not match one of the defined stages for host '$config->{host}'.")
        if ($option->{stage} && !grep(/^$option->{stage}$/, @{$config->{cluster}->{$config->{host}}->{stages}}));

    # Set the general umask allowing group-ownership permission (g+ws / each user belong to many group)
    umask 0002;

    # Change current directory, if asked for
    if ($config->{directory}) {
        chdir($config->{directory}) or die("Can't chdir to '$config->{directory}': $!");
    }

    # Print the full configuration read from files + command-line
    #print Data::Dumper->Dump([$config], [qw(config)]),"\n";
}


#=============================================================================
# @ARGV contains the remaining command(s) to run with all its proper options.
# Parses "command [arguments]" in : doloforge [options] command [arguments] ...
# Repeat until there is no more "command" on the command line.
#

# Require'd packages will be loaded from dynamically configured lib dir
push(@INC, $config->{dir}->{lib});
eval "require Doloforge::Command" or die;

while (@ARGV) {
    # The "command" passed is implemented in Doloforge::Command::* classes
    # Instanciate this class, with the global Doloforge configuration and class-specific one
    my $argv = lc(shift(@ARGV));
    my $command = &Doloforge::Command::Factory($argv, $config);
    my $class = ref($command);

    # Feed the hash with the rest of the command-line options for that command.
    # Command-line parameters will be accepted until the next non-option.
    # '--help' always exists... and is implemented in the base class...
    my $option = {};
    GetOptions($option, 'help|?', $command->getOptions())
        or $command->help();

    # Check the autorizations : if the host we're running on belongs to the --stage
    # we try to work in, or belongs to one of the defined stages, AND the host
    # is designated for that command and parameters, then we go...
    # First of all : get the stage from the command-line or stages from the config
    # While the config format allow for multiple stages per host, implementation
    # details at the daemon level are picky, and practically leaves room for one
    # stage per host only.
    my @stages;
    if ($config->{stage}) {
        # The command-line or config file specified a unique stage to consider
        push(@stages, $config->{stage});
    } else {
        # Use cluster:[hostname]:stages: as an array of stages the host is involved in.
        @stages = @{$config->{cluster}->{$config->{host}}->{stages}};
    }
    # Keep the actual stages we will run on, because the Doloforge::Service:: classes will be destroyed after the $stage is reset,
    # and their finish() method may be called without a stage, which triggers directory creation directly at / because there is no shared_dir.
    # See lib/Doloforge/Service.pm for the use of this setting.
    $config->{stages}->{this_run} = \@stages;

    # Create a notepad/scratchpad in the global configuration, for the duration of the method call.
    # This scratchpad can be used to share state between services (timestamp of modified files, etc.).
    $config->{notes} = {};

    # Each option can be a call to an object method : only the ones that were simply set,
    # and actually being methods, are called. It's up to the object not to confuse method
    # names and parameter names...
    foreach my $method (sort(keys(%$option))) {
        # The actual sub{} name is made of underscores ("_") instead of dashes ("-"), but everything else is left as-is...
        my $sub = $method;
        $sub =~ s/-/_/g;

        if (($option->{$method} eq 1) && $command->can($sub)) {

            # Save the original configured stage, as it will be replaced later on, and is necessary for the next command run...
            my $config_stage = $config->{stage};
            # Ditto for the stage's main shared directory, which may be stage-dependent if not host-dependent
            my $config_dir_shared = $config->{dir}->{shared};

            # For each of the stages we have to consider, run the same method again
            foreach my $stage (@stages) {
                # The considered stage will be available to commands in the general coniguration,
                # just as if it were hard-configured in the yaml file...
                $config->{stage} = $stage;
                $config->{dir}->{shared} = ($config_dir_shared or $config->{stages}->{$stage}->{dir}->{shared});

                # Check if the config file authorize running this method : the config entry
                # "cluster:[hostname]:command:[class]:" must have $method as one of its
                # values.
                # Do not warn if "cluster:[hostname]:command:[class]:" is not even mentionned.
                if (exists($config->{cluster}->{$config->{host}}) && exists($config->{cluster}->{$config->{host}}->{command}->{$argv})) {
                    if (($method eq 'help') || grep(/^(all|$method)$/, @{$config->{cluster}->{$config->{host}}->{command}->{$argv}})) {

                        print "Running $class->$method(" . Data::Dumper->new([$option])->Indent(0)->Terse(1)->Dump() . ") for '$stage'\n" if ($config->{verbose} >= 4);
                        # Execute the method of the class, with it's proper command-line options
                        $command->$sub($option);
                    } else {
                        print "Authorization denied for $class->$method(" . Data::Dumper->new([$option])->Indent(0)->Terse(1)->Dump() . ")\n" if ($config->{verbose} >= 1);
                    }
                }
            }

            # Restore original config stage, for the next command on the command-line.
            $config->{stage} = $config_stage;
            $config->{dir}->{shared} = $config_dir_shared;
        }
    }
    # FIXME : in case there is no method call, output an error "missing action for ..." ($command->help())
}


#=============================================================================
__END__

=encoding utf8

=head1 NAME

doloforge - Doloforge management command-line tool

=head1 SYNOPSIS

doloforge [options] command [command options...]

 Options:
    --help          brief help message
    --man           full documentation
    --config        path to the main doloforge.yaml config file
    --stage         stage we're running in, in case the host can handle many
    --directory     change directory before doing anything (useful for cron jobs started in /root)
    --verbose       verbose execution (multiple times = increase verbosity)
    --quiet         get back to no verbosity at all
 Interactive commands:
    config          show configuration parameters and initialize the host
    client          create/update a client
    project         create/update a project
    user            create/update a developer
 Automated commands:
    TODO

=head1 EXAMPLES

Begin with the following command to check the current configuration:

    doloforge config --check

Once the configuration is reviewed, setup the needed files:

    doloforge config --setup

You're now ready to create new clients, project, and developers:

    doloforge client --add --name my_client ...
    doloforge project --add --name www --client my_client ...
    doloforge user --add --name johndoe ...

Once everything is defined, issue the following to actually create files
and directories:

    doloforge config --update

You either have to run this command in each host the doloforge system is
setup, or use some replication tool to copy files and issue the --update
for you (csync2 is a good choice).

=head1 DESCRIPTION

The Doloforge system is a web development, testing and hosting platform,
based on standard packages such as Apache, mod_perl, PostgreSQL, etc.
The system allows the creation and maintenance of a cluster of hosts,
dedicated to different roles of the whole system.

B<doloforge> is the main command to manage the Doloforge system.
You first need to retreive the general configuration, then install various
users and other packages config files.

Once you're there, you have to create clients and projects (for these clients),
and add users (developers), affected to projects.
When runnng the system on multiple hosts and stages, you can publish client
and project code from stage to stage. Automatic crontabs will run periodically,
calling this command and running scripts for each client and project.

Every aspect of the automation is configured in the base configuration directory.
Check there for information about what's running.

=head1 AUTHOR

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

=head1 SEE ALSO

doloforge(1)

=cut
