#!/usr/bin/perl -w

#Author: Aaron Burgemeister
#E-mail: ab@novell.com;dajoker@gmail.com
#Version: 0.4.20100231041200
#License: GPL v3 http://www.gnu.org/licenses/gpl.html

#Be aware that this script, by default, stops and then restarts any DS instances
#that are being backed up.  If this is not something you desire to do right now
#(or whenever you run the script) take appropriate action to prevent that.

#You will need to run this with 'root' privileges to backup NICI properly in
#most default setups.  You can change rights on NICI (not recommended) or run as
#'root' (better option).  This script is, to my mind, fairly secure from user
#injection hacks but I've not tested it and I'm no expert.  With that in mind
#I feel comfortable running it on my box, or have it run automatically via some
#scheduling mechanism.  As the source is here feel free to secure it further if
#you are concerned about that.  Send me the updates if you do that, please.

#If you would like to run this without having to put in -s each time to skip the
#message about DS being stopped you can either comment out that line below in
#the main section of the script or you can create an alias (*nix*) like the
#following (the first one requires you to be in the same context as the script
#for it to run, while the second does not if you specify the path correctly):
#alias ndsrc='./ndsrc.pl -s'
#alias ndsrc='/full/path/to/script/ndsrc.pl -s'

#The script is generating logs as it goes into some array of logs.  This is
#printed at the end with one entry to a line.  All lines should start with a '0'
#meaning false...as in, no errors.  If lines start with something else (1, 2,
#127, etc.) then those probably came from a command called by the script and
#should be researched as a problem exists.  The only exception to this rule is
#when the script is listing the instances of a user and, in those cases, the
#lines start with a space and then text (no number, space, hyphen, space).  This
#is for readability.  Still, it should be fairly simple to parse if that is your
#desire.  As a note the script itself always returns 0 unless an error occurred
#during processing.  A user exiting out for any reason is not an error.

#Restoring the contents of the generated TAR (Tap ARchive) file is
#straight-forward.  By default the TAR should have full paths to the files that
#were backed up.  Note that the 'tar' command does not do well with softlinks
#when used as I am using it (there is a parameter to make it follow softlinks
#but I have opted out on that one).  It is best to have nds.conf (your
#instance's incarnation of it, I mean) refer directly to the DIB directly
#without softlinks.  The TAR, when extracted, will recreate the entire path to
#the DIB, nds.conf, and NICI information.  Going down into thost directories it
#is possible to copy the files (DIB, nds.conf, NICI) back to the locations they
#came from as a pseudo-restore technique assuming you have rights to do so.
#Note that this is NOT recommended with the DIB under most circumstances you
#are thinking of.  eDirectory implements high availability of information and
#redundancy (backups) by having multiple replicas in multiple locations.
#Restoring the DIB on one of three boxes without taking several measures to be
#safe WILL destroy your tree.  If you have not done this before, and understand
#it thoroughly on your own, do not do it.  Doing so at any time without XK2-ing
#the server is foolish at best and, again, will destroy your tree (yes, all of
#the replicas if you are dumb).  In a single-server environment restoring the
#DIB is a viable option but still, be careful.  Restoring one server's DIB on
#another server while the first server is up can also destroy things.  Be sure
#in that scenario networks are completely isolated (TCP/UDP/ICMP/IPX/SPX
#traffic).  This utility is best used for when the rest of the tree is gone
#ruined by somebody else's dumb actions as mentioned above) and the server being
#restored has a copy of the entire tree in one DIB.  Even then some cleanup of
#other replicas must be done.

#An interesting issue using the tar command to extract the files when there are
#colons in the filename shows up once in a while in certain situations.  It
#appears that trying to extract the file without preceding it with a dot and
#forward slash './' causes the tar utility to try to find a host with the name
#specified before the first colon, which is almost certainly cannot do.  This
#appears to be something specific to how files are read with Linux (or maybe
#just with tar since I could not duplicate it with the cat utility) and can
#be worked around by putting a dot slash './' before the file, renaming the
#file, 'cat-ing' the file with cat and redirecting that output to tar, or
#something along those lines.  This is only an issue if you try to extract the
#archives created with the $nowDate variable set to have colons in it.

#This script uses ndsmanage for 8.8 instances to do all its dirty work.  This is
#nice because it means you can, in theory, have your path set up for ndsmanage
#anywhere on your box and everything should still work.  Currently you will want
#to be sure you have ndsmanage loading from the same installation as the
#configuration files that are for the instances being backed up.  It is unlikely
#that ndsmanage will properly (reliably) work with instances that are in random
#paths.  The default configuration directory for the script for eDirectory 8.8+
#instances is /etc/opt/novell/eDirectory/conf/.edir/ which is the default for
#the root-based eDirectory install on Linux and Solaris (if not HP-UX/AIX).
#Because this directory is configurable (and you can load any ndsmanage you
#desire before running the script) this should work for non-root installs of
#eDirectory 8.8+.

#The current releases of eDirectory are 8.7.3.8 and 8.8.  No versions after
#those have been tested unless mentioned here later.  That said it should work
#for eDir 8.7.x, 8.6.x, and probably later versions of eDir 8.8 unless things
#change quite a bit in the configuration files and/or directories.  I have
#managed to make this work for both Linux and Solaris as of 2006-05-24.

