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   80305 use strict;
  4         39  
  4         102  
18 4     4   15 use warnings;
  4         8  
  4         177  
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   19 use base 'OSPF::LSDB';
  4         5  
  4         1367  
53 4     4   592 use File::Slurp;
  4         19201  
  4         264  
54 4     4   21 use Regexp::Common;
  4         7  
  4         33  
55 4         33 use fields qw(
56             dump
57 4     4   78975 );
  4         7  
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   1749 my $ip = $_[0].".0.0.0";
87 912         2576 $ip =~ /^$RE{net}{IPv4}{-keep}/;
88 912         78215 return $1;
89             }
90              
91             # convert time to seconds
92             sub _time2sec($) {
93 192     192   560 my @a = split(/:/, "0:0:".$_[0]);
94 192         822 return 60*(60*$a[-3] + $a[-2]) + $a[-1];
95             }
96              
97             # convert On/Off to boolean 0/1
98 52 50   52   143 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 31 my OSPF::LSDB::gated $self = shift;
128 26         53 my($router, @lines) = @_;
129 26         68 my %typename = (
130             "Router" => "pointtopoint",
131             "Transit net" => "transit",
132             "Stub net" => "stub",
133             "Virtual" => "virtual",
134             );
135 26         33 my $type;
136             my $l;
137 26         36 foreach (@lines) {
138 164 100       1525 if (/Type: ([\w ]+)\s+Cost: $DEC$/) {
    50          
    100          
    50          
139 82 50       210 defined($type = $typename{$1})
140             or die "Unknown link type: $1\n";
141 82         164 $l = { metric => $2 };
142 82         90 push @{$router->{$type.'s'}}, $l;
  82         183  
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       123 if ($type eq "transit") {
152 64         95 $l->{address} = _s2lIP($1);
153 64         116 $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       31 if ($type eq "stub") {
159 18         28 $l->{network} = _s2lIP($1);
160 18         31 $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 43 my OSPF::LSDB::gated $self = shift;
172 26         50 my @lines = @_;
173 26         25 my %router;
174 26         26 my($section, @link_lines);
175 26         34 foreach (@lines) {
176 260 100       434 if (/^\w/) {
177 74         77 undef $section;
178             }
179 260 100       1760 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    100          
    100          
    50          
180 26         66 $router{routerid} = _s2lIP($1);
181 26         42 $router{age} = _time2sec($3);
182 26         79 $router{sequence} = "0x$4";
183             } elsif (/^RouterID: $SIP\s+Area Border: $OO\s+AS Border: $OO$/) {
184 26         73 $section = "link";
185 26         40 $router{router} = _s2lIP($1);
186 26         54 $router{bits}{B} = _oo2bool($2);
187 26         37 $router{bits}{E} = _oo2bool($3);
188 26         49 $router{bits}{V} = 0; # XXX need gated dump with virtual link
189             } elsif (/^Nexthops\b/) {
190 22         51 $section = "nexthop";
191             } elsif (s/^\t//) {
192 186 100       266 if ($section eq "link") {
    50          
193 164         253 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         60 $self->parse_links(\%router, @link_lines);
204 26         67 return \%router;
205             }
206              
207             sub parse_network {
208 26     26 0 28 my OSPF::LSDB::gated $self = shift;
209 26         47 my @lines = @_;
210 26         29 my %network;
211             my($section);
212 26         32 foreach (@lines) {
213 166 100       317 if (/^\w/) {
214 142         136 undef $section;
215             }
216 166 100       1789 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    100          
    100          
    100          
    50          
217 26         58 $network{routerid} = _s2lIP($1);
218 26         48 $network{age} = _time2sec($3);
219 26         71 $network{sequence} = "0x$4";
220             } elsif (/^Router: $SIP\s+Netmask: $SIP\s+Network: $SIP$/) {
221 26         49 $network{address} = _s2lIP($1);
222 26         39 $network{netmask} = _s2lIP($2);
223             } elsif (/^Attached Router: $SIP$/) {
224 66         77 push @{$network{attachments}}, { routerid => _s2lIP($1) };
  66         117  
225             } elsif (/^Nexthops\b/) {
226 24         41 $section = "nexthop";
227             } elsif (s/^\t//) {
228 24 50       69 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         50 return \%network;
238             }
239              
240             sub parse_summary {
241 4     4 0 7 my OSPF::LSDB::gated $self = shift;
242 4         9 my @lines = @_;
243 4         5 my %summary;
244             my($section);
245 4         9 foreach (@lines) {
246 16 100       32 if (/^\w/) {
247 12         14 undef $section;
248             }
249 16 100       485 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    100          
    100          
    50          
250 4         21 $summary{routerid} = _s2lIP($1);
251 4         9 $summary{age} = _time2sec($3);
252 4         16 $summary{sequence} = "0x$4";
253             } elsif (/^LSID: $SIP\s+Network: $SIP\s+Netmask: $SIP\s+Cost: $DEC$/) {
254 4         14 $summary{address} = _s2lIP($1);
255 4         10 $summary{netmask} = _s2lIP($3);
256 4         16 $summary{metric} = $4;
257             } elsif (/^Nexthops\b/) {
258 4         9 $section = "nexthop";
259             } elsif (s/^\t//) {
260 4 50       12 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         10 return \%summary;
270             }
271              
272             sub parse_boundary {
273 4     4 0 7 my OSPF::LSDB::gated $self = shift;
274 4         7 my @lines = @_;
275 4         5 my %boundary;
276             my($section);
277 4         9 foreach (@lines) {
278 8 50       22 if (/^\w/) {
279 8         9 undef $section;
280             }
281 8 100       300 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    50          
    0          
    0          
282 4         13 $boundary{routerid} = _s2lIP($1);
283 4         10 $boundary{age} = _time2sec($3);
284 4         15 $boundary{sequence} = "0x$4";
285             } elsif (/^RouterID: $SIP\s+Cost: $DEC$/) {
286 4         18 $boundary{asbrouter} = _s2lIP($1);
287 4         14 $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         9 return \%boundary;
301             }
302              
303             sub parse_area {
304 2     2 0 3 my OSPF::LSDB::gated $self = shift;
305 2         74 my($area, @lines) = @_;
306 2         25 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         5 foreach (@lines) {
315 710 100       1055 if (/^Link State Database:/) {
    100          
316 2 50       6 die "$_ Duplicate LSDB.\n" if $lsdb;
317 2         3 $lsdb = 1;
318             } elsif ($lsdb) {
319 600 50       702 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     1067 if (! /^\t/ && @type_lines) {
325 78 100 66     177 if ($type && $type->[1]) {
326 60         92 my($name, $lsaparser) = @$type;
327 60         86 my $lsa = $lsaparser->($self, @type_lines);
328 60         104 $lsa->{area} = $area;
329 60         67 push @{$self->{ospf}{database}{$name}}, $lsa;
  60         137  
330             }
331 78         108 undef @type_lines;
332 78         77 undef $type;
333             }
334 600 100       1424 if (s/^(\w+)\t//) {
    100          
    50          
    50          
335 78 50       164 $type = $typename{$1}
336             or die "Unknown LSA type: $1\n";
337 78         137 push @type_lines, $_;
338             } elsif(s/\t//) {
339 444 50       542 $type
340             or die "No LSA type\n";
341 444         769 push @type_lines, $_;
342             } elsif (/^Retransmission List:$/) {
343 0         0 $type = [ "retransmission" ];
344             } elsif(/^$/) {
345 78         105 undef $type;
346             } else {
347 0         0 die "$_ Unknown LSA line.\n";
348             }
349             }
350             }
351 2 50       62 if (@type_lines) {
352 0         0 die "Unprocessed LSA lines:\n", @type_lines;
353             }
354             }
355              
356             sub parse_externals {
357 2     2 0 5 my OSPF::LSDB::gated $self = shift;
358 2         69 my @lines = @_;
359 2         5 my @externals;
360             my($section);
361 2         4 foreach (@lines) {
362 684 100       1238 if (/^\w/) {
363 472         464 undef $section;
364             }
365 684 100       5906 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    100          
    100          
    100          
    50          
    100          
    50          
366 132         245 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         214 $externals[-1]{address} = _s2lIP($1);
373 132         202 $externals[-1]{netmask} = _s2lIP($3);
374 132         312 $externals[-1]{metric} = $4;
375             } elsif (/^Type: ([1-2])\s+Forward: $SIP\s+Tag: $TAG\b/) {
376 132         261 $externals[-1]{type} = $1;
377 132         182 $externals[-1]{forward} = _s2lIP($2);
378             } elsif (/^Nexthops\b/) {
379 76         136 $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       168 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         224 undef $section;
393             } else {
394 0         0 die "$_ Unknown external line.\n";
395             }
396             }
397 2         202 $self->{ospf}{database}{externals} = \@externals;
398             }
399              
400             sub parse_lsdb {
401 2     2 0 4 my OSPF::LSDB::gated $self = shift;
402 2         6 my($area_lines, $external_lines) = @_;
403 2         5 foreach my $area (@{$self->{ospf}{self}{areas}}) {
  2         7  
404 2         3 $self->parse_area($area, @{$area_lines->{$area}});
  2         21  
405             }
406 2         33 $self->parse_externals(@$external_lines);
407             }
408              
409             sub parse_ospf {
410 2     2 0 8 my OSPF::LSDB::gated $self = shift;
411 2         135 my @lines = @_;
412 2         10 my(%section, %area_lines, @external_lines);
413 2         0 my($routerid, @areas);
414 2         5 foreach (@lines) {
415 1688 100       2480 if (/^\w/) {
416 32         35 undef %section;
417 32 100       279 if (/^RouterID: $SIP\s+/) {
    100          
    100          
418 2         12 $routerid = _s2lIP($1);
419             } elsif (/^Area $SIP:/) {
420 2         7 my $area = _s2lIP($1);
421 2         6 push @areas, $area;
422 2         6 $section{area} = $area;
423             } elsif (/^AS Externals\s+/) {
424 2         6 $section{external} = 1;
425             }
426             } else {
427 1656         2405 s/^\t//;
428 1656 100       2218 if ($section{area}) {
    100          
429 710         567 push @{$area_lines{$section{area}}}, $_;
  710         1046  
430             } elsif ($section{external}) {
431 684         924 push @external_lines, $_;
432             }
433             }
434             }
435 2 50       12 $self->{ospf}{self}{routerid} = $routerid
436             or die "No router id.\n";
437 2         7 $self->{ospf}{self}{areas} = \@areas;
438 2         11 $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 8 my OSPF::LSDB::gated $self = shift;
467 2         7 my %todo = @_;
468 2 50       7 if ($todo{dump}) {
469 0         0 $self->get_dump($todo{file});
470             } else {
471 2         11 @{$self->{dump}} = read_file($todo{file});
  2         4883  
472             }
473 2 50       31 my $skip = $todo{skip} + 1 if $todo{skip};
474 2         6 my($task, @ospf_lines);
475 2         4 my $n = 0;
476 2         4 foreach (@{$self->{dump}}) {
  2         7  
477 6904         5535 $n++;
478 6904 50       7162 if ($skip) {
479 0 0       0 if (/^\f$/) {
480 0         0 $skip--;
481             }
482 0         0 next;
483             }
484 6904 100       8821 if (/^\w/) {
485 30         29 undef $task;
486             }
487 6904 100       10432 if (/^Task (\w+):/) {
    100          
    100          
488 22         44 $task = lc($1);
489             } elsif (/^Done$/) {
490 2         6 last;
491             } elsif (defined $task) {
492 3210         4478 s/^\t//;
493 3210 100       4437 if ($task eq "ospf") {
494 1688         2000 push @ospf_lines, $_;
495             }
496             }
497             }
498 2 50       3 if ($n < @{$self->{dump}}) {
  2         12  
499 0         0 warn "More data in gated dump.\n";
500             }
501 2         42 $self->parse_ospf(@ospf_lines);
502 2         171 $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;