#!/usr/bin/perl -w

# Copyright (C) 2007-2012 Christoph Berg <myon@debian.org>
# Copyright (C) 2009-2010 Emil Larsson <emil.larsson@qbranch.se>
# Copyright (C) 2010-2014 Axel Beckert <abe@debian.org>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

my $ext_apt_config = "/etc/xymon";
use strict;
use Hobbit qw(file_to_list_of_regexps);

my $security_regexp = qr(/updates(?:/|$)|-security(?:/|$));
my $no_repo_accept_file = $ext_apt_config."/apt_no_repo_accept";
my $reject_file = $ext_apt_config."/apt_reject";
my @no_repo_accept = file_to_list_of_regexps($no_repo_accept_file);
my @reject = file_to_list_of_regexps($reject_file);
my $arch = `dpkg --print-architecture`; chomp($arch);

$ENV{'PATH'} = '/bin:/sbin:/usr/bin:/usr/sbin';
$ENV{'LC_ALL'} = 'C';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

my $bb = new Hobbit('apt');

if (-x '/usr/bin/lsb_release' and open(P, '-|', '/usr/bin/lsb_release -d -s')) {
    $bb->print (<P>);
    close P;
} elsif (open(F, '<', "/etc/debian_version")) {
    my $version = <F>;
    chomp $version;
    $bb->print ("Debian version $version\n");
    close F;
}

my %packages;
my %forbidden_version;

open(P, '-|', "dpkg --get-selections") or die "Couldn't execute dpkg --get-selections: $!";
while (<P>) {
    if (/^(\S+)\s+(\S+)/) { # pkg[:arch] install/hold/...
        next unless $2 eq "install" or $2 eq "hold";
        $packages{$1} = $2;
    }
}
close P;

if (-x '/usr/bin/aptitude' and -r '/var/lib/aptitude/pkgstates') {
    if (-x '/usr/bin/grep-dctrl') {
        # Find aptitude's holds
        my $command = 'grep-dctrl -s Package -n -F State 2 /var/lib/aptitude/pkgstates';
        open(P, '-|', $command) or die "Couldn't execute $command: $!";
        while (<P>) {
            chomp;
            $packages{$_} = 'hold';
        }
        close P;

        # Find aptitude's forbidden versions
        $command = 'grep-dctrl -s Package,ForbidVer -F ForbidVer -e . /var/lib/aptitude/pkgstates';
        open(P, '-|', $command) or die "Couldn't execute $command: $!";
        my $current_pkg = undef;
        while (<P>) {
            chomp;
            next unless $_;
            my ($key, $value) = split(/: /);
            if ($key eq 'Package') {
                $current_pkg = $value;
            } elsif ($key eq 'ForbidVer') {
                $forbidden_version{$current_pkg} = $value;
            } else {
                die "Don't know how to parse '$_'!";
            }
        }
        close P;
    } else {
        my $command = 'grep -c "^State: 2" /var/lib/aptitude/pkgstates';
        open(P, '-|', $command) or die "Couldn't execute $command: $!";
        my $holds_found = <P>;
        close P;

        my $warning_text = 'Warning: Package dctrl-tools seems not installed. Therefore no checking for packages set on hold via aptitude is done';
        if ($holds_found > 0) {
            $bb->color_line('yellow', "$warning_text, but hold packages are present.");
        } else {
            $bb->print("$warning_text.\n");
        }
    }
}

my %package_state = ();
open(P, '-|', "dpkg-query -l") or die "Couldn't execute dpkg-query -l: $!";
while (<P>) {
    next if /^Desired=|^\||^\+/;
    my ($state, $pkg, $version, $desc) = split();
    warn "Couldn't find package name in line: $_" unless $pkg;

    # dpkg's hold states are fine, too. See
    # http://bugs.debian.org/137771 for the aptitude' vs dpkg's hold
    # states issue.
    unless (/^ii|^rc|^hi/) {
        $package_state{$pkg} = "$_";
    }
}
close P;

my ($pkg, $inst, $cand, $pin, $pinprio, $in_dist, $dist, $has_repo);
my (@up, @upgrades, @sec, @security, @holdupgrades, @holdsecurity, @no_repo, @no_repo_pinned, @no_repo_accepted, @rejected, @broken);

sub try_pkg ()
{
    unless (exists $packages{$pkg}) {
        if (exists $packages{"$pkg:$arch"}) {
            $pkg .= ":$arch";
        }
    }
    if ($inst ne "(none)" and grep { $pkg =~ $_ } @reject) {
        push @rejected, "$pkg ($inst)";
        return
    }
    if (exists $package_state{$pkg}) {
        my ($state, $pkg, $version, $desc) =
            split(/\s+/, $package_state{$pkg});
        push @broken, "$state $pkg ($version)";
    }
    if ($inst ne "(none)" and not $has_repo) {
        if (defined $pin and $pinprio > 0) {
            push @no_repo_pinned, "$pkg ($inst) $pinprio";
        } elsif ($packages{$pkg} and $packages{$pkg} eq "hold") {
            push @no_repo_pinned, "$pkg ($inst) hold";
        } else {
            if (grep { $pkg =~ $_ } @no_repo_accept) {
                push @no_repo_accepted, "$pkg ($inst)";
            } else {
                push @no_repo, "$pkg ($inst)";
            }
        }
    }
    return if $inst eq $cand;

    # Check for hold packages
    if ($packages{$pkg} eq "hold") {
        if ($dist and $dist =~ $security_regexp) {
            push @holdsecurity, "$pkg ($inst $cand)";
        } else {
            push @holdupgrades, "$pkg ($inst $cand)";
        }
        return;
    }

    # Check for forbidden versions
    if (exists($forbidden_version{$pkg}) and
        $forbidden_version{$pkg} eq $cand) {
        if ($dist and $dist =~ $security_regexp) {
            push @holdsecurity, "$pkg ($inst <s>$cand</s>) candidate version forbidden";
        } else {
            push @holdupgrades, "$pkg ($inst <s>$cand</s>) candidate version forbidden";
        }
        return;
    }

    # Updates available and wanted
    if ($dist and $dist =~ $security_regexp) {
        push @sec, $pkg;
        push @security, "$pkg ($inst $cand)";
    } else {
        push @up, $pkg;
        push @upgrades, "$pkg ($inst $cand)";
    }
}

