Which Hyper-V VM Worker Process Belongs To Which VM?

Warning! This info is not totally accurate! Please read the next day's post for corrections.

I wanted to track down which virtual machine on my Hyper-V host was causing an inordinate amount of disk I/O on my host without logging in to each one.

In Hyper-V, you will notice that on the root partition, you will see one instance of vmms.exe (VM Management Service,) and then you will see a separate instance of vmwp.exe (VM Worker Process) for each virtual machine that is currently running.

Notice that the vmwp.exe processes run under a special user context which contains the GUID that you would find in that virtual machine's configuration files on disk. The same GUID is also supplied to vmwp.exe as an argument as the process is created, like so:

"C:\Windows\System32\vmwp.exe" c83cdee4-1a6d-4f51-9d05-e57df8403ed4

It's not immediately apparent which vmwp.exe belongs to which VM. Furthermore, the I/O charged against each individual vmwp.exe process is not necessarily indicative of what's actually happening on the virtual machine that its hosting. So we'll need to go to the performance counters instead. The "Hyper-V Virtual IDE Controller (Emulated)" set of counters should do the trick, assuming you're using the IDE controller on your VMs.

I have all the information I need now to determine which virtual machine is responsible for the large amount of I/O... but I didn't want to just do it manually. Why not write a reusable tool that can also be run on Core servers with no GUI?

So this simple script, when run on a Hyper-V host, does just that. The output looks like this:

Get-VMPidAndIO | Out-GridView

My VMs were all idle then, hence all the zeros. The screenshot loses all its dramatic flair, but whatever.

The script could easily be enhanced by supporting remote Hyper-V hosts, alternate credentials, etc. But what do you want for 30 minutes?

#Requires -Version 3
function Get-VMPidAndIO
{
<#
.SYNOPSIS
	Gets the Process ID and I/O statistics of each virtual machine running on the Hyper-V host.
.DESCRIPTION
	Gets the Process ID and I/O statistics of each virtual machine running on the Hyper-V host.
    Currently only works for VMs using virtual IDE controllers.
    Requires Powershell 3 at a minimum.
.LINK
    http://myotherpcisacloud.com
.NOTES
    Written by Ryan Ries, June 2013.
    ryan@myotherpcisacloud.com
#>
    BEGIN
    {
        Try
        {
            $VMProcesses = Get-CimInstance -Query "Select ProcessId,CommandLine From Win32_Process Where Name ='vmwp.exe'" -ErrorAction Stop
        }
        Catch
        {
            Write-Error $_.Exception.Message
            Return
        }
    }
    PROCESS
    {

    }
    END
    {
        Foreach($_ In $VMProcesses) 
        {
            $VMName = $((Get-VM | Where Id -EQ $_.CommandLine.Split(' ')[-1]).Name)            
            [PSCustomObject]@{PID=$_.ProcessId;
                              VMName=$VMName; 
                              ReadBytesPerSec=[Math]::Round($(Get-Counter "\Hyper-V Virtual IDE Controller (Emulated)($VMName`:Ide Controller)\Read Bytes/sec").CounterSamples.CookedValue, 2);
                              WriteBytesPerSec=[Math]::Round($(Get-Counter "\Hyper-V Virtual IDE Controller (Emulated)($VMName`:Ide Controller)\Write Bytes/sec").CounterSamples.CookedValue, 2); }
        }

    }
}

Set Windows Update Schedule on Server 2012 Core with Local Policy

I'm seeing a lot more Server Core deployments as 2012 adoption increases. Which I think is awesome - I love Server Core. But there are still a couple things that were completely trivial on a GUI edition of Windows that are a tad tricky on Core.

For instance, here I am on Server 2012 Core using the sconfig utility to set up Windows Automatic Updates:

sconfig

This is just a lab environment, so I don't mind automatic updates and reboots. But I have two Active Directory domain controllers in this lab, and I don't want them rebooting at the same time. So how do I change the "every day at 3:00 AM" schedule so that I can stagger the patching and reboots? On a GUI install it would be trivial of course. Here, not as much.

