#!/usr/bin/perl

###############################################################################
## Copyright (c) 2009-2012 SUSE LINUX Products GmbH, Nuernberg, Germany.
###############################################################################

use strict;
use warnings;
use FindBin;
BEGIN { unshift @INC, "$FindBin::Bin/../www/perl-lib" }

use Getopt::Long;
use File::Basename;
use Text::ASCIITable;
use Date::Format;

use SMT::Utils;
use SMT::CLI;
use SMT::Repositories;
use SMT::Filter;
use SMT::Mirror::Utils;
use SMT::Mirror::RpmMd;
use SMT::Parser::RpmMdPatches;
use SMT::Mirror::Yum;
use SMT::Parser::FilteredRepoChecker;

my $vblevel  = LOG_ERROR|LOG_WARN|LOG_INFO1|LOG_INFO2;
my $debug    = 0;
my $logfile = "/dev/null";
my $help     = 0;

my $dbreplfile = undef;
my $dbreplacement = [];

my $gentest = 0;
my $genprod = 0;
my $nohardlink = 0;

my @userpatches = ();
my $category = undef;
my $individually = 0;

my $all = 0;
my $nodesc = 0;
my $sortbyversion = 0;
my $sortbycategory = 0;

# these are for signing the testing/production snapshots, if changed
my $keyid = undef;
my $keypass = undef;

my $group = "default";
my $testingdir = "testing";
my $productiondir = "";

my $optres = GetOptions("testing|t"    => \$gentest,
                        "production|p" => \$genprod,
                        "nohardlink"   => \$nohardlink,
                        "patch=s"      => \@userpatches,
                        "category=s"   => \$category,
                        "individually|i" => \$individually,
                        "all|a"        => \$all,
                        'nodesc'       => \$nodesc,
                        'sort-by-version' => \$sortbyversion,
                        'sort-by-category' => \$sortbycategory,
                        'keyid=s'      => \$keyid,
                        'keypass=s'    => \$keypass,

                        "dbreplfile=s" => \$dbreplfile,

                        "debug|d"      => \$debug,
                        "verboselevel|v=i" => \$vblevel,
                        "logfile|L=s"  => \$logfile,
                        "help|h"       => \$help,
                        "group|g=s"    => \$group
                       );

sub printhelp
{
    my $scriptname = shift;

    print $scriptname . " status <repository>\n";
    print $scriptname . " listupdates <repository>\n";
    print "    [--patch <patch> ... | --category <category>] [--nodesc]\n";
    print "    [--sort-by-version | --sort-by-category]\n";
    print "\n";
    print $scriptname . " forbid <repository>\n";
    print "    [--patch <patch> ... | --category <category> | --all]\n";
    print "    [--individually]\n";
    print "\n";
    print $scriptname . " allow <repository>\n";
    print "    [--patch <patch> ... | --category <category> | --all]\n";
    print "    [--individually]\n";
    print "\n";
    print $scriptname . " createrepo <repository>\n";
    print "    --testing|--production [--nohardlink] [--keyid ID [--keypass password]] \n";
    print "\n";
    print $scriptname . " --help | -h\n";
    print "\n";
    print $scriptname . " listgroups \n";
    print "\n";
    print $scriptname . " creategroup <name> <testing dirname> <production dirname> \n";
    print "\n";
    print $scriptname . " removegroup <name> \n";
    print "\n";
    print __("Repository can be specified using its name and target or ID\n");
    print __("from 'smt-repos' output. See 'man smt-staging' for more deails.\n");
    print "\n";
    print __("Options:\n");
    print "--testing          (-t) " . __("Generate testing repository with selected updates.\n");
    print "--production       (-p) " . __("Generate production repository with selected updates.\n");
    print "--patch <name-ver>      " . __("Specify patch to forbid or allow using name and version.\n");
    print "--category <category>   " . __("Specify patch category to forbid or allow.\n");
    print "                        " . __("Choose one of: recommended, security, optional.\n");
    print "--nodesc                " . __("Do not print summaries and descriptions of udpates.\n");
    print "--sort-by-version       " . __("Sort patch list by patch version.\n");
    print "--sort-by-category      " . __("Sort patch list by patch category.\n");
    #print "--hardlink size         " . __("Search for duplicate files with size > 'size' (in Kilobytes) and create hardlinks\n");
    #print "                        " . __("for them\n");
    print "--group <name>     (-g) " . __("Specify a staging group name to work on (default is 'default')\n");
    print "--nohardlink            " . __("If a file already exists on local filesystem, do not\n");
    print "                        " . __("link it into the mirrored repository. Copy it instead.\n");
    print "--debug            (-d) " . __("Enable debug mode.\n");
    print "--verboselevel     (-v) " . __("Set the level of verbosity.\n");
    print "--logfile <file>   (-L) " . __("Log to specified file.\n");
}

if ($help || !$optres)
{
    printhelp(basename($0));
    exit 0;
}

if(!SMT::Utils::dropPrivileges())
{
    print STDERR __("Unable to drop privileges. Abort!\n");
    exit 1;
}

$SIG{INT}  = \&signal_handler;
$SIG{TERM} = \&signal_handler;

if (@ARGV < 1 || ($ARGV[0] ne 'listgroups' && @ARGV < 2))
{
    print STDERR __("Too few arguments. At least a command and repository ID are required.\n");
    print "\n";
    printhelp(basename($0));
    exit 1;
}

