#!/usr/bin/perl
#
# $Revision: 1.14 $
# Copyright 2013 Teleflora
#
# Install the tri-annual edir base archive.
#

use strict;
use warnings;
use Getopt::Long;
use POSIX;
use English;
use File::Spec;
use File::Basename;
use File::Temp qw/ tempfile tempdir/;

my $CVS_REVISION = '$Revision: 1.14 $';
my $PROGNAME = basename($0);

#######################################
#########   D E F A U L T S   #########
#######################################

my $DAISY_FILESYS_DEF           = "/d";
my $DAISY_DIR_DEF               = $DAISY_FILESYS_DEF . "/daisy";
my $EDIR_BASEDIR_PATH_DEF       = $DAISY_FILESYS_DEF . "/edirectories";
my $EDIR_UPDATE_CONF_DEF        = $DAISY_DIR_DEF     . "/config/edir_update.conf";
my $EDIR_LOCKFILE_PATH_DEF      = $DAISY_DIR_DEF     . "/config/edir_update.lock";
my $LOGFILE_DEF                 = $DAISY_DIR_DEF     . "/log/edir_installbase.log";
my $EDIR_BOOK_DEF               = "sep";
my $EDIR_YEAR_DEF               = "2015";
my $CRON_JOB_FILE_DEF           = "edir_update";
# maximum allowed disk space used
my $MAX_DISK_USED               = 90;

############################################
#########   E X I T  S T A T U S   #########
############################################

my $EXIT_OK                     = 0;
my $EXIT_COMMAND_LINE           = 1;
my $EXIT_MUST_BE_ROOT           = 2;
my $EXIT_PLATFORM               = 3;
my $EXIT_DISK_SPACE             = 4;
my $EXIT_BASE_ARCHIVE_PATH      = 5;
my $EXIT_EXTRACT_TMPDIR         = 6;
my $EXIT_EXTRACT_BASE_ARCHIVE   = 7;
my $EXIT_BASEDIR                = 8;
my $EXIT_BASE_ARCHIVE_CONTENTS  = 9;
my $EXIT_INSTALL_ERROR          = 10;
my $EXIT_PERMS_ERROR            = 11;
my $EXIT_FIXUP_ERROR            = 12;
my $EXIT_INTEGRATE_ERROR        = 13;
my $EXIT_CRON_JOB               = 14;
my $EXIT_UPDATE_CONFIG          = 15;
my $EXIT_LOCK_ACQUIRE           = 16;
my $EXIT_LOCK_RELEASE           = 17;
my $EXIT_CLEANUP_BASEDIR        = 18;
my $EXIT_BASE_ARCHIVE_NAME      = 19;
my $EXIT_BASE_ARCHIVE_IRREGULAR = 20;
my $EXIT_BASE_ARCHIVE_EXISTENCE = 21;

#####################################
#########   G L O B A L S   #########
#####################################

my $EMPTY                       = q{};
my $DAISY                       = 1;
my $DEBUG                       = 0;
my $DAISY_DIR                   = $DAISY_DIR_DEF;
my $LOGFILE                     = $LOGFILE_DEF;
my $CRON_JOB_FILE_PATH          = "/etc/cron.d/$CRON_JOB_FILE_DEF";
my $TIMESTAMP                   = strftime("%Y%m%d%H%M%S", localtime());
my $EDIR_LOCKFILE_PATH          = $EDIR_LOCKFILE_PATH_DEF;
my $EDIR_UPDATE_CONF_PATH       = $EDIR_UPDATE_CONF_DEF;
my $EDIR_BOOK_BINDIR            = "/bin";

##############################################
#########   C O M M A N D  L I N E   #########
##############################################

my $VERSION                     = 0;
my $HELP                        = 0;
my $VERBOSE                     = 0;
my $EDIR_BASEDIR_PATH           = $EDIR_BASEDIR_PATH_DEF;
my $EDIR_BOOK                   = $EMPTY;
my $EDIR_YEAR                   = $EMPTY;
my $CLEANUP_BASEDIR             = 1;
my $TEST_CLEANUP                = 0;
my $TEST_PARSE_EDIR_NAME        = 0;
my $TEST_PARSE_EDIR_CONTENTS    = 0;

GetOptions(
	"version"                  => \$VERSION,
	"help"                     => \$HELP,
	"verbose"                  => \$VERBOSE,
	"edir-basedir=s"           => \$EDIR_BASEDIR_PATH,
	"edir-book=s"              => \$EDIR_BOOK,
	"edir-year=s"              => \$EDIR_YEAR,
	"cleanup!"                 => \$CLEANUP_BASEDIR,
	"test-cleanup"             => \$TEST_CLEANUP,
	"test-parse-edir-name"     => \$TEST_PARSE_EDIR_NAME,
	"test-parse-edir-contents" => \$TEST_PARSE_EDIR_CONTENTS,
) || die "Error: invalid command line option, exiting...\n";


# --version
if ($VERSION) {
	print("$PROGNAME $CVS_REVISION\n");
	exit($EXIT_OK);
}

# --help
if ($HELP) {
	print("$PROGNAME $CVS_REVISION\n");
	usage();
	exit($EXIT_OK);
}

#
# there must be at least one command line argument:
# the path to the edir base archive.
#
if ($#ARGV < 0) {
	usage();
	exit($EXIT_COMMAND_LINE);
}


# --test-cleanup
if ($TEST_CLEANUP) {
    exit(ei_test_cleanup());
}

# --test-parse-edir-name
if ($TEST_PARSE_EDIR_NAME) {
    exit(ei_test_parse_edir_name());
}

# --test-parse-edir-contents
if ($TEST_PARSE_EDIR_CONTENTS) {
    exit(ei_test_parse_edir_contents());
}


#
# verify running as root.
#
if ($EUID != 0) {
    logerror("root privileges required to run this script");
    exit($EXIT_MUST_BE_ROOT);
}


exit(main());


###############################
#####   T E S T  S U B S  #####
###############################