The first thing I did was briefly look for a registry entry in HKLM:\Software\Microsoft\Windows\CurrentVersion\WindowsUpdate\, but I didn't see anything that looked like it would help me modify the Windows Update schedule like I wanted to.

These are domain members, so of course I could use Group Policy to do it, but I didn't want to create and link separate GPOs for each server that I wanted to have a different Windows Update schedule. Plus I wanted to figure out how to do it on non domain-joined machines as well.

Ah - Local Policies! To edit local policies on Core servers, we'll need to connect to them remotely from what I like to call "a tools machine." When you have a bunch of Server Core machines running out there, you should also keep one machine around that has a full GUI install. I personally like to install all my tools (like RSAT) on that one machine, and use it to centrally manage all of the Core machines remotely.

Here I am using mmc on my tools machine to add Group Policy Object Editor snapins for several of the Server Core computers: 

Local Policies

Again, the name of the snapin is Group Policy Object Editor, and target the remote machine as you add the snapin. You'll of course need RPC over TCP connectivity to the remote machine, and you'll need to modify Windows Firewall on the remote machine to allow the incoming connection. (I like to use domain Group Policies for that, so all my machines have consistent firewall settings.)

All that's left to do now is navigate to Computer Configuration > Administrative Templates > Windows Components > Windows Update, and configure the "Configure Automatic Updates" setting for the server. It allows you to modify the hour and the day of the week that Windows Update will download and install updates on that machine.

As per Active Directory Group Policy application precedence, remember that any conflicting domain-based GPO will override settings you make to a machine's Local Policy. 

Lastly - don't forget that automatic update option 4 - "Auto download and schedule the install" - is the only option here that applies to Server Core. The others won't work because Server Core can't "notify" the user of updates the way it could were the GUI installed.

My Entry for the Advanced Event #3 of the 2013 Scripting Games

Halfway done.  Here's my third entry for this year's Powershell games.  I used a workflow this time, mostly in an attempt to garner favor from the voters for using new features exclusive to PS3.  Even though the multithreading with jobs that I did in the last event is a neat idea, it really doesn't perform very well.  The workflow will likely perform better, though I don't know if it's going to handle the throttling of thread creation if I handed it a list of 500 computers.