# the first non-option argument is the command

my $command = undef;
if (not
    $ARGV[0] eq 'createrepo' ||
    $ARGV[0] eq 'status' ||
    $ARGV[0] eq 'listupdates' ||
    $ARGV[0] eq 'forbid' ||
    $ARGV[0] eq 'allow' ||
    $ARGV[0] eq 'creategroup' ||
    $ARGV[0] eq 'removegroup' ||
    $ARGV[0] eq 'listgroups')
{
    printf STDERR __("Unknown command '%s'\n"), $ARGV[0];
    print "\n";
    printhelp(basename($0));
    exit 1;
}
else
{
    $command = shift @ARGV;
}

# get a lock

if(!SMT::Utils::openLock("smt-staging"))
{
    print __("Other staging process is still running.\n");
    exit 0;
}

# open the logfile

$vblevel = LOG_ERROR|LOG_WARN|LOG_INFO1|LOG_INFO2|LOG_DEBUG|LOG_DEBUG2 if($debug);
my $log = SMT::Utils::openLog($logfile);

# get the config

my $cfg = undef;
eval
{
    $cfg = SMT::Utils::getSMTConfig();
};
if($@ || !defined $cfg)
{
    SMT::Utils::printLog($log, $vblevel, LOG_ERROR, sprintf(__("Cannot read the SMT configuration file: %s"), $@));
    SMT::Utils::unLockAndExit( "smt-mirror", 1, $log, $vblevel );
}

# connect to database (all commands need it)

my $dbh = SMT::Utils::db_connect();
if(!$dbh)
{
    if(!SMT::Utils::unLock("smt-staging"))
    {
        SMT::Utils::printLog($log, $vblevel, LOG_ERROR, __("Cannot remove lockfile."));
    }
    SMT::Utils::printLog($log, $vblevel, LOG_ERROR, __("Cannot connect to database."));
    exit 1;
}

my $useragent = SMT::Utils::createUserAgent(log => $log, vblevel => $vblevel);

if ($command eq 'creategroup')
{
    my $groupname = shift @ARGV;
    my $testingdirname = shift @ARGV;
    my $productiondirname = shift @ARGV;

    my $ret = SMT::CLI::createGroup( $groupname, $testingdirname, $productiondirname,
                                     dbh => $dbh, log => $log, vblevel => $vblevel );
    if(!SMT::Utils::unLock("smt-staging"))
    {
        SMT::Utils::printLog($log, $vblevel, LOG_ERROR, __("Cannot remove lockfile."));
    }
    if(!$ret)
    {
        exit 1;
    }
    exit 0;
}
elsif ($command eq 'removegroup')
{
    my $groupname = shift @ARGV;
    my $ret = SMT::CLI::removeGroup( $groupname, dbh => $dbh, log => $log, vblevel => $vblevel, cfg => $cfg );
    if(!SMT::Utils::unLock("smt-staging"))
    {
        SMT::Utils::printLog($log, $vblevel, LOG_ERROR, __("Cannot remove lockfile."));
    }
    if(!$ret)
    {
        exit 1;
    }
    exit 0;
}
elsif ($command eq 'listgroups')
{
    SMT::CLI::listGroups( dbh => $dbh, log => $log, vblevel => $vblevel );
    if(!SMT::Utils::unLock("smt-staging"))
    {
        SMT::Utils::printLog($log, $vblevel, LOG_ERROR, __("Cannot remove lockfile."));
    }
    exit 0;
}

# the second is the repository id or name (if there is the third argument)
my $reponameid = shift @ARGV;

# the third (optional) is always the repository target (from the smt-repos output)
my $target = shift @ARGV;

# get staging groups
($testingdir, $productiondir) = SMT::Utils::getStagingGroupPaths($dbh, $group);
if(! $testingdir)
{
    if(!SMT::Utils::unLock("smt-staging"))
    {
        SMT::Utils::printLog($log, $vblevel, LOG_ERROR, __("Cannot remove lockfile."));
    }
    SMT::Utils::printLog($log, $vblevel, LOG_ERROR, __("Unknown staging group $group."));
    exit 1;
}

# get the repository from DB (all commands need it)

my $rh = SMT::Repositories::new($dbh, $log);
my $repo = $rh->getRepository($reponameid) if (not defined $target);
my $repoid = undef;
$rh->getAndClearErrorMessage();

