Getting
to Know Your Network -- Part II
Luis Enrique Muñoz
In the first article of this series, I showed how to build a script,
aconfig, which allowed us to quickly execute commands on network
devices (http://www.sysadminmag.com/documents/s=9950/sam0602h/).
It also allowed us to use Perl to generate commands on the fly and
process the result of those commands. To tell aconfig what to do,
I wrote some small snippets of configuration commands, called ascripts.
Those ascripts are used to gather version and configuration information
from the network devices. In this article, I'll take the process
one step further and build all this into a neat network inventory.
(The complete listings for this article are available from the Sys
Admin Web site at: http://www.sysadminmag.com.)
A network inventory, for our purposes, is not much more than a
database of network devices, allowing us to store information regarding
those devices. So let's pick up just where we left off last month.
We had two files for each network device. The one with a ".version"
extension contained the results of the show version command.
The other, with a ".config" extension, contained the output of the
show run command. This is valuable because now tools like
find and grep could help us to locate interesting
information, but this approach is clearly not the best we can do.
For easier manipulation, we should now look at ways to transfer
the information on these files to a database. This database would
let us quickly find information about our network using SQL queries.
As I'll show later, this information can easily include rather nice
network diagrams. But first, let's work on getting at the juicy
bits of information.
The database schema we will be using through this article is shown
in Figure 1. For now, we are only concerned with tables "device",
"interface", "address", and "subnet". The rest will be used later
when we use additional scripts to learn about other devices plugged
into our network.
For this article, I'll be using SQLite, a small database manager
that runs on a wide variety of systems. In fact, this could even
be used for your production setup. The SQL commands to produce the
schema depicted in Listing 1 (myconfigCDBI.pm) are as follows:
CREATE TABLE device
(
device TEXT PRIMARY KEY,
location TEXT,
contact TEXT,
serial TEXT,
hardware TEXT,
software TEXT,
class TEXT
);
CREATE TABLE interface
(
interface TEXT,
device TEXT,
description TEXT,
PRIMARY KEY (interface, device),
FOREIGN KEY (device) REFERENCES device (device)
);
CREATE TABLE address
(
ip INTEGER,
interface TEXT,
device TEXT,
cidr TEXT,
type TEXT,
PRIMARY KEY (ip, interface, device),
FOREIGN KEY (cidr) REFERENCES subnet (cidr),
FOREIGN KEY (interface) REFERENCES interface (interface),
FOREIGN KEY (device) REFERENCES device (device)
);
CREATE TABLE subnet
(
cidr TEXT PRIMARY KEY,
first INTEGER,
last INTEGER
);
CREATE TABLE endpoint
(
endpoint TEXT PRIMARY KEY,
os TEXT,
time INTEGER,
vendor TEXT
);
CREATE TABLE sighting
(
endpoint TEXT,
time INTEGER,
interface TEXT,
device TEXT,
PRIMARY KEY (endpoint, time, interface, device),
FOREIGN KEY (endpoint) REFERENCES endpoint (endpoint),
FOREIGN KEY (device) REFERENCES device (device),
FOREIGN KEY (device, interface) REFERENCES interface (device, interface)
);
CREATE TABLE assignment
(
ip INTEGER,
time TEXT,
endpoint TEXT,
cidr TEXT,
PRIMARY KEY (ip, time),
FOREIGN KEY (endpoint) REFERENCES endpoint (endpoint),
FOREIGN KEY (cidr) REFERENCES subnet (cidr)
);
Provided that we place these SQL commands in the file "config.sql",
the database can easily be created with this command:
$ sqlite3 ~/config.db < config.sql
There are a few choices of what to use to access the database from
the Perl scripts used to extract information from the files in our
output directory. Personal taste seems to be a significant criterion
here. In this article, I'll use Class::DBI, which provides a nice
object-oriented layer above the database. With Class::DBI, we will
be spared the mix of Perl and SQL in the same scripts, unless we really
want that.
The examples presented in this article perform many tasks on the
database client that could be done at the server with more complex
SQL. For small networks, infrequent use, or demonstration purposes,
this is perfectly acceptable. However, when higher performance is
required, you will want to pay attention to this important detail.
I've placed all the class definitions in a single file that I
called "MyConfigCDBI.pm" (see Listing 1), so I can invoke them from
the scripts that need access to the database. I am also including
Class::DBI::AbstractSearch so that more complex searches can be
performed by the Perl scripts. We will use this later.
The code shown previously takes care of lots of things that happen
conveniently out of the way, such as providing simple accessors
to columns and relationships, building the SQL required to fetch
the requested objects, caching and efficiently managing database
resources, and updating the object when required. With this, the
next script, which is responsible for extracting device data and
loading it into the database, becomes much simpler because it does
not even have to include SQL in the code. Let's look at the important
parts of the script, "config2db" (Listing 2).
1: #!/usr/bin/perl
2:
3: use strict;
4: use warnings;
5: use lib 'scripts';
6: use IO::File;
7: use NetAddr::IP;
8: use Cisco::Reconfig;
9: use MyConfigCDBI;
10: $|++;
Lines 1 to 10 are our usual script invocation. I've added the use
lib, which allows us to conveniently tell Perl where to look for
the modules we invoke, in addition to the standard places. This lets
us "use MyConfigDBI" to bring in all the Class::DBI related definitions
presented earlier. Cisco::Reconfig will be of help to parse the configuration
files and get to what we want. NetAddr::IP will help with parsing
the IP addresses and subnets we find within the config and converting
them to the format we need in the database.
14: sub get_version
15: {
16: my $fh = new IO::File $_[0];
17: my $version; # The output of show version
18: my $class;
19:
20: unless ($fh)
21: {
22: warn "Failed to open version file $_[0]: $!\n";
23: return (map { 'Could not read version file' } 1 .. 3);
24: }
25:
26: do { local $/ = undef; $version = <$fh>; };
27: close $fh;
28:
29: my $sw = ($version =~ m/^(.+Software .+ Version .+)$/m)[0]
30: || 'Cannot find software version';
31: my $hw = ($version =~
32: m/^(cisco .+ (processor .+|bytes of \
memory\.?))$/mi)[0]
33: || 'Cannot find hardware version';
34: my $sn = ($version =~ m/processor board ID \
(\w+( \([^\)]+\))?)/mi)[0]
35: || 'Cannot find serial number';
36:
37: if ($version =~ m/cisco\s(Cat6k-|WS-C(29|3[57]|40))/)
38: { $class = 'switch' }
39: elsif ($version =~ /cisco\s(2[56]\d\d|18\d\d
40: |WS-C65\d\d|17\d\d|CISCO76\d\d)\s/ix)
41: { $class = 'router' }
42: elsif ($version =~ /cisco\sAIR-AP/)
43: { $class = 'access-point' }
44:
45: return ($sw, $hw, $sn, $class);
46: }
Lines 14 to 46 define the get_version function, which is responsible
for parsing the output of the show version command stored previously,
and they return various pieces of useful information for our inventory
-- namely, which version of software is in the device, what class
of hardware it is, serial or identification number, and a generic
class that we will use later to produce some nice diagrams.
Essentially, this code slurps in the file with the version information
and applies a number of regular expressions to it for extraction
of the relevant information. I've left here the regexes that I think
are most robust or general. Depending on which network devices you
happen to have, you may want to look at their version information
and adjust the regular expressions to fit your needs.
48: sub get_base_info
49: {
50: my %k = map { $_ => $_[0]->get($_) }
51: ('hostname', 'snmp-server contact', 'snmp-server location');
52:
53: return map { $k{$_} =~ s/^$_\s+//; chomp($k{$_}); $k{$_} } \
sort keys %k;
54: }
In our local environment, the network staff is pretty obsessive about
the SNMP configuration, which contains very useful details, such as
the location of the device and who should be contacted in case of
failure of said device. This, together with the device's hostname,
is extracted by the get_base_info function on lines 48 to 54.
56: sub get_interfaces
57: {
58: my $config = shift;
59: my @ifs = ();
60: for my $if ($config->get('interface')->all())
61: {
62: my @container = (($if->text =~ m/interface (\S+)/)[0]);
63: next unless $container[0];
64: my $desc = ($if->get('description')->text =~ \
m/description (.+)$/)[0];
65: push @container, $desc;
66: for my $addr ($if->get('ip address'))
67: {
68: next if $addr->text =~ m/no ip address/;
69: next unless $addr->text;
70: my ($ip, $mask, $class) =
71: $addr->text =~ m/ip address (\S+) \
(\S+)\s*(secondary)?/;
72: $class ||= 'primary';
73: my $i = NetAddr::IP->new($ip, $mask);
74: next unless $i;
75: push @container, [ $i, $class ];
76: }
77: push @ifs, \@container;
78: }
79: return @ifs;
80: }
Lines 56 to 80 define the get_interfaces function, which goes
through the configuration file and parses the interface information,
extracting the name, description, and possibly the list of IP addresses.
This code is made simpler with the help of Cisco::Reconfig, which
handles the subtle details of parsing.
82: MyConfig::CDBI->connection('dbi:SQLite:dbname=config.db');
With all those definitions in place, it's time to begin processing
our config files. Line 82 creates the connection to the database.
Thanks to the inheritance used when defining the Class::DBI classes,
this line is all that's needed. Note that I am not specifying the
full path of the database, so I have to run my scripts in the directory
in which the database resides.
84: for my $c (@ARGV)
85: {
86: my $config = readconfig($c);
87: my ($dev, $loc, $ctc) = get_base_info($config);
89: if (not defined $dev)
90: {
91: warn "$c does not define a device host name\n";
92: next;
93: }
...
138: }
Lines 84 to 138 simply loop over the specified configuration files
passed in the command line. In line 86, the configuration file is
read by Cisco::Reconfig's readconfig. In line 87, we start
getting basic information with the get_base_info function introduced
previously. Lines 89-93 are a simple sanity check to make sure we
parsed useful information from the given files. Otherwise, we let
our user know and skip to the next file.
95: my $vc = version_file($c);
96: my ($sw, $hw, $sn, $cl) = get_version($vc);
In lines 95-96, we invoke the get_version function to extract
information from the show version output matching the configuration
file we're reading.
100: my $device = MyConfig::CDBI::Device->find_or_create(
101: device => $dev,
102: location => $loc,
103: contact => $ctc,
104: serial => $sn,
105: hardware => $hw,
106: software => $sw,
107: class => $cl
108: );
Finally, at lines 100 through 108, we start using the services of
Class::DBI. Here, we attempt to look up a device in the database with
the given characteristics. If we're unable to find it, it gets created
automatically.
112: for my $if (get_interfaces($config))
113: {
114: my $ifname = lc shift @$if;
115: my $desc = shift @$if;
116: print "$c $device $ifname\n";
117: my $interface = MyConfig::CDBI::Interface->find_or_create(
118: interface => $ifname,
119: device => $dev,
120: description => $desc
121: );
122: for my $ip (@$if)
123: {
124: my $subnet = MyConfig::CDBI::Subnet->find_or_create(
125: cidr => $ip->[0]->network->cidr,
126: first => scalar $ip->[0]->network->numeric,
127: last => scalar $ip->[0]->broadcast->numeric,
128: );
129: my $address = MyConfig::CDBI::Address->find_or_create(
130: ip => scalar $ip->[0]->numeric,
131: interface => lc $interface->interface,
132: device => $device->device,
133: cidr => $subnet->cidr,
134: type => $ip->[1],
135: );
136: }
137: }
At lines 112 to 137, we use the same technique to add interfaces and
subnets, assigning the IP addresses of the interfaces when defined.
Now, it's only a matter of using find to invoke the script, which
in my case is done like this:
$ find ./output -name '*.config' -type f | xargs scripts/config2db
Thanks to line 116, in my case, I see many, many lines like the following:
./output/10.64.106.129.config CT-TC-ZUL loopback0
./output/10.64.106.129.config CT-TC-ZUL fastethernet0
./output/10.64.106.129.config CT-TC-ZUL serial0
./output/10.64.141.1.config CORE02-P1-BLDG1 loopback0
./output/10.64.141.1.config CORE02-P1-BLDG1 multilink1
./output/10.64.141.1.config CORE02-P1-BLDG1 multilink2
./output/10.64.141.1.config CORE02-P1-BLDG1 port-channel1
./output/10.64.141.1.config CORE02-P1-BLDG1 port-channel2
./output/10.64.141.1.config CORE02-P1-BLDG1 serial1/0/0:0
./output/10.64.141.1.config CORE02-P1-BLDG1 serial1/0/1:0
./output/10.64.141.1.config CORE02-P1-BLDG1 serial1/0/2:0
./output/10.64.141.1.config CORE02-P1-BLDG1 serial1/0/3:0
...
At this point, our database should be nicely populated with device
inventory. Let's do a simple test to show to put this information
to some use. Say, for example, that a new vulnerability exists, affecting
devices running Cisco IOS 12.0(5)WC7. We could very quickly find out
how serious this hypothetical threat would be for us:
$ sqlite3 config.db
sqlite> select count(*) from device where software like '%12.0(5)WC7%';
30
So, according to our recent inventory, 30 devices in our corporate
network would be vulnerable. We can also get an idea about where the
problem areas would lie in our network:
sqlite> select count(*),location from device where software
...> like '%12.0(5)WC7%' group by location;
10|Floor 1 Bldg1
10|Floor 2 Bldg1
9|Floor 3 Bldg1
1|Basement Bldg1
This threat could affect operations in one of our buildings. We can
get the list of IP addresses required to reach those devices and perform
the upgrade. In this last case, since the database is storing the
integer representation of the IP address we want, we will combine
the query with a one-liner Perl script to convert the data on the
fly:
$ echo "select a.ip from device d, address a where d.software like
'%12.0(5)WC7%' and d.device = a.device;" | sqlite3 config.db | \
perl -MNetAddr::IP -pe '$_ = NetAddr::IP->new($_) . "\n"'
10.64.73.10/32
10.64.73.11/32
...
With the tools introduced so far, you could also write an ascript
that goes out to those devices and determines whether they are vulnerable
in a more fine-grained way. Many vulnerabilities require a certain
configuration to be present in the device, so you could check this.
Better yet, it could apply suitable workarounds or perform an automatic
upgrade. I did this with a few thousand DSL modems a few years ago.
In the next article, we'll gather some more information about
our network, including what's plugged into it, and also produce
some pretty diagrams.
Luis has been working in various areas of computer science
since the late 1980s. Some people blame him for conspiring to bring
the Internet into his home country, where currently he spends most
of his time teaching others about Perl and taking care of network
security at the largest ISP there as its CISO. He also believes
that being a sys admin is supposed to be fun. |