When a LUN goes offline, bad things tend to happen on the host using it. It is good to know sooner rather than later if that happens. I was looking for a way to do this and what made the most sense was using PowerShell and PRTG.

Requirements

  • PRTG installation (you can get it free for up to 100 sensors!)
    • While you don't NEED PRTG that is how this script reports back. You'll need to change what the actions are for offline LUNs if you want to do other things.
  • ONTap PowerShell toolkit
    • This will need to be installed where you are using the script.
  • A user account on the NetApp(s) that the script can leverage.

Setup

You'll need to setup:

  • The module full path for the ONTap toolkit (if different).
  • The password setup section that is commented out.
  • The username ($user) you are going to use to connect to the NetApp(s).
  • The array ($netAppControllers) of your controllers. For 7-mode simply include the controller name, for CDOT add -c after the name so the script knows which is which.
  • The $exclude variable file path. (It is best to set this up even if it is a blank text file).
[cmdletbinding()]
param(
    [string]
    $Controller,
    [string]
    $action
)
#Import the OnTap module manually using the following path.
Import-Module 'C:\Program Files (x86)\NetApp\NetApp PowerShell Toolkit\Modules\DataONTAP\dataontap.psd1'

#Encrypted password hash used to get the password securely.
$File = "D:\PRTG Network Monitor\Custom Sensors\EXEXML\lunpass.txt"

#Setup password (Do this one time or whenever you change the password!:
#[Byte[]] $key = (1..16)
#$Password = "P@ssword1" | ConvertTo-SecureString -AsPlainText -Force
#$Password | ConvertFrom-SecureString -key $key | Out-File $File

#User account used to get NetApp information.
$user                     = 'your NetApp PowerShell user account'

#Key used to decrypt password
[Byte[]] $key = (1..16)

#Import the password from the file using the key.
$pass = Get-Content $File | ConvertTo-SecureString -Key $key

#Create the credential used to connect to each NetApp controller
$netAppCredential         = New-Object System.Management.Automation.PSCredential($user,$Pass)

#Set the variable which contains all of your controllers (add -c for CDOT)
$global:NetAppControllers = @('netapp1','netapp2','netapp3-c')

#LUNs to exclude
$exclude = Get-Content 'D:\PRTG Network Monitor\Custom Sensors\EXEXML\exclude.txt'

#This sets the variable of $stopTxt to null (which is set later if there is a failed LUN)
$stopTxt = $null

The functions and switch

#This function connects to the 7-Mode controllers the way they need to be connected to, as well as C-DOT.
function Connect-NetAppController {

    
    if (($foundController).Split("-")[1] -like "*c*") { 

        $nController = $foundController.Split("-")[0]
        

        
        if (Test-Connection $nController -Count 1 -Quiet) {

       
        Connect-NcController $nController -Credential $netAppCredential | Out-Null

    
        } Else {
            
           #Write-Host `n"Unable to ping: " $nController
            
        }

    } else {

        $7Controller = $foundController

        if (Test-Connection $foundController -Count 1 -Quiet) {
        
        $7Controller = $foundController

        Connect-NaController $foundController -Credential $netAppCredential | Out-Null

       
         } Else {
            
            #Write-Host `n"Unable to ping: " $7Controller
            
        }
    } 
}

#This functions takes the object created in the switch and output the information properly for PRTG to interpret
Function Display-PRTGText {
[cmdletbinding()]
param([array]
      $PRTGObject
)   
    
    $header = '<prtg>'
    $footer = '</prtg>'

    Write-Output $header
    
    foreach ($lun in $PRTGObject) {

    if ($lun.NameOff) {
        
        $stopTxt    = $true
        $offlineLun += $lun.NameOff 
        
    }    

    if ($lun.Exclusions) {
        
        $exclude    = $true
        $excludeTxt = $lun.Exclusions
        
    }
    
    Write-Output "
    
    <result>
    <channel>Total LUNs ($($lun.Name))</channel>
    <value>$($lun.TotalLuns)</value>
    </result>
    
    <result>
    <channel>Offline LUNs ($($lun.Name))</channel>
    <value>$($lun.OfflineLuns)</value>
    </result>
    
    "   
    }

    if ($stopTxt) {
        
        Write-Output "
        
        <text>Offline LUNs: $offlineLun</text>
        
        "
    } elseif (!$stopTxt -and $exclude) {
        
        Write-Output "
        
        <text>Exclusions: $excludeTxt</text>
        
        "
    }

    Write-Output $footer    
    
}


