#!/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/local';
$ENV{'MUNIN_CONFDIR'}    = '/etc/munin';   # /etc/munin,/etc/opt/munin or such
$ENV{'MUNIN_BINDIR'}     = '/usr/local/bin';
$ENV{'MUNIN_SBINDIR'}    = '/usr/local/sbin';
$ENV{'MUNIN_DOCDIR'}     = '/usr/local/doc';
$ENV{'MUNIN_LIBDIR'}     = '/usr/local/libexec/munin';    # LIBDIR/plugins contains plugin.sh
$ENV{'MUNIN_HTMLDIR'}    = '/var/www/htdocs/munin';
$ENV{'MUNIN_CGIDIR'}     = '/var/www/cgi-bin';
$ENV{'MUNIN_DBDIR'}      = '/var/db/munin';
$ENV{'MUNIN_PLUGSTATE'}  = '/var/db/munin-pluginstate'; # Put plugin state files here!
$ENV{'MUNIN_MANDIR'}     = '/usr/local/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'} = '_munin-plugin';# Default user for plugin running
$ENV{'MUNIN_VERSION'}    = '1.2.6';
$ENV{'MUNIN_PERL'}       = '/usr/bin/perl';
$ENV{'MUNIN_PERLLIB'}    = '/usr/local/libdata/perl5/site_perl';
$ENV{'MUNIN_GOODSH'}     = '/bin/sh';
$ENV{'MUNIN_BASH'}       = '/bin/bash';
$ENV{'MUNIN_PYTHON'}     = '/usr/local/bin/python2.5';
$ENV{'MUNIN_OSTYPE'}     = 'openbsd';
$ENV{'MUNIN_HOSTNAME'}   = 'localhost';
$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 ("_munin-plugin");
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)
{
    print "Usage: $0 [options]

Options:
    --help              View this message.
    --config <file>     Use <file> as configuration file. 
                        [/etc/munin/munin-node.conf]
    --servicedir <dir>  Dir where plugins are found. 
                        [/etc/munin/plugins]
    --sconfdir <dir>    Dir where plugin configurations are found. 
                        [/etc/munin/plugin-conf.d]
    --sconffile <dir>   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 (<FILE>) {
  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 (<IN>)
    {
	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] <plugin>

=head1 OPTIONS

=over 5

=item B<< --config <configfile> >>

Use E<lt>fileE<gt> as configuration file. [/etc/munin/munin-node.conf]

=item B<< --servicedir <dir> >>

Use E<lt>dirE<gt> as plugin dir. [/etc/munin/plugins]

=item B<< --sconfdir <dir> >>

Use E<lt>dirE<gt> as plugin configuration dir. [/etc/munin/plugin-conf.d]

=item B<< --sconffile <file> >>

Use E<lt>fileE<gt> 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<http://munin.sf.net/>.

=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<http://munin.sf.net/>.

=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
