#!@@PERL@@ -w # -*- perl -*- =head1 NAME apt_all - Plugin to monitor packages that should be installed on systems using apt-get (mostly Debian, but also RedHat). =head1 NOTES The differences between this plugin and the apt plugins, is that this plugin separates each distro with its own plot, and actually draws graphs. =head1 CONFIGURATION You can add some extra options to the apt call, in order to override your /etc/apt.conf defaults. [apt_all] env.options -o Debug::pkgDepCache::AutoInstall=false -o APT::Get::Show-Versions=false env.releases stable experimental =head2 options This settings defaults to the example given above. This default overrides potential local customization within /etc/apt.conf, which could interfere with the intended operation of apt or the output format required for this plugin. Please note that "apt" honors your /etc/apt.conf defaults. Thus you may need to override specific options, if you adjusted your local apt.conf setup. =head2 releases This configuration may contain a space separated list of release names. It defaults to the empty string. This default triggers the automatic detection of available distributions from the URLs of all configured repositories. =head1 USAGE This plugin needs a cronjob that runs apt-get update every hour or so Example conjob /etc/cron.d/munin-plugin-apt 53 * * * * root apt-get update > /dev/null 2>&1 23 08 * * * root apt-get update > /dev/null Remember to randomize when these cronjobs are run on your servers This plugin can also be called with the argument "update", which will run apt-get update update Updates the APT database randomly, guaranteeing there won't be more than seconds between each update. Otherwise, there is a a 1 in chance that an update will occur. =head1 MAGIC MARKERS #%# family=manual #%# capabilities=autoconf =cut # Now for the real work... use strict; use Munin::Plugin; $ENV{'LANG'}="C"; $ENV{'LC_ALL'}="C"; # The plugin is called in two ways: # - via a cron job (without the munin environment provided by "munin-run") # - as a regular plugin # Thus the location evaluation below relies on the following assumptions: # - the plugin is configured to run as "nobody" # - the munin plugin state directory location was not reconfigured my $statefile = ($ENV{MUNIN_PLUGSTATE} || '@@PLUGSTATE@@/nobody/') . "/plugin-apt_all.state"; my $apt_options = $ENV{options} || "-o Debug::pkgDepCache::AutoInstall=false -o APT::Get::Show-Versions=false"; # try to determine the currently available distributions by inspecting the repository URLs sub guess_releases() { open(my $fh, "-|", "apt-get $apt_options update --print-uris") or die("Failed to determine distribution releases via 'apt-get update --print-uris"); my %release_names; my $line; while ( ! eof($fh) ) { defined( $line = readline $fh ) or die "Failed to read line from output of 'apt-get': $!"; # example line: # 'http://ftp.debian.org/debian/dists/stable/InRelease' ftp.debian.org_debian_dists_stable_InRelease 0 if ($line =~ m#^.*/dists/([^']+)/[^/]+/[^/]+/Packages#) { my $release_name = $1; # The security updates are named like "buster/updates". The first part (before the # slash) is sufficient to mach these packages. $release_name =~ s|/updates$||; $release_names{$release_name} = 1; } } return sort keys %release_names; } # use a given 'releases' environment variable (space separated names) or inspect the repository URLs my @releases = split(/\s/, ($ENV{releases} || "")); @releases = guess_releases() unless @releases; sub get_clean_release_fieldname { my ($fieldname) = @_; # apply some minor URI-like substitution (avoiding ambiguity between slash and hyphen) $fieldname =~ s#/#_2F#g; return clean_fieldname($fieldname); } sub print_state() { if (-l $statefile) { die("$statefile is a symbolic link, refusing to read it."); } if (! -e "$statefile") { update_state(); } if (! -e "$statefile") { die("$statefile does not exist. Something wicked happened."); } open(STATE, "$statefile") or die("Couldn't open state file $statefile for reading."); while (my $line = ) { foreach my $release (@releases) { my $release_cleaned = get_clean_release_fieldname($release); # print only lines that are exected for the currently requested releases if ($line =~ /^(hold|pending)_$release_cleaned\.(value|extinfo)/) { print $line ; last; } } } close STATE; } sub update_state() { if(-l $statefile) { die("$statefile is a symbolic link, refusing to touch it."); } open(STATE, ">$statefile") or die("Couldn't open state file $statefile for writing."); foreach my $release (@releases) { my $apt="apt-get $apt_options -u dist-upgrade --print-uris --yes -t $release |"; open (APT, "$apt") or exit 22; my @pending = (); my $hold = 0; my @remove = (); my @install = (); while () { if (/^The following packages will be REMOVED:/) { my $where = 0; while () { last if (/^\S/); foreach my $package (split /\s+/) { next unless ($package =~ /\S/); push (@remove, "-$package"); } } } if (/^The following NEW packages will be installed:/) { my $where = 0; while () { last if (/^\S/); foreach my $package (split /\s+/) { next unless ($package =~ /\S/); push (@install, "+$package"); } } } if (/^The following packages will be upgraded/) { my $where = 0; while () { last if (/^\S/); foreach my $package (split /\s+/) { next unless ($package =~ /\S/); push (@pending, $package); } } } if (/^\d+\supgraded,\s\d+\snewly installed, \d+ to remove and (\d+) not upgraded/) { $hold = $1; } } push (@pending, @install) if @install; push (@pending, @remove ) if @remove; close APT; my $release_cleaned = get_clean_release_fieldname($release); print STATE "pending_$release_cleaned.value ", scalar (@pending), "\n"; if (@pending) { print STATE "pending_$release_cleaned.extinfo ", join (' ', @pending), "\n"; } print STATE "hold_$release_cleaned.value $hold\n"; } close(STATE); } sub update_helpandexit() { print("apt update -- update apt databases randomly\n\n", " maxinterval:\n", " Enforce the updating of the apt database if it has\n", " been more than 'maxinterval' many seconds since the last update.\n\n", " probability:\n", " There's a 1 in 'probability' chance that the database\n", " will be updated.\n"); exit(1); } if ($ARGV[0] and $ARGV[0] eq "autoconf") { `apt-get -v >/dev/null 2>/dev/null`; if ($? eq "0") { print "yes\n"; exit 0; } else { print "no (apt-get not found)\n"; exit 0; } } if ($ARGV[0] and $ARGV[0] eq "config") { print "graph_title Pending packages\n"; print "graph_vlabel Total packages\n"; print "graph_category system\n"; foreach my $release (@releases) { my $release_cleaned = get_clean_release_fieldname($release); print "pending_$release_cleaned.label pending ($release)\n"; print "pending_$release_cleaned.warning 0:0\n"; print "hold_$release_cleaned.label hold ($release)\n"; } exit 0; } if ($ARGV[0] and $ARGV[0] eq "update") { my $maxinterval = $ARGV[1] ? $ARGV[1] : update_helpandexit; my $probability = $ARGV[2] ? $ARGV[2] : update_helpandexit; # The state file needs an update in three situations: # * it does not exist # * $maxinterval seconds elapsed since the last update # * random $probability was hit (one out of $probability) if (!-e $statefile || ((stat($statefile))[10] + $maxinterval < time()) || (int(rand($probability)) == 0)) { system("/usr/bin/apt-get update") == 0 or die("Failed to run 'apt-get update'"); update_state(); } exit(0); } print_state (); exit 0; # vim:syntax=perl