Private/VirtualMachines.ps1
|
function Get-VirtualMachineHash { param( [Parameter(Mandatory = $true)] [VSphereHypervisorVMInfo]$VirtualMachine ) # Create a deterministic string representation by sorting properties # This ensures consistent hashing regardless of property order in JSON $sortedProperties = $VirtualMachine.PSObject.Properties | Sort-Object Name $hashInput = "" foreach ($prop in $sortedProperties) { $hashInput += "$($prop.Name):$($prop.Value);" } $hasher = [System.Security.Cryptography.SHA256]::Create() try { $bytes = [System.Text.Encoding]::UTF8.GetBytes($hashInput) $hashBytes = $hasher.ComputeHash($bytes) return [BitConverter]::ToString($hashBytes) -replace '-', '' } finally { $hasher.Dispose() } } function Get-VirtualMachinesStoragePath { param( [Parameter(Mandatory = $true)] [VSphereConnectorConfiguration]$Config ) $storageFolder = Join-Path -Path (Join-Path -Path $Config.ScriptRootPath -ChildPath $script:STORAGE_FOLDER_NAME) -ChildPath $Config.EnvironmentConfig.Name return Join-Path -Path $storageFolder -ChildPath 'virtual_machines.json' } function Read-VirtualMachinesStorage { param( [Parameter(Mandatory = $true)] [VSphereConnectorConfiguration]$Config ) $vmStoragePath = Get-VirtualMachinesStoragePath -Config $Config $vmStorageData = @{} if (Test-Path -Path $vmStoragePath) { try { $storageContent = Get-Content -Path $vmStoragePath -Raw | ConvertFrom-Json -ErrorAction Stop $storageContent.PSObject.Properties | ForEach-Object { # Handle cases where lastUpdated might not be parsed correctly $lastUpdated = if($_.Value.lastUpdated -is [datetime]) { $_.Value.lastUpdated.ToUniversalTime().ToString('o') } else { [string]$_.Value.lastUpdated } $vmData = @{ hash = $_.Value.hash lastUpdated = $lastUpdated } $vmStorageData[$_.Name] = $vmData } Write-CustomLog -Message "Loaded existing virtual machine storage from $vmStoragePath" -Severity 'DEBUG' } catch { Write-CustomLog -Message "Error reading virtual machine storage file: $($_.Exception.Message)" -Severity 'WARNING' throw "Error reading virtual machine storage file: $($_.Exception.Message)" } } return $vmStorageData } function Save-VirtualMachinesStorage { param( [Parameter(Mandatory = $true)] [hashtable]$StorageData, [Parameter(Mandatory = $true)] [VSphereConnectorConfiguration]$Config ) try { $vmStoragePath = Get-VirtualMachinesStoragePath -Config $Config $StorageData | ConvertTo-Json -Compress | Set-Content -Path $vmStoragePath -Force -ErrorAction Stop Write-CustomLog -Message "Virtual machines storage file saved successfully at '$vmStoragePath'" -Severity 'INFO' return $true } catch { Write-CustomLog -Message "Error saving virtual machines storage file: $($_.Exception.Message)" -Severity 'ERROR' return $false } } function Test-VirtualMachineChanged { param( [Parameter(Mandatory = $true)] [object]$VirtualMachineHashDetails, [Parameter(Mandatory = $true)] [hashtable]$StorageData, [Parameter(Mandatory = $true)] [datetime]$Timestamp ) $utcTimestamp = $Timestamp.ToUniversalTime() $storageId = $VirtualMachineHashDetails.storageId $vmHash =$VirtualMachineHashDetails.hash if (-not $storageId) { Write-CustomLog -Message "Skipping VM with empty storage id" -Severity 'WARNING' return $false } if ($StorageData.ContainsKey($storageId)) { $storedVmData = $StorageData[$storageId] if ([string]::IsNullOrEmpty($storedVmData.lastUpdated) -or [string]::IsNullOrEmpty($storedVmData.hash)) { Write-CustomLog -Message "VM '$storageId' found in storage but required properties are missing or empty. Treating as new VM." -Severity 'DEBUG' return $true } $vmLastUpdate = ConvertFrom-RfcUtcTimestamp -Value $storedVmData.lastUpdated $vmMaxAge = $utcTimestamp.AddDays(-1) if ($vmLastUpdate -lt $vmMaxAge) { Write-CustomLog -Message "Update required: VM '$storageId' last update was too long ago." -Severity 'DEBUG' return $true } if ($storedVmData.hash -ne $vmHash) { Write-CustomLog -Message "Storage entry '$storageId' has changed" -Severity 'DEBUG' return $true } else { Write-CustomLog -Message "Storage entry '$storageId' unchanged" -Severity 'DEBUG' return $false } } else { Write-CustomLog -Message "New storage entry found: '$storageId'" -Severity 'DEBUG' return $true } } function New-VirtualMachinesBatchPayload { param( [Parameter(Mandatory = $true)] [VSphereHypervisorPayload]$Payload ) $batchPayload = [VSphereHypervisorPayload]::new() $batchPayload.schema_version = $Payload.schema_version $batchPayload.source = $Payload.source $batchPayload.customer_environment = $Payload.customer_environment $batchPayload.version = $Payload.version return $batchPayload } function Get-VirtualMachinesDiff { param( [Parameter(Mandatory = $true)] [VSphereHypervisorPayload]$Payload, [Parameter(Mandatory = $true)] [VSphereConnectorConfiguration]$Config, [Parameter(Mandatory = $true)] [datetime]$Timestamp, [Parameter(Mandatory = $false)] [int]$BatchSize = $script:VM_ENRICHMENT_BATCH_SIZE ) try { $storageData = Read-VirtualMachinesStorage -Config $Config } catch { Write-CustomLog -Message "Change detection for virtual machines skipped. $($_.Exception.Message)" -Severity 'ERROR' return } $lastUpdated = $Timestamp.ToUniversalTime().ToString('o') $batches = [System.Collections.Generic.List[VSphereHypervisorPayload]]::new() $batchVmUpdates = [System.Collections.Generic.List[hashtable]]::new() $currentBatchPayload = New-VirtualMachinesBatchPayload -Payload $Payload $currentBatchDataItemsByHost = [ordered]@{} $currentBatchVmCount = 0 $currentBatchUpdates = @{} foreach ($dataItem in @($Payload.data)) { foreach ($vm in @($dataItem.virtual_machines)) { if ($null -eq $vm -or [string]::IsNullOrWhiteSpace($vm.name)) { Write-CustomLog -Message "Skipping VM with null value or missing name in host '$($dataItem.host.name)'" -Severity 'WARNING' continue } $vmHashDetails = @{ storageId = $vm.name hash = Get-VirtualMachineHash -VirtualMachine $vm } if (-not (Test-VirtualMachineChanged -VirtualMachineHashDetails $vmHashDetails -StorageData $storageData -Timestamp $Timestamp)) { continue } if ($currentBatchVmCount -ge $BatchSize) { $currentBatchPayload.data = @($currentBatchDataItemsByHost.Values) $batches.Add($currentBatchPayload) $batchVmUpdates.Add($currentBatchUpdates) Write-CustomLog -Message "Starting new virtual machine batch" -Severity 'DEBUG' $currentBatchPayload = New-VirtualMachinesBatchPayload -Payload $Payload $currentBatchDataItemsByHost = [ordered]@{} $currentBatchVmCount = 0 $currentBatchUpdates = @{} } $hostName = $dataItem.host.name if (-not $currentBatchDataItemsByHost.Contains($hostName)) { Write-CustomLog -Message "Current batch does not contain host '$hostName'. Creating new data item." -Severity 'DEBUG' $newDataItem = @{ host = $dataItem.host events = @() virtual_machines = [System.Collections.Generic.List[VSphereHypervisorVMInfo]]::new() } $currentBatchDataItemsByHost[$hostName] = $newDataItem } $currentBatchDataItemsByHost[$hostName].virtual_machines.Add($vm) $currentBatchVmCount++ if (-not [string]::IsNullOrWhiteSpace($vm.name)) { $currentBatchUpdates[$vm.name] = @{ hash = $vmHashDetails.hash lastUpdated = $lastUpdated } } } } if ($currentBatchVmCount -gt 0) { $currentBatchPayload.data = @($currentBatchDataItemsByHost.Values) $batches.Add($currentBatchPayload) $batchVmUpdates.Add($currentBatchUpdates) } Write-CustomLog -Message "Prepared $($batches.Count) virtual machine batches for sending" -Severity 'DEBUG' return [pscustomobject]@{ Payloads = $batches Storage = $storageData BatchVmUpdates = $batchVmUpdates } } function Send-VirtualMachines { param( [Parameter(Mandatory = $true)] [VSphereHypervisorPayload]$Payload, [Parameter(Mandatory = $true)] [VSphereConnectorConfiguration]$Config ) try { # We could use precisely the time at which devices were fetched, # but using the time at which we are preparing to send should be sufficient. $timestamp = Get-Date $diff = Get-VirtualMachinesDiff -Payload $Payload -Config $Config -Timestamp $timestamp if ($null -eq $diff -or $diff.Payloads.Count -eq 0) { Write-CustomLog -Message "No virtual machine changes to send" -Severity 'INFO' return } $requestContext = Get-HypervisorApiRequestContext -Config $Config $storage = $diff.Storage $anySent = $false for ($i = 0; $i -lt $diff.Payloads.Count; $i++) { try { $batchJson = ConvertTo-HypervisorPayloadJson -Payload $diff.Payloads[$i] $response = Invoke-HypervisorApi -RequestContext $requestContext -PayloadJson $batchJson foreach ($key in $diff.BatchVmUpdates[$i].Keys) { $storage[$key] = $diff.BatchVmUpdates[$i][$key] } $anySent = $true Write-CustomLog -Message "Successfully sent virtual machines batch. Response code: $($response.StatusCode)" -Severity 'INFO' -NoCache } catch { Write-CustomLog -Message "Failed to send virtual machines batch. Error=$($_.Exception.Message)" -Severity 'ERROR' -NoCache } } } catch { Write-CustomLog -Message "Virtual machines change detection and sending failed. Details: $($_.Exception.Message)" -Severity 'ERROR' return } if ($anySent) { Save-VirtualMachinesStorage -StorageData $storage -Config $Config | Out-Null } } |