#!/usr/bin/perl

# Copyright (C) 2018 Thorsten Kukuk
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# in Version 2 as published by the Free Software Foundation.
#
# 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, see <http://www.gnu.org/licenses/>.

=head1 NAME

update-checker - check for new updates

=head1 SYNOPSIS

update-checker [option]

=head1 DESCRIPTION

Check if updates for this system are available. If yes, inform the admin
or other tools via this depending on configured paths.

=head1 OPTIONS

  -c|--config <file>    Use different configuration file
  -v|--verbose          More verbose output
  -o|--output <string>  Specify a backend for the output.
  --update              Check for available updates via "zypper up", default
  --patch               Check for available patches via "zypper patch"
  --dup                 Check for distribution updates via "zypper dup"
  --profile <string>    Use entry from a different profile from the configuration file.
  --verbose            Print additional informations.
  --usage               Print usage
  --man                 Display manual page
  -h|-?                 Help

=cut

use strict;
use warnings;
use Pod::Usage;
use XML::Twig;
use Config::IniFiles;
use POSIX qw(locale_h);
use locale;

setlocale(LC_ALL, "C");

#
# process command line arguments
#
use Getopt::Long;
my $help = 0;
my $man = 0;
my $usage = 0;
my $configfile = '/etc/update-checker.conf';
my $verbose = 0;
my $output_ = "";
my $output_cfg = "";
my $output_arg = "";
my $output_stdout = 0;
my $output_issue = 0;
my $output_salt = 0;
my $variant = "";
my $orphaned = 0;
my $profile = "";

GetOptions('c|config=s' => \$configfile,
	   'v|verbose' => \$verbose,
	   'o|output=s' => \$output_arg,
	   'orphaned' => \$orphaned,
	   'update' => sub { $variant = "update" },
	   'up' => sub { $variant = "update" },
	   'dup' => sub { $variant = "dup" },
	   'patch' => sub { $variant = "patch" },
	   'patches' => sub { $variant = "patch" },
	   'profile' => \$profile,
	   'verbose' => \$verbose,
           'man' => \$man,
           'usage' => \$usage,
           'help|h|?' => \$help) or pod2usage(2);
pod2usage(0) if $help;
pod2usage(-exitstatus => 0, -verbose => 2) if $man;
pod2usage(-exitstatus => 0, -verbose => 0) if $usage;

my $cfg = new Config::IniFiles(-file => $configfile);
if (defined $cfg) {
    $profile = "global-$profile" if ( $profile ne "");
    $output_cfg = $cfg->val($profile, "output", "");
    $output_cfg = $cfg->val("global", "output", "stdout") if ($output_cfg eq "");
    $variant = $cfg->val("update", "variant", "update") if ($variant eq "");
    $variant = "update" if ($variant eq "up");
    $orphaned = $cfg->val("update", "orphaned", 0);
    $orphaned = 0 if ($orphaned eq "false");
    $orphaned = 1 if ($orphaned eq "true");
}

$variant = "update" if ($variant eq "");

if ($output_arg ne "") {
    $output_ = $output_arg;
} elsif ($output_cfg ne "") {
    $output_ = $output_cfg;
} else {
    $output_ = "stdout"
}

$output_ =~ s/\s+//g;
my @output_parts = split /,/, $output_;
foreach (@output_parts) {
    $output_stdout = 1 if ($_ eq "stdout");
    $output_issue = 1 if ($_ eq "issue");
    $output_salt = 1 if ($_ eq "salt");
}

if (($output_stdout + $output_issue + $output_salt) == 0) {
    $output_stdout = 1;
}

my $package_count = 0;
my $patch_count = 0;
my $dup_install_count = 0;
my $dup_update_count = 0;
my $dup_remove_count = 0;
my @packages;
my @patches;
my @dup_install;
my @dup_update;
my @dup_remove;
my $zypper_output;

print "Refreshing services and repositories\n" if $verbose;
`zypper refresh -f --services 2>&1`;
print "Read list of pending updates\n" if $verbose;
if ($variant eq "update") {
    $zypper_output = `zypper --no-refresh --xml list-updates 2>&1`;
} elsif ($variant eq "patch" || $variant eq "patches") {
    $zypper_output = `zypper --no-refresh --xml list-patches 2>&1`;
} elsif ($variant eq "dup") {
    $zypper_output = `zypper --no-refresh --xml --non-interactive dup --dry-run 2>&1`;
} else {
    warn "ERROR: unknown variant \"$variant\"\n";
    exit 1;
}

