New Question
 
 
PRTG Network Monitor

Intuitive to Use.
Easy to manage.

200.000 administrators have chosen PRTG to monitor their network. Find out how you can reduce cost, increase QoS and ease planning, as well.

Free PRTG
Download >>

 

What is this?

This knowledgebase contains questions and answers about PRTG Network Monitor and network monitoring in general. You are invited to get involved by asking and answering questions!

Learn more

 

Top Tags


View all Tags


Can I Monitor Mounted Windows Volumes with PRTG?

Votes:

0

Your Vote:

Up

Down

I have a server that basically has only volumes mounted from a Windows Server Cluster Shared Volume. I could use the WMI Volume Sensor, but it will fail as soon as I move volumes within the cluster and thus their GUIDs change. While the ID selection would work in other situations, I also have the volumes mounted into subdirectories. Is there any way to monitor this properly with PRTG?

cluster cluster-shared-volume custom-script custom-script-exe powershell prtg volumes windows windows-services wmi-disk

Created on May 26, 2017 7:28:37 AM by  Stephan Linke [Paessler Support]

Last change on Aug 2, 2017 8:13:00 AM by  Martina Wittmann [Paessler Support]



3 Replies

Accepted Answer

Votes:

2

Your Vote:

Up

Down

Monitoring Mounted Windows Volumes with PRTG

Yes, you can monitor mounted Windows Volumes as asked above with PRTG. We wrote a little script for you, just read on and see below.

Scope of the Script

The following PowerShell script allows you to monitor the available size (MB/GB/TB and percentage) of the Windows mounts on the target host. The script can also cope with GUID changes of the actual volume because it works with labels rather than GUIDs.

The sensor also features a Mounts channel that resembles the number of mounts at the time of the creation of the sensor. If one gets unmounted, the sensor will go into a down status. Remember to modify the limit accordingly if the amount of volumes changes!

The installed sensor, monitoring the Windows volumes. The installed sensor, monitoring the Windows volumes. Click here to enlarge.


Requirements

Installation

  1. Save the script to C:\Program Files (x86)\PRTG Network Monitor\Custom Sensors\EXEXML as PRTG-WindowsClusterVolumes.ps1.
  2. Create a new EXE/Script (Advanced) Sensor and configure it as follows:
    1. Program/Script: PRTG-WindowsClusterVolumes.ps1
    2. Parameter: %host
    3. Security Context: Use the Windows credentials of the parent device.

Script

### Copyright (c) 2012-2014, Svendsen Tech
### Author: Joakim Svendsen
### Get-MountPointData v1.2
# ___ ___ _____ ___
#| _ \ _ \_   _/ __|
#|  _/   / | || (_ |
#|_| |_|_\ |_| \___|
# Windows Mounts Sensor

# "Change history": 1.0 -> 1.1 = -Credential, -PromptForCredentials and -NoFormat (the latter allows math operations)
# --- " ---:        1.1 -> 1.2 = -IncludeDiskInfo to show physical disk index number (this was not trivial).
# ---------:        1.1 -> 1.3 = Added PRTG output
# Convert from one device ID format to another.
param($computerName)

$channelTemplateDiskFree = @"
<result>
<channel>{0}</channel>
<value>{1}</value>
<unit>{2}</unit>
{3}
<Float>0</Float>
</result>
"@
$channelTemplatePercent = @"
<result>
<channel>{0}</channel>
<value>{1}</value>
<unit>{2}</unit>
{3}
<Float>1</Float>
<LimitMode>1</LimitMode>
<LimitMinWarning>25</LimitMinWarning>
<LimitMinError>15</LimitMinError>
</result>
"@

$channelTemplateMountedDisks = @"
<result>
<channel>{0}</channel>
<value>{1}</value>
<unit>CustomUnit</unit>
<customunit>Mounts</customunit>
<Float>0</Float>
<LimitMode>1</LimitMode>
<LimitMinError>{2}</LimitMinError>
</result>
"@

#<VolumeSize>GigaByte</VolumeSize>

function Get-DeviceIDFromMP {
    
    param([Parameter(Mandatory=$true)][string] $VolumeString,
          [Parameter(Mandatory=$true)][string] $Directory)
    
    if ($VolumeString -imatch '^\s*Win32_Volume\.DeviceID="([^"]+)"\s*$') {
        # Return it in the wanted format.
        $Matches[1] -replace '\\{2}', '\'
    }
    else {
        # Return a presumably unique hashtable key if there's no match.
        "Unknown device ID for " + $Directory
    }
    
}