#Requires -Version 3
Function New-DiskSpaceReport
{
	<#
		.SYNOPSIS
			Gets hard drive information from one or more computers and saves it as HTML reports.
		.DESCRIPTION
			Gets hard drive information from one or more computers and saves it as HTML reports.
			The reports are saved to the specified directory with the name of the computer in
			the filename. The list of computers is processed in parallel for increased speed.
			Use the -Verbose switch if you want to see console output, which is very useful if you
			are having problems generating all the desired reports.
		.PARAMETER ComputerName
			One or more computer names from which to get information. This can be a
			comma-separated list, or a file of computer names one per line. The alias
			of this parameter is -Computer. The default value is the local computer.
		.PARAMETER Directory
			The directory to write the HTML files to. E.g., C:\Reports. The directory
			must exist. The default is the current working directory.
		.INPUTS
			[String[]]$ComputerName
			This is an array of strings representing the hostnames of the computers
			for which you want to retrieve information. This can also be supplied by
			(Get-Content file.txt). This can be piped into the cmdlet.
		.INPUTS
			[String]$Directory
			The directory to save the HTML reports to. The directory must exist.
		.OUTPUTS
			HTML files representing the information obtained from all
			the computers supplied to the cmdlet.
		.EXAMPLE
			New-DiskSpaceReport
			
			This will generate a report for the local computer and output the HTML file to
			the current working directory.			
		.EXAMPLE
			New-DiskSpaceReport -ComputerName server01,server02,server03 -Directory C:\Reports
			
			This will generate three HTML reports for the servers and save them in the C:\Reports
			directory.
		.EXAMPLE
			New-DiskSpaceReport -Computer (Get-Content .\computers.txt)
			
			This will generate HTML reports for all the computers in the computers.txt file and
			save the reports in the current working directory.
		.EXAMPLE
			,(Get-Content .\computers.txt) | New-DiskSpaceReport -Directory C:\Reports
			
			This will generate HTML reports for all the computers in the computers.txt file and
			save the reports in C:\Reports. Please note the leading comma in this example.
		.NOTES
			Scripting Games 2013 Advanced Event 3
	#>
	[CmdletBinding()]
	Param([Parameter(ValueFromPipeline=$True)]
			[Alias('Computer')]
			[String[]]$ComputerName = $Env:Computername,
		  [Parameter()]
			[ValidateScript({Test-Path $_ -PathType Container})]
			[String]$Directory = (Get-Location).Path)
	
	Write-Verbose -Message "Writing reports to $Directory..."
	
	WorkFlow BuildReports
	{
		Param([String[]]$Computers, [String]$Directory)
		ForEach -Parallel ($Computer In $Computers)
		{			
			InlineScript
			{				
				Write-Verbose -Message "Generating report for $Using:Computer..."
				$Header = @'
				<title>Disk Free Space Report</title>
				<style type=""text/css"">
					<!--
						TABLE { border-width: 1px; border-style: solid;  border-color: black; }
						TD    { border-width: 1px; border-style: dotted; border-color: black; }
					-->
				</style>
'@
				$Pre  = "<p><h2>Local Fixed Disk Report for $Using:Computer</h2></p>"
				$Post = "<hr><p style=`"font-size: 10px; font-style: italic;`">This report was generated on $(Get-Date)</p>"
				Try
				{					
					$LogicalDisks = Get-WMIObject -Query "SELECT * FROM Win32_LogicalDisk WHERE DriveType = 3" -ComputerName $Using:Computer -ErrorAction Stop | Select-Object -Property DeviceID,@{Label='SizeGB';Expression={"{0:N2}" -F ($_.Size/1GB)}},@{Label='FreeMB';Expression={"{0:N2}" -F ($_.FreeSpace/1MB)}},@{Label='PercentFree';Expression={"{0:N2}" -F (($_.Freespace/$_.Size)*100)}};
					$LogicalDisks | ConvertTo-HTML -Property DeviceID, SizeGB, FreeMB, PercentFree -Head $Header -PreContent $Pre -PostContent $Post | Out-File -FilePath $(Join-Path -Path $Using:Directory -ChildPath $Using:Computer`.html)
					Write-Verbose -Message "Report generated for $Using:Computer."
				}
				Catch
				{
					Write-Verbose -Message "Cannot build report for $Using:Computer. $($_.Exception.Message)"
				}
			}
		}
	}
	
	If($PSBoundParameters['Verbose'])
	{
		BuildReports -Computers $ComputerName -Directory $Directory -Verbose
	}
	Else
	{
		BuildReports -Computers $ComputerName -Directory $Directory
	}
}

My Entry for the Advanced Event #2 of the 2013 Scripting Games

More Powershell! I'm somewhat proud of this script.