if (not defined $repo)
{
    if (not defined $target)
    {
        # not found by CATALOGID, lets try by ID (row number) from smt-repos

        my $repos = $rh->getAllRepositories();
        my $err = $rh->getAndClearErrorMessage();
        my $count = scalar keys %$repos;

        SMT::Utils::printLog($log, $vblevel, LOG_ERROR, 'DB error: ' . $err)
            if ($err);

        if ( $reponameid =~ /^[0-9]+$/ &&
            int($reponameid) > 0 && int($reponameid) <= $count)
        {
            foreach my $r (keys %$repos)
            {
                if ($repos->{$r}->{rownr} == int($reponameid))
                {
                    $repo = $repos->{$r};
                    $repoid = $r;
                    last;
                }
            }
        }
        else
        {
            SMT::Utils::printLog($log, $vblevel, LOG_ERROR,
                sprintf __("Repository ID '%s' not found."), $reponameid);
            printf __("Use '%s' to list all available repositories.\n"), 'smt-repos';
            SMT::Utils::unLockAndExit( "smt-staging", 0, $log, $vblevel );
        }
    }
    # try to find by name & target
    else
    {
        my $repos = $rh->getAllRepositories({
            SMT::Repositories::NAME => $reponameid,
            SMT::Repositories::TARGET => $target
        });

        my $count = scalar keys %$repos;
        if ($count <= 0)
        {
            SMT::Utils::printLog($log, $vblevel, LOG_ERROR,
                sprintf __("Repository named '%s' with target '%s' not found."), $reponameid, $target);
            printf __("Use '%s' to list all available repositories.\n"), 'smt-repos';
            SMT::Utils::unLockAndExit( "smt-staging", 1, $log, $vblevel );
        }
        elsif ($count == 1)
        {
            my @k = keys %$repos;
            $repo = $repos->{$k[0]};
            $repoid = $k[0];
        }
        else
        {
            # select manually
            SMT::Utils::printLog($log, $vblevel, LOG_ERROR,
                sprintf __("Multiple repositories named '%s' with target '%s' found."), $reponameid, $target);
            print __('Selection from multiple repositories is not implemented. Use repository ID.') . "\n";
            SMT::Utils::unLockAndExit( "smt-staging", 1, $log, $vblevel );
        }
    }
}
else
{
    SMT::Utils::printLog($log, $vblevel, LOG_DEBUG,
        sprintf "Found repository by CATALOGID '%s'.", $reponameid);
    $repoid = $reponameid;
}

# $repo and $repoid should always be defined at this point

if ($repo->{STAGING} ne 'Y')
{
    SMT::Utils::printLog($log, $vblevel, LOG_ERROR,
        sprintf __("Staging is not enabled for repository '%s', target '%s'."), $repo->{NAME}, $repo->{TARGET});
    print sprintf (__('Use "%s" to enable staging for this repository.'),
        'smt repos \'' . $repo->{NAME} . '\' \'' . $repo->{TARGET} . '\' -s');
    print "\n";
    SMT::Utils::unLockAndExit( "smt-staging", 0, $log, $vblevel );
}

# now we're ready to do the real work

my $basepath = $cfg->val("LOCAL", "MirrorTo");
my $localfullrepopath = $rh->getFullRepoPath($repoid, $basepath, $group);
my $localtestingrepopath = $rh->getTestingRepoPath($repoid, $basepath, $group);
my $localproductionrepopath = $rh->getProductionRepoPath($repoid, $basepath, $group);


# list of known patch categories
my %exists_category = (
    'recommended'   => 1,
    'security'      => 1,
    'optional'      => 1);


###############################################################################
# testing/production repository status
###############################################################################

if ($command eq 'status')
{
    my $filter = SMT::Filter->new(log => $log, vblevel => $vblevel);
    $filter->load($dbh, $repoid, $group);

    print __('Reading patches...') . "\n";

    print "\n";
    print __('Repository:          ') . $repo->{NAME} . "\n";
    print __('Target:              ') . $repo->{TARGET} . "\n";

    my $fstatus = SMT::Mirror::Utils::getStatus($localfullrepopath);
    my $tstatus = SMT::Mirror::Utils::getStatus($localtestingrepopath);
    my $pstatus = SMT::Mirror::Utils::getStatus($localproductionrepopath);

    print __('Last mirrored:       ');
    if (!$fstatus)
    {
        print __('never')
    }
    else
    {
        print $repo->{LAST_MIRROR}
    }
    print "\n";

    print __('Testing snapshot:    ');
    if (!$tstatus)
    {
        print __('not created')
    }
    else
    {
        print sprintf __('created (corresponding mirror date: %s)'),
                        time2str('%Y-%m-%d %T', $tstatus)
    }
    print "\n";

    print __('Production snapshot: ');
    if (!$pstatus)
    {
        print __('not created')
    }
    else
    {
        print sprintf __('created (corresponding mirror date: %s)'),
                        time2str('%Y-%m-%d %T', $pstatus)
    }
    print "\n";

    my $parser = SMT::Parser::RpmMdPatches->new(
        log => $log,
        vblevel => $vblevel);
    $parser->resource($localfullrepopath);
    my $patches = $parser->parse("repodata/updateinfo.xml.gz");

    print "\n";
    printPatchCounts($patches, $filter);
}

###############################################################################
# list the udates
###############################################################################

