filter log/ filenames into sw.command
[dell-switch] / snmp-topology.pl
1 #!/usr/bin/perl
2 use warnings;
3 use strict;
4 use autodie;
5
6 use Data::Dump qw(dump);
7
8 my $dir="/dev/shm/snmp-topology";
9
10 my $stat;
11
12 my @dumps = @ARGV;
13 @dumps = glob("$dir/*") unless @dumps;
14
15 sub macfmt {
16         my $mac = shift;
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;
20         return $mac;
21 }
22
23 foreach my $file ( @dumps ) {
24
25         my $lines = 0;
26         my $ignored = 0;
27
28         open(my $fh, '<', $file);
29         my $sw = $file; $sw =~ s/^.*\///;
30         while(<$fh>) {
31                 chomp;
32                 $lines++;
33                 if ( m/^SNMPv2-MIB::(sysName|sysDescr)\.0 = STRING: (.+)/ ) {
34                         $stat->{$sw}->{$1} = $2;
35 =for xxx
36                 } elsif ( m/^(IF-MIB)::(ifPhysAddress)\.0 = (\w+): (.+)/ ) {
37                         my ($name,$oid,$type,$value) = ($1,$2,$3,$4);
38                         #$value =~ s/\(\d+\)$// if $type eq 'INTEGER';
39                         $stat->{$sw}->{$name}->{$oid} = $value;
40 =cut
41                 } elsif ( m/^(IF-MIB)::(ifPhysAddress)\[(\d+)\] = (\w+): (.+)/ ) {
42                         my ($name,$oid,$i,$type,$value) = ($1,$2,$3,$4,$5);
43                         #warn "# $sw ",dump($name,$oid,$i,$type,$value),$/;
44                         #$stat->{$sw}->{$name}->{$oid}->[$i] = $value;
45                         $stat->{_mac2sw}->{$value} = $sw;
46                         $stat->{_sw_mac_count}->{$sw}++;
47                 } elsif ( m/^BRIDGE-MIB::dot1dTpFdbPort\[STRING: ([^\]]+)\] = INTEGER: (\d+)/ ) {
48                         my ( $mac, $port ) = ($1,$2);
49                         push @{ $stat->{_sw_mac_port_vlan}->{$sw}->{$mac}->{$port} }, '';
50                         #$stat->{_sw_port_vlan_count}->{$sw}->{$port}->{''}++;
51                         $stat->{_sw_port_mac_count}->{$sw}->{$port}++;
52                 } elsif ( m/^Q-BRIDGE-MIB::dot1qTpFdbPort\[(\d+)\]\[STRING: ([^\]]+)\] = INTEGER: (\d+)/ ) {
53                         my ( $vlan, $mac, $port ) = ($1,$2,$3);
54                         push @{ $stat->{_sw_mac_port_vlan}->{$sw}->{$mac}->{$port} }, $vlan;
55                         #$stat->{_sw_port_vlan_count}->{$sw}->{$port}->{$vlan}++;
56                         $stat->{_sw_port_mac_count}->{$sw}->{$port}++;
57                 } else {
58                         $ignored++;
59                 }
60         }
61         #warn "# $sw ",dump( $stat->{$sw} );
62         warn "## $file $lines / $ignored ignored";
63 }
64 warn "# stat = ",dump($stat);
65 #warn "# _sw_port_vlan_count = ",dump($stat->{_sw_port_vlan_count});
66 warn "# _sw_port_mac_count = ",dump($stat->{_sw_port_mac_count});
67
68 open(my $fh, '>', '/dev/shm/mac2sw');
69 open(my $fh2, '>', '/dev/shm/mac2sw.snmp');
70 foreach my $mac ( keys %{ $stat->{_mac2sw} } ) {
71         print $fh macfmt($mac), " ", $stat->{_mac2sw}->{$mac}, "\n";
72         print $fh $mac, " ", $stat->{_mac2sw}->{$mac}, "\n";
73 };
74
75 # XXX inject additional mac in filter to include wap devices
76 my $mac_include = '/dev/shm/mac.wap';
77 if ( -e $mac_include ) {
78         my $count = 0;
79         open(my $fh, '<', $mac_include);
80         while(<$fh>) {
81                 chomp;
82                 my ($mac,$host) = split(/\s+/,$_,2);
83                 $mac =~ s/^0//; $mac =~ s/:0/:/g; # mungle mac to snmp format without leading zeros
84                 $stat->{_mac2sw}->{$mac} = $host;
85                 $count++;
86         }
87 #       warn "# $mac_include added to _mac2sw = ",dump($stat->{_mac2sw}),$/;
88         warn "# $mac_include added to _mac2sw $count hosts\n";
89 } else {
90         warn "MISSING $mac_include\n";
91 }
92
93 my $s = $stat->{_sw_mac_port_vlan};
94 foreach my $sw ( keys %$s ) {
95         foreach my $mac ( keys %{ $s->{$sw} } ) {
96                 if ( my $mac_name = $stat->{_mac2sw}->{ $mac } ) {
97                         next if $sw eq $mac_name; # mikrotik seems to see itself
98                         foreach my $port ( keys %{ $s->{$sw}->{$mac} } ) {
99                                 #$stat->{_sw_port_sw}->{$sw}->{$port}->{$mac_name} = $s->{$sw}->{$mac}->{$port};
100                                 push @{ $stat->{_sw_port_sw}->{$sw}->{$port} }, $mac_name;
101                                 push @{ $stat->{_sw_sw_port}->{$sw}->{$mac_name} }, $port;
102                         }
103                 }
104         }
105 }
106
107 warn "# _sw_port_sw = ",dump($stat->{_sw_port_sw});
108 warn "# _sw_sw_port = ",dump($stat->{_sw_sw_port});
109
110
111 my $s = $stat->{_sw_port_sw};
112 our $later;
113 my $last_later;
114
115 our @single_sw_port_visible;
116 sub single_sw_port_visible {
117         @single_sw_port_visible = ();
118         my $s = {};
119         foreach my $sw ( keys %$later ) {
120                 if ( exists $stat->{_found}->{$sw} ) {
121                         my @d = delete $later->{$sw};
122                         warn "REMOVED $sw from later it's _found! later was = ",dump( \@d );
123                         next;
124                 }
125                 my @ports = sort keys %{ $later->{$sw} };
126                 foreach my $port ( @ports ) {
127                         my @visible = uniq_visible( @{ $later->{$sw}->{$port} } );
128                         if ( $#visible < 0 ) {
129                                 warn "REMOVED $sw $port from later it's empty";
130                                 delete $later->{$sw}->{$port};
131                                 next;
132                         }
133                         $s->{$sw}->{$port} = [ @visible ];
134                         push @single_sw_port_visible, [ $sw, $port, $visible[0] ] if $#visible == 0; # single
135                 }
136         }
137         my $d_s = dump($s);
138         my $d_l = dump($later);
139         if ( $d_s ne $d_l ) {
140                 $later = $s;
141                 warn "# single_sw_port_visible = ",dump( \@single_sw_port_visible );
142                 warn "# reduced later = ",dump( $later );
143         }
144 }
145
146 sub uniq {
147         my @visible = @_;
148         my $u; $u->{$_}++ foreach @visible;
149         @visible = sort keys %$u;
150         return @visible;
151 }
152
153 sub uniq_visible {
154         my @visible = uniq(@_);
155         @visible = grep { ! exists $stat->{_found}->{$_} } @visible;
156         return @visible;
157 }
158
159 sub to_later {
160         my $sw = shift;
161         my $port = shift;
162         my @visible = uniq_visible(@_);
163         warn "# to_later $sw $port visible = ",dump( \@visible ),"\n";
164         $later->{$sw}->{$port} = [ @visible ];
165         return @visible;
166 }
167
168 while ( ref $s ) {
169 #warn "## s = ",dump($s);
170 foreach my $sw ( sort keys %$s ) {
171
172         #warn "## $sw s = ",dump($s->{$sw}),$/;
173
174         my @ports = sort { $a <=> $b } uniq( keys %{ $s->{$sw} } );
175
176         foreach my $port ( @ports ) {
177                 warn "## $sw $port => ",join(' ', @{$s->{$sw}->{$port}}),$/;
178         }
179
180         if ( $#ports == 0 ) {
181                 my $port = $ports[0];
182                 print "$sw $port TRUNK\n";
183                 push @{$stat->{_trunk}->{$sw}}, $port; # FIXME multiple trunks?
184                 #warn "## _trunk = ",dump( $stat->{_trunk} ).$/;
185
186                 my @visible = uniq_visible( @{ $s->{$sw}->{$port} } );
187                 to_later( $sw, $port, @visible );
188                 next;
189         }
190
191         foreach my $port ( @ports ) {
192                 my @visible = uniq_visible( @{ $s->{$sw}->{$port} } );
193                 warn "### $sw $port visible=",dump(\@visible),$/;
194
195                 if ( $#visible == 0 ) {
196                         warn "++++ $sw $port $visible[0]\n";
197                         #print "$sw $port $visible[0]\n";
198                         $stat->{_found}->{$visible[0]} = "$sw $port";
199                         single_sw_port_visible();
200
201                 } elsif ( $#visible > 0 ) {
202                         to_later( $sw, $port, @visible );
203                 } else {
204                         warn "#### $sw $port doesn't have anything visible, reseting visibility\n";
205                         to_later( $sw, $port, @{ $stat->{_sw_port_sw}->{$sw}->{$port} } );
206                 }
207                         
208         }
209         warn "## _found = ",dump( $stat->{_found} ),$/;
210 }
211
212 warn "NEXT later = ",dump($later),$/;
213
214 single_sw_port_visible();
215
216 my $d = dump($later);
217 if ( $d eq $last_later ) {
218         warn "FIXME later didn't change single_sw_port_visible = ",dump( \@single_sw_port_visible ),$/;
219
220         my $did_patch = 0;
221
222         while ( @single_sw_port_visible ) {
223                 my $single = shift @single_sw_port_visible;
224                 my ( $sw, $port, $visible ) = @$single;
225                 warn "XXX $sw | $port | $visible\n";
226
227                 foreach my $port ( keys %{ $later->{$visible} } ) {
228                         # check back in original full map to see if it was visible
229                         my @visible = @{ $stat->{_sw_port_sw}->{$visible}->{$port} };
230                         if ( scalar grep(/$sw/,@visible) ) {
231                                 warn "PATCH $visible $port -> $sw ONLY";
232                                 $stat->{_found}->{$sw} = "$visible $port";
233
234                                 my @d = delete $later->{$visible}->{$port};
235                                 warn "DELETED $visible $port ",dump(@d);
236                                 $did_patch++;
237
238                                 single_sw_port_visible();
239                         } else {
240                                 warn "FATAL $visible $port NO $sw IN ",dump( \@visible );
241                         }
242                 }
243
244                 if ( ! $did_patch ) {
245                         # OK, we have link from trunk probably, which port was originally visible on?
246
247                         foreach my $port ( keys %{ $stat->{_sw_port_sw}->{$visible} } ) {
248                                 my @visible = grep /$sw/, @{ $stat->{_sw_port_sw}->{$visible}->{$port} };
249                                 if  ( scalar @visible ) {
250                                         warn "PATCH-2 $visible $port -> $sw\n";
251                                         $stat->{_found}->{$sw} = "$visible $port";
252
253                                         my @d = delete $later->{$visible}->{$port};
254                                         warn "DELETED $visible $port ",dump(@d);
255                                         $did_patch++;
256
257                                         single_sw_port_visible();
258                                 } else {
259                                         warn "FATAL $visible $port _sw_port_sw doesn't have $sw";
260                                 }
261                         }
262                 }
263
264         }
265
266         warn "## applied $did_patch patches to unblock\n";
267
268         last if $d eq $last_later;
269         
270 }
271 $last_later = $d;
272
273 $later = undef;
274
275 } # while
276
277 warn "FINAL _found = ",dump( $stat->{_found} ),$/;
278 warn "FINAL _trunk = ",dump( $stat->{_trunk} ),$/;
279 warn "FINAL later  = ",dump( $later ),$/;
280
281
282 my $node;
283 my @edges;
284
285 my $ports = $ENV{PORTS} || 1; # FIXME
286
287
288 open(my $dot, '>', '/tmp/snmp-topology.dot');
289
290 my $shape = $ports ? 'record' : 'ellipse';
291 my $rankdir = 'LR'; #$ports ? 'TB' : 'LR';
292 print $dot <<"__DOT__";
293 digraph topology {
294 graph [ rankdir = $rankdir ]
295 node [ shape = $shape ]
296 edge [ color = "gray" ]
297 __DOT__
298
299 foreach my $to_sw ( keys %{ $stat->{_found} } ) {
300         my ($from_sw, $from_port) = split(/ /,$stat->{_found}->{$to_sw},2);
301         my @to_port = uniq(@{ $stat->{_trunk}->{$to_sw} });
302         my $to_port = $to_port[0];
303         warn "ERROR: $to_sw has ",dump(\@to_port), " ports instead of just one!" if $#to_port > 0;
304         no warnings;
305         printf "%s %s -> %s %s\n", $from_sw, $from_port, $to_sw, $to_port;
306         push @edges, [ $from_sw, $to_sw, $from_port, $to_port ];
307         push @{ $node->{$from_sw} }, [ $from_port, $to_sw ];
308         push @{ $node->{$to_sw} }, [ $to_port, $from_sw ]
309 }
310
311 warn "# edges = ",dump(\@edges);
312 warn "# node = ",dump($node);
313
314 if ( $ports ) {
315         foreach my $n ( keys %$node ) {
316                 no warnings;
317                 my @port_sw =
318                         sort { $a->[0] <=> $b->[0] }
319                         @{ $node->{$n} };
320 #warn "XXX $n ",dump( \@port_sw );
321                 print $dot qq!"$n" [ label="!.uc($n).'|' . join('|', map { sprintf "<%d>%2d %s", $_->[0], $_->[0], $_->[1] } @port_sw ) . qq!" ];\n!;
322         }
323 }
324
325 foreach my $e ( @edges ) {
326         if (! $ports) {
327                 print $dot sprintf qq{ "%s" -> "%s" [ taillabel="%s" ; headlabel="%s" ]\n}, @$e;
328         } else {
329                 no warnings;
330                 print $dot sprintf qq{ "%s":%d -> "%s":%d\n}, $e->[0], $e->[2], $e->[1], $e->[3];
331         }
332 }
333
334 print $dot "}\n";
335