Tuesday, 11 April 2017

NFS Exports 7 to C - Translating 7-Mode Exports into ClusterShell

I actually thought I’d posted this (NFS_Exports_7toC v1.1), and then looking back realized I hadn’t, just an old version exists (August 2015’s How to Convert 7-Mode NFS Exports into cDOT Export Policies and Rules.) There are maybe a few more 7 to C skirmishes still left to do, so perhaps there might still be some use to be had in this tool.

This is the best version of my NFS Exports 7 to C translator/converter (apologies if the code is a bit clunky). Copy and paste the below script into a text editor and save as NFS_Exports_7toC.ps1. Then simply run in PowerShell as::>


.\NFS_Exports_7toC.ps1 -FilePath {exports_file}


Image: Example of running NFS_Exports_7toC
The Script


#############################
## NFS EXPORTS 7 to C v1.1 ##
#############################

Param(
  [Parameter(Mandatory=$true)][String]$FilePath, # 7-Mode Exports File
  [String]$VserverName = "SVM1", # cDOT: Vserver Name
  [System.Array]$OverrideQtrees # Specify a list as - "vol1.folder1","vol2.folder2",... - where ??? in the path /vol/VOLNAME/??? is not a Qtree
)

[String]$FileName = $FilePath.Split("\")[0] # We use the FileName in some outputs
[String]$Global:LOut = "" # Record lines of Screen Output, which are flushed to SOut on Wr
[System.Array]$Global:SOut = @() # Used to record the ScreenOutput

Function Wr  {      
  Param([String]$DisplayThis,[String]$ForegroundColor)
  If(!$DisplayThis){ Write-Host; $Global:SOut += $Global:LOut; $Global:LOut = ""; RETURN }
  elseif(!$ForegroundColor){ Write-Host $DisplayThis -ForegroundColor White -NoNewLine }
  else { Write-Host $DisplayThis -ForegroundColor $ForegroundColor -NoNewLine }
  $Global:LOut += $DisplayThis
}; Wr

Wr "<<<<< NFS EXPORTS 7 to C v1.1 >>>>>" MAGENTA; Wr; Wr

######################################
## GET AND PROCESS THE EXPORTS FILE ##
######################################

[System.Array]$ExportsFile = Get-Content $FilePath
Wr "Loaded file from: $FilePath" GREEN; Wr

Function Process-Array {
  Param([System.Array]$ArrayToProcess)
  [System.Array]$ProcessedArray = @()
  Foreach ($Line in $ArrayToProcess){          
    ## REMOVE BLANK LINES "" AND LINES BEGINNING WITH "#" ##        
    If( ($Line -ne "") -and ($Line.substring(0,1) -ne "#") ){ $ProcessedArray += $Line }
  }
  , $ProcessedArray # RETURN THE PROCESSED ARRAY
}

$ExportsFile = Process-Array $ExportsFile; Wr

######################################################
## STEP 1: PROCESS 7-MODE EXPORTS FILE LINE BY LINE ##
######################################################

Wr ">>> STEP 1: PLEASE VERIFY THAT THE EXPORTS FILE HAS BEEN READ CORRECTLY! <<<" MAGENTA; Wr; Wr

Function Process-ExportsLine {
 
  Param([String]$Line)
      
  ##### SPLIT ON -actual= OR -sec= TO GET PATH & OPTIONS BITS (VOLNAME,QTREE,SUBQTREE) ##### 
  [System.Array]$SplitOnDashSec    = $Line.Replace("-sec=","#").Split("#")    # Expect (1)PATH_BIT[0]/(2)OPTIONS_BIT[1] {OR}
  [System.Array]$SplitOnDashActual = $Line.Replace("-actual=","#").Split("#") # Expect (1)PATH_BIT[0]/(2)OPTIONS_BIT[1]
 
  If( !( ($SplitOnDashSec.Count -eq 2) -or ($SplitOnDashActual.Count -eq 2) ) ){ RETURN } # NOT A VALID LINE ( .LineIsValid remains FALSE)
 
  If( $SplitOnDashSec.Count -eq 2 ){ ## HAS -sec= ##
    [String]$PathBit    = $SplitOnDashSec[0].Trim(" ","`t")
    [String]$OptionsBit = $SplitOnDashSec[1].Trim(" ","`t")
  } else { ## HAS -actual= ##
    $Exports.$Index.ActualSpecified = $TRUE
    [String]$PathBit    = $SplitOnDashActual[0].Trim(" ","`t")
    [String]$OptionsBit = $SplitOnDashActual[1].Trim(" ","`t")
  }
   
  $SplitOnSlash = $PathBit.Split("/") # Expect (1)[0]/(2)vol[1]/(3)VOLNAME[2]/(4){QTREE}[3]/{SUBQTREE_BIT}
  If($SplitOnSlash.Count -lt 3){ RETURN } # NOT A VALID LINE ( .LineIsValid remains FALSE)
 
  [String]$VolNameFromExport = $SplitOnSlash[2]
  $Exports.$Index.VolName    = $VolNameFromExport
 
  If($SplitOnSlash.Count -ge 4){
    $PossibleQtree = $SplitOnSlash[3].Replace("\"," ") # Qtrees names can have " " in (which is \ in exports)
    If( !($OverrideQtrees -contains ($VolNameFromExport + "." + $PossibleQtree)) ){
      $Exports.$Index.QtreeName = $PossibleQtree
    }
  }
  If($SplitOnSlash.Count -ge 5){
    [String]$PathUpToQtree = ("/vol/" + $Exports.$Index.VolName + "/" + $Exports.$Index.QtreeName + "/")
    $Exports.$Index.SubQtreeBit = $PathBit.Substring($PathUpToQtree.Length).Replace("\"," ") # Folder names can have " " in (which is \ in exports)
  }
 
  ##### HANDLE THE OPTIONS #####
 
  ## -actual= COMES 1ST ##
  If($Exports.$Index.ActualSpecified){
    $Exports.$Index.ActualEquals = $OptionsBit.Replace(",sec=","#").Split("#")[0]
    $OptionsBit                  = $OptionsBit.Replace(",sec=","#").Split("#")[1]
  }
 
  ## -sec= COMES 2ND ##
  $Exports.$Index.SecEquals = $OptionsBit.Split(",")[0]
  $OptionsBit = $OptionsBit.TrimStart($Exports.$Index.SecEquals)
 
  ## THESE REST (all separated by ,) ##
  $OptionsSplitComma = $OptionsBit.Split(",") # Note: We left a comma after the sec= bit
  For( [Int]$i=1; $i -lt ($OptionsSplitComma.Count); $i++ ){
    [String]$CheckThis = [String]($OptionsSplitComma[$i])
    $CheckThis = $CheckThis.Trim(" ","`t","`n")
    If($CheckThis.StartsWith("anon=")){ $Exports.$Index.AnonSpecified   = $TRUE }
    If($CheckThis.StartsWith("anon=")){ $Exports.$Index.AnonEquals      = $CheckThis.Substring(5) }
    If($CheckThis -eq "nosuid")       { $Exports.$Index.NoSuidSpecified = $TRUE }
    If($CheckThis -eq "ro")           { $Exports.$Index.RoSpecified     = $TRUE }
    If($CheckThis.StartsWith("ro="))  { $Exports.$Index.RoEquals        = $CheckThis.Substring(3) }
    If($CheckThis -eq "rw")           { $Exports.$Index.RwSpecified     = $TRUE }
    If($CheckThis.StartsWith("rw="))  { $Exports.$Index.RwEquals        = $CheckThis.Substring(3) }
    If($CheckThis.StartsWith("root=")){ $Exports.$Index.RootEquals      = $CheckThis.Substring(5) }
  }
  $Exports.$Index.LineIsValid = $TRUE # N.B. Could do with adding some validity tests!
}

[Int]$Index = 0 # INITIALIZE THE INDEX AS 0
[System.Object]$Exports = @{} # INITIALIZE THE $Exports OBJECT

Foreach($Line in $ExportsFile){
  $Index++
 
  ## INITIALIZE THE OBJECT TO HOLD THE 7-MODE EXPORT LINE'S DATA ##
  [System.Object]$Exports.$Index                 = @{}    # DATA STRUCTURE TO CONTAIN 7-MODE EXPORTS
         [String]$Exports.$Index.VolName         = ""     # /vol/VOLNAME                     
         [String]$Exports.$Index.QtreeName       = ""     # /vol/VOLNAME/QTREENAME           
         [String]$Exports.$Index.SubQtreeBit     = ""     # /vol/VOLNAME/QTREENAME/SUBQTREEBIT
        [Boolean]$Exports.$Index.ActualSpecified = $FALSE # actual= is specified             
         [String]$Exports.$Index.ActualEquals    = ""     # actual={path}                    
        [Boolean]$Exports.$Index.AnonSpecified   = $FALSE # anon= is specified               
         [String]$Exports.$Index.AnonEquals      = ""     # anon={uid|name}                  
        [Boolean]$Exports.$Index.NoSuidSpecified = $FALSE # nosuid is specified               
        [Boolean]$Exports.$Index.RoSpecified     = $FALSE # ro is specified (with no equals) 
         [String]$Exports.$Index.RoEquals        = ""     # ro={clientid[:clientid...]}      
        [Boolean]$Exports.$Index.RwSpecified     = $FALSE # rw is specified (with no equals) 
         [String]$Exports.$Index.RwEquals        = ""     # rw={clientid[:clientid...]}      
         [String]$Exports.$Index.RootEquals      = ""     # root={clientid[:clientid...]}    
         [String]$Exports.$Index.SecEquals       = ""     # sec={sectype[:sectype...]}
        [Boolean]$Exports.$Index.LineIsValid     = $FALSE # (... check exports line is valid ...)
       [String]$Exports.$Index.OriginalLine    = ""     # (... record the line sent for processing ...)                
  # See: https://library.netapp.com/ecmdocs/ECMP1196979/html/man5/na_exports.5.html
 
  ## GET RID OF ANY COMMENTS AT THE END OF THE LINE AND TRIM SPACES AND TABS ##
  $Exports.$Index.OriginalLine = $Line.Split("#")[0].Trim(" ","`t")
 
  ## INVOKE THE PROCESS-EXPORTS FUNCTION TO PROCESS THE EXPORTS LINE ##
  Process-ExportsLine ($Exports.$Index.OriginalLine)
 
  ## DISPLAY THE PROCESSED LINE (TO MAKE SURE WE'VE READ IT RIGHT!) ##
  Wr "ORIGINAL LINE: "; Wr $Exports.$Index.OriginalLine GREEN; Wr
  If(!$Exports.$Index.LineIsValid){ Wr "NOT A VALID LINE!" RED; Wr}
  else{
                                        Wr "Volume         : "; Wr $Exports.$Index.VolName CYAN; Wr
    If($Exports.$Index.QtreeName){
      If($OverrideQtrees){              Wr "Qtree          : "; Wr $Exports.$Index.QtreeName CYAN; Wr}
      else {                            Wr "Qtree "; Wr "(Assumed)" YELLOW; Wr ": "; Wr $Exports.$Index.QtreeName CYAN; Wr}
    }
    If($Exports.$Index.SubQtreeBit)    {Wr "SUBQTREE BIT   : "; Wr $Exports.$Index.SubQtreeBit CYAN; Wr}
    If($Exports.$Index.ActualSpecified){Wr "actual=        : "; Wr $Exports.$Index.ActualEquals CYAN; Wr}
                                        Wr "sec=           : "; Wr $Exports.$Index.SecEquals CYAN; Wr
    If($Exports.$Index.RoSpecified)    {Wr "RO             : "; Wr "Specified as ro!" YELLOW; Wr}
    If($Exports.$Index.RoEquals)       {Wr "RO HOSTS=      : "; Wr $Exports.$Index.RoEquals CYAN; Wr}
    If($Exports.$Index.RwSpecified)    {Wr "RW             : "; Wr "Specified as rw!" YELLOW; Wr}
    If($Exports.$Index.RwEquals)       {Wr "RW HOSTS=      : "; Wr $Exports.$Index.RwEquals CYAN; Wr}
    If($Exports.$Index.RootEquals)     {Wr "ROOT HOSTS=    : "; Wr $Exports.$Index.RootEquals CYAN; Wr}
    If($Exports.$Index.AnonSpecified)  {Wr "ANON =         : "; Wr $Exports.$Index.AnonEquals YELLOW; Wr}
    If($Exports.$Index.NoSuidSpecified){Wr "NOSUID         : "; Wr "Nosuid is specified." GREEN; Wr}
  };Wr
}

#########################################################################
## STEP 2: PROCESSING THE 7-MODE EXPORTS TO CDOT CLUSTERSHELL COMMANDS ##
#########################################################################

Wr ">>> STEP 2: PROCESSING THE 7-MODE EXPORTS TO CDOT CLUSTERSHELL COMMANDS <<<" MAGENTA; Wr; Wr

## CHECK FOR LINES WE CAN HANDLE ##
## ============================= ##

[System.Array]$VolExportsOnly      = @() # Array for volume exports only - /vol/VOLNAME
[System.Array]$SubVolExportsOnly   = @() # Array for sub-volume exports only - /vol/VOLNAME/SUB-FOLDER
[System.Array]$QtreeExportsOnly    = @() # Array for qtree exports only - /vol/VOLNAME/QTREENAME
[System.Array]$SubQtreeExportsOnly = @() # Array for sub-qtree exports only - /vol/VOLNAME/QTREENAME/SUB-QTREE-FOLDER

[System.Object]$cExports = @{} # Initialize cExports object
[System.Array]$cExports.ExportedVolList = @() # Create an array for the ExportedVolumeList
[System.Object]$cExports.Volume = @{} # Create an object for volumes

## CYCLE THROUGH THE 7-MODE EXPORTS ##
For($i=1; $i -le $Index; $i++){
  ## CHECK FOR VALID EXPORTS LINE ##
  If($Exports.$Index.LineIsValid){
    ## THINGS THIS PROGRAM DOESN'T HANDLE (YET) ##    
    If( $Exports.$i.ActualSpecified ){
      Wr "-actual= IS NOT SUPPORTED IN cDOT (see TR-4067)!" RED; Wr
      Wr "WON'T HANDLE: " RED; Wr $Exports.$i.OriginalLine YELLOW; Wr; Wr
    } elseif( $Exports.$i.SecEquals -ne "sys" ){
      Wr "ONLY HANDLING SEC=SYS EXPORTS!" RED; Wr
      Wr "WON'T HANDLE: " RED; Wr $Exports.$i.OriginalLine YELLOW; Wr; Wr
    } else {
      $VolName   = $Exports.$i.VolName
      $QtreeName = $Exports.$i.QtreeName              
      ## THINGS WE CAN PROCESS ##
      If(    !$QtreeName -and !$Exports.$i.SubQtreeBit){$VolExportsOnly      += $i} # 7M Exports that have NO Qtree NOR a Sub-Qtree bit
      elseif(!$QtreeName -and  $Exports.$i.SubQtreeBit){$SubVolExportsOnly   += $i} # 7M Exports that have NO Qtree BUT a Sub-Qtree (Sub-Volume) bit
      elseif( $QtreeName -and !$Exports.$i.SubQtreeBit){$QtreeExportsOnly    += $i} # 7M Exports that have a Qtree and NO Sub-Qtree bit
      elseif( $QtreeName -and  $Exports.$i.SubQtreeBit){$SubQtreeExportsOnly += $i} # 7M Exports that have a Qtree and a Sub-Qtree bit
      ## WARN ABOUT SUB QTREE/SUB VOLUME EXPORTS ##
      If( $Exports.$i.SubQtreeBit ){
        Wr "WARNING! THE FOLLOWING LINE IS A SUB QTREE/SUB VOLUME (without a QTREE) EXPORT. " YELLOW
        If($QtreeName){Wr "WILL DEFINE RULES IN AN EXPORT POLICY ON THE QTREE." CYAN; Wr}
        Else{Wr "WILL DEFINE RULES IN AN EXPORT POLICY ON THE VOLUME." CYAN; Wr}
        Wr "LINE: " CYAN; Wr $Exports.$i.OriginalLine GREEN; Wr; Wr
      }
      ## POPULATE EXPORTED VOLS LIST ##
      If( !($cExports.ExportedVolList -Contains $VolName) ){
        $cExports.ExportedVolList += $VolName # Accumulate the Exported Vols List
        [System.Object]$cExports.Volume.$VolName = @{}  # Create an object for this volume
        [Boolean]$cExports.Volume.$VolName.HasQtreeExports = $FALSE # Initialize as FALSE for has Qtree Exports
        [System.Array]$cExports.Volume.$VolName.ExportedQtreeList = @() # Create an array for exported qtrees for $VolName
      }
      ## POPULATE EXPORTED QTREES LIST ##
      If($QtreeName){
      If( !($cExports.Volume.$VolName.ExportedQtreeList -Contains $QtreeName) ){
        $cExports.Volume.$VolName.ExportedQtreeList += $QtreeName
        $cExports.Volume.$VolName.HasQtreeExports    = $TRUE
      }
      }                   
    }
  }
}

## INITIALIZE CLUSTERSHELL OUTPUT ##
## ============================== ##

[System.Array]$Output = @()
$Output += ("### 7 TO C EXPORTS TRANSLATIONS for $FileName ###")
$Output += (""),("## VOLUME MOUNT FOR EXPORTED VOLUMES ##"),("")
$cExports.ExportedVolList | Foreach { $Output += ("volume mount -vserver $VserverName -junction-path /vol/$_") } # Mount Volumes
$Output += (""),("## CREATE NO ACCESS AND READONLY EXPORT POLICIES (8.2+) ##"),("")
$Output += ("vserver export-policy create -vserver $VserverName -policyname expol_noaccess") # No Access Export Policy
$Output += ("vserver export-policy create -vserver $VserverName -policyname expol_readonly") # Read Only Export Policy
$Output += ("vserver export-policy rule create -vserver $VserverName -policyname expol_readonly -ruleindex 1 -protocol any -clientmatch 0.0.0.0/0 -rorule any -rwrule never -anon 65534 -superuser none -allow-suid true -allow-dev true")
$Output += (""),("## INITIALLY APPLY READ ONLY TO ALL EXPORTED VOLUMES ##"),("")
$cExports.ExportedVolList | Foreach { $Output += "volume modify -vserver $VserverName -volume $_ -policy expol_readonly" } # Initially apply readonly to all volumes
$Output += (""),("## INITIALLY APPLY NO ACCESS TO QTREES ON EXPORTED VOLUMES ##"),("")
$cExports.ExportedVolList | Foreach { $Output += ("qtree modify -vserver $VserverName -volume $_ -qtree !" + '"' + '"' + " -export-policy expol_noaccess") } # Initially apply no access to all qtrees
$Output += ("")

## FUNCTIONS TO FIX INCORRECT SUBNETS ON 7-MODE EXPORTS ##
## ==================================================== ##

Function Find-OctetBit{
  Param([Int]$Octet,[Int]$SubnetBit)
  $ModuloOctet = $Octet
  $ModuloOctet %=    256/([math]::Pow(2,$SubnetBit))
  $Octet -= $ModuloOctet
  RETURN $Octet
}

Function Return-CorrectSubnet {
  Param([String]$ClientMatch)
  If($ClientMatch -match "/"){
    [System.Array]$Octet = $ClientMatch.Split("/")[0].Split(".")
    If( $Octet.Count -ne 4 ){ RETURN $ClientMatch }
    [Int]$Subnet = $ClientMatch.Split("/")[1]
    If     ($Subnet -ge 24){ $ClientMatch = $Octet[0] + "." + $Octet[1] + "." + $Octet[2] + "." + [String](Find-OctetBit $Octet[3] ($Subnet -24)) + "/" + [String]$Subnet }
    elseif ($Subnet -ge 16){ $ClientMatch = $Octet[0] + "." + $Octet[1] + "." + [String](Find-OctetBit $Octet[2] ($Subnet -16)) + ".0/" + [String]$Subnet }
    elseif ($Subnet -ge 8) { $ClientMatch = $Octet[0] + "." + [String](Find-OctetBit $Octet[1] ($Subnet -8)) + ".0.0/" + [String]$Subnet }
    else                   { $ClientMatch = [String](Find-OctetBit $Octet[0] ($Subnet -0)) + ".0.0.0/" + [String]$Subnet }
  }
  RETURN $ClientMatch
}

## FUNCTION TO PROCESS ROOT HOSTS, RW HOSTS, AND RO HOSTS ##
## ====================================================== ##

[System.Array]$Global:RwRules   = @() # Initialize array for Read/Write Rules (must come before ro rules)
[System.Array]$Global:RoRules   = @() # Initialize array for Read/Only Rules
[System.Array]$Global:RwClients = @() # Initialize array for RW Clients (used to prevent duplicates)
[System.Array]$Global:RoClients = @() # Initialize array for RO Clients (used to prevent duplicates)

Function Process-AllHosts {
  Param([System.Object]$7mExport)
  [System.Array]$RootHosts = ($7mExport.RootEquals).Split(":")
  [System.Array]$RwHosts   = ($7mExport.RwEquals).Split(":")
  [System.Array]$RoHosts   = ($7mExport.RoEquals).Split(":")                            
  If($RwHosts){
    Foreach ($RwHost in $RwHosts){
      $RwHost = Return-CorrectSubnet $RwHost
      If( !($Global:RwClients -Contains $RwHost) ){
        $Global:RwClients += $RwHost
        If($RootHosts -Contains $RwHost){$Global:RwRules += ("$DefaultProtocol -clientmatch $RwHost -rorule sys -rwrule sys $Anon -superuser sys $Suid $DefaultDev")}
        else{$Global:RwRules += ("$DefaultProtocol -clientmatch $RwHost -rorule sys -rwrule sys $Anon -superuser none $Suid $DefaultDev")}
      }
    }
  }
  If($RoHosts){
    Foreach($RoHost in $RoHosts){
      $RoHost = Return-CorrectSubnet $RoHost
      If(!($Global:RoClients -Contains $RoHost)){
        $Global:RoClients += $RoHost
        If($RootHosts -Contains $RoHost){$Global:RoRules += ("$DefaultProtocol -clientmatch $RoHost -rorule sys -rwrule never $Anon -superuser sys $Suid $DefaultDev")}
        else{$Global:RoRules += ("$DefaultProtocol -clientmatch $RoHost -rorule sys -rwrule never $Anon -superuser none $Suid $DefaultDev")}
      }
    }
  }
  If($RootHosts){
    Foreach ($RootHost in $RootHosts){
      $RootHost = Return-CorrectSubnet $RootHost
      If( !($Global:RwClients -Contains $RootHost) ){
        If( !($RwHosts -contains $RootHost) -and !($RoHosts -contains $RootHost) ){
          $Global:RwClients += $RootHost
          $Global:RwRules += ("$DefaultProtocol -clientmatch $RootHost -rorule sys -rwrule sys $Anon -superuser sys $Suid $DefaultDev")
        }
      }
    }
  }
  Return $TRUE
}

## CREATE AND APPLY PER VOLUME EXPORT POLICIES (INCLUDES SUB-VOLUME EXPORTS) ##
## ========================================================================= ##

[String]$DefaultProtocol = "-protocol any"    # Not specified in 7-Mode
[String]$DefaultAnon     = "-anon 65534"      # Same default in 7-Mode (so, if not specified is this)
[String]$DefaultSuid     = "-allow-suid true" # Same default in 7-Mode (so, if not specified is this)
[String]$DefaultDev      = "-allow-dev true"  # Same default in 7-Mode (so, if not specified is this)

$Output += ("## PER VOLUME EXPORT POLICIES (INCLUDES SUB-VOLUME EXPORTS ##"),("")
Foreach($VolName in $cExports.ExportedVolList){ # Cycle through the Exported Volumes List

       [String]$ExPolicy               = ("expol_v_" + $VolName)
       [String]$ExPolRuleStart         = "vserver export-policy rule create -vserver $VserverName -policyname $ExPolicy -ruleindex " # Add rule indexes later
       [Boolean]$HasAVolOrSubVolExport = $FALSE # Initialize "Has a Vol or Sub Vol Export (for $VolName) as FALSE
       $Global:RwRules                 = @()    # Reset R/W Rules
       $Global:RoRules                 = @()    # Reset R/O Rules
       [String]$RwRule                 = ""     # Initialize as NULL general Read/Write rule
       [String]$RoRule                 = ""     # Initialize as NULL general Read/Only rule
       $Global:RwClients               = @()    # Reset R/W Clients
       $Global:RoClients               = @()    # Reset R/O Clients

       ## VOLUME EXPORTS ##
       $VolExportsOnly | Foreach {
              If($Exports.$_.AnonSpecified){   [String]$Anon = ("-anon " + $Exports.$_.AnonEquals) } else { [String]$Anon = $DefaultAnon }
              If($Exports.$_.NoSuidSpecified){ [String]$Suid = ("-allow-suid false")               } else { [String]$Suid = $DefaultSuid }   
              If($Exports.$_.VolName -eq $VolName){
                     $HasAVolOrSubVolExport = Process-AllHosts $Exports.$_ # $VolName has a volume export (Process-AllHosts always returns TRUE)
                     If($Exports.$_.RwSpecified){ $RwRule = ("$DefaultProtocol -clientmatch 0.0.0.0/0 -rorule sys -rwrule sys $Anon -superuser none $Suid $DefaultDev") }
                     If($Exports.$_.RoSpecified){ $RoRule = ("$DefaultProtocol -clientmatch 0.0.0.0/0 -rorule sys -rwrule never $Anon -superuser none $Suid $DefaultDev") }             
              }
       }

       ## SUB VOLUME EXPORTS ##
       $SubVolExportsOnly | Foreach {
              If($Exports.$_.AnonSpecified){   [String]$Anon = ("-anon " + $Exports.$_.AnonEquals) } else { [String]$Anon = $DefaultAnon }
              If($Exports.$_.NoSuidSpecified){ [String]$Suid = ("-allow-suid false")               } else { [String]$Suid = $DefaultSuid }
              If($Exports.$_.VolName -eq $VolName){
                     $HasAVolOrSubVolExport = Process-AllHosts $Exports.$_ # $VolName has a sub-volume export (Process-AllHosts always returns TRUE)
                     If($Exports.$_.RwSpecified -and !$RwRule){ $RwRule = ("$DefaultProtocol -clientmatch 0.0.0.0/0 -rorule sys -rwrule sys $Anon -superuser none $Suid $DefaultDev") }
                     If($Exports.$_.RoSpecified -and !$RoRule){ $RoRule = ("$DefaultProtocol -clientmatch 0.0.0.0/0 -rorule sys -rwrule never $Anon -superuser none $Suid $DefaultDev") }
              }
       }
      
       ## IF THE VOLUME HAS AN EXPORTED QTREE, ADD THE STANDARD READONLY RULE ##
       If( $cExports.Volume.$VolName.HasQtreeExports -and !$RwRule -and !$RoRule){ $RoRule = ("$DefaultProtocol -clientmatch 0.0.0.0/0 -rorule sys -rwrule never $DefaultAnon -superuser none $DefaultSuid $DefaultDev") }

       ## IF WE HAVE A VOLUME OR SUB VOLUME EXPORT, OUTPUT IT ##
       If($HasAVolOrSubVolExport){
              $RuleIndex = 1
              $Output += ("vserver export-policy create -vserver $VserverName -policyname $ExPolicy"   )
              $Global:RwRules | Foreach { $Output += ($ExPolRuleStart + " " + [String]$RuleIndex + " " + $_)     ; $RuleIndex++ }
              If($RwRule){ $Output += ($ExPolRuleStart + " " + [String]$RuleIndex + " " + $RwRule); $RuleIndex++ }
              If(!$RwRule){
                     $Global:RoRules | Foreach {
                           [Boolean]$NotRwClient = $TRUE
                           Foreach ($RwClient in $Global:RwClients){ If($_ -match $RwClient){ $NotRwClient = $FALSE } }
                           If($NotRwClient){ $Output += ($ExPolRuleStart + " " + [String]$RuleIndex + " " + $_); $RuleIndex++ }
                     }
              }
              If(!$RwRule -and $RoRule){ $Output += ($ExPolRuleStart + " " + [String]$RuleIndex + " " + $RoRule); $RuleIndex++ }
              $Output += ("volume modify -vserver $VserverName -volume $VolName -policy $ExPolicy"),("")
       }
}

## CREATE AND APPLY PER QTREE EXPORT POLICIES (INCLUDES SUB-QTREE EXPORTS) ##
## ======================================================================= ##

$Output += ("## PER VOLUME QTREE POLICIES (INCLUDES SUB-QTREE EXPORTS ##"),("")
Foreach($VolName in $cExports.ExportedVolList){ # Cycle through the Exported Volumes List
  Foreach($QtreeName in $cExports.Volume.$VolName.ExportedQtreeList){ # Cycle through the Exported Qtrees for $VolName
    [String]$ExPolicy                   = ("expol_q_" + $VolName + "_" + $QtreeName.Replace(" ","_")) # Qtree Names can have spaces in!
    [String]$ExPolRuleStart             = "vserver export-policy rule create -vserver $VserverName -policyname $ExPolicy -ruleindex " # Add rule indexes later
    [Boolean]$HasAQtreeOrSubQtreeExport = $FALSE # Initialize "Has a Qtree or Sub Qtree Export (for $VolName.$QtreeName) as FALSE
    $Global:RwRules                     = @()    # Reset R/W Rules
    $Global:RoRules                     = @()    # Reset R/O Rules
    [String]$RwRule                     = ""     # Initialize as NULL general Read/Write rule
    [String]$RoRule                     = ""     # Initialize as NULL general Read/Only rule
    $Global:RwClients                   = @()    # Reset R/W Clients
    $Global:RoClients                   = @()    # Reset R/O Clients
   
    ## QTREE EXPORTS ##
    $QtreeExportsOnly | Foreach {
      If($Exports.$_.AnonSpecified){   [String]$Anon = ("-anon " + $Exports.$_.AnonEquals) } else { [String]$Anon = $DefaultAnon }
      If($Exports.$_.NoSuidSpecified){ [String]$Suid = ("-allow-suid false")               } else { [String]$Suid = $DefaultSuid }
      If($Exports.$_.VolName -eq $VolName){
        If($Exports.$_.QtreeName -eq $QtreeName){
          $HasAQtreeOrSubQtreeExport = Process-AllHosts $Exports.$_ # $QtreeName has a Qtree export (Process-AllHosts always returns TRUE)
          If($Exports.$_.RwSpecified){ $RwRule = ("$DefaultProtocol -clientmatch 0.0.0.0/0 -rorule sys -rwrule sys $Anon -superuser none $Suid $DefaultDev") }
          If($Exports.$_.RoSpecified){ $RoRule = ("$DefaultProtocol -clientmatch 0.0.0.0/0 -rorule sys -rwrule never $Anon -superuser none $Suid $DefaultDev") }                  
        }
      }
    }
   
    ## SUB QTREE EXPORTS ##
    $SubQtreeExportsOnly | Foreach {           
      If($Exports.$_.AnonSpecified){   [String]$Anon = ("-anon " + $Exports.$_.AnonEquals) } else { [String]$Anon = $DefaultAnon }
      If($Exports.$_.NoSuidSpecified){ [String]$Suid = ("-allow-suid false")               } else { [String]$Suid = $DefaultSuid }   
      If($Exports.$_.VolName -eq $VolName){
        If($Exports.$_.QtreeName -eq $QtreeName){                   
          $HasAQtreeOrSubQtreeExport = Process-AllHosts $Exports.$_ # $QtreeName has a Sub-Qtree export (Process-AllHosts always returns TRUE)
          If($Exports.$_.RwSpecified -and !$RwRule){ $RwRule = ("$DefaultProtocol -clientmatch 0.0.0.0/0 -rorule sys -rwrule sys $Anon -superuser none $Suid $DefaultDev") }
          If($Exports.$_.RoSpecified -and !$RoRule){ $RoRule = ("$DefaultProtocol -clientmatch 0.0.0.0/0 -rorule sys -rwrule never $Anon -superuser none $Suid $DefaultDev") }
        }
      }                   
    }
   
    ## IF WE HAVE A QTREE OR SUB QTREE EXPORT, OUTPUT IT ##
    If($HasAQtreeOrSubQtreeExport){
      $RuleIndex = 1
      $Output += ("vserver export-policy create -vserver $VserverName -policyname $ExPolicy"    )
      $Global:RwRules | Foreach { $Output += ($ExPolRuleStart + " " + [String]$RuleIndex + " " + $_); $RuleIndex++ }
      If($RwRule){ $Output += ($ExPolRuleStart + " " + [String]$RuleIndex + " " + $RwRule); $RuleIndex++ }
      If(!$RwRule){ $Global:RoRules | Foreach { $Output += ($ExPolRuleStart + " " + [String]$RuleIndex + " " + $_); $RuleIndex++ } }
      If(!$RwRule -and $RoRule){ $Output += ($ExPolRuleStart + " " + [String]$RuleIndex + " " + $RoRule); $RuleIndex++ }
      $Output += ("qtree modify -vserver $VserverName -volume $VolName -qtree " + '"' + $QtreeName + '"' + " -export-policy $ExPolicy"),("")
    }
  }
}

##################################
## CLUSTERSHELL COMMANDS OUTPUT ##
##################################

Wr ">>> STEP 3: CLUSTERSHELL COMMANDS OUTPUT <<<" MAGENTA; Wr; Wr
Wr ("For Clustershell commands see text file: " + ($FileName + "-CONVERTED_TO_CSHELL.txt")); Wr
Wr ("For log of screen output see: " + ($FileName + "-7toC_SCREEN_OUTPUT")); Wr; Wr
$Output | Out-File ($FileName + "-CONVERTED_TO_CSHELL.txt")
$Global:SOut | Out-File ($FileName + "-7toC_SCREEN_OUTPUT.txt")
NotePad ($FileName + "-CONVERTED_TO_CSHELL.txt")