<
.SYNOPSIS
Send queries to a Citrix Delivery Controller or Citrix Cloud and present the results back as PowerShell objects
.DESCRIPTION
Based on code from https:
.PARAMETER ddc
The Delivery Controller to query
.PARAMETER daysago
Number of days ago to return records for
.PARAMETER username
Usernname to return connection failures for otherwise will return all connection failures
.EXAMPLE
'.\Get Citrix OData data.ps1' -ddc ctxddc01
Send a web request to the Delivery Controller ctxddc01 and retrieve the list of all available services
.NOTES
https:
If an auth token is not passed, the Citrix Remote PowerShell SDK must be available in order to get an auth token - https:
[CmdletBinding()]
Param
(
[Parameter(Mandatory)]
[string]$ddc ,
[double]$daysAgo = 1 ,
[string]$username
)
$VerbosePreference = $(if( $PSBoundParameters[ 'verbose' ] ) { $VerbosePreference } else { 'SilentlyContinue' })
$DebugPreference = $(if( $PSBoundParameters[ 'debug' ] ) { $DebugPreference } else { 'SilentlyContinue' })
$ErrorActionPreference = $(if( $PSBoundParameters[ 'erroraction' ] ) { $ErrorActionPreference } else { 'Stop' })
$ProgressPreference = 'SilentlyContinue'
[int]$outputWidth = 400
[bool]$join = $true
[string]$query = 'ConnectionFailureLogs'
[string]$protocol = 'http'
[int]$oDataVersion = 4
[hashtable]$dateFields = @{
'Session' = 'StartDate'
'Connection' = 'BrokeringDate'
'ConnectionFailureLog' = 'FailureDate'
}
[hashtable]$connectionFailureCodes = @{}
if( ( $PSWindow = (Get-Host).UI.RawUI ) -and ($WideDimensions = $PSWindow.BufferSize) )
{
$WideDimensions.Width = $outputWidth
$PSWindow.BufferSize = $WideDimensions
}
Function Invoke-ODataTransform
{
Param
(
[Parameter(ValueFromPipelineByPropertyName=$true,ValueFromPipeline=$true)]
$records
)
Begin
{
$propertyNames = $null
[int]$timeOffset = if( (Get-Date).IsDaylightSavingTime() ) { 1 } else { 0 }
}
Process
{
if( $records -and $records.PSObject.Properties[ 'content' ] )
{
if( ! $propertyNames )
{
$properties = ($records | Select -First 1).content.properties
if( $properties )
{
$propertyNames = $properties | Get-Member -MemberType Properties | Select -ExpandProperty name
}
else
{
$propertyNames = 'NA' -as [string]
}
}
if( $propertyNames -is [string] )
{
$records | Select -ExpandProperty value
}
else
{
ForEach( $record in $records )
{
$h = @{ 'ID' = $record.ID }
$properties = $record.content.properties
ForEach( $propertyName in $propertyNames )
{
$targetProperty = $properties.$propertyName
if($targetProperty -is [Xml.XmlElement])
{
try
{
$h.$propertyName = $targetProperty.'#text'
if( $timeOffset -and ! [string]::IsNullOrEmpty( $h.$propertyName ) -and $targetProperty.type -match 'DateTime' )
{
$h.$propertyName = (Get-Date -Date $h.$propertyName).AddHours( $timeOffset )
}
}
catch
{
}
}
else
{
$h.$propertyName = $targetProperty
}
}
[PSCustomObject]$h
}
}
}
elseif( $records -and $records.PSObject.Properties[ 'value' ] )
{
$records.value
}
}
}
Function Get-DateRanges
{
Param
(
[string]$query ,
$from ,
$to ,
[switch]$selective ,
[int]$oDataVersion
)
$field = $dateFields[ ($query -replace 's$' , '') ]
if( ! $field )
{
if( $selective )
{
return $null
}
$field = 'CreatedDate'
}
if( $oDataVersion -ge 4 )
{
if( $from )
{
"()?`$filter=$field ge $(Get-Date -date $from -format s)Z"
}
if( $to )
{
"and $field le $(Get-Date -date $to -format s)Z"
}
}
else
{
if( $from )
{
"()?`$filter=$field ge datetime'$(Get-Date -date $from -format s)'"
}
if( $to )
{
"and $field le datetime'$(Get-Date -date $to -format s)'"
}
}
}
Function Resolve-CrossReferences
{
Param
(
[Parameter(ValueFromPipelineByPropertyName=$true,ValueFromPipeline=$true)]
$properties ,
[switch]$cloud
)
Process
{
$properties | Where-Object { ( $_.Name -match '^(.*)Id$' -or $_.Name -match '^(SessionKey)$' ) -and ! [string]::IsNullOrEmpty( $Matches[1] ) } | Select-Object -Property Name | ForEach-Object `
{
[string]$id = $Matches[1]
[bool]$current = $false
if( $id -match '^Current(.*)$' )
{
$current = $true
$id = $Matches[1]
}
elseif( $id -eq 'SessionKey' )
{
$id = 'Session'
}
if( ! $tables[ $id ] -and ! $alreadyFetched[ $id ] )
{
if( $cloud )
{
$params.uri = ( "{0}://{1}.xendesktop.net/Citrix/Monitor/OData/v{2}/Data/{3}s" -f $protocol , $customerid , $version , $id )
}
else
{
$params.uri = ( "{0}://{1}/Citrix/Monitor/OData/v{2}/Data/{3}s" -f $protocol , $ddc , $version , $id )
}
$alreadyFetched.Add( $id , $id )
if( $dateFields[ $id ] )
{
$params.uri += Get-DateRanges -query $id -from $from -to $to -oDataVersion $oDataVersion
}
[hashtable]$table = @{}
try
{
Invoke-RestMethod @params | Invoke-ODataTransform | ForEach-Object `
{
$object = $_
[string]$thisId = $null
[string]$keyName = $null
if( $object.PSObject.Properties[ 'id' ] )
{
$thisId = $object.Id
$keyName = 'id'
}
elseif( $object.PSObject.Properties[ 'SessionKey' ] )
{
$thisId = $object.SessionKey
$keyname = 'SessionKey'
}
if( $thisId )
{
[string]$key = $(if( $thisId -match '\(guid''(.*)''\)$' )
{
$Matches[ 1 ]
}
else
{
$thisId
})
$object.PSObject.properties.remove( $key )
$table.Add( $key , $object )
}
ForEach( $property in $object.PSObject.Properties )
{
if( $property.MemberType -eq 'NoteProperty' -and $property.Name -ne $keyName -and $property.Name -ne 'sid' -and $property.Name -match '(.*)Id$' )
{
$property | Resolve-CrossReferences -cloud:$cloud
}
}
}
if( $table.Count )
{
Write-Verbose -Message "Adding table $id with $($table.Count) entries"
$tables.Add( $id , $table )
}
}
catch
{
$nop = $null
}
}
}
}
}
Function Resolve-NestedProperties
{
Param
(
[Parameter(ValueFromPipelineByPropertyName=$true,ValueFromPipeline=$true)]
$properties ,
$previousProperties
)
Process
{
$properties | Where-Object { $_.Name -ne 'sid' -and ( $_.Name -match '^(.*)Id$' -or $_.Name -match '^(Session)Key$' -or $_.Name -match '(EnumValue)' ) -and ! [string]::IsNullOrEmpty( $Matches[1] ) } | . { Process `
{
$property = $_
if( ( $matchedEnum = $Matches[1] ) -eq 'EnumValue' )
{
$lookupTable = $null
if( $property.Name -eq 'ConnectionFailureEnumValue' )
{
if( ! $connectionFailureCodes -or ! $connectionFailureCodes.Count )
{
Write-Verbose -Message "Resolving enum $($property.Name)"
$params[ 'uri' ] = ( "{0}://{1}/Citrix/Monitor/OData/v3/Methods/GetAllMonitoringEnums('SessionFailureCode')/Values" -f $protocol , $ddc )
if( $enums = Invoke-RestMethod @params )
{
ForEach( $enum in $enums )
{
if( $enum.id -match '\((\d+)\)$' )
{
$connectionFailureCodes.Add( $Matches[1] , ( $enum.content.properties | Select-Object -expandProperty Name ) )
}
}
}
}
$lookupTable = $connectionFailureCodes
}
if( $lookupTable )
{
if( [string]$expandedEnum = $connectionFailureCodes[ $property.Value.ToString() ] )
{
[pscustomobject]@{ ( $property.Name -replace $matchedEnum ) = ( $expandedEnum -creplace '([a-z])([A-Z])' , '$1 $2' ) }
}
else
{
Write-Warning -Message "Unable to find enum value $($property.Value) for enum $($property.Name)"
}
}
else
{
Write-Warning -Message "Unable to lookup enumeration $($property.Name)"
}
}
elseif( ! [string]::IsNullOrEmpty( ( $id = ( $Matches[1] -replace '^Current' , '')) ))
{
if ( $table = $tables[ $id ] )
{
if( $property.Value -and ( $item = $table[ ($property.Value -as [string]) ]))
{
$datum.PSObject.properties.remove( $property )
$item.PSObject.Properties | ForEach-Object `
{
[pscustomobject]@{ "$id.$($_.Name)" = $_.Value }
if( $_.Name -ne $property.Name -and ( ! $previousProperties -or ! ( $previousProperties | Where-Object Name -eq $_.Name )))
{
Resolve-NestedProperties -properties $_ -previousProperties $properties
}
}
}
}
}
}}
}
}
[hashtable]$params = @{ 'ErrorAction' = 'SilentlyContinue' }
[hashtable]$alreadyFetched = @{}
$credential = $null
if( $PSBoundParameters[ 'XDusername' ] )
{
if( ! [string]::IsNullOrEmpty( $XDpassword ) )
{
$credential = New-Object System.Management.Automation.PSCredential( $XDusername , ( ConvertTo-SecureString -AsPlainText -String $XDpassword -Force ) )
$XDpassword = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
}
else
{
Throw "Must specify password when using -username either via -password or %RandomKey%"
}
}
if( $credential )
{
$params.Add( 'Credential' , $credential )
}
else
{
$params.Add( 'UseDefaultCredentials' , $true )
}
[int]$highestVersion = $oDataVersion
$fatalException = $null
[int]$version = $oDataVersion
$services = $null
if( $query -cmatch '^[a-z]' )
{
$TextInfo = (Get-Culture).TextInfo
$query = $TextInfo.ToTitleCase( $query ).ToString()
}
if( $PsCmdlet.ParameterSetName -eq 'cloud' )
{
if( ! $PSBoundParameters[ 'authtoken' ] )
{
Add-PSSnapin -Name Citrix.Sdk.Proxy.*
if( ! ( Get-Command -Name Get-XDAuthentication -ErrorAction SilentlyContinue ) )
{
Throw "Unable to find the Get-XDAuthentication cmdlet - is the Virtual Apps and Desktops Remote PowerShell SDK installed ?"
}
Get-XDAuthentication -CustomerId $customerid
if( ! $? )
{
Throw "Failed to get authentication token for Cloud customer id $customerid"
}
$authtoken = $GLOBAL:XDAuthToken
}
$params.Add( 'Headers' , @{ 'Customer' = $customerid ; 'Authorization' = $authtoken } )
$protocol = 'https'
}
[bool]$cloud = $false
[datetime]$from = ($to = Get-Date).AddDays( -$daysAgo )
[array]$data = @( do
{
if( $oDataVersion -le 0 )
{
if( $highestVersion -le 0 )
{
break
}
$version = $highestVersion--
}
if( $PsCmdlet.ParameterSetName -eq 'cloud' )
{
$params[ 'Uri' ] = ( "{0}://{1}.xendesktop.net/Citrix/Monitor/OData/v{2}/Data/{3}" -f $protocol , $customerid , $version , $query ) + (Get-DateRanges -query $query -from $from -to $to -oDataVersion $oDataVersion)
$cloud = $true
}
else
{
$params[ 'Uri' ] = ( "{0}://{1}/Citrix/Monitor/OData/v{2}/Data/{3}" -f $protocol , $ddc , $version , $query ) + (Get-DateRanges -query $query -from $from -to $to -oDataVersion $oDataVersion)
}
Write-Verbose "URL : $($params.Uri)"
try
{
Invoke-RestMethod @params | Invoke-ODataTransform
$fatalException = $null
break
}
catch
{
$fatalException = $_
if( $_.Exception.response.StatusCode -eq 'Unauthorized' )
{
Throw $fatalException
}
elseif( $_.Exception.response.StatusCode -eq 'NotFound' )
{
$fatalException = $null
$oDataVersion = --$version
}
}
} while ( $highestVersion -gt 0 -and $version -gt 0 -and ! $fatalException ))
if( $fatalException )
{
Throw $fatalException
}
if( $services )
{
if( $services.PSObject.Properties[ 'service' ] )
{
$services.service.workspace.collection | Select-Object -Property 'title' | Sort-Object -Property 'title'
}
else
{
$services.value | Sort-Object -Property 'name'
}
}
elseif( $data -and $data.Count )
{
[hashtable]$tables = @{}
$data[0].PSObject.Properties | Resolve-CrossReferences -cloud:$cloud
[int]$originalPropertyCount = $data[0].PSObject.Properties.GetEnumerator()|Measure-Object |Select-Object -ExpandProperty Count
[int]$finalPropertyCount = -1
[array]$results = @( ForEach( $datum in $data )
{
$datum.PSObject.Properties | Where-Object { $_.Name -ne 'sid' -and ( $_.Name -match '^(.*)Id$' -or $_.Name -match '^(Session)Key$' -or $_.Name -match '(EnumValue)' ) -and ! [string]::IsNullOrEmpty( $Matches[1] ) } | . { Process `
{
$property = $_
Resolve-NestedProperties $property | ForEach-Object `
{
$_.PSObject.Properties | Where-Object MemberType -eq 'NoteProperty' | ForEach-Object `
{
Add-Member -InputObject $datum -MemberType NoteProperty -Name $_.Name -Value $_.Value
}
}
}}
if( $finalPropertyCount -lt 0 )
{
$finalPropertyCount = $datum.PSObject.Properties.GetEnumerator()|Measure-Object |Select-Object -ExpandProperty Count
Write-Verbose -Message "Expanded from $originalPropertyCount properties to $finalPropertyCount"
}
$datum
})
if( $results -and $results.Count )
{
Write-Verbose -Message "Start date is $(Get-Date -Date $from -Format G)"
$results | Where-Object { $_.FailureDate -as [datetime] -ge $from -and ( ( [string]::IsNullOrEmpty( $username ) -and $null -ne $_.PSObject.Properties[ 'User.UserName' ] ) -or ( $null -ne $_.PSObject.Properties[ 'User.UserName' ] -and $_.'User.UserName' -eq $username ) ) } | Select-Object -Property 'User.UserName' , @{n='Date';e={$_.FailureDate -as [datetime]}} , ConnectionFailure , @{n='Delivery Group';e={$_.'DesktopGroup.Name'}} , 'Machine.Name' , 'Connection.ClientAddress' , 'Connection.IsReconnect' | Format-Table -AutoSize
}
else
{
Write-Warning "No data returned"
}
}
else
{
Write-Warning "No data returned"
}