480 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
			
		
		
	
	
			480 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Perl
		
	
	
	
	
	
| package App::Netdisco::Util::Graph;
 | |
| 
 | |
| use App::Netdisco;
 | |
| 
 | |
| use Dancer qw/:syntax :script/;
 | |
| use Dancer::Plugin::DBIC 'schema';
 | |
| 
 | |
| use App::Netdisco::Util::DNS qw/hostname_from_ip ipv4_from_hostname/;
 | |
| use Graph::Undirected ();
 | |
| use GraphViz ();
 | |
| 
 | |
| use base 'Exporter';
 | |
| our @EXPORT = ('graph');
 | |
| our @EXPORT_OK = qw/
 | |
|   graph_each
 | |
|   graph_addnode
 | |
|   make_graph
 | |
| /;
 | |
| our %EXPORT_TAGS = (all => \@EXPORT_OK);
 | |
| 
 | |
| # nothing to see here, please move along...
 | |
| our ($ip, $label, $isdev, $devloc, %GRAPH, %GRAPH_SPEED);
 | |
| 
 | |
| =head1 NAME
 | |
| 
 | |
| App::Netdisco::Util::Graph
 | |
| 
 | |
| =head1 SYNOPSIS
 | |
| 
 | |
|  $ brew install graphviz   <-- install graphviz on your system
 | |
|  
 | |
|  $ ~/bin/localenv bash
 | |
|  $ cpanm --notest Graph GraphViz
 | |
|  $ mkdir ~/graph
 | |
|  
 | |
|  use App::Netdisco::Util::Graph;
 | |
|  graph;
 | |
| 
 | |
| =head1 DESCRIPTION
 | |
| 
 | |
| Generate GraphViz output from Netdisco data. Requires that the L<Graph> and
 | |
| L<GraphViz> distributions be installed.
 | |
| 
 | |
| Requires the same config as for Netdisco 1, but within a C<graph> key.  See
 | |
| C<share/config.yml> in the source distribution for an example.
 | |
| 
 | |
| The C<graph> subroutine is exported by default. The C<:all> tag will export
 | |
| all subroutines.
 | |
| 
 | |
| =head1 EXPORT
 | |
| 
 | |
| =over 4
 | |
| 
 | |
| =item graph()
 | |
| 
 | |
| Creates netmap of network.
 | |
| 
 | |
| =back
 | |
| 
 | |
| =cut
 | |
| 
 | |