sub ei_test_cleanup
{
    my $basedir_path = ei_pathto_edir_basedir();
    my $edir_book = $EDIR_BOOK_DEF;
    if ($EDIR_BOOK ne $EMPTY) {
	$edir_book = $EDIR_BOOK;
    }
    my $edir_year = $EDIR_YEAR_DEF;
    if ($EDIR_YEAR ne $EMPTY) {
	$edir_year = $EDIR_YEAR;
    }
    if (ei_basedir_cleanup($basedir_path, $edir_year, $edir_book)) {
	print "base dir cleaned up: $basedir_path\n";
    }
    else {
	print "could not cleanup base dir: $basedir_path\n";
    }

    return($EXIT_OK);
}

sub ei_test_parse_edir_name
{
    my $rc = $EXIT_OK;

    if (@ARGV == 0) {
	print "path to base archive missing\n";
	return($EXIT_COMMAND_LINE);
    }

    my $base_archive_path = (defined($ARGV[0])) ? $ARGV[0] : "edir_base_2015may.tar.gz";
    my ($edir_year, $edir_book) = ei_base_archive_parse_file_name($base_archive_path);
    if ($edir_year ne $EMPTY && $edir_book ne $EMPTY) {
	print "path to edir base archive: $base_archive_path\n";
	print "edir year = $edir_year, edir book = $edir_book\n";
    }
    else {
	print "could not get parse base achive file name: $base_archive_path\n";
	$rc = $EXIT_BASE_ARCHIVE_NAME;
    }

    return($rc);
}

sub ei_test_parse_edir_contents
{
    my $base_archive_path = (defined($ARGV[0])) ? $ARGV[0] : "edir_base_2015may.tar.gz";

    my $rc = $EXIT_OK;

    my ($edir_year, $edir_book) = ei_base_archive_parse_contents($base_archive_path);
    if ( ($edir_year ne $EMPTY) && ($edir_book ne $EMPTY) ) {
	print "path to edir base archive: $base_archive_path\n";
	print "edir year = $edir_year, edir book = $edir_book\n";
    }
    else {
	print "could not get parse base achive contents: $base_archive_path\n";
	$rc = $EXIT_BASE_ARCHIVE_CONTENTS;
    }

    return($rc);
}


###############################
#########   M A I N   #########
###############################

sub main
{
    loginfo("$PROGNAME $CVS_REVISION");

    #
    # verify the environment is as expected.
    #
    loginfo("installation environment verification");

    my $rc = ei_verify_platform();
    return ($rc) if ($rc != $EXIT_OK);

    my $daisy_topdir = $DAISY_FILESYS_DEF;
    $rc = ei_verify_disk_space($daisy_topdir);
    return ($rc) if ($rc != $EXIT_OK);

    my $basedir_path = ei_pathto_edir_basedir();
    $rc = ei_verify_edir_basedir($basedir_path);
    return ($rc) if ($rc != $EXIT_OK);

    my $base_archive_path = $ARGV[0];
    $rc = ei_verify_command_arg($base_archive_path);
    return ($rc) if ($rc != $EXIT_OK);

    my ($edir_year, $edir_book) = ei_verify_base_archive($base_archive_path);

    loginfo("verified installation environment");


    #
    # attempt to acquire the edirecotry update lockfile, ie
    # that it's ok to run.  Failure means the edirectory update
    # process must be running, so exit.
    #
    my $lockfile_path = ei_pathto_update_lockfile();
    if (ei_lockfile_acquire($lockfile_path)) {
	loginfo("lockfile acquired: $lockfile_path");
    }
    else {
	logerror("could not acquire lockfile: $lockfile_path");
	return($EXIT_LOCK_ACQUIRE);
    }

    #
    # attempt the installation.
    #
    $rc = edir_installbase($base_archive_path, $basedir_path, $edir_year, $edir_book);
    if ($rc == $EXIT_OK) {
	loginfo("installation of edir base archive successful: $base_archive_path");

	# clean up basedir (default but can be negated)
	if ($CLEANUP_BASEDIR) {
	    if (ei_basedir_cleanup($basedir_path, $edir_year, $edir_book)) {
		loginfo("cleanup of edir base directory done: $basedir_path");
	    }
	    else {
		logerror("could not clean up edir base directory: $basedir_path");
		$rc = $EXIT_CLEANUP_BASEDIR;
	    }
	}
    }
    else {
	logerror("could not install edir base archive: $base_archive_path");
    }

    #
    # don't forget to release the lock
    #
    if (ei_lockfile_release($lockfile_path)) {
	loginfo("lockfile released: $lockfile_path");
    }
    else {
	logerror("could not release lockfile: $lockfile_path");
	return($EXIT_LOCK_RELEASE);
    }

    loginfo("exiting $PROGNAME with status $rc");

    return ($rc);
}


###############################################
#########   V E R I F I C A T I O N   #########
###############################################

#
# verify running on supported platform.
#
sub ei_verify_platform
{
    my $rc = $EXIT_OK;

    my $os = get_os_version();
    if (defined($os)) {
	if ( ($os eq 'RHEL5') or ($os eq 'RHEL6') or ($os eq 'RHEL7') ) {
	    loginfo("--> verified platform: $os");
	}
	else {
	    logerror("platform not 'RHEL5', 'RHEL6, or 'RHEL7': $os");
	    $rc = $EXIT_PLATFORM;
	}
    }
    else {
	logerror("could not determine platform");
	$rc = $EXIT_PLATFORM;
    }

    return($rc);
}


#
# verify enough disk space on daisy database dir file system.
# 
sub ei_verify_disk_space
{
    my ($daisy_topdir) = @_;

    my $rc = $EXIT_OK;

    my $disk_percent_used = get_diskused($daisy_topdir);
    if ($disk_percent_used > $MAX_DISK_USED) {
	logerror("filesystem is greater than $MAX_DISK_USED% full: $daisy_topdir");
	$rc = $EXIT_DISK_SPACE;
    }
    else {
	loginfo("--> verified disk space, percent used: $disk_percent_used");
    }

    return($rc);
}