#Allowing parameters from the command line for scriptability (proposed).
#-h Help - Trumps all other options and presents usage information.
#-p Backup destination path for the TAR with the DIB. Default is /root/ otherwise.
#-c Config path for specific instance. Same as what --config-file (ndsmanage) can be pointed to. Usually an 'nds.conf' file; referenced in the instances.<UID> file.
#-d Directory for config files for eDir 8.8+.  /etc/opt/novell/eDirectory/conf/.edir/ by default.
#-s Skip the messages about DS being bounced during the backup.








#Methods placed at the top for prototyping fun.

#Method to perform the eDirectory backup.
sub eDirBackup($$$$)
{
  my($nowDate) = shift(@_);
  my($backupDir) = shift(@_);
  my($configDir) = shift(@_);
  my($configFiles) = shift(@_);
  my($eDirVersion) = geteDirVersion();
  my(@returnVal) = ();

  #If eDir version < 8.8, act.  Otherwise confirm eDir 8.8+.
  if(($eDirVersion =~ /^8\.(\d+?)\.?/) && ($1 < 8))
  {
    #Do the stuff to backup eDirectory 8.7.x.
    push(@returnVal, '0 - Detected eDirectory version <8.8; starting backup methods.');
    push(@returnVal, eDir87Backup($nowDate, $backupDir));
  }#End if
  elsif(($eDirVersion =~ /^8\.(\d+?)\.?/) && ($1 >= 8))
  {
    push(@returnVal, '0 - Detected eDirectory version >=8.8; starting backup methods.');
    $configDir = '/etc/opt/novell/eDirectory/conf/.edir/' if(length($configDir) == 0);
    push(@returnVal, eDir88Backup($nowDate, $backupDir, $configDir, $configFiles));
  }#End elsif
  else
  {
    push(@returnVal, '1 - No eDirectory detected.  Please verify you have eDirectory version 8 or higher properly installed.');
  }#End else

  return @returnVal;
}

#Method to backup eDir 8.8+.
sub eDir88Backup($$$;$)
{
  #General variables for use.
  my($nowDate) = shift(@_);
  my($backupDir) = shift(@_);
  my($eDirInstancesDir) = shift(@_);
  my($configFiles) = ((scalar(@_) > 0) ? (shift(@_)) : (''));
  my($backupPath) = '';
  my($cmd0) = '';
  my($cmdReturn) = '';
  my(@returnVal) = ();
  my($currUID) = '';
  my(%instances) = ();
  my($key0) = '';
  my($ctr0) = 0;

  #General check for ndsmanage.
  push(@returnVal, getNdsmanageAlert());

  #If nothing was requested explicitly, get stuff.  Otherwise do requested items.
  if(scalar(@{$configFiles}) == 0)
  {
    #Verify directory exists then get all the instances into a hash.
    push(@returnVal, fileExists($eDirInstancesDir, '1'));
    %instances = getInstances($eDirInstancesDir);
    push(@returnVal, selectInstances(\%instances)); #If this script should always backup all instances found comment this line out.
  }#End if
  else
  {
    #Get defined instances into a hash.
    $cmd0 = 'id -u';
    $cmdReturn = systemRunGetOutput($cmd0);
    $currUID = $cmdReturn;

    for($ctr0 = 0; $ctr0 < scalar(@{$configFiles}); ++$ctr0)
    {
      push(@returnVal, '0 - User-defined instance: ' . $configFiles->[$ctr0]);
      $instances{$currUID}{'instances'}[$ctr0] = $configFiles->[$ctr0];
    }#End for
  }#End else

  #Perform actual backup.
  foreach $key0 (keys(%instances))
  {
    for($ctr0 = 0; $ctr0 < scalar(@{$instances{$key0}{'instances'}}); ++$ctr0)
    {
      $backupPath = $backupDir . 'eDir-' . $key0 . '-' . $ctr0 . '-' . $nowDate . '.tar';
      push(@returnVal, eDir88InstanceBackup($backupPath, $instances{$key0}{'instances'}[$ctr0]));
    }#End for
  }#End foreach
  return @returnVal;
}

