#!/usr/bin/perl -w
# -*- cperl -*-
#
# Copyright (C) 2003-2006 Jimmy Olsen, Nicolai Langfeldt.
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Program to suggest what plugins to use and not

use strict;
use warnings;

use Getopt::Long;
use POSIX ();

use Data::Dumper;
use Carp;

use Munin::Common::Defaults;
use Munin::Node::Configure::PluginList;
use Munin::Node::Configure::Debug;

use Munin::Node::Config;
my $config = Munin::Node::Config->instance();

my $timeout = 10;

my @all_families     = qw/auto manual contrib/;
my @default_families = qw/auto/;


sub main
{
	parse_args();

    my $plugins = Munin::Node::Configure::PluginList->new(
        libdir     => $config->{libdir},
        servicedir => $config->{servicedir},
    );

    $plugins->load(@{$config->{families}});

    # Gather information
    if (@{ $config->{snmp} }) {
        my $snmp = init_snmp($plugins);
        $snmp->run_probes($plugins);
    }
    elsif ($config->{suggest}) {
        gather_suggestions($plugins);
    }
    else {
        print_status_list($plugins);
        exit 0;
    }

    # Display results.
    if ($config->{shell}) {
        manage_links($plugins);
    }
    else {
        show_suggestions($plugins);
    }

    # Report errors.
    # FIXME: surely exit 0 unless m-n-c itself has gone funny?
    if (my @errors = list_errors($plugins->list)) {
        print STDERR "# The following plugins caused errors:\n";
        foreach my $err (@errors) {
            print STDERR "# $err\n";
        }
        exit 1;
    }
    exit 0;
}


sub parse_args
{
	my $conffile   = "$Munin::Common::Defaults::MUNIN_CONFDIR/munin-node.conf";
	my $servicedir = "$Munin::Common::Defaults::MUNIN_CONFDIR/plugins";
	my $sconfdir   = "$Munin::Common::Defaults::MUNIN_CONFDIR/plugin-conf.d";
	my $libdir     = "$Munin::Common::Defaults::MUNIN_LIBDIR/plugins";

	my $debug;
	my ($suggest, $shell, $removes, $newer);
	my $exit_not_error = 1;
	my @families;
	my (@snmp_hosts, $snmpver, $snmpcomm, $snmpport);

	print_usage_and_exit() unless GetOptions(
		"help"            => \&print_usage_and_exit,
		"shell!"          => \$shell,
		"exitnoterror!"   => \$exit_not_error,
		"debug!"          => \$debug,
		"suggest!"        => \$suggest,
		"config=s"        => \$conffile,
		"servicedir=s"    => \$servicedir,
		"sconfdir=s"      => \$sconfdir,
		"remove-also!"    => \$removes,
		"libdir=s"        => \$libdir,
		"families=s"      => \@families,
		"version!"        => \&print_version_and_exit,
		"snmp=s"          => \@snmp_hosts,
		"snmpversion=s"   => \$snmpver,
		"snmpcommunity=s" => \$snmpcomm,
		"snmpport=i"      => \$snmpport,
		"newer=s"         => \$newer
	);

	$config->parse_config_from_file($conffile);

    # --shell implies --suggest unless --snmp was also used
    $suggest = 1 if ($shell and not @snmp_hosts);

    @families = (@families)    ? map { split /,/ } @families :
                (@snmp_hosts)  ? ('snmpauto')                :
                ($suggest)     ? @default_families           :
                                 @all_families               ;

    # Allow the user to mix multiple invocations of --snmp with the
    # comma-delimited form
    @snmp_hosts = map { split /,/ } @snmp_hosts;

	$config->reinitialize({
		timeout => $timeout,

		%$config,

		newer => $newer,
		shell => $shell,
		exit_not_error => $exit_not_error,
		suggest => $suggest,
		remove_also => $removes,

		families => \@families,

		conffile => $conffile,
		servicedir => $servicedir,
		sconfdir => $sconfdir,
		libdir => $libdir,

		snmp => \@snmp_hosts,
		snmp_version => $snmpver,
		snmp_community => $snmpcomm,
		snmp_port => $snmpport,

		DEBUG => $debug,
	});

	return;
}