#
# verify destination directory for edir base archives exists.
#
sub ei_verify_edir_basedir
{
    my ($basedir_path) = @_;

    my $rc = $EXIT_OK;

    if (ei_basedir_make($basedir_path)) {
	loginfo("--> verified edir base directory exists: $basedir_path");
    }
    else {
	logerror("could not make edir base destination dir: $basedir_path");
	$rc = $EXIT_BASEDIR;
    }

    return($rc);
}


#
# verify there is at least one command line argument and
# that it is the path to a regular file.
#
sub ei_verify_command_arg
{
    my ($base_archive_path) = @_;

    my $rc = $EXIT_OK;

    if (defined($base_archive_path) && ($base_archive_path ne $EMPTY)) {
	if (-e $base_archive_path) {
	    if (-f $base_archive_path) {
		loginfo("--> verified edir base archive path: $base_archive_path");
	    }
	    else {
		logerror("base archive is not regular file: $base_archive_path");
		$rc = $EXIT_BASE_ARCHIVE_IRREGULAR;
	    }
	}
	else {
	    logerror("edir base archive does not exist: $base_archive_path");
	    $rc = $EXIT_BASE_ARCHIVE_EXISTENCE;
	}
    }
    else {
	logerror("path to edir base archive not specified as command argument");
	$rc = $EXIT_BASE_ARCHIVE_PATH;
    }

    return($rc);
}


#
# verify that the edir base archive file name format is correct, and
# if it is, then verify that it matches the the edir year and book
# of the contents of the archive.
#
sub ei_verify_base_archive
{
    my ($base_archive_path) = @_;

    # try to get year and book from archive file name
    my ($edir_year, $edir_book) = ei_base_archive_parse_file_name($base_archive_path);

    # report what happened
    if ($edir_year) {
	loginfo("--> verified edir base archive file name format: $base_archive_path");
    }
    else {
	loginfo("--> edir base archive file name format non-standard: $base_archive_path");
    }

    # try to get year and book from archive contents
    my ($archive_year, $archive_book) = ei_base_archive_parse_contents($base_archive_path);

    # report what happened
    if ($archive_year) {
	loginfo("--> verified edir base archive contents: $base_archive_path");
    }
    else {
	logerror("--> edir base archive contents non-standard: $base_archive_path");
    }

    # if archive contents in standard format
    if ($archive_year) {
	# if archive filename in standard format
	if ($edir_year) {
	    # compare values and report
	    if ( ($edir_year eq $archive_year) && ($edir_book eq $archive_book) ) {
		loginfo("--> verified edir base archive file name and contents match: $base_archive_path");
	    }
	    else {
		loginfo("--> edir base archive file name do not match contents: $base_archive_path");
	    }
	}

	# archive contents rule
	$edir_year = $archive_year;
	$edir_book = $archive_book;
	loginfo("--> edir base archive contents used for year: $archive_year");
	loginfo("--> edir base archive contents used for book: $archive_book");
    }

    # archive contents in non standard format
    else {
	# if archive filename in standard format
	if ($edir_year) {
	    loginfo("--> edir base archive file name used for year and book: $edir_year, $edir_book");
	}
	# use default values as last resort
	else {
	    $edir_year = $EDIR_YEAR_DEF;
	    $edir_book = $EDIR_BOOK_DEF;
	    loginfo("--> default values used for year and book: $edir_year, $edir_book");
	}
    }

    return($edir_year, $edir_book);
}


#############################################
#########   A P P L I C A T I O N   #########
#############################################

#
# Install an edir base archive.
#
# Returns:
#   $EXIT_OK --> success
#   non-zero exit status on error
#
sub edir_installbase
{
    my ($base_archive_path, $basedir_path, $edir_year, $edir_book) = @_;

    loginfo("edir installation properties");
    loginfo("--> base edir archive path: $base_archive_path");
    loginfo("--> base edir directory path: $basedir_path");
    loginfo("--> edir year: $edir_year");
    loginfo("--> edir book: $edir_book");

    #
    # make a temp dir to hold archive files
    #
    my $tempdir_path = ei_tempdir_make($basedir_path);
    if ($tempdir_path) {
	loginfo("edir base archive temp dir path: $tempdir_path");
    }
    else {
	logerror("could not make temp dir to hold edir base archive");
	return ($EXIT_EXTRACT_TMPDIR);
    }

    #
    # extract the edir base archive
    #
    if (ei_base_archive_extract($base_archive_path, $tempdir_path)) {
	loginfo("contents of edir base archive extracted to: $tempdir_path");
    }
    else {
	logerror("could not extract contents of edir base archive to: $tempdir_path");
	ei_tempdir_cleanup($tempdir_path);
	return ($EXIT_EXTRACT_BASE_ARCHIVE);
    }

    #
    # install edir base archive contents
    #
    if (ei_install_edir_book($basedir_path, $edir_year, $edir_book, $tempdir_path)) {
	loginfo("edir book installed at: $basedir_path");
    }
    else {
	logerror("could not install edir book in: $basedir_path");
	ei_tempdir_cleanup($tempdir_path);
	return ($EXIT_INSTALL_ERROR);
    }

    # form path to edir in base directory
    my $edir_name = $edir_year . $edir_book;
    my $edir_path = File::Spec->catdir($basedir_path, $edir_name);

    #
    # set the perms on all the dirs and files in the new edir
    #
    if (ei_edir_book_setperms($edir_path)) {
	loginfo("perms set for edir book: $edir_path");
    }
    else {
	logerror("could not set perms for files in edir book: $edir_path");
	ei_tempdir_cleanup($tempdir_path);
	return ($EXIT_PERMS_ERROR);
    }

    #
    # fixup names of platform specific files in book
    #
    if (! ei_fixup_names($edir_path)) {
	logerror("could not fixup file names with a platform suffix: $edir_path");
	ei_tempdir_cleanup($tempdir_path);
	return ($EXIT_FIXUP_ERROR);
    }

    #
    # copy scripts and config files into default Daisy database dir
    #
    my $def_daisy_db_dir = ei_pathto_default_daisy_db_dir();
    if (ei_integrate_files($edir_path, $def_daisy_db_dir)) {
	loginfo("scripts and config files installed: $def_daisy_db_dir");
    }
    else {
	logerror("could not integrate scripts into: $def_daisy_db_dir");
	ei_tempdir_cleanup($tempdir_path);
	return ($EXIT_INTEGRATE_ERROR);
    }

    #
    # edit edir_update.conf to reflect just installed base archive
    #
    my $edir_update_conf_path = ei_pathto_edir_update_conf_file();
    if (ei_configure_update($basedir_path, $edir_year, $edir_book)) {
	loginfo("config file updated: $edir_update_conf_path");
    }
    else {
	logerror("could not update config file: $edir_update_conf_path");
	ei_tempdir_cleanup($tempdir_path);
	return ($EXIT_UPDATE_CONFIG);
    }

    #
    # install cron job
    #
    my $cron_job_file_path = ei_pathto_cron_job_file();
    if (ei_install_cron_job($cron_job_file_path)) {
	loginfo("cron job installed: $cron_job_file_path");
    }
    else {
	logerror("can't install cron job file: $cron_job_file_path");
	ei_tempdir_cleanup($tempdir_path);
	return ($EXIT_CRON_JOB);
    }

    ei_tempdir_cleanup($tempdir_path);

    return ($EXIT_OK);
}


