#!/usr/bin/perl =head1 Explore Nagios This is a frameset implementation of the Nagios data visualiser written by Martin Houston. =cut use CGI::Pretty qw{-newstyle-urls :standard *table}; use CGI::Carp qw(fatalsToBrowser); my $query = new CGI; my $script_name = $query->script_name; my $path_info = $query->path_info; # where we find the main tools my $sl = '/usr/local/share/nagios_scripts'; =head1 frameset This is not part of the CGI library so we implement it here. Parameters are row or col, splits in the form "20%,*" an arrayref of the titles of each of the frames =cut sub frameset { my $res = ''; my($query,$roworcol,$splits,$tags) = @_; die "second parameter need to be rows or cols" unless $roworcol =~ /^(rows|cols)$/; my $script_name = $query->script_name; $res .= qq{\n}; map { $res .= qq{\n} } @{$tags}; $res .= qq{}; return $res; } =head1 raw_head Like start_html but for use with framesets , just prints a head alone. Pass it a query object then all the args that you would have passed to start_html. We use start_html, pass it all our parameters but just snip off the =cut sub raw_head { my $query = shift; my $res = $query->start_html(@_); $res =~ s/$//; return $res; } =head1 main program Create the frameset if no path info and either render menu choices or render graph depending on given path info. =cut if(!$path_info) { print $query->header; print raw_head($query,"Explore Nagios"); print frameset($query,'cols','15%,*',[qw{menu output}]); exit 0; # our work is done - get re-invoked with more path info. } if($path_info =~ /output/) { if(!defined param('base')) { print header, start_html(), h1('Please choose what to show from the menu on the left...'); print qq{

This data represents the nagios configuration parsed from /usr/local/nagios/etc on this machine. 

It is parsed every time fresh so you will see any changes that you make to the data reflected right away.

In the menu on the left click the Debug mode checkbox if you want to see the graphvis input code and clear it
if you want to actually draw the SVG graph in this window.

The Detail mode radio button selects if we show ALL the contents of each configuration item, not just the names. 

This makes the graph much much larger so only turn this on once you are happy withthe size of a non detailed graph.

The rest of the pick lists are the objects that we see in the Nagios configuration. Either select the ones you are
interested in - multiple choices allowed.

OR enter a Perl regular expression into the "Custom regex" box.

If anything is in this box it takes precidence.

What happens when you press the Submit Query button is that this script attempts to draw a directed graph of the 
relationships between the different objects in the Nagios config.

This is intended as an aid to developing and maintaining Nagios configurations as the graph mode allows you to see the relationshps
between large numbers of objects e.g. to spot objects that are not assigned into the hierachy they should be in.

In the detailed mode the greyed out fields of objects are those fields that have been inherrited from another object by using the 
use feature.

In non detailed mode clicking on the name of any object will redraw the graph as if you entered a choice of just that object.

You can drag the boundary between the menu and this work area to suit.

If you want to pan around the svg graph click the middle mouse button. Your mouse will control panning within the window until
you click the button again.

As well as panning the whole graph can be scaled by using Ctrl and either the - or + signs on the numeric pad. It can also be saved - as
it is pure SVG data, so can be kept and viewed offline in a SVG data viewer/editor of your choice.

Ctrl and the mouse wheel also work to zoom the graph in and out.

We attempt to show anything that is related to the object that you select. This is still a work in progress and seems to work better for 
some sorts of relationships than others e.g. hosts <--> hostgroups seems to work well.

The graph will allways contain a legend box that gives the colour codings for the various relationships between objects being 
displayed.

If you want to get this help screen back again simply reload the whole web page.

BEWARE - this can produce very large svg files if much data is selected. You will need a fast PC with plenty of memory
to view them!

The core of this is a pair of command line tools in /usr/local/share/nagios_tools called nagparse and graphit.

Nagparse parses the nagios config and graphit produces input data for the dot program.

You can also run these on the command line to produce individual svg files. The purpose of this web front end is 
simply to make producing a graph of the relationships between specific elements easy.

Beware that there seem to be some bugs with graphvis that mean it is no longer possible to show the whole graph.