elsif ($command eq 'listupdates')
{
    if (defined $category && not $exists_category{$category})
    {
        SMT::Utils::printLog($log, $vblevel, LOG_ERROR, "Invalid category '$category'.", 0);
        print sprintf (__("Invalid category '%s'."), $category) . "\n";
        print sprintf (__("See '%s' for the list of known categories."), 'man smt-staging') . "\n";
        SMT::Utils::unLockAndExit( "smt-staging", 1, $log, $vblevel );
    }

    # read filters
    my $filter = SMT::Filter->new(log => $log, vblevel => $vblevel);
    $filter->load($dbh, $repoid, $group);

    # read patches from all repos

    my $parser = SMT::Parser::RpmMdPatches->new(
        log => $log,
        vblevel => $vblevel);
    $parser->resource($localfullrepopath);
    my $patches = $parser->parse("repodata/updateinfo.xml.gz");

    my $testpatches = {};
    if (SMT::Mirror::Utils::getStatus($localtestingrepopath))
    {
        $parser->resource($localtestingrepopath);
        $testpatches = $parser->parse("repodata/updateinfo.xml.gz");
    }

    my $prodpatches = {};
    if (SMT::Mirror::Utils::getStatus($localproductionrepopath))
    {
        $parser->resource($localproductionrepopath);
        $prodpatches = $parser->parse("repodata/updateinfo.xml.gz");
    }


    if (@userpatches)
    {
        if (defined $category)
        {
            print sprintf (__("Cannot use %s when %s is specified. Ignoring."),
                '--category', '--patch') . "\n";
        }

        foreach (@userpatches)
        {
            if (not defined $patches->{$_})
            {
                SMT::Utils::printLog($log, $vblevel, LOG_WARN,
                    sprintf (__("Patch '%s' not found."), $_));
                next;
            }

            my $patch = $patches->{$_};

            my $allowed = not $filter->matches($patch);
            my $testing = undef;
            if (defined $testpatches->{$_})
            {
                $testing = __('Yes');
                $testing .= ' (' . __('scheduled to be removed') . ')'
                    if (not $allowed);
            }
            else
            {
                $testing =  __('No');
                $testing .= ' (' . __('scheduled to be added') . ')'
                    if ($allowed);
            }
            my $production = defined $prodpatches->{$_} ? __('Yes') : __('No');

            print $_ . "\n";
            print (('-' x length $_) . "\n");
            my $ident = '    ';
            print $ident . __('Name: ') . $patch->{name} . "\n";
            print $ident . __('Version: ') . $patch->{version} . "\n";
            print $ident . __('Category: ') . $patch->{type} . "\n";
            print $ident . __('Summary: ') . $patch->{title} . "\n";
            print $ident . __('Is In Testing: ') . $testing . "\n";
            print $ident . __('Is In Production: ') . $production . "\n";

            if (not $allowed)
            {
                print $ident . __('Filtered by:') . "\n";
                print $filter->whatMatches2str($patch, 4) . "\n";
            }

            print $ident . __('Description: ') . "\n" .
                $patch->{description} . "\n\n"
                    if (not $nodesc);
        }
    }
    else
    {
        my @cols = (
            __("T"),
            __("P"),
            __("Name"),
            __("Version"),
            __("Category")
        );
        push @cols, __("Summary") if (not $nodesc);

        my $t = new Text::ASCIITable;
        $t->setCols(@cols);

        my @sorted = ();
        if ($sortbyversion)
        {
            @sorted =
                sort { $patches->{$a}->{version} <=> $patches->{$b}->{version} }
                keys %$patches;
        }
        elsif ($sortbycategory)
        {
            @sorted =
                sort { $patches->{$a}->{type} cmp $patches->{$b}->{type} }
                keys %$patches;
        }
        else # sort by name
        {
            @sorted = sort { $a cmp $b } keys %$patches;
        }

        foreach my $id (@sorted)
        {
            next if ($category && $patches->{$id}->{type} ne $category);

            # a/f/+/-
            my $testing = undef;
            if ($filter->matches($patches->{$id}))
            { $testing = defined $testpatches->{$id} ? '-' : __('f') }
            else
            { $testing = defined $testpatches->{$id} ? __('a') : '+' }

            my @row = (
                $testing,
                defined $prodpatches->{$id} ? __('a') : __('f'),
                $patches->{$id}->{name},
                $patches->{$id}->{version},
                $patches->{$id}->{type}
            );
            push @row, $patches->{$id}->{title} if (not $nodesc);
            $t->addRow(@row);
        }
        if (%$patches)
        {
            print $t->draw();
        }
        else
        {
            print __("This repository does not contain any patches.") . "\n";
        }
    }
}


###############################################################################
# allow/forbid an update
###############################################################################