#Method to perform the backup based on the file passed in.
sub eDir88InstanceBackup($$)
{
  #General variables for use.
  my($backupPath) = shift(@_);
  my($eDirConfPath) = shift(@_);
  my($cmd0) = '';
  my($cmdReturn) = 0;
  my(@returnVal) = ();
  my($instanceDIBDir) = '';

  #Logging.
  push(@returnVal, '0 - Performing backup for instance ' . $eDirConfPath . ' to ' . $backupPath . ' now.');

  #Get the DIB directory from the configuration file.
  push(@returnVal, fileExists($eDirConfPath, '1'));
  $cmd0 = 'cat ' . $eDirConfPath . ' | grep \'^n4u.server.vardir\' | sed \'s/^.*=//\'';
  $instanceVarDir = systemRunGetOutput($cmd0);
  $cmd0 = 'cat ' . $eDirConfPath . ' | grep \'^n4u.nds.dibdir\' | sed \'s/^.*=//\'';
  $instanceDIBDir = systemRunGetOutput($cmd0);

  #If the dibdir is not part of vardir we'll need to back it up as well.
  if($instanceDIBDir !~ /$instanceVarDir/)
  {
    push(@returnVal, '0 - DIBDir outside VarDir and found at ' . $instanceDIBDir);
    push(@returnVal, fileExists($instanceDIBDir, '1'));
  }#End if
  else
  {
    $instanceDIBDir = '';
  }#End else
  push(@returnVal, '0 - VarDir found at ' . $instanceVarDir);

  push(@returnVal, fileExists($instanceVarDir, '1'));

  #Stop DS.
  print "\n" . 'Stopping DS.' . "\n";
  $cmd0 = 'ndsmanage stop --config-file ' . $eDirConfPath . ' >/dev/null';
  $cmdReturn = systemRunAndGetReturnVal($cmd0);
  push(@returnVal, ($cmdReturn == 0 ? ($cmdReturn . ' - DS stopped successfully.') : ($cmdReturn . ' - DS may NOT have stopped successfully.  This could mean an invalid backup.  Verify the backup before depending on it.')));

  #Make the backup.
  print 'Creating backup file.' . "\n";
  $cmd0 = 'tar -cvf ' . $backupPath . ' ' . $instanceVarDir . ' ' . $instanceDIBDir . ' ' . $eDirConfPath . ' >/dev/null';
  $cmdReturn = systemRunAndGetReturnVal($cmd0);
  push(@returnVal, ($cmdReturn == 0 ? ($cmdReturn . ' - DS backed up to ' . $backupPath) : ($cmdReturn . ' - DS may not have been backed up to ' . $backupPath . ' properly.  An error occurred.  Check rights to the source (' . $instanceVarDir . ') and destination paths.')));

  #Make the files a bit more secure.
  $cmd0 = 'chmod 600 ' . $backupPath . ' >/dev/null';
  $cmdReturn = systemRunAndGetReturnVal($cmd0);
  push(@returnVal, ($cmdReturn == 0 ? ($cmdReturn . ' - DS backup at ' . $backupPath . ' has been secured.') : ($cmdReturn . ' - DS backup may not be as secure as it should be depending on this user\'s umask and the backup\'s location.')));

  #Start DS.
  print 'Starting DS.' . "\n\n";
  $cmd0 = 'ndsmanage start --config-file ' . $eDirConfPath . ' >/dev/null';
  $cmdReturn = systemRunAndGetReturnVal($cmd0);
  push(@returnVal, ($cmdReturn == 0 ? ($cmdReturn . ' - DS started successfully.') : ($cmdReturn . ' - DS may NOT have restarted successfully.')));

  return @returnVal;
}

#Method to allow the user to choose which instances to backup.
sub selectInstances(\%)
{
  my($instances) = shift(@_);
  my(@returnVal) = ();
  my($key0) = '';
  my($ctr0) = 0;
  my($ctr1) = 0;
  my($cmd0) = '';
  my($cmdReturn) = '';
  my($userInput) = '';
  my(@theChosen) = ();
  my(@userInstances) = ();

  push(@returnVal, '0 - No instances at all!') if(scalar(keys(%{$instances})) == 0);

  #Go through each user and then their instance(s).
  foreach $key0 (keys(%{$instances}))
  {
    push(@returnVal, '0 - The following instances found for UID ' . $key0 . ' (' . $instances->{$key0}->{'username'} . '): ');
    print "\n" . 'Please choose which instances (comma-delimited) to backup for user ' . $instances->{$key0}->{'username'} . '.' . "\n";
    print 'Entering nothing and pressing [Enter] will skip these instances.' . "\n";
    print 'Options include: \'all\', ' . "\n";
    for($ctr0 = 0; $ctr0 < scalar(@{$instances->{$key0}->{'instances'}}); ++$ctr0)
    {
      push(@returnVal, ' ' . $instances->{$key0}->{'instances'}->[$ctr0]);
      print '\'' . $ctr0 . '\' => ' . $instances->{$key0}->{'instances'}->[$ctr0] . "\n";
    }#End for
    $userInput = <>;
    chomp($userInput);
    push(@returnVal, '0 - User typed: ' . $userInput);

    @theChosen = split(/\s*,\s*/, $userInput);

    #If the user did not choose all, go deeper.
    if((!arrayContains(\@theChosen, 'all')) && (scalar(@theChosen) != 0))
    {
      @userInstances = @{$instances->{$key0}->{'instances'}};
      @{$instances->{$key0}->{'instances'}} =  ();
      #Go through all of the user-specified instances.
      for($ctr1 = 0; $ctr1 < scalar(@theChosen); ++$ctr1)
      {
        #If the options available to the user were chosen then back them up. This is quite a line.....*sigh*.
        if(($theChosen[$ctr1] < scalar(@userInstances)) && (arrayContains(\@userInstances, $userInstances[$theChosen[$ctr1]])))
        {
          push(@{$instances->{$key0}->{'instances'}}, $userInstances[$theChosen[$ctr1]]);
        }#End if
      }#End for
    }#End if
  }#End foreach

  return @returnVal;
}

