Shutting Down Running VMs – Revisited

A couple of years ago, I posted a perfectly good snippet for shutting down the running VMs on a machine. But the code there is very much the "old syntax" and not terribly elegant.  For shutting down all the running RODCs, I used:

Get-VM -Name *rodc* | Where-Object {$_.State -eq "Running" } | Foreach-Object { Stop-VM $_.Name }

Which is a nuisance to type, frankly. So, here's the PowerShell v5 version. Much slicker, much easier to remember, and a lot quicker to type.

Get-VM -Name *rodc* | Where State -eq "Running" | Stop-VM

Simpler and easier to follow. It still requires a two-pipe solution, but that's only because we only wanted to stop the running VMs without any warning messages. If we didn't care about spurious warning messages, the answer gets even simpler:

Stop-VM *rodc*

All things considered, I'm in favour of simpler. And the odd warning message doesn't concern me, so I'll go with the last solution 90% of the time. Note that this accepts a -ComputerName parameter for running against a remote computer, a -PassThru parameter for echoing out which VMs it's shutting down, and a -TurnOff parameter to just kill the VMs, rather than use an orderly shutdown.

 

Historical note:  If you want to see a really complicated way to do this, written before we had "Stop-VM" as a cmdlet, take a look at this post from 4 years ago! I'm so glad PowerShell is doing all the heavy lifting now!

 

 

Setting Console Colours

As I described in my previous post, I always open both an admin and non-admin PowerShell window when I log on to a computer. To tell the two apart, I set the background colour of the Admin window to dark red, with white text, and the non-admin window to a white background with dark blue text. The result is clearly and immediately different, warning me when I'm running as an administrator. To do that, I use the following:

$id = [System.Security.Principal.WindowsIdentity]::GetCurrent() 
$p = New-Object system.security.principal.windowsprincipal($id)

# Find out if we're running as admin (IsInRole). 
# If we are, set $Admin = $True. 
if ($p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)){ 
   $Admin = $True 
} else { 
   $Admin = $False 
}

if ($Admin) { 
      $effectivename = "Administrator" 
      $host.UI.RawUI.Backgroundcolor="DarkRed" 
      $host.UI.RawUI.Foregroundcolor="White" 
      clear-host 
   } else { 
      $effectivename = $id.name 
      $host.UI.RawUI.Backgroundcolor="White" 
      $host.UI.RawUI.Foregroundcolor="DarkBlue" 
      clear-host 
}

 

That works great for older versions of the Windows PowerShell console, but starting with PowerShell v5, that can have an unintended side effect. In PowerShell v5, we now have PSReadLine that does context sensitive colouration of the command line. But the default PowerShell console has a dark blue background, with white text. And when I changed the background colour of my non-admin PowerShell window to white, it gets a little hard to read!! So, to fix that, I use Set-PSReadLineOption to change the various kinds of context sensitive colour changes to something that works with a light background. We don't want to do that for the dark red background of the Admin window, so we'll need to check which colour we are and adjust accordingly.

First, get the current colour:

$pData = (Get-Host).PrivateData 
$curForeground = [console]::ForegroundColor 
$curBackground = [console]::BackgroundColor

You'll only want to configure the context sensitive colouring options if you're running on Windows 10 or Server 2016. Prior versions of Windows didn't have the new system console that comes with Windows 10. So you'll want to check that the build number is > 10240

$Build = (Get-WmiObject Win32_OperatingSystem).BuildNumber

If $Build -ge 10240, then set the various context sensitive tokens to work with the colour we have.

