#!/usr/bin/perl

# NOTICE_START
# Licensed Materials - Property of IBM
# "Restricted Materials of IBM" -- 5746-SM2
# (C) Copyright IBM Corp. 2007, 2009 All Rights Reserved.
# US Government Users Restricted Rights - Use, duplication or disclosure
# restricted by GSA ADP Schedule Contract with IBM Corp.
# NOTICE_END

require 5.008;

use strict;
use warnings;
use Sys::Syslog;
use I18N::Langinfo ();

my $PACKAGENAME="powervm-lx86";
my $line = "";
my $POWERVM_LX86_DIR;
my $POWERVM_LX86_BIN_DIR;
my $POWERVM_LX86_X86WORLD;
my $POWERVM_LX86_LOG_TAG="$PACKAGENAME";
my $POWERVM_LX86_ERROR_TAG="$PACKAGENAME-world-sync";
my $POWERVM_LX86_CONFIG_DIR="/etc/opt/$PACKAGENAME";
my $POWERVM_LX86_CONFIG_FILE="$POWERVM_LX86_CONFIG_DIR/config";
my $runByUser = 1;

my $terminalEncoding = I18N::Langinfo::langinfo(I18N::Langinfo::CODESET());
$terminalEncoding = 'UTF-8' if $terminalEncoding =~ /ANSI_X3/;
binmode STDOUT, ":encoding($terminalEncoding)";
binmode STDERR, ":encoding($terminalEncoding)";


sub getConfig
{
  my $configOption = shift;
  my $value = shift; # default value
  
  open(INPUTDATA, "<", "$POWERVM_LX86_CONFIG_FILE") || return $value;
  while ( $line = <INPUTDATA> )
  {
    if ( $line =~ /^$configOption=(.*)/ )
    {
      $value = $1;
    }
  }
  close(INPUTDATA);

  return $value;
}


# Need to get the installation directories here
$POWERVM_LX86_DIR = getConfig("POWERVM_LX86_LOCATION", "/opt/$PACKAGENAME");
$POWERVM_LX86_BIN_DIR = "$POWERVM_LX86_DIR/bin";
$POWERVM_LX86_X86WORLD = getConfig("SUBJECT_WORLD_ROOT", "/i386");

# No need to run if the subject world has not been installed
unless( -e "$POWERVM_LX86_X86WORLD/etc/.sw_installed_successfully" )
{
  exit(0);
}  

# Have to do this in order to load the module correctly
# since we can't assume the locale module is relative to the binary
push @INC, "$POWERVM_LX86_DIR/lib/perl5";
require powervm_lx86_scripts::L10N;


my $POWERVM_LX86="$POWERVM_LX86_BIN_DIR/$PACKAGENAME";
my $THIS_SCRIPT="/usr/sbin/$PACKAGENAME-world-sync";

my $POWERVM_LX86_USER_WHITE_LIST="$POWERVM_LX86_CONFIG_DIR/user_ignore";
my $POWERVM_LX86_USERID_WHITE_LIST="$POWERVM_LX86_CONFIG_DIR/uid_ignore";
my $POWERVM_LX86_GROUP_WHITE_LIST="$POWERVM_LX86_CONFIG_DIR/group_ignore";
my $POWERVM_LX86_GROUPID_WHITE_LIST="$POWERVM_LX86_CONFIG_DIR/gid_ignore";

my $NATIVE_MTAB_FILE="/etc/mtab";
my $SUBJECT_MTAB_FILE="$POWERVM_LX86_X86WORLD/etc/mtab";
my $TMP_MTAB_FILE="$SUBJECT_MTAB_FILE.$PACKAGENAME";

my $NATIVE_PASSWD_FILE="/etc/passwd";
my $SUBJECT_PASSWD_FILE="$POWERVM_LX86_X86WORLD/etc/passwd";
my $TMP_PASSWD_FILE="$SUBJECT_PASSWD_FILE.$PACKAGENAME";

my $NATIVE_GROUP_FILE="/etc/group";
my $SUBJECT_GROUP_FILE="$POWERVM_LX86_X86WORLD/etc/group";
my $TMP_GROUP_FILE="$SUBJECT_GROUP_FILE.$PACKAGENAME";


