Private/Spool.ps1
|
function Get-SpoolDir { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [VSphereConnectorConfiguration]$Config ) $ScriptRootPath = $Config.ScriptRootPath $vSphereEnvironment = $Config.EnvironmentConfig.Name $spoolDir = Join-Path -Path $ScriptRootPath -ChildPath (Join-Path -Path $script:SPOOL_FOLDER_NAME -ChildPath $vSphereEnvironment) 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)] [VSphereConnectorConfiguration]$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)] [VSphereConnectorConfiguration]$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 -Depth 20 if (-not $data.schema_version) { throw "Invalid spool file: missing 'schema_version'" } if (-not $data.content) { throw "Invalid spool file: missing 'content' section" } $result = [VSphereConnectorSpoolReceived]::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-VSphereHypervisorPayload { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [object]$Value ) $p = [VSphereHypervisorPayload]::new() $p.schema_version = $Value.schema_version $p.source = $Value.source $p.customer_environment = $Value.customer_environment $p.version = $Value.version $dataItems = @() foreach ($d in @($Value.data)) { if ($null -eq $d) { continue } $di = [VSphereHypervisorDataItem]::new() if ($null -ne $d.host) { $hostInfo = [VSphereHypervisorHostInfo]::new() $hostInfo.name = $d.host.name $hostInfo.cluster = $d.host.cluster $hostInfo.number_of_vms = $d.host.number_of_vms $hostInfo.power_policy = $d.host.power_policy $hostInfo.hyperthreading = $d.host.hyperthreading $di.host = $hostInfo } $events = @() foreach ($e in @($d.events)) { if ($null -eq $e) { continue } $ev = [VSphereHypervisorEvent]::new() $ev.start_time = ConvertTo-Rfc3339UtcZ -Timestamp $e.start_time $ev.duration = $e.duration if ($null -ne $e.cpu) { $cpu = [VSphereHypervisorCpuMetrics]::new() $cpu.ready_summation = $e.cpu.ready_summation $cpu.usage_average = $e.cpu.usage_average $cpu.used_summation = $e.cpu.used_summation $ev.cpu = $cpu } if ($null -ne $e.disk) { $disk = [VSphereHypervisorDiskMetrics]::new() $disk.read_average = $e.disk.read_average $disk.write_average = $e.disk.write_average $disk.max_total_latency_latest = $e.disk.max_total_latency_latest $ev.disk = $disk } if ($null -ne $e.memory) { $mem = [VSphereHypervisorMemoryMetrics]::new() $mem.swap_in_rate_average = $e.memory.swap_in_rate_average $mem.swap_out_rate_average = $e.memory.swap_out_rate_average $mem.swap_used_average = $e.memory.swap_used_average $mem.state_latest = $e.memory.state_latest $mem.vm_mem_ctl_average = $e.memory.vm_mem_ctl_average $mem.usage_average = $e.memory.usage_average $ev.memory = $mem } $events += $ev } $di.events = @($events) $dataItems += $di } $p.data = @($dataItems) return $p } function Read-SpoolData { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [VSphereConnectorConfiguration]$Config, [Parameter(Mandatory = $true)] [VSphereConnectorState]$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-VSphereHypervisorPayload -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)] [VSphereConnectorConfiguration]$Config, [Parameter(Mandatory = $true)] [datetimeoffset]$Timestamp, [Parameter(Mandatory = $true)] [object]$Content ) $path = New-SpoolFile -Config $Config -Timestamp $Timestamp $obj = [pscustomobject]@{ schema_version = "1.0.0" content = $Content } Write-SpoolFileAtomic -Path $path -Object $obj return $path } 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)] [VSphereConnectorConfiguration]$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' } } |