#Requires -Version 3
Function Get-ComputerInfo
{
	<#
		.SYNOPSIS
			Gets some basic system information about one or more remote Windows computers.
		.DESCRIPTION
			Gets some basic system information about one or more remote Windows computers.
			Specifically designed to be able to fetch information from any version of
			Windows computer from Windows 2000 up. This Cmdlet takes only one parameter,
			-ComputerName. ComputerName can be a single computer name or IP address, or it
			can be an array of computer names. You can also use a file of computer hostnames,
			one per line. This function will return the information gathered from all
			of the computers. Remember to use a leading comma when piping an array to
			this cmdlet. See the examples for more details. Powershell 3.0 is the minimum
			required on the machine that runs this cmdlet, though the target computers 
			do not need Powershell at all. Use Get-Help Get-ComputerInfo -Examples  to see
			usage examples. Example 8 is my favorite!
		.PARAMETER ComputerName
			One or more computer names from which to get information. This can be a
			comma-separated list, or a file of computer names one per line. The alias
			of this parameter is -Computer.
		.PARAMETER MaxThreads
			Default is 4. This is the maximum number of threads that are allowed to
			run simultaneously. This is useful because network operations can block
			for a long time, making threading desirable. However, when using a very 
			large list of computers, spawning a huge number of concurrent threads can
			be detrimental to the system, so thread creation should be throttled.
			The max is 32. The alias for this parameter is -Threads.
		.INPUTS
			[String[]]$ComputerName
			This is an array of strings representing the hostnames of the computers
			for which you want to retrieve information. This can also be supplied by
			(Get-Content file.txt). This can be piped into Get-ComputerInfo.
		.OUTPUTS
			A collection of objects representing the information obtained from all
			the computers supplied to the cmdlet.
		.EXAMPLE
			Get-ComputerInfo server1,server2,server3
		.EXAMPLE
			Get-ComputerInfo -ComputerName server1,server2,server3 | Format-Table
		.EXAMPLE
			Get-ComputerInfo -ComputerName (Get-Content .\computers.txt) -MaxThreads 8
		.EXAMPLE
			,(Get-Content .\computers.txt) | Get-ComputerInfo -Threads 12
			
			(Please note the leading comma in this example.)
		.EXAMPLE
			,("server1","server2","server3") | Get-ComputerInfo
			
			(Please note the leading comma in this example.)
		.EXAMPLE
			$Computers = @("server1","server2","server3")
			,$Computers | Get-ComputerInfo
		
			(Please note the leading comma in this example.)
		.EXAMPLE
			"server1" | Get-ComputerInfo
		.EXAMPLE
			Get-ComputerInfo -ComputerName ($(Get-ADComputer -Filter *).Name) | Out-GridView
		.NOTES
			Scripting Games 2013 Advanced Event 2
	#>
	[CmdletBinding()]
	Param([Parameter(Mandatory = $True, ValueFromPipeline=$True, HelpMessage = 'Computer names to scan, e.g. server01,server02,server03')]
			[Alias('Computer')]
			[String[]]$ComputerName,
		  [Parameter(Mandatory = $False)]
			[Alias('Threads')]
			[ValidateRange(1, 32)]
			[Int]$MaxThreads = 4)
	
	# This is the collection of objects that this function will eventually return.
	$ComputerInfoCollection = @()
	
	# By using the unique job name of "GetComputerInfo", we avoid interfering with any other
	# unrelated jobs that might be running by coincidence.
	$JobName = "GetComputerInfo"
	
	# Clear any old jobs with the same name before we begin. -EA Stop ensures that errors will be caught.
	Try
	{
		Get-Job -Name $JobName -ErrorAction Stop | Remove-Job -Force
	}
	Catch
	{
		# No jobs with the name $JobName were running. We don't care.
	}
	
	# This is the work to be performed by each thread in a Start-Job command.
	$Work = {
		$ComputerInfo = [PSCustomObject]@{ Name = $Args[0]; IPAddresses = $null; OSCaption = $null; MegaBytesRAM = $null; CPUSockets = $null; TotalCores = $null; }
		Try
		{			
			$ComputerInfo.IPAddresses = $([System.Net.Dns]::GetHostEntry($Args[0])).AddressList
		}
		Catch
		{
			# The hostname did not resolve to an IP address, so there is no reason to keep going.
			$ComputerInfo.IPAddresses = "Could not resolve name!"
			Return $ComputerInfo
		}
		Try
		{
			$ComputerInfo.OSCaption = $(Get-WMIObject Win32_OperatingSystem -ComputerName $Args[0] -ErrorAction Stop).Caption
		}
		Catch
		{
			$ComputerInfo.OSCaption = "$($_.Exception.Message)"
		}
		Try
		{
			$ComputerInfo.MegaBytesRAM = [Math]::Round($(Get-WMIObject Win32_ComputerSystem -ComputerName $Args[0] -ErrorAction Stop).TotalPhysicalMemory / 1MB, 0)
		}
		Catch
		{
			$ComputerInfo.MegaBytesRAM = "$($_.Exception.Message)"
		}
		Try
		{
			$CPUInfo = Get-WMIObject Win32_Processor -ComputerName $Args[0] -ErrorAction Stop
			
            # SocketDesignation does not exist on Server 2000
            # $ComputerInfo.CPUSockets = $CPUInfo.SocketDesignation.Count
            # Also, Win 2000 does not care about Hyperthreading and does not distinguish
            # cores from sockets AFAIK, so TotalCores will be null if Win 2000. Not a big deal IMO.
            $ComputerInfo.CPUSockets = $CPUInfo.DeviceID.Count
			ForEach($CPU In $CPUInfo)
			{
				$Cores += $CPU.NumberOfCores
			}
			$ComputerInfo.TotalCores = $Cores
		}
		Catch
		{
			$ComputerInfo.CPUSockets = "$($_.Exception.Message)"
			$ComputerInfo.TotalCores = "$($_.Exception.Message)"
		}
		
		Return $ComputerInfo
	}
	
	ForEach($Computer In $ComputerName)
	{
		While($(Get-Job -State "Running" | Where-Object Name -EQ $JobName).Count -GE $MaxThreads)
		{
			# Max number of concurrent running threads reached - sleep until one is available.
			Start-Sleep -Milliseconds 500
		}
		Start-Job -Name $JobName -ScriptBlock $Work -ArgumentList $Computer | Out-Null
	}
	
	# Wait for all jobs to finish.
	# Get-Job -State "Running" -Name $JobName does not work for some reason, so let's do it in two steps.
	While(Get-Job -State "Running" | Where-Object Name -EQ $JobName)
	{
		Start-Sleep -Milliseconds 500
	}
	
	# Jobs are done, let's collect the results and store it in our collection.
	ForEach($Job In Get-Job -Name $JobName)
	{
		$ComputerInfoCollection += Receive-Job $Job
	}
	
	Return $ComputerInfoCollection
}

