6 use Data::Dump qw(dump);
8 my $dir="/dev/shm/snmp-topology";
13 @dumps = glob("$dir/*") unless @dumps;
17 $mac =~ s/^([0-9a-f]):/0$1:/i;
18 while ( $mac =~ s/:([0-9a-f]):/:0$1:/ig ) {};
19 $mac =~ s/:([0-9a-f])$/:0$1/i;
23 foreach my $file ( @dumps ) {
25 open(my $fh, '<', $file);
26 my $sw = $file; $sw =~ s/^.*\///;
29 if ( m/^SNMPv2-MIB::(sysName|sysDescr)\.0 = STRING: (.+)/ ) {
30 $stat->{$sw}->{$1} = $2;
32 } elsif ( m/^(IF-MIB)::(ifPhysAddress)\.0 = (\w+): (.+)/ ) {
33 my ($name,$oid,$type,$value) = ($1,$2,$3,$4);
34 #$value =~ s/\(\d+\)$// if $type eq 'INTEGER';
35 $stat->{$sw}->{$name}->{$oid} = $value;
37 } elsif ( m/^(IF-MIB)::(ifPhysAddress)\[(\d+)\] = (\w+): (.+)/ ) {
38 my ($name,$oid,$i,$type,$value) = ($1,$2,$3,$4,$5);
39 #warn "# $sw ",dump($name,$oid,$i,$type,$value),$/;
40 #$stat->{$sw}->{$name}->{$oid}->[$i] = $value;
41 $stat->{_mac2sw}->{$value} = $sw;
42 } elsif ( m/^BRIDGE-MIB::dot1dTpFdbPort\[STRING: ([^\]]+)\] = INTEGER: (\d+)/ ) {
43 my ( $mac, $port ) = ($1,$2);
44 push @{ $stat->{_sw_mac_port_vlan}->{$sw}->{$mac}->{$port} }, '';
45 } elsif ( m/^Q-BRIDGE-MIB::dot1qTpFdbPort\[(\d+)\]\[STRING: ([^\]]+)\] = INTEGER: (\d+)/ ) {
46 my ( $vlan, $mac, $port ) = ($1,$2,$3);
47 push @{ $stat->{_sw_mac_port_vlan}->{$sw}->{$mac}->{$port} }, $vlan;
50 #warn "# $sw ",dump( $stat->{$sw} );
52 #warn "# stat = ",dump($stat);
54 open(my $fh, '>', '/dev/shm/mac2sw');
55 open(my $fh2, '>', '/dev/shm/mac2sw.snmp');
56 foreach my $mac ( keys %{ $stat->{_mac2sw} } ) {
57 print $fh macfmt($mac), " ", $stat->{_mac2sw}->{$mac}, "\n";
58 print $fh $mac, " ", $stat->{_mac2sw}->{$mac}, "\n";
61 # XXX inject additional mac in filter to include wap devices
62 my $mac_include = '/dev/shm/mac.wap';
63 if ( -e $mac_include ) {
65 open(my $fh, '<', $mac_include);
68 my ($mac,$host) = split(/\s+/,$_,2);
69 $mac =~ s/^0//; $mac =~ s/:0/:/g; # mungle mac to snmp format without leading zeros
70 $stat->{_mac2sw}->{$mac} = $host;
73 # warn "# $mac_include added to _mac2sw = ",dump($stat->{_mac2sw}),$/;
74 warn "# $mac_include added to _mac2sw $count hosts\n";
77 my $s = $stat->{_sw_mac_port_vlan};
78 foreach my $sw ( keys %$s ) {
79 foreach my $mac ( keys %{ $s->{$sw} } ) {
80 if ( my $mac_name = $stat->{_mac2sw}->{ $mac } ) {
81 next if $sw eq $mac_name; # mikrotik seems to see itself
82 foreach my $port ( keys %{ $s->{$sw}->{$mac} } ) {
83 #$stat->{_sw_port_sw}->{$sw}->{$port}->{$mac_name} = $s->{$sw}->{$mac}->{$port};
84 push @{ $stat->{_sw_port_sw}->{$sw}->{$port} }, $mac_name;
85 push @{ $stat->{_sw_sw_port}->{$sw}->{$mac_name} }, $port;
91 warn "# _sw_port_sw = ",dump($stat->{_sw_port_sw});
92 warn "# _sw_sw_port = ",dump($stat->{_sw_sw_port});
95 my $s = $stat->{_sw_port_sw};
99 our @single_sw_port_visible;
100 sub single_sw_port_visible {
101 @single_sw_port_visible = ();
103 foreach my $sw ( keys %$later ) {
104 if ( exists $stat->{_found}->{$sw} ) {
105 my @d = delete $later->{$sw};
106 warn "REMOVED $sw from later it's _found! later was = ",dump( \@d );
109 my @ports = sort keys %{ $later->{$sw} };
110 foreach my $port ( @ports ) {
111 my @visible = uniq_visible( @{ $later->{$sw}->{$port} } );
112 if ( $#visible < 0 ) {
113 warn "REMOVED $sw $port from later it's empty";
114 delete $later->{$sw}->{$port};
117 $s->{$sw}->{$port} = [ @visible ];
118 push @single_sw_port_visible, [ $sw, $port, $visible[0] ] if $#visible == 0; # single
122 my $d_l = dump($later);
123 if ( $d_s ne $d_l ) {
125 warn "# single_sw_port_visible = ",dump( \@single_sw_port_visible );
126 warn "# reduced later = ",dump( $later );
132 my $u; $u->{$_}++ foreach @visible;
133 @visible = sort keys %$u;
138 my @visible = uniq(@_);
139 @visible = grep { ! exists $stat->{_found}->{$_} } @visible;
146 my @visible = uniq_visible(@_);
147 warn "# to_later $sw $port visible = ",dump( \@visible ),"\n";
148 $later->{$sw}->{$port} = [ @visible ];
153 #warn "## s = ",dump($s);
154 foreach my $sw ( sort keys %$s ) {
156 #warn "## $sw s = ",dump($s->{$sw}),$/;
158 my @ports = sort { $a <=> $b } uniq( keys %{ $s->{$sw} } );
160 foreach my $port ( @ports ) {
161 warn "## $sw $port => ",join(' ', @{$s->{$sw}->{$port}}),$/;
164 if ( $#ports == 0 ) {
165 my $port = $ports[0];
166 #print "$sw $port TRUNK\n";
167 push @{$stat->{_trunk}->{$sw}}, $port; # FIXME multiple trunks?
168 #warn "## _trunk = ",dump( $stat->{_trunk} ).$/;
170 my @visible = uniq_visible( @{ $s->{$sw}->{$port} } );
171 to_later( $sw, $port, @visible );
175 foreach my $port ( @ports ) {
176 my @visible = uniq_visible( @{ $s->{$sw}->{$port} } );
177 warn "### $sw $port visible=",dump(\@visible),$/;
179 if ( $#visible == 0 ) {
180 warn "++++ $sw $port $visible[0]\n";
181 #print "$sw $port $visible[0]\n";
182 $stat->{_found}->{$visible[0]} = "$sw $port";
183 single_sw_port_visible();
185 } elsif ( $#visible > 0 ) {
186 to_later( $sw, $port, @visible );
188 warn "#### $sw $port doesn't have anything visible, reseting visibility\n";
189 to_later( $sw, $port, @{ $stat->{_sw_port_sw}->{$sw}->{$port} } );
193 warn "## _found = ",dump( $stat->{_found} ),$/;
196 warn "NEXT later = ",dump($later),$/;
198 single_sw_port_visible();
200 my $d = dump($later);
201 if ( $d eq $last_later ) {
202 warn "FIXME later didn't change single_sw_port_visible = ",dump( \@single_sw_port_visible ),$/;
206 while ( @single_sw_port_visible ) {
207 my $single = shift @single_sw_port_visible;
208 my ( $sw, $port, $visible ) = @$single;
209 warn "XXX $sw | $port | $visible\n";
211 foreach my $port ( keys %{ $later->{$visible} } ) {
212 # check back in original full map to see if it was visible
213 my @visible = @{ $stat->{_sw_port_sw}->{$visible}->{$port} };
214 if ( scalar grep(/$sw/,@visible) ) {
215 warn "PATCH $visible $port -> $sw ONLY";
216 $stat->{_found}->{$sw} = "$visible $port";
218 my @d = delete $later->{$visible}->{$port};
219 warn "DELETED $visible $port ",dump(@d);
222 single_sw_port_visible();
224 warn "FATAL $visible $port NO $sw IN ",dump( \@visible );
228 if ( ! $did_patch ) {
229 # OK, we have link from trunk probably, which port was originally visible on?
231 foreach my $port ( keys %{ $stat->{_sw_port_sw}->{$visible} } ) {
232 my @visible = grep /$sw/, @{ $stat->{_sw_port_sw}->{$visible}->{$port} };
233 if ( scalar @visible ) {
234 warn "PATCH-2 $visible $port -> $sw\n";
235 $stat->{_found}->{$sw} = "$visible $port";
237 my @d = delete $later->{$visible}->{$port};
238 warn "DELETED $visible $port ",dump(@d);
241 single_sw_port_visible();
243 warn "FATAL $visible $port _sw_port_sw doesn't have $sw";
250 warn "## applied $did_patch patches to unblock\n";
252 last if $d eq $last_later;
261 warn "FINAL _found = ",dump( $stat->{_found} ),$/;
262 warn "FINAL _trunk = ",dump( $stat->{_trunk} ),$/;
263 warn "FINAL later = ",dump( $later ),$/;
269 my $ports = $ENV{PORTS} || 1; # FIXME
272 open(my $dot, '>', '/tmp/snmp-topology.dot');
274 my $shape = $ports ? 'record' : 'ellipse';
275 my $rankdir = 'LR'; #$ports ? 'TB' : 'LR';
276 print $dot <<"__DOT__";
278 graph [ rankdir = $rankdir ]
279 node [ shape = $shape ]
280 edge [ color = "gray" ]
283 foreach my $to_sw ( keys %{ $stat->{_found} } ) {
284 my ($from_sw, $from_port) = split(/ /,$stat->{_found}->{$to_sw},2);
285 my @to_port = uniq(@{ $stat->{_trunk}->{$to_sw} });
286 my $to_port = $to_port[0];
287 warn "ERROR: $to_sw has ",dump(\@to_port), " ports instead of just one!" if $#to_port > 0;
288 printf "%s %s -> %s %s\n", $from_sw, $from_port, $to_sw, $to_port;
289 push @edges, [ $from_sw, $to_sw, $from_port, $to_port ];
290 push @{ $node->{$from_sw} }, [ $from_port, $to_sw ];
291 push @{ $node->{$to_sw} }, [ $to_port, $from_sw ]
294 warn "# edges = ",dump(\@edges);
295 warn "# node = ",dump($node);
298 foreach my $n ( keys %$node ) {
301 sort { $a->[0] <=> $b->[0] }
303 #warn "XXX $n ",dump( \@port_sw );
304 print $dot qq!"$n" [ label="!.uc($n).'|' . join('|', map { sprintf "<%d>%2d %s", $_->[0], $_->[0], $_->[1] } @port_sw ) . qq!" ];\n!;
308 foreach my $e ( @edges ) {
310 print $dot sprintf qq{ "%s" -> "%s" [ taillabel="%s" ; headlabel="%s" ]\n}, @$e;
313 print $dot sprintf qq{ "%s":%d -> "%s":%d\n}, $e->[0], $e->[2], $e->[1], $e->[3];