elsif ($command eq 'allow' || $command eq 'forbid')
{
    my $canfilter = canFilter($repoid, $basepath);
    SMT::Utils::unLockAndExit( "smt-staging", 1, $log, $vblevel )
        if (defined $canfilter && not $canfilter);

    my $filter = SMT::Filter->new(log => $log, vblevel => $vblevel);
    $filter->load($dbh, $repoid, $group);

    my $parser = SMT::Parser::RpmMdPatches->new(
        log => $log,
        vblevel => $vblevel);
    $parser->resource($localfullrepopath);
    my $patches = $parser->parse("repodata/updateinfo.xml.gz");

    my $allow = $command eq 'allow';

    printPatchCounts($patches, $filter);

    if (@userpatches)
    {
        if (defined $category)
        {
            print sprintf (__("Cannot use %s when %s is specified. Ignoring."),
                '--category', '--patch') . "\n";
        }
        if ($all)
        {
            print sprintf (__("Cannot use %s when %s is specified. Ignoring."),
                '--all', '--patch') . "\n";
        }
        my $count = 0;
        foreach (@userpatches)
        {
            # make sure user gave an existing patch
            if (not defined $patches->{$_})
            {
                SMT::Utils::printLog($log, $vblevel, LOG_WARN,
                    sprintf (__("Patch '%s' not found."), $_));
                next;
            }

            if ($allow)
            {
                $filter->remove(SMT::Filter->TYPE_NAME_VERSION, $_);
                if ($filter->matches($patches->{$_}))
                {
                    SMT::Utils::printLog($log, $vblevel, LOG_WARN,
                        sprintf (__("Patch '%s' is still filtered by:\n%s"),
                            $_, $filter->whatMatches2str($patches->{$_})));
                }
            }
            else
            {
                $filter->add(SMT::Filter->TYPE_NAME_VERSION, $_)
            }
        }
    }
    elsif (defined $category)
    {
        if ($all)
        {
            print sprintf (__("Cannot use %s when %s is specified. Ignoring."),
                '--all', '--category') . "\n";
        }
        if ($exists_category{$category})
        {
            if ($allow)
            {
                print sprintf (
                    __('Removing filter by category \'%s\'.'), $category) . "\n";

                $filter->remove(SMT::Filter->TYPE_SECURITY_LEVEL, $category);
                if ($individually)
                {
                    print sprintf (
                        __('Allowing all \'%s\' patches individually.'), $category) . "\n";
                    for (keys %$patches)
                    {
                        $filter->remove(SMT::Filter->TYPE_NAME_VERSION, $_)
                            if ($patches->{$_}->{type} eq $category);
                    }
                }
                else
                {
                    my @stillfiltered = ();
                    for (keys %$patches)
                    {
                        push @stillfiltered, $_
                            if ($filter->matches($patches->{$_}))
                    }

                    if (@stillfiltered)
                    {
                        print __('The following patches are still filtered by other means than category:') . "\n";
                        print $_ . "\n" for (@stillfiltered);
                        print sprintf (__('Use "%s" to see details.'),
                                'smt staging listupdates ' . $repoid .
                                ' --patch <patchname-version>') . "\n";
                        print sprintf (
                            __('Use "%s" to alow all these patches individually.'),
                            'smt staging allow ' . $repoid .
                            ' --category ' . $category . ' --individually');
                        print "\n";
                    }
                }
            }
            else
            {
                if ($individually)
                {
                    print sprintf (
                        __('Forbidding all \'%s\' patches individually.'), $category) . "\n";
                    for (keys %$patches)
                    {
                        $filter->add(SMT::Filter->TYPE_NAME_VERSION, $_)
                            if ($patches->{$_}->{type} eq $category);
                    }
                }
                else
                {
                    print sprintf (
                        __('Adding filter by category \'%s\'.'), $category) . "\n";
                    $filter->add(SMT::Filter->TYPE_SECURITY_LEVEL, $category);
                }
            }
        }
        else
        {
            SMT::Utils::printLog($log, $vblevel, LOG_ERROR, "Invalid category '$category'.", 0);
            print sprintf (__("Invalid category '%s'."), $category) . "\n";
            print sprintf (__("See '%s' for the list of known categories."), 'man smt-staging') . "\n";
            SMT::Utils::unLockAndExit( "smt-staging", 1, $log, $vblevel );
        }
    }
    elsif (defined $all)
    {
        if ($allow)
        {
            $filter->clean();
            SMT::Utils::printLog($log, $vblevel, LOG_INFO1,
                __('All patches have been allowed.'));
        }
        else
        {
            # remove all filters first, then forbid all, individually
            $filter->clean();
            $filter->add(SMT::Filter->TYPE_NAME_VERSION, $_)
                foreach (keys %$patches);
            SMT::Utils::printLog($log, $vblevel, LOG_INFO1,
                sprintf (__('All %d patches have been forbidden.'),
                    scalar keys %$patches));
        }
    }
    else
    {
        SMT::Utils::printLog($log, $vblevel, LOG_ERROR, "No patches specified.");
        print __("Specify at least one '--patch name-version'.") . "\n";
        print __("Use 'smt-staging listupdates' to see the list of available patches.") . "\n";
        SMT::Utils::unLockAndExit( "smt-staging", 1, $log, $vblevel );
    }

    # check consistency of the resulting repository before applying changes
    if ($filter->dirty() && not $all)
    {
        my $checker = SMT::Parser::FilteredRepoChecker->new(
            log => $log, vblevel => $vblevel);
        $checker->repoPath($localfullrepopath);
        $checker->filter($filter);
        printLog ($log, $vblevel, LOG_INFO2, __('Checking repository consistency...'));
        my ($result, $problems, $causes) = $checker->check();
        printLog ($log, $vblevel, LOG_DEBUG, 'DONE checking dependencies', 0);

        if (not $result)
        {
            SMT::Utils::printLog($log, $vblevel, LOG_ERROR,
                __("Dependecy problems found."));

            foreach my $pkg (keys %$problems)
            {
                printLog ($log, $vblevel, LOG_ERROR, sprintf (
                    __("Cannot remove package '%s' required by forbidden patch '%s'.\n"),
                    $pkg, $causes->{$pkg}));

                printLog ($log, $vblevel, LOG_ERROR, sprintf (
                    __("The package is needed by patch '%s', which is allowed.\n"),
                    $problems->{$pkg}));
            }

            SMT::Utils::unLockAndExit( "smt-staging", 1, $log, $vblevel );
        }
        else
        {
            SMT::Utils::printLog($log, $vblevel, LOG_INFO2, __("OK"));
        }
    }

    $filter->save($dbh, $repoid, $group);

    print "\n";
    printPatchCounts($patches, $filter);
}