sub getTrans
{
  my ($msg, @args) = @_;

  # Watch out for tuples that specify an encoding
  my @args_copy;
  foreach (@args) {
    if (UNIVERSAL::isa($_, 'ARRAY')) {
      $_ = Encode::decode($_->[1], $_->[0]);
    }
    push @args_copy, $_;
  }

  # generate the translated output
  my $trans = powervm_lx86_scripts::L10N->get_handle()->maketext($msg, @args_copy);

  return $trans;
}


sub logMessage
{
  my ($msg, @args) = @_;

  chomp $msg; # we don't want any newline sending to maketext

  # generate the translated output
  my $trans = getTrans($msg, @args);

  # anything printed to the stderr will be emailed to root by cron
  print STDERR "$POWERVM_LX86_LOG_TAG: $trans\n";
  return system("logger", "-t", $POWERVM_LX86_LOG_TAG, $trans);
}


sub writeMessage
{
  my ($msg, @args) = @_;

  # generate the translated output
  my $trans = getTrans($msg, @args);

  # anything printed to the stderr will be emailed to root by cron
  print STDERR "$POWERVM_LX86_LOG_TAG: $trans\n";
}


sub fatalError
{
  my ($errorCode, $msg, @args) = @_;

  chomp $msg; # we don't want any newline sending to maketext

  # generate the translated output
  my $trans = getTrans($msg, @args);

  if ( $errorCode == 0 )
  {
    print STDERR "[$POWERVM_LX86_ERROR_TAG]: $trans\n";
  }
  else
  {
    print STDERR "[$POWERVM_LX86_ERROR_TAG][Error: $errorCode] $trans\n";
  }
  exit(1);
}


sub checkPrivileges()
{
  if ( $> != 0 )
  {
    fatalError(1, "You must be root to run this script.");
  }
}


sub getPrintableError()
{
  return [$!,$terminalEncoding];
}


sub getLoFileName
{
  my $loopDevice = shift;
  my $loFileName = "";
  open(LOSETUPDATA, "-|") or exec("/sbin/losetup", $loopDevice);
  while ($line = <LOSETUPDATA>)
  {
    if ( $line =~ /^.*\((.*)\)$/ )
    {
      $loFileName = $1;
    }
  }
  close(LOSETUPDATA);
  return $loFileName;
}