#Method to get the instances into a usable hash.
sub getInstances($)
{
  my($eDirInstancesDir) = shift(@_);
  my($cmd0) = '';
  my($cmdReturn) = '';
  my($ctr0) = 0;
  my(@instanceFiles) = ();
  my(%instances) = ();
  my($instanceUID) = '';

  #Get the listing of the instances directory.
  $cmd0 = 'ls -1 ' . $eDirInstancesDir . 'instances.*';
  $cmdReturn = systemRunGetOutput($cmd0);

  @instanceFiles = split(/\n/, $cmdReturn);
  for($ctr0 = 0; $ctr0 < scalar(@instanceFiles); ++$ctr0)
  {
    $instanceFiles[$ctr0] =~ /instances\.(\d+)/;
    $instanceUID = $1;

    #Read all instances from the file into the hash.
    $cmd0 = 'less ' . $eDirInstancesDir . 'instances.' . $instanceUID;
    $cmdReturn = systemRunGetOutput($cmd0);
    if(length($cmdReturn) > 3)
    {
      push(@{$instances{$instanceUID}{'instances'}}, split(/\n/, $cmdReturn));
      $cmd0 = 'getent passwd ' . $instanceUID . ' | awk -F\':\' \'{print $1}\'';
      $instances{$instanceUID}{'username'} = systemRunGetOutput($cmd0);
    }#End if
  }#End for

  return %instances;
}

#Method to backup eDir 8.7.x.
sub eDir87Backup($$)
{
  #General variables for use.
  my($nowDate) = shift(@_);
  my($backupPath) = shift(@_);
  my($cmd0) = '';
  my(@returnVal) = ();
  my($eDirConf) = '/etc/nds.conf';
  my($eDirScript) = '/etc/init.d/ndsd';
  my($cmdReturn) = '';
  push(@returnVal, fileExists($eDirConf, '1'));
  my($dibDir) = systemRunGetOutput('cat ' . $eDirConf . ' | grep \'^n4u.nds.dibdir\' | sed \'s/^.*=//\'');
  $dibDir = ((length($dibDir) < 1) ? ('/etc') : ($dibDir));
  push(@returnVal, fileExists($dibDir, '1'));

    #Stop DS.
  print "\n" . 'Stopping DS.' . "\n";
  $cmdReturn = systemRunAndGetReturnVal($eDirScript . ' stop >/dev/null');
  push(@returnVal, ($cmdReturn == 0 ? ($cmdReturn . ' - DS stopped successfully.') : ($cmdReturn . ' - DS may NOT have stopped successfully.  This could mean an invalid backup.  Verify the backup before depending on it.')));

  #Backup DS.
  print 'Creating backup file.' . "\n";
  $backupPath .= 'edir-' . $nowDate . '.tar >/dev/null';
  $cmdReturn = systemRunAndGetReturnVal('tar -cvf ' . $backupPath . ' ' . $dibDir . ' ' . $eDirConf);
  push(@returnVal, ($cmdReturn == 0 ? ($cmdReturn . ' - DS backed up to ' . $backupPath) : ($cmdReturn . ' - DS may not have been backed up to ' . $backupPath . ' properly.  An error occurred.  Check rights to the source (' . $dibDir . ') and destination paths.')));

  #Make the files a bit more secure.
  $cmd0 = 'chmod 600 ' . $backupPath . ' >/dev/null';
  $cmdReturn = systemRunAndGetReturnVal($cmd0);
  push(@returnVal, ($cmdReturn == 0 ? ($cmdReturn . ' - DS backup at ' . $backupPath . ' has been secured.') : ($cmdReturn . ' - DS backup may not be as secure as it should be depending on this user\'s umask and the backup\'s location.')));

  #Start DS.
  print 'Starting DS.' . "\n\n";
  $cmdReturn = systemRunAndGetReturnVal($eDirScript . ' start >/dev/null');
  push(@returnVal, ($cmdReturn == 0 ? ($cmdReturn . ' - DS started successfully.') : ($cmdReturn . ' - DS may NOT have restarted successfully.')));

  return @returnVal;
}

