Private/Invoke-AzLocalServiceNowAdapter.ps1
|
function Invoke-AzLocalServiceNowAdapter { <# .SYNOPSIS ServiceNow Table API adapter for the AzLocal.UpdateManagement ITSM connector. .DESCRIPTION Handles every ServiceNow HTTP interaction needed by Phase 1: - Action 'GetToken' -> OAuth 2.0 client_credentials grant - Action 'FindByDedupe' -> GET /api/now/table/incident filtered by u_azlocal_dedupe_key - Action 'CreateIncident' -> POST /api/now/table/incident - Action 'AddWorkNote' -> PATCH /api/now/table/incident/{sys_id} with work_notes - Action 'AttachFile' -> POST /api/now/attachment/file?table_name=incident&table_sys_id=... - Action 'TransitionState'-> PATCH /api/now/table/incident/{sys_id} with state/close_code/close_notes - Action 'TestConnection' -> GET /api/now/table/incident?sysparm_limit=1 (auth + incident-scope probe) Phase 1 supports only the OAuth 2.0 client_credentials grant; tokens are acquired per call (caching is planned for a later phase). All HTTP calls flow through Invoke-AzLocalItsmHttp (TLS 1.2, retry, backoff). #> [CmdletBinding()] [OutputType([object])] param( [Parameter(Mandatory = $true)] [ValidateSet('GetToken','FindByDedupe','CreateIncident','AddWorkNote','AttachFile','TransitionState','TestConnection')] [string]$Action, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$InstanceUrl, # Auth (used by GetToken; otherwise ignored if -AccessToken supplied) [Parameter(Mandatory = $false)][string]$ClientId, [Parameter(Mandatory = $false)][string]$ClientSecret, # Pre-obtained bearer token (skips GetToken if supplied) [Parameter(Mandatory = $false)][string]$AccessToken, # Action-specific [Parameter(Mandatory = $false)][string]$DedupeKey, [Parameter(Mandatory = $false)][hashtable]$IncidentFields, [Parameter(Mandatory = $false)][string]$SysId, [Parameter(Mandatory = $false)][string]$WorkNote, [Parameter(Mandatory = $false)][string]$AttachmentPath, [Parameter(Mandatory = $false)][int]$NewState, [Parameter(Mandatory = $false)][string]$CloseCode, [Parameter(Mandatory = $false)][string]$CloseNotes ) $base = $InstanceUrl.TrimEnd('/') # --- Token acquisition / cache lookup --------------------------------- if ($Action -eq 'GetToken') { if ([string]::IsNullOrEmpty($ClientId) -or [string]::IsNullOrEmpty($ClientSecret)) { throw "ServiceNow GetToken requires -ClientId and -ClientSecret." } $tokenUri = "$base/oauth_token.do" $form = @{ grant_type = 'client_credentials' client_id = $ClientId client_secret = $ClientSecret } $bodyStr = ($form.GetEnumerator() | ForEach-Object { "{0}={1}" -f [Uri]::EscapeDataString($_.Key), [Uri]::EscapeDataString([string]$_.Value) }) -join '&' $resp = Invoke-AzLocalItsmHttp -Method POST -Uri $tokenUri -Body $bodyStr -ContentType 'application/x-www-form-urlencoded' if (-not $resp.access_token) { throw "ServiceNow OAuth response did not contain an access_token." } return [pscustomobject]@{ AccessToken = [string]$resp.access_token ExpiresIn = if ($resp.PSObject.Properties['expires_in']) { [int]$resp.expires_in } else { 1800 } TokenType = if ($resp.PSObject.Properties['token_type']) { [string]$resp.token_type } else { 'Bearer' } } } if ([string]::IsNullOrEmpty($AccessToken)) { throw "ServiceNow $Action requires an -AccessToken (obtain via -Action GetToken first)." } $authHeaders = @{ Authorization = "Bearer $AccessToken" Accept = 'application/json' } switch ($Action) { 'TestConnection' { $uri = "$base/api/now/table/incident?sysparm_limit=1&sysparm_fields=sys_id" return Invoke-AzLocalItsmHttp -Method GET -Uri $uri -Headers $authHeaders } 'FindByDedupe' { if ([string]::IsNullOrEmpty($DedupeKey)) { throw "FindByDedupe requires -DedupeKey." } $q = "u_azlocal_dedupe_key=$DedupeKey^stateIN1,2,3" $uri = "$base/api/now/table/incident?sysparm_query=$([Uri]::EscapeDataString($q))&sysparm_limit=1&sysparm_fields=sys_id,number,state,assigned_to,assignment_group,u_azlocal_dedupe_key" $resp = Invoke-AzLocalItsmHttp -Method GET -Uri $uri -Headers $authHeaders if ($resp.result -and $resp.result.Count -gt 0) { return $resp.result[0] } return $null } 'CreateIncident' { if (-not $IncidentFields) { throw "CreateIncident requires -IncidentFields." } $uri = "$base/api/now/table/incident" $resp = Invoke-AzLocalItsmHttp -Method POST -Uri $uri -Headers $authHeaders -Body $IncidentFields return $resp.result } 'AddWorkNote' { if ([string]::IsNullOrEmpty($SysId)) { throw "AddWorkNote requires -SysId." } if ([string]::IsNullOrEmpty($WorkNote)) { throw "AddWorkNote requires -WorkNote." } $uri = "$base/api/now/table/incident/$SysId" $resp = Invoke-AzLocalItsmHttp -Method PATCH -Uri $uri -Headers $authHeaders -Body @{ work_notes = $WorkNote } return $resp.result } 'TransitionState' { if ([string]::IsNullOrEmpty($SysId)) { throw "TransitionState requires -SysId." } if (-not $NewState) { throw "TransitionState requires -NewState." } $payload = @{ state = $NewState } if ($CloseCode) { $payload['close_code'] = $CloseCode } if ($CloseNotes) { $payload['close_notes'] = $CloseNotes } $uri = "$base/api/now/table/incident/$SysId" $resp = Invoke-AzLocalItsmHttp -Method PATCH -Uri $uri -Headers $authHeaders -Body $payload return $resp.result } 'AttachFile' { if ([string]::IsNullOrEmpty($SysId)) { throw "AttachFile requires -SysId." } if ([string]::IsNullOrEmpty($AttachmentPath)) { throw "AttachFile requires -AttachmentPath." } if (-not (Test-Path -Path $AttachmentPath -PathType Leaf)) { throw "AttachFile: file not found at '$AttachmentPath'." } $fileName = Split-Path -Path $AttachmentPath -Leaf $uri = "$base/api/now/attachment/file?table_name=incident&table_sys_id=$SysId&file_name=$([Uri]::EscapeDataString($fileName))" $bytes = [IO.File]::ReadAllBytes($AttachmentPath) $resp = Invoke-AzLocalItsmHttp -Method POST -Uri $uri -Headers $authHeaders -Body $bytes -ContentType 'application/octet-stream' return $resp.result } } } |