1#!/usr/bin/env perl 2 3use strict; 4use warnings; 5 6our $home; 7 8BEGIN { 9 use FindBin; 10 FindBin::again(); 11 12 $home = ($ENV{NETDISCO_HOME} || $ENV{HOME}); 13 14 # try to find a localenv if one isn't already in place. 15 if (!exists $ENV{PERL_LOCAL_LIB_ROOT}) { 16 use File::Spec; 17 my $localenv = File::Spec->catfile($FindBin::RealBin, 'localenv'); 18 exec($localenv, $0, @ARGV) if -f $localenv; 19 $localenv = File::Spec->catfile($home, 'perl5', 'bin', 'localenv'); 20 exec($localenv, $0, @ARGV) if -f $localenv; 21 22 die "Sorry, can't find libs required for App::Netdisco.\n" 23 if !exists $ENV{PERLBREW_PERL}; 24 } 25} 26 27BEGIN { 28 use Path::Class; 29 30 # stuff useful locations into @INC and $PATH 31 unshift @INC, 32 dir($FindBin::RealBin)->parent->subdir('lib')->stringify, 33 dir($FindBin::RealBin, 'lib')->stringify; 34 35 use Config; 36 $ENV{PATH} = $FindBin::RealBin . $Config{path_sep} . $ENV{PATH}; 37} 38 39use App::Netdisco; 40use Dancer ':script'; 41use Dancer::Plugin::DBIC 'schema'; 42use Dancer::Plugin::Passphrase; 43use App::Netdisco::Util::Statistics (); 44 45info "App::Netdisco $App::Netdisco::VERSION loaded."; 46 47use 5.010_000; 48use Term::UI; 49use Term::ReadLine; 50use Term::ANSIColor; 51 52use Archive::Extract; 53$Archive::Extract::PREFER_BIN = 1; 54use File::Slurper 'read_lines'; 55use HTTP::Tiny; 56use Digest::MD5; 57use Try::Tiny; 58use File::Path (); 59use File::Copy (); 60use Encode; 61 62=head1 NAME 63 64netdisco-deploy - Database, OUI and MIB deployment for Netdisco 65 66=head1 USAGE 67 68This script deploys the Netdisco database schema, OUI data, and MIBs. Each of 69these is an optional service which the user is asked to confirm. 70 71Pre-existing requirements are that there be a database table created and a 72user with rights to create tables in that database. Both the table and user 73name must match those configured in your environment YAML file (default 74F<~/environments/deployment.yml>). 75 76This script will download the latest MAC address vendor prefix data from the 77Internet, and update the OUI table in the database. Hence Internet access is 78required to run the script. 79 80Similarly the latest Netdisco MIB bundle is also downloaded and placed into 81the user's home directory (or C<$ENV{NETDISCO_HOME}>). 82 83If you upgrade Netdisco make sure you run this script again to make sure 84your config remains compatible. 85 86Before each upgrade also review the 87L<Release notes|https://github.com/netdisco/netdisco/wiki/Release-Notes> since 88additional steps might be required! 89 90=cut 91 92print color 'bold cyan'; 93say 'This is the Netdisco 2 deployment script.'; 94say ''; 95say 'Before we continue, the following prerequisites must be in place:'; 96say ' * Database added to PostgreSQL for Netdisco'; 97say ' * User added to PostgreSQL with rights to the Netdisco Database'; 98say ' * "~/environments/deployment.yml" file configured with Database dsn/user/pass'; 99say ' * A full backup of any existing Netdisco database data'; 100say ' * Internet access (for OUIs and MIBs)'; 101say ''; 102say 'If you are upgrading Netdisco 2 read the release notes:'; 103say 'https://github.com/netdisco/netdisco/wiki/Release-Notes'; 104say 'There you will find required and incompatible changes'; 105say 'which are not covered by this script.'; 106say ''; 107say 'You will be asked to confirm all changes to your system.'; 108say ''; 109print color 'reset'; 110 111my $term = Term::ReadLine->new('netdisco'); 112my $bool = $term->ask_yn( 113 prompt => 'So, is all of the above in place?', default => 'n', 114); 115 116exit(0) unless $bool; 117 118say ''; 119$bool = $term->ask_yn( 120 prompt => 'Would you like to deploy the database schema?', default => 'n', 121); 122deploy_db() if $bool; 123 124say ''; 125$bool = $term->ask_yn( 126 prompt => 'Download and update vendor MAC prefixes (OUI data)?', default => 'n', 127); 128deploy_oui() if $bool; 129 130say ''; 131my $default_mibhome = dir($home, 'netdisco-mibs'); 132if (setting('mibhome') and setting('mibhome') ne $default_mibhome) { 133 my $mibhome = $term->get_reply( 134 print_me => "MIB home options:", 135 prompt => "Download and update MIB files to...?", 136 choices => [setting('mibhome'), $default_mibhome, 'Skip this.'], 137 default => 'Skip this.', 138 ); 139 deploy_mibs($mibhome) if $mibhome and $mibhome ne 'Skip this.'; 140} 141else { 142 $bool = $term->ask_yn( 143 prompt => "Download and update MIB files?", default => 'n', 144 ); 145 deploy_mibs($default_mibhome) if $bool; 146} 147 148sub deploy_db { 149 system 'netdisco-db-deploy'; 150 print color 'bold blue'; 151 say 'DB schema update complete.'; 152 print color 'reset'; 153 154 print color 'bold blue'; 155 print 'Updating statistics... '; 156 App::Netdisco::Util::Statistics::update_stats(); 157 say 'done.'; 158 print color 'reset'; 159 160 if (not setting('safe_password_store')) { 161 say ''; 162 print color 'bold red'; 163 say '*** WARNING: Weak password hashes are being stored in the database! ***'; 164 say '*** WARNING: Please add "safe_password_store: true" to your ~/environments/deployment.yml file. ***'; 165 print color 'reset'; 166 } 167 168 sub _make_password { 169 my $pass = (shift || passphrase->generate_random); 170 if (setting('safe_password_store')) { 171 return passphrase($pass)->generate; 172 } 173 else { 174 return Digest::MD5::md5_hex($pass), 175 } 176 } 177 178 # set up initial admin user 179 my $users = schema('netdisco')->resultset('User'); 180 if ($users->search({-bool => 'admin'})->count == 0) { 181 say ''; 182 print color 'bold green'; 183 say 'We need to create a user for initial login. This user will be a full Administrator.'; 184 say 'Afterwards, you can go to Admin -> User Management to manage users.'; 185 print color 'reset'; 186 say ''; 187 188 my ($name, $pass) = get_userpass($term); 189 $users->create({ 190 username => $name, 191 password => _make_password($pass), 192 admin => 'true', 193 port_control => 'true', 194 }); 195 196 print color 'bold blue'; 197 say 'New user created.'; 198 print color 'reset'; 199 } 200 201 # set initial dancer web session cookie key 202 schema('netdisco')->resultset('Session')->find_or_create( 203 {id => 'dancer_session_cookie_key', a_session => \'md5(random()::text)'}, 204 {key => 'primary'}, 205 ); 206} 207 208sub get_userpass { 209 my $upterm = shift; 210 my $name = $upterm->get_reply(prompt => 'Username: '); 211 my $pass = $upterm->get_reply(prompt => 'Password: '); 212 213 unless ($name and $pass) { 214 say 'username and password cannot be empty, please try again.'; 215 ($name, $pass) = get_userpass($upterm); 216 } 217 218 return ($name, $pass); 219} 220 221sub deploy_oui { 222 my $schema = schema('netdisco'); 223 $schema->storage->disconnect; 224 my @lines = (); 225 my %data = (); 226 227 if (@ARGV) { 228 @lines = File::Slurper::read_lines($ARGV[0]); 229 } 230 else { 231 my $url = 'https://raw.githubusercontent.com/netdisco/upstream-sources/master/ieee/oui.txt'; 232 my $resp = HTTP::Tiny->new->get($url); 233 @lines = split /\n/, $resp->{content}; 234 } 235 236 if (scalar @lines > 50) { 237 foreach my $line (@lines) { 238 if ($line =~ m/^\s*(.{2}-.{2}-.{2})\s+\(hex\)\s+(.*)\s*$/i) { 239 my ($oui, $company) = ($1, $2); 240 $oui =~ s/-/:/g; 241 $company =~ s/[\r\n]//g; 242 my $abbrev = shorten($company); 243 $data{lc($oui)}{'company'} = $company; 244 $data{lc($oui)}{'abbrev'} = $abbrev; 245 } 246 } 247 248 if ((scalar keys %data) > 15_000) { 249 $schema->txn_do(sub{ 250 $schema->resultset('Oui')->delete; 251 $schema->resultset('Oui')->populate([ 252 map { 253 { oui => $_, 254 company => Encode::decode('UTF-8', $data{$_}{'company'}), 255 abbrev => Encode::decode('UTF-8', $data{$_}{'abbrev'}), 256 } 257 } keys %data 258 ]); 259 }); 260 } 261 262 print color 'bold blue'; 263 say 'OUI update complete.'; 264 } 265 else { 266 print color 'bold red'; 267 say 'OUI update failed!'; 268 } 269 270 print color 'reset'; 271} 272 273# This subroutine is based on Wireshark's make-manuf 274# http://anonsvn.wireshark.org/wireshark/trunk/tools/make-manuf 275sub shorten { 276 my $manuf = shift; 277 278 $manuf = decode("utf8", $manuf, Encode::FB_CROAK) unless @ARGV; 279 $manuf = " " . $manuf . " "; 280 281 # Remove any punctuation 282 $manuf =~ tr/',.()/ /; 283 284 # & isn't needed when Standalone 285 $manuf =~ s/ \& / /g; 286 287 # remove junk whitespace 288 $manuf =~ s/\s+/ /g; 289 290 # Remove any "the", "inc", "plc" ... 291 $manuf 292 =~ s/\s(?:the|inc|incorporated|plc|systems|corp|corporation|s\/a|a\/s|ab|ag|kg|gmbh|co|company|limited|ltd|holding|spa)(?= )//gi; 293 294 # Convert to consistent case 295 $manuf =~ s/(\w+)/\u\L$1/g; 296 297 # Deviating from make-manuf for HP 298 $manuf =~ s/Hewlett[-]?Packard/Hp/; 299 300 # Truncate all names to first two words max 20 chars 301 if (length($manuf) > 21) { 302 my @twowords = grep {defined} (split ' ', $manuf)[0 .. 1]; 303 $manuf = join ' ', @twowords; 304 } 305 306 # Remove all spaces 307 $manuf =~ s/\s+//g; 308 309 return encode( "utf8", $manuf ); 310} 311 312sub deploy_mibs { 313 my $mibhome = dir(shift); # /path/to/netdisco-mibs 314 my $fail = 0; 315 316 my $latest = 'https://github.com/netdisco/netdisco-mibs/releases/latest'; 317 my $resp = HTTP::Tiny->new->get($latest); 318 319 if ($resp->{url} =~ m/([0-9.]+)$/) { 320 my $ver = $1; 321 my $url = "https://github.com/netdisco/netdisco-mibs/releases/download/${ver}/netdisco-mibs.tar.gz"; 322 my $file = file($home, 'netdisco-mibs.tar.gz'); 323 $resp = HTTP::Tiny->new->mirror($url, $file); 324 325 if ($resp->{success}) { 326 my $ae = Archive::Extract->new(archive => $file, type => 'tgz'); 327 $ae->extract(to => $mibhome->parent->stringify); 328 329 my $from = file($mibhome->parent->stringify, "netdisco-mibs-$ver"); 330 my $to = file($mibhome->parent->stringify, 'netdisco-mibs'); 331 332 if (-d $from) { 333 File::Path::remove_tree($to, { verbose => 0 }); 334 File::Copy::move($from, $to); 335 } 336 unlink $file; 337 } 338 else { ++$fail } 339 } 340 else { ++$fail } 341 342 if ($fail) { 343 print color 'bold red'; 344 say 'MIB download failed!'; 345 } 346 else { 347 print color 'bold blue'; 348 say 'MIBs update complete.'; 349 } 350 351 print color 'reset'; 352} 353 354exit 0; 355