print "Parse zypper output\n" if $verbose;
my $Parser =
    new XML::Twig(twig_handlers=>{'stream/update-status/update-list/update' => \&GetUpdateList,
				      'stream/update-status/blocked-update-list/update' => \&GetUpdateList,
				      'stream/install-summary/to-upgrade/solvable' => \&GetDupUpdates,
				      'stream/install-summary/to-install/solvable' => \&GetDupInstalls,
				      'stream/install-summary/to-remove/solvable' => \&GetDupRemoves});
$Parser->parse($zypper_output);

@packages = sort @packages unless $package_count == 0;
@patches = sort @patches unless $patch_count == 0;
@dup_install = sort @dup_install unless $dup_install_count == 0;
@dup_update = sort @dup_update unless $dup_update_count == 0;
@dup_remove = sort @dup_remove unless $dup_remove_count == 0;

if ($orphaned == 1) {
    print "Search for orphaned packages\n" if $verbose;
    $zypper_output = `zypper --no-refresh --xml pa --orphaned  2>&1`;
    $Parser =
	new XML::Twig(twig_handlers=>{'stream/XXX' => \&GetOrphanedList});
		      $Parser->parse($zypper_output);
}

if ($package_count > 0) {
    print_stdout ("updates", $package_count, @packages) if ($output_stdout);
    create_issue ("updates", $package_count, @packages) if ($output_issue);
    update_grains ("updates", $package_count, @packages) if ($output_salt);
}

if ($patch_count > 0) {
    print_stdout ("patches", $patch_count, @patches) if ($output_stdout);
    create_issue ("patches", $patch_count, @patches) if ($output_issue);
    update_grains ("patches", $patch_count, @patches) if ($output_salt);
}

if ($dup_install_count > 0 || $dup_update_count > 0 || $dup_remove_count > 0) {
    print "Distribution update: $dup_install_count to install, $dup_update_count to update, $dup_remove_count to remove\n";
    if ($dup_install_count > 0) {
	print "List of packages to install: ";
	foreach (@dup_install) {
	    print "$_ ";
	}
	print "\n";
    }
    if ($dup_update_count > 0) {
	print "List of packages to update: ";
	foreach (@dup_update) {
	    print "$_ ";
	}
	print "\n";
    }
    if ($dup_remove_count) {
	print "List of packages to remove: ";
	foreach (@dup_remove) {
	    print "$_ ";
	}
	print "\n";
    }
}

exit;

sub print_stdout
{
    my ($type, $count, @packages) = @_;

    print "Pending $type: $count\n";
    if ($count > 0) {
	print "List of $type: ";
	foreach (@packages) {
	    print "$_ ";
	}
	print "\n";
    }
}


sub create_issue
{
    my ($type, $count, @packages) = @_;

    if (open (my $fh, '>', "/var/run/issue.d/83-$type")) {
	print $fh "Pending $type: $count\n";
	close $fh;
	`/usr/sbin/issue-generator`;
    } else {
	warn "Could not open file '/var/run/issue.d/83-$type'. $!";
    }
}

sub update_grains
{
    warn "ERROR: not yet implemented!\n";
    exit 1;
}

sub GetUpdateList
{
    my ($t, $update) = @_;

    if ($update->{'att'}->{'kind'} eq "package") {
	$package_count++;
	push @packages, $update->{'att'}->{'name'};
    } elsif ($update->{'att'}->{'kind'} eq "patch") {
	$patch_count++;
	push @patches, $update->{'att'}->{'name'};
    }
}

sub GetDupUpdates
{
    my ($t, $pkg) = @_;

    if ($pkg->{'att'}->{'type'} eq "package") {
	$dup_update_count++;
	push @dup_update, $pkg->{'att'}->{'name'};
    }
}

sub GetDupInstalls
{
    my ($t, $pkg) = @_;

    if ($pkg->{'att'}->{'type'} eq "package") {
	$dup_install_count++;
	push @dup_install, $pkg->{'att'}->{'name'};
    }
}

sub GetDupRemoves
{
    my ($t, $pkg) = @_;

    if ($pkg->{'att'}->{'type'} eq "package") {
	$dup_remove_count++;
	push @dup_remove, $pkg->{'att'}->{'name'};
    }
}