| sub graph {
 | |
|     my %CONFIG = %{ setting('graph') };
 | |
| 
 | |
|     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime();
 | |
|     my $month = sprintf("%d%02d",$year+1900,$mon+1);
 | |
| 
 | |
|     info "graph() - Creating Graphs";
 | |
|     my $G = make_graph();
 | |
| 
 | |
|     unless (defined $G){
 | |
|         print "graph() - make_graph() failed.  Try running with debug (-D).\n";
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     my @S = $G->connected_components;
 | |
| 
 | |
|     # Count number of nodes in each subgraph
 | |
|     my %S_count;
 | |
|     for (my $i=0;$i< scalar @S;$i++){
 | |
|         $S_count{$i} = scalar @{$S[$i]};
 | |
|     }
 | |
| 
 | |
|     foreach my $subgraph (sort { $S_count{$b} <=> $S_count{$a} } keys %S_count){
 | |
|         my $SUBG = $G->copy;
 | |
|         print "\$S[$subgraph] has $S_count{$subgraph} nodes.\n";
 | |
| 
 | |
|         # Remove other subgraphs from this one
 | |
|         my %S_notme = %S_count;
 | |
|         delete $S_notme{$subgraph};
 | |
|         foreach my $other (keys %S_notme){
 | |
|             print "Removing Non-connected nodes: ",join(',',@{$S[$other]}),"\n";
 | |
|             $SUBG->delete_vertices(@{$S[$other]})
 | |
|         }
 | |
| 
 | |
|         # Create the subgraph
 | |
|         my $timeout = defined $CONFIG{graph_timeout} ? $CONFIG{graph_timeout} : 60;
 | |
| 
 | |
|         eval {
 | |
|             alarm($timeout*60);
 | |
|             graph_each($SUBG,'');
 | |
|             alarm(0);
 | |
|         };
 | |
|         if ($@) {
 | |
|             if ($@ =~ /timeout/){
 | |
|                 print "! Creating Graph timed out!\n";
 | |
|             } else {
 | |
|                 print "\n$@\n";
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         # Facility to create subgraph for each non-connected network segment.
 | |
|         # Right now, let's just make the biggest one only.
 | |
|         last;
 | |
|     }
 | |
| }
 | |
| 
 | |
| =head1 EXPORT_OK
 | |
| 
 | |
| =over 4
 | |
| 
 | |
| =item graph_each($graph_obj, $name)
 | |
| 
 | |
| Generates subgraph. Does actual GraphViz calls.
 | |
| 
 | |
| =cut
 | |
| 
 | |
| sub graph_each  {
 | |
|     my ($G, $name) = @_;
 | |
|     my %CONFIG = %{ setting('graph') };
 | |
|     info "Creating new Graph";
 | |
| 
 | |
|     my $graph_defs = {
 | |
|                      'bgcolor' => $CONFIG{graph_bg}         || 'black',
 | |
|                      'color'   => $CONFIG{graph_color}      || 'white',
 | |
|                      'overlap' => $CONFIG{graph_overlap}    || 'scale',
 | |
|                      'fontpath'=> _homepath('graph_fontpath',''),
 | |
|                      'ranksep' => $CONFIG{graph_ranksep}    || 0.3,
 | |
|                      'nodesep' => $CONFIG{graph_nodesep}    || 2,
 | |
|                      'ratio'   => $CONFIG{graph_ratio}      || 'compress',
 | |
|                      'splines' => ($CONFIG{graph_splines} ? 'true' : 'false'),
 | |
|                      'fontcolor' => $CONFIG{node_fontcolor} || 'white',
 | |
|                      'fontname'  => $CONFIG{node_font}      || 'lucon',
 | |
|                      'fontsize'  => $CONFIG{node_fontsize}  || 12,
 | |
|                      };
 | |
|     my $edge_defs  = {
 | |
|                      'color' => $CONFIG{edge_color}         || 'wheat',
 | |
|                      };
 | |
|     my $node_defs  = {
 | |
|                      'shape'     => $CONFIG{node_shape}     || 'box',
 | |
|                      'fillcolor' => $CONFIG{node_fillcolor} || 'dimgrey',
 | |
|                      'fontcolor' => $CONFIG{node_fontcolor} || 'white',
 | |
|                      'style'     => $CONFIG{node_style}     || 'filled',
 | |
|                      'fontname'  => $CONFIG{node_font}      || 'lucon',
 | |
|                      'fontsize'  => $CONFIG{node_fontsize}  || 12,
 | |
|                      'fixedsize' => ($CONFIG{node_fixedsize} ? 'true' : 'false'),
 | |
|                      };
 | |
|     $node_defs->{height} = $CONFIG{node_height} if defined $CONFIG{node_height};
 | |
|     $node_defs->{width}  = $CONFIG{node_width}  if defined $CONFIG{node_width};
 | |
| 
 | |
|     my $epsilon = undef;
 | |
|     if (defined $CONFIG{graph_epsilon}){
 | |
|         $epsilon = "0." . '0' x $CONFIG{graph_epsilon} . '1';
 | |
|     }
 | |
| 
 | |
|     my %gv = (
 | |
|                directed => 0,
 | |
|                layout   => $CONFIG{graph_layout} || 'twopi',
 | |
|                graph    => $graph_defs,
 | |
|                node     => $node_defs,
 | |
|                edge     => $edge_defs,
 | |
|                width    => $CONFIG{graph_x}      || 30,
 | |
|                height   => $CONFIG{graph_y}      || 30,
 | |
|                epsilon  => $epsilon,
 | |
|               );
 | |
| 
 | |
|     my $gv = GraphViz->new(%gv);
 | |
| 
 | |
|     my %node_map = ();
 | |
|     my @nodes = $G->vertices;
 | |
| 
 | |
|     foreach my $dev (@nodes){
 | |
|         my $node_name = graph_addnode($gv,$dev);
 | |
|         $node_map{$dev} = $node_name;
 | |
|     }
 | |
| 
 | |
|     my $root_ip = defined $CONFIG{root_device}
 | |
|       ? (ipv4_from_hostname($CONFIG{root_device}) || $CONFIG{root_device})
 | |
|       : undef;
 | |
| 
 | |
|     if (defined $root_ip and defined $node_map{$root_ip}){
 | |
|         my $gv_root_name = $gv->_quote_name($root_ip);
 | |
|         if (defined $gv_root_name){
 | |
|             $gv->{GRAPH_ATTRS}->{root}=$gv_root_name;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     my @edges = $G->edges;
 | |
| 
 | |
|     while (my $e = shift @edges){
 | |
|         my $link = $e->[0];
 | |
|         my $dest = $e->[1];
 | |
|         my $speed = $GRAPH_SPEED{$link}->{$dest}->{speed};
 | |
| 
 | |
|         if (!defined($speed)) {
 | |
|             info "  ! No link speed for $link -> $dest";
 | |
|             $speed = 0;
 | |
|         }
 | |
| 
 | |
|         my %edge = ();
 | |
|         my $val = ''; my $suffix = '';
 | |
| 
 | |
|         if ($speed =~ /^([\d.]+)\s+([a-z])bps$/i) {
 | |
|             $val = $1; $suffix = $2;
 | |
|         }
 | |
| 
 | |
|         if ( ($suffix eq 'k') or ($speed =~ m/(t1|ds3)/i) ){
 | |
|             $edge{color} = 'green';
 | |
|             $edge{style} = 'dotted';
 | |
|         }
 | |
| 
 | |
|         if ($suffix eq 'M'){
 | |
|             if ($val < 10.0){
 | |
|                 $edge{color} = 'green';
 | |
|                 #$edge{style} = 'dotted';
 | |
|                 $edge{style} = 'dashed';
 | |
|             } elsif ($val < 100.0){
 | |
|                 $edge{color} = '#8b7e66';
 | |
|                 #$edge{style} = 'normal';
 | |
|                 $edge{style} = 'solid';
 | |
|             } else {
 | |
|                 $edge{color} = '#ffe7ba';
 | |
|                 $edge{style} = 'solid';
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if ($suffix eq 'G'){
 | |
|             #$edge{style} = 'bold';
 | |
|             $edge{color} = 'cyan1';
 | |
|         }
 | |
| 
 | |
|         # Add extra styles to edges (mainly for modifying width)
 | |
|         if(defined $CONFIG{edge_style}) {
 | |
|             $edge{style} .= "," . $CONFIG{edge_style};
 | |
|         }
 | |
| 
 | |
|         $gv->add_edge($link => $dest, %edge );
 | |
|     }
 | |
| 
 | |
|     info "Ignore all warnings about node size";
 | |
| 
 | |
|     if (defined $CONFIG{graph_raw} and $CONFIG{graph_raw}){
 | |
|         my $graph_raw = _homepath('graph_raw');
 | |
|         info "  Creating raw graph: $graph_raw";
 | |
|         $gv->as_canon($graph_raw);
 | |
|     }
 | |
| 
 | |
|     if (defined $CONFIG{graph} and $CONFIG{graph}){
 | |
|         my $graph_gif = _homepath('graph');
 | |
|         info "  Creating graph: $graph_gif";
 | |
|         $gv->as_gif($graph_gif);
 | |
|     }
 | |
| 
 | |
|     if (defined $CONFIG{graph_png} and $CONFIG{graph_png}){
 | |
|         my $graph_png = _homepath('graph_png');
 | |
|         info "  Creating png graph: $graph_png";
 | |
|         $gv->as_png($graph_png);
 | |
|     }
 | |
| 
 | |
|     if (defined $CONFIG{graph_map} and $CONFIG{graph_map}){
 | |
|         my $graph_map = _homepath('graph_map');
 | |
|         info "  Creating CMAP : $graph_map";
 | |
|         $gv->as_cmap($graph_map);
 | |
|     }
 | |
| 
 | |
|     if (defined $CONFIG{graph_svg} and $CONFIG{graph_svg}){
 | |
|         my $graph_svg = _homepath('graph_svg');
 | |
|         info "  Creating SVG : $graph_svg";
 | |
|         $gv->as_svg($graph_svg);
 | |
|     }
 | |
| }
 | |
| 
 | |
| =item graph_addnode($graphviz_obj, $node_ip)
 | |
| 
 | |
| Checks for mapping settings in config file and adds node to the GraphViz
 | |
| object.
 | |
| 
 | |
| =cut
 | |
| 
 | |
| sub graph_addnode {
 | |
|     my $gv = shift;
 | |
|     my %CONFIG = %{ setting('graph') };
 | |
|     my %node = ();
 | |
| 
 | |
|     $ip     = shift;
 | |
|     $label  = $GRAPH{$ip}->{dns};
 | |
|     $isdev  = $GRAPH{$ip}->{isdev};
 | |
|     $devloc = $GRAPH{$ip}->{location};
 | |
| 
 | |
|     $label = "($ip)" unless defined $label;
 | |
|     my $domain_suffix = setting('domain_suffix');
 | |
|     $label =~ s/$domain_suffix//;
 | |
|     $node{label} = $label;
 | |
| 
 | |
|     # Dereferencing the scalar by name below
 | |
|     #   requires that the variable be non-lexical (not my)
 | |
|     #   we'll create some local non-lexical versions
 | |
|     #   that will expire at the end of this block
 | |
|     # Node Mappings
 | |
|     foreach my $map (@{ $CONFIG{'node_map'} || [] }){
 | |
|         my ($var, $regex, $attr, $val) = split(':', $map);
 | |
| 
 | |
|         { no strict 'refs';
 | |
|            $var = ${"$var"};
 | |
|         }
 | |
|         next unless defined $var;
 | |
| 
 | |
|         if ($var =~ /$regex/) {
 | |
|             debug "  graph_addnode - Giving node $ip $attr = $val";
 | |
|             $node{$attr} = $val;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     # URL for image maps FIXME for non-root hosting
 | |
|     if ($isdev) {
 | |
|         $node{URL} = "/device?&q=$ip";
 | |
|     }
 | |
|     else {
 | |
|         $node{URL} = "/search?tab=node&q=$ip";
 | |
|         # Overrides any colors given to nodes above. Bug 1094208
 | |
|         $node{fillcolor} = $CONFIG{'node_problem'} || 'red';
 | |
|     }
 | |
| 
 | |
|     if ($CONFIG{'graph_clusters'} && $devloc) {
 | |
|         # This odd construct works around a bug in GraphViz.pm's
 | |
|         # quoting of cluster names.  If it has a name with spaces,
 | |
|         # it'll just quote it, resulting in creating a subgraph name
 | |
|         # of cluster_"location with spaces".  This is an illegal name
 | |
|         # according to the dot grammar, so if the name matches the
 | |
|         # problematic regexp we make GraphViz.pm generate an internal
 | |
|         # name by using a leading space in the name.
 | |
|         #
 | |
|         # This is bug ID 16912 at rt.cpan.org -
 | |
|         # http://rt.cpan.org/NoAuth/Bug.html?id=16912
 | |
|         #
 | |
|         # Another bug, ID 11514, prevents us from using a combination
 | |
|         # of name and label attributes to hide the extra space from
 | |
|         # the user.  However, since it's just a space, hopefully it
 | |
|         # won't be too noticable.
 | |
|         my($loc) = $devloc;
 | |
|         $loc = " " . $loc if ($loc =~ /^[a-zA-Z](\w| )*$/);
 | |
|         $node{cluster} = { name => $loc };
 | |
|     }
 | |
| 
 | |
|     my $rv = $gv->add_node($ip, %node);
 | |
|     return $rv;
 | |
| }
 | |
| 
 | |
| =item make_graph()
 | |
| 
 | |
| Returns C<Graph::Undirected> object that represents the discovered network.
 | |
| 
 | |
| Graph is made by loading all the C<device_port> entries that have a neighbor,
 | |
| using them as edges. Then each device seen in those entries is added as a
 | |
| vertex.
 | |
| 
 | |
| Nodes without topology information are not included.
 | |
| 
 | |
| =back
 | |
| 
 | |
| =cut
 | |
| 
 | |
| sub make_graph {
 | |
|     my $G = Graph::Undirected->new();
 | |
| 
 | |
|     my $devices = schema(vars->{'tenant'})->resultset('Device')
 | |
|         ->search({}, { columns => [qw/ip dns location /] });
 | |
|     my $links = schema(vars->{'tenant'})->resultset('DevicePort')
 | |
|         ->search({remote_ip => { -not => undef }},
 | |
|                  { columns => [qw/ip remote_ip speed remote_type/]});
 | |
|     my %aliases = map {$_->alias => $_->ip}
 | |
|         schema(vars->{'tenant'})->resultset('DeviceIp')
 | |
|           ->search({}, { columns => [qw/ip alias/] })->all;
 | |
| 
 | |
|     my %devs = ( map {($_->ip => $_->dns)}      $devices->all );
 | |
|     my %locs = ( map {($_->ip => $_->location)} $devices->all );
 | |
| 
 | |
|     # Check for no topology info
 | |
|     unless ($links->count > 0) {
 | |
|         debug "make_graph() - No topology information. skipping.";
 | |
|         return undef;
 | |
|     }
 | |
| 
 | |
|     my %link_seen = ();
 | |
|     my %linkmap   = ();
 | |
| 
 | |
|     while (my $link = $links->next) {
 | |
|         my $source = $link->ip;
 | |
|         my $dest   = $link->remote_ip;
 | |
|         my $speed  = $link->speed;
 | |
|         my $type   = $link->remote_type;
 | |
| 
 | |
|         # Check for Aliases
 | |
|         if (defined $aliases{$dest}) {
 | |
|             # Set to root device
 | |
|             $dest = $aliases{$dest};
 | |
|         }
 | |
| 
 | |
|         # Remove loopback - After alias check (bbaetz)
 | |
|         if ($source eq $dest) {
 | |
|             debug "  make_graph() - Loopback on $source";
 | |
|             next;
 | |
|         }
 | |
| 
 | |
|         # Skip IP Phones
 | |
|         if (defined $type and $type =~ /ip.phone/i) {
 | |
|             debug "  make_graph() - Skipping IP Phone. $source -> $dest ($type)";
 | |
|             next;
 | |
|         }
 | |
|         next if exists $link_seen{$source}->{$dest};
 | |
| 
 | |
|         push(@{ $linkmap{$source} }, $dest);
 | |
| 
 | |
|         # take care of reverse too
 | |
|         $link_seen{$source}->{$dest}++;
 | |
|         $link_seen{$dest}->{$source}++;
 | |
| 
 | |
|         $GRAPH_SPEED{$source}->{$dest}->{speed}=$speed;
 | |
|         $GRAPH_SPEED{$dest}->{$source}->{speed}=$speed;
 | |
|     }
 | |
| 
 | |
|     foreach my $link (keys %linkmap) {
 | |
|         foreach my $dest (@{ $linkmap{$link} }) {
 | |
| 
 | |
|             foreach my $side ($link, $dest) {
 | |
|                 unless (defined $GRAPH{$side}) {
 | |
|                     my $is_dev = exists $devs{$side};
 | |
|                     my $dns = $is_dev ?
 | |
|                               $devs{$side} :
 | |
|                               hostname_from_ip($side);
 | |
| 
 | |
|                     # Default to IP if no dns
 | |
|                     $dns = defined $dns ? $dns : "($side)";
 | |
| 
 | |
|                     $G->add_vertex($side);
 | |
|                     debug "  make_graph() - add_vertex('$side')";
 | |
| 
 | |
|                     $GRAPH{$side}->{dns} = $dns;
 | |
|                     $GRAPH{$side}->{isdev} = $is_dev;
 | |
|                     $GRAPH{$side}->{seen}++;
 | |
|                     $GRAPH{$side}->{location} = $locs{$side};
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             $G->add_edge($link,$dest);
 | |
|             debug "  make_graph - add_edge('$link','$dest')";
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return $G;
 | |
| }
 | |
| 
 | |
| sub _homepath {
 | |
|     my ($path, $default) = @_;
 | |
| 
 | |
|     my $home = $ENV{NETDISCO_HOME};
 | |
|     my $item = setting('graph')->{$path} || $default;
 | |
|     return undef unless defined($item);
 | |
| 
 | |
|     if ($item =~ m,^/,) {
 | |
|         return $item;
 | |
|     }
 | |
|     else {
 | |
|         $home =~ s,/*$,,;
 | |
|         return $home . "/" . $item;
 | |
|     }
 | |
| }
 | |
| 
 | |
| 1;
 |