#!/usr/bin/perl
# $Id: youri-check.in 1615 2007-04-11 11:45:40Z guillomovitch $

=head1 NAME

youri-check - package check agent

=head1 VERSION

Version 1.0

=head1 SYNOPSIS

youri-check [options] <test|report>

Options:

    --config <file>        use file <file> as config file
    --skip-media <media>   skip media <media>
    --skip-test <test>     skip test <test>
    --skip-report <report> skip report <report>
    --parallel             parallel run
    --verbose              verbose run
    --timestamp            add timestamps in output
    --test                 test run
    --list <category>      list items from given category
    --help                 print this help message

=head1 DESCRIPTION

B<youri-check> allows to check packages in a repository.

In test mode, all medias defined in configuration are passed to a list of
test plugins, each of them storing their result in a persistent resultset. In
report mode, this resultset is passed to a list of report plugins, each of them
producing arbitrary effects.

In normal run, all tests or reports are processed sequentially, whereas they
are processed simultaneously in parallel run (using B<--parallel> option). The
second one is faster, but generally more expensive in term of memory usage,
even if parsed headers caching is automatically desactivated. Also, some
configurations (such as the use of sqlite as result database) are not
compatible with parallel mode.

=head1 OPTIONS

=over

=item B<--config> <file>

Use given file as configuration, instead of normal one.

=item B<--skip-media> <media>

Skip media with given identity.

=item B<--skip-test> <test>

Skip test with given identity.

=item B<--skip-report> <report>

Skip report with given identity.

=item B<--parallel>

Run all plugins parallelously

=item B<--verbose>

Produce more verbose output (can be used more than once)

=item B<--timestamp>

Add timestamps in output.

=item B<--test>

Don't perform any modification.

=item B<--list> I<category>

List available items from given category and exits. Category must be either
B<medias>.

=item B<--help>

Print a brief help message and exits.

=back

=head1 CONFIGURATION

Configuration is read from the first file found among:

=over

=item * the one specified by B<--config> option on command-line

=item * $HOME/.youri/check.conf

=item * /etc/youri/check.conf

=back

The configuration file should be a YAML-format files, with the following
mandatory top-level directives:

=over

=item B<resultset>

The definition of the resultset plugin to be used.

=item B<medias>

The list of available media plugins, indexed by their identity.

=item B<tests>

The list of available test plugins, indexed by their identity.

=item B<reports>

The list of available report plugins, indexed by their identity.

=back

Additional optional top-level directives:

=over

=item B<netconfig>

libnet configuration options (see Net::Config).

=item B<resolver>

The definition of the resolver plugin to be used.

=item B<preferences>

The definition of the preferences plugin to be used.

=back

Here is a sample configuration:

    # Use a local sqlite database for storing results
    resultset:
        class: Youri::Check::Resultset::DBI
        options:
            driver: sqlite
            base: youri

    # Test packages older than 12 months
    checks:
        age:
            class: Youri::Check::Test::Age
            options:
                max: 12 months
                pattern: %m months

    # Reports results as HTML files
    reports:
        file:
            class: Youri::Check::Report::File
            options:
                to: ${home}/youri
                global: 1
                individual: 1
                formats:
                    html:
                        class: Youri::Check::Report::File::Format::HTML

    # Test PLF free media for cooker/i586
    medias:
        free:
            class: Youri::Media::URPM
            options:
                name: free
                type: binary
                hdlist: ftp://ftp.free.fr/pub/Distributions_Linux/plf/mandriva/cooker/free/binary/i586/hdlist.cz

See commented configuration files in the distribution for more complex
examples.

=head1 SEE ALSO

Youri::Config, for additional details about configuration file format.

Each used plugin man page, for available options.

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2002-2006, YOURI project

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

=cut

use strict;
use warnings;
use lib '/usr/share/youri/lib';

