File Coverage

blib/lib/OSPF/LSDB/Cisco.pm
Criterion Covered Total %
statement 219 236 92.8
branch 198 252 78.5
condition 5 8 62.5
subroutine 17 18 94.4
pod 1 10 10.0
total 440 524 83.9


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 3     3   75738 use strict;
  3         12  
  3         79  
18 3     3   11 use warnings;
  3         13  
  3         117  
19              
20             =pod
21              
22             =head1 NAME
23              
24             OSPF::LSDB::Cisco - parse Cisco OSPF link state database
25              
26             =head1 SYNOPSIS
27              
28             use OSPF::LSDB::Cisco;
29              
30             my $cisco = OSPF::LSDB::Cisco-Enew();
31              
32             my $cisco = OSPF::LSDB::Cisco-Enew(ssh => "user@host");
33              
34             $cisco-Eparse(%files);
35              
36             =head1 DESCRIPTION
37              
38             The OSPF::LSDB::Cisco module parses the output of the Cisco OSPF
39             IOS and fills the L base object.
40             The output of
41             C,
42             C,
43             C,
44             C,
45             C,
46             C
47             is needed.
48             It can be given as separate files or obtained dynamically.
49             In the latter case B is invoked.
50             If the object has been created with the C argument, the specified
51             user and host are used to login otherwise C is used as host
52             name.
53              
54             There is only one public method:
55              
56             =cut
57              
58             package OSPF::LSDB::Cisco;
59 3     3   13 use base 'OSPF::LSDB';
  3         4  
  3         937  
60 3     3   821 use File::Slurp;
  3         33452  
  3         167  
61 3     3   29 use Regexp::Common;
  3         7  
  3         24  
62 3         26 use fields qw(
63             selfid
64             router network summary boundary external
65 3     3   75355 );
  3         6  