sub print_usage_and_exit
{
	my $all_families     = join(', ', @all_families);
	my $default_families = join(', ', @default_families);

	print qq{Usage: $0 [options]

Options:
    --help                  View this help page
    --version               Show version information

    --debug                 View debug information (very verbose).  All
                            debugging output is printed on STDOUT but each
                            line is prefixed with '#'.  Only errors are
                            printed on STDERR.

    --config <file>         Override configuration file
                                [$Munin::Common::Defaults::MUNIN_CONFDIR/munin.conf]
    --servicedir <dir>      Override plugin directory
                                [$Munin::Common::Defaults::MUNIN_CONFDIR/plugins/]
    --sconfdir <dir>        Override plugin configuration directory
                                [$Munin::Common::Defaults::MUNIN_CONFDIR/plugin-conf.d]
    --libdir <dir>          Override plugin lib
                                [$Munin::Common::Defaults::MUNIN_LIBDIR/plugins/]

    --families <family,...> Override families ($all_families) [$default_families]
    --suggest               Show suggestions instead of status
    --shell                 Show shell commands (implies --suggest)
    --exitnoterror          Do not consider non-zero exit-value as error
    --remove-also           Also show rm-commands when doing --shell

    --newer <version>       Only show suggestions related to plugins
                            included more recently than version <version>.
                            Only supported along with --shell.

    --snmp <host|cidr>      Do SNMP probing on the host or CIDR network
                            (e.g. "192.168.1.0/24"). This may take some
                            time, especially if the probe includes many
                            hosts. This option can be specified multiple
                            times, or once with a comma-separated list, to
                            include more than one host/CIDR.

    --snmpversion <ver>     Set SNMP version (1, 2c or 3) [2c]
    --snmpcommunity <comm>  Set SNMP community string [public]
    --snmpport <port>       Set SNMP port [161]

By default this program shows which plugins are activated on the system.

If you specify --suggest, it will present a table of plugins that will
probably work (according to the plugins' autoconf command).

If you specify --shell, shell commands to install those same plugins
will be printed. These can be reviewed or piped directly into a shell to
install the plugins.

};

	exit 0;
}


sub print_version_and_exit
{
	print qq{munin-node-configure (munin-node) version $Munin::Common::Defaults::MUNIN_VERSION.
Written by Jimmy Olsen

Copyright (C) 2003-2006 Jimmy Olsen

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
};

	exit 0;
}


sub print_table_entry
{
	printf "%-26s | %-4s | %-39s\n", @_;
	return;
}


### Reporting current status ###################################################

# For each available plugin, prints a line detailing whether or not it's
# installed, and (if it's a wildcard plugin) what identities are currently
# in use
sub print_status_list
{
	my ($plugins) = @_;

	print_table_entry("Plugin", "Used", "Extra information");
	print_table_entry("------", "----", "-----------------");

	foreach my $plugin ($plugins->list) {
        print_table_entry(
            $plugin->{name},
            $plugin->is_installed,
            $plugin->installed_services_string
        );
	}

	return;
}


### Reporting and managing suggestions #########################################

# Asks each available autoconf plugin whether or not it should be installed,
# and (if it's a wildcard plugin) its suggested profiles.
sub gather_suggestions
{
	my ($plugins) = @_;

	# We're going to be running plugins
    Munin::Node::Service->prepare_plugin_environment($plugins->names);

	foreach my $plugin ($plugins->list) {
		fetch_plugin_autoconf($plugin);
        fetch_plugin_suggestions($plugin);
	}

	return;
}


# Prints out the tabular representation of the suggestion
sub show_suggestions
{
	my ($plugins) = @_;

    print_table_entry("Plugin", "Used", "Suggestions");
    print_table_entry("------", "----", "-----------");

    foreach my $plugin ($plugins->list) {
        print_table_entry(
            $plugin->{name},
            $plugin->is_installed,
            $plugin->suggestion_string
        );
    }
	return;
}


# prints shell commands to get the system into the recommended
# state by adding or removing symlinks
sub manage_links
{
    my ($plugins) = @_;

    foreach my $plugin ($plugins->list) {
        link_add($plugin->{path}, $_) foreach $plugin->services_to_add;
        if ($config->{remove_also}) {
            link_remove($_) foreach $plugin->services_to_remove;
        }
    }
    return;
}


# Prints a shell-command to remove a given symlink from the servicedir
sub link_remove
{
    my ($service) = @_;
    return unless (-l "$config->{servicedir}/$service"); # Strange...
    print "rm -f '$config->{servicedir}/$service'\n";
	return;
}


# Prints a shell-command to add a symlink called $service pointing to
# the plugin.
sub link_add
{
	my ($plugin, $service) = @_;
    print "ln -s '$plugin' '$config->{servicedir}/$service'\n";
	return;
}


### SNMP probing ###############################################################

# Prepares for SNMP probing
sub init_snmp
{
    my ($plugins) = @_;

	unless (eval { require Munin::Node::SNMPConfig; }) {
        die "# ERROR: Cannot perform SNMP probing since Munin::Node::SNMPConfig module is not available.\n",
            $@;
	}

    Munin::Node::Service->prepare_plugin_environment($plugins->names);

    fetch_plugin_snmpconf($_) foreach ($plugins->list);

    return Munin::Node::SNMPConfig->new(
        hosts      => $config->{snmp},
        community  => $config->{snmp_community},
        version    => $config->{snmp_version},
        port       => $config->{snmp_port},
    );
}


### Running plugins and analysing responses ####################################

# Runs the plugin with argument $mode (eg. 'suggest', 'autoconf') and runs
# tests on the results.  Assuming no errors were detected, returns a list
# of the lines printed to STDOUT, with any debug output removed.
sub run_plugin
{
    my ($plugin, $mode) = @_;
    my $name = $plugin->{name};

	DEBUG("Running '$mode' on $name" );
	my $res = Munin::Node::Service->fork_service($config->{libdir},
	                                             $name, $mode);

	# No if it timed out
	if ($res->{timed_out}) {
        $plugin->log_error("Timed out during $mode");
		return;
	}
	elsif ($res->{retval}) {
		# Non-zero exit is an immediate fail
		my $plugin_exit   = $res->{retval} >> 8;
		my $plugin_signal = $res->{retval} & 127;

		# Definitely a bad sign
		if ($plugin_signal) {
            $plugin->log_error("Died with signal $plugin_signal during $mode");
			return;
		}
		elsif ($plugin_exit) {
            $plugin->log_error("Non-zero exit during $mode ($plugin_exit)");

			# Verboten according to the specification, but lots of plugins
            # still exit(1) it during autoconf.  So making it a non-fatal error
			# for the time being
			return unless ($mode eq 'autoconf'
			           and $plugin_exit == 1
			           and $config->{exit_not_error});
		}
	}

	# No if there is anything on stderr that's not debug
	if (grep !/^#/, @{ $res->{stderr} }) {
        $plugin->log_error("Junk printed to stderr");
		# FIXME: log the other output
		return;
	}

    # Ignore debug output
	my @response = grep !/^#/, @{ $res->{stdout} };
    $plugin->log_error('Nothing printed to stdout') unless (scalar @response);

    return @response;
}


# Runs the given plugin, and records whether it thinks it should be installed.
# Sets the 'default' and 'defaultreason' fields
sub fetch_plugin_autoconf
{
	my ($plugin) = @_;

    return unless ($plugin->{capabilities}{autoconf});

    my @response = run_plugin($plugin, 'autoconf') or return;
	return $plugin->parse_autoconf_response(@response);
}


# Runs the given wildcard plugin and saves a list of suggested profiles
# in the 'suggestions' field
sub fetch_plugin_suggestions
{
	my ($plugin) = @_;

	# Only run if the autoconf gave the go-ahead
	return unless ($plugin->{default} eq "yes");

    return unless ($plugin->{capabilities}{suggest});

    my @suggested = run_plugin($plugin, 'suggest');
	return $plugin->parse_suggest_response(@suggested);
}


# Runs a given snmpconf-capable plugin, and notes the parameters it returns
sub fetch_plugin_snmpconf
{
	my ($plugin) = @_;

    return unless ($plugin->{capabilities}{snmpconf});

    my @response = run_plugin($plugin, 'snmpconf');
	return $plugin->parse_snmpconf_response(@response);
}


### Debugging and error reporting ##############################################

sub list_errors
{
    my @error_list;
    foreach my $plugin (@_) {
        if (my @errors = @{$plugin->{errors}}) {
            push @error_list, "$plugin->{name}:";
            push @error_list, map { "\t$_" } @errors;
        }
    }
    return @error_list;
}


exit main() unless caller;


1;

__END__

=head1 NAME

munin-node-configure - View and modify which plugins are enabled.

=head1 SYNOPSIS

munin-node-configure [options]

=head1 OPTIONS

=over 5

=item B<< --help >>

View this help page

=item B<< --version >>

Show version information

=item B<< --debug >>

Print debug information (very verbose).  All debugging output is
printed on STDOUT but each line is prefixed with '#'.  Only errors are
printed on STDERR.

=item B<< --config <file> >>

Override configuration file [@@CONFDIR@@/munin-node.conf]

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

Override plugin dir [@@CONFDIR@@/plugins/]

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

Override plugin configuration directory [@@CONFDIR@@/plugin-conf.d/]

=item B<< --libdir <dir> >>

Override plugin lib [@@LIBDIR@@/plugins/]

=item B<< --families <family,...> >>

Override families (auto, manual, contrib) [auto]

=item B<< --suggest >>

Show suggestions instead of status

=item B<< --shell >>

Show shell commands instead of a table (implies --suggest unless --snmp was
also provided)

=item B<< --exitnoterror >>

Do not consider non-zero exit-value as error

=item B<< --remove-also >>

Also show rm-commands when doing --shell

=item B<< --newer <version> >>

Only show suggestions related to plugins included more recently than
version <version>.

=item B<< --snmp <host|cidr,...> >>

Do SNMP probing on the host or CIDR network (e.g. "192.168.1.0/24"). This may
take some time, especially if the probe includes many hosts. This option can
be specified multiple times, or as a comma-separated list, to include more
than one host/CIDR.

=item B<< --snmpversion <ver> >>

Set the SNMP version (1, 2c or 3) to use when probing. Default is "2c".

=item B<< --snmpcommunity <comm> >>

Set SNMP community string to use when probing. Default is "public".

=item B<< --snmpport <port> >>

Set SNMP port. Default is "161".

=back

=head1 DESCRIPTION

munin-node-configure is a script to show which plugins are currently enabled
on the current node.  It can also suggest changes to this list.

=head1 FILES

    @@CONFDIR@@/munin-node.conf
    @@CONFDIR@@/plugin-conf.d/*
    @@CONFDIR@@/plugins/*
    @@LIBDIR@@/plugins/plugins.history
    @@LIBDIR@@/plugins/*

=head1 VERSION

This is munin-node-configure v@@VERSION@@.

=head1 AUTHORS

Jimmy Olsen, Nicolai Langfeldt

=head1 BUGS

Please see L<http://munin.projects.linpro.no/report/1>.

=head1 COPYRIGHT

Copyright (C) 2003-2006 Jimmy Olsen, Nicolai Langfeldt.

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: sw=4 : ts=4 : expandtab