#Method to backup NICI.
#This probably requires 'root' rights no matter what else is happening on the box
#(regular eDir install for root user, eDir install for other user, non-root install).
#By default you can browse to the NICI directory but you can't see or read anything
#unless you are a somebody.
sub niciBackup($$)
{
  #General variables for use.
  my($nowDate) = shift(@_);
  my($niciTarPath) = shift(@_);
  my($cmd0) = '';
  my(@returnVal) = ();
  my($cmdReturn) = '';
  my($niciDataLoc) = '';
  my($niciConfLoc) = '';
  my($nici64ConfLoc) = '';
  my($nici64Present) = 0;

  #Pull off the NICI version.
  my($niciVer) = getNICIVer();

  #Finish setting up the NICI backup path.
  $niciTarPath .= 'nici-' . $nowDate . '.tar';

  #Run the command after testing for the version.  Currently this tests
  #for 2.6 and other versions which makes the (bold?) assumption that
  #the path will not change for a while with the newer versions.  I believe
  #this is okay for now and cannot make it any-more forward-compatible
  #because my crystal ball is broken (was running windoze and has the blue
  #hue of death).
  if(length($niciVer) < 2)
  {
    #No NICI, it seems.
    push(@returnVal, '1 - There appears to be a lack of NICI on the box.');
    return @returnVal;
  }#End if
  elsif($niciVer =~ /^2\.6.+/)
  {
    push(@returnVal, '0 - NICI 2.6.x detected');
    $niciDataLoc = '/var/novell';
    $niciConfLoc = '/etc/nici.cfg';
  }#End elsif
  else
  {
    push(@returnVal, '0 - NICI >2.6 detected');
    $niciDataLoc = '/var/opt/novell';
    $niciConfLoc = '/etc/opt/novell/nici.cfg';
    if($niciVer =~ /64.+/) {
      push(@returnVal, '0 - NICI 64-bit detected.');
      $nici64ConfLoc .= '/etc/opt/novell/nici64.cfg';
      $nici64Present = 1;
    }#End if
  }#End else

  #Verify availability of data.
  push(@returnVal, fileExists($niciConfLoc, '1'));
  push(@returnVal, fileExists($niciDataLoc, '1'));
  if($nici64Present) {
    push(@returnVal, fileExists($nici64ConfLoc, '1'));
  }#End if

  #Generate the command.
  $cmd0 = 'tar -cvf ' . $niciTarPath . ' ' . $niciDataLoc . '/nici' . ' ' . $niciConfLoc . ' ' . $nici64ConfLoc . ' >/dev/null';

  #Run the command and capture the return value.
  $cmdReturn = systemRunAndGetReturnVal($cmd0); 

  #If all went well, great.  Otherwise, less-great.
  #Quick hack to clean up the NICI version variable in case 32-bit and 64-bit NICI are both present.
  $niciVer =~ s/(\S+)\n.+/$1/g;
  if($cmdReturn == 0)
  {
    push(@returnVal, $cmdReturn . ' - NICI ' . $niciVer . ' backed up to ' . $niciTarPath);

    #Make the files a bit more secure.
    $cmd0 = 'chmod 600 ' . $niciTarPath . ' >/dev/null';
    $cmdReturn = systemRunAndGetReturnVal($cmd0);
    push(@returnVal, ($cmdReturn == 0 ? ($cmdReturn . ' - NICI backup at ' . $niciTarPath . ' has been secured.') : ($cmdReturn . ' - NICI backup may not be as secure as it should be depending on this user\'s umask and the backup\'s location.')));

  }#End if
  else
  {
    push(@returnVal, $cmdReturn . ' - NICI ' . $niciVer . ' NOT successfully backed up to ' . $niciTarPath . '.  Verify rights to the NICI source and backup location.');
  }#End else

  return @returnVal;
}

#Get the OS platform.
sub getPlatformOS()
{
  my($cmd0) = 'uname -a';
  my($cmdReturn) = '';

  $cmdReturn = systemRunGetOutput($cmd0);
  $cmdReturn =~ s/(\w+?)\s.+/$1/;

  return $cmdReturn;
}

#Get eDir version from box.  Refinement needed here.
sub geteDirVersion()
{
  my($eDirString) = '';
  my($cmd0) = '';
  my($verString) = '';
  my($os) = getPlatformOS();

  #Get the eDir version.
  #Do different things based on which OS is in use.
  if($os =~ /sun/i)
  {
    $cmd0 = 'pkgparam -v NDSserv | grep "^VERSION" | awk -F"=" \'{print $2}\'';
    $verString = systemRunGetOutput($cmd0);
    $verString =~ s/\'(.+)\'/$1/;
  }#End if
  elsif($os =~ /linux/i)
  {
    $cmd0 = 'rpm -qi NDSserv | grep "Version" | awk \'{print $3}\'';
    $verString = systemRunGetOutput($cmd0);

    #If DS version not found keep looking.
    if(length($verString) < 2)
    {
      $cmd0 = 'rpm -qi novell-NDSserv | grep "Version" | awk \'{print $3}\'';
      $verString = systemRunGetOutput($cmd0);
    }#End if
  }#End elsif

  return $verString;

  #Detect eDir version notes.
  #One way to do it.....return 0 == 8.8, return 1 != 8.8.
  #rpm -qa | grep -i ndsbase | grep '8.8'
  #
  #or
  #
  #Linux:    rpm -qi NDSserv | grep "Version" | awk '{print $3}'
  #AIX:    lslpp -L | grep NDSserv | awk '{print $2}'
  #Solaris:    pkgparam -v NDSserv | grep "VERSION" | awk -F"=" '{print $2}'
  #if (majorVersion == 8 && minorVersion == 7 && revision >= 3)
  # EDIR_INSTALLED=TRUE
  #
  #and
  #
  #Linux:    rpm -qi novell-NDSserv | grep "Version" | awk '{print $3}'
  #AIX:    lslpp -L | grep NDSserv | awk '{print $2}'
  #Solaris:    pkgparam -v NDSserv | grep "VERSION" | awk -F"=" '{print $2}'
  #if ((majorVersion > 8) || (majorVersion >= 8 && minorVersion >= 8))
  # EDIR_INSTALLED=TRUE
  # EDIR88_INSTALLED=TRUE
}

#Get NICI version from box.
sub getNICIVer()
{
  my($cmd0) = '';
  my($returnVal) = '';
  my($os) = getPlatformOS();

  #Generate the command based on platform, then run it and make the results
  #nice.
  if($os =~ /sun/i)
  {
    $cmd0 = 'pkgparam -v NOVLniu0 | grep "VERSION" | awk -F"=" \'{print $2}\'';
    $returnVal = systemRunGetOutput($cmd0);
    $returnVal =~ s/\'(.+)\'/$1/;
  }#End if
  elsif($os =~ /linux/i)
  {
    $cmd0 = 'rpm -qa | grep nici';
    $returnVal = systemRunGetOutput($cmd0);
    $returnVal =~ s/nici-(.+)/$1/;
  }#End elsif

  return $returnVal;
}