# PowerShell v5 uses PSReadLineOptions to do syntax highlighting. 
# Base the color scheme on the background color 
If ( $curBackground -eq "White" ) { 
      Set-PSReadLineOption -TokenKind None      -ForegroundColor DarkBlue  -BackgroundColor White 
      Set-PSReadLineOption -TokenKind Comment   -ForegroundColor DarkGray  -BackgroundColor White 
      Set-PSReadLineOption -TokenKind Keyword   -ForegroundColor DarkGreen -BackgroundColor White 
      Set-PSReadLineOption -TokenKind String    -ForegroundColor Blue      -BackgroundColor White 
      Set-PSReadLineOption -TokenKind Operator  -ForegroundColor Black     -BackgroundColor White 
      Set-PSReadLineOption -TokenKind Variable  -ForegroundColor DarkCyan  -BackgroundColor White 
      Set-PSReadLineOption -TokenKind Command   -ForegroundColor DarkRed   -BackgroundColor White 
      Set-PSReadLineOption -TokenKind Parameter -ForegroundColor DarkGray  -BackgroundColor White 
      Set-PSReadLineOption -TokenKind Type      -ForegroundColor DarkGray  -BackgroundColor White 
      Set-PSReadLineOption -TokenKind Number    -ForegroundColor Red       -BackgroundColor White 
      Set-PSReadLineOption -TokenKind Member    -ForegroundColor DarkBlue  -BackgroundColor White 
      $pData.ErrorForegroundColor   = "Red" 
      $pData.ErrorBackgroundColor   = "Gray" 
      $pData.WarningForegroundColor = "DarkMagenta" 
      $pData.WarningBackgroundColor = "White" 
      $pData.VerboseForegroundColor = "DarkYellow" 
      $pData.VerboseBackgroundColor = "DarkCyan" 
   } elseif ($curBackground -eq "DarkRed") { 
      Set-PSReadLineOption -TokenKind None      -ForegroundColor White    -BackgroundColor DarkRed 
      Set-PSReadLineOption -TokenKind Comment   -ForegroundColor Gray     -BackgroundColor DarkRed 
      Set-PSReadLineOption -TokenKind Keyword   -ForegroundColor Yellow   -BackgroundColor DarkRed 
      Set-PSReadLineOption -TokenKind String    -ForegroundColor Cyan     -BackgroundColor DarkRed 
      Set-PSReadLineOption -TokenKind Operator  -ForegroundColor White    -BackgroundColor DarkRed 
      Set-PSReadLineOption -TokenKind Variable  -ForegroundColor Green    -BackgroundColor DarkRed 
      Set-PSReadLineOption -TokenKind Command   -ForegroundColor White    -BackgroundColor DarkRed 
      Set-PSReadLineOption -TokenKind Parameter -ForegroundColor Gray     -BackgroundColor DarkRed 
      Set-PSReadLineOption -TokenKind Type      -ForegroundColor Magenta  -BackgroundColor DarkRed 
      Set-PSReadLineOption -TokenKind Number    -ForegroundColor Yellow   -BackgroundColor DarkRed 
      Set-PSReadLineOption -TokenKind Member    -ForegroundColor White    -BackgroundColor DarkRed 
      $pData.ErrorForegroundColor   = "Yellow" 
      $pData.ErrorBackgroundColor   = "DarkRed" 
      $pData.WarningForegroundColor = "Magenta" 
      $pData.WarningBackgroundColor = "DarkRed" 
      $pData.VerboseForegroundColor = "Cyan" 
      $pData.VerboseBackgroundColor = "DarkRed" 
   } 
}

Finally, let's make sure that console window is the right size, and while we're at it, set the window title. (This is a workaround for a PITA bug in recent builds of Windows 10/Server 2016 that seems to have problems setting the console window size and keeping it!)

$host.ui.rawui.WindowTitle = $effectivename + "@" + $HostName +" >" 
$Host.UI.RawUI.WindowSize = New-Object System.Management.Automation.Host.Size(120,40)

 

Now, with all of this, you have effective, context-sensitive, command-line colouring of your PowerShell windows.

More $Profile Tricks — Automatically Opening an Admin Window

I run as a limited user during my normal work, but I always keep one or more Admin windows open. These are logged in to my Domain Administrator account, running "As Administrator". And I make sure I can tell that I'm running in that window by setting the colour scheme with a nice, dark red, background. Hard to miss! (I'll show you how to do that in my next post. ) So, how do I do all that? Well, it starts by automatically opening a PowerShell window when I first log on, as described earlier here.

When that starts, I include code in my $Profile to first check how many PowerShell windows are already running, so I don't start opening more if I don't need them.

$PSH = Get-Process PowerShell

Simple, and let's me get a count with $PSH.count. If this is the first PowerShell window I've opened ($PSH.count -lt 2) and this isn't already an admin window, then I open an admin window. Let's break this down: First, am I running as an Administrator?

# First, initialize $Admin to false 
$Admin = $False

# Then, find out who we are... 
$id = [System.Security.Principal.WindowsIdentity]::GetCurrent() 
$p = New-Object system.security.principal.windowsprincipal($id)