# Thanks to Justin Rich (jrich523) for this C# snippet.
# https://jrich523.wordpress.com/2015/02/27/powershell-getting-the-disk-drive-from-a-volume-or-mount-point/
$STGetDiskClass = @"
using System;
using Microsoft.Win32.SafeHandles;
using System.IO;
using System.Runtime.InteropServices;

public class STGetDisk
{
    private const uint IoctlVolumeGetVolumeDiskExtents = 0x560000;

    [StructLayout(LayoutKind.Sequential)]
    public struct DiskExtent
    {
        public int DiskNumber;
        public Int64 StartingOffset;
        public Int64 ExtentLength;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct DiskExtents
    {
        public int numberOfExtents;
        public DiskExtent first;
    }

    [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    private static extern SafeFileHandle CreateFile(
    string lpFileName,
    [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
    [MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
    IntPtr lpSecurityAttributes,
    [MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
    [MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
    IntPtr hTemplateFile);

    [DllImport("Kernel32.dll", SetLastError = false, CharSet = CharSet.Auto)]
    private static extern bool DeviceIoControl(
    SafeFileHandle hDevice,
    uint IoControlCode,
    [MarshalAs(UnmanagedType.AsAny)] [In] object InBuffer,
    uint nInBufferSize,
    ref DiskExtents OutBuffer,
    int nOutBufferSize,
    ref uint pBytesReturned,
    IntPtr Overlapped
    );

    public static string GetPhysicalDriveString(string path)
    {
        //clean path up
        path = path.TrimEnd('\\');
        if (!path.StartsWith(@"\\.\"))
            path = @"\\.\" + path;

        SafeFileHandle shwnd = CreateFile(path, FileAccess.Read, FileShare.Read | FileShare.Write, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);
        if (shwnd.IsInvalid)
        {
            //Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
            Exception e = Marshal.GetExceptionForHR(Marshal.GetLastWin32Error());
        }

        uint bytesReturned = new uint();
        DiskExtents de1 = new DiskExtents();
        bool result = DeviceIoControl(shwnd, IoctlVolumeGetVolumeDiskExtents, IntPtr.Zero, 0, ref de1, Marshal.SizeOf(de1), ref bytesReturned, IntPtr.Zero);
        shwnd.Close();

        if (result)
            return @"\\.\PhysicalDrive" + de1.first.DiskNumber;
        return null;
    }
}
"@
try {
    Add-Type -TypeDefinition $STGetDiskClass -ErrorAction Stop
}
catch {
    if (-not $Error[0].Exception -like '*The type name * already exists*') {
        Write-Warning -Message "Error adding [STGetDisk] class locally."
    }
}

function Get-MountPointData {
    
    [CmdletBinding(
        DefaultParameterSetName='NoPrompt'
    )]
    param(
        [Parameter(Mandatory=$true)][string[]] $ComputerName,
        [Parameter(ParameterSetName='Prompt')][switch] $PromptForCredentials,
        [Parameter(ParameterSetName='NoPrompt')][System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty,
        [switch] $IncludeRootDrives,
        [switch] $NoFormat,
        [switch] $IncludeDiskInfo
    )
    
    foreach ($Computer in $ComputerName) {
        
        $WmiHash = @{
            ComputerName = $Computer
            ErrorAction  = 'Stop'
        }
        #if ($PromptForCredentials -and $Credential.Username) {
        #    Write-Warning "You specified both -PromptForCredentials and -Credential. Prompting overrides."
        #}
        if ($PSCmdlet.ParameterSetName -eq 'Prompt') {
            $WmiHash.Credential = Get-Credential
        }
        elseif ($Credential.Username) {
            $WmiHash.Credential = $Credential
        }
        try {
            # Collect mount point device IDs and populate a hashtable with IDs as keys
            $MountPointData = @{}
            Get-WmiObject @WmiHash -Class Win32_MountPoint | 
                Where-Object {
                    if ($IncludeRootDrives) {
                        $true
                    }
                    else {
                        $_.Directory -NotMatch '^\s*Win32_Directory\.Name="[a-z]:\\{2}"\s*$'
                    }
                } |
                ForEach-Object {
                    $MountPointData.(Get-DeviceIDFromMP -VolumeString $_.Volume -Directory $_.Directory) = $_.Directory
            }
            $Volumes = @(Get-WmiObject @WmiHash -Class Win32_Volume | Where-Object {
                    if ($IncludeRootDrives) { $true } else { -not $_.DriveLetter }
                } | 
                Select-Object Label, Caption, Capacity, FreeSpace, FileSystem, DeviceID, @{n='Computer';e={$Computer}} )
        }
        catch {
            Write-Error "${Computer}: Terminating WMI error (skipping): $_"
            continue
        }
        if (-not $Volumes.Count) {
            Write-Error "${Computer}: No mount points found. Skipping."
            continue
        }
        if ($PSBoundParameters['IncludeDiskInfo']) {
            $DiskDriveWmiInfo = Get-WmiObject @WmiHash -Class Win32_DiskDrive
        }
        $Volumes | ForEach-Object {
            if ($MountPointData.ContainsKey($_.DeviceID)) {
                # Let's avoid dividing by zero, it's so disruptive.
                if ($_.Capacity) {
                    $PercentFree = $_.FreeSpace*100/$_.Capacity
                }
                else {
                    $PercentFree = 0
                }
                $_ | Select-Object -Property DeviceID, Computer, Label, Caption, FileSystem, @{n='Size (GB)';e={$_.Capacity/1GB}},
                    @{n='Free space';e={$_.FreeSpace}}, @{n='Percent free';e={$PercentFree}}
            }
        } | Sort-Object -Property 'Percent free', @{Descending=$true;e={$_.'Size (GB)'}}, Label, Caption |
            Select-Object -Property $(if ($NoFormat) {
                @{n='ComputerName'; e={$_.Computer}},
                @{n='Label';        e={$_.Label}},
                @{n='Caption';      e={$_.Caption}},
                @{n='FileSystem';   e={$_.FileSystem}},
                @{n='Size (GB)';    e={$_.'Size (GB)'}},
                @{n='Free space';   e={$_.'Free space'}},
                @{n='Percent free'; e={$_.'Percent free'}},
                $(if ($PSBoundParameters['IncludeDiskInfo']) {
                    @{n='Disk Index'; e={
                        try {
                            $ScriptBlock = {
                                param($GetDiskClass, $DriveString)
                                try {
                                    Add-Type -TypeDefinition $GetDiskClass -ErrorAction Stop
                                }
                                catch {
                                    #Write-Error -Message "${Computer}: Error creating class [STGetDisk]"
                                    return "Error creating [STGetDisk] class: $_"
                                }
                                return [STGetDisk]::GetPhysicalDriveString($DriveString)
                            }
                            if ($Credential.Username) {
                                $PhysicalDisk = Invoke-Command -ComputerName $Computer -Credential $Credential -ScriptBlock $ScriptBlock -ArgumentList $STGetDiskClass, $(if ($_.Caption -imatch '\A[a-z]:\\\z') { $_.Caption } else { $_.DeviceID.TrimStart('\?') })
                            }
                            else {
                                $PhysicalDisk = Invoke-Command -ComputerName $Computer -ScriptBlock $ScriptBlock -ArgumentList $STGetDiskClass, $(if ($_.Caption -imatch '\A[a-z]:\\\z') { $_.Caption } else { $_.DeviceID.TrimStart('\?') })
                            }
                            if ($PhysicalDisk -like 'Error*') {
                                "Error: $PhysicalDisk"
                            }
                            else {
                                ($DiskDriveWmiInfo | Where-Object { $PhysicalDisk } | Where-Object { $PhysicalDisk.Trim() -eq $_.Name } | Select-Object -ExpandProperty Index) -join '; '
                            }
                        }
                        catch {
                            "Error: $_"
                        }
                    } # end of disk index expression
                    } # end of if disk index hashtable 
                }) # end of if includediskinfo parameter subexpression and if
            }
            else {
                @{n='ComputerName'; e={$_.Computer}},
                @{n='Label';        e={$_.Label}},
                @{n='Caption';      e={$_.Caption}},
                @{n='FileSystem';   e={$_.FileSystem}},
                @{n='Size (GB)';    e={$_.'Size (GB)'.ToString('N')}},
                @{n='Free space';   e={$_.'Free space'.ToString('N')}},
                @{n='Percent free'; e={$_.'Percent free'.ToString('N')}},
                $(if ($PSBoundParameters['IncludeDiskInfo']) {
                    @{n='Disk Index'; e={
                        try {
                            $ScriptBlock = {
                                param($GetDiskClass, $DriveString)
                                try {
                                    Add-Type -TypeDefinition $GetDiskClass -ErrorAction Stop
                                }
                                catch {
                                    #Write-Error -Message "${Computer}: Error creating class [STGetDisk]"
                                    return "Error creating [STGetDisk] class: $_"
                                }
                                return [STGetDisk]::GetPhysicalDriveString($DriveString)
                            }
                            if ($Credential.Username) {
                                $PhysicalDisk = Invoke-Command -ComputerName $Computer -Credential $Credential -ScriptBlock $ScriptBlock -ArgumentList $STGetDiskClass, $(if ($_.Caption -imatch '\A[a-z]:\\\z') { $_.Caption } else { $_.DeviceID.TrimStart('\?') })
                            }
                            else {
                                $PhysicalDisk = Invoke-Command -ComputerName $Computer -ScriptBlock $ScriptBlock -ArgumentList $STGetDiskClass, $(if ($_.Caption -imatch '\A[a-z]:\\\z') { $_.Caption } else { $_.DeviceID.TrimStart('\?') })
                            }
                            if ($PhysicalDisk -like 'Error*') {
                                "Error: $PhysicalDisk"
                            }
                            else {
                                ($DiskDriveWmiInfo | Where-Object { $PhysicalDisk } | Where-Object { $PhysicalDisk.Trim() -eq $_.Name } | Select-Object -ExpandProperty Index) -join '; '
                            }
                        }
                        catch {
                            "Error: $_"
                        }
                    } # end of disk index expression
                    } # end of if disk index hashtable 
                }) # end of if includediskinfo parameter subexpression and if
            }) # end of if $NoFormat
    }
}

function This-PRTGOut ($computerName) { 
[System.Globalization.NumberFormatInfo]::CurrentInfo.NumberGroupSeparator = "";
[System.Globalization.NumberFormatInfo]::CurrentInfo.NumberDecimalSeparator = ".";
$Channels = "";

$DiskCount = 0; 
$Disks = (Get-MountPointData -ComputerName $ComputerName -IncludeDiskInfo)

$Disks | ft -AutoSize

foreach($Disk in $Disks)
{
    if($Disk.Label.Length -ne 0){
        $DiskCount++; 
        $Channels += ([string]::Format($channelTemplateDiskFree,"[$($Disk.Label)] Free",[double]$Disk."Free Space","BytesDisk","<VolumeSize>GigaByte</VolumeSize>"));
        $Channels += ([string]::Format($channelTemplatePercent,"[$($Disk.Label)] Free %",[int]$Disk."Percent free","Percent",""));
    }
}

$channels += ([string]::Format($channelTemplateMountedDisks,"Mounts",$DiskCount,$DiskCount));

write-host "<prtg>";
write-host $Channels; 
write-host "</prtg>";

}

This-PRTGOut -computerName $computername;

Thanks a lot to Joakim Svendsen and Justin Rich!

Note

This article is provided for your information only. The steps described here have been tested carefully. However, we cannot offer deep technical support for customizing this script nor for writing your own scripts.

More

Are you looking for more custom scripts and sensors than PRTG's native sensors? Then have a look at our Paessler Script World!

Created on May 26, 2017 7:48:32 AM by  Stephan Linke [Paessler Support]

Last change on Aug 2, 2017 11:16:28 AM by  Martina Wittmann [Paessler Support]



Votes:

0

Your Vote:

Up

Down

Could someone please help updating this script which will make it work in PRTG ver. 19.2.50.2842? As now it is returning a "XML: The returned XML does not match the expected schema. (code: PE233) -- JSON: The returned JSON does not match the expected structure (Invalid JSON.). (code: PE231)". It seems like it has to do with usage of "" for parameters and some other changes since this script was released. Thank you.

Created on May 28, 2019 8:49:48 AM by  CWWsysop (0)



Votes:

0

Your Vote:

Up

Down

Dear CWWsysop,

in order to debug this, please go to the sensor's "Settings" tab, and enable "Write Exe result to disk". After the next sensor scan, find the logs in "C:\ProgramData\Paessler\PRTG Network Monitor\Logs (Sensors)".

One of the files contains the script output. Probably the script threw an error, so that no valid XML result was created.

Created on May 28, 2019 11:54:37 AM by  Arne Seifert [Paessler Support]



Please log in or register to enter your reply.


Disclaimer: The information in the Paessler Knowledge Base comes without warranty of any kind. Use at your own risk. Before applying any instructions please exercise proper system administrator housekeeping. You must make sure that a proper backup of all your data is available.