###############################################################################
# create testing/production repo
###############################################################################

elsif ($command eq 'createrepo')
{
    if ($gentest && $genprod)
    {
        SMT::Utils::printLog($log, $vblevel, LOG_ERROR, sprintf __("Options %s and %s cannot be used together."), '--testing', '--production');
        SMT::Utils::unLockAndExit( "smt-staging", 1, $log, $vblevel );
    }
    elsif (!($gentest || $genprod))
    {
        SMT::Utils::printLog($log, $vblevel, LOG_ERROR, sprintf __("%s or %s?"), '--testing', '--production');
        SMT::Utils::unLockAndExit( "smt-staging", 1, $log, $vblevel );
    }

    my $mirrorsrc = $cfg->val("LOCAL", "MirrorSRC");
    if (defined $mirrorsrc && lc($mirrorsrc) eq "false")
    { $mirrorsrc = 0; }
    else
    { $mirrorsrc = 1; }

    # source

    my $sourcerepourl = 'file://';
    if ($gentest)
    {
        $sourcerepourl .= $localfullrepopath;
    }
    else
    {
        if (not -e $localtestingrepopath . '/repodata/repomd.xml' &&
            SMT::Mirror::Utils::getStatus($localtestingrepopath))
        {
            SMT::Utils::printLog($log, $vblevel, LOG_ERROR,
                __('Testing repository does not exist or is not generated properly, cannot generate production repository.'));
            print __('Generate the testing repository first.') . "\n";
            SMT::Utils::unLockAndExit( "smt-staging", 1, $log, $vblevel );
        }
        # always generate the production repo from the testing repo
        $sourcerepourl .= $localtestingrepopath;
    }

    # target

    my $localtargetrepopath = ($gentest ?
        $rh->getTestingRepoPath($repoid, '', $group) :
        $rh->getProductionRepoPath($repoid, '', $group));


    my $filter = SMT::Filter->new(log => $log, vblevel => $vblevel);
    $filter->load($dbh, $repoid, $group);

    my $mirror = undef;
    if ( SMT::Utils::doesFileExist($useragent, "$sourcerepourl/headers/header.info"))
    {
        $mirror = SMT::Mirror::Yum->new(
            log        => $log,
            dbh        => $dbh,
            vblevel    => $vblevel,
            filter     => $gentest ? $filter : undef,
            nohardlink => $nohardlink,
            mirrorsrc  => $mirrorsrc,
            useragent  => $useragent);
    }
    else
    {
        $mirror = SMT::Mirror::RpmMd->new(
            log        => $log,
            dbh        => $dbh,
            vblevel    => $vblevel,
            filter     => $gentest ? $filter : undef,
            nohardlink => $nohardlink,
            mirrorsrc  => $mirrorsrc,
            useragent  => $useragent);
    }


    $mirror->uri($sourcerepourl);
    $mirror->localBasePath($basepath);
    $mirror->localRepoPath($localtargetrepopath);

    if (not defined $keyid)
    {
        $keyid = $cfg->val('LOCAL', 'signingKeyID');
        if ($keyid)
        {
            SMT::Utils::printLog($log, $vblevel, LOG_DEBUG,
                'Using key ID ' . $keyid . ' from smt.conf\'s signingKeyID')
        }
    }

    if ($gentest && $keyid && not defined $keypass)
    {
        print __('Signing key passphrase: ');
        system("stty -echo");
        $keypass = <STDIN>;
        chomp $keypass;
        print "\n";
        system("stty echo");
    }

    my $errcnt = $mirror->mirror(force => 1, keyid => $keyid, keypass => $keypass);
    if ($errcnt)
    {
        SMT::Utils::printLog($log, $vblevel, LOG_ERROR, 'Something went wrong');
        SMT::Utils::unLockAndExit( "smt-staging", 1, $log, $vblevel );
    }
    else
    {
        SMT::Mirror::Utils::copyStatus(
            $genprod ? $localtestingrepopath : $localfullrepopath,
            SMT::Utils::cleanPath($basepath, $localtargetrepopath));
        SMT::Utils::printLog($log, $vblevel, LOG_INFO1,
            sprintf __("Repository successfully generated at %s."), $localtargetrepopath);
    }
}

SMT::Utils::unLockAndExit( "smt-staging", 0, $log, $vblevel );

sub printPatchCounts
{
    my ($patches, $filter) = @_;
    my $forbidden = 0;
    my $allowed = 0;
    foreach (values %$patches)
    {
        if ($filter->matches($_))
        { $forbidden++ }
        else
        { $allowed++ }
    }
    SMT::Utils::printLog($log, $vblevel, LOG_INFO1,
        sprintf (__("Total patches: %d"), $forbidden + $allowed));
    SMT::Utils::printLog($log, $vblevel, LOG_INFO1,
        sprintf (__("Allowed:       %d"), $allowed));
    SMT::Utils::printLog($log, $vblevel, LOG_INFO1,
        sprintf (__("Forbidden:     %d"), $forbidden));
}