sub generateNewMtabFile
{
  my $listMountsCmd = shift;
  my $mtabFile = shift;

  open(INPUTDATA, "-|") or exec(@$listMountsCmd) or fatalError(2, "Failed to get current set of mount entries.");
  open(OUTPUTDATA, ">", $mtabFile) || fatalError(3, "Cannot open \"[_1]\": [_2].",
                                                    [$mtabFile, $terminalEncoding], getPrintableError());

  # Try to read the target mtab file to check extra options.
  my $targetMtabDataAvailable = 0;
  my @targetMtabData;
  if(open(MTABDATA,"/etc/mtab"))
  {
    @targetMtabData = <MTABDATA>;
    close(MTABDATA);
    $targetMtabDataAvailable = 1;
  }

  while ($line = <INPUTDATA>)
  {
    my $processedLine;
    if ( $line =~ /^(\S*\/dev\/loop\d)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+).*/ )
    {
      my $loFileName = getLoFileName($1);
      my $device = $1;
      my $mountPoint = $2;
      my $fsType = $3;
      my $options = $4;
      my $uid = $5;
      my $gid = $6;

      # It is possible to mount the same file to loop devices (say loop1 and loop 2) in two ways:
      #   1. use losetup and then a simple mount with no fancy arguments
      #   2. use mount with fancy arguments
      # For two mounts done differently like this, the options diplayed
      # in the mtab file will be different, but the options in the proc/mounts file will be the same.
      # As we use the /proc/mounts file here we get the output wrong, as we can't
      # detect the different way they were mounted. So our mtab file is not right.
      # To rectify this, when we detect the device mount in the /proc/mounts file
      # we check the target /etc/mtab file (if it exists) to determine which way it was mounted.
      # Then we can determine what is the correct options output. Ugh!
      # P.S. mounting with fancy arguments requires loop specification in the options
      my $loopInOptions = 0;
      my $mtabLine;

      # ensure the target /etc/mtab is available
      if ($targetMtabDataAvailable == 1) {
        # loop through the target mtab entries, looking for any with the
        # device loop specified in the options
        foreach $mtabLine (@targetMtabData) {
          # search pattern: / name space(s) mountpoint space(s) fstype space(s) options space(s) uid space(s) guid /
          if( $mtabLine =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+).*/ ) {
          my $mtaboptions = $4;

            # have we got a loop device specified in the target mtab options?
            if($mtaboptions =~ /^(\S+,)(loop=)(\/dev\/loop\d).*/ ) {
              my $mtabDevice = $3;
              # if the loop device in the /proc/mounts is the same as this entry in the /etc/mtab
              if ($device eq $mtabDevice) {
                # we need to show the loop device in the options
                $loopInOptions = 1;
              }
            }
          }
        }
      }

      # get rid of any subject world roots
      if ( $device =~ /^$POWERVM_LX86_X86WORLD(\S+)$/ )
      {
        $device = $1;
      }
      if ( $loFileName =~ /^$POWERVM_LX86_X86WORLD(\S+)$/ )
      {
        $loFileName = $1;
      }
      if ( $mountPoint =~ /^$POWERVM_LX86_X86WORLD(\S+)$/ )
      {
        $mountPoint = $1;
      }

      if ($loopInOptions == 1) {
        $processedLine = "$loFileName $mountPoint $fsType $options,loop=$device $uid $gid";
      } else {
        # if proc/mounts and mtab are different use the original device name
        $processedLine = "$device $mountPoint $fsType $options $uid $gid";
      }
    }
    else
    {
      chomp($line);
      $processedLine = $line;
    }

   if( $targetMtabDataAvailable and $processedLine =~ /^(\S*\/\S*)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)/ )
    {
      my $dev = $1;
      my $mountpoint = $2;
      my $fstype = $3;
      my $opts = $4;
      my $other = $5;

      foreach my $mtabEntry (@targetMtabData)
      {
        if( $mtabEntry =~ /^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/ )
        {
          if ( ($1 eq $dev) and !($4 eq $opts) )
          {
            $processedLine = "$dev $mountpoint $fstype $4 $other";
          }
        }
      }
    }
    print OUTPUTDATA "$processedLine\n";
  }

  close(OUTPUTDATA);
  close(INPUTDATA);
}


sub syncMtabFile
{
  generateNewMtabFile([$POWERVM_LX86, qw|/bin/cat /proc/mounts|], $TMP_MTAB_FILE);
  # We should preserve the permissions and ownerships of the original file
  system("chmod", "--reference=$POWERVM_LX86_X86WORLD/etc/mtab", $TMP_MTAB_FILE);
  system("chown", "--reference=$POWERVM_LX86_X86WORLD/etc/mtab", $TMP_MTAB_FILE);
  rename($TMP_MTAB_FILE, $SUBJECT_MTAB_FILE);
  return 0;
}


sub generateNewUserEmail
{
  my $userAccount  = shift;
  my $passwdEntry  = shift;
  my $passwordFile = shift;

  my $translatedIdOutput = "";

  open(INPUTDATA, "-|") or do {
    close(STDERR);
    exec($POWERVM_LX86, "/usr/bin/id", $userAccount);
  };
  $translatedIdOutput = <INPUTDATA>;
  close(INPUTDATA);

  if ( $? != 0 )
  {
    $translatedIdOutput = getTrans("(Unable to run command '[_1] /usr/bin/id [_2]')", [$POWERVM_LX86, $terminalEncoding], $userAccount);
    $translatedIdOutput .= "\n";
  }

  writeMessage("_NEW_USER_EMAIL", [$passwordFile, $terminalEncoding], [$passwordFile, $terminalEncoding], $passwdEntry,
                                  $userAccount, [$translatedIdOutput, $terminalEncoding], $userAccount, $userAccount, $userAccount, $userAccount,
                                  $userAccount);
  writeMessage("_EMAIL_DISABLE", $THIS_SCRIPT);
  print STDERR "----------\n\n";
}


