#! /usr/bin/env perl use strict; use warnings; # install.pl # # installer for perl script, in this case, sysinfo # # Revision history # # Version 1.1.7 20161010 RWR # Added ability to validate required libraries are installed # # version 1.2 20170327 RWR # did some major modifications to correctly work on BSD systems also # # version 2.0 20190330 RWR # changed it so all configs are YAML # # version 3.0 20191105 RWR # set up so all options are presented on initial screen # user can choose options to edit # rest of install is automatic # # version 3.1 20191112 RWR # Added logging. Log file will be written to the install directory with # the application name.log as the name (application name taken from installer_config.pl) # # version 3.1.1 20191117 RWR # Added the ability to verify cpan is installed and configured on systems which need it # # version 3.1.2 20200215 RWR # Adding version comparisons # find our location and use it for searching for libraries BEGIN { use FindBin; use File::Spec; use lib File::Spec->catdir($FindBin::Bin); eval( 'use YAML::Tiny' ); } my $sourceDir = File::Spec->catdir($FindBin::Bin); use Digest::MD5 qw(md5_hex); # define the version number # see https://metacpan.org/pod/release/JPEACOCK/version-0.97/lib/version.pod use version; our $VERSION = version->declare("v3.001.002"); use Data::Dumper; use File::Basename; use Getopt::Long; our %install; our %operatingSystems; our %libraries; our %binaries; do "$sourceDir/installer_config.pl"; # # set up log file my $logFile = $install{'application name'}; $logFile =~ s/ /_/g; $logFile = "$sourceDir/$logFile.log"; Getopt::Long::Configure ("bundling"); # allow -vd --os='debian' my $dryRun = 0; my $os; my $help = 0; my $version = 0; my @messages; # stores any messages we want to show up at the end my @feedback; # store feedback when we execute command line actions my %configuration; # simple display if --help is passed sub help { my $oses = join( ' ', keys %operatingSystems ); use File::Basename; print basename($0) . " $VERSION\n"; print <; chomp $thisAnswer; $thisAnswer = $default unless $thisAnswer; return $thisAnswer; } ####################################################### # function to simply log things # first parameter is the priority, if <= $logDef->{'log level'} will print # all subsequent parameters assumed to be strings to sent to the log # returns 0 on failure # 1 on success # 2 if priority > log level # -1 if $logDef is unset # currently, only logs to a file ####################################################### sub logIt { open LOG, ">>$logFile" or die "Could not append to $logFile: $!\n"; while ( my $t = shift ) { print LOG &timeStamp() . "\t$t\n"; } close LOG; return 1; } # attempt to locate the operating system. # if found, will set some defaults for it. sub setUpOperatingSystemSpecific { my ( $install, $operatingSystems, $os, $installDir ) = @_; &logIt( 'Entering setUpOperatingSystemSpecific' ); &logIt( "They passed $os in as the \$os" ); if ( $os ) { # We found the OS, set up some defaults $$install{'os'} = $os; &logIt( "Setting keys for operating system" ); # merge operatingSystems into install foreach my $key ( keys %{$operatingSystems->{$os}} ) { if ( $key eq 'files' ) { $install->{'files'} = { %{$install->{'files'}}, %{$operatingSystems->{$os}->{'files'}} } } else { $install->{$key} = $operatingSystems->{ $os }->{$key}; } } # if it is a known OS } # if return $os; } # getOperatingSystem # validates the libraries needed exist # simply eval's each library. If it doesn't exist, creates a list of # commands to be executed to install it, and displays missing libraries # offering to install them if possible. sub validateLibraries { my ( $libraries, $os ) = @_; &logIt( 'Entering validateLibraries' ); my %return; foreach my $lib ( keys %$libraries ) { &logIt( "Checking on library $lib" ); eval( "use $lib;" ); if ( $@ ) { $return{ $lib } = $libraries->{$lib}->{$os} ? $libraries->{$lib}->{$os} : 'UNK'; } } return \%return; } # validateLibraries # check for any required binaries sub validateBinaries { my ( $binaries, $os ) = @_; &logIt( 'Entering validateBinaries' ); my %return; foreach my $bin ( keys %$binaries ) { unless ( `which $bin` ) { $return{$bin} = $binaries->{$bin}->{$os} ? $binaries->{$bin}->{$os} : 'UNK'; } } return \%return; } # validateBinaries # get some input from the user and decide how to install/upgrade/remove/whatever sub getInstallActions { my $install = shift; &logIt( 'Entering getInstallActions' ); if ( -d $install->{'confdir'} ) { $install->{'action'} = "upgrade"; } else { $install->{'action'} = 'install'; } $install->{'build config'} = 'Y'; $install->{'setup cron'} = 'Y'; } # locate all items in $hash which have one of the $placeholder elements in it # and replace, ie is replaced with the actual binary directory sub doPlaceholderSubstitution { my ($hash, $placeholder) = @_; &logIt( 'Entering doPlaceholderSubstitution' ); return if ref $hash ne 'HASH'; foreach my $key ( keys %$hash ) { if ( ref( $$hash{$key} ) ) { &doPlaceholderSubstitution( $$hash{$key}, $placeholder ); } else { foreach my $place ( keys %$placeholder ) { $$hash{$key} =~ s/$place/$$placeholder{$place}/; } # foreach } # if..else } # foreach return; } # This will go through and first, see if anything is a directory, in # which case, we'll create new entries for all files in there. # then, it will do keyword substitution of and # to populate the target. # When this is done, each file should have a source and target that is # a fully qualified path and filename sub populateSourceDir { my ( $install, $sourceDir ) = @_; &logIt( 'Entering populateSourceDir' ); my %placeHolders = ( '' => $$install{'bindir'}, '' => $$install{'confdir'}, '' => $$install{'default owner'}, '' => $$install{'default group'}, '' => $$install{'default permission'}, '' => $sourceDir ); my $allFiles = $$install{'files'}; # find all directory entries and load files in that directory into $$install{'files'} foreach my $dir ( keys %$allFiles ) { if ( defined( $$allFiles{$dir}{'type'} ) && $$allFiles{$dir}{'type'} eq 'directory' ) { &logIt( "Found directory $dir" ); if ( opendir( my $dh, "$sourceDir/$dir" ) ) { my @files = map{ $dir . '/' . $_ } grep { ! /^\./ && -f "$sourceDir/$dir/$_" } readdir( $dh ); &logIt( "\tFound files " . join( ' ', @files ) ); foreach my $file ( @files ) { $$allFiles{ $file }{'type'} = 'file'; if ( $dir eq 'modules' ) { $$allFiles{ $file }{'permission'} = ( $file =~ m/$$install{'modules'}/ ) ? '0700' : '0600'; } else { $$allFiles{ $file }{'permission'} = $$allFiles{ $dir }{'permission'}; } $$allFiles{ $file }{'owner'} = $$allFiles{ $dir }{'owner'}; $$allFiles{ $file }{'target'} = $$allFiles{ $dir }{'target'}; } # foreach closedir $dh; } # if opendir } # if it is a directory } # foreach # find all files, and set the source directory, and add the filename to # the target foreach my $file ( keys %$allFiles ) { $$allFiles{$file}{'source'} = "$sourceDir/$file"; $$allFiles{$file}{'target'} .= "/$file"; } # foreach # finally, do place holder substitution. This recursively replaces all keys # in %placeHolders with the values. &doPlaceholderSubstitution( $install, \%placeHolders ); &logIt( "Exiting populateSourceDir with values\n" . Dumper( $install ) ) ; return 1; } # populateSourceDir sub GetPermission { my $install = shift; &logIt( 'Entering GetPermission' ); print "Ready to install, please verify the following\n"; print "A. Operating System: " . $install->{'os'} . "\n"; print "B. Installation Type: " . $install->{'action'} . "\n"; print "C. Using Seed file: " . $install->{'configuration seed file'} . "\n" if -e $install->{'configuration seed file'}; print "D. Target Binary Directory: " . $install->{'bindir'} . "\n"; print "E. Target Configuration Directory: " . $install->{'confdir'} . "\n"; print "F. Automatic Running: " . ( $install->{'crontab'} ? $install->{'crontab'} : 'No' ) . "\n"; print "G. Install Missing Perl Libraries\n"; foreach my $task ( sort keys %{ $install->{'missing libraries'} } ) { print "\t$task -> " . $install->{'missing libraries'}->{$task}->{'command'} . ' ' . $install->{'missing libraries'}->{$task}->{'parameter'} . "\n"; } print "H. Install Missing Binaries\n"; foreach my $task ( sort keys %{ $install->{'missing binaries'} } ) { print "\t$task -> " . $install->{'missing binaries'}->{$task}->{'command'} . ' ' . $install->{'missing binaries'}->{$task}->{'parameter'} . "\n"; } return &yesno( "Do you want to proceed?" ); } # note, this fails badly if you stick non-numerics in the version number # simply create a "number" from the version which may have an arbitrary # number of digits separated by a period, for example # 1.2.5 # while there are digits, divide current calculation by 100, then add the last # digit popped of. So, 1.2.5 would become # 1 + 2/100 + 5/10000, or 1.0205 # and 1.25.16 would become 1.2516 # # Will give invalid results if any set of digits is more than 99 sub dotVersion2Number { my $dotVersion = shift; my @t = split( '\.', $dotVersion ); #print "\n$dotVersion\n" . join( "\n", @t ) . "\n"; my $return = 0; while ( @t ) { $return /= 100; $return += pop @t; } #print "$return\n"; return $return; } # there is a file named VERSIONS. We get the values out of the install # directory and (if it exists) the target so we can decide what needs # to be updated. sub getVersions { my $install = shift; &logIt( 'Entering getVersions' ); my $currentVersionFile = $install->{'files'}->{'VERSION'}->{'target'}; my $newVersionFile = $install->{'files'}->{'VERSION'}->{'source'}; if ( open FILE,"<$currentVersionFile" ) { while ( my $line = ) { chomp $line; my ( $filename, $version, $checksum ) = split( "\t", $line ); $install{'files'}->{$filename}->{'installed version'} = $version ? $version : ''; $install{'files'}->{$filename}->{'installed checksum'} = $checksum ? $checksum : ''; } close FILE; } if ( open FILE,"<$newVersionFile" ) { while ( my $line = ) { chomp $line; my ( $filename, $version, $checksum ) = split( "\t", $line ); $install->{'files'}->{$filename}->{'new version'} = $version ? $version : ''; $install->{'files'}->{$filename}->{'new checksum'} = $checksum ? $checksum : ''; } close FILE; } foreach my $file ( keys %{$$install{'files'}} ) { $install{'files'}->{$file}->{'installed version'} = '' unless defined $install->{'files'}->{$file}->{'installed version'}; $install{'files'}->{$file}->{'new version'} = '' unless defined $install->{'files'}->{$file}->{'new version'}; } return 1; } # getVersions # checks if a directory exists and, if not, creates it my %directories; # holds list of directories already created so no need to do an I/O sub checkDirectoryExists { my ( $filename,$mod,$owner ) = @_; $mod = "0700" unless $mod; $owner = "root:root" unless $owner; &logIt( "Checking Directory for $filename with $mod and $owner" ); my ($fn, $dirname) = fileparse( $filename ); logIt( "\tParsing out $dirname and $filename" ); return '' if exists $directories{$dirname}; if ( -d $dirname ) { $directories{$dirname} = 1; return ''; } if ( &runCommand( "mkdir -p $dirname", "chmod $mod $dirname", "chown $owner $dirname" ) ) { $directories{$dirname} = 1; } return ''; } # runs a system command. Also, if in testing mode, simply shows what # would have been done. sub runCommand { while ( my $command = shift ) { if ( $dryRun ) { print "$command\n"; } else { `$command`; } } return 1; } # runCommand # this actually does the installation, except for the configuration sub doInstall { my $install = shift; &logIt( 'Entering doInstall' ); my $fileList = $install->{'files'}; &checkDirectoryExists( $install->{'bindir'} . '/', $install->{'default permission'}, $install->{'default owner'} . ':' . $install->{'default group'} ); foreach my $file ( keys %$fileList ) { next unless ( $fileList->{$file}->{'type'} && $fileList->{$file}->{'type'} eq 'file' ); # *********** Removed error checking to get this working; should reenable later # if ( version->parse( $fileList->{$file}->{'installed version'} ) && version->parse( $fileList->{$file}->{'installed version'} ) < version->parse( $fileList->{$file}->{'new version'} ) ) { # # we have a new version, so overwrite it # } elsif ( $fileList->{$file}->{'installed checksum'} eq $fileList->{$file}->{'installed checksum'} ) { # has file been modified # } # # # next if $install->{'action'} eq 'upgrade' && ! defined( $fileList->{$file}->{'installed version'} ) # || # ( &dotVersion2Number( $fileList->{$file}->{'new version'} ) <= &dotVersion2Number($fileList->{$file}->{'installed version'} ) ); &checkDirectoryExists( $fileList->{$file}->{'target'}, $install->{'default permission'}, $install->{'default owner'} . ':' . $install->{'default group'} ); &runCommand( "cp $fileList->{$file}->{'source'} $fileList->{$file}->{'target'}", "chmod $fileList->{$file}->{'permission'} $fileList->{$file}->{'target'}", "chown $fileList->{$file}->{'owner'} $fileList->{$file}->{'target'}" ); # if there is a post action, run it and store any return in @feedback push @feedback, `$fileList->{$file}->{'post action'}` if defined $fileList->{$file}->{'post action'}; # if there is a message to be passed, store it in @messages push @messages, $fileList->{$file}->{'message'} if defined $fileList->{$file}->{'message'}; } # foreach file # set up crontab, if necessary &runCommand( $install->{'crontab'} ) if defined ( $install->{'crontab'} ); return 1; } # installs binaries and libraries sub installOSStuff { my $install = shift; my %commands; my @actions = values( %{ $install->{'missing libraries'} } ); push @actions, values( %{ $install->{'missing binaries'} } ); foreach my $action ( @actions ) { $commands{$action->{'command'}} .= ' ' . $action->{'parameter'}; #&logIt( $action ); #&runCommand( $action ); } foreach my $command ( keys %commands ) { print "Running command $command $commands{$command}\n"; `$command $commands{$command}`; } } ################################################################ # validateCPAN # # some of the systems will need to run cpan to get some perl modules. # this will go through each of them and see if command starts with cpan # and, if so, will check that cpan is installed and configured for root. # # when cpan is installed, it requires one manual run as root from the cli # to determine where to get the files. If that is not done, cpan can not # be controlled by a program. We check to see if cpan is installed, then # verify /root/.cpan has been created by the configuration tool ################################################################ sub validateCPAN { my $libraries = shift; my $needCPAN = 0; foreach my $app ( keys %$libraries ) { if ( $libraries->{$app}->{'command'} =~ m/^cpan/ ) { $needCPAN = 1; last; } } return unless $needCPAN; if ( `which cpan` ) { die "****ERROR****\nWe need cpan, and it is installed, but not configured\nrun cpan as root one time to configure it\n" unless -d '/root/.cpan'; } else { die 'In order to install on this OS, we need cpan, which should have been installed with perl.' . " Can not continue until cpan is installed and configured\n"; } } ################################################################ # Main Code # ################################################################ # handle any command line parameters that may have been passed in GetOptions ( "os|o=s" => \$os, # pass in the operating system "dryrun|n" => \$dryRun, # do NOT actually do anything 'help|h' => \$help, 'version|v' => \$version ) or die "Error parsing command line\n"; if ( $help ) { &help() ; exit; } if ( $version ) { use File::Basename; print basename($0) . " $VERSION\n"; exit; } print "Logging to $logFile\n"; &logIt( 'Beginning installation' ); $install{'os'} = &setUpOperatingSystemSpecific( \%install, \%operatingSystems, $os ? $os : `$sourceDir/determineOS`, $sourceDir ); &logIt( "Operating System is $install{'os'}" ); $install{'missing libraries'} = &validateLibraries( \%libraries, $install{'os'} ); &logIt( "Missing Libraries\n" . Dumper( $install{'missing libraries'} ) ); $install{'missing binaries'} = &validateBinaries( \%binaries, $install{'os'} ); &logIt( "Missing binaries\n" . Dumper( $install{'missing binaries'} ) ); if ( $install{'missing libraries'} ) { &validateCPAN( $install{'missing libraries'} ); } &getInstallActions( \%install ); &logIt( "Completed getInstallActions\n" . Dumper( \%install ) ); &populateSourceDir( \%install, $sourceDir ); if ( &GetPermission( \%install ) ) { &installOSStuff( \%install ); &getVersions( \%install ) unless $install{'action'} eq 'new'; &doInstall( \%install ); } else { die "Please fix whatever needs to be done and try again\n"; &logIt( "User chose to kill process" ); } &logIt( "Installation done, running \&postInstall if it exists" ); &postInstall( \%install )if defined( &postInstall ); 1;