use Youri::Config;
use Youri::Utils 0.002;
use Pod::Usage;
use Net::Config qw/%NetConfig/;

my $config = Youri::Config->new(
    args => {
        'skip-test'   => '=s@',
        'skip-report' => '=s@',
        'skip-media'  => '=s@',
        'timestamp'   => '!',
        'parallel'    => '|p!',
        'test'        => '|t!',
        'list'        => '|l!'
    },
    directories => [ "$ENV{HOME}/.youri", '/etc/youri' ],
    file   => 'check.conf',
);

my @media_ids = split(/\s+/, $config->get_param('medias'));

if ($config->get_arg('list')) {
    my $category = $ARGV[0];
    pod2usage(-verbose => 0, -message => "No category specified, aborting\n")
        unless $category;
    if ($category eq 'medias') {
        print join(' ', keys %{$config->get_param('medias')}) . "\n";
    } else {
        pod2usage(
            -verbose => 0,
            -message => "Invalid category $category, aborting\n"
        )
    }
    exit 0;
}

pod2usage(
    -verbose => 0,
    -message => "No mode specified, aborting\n"
) unless @ARGV;

my $mode = $ARGV[0];

# convenient global flags
my $test      = $config->get_arg('test');
my $verbose   = $config->get_arg('verbose');
my $timestamp = $config->get_arg('timestamp');
my $parallel  = $config->get_arg('parallel');

# libnet configuration
my $netconfig_conf = $config->get_param('netconfig');
if ($netconfig_conf) {
    $NetConfig{$_} = $netconfig_conf->{$_} foreach keys %{$netconfig_conf};
}

# resultset creation
my $resultset_conf = $config->get_param('resultset');
die "No resultset defined" unless $resultset_conf;

log_message("Creating resultset", $timestamp, $parallel) if $verbose;
my $resultset = create_instance(
    'Youri::Check::Resultset',
    $resultset_conf,
    {
        test     => $test,
        verbose  => $verbose > 0 ? $verbose - 1 : 0,
        parallel => $parallel
    }
);

my $children;