sub ei_basedir_cleanup
{
    my ($basedir_path, $edir_year, $edir_book) = @_;

    #
    # the following array and the foreach loop below maps the
    # current book name into the previous book name and year.
    #
    my @prev_bookdir_map = (
	# current book     subtract       prev book
	["jan",            1,             "sep"],
	["sep",            0,             "may"],
	["may",            0,             "jan"]
    );

    my $prev_bookdir_name = "";
    foreach my $map_entry (@prev_bookdir_map) {
	if ($edir_book eq $map_entry->[0]) {
	    $prev_bookdir_name = ($edir_year - $map_entry->[1]) . $map_entry->[2];
	}
    }

    # form the path to the previous book directory
    my $prev_bookdir_path = File::Spec->catdir($basedir_path, $prev_bookdir_name);

    # form the path to the current book directory
    my $bookdir_name = $edir_year . $edir_book;
    my $bookdir_path = File::Spec->catdir($basedir_path, $bookdir_name);

    # now remove every previous book other than the current and
    # the previous book directories.
    my @basedir_files = glob("$basedir_path/*");
    foreach my $file (@basedir_files) {
	if ($file eq $bookdir_path) {
	    next;
	}
	if ($file eq $prev_bookdir_path) {
	    next;
	}

	# any file/directory not 7 chars long gets a pass
	my $filename = basename($file);
	if (length($filename) != 7) {
	    next;
	}

	# isolate parts of filename
	my $year = substr($filename, 0, 4); 
	my $month = substr($filename, 4, 3); 

	#
	# if we get to this point, and there is a file/directory that has
	# a name 7 chars long, # starts with 4 digits, and ends with "jan" or
	# "may" or "sep", remove it.
	#
	if ($year =~ m/\d\d\d\d/) {
	    if ($month eq 'jan' || $month eq 'may' || $month eq 'sep') {
		system("rm -rf $file");
		loginfo("removed from $basedir_path: $file");
	    }
	}
    }

    return(1);
}


#
# Verify that the edir base directory exists and if it does not,
# attempt to make it.  Set the ownerships and perms.
#
# Returns
#   1 on success
#   0 on error
#
sub ei_basedir_make
{
    my ($edir_basedir) = @_;

    unless (-d $edir_basedir) {
 	system("mkdir -p $edir_basedir");
	unless (-d $edir_basedir) {
	    return (0);
	}
    }
    system("chown tfsupport:dsyadmins $edir_basedir");
    system("chmod 755 $edir_basedir");

    return (1);
}


#
# Make a tempdir to contain the extracted contents of the base archive.
#
# Returns
#   path to tempdir on success
#   empty string on error
#
sub ei_tempdir_make
{
    my ($edir_basedir) = @_;

    my $template = $PROGNAME . "-XXXXXXX";
    my $tempdir_path = $EMPTY;

    # make a directory using given template and put it where specified
    my $rc_eval = eval {
	$tempdir_path = tempdir( $template, DIR => $edir_basedir );
	1;
    };
    # was there an error in &tempdir()? If so, $@ will contain msg.
    if ($@ || $rc_eval != 1) {
	logerror("can't make temp dir for edir base archive extraction: $tempdir_path");
	logerror("$@");
	return ($EMPTY);
    }
    # no error from &tempdir() but did it make the dir?
    unless (-d $tempdir_path) {
	logerror("can't make temp dir for edir base archive extraction");
	logerror("template for temp dir name: $template");
	logerror("path to temp dir location: $edir_basedir");
	return ($EMPTY);
    }

    system("chown tfsupport:dsyadmins $tempdir_path");
    system("chmod 0755 $tempdir_path");

    return ($tempdir_path);
}


sub ei_tempdir_cleanup
{
    my ($tempdir_path) = @_;

    my $rc = 1;

    system("rm -rf $tempdir_path");
    if ($? == 0) {
	loginfo("temp dir removed: $tempdir_path");
    }
    else {
	logerror("could not remove temp dir: $tempdir_path");
	$rc = 0;
    }

    return($rc);
}


#
# extract the edir base archive into a temp location and
# then verify the extraction was a success.
#
# Returns
#   1 on success
#   0 on error
#
sub ei_base_archive_extract
{
    my ($edir_base_archive, $tempdir_path) = @_;

    # extract contents of edir base archive to tempdir
    system("tar -C $tempdir_path -xf $edir_base_archive");
    if ($? != 0) {
	logerror("can't untar edir base archive: $edir_base_archive");
	return (0);
    }
    
    # the extraction was successful if there are some entries
    # in the temp dir...
    my @tempdir_ents = glob("$tempdir_path/*");
    unless (@tempdir_ents) {
	logerror("edir base archive empty: $edir_base_archive");
	return (0);
    }

    return (1);
}