sub generateNewGroupEmail
{
  my $groupAccount = shift;
  my $groupEntry   = shift;
  my $groupFile    = shift;

  writeMessage("_NEW_GROUP_EMAIL", [$groupFile, $terminalEncoding], [$groupFile, $terminalEncoding], $groupEntry,
                                   $groupAccount, $groupAccount);
  writeMessage("_EMAIL_DISABLE", $THIS_SCRIPT);
  print STDERR "----------\n\n";
}


sub generateAliasedUserEmail
{
  my $userAccount        = shift;
  my $userId             = shift;
  my $subjectPasswdEntry = shift;
  my $targetPasswdEntry  = shift;
  my $passwordFile       = shift;

  writeMessage("_NEW_UID_ALIAS_EMAIL", [$passwordFile, $terminalEncoding], $userId, [$passwordFile, $terminalEncoding],
                                       $subjectPasswdEntry, $targetPasswdEntry, $userId, $userId, $userAccount, $userAccount,
                                       $userAccount, $userAccount, $userId, $userId, [$POWERVM_LX86_X86WORLD, $terminalEncoding]);
  writeMessage("_EMAIL_DISABLE", $THIS_SCRIPT);
  print STDERR "----------\n\n";
}


sub generateAliasedGroupEmail
{
  my $groupAccount      = shift;
  my $groupId           = shift;
  my $subjectGroupEntry = shift;
  my $targetGroupEntry  = shift;
  my $groupFile         = shift;

  writeMessage("_NEW_GID_ALIAS_EMAIL", [$groupFile, $terminalEncoding], $groupId, [$groupFile, $terminalEncoding],
                                       $subjectGroupEntry, $targetGroupEntry, $groupId, $groupId, $groupAccount,
                                       $groupAccount, $groupAccount, $groupAccount, $groupId, $groupId,
                                       [$POWERVM_LX86_X86WORLD, $terminalEncoding]);
  writeMessage("_EMAIL_DISABLE", $THIS_SCRIPT);
  print STDERR "----------\n\n";
}