#This switch takes action based on what you specify in the parameter. Right now there is only one: luncheck
Switch ($action) {
    
    'luncheck' {
          
        if ($controller -eq 'all') { 
    
            [array]$cModeControllers = $NetAppControllers
     
        } else {
            
            [array]$cmodecontrollers = $controller

        }
        
        #Set/Clear this array to be used at the NetApp Object
        [array]$netAppObject = $null
        
        foreach ($array in $cModeControllers) {
            $foundcontroller = $array
            $controller      = $array   
            $numLuns         = 0
            $numOn           = 0
            $numOff          = 0
            $offline         = $null
            $online          = $null
            $luns            = $null
            $omit            = $null
            $newLuns         = $null
            $text            = $null
            
            #if it is a CDOT controllers (I add -c in the name to segement them out)
            if (($foundController).Split("-")[1] -like "*c*") {

                Connect-NetAppController 
                
                $luns    = Get-NCLun | Where-Object{$_.Path -notlike "*test*"}

                if ($exclude.count -gt 0) {

                    $omit = $exclude | ForEach-Object{$no = $_;$luns | Where-Object{$_.path -like $no}}
                    $luns = $luns | Where-Object{$_.Path -notin $omit.Path}
                
                }
                
                $numLuns = $luns.count
                $offline = $luns | Where-Object{$_.Online -ne $true}
                $online  = $luns | Where-Object{$_.Online -eq $true}
                $numOff  = $offline.count
                $numOn   = $online.count       
                
                $netAppProp     = @{Name=$controller}
                $netAppProp     += @{TotalLuns=$numLuns}
                $netAppProp     += @{OfflineLuns=$numOff}
                $netAppProp     += @{OnlineLuns=$numOn}
                $netAppProp     += @{Exclusions=$exclude}
                
                if ($numOff -gt 0) {
                    
                    foreach ($off in $offline) {[array]$text += 'Lun: ' + $off.path + ' VServer: ' + $off.vserver}                
                    $netAppProp += @{NameOff=$text}
                    
                }

                $naO             = New-Object PSObject -Property $netAppProp
                $netAppObject   += $naO       
                                      
            #Else they are 7-mode so use 7-mode commands            
            } else {

                Connect-NetAppController 
                
                $luns    = Get-NALun | Where-Object{$_.Path -notlike "*test*"}  
                
                if ($exclude.count -gt 0) {
                    
                    $omit = $exclude | ForEach-Object{$no = $_;$luns | Where-Object{$_.path -like $no}}
                    $luns = $luns | Where-Object{$_.Path -notin $omit.Path}
                
                }
                
                $numLuns = $luns.count
                $offline = $luns | Where-Object{$_.Online -ne $true}
                $online  = $luns | Where-Object{$_.Online -eq $true}
                $numOff  = $offline.count
                $numOn   = $online.count
                
                $netAppProp     = @{Name=$controller}
                $netAppProp     += @{TotalLuns=$numLuns}
                $netAppProp     += @{OfflineLuns=$numOff}
                $netAppProp     += @{OnlineLuns=$numOn} 
                $netAppProp     += @{Exclusions=$exclude}
                              
                if ($numOff -gt 0) {
                    
                    foreach ($off in $offline) {[array]$text += 'Lun: ' + $off.path}           
                    $netAppProp += @{NameOff=$text}
                    
                }
                
                $naO             = New-Object PSObject -Property $netAppProp
                $netAppObject   += $naO    

            }      
        
        }
   
        Display-PRTGText $netAppObject
    
    }
}

The code lets you run the script specifying "all" controllers (which will use the array defined at the beginning of the script), or a controller of your choice.

The function Display-PRTGText takes the $netAppObject object that is built below in the switch option luncheck. It first displays the PRTG header, the LUN information, and finally the PRTG footer. 

The switch looks for the action you'd like to take. Right now there is only one, "luncheck". You can add more for different sensor types to check different things based on what you pass to -action.

Examples

To test the script, run it as follows:

.\Lun-Check.ps1 -Controller yourcontroller-c -action luncheck

The above code will execute and if all goes well it will return results with opening and closing <prtg> tags.

If there are any offline, they will be returned into the PRTG sensor via the <text> tag. 

Exclusions

