Private/Spool.ps1
|
function Get-SpoolDir { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [NutanixConnectorConfiguration]$Config ) $ScriptRootPath = $Config.ScriptRootPath $nutanixEnvironment = $Config.EnvironmentConfig.Name $spoolDir = Join-Path -Path $ScriptRootPath -ChildPath (Join-Path -Path $script:SPOOL_FOLDER_NAME -ChildPath $nutanixEnvironment) if (-not (Test-Path -Path $spoolDir)) { New-Item -Path $spoolDir -ItemType Directory -Force | Out-Null Write-CustomLog -Message "Created spool directory: $spoolDir" -Severity 'INFO' } return $spoolDir } function New-SpoolFile { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [NutanixConnectorConfiguration]$Config, [Parameter(Mandatory = $true)] [datetimeoffset]$Timestamp ) $spoolDir = Get-SpoolDir -Config $Config $ts = ConvertTo-FilesafeTimestamp -Timestamp $Timestamp $id = [Guid]::NewGuid().ToString('N') return (Join-Path -Path $spoolDir -ChildPath "$ts-received-$id.json") } function Get-SpoolFilesPending { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [NutanixConnectorConfiguration]$Config ) $spoolDir = Get-SpoolDir -Config $Config return Get-ChildItem -Path $spoolDir -Filter "*-received-*.json" -File | Sort-Object -Property Name } function Read-SpoolReceived { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path ) try { if (-not (Test-Path -Path $Path -PathType Leaf)) { throw "Spool file not found." } $json = Get-Content -Path $Path -Raw -Encoding utf8 $data = $json | ConvertFrom-Json if (-not $data.schema_version) { throw "Invalid spool file: missing 'schema_version'" } if (-not $data.content) { throw "Invalid spool file: missing 'content' section" } $result = [NutanixConnectorSpoolReceived]::new() $result.schema_version = $data.schema_version $result.content = $data.content return $result } catch { $errorMessage = "Failed to read spool received file '$Path'. Error=$($_.Exception.Message)" Write-CustomLog -Message $errorMessage -Severity 'ERROR' throw $errorMessage } } function ConvertTo-NutanixHypervisorHostInfo { param([object]$Value) if ($null -eq $Value) { return $null } $hostInfo = [NutanixHypervisorHostInfo]::new() $hostInfo.name = $Value.name $hostInfo.cluster = $Value.cluster $hostInfo.number_of_vms = $Value.number_of_vms $hostInfo.power_policy = $Value.power_policy $hostInfo.hyperthreading = $Value.hyperthreading return $hostInfo } function ConvertTo-NutanixHypervisorEvent { param([object]$Value) $hypervisorEvent = [NutanixHypervisorEvent]::new() $hypervisorEvent.start_time = ConvertTo-Rfc3339UtcZ -Timestamp $Value.start_time $hypervisorEvent.duration = $Value.duration if ($null -ne $Value.cpu) { $cpu = [NutanixHypervisorCpuMetrics]::new() $cpu.number_of_threads = $Value.cpu.number_of_threads $cpu.number_of_packages = $Value.cpu.number_of_packages $cpu.number_of_vcpus = $Value.cpu.number_of_vcpus $cpu.ready_summation = $Value.cpu.ready_summation $cpu.usage_average = $Value.cpu.usage_average $cpu.used_summation = $Value.cpu.used_summation $hypervisorEvent.cpu = $cpu } if ($null -ne $Value.disk) { $disk = [NutanixHypervisorDiskMetrics]::new() $disk.read_average = $Value.disk.read_average $disk.write_average = $Value.disk.write_average $disk.max_total_latency_latest = $Value.disk.max_total_latency_latest $hypervisorEvent.disk = $disk } if ($null -ne $Value.memory) { $memory = [NutanixHypervisorMemoryMetrics]::new() $memory.swap_in_rate_average = $Value.memory.swap_in_rate_average $memory.swap_out_rate_average = $Value.memory.swap_out_rate_average $memory.swap_used_average = $Value.memory.swap_used_average $memory.state_latest = $Value.memory.state_latest $memory.vm_mem_ctl_average = $Value.memory.vm_mem_ctl_average $memory.usage_average = $Value.memory.usage_average $memory.installed = $Value.memory.installed $memory.committed = $Value.memory.committed $hypervisorEvent.memory = $memory } return $hypervisorEvent } function ConvertTo-NutanixHypervisorVMInfo { param([object]$Value) $virtualMachine = [NutanixHypervisorVMInfo]::new() $virtualMachine.name = $Value.name $virtualMachine.guest_tools_version = $Value.guest_tools_version $virtualMachine.resource_pool = $Value.resource_pool $virtualMachine.cpu_limit = $Value.cpu_limit $virtualMachine.cpu_shares = $Value.cpu_shares $virtualMachine.disk_io_limit = $Value.disk_io_limit return $virtualMachine } function ConvertTo-NutanixHypervisorDataItem { param([object]$Value) $dataItem = [NutanixHypervisorDataItem]::new() $dataItem.host = ConvertTo-NutanixHypervisorHostInfo -Value $Value.host $events = @() foreach ($hypervisorEvent in @($Value.events)) { if ($null -ne $hypervisorEvent) { $events += ConvertTo-NutanixHypervisorEvent -Value $hypervisorEvent } } $dataItem.events = @($events) $virtualMachines = @() foreach ($virtualMachine in @($Value.virtual_machines)) { if ($null -ne $virtualMachine) { $virtualMachines += ConvertTo-NutanixHypervisorVMInfo -Value $virtualMachine } } $dataItem.virtual_machines = @($virtualMachines) return $dataItem } function ConvertTo-NutanixHypervisorPayload { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [object]$Value ) $p = [NutanixHypervisorPayload]::new() $p.schema_version = $Value.schema_version $p.source = $Value.source $p.customer_environment = $Value.customer_environment $p.version = $Value.version $dataItems = @() foreach ($dataItem in @($Value.data)) { if ($null -ne $dataItem) { $dataItems += ConvertTo-NutanixHypervisorDataItem -Value $dataItem } } $p.data = @($dataItems) return $p } function Read-SpoolData { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [NutanixConnectorConfiguration]$Config, [Parameter(Mandatory = $true)] [NutanixConnectorState]$State ) try { $files = @(Get-SpoolFilesPending -Config $Config | Sort-Object -Property Name) if ($files.Count -eq 0) { return } $cutoffUtc = if ([string]::IsNullOrWhiteSpace($State.watermarks.last_sent_utc)) { $null } else { ConvertFrom-RfcUtcTimestamp -Value $State.watermarks.last_sent_utc } $payloads = @() foreach ($file in $files) { $tsPart = ($file.BaseName -split '-received-')[0] $tsPart = ConvertFrom-FilesafeTimestamp -Value $tsPart $receivedTimestampUtc = ConvertFrom-RfcUtcTimestampOrNull -Value $tsPart if ($null -ne $cutoffUtc -and $null -ne $receivedTimestampUtc -and $receivedTimestampUtc -le $cutoffUtc) { continue } $received = Read-SpoolReceived -Path $file.FullName $payloads += @(ConvertTo-NutanixHypervisorPayload -Value $received.content) } if ($payloads.Count -eq 0) { return } return $payloads } catch { $errorMessage = "Failed to read spool data. Error=$($_.Exception.Message)" Write-CustomLog -Message $errorMessage -Severity 'ERROR' throw $errorMessage } } function Write-SpoolReceived { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [NutanixConnectorConfiguration]$Config, [Parameter(Mandatory = $true)] [datetimeoffset]$Timestamp, [Parameter(Mandatory = $true)] [object]$Content ) $path = New-SpoolFile -Config $Config -Timestamp $Timestamp $contentToSpool = Clear-SpoolContent -Content $Content $obj = [pscustomobject]@{ schema_version = "1.0.0" content = $contentToSpool } Write-SpoolFileAtomic -Path $path -Object $obj return $path } function Clear-SpoolContent { [CmdletBinding()] param( [object]$Content ) # Clone before sanitizing to avoid mutating the in-memory payload used by the caller. $sanitizedContent = ($Content | ConvertTo-Json -Depth 20) | ConvertFrom-Json foreach ($dataItem in @($sanitizedContent.data)) { if ($null -eq $dataItem) { continue } if ($null -ne $dataItem.PSObject.Properties['virtual_machines']) { $dataItem.PSObject.Properties.Remove('virtual_machines') } } return $sanitizedContent } function Write-SpoolFileAtomic { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [object]$Object ) try { $json = $Object | ConvertTo-Json -Depth 20 $tempPath = "$Path.tmp-$([guid]::NewGuid().ToString('N'))" Set-Content -Path $tempPath -Value $json -Encoding utf8 Move-Item -Path $tempPath -Destination $Path -Force Write-CustomLog -Message "Spooled file written atomically: '$Path'" -Severity 'INFO' } catch { $errorMessage = "Failed to write spool file. Path=$Path Error=$($_.Exception.Message)" Write-CustomLog -Message $errorMessage -Severity 'ERROR' throw $errorMessage } } function Remove-SpoolStaleFiles { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [NutanixConnectorConfiguration]$Config, [Parameter(Mandatory = $true)] [int]$MaxFileCount ) $spoolDir = Get-SpoolDir -Config $Config try { $files = Get-ChildItem -Path $spoolDir -Filter '*-received-*.json' -File | Sort-Object -Property Name if ($files.Count -le $MaxFileCount) { return } $toDelete = $files | Select-Object -First ($files.Count - $MaxFileCount) foreach ($file in $toDelete) { try { Remove-Item -Path $file.FullName -Force Write-CustomLog -Message "Spool cleanup deleted: $($file.Name)" -Severity 'DEBUG' } catch { Write-CustomLog -Message "Failed to delete spool file '$($file.Name)'. Error=$($_.Exception.Message)" -Severity 'WARNING' } } } catch { Write-CustomLog -Message "Spool cleanup failed. Dir=$spoolDir Error=$($_.Exception.Message)" -Severity 'WARNING' } } |