sub checkPasswdFile
{
  my @userWhiteList = ();
  my @userIdWhiteList = ();
  my @subjectData;
  my @targetData;

  if ( open(INPUTDATA, "<", $POWERVM_LX86_USER_WHITE_LIST) )
  {
    @userWhiteList = <INPUTDATA>;
    close(INPUTDATA);
  }

  if ( open(INPUTDATA, "<", $POWERVM_LX86_USERID_WHITE_LIST) )
  {
    @userIdWhiteList = <INPUTDATA>;
    close(INPUTDATA);
  }

  open(INPUTDATA, "<", $SUBJECT_PASSWD_FILE) || fatalError(3, "Cannot open \"[_1]\": [_2].",
                                                              [$SUBJECT_PASSWD_FILE, $terminalEncoding], getPrintableError());
  @subjectData = <INPUTDATA>;
  close(INPUTDATA);

  open(INPUTDATA, "<", $NATIVE_PASSWD_FILE) || fatalError(3, "Cannot open \"[_1]\": [_2].", $NATIVE_PASSWD_FILE, getPrintableError());
  @targetData = <INPUTDATA>;
  close(INPUTDATA);

  my $mustSynchronise = 0;

  # Look for new users in subject world
  foreach my $subjectLine (@subjectData)
  {
    if( $subjectLine =~ /^([^+].*)\:(.*)\:(.*)\:(.*)\:(.*)\:(.*)\:(.*)$/ )
    {
      my $foundMatch = 0;
      my $foundAlias = 0;
      my $subjectAccount = $1;
      my $subjectUid = $3;
      my $aliasedTargetEntry = "";
      foreach my $targetLine (@targetData)
      {
        if ( $targetLine =~ /^([^+].*)\:(.*)\:(.*)\:(.*)\:(.*)\:(.*)\:(.*)$/ )
        {
          if ( $1 eq $subjectAccount )
          {
            $foundMatch = 1;
          }
          elsif ( $3 == $subjectUid )
          {
            $aliasedTargetEntry = $targetLine;
            $foundAlias = 1;
          }
        }
      }
      if ( 0 == $foundMatch )
      {
        my $warnNewUser = 1;
        foreach my $userWhiteListLine (@userWhiteList)
        {
          if ( $userWhiteListLine =~ /^(.+)$/ )
          {
            if ( $1 eq $subjectAccount )
            {
              $warnNewUser = 0;
            }
          }
        }
        if ( $warnNewUser == 1 )
        {
          logMessage("New user account ('[_1]') found in [_2].", $subjectAccount, [$SUBJECT_PASSWD_FILE, $terminalEncoding]);
          if ( $runByUser == 0 )
          {
            generateNewUserEmail($subjectAccount, $subjectLine, $SUBJECT_PASSWD_FILE);
          }
        }
        $mustSynchronise = 1;
      }
      if ( 1 == $foundAlias )
      {
        my $warnAliasedUserId = 1;
        foreach my $userIdWhiteListLine (@userIdWhiteList)
        {
          if ( $userIdWhiteListLine =~ /^(\d+)$/ )
          {
            if ( $1 == $subjectUid )
            {
              $warnAliasedUserId = 0;
            }
          }
        }
        if ( $warnAliasedUserId == 1 )
        {
          logMessage("Aliased user id ('[_1]') found in [_2].", $subjectUid, [$SUBJECT_PASSWD_FILE, $terminalEncoding]);
          if ( $runByUser == 0 )
          {
            generateAliasedUserEmail($subjectAccount, $subjectUid, $subjectLine, $aliasedTargetEntry, $SUBJECT_PASSWD_FILE);
          }
        }
        $mustSynchronise = 1;
      }
    }
  }

  return $mustSynchronise;
}


sub checkGroupFile
{
  my @groupWhiteList = ();
  my @groupIdWhiteList = ();
  my @subjectData;
  my @targetData;

  if ( open(INPUTDATA, "<", $POWERVM_LX86_GROUP_WHITE_LIST) )
  {
    @groupWhiteList = <INPUTDATA>;
    close(INPUTDATA);
  }

  if ( open(INPUTDATA, "<", $POWERVM_LX86_GROUPID_WHITE_LIST) )
  {
    @groupIdWhiteList = <INPUTDATA>;
    close(INPUTDATA);
  }

  open(INPUTDATA, "<", $SUBJECT_GROUP_FILE) || fatalError(3, "Cannot open \"[_1]\": [_2].",
                                                             [$SUBJECT_GROUP_FILE, $terminalEncoding], getPrintableError());
  @subjectData = <INPUTDATA>;
  close(INPUTDATA);

  open(INPUTDATA, "<", $NATIVE_GROUP_FILE) || fatalError(3, "Cannot open \"[_1]\": [_2].", $NATIVE_GROUP_FILE, getPrintableError());
  @targetData = <INPUTDATA>;
  close(INPUTDATA);

  my $mustSynchronise = 0;

  # Look for new users in subject world
  foreach my $subjectLine (@subjectData)
  {
    if ( $subjectLine =~ /^([^+].*)\:(.*)\:(.*)\:(.*)$/ )
    {
      my $foundMatch = 0;
      my $foundAlias = 0;
      my $subjectGroup = $1;
      my $subjectGid = $3;
      my $aliasedTargetEntry = "";
      foreach my $targetLine (@targetData)
      {
        if ( $targetLine =~ /^([^+].*)\:(.*)\:(.*)\:(.*)$/ )
        {
          if ( $1 eq $subjectGroup )
          {
            $foundMatch = 1;
          }
          elsif ( $3 == $subjectGid )
          {
            $aliasedTargetEntry = $targetLine;
            $foundAlias = 1;
          }
        }
      }
      if ( 0 == $foundMatch )
      {
        my $warnNewGroup = 1;
        foreach my $groupWhiteListLine (@groupWhiteList)
        {
          if ( $groupWhiteListLine =~ /^(.+)$/ )
          {
            if ( $1 eq $subjectGroup )
            {
              $warnNewGroup = 0;
            }
          }
        }
        if ($warnNewGroup == 1 )
        {
          logMessage("New group ('[_1]') found in [_2].", $subjectGroup, [$SUBJECT_GROUP_FILE, $terminalEncoding]);
          if ( $runByUser == 0 )
          {
            generateNewGroupEmail($subjectGroup, $subjectLine, $SUBJECT_GROUP_FILE);
          }
          $mustSynchronise = 1;
        }
      }
      if ( 1 == $foundAlias )
      {
        my $warnAliasedGroupId = 1;
        foreach my $groupIdWhiteListLine (@groupIdWhiteList)
        {
          if ( $groupIdWhiteListLine =~ /^(\d+)$/ )
          {
            if ( $1 == $subjectGid )
            {
              $warnAliasedGroupId = 0;
            }
          }
        }
        if ( $warnAliasedGroupId == 1 )
        {
          logMessage("Aliased group id ('[_1]') found in [_2].", $subjectGid, [$SUBJECT_GROUP_FILE, $terminalEncoding]);
          if ( $runByUser == 0 )
          {
            generateAliasedGroupEmail($subjectGroup, $subjectGid, $subjectLine, $aliasedTargetEntry, $SUBJECT_GROUP_FILE);
          }
        }
        $mustSynchronise = 1;
      }
    }
  }

  return $mustSynchronise;
}