You can exclude certain LUNs from being checked. If you have decommissioned a server or are testing anything that involves setting a LUN's state to be offline this can be handy. Add entries to exclude.txt line by line. You can include either the full LUN path, or anything in the path name with wildcards around it.  Please note that there is also a line that omits any LUN with "test" in the name. To change this, remove that if line.

Examples:

  • /vol/finance1/lun1
  • *finance*

The first example will omit just that one LUN, whereas the second example will omit any LUN with finance in the path. Any exclusions you do add will be noted in the sensor's message as text if no LUNs are currently offline.

Once you have verified you received the results from the script as needed, we can now perform the sensor setup in PRTG.

PRTG Setup

You'll want to perform the following steps in PRTG:

  1. Ensure you have the script in your EXEXML custom sensor folder. Example: "D:\PRTG Network Monitor\Custom Sensors\EXEXML\lun-check.ps1"
  2. Create an "EXE/Script Advanced" sensor on a device of your choice.
  3. Select the lun-check.ps1 script in the drop down.
  4. Set the following parameters: -controller all -action luncheck
  5. Modify the timeout of the script to 180 seconds.

If you do not want to use the array with all the NetApp(s) change 'all' to the name of a controller. If it is a CDOT controller, be sure to add -c to the name.

You should now see channels for total and offline LUNs.

I have it setup to show the sensor as down if there are any LUNs offline. To do this, be sure to change the channel limits accordingly. It will return the path of the LUN and the VServer it is on (CDOT) or just the path for 7-Mode. This is displayed in the sensor's message as text.

 

And finally, you'll want to tweak the notification settings of the sensor if for some reason you want it different than the parent's settings.

Full Code

[cmdletbinding()]
param(
    [string]
    $Controller,
    [string]
    $action
)
#Import the OnTap module manually using the following path.
Import-Module 'C:\Program Files (x86)\NetApp\NetApp PowerShell Toolkit\Modules\DataONTAP\dataontap.psd1'

#Encrypted password hash used to get the password securely.
$File = "D:\PRTG Network Monitor\Custom Sensors\EXEXML\lunpass.txt"

#Setup password (Do this one time or whenever you change the password!:
#[Byte[]] $key = (1..16)
#$Password = "P@ssword1" | ConvertTo-SecureString -AsPlainText -Force
#$Password | ConvertFrom-SecureString -key $key | Out-File $File

#User account used to get NetApp information.
$user                     = 'your NetApp PowerShell user account'

#Key used to decrypt password
[Byte[]] $key = (1..16)

#Import the password from the file using the key.
$pass = Get-Content $File | ConvertTo-SecureString -Key $key

#Create the credential used to connect to each NetApp controller
$netAppCredential         = New-Object System.Management.Automation.PSCredential($user,$Pass)

#Set the variable which contains all of your controllers (add -c for CDOT)
$global:NetAppControllers = @('netapp1','netapp2','netapp3-c')

#LUNs to exclude
$exclude = Get-Content 'D:\PRTG Network Monitor\Custom Sensors\EXEXML\exclude.txt'

#This sets the variable of $stopTxt to null (which is set later if there is a failed LUN)
$stopTxt = $null

#This function connects to the 7-Mode controllers the way they need to be connected to, as well as C-DOT.
function Connect-NetAppController {

    
    if (($foundController).Split("-")[1] -like "*c*") { 

        $nController = $foundController.Split("-")[0]
        

        
        if (Test-Connection $nController -Count 1 -Quiet) {

       
        Connect-NcController $nController -Credential $netAppCredential | Out-Null

    
        } Else {
            
           #Write-Host `n"Unable to ping: " $nController
            
        }

    } else {

        $7Controller = $foundController

        if (Test-Connection $foundController -Count 1 -Quiet) {
        
        $7Controller = $foundController

        Connect-NaController $foundController -Credential $netAppCredential | Out-Null

       
         } Else {
            
            #Write-Host `n"Unable to ping: " $7Controller
            
        }
    } 
}

