#!/usr/bin/perl

# apt-dater - terminal-based remote package update manager
#
# Authors:
#   Andre Ellguth <ellguth@ibh.de>
#   Thomas Liske <liske@ibh.de>
#
# Copyright Holder:
#   2008-2014 (C) IBH IT-Service GmbH [http://www.ibh.de/apt-dater/]
#
# License:
#   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 package; if not, write to the Free Software
#   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
#

use AptPkg::Config '$_config';
use AptPkg::System '$_system';
use AptPkg::Cache;
use ImVirt;
use strict;
use warnings;

my $CFGFILE = '/etc/apt-dater-host.conf';
my $DPKGTOOL = 'apt-get';
my $APTUPGRADE = 'dist-upgrade';
my $ASSUMEYES = 0;
my $GETROOT = 'sudo';
my $CLEANUP = 0;
my $FORBID_REFRESH = 0;
my $FORBID_UPGRADE = 0;
my $FORBID_INSTALL = 0;
my $UUIDFILE = '/etc/apt-dater-host.uuid';
my @CLUSTERS;
my $NEEDRESTART;
my $RUNNEEDREST;

my $CMD = shift;
my $ADPROTO = '0.7';

$ENV{'LC_ALL'} = 'C';

if (-r $CFGFILE) {
    eval `cat "$CFGFILE"` ;

    if($@ ne '') {
	print "ADPROTO: $ADPROTO\n";
	print "ADPERR: Invalid config $CFGFILE: $@\n";
	exit;
    }
}

$GETROOT = '' if($> == 0);

die "Don't call this script directly!\n" unless (defined($CMD));
if($CMD eq 'sshkey') {
    die "Sorry, no shell access allowed!\n"
      unless(defined($ENV{'SSH_ORIGINAL_COMMAND'}));

    @ARGV = split(' ', $ENV{'SSH_ORIGINAL_COMMAND'});

    shift;
    $CMD = shift;
}
die "Invalid command '$CMD'!\n" unless ($CMD=~/^(refresh|status|upgrade|install|kernel)$/);

if ($CMD eq 'refresh') {
    print "ADPROTO: $ADPROTO\n";
    if ($FORBID_REFRESH) {
	print STDERR "\n\n** Sorry, apt-dater based refreshs on this host are disabled! **\n\n"
    }
    else {
	&do_refresh;
    }
    &do_status;
    &do_kernel;
}
elsif ($CMD eq 'status') {
    print "ADPROTO: $ADPROTO\n";
    &do_status;
    &do_kernel;
}
elsif ($CMD eq 'upgrade') {
    if ($FORBID_UPGRADE) {
	print STDERR "\n\n** Sorry, apt-dater based upgrades on this host are disabled! **\n\n";
    }
    else {
	&do_upgrade;
	&do_cleanup;
    }
}
elsif ($CMD eq 'install') {
    if ($FORBID_INSTALL) {
	print STDERR "\n\n** Sorry, apt-dater based installations on this host are disabled! **\n\n";
    }
    else {
	&do_install(@ARGV);
	&do_cleanup;
    }
}
elsif ($CMD eq 'kernel') {
    print "ADPROTO: $ADPROTO\n";
    &do_kernel;
}
else {
    die "Internal error!\n";
}


sub do_refresh() {
    if(system(($GETROOT ? $GETROOT : ()), $DPKGTOOL, 'update')) {
	print "\nADPERR: Failed to execute '$GETROOT $DPKGTOOL update' ($?).\n";
	exit(1);
    }
}

sub get_virt() {
    return imv_get(IMV_PROB_DEFAULT);
}

sub get_uname() {
    my $kernel;
    my $machine;

    chomp($kernel = `uname -s`);
    chomp($machine = `uname -m`);
    return "$kernel|$machine";
}