if ($mode eq 'test') {

    # additional objects

    my $resolver;
    my $resolver_conf = $config->get_param('resolver');
    if ($resolver_conf) {
        log_message("Creating maintainer resolver", $timestamp, $parallel)
            if $verbose;
        eval {
            $resolver = create_instance(
                'Youri::Check::Maintainer::Resolver',
                $resolver_conf,
                {
                    test    => $test,
                    verbose => $verbose > 1 ? $verbose - 2 : 0,
                }
            );
        };
        print STDERR "Failed to create maintainer resolver: $@\n" if $@;
    }

    my $preferences;
    my $preferences_conf = $config->get_param('preferences');
    if ($preferences_conf) {
        log_message("Creating maintainer preferences", $timestamp, $parallel)
            if $verbose;
        eval {
            $preferences = create_instance(
                'Youri::Check::Maintainer::Preferences',
                $preferences_conf,
                {
                    test      => $test,
                    verbose   => $verbose > 1 ? $verbose - 2 : 0,
                }
            );
        };
        print STDERR "Failed to create maintainer preferences: $@\n" if $@;
    }

    my @medias;
    my $skip_media = $config->get_arg('skip-media');
    my %skip_media = $skip_media ?  map { $_ => 1 } @{$skip_media} : ();
    foreach my $id (keys %{$config->get_param('medias')}) {
        next if $skip_media{$id};
        log_message("Creating media $id", $timestamp, $parallel) if $verbose;
        my $media_conf = $config->get_param('medias')->{$id};
        eval {
            push(
                @medias,
                 create_instance(
                    'Youri::Media',
                    $media_conf,
                    {
                        id      => $id,
                        test    => $test,
                        verbose => $verbose > 0 ? $verbose - 1 : 0,
                        cache   => $parallel ? 0 : 1
                    }
                )
            );
        };
        print STDERR "Failed to create media $id: $@\n" if $@;
    }

    # prepare resultset
    $resultset->reset();
    $resultset->set_resolver($resolver) if $resolver;

    my $skip_test = $config->get_arg('skip-test');
    my %skip_test = $skip_test ?  map { $_ => 1 } @{$skip_test} : ();
    foreach my $id (keys %{$config->get_param('tests')}) {
        next if $skip_test{$id};
        log_message("Creating test $id", $timestamp, $parallel) if $verbose;
        my $test;
        my $test_conf = $config->get_param('tests')->{$id};
        eval {
            $test = create_instance(
                'Youri::Check::Test',
                $test_conf,
                {
                    id         => $id,
                    test       => $test,
                    verbose    => $verbose > 0 ? $verbose - 1 : 0,
                    resolver   => $resolver,
                    preferences => $preferences,
                }
            );
        };
        if ($@) {
            print STDERR "Failed to create test $id: $@\n";
        } else {
            if ($parallel) {
                # fork
                my $pid = fork;
                die "Can't fork: $!" unless defined $pid;
                if ($pid) {
                    # parent process
                    $children++;
                    next;
                } else {
                    log_message(
                        "Forking child process $id", $timestamp, $parallel
                    ) if $verbose;
                }
            }
            eval {
                $test->prepare(@medias);
            };
            if ($@) {
                print STDERR "Failed to prepare test $id: $@\n";
            } else {
                # clone resultset in child process
                $resultset = $parallel ?
                    $resultset->clone() :
                    $resultset;

                foreach my $media (@medias) {
                    next if $media->skip_test($id);
                    my $media_id = $media->get_id();
                    log_message(
                        "running test $id on media $media_id",
                        $timestamp,
                        $parallel
                    ) if $verbose;
                    eval {
                        $test->run($media, $resultset);
                    };
                    if ($@) {
                        print STDERR "Failed to run test $id on media $media_id: $@\n";
                    }
                }
            }
            if ($parallel) {
                # child process
                log_message(
                    "Finishing child process $id", $timestamp, $parallel
                ) if $verbose;
                exit;
            }
        }
    }

} elsif ($mode eq 'report') {

    my $skip_report = $config->get_arg('skip-report');
    my %skip_report = $skip_report ?  map { $_ => 1 } @{$skip_report} : ();
    foreach my $id (keys %{$config->get_param('reports')}) {
        next if $skip_report{$id};
        log_message("Creating report $id", $timestamp, $parallel) if $verbose;
        my $report;
        my $report_conf = $config->get_param('reports')->{$id};
        eval {
            $report = create_instance(
                'Youri::Check::Report',
                $report_conf,
                {
                    id      => $id,
                    test    => $test,
                    verbose => $verbose > 0 ? $verbose - 1 : 0,
                    config  => $config,
                }
            );
        };
        if ($@) {
            print STDERR "Failed to create report $id: $@\n";
        } else {
            if ($parallel) {
                # fork
                my $pid = fork;
                die "Can't fork: $!" unless defined $pid;
                if ($pid) {
                    # parent process
                    $children++;
                    next;
                } else {
                    log_message(
                        "Forking child process $id", $timestamp, $parallel
                    ) if $verbose;
                }
            }

            # clone resultset in child process
            $resultset = $parallel ?
                $resultset->clone() :
                $resultset;

            log_message("running report $id", $timestamp, $parallel)
                if $verbose;
            eval {
                $report->run($resultset);
            };
            if ($@) {
                print STDERR "Failed to run report $id: $@\n";
            }

            if ($parallel) {
                # child process
                log_message(
                    "Finishing child process $id", $timestamp, $parallel
                ) if $verbose;
                exit;
            }
        }
    }
} else {
    die "Invalid mode $mode";
}

# wait for all forked processus termination
while ($children) {
    wait;
    $children--;
}

log_message("Finishing", $timestamp, $parallel) if $verbose;