# check if we can do filtering for this repo
sub canFilter
{
    my ($repoid, $basepath) = @_;
    my $canfilter = $rh->filteringAllowed($repoid, $basepath);
    if (not defined $canfilter)
    {
        SMT::Utils::printLog($log, $vblevel, LOG_WARN,
            sprintf __("Cannot determine whether filtering is allowed for repository '%s', target '%s'."), $repo->{NAME}, $repo->{TARGET});
        print __("See the log for more details.\n");
        print __('Trying to proceed anyway.') . "\n";
    }
    elsif (!$canfilter)
    {
        SMT::Utils::printLog($log, $vblevel, LOG_INFO1,
            sprintf __("Filtering is not allowed for repository '%s', target '%s'."), $repo->{NAME}, $repo->{TARGET});
        print __("The repository either does not contain update metadata, or the metadata are in an unsupported format.\n");
        print __("Only openSUSE 11.x, SLE-11, and Fedora/RedHat update repositories are supported.\n");
    }
    return $canfilter;
}

sub signal_handler
{
  SMT::Utils::printLog($log, $vblevel, LOG_INFO1, "Interrupted by signal. Exiting.");
  SMT::Utils::unLockAndExit("smt-mirror", 1, $log, $vblevel);
}


#
# Manpage
#

=head1 NAME

smt staging

=head1 SYNOPSIS

smt staging <command> <repository> [options]

smt staging status <repository> [general-options]

smt staging listupdates <repository> [general-options]
    [--patch <patch> ... | --category <category>] [--nodesc]
    [--sort-by-version | --sort-by-category]

smt staging forbid <repository> [general-options]
    [--patch <patch> ... | --category <category> | --all]
    [--individually]

smt staging allow <repository> [general-options]
    [--patch <patch> ... | --category <category> | --all] [--individually]
    [--individually]

smt staging createrepo <repository> [general-options]
    --testing|--production [--nohardlink]

smt staging listgroups [general-options]

smt staging creategroup <name> <testing dirname> <production dirname> [general-options]

smt staging removegroup <name> [general-options]

smt staging --help | -h

=head1 DESCRIPTION

C<smt staging> script allows setting up patch (update) filters for update
repositories and generate repositories for testing or use in production
environment.

Use the B<listupdates> command to list available patches and their
allowed/forbidden status, B<allow>/B<forbid> commands to allow or forbid
specified patches, and, finally B<createrepo> to generate testing or production
repository containing only allowed patches. The B<status> command gives
information about testing and production snapshots up-to-date status and patch
counts.

The commands B<listgroups>, B<creategroup> and B<removegroup> can be used to
list, create or remove staging groups. There is always one group available with
the name I<default>. The default group has the path I<full>, I<testing> and I<''>.
With creating a new group new pathes can be specified.

=head1 TERMINOLOGY

=over 4

=item patch

An update of a package or group of packages. The term I<update> and I<patch> are
interchangable throughout this manual and in the C<smt staging> script.

=back

=head1 ARGUMENTS

The first argument of C<smt staging> is always the B<command>. The command must
be followed by I<repository>. Repository can be specified using the I<ID> or
I<Name and Target> from the table returned by C<smt repos>. The third alternative
is to use the hexadecimal I<Repository ID> found in C<smt repos -v> output.

Examples:

=over 4

=item $ smt staging createrepo 1 --testing

creates testing repository from repository number 1 from C<smt repos> table.

=item $ smt staging listupdates SLED10-SP1-Updates sled-10-i586

lists patches from SLED10-SP1-Updates repository for sled-10-i586. Use
C<smt repos> to get a list of all repository names and targets.

=back

=head1 RECOMMENDED WORKFLOW

 1) Enable staging for the desired repository
    $ smt repos -s
 2) Mirror the remote repository (you can get the ID from C<smt repos -v>)
    $ smt mirror [--repository Repostiory_ID]
 3) See available patches
    $ smt staging listupdates <Repository_ID> [--patch <patchid>]
 4) Allow/forbid the desired patches by their ID or Category
    $ smt staging allow <Repository_ID> --patch foo-312
 5) Recheck which patches are allowed/forbidden
    $ smt staging listupdates <Repository_ID> --nodesc
 6) Create the testing repository
    $ smt staging createrepo <Repository_ID> --testing [--keyid <keyid>]
 7) Test installation and functionality of the patches in testing clients
 8) If no problems were discovered during the testing, create the production
    repository
    $ smt staging createrepo <Repository_ID> --production [--keyid <keyid>]
 9) Update your production clients or monitor them as they update automatically


=head1 GENERAL OPTIONS

The following options apply to any command.

=over 4

=item -L, --logfile <filepath>

Write log to the specified file. If the file does not exist, it will be
created.

=item -d, --debug

Turn on debugging output and log.

=item --verboselevel -v <level>

Set the output verbose level. The following categories exists.
These categories can be bitwise-or'd to use as verbose level.

=over 4

=item error messages

Value: 0x0001 == 1

=item warning messages

Value: 0x0002 == 2

=item info messages 1

Value: 0x0004 == 4

=item info messages 2

Value: 0x0008 == 8

=item debug messages 1

Value: 0x0010 == 16

=item debug messages 2

Value: 0x0020 == 32

=item debug messages 3

Value: 0x0040 == 64

=back

The default verbose level is 15 (error, warning and all info messages).
B<--debug> set the verbose level to 63.

=back

=head1 COMMANDS

