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   86017 use strict;
  4         18  
  4         111  
18 4     4   18 use warnings;
  4         7  
  4         186  
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   22 use base 'OSPF::LSDB';
  4         7  
  4         1884  
53 4     4   580 use File::Slurp;
  4         20514  
  4         273  
54 4     4   25 use Regexp::Common;
  4         6  
  4         31  
55 4         24 use fields qw(
56             dump
57 4     4   87466 );
  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   2069 my $ip = $_[0].".0.0.0";
87 912         2838 $ip =~ /^$RE{net}{IPv4}{-keep}/;
88 912         89226 return $1;
89             }
90              
91             # convert time to seconds
92             sub _time2sec($) {
93 192     192   622 my @a = split(/:/, "0:0:".$_[0]);
94 192         950 return 60*(60*$a[-3] + $a[-2]) + $a[-1];
95             }
96              
97             # convert On/Off to boolean 0/1
98 52 50   52   150 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 37 my OSPF::LSDB::gated $self = shift;
128 26         52 my($router, @lines) = @_;
129 26         81 my %typename = (
130             "Router" => "pointtopoint",
131             "Transit net" => "transit",
132             "Stub net" => "stub",
133             "Virtual" => "virtual",
134             );
135 26         34 my $type;
136             my $l;
137 26         51 foreach (@lines) {
138 164 100       1762 if (/Type: ([\w ]+)\s+Cost: $DEC$/) {
    50          
    100          
    50          
139 82 50       225 defined($type = $typename{$1})
140             or die "Unknown link type: $1\n";
141 82         180 $l = { metric => $2 };
142 82         104 push @{$router->{$type.'s'}}, $l;
  82         218  
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       134 if ($type eq "transit") {
152 64         105 $l->{address} = _s2lIP($1);
153 64         109 $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       37 if ($type eq "stub") {
159 18         25 $l->{network} = _s2lIP($1);
160 18         35 $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 52 my OSPF::LSDB::gated $self = shift;
172 26         52 my @lines = @_;
173 26         28 my %router;
174 26         31 my($section, @link_lines);
175 26         42 foreach (@lines) {
176 260 100       483 if (/^\w/) {
177 74         88 undef $section;
178             }
179 260 100       2098 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    100          
    100          
    50          
180 26         57 $router{routerid} = _s2lIP($1);
181 26         57 $router{age} = _time2sec($3);
182 26         87 $router{sequence} = "0x$4";
183             } elsif (/^RouterID: $SIP\s+Area Border: $OO\s+AS Border: $OO$/) {
184 26         54 $section = "link";
185 26         48 $router{router} = _s2lIP($1);
186 26         48 $router{bits}{B} = _oo2bool($2);
187 26         43 $router{bits}{E} = _oo2bool($3);
188 26         81 $router{bits}{V} = 0; # XXX need gated dump with virtual link
189             } elsif (/^Nexthops\b/) {
190 22         57 $section = "nexthop";
191             } elsif (s/^\t//) {
192 186 100       306 if ($section eq "link") {
    50          
193 164         283 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         94 $self->parse_links(\%router, @link_lines);
204 26         78 return \%router;
205             }
206              
207             sub parse_network {
208 26     26 0 36 my OSPF::LSDB::gated $self = shift;
209 26         46 my @lines = @_;
210 26         40 my %network;
211             my($section);
212 26         39 foreach (@lines) {
213 166 100       372 if (/^\w/) {
214 142         168 undef $section;
215             }
216 166 100       2067 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    100          
    100          
    100          
    50          
217 26         63 $network{routerid} = _s2lIP($1);
218 26         54 $network{age} = _time2sec($3);
219 26         84 $network{sequence} = "0x$4";
220             } elsif (/^Router: $SIP\s+Netmask: $SIP\s+Network: $SIP$/) {
221 26         52 $network{address} = _s2lIP($1);
222 26         48 $network{netmask} = _s2lIP($2);
223             } elsif (/^Attached Router: $SIP$/) {
224 66         94 push @{$network{attachments}}, { routerid => _s2lIP($1) };
  66         132  
225             } elsif (/^Nexthops\b/) {
226 24         48 $section = "nexthop";
227             } elsif (s/^\t//) {
228 24 50       65 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         55 return \%network;
238             }
239              
240             sub parse_summary {
241 4     4 0 7 my OSPF::LSDB::gated $self = shift;
242 4         12 my @lines = @_;
243 4         6 my %summary;
244             my($section);
245 4         7 foreach (@lines) {
246 16 100       39 if (/^\w/) {
247 12         17 undef $section;
248             }
249 16 100       565 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    100          
    100          
    50          
250 4         18 $summary{routerid} = _s2lIP($1);
251 4         10 $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         11 $summary{address} = _s2lIP($1);
255 4         12 $summary{netmask} = _s2lIP($3);
256 4         18 $summary{metric} = $4;
257             } elsif (/^Nexthops\b/) {
258 4         10 $section = "nexthop";
259             } elsif (s/^\t//) {
260 4 50       13 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         9 return \%summary;
270             }
271              
272             sub parse_boundary {
273 4     4 0 9 my OSPF::LSDB::gated $self = shift;
274 4         9 my @lines = @_;
275 4         5 my %boundary;
276             my($section);
277 4         7 foreach (@lines) {
278 8 50       22 if (/^\w/) {
279 8         10 undef $section;
280             }
281 8 100       382 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    50          
    0          
    0          
282 4         14 $boundary{routerid} = _s2lIP($1);
283 4         11 $boundary{age} = _time2sec($3);
284 4         17 $boundary{sequence} = "0x$4";
285             } elsif (/^RouterID: $SIP\s+Cost: $DEC$/) {
286 4         13 $boundary{asbrouter} = _s2lIP($1);
287 4         27 $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         35 return \%boundary;
301             }
302              
303             sub parse_area {
304 2     2 0 8 my OSPF::LSDB::gated $self = shift;
305 2         69 my($area, @lines) = @_;
306 2         27 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         4 my($lsdb, $type, @type_lines);
314 2         5 foreach (@lines) {
315 710 100       1165 if (/^Link State Database:/) {
    100          
316 2 50       6 die "$_ Duplicate LSDB.\n" if $lsdb;
317 2         4 $lsdb = 1;
318             } elsif ($lsdb) {
319 600 50       778 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     1231 if (! /^\t/ && @type_lines) {
325 78 100 66     195 if ($type && $type->[1]) {
326 60         105 my($name, $lsaparser) = @$type;
327 60         109 my $lsa = $lsaparser->($self, @type_lines);
328 60         108 $lsa->{area} = $area;
329 60         70 push @{$self->{ospf}{database}{$name}}, $lsa;
  60         162  
330             }
331 78         131 undef @type_lines;
332 78         94 undef $type;
333             }
334 600 100       1618 if (s/^(\w+)\t//) {
    100          
    50          
    50          
335 78 50       183 $type = $typename{$1}
336             or die "Unknown LSA type: $1\n";
337 78         152 push @type_lines, $_;
338             } elsif(s/\t//) {
339 444 50       628 $type
340             or die "No LSA type\n";
341 444         669 push @type_lines, $_;
342             } elsif (/^Retransmission List:$/) {
343 0         0 $type = [ "retransmission" ];
344             } elsif(/^$/) {
345 78         112 undef $type;
346             } else {
347 0         0 die "$_ Unknown LSA line.\n";
348             }
349             }
350             }
351 2 50       41 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         59 my @lines = @_;
359 2         10 my @externals;
360             my($section);
361 2         11 foreach (@lines) {
362 684 100       1451 if (/^\w/) {
363 472         536 undef $section;
364             }
365 684 100       7209 if (/^AdvRtr: $SIP\s+Len: $DEC\s+Age: $TIME\s+Seq: $HEX$/) {
    100          
    100          
    100          
    50          
    100          
    50          
366 132         277 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         249 $externals[-1]{address} = _s2lIP($1);
373 132         227 $externals[-1]{netmask} = _s2lIP($3);
374 132         352 $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         214 $externals[-1]{forward} = _s2lIP($2);
378             } elsif (/^Nexthops\b/) {
379 76         191 $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       179 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         269 undef $section;
393             } else {
394 0         0 die "$_ Unknown external line.\n";
395             }
396             }
397 2         99 $self->{ospf}{database}{externals} = \@externals;
398             }
399              
400             sub parse_lsdb {
401 2     2 0 4 my OSPF::LSDB::gated $self = shift;
402 2         8 my($area_lines, $external_lines) = @_;
403 2         4 foreach my $area (@{$self->{ospf}{self}{areas}}) {
  2         6  
404 2         2 $self->parse_area($area, @{$area_lines->{$area}});
  2         23  
405             }
406 2         31 $self->parse_externals(@$external_lines);
407             }
408              
409             sub parse_ospf {
410 2     2 0 7 my OSPF::LSDB::gated $self = shift;
411 2         141 my @lines = @_;
412 2         11 my(%section, %area_lines, @external_lines);
413 2         0 my($routerid, @areas);
414 2         5 foreach (@lines) {
415 1688 100       2876 if (/^\w/) {
416 32         42 undef %section;
417 32 100       303 if (/^RouterID: $SIP\s+/) {
    100          
    100          
418 2         9 $routerid = _s2lIP($1);
419             } elsif (/^Area $SIP:/) {
420 2         5 my $area = _s2lIP($1);
421 2         6 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         2810 s/^\t//;
428 1656 100       2537 if ($section{area}) {
    100          
429 710         653 push @{$area_lines{$section{area}}}, $_;
  710         1201  
430             } elsif ($section{external}) {
431 684         1017 push @external_lines, $_;
432             }
433             }
434             }
435 2 50       19 $self->{ospf}{self}{routerid} = $routerid
436             or die "No router id.\n";
437 2         5 $self->{ospf}{self}{areas} = \@areas;
438 2         7 $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 7 my OSPF::LSDB::gated $self = shift;
467 2         7 my %todo = @_;
468 2 50       6 if ($todo{dump}) {
469 0         0 $self->get_dump($todo{file});
470             } else {
471 2         10 @{$self->{dump}} = read_file($todo{file});
  2         5733  
472             }
473 2 50       35 my $skip = $todo{skip} + 1 if $todo{skip};
474 2         4 my($task, @ospf_lines);
475 2         4 my $n = 0;
476 2         4 foreach (@{$self->{dump}}) {
  2         7  
477 6904         6354 $n++;
478 6904 50       8156 if ($skip) {
479 0 0       0 if (/^\f$/) {
480 0         0 $skip--;
481             }
482 0         0 next;
483             }
484 6904 100       10155 if (/^\w/) {
485 30         36 undef $task;
486             }
487 6904 100       11934 if (/^Task (\w+):/) {
    100          
    100          
488 22         50 $task = lc($1);
489             } elsif (/^Done$/) {
490 2         8 last;
491             } elsif (defined $task) {
492 3210         7763 s/^\t//;
493 3210 100       4813 if ($task eq "ospf") {
494 1688         2324 push @ospf_lines, $_;
495             }
496             }
497             }
498 2 50       4 if ($n < @{$self->{dump}}) {
  2         10  
499 0         0 warn "More data in gated dump.\n";
500             }
501 2         32 $self->parse_ospf(@ospf_lines);
502 2         125 $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;