To use this article, you will need to have a functioning Nagios environment with the CHECK_NRPE plugin. Your Hyper-V hosts will need to have NSClient++ installed. Both will need to have the configuration as explained near the end of my Nagios configuration article.
Update History
- May 24th, 2017
- Updated hvdiskbase.ps1 to version 1.2. Corrects sporadic issues for cluster VMs introduced in 1.1. Depends on hvvmbase 1.0.2
- Updated hvvmbase.ps1 to version 1.0.2. Adds resilience to cluster VM location
- May 23rd, 2017
- Updated hvdiskbase.ps1 to version 1.1.1. Minor changes to improve dependent script error controls.
- May 21st, 2017
- Updated hvdiskbase.ps1 to version 1.1. Breaking change: This updates the Get-ANVMDisk function so that it works with offline as well as online virtual machines. It does not affect the scripts published on this site.
- May 17th, 2017
- Updated hvvmbase.ps1 to version 1.0.1
- Updated hvdiskbase.ps1 to version 1.0.1
- Added anutilitybase.ps1
Nagios Configuration Files
This section shows changes and additions that must be made to the Nagios configuration files.
Modify the displayed portion of these files with the highlighted section. If nothing is highlighted, then just add it to the bottom.
Important notes:
- Do not forget to swap out my host and object names for your own!
- Do not forget to use ‘service nagios checkconfig’ before trying to apply ANY changes.
After the configuration changes are complete, use service nagios restart on the Nagios system to apply them.
/usr/local/nagios/etc/nagios.cfg
Notice that you must uncomment the windows.cfg line.
# OBJECT CONFIGURATION FILE(S) # These are the object configuration files in which you define hosts, # host groups, contacts, contact groups, services, etc. # You can split your object definitions across several config files # if you wish (as shown below), or keep them all in a single config file. # You can specify individual object config files as shown below: cfg_file=/usr/local/nagios/etc/objects/commands.cfg cfg_file=/usr/local/nagios/etc/objects/contacts.cfg cfg_file=/usr/local/nagios/etc/objects/timeperiods.cfg cfg_file=/usr/local/nagios/etc/objects/templates.cfg # Definitions for monitoring Hyper-V Hosts cfg_file=/usr/local/nagios/etc/objects/hypervhost.cfg # Definitions for monitoring the local (Linux) host cfg_file=/usr/local/nagios/etc/objects/localhost.cfg # Definitions for monitoring a Windows machine cfg_file=/usr/local/nagios/etc/objects/windows.cfg
/usr/local/nagios/etc/objects/commands.cfg
The backing scripts for the commands listed below will appear in other articles. These are examples of how they will be entered. If you have a method that fits your environment more efficiently, use it.
################################################################################ # # Hyper-V Host Commands # ################################################################################ # $ARG1$: age that triggers a warning condition. use one letter (m = minute, h = hour, d = day, w = week) and one number. ex: 3d for 3 days. order does not matter # $ARG2$ age that triggers a critical condition define command{ command_name check-checkpoint-age command_line $USER1$/check_nrpe -H $HOSTADDRESS$ -t 30 -p 5666 -c check_checkpointage -a $ARG1$ $ARG2$ } # $ARG1$: virtual machine name (as shown in Get-VM) # $ARG2$: s for SCSI, i for IDE # $ARG3$: controller number that contains the disk ex: 0 # $ARG4$: disk number on the controller # $ARG5$: percentage of expanded space that triggers a warning condition, ex: 75 # $ARG6$: percentage of expanded space that triggers an error condition, ex: 90 define command{ command_name check-dynamicdisk-size command_line $USER1$/check_nrpe -H $HOSTADDRESS$ -t 30 -p 5666 -c check_vmdyndisksize -a $ARG1$ $ARG2$ $ARG3$ $ARG4$ $ARG5$ $ARG6$ }
/usr/local/nagios/etc/objects/hypervhost.cfg
This file doesn’t exist — you’ll have to create it. If you do not have a Hyper-V cluster, ignore those sections.
############################################################################### ############################################################################### # # HYPER-V HOSTGROUP DEFINITION # ############################################################################### ############################################################################### define hostgroup{ hostgroup_name hyper-v-servers alias Hyper-V Servers members svhv1, svhv2 } ############################################################################### ############################################################################### # # HYPER-V HOST DEFINITIONS # ############################################################################### ############################################################################### define host{ use windows-server host_name svhv1 alias Hyper-V Host 1 address 192.168.25.10 } define host{ use windows-server host_name svhv2 alias Hyper-V Host 2 address 192.168.25.11 } ############################################################################### ############################################################################### # # HYPER-V CLUSTER HOSTGROUP DEFINITION # ############################################################################### ############################################################################### define hostgroup{ hostgroup_name hyper-v-clusters alias Hyper-V Clusters members clhv1 } ############################################################################### ############################################################################### # # HYPER-V CLUSTER HOST OBJECT DEFINITION # ############################################################################### ############################################################################### define host{ use generic-hyper-v-cluster host_name clhv1 alias Hyper-V Cluster 1 address 192.168.25.20 } ############################################################################### ############################################################################### # # HYPER-V SERVICE DEFINITIONS # ############################################################################### ############################################################################### # this is a highly-available VM; only check the cluster object # make sure the account NSClient++ is running under has administrative access to all nodes! define service{ use generic-service host_name clhv1 service_description VM svmanage: OS Disk Expansion check_command check-dynamicdisk-size!svmanage!i!0!0!75!90 } # this is a non-HA VM; only check the owning host define service{ use generic-service host_name svhv1 service_description VM svdc1: OS Disk Expansion check_command check-dynamicdisk-size!svdc1!s!0!0!75!90 } # this is a non-HA VM; only check the owning host define service{ use generic-service host_name svhv1 service_description VM svlmon1: OS Disk Expansion check_command check-dynamicdisk-size!svlmon1!i!0!0!75!90 } # this is a non-HA VM; only check the owning host define service{ use generic-service host_name svhv2 service_description VM svdc2: OS Disk Expansion check_command check-dynamicdisk-size!svdc2!s!0!0!75!90 }
/usr/local/nagios/etc/objects/templates.cfg
This addition is only necessary if you will be monitoring a Hyper-V cluster.
#### Hyper-V Cluster Template #### define host{ name generic-hyper-v-cluster use generic-host check_period 24x7 check_interval 5 retry_interval 1 max_check_attempts 10 check_command check-host-alive notification_period 24x7 notification_interval 30 notification_options d,r hostgroups hyper-v-clusters register 0 }
NSClient++ Configuration
The additions and changes in this section are made on Hyper-V hosts that are running NSClient++. No checks are included in this article. These scripts are required for scripts that I’ll present in other articles.
All scripts have been tested with PowerShell version 4.0. There is no known reason why they would not work with PowerShell 5.0 or later.
A Note on the PowerShell Scripts
When I share scripts with you, it is with the genuine hope that they’ll be useful. I certainly don’t mind if you pick them apart for learning, for improving, for bug hunting, or pretty much anything that doesn’t involve stealing my work to claim as your own. If you look at these scripts, you will see many, many, many places where I could have used a completely different cmdlet and saved myself lots of trouble and several lines of script. You’ll also notice that I used almost zero comments. I promise you that I did not intentionally make these scripts difficult to read. These scripts are going to be executed frequently on every host where they reside. I also have no idea what you have or have not done in your environment. Therefore, I rigidly adhered to two goals when designing these scripts:
- Reduction of non-essential information. This mostly meant, “very few comments”. I don’t know how much it really saves, but I tend to write almost as many comment lines as script lines so it seemed possible that forcing the PowerShell interpreter to examine and ignore dozens of comment lines would become a drag in aggregate. I avoided positional parameters and aliases because I did not feel that the readability trade-off was worth the effort when I was also avoiding comments and also because I assume that PowerShell burns up CPU cycles figuring out positional parameters and aliases just like people do.
- No assumptions of the presence of any PowerShell modules except what is in the base package. If any of my Nagios scripts call a cmdlet that doesn’t exist, either you didn’t copy everything that I told you to or your PowerShell installation is damaged. I did not use common Hyper-V or Failover Clustering cmdlets because I cannot be certain that Get-VM will work on a Hyper-V host or that Get-ClusterResource will work on a failover cluster member because their PowerShell modules are optional. Additionally, I assume that not requiring PowerShell to load up or hook into those DLLs or follow the deeper logic trails that Microsoft used in those cmdlets should speed up processing to some degree.
I did attempt to make the scripts as legible as possible while following those goals.
Remember that every host must have its execution policy set to RemoteSigned or something less restrictive. You can run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned.
C:\Program Files\NSClient++\nsclient.ini
The base Nagios article includes a section on configuring the ini file correctly. The portion that establishes PowerShell functionality and a couple of samples for checks are shown below:
[/settings/external scripts] allow arguments = true [/settings/external scripts/wrappings] ps1=cmd /c echo scripts\%SCRIPT% %ARGS%; exit($lastexitcode) | powershell.exe -noprofile -nologo -command - [/settings/external scripts/wrapped scripts] check_checkpointage=check_hvcheckpointage.ps1 $ARG1$ $ARG2$ check_vmdyndisksize=check_hvvmdyndisksize.ps1 $ARG1$ $ARG2$ $ARG3$ $ARG4$ $ARG5$ $ARG6$
C:\Program Files\NSClient++\scripts\hvvmbase.ps1
This script contains some general-purpose and virtual machine-related functions that will be used by my other scripts that perform Hyper-V host checks. This file does not exist and must be created.
<# hvvmbase.ps1 Written by Eric Siron (c) Altaro Software 2017 Version 1.0.2 May 24, 2017 Intended for use with the NSClient++ module from http://nsclient.org Provides reusable functions for other check scripts. #> if(-not $HVVMBaseIncluded) { $HVVMBaseIncluded = $true $UtilityBase = Join-Path -Path $PSScriptRoot -ChildPath 'anutilitybase.ps1' if(-not (Test-Path -Path $UtilityBase)) { Write-Host ('Required file {0} not found' -f $UtilityBase) exit 3 } . $UtilityBase function Get-ANVMBaseVersion { New-Object System.Version(1, 0, 2, 0) } function Get-ANVMsAll { param( [Parameter()][String]$ComputerName = $env:COMPUTERNAME ) Get-WmiObject -Namespace root\virtualization\v2 -Class Msvm_ComputerSystem -ComputerName $ComputerName } function Get-ANVMWmiObjectFromComputer { param( [Parameter(Position=1)][String]$ComputerName, [Parameter(ParameterSetName='By Name', Position=2)][String]$VMName, [Parameter(ParameterSetName='By ID')][String]$VMID ) if($VMName) { $Filter = 'ElementName="{0}"' -f $VMName } elseif($VMID) { $Filter = 'Name="{0}"' -f $VMID } else { return } $GWMIParams = @{Namespace = 'root\virtualization\v2'; Class = 'Msvm_ComputerSystem'; Filter = $Filter; ErrorAction = ([System.Management.Automation.ActionPreference]::SilentlyContinue) } if($ComputerName -and $ComputerName -ne $env:COMPUTERNAME) { $GWMIParams.Add('ComputerName', $ComputerName) } Get-WmiObject @GWMIParams # -ComputerName $ComputerName -Namespace 'root\virtualization\v2' -Class 'Msvm_ComputerSystem' -Filter $Filter -ErrorAction SilentlyContinue } # will find ANY VM on the local host and any HA VM in the same cluster. will NOT find non-HA VMs on other nodes # if multiple VMs have the same name, will return whichever one that it wants to. I'm not sure how it decides. #sorrynotsorry function Get-ANVMWmiObject { param( [Parameter(Position=1)][String]$VMName ) if(-not $VMName) { Write-Host 'No virtual machine name was supplied' Exit 2 } # look locally first $VM = Get-ANVMWmiObjectFromComputer -ComputerName '.' -VMName $VMName if((-not $VM) -band (ANIsClustered)) { $VMsOnOtherHosts = Get-WmiObject -Namespace root\MSCluster -Class MSCluster_Resource -Property OwnerNode, Id -Filter ('Type="Virtual Machine" AND NOT OwnerNode="{0}"' -f $env:COMPUTERNAME) $ClusterKey = $null try { $ClusterKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Default) } catch [System.Security.SecurityException], [System.UnauthorizedAccessException] { Write-Host ('NSClient++ user "{0}" cannot access the registry on "{1}"' -f $env:USERNAME, $OtherHostVM.OwnerNode, $env:COMPUTERNAME) Exit 2 } catch { Write-Host ('Could not open registry on "{0}": {1}' -f $env:COMPUTERNAME, $_) Exit 2 } foreach($OtherHostVM in $VMsOnOtherHosts) { try { $Subkey = $ClusterKey.OpenSubKey(('Cluster\Resources\{0}\Parameters' -f $OtherHostVM.Id), $false) $VMId = $Subkey.GetValue('VmID') } catch { Write-Host ('Could not open a cluster parameters key on "{0}": {1}' -f $env:COMPUTERNAME, $_) Exit 2 } $Subkey.Close() $VM = Get-ANVMWmiObjectFromComputer -ComputerName $OtherHostVM.OwnerNode -VMID $VMId if($VM -ne $null -and $VM.ElementName -eq $VMName) { break } else { $VM = $null } } } if(-not $VM) { Write-Host ('Virtual machine "{0}" could not be found!' -f $VMName) Exit 2 } $VM } function Get-ANVMIsClustered { param([Parameter(Position=1)][System.Management.ManagementObject]$VM) $IsClustered = $false $VMHostData = $VM.GetRelated('Msvm_VirtualSystemSettingData').GetRelated('Msvm_KvpExchangeComponentSettingData').HostOnlyItems foreach ($XMLKVPItem in $VMHostData) { $KVPKey = ([XML]$XMLKVPItem).SelectSingleNode('/INSTANCE/PROPERTY[@NAME=''Name'']/VALUE/child::text()').Value if($KVPKey -eq 'VirtualMachineIsClustered') { $KVPValue = ([XML]$XMLKVPItem).SelectSingleNode('/INSTANCE/PROPERTY[@NAME=''Data'']/VALUE/child::text()').Value $IsClustered = $KVPValue -eq 'TRUE' } } $IsClustered } }
C:\Program Files\NSClient++\scripts\hvdiskbase.ps1
This script contains functions that will be used by my other scripts that work with Hyper-V virtual hard disks. This file does not exist and must be created.
<# hvdiskbase.ps1 Written by Eric Siron (c) Altaro Software 2017 Version 1.2.0 May 24, 2017 Intended for use with the NSClient++ module from http://nsclient.org Provides reusable functions for other check scripts. #> if(-not $HVDiskBaseIncluded) { $HVDiskBaseIncluded = $true $UtilityBase = Join-Path -Path $PSScriptRoot -ChildPath 'anutilitybase.ps1' if(-not (Test-Path -Path $UtilityBase)) { Write-Host ('Required file {0} not found' -f $UtilityBase) exit 3 } . $UtilityBase $VMBase = Join-Path -Path $PSScriptRoot -ChildPath 'hvvmbase.ps1' . $VMBase if(-not (Test-Path -Path $VMBase)) { Write-Host ('Required file {0} not found' -f $VMBase) exit 3 } $CurrentVMScriptVer = Get-ANVMBaseVersion $RequiredVMScriptVer = New-Object System.Version(1, 0, 2, 0) if($CurrentVMScriptVer -lt $RequiredVMScriptVer) { Write-Host ('Required VM base script version: {0}. Current version: {1}' -f $RequiredVMScriptVer, $CurrentVMScriptVer) exit(3) } function Get-ANDiskBaseVersion { New-Object System.Version(1, 2, 0, 0) } function Get-ANImageManagementService { param([Parameter(Position=1)][String]$ComputerName) if([String]::IsNullOrEmpty($ComputerName)) { $ComputerName = '.' } Get-WmiObject -ComputerName $ComputerName -Namespace root\virtualization\v2 -Class Msvm_ImageManagementService } function Get-ANVMDisk { param( [Parameter(Position=1)][String]$VMName = '', [Parameter(Position=2)][String]$ControllerType = 'i', [Parameter(Position=3)][UInt32]$ControllerNumber = '0', [Parameter(Position=4)][UInt32]$ControllerLocation = '0' ) $VM = Get-ANVMWmiObject -VMName $VMName if($VM) { if($ControllerType.Contains('s')) { $SearchType = 6 } else { $SearchType = 5 } $ComputerName = [String]::Empty if($VM.__SERVER -ne $env:COMPUTERNAME) { $ComputerName = $VM.__SERVER } $GwmiParams = @{Namespace = 'root\virtualization\v2'; Class = 'Msvm_ResourceAllocationSettingData'; Filter = ('InstanceID LIKE "%{0}%" AND ResourceType="{1}"' -f $VM.Name, $SearchType)} if($ComputerName) { $GwmiParams.Add('ComputerName', $ComputerName)} $VMRASD = Get-WmiObject @GwmiParams if($VMRASD -and $VMRASD.Count) { $VMRASD = $VMRASD[$ControllerNumber] } if($VMRASD) { $SearchPath = $VMRASD.Path.Path $SearchPath = $SearchPath.Substring(0, $SearchPath.Length - 1) $SearchPath += '\\{0}\\D"' -f $ControllerLocation $SearchPath = $SearchPath.Replace('\', '\\') $GwmiParams.Class = 'Msvm_StorageAllocationSettingData' $GwmiParams.Filter = ("Parent='{0}'" -f $SearchPath) $VMSASD = Get-WmiObject @GwmiParams if($VMSASD) { $VMSASD } else { 'No disk found at the specified controller location' exit 3 } } else { 'Specified controller was not found' exit 3 } } else { Write-Host ('Virtual machine "{0}" not found' -f $VMName) exit 3 } } function Get-ANVHDSettingData { param( [Parameter(Position=1)][System.Management.ManagementObject]$Disk ) if($Disk -and $Disk.HostResource[0].ToLower().Contains('.vhd')) { $ImageManagementService = Get-ANImageManagementService -ComputerName $Disk.__SERVER $SettingData = $ImageManagementService.GetVirtualHardDiskSettingData($Disk.HostResource[0]) if($SettingData.ReturnValue -eq 0) { try { $XMLData = [xml]$SettingData.SettingData if($XMLData.SelectSingleNode('/INSTANCE/@CLASSNAME').Value -ne 'Msvm_VirtualHardDiskSettingData') { throw('Data is not of type Msvm_VirtualHardDiskSettingData') } } catch { Write-Host ('Error parsing setting data for {0}: {1}' -f $Disk.HostResource[0], $_) Exit 2 } $RawBlockSize = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''BlockSize'']/VALUE/child::text()').Value $RawFormat = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''Format'']/VALUE/child::text()').Value $RawType = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''Type'']/VALUE/child::text()').Value $RawParentPath = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''ParentPath'']/VALUE/child::text()').Value $RawPath = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''Path'']/VALUE/child::text()').Value $RawLogicalSectorSize = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''LogicalSectorSize'']/VALUE/child::text()').Value $RawMaxInternalSize = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''MaxInternalSize'']/VALUE/child::text()').Value $RawPhysicalSectorSize = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''PhysicalSectorSize'']/VALUE/child::text()').Value $RawVirtualDiskId = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''VirtualDiskId'']/VALUE/child::text()').Value try { $WMISettingData = ([wmiclass]('\\{0}\root\virtualization\v2:Msvm_VirtualHardDiskSettingData' -f $Disk.__SERVER)).CreateInstance() } catch { Write-Host ('Unable to create a disk setting data object: {0}' -f $_) Exit 2 } $WmiSettingData.BlockSize = $RawBlockSize $WmiSettingData.Format = $RawFormat # Fixed = 2, Dynamic = 3, Differencing = 4 $WmiSettingData.Type = $RawType # VHD = 2, VHDX = 3 $WMISettingData.ParentPath = $RawParentPath $WmiSettingData.Path = $RawPath $WMISettingData.LogicalSectorSize = $RawLogicalSectorSize $WmiSettingData.MaxInternalSize = $RawMaxInternalSize $WmiSettingData.PhysicalSectorSize = $RawPhysicalSectorSize $WmiSettingData.VirtualDiskId = $RawVirtualDiskId $WmiSettingData } else { Write-Host ('Unable to retrieve setting data for {0}' -f $Disk.HostResource[0]) Exit 2 } } else { Write-Host 'Disk not found or is not a virtual hard disk' Exit 3 } } function Get-ANVHDState { param( [Parameter(Position=1)][System.Management.ManagementObject]$Disk ) $ImageManagementService = Get-ANImageManagementService -ComputerName $Disk.__SERVER $State = $ImageManagementService.GetVirtualHardDiskState($Disk.HostResource[0]) if($null -ne $State) { try { $XMLData = [xml]$State.State if($XmlData.SelectSingleNode('/INSTANCE/@CLASSNAME').Value -ne 'Msvm_VirtualHardDiskState') { throw('Data is not of type Msvm_VirtualHardDiskState') } } catch { Write-Host ('Error parsing state information for {0}: {1}' -f $Disk.HostResource[0], $_) Exit 2 } $RawAlignment = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''Alignment'']/VALUE/child::text()').Value $RawFileSize = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''FileSize'']/VALUE/child::text()').Value $RawFragmentationPercentage = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''FragmentationPercentage'']/VALUE/child::text()').Value $RawInUse = $XmlData.SelectSingleNode('//PROPERTY[@NAME = ''InUse'']/VALUE/child::text()').Value $RawMinInternalSize = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''MinInternalSize'']/VALUE/child::text()').Value $RawPhysicalSectorSize = $XMLData.SelectSingleNode('//PROPERTY[@NAME = ''PhysicalSectorSize'']/VALUE/child::text()').Value try { $WMIStateData = ([wmiclass]('\\{0}\root\virtualization\v2:Msvm_VirtualHardDiskState' -f $Disk.__SERVER)).CreateInstance() } catch { Write-Host ('Unable to create a disk state object: {0}' -f $_) Exit 2 } $WMIStateData.Alignment = $RawAlignment $WMIStateData.FileSize = $RawFileSize $WMIStateData.FragmentationPercentage = $RawFragmentationPercentage $WMIStateData.InUse = $RawInUse $WMIStateData.MinInternalSize = $RawMinInternalSize $WMIStateData.PhysicalSectorSize = $RawPhysicalSectorSize $WMIStateData } else { Write-Host ('Unable to retrieve state information for {0}' -f $Disk.HostResource[0]) Exit 2 } } }
C:\Program Files\NSClient++\scripts\anutilitybase.ps1
This script contains functions that will be used by my other scripts. Most will not work without it. This file does not exist and must be created.
<# anutilitybase.ps1 Written by Eric Siron (c) Altaro Software 2017 Version 1.0 May 17th, 2017 Intended for use with the NSClient++ module from http://nsclient.org Provides reusable functions for other check scripts. #> if(-not $ANUtilityBaseIncluded) { $ANUtilityBaseIncluded = $true function Get-ANUtilityBaseVersion { New-Object System.Version(1, 0, 0, 0) } function Format-ANStorageNumberAsFriendly { param( [Parameter(Position=1)][UInt64]$Number, [Parameter(Position=2)][Switch]$AsBits ) $DigitLength = $Number.ToString().Length $DigitGrouping = [Math]::Floor($DigitLength / 3) $DigitGroupLocatorModulus = $DigitLength % 3 if($DigitGrouping -gt 0 -and -$DigitGroupLocatorModulus -eq 0) { $DigitGrouping -= 1 } if($AsBits) { $Tag = 'b' } else { $Tag = 'B' } switch($DigitGrouping) { 0 { $ShortenedNumber = $Number; $Suffix = '' } 1 { $ShortenedNumber = $Number / 1KB; $Suffix = 'K' } 2 { $ShortenedNumber = $Number / 1MB; $Suffix = 'M' } 3 { $ShortenedNumber = $Number / 1GB; $Suffix = 'G' } 4 { $ShortenedNumber = $Number / 1TB; $Suffix = 'T' } default { $ShortenedNumber = $Number / 1PB; $Suffix = 'P' } } '{0:N2} {1}{2}' -f $ShortenedNumber, $Suffix, $Tag } function Format-ANNumberAsPercent { param([Parameter(Position=1)]$Number) '{0:N2}%' -f ($Number * 100) } function Get-ANIsClustered { [bool](Get-WmiObject -Namespace root -Class '__NAMESPACE' -Filter 'Name="MSCluster"') } Set-Alias -Name ANIsClustered -Value Get-ANIsClustered }