#
# get the name of the edir from the edir base archive.
#
# the "edir name" that is part of the edir base archive filename
# must match the "edir name" inside the archive.
#
# Returns
#   list with year and book
#   list with year and book empty strings if not found
#
sub ei_base_archive_parse_contents
{
    my ($base_archive_path) = @_;

    my $edir_year = $EMPTY;
    my $edir_book = $EMPTY;

    # first line of tar output will be directory whose name
    # is the concatenation of year and book name
    my @edir_name_inside = ();
    if (open(my $tarfh, '-|', "tar tf $base_archive_path")) {
	my $line = <$tarfh>;
	if ($line =~ /([[:digit:]]{4})([[:alpha:]]{3})/) {
	    ($edir_year, $edir_book) = ($1, $2);
	}
	else {
	    logerror("could not recognize contents of edir base archive: $base_archive_path");
	}
	close($tarfh);
    }
    else {
	logerror("could not get list of files in edir base archive: $base_archive_path");
    }

    return ($edir_year, $edir_book);
}


#
# parse edir bare archive file name and if in correct format,
# return edir name, ie edir year and edir book.
#
# the name of the archive must be of the form:
#
#   'edir_base_' . $year . $book . '.tar.gz'
#
# where $year is a 4 digit number and $book is "jan", "may", or "sep".
#
# eg, 'edir_base_2014sep.tar.gz
#
# Returns
#   array with non-empty edir year and book
#   array with 2 empty strings
#
sub ei_base_archive_parse_file_name
{
    my ($base_archive_path) = @_;

    my $edir_year = $EMPTY;
    my $edir_book = $EMPTY;

    my $base_archive_name = basename($base_archive_path);
    if ($base_archive_name =~ m/edir_base_([[:digit:]]{4})([[:alpha:]]{3})\.tar\.gz/) {
	$edir_year = $1;
	$edir_book = $2;
    }

    return ($edir_year, $edir_book);
}


#
# install the extracted book.
#
# remove any old tree and move the new tree into place.
#
# Returns
#   1 on success
#   0 on error
#
sub ei_install_edir_book
{
    my ($basedir_path, $edir_year, $edir_book, $srcdir_path) = @_;

    # first form the path to the old edir
    my $edir_name = $edir_year . $edir_book;
    my $edir_path = File::Spec->catdir($basedir_path, $edir_name);
    system("rm -rf $edir_path");
    if (-d $edir_path) {
	logerror("could not remove old edir: $edir_path");
	return (0);
    }

    # form path to new edir and move it to base dir
    my $new_edir_path = File::Spec->catdir($srcdir_path, $edir_name);
    system("mv $new_edir_path $basedir_path");
    unless (-d $edir_path) {
	logerror("could not install new edir book: $edir_path");
	return (0);
    }

    return (1);
}


sub ei_edir_book_setperms
{
    my ($edir_book_path) = @_;

    if (-d $edir_book_path) {
	if ($VERBOSE) {
	    print("setting perms on: $edir_book_path\n");
	}

	system("chown tfsupport:dsyadmins $edir_book_path");
	system("chmod 755 $edir_book_path");
	loginfo("perms set perms on: $edir_book_path");
    }
    else {
	logerror("edir book directory does not exist: $edir_book_path");
	return (0);
    }

    my @book_dirs = qw(
	bin
	config
	dav
	dirfull
	doc
	facfull
	xml
    );
    foreach my $book_dir (@book_dirs) {
	my $book_dir_path = "$edir_book_path/$book_dir";
	if (-d $book_dir_path) {
	    if ($VERBOSE) {
		print("setting perms on: $book_dir_path\n");
	    }

	    # dirs
	    system("chown tfsupport:dsyadmins $book_dir_path");
	    system("chmod 755 $book_dir_path");
	    # dir contents
	    system("chown tfsupport:dsyadmins $book_dir_path/*");
	    my $mode = ($book_dir eq "bin") ? "755" : "644";
	    system("chmod $mode $book_dir_path/*");

	    loginfo("perms set on directory and contents: $book_dir_path");
	}
	else {
	    loginfo("directory not present in edir base archive: $book_dir_path");
	}
    }

    my @edir_book_working_dirs = qw(
	deltas
	patchdir
	tel
    );
    foreach (@edir_book_working_dirs) {
	system("mkdir $edir_book_path/$_");
	if ($VERBOSE) {
	    print("setting perms on: $edir_book_path/$_\n");
	}

	system("chown tfsupport:dsyadmins $edir_book_path/$_");
	system("chmod 755 $edir_book_path/$_");
	loginfo("perms set on: $edir_book_path/$_");
    }

    return (1);
}


sub ei_fixup_names
{
    my ($edir_book_path) = @_;

    my $os = get_os_version();

    # make a copy of platform specific files to a name without platform suffix.
    my $edir_bindir = ei_pathto_edir_book_bindir($edir_book_path);
    my @bin_files = glob("$edir_bindir/*");
    foreach my $bin_file (@bin_files) {
	if ($bin_file =~ /(.+)(-$os)$/x) {
	    my $dst_file = $1;
	    system("cp $bin_file $dst_file");
	    if (-f $dst_file) {
		loginfo("$bin_file copied to: $dst_file");
		system("chown tfsupport:daisy $dst_file");
		system("chmod 550 $dst_file");
		loginfo("perms set on: $dst_file");
	    }
	    else {
		logerror("could not copy $bin_file to: $dst_file");
	    }
	}
    }

    return (1);
}