sub do_status() {
    # initialise the global config object with the default values and
    # setup the $_system object
    $_config->init;
    $_system = $_config->system;

    # supress cache building messages
    $_config->{quiet} = 2;

    # set up the cache
    my $cache = AptPkg::Cache->new;

    # retrieve lsb informations
    unless (open(HLSB, "lsb_release -a 2> /dev/null |")) {
	print "\nADPERR: Failed to execute 'lsb_release -a' ($!).\n";
	exit(1);
    }
    my %lsb;
    while(<HLSB>) {
	chomp;

	$lsb{$1}=$2 if (/^(Distributor ID|Release|Codename):\s+(\S.*)$/);
    }
    close(HLSB);
    if($?) {
	print "\nADPERR: Error executing 'lsb_release -a' ($?).\n";
	exit(1);
    }
    print "LSBREL: $lsb{'Distributor ID'}|$lsb{'Release'}|$lsb{'Codename'}\n";

    # retrieve virtualization informations
    print "VIRT: ".&get_virt."\n";

    # retrieve uname informations
    print "UNAME: ".&get_uname."\n";

    # calculate forbid mask
    my $mask = 0;
    $mask |= 1 if ($FORBID_REFRESH);
    $mask |= 2 if ($FORBID_UPGRADE);
    $mask |= 4 if ($FORBID_INSTALL);
    print "FORBID: $mask\n";

    # add installation UUID if available
    if(-r $UUIDFILE && -s $UUIDFILE) {
	print "UUID: ", `head -n 1 "$UUIDFILE"`;
    }

    # add cluster name if available
    foreach my $CLUSTER (@CLUSTERS) {
	print "CLUSTER: $CLUSTER\n";
    }

    # add needrestart batch output
    system(($GETROOT ? $GETROOT : ()), $NEEDRESTART, '-b') if($NEEDRESTART);

    # get packages which might be upgraded
    my %updates;
    my %holds;
    my $pos = 0;
    my $DPKGARGS;

    if($DPKGTOOL eq "apt-get") {
	$DPKGARGS = "--quiet --simulate --fix-broken --allow-unauthenticated";
    }
    elsif($DPKGTOOL eq "aptitude") {
	$DPKGARGS = "--verbose --assume-yes --quiet --simulate -f --allow-untrusted";
    }
    else {
	# unkown DPKG frontend - fallback to apt-get
	$DPKGTOOL = "apt-get";
	$DPKGARGS = "--quiet --simulate --fix-broken --allow-unauthenticated";
    }

    unless(open(HAPT, "$DPKGTOOL $DPKGARGS dist-upgrade |")) {
	print "\nADPERR: Failed to execute '$DPKGTOOL $DPKGARGS dist-upgrade' ($!).\n";
	exit(1);
    }
    while(<HAPT>) {
	chomp;
	
	if($pos == 0) {
	    $pos=1 if (/^The following packages have been kept back/);
	    $pos=2 if (/^The following packages will be upgraded:/);
	    next;
	}

	if($pos == 1) {
	    unless (/^\s/) {
		$pos++;
		next;
	    }
	    while(/^\s*(\S+)(\s(.+))?$/) {
		$holds{$1} = 1;
		if(defined($2)) {
		    $_ = $2;
		}
		else {
		    $_ = '';
		}
	    }
	}

	$updates{$1} = $2 if (/^Inst (\S+) \[.+\] \((\S+) /);
    }
    close(HAPT);
    if($?) {
	print "\nADPERR: Error executing '$DPKGTOOL $DPKGARGS dist-upgrade' ($?).\n";
	exit(1);
    }

    # get version of installed packages
    my %installed;
    my %status;
    my $arch = `dpkg --print-architecture`;
    chomp($arch);
    unless(open(HDPKG, "dpkg-query --show --showformat='\${Package} \${Version} \${Status} \${Architecture}\\n' |")) {
	print "\nADPERR: Failed to execute \"dpkg-query --show --showformat='\${Package} \${Version} \${Status} \${Architecture}\\n'\" ($!).\n";
	exit(1);
    }

    while(<HDPKG>) {
	chomp;

	next unless (/^(\S+) (\S+) (\S+) (\S+) (\S+) (\S+)$/);

	my $pkg = $1;
	$pkg .= ":$6" if($6 ne q(all) && $6 ne $arch);

	$installed{$pkg} = $2 ;
	if($holds{$pkg}) {
	    $status{$pkg} = 'h';
	}
	elsif($updates{$pkg}) {
	    $status{$pkg} = 'u';
	}
	else {
	    $status{$pkg} = substr($3, 0, 1);
	    
	    if ($status{$pkg} eq 'i') {
		my $p = $cache->{$1};

		unless ($5 eq 'installed') {
		    $status{$pkg} = "b=$5";
		}
		elsif ($p) {
		    if (my $available = $p->{VersionList}) {
			my $extra = 1;
			for my $v (@$available) {
			    for my $f (map $_->{File}, @{$v->{FileList}}) {
				$extra = 0 unless ($f->{FileName} eq '/var/lib/dpkg/status');
			    }
			}
			$status{$pkg} = 'x' if($extra);
		    }
		}
	    }
	}
    }
    close(HDPKG);
    if($?) {
	print "\nADPERR: Error executing \"dpkg-query --show --showformat='\${Package} \${Version} \${Status}\\n'\" ($?).\n";
	exit(1);
    }

    foreach my $pkg (keys %installed) {
	print "STATUS: $pkg|$installed{$pkg}|$status{$pkg}";
	if (exists($updates{$pkg})) {
	    print "=$updates{$pkg}" ;
	}
	
	print "\n";
    }
}

sub do_upgrade() {
    my $UpgradeCmd = "$DPKGTOOL ";
    $UpgradeCmd .= '-o Aptitude::Delete-Unused=false ' if($DPKGTOOL eq 'aptitude');
    $UpgradeCmd .= $APTUPGRADE;

    # drop -y if any packages would be removed
    if($ASSUMEYES) {
	unless(open(HAPT, "$UpgradeCmd --simulate --assume-yes |")) {
	    print "\nADPERR: Failed to execute '$UpgradeCmd --simulate' ($!).\n";
	    exit(1);
	}
	while(<HAPT>) {
	    chomp;
	
	    if(/^The following packages will be REMOVED:/) {
		$ASSUMEYES = undef;
		last;
	    }
	    last if(/^\d+ upgraded, \d+ newly installed/);
	}
	close(HAPT);

	$UpgradeCmd .= ' --assume-yes' if($ASSUMEYES);
    }

    system("$GETROOT $UpgradeCmd");
}

sub do_install() {
    system(($GETROOT ? $GETROOT : ()), $DPKGTOOL, 'install', @_);
}

sub do_kernel() {
    my $infostr = 'KERNELINFO:';
    my $verfile = '/proc/version';
    my $versignfile = '/proc/version_signature';
    my $version = `uname -r`;
    chomp($version);

    unless(-r $verfile) {
	print "$infostr 9 $version\n";
	return;
    }

    my $vers = '';
    if (-r $versignfile) {
      unless(`cat $versignfile` =~ /^Ubuntu (\S+)-\S+ \S+$/) {
          print "$infostr 2 $version\n";
          return;
      }
      $vers = $1;
    }
    else {
      my $vstr = `cat $verfile`;
      unless($vstr =~ /^\S+ \S+ \S+ \(Debian ([^\)]+)\)/ ||
             $vstr =~ /^\S+ \S+ \S+ \(debian-kernel\@lists\.debian\.org\) .+ Debian (\S+)$/) {
          print "$infostr 2 $version\n";
          return;
      }
      $vers = $1;
    }

    my $reboot = 0;
    unless(open(HDPKG, "dpkg-query -W -f='\${Version} \${Status;20} \${Maintainer} \${Provides}\n' 'linux-image*'|grep -E 'install ok installed (Debian|Ubuntu) Kernel Team'|grep linux-image|")) {
	print "$infostr 9 $version\n";
	return;
    }
    while(<HDPKG>) {
	next unless (/^(\S+)\s/);
	
	$reboot=1 unless (system("dpkg", "--compare-versions", $vers, "lt", $1) >> 8);
    }
    close(HDPKG);

    print "$infostr $reboot $version\n";
}

sub do_cleanup() {
    system(($GETROOT ? $GETROOT : ()), $NEEDRESTART) if($RUNNEEDREST);

    return unless $CLEANUP;

    system(($GETROOT ? $GETROOT : ()), $DPKGTOOL, 'clean');
}