#Method to run a command and get the output from it.  Output is 'chomp'-d.
sub systemRunGetOutput($)
{
  my($cmd0) = shift(@_);
  my($cmdReturn) = '';
  $cmdReturn = `$cmd0`;
  chomp($cmdReturn) if(length($cmdReturn) != 0);
  return $cmdReturn;
}

#Method to run a command and return the return value.
sub systemRunAndGetReturnVal($)
{
  my($cmd0) = shift(@_);
  return system($cmd0) >> 8;
}

#Print resulting logs to the screen.
sub getLog(@)
{
  my(@log) = @_;

  #Join logs printing them out.
  return "\n\n" . join("\n", @log) . "\n\n";
}

#Sort stuff numerically ascending.
sub byNumericAsc($$)
{
  $a <=> $b;
}

#Sort stuff numerically descending.
sub byNumericDesc($$)
{
  $b <=> $a;
}

#Determine if a specified value exists in an array.
sub arrayContains(\@$)
{
  my($haystack) = shift(@_);
  my($needle) = shift(@_);
  if(!ref($haystack)){$haystack = \$haystack;} #If not a reference, make it one. Need to test this more.
  my($ctr0) = 0;

  #Find the needle in the haystack.
  for($ctr0 = 0; $ctr0 < scalar(@{$haystack}); ++$ctr0)
  {
    return 1 if($haystack->[$ctr0] eq $needle); 
  }#End for
  return 0;
}

#Method to get options from the command line with the getopts modules included
#in Perl.
sub getArgs($)
{
  use Getopt::Std;
  my(%opts) = ();
  my($validOptString) = shift(@_);

  #Do the work.
  getopts($validOptString, \%opts);

  return %opts;
}

#Method to repeat character(s) a number of times.
sub repeatChars($$)
{
  my($chars) = shift(@_);
  my($repeats) = shift(@_);
  my($ctr0) = 0;
  my($output) = '';

  for($ctr0 = 0; $ctr0 < $repeats; ++$ctr0)
  {
    $output .= $chars;
  }#End for
  return $output;
}

#Method to alert about ndsmanage's path.
sub getNdsmanageAlert()
{
  my(@returnVal) = ();
  my($userInput) = '';
  my($cmd0) = 'ndsmanage -h 2>/dev/null 1>&2';

  if(systemRunAndGetReturnVal($cmd0) ne '0')
  {
    push(@returnVal, '0 - ndsmanage is not in path...prompting user regarding this.');
    print "\n\n" . 'You do not seem to have \'ndsmanage\' in your path and may have strange' . "\n" .
      'errors running this script as a result.  Normally you can add it by typing' . "\n" .
      '\'. /opt/novell/eDirectory/bin/ndspath\' from the command line.  Should' . "\n" .
      'the script continue running (y/n)? ';

    $userInput = <>;
    chomp($userInput);
    push(@returnVal, '0 - User typed: ' . $userInput);

    #If the user put an 'n' in the text anywhere...even once, stop.
    if($userInput =~ /n/i)
    {
      push(@returnVal, '0 - User opted to stop instead of trying to run without ndsmanage.');
      print getLog(@returnVal);
      exit 0;
    }#End if

    push(@returnVal, '0 - User did not opt to stop. Continuing.');

  }#End if

  push(@returnVal, '0 - \'ndsmanage\' ran properly without any path specified.');

  return @returnVal;
}

#Method to prompt user about how DS will be unloaded...get their confirmation.
#At some point perhaps this should be broken out per instance, but for now, no.
#That makes the scriptability of this entire thing more complex.
sub getDSUnloadingAlert()
{
  my(@returnVal) = ();
  my($userInput) = '';

  push(@returnVal, '0 - Prompting user about DS being stopped and getting input.');
  print "\n\n" . 'ALERT!!!! DS will be stopped and restarted for instances that are' . "\n" .
    'backed up!  If this is not your intention press \'n\' now to stop this whole' . "\n" .
    'process (typing anything else, or nothing, will continue): ';
  $userInput = <>;
  chomp($userInput);
  push(@returnVal, '0 - User typed: ' . $userInput);

  #If the user put an 'n' in the text anywhere...even once, stop.
  if($userInput =~ /n/i)
  {
    push(@returnVal, '0 - User opted to stop instead of killing DS instances.');
    print getLog(@returnVal);
    exit 0;
  }#End if

  push(@returnVal, '0 - User did not opt to stop. Continuing.');

  return @returnVal;
}

