What is this?

This knowledgebase contains questions and answers about PRTG Network Monitor and network monitoring in general.

Learn more

PRTG Network Monitor

Intuitive to Use. Easy to manage.
More than 500,000 users rely on Paessler PRTG every day. Find out how you can reduce cost, increase QoS and ease planning, as well.

Free Download

Top Tags


View all Tags

Can I Monitor Mounted Windows Volumes with PRTG?

Votes:

0

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]



7 Replies

Accepted Answer

Votes:

2

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 %programfiles(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 the native sensors of PRTG? Then have a look at our PRTG Sensor Hub!

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

Last change on Sep 18, 2020 10:04:49 AM by  Maike Guba [Paessler Support] (2,404) 2 1



Votes:

0

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



Votes:

0

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]



Votes:

0

In PRTG Version 19.4.52.3515 we have inexplicable issues with the script.

  • When the monitored Volume Mountpoint goes down, it is not reported by PRTG
  • When it comes up again, it is reported.

Does anyone else has this problem?

Created on Oct 25, 2019 12:26:49 PM



Votes:

0

Did this occur in previous PRTG versions as well? Not really sure how the script copes with volumes that went missing. Say the volumes are down, is the mount point still listed in the output of

Get-WmiObject @WmiHash -Class Win32_MountPoint

...once it's down? What exactly do you mean with "when it comes up again, it is reported?

Created on Oct 28, 2019 11:40:43 AM by  Stephan Linke [Paessler Support]



Votes:

0

Hi, thanks for this helpful script!

I can't monitor the free space, i gave me just free percentage. The Value returned for free % has to be divided by hundred, so i changed the code:

from

$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}}
            }

to

$Volumes | ForEach-Object {
            if ($MountPointData.ContainsKey($_.DeviceID)) {
                # Let's avoid dividing by zero, it's so disruptive.
                if ($_.Capacity) {
                    $PercentFree = $_.FreeSpace/$_.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}}
            }

Why can't i see the free space in GB?

The System is HPE MSA2060 with HP DL360 Gen10 and Windows Server 2022 Datacenter. Hyper-V Failover Cluster is installed.

Created on May 4, 2023 8:24:27 AM

Last change on May 5, 2023 7:05:42 AM by  Felix Wiesneth [Paessler Support]



Votes:

0

@Jochen,

Do you get the values when you run the script manually?


Kind regards,
Sasa Ignjatovic, Tech Support Team

Created on May 9, 2023 7:15:47 AM by  Sasa Ignjatovic [Paessler Support]




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.