sub checkIsHealthy
{
  return system("/etc/init.d/$PACKAGENAME qstatus 1>/dev/null 2>/dev/null");
}

sub checkRcmonitor
{
 if ( system("/etc/init.d/$PACKAGENAME-rcmonitor status 1>/dev/null 2>/dev/null") != 0 )
 {
   system("/etc/init.d/$PACKAGENAME-rcmonitor start 1>/dev/null 2>/dev/null");
 }
}

my $cmdlineArg;
my $CHECK_OR_SYNC = "";

# Simple argument parser.
while ( $cmdlineArg = shift @ARGV )
{
  if ( $cmdlineArg eq "-d" )
  {
    $runByUser = 0;
  }
  else
  {
    $CHECK_OR_SYNC = $cmdlineArg;
  }
}

my $isRunning = checkIsHealthy();
if ( $isRunning != 0 )
{
  if ( $runByUser == 1 )
  {
    fatalError(4, "The $PACKAGENAME-daemon is not running. Please start the $PACKAGENAME-daemon.");
  }
  else
  {
    exit(0);
  }
}

# if no argument specified, pick up config from file
if ( $CHECK_OR_SYNC eq "" )
{
  $CHECK_OR_SYNC = getConfig("WORLD_CHECK_OR_SYNC", "check_all");
}

# Make sure we are running in a visible directory otherwise
# the translator will not start from an invalid directory
chdir("$POWERVM_LX86_X86WORLD");

# Make sure that all temporary files we create are safe and
# not writeable by anyone apart from us
umask(0077);

# Lets check that the rcmonitor has not died

checkRcmonitor();

if ( $CHECK_OR_SYNC eq "sync_all" )
{
  checkPrivileges();
  syncMtabFile();
  checkPasswdFile();
  checkGroupFile();
  exit(0);
}
elsif ( $CHECK_OR_SYNC eq "check_all" )
{
  checkPasswdFile();
  checkGroupFile();
  exit(0);
}
elsif ( $CHECK_OR_SYNC eq "check_passwd" )
{
  checkPasswdFile();
  exit(0);
}
elsif ( $CHECK_OR_SYNC eq "check_group" )
{
  checkGroupFile();
  exit(0);
}
elsif ( $CHECK_OR_SYNC eq "force_sync_mtab" )
{
  checkPrivileges();
  syncMtabFile();
  exit(0);
}
elsif ( $CHECK_OR_SYNC eq "none" )
{
  # do nothing
  exit(0);
}
else
{
  fatalError(5, "Unrecognised option 'WORLD_CHECK_OR_SYNC=[_1]'.", $CHECK_OR_SYNC);
}