=over 4

=item status

Shows the date and time of the last mirroring of the full repository and status
of the testing and production repository, plus some other information.

The testing and production repository status is determined by the date of the
full mirror from which the testing (and then production) repository was created.
The output is as follows:
 'not created'
    the repository has not ever been (successfully) created
 'created (corresponding mirror date: YYYY-MM-DD hh:mm:ss)'
    meaning the repository has been successfully created from the full mirror
    which was mirrored on the shown date.

=item listupdates

Lists all available patches and marks their presence in the testing
and production repositories. When specific patches are specified via the
I<--patch> options, detailed information is shown for these patches.

The output table contains the follwing columns:
 T - whether the patch is or is scheduled to be in the testing repository.
     The following values can be found in this column:
     a - (allowed) the patch is in the testing repository
     f - (forbidden) the patch is not in the testing repository
     + - the patch is scheduled to be added to the testing repository
     - - the patch is scheduled to be removed from the testing repository
 P - whether the patch is in the production repository. Possible values:
     a - (allowed) the patch is in the production repository
     f - (forbidden) the patch is not in the production repository
 Name     - patch name
 Version  - patch version
 Category - patch category (security, recommended, or optional)
 Summary  - brief summary of the issues the patch fixes. This column can be
            hidden from view using the I<--nodesc> option.

Use the I<--sort-by-*> options to sort the table by version or category. Default
sorting is by patch name.

The detailed output (shown when I<--patch> is specified) contains additional
information like which properties are used to filter (forbid) the patch,
full description, etc.

=item allow/forbid

Forbidding a patch means adding a filter that will remove the patch from the
repository when creating the testing or production snapshot, thus make it
invisible for the clients.

Patches can be
forbidden in two ways: individually, using their ID (patchname-version), or by
category (security, optional, etc.). The C<allow> and C<forbid> command together
with I<--patch>, I<--category>, I<--all>, and I<--individually> options allows
to do this.

After making changes with C<allow> or C<forbid>, make sure to use the
C<listupdates> command to re-check which patches are scheduled to appear in or
be removed from the testing snapshot.

Note that a patch can be forbidden in multiple ways (currently the above two)
at the same time. For example, if you have forbidden all optional patches using
C<forbid RepoID --category optional> and then try to allow one of the optional
patches using C<allow RepoID --patch foo-3>, the program will issue a warning
that the foo-3 patch is still forbidden by category 'optional'. You have to do
the following in this case (if that is what you really want):

 $ smt staging allow RepoID --category optional
 $ smt staging forbid RepoID --category optional --individually
 $ smt staging allow RepoID --patch foo-3

=item listgroups

List all available staging groups. The values for B<Testing Directory Name> and
B<Production Directory Name> are directoy names in I</srv/www/htdocs/repo/>.

=item creategroup

Create a new group. Required parameter are: B<group name>, B<Testing Directory Name>
and B<Production Directory Name>.

=item removegroup

Remove a group. Required parameter is B<group name>.

=back

=head1 COMMAND-SPECIFIC OPTIONS

=over 4

=item --patch

Specify a patch using its ID, that is 'I<patchname-version>'. To get a list
of available patches, use the B<listupates> command. This option can be
used multiple times.

This option is used in the B<allow>, B<forbid>, and B<listupdates> commands.

If used in B<listupdates>, the command will print detailed information about
the specified patches.

=item --category

Specify patch (update) category. The following categories are available:
 security
 recommended
 optional

This option is used in the B<allow>, B<forbid>, and B<listupdates> commands.

=item --all

Allow or forbid all patches in the B<allow> or B<forbid> commands.

=item --individually

Allow or forbid multiple patches (e.g. by category) one by one, that is,
as if the I<--patch> option had been used on each of the patches.

=item --testing

Used in the B<createrepo> command to generate a repository for testing.
The testing repository will be generated from the full unfiltered local mirror
of the remote repository and written into <MirrorTo>/repo/testing directory,
where MirrorTo is the value taken from the smt.conf configuration file.

=item --production

Used in the B<createrepo> command to generate a repository for production use.
This command should be used after testing have been made on the testing
reposity.

The 'production' repository will be generated from the testing repository
and written into <MirrorTo>/repo directory, where MirrorTo is the value taken
from the smt.conf configuration file. If the testing repository does not exist,
the production repository will be generated from the full unfiltered local
mirror of the remote repository.

=item --group

Specify on which group the command should work on. The default for --group
is B<default>.

=item --nohardlink

Avoid creating of hard links instead of copying files when creating
the testing or production repositories using B<createrepo> command.
If not specified, hard links are created instead of copying wherever possible
to save space and time.

=item --nodesc

Do not print patch descriptions and summaries to save some screen space and make
the output more readable.

Can be used with the B<listupdates> command.

=item --sort-by-version

Sort the C<listupdates> table by patch version. The higher the version, the
newer the patch should be.

=item --sort-by-category

Sort the C<listupdates> table by patch category.

=back

=head1 AUTHORS and CONTRIBUTORS

Jan Kupec

=head1 LICENSE

Copyright (c) 2009-2012 SUSE LINUX Products GmbH, Nuernberg, Germany.

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 2 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, write to the Free Software Foundation, Inc., 675 Mass
Ave, Cambridge, MA 02139, USA.

=cut
