#
#
# osds_acfsrVerifyConfig.pm
#
# Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
#
#    NAME
#      osds_acfsrVerifyConfig.pm
#
#    DESCRIPTION
#       - Test that all expected /dev/acfsr devices exist
#         and remove any that should not be there.
#       - Examine 'targetcli ls' output and look for errors:
#            - test for valid ACLs
#            - test for valid LUNs
#            - test for valid portals
#
#   DEBUGGING
#      I found it easiest to test for syntax errors by running 'acfsremote'
#      without any arguments. Then to test functionality by creating one or
#      more exports and examining the $log_path file. Remember that there will
#      be a time delay. You may want to uncomment some logEvent(DEBUG: <msg>)
#      calls or add some new ones.
#
#
use strict;
use Fcntl ':mode';

# From S_IXUSR stat.h
my $S_IXUSR = 00100;

# log output indent number of spaces
my $indent = 0;

package osds_acfsrVerifyConfig;
our @ISA = qw(Exporter);
our @EXPORT = qw(osds_acfsrVerifyConfig);

use acfslib;
use osds_acfslib;
use osds_unix_linux_acfslib;
use osds_acfsremote;

sub osds_acfsrVerifyConfig
{
  my $targetcliCmd     = "/bin/targetcli";
  my $devAcfsr         = "/dev/acfsr";
  my $scanBlockDevs    = 0;  # scanning block devices in 'targetcli ls' output
  my $scanIscsiDevs    = 0;  # scanning iSCSI devices in 'targetcli ls' output
  my $numBdevsFound    = 0;  # num blk devs found in 'targetcli ls'
  my $numBdevsExpected = 0;  # num blk devs expected == total num exports
  my (@allAcfsrDevs);        # list of devs in /dev/acfsr
  my (@validAcfsrDevs);      # devs that passed validation
  my (@invalidAcfsrDevs);    # devs that failed validation
  my %validHash;             # a hash of @validAcfsrDevs for fast lookup
  my $fileMode;              # mode bits from stat used to validate cmd access
  my $rc = 0;                # internal command return code
  my $index;
  my $output;
  my $dsf;
  my $fh;

  $indent = 0;
  if ($ENV{USM_NO_TARGETCLI_CHECK})
  {
    logEvent("Testing /dev/acfsr and iSCSI configuration disabled", $indent);
    return;
  }
  else
  {
    logEvent("Testing /dev/acfsr and iSCSI configuration", $indent);
  }
  $indent = 2;

  # Detect all block devices in /dev/acfsr.
  (@allAcfsrDevs) = `find /dev/acfsr -maxdepth 1 -type b`;
  if ($? != 0)
  {
    logEvent("Unable to access $devAcfsr", $indent);
    # Continue so we can see what 'targetcli ls' output looks like.
  }
  else
  {
    my $acfsrDevCnt = @allAcfsrDevs;
    if ($acfsrDevCnt == 0)
    {
      logEvent("no $devAcfsr devices found", $indent);
      # Continue so we can see what 'targetcli ls' output looks like.
    }
  }

  # Make sure that targetcli exists and is executable
  (undef, undef, $fileMode) = stat $targetcliCmd or $rc = 1;
  if ($rc == 1)
  {
    logEvent("$targetcliCmd does not exist", $indent);
    return;
  }
  else
  {
    if (!($fileMode & ~$S_IXUSR))
    {
      logEvent("$targetcliCmd not user executable", $indent);
      return;
    }
  }

  # Make sure that advmutil exists and is executable
  (undef, undef, $fileMode) = stat $ADVMUTIL or $rc = 1;
  if ($rc == 1)
  {
    logEvent("$ADVMUTIL does not exist", $indent);
    return;
  }
  else
  {
    if (!($fileMode & ~$S_IXUSR))
    {
      logEvent("$ADVMUTIL not user executable", $indent);
      return;
    }
  }

  # Initialize the allAcfsrDevs array.
  # We will later remove devices found in 'targetcli ls' from @allAcfsrDevs.
  # At the end, if there are still elements in @allAcfsrDevsr, we have
  # a stale device that shouldn't be there.
  for ($index = 0; $allAcfsrDevs[$index]; $index++)
  {
    chomp $allAcfsrDevs[$index];
    # logEvent("DEBUG: found $allAcfsrDevs[$index] in $acfsrDev", $indent);
  }

  # DEBUG - dump 'targetcli ls' output
  # open $fh, "$targetcliCmd ls |";
  # open TLS, ">>/tmp/targetcliOut";
  # while(<$fh>)
  # {
  #   print TLS $_;
  # }
  # close $fh;
  # close TLS;

  $rc = 0;
  open $fh, "$targetcliCmd ls |" or $rc = 1;
  if ($rc == 1)
  {
    # no point in continuing.
    logEvent("'$targetcliCmd ls' failed", $indent);
    return;
  }

  while(<$fh>)
  {
    if ($_ =~ /o- block/)
    {
      $scanBlockDevs = 1;
      $scanIscsiDevs = 0;
    }
    elsif ($_ =~ /o- iscsi/)
    {
      $scanBlockDevs = 0;
      $scanIscsiDevs = 1;
    }

    ### test for valid /dev/acfsr block devices
    if ($scanBlockDevs)
    {
      if ($_ =~ /o- acfsr-/)
      {
        my $rdev = 0;

        (undef, $dsf) = split /\[/, $_;
        ($dsf, undef) = split / /, $dsf;

        # Wait for the device to appear, if needed (it should already be there).
        for ($index = 0; $index < 30 && !$rdev; $index++)
        {
          $rdev = (stat($dsf))[6];
          sleep(1) if (!$rdev);
        }

        if ($rc == 0)
        {
          $numBdevsFound++;
          logEvent("Expected device $dsf found", $indent);
          # Remove $dsf from @allAcfsrDevs
          for ($index = 0;  $allAcfsrDevs[$index]; $index++)
          {
            if ($dsf =~ $allAcfsrDevs[$index])
            {
              # We found a device that was expected to be there.
              # Remove it from the allAcfsrDevs array. Anything left over at
              # the end means that there's something unexpected in /dev/acfsr.
              splice(@allAcfsrDevs, $index, 1);
            }
          }    # for
        }    # rc
        else
        {
          # We have a device in /dev/acfsr that is not in 'targetcli ls'.
          #
          # We can't call teardown in the current thread because of lock
          # contention (mtx_ASLED) in the driver leading to a deadlock.
          my $pid = fork();
          if (not $pid)
          {
            logEvent("$ADVMUTIL export teardown -o dsf $dsf", $indent);
            $output = `$ADVMUTIL export teardown -o dsf $dsf`;
            if ($? == 0)
            {
              logEvent("teardown of $dsf' success", $indent);
            }
            else
            {
              logEvent("export teardown of $dsf' failed", $indent);
              logEvent($output, $indent);
            }
            return;
          }
        }
      }      # o- acfsr-
    }      # scanBlockDevs

    if ($scanIscsiDevs)
    {
      my $line = $_;
      my $validDsf;

      next if (!(($line =~ /TPG/) && ($line =~ /acfs/)));

      while ($line)
      {
        if (($line =~ /TPG/) && ($line =~ /acfs/))
        {
          my $seq;

          # "01" == iSCSI.
          ($dsf, $seq) = getDsfFromTpg($line, "01");
          ($line, $validDsf) = scanAcfsIscsi($line, $fh);
          if ($validDsf)
          {
            push @validAcfsrDevs, $dsf;
          }
          else
          {
            push @invalidAcfsrDevs, $dsf;
          }
        }
      }
    }
  }

  close $fh;

  if ($numBdevsFound)   # Number of /dev/acfsr entries shown in 'targetcli ls'.
  {
    my $rc = 0;
    open EX, "$ADVMUTIL export list -n -o exportName |" or $rc = 1;

    if ($rc)
    {
      logEvent("advmutil export list error", $indent);
    }
    while (<EX>)
    {
      $numBdevsExpected++;
    }
    close EX if ($rc == 0);
    # logEvent("DEBUG: '$ADVMUTIL export list -n -o exportName':", $indent);
    # logEvent("DEBUG: \tshows $numBdevsExpected exports", $indent);
    # logEvent("DEBUG: numBdevsExpected = $numBdevsExpected", $indent);
  }

  if ($numBdevsFound > $numBdevsExpected)
  {
    # There are more /dev/acfsr targets in 'targetcli ls' than
    # 'advmutil export list' expected.
    logEvent(
         "$devAcfsr devices found/expected: $numBdevsFound/$numBdevsExpected",
         $indent);
  }

  # We may have partially valid devices - such as an export with multiple
  # transports - in which case we don't want to tear them down.
  # For example, /dev/acfsr/foo may be valid for sequence
  # number 0 but invalid for sequence number 1.
  # If the dsf is in both the valid and the invalid arrays, we leave it alone.
  @validHash{@validAcfsrDevs}=();
  foreach $dsf (@invalidAcfsrDevs)
  {
    if (exists $validHash{$dsf})
    {
      logEvent("'$dsf' exists in both valid & invalid arrays", $indent);
    }
    else
    {
      # The device is only in the invalid array.
      logEvent("invalid device $dsf found - tearing down export", $indent);

      # We can't call teardown in the current thread because of lock contention
      # (mtx_ASLED) in the driver leading to a deadlock.
      my $pid = fork();
      if (not $pid)
      {
        logEvent("$ADVMUTIL export teardown -o dsf $dsf", $indent);
        $output = `$ADVMUTIL export teardown -o dsf $dsf`;
        if ($? == 0)
        {
          logEvent("teardown of $dsf' success", $indent);
        }
        else
        {
          logEvent("export teardown of $dsf' failed", $indent);
          logEvent($output, $indent);
        }
        return;
      }
    }
  }

  foreach $dsf (@allAcfsrDevs)
  {
    logEvent("Note: '$dsf' exists but is not found in the iSCSI configuration",
             $indent);
  }

  return;
}

# Get the dsf from the 'targetcli ls' 'TPGs' line.
sub getDsfFromTpg
{
  my ($line, $tt) = @_;
  my $acfsr;
  my $mc;
  my $seq;
  my $vol;

  (undef, $acfsr, $mc, $seq, $vol) = split /:/, $line;
  (undef, $vol) = split /vol-/, $vol;
  ($vol, undef) = split / /, $vol;
  return ("/dev/$acfsr/$mc-$tt-$vol", $seq);
}

# From /dev/acfsr/xxxxxx-yy-xxx, get the individual components.
sub getAcfsrComponents
{
  my ($dsf) = @_;

  (undef, $dsf) = split /\/acfsr\//, $dsf;
  return (my $mc, my $tt, my $vol) = split /-/, $dsf;
}

# We have an ACFSR device from the 'targetcli ls' 'TPGs' line.
# See that it has a valid ACL, LUN, and portal.
sub scanAcfsIscsi
{
  my ($tpgsLine, $fh) = @_;      # current line (TPGs) in targetcli ls / handle
  my $haveTpg        = 0;        # set if we have a valid TPG
  my $haveAcl        = 0;        # set if we have a valid ACL
  my $haveLun        = 0;        # set if we have a valid LUN
  my $havePortal     = 0;        # set if we have a valid portal
  my $returnLine     = 0;        # func return value
  my $validDsf       = 1;        # func return value
  my $mc;                        # Member Cluster ID
  my $seq;                       # sequence number
  my $vol;                       # volume number
  my $dsf;                       # device special file

  # "01" == iSCSI.
  ($dsf, $seq) = getDsfFromTpg($tpgsLine, "01");

  ($mc, undef, $vol) = getAcfsrComponents($dsf);
  if (($_ =~ /$mc/) && ($_ =~ /$seq/) && ($_ =~ /-$vol/))
  {
    $haveTpg = 1;
  }

  # Test the validity of this dsf
  while(<$fh>)
  {
    # Do we have an ACL? TBD: there could be more than 1
    if (($_ =~ /o- mapped_lun/) && ($_ =~ /$mc/) && ($_ =~ /-$vol/))
    {
      $haveAcl = 1;
    }

    # Do we have a lun? TBD: there could be more than 1
    if (($_ =~ /o- lun/) && ($_ =~ /$mc/) && ($_ =~ /-$vol/) && ($_ =~ /acfsr/))
    {
      $haveLun = 1;
    }


    # Do we have a portal? TBD: there could be more than 1
    if ($_ =~ /o- portals/)
    {
      # The portal is on the next line
      $havePortal = 1;
      next;
    }
    if ($havePortal == 1)
    {
      # I don't know what an IPv6 address looks like in 'targetcli ls'
      # making a best guess for now. Assuming [xxxxx:yyyyy:zzzzz]:<port>.
      my (undef, $ipAddr) = split /o- /, $_;
      ($ipAddr, undef) = split / /, $ipAddr;
      # DEBUG to force teardown: $ipAddr = "1.2.foo.3";
      my $testAddr = $ipAddr;
      my $cnt = split /:/, $testAddr;
      my $validIP = 0;

      if ($cnt == 2)
      {
        # one colon so IPv4
        ($ipAddr, undef) = split /\:/, $testAddr;
        $validIP = 1;
      }

      # More than one colon means IPv6 but it may have a port also (?????)
      if ($cnt > 2)
      {
        # Remove the port number
        (undef, $testAddr) = split /\[/, $ipAddr;
        ($ipAddr, undef) = split /\]/, $testAddr;
      }

      if ($validIP)
      {
        my ($name) = gethostbyname $ipAddr;
        if ($? == 0)
        {
          $havePortal = 2;
        }
        else
        {
          logEvent("invalid IP address format '$ipAddr'", $indent);
        }
      }
    }

    # We're starting on the next ACFS device
    if (($_ =~ /TPG/) && ($_ =~ /acfs/))
    {
      # On next call to scanAcfsIscsi(), resume with the current line.
      $returnLine = $_;
      last;
    }
  }

  # See if we have a valid /dev/acfsr entry.
  if (!$haveTpg)
  {
    logEvent("missing TPG for $dsf seqNum $seq", $indent);
    $validDsf = 0;
  }
  if (!$haveLun)
  {
    logEvent("missing lun for $dsf seqNum $seq", $indent);
    $validDsf = 0;
  }
  if (!$haveAcl)
  {
    logEvent("missing ACL for $dsf seqNum $seq", $indent);
    $validDsf = 0;
  }
  if ($havePortal != 2)
  {
    logEvent("missing or invalid portal for $dsf seqNum $seq", $indent);
    $validDsf = 0;
  }
  else
  {
     logEvent("Valid dsf $dsf sequence $seq found", $indent);
  }

  # If this dsf *sequence* is invalid, we delete it from the iSCSI
  # configuration since it is unusable.
  # An 'advmutil mapping -c <clusterName> -r' can then
  # retry adding the components.
  if (!$validDsf)
  {
    my $target;
    my $cmd;

    # Build the target for the "/iscsi/ delete" targetcli command.
    # The $target format is:
    #     iqn.2015-12.com.oracle:\acfsr:\000001:\0:\vol-000
    (undef, $target) = split /o- /, $tpgsLine;
    ($target, undef) = split / \.\.\./, $target;
    $target =~ s/:/:\\/g;

    $cmd = "/bin/targetcli /iscsi/ delete $target";
    `$cmd`;
    if ($? == 0)
    {
      logEvent("deleted target $target attributes", $indent);
    }
    else
    {
      logEvent("failed to delete target $target attributes", $indent);
    }
  }

  return ($returnLine, $validDsf);
}

sub logEvent
{
  my ($msg, $indent) = @_;
  my $i;

  open LOG, ">> $osds_acfsremote::log_path";

  for ($i = 0; $i < $indent; $i++)
  {
    print LOG " ";
  }
  print LOG "$msg\n";
  close LOG;
}

1;