# Find out if we're running as admin (IsInRole). 
if ($p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)){ 
   $Admin = $True 
}

Now, a function to run an elevated window as the domain administrator.

Function Start-myAdmin () { 
# Get admin credentials 
$AdminCred = Get-Credential 
        -UserName TreyResearch\Administrator ` 
        -Message "Enter your password for the Administrator account: " 
# Start an elevated window, but with designated creds. 
Start-Process PowerShell.exe ` 
       -Credential $AdminCred ` 
       -ArgumentList "Start-Process PowerShell.exe -Verb RunAs" ` 
       -NoNewWindow

}

Good. So we're going to want to start that admin window automatically when we first logon. We do that by checking the count of open PowerShell windows ($PSH.count)

# If this isn't already an Admin window, and we don't have lots of other PowerShell 
# windows open, then we start an Admin PowerShell window. 
if ( ! $Admin  ) {
   cd $home
   if ($Psh.count -lt 2 ) {
      Start-myAdmin
   }
}

Oh, and we'll want an alias to be able to open up another one whenever we need it.

Set-Alias Admin -Value Start-myAdmin

 

Clearing the Archive Attribute Bit

I use a whole set of PowerShell scripts to create my lab environment when I’m working on a book or article. The master scripts all reside in a Build directory on my labhost. The problem is that when something goes wonky, or I need to tweak a script to do something a bit different, I’m usually working from the lab client, not the host. What I don’t want to do is lose any changes I’ve made if I blow away a client (which happens fairly often in a lab!) So I try to remember to copy the change up to the master Build directory. But that’s not 100%, so I wanted a quick way to copy only the changed files back up to the host. The easy way to do it should be using the filesystem’s Archive attribute bit, but it turns out Windows PowerShell is just plain brain-dead about how it handles file attributes. You have to resort to binary ANDs and binary XORs to manipulate them. Well, that’s just not fun. So, having spent the time to get it working, I thought I’d just share the script I use to clear the Archive attribute for one or many files. Enjoy.

<# 
.Synopsis 
Clears the archive bit on a set of files 
.Description 
Clear-myArchiveBit returns a list of files whose archive bit is set to on, and resets 
them to off. This is command supports the -Recurse parameter. 
.Example 
Clear-myArchiveBit *.ps1 
This command clears the archive bits of all ps1 files in the current directory

.Example 
Clear-myArchiveBit *.ps1 -Path $home\psbin -recurse 
This command clears the archive bit of all PS1 files in the $home\psbin directory, 
and all subdirectories of $home\psbin.

.Parameter Filter 
The filename filter to use. Aliases are Name and Filename. Default is *. 
.Parameter Path 
The path to the files to be changed. Default is ".". 
.Parameter Recurse 
If specified, the change is made to the Path and all subdirectories of the Path. 
.Inputs 
[string] 
[string] 
[switch] 
.Notes 
    Author: Charlie Russel 
 Copyright: 2016 by Charlie Russel 
          : Permission to use is granted but attribution is appreciated 
   Initial: 16 May, 2016 (cpr) 
   ModHist: 
          : 
#> 
[CmdletBinding()] 
Param( 
     [Parameter(Mandatory=$False,ValueFromPipeline=$True,Position=0)] 
     [alias("Name","Filename")] 
     [string]$Filter = "*", 
     [Parameter(Mandatory=$false,ValueFromPipeline=$false,Position=1)] 
     [string]$Path = ".", 
     [Parameter(Mandatory=$false,ValueFromPipeline=$false)] 
     [switch]$Recurse 
     ) 
Write-Verbose "Clearing the Archive bit on $filter in $path" 
# This is how we'll XOR the bit when we get to that. 
$Archive = [io.fileattributes]::Archive

# First, get all the files whose Archive bit is currently set. 
if ($Recurse) { 
   $chgdFiles = Get-ChildItem -Filter $Filter -Path $path -Recurse ` 
                  | Where {$_.mode -match "a" } 
} else { 
   $chgdFiles = Get-ChildItem -Filter $Filter -Path $path ` 
                  | Where {$_.mode -match "a" } 
}

# Now, we clear the bit on those files using a binary XOR. 
ForEach ($file in $chgdFiles ) { 
   Set-ItemProperty -Path $file.FullName ` 
                    -Name Attributes ` 
                    -Value ((Get-ItemProperty $file.FullName).Attributes ` 
                            -bXOR $Archive ) 
} 
Write-Verbose "The following files have had their Archive attribute cleared: " 
Write-Verbose "" 
$chgdFiles | Write-Verbose

Starting Windows PowerShell Automatically

Most system administrators routinely start a command line window of some sort when they log onto a computer. For me, obviously, that’s a PowerShell window. Actually, two PowerShell windows – one as a limited user, the other as an administrator, but that trick is for a different blog post. For this one, I’ll show you how to have Windows automatically start a PowerShell windows whenever you long on to your computer.

The key to this is the Run key in the registry. Specifically HKCU:\Software\Microsoft\Windows\CurrentVersion\Run. Programs listed in this key automatically start when the user (HKCU) logs on to the computer. So, all we need to do is insert a new value into the key for PowerShell. Easy enough, with New-ItemProperty. Here’s a function to do that:

Function Set-myPowerShellStart () {
   if (Get-ItemProperty HKCU:\Software\Microsoft\Windows\CurrentVersion\Run `
                        -Name "*PowerShell" 2>$NULL ) {
      write-verbose "Already an entry for starting PowerShell"
   } else {
      New-ItemProperty HKCU:\Software\Microsoft\Windows\CurrentVersion\Run `
                       -Name  "Windows PowerShell" `
                       -Value "C:\Windows\system32\WindowsPowerShell\v1.0\PowerShell.exe"
      write-verbose "Added value for automatic PowerShell start"
   }
}

Now, all you need to do is call that function, and it will insert a key in the registry, and the next time you log on to the computer, you’ll automatically have a PowerShell window open and ready to go.

Promoting a new domain controller

I’ve been working with Windows Server 2016 CTP5 recently, and because I installed it without the Desktop Experience (what we used to call a Server Core installation), I’m having to do everything in Windows PowerShell. No complaints, I enjoy it, but it does force me to think about things a bit sometimes.

One of the tasks I needed to do was promote a new server to be a secondary domain controller. The PowerShell command for this is: Install-ADDSDomainController. But before you start promoting a new DC, it’s a really good idea to test that the promotion will succeed by using the Test-ADDSDomainControllerInstallation cmdlet. So, I combined the two steps into a simple script that allows you to run the test, and if the output looks clean, finish the installation and initiate a reboot.

The script is smart enough to realize you haven’t installed the ActiveDirectory feature yet, and goes ahead and installs it for you.

<# 
.Synopsis 
Tests a candidate domain controller, and then promotes it to DC.

.Description 
Promote-myDC first tests if a domain controller can be successfully promoted, and, 
if the user confirms that the test was successful, completes the promotion and 
restarts the new domain controller. 

.Example 
Promote-myDC -Domain TreyResearch.net

Tests if the local server can be promoted to domain controller for the 
domain TreyResearch.net. The user is prompted after the test completes 
and must press the Y key to continue the promotion. 

.Parameter Domain 
The domain to which the server will be promoted to domain controller. 

.Inputs 
[string] 

.Notes 
    Author: Charlie Russel 
 Copyright: 2016 by Charlie Russel 
          : Permission to use is granted but attribution is appreciated 
   Initial: 05/14/2016 (cpr) 
   ModHist: 
          : 
#> 
[CmdletBinding()] 
Param( 
     [Parameter(Mandatory=$True,Position=0)] 
     [string] 
     $Domain 
     )

Write-Verbose "Testing if ADDSDeployment module is available" 
If ( ! (Get-Module ADDSDeployment )) { 
   Write-Verbose "Installing the ActiveDirectory Windows Feature, since you seem to have forgotten that." 
   Install-WindowsFeature -Name ActiveDirectory -IncludeManagementTools 
   Write-Host "" 
}

If ( ! (Get-Module ADDSDeployment )) { 
   throw "Failed to install the ActiveDirectory Windows Feature." 
}

Write-Verbose "Testing if server $env:computername can be promoted to DC in the $Domain domain" 
Write-Host "" 
Test-ADDSDomainControllerInstallation ` 
      -NoGlobalCatalog:$false ` 
      -CreateDnsDelegation:$false ` 
      -CriticalReplicationOnly:$false ` 
      -DatabasePath "C:\Windows\NTDS" ` 
      -DomainName $Domain ` 
      -LogPath "C:\Windows\NTDS" ` 
      -NoRebootOnCompletion:$false ` 
      -SiteName "Default-First-Site-Name" ` 
      -SysvolPath "C:\Windows\SYSVOL" ` 
      -InstallDns:$true ` 
      -Force 
Write-Host "" 
Write-Host "" 
Write-Host ""

Write-Host -NoNewLine "If the above looks correct, press Y to continue...  " 
$Key = [console]::ReadKey($true) 
$sKey = $key.key

Write-Verbose "The $sKey key was pressed." 
Write-Host "" 
Write-Host "" 
If ( $sKey -eq "Y" ) { 
   Write-Host "The $sKey key was pressed, so proceeding with promotion of $env:computername to domain controller." 
   Write-Host "" 
   sleep 5 
   Install-ADDSDomainController ` 
      -SkipPreChecks ` 
      -NoGlobalCatalog:$false ` 
      -CreateDnsDelegation:$false ` 
      -CriticalReplicationOnly:$false ` 
      -DatabasePath "C:\Windows\NTDS" ` 
      -DomainName $Domain ` 
      -InstallDns:$true ` 
      -LogPath "C:\Windows\NTDS" ` 
      -NoRebootOnCompletion:$false ` 
      -SiteName "Default-First-Site-Name" ` 
      -SysvolPath "C:\Windows\SYSVOL" ` 
      -Force:$true 
} else { 
   Write-Host "The $sKey key was pressed, exiting to allow you to fix the problem." 
   Write-Host "" 
   Write-Host "" 
}

	                    
	                

A More Readable Path

The path on many modern computers gets to be so long that reading it is a nuisance. Worse, from within Windows PowerShell, there isn’t a built in equivalent to the old cmd “path” command. Yet all the information we need is there, it’s just not where we might expect it. The actual executable path for a PowerShell session is stored in $ENV:path. Here’s a quick function you put in your $profile to print out the PATH, one line for each item:

Function path { 
   $p=$env:path 
   $p.Split(':') 
}

Simple, wasn't it? Now, when you type "path" from the Windows PowerShell prompt, you'll see something like this:

PSH> path 
C:\Windows\system32 
C:\Windows 
C:\Windows\System32\Wbem 
C:\Windows\System32\WindowsPowerShell\v1.0\ 
C:\Users\charlie.russel\psbin

Unmapping Network Drives

Unmapping network drives with PowerShell should be easy, and it is, but with some caveats. If you always create your network drive mappings with New-PSDrive, then it's easy to unmap them with Remove-PSDrive. But if some of them are created with Group Policy, some of them with the legacy "net use" commands, some of them with New-SmbMapping, and some with New-PSDrive, it's really not that easy to fully clean up the list of mapped drives. I've used a variety of techniques over the years to map and unmap drives, and I've finally come to the conclusion that using New-PSDrive and Remove-PSDrive is the cleanest way for most things. However, if you've got a mix of mapped drives, created with more than one method, here's a script to remove them all.

<#
.SYNOPSIS
Unmaps network drives
.DESCRIPTION
Unmapdrives removes all currently mapped network drives. It's smart enough to 
remove drives mapped with "net use", "New-SmbMapping" and "New-PSDrive". This 
cmdlet accepts no parameters and assumes -Force for all unmappings. 

.EXAMPLE
UnMapDrives 
Unmaps all currently mapped network drives 

.NOTES
    Author: Charlie Russel
 Copyright: 2015 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 06/27/2015 (cpr)
   ModHist:
 :
#>
[CmdletBinding()]

# Build a dynamic list of currently mapped drives
$DriveList = Get-WMIObject Win32_LogicalDisk `
     | Where-Object { $_.DriveType -eq 4 }

# Don't bother running this if we don't have any mapped drives
 if ($DriveList) { 
    $SmbDriveList = $DriveList.DeviceID
 } else {
    Write-Host "No mapped drives found"
    Return
}

Write-host "Unmapping drive: " -NoNewLine
Write-Host $SmbDriveList
Write-Host " "

Foreach ($drive in $SmbDriveList) {
    $psDrive = $drive -replace ":" #remove unwanted colon from PSDrive name
    Remove-SmbMapping -LocalPath $Drive -Force -UpdateProfile
    If ( (Get-PSDrive -Name $psDrive) 2>$Null ) {
       Remove-PSDrive -Name $psDrive -Force
    }
}
Write-Host " "

# Report back all FileSystem drives to confirm that only local drives are present. 
Get-PSDrive -PSProvider FileSystem

This is the simple form of a more generalized script that can accept a parameter of either -All or a list of mapped drive letters to remove. (And yes, unmap isn't an approved verb. But this script started out life many, many years ago as a batch file ("unmapdrives.cmd"), so it's still got the same base name it has always had, because that's what my fingers remember!)

Finally, the death of CMD!

Well, OK, it’s still there in Windows 10 But finally, completely, I no longer need it for ANYTHING! The final piece that I still used is now available in PowerShell v5. The latest publicly available Preview Build of Windows 10 Technical Preview (build 9926), includes updates to New-Item to support Junctions and Hard Links. Finally!

 

There’s no direct help available for it yet (I checked and downloaded the very latest), but a little poking around produced:

PS> New-Item -Type HardLink -Path 2ndTest.txt -Value Test.txt

I can edit 2ndTest.txt and the changes appear in Test.txt, and I can delete Test.txt and 2ndTest.txt is still there and editable. Interestingly, I do NOT need to run this as an administrator, assuming I have permissions in the folder where I’m creating the second (or third or 15th file). And, as expected, this can’t cross drive boundaries.

 

This build also includes support for directory Junctions, (-Type Junction) so between these two, I think we finally have all we need to kiss goodbye all further use of cmd.exe. (I really hate having to use cmd c/ from my PowerShell to get something done. Really. It’s so DOS.)

 

My thanks to the PowerShell team who took my challenge and mantra of Death To CMD to heart and have finally made Windows PowerShell the tool I can use for everything.

(Note: Currently the most current build available for download separately is the November build which doesn’t have these two New-Item features, so the only way to get them right now is in the latest Windows 10 build. But that’s likely to change soon.)

 

ETA: The February WMF 5.0 build is available from Windows Download Centre. This includes support for Windows Server 2012, Windows Server 2012 R2, and Windows 8.1 Pro and Enterprise. And yes, the changes to New-Item are in there. 🙂

 

Oh, and while we’re at it – there’s all sorts of other goodies in PowerShell v5, including OneGet, a great way to get new modules for your PowerShell. Check it out.

Getting the Free Disk Space of Remote Computers

This started out as a simple script to try to get the free space on one of my servers, but I quickly discovered that using WMI’s Win32_LogicalDisk could only give me part of the solution. The catch is that Win32_LogicalDisk doesn’t return the information about volumes that aren’t assigned drive letters. Which is a problem if what you really need to know is how close to out of space your backup disk is! Because Windows Server Backup takes total control of the backup target disk, and doesn’t mount it as a drive letter, you need to use a different WMI Class to get the information you need. After asking some friends (PowerShell MVPs ROCK!), I was pointed to Win32_Volume, which returns all sorts of information about disk volumes whether they are assigned drive letters or not.

The next issue was how to get the actual information I wanted, and then to format it so that it actually made some sense. For example:

PSH> (Get-WmiObject –ComputerName Server1 –Class Win32_Volume).FreeSpace
21654667264
103541030912
75879378944
142417367040
5500928
565375053824
PSH>

This doesn’t really cut it. OK, let’s try at least getting it into a table:

PSH> Get-WmiObject –ComputerName Server1 –Class Win32_Volume | ft –auto DriveLetter,Label,FreeSpace
DriveLetter Label                               FreeSpace 
----------- -----                               --------- 
C:                                            21655351296 
D:          DATA                             103541030912 
E:          EXCHANGE                          75879378944 
F:          FILES                            142417367040 
Y:          New Volume                            5500928 
            Server1 2014_10_15 10:57 DISK_03 565375053824

Well, that’s a bit more useful, but frankly, that number for the backup volume seems big, but is it 500 GB, or 50 GB? At first glance, I have no idea. And if it’s 50 GB, I’m in trouble, but if it’s 500 GB, we’re fine. So, we need to do a bit of manipulation to the output from Format-Table, and the tool for this is to create an Expression that allows you to calculate and format a result in a way that makes more sense. For this, we use an expression as the property to display. So, for example, to display that “565375053824” as Gigabytes, we use:

PSH>
Get-WmiObject –ComputerName Server1 –Class Win32_Volume `
       | ft –auto DriveLetter,`
                  Label,`
                  @{Label=”Free(GB)”;Expression={'{0:N0}’ –F ($_.FreeSpace/1GB)}}
DriveLetter Label                            Free(GB) 
----------- -----                            -------- 
C:                                           20 
D:          DATA                             96 
E:          EXCHANGE                         71 
F:          FILES                            133 
Y:          New Volume                       0 
            Server1 2014_10_15 10:57 DISK_03 527 
PSH>

Now we’re getting somewhere. But what did we do? We use the @{} to tell Format-Table that we were going to use an expression to define a column of data. The Label=”Free(GB)” creates a new column header, and the Expression={“{0:N0}” –F  means we’re going to have a numeric value (including thousands separators) with no decimal values. The calculated value for the column is ($_.FreeSpace/1GB).

So we now have a useful listing of free space on the remote server. Of course, it might be even more useful to know the percentage free. No problem, for that we use the formatting expression “{0:P0}” to express the column as a percentage, and use the calculation of ($_.FreeSpace/$_.Capacity), letting PowerShell do the work of converting that to a percentage. So:

PSH>
Get-WmiObject –ComputerName Server1 –Class Win32_Volume `
       | ft –auto DriveLetter,`
                  Label,`
                  @{Label=”Free(GB)”;Expression={“{0:N0}” –F ($_.FreeSpace/1GB)}},`
                  @{Label=”%Free”;Expression={“{0:P0}” –F ($_.FreeSpace/$_.Capacity)}}
DriveLetter Label                         Free(GB)  %Free 
----------- -----                         --------  ----- 
C:                                            20      17 % 
D:          DATA                              96      48 % 
E:          EXCHANGE                          71      71 % 
F:          FILES                             133     18 % 
Y:          New Volume                        0       58 % 
            Server1 12014_10_15 10:57 DISK_03 527     51 % 
PSH>

Now we almost have it. Next, it would probably be useful to get the total capacity of the disk while we’re at it, and since I have more than one server, we should probably plan on passing this whole thing an array of computer names. So, the final script, at least for this first pass:

# ********************************************* 
# ScriptName: Get-myFreeSpace.ps1 
# 
# Description: Script to get the free disk space 
#            : on a remote computer and display it usefully 
# 
# ModHist: 26/11/2014 - Initial, Charlie 
#        : 
# 
# 
# ********************************************* 
[CmdletBinding()] 
Param ([Parameter(Mandatory=$False,Position=0)] 
         [String[]]$ComputerName = "Server1") 
Write-Host "" 
ForEach ( $Name in $ComputerName ) { 
   Write-Host "Disk Utilization for server $Name is: " 
   Get-WmiObject  -ComputerName $Name -Class Win32_Volume ` 
      | Format-Table  -auto ` 
         @{Label="Drive";` 
            Expression={$_.DriveLetter};` 
            Align="Right"},` 
         @{Label="Free(GB)";` 
            Expression={"{0:N0}" -f ($_.FreeSpace/1GB)};` 
            Align="Right"},` 
         @{Label="% Free";` 
            Expression={"{0:P0}" -f ($_.FreeSpace / $_.Capacity)};` 
            Align="Right"},` 
         @{Label="Size(GB)";` 
            Expression={"{0:N0}" -f ($_.Capacity / 1GB)};` 
            Align="Right"},` 
         @{Label="Volume Label";` 
            Expression={$_.Label};` 
            Width=25} 
}

You’ll see I tweaked the formatting to right align the calculated expressions, and gave my volume label column some extra space to improve readability. The result is:

PSH> Get-myFreeSpace.ps1 –ComputerName “Server1”,”Server2”
Disk Utilization for server Server1 is:
 Drive Free(GB) % Free Size(GB) Volume Label 
----- -------- ------ -------- ------------ 
   C:       20   17 %      120 
   D:       96   48 %      200 DATA 
   E:       71   71 %      100 EXCHANGE 
   F:      133   18 %      750 FILES 
   Y:        0   58 %        0 New Volume 
           527   51 %    1,024 Server1 2014_10_15 10:57 DISK_03 
   
Disk Utilization for server Server2 is:
Drive Free(GB) % Free Size(GB) Volume Label 
----- -------- ------ -------- ------------ 
             0   25 %        0 
   D:    1,697   53 %    3,214 Data 
   C:      484   95 %      512 
PSH>