Functions/Private/Get-ClusterSnapshot.ps1
|
function Get-ClusterSnapshot { <# .SYNOPSIS Collects a point-in-time resource snapshot from every Up node in a Failover Cluster. .OUTPUTS PSCustomObject with Nodes (host metrics) and VMs (per-VM metrics + HostNode annotation). #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ClusterName, [int]$SampleCount = 5, [int]$SampleIntervalSeconds = 2 ) $nodes = Get-ClusterNode -Cluster $ClusterName -ErrorAction Stop | Where-Object { $_.State -eq 'Up' } if (-not $nodes) { throw "No active nodes found in cluster '$ClusterName'." } Write-Verbose "Collecting metrics from $($nodes.Count) node(s) in '$ClusterName'..." $collectionBlock = { param([int]$SampleCount, [int]$SampleInterval) # ── CPU (averaged over N samples) ────────────────────────────────────── $cpuSamples = Get-Counter '\Processor(_Total)\% Processor Time' ` -SampleInterval $SampleInterval -MaxSamples $SampleCount -ErrorAction Stop $cpuUtil = ($cpuSamples.CounterSamples | Measure-Object -Property CookedValue -Average).Average # ── Memory ──────────────────────────────────────────────────────────── $os = Get-CimInstance Win32_OperatingSystem $totalMemMB = [Math]::Round($os.TotalVisibleMemorySize / 1KB, 0) # KB → MB $availMemMB = [Math]::Round($os.FreePhysicalMemory / 1KB, 0) $lpCount = (Get-CimInstance Win32_Processor | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum # ── Network (2-second delta on all Up physical adapters) ─────────────── $upAdapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' -and $_.Speed -gt 0 } $totalCapacityBps = ($upAdapters | Measure-Object -Property Speed -Sum).Sum $snap1 = $upAdapters | ForEach-Object { $s = $_ | Get-NetAdapterStatistics [PSCustomObject]@{ Name = $_.Name; Bytes = $s.ReceivedBytes + $s.SentBytes } } $t1 = [DateTime]::UtcNow Start-Sleep -Seconds 2 $snap2 = $upAdapters | ForEach-Object { $s = $_ | Get-NetAdapterStatistics [PSCustomObject]@{ Name = $_.Name; Bytes = $s.ReceivedBytes + $s.SentBytes } } $elapsed = ([DateTime]::UtcNow - $t1).TotalSeconds $totalBytesPerSec = 0 foreach ($a1 in $snap1) { $a2 = $snap2 | Where-Object { $_.Name -eq $a1.Name } if ($a2) { $totalBytesPerSec += ($a2.Bytes - $a1.Bytes) / $elapsed } } $netUtil = if ($totalCapacityBps -gt 0) { [Math]::Min(100, [Math]::Round(($totalBytesPerSec * 8 / $totalCapacityBps) * 100, 1)) } else { 0 } # ── Per-VM metrics ──────────────────────────────────────────────────── $runningVMs = Get-VM | Where-Object { $_.State -eq 'Running' } $vmMetrics = foreach ($vm in $runningVMs) { $mem = Get-VMMemory -VMName $vm.Name -ErrorAction SilentlyContinue # Dynamic Memory pressure: ~100 = balanced, >100 = starved, <100 = surplus $pressure = 100 if ($mem -and $mem.DynamicMemoryEnabled) { try { # Counter instance name must exactly match the VM name $pc = Get-Counter "\Hyper-V Dynamic Memory VM($($vm.Name))\Current Pressure" ` -MaxSamples 1 -ErrorAction Stop $pressure = [Math]::Round($pc.CounterSamples[0].CookedValue, 1) } catch { $pressure = 100 # Counter unavailable — assume balanced } } [PSCustomObject]@{ VMName = $vm.Name VMId = $vm.Id.ToString() CpuUtilization = $vm.CPUUsage # 0-100 % ProcessorCount = $vm.ProcessorCount MemoryAssignedMB = [Math]::Round($vm.MemoryAssigned / 1MB, 0) MemoryDemandMB = [Math]::Round($vm.MemoryDemand / 1MB, 0) DynamicMemoryEnabled = ($mem -and $mem.DynamicMemoryEnabled) MemoryPressure = $pressure } } [PSCustomObject]@{ NodeName = $env:COMPUTERNAME CpuUtilization = [Math]::Round($cpuUtil, 1) TotalMemoryMB = $totalMemMB AvailableMemoryMB = $availMemMB UsedMemoryMB = $totalMemMB - $availMemMB LogicalProcessorCount = $lpCount NetworkUtilization = $netUtil VMs = $vmMetrics } } $nodeSnapshots = foreach ($node in $nodes) { Write-Verbose " Querying node: $($node.Name)" try { Invoke-Command -ComputerName $node.Name -ErrorAction Stop ` -ArgumentList $SampleCount, $SampleIntervalSeconds ` -ScriptBlock $collectionBlock } catch { Write-Warning "Failed to collect metrics from '$($node.Name)': $_" $null } } $nodeSnapshots = @($nodeSnapshots | Where-Object { $_ -ne $null }) # Flatten VM list with host annotation $allVMs = foreach ($snap in $nodeSnapshots) { foreach ($vm in $snap.VMs) { $vm | Add-Member -NotePropertyName 'HostNode' -NotePropertyValue $snap.NodeName -Force -PassThru } } [PSCustomObject]@{ ClusterName = $ClusterName Timestamp = Get-Date Nodes = $nodeSnapshots VMs = @($allVMs) } } |