Probably the Craziest Powershell One-Liner I've Written To Date

Someone at work asked me to identify duplicate computers in two separate AD forests, and remove the one that was no longer needed.  It's assumed as part of business policy that there should not be duplicate server hostnames anywhere in the company - even if they reside in different forests or domains.  But for some reason or another, a computer might get migrated from DomainA to DomainB, but the computer object stays behind in the old domain, etc.  So I decided to just collect all the computers from DomainA and DomainB (in ForestA and ForestB respectively), point out the computer accounts that had the same name in each domain, and list their PwdLastSet attribute next to their name.  If the machine had not updated its password in over 30 days in DomainA, while the machine password was up to date in DomainB, then it was reasonably safe to assume that the machine had been migrated out of DomainA and into DomainB, or vice versa.

I only had Powershell v2 on hand, so I didn't have the relative luxury of automatic foreach, etc.  After collecting the computer objects like $DomainAComputers = Get-ADComputer -Filter * -Properties *, check out this hideous monstrosity I came up with to compare them in a single line:

PS C:\Users\ryan> foreach($C In $(Compare-Object $($DomainAComputers|?{!($_.DistinguishedName.Contains("Disabled Accounts"))}|%{$_.Name}) $($DomainBComputers|?{!($_.DistinguishedName.Contains("Disabled Accounts"))}|%{$_.Name}) -IncludeEqual | ? { $_.SideIndicator -eq "==" })) { $o = $($DomainAComputers|?{$_.Name -eq $C.InputObject}); $n = $($DomainBComputers|?{$_.Name -eq $C.InputObject}); $o.DnsHostName + "`t" + $o.PasswordLastSet + "`t" + $n.DnsHostName + "`t" + $n.PasswordLastSet }

The output looks like this:

computer1.domainA.com    04/17/2013    computer1.domainB.com    01/21/2010
computer2.domainA.com    05/05/2013    computer2.domainB.com    10/11/2011
etc...

You can easily see now that the two computers in DomainA are active, while the computer objects of the same name in DomainB are stale, so I'll delete them.

Now don't get me wrong - this is not elegant or clever. It's thoroughly unreadable and ugly and I'd not brag about it except to say, "Haha, look how much s*#! I can cram on one single line of Powershell!"

A couple things that I thought were interesting:

  • Get-ADComputer gives you a free pseudo-attribute called PasswordLastSet, which is a nicely formatted DateTime. But it's not a "real" attribute of the object in Active Directory. Rather, it's the Powershell cmdlet's courtesy attribute where it automatically converts the real attribute - PwdLastSet - from file time (epoch seconds) to a .NET DateTime object. Many of the Active Directory cmdlets work that way.
  • Compare-Object -ExcludeDifferent didn't seem to work and I'm not sure why.  So I had to just use -IncludeEqual instead and isolate the names that were equal.