1package App::Netdisco::Worker::Plugin::MakeRancidConf;
2
3use strict;
4use warnings;
5
6use Dancer ':syntax';
7use Dancer::Plugin::DBIC;
8
9use App::Netdisco::Worker::Plugin;
10use aliased 'App::Netdisco::Worker::Status';
11
12use Path::Class;
13use List::Util qw/pairkeys pairfirst/;
14use File::Slurper qw/read_lines write_text/;
15use App::Netdisco::Util::Permission 'check_acl_no';
16
17register_worker({ phase => 'main' }, sub {
18  my ($job, $workerconf) = @_;
19  my $config = setting('rancid') || {};
20
21  my $domain_suffix = setting('domain_suffix') || '';
22  my $delimiter = $config->{delimiter} || ';';
23  my $down_age  = $config->{down_age} || '1 day';
24  my $default_group = $config->{default_group} || 'default';
25
26  my $rancidconf = $config->{rancid_conf} || '/etc/rancid';
27  my $rancidcvsroot = $config->{rancid_cvsroot}
28    || dir($ENV{NETDISCO_HOME}, 'rancid')->stringify;
29  mkdir $rancidcvsroot if ! -d $rancidcvsroot;
30  return Status->error("cannot create or access rancid cvsroot: $rancidcvsroot")
31    if ! -d $rancidcvsroot;
32
33  my $allowed_types = {};
34  foreach my $type (qw/base conf/) {
35    my $type_file = file($rancidconf, "rancid.types.$type")->stringify;
36    debug sprintf("trying rancid configuration file %s\n", $type_file);
37    next unless -f $type_file;
38    my @lines = read_lines($type_file);
39    foreach my $line (@lines) {
40      next if $line =~ m/^(?:\#|\$)/;
41      $allowed_types->{$1} += 1 if $line =~ m/^([a-z0-9_\-]+);login;.*$/;
42    }
43  }
44
45  return Status->error("You didn't have any device types configured in your rancid installation.")
46    if ! scalar keys %$allowed_types;
47
48  my $devices = schema('netdisco')->resultset('Device')->search(undef, {
49    '+columns' => { old =>
50      \['age(now(), last_discover) > ?::interval', $down_age] },
51  });
52
53  $config->{groups}    ||= { default => 'any' };
54  $config->{vendormap} ||= {};
55  $config->{excluded}    ||= [];
56  $config->{by_ip}       ||= [];
57  $config->{by_hostname} ||= [];
58
59  # fix #686 excluded setting should be (ACL) list not dict
60  if (ref {} eq ref $config->{excluded}) {
61    $config->{excluded} = [ values %{ $config->{excluded} } ];
62  }
63
64  my $routerdb = {};
65  my $routerunicity = {};
66  while (my $d = $devices->next) {
67
68    if (check_acl_no($d, $config->{excluded})) {
69      debug " skipping $d: device excluded of export";
70      next
71    }
72
73    my $name = check_acl_no($d, $config->{by_ip}) ? $d->ip : ($d->dns || $d->name);
74    $name =~ s/$domain_suffix$// if check_acl_no($d, $config->{by_hostname});
75
76    my ($group) =
77      (pairkeys pairfirst { check_acl_no($d, $b) } %{ $config->{groups} }) || $default_group;
78
79    if (exists($routerunicity->{$group}->{$name})) {
80      debug " skipping $d: device excluded because already present in export list";
81      next;
82    }
83
84    my ($vendor) =
85      (pairkeys pairfirst { check_acl_no($d, $b) } %{ $config->{vendormap} })
86        || $d->vendor;
87
88    if (not ($name and $vendor)) {
89      debug " skipping $d: the name or vendor is not defined";
90      next
91    } elsif ($vendor =~ m/(?:enterprises\.|netdisco)/) {
92      debug " skipping $d with unresolved vendor: $vendor";
93      next;
94    } elsif (scalar keys %$allowed_types and !exists($allowed_types->{$vendor})) {
95      debug " skipping $d: $vendor doesn't exist in rancid's vendor list";
96      next;
97    }
98
99    push @{$routerdb->{$group}},
100      (sprintf "%s${delimiter}%s${delimiter}%s", $name, $vendor,
101        ($d->get_column('old') ? 'down' : 'up'));
102    $routerunicity->{$group}->{$name} = 1;
103  }
104
105  foreach my $group (keys %$routerdb) {
106    mkdir dir($rancidcvsroot, $group)->stringify;
107    my $content = "#\n# Router list file for rancid group $group.\n";
108    $content .= "# Generate automatically by App::Netdisco::Worker::Plugin::MakeRancidConf\n#\n";
109    $content .= join "\n", sort @{$routerdb->{$group}};
110    write_text(file($rancidcvsroot, $group, 'router.db')->stringify, "${content}\n");
111  }
112
113  return Status->done('Wrote rancid configuration.');
114});
115
116true;
117
118=encoding utf8
119
120=head1 NAME
121
122MakeRancidConf - Generate rancid Configuration
123
124=head1 INTRODUCTION
125
126This worker will generate a rancid configuration for all devices in Netdisco.
127
128Optionally you can provide configuration to control the output, however the
129defaults are sane for rancid versions 3.x and will create one rancid group
130called C<default> which contains all devices. Those devices not discovered
131successfully within the past day will be marked as C<down> for rancid to skip.
132Configuration is saved to the F<~/rancid> subdirectory of Netdisco's home folder.
133
134Note that this only generates the router.db files, you will still need to
135configure rancid's F<.cloginrc> and schedule C<rancid-run> to run.
136
137You could run this worker at 09:05 each day using the following configuration:
138
139 schedule:
140   makerancidconf:
141     when: '5 9 * * *'
142
143Since MakeRancidConf is a worker module it can also be run via C<netdisco-do>:
144
145 ~/bin/netdisco-do makerancidconf
146
147Skipped devices and the reason for skipping them can be seen by using C<-D>:
148
149 ~/bin/netdisco-do makerancidconf -D
150
151=head1 CONFIGURATION
152
153Here is a complete example of the configuration, which must be called
154C<rancid>. All keys are optional:
155
156 rancid:
157   rancid_cvsroot:  '$ENV{NETDISCO_HOME}/rancid' # default
158   rancid_conf:     '/etc/rancid'                # default
159   down_age:        '1 day'                      # default
160   delimiter:       ';'                          # default
161   default_group:   'default'                    # default
162   groups:
163     groupname1:    'host_group1_acl'
164     groupname2:    'host_group2_acl'
165   vendormap:
166     vname1:        'host_group3_acl'
167     vname2:        'host_group4_acl'
168   excluded:
169     - 'host_group5_acl'
170     - 'another.host.example.com'
171   by_ip:           'host_group6_acl'
172   by_hostname:     'host_group7_acl'
173
174Note that the default directory for writing files is not F</var/lib/rancid> so
175you may wish to set this in C<rancid_cvsroot>, (especially if migrating from the old
176C<netdisco-rancid-export> script).
177
178Any values above that are a host group ACL will take either a single item or
179a list of network identifiers or device properties. See the L<ACL
180documentation|https://github.com/netdisco/netdisco/wiki/Configuration#access-control-lists>
181wiki page for full details. We advise you to use the C<host_groups> setting
182and then refer to named entries in that, for example:
183
184 host_groups:
185   coredevices: '192.0.2.0/24'
186   edgedevices: '172.16.0.0/16'
187   grp-nxos:    'os:nx-os'
188
189 rancid:
190   groups:
191     core_devices: 'group:coredevices'
192     edge_devices: 'group:edgedevices'
193   vendormap:
194     cisco-nx:     'group:grp-nxos'
195   by_ip:          'any'
196
197Do not forget that rancid also needs configuring when adding a new group,
198such as scheduling the group to run, adding it to F<rancid.conf>, setting up the
199email config and creating the repository with C<rancid-cvs>.
200
201=head2 C<rancid_conf>
202
203The location where the rancid configuration (F<rancid.types.base> and
204F<rancid.types.conf>) is installed. It will be used to check the existence
205of device types before exporting the devices to the rancid configuration. If no match
206is found the device will not be added to rancid.
207
208=head2 C<rancid_cvsroot>
209
210The location to write rancid group configuration files (F<router.db>) into. A
211subdirectory for each group will be created.
212
213=head2 C<down_age>
214
215This should be the same or greater than the interval between regular discover
216jobs on your network. Devices which have not been discovered within this time
217will be marked as C<down> to rancid.
218
219The format is any time interval known and understood by PostgreSQL, such as at
220L<https://www.postgresql.org/docs/10/static/functions-datetime.html>.
221
222=head2 C<delimiter>
223
224Set this to the delimiter character for your F<router.db> entries if needed to
225be different from the default, the default is C<;>.
226
227=head2 C<default_group>
228
229Put devices into this group if they do not match any other groups defined.
230
231=head2 C<groups>
232
233This dictionary maps rancid group names with configuration which will match
234devices in the Netdisco database.
235
236The left hand side (key) should be the rancid group name, the right hand side
237(value) should be a L<Netdisco
238ACL|https://github.com/netdisco/netdisco/wiki/Configuration#access-control-lists>
239to select devices in the Netdisco database.
240
241=head2 C<vendormap>
242
243If the device vendor in Netdisco is not the same as the rancid vendor script or
244device type, configure a mapping here.
245
246The left hand side (key) should be the rancid device type, the right hand side
247(value) should be a L<Netdisco
248ACL|https://github.com/netdisco/netdisco/wiki/Configuration#access-control-lists>
249to select devices in the Netdisco database.
250
251Note that vendors might have a large array of operating systems which require
252different rancid modules. Mapping operating systems to rancid device types is
253a good solution to use the correct device type. Example:
254
255 host_groups:
256   grp-ciscosb:   'os:ros'
257
258 rancid:
259   vendormap:
260     cisco-sb:    'group:grp-ciscosb'
261
262=head2 C<excluded>
263
264L<Netdisco
265ACL|https://github.com/netdisco/netdisco/wiki/Configuration#access-control-lists>
266to identify devices that will be excluded from the rancid configuration.
267
268=head2 C<by_ip>
269
270L<Netdisco
271ACL|https://github.com/netdisco/netdisco/wiki/Configuration#access-control-lists>
272to select devices which will be written to the rancid config as an IP address,
273instead of the DNS FQDN or SNMP hostname.
274
275=head2 C<by_hostname>
276
277L<Netdisco
278ACL|https://github.com/netdisco/netdisco/wiki/Configuration#access-control-lists>
279to select devices which will have the unqualified hostname written to the
280rancid config. This is done simply by stripping the C<domain_suffix>
281configuration setting from the device FQDN.
282
283=head1 SEE ALSO
284
285=over 4
286
287=item *
288
289L<http://www.shrubbery.net/rancid/>
290
291=item *
292
293L<https://github.com/ytti/oxidized>
294
295=item *
296
297L<https://github.com/netdisco/netdisco/wiki/Configuration#access-control-lists>
298
299=back
300
301=cut
302