#Method to get usage information for the script.
sub getUsage()
{
  my($output) = '';
  $output .= "\n\n";
  $output .= 'The -c switches is used on its own (without -d). The -p and -s switches can be' . "\n" .
    'used can be used with any set of options.  The -d option is trumped by the' . "\n" .
    '-c option.  The -d (conf directory) option is only implemented for 8.8+' . "\n" .
    'backups and is most-useful for non-root installs of eDirectory.' . "\n\n";
  $output .= 'Usage: ./ndsrc.pl -h                     #Show help information. This info here.' . "\n";
  $output .= '       ./ndsrc.pl -p /tmp/backup/path/goes/here' . "\n";
  $output .= '       ./ndsrc.pl -c /etc/nds/conf/file/nds.conf[,/etc/other/conf/file/nds.conf]' . "\n";
  $output .= '       ./ndsrc.pl -d /etc/opt/novell/eDirectory/conf/.edir/' . "\n";
  $output .= '       ./ndsrc.pl -s             #Force through the reminders that are defaults.' . "\n";
  $output .= "\n";
  print STDERR $output;
  exit 0;
}

#Method to clean the user's command line arguments.
sub cleanCmdLineArgs
{
  my(@returnVal) = ();
  my($argsHash) = shift(@_);
  my(@configFilesArray) = ();

  #Be sure the backup directory path has a trailing slash.
  $argsHash->{'p'} .= ((substr($argsHash->{'p'}, -1) eq '/') ? ('') : ('/')) if(defined($argsHash->{'p'}));

  #Make user-specific configurations into a hash, comma-delimited.
  if(defined($argsHash->{'c'}))
  {
    @configFilesArray = split(/,/, $argsHash->{'c'});
    undef($argsHash->{'c'});
    @{$argsHash->{'c'}} = @configFilesArray;
  }#End if

  #Make sure any user-specified directories have trailing slashes.
  $argsHash->{'d'} .= ((substr($argsHash->{'d'}, -1) eq '/') ? ('') : ('/')) if(defined($argsHash->{'d'}));

  return @returnVal;
}

#Detect if the file specified exists.
sub fileExists($;$)
{
  my(@returnVal) = ();
  my($filePath) = shift(@_);
  my($fatalFailure) = ((defined($_[0])) ? (shift(@_)) : ('1'));

  #If the file exists make note. Otherwise do something else.
  if(-e $filePath)
  {
    push(@returnVal, '0 - File found at: ' . $filePath);
  }#End if
  else
  {
    push(@returnVal, '1 - File NOT found at: ' . $filePath);
    push(@returnVal, '1 - FATAL Missing File') if($fatalFailure);
    print getLog(@returnVal) if($fatalFailure);
    exit 1 if($fatalFailure);
  }#End else

  return @returnVal;
}

#Code from website: http://www.devdaily.com/scw/perl/perl-5.8.5/ext/Storable/t/s
# Given an object, dump its transitive data closure
sub dumpAll
{
  local(%dump) = ('SCALAR' => 'dump_scalar', 'ARRAY' => 'dump_array', 'HASH' => 'dump_hash', 'REF' => 'dump_ref');
  my($object) = shift(@_);
  my($complainOnDuplicate) = ((scalar(@_) > 0) ? shift(@_) : '1');

  #If somebody specified what to do about duplicates make note of it.
  if(scalar(@_) > 0)
  {
    $complainOnDuplicate = shift(@_);
  }#End if

  die('Not a reference!') unless(ref($object));
  local(%dumped);
  local($numSpaces) = -1;
  local($count) = 0;
  local($dumped) = '';
  recursive_dump($object, $complainOnDuplicate);
  return $dumped;
}

# This is the root recursive dumping routine that may indirectly be
# called by one of the routine it calls...
# The link parameter is set to false when the reference passed to
# the routine is an internal temporay variable, implying the object's
# address is not to be dumped in the %dumped table since it's not a
# user-visible object.
sub recursive_dump
{
  my($object) = shift(@_);
  my($link) = ((scalar(@_) > 0) ? shift(@_) : '1');

  ++$numSpaces;

  # Get something like SCALAR(0x...) or TYPE=SCALAR(0x...).
  # Then extract the bless, ref and address parts of that string.

  my($what) = "$object";    # Stringify
  my($bless, $ref, $addr) = $what =~ /^(\w+)=(\w+)\((0x.*)\)$/;
  ($ref, $addr) = $what =~ /^(\w+)\((0x.*)\)$/ unless($bless);

  # Special case for references to references. When stringified,
  # they appear as being scalars. However, ref() correctly pinpoints
  # them as being references indirections. And that's it.

  $ref = 'REF' if(ref($object) eq 'REF');

  # Make sure the object has not been already dumped before.
  # We don't want to duplicate data. Retrieval will know how to
  # relink from the previously seen object.

  if(($link == 1) && $dumped{$addr}++)
  {
    my $num = $object{$addr};
    $dumped .= repeatChars(' ', $numSpaces) . 'OBJECT #' . $num . ' seen' . "\n";
    return;
  }#End if

  my($objCount) = $count++;
  $object{$addr} = $objCount;

  # Call the appropriate dumping routine based on the reference type.
  # If the referenced was blessed, we bless it once the object is dumped.
  # The retrieval code will perform the same on the last object retrieved.

  die('Unknown simple type \'' . $ref . '\'') unless(defined $dump{$ref});

  $dumped .= repeatChars(' ', $numSpaces) . 'OBJECT ' . $objCount . "\n";
  &{$dump{$ref}}($object, $link);  # Dump object
  myBless($bless) if($bless);  # Mark it as blessed, if necessary
  --$numSpaces;

  return;
}

