Private/VirtualMachines.ps1
|
function Get-VirtualMachineHash { param( [Parameter(Mandatory = $true)] [NutanixHypervisorVMInfo]$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)] [NutanixConnectorConfiguration]$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)] [NutanixConnectorConfiguration]$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)] [NutanixConnectorConfiguration]$Config ) $tempPath = $null try { $vmStoragePath = Get-VirtualMachinesStoragePath -Config $Config $json = $StorageData | ConvertTo-Json -Compress $tempPath = "$vmStoragePath.tmp-$([Guid]::NewGuid().ToString('N'))" Set-Content -Path $tempPath -Value $json -Encoding utf8 -ErrorAction Stop Move-Item -Path $tempPath -Destination $vmStoragePath -Force -ErrorAction Stop Write-CustomLog -Message "Virtual machines storage file saved successfully at '$vmStoragePath'" -Severity 'INFO' return $true } catch { if ($null -ne $tempPath) { Remove-Item -Path $tempPath -Force -ErrorAction SilentlyContinue } 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]$VmMaxAge ) $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 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)] [NutanixHypervisorPayload]$Payload ) $batchPayload = [NutanixHypervisorPayload]::new() $batchPayload.schema_version = $Payload.schema_version $batchPayload.source = $Payload.source $batchPayload.customer_environment = $Payload.customer_environment $batchPayload.version = $Payload.version return $batchPayload } function New-VirtualMachinesBatchState { param( [Parameter(Mandatory = $true)] [NutanixHypervisorPayload]$Payload ) return [pscustomobject]@{ Payload = New-VirtualMachinesBatchPayload -Payload $Payload DataItemsByHost = [ordered]@{} VmCount = 0 Updates = @{} TotalVmCount = 0 } } function Complete-VirtualMachinesBatch { param( [Parameter(Mandatory = $true)] [pscustomobject]$BatchState, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.Generic.List[NutanixHypervisorPayload]]$Batches, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.Generic.List[hashtable]]$BatchVmUpdates, [Parameter(Mandatory = $true)] [NutanixHypervisorPayload]$Payload ) if ($BatchState.VmCount -eq 0) { return } $BatchState.Payload.data = @($BatchState.DataItemsByHost.Values) $Batches.Add($BatchState.Payload) $BatchVmUpdates.Add($BatchState.Updates) $BatchState.Payload = New-VirtualMachinesBatchPayload -Payload $Payload $BatchState.DataItemsByHost = [ordered]@{} $BatchState.VmCount = 0 $BatchState.Updates = @{} } function Add-VirtualMachineToBatch { param( [Parameter(Mandatory = $true)] [object]$DataItem, [Parameter(Mandatory = $true)] [NutanixHypervisorVMInfo]$VirtualMachine, [Parameter(Mandatory = $true)] [hashtable]$VirtualMachineHashDetails, [Parameter(Mandatory = $true)] [string]$LastUpdated, [Parameter(Mandatory = $true)] [int]$BatchSize, [Parameter(Mandatory = $true)] [pscustomobject]$BatchState, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.Generic.List[NutanixHypervisorPayload]]$Batches, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.Generic.List[hashtable]]$BatchVmUpdates, [Parameter(Mandatory = $true)] [NutanixHypervisorPayload]$Payload ) if ($BatchState.VmCount -ge $BatchSize) { Complete-VirtualMachinesBatch -BatchState $BatchState -Batches $Batches -BatchVmUpdates $BatchVmUpdates -Payload $Payload Write-CustomLog -Message "Starting new virtual machine batch" -Severity 'DEBUG' } $hostName = $DataItem.host.name if (-not $BatchState.DataItemsByHost.Contains($hostName)) { Write-CustomLog -Message "Current batch does not contain host '$hostName'. Creating new data item." -Severity 'DEBUG' $BatchState.DataItemsByHost[$hostName] = @{ host = $DataItem.host events = @() virtual_machines = [System.Collections.Generic.List[NutanixHypervisorVMInfo]]::new() } } $BatchState.DataItemsByHost[$hostName].virtual_machines.Add($VirtualMachine) $BatchState.VmCount++ $BatchState.TotalVmCount++ $BatchState.Updates[$VirtualMachine.name] = @{ hash = $VirtualMachineHashDetails.hash lastUpdated = $LastUpdated } } function Add-ChangedVirtualMachineToBatch { param( [Parameter(Mandatory = $true)] [object]$DataItem, [Parameter(Mandatory = $false)] [AllowNull()] [NutanixHypervisorVMInfo]$VirtualMachine, [Parameter(Mandatory = $true)] [hashtable]$StorageData, [Parameter(Mandatory = $true)] [datetime]$VmMaxAge, [Parameter(Mandatory = $true)] [string]$LastUpdated, [Parameter(Mandatory = $true)] [int]$BatchSize, [Parameter(Mandatory = $true)] [pscustomobject]$BatchState, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.Generic.List[NutanixHypervisorPayload]]$Batches, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.Generic.List[hashtable]]$BatchVmUpdates, [Parameter(Mandatory = $true)] [NutanixHypervisorPayload]$Payload ) if ($null -eq $VirtualMachine -or [string]::IsNullOrWhiteSpace($VirtualMachine.name)) { Write-CustomLog -Message "Skipping VM with null value or missing name in host '$($DataItem.host.name)'" -Severity 'WARNING' return } $vmHashDetails = @{ storageId = $VirtualMachine.name hash = Get-VirtualMachineHash -VirtualMachine $VirtualMachine } if (-not (Test-VirtualMachineChanged -VirtualMachineHashDetails $vmHashDetails -StorageData $StorageData -VmMaxAge $VmMaxAge)) { return } Add-VirtualMachineToBatch -DataItem $DataItem -VirtualMachine $VirtualMachine -VirtualMachineHashDetails $vmHashDetails -LastUpdated $LastUpdated -BatchSize $BatchSize -BatchState $BatchState -Batches $Batches -BatchVmUpdates $BatchVmUpdates -Payload $Payload } function Get-VirtualMachinesDiff { param( [Parameter(Mandatory = $true)] [NutanixHypervisorPayload]$Payload, [Parameter(Mandatory = $true)] [NutanixConnectorConfiguration]$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') $utcTimestamp = $Timestamp.ToUniversalTime() $cacheExpiration = [System.Xml.XmlConvert]::ToTimeSpan($Config.NexthinkAPI.VmCacheExpiration) $vmMaxAge = $utcTimestamp.Add(-$cacheExpiration) $batches = [System.Collections.Generic.List[NutanixHypervisorPayload]]::new() $batchVmUpdates = [System.Collections.Generic.List[hashtable]]::new() $batchState = New-VirtualMachinesBatchState -Payload $Payload foreach ($dataItem in @($Payload.data)) { foreach ($vm in @($dataItem.virtual_machines)) { Add-ChangedVirtualMachineToBatch -DataItem $dataItem -VirtualMachine $vm -StorageData $storageData -VmMaxAge $vmMaxAge -LastUpdated $lastUpdated -BatchSize $BatchSize -BatchState $batchState -Batches $batches -BatchVmUpdates $batchVmUpdates -Payload $Payload } } Complete-VirtualMachinesBatch -BatchState $batchState -Batches $batches -BatchVmUpdates $batchVmUpdates -Payload $Payload Write-CustomLog -Message "Prepared $($batches.Count) virtual machine batches for sending" -Severity 'DEBUG' return [pscustomobject]@{ Payloads = $batches Storage = $storageData BatchVmUpdates = $batchVmUpdates TotalVmCount = $batchState.TotalVmCount } } function Send-VirtualMachines { param( [Parameter(Mandatory = $true)] [NutanixHypervisorPayload]$Payload, [Parameter(Mandatory = $true)] [NutanixConnectorConfiguration]$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 ` -BatchSize $Config.NexthinkAPI.RequestBatchSize if ($null -eq $diff -or $diff.Payloads.Count -eq 0) { Write-CustomLog -Message "No virtual machine changes to send" -Severity 'INFO' return } Write-CustomLog -Message "VMs to send after device change detection filtering: $($diff.TotalVmCount)" -Severity 'INFO' $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] -ExcludeHypervisorEvents $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 } } |