1package App::Netdisco::Configuration;
2
3use App::Netdisco::Environment;
4use App::Netdisco::Util::DeviceAuth ();
5use Dancer ':script';
6
7use Path::Class 'dir';
8use Net::Domain 'hostdomain';
9use File::ShareDir 'dist_dir';
10use URI::Based;
11
12BEGIN {
13  if (setting('include_paths') and ref [] eq ref setting('include_paths')) {
14    # stuff useful locations into @INC
15    push @{setting('include_paths')},
16         dir(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'nd-site-local', 'lib')->stringify
17      if (setting('site_local_files'));
18    unshift @INC, @{setting('include_paths')};
19  }
20}
21
22# set up database schema config from simple config vars
23if (ref {} eq ref setting('database')) {
24    # override from env for docker
25
26    setting('database')->{name} =
27      ($ENV{NETDISCO_DB_NAME} || $ENV{NETDISCO_DBNAME} || setting('database')->{name});
28
29    setting('database')->{host} =
30      ($ENV{NETDISCO_DB_HOST} || setting('database')->{host});
31
32    setting('database')->{host} .= (';'. $ENV{NETDISCO_DB_PORT})
33      if (setting('database')->{host} and $ENV{NETDISCO_DB_PORT});
34
35    setting('database')->{user} =
36      ($ENV{NETDISCO_DB_USER} || setting('database')->{user});
37
38    setting('database')->{pass} =
39      ($ENV{NETDISCO_DB_PASS} || setting('database')->{pass});
40
41    my $name = setting('database')->{name};
42    my $host = setting('database')->{host};
43    my $user = setting('database')->{user};
44    my $pass = setting('database')->{pass};
45
46    my $dsn = "dbi:Pg:dbname=${name}";
47    $dsn .= ";host=${host}" if $host;
48
49    # set up the netdisco schema now we have access to the config
50    # but only if it doesn't exist from an earlier config style
51    setting('plugins')->{DBIC}->{netdisco} ||= {
52        dsn  => $dsn,
53        user => $user,
54        password => $pass,
55        options => {
56            AutoCommit => 1,
57            RaiseError => 1,
58            auto_savepoint => 1,
59            pg_enable_utf8 => 1,
60        },
61        schema_class => 'App::Netdisco::DB',
62    };
63
64    foreach my $c (@{setting('external_databases')}) {
65        my $schema = delete $c->{tag} or next;
66        next if $schema eq 'netdisco';
67        setting('plugins')->{DBIC}->{$schema} = $c;
68        setting('plugins')->{DBIC}->{$schema}->{schema_class}
69          ||= 'App::Netdisco::GenericDB';
70    }
71}
72
73# always set this
74$ENV{DBIC_TRACE_PROFILE} = 'console';
75
76# override from env for docker
77config->{'community'} = ($ENV{NETDISCO_RO_COMMUNITY} ?
78  [split ',', $ENV{NETDISCO_RO_COMMUNITY}] : config->{'community'});
79config->{'community_rw'} = ($ENV{NETDISCO_RW_COMMUNITY} ?
80  [split ',', $ENV{NETDISCO_RW_COMMUNITY}] : config->{'community_rw'});
81
82# if snmp_auth and device_auth not set, add defaults to community{_rw}
83if ((setting('snmp_auth') and 0 == scalar @{ setting('snmp_auth') })
84    and (setting('device_auth') and 0 == scalar @{ setting('device_auth') })) {
85  config->{'community'} = [ @{setting('community')}, 'public' ];
86  config->{'community_rw'} = [ @{setting('community_rw')}, 'private' ];
87}
88# fix up device_auth (or create it from old snmp_auth and community settings)
89# also imports legacy sshcollector config
90config->{'device_auth'}
91  = [ App::Netdisco::Util::DeviceAuth::fixup_device_auth() ];
92
93# defaults for workers
94setting('workers')->{queue} ||= 'PostgreSQL';
95if ($ENV{ND2_SINGLE_WORKER}) {
96  setting('workers')->{tasks} = 1;
97  delete config->{'schedule'};
98}
99
100# force skipped DNS resolution, if unset
101setting('dns')->{hosts_file} ||= '/etc/hosts';
102setting('dns')->{no} ||= ['fe80::/64','169.254.0.0/16'];
103
104# set max outstanding requests for AnyEvent::DNS
105$ENV{'PERL_ANYEVENT_MAX_OUTSTANDING_DNS'}
106  = setting('dns')->{max_outstanding} || 50;
107$ENV{'PERL_ANYEVENT_HOSTS'} = setting('dns')->{hosts_file};
108
109# load /etc/hosts
110setting('dns')->{'ETCHOSTS'} = {};
111{
112  # AE::DNS::EtcHosts only works for A/AAAA/SRV, but we want PTR.
113  # this loads+parses /etc/hosts file using AE. dirty hack.
114  use AnyEvent::Socket 'format_address';
115  use AnyEvent::DNS::EtcHosts;
116  AnyEvent::DNS::EtcHosts::_load_hosts_unless(sub{},AE::cv);
117  no AnyEvent::DNS::EtcHosts; # unimport
118
119  setting('dns')->{'ETCHOSTS'}->{$_} =
120    [ map { [ $_ ? (format_address $_->[0]) : '' ] }
121          @{ $AnyEvent::DNS::EtcHosts::HOSTS{ $_ } } ]
122    for keys %AnyEvent::DNS::EtcHosts::HOSTS;
123}
124
125# override from env for docker
126if ($ENV{NETDISCO_DOMAIN}) {
127  if ($ENV{NETDISCO_DOMAIN} eq 'discover') {
128    delete $ENV{NETDISCO_DOMAIN};
129    if (! setting('domain_suffix')) {
130      info 'resolving domain name...';
131      config->{'domain_suffix'} = hostdomain;
132    }
133  }
134  else {
135    config->{'domain_suffix'} = $ENV{NETDISCO_DOMAIN};
136  }
137}
138
139# convert domain_suffix from scalar or list to regexp
140
141config->{'domain_suffix'} = [setting('domain_suffix')]
142  if ref [] ne ref setting('domain_suffix');
143
144if (scalar @{ setting('domain_suffix') }) {
145  my @suffixes = map { (ref qr// eq ref $_) ? $_ : quotemeta }
146                    @{ setting('domain_suffix') };
147  my $buildref = '(?:'. (join '|', @suffixes) .')$';
148  config->{'domain_suffix'} = qr/$buildref/;
149}
150else {
151  config->{'domain_suffix'} = qr//;
152}
153
154# convert radius and tacacs from single to lists
155
156if (ref {} eq ref setting('radius')
157  and exists setting('radius')->{'secret'}) {
158
159  my $servers = (ref [] eq ref setting('radius')->{'server'}
160    ? setting('radius')->{'server'} : [setting('radius')->{'server'}]);
161  config->{'radius'} = [
162    Secret => setting('radius')->{'secret'},
163    NodeList => $servers,
164  ];
165}
166
167if (ref {} eq ref setting('tacacs')
168  and exists setting('tacacs')->{'key'}) {
169
170  config->{'tacacs'} = [
171    Host => setting('tacacs')->{'server'},
172    Key  => setting('tacacs')->{'key'} || setting('tacacs')->{'secret'},
173    Port => (setting('tacacs')->{'port'} || 'tacacs'),
174    Timeout => (setting('tacacs')->{'timeout'} || 15),
175  ];
176}
177elsif (ref [] eq ref setting('tacacs')) {
178  my @newservers = ();
179  foreach my $server (@{ setting('tacacs') }) {
180    push @newservers, [
181      Host => $server->{'server'},
182      Key  => $server->{'key'} || $server->{'secret'},
183      Port => ($server->{'port'} || 'tacacs'),
184      Timeout => ($server->{'timeout'} || 15),
185    ];
186  }
187  config->{'tacacs'} = [ @newservers ];
188}
189
190# support unordered dictionary as if it were a single item list
191if (ref {} eq ref setting('device_identity')) {
192  config->{'device_identity'} = [ setting('device_identity') ];
193}
194else { config->{'device_identity'} ||= [] }
195
196# copy devices_no and devices_only into others
197foreach my $name (qw/devices_no devices_only
198                    discover_no macsuck_no arpnip_no nbtstat_no
199                    discover_only macsuck_only arpnip_only nbtstat_only/) {
200  config->{$name} ||= [];
201  config->{$name} = [setting($name)] if ref [] ne ref setting($name);
202}
203foreach my $name (qw/discover_no macsuck_no arpnip_no nbtstat_no/) {
204  push @{setting($name)}, @{ setting('devices_no') };
205}
206foreach my $name (qw/discover_only macsuck_only arpnip_only nbtstat_only/) {
207  push @{setting($name)}, @{ setting('devices_only') };
208}
209
210# legacy config item names
211
212config->{'devport_vlan_limit'} =
213  config->{'deviceport_vlan_membership_threshold'}
214  if setting('deviceport_vlan_membership_threshold')
215     and not setting('devport_vlan_limit');
216delete config->{'deviceport_vlan_membership_threshold'};
217
218config->{'schedule'} = config->{'housekeeping'}
219  if setting('housekeeping') and not setting('schedule');
220delete config->{'housekeeping'};
221
222# used to have separate types of worker
223if (exists setting('workers')->{interactives}
224    or exists setting('workers')->{pollers}) {
225
226    setting('workers')->{tasks} ||=
227      (setting('workers')->{pollers} || 0)
228      + (setting('workers')->{interactives} || 0);
229
230    delete setting('workers')->{pollers};
231    delete setting('workers')->{interactives};
232}
233
234# moved the timeout setting
235setting('workers')->{'timeout'} = setting('timeout')
236  if defined setting('timeout')
237     and !defined setting('workers')->{'timeout'};
238
239# 0 for workers max_deferrals and retry_after is like disabling
240# but we need to fake it with special values
241setting('workers')->{'max_deferrals'} ||= (2**30);
242setting('workers')->{'retry_after'}   ||= '100 years';
243
244# schedule expire used to be called expiry
245setting('schedule')->{expire} ||= setting('schedule')->{expiry}
246  if setting('schedule') and exists setting('schedule')->{expiry};
247delete config->{'schedule'}->{'expiry'} if setting('schedule');
248
249# upgrade reports config from hash to list
250if (setting('reports') and ref {} eq ref setting('reports')) {
251    config->{'reports'} = [ map {{
252        tag => $_,
253        %{ setting('reports')->{$_} }
254    }} keys %{ setting('reports') } ];
255}
256
257# add system_reports onto reports
258config->{'reports'} = [ @{setting('system_reports')}, @{setting('reports')} ];
259
260# set swagger ui location
261#config->{plugins}->{Swagger}->{ui_dir} =
262  #dir(dist_dir('App-Netdisco'), 'share', 'public', 'swagger-ui')->absolute;
263
264# setup helpers for when request->uri_for() isn't available
265# (for example when inside swagger_path())
266config->{url_base}
267  = URI::Based->new((config->{path} eq '/') ? '' : config->{path});
268config->{api_base}
269  = config->{url_base}->with('/api/v1')->path;
270
271true;
272