#!/usr/bin/perl -w
#
# Script rpi-apt.pl is a xymon client-side script. It retrieves the list of
# outstanding patches for raspbian. These are divided into two groups,
# security-related and the rest. At the same time, this script will reguarly
# update the local cache of the package lists.
#
# Written by W.J.M. Nelis, wim.nelis@ziggo.nl, 201712
#
use strict ;
use Time::Piece ;
use Time::Local ;
#
# Installation constants.
# -----------------------
#
my $XyDisp = $ENV{XYMSRV} ; # Name of monitor server
my $XySend = $ENV{XYMON} ; # Monitor interface program
my $XyLife = '+19h' ; # Status lifetime, default 30m
my $FmtDate = "%Y.%m.%d %H:%M:%S" ; # Default date format
$FmtDate = $ENV{XYMONDATEFORMAT} if exists $ENV{XYMONDATEFORMAT} ;
my $HostName= `hostname` ; # 'Source' of this test
chomp( $HostName ) ; # Remove trailing newline
my $TestName= 'apt' ; # Name of test
my @ColourOf= ( 'red', 'yellow', 'clear', 'green' ) ;
my %ColourOf= () ;
for ( my $i=0 ; $i<@ColourOf ; $i++ ) { $ColourOf{$ColourOf[$i]}= $i ; }
# Package list cache update parameters.
my $uplcFileName= '/var/cache/apt/pkgcache.bin' ;
my $OneDay= 24*60*60 ; # Length of one day, expressed in [s]
my $uplcRefreshAge= 2 * $OneDay ; # Minimum age of cache before refresh
my $uplcRefreshCmd= 'sudo apt-get update' ; # Cache refresh command
#
# Updatable package list parameters.
my $upCmd= 'apt-get --just-print upgrade' ; # Command
my $upCnt= 0 ; # Total number of upgradable packages
#
# Global variables.
# -----------------
#
my $Now= localtime ; # Timestamp of tests
$Now= $Now->strftime( $FmtDate ) ;
my $Colour= $#ColourOf ; # Test status
my $Result= '' ; # Message to sent to Xymon
my %Class= ( High => 'red', Normal => 'yellow', Postponed => 'green' ) ;
my %UPL = () ;
$UPL{$_} = {} foreach ( keys %Class ) ;
my @Lines ;
sub min($$) { return $_[0] < $_[1] ? $_[0] : $_[1] ; }
#
# Function UpdatePackageListCache updates the package list cache, if it hasn't
# been updated for some time.
#
sub UpdatePackageListCache() {
my (@Time,$Time) ; # Time of last change of cache
@Lines= `stat $uplcFileName` ; # Cache file statistics
return unless @Lines ; # Ignore error condition
foreach ( @Lines ) {
next unless @Time= m/^Modify: (\d{4})-(\d+)-(\d+) (\d+):(\d+):(\d+)\./ ;
$Time[1]-- ; # Adjust month ordinal
$Time= timelocal( reverse @Time ) ; # Compute time stamp
return if time - $Time < $uplcRefreshAge ;
`$uplcRefreshCmd` ; # Update package list cache
last ; # Skip rest of lines
} # of foreach
} # of UpdatePackageListCache
#
# Function GetListOfUpdates gets the list of the updatable packages. A typical
# line of output, specifying an updatable package, looks like the following
# line:
# Inst python2.7 [2.7.13-2] (2.7.13-2+deb9u2 Raspbian:stable [armhf]) []
#
sub GetListOfUpdates() {
my $InPostponed= 0 ; # Flag: in list of kept back packages
@Lines= `$upCmd` ; # Current list of updatable packages
foreach ( @Lines ) {
chomp ;
# Handle the list of packages which are not to be upgraded (which are kept
# back). The format of such a list is as follows:
# The following packages have been kept back:
# firmware-brcm80211
#
if ( $InPostponed ) {
if ( m/^\s\s\S/ ) {
foreach my $pn ( split ) {
next unless defined $pn;
$UPL{Postponed}{$pn}= [ $pn, '?', '?' ] ;
$upCnt++ ;
} # of foreach
next ; # Line is handled
} else {
$InPostponed= 0 ; # End of section found
} # of else
} elsif ( m/^The following packages have been kept back/ ) {
$InPostponed= 1 ; # Start of section found
next ; # Line is handled
} # of elsif
if ( m/^Inst (.+?) \[(.+?)] \((.+?) / ) {
if ( m/securi/i ) {
$UPL{High}{$1} = [ $1, $2, $3 ] ;
} else {
$UPL{Normal}{$1}= [ $1, $2, $3 ] ;
} # of else
$upCnt++ ;
} # of if
} # of foreach
} # of GetListOfUpdates
#
# Function BuildMessage creates the message to be sent to Xymon. Any errors
# should be recorded in %ErrMsg (too).
#
sub BuildMessage() {
my $SubClr ;
#
# Build a table showing a summary of the results.
#
$Result = $upCnt == 0 ? "\n" : "Summary\n\n" ;
$Result.= "<table>\n" ;
$Result.= " <tr><th>Priority</th><th>Count</th><th>Status</th></tr>\n" ;
foreach ( sort keys %UPL ) {
$SubClr= keys %{$UPL{$_}} ? $ColourOf{$Class{$_}} : $ColourOf{'green'} ;
$Colour= min( $Colour, $SubClr ) ;
$SubClr= $ColourOf[$SubClr] ;
$Result.= " <tr><td> $_ </td>" .
"<td align='right'>" . scalar(keys %{$UPL{$_}}) . "</td>" .
"<td> &$SubClr</tr>\n" ;
} # of foreach
$Result.= " <tr><td> </td><td>-----</td><td> </td></tr>\n" ;
$Result.= " <tr><td> Total </td><td align='right'>$upCnt</td>" .
"<td> &$ColourOf[$Colour]</td></tr>\n" ;
$Result.= "</table>\n" ;
return if $upCnt == 0 and keys(%{$UPL{Postponed}}) == 0 ;
$Result.= "\nList of upgradeable packages\n\n" ;
$Result.= "<table>\n" ;
$Result.= " <tr><th>Package</th><th>Priority</th>" .
"<th>Current version</th><th>New version</th></tr>\n" ;
foreach my $class ( sort keys %UPL ) {
my $hr= $UPL{$class} ;
foreach my $up ( sort keys %$hr ) {
$Result.= " <tr><td> $up </td><td> $class </td>" .
"<td> $$hr{$up}[1] </td><td> $$hr{$up}[2] </td></tr>\n" ;
} # of foreach
} # of foreach
$Result.= "</table>\n" ;
} # of BuildMessage
#
# Function InformXymon sends the message, in global variable $Result, to the
# Xymon server. Any error messages in %ErrMsg are prepended to the message and
# the status (colour) of the message is adapted accordingly.
#
sub InformXymon($) {
$Colour= $ColourOf[$Colour] ;
$Result= "\"status$XyLife $HostName.$TestName $Colour $Now\n" .
"<b>$_[0]</b>\n\n$Result\"\n" ;
`$XySend $XyDisp $Result` ; # Inform Xymon
} # of InformXymon
#
# MAIN PROGRAM.
# =============
#
UpdatePackageListCache ; # `apt-get update`
GetListOfUpdates ; # Retrieve list of outstanding updates
BuildMessage ; # Build message for xymon
InformXymon( 'Package upgrades' ) ; # Send message to xymon server