#This functions takes the object created in the switch and output the information properly for PRTG to interpret
Function Display-PRTGText {
[cmdletbinding()]
param([array]
      $PRTGObject
)   
    
    $header = '<prtg>'
    $footer = '</prtg>'

    Write-Output $header
    
    foreach ($lun in $PRTGObject) {

    if ($lun.NameOff) {
        
        $stopTxt    = $true
        $offlineLun += $lun.NameOff 
        
    }    

    if ($lun.Exclusions) {
        
        $exclude    = $true
        $excludeTxt = $lun.Exclusions
        
    }
    
    Write-Output "
    
    <result>
    <channel>Total LUNs ($($lun.Name))</channel>
    <value>$($lun.TotalLuns)</value>
    </result>
    
    <result>
    <channel>Offline LUNs ($($lun.Name))</channel>
    <value>$($lun.OfflineLuns)</value>
    </result>
    
    "   
    }

    if ($stopTxt) {
        
        Write-Output "
        
        <text>Offline LUNs: $offlineLun</text>
        
        "
    } elseif (!$stopTxt -and $exclude) {
        
        Write-Output "
        
        <text>Exclusions: $excludeTxt</text>
        
        "
    }

    Write-Output $footer    
    
}


#This switch takes action based on what you specify in the parameter. Right now there is only one: luncheck
Switch ($action) {
    
    'luncheck' {
          
        if ($controller -eq 'all') { 
    
            [array]$cModeControllers = $NetAppControllers
     
        } else {
            
            [array]$cmodecontrollers = $controller

        }
        
        #Set/Clear this array to be used at the NetApp Object
        [array]$netAppObject = $null
        
        foreach ($array in $cModeControllers) {
            $foundcontroller = $array
            $controller      = $array   
            $numLuns         = 0
            $numOn           = 0
            $numOff          = 0
            $offline         = $null
            $online          = $null
            $luns            = $null
            $omit            = $null
            $newLuns         = $null
            $text            = $null
            
            #if it is a CDOT controllers (I add -c in the name to segement them out)
            if (($foundController).Split("-")[1] -like "*c*") {

                Connect-NetAppController 
                
                $luns    = Get-NCLun | Where-Object{$_.Path -notlike "*test*"}

                if ($exclude.count -gt 0) {

                    $omit = $exclude | ForEach-Object{$no = $_;$luns | Where-Object{$_.path -like $no}}
                    $luns = $luns | Where-Object{$_.Path -notin $omit.Path}
                
                }
                
                $numLuns = $luns.count
                $offline = $luns | Where-Object{$_.Online -ne $true}
                $online  = $luns | Where-Object{$_.Online -eq $true}
                $numOff  = $offline.count
                $numOn   = $online.count       
                
                $netAppProp     = @{Name=$controller}
                $netAppProp     += @{TotalLuns=$numLuns}
                $netAppProp     += @{OfflineLuns=$numOff}
                $netAppProp     += @{OnlineLuns=$numOn}
                $netAppProp     += @{Exclusions=$exclude}
                
                if ($numOff -gt 0) {
                    
                    foreach ($off in $offline) {[array]$text += 'Lun: ' + $off.path + ' VServer: ' + $off.vserver}                
                    $netAppProp += @{NameOff=$text}
                    
                }

                $naO             = New-Object PSObject -Property $netAppProp
                $netAppObject   += $naO       
                                      
            #Else they are 7-mode so use 7-mode commands            
            } else {

                Connect-NetAppController 
                
                $luns    = Get-NALun | Where-Object{$_.Path -notlike "*test*"}  
                
                if ($exclude.count -gt 0) {
                    
                    $omit = $exclude | ForEach-Object{$no = $_;$luns | Where-Object{$_.path -like $no}}
                    $luns = $luns | Where-Object{$_.Path -notin $omit.Path}
                
                }
                
                $numLuns = $luns.count
                $offline = $luns | Where-Object{$_.Online -ne $true}
                $online  = $luns | Where-Object{$_.Online -eq $true}
                $numOff  = $offline.count
                $numOn   = $online.count
                
                $netAppProp     = @{Name=$controller}
                $netAppProp     += @{TotalLuns=$numLuns}
                $netAppProp     += @{OfflineLuns=$numOff}
                $netAppProp     += @{OnlineLuns=$numOn} 
                $netAppProp     += @{Exclusions=$exclude}
                              
                if ($numOff -gt 0) {
                    
                    foreach ($off in $offline) {[array]$text += 'Lun: ' + $off.path}           
                    $netAppProp += @{NameOff=$text}
                    
                }
                
                $naO             = New-Object PSObject -Property $netAppProp
                $netAppObject   += $naO    

            }      
        
        }
   
        Display-PRTGText $netAppObject
    
    }
}

I wrote this to fill a need rather quickly. For error handling, the PRTG sensor itself does a good job at telling you why and where the script broke. As for variable renaming and cleanup, that is something I am always striving to do.

I hope you found this helpful, and as always feedback is appreciated!

-Ginger Ninja