Subversion Repositories camp_sysinfo_client_3

Rev

Rev 208 | Rev 211 | 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) and --inplace, which assumes 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' );
}

my $sourceDir = File::Spec->catdir($FindBin::Bin);

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 "$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 $quiet = 0;
my $inplace = 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
   --inplace - Do an inplace upgrade
   
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 populateSourceDir' );
   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'}",            
            "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
   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'};
   }
   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,
            'inplace|i'   => \$inplace
            ) 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 : `$sourceDir/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 ) unless $inplace || $install{'action'} eq 'new';
   # move the binaries in place unless they are already there
   &doInstall( \%install ) unless $inplace;
} 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" );

&postInstall( \%install, $quiet ) if defined( &postInstall );

1;