open(P, '-|', qw(apt-cache policy), sort keys %packages) or die "Couldn't execute 'apt-cache policy ...': $!";
while (<P>) {
    if (/^(\S+):/) {
        my $next_pkg = $1;
        try_pkg () if $pkg;
        $pkg = $next_pkg;
        undef $dist;
        undef $has_repo;
        undef $pin;
        undef $pinprio;
    }
    $inst = $1 if / +Installed: (.+)/;
    $cand = $1 if / +Candidate: (.+)/;
    $pin = $1 if / +Package pin: (.+)/ and $1 eq $inst;
    if (/^[ *]+(\S+) (\d+)$/) {
        $in_dist = ($1 eq $cand);
        $pinprio = $2;
    }
    if ($in_dist and /^ +\d+ \S+ (\S+)/) { # 700 http://localhost lenny/main Packages
        $dist .= "$1 ";
        $has_repo = 1 if m( (https?|ftp)://| file:);
    }
}
try_pkg ();
close P;

sub pkgreport($$\@;\@) {
    my ($title, $color, $longlist, $shortlist) = @_;

    if (@{$longlist}) {
        $bb->print("\n");
        my $number = scalar @{$longlist};
        $bb->color_line($color, "$title ($number):");
        $bb->print(' apt-get install ' . join (' ', @{$shortlist})) if $shortlist;
        $bb->print("\n");
        foreach (sort @{$longlist}) { $bb->print("   $_\n"); }
    }
}

pkgreport('Rejected packages', 'red', @rejected);
pkgreport('Security updates', 'red', @security, @sec);
pkgreport('Broken or unconfigured packages', 'yellow', @broken);
pkgreport('Other updates', 'yellow', @upgrades, @up);
pkgreport('Security updates on hold', 'green', @holdsecurity);
pkgreport('Other updates on hold', 'green', @holdupgrades);
pkgreport('Packages not installed from apt repositories', 'yellow', @no_repo);
pkgreport('Pinned/held packages not installed from apt repositories', 'green', @no_repo_pinned);
pkgreport('Accepted packages not installed from apt repositories', 'green', @no_repo_accepted);

$bb->print("\n");

# apt-get update will also exit with status 0 on some errors, and
# /var/lib/apt/lists/lock will be updated in any case. We suggest to use
# something like the following in /etc/cron.d/:
# 44 */4        * * *        root  apt-get update -qq > /var/lib/apt/update_output 2>&1 && [ ! -s /var/lib/apt/update_output ] && date -u > /var/lib/apt/update_success

# stamp files which should be all checked, take the newest one as
# time-stamp for the last update.
my @stamp_files_check_all = qw(
        /var/lib/apt/update_success
        /var/lib/apt/periodic/update-success-stamp
        /var/lib/apt/periodic/update-stamp
);

# stamp files in order of decreasing usefulness, just check the first
# one found and only if none of the files mentioned above were found.
my @stamp_files_check_first = qw(
        /var/lib/apt/lists
        /var/lib/apt/lists/partial
        /var/cache/apt/pkgcache.bin
        /var/lib/apt/lists/lock
);

my $last_update = 'never';
foreach my $stamp_file (@stamp_files_check_all) {
    my $last_update_tmp = -M $stamp_file;
    if ($last_update_tmp and
        ($last_update eq 'never' or $last_update_tmp < $last_update)) {
        $last_update = $last_update_tmp;
    }
}

if ($last_update eq 'never') {
    foreach my $stamp_file (@stamp_files_check_first) {
        if (-e  $stamp_file) {
            $last_update = -M _;
            last
        }
    }
}

my $updatecolor;
if ($last_update eq 'never' or $last_update >= 7) {
    $updatecolor = 'red';
} elsif ($last_update >= 1.5 or $last_update < 0) {
    $updatecolor = 'yellow';
} else {
    $updatecolor = 'green';
}
$bb->color_line($updatecolor, "Last apt update: ".
                ($last_update eq 'never' ? 'never' :
                 sprintf("%.1f day(s) ago\n", $last_update)));

# If /var/lib/apt/update_output is present, print its content

my $outputfile = '/var/lib/apt/update_output';
if (-s $outputfile) {
    $bb->print("\n");
    my $mtime = scalar localtime((stat $outputfile)[9]);
    $bb->color_line('red', "Errors from $mtime:\n");
    open(F, '<', $outputfile) or die "Can't read from $outputfile: $!";
    while (<F>) {
        $bb->print("   $_");
    }
    close F;
}

$bb->send;