66              
67             # shortcut
68             my $IP = qr/$RE{net}{IPv4}{-keep}/;
69              
70             # convert prefix length to packed IPv4 address
71 16     16   93 sub _prefix2pack($) { pack("B32", "1"x$_[0]."0"x(32-$_[0])) }
72              
73             # convert packed IPv4 address to decimal dotted format
74 16     16   211 sub _pack2ip($) { join('.', unpack("CCCC", $_[0])) }
75              
76             sub ssh_show {
77 0     0 0 0 my OSPF::LSDB::Cisco $self = shift;
78 0   0     0 my $host = $self->{ssh} || "cisco";
79 0         0 my @cmd = ("ssh", $host, "show", "ip", "ospf", @_);
80 0 0       0 my @lines = wantarray ? `@cmd` : scalar `@cmd`;
81 0 0       0 die "Command '@cmd' failed: $?\n" if $?;
82 0 0       0 return wantarray ? @lines : $lines[0];
83             }
84              
85             sub read_files {
86 1     1 0 1 my OSPF::LSDB::Cisco $self = shift;
87 1         4 my %files = @_;
88 1         2 my $file = $files{selfid};
89 1 50       5 my @lines = $file ? read_file($file) : $self->ssh_show();
90 1         158 $self->{selfid} = \@lines;
91 1         5 my %show = (
92             router => "router",
93             network => "network",
94             summary => "summary",
95             boundary => "asbr-summary",
96             external => "external",
97             );
98 1         3 foreach (qw(router network summary boundary external)) {
99 5         10 my $file = $files{$_};
100             my @lines = $file ?
101 5 50       15 read_file($file) : $self->ssh_show("database", $show{$_});
102 5         652 $self->{$_} = \@lines;
103             }
104             }
105              
106             sub parse_self {
107 1     1 0 2 my OSPF::LSDB::Cisco $self = shift;
108 1         2 my($routerid, @areas);
109 1         1 foreach (@{$self->{selfid}}) {
  1         3  
110 63         108 s/\r\n/\n/;
111 63 100       356 if (/^ Routing Process "[- \w]*" with ID $IP$/) {
    100          
    100          
112 1         4 $routerid = $1;
113             } elsif (/^ Area $IP(?: \(Inactive\))?$/) {
114 2         5 push @areas, $1;
115             } elsif (/^ Area BACKBONE\((0\.0\.0\.0)\)(?: \(Inactive\))?$/) {
116 1         3 push @areas, $1;
117             }
118             }
119 1         4 $self->{ospf}{self} = { routerid => $routerid, areas => \@areas };
120             }
121              
122             sub parse_router {
123 1     1 0 2 my OSPF::LSDB::Cisco $self = shift;
124 1         3 my($area, $router, $link) = ("", "", "");
125 1         2 my(@routers, $lnum, $type, $r, $l);
126 1         1 foreach (@{$self->{router}}) {
  1         4  
127 109         9039 s/\r\n/\n/;
128 109 100 66     742 if (/^ +OSPF Router with ID \($IP\) \(Process ID \d+\)$/) {
    100 100        
    100          
    100          
    100          
129             # XXX TOOD implement support for multiple processes
130 1         2 next;
131             } elsif (/^\t+Router Link States \(Area $IP\)$/) {
132 2 50       6 die "$_ Link $link of router $router in area $area not finished.\n"
133             if $l;
134 2 50       3 die "$_ Too few links at router $router in area $area.\n" if $lnum;
135 2 50       4 die "$_ Router $router in area $area not finished.\n" if $r;
136 2         4 $area = $1;
137 2         3 next;
138             } elsif (/^$/) {
139 22 100       39 if ($l) {
140 7 50       13 die "Link $link of router $router in area $area without type.\n"
141             if ! $type;
142 7         6 push @{$r->{$type.'s'}}, $l;
  7         17  
143 7         10 undef $type;
144 7         8 undef $l;
145             }
146 22 100       25 if (! $lnum) {
147 15         16 undef $r;
148             }
149 22         35 next;
150             } elsif (! $r && /^ \w/) {
151 5 50       8 die "$_ No area for router defined.\n" if ! $area;
152 5         6 $router = "";
153 5         17 $r = { area => $area, bits => { V => 0, E => 0, B => 0 } };
154 5         12 push @routers, $r;
155             } elsif (! $l && /^ {4}\w/) {
156 7 50       10 die "$_ Too many links at router $router in area $area.\n"
157             if ! $lnum;
158 7         10 $lnum--;
159 7         8 $link = "";
160 7         11 $l = {};
161             }
162 84 100       273 if (/^ LS age: (?:MAXAGE\()?$RE{num}{int}{-keep}\)?$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    50          
    50          
    50          
163 5         618 $r->{age} = $1;
164             } elsif (/^ LS Type: ([\w ()]+)$/) {
165 5 50       486 die "$_ Type of router-LSA is $1 and not Links Router ".
166             "in area $area.\n" if $1 ne "Router Links";
167             } elsif (/^ Link State ID: $IP$/) {
168 5         532 $r->{router} = $1;
169 5         25 $router = $1;
170             } elsif (/^ Advertising Router: $IP$/) {
171 5         492 $r->{routerid} = $1;
172             } elsif (/^ LS Seq Number: $RE{num}{hex}{-keep}$/) {
173 5         1006 $r->{sequence} = "0x$1";
174             } elsif (/^ AS Boundary Router$/) {
175 1         199 $r->{bits}{E} = 1;
176             } elsif (/^ Area Border Router$/) {
177 2         399 $r->{bits}{B} = 1;
178             } elsif (/^ Number of Links: $RE{num}{int}{-keep}$/) {
179 5         1437 $lnum = $1;
180             } elsif (/^ Link connected to: ([\w -]+)$/) {
181 7 50       2035 if ($1 eq "a Point-to-Point") {
    100          
    50          
    0          
182 0         0 $type = "pointtopoint";
183             } elsif ($1 eq "a Transit Network") {
184 6         56 $type = "transit";
185             } elsif ($1 eq "a Stub Network") {
186 1         10 $type = "stub";
187             } elsif ($1 eq "a Virtual Link") {
188 0         0 $type = "virtual";
189             } else {
190 0         0 die "$_ Unknown link type $1 at router $router ".
191             "in area $area.\n";
192             }
193             } elsif (/^ Link ID \(Neighbors Router ID\): $IP$/) {
194 0         0 $l->{routerid} = $1;
195 0         0 $link = $1;
196             } elsif (/^ \(Link ID\) Designated Router address: $IP$/) {
197 6         1764 $l->{address} = $1;
198 6         58 $link = $1;
199             } elsif (/^ \(Link ID\) Network\/subnet number: $IP$/) {
200 1         301 $l->{network} = $1;
201 1         11 $link = $1;
202             } elsif (/^ \(Link Data\) Router Interface address: $IP$/) {
203 6         1803 $l->{interface} = $1;
204             } elsif (/^ \(Link Data\) Network Mask: $IP$/) {
205 1         297 $l->{netmask} = $1;
206             } elsif (/^ Number of TOS metrics: 0$/) {
207             # TOS metrics unsupported
208             } elsif (/^ TOS 0 Metrics: $RE{num}{int}{-keep}$/) {
209 7         2784 $l->{metric} = $1;
210             } elsif (/^ {4}\w/) {
211 0         0 die "$_ Unknown line at link $link of router $router ".
212             "in area $area.\n";
213             } elsif (/^ Routing Bit Set on this LSA$/) {
214             } elsif (/^ Adv Router is not-reachable$/) {
215             } elsif (/^ Delete flag is set for this LSA$/) {
216             } elsif (! /^ (Options|Checksum|Length):/) {
217 0         0 die "$_ Unknown line at router $router in area $area.\n";
218             }
219             }
220 1 50       4 die "Link $link of router $router in area $area not finished.\n" if $l;
221 1 50       2 die "Too few links at router $router in area $area.\n" if $lnum;
222 1 50       2 die "Router $router in area $area not finished.\n" if $r;
223 1         5 $self->{ospf}{database}{routers} = \@routers;
224             }
225              
226             sub parse_network {
227 1     1 0 2 my OSPF::LSDB::Cisco $self = shift;
228 1         3 my($area, $network) = ("", "");
229 1         1 my(@networks, $attachments, $n);
230 1         2 foreach (@{$self->{network}}) {
  1         3  
231 47         3916 s/\r\n/\n/;
232 47 100       335 if (/^ +OSPF Router with ID \($IP\) \(Process ID \d+\)$/) {
    100          
    100          
    100          
233             # XXX TOOD implement support for multiple processes
234 1         3 next;
235             } elsif (/^\t+Net Link States \(Area $IP\)$/) {
236 2 50       5 die "$_ Attached routers of network $network in area $area ".
237             "not finished.\n" if $attachments;
238 2 50       4 die "$_ Network $network in area $area not finished.\n" if $n;
239 2         5 $area = $1;
240 2         3 next;
241             } elsif (/^$/) {
242 8         10 undef $attachments;
243 8         7 undef $n;
244 8         14 next;
245             } elsif (! $n) {
246 3 50       6 die "$_ No area for network defined.\n" if ! $area;
247 3         4 $network = "";
248 3         29 $n = { area => $area };
249 3         5 push @networks, $n;
250             }
251 36 100       130 if (/^ LS age: (?:MAXAGE\()?$RE{num}{int}{-keep}\)?$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    50          
    50          
252 3         290 $n->{age} = $1;
253             } elsif (/^ LS Type: ([\w ()]+)$/) {
254 3 50       294 die "$_ Type of network-LSA is $1 and not Network Links ".
255             "in area $area.\n" if $1 ne "Network Links";
256             } elsif (/^ Link State ID: $IP \(address of Designated Router\)$/) {
257 3         280 $n->{address} = $1;
258 3         15 $network = $1;
259             } elsif (/^ Advertising Router: $IP$/) {
260 3         317 $n->{routerid} = $1;
261             } elsif (/^ LS Seq Number: $RE{num}{hex}{-keep}$/) {
262 3         610 $n->{sequence} = "0x$1";
263             } elsif (/^ Network Mask: \/$RE{num}{int}{-keep}$/) {
264 3         840 $n->{netmask} = _pack2ip(_prefix2pack($1));
265             } elsif (/^\tAttached Router: $IP$/) {
266 6 100       1728 if (! $attachments) {
267 3         4 $attachments = [];
268 3         5 $n->{attachments} = $attachments;
269             }
270 6         62 push @$attachments, { routerid => $1 };
271             } elsif (/^ Routing Bit Set on this LSA$/) {
272             } elsif (/^ Adv Router is not-reachable$/) {
273             } elsif (/^ Delete flag is set for this LSA$/) {
274             } elsif (! /^ (Options|Checksum|Length):/) {
275 0         0 die "$_ Unknown line at network $network in area $area.";
276             }
277             }
278 1         4 $self->{ospf}{database}{networks} = \@networks;
279             }
280              
281             sub parse_summary {
282 1     1 0 2 my OSPF::LSDB::Cisco $self = shift;
283 1         2 my($area, $summary) = ("", "");
284 1         2 my(@summarys, $s);
285 1         2 foreach (@{$self->{summary}}) {
  1         11  
286 52         5085 s/\r\n/\n/;
287 52 100       370 if (/^ +OSPF Router with ID \($IP\) \(Process ID \d+\)$/) {
    100          
    100          
    100          
288             # XXX TOOD implement support for multiple processes
289 1         2 next;
290             } elsif (/^\t+Summary Net Link States \(Area $IP\)$/) {
291 2 50       5 die "$_ Summary $summary in area $area not finished.\n" if $s;
292 2         4 $area = $1;
293 2         4 next;
294             } elsif (/^$/) {
295 9         14 undef $s;
296 9         15 next;
297             } elsif (! $s) {
298 4 50       7 die "$_ No area for summary defined.\n" if ! $area;
299 4         5 $summary = "";
300 4         8 $s = { area => $area };
301 4         6 push @summarys, $s;
302             }
303 40 100       119 if (/^ LS age: (?:MAXAGE\()?$RE{num}{int}{-keep}\)?$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    50          
    50          
    50          
304 4         446 $s->{age} = $1;
305             } elsif (/^ LS Type: ([\w ()]+)$/) {
306 4 50       387 die "$_ Type of summary-LSA is $1 and not Summary Links(Network) ".
307             "in area $area.\n" if $1 ne "Summary Links(Network)";
308             } elsif (/^ Link State ID: $IP \(summary Network Number\)$/) {
309 4         373 $s->{address} = $1;
310 4         22 $summary = $1;
311             } elsif (/^ Advertising Router: $IP$/) {
312 4         392 $s->{routerid} = $1;
313             } elsif (/^ LS Seq Number: $RE{num}{hex}{-keep}$/) {
314 4         828 $s->{sequence} = "0x$1";
315             } elsif (/^ Network Mask: \/$RE{num}{int}{-keep}$/) {
316 4         1132 $s->{netmask} = _pack2ip(_prefix2pack($1));
317             } elsif (/^\tTOS: 0 \tMetric: $RE{num}{int}{-keep} $/) {
318 4         1530 $s->{metric} = $1;
319             } elsif (/^ Routing Bit Set on this LSA$/) {
320             } elsif (/^ Adv Router is not-reachable$/) {
321             } elsif (/^ Delete flag is set for this LSA$/) {
322             } elsif (! /^ (Options|Checksum|Length):/) {
323 0         0 die "$_ Unknown line at summary $summary in area $area.\n";
324             }
325             }
326 1         5 $self->{ospf}{database}{summarys} = \@summarys;
327             }
328              
329             sub parse_boundary {
330 1     1 0 2 my OSPF::LSDB::Cisco $self = shift;
331 1         2 my($area, $boundary) = ("", "");
332 1         2 my(@boundarys, $b);
333 1         2 foreach (@{$self->{boundary}}) {
  1         4  
334 16         1420 s/\r\n/\n/;
335 16 100       193 if (/^ +OSPF Router with ID \($IP\) \(Process ID \d+\)$/) {
    100          
    100          
    100          
336             # XXX TOOD implement support for multiple processes
337 1         3 next;
338             } elsif (/^\t+Summary ASB Link States \(Area $IP\)$/) {
339 1 50       4 die "$_ Boundary $boundary in area $area not finished.\n" if $b;
340 1         2 $area = $1;
341 1         2 next;
342             } elsif (/^$/) {
343 4         6 undef $b;
344 4         9 next;
345             } elsif (! $b) {
346 1 50       3 die "$_ No area for boundary defined.\n" if ! $area;
347 1         2 $boundary = "";
348 1         2 $b = { area => $area };
349 1         2 push @boundarys, $b;
350             }
351 10 100       31 if (/^ LS age: (?:MAXAGE\()?$RE{num}{int}{-keep}\)?$/) {
    100          
    100          
    100          
    100          
    100          
    50          
    50          
    50          
    50          
352 1         134 $b->{age} = $1;
353             } elsif (/^ LS Type: ([\w ()]+)$/) {
354 1 50       103 die "$_ Type of boundary-LSA is $1 and not ".
355             "Summary Links(AS Boundary Router) in area $area.\n"
356             if $1 ne "Summary Links(AS Boundary Router)";
357             } elsif (/^ Link State ID: $IP \(AS Boundary Router address\)$/) {
358 1         96 $b->{asbrouter} = $1;
359 1         6 $boundary = $1;
360             } elsif (/^ Advertising Router: $IP$/) {
361 1         115 $b->{routerid} = $1;
362             } elsif (/^ LS Seq Number: $RE{num}{hex}{-keep}$/) {
363 1         206 $b->{sequence} = "0x$1";
364             } elsif (/^\tTOS: 0 \tMetric: $RE{num}{int}{-keep} $/) {
365 1         288 $b->{metric} = $1;
366             } elsif (/^ Routing Bit Set on this LSA$/) {
367             } elsif (/^ Adv Router is not-reachable$/) {
368             } elsif (/^ Delete flag is set for this LSA$/) {
369             } elsif (! /^ (Options|Checksum|Length|Network Mask):/) {
370 0         0 die "$_ Unknown line at boundary $boundary in area $area.\n";
371             }
372             }
373 1         4 $self->{ospf}{database}{boundarys} = \@boundarys;
374             }
375              
376             sub parse_external {
377 1     1 0 2 my OSPF::LSDB::Cisco $self = shift;
378 1         2 my $external = "";
379 1         1 my(@externals, $e);
380 1         2 foreach (@{$self->{external}}) {
  1         3  
381 149         21955 s/\r\n/\n/;
382 149 100       693 if (/^ +OSPF Router with ID \($IP\) \(Process ID \d+\)$/) {
    100          
    100          
    100          
383             # XXX TOOD implement support for multiple processes
384 1         3 next;
385             } elsif (/^\t+Type-5 AS External Link States$/) {
386 1 50       3 die "$_ External $external not finished.\n" if $e;
387 1 50       4 die "$_ Too many external sections.\n", if @externals;
388 1         2 next;
389             } elsif (/^$/) {
390 12         17 undef $e;
391 12         19 next;
392             } elsif (! $e) {
393 9         16 $external = "";
394 9         10 $e = {};
395 9         15 push @externals, $e;
396             }
397 135 100       400 if (/^ LS age: (?:MAXAGE\()?$RE{num}{int}{-keep}\)?$/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    50          
    50          
398 9         866 $e->{age} = $1;
399             } elsif (/^ LS Type: ([\w ()]+)$/) {
400 9 50       863 die "$_ Type of external-LSA is $1 and not AS External Link.\n"
401             if $1 ne "AS External Link";
402             } elsif (/^ Link State ID: $IP \(External Network Number \)$/) {
403 9         846 $e->{address} = $1;
404 9         41 $external = $1;
405             } elsif (/^ Advertising Router: $IP$/) {
406 9         883 $e->{routerid} = $1;
407             } elsif (/^ LS Seq Number: $RE{num}{hex}{-keep}$/) {
408 9         1841 $e->{sequence} = "0x$1";
409             } elsif (/^ Network Mask: \/$RE{num}{int}{-keep}$/) {
410 9         2582 $e->{netmask} = _pack2ip(_prefix2pack($1));
411             } elsif (/^\tMetric Type: ([1-2]) /) {
412 9         2689 $e->{type} = $1;
413             } elsif (/^\tMetric: $RE{num}{int}{-keep} $/) {
414 9         4049 $e->{metric} = $1;
415             } elsif (/^\tForward Address: $IP$/) {
416 9         3705 $e->{forward} = $1;
417             } elsif (/^\t(TOS|External Route Tag):/) {
418             } elsif (/^ Routing Bit Set on this LSA$/) {
419             } elsif (/^ Adv Router is not-reachable$/) {
420             } elsif (/^ Delete flag is set for this LSA$/) {
421             } elsif (! /^ (Options|Checksum|Length):/) {
422 0         0 die "$_ Unknown line at external $external.";
423             }
424             }
425 1         399 $self->{ospf}{database}{externals} = \@externals;
426             }
427              
428             sub parse_lsdb {
429 1     1 0 3 my OSPF::LSDB::Cisco $self = shift;
430 1         3 $self->parse_router();
431 1         5 $self->parse_network();
432 1         4 $self->parse_summary();
433 1         4 $self->parse_boundary();
434 1         4 $self->parse_external();
435             }
436              
437             =pod
438              
439             =over 4
440              
441             =item $self-Eparse(%files)
442              
443             This function takes a hash with file names as value containing the
444             Cisco C output data.
445             The hash keys are named C, C, C, C,
446             C, C.
447             If a hash entry is missing, B to the Cisco router is run instead
448             to obtain the information dynamically.
449              
450             The complete OSPF link state database is stored in the B field
451             of the base class.
452              
453             =back
454              
455             =cut
456              
457             sub parse {
458 1     1 1 5 my OSPF::LSDB::Cisco $self = shift;
459 1         4 my %files = @_;
460 1         5 $self->read_files(%files);
461 1         4 $self->parse_self();
462 1         4 $self->parse_lsdb();
463 1         8 $self->{ospf}{ipv6} = 0;
464             }
465              
466             =pod
467              
468             This module has been tested with Cisco IOS 12.4.
469             If it works with other versions is unknown.
470              
471             =head1 ERRORS
472              
473             The methods die if any error occurs.
474              
475             =head1 SEE ALSO
476              
477             L
478              
479             L
480              
481             =head1 AUTHORS
482              
483             Alexander Bluhm
484              
485             =head1 BUGS
486              
487             Cisco support is experimental.
488             This module is far from complete.
489              
490             No support for multiple router processes.
491              
492             No support for IPv6.
493              
494             =cut
495              
496             1;