#!/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.
#

use strict;
use warnings;

use Getopt::Long;

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 @all_families     = qw/auto manual contrib/;
my @default_families = qw/auto/;


sub main
{
	parse_args();

    my $plugins = $config->{plugins};
    $plugins->load();

    # Gather information
    if ($config->{snmp}) {
        $plugins->{library}->prepare_plugin_environment($plugins->names);
        fetch_plugin_snmpconf($plugins, $_) foreach ($plugins->list);

        $config->{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", @errors;
        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 = 0;
	my $pidebug = 0;
	my ($suggest, $shell, $removes, $newer);
	my $exit_not_error = 1;
	my @families;
	my (@snmp_hosts, $snmpver, $snmpcomm, $snmpport);
    my ($snmp3username, $snmp3authpass, $snmp3authproto, $snmp3privpass, $snmp3privproto);

    print_usage_and_exit() unless GetOptions(
        'help'            => \&print_usage_and_exit,
        'version'         => \&print_version_and_exit,

        'suggest!'        => \$suggest,
        'exitnoterror!'   => \$exit_not_error,
        'newer=s'         => \$newer,

        'shell!'          => \$shell,
        'remove-also!'    => \$removes,

        'debug!'          => \$debug,
        'pidebug!'        => \$pidebug,

        # paths
        'config=s'        => \$conffile,
        'servicedir=s'    => \$servicedir,
        'sconfdir=s'      => \$sconfdir,
        'libdir=s'        => \$libdir,
        'families=s'      => \@families,

        # SNMP
        'snmp=s'          => \@snmp_hosts,
        'snmpversion=s'   => \$snmpver,
        'snmpport=i'      => \$snmpport,
        # SNMPv1/2c
        'snmpcommunity=s' => \$snmpcomm,
        # SNMPv3
        'snmpusername=s'  => \$snmp3username,
        'snmpauthpass=s'  => \$snmp3authpass,
        'snmpauthproto=s' => \$snmp3authproto,
        'snmpprivpass=s'  => \$snmp3privpass,
        'snmpprivproto=s' => \$snmp3privproto,
    );

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

    my $snmp = init_snmp(
        hosts        => \@snmp_hosts,
        version      => $snmpver,
        port         => $snmpport,

        community    => $snmpcomm,

        username     => $snmp3username,
        authpassword => $snmp3authpass,
        authprotocol => $snmp3authproto,
        privpassword => $snmp3privpass,
        privprotocol => $snmp3privproto,
    ) if @snmp_hosts;

    my $plugins = Munin::Node::Configure::PluginList->new(
        libdir     => $libdir,
        servicedir => $servicedir,

        pidebug    => $pidebug,

        families   => \@families,
        newer      => $newer,
    );

    $config->reinitialize({
        %$config,

        exit_not_error => $exit_not_error,
        suggest        => $suggest,

        shell          => $shell,
        remove_also    => $removes,

        snmp => $snmp,

	plugins => $plugins,

        conffile   => $conffile,
        servicedir => $servicedir,  # needed for creating/removing links
        sconfdir   => $sconfdir,

        DEBUG   => $debug,
    });

    return;
}


sub print_usage_and_exit
{
    require Pod::Usage;
    Pod::Usage::pod2usage(-verbose => 1);
}


sub print_version_and_exit
{
    require Pod::Usage;
    Pod::Usage::pod2usage(
        -verbose => 99,
        -sections => 'VERSION|COPYRIGHT',
    );
}


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
    $plugins->{library}->prepare_plugin_environment($plugins->names);

    foreach my $plugin ($plugins->list) {
        fetch_plugin_autoconf($plugins, $plugin);
        fetch_plugin_suggestions($plugins, $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 ###############################################################

sub init_snmp
{
    unless (eval { require Munin::Node::SNMPConfig; }) {
        die "# ERROR: Cannot perform SNMP probing as Munin::Node::SNMPConfig module is not available.\n",
            $@;
    }
    return Munin::Node::SNMPConfig->new(@_);
}


### 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 ($plugins, $plugin, $mode) = @_;
    my $name = $plugin->{name};

	DEBUG("Running '$mode' on $name" );
    my $res = $plugins->{library}->fork_service($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 (my @junk = grep !/^#/, @{ $res->{stderr} }) {
	       $plugin->log_error("Junk printed to stderr");
	       DEBUG("Junk printed to stderr: @junk");
	       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 ($plugins, $plugin) = @_;

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

    my @response = run_plugin($plugins, $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 ($plugins, $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($plugins, $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 ($plugins, $plugin) = @_;

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

    my @response = run_plugin($plugins, $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}:\n";
            push @error_list, map { "# \t$_\n" } @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 DESCRIPTION

B<munin-node-configure> reports which plugins are enabled on the current node,
and suggest changes to this list.

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

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

If you specify C<--snmp>, followed by a list of hosts, it will present a table
of SNMP plugins that they support.

If you additionally specify C<--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.


=head1 GENERAL OPTIONS

=over 4

=item B<< --help >>

Show this help page.

=item B<< --version >>

Show version information.

=item B<< --debug >>

Print debug information on the operations of C<munin-node-configure>.  This can
be very verbose.

All debugging output is printed to STDOUT, and each line is prefixed with '#'.
Only errors are printed to STDERR.

=item B<< --pidebug >>

Plugin debug.  Sets the environment variable MUNIN_DEBUG to 1 so that plugins
may enable debugging.

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

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

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

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

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

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

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

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

=item B<< --exitnoterror >>

Do not consider plugins that exit non-zero exit-value as error.

=item B<< --suggest >>

Suggest plugins that might be added or removed, instead of those that are
currently enabled.

=back


=head2 OUTPUT OPTIONS

By default, C<munin-node-configure> will print out a table summarising the
results.

=over 4

=item B<< --shell >>

Instead of a table, print shell commands to install the new plugin suggestions.

This implies C<--suggest>, unless C<--snmp> was also enabled.  By default, it
will not attempt to remove any plugins.

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

When C<--shell> is enabled, also provide commands to remove plugins that are no
longer applicable from the service directory.

=back


=head2 PLUGIN SELECTION OPTIONS

=over 4

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

Override the list of families that will be used (auto, manual, contrib,
snmpauto).  Multiple families can be specified as a comma-separated list, by
repeating the C<--families> option, or as a combination of the two.

When listing installed plugins, the default families are 'auto', 'manual' and
'contrib'.  Only 'auto' plugins are checked for suggestions.  SNMP probing is only
performed on 'snmpauto' plugins.

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

Only consider plugins added to the Munin core since <version>.  This option is
useful when upgrading, since it can prevent plugins that have been manually
removed from being reinstalled.  This only applies to plugins in the 'auto'
family.

=back

=head2 SNMP Options

=over 4

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

Probe the SNMP agents on the host or CIDR network (e.g. "192.168.1.0/24"), to
see what plugins they support. This may take some time, especially if the many
hosts are specified.

This option can be specified multiple times, or as a comma-separated list, to
include more than one host/CIDR.

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

The SNMP version (1, 2c or 3) to use. ['2c']

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

The SNMP port to use [161]

=item B<SNMP 1/2c authentication>

SNMP versions 1 and 2c use a "community string" for authentication.  This is
a shared password, sent in plaintext over the network.

=item B<< --snmpcommunity <string> >>

The community string for version 1 and 2c agents.  ['public'] (If this works
your device is probably very insecure and needs a security checkup).

=item B<SNMP 3 authentication>

SNMP v3 has three security levels. Lowest is C<noAuthNoPriv>, which provides
neither authentication nor encryption.  If a username and C<authpassword> are
given it goes up to C<authNoPriv>, and the connection is authenticated.  If
C<privpassword> is also given the security level becomes C<authPriv>, and the
connection is authenticated and encrypted.

B<Note>: Encryption can slow down slow or heavily loaded network devices.  For
most uses C<authNoPriv> will be secure enough -- the password is sent over the
network encrypted in any case.

ContextEngineIDs are not (yet) supported.

For further reading on SNMP v3 security models please consult RFC3414 and the
documentation for L<Net::SNMP>.

=item B<< --snmpusername <name> >>

Username.  There is no default.

=item B<< --snmpauthpassword <password> >>

Authentication password.  Optional when encryption is also enabled, in which
case defaults to the privacy password (C<--snmpprivpassword>).

=item B<< --snmpauthprotocol <protocol> >>

Authentication protocol.  One of 'md5' or 'sha' (HMAC-MD5-96, RFC1321 and
SHA-1/HMAC-SHA-96, NIST FIPS PIB 180, RFC2264).  ['md5']

=item B<< --snmpprivpassword <password> >>

Privacy password to enable encryption.  There is no default.  An empty ('')
password is considered as no password and will not enable encryption.

Privacy requires a privprotocol as well as an authprotocol and a authpassword,
but all of these are defaulted (to 'des', 'md5', and the privpassword value,
respectively) and may therefore be left unspecified.

=item B<< --snmpprivprotocol <protocol> >>

If the privpassword is set this setting controls what kind of encryption is
used to achieve privacy in the session.  Only the very weak 'des' encryption
method is supported officially.  ['des']

munin-node-configure also supports '3des' (CBC-3DES-EDE, aka Triple-DES, NIST
FIPS 46-3) as specified in IETF draft-reeder-snmpv3-usm-3desede.  Whether or
not this works with any particular device, we do not know.

=back

=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 (munin-node) v@@VERSION@@.

$Id$

=head1 AUTHORS

Jimmy Olsen, Nicolai Langfeldt, Matthew Boyle

=head1 BUGS

Please see L<http://munin-monitoring.org/report/1>.

=head1 COPYRIGHT

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

Copyright (C) 2009-2010 Matthew Boyle

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
