PowerShell Get-MountPointData Cmdlet

From Svendsen Tech PowerShell Wiki
Jump to: navigation, search

Featured here is a "Get-MountPointData" cmdlet, that retrieves free/used space on mount points on a local or remote Windows server. It displays information about all mount points that are listed in the Win32_MountPoint WMI class (which includes "regular" drives, but they're filtered out by default), and combines it with the information about size and used space in the Win32_Volume WMI class, based on the device ID (formats are slightly different, so a conversion is made).

In the latest version, v1.2 as of 2015-03-01, it also maps the physical disk index to the mount points and/or drive letters. This is not at all trivial, and a PowerShell solution didn't exist in an easily accessible, public form until this early March / late February 2015 weekend. Read more in the "change log" in the downloads section.

Get-MountPointData-v1 1-disk-index-example.png



This has a few side-effects, such as filtering out the default "System Reserved" partition entry (introduced in Win 7 / Server 2008 R2, I think) that's listed if you only look at the Win32_Volume class and filter on something like the DriveLetter property not being set.

It uses WMI against remote computers with the -ComputerName parameter. Specify the name "localhost" for data about the local host. You can specify multiple servers separated by commas. I'll look into writing a CIM version.

I also added a parameter called -IncludeRootDrives which allows you to look at regular disk drive letters on the desired servers as well, in the same way.

The parameter -IncludeDiskInfo (v1.2+) requires PSRemoting to work.

Output is by default sorted on free available space in percent, then descendingly on drive size (if the percentage is exactly the same), then by the label property and finally by caption.

I implemented it as functions, but didn't package it as a module. You can dot-source the ps1 file you download to get the functions in the currently available scope, or put it in your PowerShell profile. It is also possible to name the file Get-MountPointData.psm1 and put it in a subfolder of one of your $env:PSModulePath folders; if so, you would later import it with Import-Module, or have it autoloaded with PSv3 and up. You can also import it from a relative path with Import-Module.

The script is written so it's fairly easy to add other properties that you might want to list.

Download

2015-03-01: Quickly spun around and added -Credential support for the Invoke-Command that's used when you supply the -IncludeDiskInfo parameter. The rest is WMI. I should look into a CIM version.

2015-03-01: New version that also supports listing disk index for the physical disk a mount point resides on. This is not at all trivial and required a custom type written in C#. Thanks to Justin Rich for doing the hard work. This version doesn't appear to work with PowerShell v2 on my Vista lab machine, but works on a 2008 R2 server with v2. It seems to work against _remote_ computers with v2 regardless. The cmdlet is significantly slower with the -IncludeDiskInfo parameter and this parameter requires PSRemoting to the target (because I need to add the [GetDisk] type remotely).

2014-11-07: Uploaded a new version I wrote months ago when I needed math operations on the space. Use the parameter -NoFormat for that. Also added support for specifying credentials with -Credential or -PromptForCredentials

Parameters

ComputerName Target computer(s).
PromptForCredentials Prompts for credentials to use for the WMI calls, and for Invoke-Command with the -IncludeDiskInfo parameter.
Credential Pass a credentials object to use for the WMI calls, and for Invoke-Command with the -IncludeDiskInfo parameter.
IncludeRootDrives Includes volumes with drive letters.
NoFormat Do not format numbers, so you can do math operations easily (very useful in certain situations).
IncludeDiskInfo Includes disk index number of the disk the mount point or drive letter resides on (should also support spanned volumes and multiple indices, but that's untested). Requires administrator privileges.

Examples

A few examples

Showing Used/Free Space On Mount Points

Here I show the used/free space on the server file_server01's mount points, using the Get-MountPointData cmdlet.

PS C:\> . .\scripts\Get-MountPointData.ps1
PS C:\> Get-MountPointData -ComputerName file_server01 | Format-Table -AutoSize

Computer        Label       Caption            FileSystem Size (GB) Free space Percent free
--------        -----       -------            ---------- --------- ---------- ------------
file_server01   Dept0001    E:\foo\Dept0001    NTFS       250.00    37.73      15.09
file_server01   Dept0002    E:\foo\Dept0002    NTFS       500.00    83.13      16.63
file_server01   Dept0003    E:\foo\Dept0003    NTFS       350.00    73.38      20.97
file_server01   Dept0004    E:\bar\Dept0004    NTFS       150.00    50.37      33.58
file_server01   Dept0005    E:\bar\Dept0005    NTFS       150.00    56.89      37.93
file_server01   Dept0006    E:\bar\Dept0006    NTFS       300.00    124.42     41.47
file_server01   Dept0007    E:\baz\Dept0007    NTFS       100.00    45.10      45.11
file_server01   Dept0008    E:\baz\Dept0008    NTFS       70.00     31.90      45.57
file_server01   Dept0009    E:\foo\Dept0009    NTFS       300.00    157.08     52.36
file_server01   Dept0010    E:\foo\Dept0010    NTFS       200.00    109.37     54.68
file_server01   Dept0011    E:\foo\Dept0011    NTFS       300.00    191.93     63.98

Including Root Drives

Here's an example of a server without mount points, where I look only at the root drives, using the -IncludeRootDrives parameter.

PS C:\> Get-MountPointData -Comp filesrv2 -IncludeRootDrives | ft -a

Computer Label Caption FileSystem Size (GB) Free space Percent free
-------- ----- ------- ---------- --------- ---------- ------------
filesrv2       H:\                0.00      0.00
filesrv2 USER  D:\     NTFS       1,945.59  157.36     8.09
filesrv2 User3 F:\     NTFS       711.99    85.11      11.95
filesrv2 User4 G:\     NTFS       711.99    115.16     16.17
filesrv2 User2 E:\     NTFS       549.99    137.36     24.97
filesrv2       C:\     NTFS       48.83     22.24      45.54

"H:\" is an optical drive. The drives are sorted descendingly after percentage of free space by default, but you can use -Noformat and sort on size, free space, multiple values, or whatever you want.

Sorting By Drive Letter

To sort by drive letter, you can add "Sort Computer, Caption" to the pipeline before Format-Table -AutoSize. If you only target a single server, you can omit the "Computer" property and sort by caption only.

PS C:\> Get-MountPointData -Comp filesrv2 -IncludeRootDrives | Sort Computer, Caption | ft -a

Computer Label Caption FileSystem Size (GB) Free space Percent free
-------- ----- ------- ---------- --------- ---------- ------------
filesrv2       C:\     NTFS       48.83     22.24      45.54
filesrv2 USER  D:\     NTFS       1,945.59  157.36     8.09
filesrv2 User2 E:\     NTFS       549.99    137.36     24.97
filesrv2 User3 F:\     NTFS       711.99    85.11      11.95
filesrv2 User4 G:\     NTFS       711.99    115.16     16.17
filesrv2       H:\                0.00      0.00

Code

Code for the v1.2 cmdlet.

### Copyright (c) 2012-2014, Svendsen Tech
### Author: Joakim Svendsen
### Get-MountPointData v1.2

# "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).

# Convert from one device ID format to another.
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/1GB}}, @{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
    }
}