#
# copy scripts and config files into specified Daisy database dir.
#
sub ei_integrate_files
{
    my ($edir_book_path, $daisy_dir) = @_;

    my @script_config_bom = (
	[ 'script',    'edir_installbase.pl',  'tfsupport', 'daisy', '755' ],
	[ 'script',    'edir_update.pl',       'tfsupport', 'daisy', '755' ],
	[ 'script',    'edir_revert.pl',       'tfsupport', 'daisy', '755' ],
	[ 'config',    'edir_update.conf',     'tfsupport', 'daisy', '640' ],
    );

    for (my $i=0; $i <= $#script_config_bom; $i++) {

	my $file_type = $script_config_bom[$i][0];
	my $sub_dir = ($file_type eq 'script') ? 'bin' : $file_type;

	my $file_name = $script_config_bom[$i][1];

	my $src_dir = $edir_book_path . '/' . $sub_dir;
	my $src_path = $src_dir . '/' . $file_name;
	my $dst_dir = $daisy_dir . '/' . $sub_dir;
	my $dst_path = $dst_dir . '/' . $file_name;

	my $owner = $script_config_bom[$i][2];
	my $group = $script_config_bom[$i][3];
	my $mode = $script_config_bom[$i][4];

	unless (-f $src_path) {
	    logerror("file to install does not exist: $src_path");
	    next;
	}

	unless (-d $dst_dir) {
	    logerror("dest dir of file to install ($src_path) does not exist: $dst_dir");
	    next;
	}

	my $src_file_name = basename($src_path);
	system("cp $src_path $dst_dir");
	loginfo("$file_type file $src_file_name copied to: $dst_dir");
	system("chown $owner:$group $dst_path");
	system("chmod $mode $dst_path");
	loginfo("perms set for: $dst_path");
    }

    return (1);
}


#
# Edit config file for the "edir_update.pl" script.
#
# Returns
#   1 on success
#   0 on error
#
sub ei_configure_update
{
    my ($edir_basedir, $edir_year, $edir_book) = @_;

    my $conf_file = ei_pathto_edir_update_conf_file();
    my $new_conf_file = "$conf_file.$$";

    if (! -f $conf_file) {
	logerror("config file does not exist: $conf_file");
	return(0);
    }

    #
    # remove any old book and year lines, add appropriate new ones
    # at the right spot.
    #
    if (open(my $oldcf, '<', $conf_file)) {
	if (open(my $newcf, '>', $new_conf_file)) {
	    while (<$oldcf>) {
		my $line = $_;
		if ($line =~ /^edir-book=/x) {
		    next;
		}
		if ($line =~ /^\#edir-book=/x) {
		    print($newcf $line);
		    print($newcf "edir-book=$edir_book\n");
		    next;
		}
		if ($line =~ /^edir-year=/x) {
		    next;
		}
		if ($line =~ /^\#edir-year=/x) {
		    print($newcf $line);
		    print($newcf "edir-year=$edir_year\n");
		    next;
		}
		print($newcf $line);
	    }

	    close($oldcf);
	    close($newcf);

	}
	else {
	    logerror("could not make new config file: $new_conf_file");
	    return(0);
	}
    }
    else {
	logerror("could not open existing config file: $conf_file");
	return(0);
    }


    # new conf file should exist and non-zero in size
    if (-z $new_conf_file) {
	logerror("edit failed: $conf_file is a zero size file");
	system("rm $new_conf_file");
	return(0);
    }

    system("chmod --reference=$conf_file $new_conf_file");
    system("chown --reference=$conf_file $new_conf_file");
    system("cp $new_conf_file $conf_file");
    system("rm $new_conf_file");

    return(1);
}


#
# Install the cron job.
#
# If a cron job file already exists, overwrite it.
#
# Returns
#   1 on success
#   0 on error
#   
sub ei_install_cron_job
{
    my ($cron_job_file) = @_;

    # make a new cron job file
    if (open(my $cfh, ">", $cron_job_file)) {
	ei_write_cron_job_file($cfh);
	close($cfh);

	system("chown root:root $cron_job_file");
	system("chmod 644 $cron_job_file");
    }
    else {
	logerror("could not install new cron job file: $cron_job_file");
	return (0);
    }

    return (1);
}


sub ei_write_cron_job_file
{
    my ($cfh) = @_;

    my $timestamp = strftime("%Y-%m-%d", localtime());

    print($cfh "#\n");
    print($cfh "# Generated by $PROGNAME $CVS_REVISION $timestamp\n");
    print($cfh "#\n");

    print($cfh "#\n");
    print($cfh "# Cron job file for nightly edir updates - do not edit.\n");
    print($cfh "#\n");

    my $cron_time = "0 02 * * *";
    my $cron_user = "root";
    my $cron_cmd = "(/usr/bin/test -e /d/daisy/bin/edir_update.pl && cd /d/daisy && /d/daisy/bin/edir_update.pl)";
    print($cfh "$cron_time $cron_user $cron_cmd\n");

    return (1);
}


#
# attempt to acquire a lockfile.
#
# Returns
#   1 if successful
#   0 if not
#
sub ei_lockfile_acquire
{
    my ($lockfile_path) = @_;

    if (sysopen(LFH, $lockfile_path, O_EXCL|O_CREAT)) {
	close(LFH);
	return(1);
    }

    return(0);
}


#
# release a lockfile.
#
# Returns
#   1 if successful
#   0 if not
#
sub ei_lockfile_release
{
    my ($lockfile_path) = @_;

    my $rc = 1;

    if (-f $lockfile_path) {
	unless (unlink($lockfile_path)) {
	    $rc = 0;
	}
    }
    else {
	loginfo("lockfile unlink not attempted, lockfile does not exist: $lockfile_path");
    }

    return(1);
}


############################################
######### P A T H T O  S U B S  ###########
############################################

sub ei_pathto_default_daisy_db_dir
{
    return($DAISY_DIR_DEF);
}

sub ei_pathto_edir_update_conf_file
{
    return($EDIR_UPDATE_CONF_PATH);
}

sub ei_pathto_cron_job_file
{
    return($CRON_JOB_FILE_PATH);
}

sub ei_pathto_edir_basedir
{
    return($EDIR_BASEDIR_PATH);
}

sub ei_pathto_edir_book_bindir
{
    my ($edir_book_path) = @_;

    return($edir_book_path . $EDIR_BOOK_BINDIR);
}

sub ei_pathto_update_lockfile
{
    return($EDIR_LOCKFILE_PATH);
}


