File Coverage

blib/lib/OSPF/LSDB/gated.pm
Criterion Covered Total %
statement 201 251 80.0
branch 111 168 66.0
condition 5 11 45.4
subroutine 19 20 95.0
pod 1 11 9.0
total 337 461 73.1


line stmt bran cond sub pod time code
1             ##########################################################################
2             # Copyright (c) 2010-2021 Alexander Bluhm
3             #
4             # Permission to use, copy, modify, and distribute this software for any
5             # purpose with or without fee is hereby granted, provided that the above
6             # copyright notice and this permission notice appear in all copies.
7             #
8             # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9             # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10             # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11             # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12             # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13             # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14             # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15             ##########################################################################
16              
17 4     4   84859 use strict;
  4         14  
  4         101  
18 4     4   18 use warnings;
  4         14  
  4         168  
19              
20             =pod
21              
22             =head1 NAME
23              
24             OSPF::LSDB::gated - parse B OSPF link state database
25              
26             =head1 SYNOPSIS
27              
28             use OSPF::LSDB::gated;
29              
30             my $gated = OSPF::LSDB::gated-Enew();
31              
32             my $gated = OSPF::LSDB::gated-Enew(ssh => "user@host");
33              
34             $gated-Eparse(%todo);
35              
36             =head1 DESCRIPTION
37              
38             The OSPF::LSDB::gated module parses the OSPF part of a B
39             dump file and fills the L base object.
40             An existing F file can be given or it can be created
41             dynammically.
42             In the latter case B is invoked if permissions are not
43             sufficient to run B.
44             If the object has been created with the C argument, the specified
45             user and host are used to login and run B there.
46              
47             There is only one public method:
48              
49             =cut
50              
51             package OSPF::LSDB::gated;
52 4     4   21 use base 'OSPF::LSDB';
  4         7  
  4         1412  
53 4     4   515 use File::Slurp;
  4         20756  
  4         267  
54 4     4   24 use Regexp::Common;
  4         8  
  4         32  
55 4         33 use fields qw(
56             dump
57 4     4   89848 );
  4         9  
58              
59             # add a Regexp::Common regep that recognizes short IP addresses
60             my $IPunitdec = q{(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})};
61             my $IPdefsep = '[.]';
62             Regexp::Common::pattern
63             name => [qw (net sIPv4)],
64             create => "(?k:$IPunitdec(?:$IPdefsep$IPunitdec){0,3})",
65             ;
66              
67             # add a Regexp::Common regep that recognizes time in 0:00:00 format
68             my $time60 = q{(?:[0-6]?[0-9])};
69             Regexp::Common::pattern
70             name => [qw (time)],
71             create => "(?k:(?:[0-9]+:)?$time60:$time60|(?:$time60:)?$time60)",
72             ;
73              
74             # shortcut
75             my $IP = qr/$RE{net}{IPv4}{-keep}/;
76             my $SIP = qr/$RE{net}{sIPv4}{-keep}/;
77             my $TIME = qr/$RE{time}{-keep}/;
78             my $DEC = qr/([0-9]+)/;
79             my $NUM = qr/$RE{num}{dec}{-keep}/;
80             my $HEX = qr/$RE{num}{hex}{-keep}/;
81             my $OO = qr/(On|Off)/;
82             my $TAG = qr/(?:$DEC|Invalid tag: $HEX)/;
83              
84             # convert short IP to long IP
85             sub _s2lIP($) {
86 912     912   2002 my $ip = $_[0].".0.0.0";
87 912         2815 $ip =~ /^$RE{net}{IPv4}{-keep}/;
88 912         91769 return $1;
89             }
90              
91             # convert time to seconds
92             sub _time2sec($) {
93 192     192   650 my @a = split(/:/, "0:0:".$_[0]);
94 192         1077 return 60*(60*$a[-3] + $a[-2]) + $a[-1];
95             }
96              
97             # convert On/Off to boolean 0/1
98 52 50   52   180 sub _oo2bool($) { $_[0] eq "On" ? 1 : $_[0] eq "Off" ? 0 : undef }
    100          
