Nexthink-Citrix-Connector.psm1
<#PSScriptInfo .VERSION 1.7.2 .GUID 8249e67f-497d-4cef-aed1-4b2ccf56f974 .AUTHOR integration.madrid@nexthink.com .COMPANYNAME Nexthink .PROJECTURI https://github.com/nexthink/connectors-platform.copla-citrix-onprem-connector #> <# .DESCRIPTION This script gathers information from Citrix devices and send it to Enrichment API #> # End of parameters definition $env:Path = "$env:SystemRoot\system32;$env:SystemRoot;$env:SystemRoot\System32\Wbem;$env:SystemRoot\System32\WindowsPowerShell\v1.0\" # # Constants definition # New-Variable -Name 'LOG_FORMAT' -Value "[%{timestamp:+yyyy-MM-dd HH:mm:ss.fffffzzz}][%{level:-7}][%{lineno:3}] %{message}" -Option ReadOnly -Scope Script -Force New-Variable -Name 'LOG_RETENTION_DAYS' -Value 7 -Option ReadOnly -Scope Script -Force New-Variable -Name 'LOG_LEVEL' -Value 'INFO' -Option ReadOnly -Scope Script -Force New-Variable -Name 'CITRIX_ENV_CONFIG_NOT_FOUND' -Value 'Configuration for Citrix environment not found' -Option ReadOnly -Scope Script -Force New-Variable -Name 'MISSING_FIELDS' -Value 'Missing required fields' -Option ReadOnly -Scope Script -Force # CITRIX REST API New-Variable -Name 'CITRIX_DIRECTOR_HOST' -Value 'localhost' -Option ReadOnly -Scope Script -Force New-Variable -Name 'CITRIX_CONTROLLER_HOST' -Value 'localhost' -Option ReadOnly -Scope Script -Force New-Variable -Name 'FULL_ENRICHMENT_QUERY' ` -Value ('http://{0}/citrix/monitor/odata/v4/data/Machines?$select=DnsName,HostingServerName,IsPendingUpdate' + '&$expand=DesktopGroup($select=Name,DesktopKind,SessionSupport),Hypervisor($select=Name),Catalog($select=ProvisioningSchemeId)' + '&$filter=(DnsName ne null) and (DnsName ne '''') and (DesktopGroup ne null)') ` -Option ReadOnly -Scope Script -Force New-Variable -Name 'DELTA_ENRICHMENT_QUERY' ` -Value ('http://{0}/citrix/monitor/odata/v4/data/Machines?$select=DnsName,HostingServerName,IsPendingUpdate' + '&$expand=DesktopGroup($select=Name,DesktopKind,SessionSupport),Hypervisor($select=Name),Catalog($select=ProvisioningSchemeId)' + '&$filter=(DnsName ne null) and (DnsName ne '''') and (DesktopGroup ne null) and (PoweredOnDate ge {1})') ` -Option ReadOnly -Scope Script -Force # Buffer to cover delays in reporting data from Citrix # Use a configurable(constant) buffer added to the last delta retrieval time to filter PoweredOnDate, compensating for unknown event/API latency and clock misalignments. New-Variable -Name 'DELTA_BUFFER_IN_SECONDS' -Value 10 -Option ReadOnly -Scope Script -Force # HYPERVISOR TYPE & VIRTUALIZATION TYPE New-Variable -Name 'UNKNOWN' -Value 0 -Option ReadOnly -Scope Script -Force New-Variable -Name 'SHARED' -Value 1 -Option ReadOnly -Scope Script -Force New-Variable -Name 'PERSONAL' -Value 2 -Option ReadOnly -Scope Script -Force New-Variable -Name 'POOLED' -Value 3 -Option ReadOnly -Scope Script -Force # DESKTOP BROKER New-Variable -Name 'CITRIX_CVAD' -Value 1 -Option ReadOnly -Scope Script -Force # ENRICHMENT API New-Variable -Name 'ENRICHMENT_API_HOST' -Value 'api.eu-west-3.dev.nexthink.cloud' -Option ReadOnly -Scope Script -Force New-Variable -Name 'TARGET_CREDENTIALS_NAME' -Value 'nxt-citrix-credentials' -Option ReadOnly -Scope Script -Force New-Variable -Name 'JWT_URL' -Value 'https://{0}/api/v1/token' -Option ReadOnly -Scope Script -Force New-Variable -Name 'ENRICHMENT_URL' -Value 'https://{0}/api/v1/enrichment/data/fields' -Option ReadOnly -Scope Script -Force New-Variable -Name 'REQUEST_BATCH_SIZE' -Value 1000 -Option ReadOnly -Scope Script -Force # EXIT CODES New-Variable -Name 'EXIT_CODE_OK' -Value 0 -Option ReadOnly -Scope Script -Force New-Variable -Name 'EXIT_CODE_ERROR' -Value 1 -Option ReadOnly -Scope Script -Force # TRACE ID New-Variable -Name 'TRACE_ID_HEADER' -Value 'x-enrichment-trace-id' -Option ReadOnly -Scope Script -Force New-Variable -Name 'TRACE_ID_VALUE' -Value ([Guid]::NewGuid()) -Option ReadOnly -Scope Script -Force # JOBS New-Variable -Name 'GET_HYPERVISOR_TYPES_JOB' -Value 'Get-HypervisorTypes' -Option ReadOnly -Scope Script -Force New-Variable -Name 'GET_DISK_IMAGES_JOB' -Value 'Get-DiskImages' -Option ReadOnly -Scope Script -Force # TIMESTAMP New-Variable -Name 'TIMESTAMP' -Value '1970-01-01T00:00:00Z' -Option ReadOnly -Scope Script -Force New-Variable -Name 'EXECUTION_START_DATE' -Value (Get-Date) -Option ReadOnly -Scope Script -Force # # Invoke Main # function Invoke-Main { param( [Parameter(Mandatory = $true)] [string]$CitrixEnvironment, [Parameter(Mandatory = $true)] [string]$ScriptRootPath ) $Version = "1.7.2" $exitCode = $EXIT_CODE_OK $StoreFolder = "$ScriptRootPath\Store" try { Initialize-EnrichmentProcess -CitrixEnvironment $CitrixEnvironment -ScriptRootPath $ScriptRootPath -StoreFolder $StoreFolder Write-CustomLog -Message "Starting Citrix connector $Version for environment $CitrixEnvironment" -Severity 'INFO' Start-EnrichmentProcess -CitrixEnvironment $CitrixEnvironment -StoreFolder $StoreFolder } catch { Write-CustomLog -Message "The execution stopped unexpectedly. Details: $($_.Exception.Message)" -Severity 'ERROR' $exitCode = $EXIT_CODE_ERROR } Write-CustomLog -Message "Stopping Citrix connector $Version with exit code $exitCode`n" -Severity 'INFO' Wait-Logging return $exitCode } function Initialize-EnrichmentProcess { param( [Parameter(Mandatory = $true)] [string]$CitrixEnvironment, [Parameter(Mandatory = $true)] [string]$ScriptRootPath, [Parameter(Mandatory = $true)] [string]$StoreFolder ) # Initialize path-dependent variables $logsFolder = Join-Path -Path (Join-Path -Path $ScriptRootPath -ChildPath "Logs") -ChildPath $CitrixEnvironment $configFileFolder = Join-Path -Path (Join-Path -Path $ScriptRootPath -ChildPath "Config") -ChildPath "config.json" New-Variable -Name 'SCRIPT_FOLDER' -Value $ScriptRootPath -Option ReadOnly -Scope Script -Force New-Variable -Name 'LOGS_FOLDER' -Value $logsFolder -Option ReadOnly -Scope Script -Force New-Variable -Name 'LOGFILE_NAME' -Value (Join-Path -Path $logsFolder -ChildPath "CitrixConnector-%{+yyyyMMdd}.log") -Option ReadOnly -Scope Script -Force New-Variable -Name 'ZIPFILE_NAME' -Value (Join-Path -Path $logsFolder -ChildPath "RotatedLogs.zip") -Option ReadOnly -Scope Script -Force New-Variable -Name 'CONFIG_FILE_NAME' -Value $configFileFolder -Option ReadOnly -Scope Script -Force Initialize-Folder -Path $logsFolder Initialize-Folder -Path $StoreFolder Initialize-Logger Get-ConfigData -CitrixEnvironment $CitrixEnvironment -ConfigFilePath $configFileFolder # Needed to update log level after reading it from the configuration Initialize-Logger New-Variable -Name 'TIMESTAMP' -Value $(Get-Date -Format o) -Option ReadOnly -Scope Script -Force } function Test-ShouldRunFullScan { param( [Parameter(Mandatory = $true)] [string]$LastFullRun ) try { $lastRunTime = Get-StringToDate -DateString $LastFullRun $hourAgo = (Get-NowDateInUTC).AddHours(-1) $isOlderThanHour = $lastRunTime -lt $hourAgo Write-CustomLog -Message "Last full run ($lastRunTime) was $(if ($isOlderThanHour) { 'more' } else { 'less' }) than an hour ago ($hourAgo)" -Severity 'INFO' return $isOlderThanHour } catch { Write-CustomLog -Message "Error checking last full run timestamp: $($_.Exception.Message)" -Severity 'ERROR' # If there's an error reading/parsing the timestamp, consider it as older than an hour return $true } } function Start-EnrichmentProcess { param( [Parameter(Mandatory = $true)] [string]$CitrixEnvironment, [Parameter(Mandatory = $true)] [string]$StoreFolder ) $nextLink = $null $hypervisorTypes = Get-HypervisorTypes $diskImages = Get-DiskImages $StoreFilePath = "$StoreFolder\timestamps.json" $timestampsFromFileStore = Read-TimestampFile -FilePath $StoreFilePath $devices = @() do { $isFullScan = $true if ($null -ne $timestampsFromFileStore -and $null -ne $timestampsFromFileStore.last_full_run) { $isFullScan = Test-ShouldRunFullScan -LastFullRun $timestampsFromFileStore.last_full_run } $citrixQuery = if ($isFullScan) { [String]::Format($FULL_ENRICHMENT_QUERY, $CITRIX_DIRECTOR_HOST) } else { Get-MachineDeltaQuery -LastDeltaRun $timestampsFromFileStore.last_delta_run } $citrixResponse = Read-ApiCitrixData -CitrixQuery $citrixQuery $nextLink = $citrixResponse.nextLink $devices = $devices + $citrixResponse.devices } while ($nextLink) $totalDevicesToSend = $devices.Count if (!$totalDevicesToSend) { return } Write-CustomLog -Message "$totalDevicesToSend device(s) retrieved in total from Citrix API." -Severity 'INFO' for ($devicesOffset = 0; $devicesOffset -lt $totalDevicesToSend; $devicesOffset += $REQUEST_BATCH_SIZE) { $deviceLimit = [Math]::Min($totalDevicesToSend - 1, $devicesOffset + $REQUEST_BATCH_SIZE - 1) $fullCitrixData = Expand-CitrixDataToSend -CitrixData $devices[$devicesOffset..$deviceLimit] -HypervisorTypes $hypervisorTypes -DiskImages $diskImages Send-EnrichmentRequest -FullCitrixData $fullCitrixData -FullScan $isFullScan -CitrixEnvironment $CitrixEnvironment } $lastFullRun = $timestampsFromFileStore.last_full_run; $now = Get-NowDateInUTCAsString if ($isFullScan) { $lastFullRun = $now; } # Always set lastDeltaRun to now, if it was a full run means the delta data is included # Should write all timestamps in Universal Time Write-TimestampFile -FilePath $StoreFilePath -LastFullRun $lastFullRun -LastDeltaRun $now } function Get-MachineDeltaQuery { param( [Parameter(Mandatory = $true)] [string]$LastDeltaRun ) $LastDeltaRunDate = Get-StringToDate -DateString $LastDeltaRun $lastDeltaRunWithBuffer = Get-DateAsString -Date ($LastDeltaRunDate.AddSeconds(-$DELTA_BUFFER_IN_SECONDS)) Write-CustomLog -Message "Retrieving delta data only. Powered on Machines since [$lastDeltaRunWithBuffer]" -Severity 'INFO' return [String]::Format($DELTA_ENRICHMENT_QUERY, $CITRIX_DIRECTOR_HOST, $lastDeltaRunWithBuffer) } function Read-ApiCitrixData ([String]$CitrixQuery) { try { $citrixUserCredentials = Get-CitrixCredentials $citrixResponse = Invoke-WebRequest -Credential $citrixUserCredentials -Uri $CitrixQuery -UseBasicParsing $citrixResponseContent = ConvertFrom-Json $([String]::new($citrixResponse.Content)) $nextLink = $citrixResponseContent.'@odata.nextLink' $listCitrixDevices = $citrixResponseContent.value } catch [Net.WebException], [IO.IOException] { Write-CustomLog "Error sending request to Citrix API. Details: [$($_.Exception.response.StatusCode.value__)] $($_.ErrorDetails)" -Severity "ERROR" throw "Unable to access Citrix API. Details: $($_.Exception.Message)" } catch { throw "Error retrieving devices from Citrix API. Details: $($_.toString())" } Write-CustomLog -Message "$($listCitrixDevices.Count) device(s) retrieved from Citrix API." -Severity 'DEBUG' return @{ devices = $listCitrixDevices; nextLink = $nextLink } } function Get-CitrixCredentials { return $(Get-StoredCredential -Target $CITRIX_CREDENTIALS_NAME) } # # Local Store # function Write-TimestampFile { param( [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter(Mandatory = $true)] [string]$LastFullRun, [Parameter(Mandatory = $true)] [string]$LastDeltaRun ) try { $timestamps = @{ last_full_run = $LastFullRun last_delta_run = $LastDeltaRun } # Convert to JSON and write to file $jsonContent = $timestamps | ConvertTo-Json Write-CustomLog -Message "Updating timestamps in store file - Last Full Run: [$($LastFullRun)], Last Delta Run: [$($LastDeltaRun)]" -Severity 'INFO' $jsonContent | Out-File -FilePath $FilePath -Force -Encoding UTF8 Write-CustomLog -Message "Successfully wrote timestamps to $FilePath" -Severity 'INFO' } catch { Write-CustomLog -Message "Failed to write timestamps to file: $($_.Exception.Message)" -Severity 'ERROR' throw "Failed to write timestamps to file: $($_.Exception.Message)" } } function Read-TimestampFile { param( [Parameter(Mandatory = $true)] [string]$FilePath ) try { if (Test-Path $FilePath) { $content = Get-Content -Path $FilePath -Raw | ConvertFrom-Json return @{ last_full_run = $content.last_full_run last_delta_run = $content.last_delta_run } } else { Write-CustomLog -Message "Timestamp file not found at $FilePath. If this is the first run, this is expected and the file will be created." -Severity 'INFO' return $null } } catch { Write-CustomLog -Message "Failed to read timestamps from file: $($_.Exception.Message)" -Severity 'ERROR' throw "Failed to read timestamps from file: $($_.Exception.Message)" } } # # Date functions # function Get-StringToDate { param( [Parameter(Mandatory = $true)] [string]$DateString ) return ([DateTime]::ParseExact($DateString, "yyyy-MM-ddTHH:mm:ssZ", [System.Globalization.CultureInfo]::InvariantCulture)).ToUniversalTime() } function Get-NowDateInUTC { return (Get-Date).ToUniversalTime() } function Get-DateAsString { param( [Parameter(Mandatory = $true)] [DateTime]$Date ) return $Date.ToString("yyyy-MM-ddTHH:mm:ssZ") } function Get-NowDateInUTCAsString { return Get-DateAsString -Date $(Get-NowDateInUTC) } # # Logging # function Write-CustomLog ([String]$Message, [String]$Severity = 'INFO') { Write-Log -Message $Message -Level $Severity } function Initialize-Folder ([String]$Path) { try { if (-not (Test-Path -Path $Path)) { [Void](New-Item -Path $Path -ItemType 'Directory' -Force -ErrorAction Stop) } } catch { throw "Error creating folder at $Path." } } function Initialize-Logger { Add-LoggingTarget -Name File -Configuration @{ Path = $LOGFILE_NAME Encoding = 'unicode' Level = $LOG_LEVEL Format = $LOG_FORMAT RotateAfterAmount = $LOG_RETENTION_DAYS RotateAmount = 1 CompressionPath = $ZIPFILE_NAME } Set-LoggingCallerScope 2 } # # Config functions # function Get-ConfigData { param( [Parameter(Mandatory = $true)] [string]$CitrixEnvironment, [Parameter(Mandatory = $true)] [string]$ConfigFilePath ) $configData = Read-ConfigFile -ConfigFilePath $ConfigFilePath $citrixEnvironmentConfigData = Get-CitrixEnvironmentConfigData -ConfigData $configData -CitrixEnvName $CitrixEnvironment if ($null -eq $citrixEnvironmentConfigData) { throw "$CITRIX_ENV_CONFIG_NOT_FOUND" } if (-not (Test-ConfigData -ConfigData $configData -CitrixEnvConfigData $citrixEnvironmentConfigData)) { throw "$MISSING_FIELDS" } New-Variable -Name 'LOG_RETENTION_DAYS' -Value $configData.Logging.LogRetentionDays -Option ReadOnly -Scope Script -Force New-Variable -Name 'LOG_LEVEL' -Value $configData.Logging.LogLevel -Option ReadOnly -Scope Script -Force New-Variable -Name 'CITRIX_DIRECTOR_HOST' -Value $citrixEnvironmentConfigData.CitrixDirectorFQDN -Option ReadOnly -Scope Script -Force New-Variable -Name 'CITRIX_CONTROLLER_HOST' -Value $citrixEnvironmentConfigData.CitrixControllerFQDN -Option ReadOnly -Scope Script -Force New-Variable -Name 'CITRIX_CREDENTIALS_NAME' -Value $citrixEnvironmentConfigData.WindowsCredentialEntry -Option ReadOnly -Scope Script -Force New-Variable -Name 'ENRICHMENT_API_HOST' -Value $configData.NexthinkAPI.HostFQDN -Option ReadOnly -Scope Script -Force New-Variable -Name 'TARGET_CREDENTIALS_NAME' -Value $configData.NexthinkAPI.WindowsCredentialEntry -Option ReadOnly -Scope Script -Force New-Variable -Name 'REQUEST_BATCH_SIZE' -Value $configData.NexthinkAPI.RequestBatchSize -Option ReadOnly -Scope Script -Force } function Read-ConfigFile { param( [Parameter(Mandatory = $true)] [string]$ConfigFilePath ) try { return (Get-Content "$ConfigFilePath" -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop) } catch { throw "Error loading config file. Details: $($_.toString())" } } function Get-CitrixEnvironmentConfigData($ConfigData, $CitrixEnvName) { return $ConfigData.CitrixEnvironments | Where-Object { $_.Name -eq $CitrixEnvName } } function Test-ConfigData($ConfigData, $CitrixEnvConfigData) { return ($ConfigData.Logging.LogRetentionDays -and $ConfigData.Logging.LogLevel -and $ConfigData.NexthinkAPI.HostFQDN -and ` $ConfigData.NexthinkAPI.WindowsCredentialEntry -and $ConfigData.NexthinkAPI.RequestBatchSize -and ` $CitrixEnvConfigData.Name -and $CitrixEnvConfigData.CitrixDirectorFQDN -and $CitrixEnvConfigData.CitrixControllerFQDN -and ` $CitrixEnvConfigData.WindowsCredentialEntry) } # # Citrix functions # function Set-HypervisorType([Object[]]$CitrixData, [hashtable]$HypervisorTypes) { if ($HypervisorTypes.Count) { foreach ($device in $CitrixData) { $hypervisorType = "" if ($null -eq $device.Hypervisor -or $null -eq $device.Hypervisor.Name) { $device.Hypervisor = [PSCustomObject]@{} } else { $hypervisorType = $HypervisorTypes[$device.Hypervisor.Name] } $device.Hypervisor | Add-Member -NotePropertyName Type -NotePropertyValue $hypervisorType } } return $CitrixData } function Get-HypervisorTypes() { [hashtable]$hypervisorTypes = @{} try { $hypervisorTypesList = Get-HypervisorTypesFromBroker if (-not $hypervisorTypesList) { throw } foreach ($hypervisorType in $hypervisorTypesList) { $hypervisorTypes[$hypervisorType.Name] = $hypervisorType.HypHypervisorType if ('' -eq $hypervisorType.HypHypervisorType) { Write-CustomLog -Message "Empty hypervisor type for hypervisor '$($hypervisorType.Name)'" -Severity 'WARNING' } } } catch { Write-CustomLog -Message "Error retrieving hypervisors. Hypervisor name won't be enriched." -Severity 'ERROR' } return $hypervisorTypes } function Get-HypervisorTypesFromBroker() { $scriptBlock = { Add-PSSnapin -Name Citrix.Broker* -ErrorAction SilentlyContinue; (Get-BrokerHypervisorConnection -AdminAddress $Using:CITRIX_CONTROLLER_HOST | Select-Object -Property Name, HypHypervisorType) } [PSCredential]$citrixUserCredentials = Get-CitrixCredentials [Void](Start-Job -Name $GET_HYPERVISOR_TYPES_JOB -ScriptBlock $scriptBlock -Credential $citrixUserCredentials) [Void](Wait-Job -Name $GET_HYPERVISOR_TYPES_JOB) return Receive-Job -Name $GET_HYPERVISOR_TYPES_JOB } function Set-DiskImage([Object[]]$CitrixData, [hashtable]$DiskImages) { if ($DiskImages.Count) { foreach ($device in $CitrixData) { $diskImage = "" if ($null -eq $device.Catalog -or $null -eq $device.Catalog.ProvisioningSchemeId) { $device.Catalog = [PSCustomObject]@{} } else { $fullDiskImage = $DiskImages[$device.Catalog.ProvisioningSchemeId] $diskImage = if ($null -ne $fullDiskImage) { $fullDiskImage.Split('\')[-1] } else { $null } } $device.Catalog | Add-Member -NotePropertyName DiskImage -NotePropertyValue $diskImage } } return $CitrixData } function Get-DiskImages() { [hashtable]$diskImages = @{} try { $provSchemesList = Get-DiskImagesFromProvScheme if (-not $provSchemesList) { throw } foreach ($provScheme in $provSchemesList) { $diskImages[[String]$provScheme.ProvisioningSchemeUid] = $provScheme.MasterImageVM if ('' -eq $provScheme.MasterImageVM) { Write-CustomLog -Message "Empty disk image for prov scheme '$($provScheme.ProvisioningSchemeUid)'" -Severity 'WARNING' } } } catch { Write-CustomLog -Message "Error retrieving disk images. Disk image won't be enriched." -Severity 'ERROR' } return $diskImages } function Get-DiskImagesFromProvScheme() { $scriptBlock = { Add-PSSnapin -Name Citrix.Broker* -ErrorAction SilentlyContinue; (Get-ProvScheme -AdminAddress $Using:CITRIX_CONTROLLER_HOST | Select-Object -Property ProvisioningSchemeUid, MasterImageVM) } [PSCredential]$citrixUserCredentials = Get-CitrixCredentials [Void](Start-Job -Name $GET_DISK_IMAGES_JOB -ScriptBlock $scriptBlock -Credential $citrixUserCredentials) [Void](Wait-Job -Name $GET_DISK_IMAGES_JOB) return Receive-Job -Name $GET_DISK_IMAGES_JOB } function Expand-CitrixDataToSend ([Object[]]$CitrixData, [hashtable]$HypervisorTypes, [hashtable]$DiskImages) { $citrixDataWithHypervisor = Set-HypervisorType -CitrixData $CitrixData -HypervisorTypes $HypervisorTypes return Set-DiskImage -CitrixData $citrixDataWithHypervisor -DiskImages $DiskImages } function Send-EnrichmentRequest ([Object[]]$FullCitrixData, [Boolean]$FullScan, [String]$CitrixEnvironment) { # Time in milliseconds since the script started up to when the enrichment request is sent $executionDuration = [Int](((Get-Date) - $EXECUTION_START_DATE).TotalMilliseconds) $clientTelemetry = Get-ClientTelemetry -ExecutionDuration $executionDuration -FullScan $FullScan $jwt = Get-Jwt $enrichmentEndpoint = [String]::Format($ENRICHMENT_URL, $ENRICHMENT_API_HOST) $enrichmentBody = Get-JsonFromCitrixData -CitrixData $FullCitrixData -ClientTelemetry $clientTelemetry -CitrixEnvironment $CitrixEnvironment $response = Invoke-SendDataToEnrichmentAPI -EndpointURL $enrichmentEndpoint -JsonPayload $enrichmentBody -Jwt $jwt switch ( $response.statusCode ) { '200' { Write-CustomLog -Message "[$TRACE_ID_VALUE] Batch with $($FullCitrixData.Count) devices successfully processed by Enrichment API." -Severity 'INFO' } '207' { Write-CustomLog -Message "[$TRACE_ID_VALUE] Batch with $($FullCitrixData.Count) devices partially processed by Enrichment API." -Severity 'INFO' Write-CustomLog -Message "[$TRACE_ID_VALUE] Partial success response: $($response.content)" -Severity 'INFO' } default { $message = "[$TRACE_ID_VALUE] Error sending request to Enrichment API with status code: $($response.statusCode)" Write-CustomLog -Message $message -Severity 'ERROR' throw $message } } } function Get-Jwt () { $jwtUrl = [String]::Format($JWT_URL, $ENRICHMENT_API_HOST) if (-not (Test-ValidWebUrl($jwtUrl))) { throw "Invalid URL to retrieve the token: $jwtUrl" } try { $credentials = Get-ClientCredentials -Target $TARGET_CREDENTIALS_NAME $basicHeader = Get-StringAsBase64 -InputString "$($credentials.clientId):$($credentials.clientSecret)" $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $headers.Add('Authorization', "Basic $basicHeader") $response = Invoke-WebRequest -Uri $jwtUrl -Method 'POST' -Headers $headers -UseBasicParsing $parsedResponse = ConvertFrom-Json $([String]::new($response.Content)) return $parsedResponse.access_token } catch [Net.WebException], [IO.IOException] { Write-CustomLog "Error sending request to get the JWT token. Details: [$($_.Exception.response.StatusCode.value__)] $($_.ErrorDetails)" -Severity "ERROR" throw "Unable to access token endpoint. Details: $($_.Exception.Message)" } catch { throw "An error occurred that could not be resolved. Details: $($_.Exception.Message)" } } function Test-ValidWebUrl($UrlToValidate) { $uri = $UrlToValidate -as [System.Uri] $null -ne $uri.AbsoluteURI -and $uri.Scheme -match '[http|https]' } function Get-ClientCredentials ([String]$Target) { $storedCredentials = Get-StoredCredential -Target $Target if ($storedCredentials -and $null -ne $storedCredentials.UserName -and $null -ne $storedCredentials.Password ) { $userName = $storedCredentials.UserName $securePassword = $storedCredentials.Password $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword) $unsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) return @{ clientId = $userName; clientSecret = $unsecurePassword } } else { throw "Credentials not found or they are empty for Target: $Target" } } function Get-StringAsBase64 ([String]$InputString) { $Bytes = [System.Text.Encoding]::UTF8.GetBytes($InputString) $EncodedText = [Convert]::ToBase64String($Bytes) return $EncodedText } function Get-JsonFromCitrixData ([Object[]]$CitrixData, [PSCustomObject]$ClientTelemetry, [String]$CitrixEnvironment) { $jsonResult = '{"enrichments": [' foreach ($device in $CitrixData) { try { if (Test-MachineIsValid($device)) { $deviceName = Get-MachineName($device.DnsName) $virtualizationType = Get-VirtualizationType -DesktopKind $device.DesktopGroup.DesktopKind -SessionSupport $device.DesktopGroup.SessionSupport $currentRow = '{"identification":[{"name":"device/device/name","value":"' + $deviceName + '"}],' $currentRow = $currentRow + '"fields":[{"name":"device/device/virtualization/desktop_pool","value":"' + $device.DesktopGroup.Name + '"}' $currentRow = $currentRow + ',{"name":"device/device/virtualization/type","value": ' + $virtualizationType + '}' $currentRow = $currentRow + ',{"name":"device/device/virtualization/hostname","value":"' + $device.HostingServerName + '"}' if ($null -ne $device.Hypervisor.Type) { $currentRow = $currentRow + ',{"name":"device/device/virtualization/hypervisor_name","value":"' + $device.Hypervisor.Type + '"}' } $currentRow = $currentRow + ',{"name":"device/device/virtualization/environment_name","value":"' + $CitrixEnvironment +'"}' $currentRow = $currentRow + ',{"name":"device/device/virtualization/desktop_broker","value": ' + $CITRIX_CVAD + '}' if ($false -eq $device.IsPendingUpdate -and $null -ne $device.Catalog.DiskImage) { $currentRow = $currentRow + ',{"name":"device/device/virtualization/disk_image","value":"' + $device.Catalog.DiskImage + '"}' } $currentRow = $currentRow + ',{"name":"device/device/virtualization/last_update","value":"' + $TIMESTAMP + '"}' $currentRow = $currentRow + ']},' $jsonResult = $jsonResult + $currentRow } else { Write-CustomLog -Message "Invalid device: '$($device | ConvertTo-Json -Compress)'" -Severity 'DEBUG' } } catch { Write-CustomLog -Message "Error processing device '$deviceName'. Details: $($_.Exception.Message)" -Severity 'ERROR' } } if ($jsonResult.EndsWith(',')) { $jsonResult = $jsonResult.Substring(0, $jsonResult.Length - 1) } $clientTelemetryJson = $clientTelemetry | ConvertTo-Json -Compress return $jsonResult + '], "domain":"citrix", "clientTelemetry":' + $clientTelemetryJson + '}' } function Get-ClientTelemetry([Int]$ExecutionDuration, [Boolean]$FullScan) { $Version = "1.7.2" # Find a way to read from module manifest return [PSCustomObject]@{ version = $Version executionDurationInMs = $ExecutionDuration fullScan = $FullScan } } function Test-MachineIsValid([PSCustomObject]$Device) { if ($null -ne $Device.DnsName -and $null -ne $Device.DesktopGroup -and $null -ne $Device.DesktopGroup.Name -and '' -ne $Device.DesktopGroup.Name -and $null -ne $Device.DesktopGroup.DesktopKind -and $null -ne $Device.DesktopGroup.SessionSupport ) { $deviceName = Get-MachineName($Device.DnsName) if ('' -ne $deviceName) { return $true } } return $false } function Get-MachineName([String]$DnsName) { return $DnsName.Split('.')[0].ToUpper() } function Get-VirtualizationType([Int]$DesktopKind, [Int]$SessionSupport) { $separator = '@' $switchKey = '' + $DesktopKind + $separator + $SessionSupport $result = switch ( $switchKey ) { '1@2' { $SHARED } '0@1' { $PERSONAL } '1@1' { $POOLED } default { $UNKNOWN } } return $result } function Invoke-SendDataToEnrichmentAPI ([String]$EndpointUrl, [String]$JsonPayload, [String]$Jwt) { try { $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $headers.Add('Content-Type', 'application/json') $headers.Add('Authorization', "Bearer $Jwt") $headers.Add($TRACE_ID_HEADER, $TRACE_ID_VALUE) $response = Invoke-WebRequest -Uri $EndpointUrl -Method 'POST' -Headers $headers -Body $JsonPayload -UseBasicParsing $statusCode = $response.StatusCode $content = $response.Content } catch { Write-CustomLog -Message "[$TRACE_ID_VALUE] Error sending request to Enrichment API. Details: $($_.Exception.Message)" -Severity 'ERROR' Write-CustomLog -Message "[$TRACE_ID_VALUE] Error message: $_" -Severity 'ERROR' $statusCode = $_.Exception.response.StatusCode.value__ } return @{ statusCode = $statusCode; content = $content } } # # Module exports - Only export when running as a module # try { Export-ModuleMember -Function Invoke-Main, Initialize-EnrichmentProcess, Start-EnrichmentProcess, Get-ConfigData, Read-ConfigFile, Get-CitrixEnvironmentConfigData, Test-ConfigData, Get-HypervisorTypes, Get-HypervisorTypesFromBroker, Get-DiskImages, Get-DiskImagesFromProvScheme, Set-HypervisorType, Set-DiskImage, Expand-CitrixDataToSend, Get-JsonFromCitrixData, Test-MachineIsValid, Get-MachineName, Get-VirtualizationType, Get-Jwt, Get-ClientCredentials, Get-StringAsBase64, Test-ValidWebUrl, Send-EnrichmentRequest, Invoke-SendDataToEnrichmentAPI, Read-ApiCitrixData, Get-CitrixCredentials, Get-MachineDeltaQuery, Test-ShouldRunFullScan, Write-TimestampFile, Read-TimestampFile, Get-StringToDate, Get-NowDateInUTC, Get-DateAsString, Get-NowDateInUTCAsString, Write-CustomLog, Initialize-Folder, Initialize-Logger } catch [System.InvalidOperationException] { # Expected when dot-sourced as script during testing } catch { # Silently ignore any other export errors } |