Skip to content

Instantly share code, notes, and snippets.

@stephanlinke
Created July 26, 2018 06:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stephanlinke/3cf39a40dea862171e383fc9553899b1 to your computer and use it in GitHub Desktop.
Save stephanlinke/3cf39a40dea862171e383fc9553899b1 to your computer and use it in GitHub Desktop.
PRTG-HyperV - Monitor SCVMM host servers and virtual machines within PRTG
#requires -version 4.0
# ___ ___ _____ ___
#| _ \ _ \_ _/ __|
#| _/ / | || (_ |
#|_| |_|_\ |_| \___|
# SCVMM & HyperV-VM Sensors
# ================================
# This sensor will monitor HyperV hosts and their virtual machines using SCVMM.
# It also supports metascan (https://kb.paessler.com/en/topic/68109), so you don't have
# to do all the configuration work. Note that this will only work in an upcoming stable release
#
# Version History
# ----------------------------
# 1.4 [Added] Support for Remote Probes
# 1.3 [Changed] Switched to HTTP Push Sensor based system. No device templates needed, better for large environments.
# [Notes ] It's an simple EXE/Script sensor now, no longer EXE/Script Advanced.
# Device templates are no longer needed, as the sensor automatically creates new sensors for new hosts and VMs with each run
# With every run, we only run Get-VMHost and Get-VM once on the SCVMM host, receiving all objects at once, not once for every VM
# A GUID file is maintained for every SCVMM host that will prevent sensor duplicates
# Performance wise, it should be way easier and faster. Three VMs and one host take 2s to scan.
# Better, more verbose script output to see what the script actually does, receives and evaluates.
# 1.2 [Changed] Updated device template, changed naming template of VM sensors
# 1.13 [Fixed] Paramter was wrongly passed to Get-VMHost and Get-VM
# 1.12 [Fixed] Values weren't retrieved correctly when multiple VMs were present.
# 1.1 [Fixed] Values weren't retrieved correctly due to lookup bug
# 1.0 Initial Release
#
# Credits
# ----------------------------
# RICHARD GARNER, computerservicecentre.com
# For going through countless tests and bugfixing with me. And improvement ideas - Thanks! :)
# # # # # # # # # # # # # # # # # # # # # # # # # #
<#
.SYNOPSIS
This sensor will monitor HyperV hosts and their virtual machines using SCVMM.
.DESCRIPTION
Installation:
1. Save the script as PRTG-SCVMM-HyperV.ps1 under <PRTG Application folder>\Custom Sensors\EXE\
2. Open up your SCVMM device in PRTG
3. Add a new HTTP Push Data (Advanced) sensor to the device, name it any way you like (e.g. "Template Sensor"), and pause it. Note it's sensor ID
4. Create a new EXE/Script sensor and select the PRTG-SCVMM-HyperV.ps1 script
5. Set the timeout to 900 seconds
6. Enter the following parameters:
-prtgHostName <your prtg url, e.g. http://prtg.acme.com or https://prtg.acme.com>
-prtgPushPort <the push port you're using in the template sensor
-prtgProbeAddress <the address of the remote probe the sensor resides on (if so)>
-prtgUserName <a user with read/write permissions to the device, or a PRTG administrator>
-prtgPasshash <the passhash of the above user>
-prtgTemplateSensor <the ID (not token) of the HTTP Push sensor>
-prtgScvmmDeviceID %deviceid
-ComputerName %host
-Userdomain %windowsdomain
-Username %windowsuser
-Password %windowspassword
When you enter the data in the script directly, you can also test it on the fly. Initially, it will look like the attached screenshot #1, consequential runs will look like #2.
Note that the sensors may not have data directly after the first run. This is due to the startup of the sensors taking some time and the script may push values prior to that.
.PARAMETER prtgHostName
The URL of your PRTG webinterface, e.g. http://prtg.acme.com or https://prtg.acme.com
.PARAMETER prtgPort
The port used by your PRTG webinterface, e.g. 80 or 443
.PARAMETER prtgPushPort
The push port you configured in the template HTTP Push Advanced sensor
.PARAMETER prtgProbeAddress
If your SCVMM is monitored by a remote probe, enter it's address here, e.g. http://prtgprobe.acme.com
.PARAMETER prtgUserName
A user with read/write permissions to the device, or a PRTG administrator
.PARAMETER prtgPassHash
The passhash of the above user
.PARAMETER prtgScvmmDeviceId
This is the ID of the device that resembles your SCVMM server - use %deviceid
.PARAMETER prtgTemplateSensor
This is the ID of the HTTP Push Data (Advanced) template sensor (not the sensor token, but the PRTG ID)
.PARAMETER ComputerName
Your SCVMM host - use %host here.
.PARAMETER Userdomain
The domain of the user that will be used for remote powershell access - use %windowsdomain
.PARAMETER Username
The username of the user that will be used for remote powershell access - use %windowsuser
.PARAMETER Password
The password of the user that will be used for remote powershell access - use %windowspassword
.PARAMETER verbose
When this is set, the script will output debug messages. This can always be enabled as it doesn't bother PRTG.
.EXAMPLE
C:\PS> .\Get-Events.ps1 -ComputerName %host -Username "%windowsdomain\%windowsuser" -Password "%windowspassword" -ProviderName "Microsoft-Windows-Immersive-Shell" -Channel "Microsoft-Windows-TWinUI/Operational" -LimitEntries 1 -MaxAge 1 -EventID 1719 -Level 4
#>
param(
[string]$prtgHostName = "",
[string]$prtgProbeAddress,
[string]$prtgPort = 80,
[int]$prtgPushPort = 5050,
[string]$prtgUserName = '',
[string]$prtgPasshash = 0,
[int]$prtgScvmmDeviceId = 0,
[int]$prtgTemplateSensor = 0,
[string]$computername = "",
[string]$userdomain = "",
[string]$username = "",
[string]$password = "",
[switch]$verbose = $true
)
$global:counter = 0;
$scriptPath = "C:\Program Files (x86)\PRTG Network Monitor\Custom Sensors\EXE";
$guidFile = [string]::Format("{0}\{1}-guidfile.dat",$scriptPath,$computername);
$global:webclient = New-Object System.Net.WebClient;
# this will output debug messages to the console
function Console-ShowMessage([string]$type,$message){
if($verbose){
Write-Host ("[{0}] " -f (Get-Date)) -NoNewline;
switch ($type){
"success" { Write-Host " success " -BackgroundColor Green -ForegroundColor White -NoNewline; }
"information" { Write-Host " information " -BackgroundColor DarkCyan -ForegroundColor White -NoNewline; }
"warning" { Write-Host " warning " -BackgroundColor DarkYellow -ForegroundColor White -NoNewline; }
"error" { Write-Host " error " -BackgroundColor DarkRed -ForegroundColor White -NoNewline; }
default { Write-Host " notes " -BackgroundColor DarkGray -ForegroundColor White -NoNewline; }
}
Write-Host (" {0}{1}" -f $message,$Global:blank)
}
}
# if the guid files don't exist, create them
if(!(Test-Path $guidFile)) { New-Item -ItemType File -Path $guidFile | Out-Null; Console-ShowMessage -type "information" -message "created GUID file: $($guidFile)"; }
Console-ShowMessage "information" "GUID file path: $($guidFile)";
$Stopwatch = [system.diagnostics.stopwatch]::new()
#region xml item
$channel = @"
<result>
<channel>{0}</channel>
<value>{1}</value>{2}
<showChart>{3}</showChart>
{4}
{5}
{6}
</result>
"@
#endregion
#region Function Library
# this will return a value only if it's an integer
function This-Numeric ($Value) {
return $Value -match "^[\d\.]+$"
}
# Since we need lookup files to show this properly, we'll have to convert
# the states received to integers
function This-StateConvert([string]$Type, [string]$CurrentStatus){
# replacement tables
[hashtable]$States = @{
"OverallStatesUnknown" = -1
"OverallStatesAdding " = 1
"OverallStatesNotResponding" = 2
"OverallStatesReassociating" = 3
"OverallStatesRemoving" = 4
"OverallStatesUpdating" = 5
"OverallStatesPending" = 6
"OverallStatesMaintenanceMode" = 7
"OverallStatesNeedsAttention" = 8
"OverallStateOk" = 9
"OverallStatesLimited" = 10
"CommunicationStateUnknown" = -1
"CommunicationStateResponding" = 0
"CommunicationStateNotResponding" = 1
"CommunicationStateAccessDenied " = 2
"CommunicationStateConnecting" = 3
"CommunicationStateDisconnecting" = 4
"CommunicationStateResetting" = 5
"CommunicationStateNoConnection" = 6
"ComputerStateUnknown" = -1
"ComputerStateAdding" = 0
"ComputerStateRemoving" = 1
"ComputerStateResponding" = 2
"ComputerStateNotResponding" = 3
"ComputerStateAccessDenied" = 4
"ComputerStateUpdating" = 5
"ComputerStateReassociating" = 6
"ComputerStatePending" = 7
"ComputerStateMaintenanceMode" = 8
"ClusterNodeStatusUnknown" = 0
"ClusterNodeStatusRunning" = 1
"ClusterNodeStatusStopped" = 2
"ClusterNodeStatusNoCluster" = 3
"VirtualServerStateUnknown" = 0
"VirtualServerStateRunning" = 1
"VirtualServerStateStopped" = 2
"VMStateUnknown" = -1
"VMStateRunning" = 0
"VMStatePowerOff" = 1
"VMStatePoweringOff" = 2
"VMStateSaved" = 3
"VMStateSaving" = 4
"VMStateRestoring" = 5
"VMStatePaused" = 6
"VMStateDiscardSavedState" = 10
"VMStateStarting" = 11
"VMStateMergingDrives" = 12
"VMStateDeleting" = 13
"VMStateDiscardingDrives" = 80
"VMStatePausing" = 81
"VMStateUnderCreation" = 100
"VMStateCreationFailed" = 101
"VMStateStored" = 102
"VMStateUnderTemplateCreation" = 103
"VMStateTemplateCreationFailed" = 104
"VMStateCustomizationFailed" = 105
"VMStateUnderUpdate" = 106
"VMStateUpdateFailed" = 107
"VMStateUnderMigration" = 200
"VMStateMigrationFailed" = 201
"VMStateCreatingCheckpoint" = 210
"VMStateDeletingCheckpoint" = 211
"VMStateRecoveringCheckpoint" = 212
"VMStateCheckpointFailed" = 213
"VMStateInitializingCheckpointOperation" = 214
"VMStateFinishingCheckpointOperation" = 215
"VMStateMissing" = 220
"VMStateHostNotResponding" = 221
"VMStateUnsupported" = 222
"VMStateIncompleteVMConfig" = 223
"VMStateUnsupportedSharedFiles" = 224
"VMStateUnsupportedCluster" = 225
"VMStateP2VCreationFailed" = 240
"VMStateV2VCreationFailed" = 250
}
return $states[$Type+$CurrentStatus]
}
# This will turn the metrics received from the SCVMM into PRTG channels,
# with their corresponding channel settings.
function This-PrtgXmlWrapper($Metrics){
$cleanMetrics = $Metrics;
foreach ($metric in $cleanMetrics.GetEnumerator() | sort -Property Name)
{
$Lookup = "";
$Volume = "";
#region Metric Properties Replacement
# get the unit of the item
If($metric.Value[1] -ne 0)
{ $Unit = [string]::Format("<unit>{0}</unit>", $metric.Value[1])}
else
{ $Unit = "<unit>Count</unit>" }
# get the value lookup of the item
If($metric.Value[2] -ne 0)
{ $Lookup = [string]::Format("<ValueLookup>{0}</ValueLookup>",$metric.Value[2]); }
# should the metric show up in the graph?
If($metric.Value[3] -eq 0)
{ $showChart = 0 }
else
{ $showChart = 1; }
# can the item fire a change trigger?
if($metric.Value[4] -ne 0)
{ $notifyChanged = "<NotifyChanged></NotifyChanged>" }
else
{ $notifyChanged = "" }
if($metric.value[5])
{ $Volume = "<VolumeSize>$($metric.value[5])</VolumeSize>" }
#endregion
# replace the state strings with their corresponding IDs
if(!(This-Numeric $metric.Value[0]))
{ $metric.Value[0] = (This-StateConvert -Type $metric.Name -CurrentStatus $metric.Value[0]) }
$channels += [string]::Format($channel,$metric.Name, $metric.Value[0], $Unit,$showChart,$notifyChanged,$Lookup,$Volume)
}
return $channels;
}
# If there's an error, only this will be outputted
function This-PrtgError($Message){
if(!($verbose)){
Write-Host "<?xml version='1.0' encoding='UTF-8' ?><prtg>"
Write-Host ([string]::Format("<error>1</error><text>{0}</text>",$Message));
Write-Host "</prtg>";
exit 0;
}
}
# We need to ignore $null values, this will cleanse the metrics accordingly
function This-ResultSanitize($Metrics){
$CleanMetric = @{};
foreach ($metric in $Metrics.GetEnumerator() | sort -Property Name)
{ if($metric.Value[0] -ne $null) {$CleanMetric.Add($metric.key,$metric.value)} }
return $CleanMetric;
}
# @Output: PSCredentialObject
#
# This function will generate a PowerShell Credential object for the
# given username, password and domainname
function This-GenerateCredential(){
try{
# Generate credentials object for authentication
$SecPasswd = ConvertTo-SecureString $password -AsPlainText -Force
$Credentials = New-Object System.Management.Automation.PSCredential ([string]::Format("{0}\{1}",$userdomain,$username), $secpasswd)
return $Credentials;
}
catch{
catch{ This-PrtgError -Message "Couldn't connect to the server or execute the given commands. Please check if WinRM is enabled and allowed." }
}
}
# @Output: Update HTTP Push (Advanced) sensors
function This-UpdateHTTPPushSensors {
param([string]$type, [pscustomobject[]]$objects)
$counter = 0;
# first load the GUID list to check the GUIDs against
$guidList = ( Get-Content $guidFile );
# add sensors that are not in our list as a HTTP Push Content sensor
foreach($object in $objects){
if(!($guidList -match $object.id))
{
if(This-AddHTTPPushSensors -sensorName $object.name -Token $object.id)
{
Console-ShowMessage -type "success" -message "Sensor for $($object.name) created successfully.";
# append the GUID to the file so we don't add duplicates in future scans
$object | Select "ID" | ft -HideTableHeaders | Out-File -FilePath $guidFile -Append
$counter++;
$global:counter++;
}
else
{
Console-ShowMessage -type "error" -message "Could not create sensor for $($object.name).";
}
}
else
{ Console-ShowMessage -type "information" -message "$($object.name) with ID $($object.id) is already existing as a sensor."; }
}
Console-ShowMessage -type "information" -message "$($counter) new $($type) have been added to PRTG.";
}
# @Output: Create HTTP Push (Advanced) sensors
#
# This function will generate the PRTG Metascan Items for the VMs in XML format
function This-AddHTTPPushSensors {
param([string]$sensorName, [guid]$token)
try{
Console-ShowMessage "information" "Starting sensor creation for $($sensorName) (GUID: $($token))";
# create the URL for duplicating the original sensor
$url = [string]::Format("{0}:{1}/api/duplicateobject.htm?id={2}&name={3}&targetid={4}&username={5}&passhash={6}",
$prtgHostName, $prtgPort, $prtgTemplateSensor, $sensorName, $prtgScvmmDeviceId, $prtgUserName, $prtgPasshash);
# call the URL and store the content, we need the actual ID of the new sensor to change the settings and unpause it
$request = [System.Net.WebRequest]::Create($url);
$request.AllowAutoRedirect = $false
$response=$request.GetResponse();
If ($response.StatusCode -eq "Redirect")
{
$response.GetResponseHeader("Location") -match '\d{3,}' | Out-Null;
$newSensorID = $Matches[0];
Console-ShowMessage -type "success" -message "Sensor created successfully. New sensor ID: $($newSensorID)";
}
Else
{ Console-ShowMessage "error" "Sensor creation failed. PRTG returned: $($response.StatusCode)"; }
# modify the token to resemble the GUID of the device
$url = [string]::Format("{0}:{1}/api/setobjectproperty.htm?id={2}&name={3}&value={4}&username={5}&passhash={6}",
$prtgHostName, $prtgPort, $newSensorID, "httppushtoken", $token, $prtgUserName, $prtgPasshash);
$UpdateSensorResult = (Invoke-WebRequest $url)
if($UpdateSensorResult.StatusCode -eq "200")
{ Console-ShowMessage -type "success" -message "Changed sensor token to GUID of the host/vm."; }
# start the sensors
$url = [string]::Format("{0}:{1}/api/pause.htm?id={2}&action=1&username={3}&passhash={4}",
$prtgHostName, $prtgPort, $newSensorID, $prtgUserName, $prtgPasshash);
$StartSensorResult = (Invoke-WebRequest $url)
if($StartSensorResult.StatusCode -eq "200")
{ Console-ShowMessage -type "success" -message "Sensor has been started succesfully."; }
return $true
}
catch{ return $false; }
}
# @Output: PowerShell Object Array with all hosts
#
# This function will retrieve all VM hosts available
function This-GetHostsAndVms() {
Console-ShowMessage -type "information" -message "Retrieving HyperV hosts and VMs...";
try{
$objects = (Invoke-Command -ComputerName $computername -ScriptBlock {
# load the correct snapins:
if ((new-object 'version' (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft System Center Virtual Machine Manager Server\Setup" ProductVersion | select -expandproperty productversion)) -ge (new-object 'Version' 3,0)) { Import-Module ((Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft System Center Virtual Machine Manager Server\Setup" InstallPath | select -ExpandProperty InstallPath) + "bin\psModules\virtualmachinemanager") } else { Add-PSSnapin Microsoft.SystemCenter.VirtualMachineManager }
if ((new-object 'version' (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft System Center Virtual Machine Manager Server\Setup" ProductVersion | select -expandproperty productversion)) -ge (new-object 'Version' 6,2)) { Get-SCVMMServer localhost | Out-Null } else { Get-VMMServer localhost | Out-Null }
$hosts = (Get-VMHost);
$vms = (Get-VM);
# The cmdlets above give back a single object or an array of objects.
# For the sake of simplicity, let's convert them, so they're always arrays
if($hostList -isnot [system.array])
{ $hostList = @($hosts) }
if($vmList -isnot [system.array])
{ $vmList = @($vms) }
$objects = @($hostList,$vmList)
return $objects;
} -Credential (This-GenerateCredential)
)
return $objects;
}
catch{
Console-ShowMessage -type "error" -message "Couldn't connect to the server or execute the given commands. Please check if WinRM is enabled and allowed.";
This-PrtgError -Message "Couldn't connect to the server or execute the given commands. Please check if WinRM is enabled and allowed."
}
}
# @Param token: the GUID of the VM/host
# @Param data: the channels of the VM/host
function This-PushData([string]$token, [string]$data){
$pushData = ([string]::Format("<prtg>{0}</prtg>",$data)).replace("`n"," ").Replace(" ",'%20');
if($prtgProbeAddress)
{ $url = [string]::Format("{0}:{1}/{2}?content={3}",($prtgProbeAddress -replace "https","http"),$prtgPushPort,$token,$pushData); }
else
{ $url = [string]::Format("{0}:{1}/{2}?content={3}",($prtgHostName -replace "https","http"),$prtgPushPort,$token,$pushData); }
try {
Invoke-WebRequest $url -Method Get | Out-Null
Console-ShowMessage -type "success" -message "Data for host with ID $($token) pushed to corresponding sensor.";
}
catch {
Console-ShowMessage -type "error" -message "Couldn't push the data to the configured PRTG server using the specified token ($($token)).";
This-PrtgError -Message "Couldn't push the data to the configured PRTG server using the specified token ($($token))."
}
}
function This-GetSCVMMObjects(){
# lets retrieve hosts and vms first
$Objects = (This-GetHostsAndVms)
# ... and seperate them
$VMHosts = $objects[0];
$VMs = $objects[1];
Console-ShowMessage -type "success" -message "$($VMHosts.count) HyperV hosts have been found.";
Console-ShowMessage -type "success" -message "$($VMs.count) Virtual Machines have been found.";
# let's add the sensor first, before we do anything
This-UpdateHTTPPushSensors -objects $VMHosts -type "HyperV Hosts"
This-UpdateHTTPPushSensors -objects $VMs -type "Virtual Machines"
# we have to wait for the sensors to become active.
if($global:counter -gt 0){
Console-ShowMessage -type "information" "Waiting for new push sensors to become active"
Start-Sleep 10;
}
# now that the sensors are there, let's push the values to them
foreach($VMHost in $VMHosts){
$metrics = @{
"OverallState" = @($VMHost.OverallState,"Custom","prtg.standardlookups.hyperv.hoststatus",1,"<NotifyChanged>");
"CommunicationState" = @($VMHost.CommunicationState,"Custom","prtg.standardlookups.hyperv.communicationstate");
"CpuUtilization" = @($VMHost.CpuUtilization,"CPU",0,1,1,0);
"TotalMemory" = @($VMHost.TotalMemory,"BytesMemory",0,1,0,"Kilobyte");
"AvailableMemory" = @(($VMHost.AvailableMemory * 1024 * 1024),"BytesMemory",0,1,0,"MegaByte");
"ClusterNodeStatus" = @($VMHost.ClusterNodeStatus,"Custom","prtg.standardlookups.hyperv.clusternodestatus");
"VirtualServerState" = @($VMHost.VirtualServerState,"Custom","prtg.standardlookups.hyperv.virtualserverstate");
"ComputerState" = @($VMHost.ComputerState,"Custom","prtg.standardlookups.hyperv.computerstate");
"HostCluster" = @($VMHost.HostCluster,"Custom","prtg.standardlookups.hyperv.clusternodestatus"); }
# first, we'll format the data and sanatize the metrics
$channels = (This-PrtgXmlWrapper -Metrics $metrics);
# ...then we'll push it to PRTG. Don't forget to join the array!
This-PushData -token $VMHost.id -data $channels;
}
# The same thing again, but for the VMs
foreach($VM in $VMs){
$metrics = @{
"VMState" = @($VM.Status,"Custom","prtg.standardlookups.hyperv.vmstatus",0,"<NotifyChanged>");
"CPU Usage" = @($VM.PerfCPUUtilization,"Percent",0,0,0,0);
"PerfDiskBytesRead" = @($VM.PerfDiskBytesRead,"BytesDisk",0,0,0,0);
"PerfDiskBytesWrite" = @($VM.PerfDiskBytesWrite,"BytesDisk",0,0,0,0); }
# first, we'll format the data and sanatize the metrics ...
$channels = (This-PrtgXmlWrapper -Metrics $metrics);
# ...then we'll push it to PRTG.
This-PushData -token $VM.id -data $channels;
}
}
#
$Stopwatch.Start();
This-GetSCVMMObjects; $Stopwatch.Stop();
Console-ShowMessage -type "information" -message "Elapsed Time: $($Stopwatch.Elapsed)";
$objects = (((gc $guidFile) | ? {$_.trim() -ne "" } | Measure-Object).Count)
write-host ([string]::Format("{0}:Monitoring {2} objects. Last scan took {1} seconds and added {0} objects.",$global:counter,$Stopwatch.Elapsed.Seconds,$objects));
@HaikoHertes
Copy link

Hi. Thanks for this great work. My sensor is creating every VM and Host once every time the script runs so I have multiple copies of each VM then (also comsuming the license). What could I do to prevent this? Thanks!

@HaikoHertes
Copy link

The script was able to create the GUID file but not put content into it - I have just granted Everyone access to the file, although I wonder why it can create but not write into it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment