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>

Finding out what RAM DIMMs are installed on a computer without opening the box

OK, so I needed to know exactly what RAM was installed on a computer and how many slots there were. I could have had the user shutdown his machine, opened up the box, got out a flashlight, popped out the DIMMs, written down the obscure part numbers, plugged them back in, and closed it up. But not only was that way too much like work, it would totally have disrupted his workday. So, instead, I let PowerShell and WMI do the heavy lisfting. The following script will get the currently installed memory modules, the maximum RAM supported on the computer, and the number slots available.



#****************************************

#

# Script Name: Get-MemoryModule.ps1

#

# Script to get the number of memory slots, and the installed memory, on

# the local computer. Could easily be updated to go against remote computers.

#

# ModHist: 12/29/2012 - initial, Charlie.

#        : 08/26/2014 - Charlie. Added logic to get slots and max RAM.

#        :

#****************************************

$strComputer = "."

$ComputerName = (hostname)

$colSlots = Get-WmiObject `

               -class "Win32_PhysicalMemoryArray" `

               -namespace "root\CIMV2" `

               -Computername $strComputer

$nSlots = $colSlots.MemoryDevices

$nMax = $colSlots.MaxCapacity

$gbMax = $nMax/(1024*1024)

$colModules = Get-WMIObject `

                -class "Win32_PhysicalMemory" `

                -namespace "root\CIMV2" `

                -computername $strComputer

foreach ($objItem in $colModules) {

Write-host "Bank Label: " $objItem.BankLabel

write-host "Capacity: " $objItem.Capacity

write-host "Caption: " $objItem.Caption

write-host "Creation Class Name: " $objItem.CreationClassName

write-host "Data Width: " $objItem.DataWidth

write-host "Description: " $objItem.Description

write-host "Device Locator: " $objItem.DeviceLocator

write-host "Form Factor: " $objItem.FormFactor

write-host "Hot-Swappable: " $objItem.HotSwappable

write-host "Installation Date: " $objItem.InstallDate

write-host "Interleave Data Depth: " $objItem.InterleaveDataDepth

write-host "Interleave Position: " $objItem.InterleavePosition

write-host "Manufacturer: " $objItem.Manufacturer

write-host "Memory Type: " $objItem.MemoryType

write-host "Model: " $objItem.Model

write-host "Name: " $objItem.Name

write-host "Other Identifying Information: " $objItem.OtherIdentifyingInfo

write-host "Part Number: " $objItem.PartNumber

write-host "Position In Row: " $objItem.PositionInRow

write-host "Powered-On: " $objItem.PoweredOn

write-host "Removable: " $objItem.Removable

write-host "Replaceable: " $objItem.Replaceable

write-host "Serial Number: " $objItem.SerialNumber

write-host "SKU: " $objItem.SKU

write-host "Speed: " $objItem.Speed

write-host "Status: " $objItem.Status

write-host "Tag: " $objItem.Tag

write-host "Total Width: " $objItem.TotalWidth

write-host "Type Detail: " $objItem.TypeDetail

write-host "Version: " $objItem.Version

write-host

}

Write-Host "Computer $ComputerName has $nSlots Memory Slots, and a Maximum Memory of $gbMax GigaBytes of RAM" $colModules | ft -auto "BankLabel",@{Label="CurrentMem(MB)";Expression={$_.Capacity/(1024*1024)}},"Speed"

I’ve shown the “local” version of this, but it would be trivial to modify the script to run against a remote computer by adding support for a –ComputerName parameter. Also, at least some computers (most notably laptops) will not report their maximum supported memory correctly. I’ve run this script against the 6 physical computers running here and all worked fine expect for my HP laptop. That has 16 GB of RAM in it, but says the maximum RAM is 8 GB.

Getting the IP addresses of running VMs

 

Ben Armstrong posted a great little tip on his blog the other day. He has a little one-line PowerShell command that gives you a listing of all the running VMs on a host, and the IP addresses being used by each of them.
Get-VM | ?{$_.State -eq "Running"}   Get-VMNetworkAdapter | Select VMName, IPAddresses

Add the -ComputerName parameter, with the name of your Hyper-V server in the above, and you've got a really useful little script to figure out which VM has an address it shouldn't. Of course, I just had to tweak it a bit, by changing that last Select to a simple format-table, which allows me to get rid of the unnecessary whitespace with a -auto parameter. Thus:
PSH> Get-VM -computername cpr-asus-lap | ?{$_.State -eq "Running"} | Get-VMNetworkAdapter | ft -auto VMName, IPAddresses
VMName      IPAddresses
------      -----------
trey-cdc-05 {192.168.10.5, fe80::812e:a888:ac40:666b, 2001:db8:0:10::5}
trey-dc-02  {192.168.10.2, fe80::312c:a27c:c87e:3f98, 2001:db8:0:10::2}
trey-wds-11 {192.168.10.11, fe80::4520:ea29:54bb:9b41, 2001:db8:0:10::b}

Thanks, Ben. That's a useful one!

Shutting Down Running VMs

A quick Hyper-V PowerShell one-liner today. This one will gracefully stop all virtual machines with RODC in their name.
 Get-VM -Name *rodc* | Where-Object {$_.State -eq "Running" } | Foreach-Object { Stop-VM $_.Name }

I needed this because I was cloning an RODC that I had virtualized, and I wanted a quick way to shut it down gracefully without actually logging on to it. (It's on a private subnet that is a nuisance for some things.) Then I simply extended it a bit to get all my RODC's and to make sure that I didn't try to stop a VM that was already stopped.

Charlie.

And, because I'm lazy and don't want to bother logging in to my VM host server, I modified this to get all the running VMs on the server and then stop them.
Get-VM -ComputerName Trey-VMHost `
          | Where-Object {$_.State -eq "Running" } `
          | Foreach-Object { Stop-VM $_.Name -ComputerName $_.ComputerName }

Notice that by the time I get to the end of the pipeline, I need to tell Stop-VM what computer these are running on. No problem, that's part of the object I'm passing in to it anyway. And, you could easily extend this by providing an array of host names to Get-VM to stop multiple VMs on multiple hosts.

Set Network Location to Private in Windows 8.1/Server 2012 R2

One of those annoyances that sometimes happen with the new Network Location in Windows 8.x is that the network gets mis-identified as Public when it should be Private, or the other way around. Changing this in the GUI is certainly possible, but annoying, so let's take advantage of the improved Windows PowerShell support in Windows 8.1 and do it quickly and easily. First, let's open up an elevated PowerShell window from our limited user session:
PSH> Start-Process WindowsPowerShell.exe -verb RunAs

Now, in that window, let's find out what our current network location is set to:
PSH> Get-NetConnectionProfile
...<snip>...
Name : Unidentified network
InterfaceAlias : vEthernet (Local-10)
InterfaceIndex : 18
NetworkCategory : Public
IPv4Connectivity : LocalNetwork
IPv6Connectivity : LocalNetwork

From this, we see that the problem interface has an Interface Index of 18, so:
PSH> Set-NetConnectionProfile  -InterfaceIndex 18 -NetworkCategory Private

And we're done.

Installing .Net 3.5 (and earlier) on Windows 8 and Windows 8.1

There are some older applications that require earlier versions of .NET than the version included and enabled by default on Windows 8 and Windows 8.1. Normally you can enable the .NET Framework 3.5 (which includes .NET 2.0 and .NET 3.0) as a Windows Feature. (Control Panel, Programs and Features, Turn Windows Features on or off.) But in some environments, especially those using SBS and WSUS, you may get an error message:
0x800f0906: “Windows couldn't connect to the Internet to download necessary files. Make sure that you're connected to the Internet, and click Retry to try again.”

This, of course, is totally bogus because the computer IS connected to the Internet, but save yourself some grief and do it from the command line in the first place. The files you need are part of the DVD (or USB stick or whatever) that you used to install Windows. Just insert that media and from an elevated command or PowerShell prompt type:
PSH> DISM /Online /Enable-Feature /FeatureName:NetFx3 /All /LimitAccess /Source:E:\Sources\SxS Deployment Image Servicing and Management tool Version: 6.3.9600.16384 Image Version: 6.3.9600.16384 Enabling feature(s) [==========================100.0%==========================] The operation completed successfully.

Where the E: drive in E:\Sources\SxS  is replaced by the drive letter of the DVD, USB or other media source or your Windows distribution media. Or, the pure PowerShell way:
PSH> Enable-WindowsOptionalFeature -Online -FeatureName "NetFx3" -All -LimitAccess -Source "E:\Sources\SxS"

Now, whatever application it is that needs this older version of the .NET Framework will be happy. (Zune 4.8, in my case.)

Updated: 13 November 2013: Added PSH equivalent.
:   2 June, 2014: Fixed curly quotes
:   13 October, 2014: Confirmed, the same command works in Windows 10 Preview

 

Getting Drive Information Using WMI and PowerShell

In my ongoing quest to remove dependency on legacy DOS commands, I recently created this script to get a list of all the local and mapped drives on a machine and how much free space each has on it.  This script could be easily extended to display additional information (such as the underlying filesystem type) by simply adding to the output tables. All the information is already captured in the Get-WMIObject call at the beginning, but the fun is in the formatting (and math)  of the tables to provide useful information at a glance.
 # Filename: GetDrives.ps1
 #
 # A script to get a list and information on the "local" drives without using "Net"
 #
 # ModHist: 08/05/2013 - Initial
 #                   :
 #
 # ***********************************************************************
 $LogicalDisks = Get-WMIObject Win32_LogicalDisk
 $LocalHDisks = $LogicalDisks | Where-Object { $_.DriveType -eq 3 }
 $RemoteDrives = Get-WMIObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq 4 }
 $OpticalDrives = Get-WMIObject Win32_CDRomDrive
$LocalHDisks | ft -auto @{Label="Drive";`
                           Expression={$_.DeviceID};
                           width=5
                           align="Right"},`
                        @{Label="Volume Label";`
                           Expression={$_.VolumeName};                                                         
                           Width=25},` 
                        @{Label="%Free";`
                           Expression={[int]($_.FreeSpace/$_.Size * 100)};`
                           Width=8},`
                        @{Label="GBFree";`
                           Expression={$([math]::round(($_.FreeSpace/1gb),0))};`
                           Width=8},`
                        @{Label="Size(GB)";`
                           Expression={$([math]::round(($_.Size/1gb),0))};`
                           Width=8}
 $OpticalDrives | Sort Drive | ft -auto `
                        @{Label="Drive";`
                           Expression={$_.Drive};`
                           Width=5
                           Align="Right"},
                        @{Label="Capabilities";`
                           Expression={ if ($_.Capabilities -eq 4) { `
                             "Read/Write" `
                           } else { `
                             "Read-Only"}}},`
                        @{Label="Disk Volume Label";`
                           Expression={$_.VolumeName}}
 $RemoteDrives | ft -auto @{Label="Drive";`
                           Expression={$_.DeviceID};`
                           width=5
                           align="Right"},`
                        @{Label="Remote Share";`
                           Expression={$_.ProviderName};`
                           Width=25},
                        @{Label="%Free";`
                           Expression={[int]($_.FreeSpace/$_.Size * 100)};`
                           Width=8},`
                        @{Label="GBFree";`
                           Expression={$([math]::round(($_.FreeSpace/1gb),0))};`
                           Width=8},`
                        @{Label="Size(GB)";`
                           Expression={$([math]::round(($_.Size/1gb),0))};`
                           Width=8}
 

If you find this script interesting or useful, great. And if you have improvements to it, I'd be delighted to hear about them.

Charlie.

More $profile madness

As usual, a minor glitch in my workflow has me mucking around with my $profile. It all started because I normally run Word, Excel, and other Office applications from the command line (as I do many non-MS applications.) To do this, I have functions and aliases in my $profile. These let me start Word, for example, by typing "word" on the PowerShell command line along with any filenames I want to open. So far, so good. But I recently started using Office 2013 on some machines, and this broke my functions for those machines. And at least one of them is running the 640-bit version of office. Hmmm. That could just be a problem. Now I could manually edit the functions in those $profiles, but that's just kludgy. I like to have a single $profile that I load on any new machine I work on. So, rather than the hard-coded paths, which were never really elegant, but worked fine when I only had one version running, it's time to be a bit more general. So, I added the following function to my $profile:
Function Find-Office {
   # if on 32-bit, or running 64-bit office on 64-bit machine, this will find office
   $Global:[[code]]czo2Olwib2ZmaWNlXCI7e1smKiZdfQ==[[/code]] = Get-ChildItem `
              -file "Outlook.exe" `
              -path "C:\Program Files\Microsoft Office\" `
              -recurse
    if (! $office) {
    # If we didn't find it there, look down the (x86) path. But only if we didn't find it
    $Global:[[code]]czo2Olwib2ZmaWNlXCI7e1smKiZdfQ==[[/code]] = Get-ChildItem `
              -file "Outlook.exe" `
              -path "C:\Program Files (x86)\Microsoft Office\" `
              -recurse
    }
}

Now, a couple of comments here -- I made the $Office variable Global so that I could get at the value outside of my profile, since there's a fair amount of information and properties in it.  The other thing is that while I'm searching for Outlook, it really could have been any of the core Office apps that I know get installed every time I install Office. If you're running Office Home & Student edition, however, you'd best choose a different executable, such as "winword.exe".

So, now that we know where Office is, we can extract the path to it:
$OffPath = (Find-Office).DirectoryName

(note, we're still inside my $profile, but we won't bother to make $OffPath global, since we can easily get it again if we need it. )

Finally, the functions that our aliases will call to start the core Office applications:
function Run-Word { & "$OffPath\WinWord.exe" $args}
function Run-Excel { & "$OffPath\Excel.exe" $args }
function Run-OneNote { & "$OffPath\OneNote.exe" $args }
function Run-Outlook { & "$OffPath\Outlook.exe" $args }

The aliases are simply calls to these functions in the form:
set-alias word Run-Word

Nothing fancy there. But now I've got easy command line access to my core Office applications. (As you can see, I don't use PowerPoint any more than I have to. :))

OneNote 2010 and Windows Server 2012 RD Session Host

You can install Office 2010 in an RD Session Host (ie, Terminal Server), if you have a volume license version. (If you're using MSDN versions for testing, there's a special key for use in an RD Session Host environment see: http://support.microsoft.com/kb/983443.)

Great, and you can go ahead and use all the various features of Office. Except OneNote, which fails with an error message telling you to install the Desktop Experience Feature since you're on Windows Server. Well, that's great, and if you're on Server 2008 R2, that's the solution. BUT, if you're running on Windows Server 2012, there is no longer a Desktop Experience Feature. It's GONE. However, what OneNote is looking for is "Ink and Handwriting Services ".

To solve the problem, and install the necessary feature, run:
PS C:\> Add-WindowsFeature InkAndHandwritingServices

This will automatically load the ServerManager module (isn't PowerShell v3 cool?!) and add the necessary feature. This WILL need a reboot, so be prepared.

Charlie.

 

Windows Azure PowerShell

For those of you using Windows Azure, we finally have official PowerShell support for managing your Azure subscriptions. With the CTP release of the 1.7 SDK, you can install the Windows Azure PowerShell cmdlets. The download is here.

 

Keep in mind that this is a CTP, and the first version of the cmdlets released,  so there are a few rough edges and missing functionality still. But they are already very usable and a big improvement if you need to manage more than a single image in Windows Azure. As I get time, I'll be posting more stuff here, but I also recommend you add Michael Washam's blog to your favourites.

 

Charlie.