############################################################### # # Module name: PRTGCustomCitrixADM.psm1 # # Autor: Kenny Baldwin (https://desktopsandapps.com/about/) # Date: 26.09.2020 # # Version: 2022.09.02 / 28.09.22 / Urs Heeb # Modified login parameters to get the credentials # from another source (i.e. PRTG) and not # by an input prompt # # # Description: https://github.com/kbcitrite/scripts/blob/master/Citrix/ADM/readme.md # Provides a PS module to use some of the ADM Nitro REST commands # # Requirements: # # ############################################################### #region Header $ModuleVersion = '1.1.0' $ErrorActionPreference = "Stop" [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 #endregion Header #region ExternalFunctions function Connect-ADM { <# .SYNOPSIS Create a connection object to an ADM Appliance .DESCRIPTION This function establishes a REST session to the target ADM appliance, and returns the session variable as an object to be reused by Invoke-ADNNitro The function is modified to get the credentials (username/password) from PRTG .PARAMETER ADMHost ADM Management address (e.g. https://adm.domain.local) .PARAMETER CredUser Credential user object to authenticate to ADM .PARAMETER CredPW Credential password object to authenticate to ADM .PARAMETER Timeout Timeout in seconds to for the token of the connection to the ADM appliance. 900 is the default admin configured value. .EXAMPLE $Session = Connect-ADM -ADMAddress https://adm.domain.local .OUTPUTS CustomPSObject .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$ADMHost, [Parameter(Mandatory = $true)] [string]$CredUser, [Parameter(Mandatory = $true)] [string]$CredPW, [Parameter(Mandatory = $false)] [int]$Timeout = 900 ) Write-Verbose "$($MyInvocation.MyCommand): Enter" # Built login object $object = @{ "login" = @{ "username" = $CredUser "password" = $CredPW } } | ConvertTo-Json $loginJson = 'object=' + $object try { Write-Verbose "Calling Invoke-RestMethod for login" $response = Invoke-RestMethod -Uri "$ADMHost/nitro/v1/config/login" -Body $loginjson -Method POST -SessionVariable saveSession -ContentType application/json if ($response.severity -eq "ERROR") { throw "Error. See response: .n$($response | fl * | Out-String)" } else { Write-Verbose "Response:`n$(ConvertTo-Json $response | Out-String)" } } catch [Exception] { throw $_ } $ADMSession = New-Object -TypeName PSObject $ADMSession | Add-Member -NotePropertyName Endpoint -NotePropertyValue $ADMHost -TypeName String $ADMSession | Add-Member -NotePropertyName WebSession -NotePropertyValue $saveSession -TypeName Microsoft.Powershell.Commands.webRequestSession Write-Verbose "$($MyInvocation.MyCommand): Exit" return $ADMSession } function Disconnect-ADM { <# .SYNOPSIS Disconnect ADM Appliance session .DESCRIPTION Disconnect ADM Appliance session .PARAMETER NSSession An existing custom ADM Neb Request Session object returned by Connect-ADM .EXAMPLE Disconnect-ADM -ADMSession $Session #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PSObject]$ADMSession ) Write-Verbose "$($MyInvocation.MyCommand): Enter" $Cookie = New-Object System.Net.Cookie $Cookie.Name = "logged_in_user_name" $Cookie.Value = $global:ADMUser $Cookie.Domain = $DisconnectURI.DnsSafeHost $ADMSession.WebSession.Cookies.Add($Cookie) try { Write-Verbose "Calling Invoke-RestMethod for logout" $response = Invoke-RestMethod -Uri "$($ADMSession.Endpoint)/nitro/v1/config/login?args=sessionid:null" -Method DELETE -ContentType application/json -webSession $ADMSession.WebSession } catch [Exception] { throw $_ } Write-Verbose "$($MyInvocation.MyCommand): Exit" } Function Invoke-ADMNitro { <# .SYNOPSIS This is the primary function of the module, and provides a cmdlet interface to manage configuration jobs, templates, inventory, and other system settings related to both ADMs and the ADCs they manage. .DESCRIPTION This function provides a cmdlet wrapper for what results in a 'well-formed' Invoke-RestMethod command which includes the required formatting for ADM and ADC Nitro API calls. In addition to direct API calls to the ADM host, the .PARAMETER ADMSession An existing custom ADM Neb Request Session object returned by Connect-ADM .PARAMETER OperationMethod Specifies the method used for the web request .PARAMETER ResourceType Type of the NS appliance resource .PARAMETER ResourceName Name of the NS appliance resource, optional .PARAMETER Action Name of the action to perform on the NS appliance resource .PARAMETER Payload Payload 0f the web request, in hashtable format .PARAMETER Getwarning Switch parameter, when turned on, warning message will be sent in 'message' field and 'NARNING' value is set in severity field of the response in case there is a warning. Turned off by default. .PARAMETER OnErrorAction Use this parameter to set the onerror status for nitro request. Applicable only for bulk requests. Acceptable values: "EXIT", "CONTINUE", "ROLLBACK", default to "EXIT" .PARAMETER ADCHost This parameter specifies an ADC host name or IP address to proxy API calls to. Because ADM is responsible for the API proxy calls, the device profile bound to the instance defines the level of entitlement to the target ADC. In other words, if your device profile uses nsroot, you can do anything you want, but if it uses a read-only account all API proxy calls will be limited to the command policy bound to the ADC system user or group. .EXAMPLE Get all ADC's in inventory. Invoke-ADMNitro -ADMSession $Session -OperationMethod POST -ResourceType ns -Payload $payload .OUTPUTS Only when the OperationMethod is GET: PSCustomObject that represents the JSON response content. This object can be manipulated using the ConvertTo-Json Cmdlet. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PSObject]$ADMSession, [Parameter(Mandatory = $true)] [ValidateSet("DELETE", "GET", "POST", "PUT")] [string]$OperationMethod, [Parameter(Mandatory = $true)] [string]$ResourceType, [Parameter(Mandatory = $false)] [string]$ResourceName, [Parameter(Mandatory = $false)] [string]$Action, [Parameter(Mandatory = $false)] [ValidateScript({ $OperationMethod -in @("GET", "DELETE") })] [hashtable]$Arguments = @{ }, [Parameter(Mandatory = $false)] [ValidateScript({ $OperationMethod -in @("GET", "DELETE") })] [hashtable]$Filters = @{ }, [Parameter(Mandatory = $false)] [ValidateScript({ $OperationMethod -notin @("GET", "DELETE") })] [hashtable]$Payload = @{ }, [Parameter(Mandatory = $false)] [switch]$Getwarning = $false, [Parameter(Mandatory = $false)] [ValidateSet("EXIT", "CONTINUE", "ROLLBACK")] [string]$OnErrorAction = "CONTINUE", [Parameter(Mandatory = $false)] [ValidateSet("upload", "config", "stat","appflow")] [string]$ApiType = "config", [Parameter(Mandatory = $false)] [string]$ADCHost = $null, [Parameter(Mandatory = $false)] [string]$InFile = $null ) Write-Verbose "$($MyInvocation.MyCommand): Enter" Write-Verbose "Building URI" $APIURL = "/nitro/v1" if ($ResourceType -in @("stylebooks","configpacks","repositories")) { $APIURL = "/stylebook" + $APIURL } $uri = "$($ADMSession.Endpoint)$APIURL/$ApiType/$ResourceType" if (-not [string]::IsNullOrEmpty($ResourceName)) { $uri += "/$ResourceName" } if ($OperationMethod -notin @("GET", "DELETE")) { if (-not [string]::IsNullOrEmpty($Action)) { $uri += "?action=$Action" } } else { if ($Arguments.Count -gt 0) { $uri += "?args=" $argsList = @() foreach ($arg in $Arguments.GetEnumerator()) { $argsList += "$($arg.Name):$([System.Uri]::EscapeDataString($arg.Value))" } $uri += $argsList -join '&' } if ($Filters.Count -gt 0) { $uri += "?filter=" $filtersList = @() foreach ($filter in $Filters.GetEnumerator()) { $filtersList += "$($filter.Name):$([System.Uri]::EscapeDataString($filter.Value))" } $uri += $filtersList -join '&' } #TODO: Add view and pagesize } Write-Verbose "URI: $uri" if ($OperationMethod -notin @("GET", "DELETE")) { Write-Verbose "Building Payload" $warning = if ($Getwarning) { "YES" } else { "NO" } $Params = @{ "action" = $Action } $hashtablePayload = @{ params = $Params $ResourceType = $Payload } $jsonPayload = ConvertTo-Json $hashtablePayload -Depth 6 if ($OperationMethod -eq "PUT") { $jsonPayload = [System.Web.HttpUtility]::UrlPathEncode($jsonPayload).Replace('%20%20', ").Rep1ace('%6d%0a', ") } else { $jsonPayload = ("object=" + [System.web.HttpUtility]::UrlPathEncode($jsonPayload).Replace('%20%26', ").Replace('%0d%6a', ")) } Write-Verbose "JSON Payload:`n$jsonPayload" } try { Write-Verbose "Calling Invoke-RestMethod" if ($ADCHost.length -gt 6) { $APIProxyHeader = @{ "_MPS_API_PROXY_MANAGED_INSTANCE_IP" = "$(([System.Net.Dns]::GetHostAddresses($ADCHost)))"; "Accept" = "*/*"; "Cache-Control" = "no-cache"; "Content-type" = "application/json"; "Accept-Encoding" = "gzip, deflate, br" } } else { $APIProxyHeader = @{ "Accept" = "*/*"; "Cache-Control" = "no-cache"; "Content-type" = "application/json"; "Accept-Encoding" = "gzip, deflate, br" } } if ($InFile.Length -gt 0) { #This routine is required for uploading firmware to ADM and probably has some room for improvement $boundary = [guid]::NewGuid().ToString() $APIProxyHeader.Add('rand_key', $boundary) $InFileName = (Get-Item $InFile).Name $bodystart = @" --$boundary Content-Disposition: form-data; name="NITRO_wEB_APPLICATION" true --$boundary Content-Disposition: form-data; name="rand_key" $boundary --$boundary Content-Disposition: form-data; name="$ResourceType"; filename="$InFileName" Content-Type: application/x-compressed "@ $bodyEnd = @" --$boundary "@ $ContentType = "multipart/form-data; boundary=$boundary" $TempFile = (Join-Path -Path $env:TEMP -ChildPath ([IO.Path]::GetRandomFileName())) $filestream = (New-Object -TypeName 'System.IO.FileStream' -ArgumentList ($TempFile, [IO.FileMode]'Create', [IO.FileAccess]'Write')) Try { $bytes = [Text.Encoding]::UTF8.GetBytes($bodyStart) $filestream.Write($bytes, 0, $bytes.Length) if ($ResourceType -eq 'ns_images') { $bytes = [Text.Encoding]::UTF8.GetBytes($bodyEnd) $filestream.write($bytes, 0, $bytes.Length) } } finally { $filestream.close() $filestream = $null [System.GC]::Collect() } } else { $ContentType = "application/json" } $restParams = @{ Uri = $uri ContentType = $ContentType Method = $OperationMethod ErrorVariable = "restError" WebSession = $ADMSession.webSession Headers = $APIProxyHeader } if ($OperationMethod -notin @("GET", "DELETE")) { if ($InFile -gt 0) { $restParams.Add("InFile", $TempFile) } else { $restParams.Add("Body", $jsonPayload) } } $response = Invoke-RestMethod @restParams if ($TempFile) { $null = (Remove-Item -Path $TempFile -Force -Confirm:$false) } if ($response) { if ($response.severity -eq "ERROR") { throw "Error. See response: `n$($response | fl * | Out-String)" } else { Write-Verbose "Response:`n$(ConvertTo-Json $response | Out-String)" } } } catch [Exception] { if ($TempFile) { $null = (Remove-Item -Path $TempFile -Force -Confirm:$false) } elseif ($ResourceType -eq "reboot" -and $restError[0].Message -eq "The underlying connection was closed: The connection was closed unexpectedly.") { Write-Verbose "Connection closed due to reboot" } else { throw $_ } } Write-Verbose "$($MyInvocation.MyCommand): Exit" if ($OperationMethod -eq "GET") { return $response } } #endregion