# Indicate that current object is blessed
sub myBless
{
  my($class) = @_;
  $dumped .= repeatChars(' ', $numSpaces) . 'BLESS ' . $class . "\n";
  return;
}

# Dump single scalar
sub dump_scalar(\$)
{
  my($sref) = @_;
  my($scalar) = $$sref;
  unless(defined $scalar)
  {
    $dumped .= repeatChars(' ', $numSpaces) . 'UNDEF' . "\n";
    $numSpaces -= 2;
    return;
  }#End unless
  my($len) = length($scalar);
  $dumped .= repeatChars(' ', $numSpaces) . 'SCALAR len=' . $len . ' ' . $scalar. "\n";
  return;
}

# Dump array
sub dump_array(\@)
{
  my($aref) = shift(@_);
  my($link) = ((scalar(@_) > 0) ? shift(@_) : '1');
  my($items) = (0 + @{$aref});

  ++$numSpaces;
  $dumped .= repeatChars(' ', $numSpaces) . 'ARRAY items=' . $items. "\n";
  foreach $item (@{$aref})
  {
    unless (defined $item)
    {
      $dumped .= repeatChars(' ', $numSpaces) . 'ITEM_UNDEF' . "\n";
      next;
    }#End unless
    $dumped .= repeatChars(' ', $numSpaces) . 'ITEM ';
    recursive_dump(\$item, $link);
  }#End foreach
  --$numSpaces;

  return;
}

# Dump hash table
sub dump_hash(\%)
{
  my($href) = @_;
  my($link) = ((scalar(@_) > 0) ? shift(@_) : '1');
  my($items) = scalar(keys %{$href});

  ++$numSpaces;
  $dumped .= repeatChars(' ', $numSpaces) . 'HASH items=' . $items . "\n";
  foreach $key (sort keys %{$href})
  {
    $dumped .= repeatChars(' ', $numSpaces) . 'KEY' . "\n";
    recursive_dump(\$key, $link);
    unless(defined $href->{$key})
    {
      $dumped .= repeatChars(' ', $numSpaces) . 'VALUE_UNDEF' . "\n";
      next;
    }#End unless
    $dumped .= repeatChars(' ', $numSpaces) . 'VALUE' . "\n";
    recursive_dump(\$href->{$key}, $link);
  }#End foreach
  --$numSpaces;

  return;
}

# Dump reference to reference
sub dump_ref(\$)
{
  my($rref) = shift(@_);
  my($link) = ((scalar(@_) > 0) ? shift(@_) : '1');
  my($deref) = $$rref;        # Follow reference to reference
  $dumped .= repeatChars(' ', $numSpaces) . 'REF ';
  recursive_dump($deref, $link);    # $dref is a reference
  return;
}
#End Code from website: http://www.devdaily.com/scw/perl/perl-5.8.5/ext/Storable/t/s








#Creating a $nowDate variable that contains the current timestamp in some format
#that is useful.  The reason for this is so we can easily create concurrent
#backups without having to do a bunch of nasty checking for the newest file.
#It is up to the user to specify the date format from one of the following.
#This can all be removed with minor work as well but it is not recommended.
#Concurrent backups can be very helpful when it comes to corruption problems
#in case this script has created a backup since the corruption (overwriting
#something that was not corrupt).  By default the format is specified as:
#YYYY-MM-DD-HH:MM:SS format.  There is a commented out line below for setting
#the format to the number of seconds since the Unix epoch.  Default works,
#alternative works, completely up to user.  If you use the latter be sure to
#comment out the former or you will get errors about how you're declaring the
#$nowDate variable multiple times.
#
#Date in Y-M-D-H:M:S format: date +%Y-%m-%d-%H:%M:%S
#This creates something like 2005-12-06-17:35:06
my($nowDate) = systemRunGetOutput('date +%Y-%m-%d-%H:%M:%S');
#Alternative date in UnixTime (seconds since 1970 epoch) format: date +%s
#my($nowDate) = systemRunGetOutput('date +%s');

#Declaring stuff.
use warnings;

#Other variables.
my(@log) = ();
my(%cmdLineParams) = getArgs('shp:c:d:');
my($backupDir) = '';
my(@configFiles) = ();
my($configDir) = '';

#Parameters should be properly formatted and then put to real variables.
#Defaults are also specified here in case a user does not do anything.
push(@log, cleanCmdLineArgs(\%cmdLineParams));
$backupDir = ((defined($cmdLineParams{'p'})) ? ($cmdLineParams{'p'}) : ('/root/'));
push(@configFiles, @{$cmdLineParams{'c'}}) if((defined($cmdLineParams{'c'})));
$configDir = ((defined($cmdLineParams{'d'})) ? ($cmdLineParams{'d'}) : (''));

#$backupDir = '/tmp/'; #DEBUG - Remove when ready for use in production.

#They need help.  Help them.
getUsage() if(defined($cmdLineParams{'h'}));

#Pre-run checks, if not skipping them.
if(!(defined($cmdLineParams{'s'})))
{
  push(@log, getDSUnloadingAlert());
}#End if

#Backup appropriate eDirectory files.
push(@log, eDirBackup($nowDate, $backupDir, $configDir, \@configFiles));

#Backup appropriate NICI files.
push(@log, niciBackup($nowDate, $backupDir));  


#Wrap it up.
print STDERR getLog(@log);
exit 0;


