Rev 210 | Rev 214 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed
#! /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
#
# version 3.3.0 20230308 RWR
# major revision. Allows --quiet (don't ask questions). Correctly handles when the source has been installed via a
# separate process. Mainly designed to allow checkout from svn repository.
# find our location and use it for searching for libraries
BEGIN {
use FindBin;
use File::Spec;
# use libraries from the directory this script is in
use lib File::Spec->catdir($FindBin::Bin);
# and its parent
use lib File::Spec->catdir( $FindBin::Bin . '/../' );
eval( 'use YAML::Tiny' );
}
use Cwd qw(abs_path);
my $installerDir = abs_path(File::Spec->catdir($FindBin::Bin) );
my $sourceDir = abs_path($installerDir . '/..');
use Digest::MD5 qw(md5_hex);
use File::Copy;
# define the version number
# see https://metacpan.org/pod/release/JPEACOCK/version-0.97/lib/version.pod
use version;
our $VERSION = version->declare("v3.003.000");
use Data::Dumper;
use File::Basename;
use Getopt::Long;
our %install;
our %operatingSystems;
our %libraries;
our %binaries;
# load the definitions from installer_config.pl
do "$installerDir/installer_config.pl";
#
# set up log file
my $logFile = $install{'application name'};
$logFile =~ s/ /_/g;
$logFile = "$installerDir/$logFile.log";
Getopt::Long::Configure ("bundling"); # allow -vd --os='debian'
my $dryRun = 0;
my $os;
my $help = 0;
my $version = 0;
my $quiet = 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 <<END
$0 [options]
Options:
--os - osname is one of [$oses]
--dryrun - do not actually do anything, just tell you what I'd do
--version - display version and exit
--quiet - Do not ask questions, just do stuff
For an inplace upgrade, it is assumed the source code has been
downloaded and installed into the correct directories already
END
}
#######################################################
#
# timeStamp
#
# return current system date as YYYY-MM-DD HH:MM:SS
#
#######################################################
sub timeStamp {
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
return sprintf "%4d-%02d-%02d %02d:%02d:%02d",$year+1900,$mon+1,$mday,$hour,$min,$sec;
}
sub yesno {
my ( $prompt, $default ) = @_;
$default = 'yes' unless $default;
my $answer = &getAnswer( $prompt, $default eq 'yes' ? ('yes','no' ) : ('no', 'yes') );
return lc( substr( $answer, 0, 1 ) ) eq 'y';
}
# prompt the user for a response, then allow them to enter it
# the first response is considered the default and is printed
# in all caps if more than one exists
# first answer is used if they simply press the Enter
# key. The response is returned all lower case if more than one
# exists.
# it is assumed
sub getAnswer {
my ( $prompt, @answers ) = @_;
$answers[0] = '' unless defined( $answers[0] );
my $default = $answers[0];
my $onlyOneAnswer = scalar( @answers ) == 1;
print $prompt . '[ ';
$answers[0] = uc $answers[0] unless $onlyOneAnswer;
print join( ' | ', @answers ) . ' ]: ';
my $thisAnswer = <>;
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 <binddir> 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 <bindir> and <confdir>
# 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 massageInstallValues {
my ( $install, $sourceDir ) = @_;
&logIt( 'Entering massageInstallValues' );
my %placeHolders =
(
'<bindir>' => $$install{'bindir'},
'<confdir>' => $$install{'confdir'},
'<default owner>' => $$install{'default owner'},
'<default group>' => $$install{'default group'},
'<default permission>' => $$install{'default permission'},
'<installdir>' => $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 = <FILE> ) {
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 = <FILE> ) {
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'} ) );
# check the directory and permissions, creating directory and setting permissions if necessary
&checkDirectoryExists( $fileList->{$file}->{'target'}, $install->{'default permission'}, $install->{'default owner'} . ':' . $install->{'default group'} );
# copy the files to the new directory
&runCommand( "cp $fileList->{$file}->{'source'} $fileList->{$file}->{'target'}" ) unless $fileList->{$file}->{'source'} eq $fileList->{$file}->{'target'};
&runCommand( "chmod $fileList->{$file}->{'permission'} $fileList->{$file}->{'target'}",
"chown $fileList->{$file}->{'owner'} $fileList->{$file}->{'target'}"
) if -e $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
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 ) {
&logIt( $command );
print "Running command $command $commands{$command}\n" unless $quiet;
`$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";
}
}
# set up the config file.
# at worst, we'll have an empty configuration file
sub checkConfig {
my $install = shift;
# if the configuration file does not exist, create one
if ( not -e $install->{'configuration'}->{'configuration file'} ) {
&logIt( "Configuration file " . $install->{'configuration'}->{'configuration file'} . " does not exist, creating" );
&checkDirectoryExists(
$install->{'configuration'}->{'configuration file'},
$install->{'configuration'}->{'permission'},
$install->{'configuration'}->{'owner'}
);
if ( -e $install->{'configuration seed file'} ) {
&logIt( "Seed file " . $install->{'configuration seed file'} . " exists, using as default" );
copy( $install->{'configuration seed file'}, $install->{'configuration'}->{'configuration file'} );
} else { # we don't have a seed, and we don't have a configuration file, so just give it an empty one
&logIt( "No seed file or configuration file, creating an empty YAML" );
open YAML, '>' . $install->{'configuration'}->{'configuration file'}
or die "Could not create configuration file " . $install->{'configuration'}->{'configuration file'} . ": $!\n";
print YAML "# This is an empty configuration file\n---\n";
close YAML;
}
}
}
# create a cron entry if necessary
sub cronTab {
my $install = shift;
# set up crontab, if necessary
&logIt("Setting up crontab as " . $install->{'crontab'} );
&runCommand( $install->{'crontab'} ) if defined ( $install->{'crontab'} );
}
################################################################
# 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,
'quiet|q' => \$quiet
) 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' );
# determine the operating system and set up installer hash with correct values
$install{'os'} = &setUpOperatingSystemSpecific( \%install, \%operatingSystems, $os ? $os : `$installerDir/determineOS`, $sourceDir );
&logIt( "Operating System is $install{'os'}" );
# check libraries necessary are loaded, and if not, track the ones we need
$install{'missing libraries'} = &validateLibraries( \%libraries, $install{'os'} );
&logIt( "Missing Libraries\n" . Dumper( $install{'missing libraries'} ) );
# Check that required binaries are installed and create list of the ones we need
$install{'missing binaries'} = &validateBinaries( \%binaries, $install{'os'} );
&logIt( "Missing binaries\n" . Dumper( $install{'missing binaries'} ) );
# if we need libraries and the os needs to use CPAN, validate CPAN
&validateCPAN( $install{'missing libraries'} ) if ( $install{'missing libraries'} );
# determine if it is an upgrade or fresh install and default to creating conf and cron
&getInstallActions( \%install );
&logIt( "Completed getInstallActions\n" . Dumper( \%install ) );
# go through %install and replace all variables with correct values
# moving directories as needed in the %install
&massageInstallValues( \%install, $sourceDir );
#print Dumper( \%install ) ; die;
# ask permission if $quiet not set
if ( $quiet || &GetPermission( \%install ) ) {
# install binaries and libraries as needed
&installOSStuff( \%install );
# do a version comparison unless we already have it in place, or it is a new install
&getVersions( \%install );
# move the binaries in place unless they are already there
&doInstall( \%install );
} else {
die "Please fix whatever needs to be done and try again\n";
&logIt( "User chose to kill process" );
}
# hack to set permissions.
`chown root:root $install{bindir}`;
`chmod 700 $install{bindir}`;
# set up automatic running
&cronTab( \%install );
# make sure something is in the configuration file
&checkConfig( \%install );
&logIt( "Installation done, running \&postInstall if it exists" );
my $configCommand = &postInstall( \%install, $quiet ) if defined( &postInstall );
print "$configCommand\n";
1;