Get the message: Error: trouble in init_rank - no known solution at this time. Workaround is to pick subsets only.
}; print end_html; } else { my $errormsg = ''; my $pat = ''; # need to make up pat from other params # first and foremost - is this a pass through? if(defined $query->param('pat') && $query->param('pat') ne '') { # do nothing } elsif(defined $query->param('regex') && $query->param('regex') ne '') { # we assume the user knows what they are doing # compile it and if correct just pass this $@ = ''; eval {qr{$query->param('regex')}}; if($@ eq '') { # we get our pat parameter direct $query->param(-name=>'pat',-values=>[$query->param('regex')]); } else { $errormsg = "$_ bad pattern: $@"; } } else { my @elems = (); # we need to make up a regex out of our other parameters for my $p($query->param()) { next if $p =~ /^(base|regex|pat|debug|detail)$/; if($query->param($p)) { next if $query->param($p) eq ''; # if we select the empty value it counts as unselected map { push @elems, "$p:$_"} $query->param($p); } } # must do lots or regular expressions, not one big one # s/^/\^/ for @elems; # anchor # we get our pat parameter direct $query->param(-name=>'pat',-values=>[@elems]); } if($errormsg ne '' || defined param('debug')) { print header(-type => 'text/plain', -expires=>'now'); } else { # NOT html - pure svg output in this frame! print header(-type => 'image/svg+xml', -expires=>'now'); } my $base = $query->param('base') || '/usr/local/nagios/etc'; # fetch back what we set my (@pat) = $query->param('pat'); $_ = "'$_'" for @pat; # we have one troublesome service name NSClient++! s/\+/\\\+/g for @pat; my $sl = '/usr/local/share/nagios_scripts'; my $envoptions = ''; if(defined $query->param('detail') && $query->param('detail') eq 'true') { # control the separate graphit program $envoptions = "DETAIL=1"; } if($errormsg ne '' || (defined $query->param('debug') && $query->param('debug') ne '')) { if($errormsg ne '') { print $errormsg; } else { print "\n"; print "# Debug output - toggle the debug flag off to see actual graph (Note you can use this format with Twiki GraphVis plugin.")\n"; print "# Params\n"; for($query->param()) { print "# $_ = " . join(', ',$query->param($_)) . "\n"; } print '#', '-' x 80, "\n"; print "# Pattern used: " . join(' | ', @pat) . "\n"; print '#', '-' x 80, "\n"; system("$sl/nagparse $base | $envoptions $sl/graphit @pat 2>&1 "); print "\n"; } } else { # go plot the graph system("$sl/nagparse $base | $envoptions $sl/graphit @pat | dot -Tsvg"); } } } elsif($path_info =~ /menu/) { my $config; my $base = $query->param('base') || '/usr/local/nagios/etc'; my $c = join('',qx{$sl/nagparse $base}); $config = eval $c; my ($byclass,$files) = (analconfig($config)); my $file_labels = {}; # strip the first bit out of what is shown as it is always the same map { $file_labels->{$_} = $_; $file_labels->{$_} =~ s{^$base/}{}; } keys %{$files}; print header,start_html(), h1('Menu for nagios configuration graph'); print p("There are " . scalar(keys %$config) . " config items seen at $base"); print start_form(-action=>"$script_name/output", -target=>"output", -method=>'POST'); # this in fact hard coded - needs to be fixed, but for now do not offer it as a choice! print hidden(-name=>'base', -default=>$base); print start_table(); print TR(td({-colspan=>2}, $query->submit())); print TR(th('Debug mode (no graph drawing) - but can be used to save the DOT code to enter into Twiki.'), td($query->checkbox_group(-name=>'debug',-values=>['true'],-defaults=>['true']))); print TR(th('Detail mode (show ALL fields) - grey fields are those inherried by use'), td($query->radio_group(-name=>'detail',-values=>['true','false'],-defaults=>['false']))); for my $class (sort keys %{$byclass}) { my @values = ('',sort keys %{$byclass->{$class}}); my $sensible = (scalar(@values) > 7 ? 7 : 3); print TR(th(ucfirst $class),td($query->scrolling_list(-name=>$class, -values=>\@values, -size=>$sensible, -multiple=>'true'))); } print TR(th('Source File'), td($query->popup_menu(-name=>'file', -labels=>$file_labels, -values=>[sort keys %{$files}], -size=>10, -multiple=>'true'))); print TR(th(b('OR') . ' Custom regex'),td($query->textfield(-name=>'regex'))); print TR(td({-colspan=>2}, $query->submit())); print end_table(); print end_form(); print end_html; } =head1 deref Tricky code for deciding on if an object is derived via use or directly defined in that there is an extra ref for use derived objects. What we do to mark such tings is put a one element hash, the key of which is where the value is derived and the value is what we otherwise use. If the $suppress arg is defined and we are a ref instead return undef, or else it messes up our object identification! =cut sub deref { my($x,$suppress) = @_; if(ref($x) =~ /^HASH/) { # this shows it is inherrited return undef if defined $suppress; for(keys %{$x}) { # warn " $_ = ", $x->{$_}; return (wantarray ? ($x->{$_},$_) : $x->{$_}); } } else { return (wantarray ? ($x,undef) : $x); } } =head1 get_ident Nagios does not have a universal identifier that is globally unique across all object types. This code is object type specific and makes up our best guess at what is going to be a suitable identifier both for our referring to the object on the graph, and for fornding relationships between objects. The identifier is prefixed with the object type as otherwise we would have big namespace collision issues. There may be some of the minior object types still missing. Need to be added as required. If we are evaluated in array context we add a second value which is true if the dereference was needed. We suppress any use following for this - it would be bad! Rather have undef. =cut sub get_ident { my $obj = shift; return "Unknown - no TYPE" unless defined $obj->{TYPE}; if($obj->{TYPE} eq 'host') { return "host:" . (scalar(deref($obj->{name},1)) || scalar(deref($obj->{host_name},1)) || scalar(deref($obj->{address},1)) || "UNKNOWN host"); } elsif($obj->{TYPE} eq 'hostgroup') { return "hostgroup:" . (scalar(deref($obj->{hostgroup_name},1)) || scalar(deref($obj->{alias},1)) || "UNKNOWN hostgroup"); } elsif($obj->{TYPE} eq 'service') { return "service:" . (scalar(deref($obj->{service_description},1)) || scalar(deref($obj->{alias},1)) || "UNKNOWN service"); } elsif($obj->{TYPE} eq 'servicegroup') { return "servicegroup:" . (scalar(deref($obj->{servicegroup_name},1)) || scalar(deref($obj->{alias},1)) || "UNKNOWN servicegroup"); } elsif($obj->{TYPE} eq 'timeperiod') { return "timeperiod:" . (scalar(deref($obj->{timeperiod_name},1)) || scalar(deref($obj->{alias},1)) || "UNKNOWN timeperiod"); } elsif($obj->{TYPE} eq 'command') { return "command:" . (scalar(deref($obj->{command_name},1)) || "UNKNOWN command"); } elsif($obj->{TYPE} eq 'contact') { return "contact:" . (scalar(deref($obj->{contact_name},1)) || scalar(deref($obj->{alias},1)) || "UNKNOWN contact"); } elsif($obj->{TYPE} eq 'contactgroup') { return "contactgroup:" . (scalar(deref($obj->{contactgroup_name},1)) || scalar(deref($obj->{alias},1)) || "UNKNOWN contactgroup"); } else { # dont know how to deal with the rest yet - easy enough return $obj->{TYPE} . ":UNKNOWN!"; } } sub analconfig { my $config = shift; my $invconf = {}; my $files = {}; for my $ob (keys %{$config}) { my ($f,$n) = split(/:/,$ob); $files->{$f} = ''; # just record the files we have seen my $id = get_ident($config->{$ob}); my($type,$label) = split(':',$id,2); $invconf->{$type} = {} unless defined $invconf->{$type}; $invconf->{$type}->{$label} = $ob; # so can pick rest up } return ($invconf,$files); }