#!/usr/bin/perl -wT # -*- perl -*- # Copyright (C) 2004-2006 # # 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; version 2 dated June, # 1991. # # 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, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # $Id: munin-run.in 1396 2008-01-20 19:32:46Z matthias $ # use strict; use vars qw(@ISA); use Getopt::Long; # Variable is set at build, with values detected by the makefiles my $HAS_SETR = 1; # "Clean" environment to disable taint-checking on the environment. We _know_ # that the environment is insecure, but we want to let admins shoot themselves # in the foot with it, if they want to. foreach my $key (keys %ENV) { if ($ENV{$key} =~ /^(.*)$/) { $ENV{$key} = $1; } else { delete $ENV{$key}; } } $0 =~ /^(.*)$/; # for some strange reason won't "$0 = $0;" work. $0 = $1; # Make configuration settings available at runtime. $ENV{'MUNIN_PREFIX'} = '/usr'; $ENV{'MUNIN_CONFDIR'} = '/etc/munin'; # /etc/munin,/etc/opt/munin or such $ENV{'MUNIN_BINDIR'} = '/usr/bin'; $ENV{'MUNIN_SBINDIR'} = '/usr/sbin'; $ENV{'MUNIN_DOCDIR'} = '/usr/doc'; $ENV{'MUNIN_LIBDIR'} = '/usr/share/munin'; # LIBDIR/plugins contains plugin.sh $ENV{'MUNIN_HTMLDIR'} = '/var/www/munin'; $ENV{'MUNIN_CGIDIR'} = '/usr/lib/cgi-bin'; $ENV{'MUNIN_DBDIR'} = '/var/lib/munin'; $ENV{'MUNIN_PLUGSTATE'} = '/var/lib/munin/plugin-state'; # Put plugin state files here! $ENV{'MUNIN_MANDIR'} = '/usr/man'; $ENV{'MUNIN_LOGDIR'} = '/var/log/munin'; $ENV{'MUNIN_STATEDIR'} = '/var/run/munin'; # This is for .pid files $ENV{'MUNIN_USER'} = 'munin'; # User munin runs as (mostly) $ENV{'MUNIN_GROUP'} = 'munin'; # Group ditto $ENV{'MUNIN_PLUGINUSER'} = 'nobody';# Default user for plugin running $ENV{'MUNIN_VERSION'} = '1.2.6'; $ENV{'MUNIN_PERL'} = '/usr/bin/perl'; $ENV{'MUNIN_PERLLIB'} = '/usr/share/perl5'; $ENV{'MUNIN_GOODSH'} = '/bin/sh'; $ENV{'MUNIN_BASH'} = '/bin/bash'; $ENV{'MUNIN_PYTHON'} = '/usr/bin/env python'; $ENV{'MUNIN_OSTYPE'} = 'linux'; $ENV{'MUNIN_HOSTNAME'} = 'localhost.localdomain'; $ENV{'MUNIN_MKTEMP'} = 'mktemp -p /tmp/ $1'; my %services; my %nodes; my $servicedir="/etc/munin/plugins"; my $sconfdir="/etc/munin/plugin-conf.d"; my $conffile="/etc/munin/munin-node.conf"; my $sconffile=undef; my $FQDN=""; my $do_usage = 0; my $DEBUG = 0; my $do_version = 0; my $VERSION='1.2.6'; my $defuser = getpwnam ("nobody"); my $defgroup= getgrnam ("munin"); my $paranoia = 0; my @ignores = (); my %sconf = (); $do_usage=1 unless GetOptions ( "config=s" => \$conffile, "debug!" => \$DEBUG, "version!" => \$do_version, "servicedir=s" => \$servicedir, "sconfdir=s" => \$sconfdir, "sconffile=s" => \$sconffile, "paranoia!" => \$paranoia, "help" => \$do_usage ); if ($do_usage or ! defined($ARGV[0])) { print "Usage: $0 [options] Options: --help View this message. --config Use as configuration file. [/etc/munin/munin-node.conf] --servicedir Dir where plugins are found. [/etc/munin/plugins] --sconfdir Dir where plugin configurations are found. [/etc/munin/plugin-conf.d] --sconffile Dir where plugins are found. Overrides sconfdir. [undefined] --[no]paranoia Only run plugins owned by root. Check permissions. [--paranoia] --debug View debug messages. --version View version information. "; exit 0; } if ($conffile =~ /^([-\/\@_\w\.]+)$/) { $conffile = $1; # $data now untainted } else { die "Bad data in $conffile"; # log this somewhere } if ($sconfdir =~ /^([-\/\@_\w\.]+)$/) { $sconfdir = $1; # $data now untainted } else { die "Bad data in $sconfdir"; # log this somewhere } if (defined $sconffile and $sconffile =~ /^([-\/\@_\w\.]+)$/) { $sconffile = $1; # $data now untainted } elsif (defined $sconffile) { die "Bad data in $sconffile"; # log this somewhere } if ($servicedir =~ /^([-\/\@_\w\.]+)$/) { $servicedir = $1; # $data now untainted } else { die "Bad data in $servicedir"; # log this somewhere } if ($do_version) { print <<"EOT"; munin-run (munin-node) version $VERSION. Written by Jimmy Olsen / Linpro AS Copyright (C) 2002-2005 This is free software released under the GNU General Public License. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, please refer to the file COPYING that is included with this software or refer to http://www.fsf.org/licensing/licenses/gpl.txt EOT exit 0; } # Check permissions of configuration if (!&check_perms ($servicedir) or !&check_perms ($conffile)) { die "Fatal error. Bailing out."; } if (! -f $conffile) { print "ERROR: Cannot open $conffile\n"; exit 1; } open FILE,$conffile or die "Cannot open $conffile\n"; while () { chomp; s/#.*//; # no comments s/^\s+//; # no leading white s/\s+$//; # no trailing white next unless length; # anything left? /(^\w*)\s+(.*)/; if (($1 eq "host_name" or $1 eq "hostname") and $2) { $FQDN=$2; } elsif (($1 eq "default_plugin_user" or $1 eq "default_client_user") and $2) { my $tmpid = $2; my $defuser = &get_uid ($tmpid); if (! defined ($defuser)) { die "Default user defined in \"$conffile\" does not exist ($tmpid)"; } } elsif (($1 eq "default_plugin_group" or $1 eq "default_client_group") and $2) { my $tmpid = $2; $defgroup = &get_gid ($tmpid); if (! defined ($defgroup)) { die "Default group defined in \"$conffile\" does not exist ($tmpid)"; } } elsif (($1 eq "paranoia") and defined $2) { if ("$2" eq "no" or "$2" eq "false" or "$2" eq "off" or "$2" eq "0") { $paranoia = 0; } else { $paranoia = 1; } } elsif (($1 eq "ignore_file") and defined $2) { push @ignores, $2; } } $FQDN ||= &get_fq_hostname; $ENV{'FQDN'}=$FQDN; # Some locales uses "," as decimal separator. This can mess up a lot # of plugins. $ENV{'LC_ALL'}='C'; &load_services; exit; ### over-ridden subs below sub load_services { if ($sconffile) { if (!&load_auth_file ("", $sconffile, \%sconf)) { warn "Something wicked happened while reading \"$sconffile\". Check the previous log lines for spesifics."; } } else { if (opendir (DIR,$sconfdir)) { FILES: for my $file (grep { -f "$sconfdir/$_" } readdir (DIR)) { next if $file =~ m/^\./; # Hidden files next if $file !~ m/^([-\w.]+)$/; # Skip if any weird chars $file = $1; # Not tainted anymore. foreach my $regex (@ignores) { next FILES if $file =~ /$regex/; } if (!&load_auth_file ($sconfdir, $file, \%sconf)) { warn "Something wicked happened while reading \"$servicedir/$file\". Check the previous log lines for spesifics."; } } closedir (DIR); } } opendir (DIR,$servicedir) || die "Cannot open plugindir: $servicedir $!"; FILES: for my $file (grep { -f "$servicedir/$_" } readdir(DIR)) { next if $file =~ m/^\./; # Hidden files next if $file =~ m/.conf$/; # Config files next if $file !~ m/^([-\w.:]+)$/; # Skip if any weird chars $file = $1; # Not tainted anymore. foreach my $regex (@ignores) { next FILES if $file =~ /$regex/; } next if (! -x "$servicedir/$file"); # File not executeable next unless ($file =~ /^$ARGV[0]$/); print "# file: '$file'\n" if $DEBUG; my $arg = undef; if (defined $ARGV[1]) { if ($ARGV[1] =~ /^c/i) { $arg = "config"; } elsif ($ARGV[1] =~ /^a/i) { $arg = "autoconf"; } elsif ($ARGV[1] =~ /^snmp/i) { $arg = "snmpconf"; } elsif ($ARGV[1] =~ /^s/i) { $arg = "suggest"; } } $services{$file}=1; my @rows = run_service($file, $arg); my $node = $FQDN; for my $row (@rows) { print "# row: $row\n" if $DEBUG; if ($row =~ m/^host_name (.+)$/) { print "# Found host_name, using it\n" if $DEBUG; $node = $1; } } $nodes{$node}{$file}=1; } closedir DIR; print "ERROR: Could not execute plugin (plugin doesn't exist?).\n"; exit 1; } sub run_service { my ($service,$command) = @_; $command ||=""; my @lines = ();; my $timed_out = 0; if ($services{$service}) { my $child = 0; local $SIG{ALRM} = sub { $timed_out = 1; }; # Setting environment $sconf{$service}{user} = &get_var (\%sconf, $service, 'user'); $sconf{$service}{group} = &get_var (\%sconf, $service, 'group'); $sconf{$service}{command} = &get_var (\%sconf, $service, 'command'); &get_var (\%sconf, $service, 'env', \%{$sconf{$service}{env}}); if ($< == 0) # If root { # Giving up gid egid uid euid my $u = (defined $sconf{$service}{'user'}? $sconf{$service}{'user'}: $defuser); my $g = $defgroup; my $gs = "$g $g" . (defined $sconf{$service}{'group'}?" $sconf{$service}{group}":""); print "# Want to run as euid/egid $u/$g\n" if $DEBUG; if ($HAS_SETR) { $( = $g unless $g == 0; $< = $u unless $u == 0; } $) = $gs unless $g == 0; $> = $u unless $u == 0; if ($> != $u or $g != (split (' ', $)))[0]) { print "# Can't drop privileges. Bailing out. (wanted uid=", ($sconf{$service}{'user'} || $defuser), " gid=\"", $gs, "\"($g), got uid=$> gid=\"$)\"(", (split (' ', $)))[0], ").\n"; exit 1; } print "# Running as uid/gid/euid/egid $/$)\n" if $DEBUG; if (!&check_perms ("$servicedir/$service")) { print "# Error: unsafe permissions. Bailing out."; exit 1; } } # Setting environment... if (exists $sconf{$service}{'env'} and defined $sconf{$service}{'env'}) { foreach my $key (keys %{$sconf{$service}{'env'}}) { print "# Setting environment $key=$sconf{$service}{env}{$key}\n" if $DEBUG; $ENV{$key} = $sconf{$service}{env}{$key}; } } if (exists $sconf{$service}{'command'} and defined $sconf{$service}{'command'}) { my @run = (); foreach my $t (@{$sconf{$service}{'command'}}) { if ($t =~ /^%c$/) { push (@run, "$servicedir/$service", $command); } else { push (@run, $t); } } print "# About to run \"", join (' ', @run), "\"\n" if $DEBUG; exec (@run) if @run; } else { print "# DEBUG: About to exec \"$servicedir/$service\"\n" if $DEBUG; if (!exec ("$servicedir/$service", $command)) { print "no (could not execute plugin)\n"; exit 1; } } } else { print "# Unknown service\n"; } chomp @lines; return (@lines); } sub get_fq_hostname { my $hostname; eval { require Sys::Hostname; $hostname = (gethostbyname(Sys::Hostname::hostname()))[0]; }; return $hostname if $hostname; $hostname = `hostname`; # Fall$ chomp($hostname); $hostname =~ s/\s//g; return $hostname; } sub get_uid { my $user = shift; return undef if (!defined $user); if ($user !~ /\d/) { $user = getpwnam ($user); } return $user; } sub get_gid { my $group = shift; return undef if (!defined $group); if ($group !~ /\d/) { $group = getgrnam ($group); } return $group; } sub load_auth_file { my ($dir, $file, $sconf) = @_; my $service = $file; if (!defined $dir or !defined $file or !defined $sconf) { return undef; } return undef if (length $dir and !&check_perms ($dir)); return undef if (!&check_perms ("$dir/$file")); if (!open (IN, "$dir/$file")) { warn "Could not open file \"$dir/$file\" for reading ($!), skipping plugin\n"; return undef; } while () { chomp; s/#.*$//; next unless /\S/; if (/^\s*\[([^\]]+)\]\s*$/) { $service = $1; } elsif (/^\s*user\s+(\S+)\s*$/) { my $optional = 0; my $user = $1; if ($user =~ /^\(([^)]+)\)$/) { $optional = 1; $user = $1; } my $u = &get_uid ($user); if (!defined $u and !$optional) { warn "User \"$user\" in configuration file \"$dir/$file\" nonexistant. Skipping plugin."; return undef; } elsif (!defined $u and $optional) { print ("# DEBUG: Skipping \"$user\" (optional).\n") if $DEBUG; next; } else { $sconf->{$service}{'user'} = $u; } } elsif (/^\s*group\s+(.+)\s*$/) { my $tmpid = $1; foreach my $group (split /\s*,\s*/, $tmpid) { my $optional = 0; if ($group =~ /^\(([^)]+)\)$/) { $optional = 1; $group = $1; } my $g = &get_gid ($group); if (!defined $g and !$optional) { warn "Group \"$group\" in configuration file \"$dir/$file\" nonexistant. Skipping plugin."; return undef; } elsif (!defined $g and $optional) { print "# DEBUG: Skipping \"$group\" (optional).\n" if $DEBUG; next; } if (!defined $sconf->{$service}{'group'}) { $sconf->{$service}{'group'} = $g; } else { $sconf->{$service}{'group'} .= " $g"; } } } elsif (/^\s*command\s+(.+)\s*$/) { @{$sconf->{$service}{'command'}} = split (/\s+/, $1); } elsif (/^\s*host_name\s+(.+)\s*$/) { $sconf->{$service}{'host_name'} = $1; } elsif (/^\s*timeout\s+(\d+)\s*$/) { $sconf->{$service}{'timeout'} = $1; print "# DEBUG: $service: setting timeout to $1\n" if $DEBUG; } elsif (/^\s*(allow)\s+(.+)\s*$/ or /^\s*(deny)\s+(.+)\s*$/) { push (@{$sconf->{$service}{'allow_deny'}}, [$1, $2]); print "# DEBUG: Pushing allow_deny: $1, $2\n" if $DEBUG; } elsif (/^\s*env\s+([^=\s]+)\s*=\s*(.+)$/) { $sconf->{$service}{'env'}{$1} = $2; print "# Saving $service->env->$1 = $2...\n" if $DEBUG; warn "Warning: Deprecated format in \"$dir/$file\" under \"[$service]\" (\"env $1=$2\" should be rewritten to \"env.$1 $2\")."; } elsif (/^\s*env\.(\S+)\s+(.+)$/) { $sconf->{$service}{'env'}{$1} = $2; print "# Saving $service->env->$1 = $2...\n" if $DEBUG; } elsif (/^\s*(\w+)\s+(.+)$/) { $sconf->{$service}{'env'}{"lrrd_$1"} = $2; print "# Saving $service->env->lrrd_$1 = $2...\n" if $DEBUG; warn "Warning: Deprecated format in \"$dir/$file\" under \"[$service]\" (\"$1 $2\" should be rewritten to \"env lrrd_$1=$2\")."; } elsif (/\S/) { warn "Warning: Unknown config option in \"$dir/$file\" under \"[$service]\": $_"; } } close (IN); return 1; } sub check_perms { my $target = shift; my @stat; return undef if (!defined $target); return 1 if (!$paranoia); if (! -e "$target") { warn "Failed to check permissions on nonexistant target: \"$target\""; return undef; } @stat = stat ($target); if (!$stat[4] == 0 or ($stat[5] != 0 and $stat[2] & 00020) or ($stat[2] & 00002)) { warn "Warning: \"$target\" has dangerous permissions (", sprintf ("%04o", $stat[2] & 07777), ")."; return 0; } if (-f "$target") # Check dir as well { (my $dirname = $target) =~ s/[^\/]+$//; return &check_perms ($dirname); } return 1; } sub get_var { my $sconf = shift; my $name = shift; my $var = shift; my $env = shift; if ($var eq 'env' and !defined $env) { %{$env} = (); } if ($var ne 'env' and exists $sconf->{$name}{$var}) { return $sconf->{$name}{$var}; } # Deciding environment foreach my $wildservice (grep (/\*$/, reverse sort keys %{$sconf})) { (my $tmpservice = $wildservice) =~ s/\*$//; next unless ($name =~ /^$tmpservice/); print "# Checking $wildservice...\n" if $DEBUG; if ($var eq 'env') { if (exists $sconf->{$wildservice}{'env'}) { foreach my $key (keys %{$sconf->{$wildservice}{'env'}}) { if (! exists $sconf->{$name}{'env'}{$key}) { $sconf->{$name}{'env'}{$key} = $sconf->{$wildservice}{'env'}{$key}; print "# Saving $wildservice->$key\n" if $DEBUG; } } } } else { if (! exists $sconf->{$name}{$var} and exists $sconf->{$wildservice}{$var}) { return ($sconf->{$wildservice}{$var}); } } } return $env; } 1; =head1 NAME munin-run - A program to run munin-node plugins from the command line =head1 SYNOPSIS munin-run [--options] =head1 OPTIONS =over 5 =item B<< --config >> Use EfileE as configuration file. [/etc/munin/munin-node.conf] =item B<< --servicedir >> Use EdirE as plugin dir. [/etc/munin/plugins] =item B<< --sconfdir >> Use EdirE as plugin configuration dir. [/etc/munin/plugin-conf.d] =item B<< --sconffile >> Use EfileE as plugin configuration. [undefined] =item B< --help > View this help message. =item B< --debug > Print debug messages. Debug messages are sent to both STDOUT and are prefixed with "#" (this makes it easier for other parts of munin to use munin-run and still have --debug on). Only errors go to STDERR. =item B< --version > Show version information. =back =head1 DESCRIPTION Munin-node is a daemon that Munin connects to fetch data. This data is stored in .rrd-files on the central server , and later graphed and htmlified. munin-run is a perlscript to run the plugins used by the munin-node daemon from the command line. It's helpful to debug plugins as they are run the same way with the same user and same environment settings as within the munin-node. For more information, see the documentation section at L. =head1 FILES /etc/munin/munin-node.conf /etc/munin/plugins/* /etc/munin/plugin-conf.d/* /var/run/munin/munin-node.pid /var/log/munin/munin-node =head1 VERSION This is munin-node v1.2.6 =head1 AUTHORS Audun Ytterdal, Jimmy Olsen, Tore Anderson, Nicolai Langfeldt. =head1 BUGS munin-node does, as of now, not check the syntax of the configuration file. Please report other bugs in the bug tracker at L. =head1 COPYRIGHT Copyright (C) 2002-2006 Audun Ytterdal, Jimmy Olsen, Tore Anderson, Nicolai Langfeldt / Linpro AS. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. This program is released under the GNU General Public License =cut # vim:syntax=perl