99              
100             sub get_dump {
101 0     0 0 0 my OSPF::LSDB::gated $self = shift;
102 0   0     0 my $file = $_[0] || "/var/tmp/gated_dump";
103 0 0       0 if (-e $file) {
104 0         0 my @cmd = ("mv", "-f", $file, "$file.old");
105 0 0       0 unshift @cmd, "sudo" if $> != 0;
106 0         0 system(@cmd);
107             }
108 0         0 my @cmd = qw(gdc dump);
109 0 0       0 if ($self->{ssh}) {
110 0         0 unshift @cmd, "ssh", $self->{ssh};
111             } else {
112 0 0       0 unshift @cmd, "sudo" if $> != 0;
113             }
114 0 0       0 system(@cmd)
115             and die "Command '@cmd' failed: $?\n";
116 0         0 sleep(1); # XXX when is gated finished ?
117 0 0       0 if ($self->{ssh}) {
118 0         0 @cmd = ("ssh", $self->{ssh}, "cat", $file);
119 0         0 @{$self->{dump}} = `@cmd`;
  0         0  
120 0 0       0 die "Command '@cmd' failed: $?\n" if $?;
121             } else {
122 0         0 @{$self->{dump}} = read_file($file);
  0         0  
123             }
124             }
125              
126             sub parse_links {
127 26     26 0 41 my OSPF::LSDB::gated $self = shift;
128 26         58 my($router, @lines) = @_;
129 26         151 my %typename = (
130             "Router" => "pointtopoint",
131             "Transit net" => "transit",
132             "Stub net" => "stub",
133             "Virtual" => "virtual",
134             );
135 26         38 my $type;
136             my $l;
137 26         41 foreach (@lines) {
138 164 100       1833 if (/Type: ([\w ]+)\s+Cost: $DEC$/) {
    50          
    100          
    50          
139 82 50       226 defined($type = $typename{$1})
140             or die "Unknown link type: $1\n";
141 82         188 $l = { metric => $2 };
142 82         106 push @{$router->{$type.'s'}}, $l;
  82         213  
143             } elsif (/RouterID: $SIP\s+Address: $SIP$/) {
144 0 0 0     0 if ($type eq "pointtopoint" || $type eq "virtual") {
145 0         0 $l->{routerid} = _s2lIP($1);
146 0         0 $l->{interface} = _s2lIP($2);
147             } else {
148 0         0 die "$_ Bad line for link type $type.\n";
149             }
150             } elsif (/DR: $SIP\s+Address: $SIP$/) {
151 64 50       125 if ($type eq "transit") {
152 64         121 $l->{address} = _s2lIP($1);
153 64         118 $l->{interface} = _s2lIP($2);
154             } else {
155 0         0 die "$_ Bad line for link type $type.\n";
156             }
157             } elsif (/Network: $SIP\s+NetMask: $SIP$/) {
158 18 50       56 if ($type eq "stub") {
159 18         35 $l->{network} = _s2lIP($1);
160 18         39 $l->{netmask} = _s2lIP($2);
161             } else {
162 0         0 die "$_ Bad line for link type $type.\n";
163             }
164             } else {
165 0         0 die "$_ Unknown link line.\n";
166             }
167             }
168             }
169              
170             sub parse_router {
171 26     26 0 56 my OSPF::LSDB::gated $self = shift;
172 26         58 my @lines = @_;
173 26         30 my %router;
174 26         36 my($section, @link_lines);
175 26         40 foreach (@lines) {
176 260 100       486 if (/^\w/) {
177 74         84 undef $section;
178             }
179 260 100       2221 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    100          
    100          
    50          
180 26         69 $router{routerid} = _s2lIP($1);
181 26         59 $router{age} = _time2sec($3);
182 26         96 $router{sequence} = "0x$4";
183             } elsif (/^RouterID: $SIP\s+Area Border: $OO\s+AS Border: $OO$/) {
184 26         65 $section = "link";
185 26         49 $router{router} = _s2lIP($1);
186 26         61 $router{bits}{B} = _oo2bool($2);
187 26         38 $router{bits}{E} = _oo2bool($3);
188 26         61 $router{bits}{V} = 0; # XXX need gated dump with virtual link
189             } elsif (/^Nexthops\b/) {
190 22         56 $section = "nexthop";
191             } elsif (s/^\t//) {
192 186 100       326 if ($section eq "link") {
    50          
193 164         420 push @link_lines, $_;
194             } elsif ($section eq "nexthop") {
195             # not part of LSDB, redundant in gated dump
196             } else {
197 0         0 die "$_ No router section.\n";
198             }
199             } else {
200 0         0 die "$_ Unknown router line.\n";
201             }
202             }
203 26         69 $self->parse_links(\%router, @link_lines);
204 26         79 return \%router;
205             }
206              
207             sub parse_network {
208 26     26 0 38 my OSPF::LSDB::gated $self = shift;
209 26         52 my @lines = @_;
210 26         35 my %network;
211             my($section);
212 26         40 foreach (@lines) {
213 166 100       353 if (/^\w/) {
214 142         158 undef $section;
215             }
216 166 100       2125 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    100          
    100          
    100          
    50          
217 26         69 $network{routerid} = _s2lIP($1);
218 26         60 $network{age} = _time2sec($3);
219 26         82 $network{sequence} = "0x$4";
220             } elsif (/^Router: $SIP\s+Netmask: $SIP\s+Network: $SIP$/) {
221 26         57 $network{address} = _s2lIP($1);
222 26         49 $network{netmask} = _s2lIP($2);
223             } elsif (/^Attached Router: $SIP$/) {
224 66         83 push @{$network{attachments}}, { routerid => _s2lIP($1) };
  66         144  
225             } elsif (/^Nexthops\b/) {
226 24         52 $section = "nexthop";
227             } elsif (s/^\t//) {
228 24 50       77 if ($section eq "nexthop") {
229             # not part of LSDB, redundant in gated dump
230             } else {
231 0         0 die "$_ No network section.\n";
232             }
233             } else {
234 0         0 die "$_ Unknown network line.\n";
235             }
236             }
237 26         59 return \%network;
238             }
239              
240             sub parse_summary {
241 4     4 0 6 my OSPF::LSDB::gated $self = shift;
242 4         11 my @lines = @_;
243 4         10 my %summary;
244             my($section);
245 4         9 foreach (@lines) {
246 16 100       43 if (/^\w/) {
247 12         15 undef $section;
248             }
249 16 100       622 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    100          
    100          
    50          
250 4         22 $summary{routerid} = _s2lIP($1);
251 4         11 $summary{age} = _time2sec($3);
252 4         18 $summary{sequence} = "0x$4";
253             } elsif (/^LSID: $SIP\s+Network: $SIP\s+Netmask: $SIP\s+Cost: $DEC$/) {
254 4         12 $summary{address} = _s2lIP($1);
255 4         11 $summary{netmask} = _s2lIP($3);
256 4         16 $summary{metric} = $4;
257             } elsif (/^Nexthops\b/) {
258 4         11 $section = "nexthop";
259             } elsif (s/^\t//) {
260 4 50       16 if ($section eq "nexthop") {
261             # not part of LSDB, redundant in gated dump
262             } else {
263 0         0 die "$_ No summary section.\n";
264             }
265             } else {
266 0         0 die "$_ Unknown summary line.\n";
267             }
268             }
269 4         11 return \%summary;
270             }
271              
272             sub parse_boundary {
273 4     4 0 8 my OSPF::LSDB::gated $self = shift;
274 4         10 my @lines = @_;
275 4         8 my %boundary;
276             my($section);
277 4         7 foreach (@lines) {
278 8 50       25 if (/^\w/) {
279 8         10 undef $section;
280             }
281 8 100       402 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    50          
    0          
    0          
282 4         18 $boundary{routerid} = _s2lIP($1);
283 4         12 $boundary{age} = _time2sec($3);
284 4         19 $boundary{sequence} = "0x$4";
285             } elsif (/^RouterID: $SIP\s+Cost: $DEC$/) {
286 4         15 $boundary{asbrouter} = _s2lIP($1);
287 4         15 $boundary{metric} = $2;
288             } elsif (/^Nexthops\b/) {
289 0         0 $section = "nexthop";
290             } elsif (s/^\t//) {
291 0 0       0 if ($section eq "nexthop") {
292             # not part of LSDB, redundant in gated dump
293             } else {
294 0         0 die "$_ No boundary section.\n";
295             }
296             } else {
297 0         0 die "$_ Unknown boundary line.\n";
298             }
299             }
300 4         10 return \%boundary;
301             }
302              
303             sub parse_area {
304 2     2 0 4 my OSPF::LSDB::gated $self = shift;
305 2         84 my($area, @lines) = @_;
306 2         37 my %typename = (
307             Stub => [ "stubs" ], # not an RFC LSA type, redundant in gated dump
308             Router => [ routers => \&parse_router ],
309             SumNet => [ summarys => \&parse_summary ],
310             SumASB => [ boundarys => \&parse_boundary ],
311             Net => [ networks => \&parse_network ],
312             );
313 2         5 my($lsdb, $type, @type_lines);
314 2         7 foreach (@lines) {
315 710 100       1191 if (/^Link State Database:/) {
    100          
316 2 50       6 die "$_ Duplicate LSDB.\n" if $lsdb;
317 2         6 $lsdb = 1;
318             } elsif ($lsdb) {
319 600 50       783 if (/^Retransmission List:$/) {
320 0 0       0 $type
321             or die "Retransmission without LSA type\n";
322 0         0 warn "Retransmission list for $type->[0]\n";
323             }
324 600 100 100     1292 if (! /^\t/ && @type_lines) {
325 78 100 66     206 if ($type && $type->[1]) {
326 60         100 my($name, $lsaparser) = @$type;
327 60         106 my $lsa = $lsaparser->($self, @type_lines);
328 60         117 $lsa->{area} = $area;
329 60         69 push @{$self->{ospf}{database}{$name}}, $lsa;
  60         173  
330             }
331 78         127 undef @type_lines;
332 78         104 undef $type;
333             }
334 600 100       1579 if (s/^(\w+)\t//) {
    100          
    50          
    50          
335 78 50       190 $type = $typename{$1}
336             or die "Unknown LSA type: $1\n";
337 78         159 push @type_lines, $_;
338             } elsif(s/\t//) {
339 444 50       673 $type
340             or die "No LSA type\n";
341 444         792 push @type_lines, $_;
342             } elsif (/^Retransmission List:$/) {
343 0         0 $type = [ "retransmission" ];
344             } elsif(/^$/) {
345 78         117 undef $type;
346             } else {
347 0         0 die "$_ Unknown LSA line.\n";
348             }
349             }
350             }
351 2 50       71 if (@type_lines) {
352 0         0 die "Unprocessed LSA lines:\n", @type_lines;
353             }
354             }
355              
356             sub parse_externals {
357 2     2 0 6 my OSPF::LSDB::gated $self = shift;
358 2         84 my @lines = @_;
359 2         9 my @externals;
360             my($section);
361 2         7 foreach (@lines) {
362 684 100       1504 if (/^\w/) {
363 472         523 undef $section;
364             }
365 684 100       7175 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    100          
    100          
    100          
    50          
    100          
    50          
366 132         292 push @externals, {
367             routerid => _s2lIP($1),
368             age => _time2sec($3),
369             sequence => "0x$4",
370             };
371             } elsif (/^LSID: $SIP\s+Network: $SIP\s+Netmask: $SIP\s+Cost: $DEC$/) {
372 132         270 $externals[-1]{address} = _s2lIP($1);
373 132         300 $externals[-1]{netmask} = _s2lIP($3);
374 132         349 $externals[-1]{metric} = $4;
375             } elsif (/^Type: ([1-2])\s+Forward: $SIP\s+Tag: $TAG\b/) {
376 132         292 $externals[-1]{type} = $1;
377 132         207 $externals[-1]{forward} = _s2lIP($2);
378             } elsif (/^Nexthops\b/) {
379 76         161 $section = "nexthop";
380             } elsif (/^Retransmission List:$/) {
381 0         0 $section = "retransmission";
382 0         0 warn "Retransmission list for external\n";
383             } elsif (s/^\t//) {
384 76 50       204 if ($section eq "nexthop") {
    0          
385             # not part of LSDB, redundant in gated dump
386             } elsif ($section eq "retransmission") {
387             # not part of LSDB, internal gated information
388             } else {
389 0         0 die "$_ No external section.\n";
390             }
391             } elsif (/^$/) {
392 136         262 undef $section;
393             } else {
394 0         0 die "$_ Unknown external line.\n";
395             }
396             }
397 2         175 $self->{ospf}{database}{externals} = \@externals;
398             }
399              
400             sub parse_lsdb {
401 2     2 0 9 my OSPF::LSDB::gated $self = shift;
402 2         10 my($area_lines, $external_lines) = @_;
403 2         5 foreach my $area (@{$self->{ospf}{self}{areas}}) {
  2         18  
404 2         6 $self->parse_area($area, @{$area_lines->{$area}});
  2         19  
405             }
406 2         37 $self->parse_externals(@$external_lines);
407             }
408              
409             sub parse_ospf {
410 2     2 0 9 my OSPF::LSDB::gated $self = shift;
411 2         184 my @lines = @_;
412 2         14 my(%section, %area_lines, @external_lines);
413 2         0 my($routerid, @areas);
414 2         8 foreach (@lines) {
415 1688 100       2853 if (/^\w/) {
416 32         50 undef %section;
417 32 100       424 if (/^RouterID: $SIP\s+/) {
    100          
    100          
418 2         12 $routerid = _s2lIP($1);
419             } elsif (/^Area $SIP:/) {
420 2         8 my $area = _s2lIP($1);
421 2         8 push @areas, $area;
422 2         7 $section{area} = $area;
423             } elsif (/^AS Externals\s+/) {
424 2         7 $section{external} = 1;
425             }
426             } else {
427 1656         2775 s/^\t//;
428 1656 100       2481 if ($section{area}) {
    100          
429 710         675 push @{$area_lines{$section{area}}}, $_;
  710         1231  
430             } elsif ($section{external}) {
431 684         1031 push @external_lines, $_;
432             }
433             }
434             }
435 2 50       17 $self->{ospf}{self}{routerid} = $routerid
436             or die "No router id.\n";
437 2         6 $self->{ospf}{self}{areas} = \@areas;
438 2         12 $self->parse_lsdb(\%area_lines, \@external_lines);
439             }
440              
441             =pod
442              
443             =over 4
444              
445             =item $self-Eparse(%todo)
446              
447             This function takes a hash describing how the OSPF LSDB can be
448             obtained.
449             The bool value of C specifies wether the dump file should be
450             created dynamically by calling B.
451             The C parameter contains the path to the F file,
452             it defaults to F.
453             The dump file may contain more than one instance of the gated memory
454             dump separated by form feeds.
455             If the numeric B paremeter is set, that many dumps from the
456             beginning of the file are skipped and the next one is used.
457              
458             The complete OSPF link state database is stored in the B field
459             of the base class.
460              
461             =back
462              
463             =cut
464              
465             sub parse {
466 2     2 1 9 my OSPF::LSDB::gated $self = shift;
467 2         10 my %todo = @_;
468 2 50       9 if ($todo{dump}) {
469 0         0 $self->get_dump($todo{file});
470             } else {
471 2         16 @{$self->{dump}} = read_file($todo{file});
  2         5672  
472             }
473 2 50       37 my $skip = $todo{skip} + 1 if $todo{skip};
474 2         5 my($task, @ospf_lines);
475 2         5 my $n = 0;
476 2         4 foreach (@{$self->{dump}}) {
  2         10  
477 6904         6537 $n++;
478 6904 50       8379 if ($skip) {
479 0 0       0 if (/^\f$/) {
480 0         0 $skip--;
481             }
482 0         0 next;
483             }
484 6904 100       10486 if (/^\w/) {
485 30         42 undef $task;
486             }
487 6904 100       11936 if (/^Task (\w+):/) {
    100          
    100          
488 22         73 $task = lc($1);
489             } elsif (/^Done$/) {
490 2         11 last;
491             } elsif (defined $task) {
492 3210         5342 s/^\t//;
493 3210 100       4833 if ($task eq "ospf") {
494 1688         2345 push @ospf_lines, $_;
495             }
496             }
497             }
498 2 50       6 if ($n < @{$self->{dump}}) {
  2         18  
499 0         0 warn "More data in gated dump.\n";
500             }
501 2         66 $self->parse_ospf(@ospf_lines);
502 2         177 $self->{ospf}{ipv6} = 0;
503             }
504              
505             =pod
506              
507             This module has been tested with gated 3.6.
508             If it works with other versions is unknown.
509              
510             =head1 ERRORS
511              
512             The methods die if any error occurs.
513              
514             =head1 SEE ALSO
515              
516             L
517              
518             L
519              
520             =head1 AUTHORS
521              
522             Alexander Bluhm
523              
524             =cut
525              
526             1;