#########################################
#########   F U N C T I O N S   #########
#########################################

sub usage
{
    print "Usage:\n";
    print "$PROGNAME --version\n";
    print "$PROGNAME --help\n";
    print("$PROGNAME [options] edir_base_archive.tar.gz\n");
    print "where options are:\n";
    print "\t--verbose                    output extra info\n";
    print "\t--edir-basedir=s             path to top of installation tree\n";
    print "\t--edir-book=s                name of book (jan, may, sep)\n";
    print "\t--edir-year=s                year of book (YYYY)\n";
    print "\n";

    return (1);
}


#
# Return a list of verified POS dirs.  For Daisy, remember, there can be
# more than one daisy database dir!
#
sub get_daisy_dirs
{
    my @daisy_dirs = ();
    my $daisy_top_dir = '/d';

    # Daisy POS can have one or more database dirs
    my @daisy_db_dirs = glob("$daisy_top_dir/*");
    for my $daisy_db_dir (@daisy_db_dirs) {

	# must be a directory
	next unless (-d $daisy_db_dir);

	# skip old daisy dirs
	next if ($daisy_db_dir =~ /.+-\d{12}$/x);

	# must contain the magic files
	next unless(-e "$daisy_db_dir/flordat.tel");
	next unless(-e "$daisy_db_dir/control.dsy");

	# must be daisy 8.0+
	next unless (-d "$daisy_db_dir/bin");

	push(@daisy_dirs, $daisy_db_dir);
    }

    return (@daisy_dirs);
}


#
# Return the percentage of disk being used.
# This gives us a clue as to whether the disk is already "at capacity".
#
sub get_diskused
{
    my ($directory) = @_;

    my $returnval = -1;

    if (-d "$directory") {
	if (open(my $pipe, '-|', "df -h $directory")) {
	    while (<$pipe>) {
		if (/\s+(\d+)\%\s+/x) {
		    $returnval = $1;
		    last;
		}
	    }
	    close($pipe);
	}
    }

    return ($returnval);
}


sub get_os_version
{
    my $os = $EMPTY;

    my $REDHAT_VERSION_FILE = '/etc/redhat-release';
    my $redhat_version = $EMPTY;

    if (open(my $file, '<', $REDHAT_VERSION_FILE)) {
	$redhat_version = <$file>;
	chomp($redhat_version);
	close($file);
    }
    else {
	logerror("Can't open Red Hat version file: $REDHAT_VERSION_FILE");
	return (undef);
    }

    # Redhat Enterprise Linux Server 7
    if ($redhat_version =~ /(release)(\s+)(7)/x) {
	$os = 'RHEL7';
    }

    # Redhat Enterprise Linux Server 6
    if ($redhat_version =~ /(release)(\s+)(6)/x) {
	$os = 'RHEL6';
    }

    # Redhat Enterprise Linux Server 5
    elsif ($redhat_version =~ /(release)(\s+)(5)/x) {
	$os = 'RHEL5';
    }

    return ($os);
}


sub loginfo
{
    my ($message) = @_;

    chomp($message);
    $message = "<I> $message";
    logit($message);

    return (1);
}


sub logerror
{
    my ($message) = @_;

    chomp($message);
    $message = "<E> $message";
    logit($message);

    return (1);
}


sub logit
{
    my ($message) = @_;

    my $logtime = strftime("%Y-%m-%d %H:%M:%S", localtime());

    chomp($message);
    if (open(my $logfh, '>>', $LOGFILE)) {
	print($logfh "$logtime ($PROGNAME-$PID) $message\n");
	close($logfh);
    }
    else {
	print "$logtime ($PROGNAME-$PID) $message\n";
    }

    if ($VERBOSE) {
	print $message . "\n";
    }

    return (1);
}


#
# Subroutine to run system commands.  This logs an entry.
#
sub mysystem
{
    my ($command) = @_;

    my $rc = $EMPTY;

    system("$command >> $LOGFILE 2>> $LOGFILE");
    $rc = WEXITSTATUS($?);
    loginfo("$command Returned: $rc");

    return ($rc)
}


#################################################
#########   D O C U M E N T A T I O N   #########
#################################################

__END__

=pod

=head1 NAME

I<edir_installbase.pl> - script to install a Daisy directory base archive

=head1 VERSION

This documentation refers to version: $Revision: 1.14 $


=head1 SYNOPSIS

=over 4

=item F<edir_installbase.pl> B<--version>

=item F<edir_installbase.pl> B<--help>

=item F<edir_installbase.pl> [B<--options>] archive_path

=back

=head1 OPTIONS

=over 4

=item B<--version>

Output the version number of the script and exit.

=item B<--help>

Output a short help message and exit.

=item B<--verbose>

Report extra information.

=item B<--edir-basedir=path>

The path to an alternative top level directory for the
installation of eDirectory archives.
The default value is F</d/edirectories>.

=item B<--edir-book=string>

Specify the "book" of the directory, ie "jan", "may", or "sep".

=item B<--edir-year=yyyy>

Specify the 4 digit decimal year of the directory being installed.

=back




=head1 DESCRIPTION

The I<edir_installbase.pl> script is a Perl program which installs
a Teleflora Daisy POS edirectory base archive.
Also, it configures the infrastructure of the installed archive
to enable it to produce daily edirectory updates.

Note, in this documentation, "edirectory" refers to the
"Daisy electronic floral directory", that is, a list of 
Teleflora member florists and information about them.
When just "directory" is used, it refers a Linux file system object.
The term "edirectory" is sometimes shortened to just "edir".

The one required command line argument is
the path to the file containing the edirectory base archive.
Only one edirectory base archive may be specified.
The edirectory base archive is a compressed (with gzip) tar archive.

Upon execution, the script first verifies several aspects of the
environment that are required for successful completion.
If any of these checks fail, an error is logged, and
the script exits with an appropriate exit status.
For some cases, it also outputs a usage message.

The verification checks performed are:

=over 4

=item 1.

