filter log/ filenames into sw.command
[dell-switch] / sbw-parse.pl
1 #!/usr/bin/perl
2 use warnings;
3 use strict;
4 use autodie;
5
6 # ./sw-name-mac.sh
7
8 # usage: NAME_MAC=/dev/shm/file-with-name-space-mac sbw-parse.pl [optional-switch-snmpbulkwalk-dump]
9 #  
10 # cat /dev/shm/neighbors.tab | grep MikroTik | tee /dev/stderr | awk '{ print $7 "_" $8 " " $3 }' > /tmp/name-mac
11 # NAME_MAC=/tmp/name-mac ./sbw-parse.pl
12
13 use Data::Dump qw(dump);
14
15 $|=1; # flush stdout
16
17 my $debug = $ENV{DEBUG} || 0;
18
19 my @cols = qw( ifName ifHighSpeed ifAdminStatus ifOperStatus ifType dot1dStpPortPathCost ifAlias );
20
21 use lib 'lib';
22 use name2mac;
23
24 $name2mac::debug = 1;
25 my $mac2name = $name2mac::mac2name;
26
27 sub mac2name {
28         my ( $mac, $name ) = @_;
29
30         $mac = lc($mac);
31
32         if ( exists $mac2name->{$mac} ) {
33                 my $mac_name = $mac2name->{$mac};
34                 warn "ERROR: name different $name != $mac_name" if $name && $name ne $mac_name;
35                 return ( $mac, $mac_name );
36         }
37         return ( $mac, $name );
38 }
39
40
41 my $mac_name_use;
42
43 my $sw;
44
45 sub fix_mac {
46         my $mac = shift;
47         $mac = lc($mac);
48         $mac =~ s/^([0-9a-f]):/0$1:/;
49         while ( $mac =~ s/:([0-9a-f]):/:0$1:/g ) {};
50         $mac =~ s/:([0-9a-f])$/:0$1/;
51 #       warn "# $mac\n";
52         return $mac;
53 }
54
55 sub sw_name_mac_port {
56         my ( $name, $mac, $port ) = @_;
57         $mac = fix_mac($mac);
58         if ( exists $mac2name->{$mac} ) {
59                 my $sw_name = $mac2name->{$mac};
60                 $sw->{$name}->{port_for_switch}->{$port}->{ $sw_name }++;
61                 #print "## $name $port $sw_name\n";
62         }
63 }
64
65 my $gv; # collect for graphviz
66
67 my @files = @ARGV;
68 @files = glob('snmpbulkwalk/*') unless @files;
69
70 foreach my $file ( @files ) {
71         my ( undef, $name ) = split(/\//, $file);
72         print "# $name $file\n" if $debug;
73
74         if ( -s "/dev/shm/$file" ) {
75                 if ( -M $file < -M "/dev/shm/$file" ) {
76                         warn "UPDATE $file\n";
77                         system "cp -pv /dev/shm/$file $file";
78                 }
79                 $file="/dev/shm/$file";
80         } else {
81                 warn "WARNING: using old file $file\n";
82         }
83
84         open(my $f, '<', $file);
85         while(<$f>) {
86                 chomp;
87                 if ( m/::(sysName|sysLocation|ipDefaultTTL|dot1dStpPriority|dot1dStpTopChanges|dot1dStpDesignatedRoot|dot1dStpRootCost|dot1dStpRootPort|dot1qNumVlans)\./ ) {
88                         my ( undef, $v ) = split(/ = \w+: /,$_,2);
89                         $sw->{$name}->{$1} = $v;
90                 } elsif ( m/::(ifMtu|ifHighSpeed|ifSpeed)\[(\d\d?)\] = (?:INTEGER|Gauge32): (\d+)/ ) {
91                         $sw->{$name}->{$1}->[$2] = $3;
92                         # Dell PowerConnect 5548 doeesn't have ifHighSpeed
93                         if ( $1 eq 'ifSpeed' && ! exists $sw->{$name}->{'ifHighSpeed'}->[$2] ) {
94                                 $sw->{$name}->{'ifHighSpeed'}->[$2] = $3 / 1_000_000;
95                         }
96                 } elsif ( m/::(ifPhysAddress)\[(\d\d?)\] = STRING: ([0-9a-f:]+)/ ) {
97                         $sw->{$name}->{$1}->[$2] = fix_mac($3);
98                 } elsif ( m/::(ifDescr|ifName|ifAlias)\[(\d\d?)\] = STRING: (.+)/ ) {
99                         $sw->{$name}->{$1}->[$2] = $3;
100                         if ( $1 eq 'ifName' ) {
101                                 my ( $if_name, $port ) = ($3,$2);
102                                 $sw->{$name}->{port_name_to_number}->{$if_name} = $port;
103                         }
104                         if ( $1 eq 'ifDescr' ) { # some switches miss ifName
105                                 my ( $if_name, $port ) = ($3,$2);
106                                 $if_name =~ s/gigabitethernet/ge/;
107                                 $if_name =~ s/tengigabitethernet/te/;
108                                 $sw->{$name}->{'ifName'}->[$port] = $if_name
109                                         if ( ! exists $sw->{name}->{'ifName'} );
110                                 $sw->{$name}->{port_name_to_number}->{$if_name} = $port 
111                                         if ( ! exists $sw->{$name}->{port_name_to_number}->{$if_name} );
112                         }
113                 } elsif ( m/::(ifAdminStatus|ifOperStatus|ifType|dot3StatsDuplexStatus)\[(\d\d?)\] = INTEGER: (\w+)\(/ ) {
114                         $sw->{$name}->{$1}->[$2] = $3;
115                 } elsif ( m/::(dot1dStpPortPathCost)\[(\d\d?)\] = INTEGER: (\d+)/ ) {
116                         $sw->{$name}->{$1}->[$2] = $3;
117                 } elsif ( m/::(dot1dTpFdbPort)\[STRING: ([0-9a-f:]+)\] = INTEGER: (\d+)/ ) {
118                         $sw->{$name}->{$1}->{ fix_mac($2) } = $3;
119                         sw_name_mac_port( $name, $2, $3 );
120                 } elsif ( m/::(dot1qTpFdbPort)\[(\d+)\]\[STRING: ([0-9a-f:]+)\] = INTEGER: (\d+)/ ) {
121                         $sw->{$name}->{$1}->{ fix_mac($3) } = [ $4, $2 ]; # port, vlan
122                         sw_name_mac_port( $name, $3, $4 );
123                 }
124
125                 # dot1qVlanCurrentEgressPorts
126                 # dot1qVlanCurrentUntaggedPorts
127                 # dot1qVlanStaticName
128                 # dot1qVlanStaticEgressPorts
129                 # dot1qVlanStaticUntaggedPorts
130                 # dot1qPvid
131
132                 # entPhysicalDescr
133                 # entPhysicalClass
134
135                 #print "## $_<--\n";
136         }
137         warn "# sw $name = ",dump($sw->{$name}) if $debug;
138
139         foreach my $port ( 1 .. $#{ $sw->{$name}->{ifDescr} } ) {
140                 print "$name $port";
141                 foreach my $oid ( @cols ) {
142                         if ( $oid eq 'ifAlias' ) {
143                                 if ( defined( $sw->{$name}->{$oid}->[$port] ) ) {
144                                         print " [",$sw->{$name}->{$oid}->[$port],"]";
145                                 }
146                         } elsif ( defined $sw->{$name}->{$oid}->[$port] ) {
147                                 print " ", $sw->{$name}->{$oid}->[$port];
148                         } else {
149                                 print " ?";
150                                 #warn "MISSING $name $oid $port\n";
151                         }
152
153                 }
154                 if ( exists( $sw->{$name}->{port_for_switch}->{ $port } ) ) {
155                         my @visible = sort keys %{ $sw->{$name}->{port_for_switch}->{ $port } };
156                         print " ",join(',', @visible);
157
158                         if ( scalar @visible == 1 ) {
159                                 $gv->{$name}->{$port}->{ $visible[0] }->{ 'no_port' } = [$port,0]; # no port
160                         }
161                 }
162                 print "\n";
163         }}
164
165 # fix ifPhysAddress
166
167 # read neighbour visibility from lldp
168
169 sub port2number {
170         my ($name,$port) = @_;
171
172         return $port if $port =~ m/^\d+$/;
173         $port =~ s{bridge\d*/}{};  # remove mikrotik port prefix
174         $port =~ s{,bridge\d*$}{}; # remove mikrotik port suffix
175         $port =~ s{,bonding\d*$}{}; # remove mikrotik port suffix
176
177         if ( exists $sw->{$name}->{port_name_to_number}->{$port} ) {
178                 return $sw->{$name}->{port_name_to_number}->{$port};
179         }
180
181         # gigabitethernet1/0/45 or gi1/0/45
182         if ( $port =~ m{(.+)1/0/(\d+)$} ) {
183                 my ($type,$port) = ($1,$2);
184                 if ( $type =~ m{gi}i ) {
185                         return $port;
186                 } elsif ( $type =~ m{te}i ) {
187                         return $port <= 4 ? $port + 10000 : $port;
188                 } else {
189                         die "unknown [$type]";
190                 }
191         }
192
193         # linux
194         if ( $port =~ m{eth(\d+)} ) {
195                 return $1;
196         }
197
198         my @fuzzy = grep { m/^$port/ } keys %{ $sw->{$name}->{port_name_to_number} };
199         if ( scalar @fuzzy == 1 ) {
200                 return $sw->{$name}->{port_name_to_number}->{$fuzzy[0]}
201         }
202
203         warn "ERROR [$_] can't find port $port for $name in ",dump( $sw->{$name}->{port_name_to_number} );
204 }
205
206 sub fix_sw_name {
207         my $name = shift;
208         if ( $name eq 'rack3-lib' ) {
209                 $name = 'sw-lib-srv';
210         }
211         return $name;
212 }
213
214
215 open(my $n_fh, '<', '/dev/shm/neighbors.tab');
216 while(<$n_fh>) {
217         chomp;
218         my ( $sw1, $port1, undef, $port2, $sw2, undef ) = split(/\t/, $_, 6 );
219         next if $port2 =~ m/:/; # skip mac in port number (wap lldp leek)
220         next unless $sw2 && $port2;
221         $sw1 = fix_sw_name($sw1);
222         my $port1_nr = port2number( $sw1, $port1 );
223         my $port2_nr = port2number( $sw2, $port2 );
224
225         if ( $sw->{$sw1}->{ifType}->[$port1_nr] ne 'ethernetCsmacd' ) {
226                 warn "SKIP $sw1 $port1_nr $sw->{$sw1}->{ifType}->[$port1_nr]\n";
227                 next;
228         } elsif ( $port1 =~ m{bridge} ) { # skip mikrotik bridges
229 #               warn "SKIP $sw1 $port1_nr $port1\n";
230 #               next;
231         }
232
233         $gv->{$sw1}->{$port1_nr}->{$sw2}->{$port2_nr} = [ $port1, $port2 ];
234         delete $gv->{$sw1}->{$port1_nr}->{$sw2}->{'no_port'} if exists $gv->{$sw1}->{$port1_nr}->{$sw2}->{'no_port'};
235 }
236
237 # FIXME sw-b101 doesn't have lldp so we insert data here manually from pictures
238 sub fake_gv {
239         my ($sw1, $p1, $sw2, $p2) = @_;
240         $gv->{$sw1}->{$p1}->{$sw2}->{$p2} = [ $p1, $p2 ];
241         $gv->{$sw2}->{$p2}->{$sw1}->{$p1} = [ $p2, $p1 ];
242 }
243 delete $gv->{'sw-b101'};
244 fake_gv( 'sw-b101' => 3, 'sw-b101' => 4 ); # vlan1-to-vlan2 / vlan2-to-vlan1
245
246 fake_gv( 'sw-b101' => 21, 'sw-rack2' => 50 );
247 fake_gv( 'sw-b101' => 23, 'sw-lib-srv' => 48 );
248 fake_gv( 'sw-b101' => 24, 'sw-rack1' => 48 );
249
250 #delete $gv->{'sw-ganeti'};
251 # spf-16 -> "sw-rack2"        => { 2 => ["16-sfp-uplink-b101,bridge", "te1/0/2"] },
252
253 print "# gv = ",dump( $gv );
254
255 open(my $dot_fh, '>', '/dev/shm/network.dot');
256 print $dot_fh qq|
257 digraph topology {
258 graph [ rankdir = LR ]
259 node [ shape = record; fontname="Helvetica" ]
260 edge [ color = "gray" ]
261 |;
262
263 my @edges;
264 my $node;
265
266 foreach my $sw1 ( sort keys %$gv ) {
267         foreach my $p1 ( sort { $a <=> $b } keys %{ $gv->{$sw1} } ) {
268                 foreach my $sw2 ( sort keys %{ $gv->{$sw1}->{$p1} } ) {
269                         if ( $sw1 eq $sw2 ) {
270                                 warn "SKIP same switch $sw1 == $sw2\n";
271                                 next;
272                         }
273                         #push @{ $node->{$sw1} }, [ $p1, $sw2 ];
274                         foreach my $p2 ( keys %{ $gv->{$sw1}->{$p1}->{$sw2} } ) {
275                                 push @edges, [ $sw1, $sw2, $p1, $p2 ];
276                                 ##push @{ $node->{$sw2} }, [ $p2, $sw1 ];
277                                 push @{ $node->{$sw1} }, [ $p1, $sw2, $p2 ];
278                         }
279                 }
280         }
281 }
282
283 foreach my $n ( sort keys %$node ) {
284         no warnings;
285         my @port_sw =
286                 sort { join(' ',@$a) <=> join(' ',@$b) }
287                 @{ $node->{$n} };
288         print $dot_fh qq!"$n" [ label="!.uc($n).'|' . join('|', map {
289                 $mac_name_use->{$_->[1]}++;
290                 sprintf "<%d>%2d %s%s", $_->[0], $_->[0], $_->[1], $_->[2] eq 'no_port' ? '' : ' ' . $_->[2]
291         } @port_sw ) . qq!" ];\n!;
292 }
293
294 foreach my $e ( sort { join(' ',@$a) cmp join(' ', @$b) } @edges ) {
295         no warnings;
296         if ( $e->[3] == 0 ) {
297                 #print $dot_fh sprintf qq{ "%s":%d -> "%s"\n}, $e->[0], $e->[2], $e->[1];
298         } else {
299                 print $dot_fh sprintf qq{ "%s":%d -> "%s":%d\n}, $e->[0], $e->[2], $e->[1], $e->[3];
300         }
301 }
302
303 sub git_commit {
304         return if $debug;
305         my $file = shift;
306         $file =~ s{/dev/shm/}{} && warn "## git_commit $file";
307         system qq{git -C /dev/shm add -f $file && git -C /dev/shm commit -m "\$( date +%Y-%m-%d ) $file" $file };
308 }
309
310 warn "## mac_name_use ",dump( $mac_name_use );
311
312 my @mac_name_use_zero = sort grep { $mac_name_use->{$_} == 0 } keys %$mac_name_use;
313 warn "# mac_name_use_zero=",dump( \@mac_name_use_zero );
314 open(my $zero_fh, '>', '/dev/shm/mac_name_use.0');
315 print $zero_fh "$_\n" foreach map {
316         s/^\w+\s//; $_; # remote msw_ prefix and leave only mac
317 } @mac_name_use_zero;
318 close($zero_fh);
319 git_commit 'mac_name_use.0';
320
321 print $dot_fh qq|
322 }
323 |;
324 close($dot_fh);
325 git_commit 'network.dot';
326
327 system "dot -Tsvg /dev/shm/network.dot > /var/www/network.svg";
328 #system 'git -C /dev/shm commit -m $( date +%Y-%m-%d ) network.dot' if ! $debug;
329
330 warn "# strange neihbours:";
331 system "grep -f /dev/shm/mac_name_use.0 /dev/shm/neighbors.tab";