diff --git a/traffic_ops/build/traffic_ops.spec b/traffic_ops/build/traffic_ops.spec index c2df00626d..75fa5d1c38 100644 --- a/traffic_ops/build/traffic_ops.spec +++ b/traffic_ops/build/traffic_ops.spec @@ -122,7 +122,6 @@ Built: %(date) by %{getenv: USER} # install if [ "$1" = "1" ]; then # see postinstall, the .reconfigure file triggers init(). - /bin/touch %{PACKAGEDIR}/.reconfigure echo -e "\nRun /opt/traffic_ops/install/bin/postinstall from the root home directory to complete the install.\n" fi diff --git a/traffic_ops/install/bin/input.json b/traffic_ops/install/bin/input.json new file mode 100644 index 0000000000..85c4d03d92 --- /dev/null +++ b/traffic_ops/install/bin/input.json @@ -0,0 +1,175 @@ +{ + "/opt/traffic_ops/app/conf/production/database.conf":[ + { + "Database type":"pg", + "config_var":"type" + }, + { + "Database name":"traffic_ops_db", + "config_var":"dbname" + }, + { + "Database server hostname IP or FQDN":"localhost", + "config_var":"hostname" + }, + { + "Database port number":"3306", + "config_var":"port" + }, + { + "Traffic Ops database user":"traffic_ops", + "config_var":"user" + }, + { + "Traffic Ops database password":"default", + "config_var":"password", + "hidden":"1" + } + ], + "/opt/traffic_ops/app/db/dbconf.yml":[ + { + "Database server root (admin) username":"root", + "config_var":"dbAdminUser" + }, + { + "Database server admin password":"default", + "config_var":"dbAdminPw", + "hidden":"1" + }, + { + "Download Maxmind Database?":"yes", + "config_var":"maxmind" + } + ], + "/opt/traffic_ops/app/conf/cdn.conf":[ + { + "Generate a new secret?":"yes", + "config_var":"genSecret" + }, + { + "Number of secrets to keep?":"10", + "config_var":"keepSecrets" + } + ], + "/opt/traffic_ops/app/conf/ldap.conf":[ + { + "Do you want to set up LDAP?":"no", + "config_var":"setupLdap" + }, + { + "LDAP server hostname":"", + "config_var":"hostname" + }, + { + "LDAP Admin DN":"", + "config_var":"admin_dn" + }, + { + "LDAP Admin Password":"", + "config_var":"password", + "hidden":"1" + }, + { + "LDAP Search Base":"", + "config_var":"search_base" + } + ], + "/opt/traffic_ops/install/data/json/users.json":[ + { + "Administration username for Traffic Ops":"root", + "config_var":"tmAdminUser" + }, + { + "Password for the admin user":"default", + "config_var":"tmAdminPw", + "hidden":"1" + } + ], + "/opt/traffic_ops/install/data/profiles/":[ + { + "Add custom profiles?":"no", + "config_var":"custom_profiles" + } + ], + "/opt/traffic_ops/install/data/json/openssl_configuration.json":[ + { + "Do you want to generate a certificate?":"yes", + "config_var":"genCert" + }, + { + "Country Name (2 letter code)":"XX", + "config_var":"country" + }, + { + "State or Province Name (full name)":"Default State", + "config_var":"state" + }, + { + "Locality Name (eg, city)":"Default City", + "config_var":"locality" + }, + { + "Organization Name (eg, company)":"Default Company Ltd", + "config_var":"company" + }, + { + "Organizational Unit Name (eg, section)":"", + "config_var":"org_unit" + }, + { + "Common Name (eg, your name or your server's hostname)":"example.com", + "config_var":"common_name" + }, + { + "RSA Passphrase":"password", + "config_var":"rsaPassword", + "hidden":"1" + } + ], + "/opt/traffic_ops/install/data/json/profiles.json":[ + { + "Traffic Ops url":"https://localhost", + "config_var":"tm.url" + }, + { + "Human-readable CDN Name. (No whitespace, please)":"kabletown_cdn", + "config_var":"cdn_name" + }, + { + "Health Polling Interval (milliseconds)":"8000", + "config_var":"health_polling_int" + }, + { + "DNS sub-domain for which your CDN is authoritative":"cdn1.kabletown.net", + "config_var":"dns_subdomain" + }, + { + "TLD SOA admin":"traffic_ops", + "config_var":"soa_admin" + }, + { + "TrafficServer Drive Prefix":"/dev/sd", + "config_var":"driver_prefix" + }, + { + "TrafficServer RAM Drive Prefix":"/dev/ram", + "config_var":"ram_drive_prefix" + }, + { + "TrafficServer RAM Drive Letters (comma separated)":"0,1,2,3,4,5,6,7", + "config_var":"ram_drive_letters" + }, + { + "Health Threshold Load Average":"25", + "config_var":"health_thresh_load_avg" + }, + { + "Health Threshold Available Bandwidth in Kbps":"1750000", + "config_var":"health_thresh_kbps" + }, + { + "Traffic Server Health Connection Timeout (milliseconds)":"2000", + "config_var":"health_connect_timeout" + } + ] +} diff --git a/traffic_ops/install/bin/postinstall b/traffic_ops/install/bin/postinstall index 6a747daab7..97f502821a 100755 --- a/traffic_ops/install/bin/postinstall +++ b/traffic_ops/install/bin/postinstall @@ -1,6 +1,5 @@ #!/usr/bin/perl -# # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,702 +14,791 @@ # limitations under the License. # -use strict; -use warnings; - use lib qw(/opt/traffic_ops/install/lib /opt/traffic_ops/install/lib/perl5 /opt/traffic_ops/app/local/lib/perl5 /opt/traffic_ops/app/lib); $ENV{PATH} = "/opt/traffic_ops/install/bin:$ENV{PATH}"; $ENV{PERL5LIB} = "/opt/traffic_ops/install/lib:/opt/traffic_ops/install/lib/perl5:/opt/traffic_ops/app/local/lib/perl5:/opt/traffic_ops/app/lib"; -use DBI; -use JSON; -use InstallUtils qw{ :all }; +use strict; +use warnings; + +use Safe; +use POSIX; +use File::Basename qw{dirname}; +use File::Path qw{make_path}; use Digest::SHA1 qw(sha1_hex); -use Data::Dumper; -use File::Temp; -use WWW::Curl::Easy; -use LWP::UserAgent; -use File::Copy; - -my $database_conf = "/opt/traffic_ops/app/conf/production/database.conf"; -my $ldap_conf = "/opt/traffic_ops/app/conf/ldap.conf"; -my $cdn_conf = "/opt/traffic_ops/app/conf/cdn.conf"; -my $migrations_dbconf = "/opt/traffic_ops/app/db/dbconf.yml"; -my $post_install_cfg = "/opt/traffic_ops/install/data/json/post_install.json"; -my $users_file = "/opt/traffic_ops/install/data/json/users.json"; -my $profile_dir = "/opt/traffic_ops/install/data/profiles/"; -my %dbdriver = ( mysql => "mymysql", ); - -my $reconfigure = "/opt/traffic_ops/.reconfigure"; -my $reconfigure_defaults = "/opt/traffic_ops/.reconfigure_defaults"; -my $tmAdminUser = ""; -my $tmAdminPw = ""; +use Data::Dumper qw(Dumper); +use Scalar::Util qw(looks_like_number); +use Getopt::Long; + +use InstallUtils; +use BuildPerlDeps; +use GenerateCert qw{ :all }; +use ProfileCleanup; + +# paths of the output configuration files +my $databaseConfFile = "/opt/traffic_ops/app/conf/production/database.conf"; +my $dbConfFile = "/opt/traffic_ops/app/db/dbconf.yml"; +my $cdnConfFile = "/opt/traffic_ops/app/conf/cdn.conf"; +my $ldapConfFile = "/opt/traffic_ops/app/conf/ldap.conf"; +my $usersConfFile = "/opt/traffic_ops/install/data/json/users.json"; +my $profilesConfFile = "/opt/traffic_ops/install/data/profiles/"; +my $opensslConfFile = "/opt/traffic_ops/install/data/json/openssl_configuration.json"; +my $paramConfFile = "/opt/traffic_ops/install/data/json/profiles.json"; + +my $custom_profile_dir = $profilesConfFile . "custom"; + +# stores parameters for traffic ops config my $parameters; -my $installMsg = << 'EOF'; +# location of traffic ops profiles +my $profileDir = "/opt/traffic_ops/install/data/profiles/"; +my $post_install_cfg = "/opt/traffic_ops/install/data/json/post_install.json"; + +# log file for the installer +my $logFile = "/var/log/traffic_ops/postinstall.log"; + +# debug mode +my $debug = 0; + +# log file for cpan output +my $cpanLogFile = "/var/log/traffic_ops/cpan.log"; + +# whether or not to reconfigure traffic ops +my $reconfigure = 0; + +# used to check for .reconfigure_defaults file for backwards compatability +my $reconfigure_defaults = "/opt/traffic_ops/.reconfigure_defaults"; + +# old way of reconfiguring postinstall - only here to check for file and let user know it is deprecated +my $reconfigure_file = "/opt/traffic_ops/.reconfigure"; + +# maximum size the uncompressed log file should be before rotating it - rotating it copies the current log +# file to the same name appended with .bkp replacing the old backup if any is there +my $maxLogSize = 10000000; #bytes -This script will initialize the Traffic Ops database. -Please enter the following information in order to completely -configure the Traffic Ops mysql database. +# whether to create a config file with default values +my $dumpDefaults; -EOF +# configuration file output with answers which can be used as input to postinstall +my $outputConfigFile = "/opt/traffic_ops/install/bin/configuration_file.json"; -sub readJson { - my $file = shift; - open( my $fh, '<', $file ) or return; - local $/; # slurp mode - my $text = <$fh>; - undef $fh; - return JSON->new->utf8->decode($text); +my $inputFile = ""; +my $automatic = 0; +my $defaultInputs; + +sub getDbDriver { + return "pg"; } -sub writeJson { - my $file = shift; - open( my $fh, '>', $file ) or die("open(): $!"); - foreach my $data (@_) { - my $json_text = JSON->new->utf8->encode($data); - print $fh $json_text, "\n"; - } - close $fh; +sub getInstallPath { + my $relPath = shift; + return join( '/', "/tmp/traffic_ops", $relPath ); } -sub writeYamlToFH { - my $fh = shift; - my $data = shift; - my $level = shift || 0; - my $prefix = shift || ''; - - my $type = ref($data); - my $indent = ' ' x $level; - if ( $type eq '' ) { - - # scalar - print $fh "$indent$prefix$data\n"; - } - elsif ( $type eq 'HASH' ) { - foreach my $key ( keys %$data ) { - my $value = $data->{$key}; - if ( ref($value) eq '' ) { - print $fh "$indent$key: $value\n"; - } - else { - print $fh "$indent$key:\n"; - writeYamlToFH( $fh, $data->{$key}, $level + 1 ); - } - } - } - elsif ( $type eq 'ARRAY' ) { - foreach my $d (@$data) { - writeYamlToFH( $fh, $d, $level + 1, '- ' ); - } - } +# given a var to the hash of config_var and question, will return the question +sub getConfigQuestion { + my $var = shift; + foreach my $key ( keys $var ) { + if ( $key ne "hidden" && $key ne "config_var" ) { + return $key; + } + } } -sub writeYaml { - my $file = shift; - my $data = shift; - open my $fh, '>', $file or die "open(): $!"; - writeYamlToFH( $fh, $data ); +# question: The question given in the config file +# config_answer: The answer given in the config file - if no config file given will be defaultInput +# hidden: Whether or not the answer should be hidden from the terminal and logs, ex. passwords +# +# Determines if the script is being run in complete interactive mode and prompts user - otherwise +# returns answer to question in config or defaults + +sub getField { + my $question = shift; + my $config_answer = shift; + my $hidden = shift; + + # if there is no config file and not in automatic mode prompt for all questions with default answers + if ( !$inputFile && !$automatic ) { + + # if hidden then dont show password in terminal + if ($hidden) { + return promptPasswordVerify($question); + } + else { + return promptUser( $question, $config_answer ); + } + } + + return $config_answer; } -# Init. -sub init () { - my $c = readJson($database_conf); - my %dbconf = %$c; - my $dbAdminUser; - my $dbAdminPw; - - # loop exits on successful db connect - while (1) { - execCommand( "/usr/bin/tput", "clear" ); - - if ($DBI::errstr) { - print "Error connecting to database using the supplied information: $DBI::errstr\n"; - } - - print "\n$installMsg\n"; - - $dbconf{type} = promptUser( "Database type", $dbconf{type} || "mysql" ); - $dbconf{dbname} = promptUser( "Database name", $dbconf{dbname} || "traffic_ops_db" ); - $dbconf{hostname} = promptUser( "Database server hostname IP or FQDN", $dbconf{hostname} || "localhost" ); - $dbconf{port} = promptUser( "Database port number", $dbconf{port} || "3306" ); - $dbconf{user} = promptUser( "Traffic Ops database user", $dbconf{user} || "traffic_ops" ); - $dbconf{password} = promptPasswordVerify("Password for $dbconf{user}"); - $dbconf{description} = "$dbconf{type} database on $dbconf{hostname}:$dbconf{port}"; - print "\n"; - $dbAdminUser = promptUser( "Database server root (admin) user name", "root" ); - $dbAdminPw = promptPassword("Database server $dbAdminUser password"); - - print "Database Type: $dbconf{type}\n"; - print "Database Name: $dbconf{dbname}\n"; - print "Hostname: $dbconf{hostname}\n"; - print "Port: $dbconf{port}\n"; - print "Database User: $dbconf{user}\n"; - my $ans = promptUser( "Is the above information correct (y/n)", "n" ); - - if ( $ans eq "y" ) { - my $dsn = sprintf( "DBI:mysql:%s:%s:%s", "mysql", $dbconf{hostname}, $dbconf{port} ); - my $dbh = DBI->connect( $dsn, $dbAdminUser, $dbAdminPw ); - if ($dbh) { - - # Success! - $dbh->disconnect(); - last; - } - } - } - - writeJson( $database_conf, \%dbconf ); - print "\nThe database properties have been saved to $database_conf\n"; - - # migrations dbconf is in YAML - my $driver = $dbdriver{ $dbconf{type} }; - my %migrations = ( production => { driver => $driver, open => "tcp:$dbconf{hostname}:$dbconf{port}*$dbconf{dbname}/$dbconf{user}/$dbconf{password}" } ); - writeYaml( $migrations_dbconf, \%migrations ); - - my $msg = << 'EOF'; - - The database configuration has been saved. Now we need to set some custom - fields that are necessary for the CDN to function correctly. - -EOF - - print $msg, "\n"; - while (1) { - - my $tmurl = promptUser( "Traffic Ops url", $parameters->{"tm.url"} || "https://localhost" ); - $parameters->{"tm.url"} = $tmurl; - $parameters->{"tm.infourl"} = "$tmurl/info"; - - $parameters->{cdnname} = promptUser( "Human-readable CDN Name. (No whitespace, please)", $parameters->{cdnname} || "kabletown_cdn" ); - $parameters->{domainname} = promptUser( "DNS sub-domain for which your CDN is authoritative", $parameters->{domainname} || "cdn1.kabletown.net" ); - - my $geolocationUrl = "$tmurl/routing/GeoIP2-City.mmdb.gz"; - $parameters->{"geolocation.polling.url"} = $geolocationUrl; - - my $coverageZoneUrl = "$tmurl/routing/coverage-zone.json"; - $parameters->{"coveragezone.polling.url"} = $coverageZoneUrl; - - my $centosTarballFqn = ''; - my $skip; - while (1) { - $centosTarballFqn = promptUser( "Fully qualified name of your CentOS ISO kickstart tarball, or 'na' to skip and add files later", - "/var/cache/centos72.tgz" ); - if ( $centosTarballFqn eq 'na' ) { - $skip = 1; - last; - } - if ( -f $centosTarballFqn ) { - last; - } - print "\nNo file named $centosTarballFqn found.\n\n"; - } - - my $kickstartFilesFqn = promptUser( "Fully qualified location to store your ISO kickstart files", "/var/www/files" ); - my $parametersJson = "/opt/traffic_ops/install/data/json/parameter.json"; - - ## Replace parameter with $kickstartFilesFqn - open( my $json_fh, "<:encoding(UTF-8)", $parametersJson ) - or die("Can't open \$filename\": $!\n"); - - my $json = JSON->new; - my @json_obj; - while ( my $json_text = <$json_fh> ) { - my $data = $json->decode($json_text); - - if ( $data->{"name"} eq "kickstart.files.location" ) { - $data->{"value"} = $kickstartFilesFqn; - } - push @json_obj, $data; - } - writeJson( $parametersJson, @json_obj ); - - execCommand( "/bin/cp", "/opt/traffic_ops/install/data/perl/osversions.cfg", $kickstartFilesFqn ); - - if ( !$skip ) { - print "\nUncompressing CentOS ISO kickstart tarball.\n"; - print "\nFirst creating $kickstartFilesFqn.\n"; - execCommand( "/bin/mkdir", "-p", $kickstartFilesFqn ); - print "\nUncompressing $centosTarballFqn.\n"; - execCommand( "/bin/tar", "-xzf", $centosTarballFqn, "-C", $kickstartFilesFqn ); - } - - print "\nTraffic Ops URL: $parameters->{'tm.url'}\n"; - print "Traffic Ops Info URL: $parameters->{'tm.infourl'}\n"; - print "Domainname: $parameters->{domainname}\n"; - print "CDN Name: $parameters->{cdnname}\n"; - print "GeoLocation Polling URL: $parameters->{'geolocation.polling.url'}\n"; - print "CoverageZone Polling URL: $parameters->{'coveragezone.polling.url'}\n\n"; - my $ans = promptUser( "Is the above information correct (y/n)", "n" ); - if ( $ans eq 'y' ) { - last; - } - } - writeJson( $post_install_cfg, $parameters ); - print "Install information has been saved to $post_install_cfg\n\n"; - - print "\nAdding an administration user to the Traffic Ops database.\n\n"; - my %user = (); - $tmAdminUser = promptUser( "Administration username for Traffic Ops", 'admin' ); - $user{username} = $tmAdminUser; - $tmAdminPw = promptPasswordVerify("Password for the admin user $tmAdminUser"); - $user{password} = sha1_hex($tmAdminPw); - - writeJson( $users_file, \%user ); - - my $ans = promptUser( "Do you wish to create an ldap configuration for access to traffic ops [y/n] ?", "n" ); - if ( $ans eq "y" ) { - my %ldapconf = readJson($ldap_conf); - while (1) { - $ldapconf{host} = promptUser( "LDAP server hostname", $ldapconf{host} || "ldap.foobar.com" ); - $ldapconf{admin_dn} = promptUser( "LDAP Admin DN", $ldapconf{admin_dn} || 'admin@foobar.com' ); - $ldapconf{admin_pass} = promptPasswordVerify("LDAP Admin Password"); - $ldapconf{search_base} = promptUser( "LDAP Search Base", "dc=foobar,dc=com" ); - my $correct = promptUser( "Are the above values correct [y/n]?", "y" ); - if ( $correct eq 'y' ) { - last; - } - } - writeJson( $ldap_conf, \%ldapconf ); - print "The ldap configuration has been saved.\n\n"; - } - - # Prompt for new secret - writeSecret($cdn_conf); - - # - # Call mysql initialization script. - # - print "Creating database\n"; - my $result = execCommand( "/opt/traffic_ops/install/bin/create_db", $dbAdminUser, $dbAdminPw ); - if ( $result != 0 ) { - print "failed to create the database.\n"; - exit 1; - } - - print "Setting up database\n"; - chdir("/opt/traffic_ops/app"); - $result = execCommand( "/usr/bin/perl", "db/admin.pl", "--env=production", "setup" ); - - if ( $result != 0 ) { - print "Database initialization failed.\n"; - exit 2; - } - else { - print "Database initialization succeeded.\n"; - } - - $result = execCommand( "/opt/traffic_ops/install/bin/dataload", $dbAdminUser, $dbAdminPw ); - if ( $result != 0 ) { - print "failed to load seed data.\n"; - exit 1; - } - - print "Downloading MaxMind data.\n"; - chdir("/opt/traffic_ops/app/public/routing"); - $result = execCommand("/usr/bin/wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz"); - if ( $result != 0 ) { - print "failed to download MaxMind data.\n"; - - # exit 1; - } - - print "Copying coverage zone file to public dir.\n"; - $result = execCommand("/bin/mv /opt/traffic_ops/app/public/coverage-zone.json ."); - if ( $result != 0 ) { - print "failed to copy coverage zone file.\n"; - - # exit 1; - } - - if ( -x "/usr/bin/openssl" ) { - print "\nInstalling SSL Certificates.\n\n"; - $result = execCommand("/opt/traffic_ops/install/bin/generateCert"); - - if ( $result != 0 ) { - print "\nSSL Certificate Installation failed.\n"; - exit 3; - } - else { - print "\nSSL Certificates have been installed.\n"; - } - } - else { - print "Unable to install SSL certificates as openssl is not installed.\n"; - print "Install openssl and then run /opt/traffic_ops/install/bin/generateCert to install SSL certificates.\n"; - exit 4; - } -} # end of Init - -sub writeSecret { - print "\n\nTraffic Ops requires a secret key to generate authentication cookies.\n\n"; - - # read conf file -- see if secrets already there - my $cdnh = do $cdn_conf; - unless ( ref($cdnh) eq 'HASH' ) { - my $err = $@ || $! || ' -- not a HASH'; - if ($err) { - print "Could not load $cdn_conf $err"; - exit 4; - } - } - - # newSecret - my $secrets = $cdnh->{secrets}; - if ( ( ref $secrets eq 'ARRAY' ) && scalar @$secrets > 0 ) { - print "One or more secrets found in $cdn_conf.\n"; - my $ans = promptUser( " Do want to add a new one (only 2 will be kept) [y/n] ?", "y" ); - if ( $ans eq "n" ) { - - # nothing further to do... - return; - } - } - my $new_secret = ""; - while ( length $new_secret == 0 ) { - print "Adding a new secret.\n"; - my $ans = promptUser( " Do you want one generated for you [y/n] ?", "y" ); - if ( $ans eq "n" ) { - $new_secret = promptUser( "Secret key:", "" ); - } - else { - - # create random word 12 chars long - $new_secret = randomWord(12); - } - } - - # keep 2 at most.. - unshift( @$secrets, $new_secret ); - if ( scalar @$secrets > 2 ) { - $#{$secrets} = 1; - } - - # dump conf data in compact but readable form - my $dumper = Data::Dumper->new( [$cdnh] ); - $dumper->Indent(1)->Terse(1)->Quotekeys(0); - - # write whole config to temp file in pwd (keeps in same filesystem) - my $tmpfile = File::Temp->new(DIR => '.'); - print $tmpfile $dumper->Dump(); - close $tmpfile; - - # rename current config file to something unique so it's not lost - my $backup_num = 0; - my $backup_name; - do { - $backup_num++; - $backup_name = "$cdn_conf.backup$backup_num"; - } while ( -e $backup_name ); - rename( $cdn_conf, $backup_name ) or die("rename(): $!"); - - # rename temp file to cdn.conf and set ownership/permissions same as backup - my @stats = stat($backup_name); - my ( $uid, $gid, $perm ) = @stats[ 4, 5, 2 ]; - rename( $tmpfile, $cdn_conf ) or die("rename(): $!"); - - chown $uid, $gid, $cdn_conf; - chmod $perm, $cdn_conf; +# userInput: The entire input config file which is either user input or the defaults +# fileName: The name of the output config file given by the input config file +# +# Loops through an input config file and determines answers to each question using getField +# and returns the hash of answers + +sub getConfig { + my $userInput = shift; + my $fileName = shift; + + my %config; + + if ( !defined $userInput->{$fileName} ) { + InstallUtils::logger( "No $fileName found in config", "error" ); + } + + InstallUtils::logger( "===========$fileName===========", "info" ); + + foreach my $var ( @{ $userInput->{$fileName} } ) { + my $question = getConfigQuestion($var); + my $hidden = $var->{"hidden"} if ( exists $var->{"hidden"} ); + my $answer = $config{ $var->{"config_var"} } = getField( $question, $var->{$question}, $hidden ); + + $config{ $var->{"config_var"} } = $answer; + if ( !$hidden ) { + InstallUtils::logger( "$question: $answer", "info" ); + } + } + return %config; } -chdir("/opt/traffic_ops/install/bin"); - -$parameters = readJson($post_install_cfg); -if ( -f $reconfigure ) { - my $rc = execCommand( "/opt/traffic_ops/install/bin/build_trafficops_perl_library", "-i" ); - if ( $rc != 0 ) { - print "ERROR: failed to install perl dependencies, check the console output and rerun postinstall once you've resolved the error.\n"; - exit 5; - } - $rc = execCommand( "./download_web_deps", "-i" ); - if ( $rc != 0 ) { - print "ERROR: failed to install Traffic Ops Web dependencies, check the console output and rerun postinstall once you've resolved the error.\n"; - } - init(); - unlink($reconfigure); +# userInput: The entire input config file which is either user input or the defaults +# dbFileName: The filename of the output config file for the database +# toDBFileName: The filename of the output config file for the Traffic Ops database +# +# Generates a config file for the database based on the questions and answers in the input config file + +sub generateDbConf { + my $userInput = shift; + my $dbFileName = shift; + my $toDBFileName = shift; + + my %dbconf = getConfig( $userInput, $dbFileName ); + $dbconf{"description"} = "$dbconf{type} database on $dbconf{hostname}:$dbconf{port}"; + make_path( dirname($dbFileName), { mode => 0755 } ); + InstallUtils::writeJson( $dbFileName, \%dbconf ); + InstallUtils::logger( "Database configuration has been saved", "info" ); + + # broken out into separate file/config area + my %todbconf = getConfig( $userInput, $toDBFileName ); + + # No YAML library installed, but this is a simple file.. + open( my $fh, '>', $toDBFileName ) or errorOut("Can't write to $toDBFileName!"); + print $fh "production:\n"; + print $fh " driver: ", getDbDriver() . "\n"; + print $fh " open: tcp:$dbconf{hostname}:$dbconf{port}*$dbconf{dbname}/$dbconf{user}/$dbconf{password}\n"; + close $fh; + + return \%todbconf; } -else { - my $rc = execCommand("/opt/traffic_ops/install/bin/build_trafficops_perl_library"); - if ( $rc != 0 ) { - print "ERROR: failed to install perl dependencies, check the console output and rerun postinstall once you've resolved the error.\n"; - exit 6; - } - $rc = execCommand( "./download_web_deps", "-i" ); - if ( $rc != 0 ) { - print "ERROR: failed to install Traffic Ops Web dependencies, check the console output and rerun postinstall once you've resolved the error.\n"; - } + +# userInput: The entire input config file which is either user input or the defaults +# fileName: The filename of the output config file +# +# Generates a config file for the CDN + +sub generateCdnConf { + my $userInput = shift; + my $fileName = shift; + + my %cdnConfiguration = getConfig( $userInput, $fileName ); + + # First, read existing one -- already loaded with a bunch of stuff + my $cdnConf; + if ( -f $fileName ) { + $cdnConf = Safe->new->rdo($fileName) or errorOut("Error loading $fileName: $@"); + } + if ( lc $cdnConfiguration{genSecret} =~ /^y(?:es)?/ ) { + my @secrets = @{ $cdnConf->{secrets} }; + my $newSecret = InstallUtils::randomWord(); + unshift @secrets, InstallUtils::randomWord(); + if ( $cdnConfiguration{keepSecrets} > 0 && $#secrets > $cdnConfiguration{keepSecrets} - 1 ) { + + # Shorten the array to requested length + $#secrets = $cdnConfiguration{keepSecrets} - 1; + } + } + InstallUtils::writePerl( $fileName, $cdnConf ); } -sub profile_replace { - my($profile) = @_; - my $profile_bak = $profile . ".bak"; - rename($profile, $profile_bak) or die("rename(): $!"); - open(my $fh, '<', $profile_bak) or die("open(): $!"); - open(my $ofh, '>', $profile) or die("open(): $!"); - while (<$fh>) { - s/{{.TmUrl}}/$parameters->{'tm.url'}/g; - s/{{.TmInfoUrl}}/$parameters->{"tminfo.url"}/g; - s/{{.TmInstanceName}}/$parameters->{"cdnname"}/g; - s/{{.GeolocationPollingUrl}}/$parameters->{"geolocation.polling.url"}/g; - s/{{.Geolocation6PollingUrl}}/$parameters->{"geolocation6.polling.url"}/g; - s/{{.TmUrl}}/$parameters->{'tm.url'}/g; - s/{{.TmToolName}}/Traffic Ops/g; - s/{{.HealthPollingInterval}}/$parameters->{"health.polling.interval"}/g; - s/{{.CoveragezonePollingUrl}}/$parameters->{"coveragezone.polling.url"}/g; - s/{{.DomainName}}/$parameters->{"domainname"}/g; - s/{{.TldSoaAdmin}}/$parameters->{"tld.soa.admin"}/g; - s/{{.DrivePrefix}}/$parameters->{"Drive_Prefix"}/g; - s/{{.HealthThresholdLoadavg}}/$parameters->{"health.threshold.loadavg"}/g; - s/{{.HealthThresholdAvailableBandwidthInKbps}}/$parameters->{"health.threshold.availableBandwidthInKbps"}/g; - s/{{.RAMDrivePrefix}}/$parameters->{"RAM_Drive_Prefix"}/g; - s/{{.RAMDriveLetters}}/$parameters->{"RAM_Drive_Letters"}/g; - s/{{.HealthConnectionTimeout}}/$parameters->{"health.connection.timeout"}/g; - s#{{.CronOrtSyncds}}#*/15 * * * * root /opt/ort/traffic_ops_ort.pl syncds warn $parameters->{'tm.url'} $tmAdminUser:$tmAdminPw > /tmp/ort/syncds.log 2>&1#g; - print $ofh $_; - } - close $fh; - close $ofh; - unlink $profile_bak; +# userInput: The entire input config file which is either user input or the defaults +# fileName: The filename of the output config file +# +# Generates an LDAP config file + +sub generateLdapConf { + my $userInput = shift; + my $fileName = shift; + + my $useLdap = $userInput->{$fileName}[0]->{"Do you want to set up LDAP?"}; + + if ( !lc $useLdap =~ /^y(?:es)?/ ) { + InstallUtils::logger( "Not setting up ldap", "info" ); + return; + } + + my %ldapConf = getConfig( $userInput, $fileName ); + + make_path( dirname($fileName), { mode => 0755 } ); + InstallUtils::writeJson( $fileName, \%ldapConf ); } -sub replace_profile_templates() { - while (!defined $parameters->{'tm.url'} || $parameters->{'tm.url'} eq "") { - $parameters->{'tm.url'} = InstallUtils::promptUser ("Traffic Ops url", "https://localhost"); - } - while (!defined $parameters->{"tminfo.url"} || $parameters->{"tminfo.url"} eq "") { - $parameters->{"tminfo.url"} = "$parameters->{'tm.url'}/info" - } - while (!defined $parameters->{"cdnname"} || $parameters->{"cdnname"} eq "") { - $parameters->{"cdnname"} = InstallUtils::promptUser ("Human-readable CDN Name. (No whitespace, please)", "kabletown_cdn"); - } - while (!defined $parameters->{"geolocation.polling.url"} || $parameters->{"geolocation.polling.url"} eq "") { - $parameters->{"geolocation.polling.url"} = "$parameters->{'tm.url'}/routing/GeoIP2-City.mmdb.gz"; - } - while (!defined $parameters->{"geolocation6.polling.url"} || $parameters->{"geolocation6.polling.url"} eq "") { - $parameters->{"geolocation6.polling.url"} = "$parameters->{'tm.url'}/routing/GeoIP2-Cityv6.mmdb.gz"; - } - while (!defined $parameters->{"health.polling.interval"} || $parameters->{"health.polling.interval"} eq "") { - $parameters->{"health.polling.interval"} = InstallUtils::promptUser ("Health Polling Interval (milliseconds)", "8000"); - } - while (!defined $parameters->{"coveragezone.polling.url"} || $parameters->{"coveragezone.polling.url"} eq "") { - $parameters->{"coveragezone.polling.url"} = "$parameters->{'tm.url'}/routing/coverage-zone.json" - } - while (!defined $parameters->{"domainname"} || $parameters->{"domainname"} eq "") { - $parameters->{"domainname"} = InstallUtils::promptUser ("DNS sub-domain for which your CDN is authoritative", "cdn1.kabletown.net"); - } - while (!defined $parameters->{"tld.soa.admin"} || $parameters->{"tld.soa.admin"} eq "") { - $parameters->{"tld.soa.admin"} = InstallUtils::promptUser ("TLD SOA admin", "traffic_ops"); - } - while (!defined $parameters->{"Drive_Prefix"} || $parameters->{"Drive_Prefix"} eq "") { - $parameters->{"Drive_Prefix"} = InstallUtils::promptUser ("TrafficServer Drive Prefix", "/dev/sd"); - } - while (!defined $parameters->{"RAM_Drive_Prefix"} || $parameters->{"RAM_Drive_Prefix"} eq "") { - $parameters->{"RAM_Drive_Prefix"} = InstallUtils::promptUser ("TrafficServer RAM Drive Prefix", "/dev/ram"); - } - while (!defined $parameters->{"RAM_Drive_Letters"} || $parameters->{"RAM_Drive_Letters"} eq "") { - $parameters->{"RAM_Drive_Letters"} = InstallUtils::promptUser ("TrafficServer RAM Drive Letters (comma separated)", "0,1,2,3,4,5,6,7"); - } - while (!defined $parameters->{"health.threshold.loadavg"} || $parameters->{"health.threshold.loadavg"} eq "") { - $parameters->{"health.threshold.loadavg"} = InstallUtils::promptUser ("Health Threshold Load Average", "25"); - } - while (!defined $parameters->{"health.threshold.availableBandwidthInKbps"} || $parameters->{"health.threshold.availableBandwidthInKbps"} eq "" || $parameters->{"health.threshold.availableBandwidthInKbps"} eq ">") { - $parameters->{"health.threshold.availableBandwidthInKbps"} = ">" . InstallUtils::promptUser ("Health Threshold Available Bandwidth in Kbps", "1750000"); - } - while (!defined $parameters->{"health.connection.timeout"} || $parameters->{"health.connection.timeout"} eq "") { - $parameters->{"health.connection.timeout"} = InstallUtils::promptUser ("Traffic Server Health Connection Timeout (milliseconds)", "2000"); - } - - profile_replace($profile_dir . "profile.global.traffic_ops"); - profile_replace($profile_dir . "profile.traffic_monitor.traffic_ops"); - profile_replace($profile_dir . "profile.traffic_router.traffic_ops"); - profile_replace($profile_dir . "profile.trafficserver_edge.traffic_ops"); - profile_replace($profile_dir . "profile.trafficserver_mid.traffic_ops"); - writeJson( $post_install_cfg, $parameters ); +sub generateUsersConf { + my $userInput = shift; + my $fileName = shift; + + my %user = (); + my %config = getConfig( $userInput, $fileName ); + + $user{username} = $config{tmAdminUser}; + $user{password} = sha1_hex( $config{tmAdminPw} ); + + InstallUtils::writeJson( $fileName, \%user ); + $user{password} = $config{tmAdminPw}; + return \%user; } -# Takes the Traffic Ops URI, user, and password. -# Returns the cookie, or the empty string on error -sub get_traffic_ops_cookie { - my($uri, $user, $pass) = @_; - - my $loginUri = "/api/1.2/user/login"; - - my $curl = WWW::Curl::Easy->new; - my $response_body = ""; - open(my $fileb, ">", \$response_body); - my $loginData = JSON::encode_json({ u => $user, p => $pass}); - $curl->setopt(WWW::Curl::Easy::CURLOPT_URL, $uri . $loginUri); - $curl->setopt(WWW::Curl::Easy::CURLOPT_SSL_VERIFYPEER, 0); - $curl->setopt(WWW::Curl::Easy::CURLOPT_HEADER, 1); # include header in response - $curl->setopt(WWW::Curl::Easy::CURLOPT_NOBODY, 1); # disclude body in response - $curl->setopt(WWW::Curl::Easy::CURLOPT_POST, 1); - $curl->setopt(WWW::Curl::Easy::CURLOPT_POSTFIELDS, $loginData); - $curl->setopt(WWW::Curl::Easy::CURLOPT_WRITEDATA, $fileb); # put response in this var - $curl->perform(); - - my $cookie = $response_body; - if($cookie =~ /mojolicious=(.*); expires/) - { - $cookie = $1; - } - else - { - $cookie = "" - } - return $cookie; +sub generateProfilesDir { + my $userInput = shift; + my $fileName = shift; + + my $userIn = $userInput->{$fileName}; } -# Takes the filename of a Traffic Ops (TO) profile to import, the TO URI, and the TO login cookie -sub profile_import_single { - my($profileFilename, $uri, $trafficOpsCookie) = @_; - print "Importing Profiles with: " . "curl -v -k -X POST -H \"Cookie: mojolicious=$trafficOpsCookie\" -F \"filename=$profileFilename\" -F \"profile_to_import=\@$profileFilename\" $uri/profile/doI\ -mport"; - my $rc = execCommand("curl -v -k -X POST -H \"Cookie: mojolicious=$trafficOpsCookie\" -F \"filename=$profileFilename\" -F \"profile_to_import=\@$profileFilename\" $uri/profile/doImport"); - if ( $rc != 0 ) { - print "ERROR: failed to import Traffic Ops profile, check the console output and rerun postinstall once you've resolved the error.\n"; +sub generateOpenSSLConf { + my $userInput = shift; + my $fileName = shift; + + if ( !defined $userInput->{$fileName} ) { + InstallUtils::logger( "No OpenSSL Configuration - questions will be asked", "info" ); + + # write an empty config so openssl does not use an old file + InstallUtils::writeJson( $fileName, my %emptyConfig ); + return; } + + my %config = getConfig( $userInput, $fileName ); + + InstallUtils::writeJson( $fileName, \%config ); + return \%config; } -sub import_profiles() { - while (length $tmAdminUser == 0) { - $tmAdminUser = InstallUtils::promptUser ("Administration username for Traffic Ops"); - } - while ($tmAdminPw eq "") { - $tmAdminPw = InstallUtils::promptUser ("Password for the admin user $tmAdminUser", "", 1); - } - while (!defined $parameters->{'tm.url'} || length $parameters->{'tm.url'} == 0) { - $parameters->{'tm.url'} = InstallUtils::promptUser ("Traffic Ops url", "https://localhost"); - } - - print "Importing profiles...\n"; - # \todo take as params - my $toUri = $parameters->{'tm.url'}; - my $toUser = $tmAdminUser; - my $toPass = $tmAdminPw; - - my $toCookie = get_traffic_ops_cookie($toUri, $toUser, $toPass); - - print "Got cookie: " . $toCookie; - - # \todo use an array? - print "Importing Global profile...\n"; - profile_import_single($profile_dir . "profile.global.traffic_ops", $toUri, $toCookie); - print "Importing Traffic Monitor profile...\n"; - profile_import_single($profile_dir . "profile.traffic_monitor.traffic_ops", $toUri, $toCookie); - print "Importing Traffic Router profile...\n"; - profile_import_single($profile_dir . "profile.traffic_router.traffic_ops", $toUri, $toCookie); - print "Importing TrafficServer Edge profile...\n"; - profile_import_single($profile_dir . "profile.trafficserver_edge.traffic_ops", $toUri, $toCookie); - print "Importing TrafficServer Mid profile...\n"; - profile_import_single($profile_dir . "profile.trafficserver_mid.traffic_ops", $toUri, $toCookie); - print "Finished Importing Profiles.\n"; +sub generateParamConf { + my $userInput = shift; + my $fileName = shift; + + my %config = getConfig( $userInput, $fileName ); + InstallUtils::writeJson( $fileName, \%config ); + return \%config; } -print "\nStarting Traffic Ops.\n\n"; -execCommand("/sbin/service traffic_ops start"); - -print "\nWaiting for Traffic Ops to start.\n\n"; -sleep(5); - -sub profiles_exist { - if ( -f $reconfigure_defaults ) { - print "Default profiles were previously created. Remove " . $reconfigure_defaults . " to create again.\n"; - return 1; - } - - while ( length $tmAdminUser == 0 ) { - $tmAdminUser = - InstallUtils::promptUser("Administration username for Traffic Ops"); - } - while ( $tmAdminPw eq "" ) { - $tmAdminPw = - InstallUtils::promptUser( "Password for the admin user $tmAdminUser", - "", 1 ); - } - while ( !defined $parameters->{'tm.url'} - || length $parameters->{'tm.url'} == 0 ) - { - $parameters->{'tm.url'} = - InstallUtils::promptUser( "Traffic Ops url", "https://localhost" ); - } - - my $uri = $parameters->{'tm.url'}; - my $toCookie = get_traffic_ops_cookie( $parameters->{'tm.url'}, - $tmAdminUser, $tmAdminPw ); - - my $profileEndpoint = "/api/1.2/profiles.json"; - - my $ua = LWP::UserAgent->new; - $ua->ssl_opts( verify_hostname => 0, SSL_verify_mode => 0x00 ); - my $req = HTTP::Request->new( GET => $uri . $profileEndpoint ); - $req->header( 'Cookie' => "mojolicious=" . $toCookie ); - my $resp = $ua->request($req); - - if ( !$resp->is_success ) { - print "Error checking if profiles exist: " . $resp->status_line . "\n"; - return 1; # return true, so we don't attempt to create profiles - } - my $message = $resp->decoded_content; - - my $profiles = JSON->new->utf8->decode($message); - if ( ( !defined $profiles->{"response"} ) - || ( ref $profiles->{"response"} ne 'ARRAY' ) ) - { - print "Error checking if profiles exist: invalid JSON: $message\n"; - return 1; # return true, so we don't attempt to create profiles - } - - my $num_profiles = scalar( @{ $profiles->{"response"} } ); - print "Existing Profile Count: $num_profiles\n"; - - my %initial_profiles = ( - "INFLUXDB" => 1, - "RIAK_ALL" => 1, - "TRAFFIC_STATS" => 1, - "TRAFFIC_PORTAL" => 1 - ); - - my $profiles_response = $profiles->{"response"}; - foreach my $profile (@$profiles_response) { - if ( !exists $initial_profiles{ $profile->{"name"} } ) { - print "Found existing profile (" . $profile->{"name"} . ")\n"; - open( my $reconfigure_defaults_file, '>', $reconfigure_defaults ) or die("Failed to open() $reconfigure_defaults: $!"); - close( $reconfigure_defaults_file ); - return 1; - } - } - return 0; +# check default values for missing config_var parameter +sub sanityCheckDefaults { + foreach my $file ( ( keys $defaultInputs ) ) { + foreach my $defaultValue ( @{ $defaultInputs->{$file} } ) { + my $question = getConfigQuestion($defaultValue); + + if ( !defined $defaultValue->{"config_var"} + || $defaultValue->{"config_var"} eq "" ) + { + errorOut("Question '$question' in file '$file' has no config_var"); + } + } + } } -if ( !profiles_exist() ) { - print "Creating default profiles...\n"; - replace_profile_templates(); - import_profiles(); - profiles_exist(); # call again to create $reconfigure_defaults file if import was successful +# userInput: The entire input config file which is either user input or the defaults +# +# Checks the input config file against the default inputs. If there is a question located in the default inputs which +# is not located in the input config file it will output a warning message. + +sub sanityCheckConfig { + my $userInput = shift; + my $diffs = 0; + + foreach my $file ( ( keys $defaultInputs ) ) { + if ( !defined $userInput->{$file} ) { + InstallUtils::logger( "File '$file' found in defaults but not config file", "warn" ); + $userInput->{$file} = []; + } + + foreach my $defaultValue ( @{ $defaultInputs->{$file} } ) { + + my $found = 0; + foreach my $configValue ( @{ $userInput->{$file} } ) { + if ( $defaultValue->{"config_var"} eq $configValue->{"config_var"} ) { + $found = 1; + } + } + + # if the question is not found in the config file add it from defaults + if ( !$found ) { + my $question = getConfigQuestion($defaultValue); + InstallUtils::logger( "Question '$question' found in defaults but not in '$file'", "warn" ); + + my %temp; + my $answer; + my $hidden = exists $defaultValue->{"hidden"} && $defaultValue->{"hidden"} ? 1 : 0; + + # in automatic mode add the missing question with default answer + if ($automatic) { + $answer = $defaultValue->{$question}; + InstallUtils::logger( "Adding question '$question' with default answer " . ( $hidden ? "" : "'$answer'" ), "info" ); + } + + # in interactive mode prompt the user for answer to missing question + else { + InstallUtils::logger( "Prompting user for answer", "info" ); + if ($hidden) { + $answer = promptPasswordVerify($question); + } + else { + $answer = promptUser( $question, $defaultValue->{$question} ); + } + } + + %temp = ( + "config_var" => $defaultValue->{"config_var"}, + $question => $answer + ); + + if ($hidden) { + $temp{"hidden"} .= "true"; + } + + push $userInput->{$file}, \%temp; + + $diffs++; + } + } + } + + InstallUtils::logger( "File sanity check complete - found $diffs difference" . ( $diffs == 1 ? "" : "s" ), "info" ); } -else { - print "Not creating default profiles.\n"; + +# A function which returns the default inputs data structure. These questions and answers will be used if there is no +# user input config file or if there are questions in the input config file which do not have answers + +sub getDefaults { + return { + $databaseConfFile => [ + { + "Database type" => "pg", + "config_var" => "type" + }, + { + "Database name" => "traffic_ops", + "config_var" => "dbname" + }, + { + "Database server hostname IP or FQDN" => "localhost", + "config_var" => "hostname" + }, + { + "Database port number" => "3306", + "config_var" => "port" + }, + { + "Traffic Ops database user" => "traffic_ops", + "config_var" => "user" + }, + { + "Password for Traffic Ops database user" => "", + "config_var" => "password", + "hidden" => "true" + } + + ], + $dbConfFile => [ + { + "Database server root (admin) user" => "root", + "config_var" => "dbAdminUser" + }, + { + "Password for database server admin" => "", + "config_var" => "dbAdminPw", + "hidden" => "true" + }, + { + "Download Maxmind Database?" => "yes", + "config_var" => "maxmind" + } + ], + $cdnConfFile => [ + { + "Generate a new secret?" => "yes", + "config_var" => "genSecret" + }, + { + "Number of secrets to keep?" => "10", + "config_var" => "keepSecrets" + } + ], + $ldapConfFile => [ + { + "Do you want to set up LDAP?" => "no", + "config_var" => "setupLdap" + }, + { + "LDAP server hostname" => "", + "config_var" => "hostname" + }, + { + "LDAP Admin DN" => "", + "config_var" => "admin_dn" + }, + { + "LDAP Admin Password" => "", + "config_var" => "password", + "hidden" => "true" + }, + { + "LDAP Search Base" => "", + "config_var" => "search_base" + } + ], + $usersConfFile => [ + { + "Administration username for Traffic Ops" => "admin", + "config_var" => "tmAdminUser" + }, + { + "Password for the admin user" => "", + "config_var" => "tmAdminPw", + "hidden" => "true" + } + ], + $profilesConfFile => [ + { + "Add custom profiles?" => "no", + "config_var" => "custom_profiles" + } + ], + $opensslConfFile => [ + { + "Do you want to generate a certificate?" => "yes", + "config_var" => "genCert" + }, + { + "Country Name (2 letter code)" => "XX", + "config_var" => "country" + }, + { + "State or Province Name (full name)" => "San Jose", + "config_var" => "state" + }, + { + "Locality Name (eg, city)" => "Default City", + "config_var" => "locality" + }, + { + "Organization Name (eg, company)" => "Default Company Ltd", + "config_var" => "company" + }, + { + "Organizational Unit Name (eg, section)" => "", + "config_var" => "org_unit" + }, + { + "Common Name (eg, your name or your server's hostname)" => "example.com", + "config_var" => "common_name" + }, + { + "RSA Passphrase" => "", + "config_var" => "rsaPassword", + "hidden" => "true" + } + ], + $paramConfFile => [ + { + "Traffic Ops url" => "https://localhost", + "config_var" => "tm.url" + }, + { + "Human-readable CDN Name. (No whitespace, please)" => "kabletown_cdn", + "config_var" => "cdn_name" + }, + { + "Health Polling Interval (milliseconds)" => "8000", + "config_var" => "health_polling_int" + }, + { + "DNS sub-domain for which your CDN is authoritative" => "cdn1.kabletown.net", + "config_var" => "dns_subdomain" + }, + { + "TLD SOA admin" => "traffic_ops", + "config_var" => "soa_admin" + }, + { + "TrafficServer Drive Prefix" => "/dev/sd", + "config_var" => "driver_prefix" + }, + { + "TrafficServer RAM Drive Prefix" => "/dev/ram", + "config_var" => "ram_drive_prefix" + }, + { + "TrafficServer RAM Drive Letters (comma separated)" => "0,1,2,3,4,5,6,7", + "config_var" => "ram_drive_letters" + }, + { + "Health Threshold Load Average" => "25", + "config_var" => "health_thresh_load_avg" + }, + { + "Health Threshold Available Bandwidth in Kbps" => "1750000", + "config_var" => "health_thresh_kbps" + }, + { + "Traffic Server Health Connection Timeout (milliseconds)" => "2000", + "config_var" => "health_connect_timeout" + } + + ] + }; } -#print "\nRunning smoke tests.\n\n"; -#$rc = execCommand ("/opt/traffic_ops/install/bin/systemtest", "localhost", $user{username}, $tmAdminPw, "0"); -{ - my $ans = promptUser( "\nInstall Cron entry to clean install .iso files older than 7 days? [y/n]", "n" ); - if ($ans eq "y" || $ans eq "Y") { - execCommand( "/bin/echo \"00 04 * * * root /bin/find /opt/traffic_ops/app/public/iso/*.iso -mtime +7 -exec /bin/rm {} \; > /dev/null 2>&1 \" > /etc/cron.d/trafops_clean_isos" ); - } +# carried over from old postinstall +# +# todbconf: The database configuration to be used +# opensslconf: The openssl configuration if any + +sub setupDatabase { + my $todbconf = shift; + my $opensslconf = shift; + my $genCert = shift; + my $setupMaxmind = shift; + + InstallUtils::logger( "Setting up database", "info" ); + chdir("/opt/traffic_ops/app"); + my $result = InstallUtils::execCommand( "/usr/bin/perl", "db/admin.pl", "--env=production", "setup" ); + + if ( $result != 0 ) { + errorOut("Database initialization failed"); + } + else { + InstallUtils::logger( "Database initialization succeeded", "info" ); + } + + if ( $setupMaxmind =~ /^y(?:es)?/ ) { + InstallUtils::logger( "Downloading Maxmind data", "info" ); + chdir("/opt/traffic_ops/app/public/routing"); + $result = InstallUtils::execCommand("/usr/bin/wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz"); + if ( $result != 0 ) { + InstallUtils::logger( "Failed to download MaxMind data", "error" ); + } + } + else { + InstallUtils::logger("Not downloading Maxmind data"); + } + + InstallUtils::logger( "Copying coverage zone file to public dir", "info" ); + $result = InstallUtils::execCommand("/bin/mv /opt/traffic_ops/app/public/coverage-zone.json ."); + if ( $result != 0 ) { + InstallUtils::logger( "Failed to copy coverage zone file", "error" ); + } + + if ( lc $genCert =~ /^y(?:es)?/ ) { + if ( -x "/usr/bin/openssl" ) { + InstallUtils::logger( "Installing SSL Certificates", "info" ); + $result = GenerateCert::createCert($opensslconf); + + if ( $result != 0 ) { + errorOut("SSL Certificate Installation failed"); + } + else { + InstallUtils::logger( "SSL Certificates have been installed", "info" ); + } + } + else { + InstallUtils::logger( "Unable to install SSL certificates as openssl is not installed", "error" ); + InstallUtils::logger( "Install openssl and then run /opt/traffic_ops/install/bin/generateCert to install SSL certificates", "error" ); + exit 4; + } + } + else { + InstallUtils::logger( "Not generating openssl certification", "info" ); + } } -{ - my $ans = promptUser( "\nShutdown Traffic Ops [y/n]", "n" ); - if ( $ans eq "y" ) { - print "\nShutting down Traffic Ops.\n\n"; - execCommand( "/sbin/service", "traffic_ops", "stop" ); - } +# -cfile - Input File: The input config file used to ask and answer questions +# -a - Automatic mode: If there are questions in the config file which do not have answers, the script +# will look to the defaults for the answer. If the answer is not in the defaults +# the script will exit +# -r - Reconfigure: Whether or not to reconfigure the database and check perl dependencies - This will recreate the database +# -defaults - Defaults: Writes out a configuration file with defaults which can be used as input +# -debug - Debug Mode: More output to the terminal +# -h - Help: Basic command line help menu + +sub main { + my $help = 0; + + # help string + my $usageString = "Usage: postinstall [-a] [-debug] [-defaults] [-r] -cfile=[config_file]\n"; + + GetOptions( + "cfile=s" => \$inputFile, + "automatic" => \$automatic, + "reconfigure" => \$reconfigure, + "defaults" => \$dumpDefaults, + "debug" => \$debug, + "help" => \$help + ) or die($usageString); + + # stores the default questions and answers + $defaultInputs = getDefaults(); + + if ($help) { + print $usageString; + return; + } + + # check if the user running postinstall is root + if ( $ENV{USER} ne "root" ) { + errorOut("You must run this script as the root user"); + } + + InstallUtils::initLogger( $debug, $logFile ); + + if ( -f "$logFile.gz" ) { + InstallUtils::execCommand( "/bin/gunzip", "$logFile.gz" ); + } + + InstallUtils::logger( "Starting postinstall", "info" ); + + InstallUtils::logger( "Debug is on", "info" ); + + if ($automatic) { + InstallUtils::logger( "Running in automatic mode", "info" ); + } + + # check if the reconfigure_file is present on the system - if it is let the user know its deprecated + # and exit with an error + if ( -f $reconfigure_file ) { + InstallUtils::logger( "$reconfigure_file file is depreprecated - please remove and rerun postinstall", "error" ); + return; + } + + if ($dumpDefaults) { + InstallUtils::logger( "Writing default configuration file to $outputConfigFile", "info" ); + InstallUtils::writeJson( $outputConfigFile, $defaultInputs ); + return; + } + + InstallUtils::logger( "Postinstall " . ( defined $reconfigure ? "in" : "not" ) . " in reconfigure mode", "info" ); + + InstallUtils::rotateLog($cpanLogFile); + + if ( -s $logFile > $maxLogSize ) { + InstallUtils::logger( "Postinstall log above max size of $maxLogSize bytes - rotating", "info" ); + rotateLog($logFile); + } + + # used to store the questions and answers provided by the user + my $userInput; + + # if no input file provided use the defaults + if ( $inputFile eq "" ) { + InstallUtils::logger( "No input file given - using defaults", "info" ); + $userInput = $defaultInputs; + } + else { + InstallUtils::logger( "Using input file $inputFile", "info" ); + + # check if the input file exists + errorOut("File '$inputFile' not found") if ( !-f $inputFile ); + + # read and store the input file + $userInput = InstallUtils::readJson($inputFile); + } + + # sanity check the defaults if running them automatically + sanityCheckDefaults(); + + # check the input config file against the defaults to check for missing questions + sanityCheckConfig($userInput) if ( $inputFile ne "" ); + + chdir("/opt/traffic_ops/install/bin"); + + # if reconfigure is set then rebuild the perl deps + if ($reconfigure) { + my $rc = BuildPerlDeps::build( 1, $cpanLogFile ); + if ( $rc != 0 ) { + errorOut("Failed to install perl dependencies, check the console output and rerun postinstall once you've resolved the error"); + } + $rc = InstallUtils::execCommand( "./download_web_deps", "-i" ); + if ( $rc != 0 ) { + errorOut("Failed to install Traffic Ops Web dependencies, check the console output and rerun postinstall once you've resolved the error"); + } + } + else { + my $rc = BuildPerlDeps::build(); + if ( $rc != 0 ) { + errorOut("Failed to install perl dependencies, check the console output and rerun postinstall once you've resolved the error"); + } + $rc = InstallUtils::execCommand( "./download_web_deps", "-i" ); + if ( $rc != 0 ) { + errorOut("Failed to install Traffic Ops Web dependencies, check the console output and rerun postinstall once you've resolved the error"); + } + } + + # The generator functions handle checking input/default/automatic mode + + # todbconf will be used later when setting up the database + my $todbconf = generateDbConf( $userInput, $databaseConfFile, $dbConfFile ); + generateCdnConf( $userInput, $cdnConfFile ); + generateLdapConf( $userInput, $ldapConfFile ); + my $adminconf = generateUsersConf( $userInput, $usersConfFile ); + my $custom_profile = generateProfilesDir( $userInput, $profilesConfFile ); + my $opensslconf = generateOpenSSLConf( $userInput, $opensslConfFile ); + my $paramconf = generateParamConf( $userInput, $paramConfFile ); + + if ( !-f $post_install_cfg ) { + InstallUtils::writeJson( $post_install_cfg, {} ); + } + + # if reconfigure is set then setup the database + if ($reconfigure) { + if ($automatic) { + setupDatabase( $todbconf, $opensslConfFile, $opensslconf->{"genCert"}, $todbconf->{"maxmind"} ); + } + else { + setupDatabase( $todbconf, 0, $opensslconf->{"genCert"}, $todbconf->{"maxmind"} ); + } + } + + InstallUtils::logger( "Starting Traffic Ops", "info" ); + InstallUtils::execCommand("/sbin/service traffic_ops start"); + + InstallUtils::logger( "Waiting for Traffic Ops to start", "info" ); + + if ( -f $post_install_cfg ) { + $parameters = InstallUtils::readJson($post_install_cfg); + } + + if ( !ProfileCleanup::profiles_exist( $adminconf, $paramconf->{"tm.url"}, $parameters, $reconfigure_defaults, $reconfigure ) ) { + InstallUtils::logger( "Creating default profiles...", "info" ); + ProfileCleanup::replace_profile_templates( $paramconf, $adminconf, $post_install_cfg, $parameters, $profileDir ); + ProfileCleanup::import_profiles( $paramconf->{"tm.url"}, $adminconf, $profileDir ); + + # call again to create $reconfigure_defaults file if import was successful + ProfileCleanup::profiles_exist( $adminconf, $paramconf->{"tm.url"}, $parameters, $reconfigure_defaults, $reconfigure ); + } + else { + InstallUtils::logger( "Not creating default profiles", "info" ); + } + + if ( $custom_profile =~ /^y(?:es)?/ ) { + ProfileCleanup::add_custom_profiles( $custom_profile_dir, $adminconf, $parameters->{"tm.url"} ); + } + + InstallUtils::logger("Postinstall complete"); + + InstallUtils::execCommand( "/bin/gzip", "$logFile" ); } -print "\nTo start Traffic Ops: service traffic_ops start\n"; -print "To stop Traffic Ops: service traffic_ops stop\n"; -print "\n"; +main; -exit 0; +# vi:syntax=perl diff --git a/traffic_ops/install/lib/BuildPerlDeps.pm b/traffic_ops/install/lib/BuildPerlDeps.pm new file mode 100644 index 0000000000..2bf5eedbd8 --- /dev/null +++ b/traffic_ops/install/lib/BuildPerlDeps.pm @@ -0,0 +1,103 @@ +#!/usr/bin/perl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +use lib qw(/opt/traffic_ops/install/lib /opt/traffic_ops/lib/perl5 /opt/traffic_ops/app/lib); + +package BuildPerlDeps; + +use strict; +use warnings; + +use InstallUtils qw{ :all }; + +use base qw{ Exporter }; +our @EXPORT_OK = qw{ build }; +our %EXPORT_TAGS = ( all => \@EXPORT_OK ); + +sub build { + my $opt_i = shift; + my $cpanLogFile = shift; + + my @dependencies = ( "expat-devel", "mod_ssl", "mkisofs", "libpcap", "libpcap-devel", "libcurl", "libcurl-devel", "mysql-server", "mysql-devel", "openssl", "openssl-devel", "cpan", "gcc", "make", "pkgconfig", "automake", "autoconf", "libtool", "gettext", "libidn-devel" ); + + my $msg = << 'EOF'; + +This script will build and package the required Traffic Ops perl modules. +In order to complete this operation, Development tools such as the gcc +compiler will be installed on this machine. + +EOF + + $ENV{PERL_MM_USE_DEFAULT} = 1; + $ENV{PERL_MM_NONINTERACTIVE} = 1; + $ENV{AUTOMATED_TESTING} = 1; + + my $result; + + if ( $ENV{USER} ne "root" ) { + errorOut("You must run this script as the root user"); + } + + InstallUtils::logger( $msg, "info" ); + + chdir("/opt/traffic_ops/app"); + + if ( defined $opt_i && $opt_i == 1 ) { + if ( !-x "/usr/bin/yum" ) { + errorOut("You must install 'yum'"); + } + + InstallUtils::logger( "Installing dependencies", "info" ); + $result = InstallUtils::execCommand( "/usr/bin/yum", "-y", "install", @dependencies ); + if ( $result != 0 ) { + errorOut("Dependency installation failed, look through the output and correct the problem"); + } + InstallUtils::logger( "Building perl modules", "info" ); + + $result = InstallUtils::execCommand( "/usr/bin/cpan", "pi_custom_log=" . $cpanLogFile, "-if", "YAML" ); + if ( $result != 0 ) { + errorOut("Failed to install YAML, look through the output and correct the problem"); + } + + $result = InstallUtils::execCommand( "/usr/bin/cpan", "pi_custom_log=" . $cpanLogFile, "-if", "MIYAGAWA/Carton-v1.0.15.tar.gz" ); + if ( $result != 0 ) { + errorOut("Failed to install Carton, look through the output and correct the problem"); + } + } + + $result = InstallUtils::execCommand( "/usr/local/bin/carton", "install" ); + if ( $result != 0 ) { + errorOut("Failure to build required perl modules, check the output and correct the problem"); + } + + if ( !-s "/opt/traffic_ops/lib/perl5" ) { + InstallUtils::logger( "Linking perl libraries...", "info" ); + if ( !-d "/opt/traffic_ops/lib" ) { + mkdir("/opt/traffic_ops/lib"); + } + symlink( "/opt/traffic_ops/app/local/lib/perl5", "/opt/traffic_ops/lib/perl5" ); + InstallUtils::execCommand( "/bin/chown", "-R", "trafops:trafops", "/opt/traffic_ops/lib" ); + } + InstallUtils::logger( "Installing perl scripts", "info" ); + chdir("/opt/traffic_ops/app/local/bin"); + my $rc = InstallUtils::execCommand( "/bin/cp", "-R", ".", "/opt/traffic_ops/app/bin" ); + if ( $rc != 0 ) { + InstallUtils::logger( "Failed to copy perl scripts to /opt/traffic_ops/app/bin", "error" ); + } + + return 0; +} + +1; diff --git a/traffic_ops/install/lib/GenerateCert.pm b/traffic_ops/install/lib/GenerateCert.pm new file mode 100644 index 0000000000..644e4829e1 --- /dev/null +++ b/traffic_ops/install/lib/GenerateCert.pm @@ -0,0 +1,240 @@ +#!/usr/bin/perl + +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package GenerateCert; + +use strict; + +use lib qw(/opt/traffic_ops/install/lib /opt/traffic_ops/lib/perl5 /opt/traffic_ops/app/lib); + +use base qw{ Exporter }; +our @EXPORT_OK = qw{ createCert }; +our %EXPORT_TAGS = ( all => \@EXPORT_OK ); + +use JSON; +use InstallUtils; +use File::Temp; +use Data::Dumper; +use File::Copy; +use InstallUtils qw{ :all }; + +my $ca = "/etc/pki/tls/certs/localhost.ca"; +my $csr = "/etc/pki/tls/certs/localhost.csr"; +my $cert = "/etc/pki/tls/certs/localhost.crt"; +my $cdn_conf = "/opt/traffic_ops/app/conf/cdn.conf"; +my $key = "/etc/pki/tls/private/localhost.key"; +my $msg = << 'EOF'; + + We're now running a script to generate a self signed X509 SSL certificate. + +EOF + +sub writeCdn_conf { + my $cdn_conf = shift; + + # listen param to be inserted + my $listen_str = "https://[::]:443?cert=${cert}&key=${key}&ca=${ca}&verify=0x00&ciphers=AES128-GCM-SHA256:HIGH:!RC4:!MD5:!aNULL:!EDH:!ED"; + + # load as perl hash to find string to be replaced + my $cdnh = do $cdn_conf; + if ( exists $cdnh->{hypnotoad} ) { + $cdnh->{hypnotoad}{listen} = [$listen_str]; + } + else { + # add the whole hypnotoad config without affecting anything else in the config + $cdnh->{hypnotoad} = { + listen => [$listen_str], + user => 'trafops', + group => 'trafops', + pid_file => '/var/run/traffic_ops.pid', + workers => 48, + }; + } + + # dump conf data in compact but readable form + my $dumper = Data::Dumper->new( [$cdnh] ); + $dumper->Indent(1)->Terse(1)->Quotekeys(0); + + # write whole config to temp file in pwd (keeps in same filesystem) + my $tmpfile = File::Temp->new( DIR => '.' ); + print $tmpfile $dumper->Dump(); + close $tmpfile; + + # make backup of current file + my $backup_num = 0; + my $backup_name; + do { + $backup_num++; + $backup_name = "$cdn_conf.backup$backup_num"; + } while ( -e $backup_name ); + rename( $cdn_conf, $backup_name ) or die("rename(): $!"); + + # rename temp file to cdn.conf and set ownership/permissions same as backup + my @stats = stat($backup_name); + my ( $uid, $gid, $perm ) = @stats[ 4, 5, 2 ]; + move( "$tmpfile", $cdn_conf ) or die("move(): $!"); + + chown $uid, $gid, $cdn_conf; + chmod $perm, $cdn_conf; +} + +# execOpenssl takes a description of the command being done, and an array of arguments to OpenSSL, +# and tries to execute the command, on failure prompting the user to retry. +# The description should be capitalized, but not terminated with punctuation. +# Returns the OpenSSL exit code. +sub execOpenssl { + my ( $description, @args ) = @_; + InstallUtils::logger( $description, "info" ); + my $result = 1; + while ( $result != 0 ) { + $result = InstallUtils::execCommand( "openssl", @args ); + if ( $result != 0 ) { + my $ans = ""; + while ( $ans !~ /^[yY]/ && $ans !~ /^[nN]/ ) { + $ans = InstallUtils::promptUser( $description . " failed. Try again (y/n)", "y" ); + } + if ( $ans =~ /^[nN]/ ) { + return $result; + } + } + } + return $result; +} + +sub createCert { + + # the file used for ssl configuration + my $opensslconf = shift; + + if ( !defined $opensslconf ) { + InstallUtils::logger( "No input file - running openssl configuration in interactive mode", "info" ); + } + + InstallUtils::logger( $msg, "info" ); + + InstallUtils::logger( "Postinstall SSL Certificate Creation", "info" ); + + my $params; + my $passphrase; + + # load the parameters for the certificate + if ( defined $opensslconf ) { + my $config = InstallUtils::readJson($opensslconf); + if ( defined $config->{country} ) { + + # the parameters to auto generate the certificate + $params = "/C=$config->{country}/ST=$config->{state}/L=$config->{locality}/O=$config->{company}/OU=$config->{org_unit}/CN=$config->{common_name}/"; + + $passphrase = $config->{rsaPassword}; + } + } + + InstallUtils::logger( "The server key has been generated", "info" ); + + if ($params) { + if ( execOpenssl( "Generating an RSA Private Server Key", "genrsa", "-des3", "-out", "server.key", "-passout", "pass:$passphrase", "1024" ) != 0 ) { + exit 1; + } + if ( execOpenssl( "Creating a Certificate Signing Request (CSR)", "req", "-new", "-key", "server.key", "-out", "server.csr", "-passin", "pass:$passphrase", "-subj", $params ) != 0 ) { + exit 1; + } + } + else { + if ( execOpenssl( "Generating an RSA Private Server Key", "genrsa", "-des3", "-out", "server.key", "1024" ) != 0 ) { + exit 1; + } + if ( execOpenssl( "Creating a Certificate Signing Request (CSR)", "req", "-new", "-key", "server.key", "-out", "server.csr") != 0 ) { + exit 1; + } + } + + InstallUtils::logger( "The Certificate Signing Request has been generated", "info" ); + + InstallUtils::execCommand( "/bin/mv", "server.key", "server.key.orig" ); + + if ($params) { + if ( execOpenssl( "Removing the pass phrase from the server key", "rsa", "-in", "server.key.orig", "-out", "server.key", "-passin", "pass:$passphrase" ) != 0 ) { + exit 1; + } + } + else { + if ( execOpenssl( "Removing the pass phrase from the server key", "rsa", "-in", "server.key.orig", "-out", "server.key") != 0 ) { + exit 1; + } + } + + InstallUtils::logger( "The pass phrase has been removed from the server key", "info" ); + + if ( execOpenssl( "Generating a Self-signed certificate", "x509", "-req", "-days", "365", "-in", "server.csr", "-signkey", "server.key", "-out", "server.crt" ) != 0 ) { + exit 1; + } + InstallUtils::logger( "A server key and self signed certificate has been generated", "info" ); + + InstallUtils::logger( "Installing the server key and server certificate", "info" ); + + my $result = InstallUtils::execCommand( "/bin/cp", "server.key", "$key" ); + if ( $result != 0 ) { + errorOut("Failed to install the private server key"); + } + $result = InstallUtils::execCommand( "/bin/chmod", "600", "$key" ); + $result = InstallUtils::execCommand( "/bin/chown", "trafops:trafops", "$key" ); + + if ( $result != 0 ) { + errorOut("Failed to install the private server key"); + } + + InstallUtils::logger( "The private key has been installed", "info" ); + InstallUtils::logger( "Installing the self signed certificate", "info" ); + + $result = InstallUtils::execCommand( "/bin/cp", "server.crt", "$cert" ); + + if ( $result != 0 ) { + errorOut("Failed to install the self signed certificate"); + } + + $result = InstallUtils::execCommand( "/bin/chmod", "600", "$cert" ); + $result = InstallUtils::execCommand( "/bin/chown", "trafops:trafops", "$cert" ); + + if ( $result != 0 ) { + errorOut("Failed to install the self signed certificate"); + } + + InstallUtils::logger( "Saving the self signed csr", "info" ); + $result = InstallUtils::execCommand( "/bin/cp", "server.csr", "$csr" ); + + if ( $result != 0 ) { + errorOut("Failed to save the self signed csr"); + } + $result = InstallUtils::execCommand( "/bin/chmod", "664", "$csr" ); + $result = InstallUtils::execCommand( "/bin/chown", "trafops:trafops", "$csr" ); + + writeCdn_conf($cdn_conf); + + my $msg = << 'EOF'; + + The self signed certificate has now been installed. + + You may obtain a certificate signed by a Certificate Authority using the + server.csr file saved in the current directory. Once you have obtained + a signed certificate, copy it to /etc/pki/tls/certs/localhost.crt and + restart Traffic Ops. + +EOF + + InstallUtils::logger( $msg, "info" ); + + return 0; +} diff --git a/traffic_ops/install/lib/InstallUtils.pm b/traffic_ops/install/lib/InstallUtils.pm index adc0566ece..c6e1840ac0 100644 --- a/traffic_ops/install/lib/InstallUtils.pm +++ b/traffic_ops/install/lib/InstallUtils.pm @@ -1,100 +1,246 @@ +#!/usr/bin/perl + # +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # +# http://www.apache.org/licenses/LICENSE-2.0 # +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # package InstallUtils; use Term::ReadPassword; +use JSON; +use IO::Pipe; use base qw{ Exporter }; -our @EXPORT_OK = qw{ execCommand randomWord promptUser promptRequired promptPassword promptPasswordVerify trim}; +our @EXPORT_OK = qw{ execCommand randomWord promptUser promptRequired promptPassword promptPasswordVerify trim readJson writeJson writePerl errorOut logger rotateLog}; our %EXPORT_TAGS = ( all => \@EXPORT_OK ); +my $logFile; +my $debug; + +sub initLogger { + $debug = shift; + $logFile = shift; +} + sub execCommand { - my ( $cmd, @args ) = @_; - system( $cmd, @args ); - my $result = $? >> 8; - return $result; + my ( $command, @args ) = @_; + + my $pipe = IO::Pipe->new; + my $pid; + my $result = 0; + my $customLog = ""; + + # find log file in args and remove if found + # if there is a string in the list of args which starts with 'pi_custom_log=' then remove it from the parameters and use + # it as the log file for the exec + foreach my $var (@args) { + if ( index( $var, "pi_custom_log=" ) != -1 ) { + $customLog = ( split( /=/, $var ) )[1]; + splice( @args, index( $var, "pi_custom_log=" ), 1 ); + logger( "Using custom log '$customLog'", "info" ); + } + } + + # create pipe between child and parent and redirect output from child to parent for logging + my $child = open READER, '-|'; + defined $child or die "pipe/fork: $!\n"; + if ($child) { #parent + while ( $line = ) { + + # log all output from child pipe + if ( $customLog ne "" ) { + logger( $line, "info", $customLog ); + } + else { + logger( $line, "info" ); + } + } + } + else { #child + # redirect stderr to stdout so parent can read + open STDERR, '>&STDOUT'; + exec( $command, @args ) or exit(1); + } +} + +# log the error and then kill the process +sub errorOut { + logger( @_, "error" ); + die; +} + +# moves a log to file to a backup file with the same name appended with .bkp +# This function is intended to keep log file sizes low and is called from postinstall +sub rotateLog { + my $logFileName = shift; + + if ( !-f $logFileName ) { + logger( "Log file '$logFileName' does not exist - not rotating log", "warn" ); + return; + } + + execCommand( '/bin/mv', '-f', $logFileName, $logFileName . '.bkp' ); + logger( "Rotated log $logFileName", "info" ); +} + +# outputs logging messages to terminal and log file +sub logger { + my $output = shift; + my $type = shift; + + # optional custom log file to use instead of main log file used by postinstall + # cpan uses a custom log file because of its size + my $customLogFile = shift; + + my $message = $output; + if ( index( $message, "\n" ) == -1 ) { + $message = $message . "\n"; + } + + # if in debug mode or message is more critical than info print to console + if ( $debug || ( defined $type && $type ne "" && $type ne "info" ) ) { + print($message); + } + + # output to log file + my $fh; + my $result = 0; + if ( defined $customLogFile && $customLogFile ne "" ) { + open $fh, '>>', $customLogFile or die("Couldn't open log file '$customLogFile'"); + $result = 1; + } + else { + if ($logFile) { + open( $fh, '>>', $logFile ) or die("Couldn't open log file '$logFile'"); + $result = 1; + } + } + + if ($result) { + print $fh localtime . ": " . uc($type) . ' ' . $message; + close $fh; + } } sub randomWord { - my $length = shift || 12; - my $secret = ''; - while ( length($secret) < $length ) { - my $c = chr( rand(0x7F) ); - if ( $c =~ /\w/ ) { - $secret .= $c; - } - } - return $secret; + my $length = shift || 12; + my $secret = ''; + while ( length($secret) < $length ) { + my $c = chr( rand(0x7F) ); + if ( $c =~ /\w/ ) { + $secret .= $c; + } + } + return $secret; } sub promptUser { - my ( $promptString, $defaultValue, $noEcho ) = @_; - - if ($defaultValue) { - print $promptString, " [", $defaultValue, "]: "; - } - else { - print $promptString, ": "; - } - - if ( defined $noEcho && $noEcho ) { - my $response = read_password(''); - if ( ( !defined $response || $response eq '' ) && ( defined $defaultValue && $defaultValue ne '' ) ) { - $response = $defaultValue; - } - return $response; - } - else { - $| = 1; - $_ = ; - chomp; - - if ("$defaultValue") { - return $_ ? $_ : $defaultValue; - } - else { - return $_; - } - return $_; - } + my ( $promptString, $defaultValue, $noEcho ) = @_; + + if ($defaultValue) { + print $promptString, " [", $defaultValue, "]: "; + } + else { + print $promptString, ": "; + } + + if ( defined $noEcho && $noEcho ) { + my $response = read_password(''); + if ( ( !defined $response || $response eq '' ) && ( defined $defaultValue && $defaultValue ne '' ) ) { + $response = $defaultValue; + } + return $response; + } + else { + $| = 1; + $_ = ; + chomp; + + if ("$defaultValue") { + return $_ ? $_ : $defaultValue; + } + else { + return $_; + } + return $_; + } } sub promptRequired { - my $val = ''; - while ( length($val) == 0 ) { - $val = promptUser(@_); - } - return $val; + my $val = ''; + while ( length($val) == 0 ) { + $val = promptUser(@_); + } + return $val; } sub promptPassword { - my $prompt = shift; - my $pw = promptRequired( $prompt, '', 1 ); - return $pw; + my $prompt = shift; + my $pw = promptRequired( $prompt, '', 1 ); + return $pw; } sub promptPasswordVerify { - my $prompt = shift; - my $pw = shift; - - while (1) { - $pw = promptPassword($prompt); - my $verify = promptPassword("Re-Enter $prompt"); - last if $pw eq $verify; - print "\nError: passwords do not match, try again.\n\n"; - } - return $pw; + my $prompt = shift; + my $pw = shift; + + while (1) { + $pw = promptPassword($prompt); + my $verify = promptPassword("Re-Enter $prompt"); + last if $pw eq $verify; + print "\nError: passwords do not match, try again.\n\n"; + } + return $pw; } sub trim { - my $str = shift; + my $str = shift; + + $str =~ s/^\s+//; + $str =~ s/^\s+$//; + + return $str; +} + +sub readJson { + my $file = shift; + open( my $fh, '<', $file ) or die("open(): $!"); + local $/; # slurp mode + my $text = <$fh>; + undef $fh; + return JSON->new->utf8->decode($text); +} + +sub writeJson { + my $file = shift; + open( my $fh, '>', $file ) or die("open(): $!"); + logger( "Writing json to $file", "info" ); + foreach my $data (@_) { + my $json_text = JSON->new->utf8->pretty->encode($data); + print $fh $json_text, "\n"; + } + close $fh; +} + +sub writePerl { + my $file = shift; + my $data = shift; - $str =~ s/^\s+//; - $str =~ s/^\s+$//; + open( my $fh, '>', $file ) or die("open(): $!"); + my $dumper = Data::Dumper->new( [$data] ); - return $str; + # print without var names and with simple indentation + print $fh $dumper->Terse(1)->Dump(); + close $fh; } 1; diff --git a/traffic_ops/install/lib/ProfileCleanup.pm b/traffic_ops/install/lib/ProfileCleanup.pm new file mode 100644 index 0000000000..d9828cc2c1 --- /dev/null +++ b/traffic_ops/install/lib/ProfileCleanup.pm @@ -0,0 +1,251 @@ +#!/usr/bin/perl + +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package ProfileCleanup; + +use warnings; +use strict; + +use InstallUtils qw{ :all }; +use WWW::Curl::Easy; +use LWP::UserAgent; + +use base qw{ Exporter }; +our @EXPORT_OK = qw{ replace_profile_templates import_profiles profiles_exist add_custom_profiles }; +our %EXPORT_TAGS = ( all => \@EXPORT_OK ); + +sub profile_replace { + my $profile = shift; + my $adminconf = shift; + my $parameters = shift; + + my $profile_bak = $profile . ".bak"; + InstallUtils::logger( "Replacing parameters in profile: $profile", "info" ); + rename( $profile, $profile_bak ) or die("rename(): $!"); + open( my $fh, '<', $profile_bak ) or die("open(): $!"); + open( my $ofh, '>', $profile ) or die("open(): $!"); + while (<$fh>) { + s/{{.TmUrl}}/$parameters->{'tm.url'}/g; + s/{{.TmInfoUrl}}/$parameters->{"tminfo.url"}/g; + s/{{.TmInstanceName}}/$parameters->{"cdnname"}/g; + s/{{.GeolocationPollingUrl}}/$parameters->{"geolocation.polling.url"}/g; + s/{{.Geolocation6PollingUrl}}/$parameters->{"geolocation6.polling.url"}/g; + s/{{.TmUrl}}/$parameters->{'tm.url'}/g; + s/{{.TmToolName}}/Traffic Ops/g; + s/{{.HealthPollingInterval}}/$parameters->{"health.polling.interval"}/g; + s/{{.CoveragezonePollingUrl}}/$parameters->{"coveragezone.polling.url"}/g; + s/{{.DomainName}}/$parameters->{"domainname"}/g; + s/{{.TldSoaAdmin}}/$parameters->{"tld.soa.admin"}/g; + s/{{.DrivePrefix}}/$parameters->{"Drive_Prefix"}/g; + s/{{.HealthThresholdLoadavg}}/$parameters->{"health.threshold.loadavg"}/g; + s/{{.HealthThresholdAvailableBandwidthInKbps}}/$parameters->{"health.threshold.availableBandwidthInKbps"}/g; + s/{{.RAMDrivePrefix}}/$parameters->{"RAM_Drive_Prefix"}/g; + s/{{.RAMDriveLetters}}/$parameters->{"RAM_Drive_Letters"}/g; + s/{{.HealthConnectionTimeout}}/$parameters->{"health.connection.timeout"}/g; + s#{{.CronOrtSyncds}}#*/15 * * * * root /opt/ort/traffic_ops_ort.pl syncds warn $parameters->{'tm.url'} $adminconf->{tmAdminUser}:$adminconf->{tmAdminPw} > /tmp/ort/syncds.log 2>&1#g; + print $ofh $_; + } + close $fh; + close $ofh; + unlink $profile_bak; +} + +sub replace_profile_templates { + my $conf = shift; + my $adminconf = shift; + my $post_install_cfg = shift; + my $parameters = shift; + my $profileDir = shift; + + $parameters->{'tm.url'} = $conf->{"tm.url"}; + $parameters->{"tminfo.url"} = "$parameters->{'tm.url'}/info"; + $parameters->{"cdnname"} = $conf->{"cdn_name"}; + $parameters->{"geolocation.polling.url"} = "$parameters->{'tm.url'}/routing/GeoIP2-City.mmdb.gz"; + $parameters->{"geolocation6.polling.url"} = "$parameters->{'tm.url'}/routing/GeoIP2-Cityv6.mmdb.gz"; + $parameters->{"health.polling.interval"} = $conf->{"health_polling_int"}; + $parameters->{"coveragezone.polling.url"} = "$parameters->{'tm.url'}/routing/coverage-zone.json"; + $parameters->{"domainname"} = $conf->{"dns_subdomain"}; + $parameters->{"tld.soa.admin"} = $conf->{"soa_admin"}; + $parameters->{"Drive_Prefix"} = $conf->{"driver_prefix"}; + $parameters->{"RAM_Drive_Prefix"} = $conf->{"ram_drive_prefix"}; + $parameters->{"RAM_Drive_Letters"} = $conf->{"ram_drive_letters"}; + $parameters->{"health.threshold.loadavg"} = $conf->{"health_thresh_load_avg"}; + $parameters->{"health.threshold.availableBandwidthInKbps"} = substr( $conf->{"health_thresh_kbps"}, 0, 1 ) eq ">" ? "" : ">" . $conf->{"health_thresh_kbps"}; + $parameters->{"health.connection.timeout"} = $conf->{"health_connect_timeout"}; + + profile_replace( $profileDir . "profile.global.traffic_ops", $adminconf, $parameters ); + profile_replace( $profileDir . "profile.traffic_monitor.traffic_ops", $adminconf, $parameters ); + profile_replace( $profileDir . "profile.traffic_router.traffic_ops", $adminconf, $parameters ); + profile_replace( $profileDir . "profile.trafficserver_edge.traffic_ops", $adminconf, $parameters ); + profile_replace( $profileDir . "profile.trafficserver_mid.traffic_ops", $adminconf, $parameters ); + writeJson( $post_install_cfg, $parameters ); +} + +# Takes the Traffic Ops URI, user, and password. +# Returns the cookie, or the empty string on error +sub get_traffic_ops_cookie { + my ( $uri, $user, $pass ) = @_; + + my $loginUri = "/api/1.2/user/login"; + + my $curl = WWW::Curl::Easy->new; + my $response_body = ""; + open( my $fileb, ">", \$response_body ); + my $loginData = JSON::encode_json( { u => $user, p => $pass } ); + $curl->setopt( WWW::Curl::Easy::CURLOPT_URL, $uri . $loginUri ); + $curl->setopt( WWW::Curl::Easy::CURLOPT_SSL_VERIFYPEER, 0 ); + $curl->setopt( WWW::Curl::Easy::CURLOPT_HEADER, 1 ); # include header in response + $curl->setopt( WWW::Curl::Easy::CURLOPT_NOBODY, 1 ); # disclude body in response + $curl->setopt( WWW::Curl::Easy::CURLOPT_POST, 1 ); + $curl->setopt( WWW::Curl::Easy::CURLOPT_POSTFIELDS, $loginData ); + $curl->setopt( WWW::Curl::Easy::CURLOPT_WRITEDATA, $fileb ); # put response in this var + $curl->perform(); + + my $cookie = $response_body; + if ( $cookie =~ /mojolicious=(.*); expires/ ) { + $cookie = $1; + } + else { + $cookie = ""; + } + return $cookie; +} + +# Takes the filename of a Traffic Ops (TO) profile to import, the TO URI, and the TO login cookie +sub profile_import_single { + my ( $profileFilename, $uri, $trafficOpsCookie ) = @_; + InstallUtils::logger( "Importing Profiles with: " . "curl -v -k -X POST -H \"Cookie: mojolicious=$trafficOpsCookie\" -F \"filename=$profileFilename\" -F \"profile_to_import=\@$profileFilename\" $uri/profile/doImport", "info" ); + my $rc = InstallUtils::execCommand("curl -v -k -X POST -H \"Cookie: mojolicious=$trafficOpsCookie\" -F \"filename=$profileFilename\" -F \"profile_to_import=\@$profileFilename\" $uri/profile/doImport"); + if ( $rc != 0 ) { + InstallUtils::logger( "Failed to import Traffic Ops profile, check the console output and rerun postinstall once you've resolved the error", "error" ); + } +} + +sub import_profiles { + my $toUri = shift; + my $adminconf = shift; + my $profileDir = shift; + + InstallUtils::logger( "Importing profiles...", "info" ); + + my $toUser = $adminconf->{"username"}; + my $toPass = $adminconf->{"password"}; + + my $toCookie = get_traffic_ops_cookie( $toUri, $toUser, $toPass ); + + InstallUtils::logger( "Got cookie: " . $toCookie, "info" ); + + # \todo use an array? + InstallUtils::logger( "Importing Global profile...", "info" ); + profile_import_single( $profileDir . "profile.global.traffic_ops", $toUri, $toCookie ); + InstallUtils::logger( "Importing Traffic Monitor profile...", "info" ); + profile_import_single( $profileDir . "profile.traffic_monitor.traffic_ops", $toUri, $toCookie ); + InstallUtils::logger( "Importing Traffic Router profile...", "info" ); + profile_import_single( $profileDir . "profile.traffic_router.traffic_ops", $toUri, $toCookie ); + InstallUtils::logger( "Importing TrafficServer Edge profile...", "info" ); + profile_import_single( $profileDir . "profile.trafficserver_edge.traffic_ops", $toUri, $toCookie ); + InstallUtils::logger( "Importing TrafficServer Mid profile...", "info" ); + profile_import_single( $profileDir . "profile.trafficserver_mid.traffic_ops", $toUri, $toCookie ); + InstallUtils::logger( "Finished Importing Profiles.", "info" ); +} + +sub profiles_exist { + my $config = shift; + my $tmurl = shift; + my $parameters = shift; + my $reconfigure_defaults = shift; + my $reconfigure = shift; + + if ( -f $reconfigure_defaults ) { + InstallUtils::logger( "Default profiles were previously created. Remove " . $reconfigure_defaults . " to create again", "warn" ); + return 1; + } + + $parameters->{'tm.url'} = $tmurl; + + InstallUtils::logger( "Checking profiles at $tmurl using username " . $config->{"username"}, "info" ); + + my $uri = $parameters->{'tm.url'}; + my $toCookie = get_traffic_ops_cookie( $parameters->{'tm.url'}, $config->{"username"}, $config->{"password"} ); + + my $profileEndpoint = "/api/1.2/profiles.json"; + + my $ua = LWP::UserAgent->new; + $ua->ssl_opts( verify_hostname => 0, SSL_verify_mode => 0x00 ); + my $req = HTTP::Request->new( GET => $uri . $profileEndpoint ); + $req->header( 'Cookie' => "mojolicious=" . $toCookie ); + my $resp = $ua->request($req); + + if ( !$resp->is_success ) { + InstallUtils::logger( "Error checking if profiles exist: " . $resp->status_line, "error" ); + return 1; # return true, so we don't attempt to create profiles + } + my $message = $resp->decoded_content; + + my $profiles = JSON->new->utf8->decode($message); + if ( ( !defined $profiles->{"response"} ) + || ( ref $profiles->{"response"} ne 'ARRAY' ) ) + { + InstallUtils::logger( "Error checking if profiles exist: invalid JSON: $message", "error" ); + return 1; # return true, so we don't attempt to create profiles + } + + my $num_profiles = scalar( @{ $profiles->{"response"} } ); + InstallUtils::logger( "Existing Profile Count: $num_profiles", "info" ); + + my %initial_profiles = ( + "INFLUXDB" => 1, + "RIAK_ALL" => 1, + "TRAFFIC_STATS" => 1 + ); + + my $profiles_response = $profiles->{"response"}; + foreach my $profile (@$profiles_response) { + if ( !exists $initial_profiles{ $profile->{"name"} } ) { + InstallUtils::logger( "Found existing profile (" . $profile->{"name"} . ")", "info" ); + open( my $reconfigure_defaults, '>', $reconfigure ) or die("Failed to open() $reconfigure_defaults: $!"); + close($reconfigure_defaults); + return 1; + } + } + return 0; +} + +sub add_custom_profiles { + my $custom_profile_dir = shift; + my $adminconf = shift; + my $toUri = shift; + + return if ( !-e $custom_profile_dir ); + + opendir( DH, $custom_profile_dir ) || return; + my @profiles = readdir(DH); + closedir(DH); + @profiles = grep( /^profile\..*\.traffic_ops$/, @profiles ); + + return if ( scalar @profiles == 0 ); + + my $toUser = $adminconf->{tmAdminUser}; + my $toPass = $adminconf->{tmAdminPw}; + my $toCookie = get_traffic_ops_cookie( $toUri, $toUser, $toPass ); + + foreach my $profile (@profiles) { + print "\nimport profile " . $custom_profile_dir . $profile . "\n\n"; + profile_import_single( $custom_profile_dir . $profile, $toUri, $toCookie ); + } +} + +1; diff --git a/traffic_ops/install/lib/WebDep.pm b/traffic_ops/install/lib/WebDep.pm index e7db9b68da..edf25fb532 100644 --- a/traffic_ops/install/lib/WebDep.pm +++ b/traffic_ops/install/lib/WebDep.pm @@ -1,6 +1,7 @@ package WebDep; # +# Copyright 2015 Comcast Cable Communications Management, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -30,205 +31,205 @@ my @columns = qw{name version cdn_location compression filename source_dir final # class method returns list of WebDep objects loaded from file sub getDeps { - my $class = shift; - - my $webdeps_file = shift; - my @deps; - open my $fh, '<', $webdeps_file or die "Can't open $webdeps_file\n"; - - # final_dir within webdeps_file are absolute or relative to directory that file is in - my $oldcwd = getcwd(); - my $webdeps_dir = dirname($webdeps_file); - chdir($webdeps_dir); - while (<$fh>) { - - # comments only if line starts with # - next if /^#/; - chomp; - my @parts = split( /\s*,\s*/, $_ ); - my $obj = bless {}, $class; - for my $attrib (@columns) { - $obj->{$attrib} = shift @parts; - } - $obj->{filename} =~ s{^/}{}; - $obj->{source_dir} =~ s{/$}{}; - $obj->{source_dir} =~ s{/$}{}; - - my $final_dir = $obj->{final_dir}; - if ( !-d $final_dir ) { - print "Making dir: $final_dir\n"; - mkpath($final_dir); - } - $obj->{final_dir} = abs_path($final_dir); - push( @deps, $obj ); - } - chdir($oldcwd); - return @deps; + my $class = shift; + + my $webdeps_file = shift; + my @deps; + open my $fh, '<', $webdeps_file or die "Can't open $webdeps_file\n"; + + # final_dir within webdeps_file are absolute or relative to directory that file is in + my $oldcwd = getcwd(); + my $webdeps_dir = dirname($webdeps_file); + chdir($webdeps_dir); + while (<$fh>) { + + # comments only if line starts with # + next if /^#/; + chomp; + my @parts = split( /\s*,\s*/, $_ ); + my $obj = bless {}, $class; + for my $attrib (@columns) { + $obj->{$attrib} = shift @parts; + } + $obj->{filename} =~ s{^/}{}; + $obj->{source_dir} =~ s{/$}{}; + $obj->{source_dir} =~ s{/$}{}; + + my $final_dir = $obj->{final_dir}; + if ( !-d $final_dir ) { + print "Making dir: $final_dir\n"; + mkpath($final_dir); + } + $obj->{final_dir} = abs_path($final_dir); + push( @deps, $obj ); + } + chdir($oldcwd); + return @deps; } sub getSrcFileName { - my $self = shift; - my @parts; - push @parts, $self->{source_dir} if $self->{source_dir} ne ''; - push @parts, $self->{filename}; - return join( '/', @parts ); + my $self = shift; + my @parts; + push @parts, $self->{source_dir} if $self->{source_dir} ne ''; + push @parts, $self->{filename}; + return join( '/', @parts ); } sub getDestFileName { - my $self = shift; - return join( '/', $self->{final_dir}, $self->{filename} ); + my $self = shift; + return join( '/', $self->{final_dir}, $self->{filename} ); } sub getDownloadContent { - my $self = shift; - my ( $response_body, $err ) = _getDownloadContent( $self->{cdn_location} ); - return ( $response_body, $err ); + my $self = shift; + my ( $response_body, $err ) = _getDownloadContent( $self->{cdn_location} ); + return ( $response_body, $err ); } sub getContent { - my $self = shift; - my ( $content, $err ) = $self->getDownloadContent(); - if ( defined $err ) { - die "$err\n"; - } - my $srcfn = $self->getSrcFileName(); - if ( !exists $self->{content} ) { - if ( $self->{compression} eq 'zip' ) { - - #print "Unzipping $srcfn\n"; - my $u = IO::Uncompress::Unzip->new( \$content ) or die "IO::Uncompress::Unzip failed: $UnzipError\n"; - my $found; - while ( $u->nextStream() > 0 && !$u->eof() ) { - my $name = $u->getHeaderInfo()->{Name}; - if ( $name eq $srcfn ) { - $found = $name; - last; - } - } - if ( !defined $found ) { - die "$srcfn not found in " . $self->{cdn_location} . "\n"; - } - - undef $/; # slurp mode - $content = <$u>; - $u->close(); - } - - $self->{content} = $content; - } - return $self->{content}; + my $self = shift; + my ( $content, $err ) = $self->getDownloadContent(); + if ( defined $err ) { + die "$err\n"; + } + my $srcfn = $self->getSrcFileName(); + if ( !exists $self->{content} ) { + if ( $self->{compression} eq 'zip' ) { + + #print "Unzipping $srcfn\n"; + my $u = IO::Uncompress::Unzip->new( \$content ) or die "IO::Uncompress::Unzip failed: $UnzipError\n"; + my $found; + while ( $u->nextStream() > 0 && !$u->eof() ) { + my $name = $u->getHeaderInfo()->{Name}; + if ( $name eq $srcfn ) { + $found = $name; + last; + } + } + if ( !defined $found ) { + die "$srcfn not found in " . $self->{cdn_location} . "\n"; + } + + undef $/; # slurp mode + $content = <$u>; + $u->close(); + } + + $self->{content} = $content; + } + return $self->{content}; } sub needsUpdating { - my $self = shift; - my $fn = $self->getDestFileName(); + my $self = shift; + my $fn = $self->getDestFileName(); - # checksum and compare - open my $fh, '<', $fn or die "Can't open existing file: $fn\n"; - my $md5_existing = md5_hex(<$fh>); - close $fh; + # checksum and compare + open my $fh, '<', $fn or die "Can't open existing file: $fn\n"; + my $md5_existing = md5_hex(<$fh>); + close $fh; - my $md5_new = md5_hex( $self->getContent() ); - my $needsUpdating = ( $md5_new ne $md5_existing ); - return $needsUpdating; + my $md5_new = md5_hex( $self->getContent() ); + my $needsUpdating = ( $md5_new ne $md5_existing ); + return $needsUpdating; } sub update { - my $self = shift; - my $err; - my $srcfn = $self->getSrcFileName(); - my $destfn = $self->getDestFileName(); - my $action = ""; - - # download archive - if ( -f $destfn ) { - if ( !$self->needsUpdating() ) { - $action = "Kept"; - return ( $action, $err ); - } - - # exists but needs to be replaced - $action = "Replaced"; - } - else { - $action = "Created"; - } - my $content = $self->getContent(); - open my $ofh, '>', $destfn or $err = "Can't write to $destfn"; - if ( !defined $err ) { - print $ofh $content; - close $ofh; - } - return ( $action, $err ); + my $self = shift; + my $err; + my $srcfn = $self->getSrcFileName(); + my $destfn = $self->getDestFileName(); + my $action = ""; + + # download archive + if ( -f $destfn ) { + if ( !$self->needsUpdating() ) { + $action = "Kept"; + return ( $action, $err ); + } + + # exists but needs to be replaced + $action = "Replaced"; + } + else { + $action = "Created"; + } + my $content = $self->getContent(); + open my $ofh, '>', $destfn or $err = "Can't write to $destfn"; + if ( !defined $err ) { + print $ofh $content; + close $ofh; + } + return ( $action, $err ); } #################################################################### # Utilities sub execCommand { - my ( $command, @args ) = @_; - my $pid = fork(); - my $result = 0; - - if ( $pid == 0 ) { - exec( $command, @args ); - exit 0; - } - else { - wait; - $result = $?; - if ( $result != 0 ) { - print "ERROR executing: $command, args: " . join( ' ', @args ) . "\n"; - } - } - return $result; + my ( $command, @args ) = @_; + my $pid = fork(); + my $result = 0; + + if ( $pid == 0 ) { + exec( $command, @args ); + exit 0; + } + else { + wait; + $result = $?; + if ( $result != 0 ) { + print "ERROR executing: $command, args: " . join( ' ', @args ) . "\n"; + } + } + return $result; } sub curlMe { - my $url = shift; - my $curl = WWW::Curl::Easy->new; - my $response_body; - my $err; # undef if no error - - $curl->setopt( CURLOPT_VERBOSE, 0 ); - if ( $url =~ m/https/ ) { - $curl->setopt( CURLOPT_SSL_VERIFYHOST, 0 ); - $curl->setopt( CURLOPT_SSL_VERIFYPEER, 0 ); - } - $curl->setopt( CURLOPT_IPRESOLVE, 1 ); - $curl->setopt( CURLOPT_FOLLOWLOCATION, 1 ); - $curl->setopt( CURLOPT_CONNECTTIMEOUT, 5 ); - $curl->setopt( CURLOPT_TIMEOUT, 15 ); - $curl->setopt( CURLOPT_HEADER, 0 ); - $curl->setopt( CURLOPT_URL, $url ); - $curl->setopt( CURLOPT_WRITEDATA, \$response_body ); - my $retcode = $curl->perform; - my $response_code = $curl->getinfo(CURLINFO_HTTP_CODE); - - if ( $response_code != 200 ) { - $err = "Got HTTP $response_code response for '$url'"; - } - elsif ( length($response_body) == 0 ) { - $err = "URL: $url returned empty!!"; - } - return ( $response_body, $err ); + my $url = shift; + my $curl = WWW::Curl::Easy->new; + my $response_body; + my $err; # undef if no error + + $curl->setopt( CURLOPT_VERBOSE, 0 ); + if ( $url =~ m/https/ ) { + $curl->setopt( CURLOPT_SSL_VERIFYHOST, 0 ); + $curl->setopt( CURLOPT_SSL_VERIFYPEER, 0 ); + } + $curl->setopt( CURLOPT_IPRESOLVE, 1 ); + $curl->setopt( CURLOPT_FOLLOWLOCATION, 1 ); + $curl->setopt( CURLOPT_CONNECTTIMEOUT, 5 ); + $curl->setopt( CURLOPT_TIMEOUT, 15 ); + $curl->setopt( CURLOPT_HEADER, 0 ); + $curl->setopt( CURLOPT_URL, $url ); + $curl->setopt( CURLOPT_WRITEDATA, \$response_body ); + my $retcode = $curl->perform; + my $response_code = $curl->getinfo(CURLINFO_HTTP_CODE); + + if ( $response_code != 200 ) { + $err = "Got HTTP $response_code response for '$url'"; + } + elsif ( length($response_body) == 0 ) { + $err = "URL: $url returned empty!!"; + } + return ( $response_body, $err ); } { - # cache cdn locs -- some files extracted from same downloaded archive - my %content_for; - - sub _getDownloadContent { - my $cdnloc = shift; - my $err; - if ( !exists $content_for{$cdnloc} ) { - my $response_body; - ( $response_body, $err ) = curlMe($cdnloc); - $content_for{$cdnloc} = $response_body; - } - - # could be undef indicating previous error - return ( $content_for{$cdnloc}, $err ); - } + # cache cdn locs -- some files extracted from same downloaded archive + my %content_for; + + sub _getDownloadContent { + my $cdnloc = shift; + my $err; + if ( !exists $content_for{$cdnloc} ) { + my $response_body; + ( $response_body, $err ) = curlMe($cdnloc); + $content_for{$cdnloc} = $response_body; + } + + # could be undef indicating previous error + return ( $content_for{$cdnloc}, $err ); + } } 1;