platform check: RHEL5, RHEL6 and RHEL7 are supported.

=item 2.

disk space check: the Daisy file system must not be
greater than 90% full.

=item 3.

base directory check: the Linux directory where the
edirectory base archives are installed - if it doesn't exist,
it will be made.

=item 4.

command line argument check: the one and only command line argument
must be the path to the edir base archive and
it must be a regular Linux file.

=item 5.

base archive check: the format of the filename for the
base archive specified on the command line must conform
to a standard format.
This standard format indicates the year and the month
that the edirectory was released.
Additionally, the base archive contents must match the 
year and month indicated by the filename.

=back

After the initial checks are complete, the installation begins.
The installation essentially consists of the following steps:

=over 4

=item 1. acquire edir update lock

=item 2. install edir files

=item 3. cleanup old edir files

=item 4. release edir update lock

=back


Outlined below are details about each of the installation steps.

=over 4

=item a.

First, an attempt is made to acquire the edirectory update lock.
If the lock can not be acquired, the script logs an error and exits.

=item b.

A a temporary directory is then made to hold the contents of the
edir base archive to be installed.
The contents of the entire edir base archive are then extracted into
the temporary directory.

=item c.

The files of the edir base archive are then installed,
ie, copied to F</d/edirectories>.
If there exists an old book and year of the same name, it is removed
before the new files are copied.

=item d.

The set of edir update scripts are then installed and
there owner, group and perms are set.
The F<edir_update.conf> file is also installed.

=item e.

The config file for the F<edir_update.pl> script is edited with
the appropriate values for the base files there were just installed.

=item f.

Finally, the nightly edir update cron job is installed.
By default, the cron job is called every 4 hours, and
runs the F<GetDeltas> program which downloads any available deltas.
Once a night between 2am and 3am, the F<maketel> program is run
to incorporate any deltas downloaded in the previous 24 hours.
If there are any new deltas, they are incorporated and a new set
of "tel files" are generated and installed.
If  there are no new deltas, then nothing is done.

=item g.

Other than the current florist directory and the immediately preceeding
florist directory,
the edir base directory is cleaned up of all files and directories whose
name is of the form of a florist directory, ie a name that is 7 characters
in length with the first 4 characters digits followed by
"jan", or "may", or "sep".

=item h.

The edir update lock file is released.

=back

=head1 EXAMPLES

Given an offcyle edir base archive F</tmp/edir_base_2013may.tar.gz>,
to install the base archive, enter the following command:

 $ sudo perl edir_installbase.pl /tmp/edir_base_2013may.tar.gz


=head1 FILES

=over 4

=item F</d/daisy>

Default Daisy database directory - must exist.

=item F</d/edirectories>

Default directory for installation of Daisy base edirectory archives.

=item F</d/daisy/config/edir_update.conf>

Default config file for the F<edir_update.pl> script.

=item F</etc/cron.d/edir_update>

The new cron job file which drives daily edir updates.

=item F</d/daisy/log/edir_installbase.log>

The log file for the edir installation script.

=item F</d/daisy/config/edir_update.lock>

The edir update lock file.

=back


=head1 DIAGNOSTICS

=over 4

=item Exit status 0 ($EXIT_OK)

Successful completion.

=item Exit status 1 ($EXIT_COMMAND_LINE)

In general, there was an issue with the syntax of the command line.

=item Exit status 2 ($EXIT_MUST_BE_ROOT)

The user must be root or running under "sudo".

=item Exit status 3 ($EXIT_PLATFORM)

The platform version string could not be determined or
the platform is not "RHEL5" or "RHEL6" or "RHEL7".

=item Exit status 4 ($EXIT_DISK_SPACE)

There is not enough disk space to install edir archive.

=item Exit status 5 ($EXIT_BASE_ARCHIVE_PATH)

The command line argument specifying the path to the
base archive file is empty.

=item Exit status 6 ($EXIT_EXTRACT_TMPDIR)

A temporary directory to contain the edir base archive contents
could not be made.

=item Exit status 7 ($EXIT_EXTRACT_BASE_ARCHIVE)

The contents of edir base archive could not be extracted into
the temporary directory.

=item Exit status 8 ($EXIT_BASEDIR)

The edir directory did not exist and could not be made.

=item Exit status 9 ($EXIT_BASE_ARCHIVE_CONTENTS)

The edir base archive does not contain the contents that
match either the defaults or the values specified on the command line.

=item Exit status 10 ($EXIT_INSTALL_ERROR)

Removal of the old edir files and installation of the new edir files failed.

=item Exit status 11 ($EXIT_PERMS_ERROR)

Setting the owner, group, and perms for the new edir files failed.

=item Exit status 12 ($EXIT_FIXUP_ERROR)

Installation of programs contained in the new edir base archive failed.

=item Exit status 13 ($EXIT_INTEGRATE_ERROR)

Installation of scripts and config files from the new edir base archive failed.

=item Exit status 14 ($EXIT_CRON_JOB)

Installation of the cron job file failed.

=item Exit status 15 ($EXIT_UPDATE_CONFIG)

The update of the F<edir_update.conf> file failed.

=item Exit status 16 ($EXIT_LOCK_ACQUIRE)

The update lockfile could not be acquired.

=item Exit status 17 ($EXIT_LOCK_RELEASE)

The update lockfile could not be released.

=item Exit status 18 ($EXIT_CLEANUP_BASEDIR)

Old files could not be removed from the base directory
after the successful installation of a new book.

=item Exit status 19 ($EXIT_BASE_ARCHIVE_NAME)

The format of the file name of the edir base archive is not recognized.

=item Exit status 20 ($EXIT_BASE_ARCHIVE_IRREGULAR)

The path to the edir base archive specified on the command line
is not a regular file.

=item Exit status 21 ($EXIT_BASE_ARCHIVE_EXISTENCE)

The path to the edir base archive specified on the command line
does not point to any file.

=back


=head1 SEE ALSO

I<GetDeltas(1)>, I<maketel(1)>, I<edir_update.pl>, I<edir_revert.pl>


=cut
