VMware.Skyline.InsightsApi.psm1
<#
Copyright 2021 VMware, Inc. SPDX-License-Identifier: BSD-2-Clause #> Function Connect-SkylineInsights { <# .NOTES =========================================================================== Created by: Brian Wuchner Date: February 21, 2022 Blog: www.enterpriseadmins.org Twitter: @bwuch =========================================================================== .SYNOPSIS Use this function to create the auth header to connect to Skyline Insights API .DESCRIPTION This function will allow you to connect to a Skyline Insights API. A global variable will be set with the Servername & Header value for use by other functions. .EXAMPLE PS C:\> Connect-SkylineInsights -apiKey 'my-key-from-csp' This will use the provided API key to create a connection to Skyline Insights. .EXAMPLE PS C:\> Connect-SkylineInsights -apiKey 'my-key-from-csp' -SaveCredentials This will use the PowerCLI VICredentialStore Item to save the provided API key. On next use this key will be provided automatically. #> param( [string]$apiKey, [switch]$SaveCredentials, [Parameter(DontShow)]$cspApi = 'console.cloud.vmware.com', [Parameter(DontShow)]$skylineApi = 'skyline.vmware.com' ) if ($PSEdition -eq 'Core' -And $SaveCredentials) { write-error 'The parameter SaveCredentials of Connect-SkylineInsights cmdlet is not supported on PowerShell Core.' return } if ($PSEdition -eq 'Core' -AND !$apiKey) { write-error 'An API key is required.' return } # Create VICredentialStore item to save the API key if ($apiKey -AND $SaveCredentials) { if ( (Get-Command Get-VICredentialStoreItem -ErrorAction:SilentlyContinue | Measure-Object).Count -gt 0 ) { $savedCred = Get-VICredentialStoreItem -host $skylineApi -ErrorAction:SilentlyContinue if ($savedCred) { $savedCred | Remove-VICredentialStoreItem -Confirm:$false } New-VICredentialStoreItem -Host $skylineApi -User 'api-key' -Password $apiKey } else { Write-Warning 'Use of -SaveCredentials requires the PowerCLI VICredentialStoreItem cmdlets.' } } if (!$apiKey) { if ( (Get-Command Get-VICredentialStoreItem -ErrorAction:SilentlyContinue | Measure-Object).Count -gt 0 ) { $savedCred = Get-VICredentialStoreItem -host $skylineApi -ErrorAction:SilentlyContinue } if ( ($savedCred | Measure-Object).Count -eq 1) { $apiKey = $savedCred.Password } else { write-error 'An API key is required.' return } } $loginHeader = @{ 'Accept' = 'application/json' 'Content-Type' = 'application/x-www-form-urlencoded' } $loginBody = @{'refresh_token' = $apiKey } try { $webRequest = Invoke-RestMethod -Uri "https://$cspApi/csp/gateway/am/api/auth/api-tokens/authorize?grant_type=refresh_token" -method POST -Headers $loginHeader -Body $loginBody $global:DefaultSkylineConnection = New-Object psobject -property @{ 'Name'=$skylineApi; 'CSPName'=$cspApi; 'ConnectionDetail'=$webRequest; APIKey = $apiKey; 'Refresh_Token'=$webRequest.refresh_token; 'SkylineAPI'="https://$skylineApi/public/api/data"; PSTypeName='SkylineConnection' } # Return the connection object $global:SkylineInsightsApiQueryCount = 0 $global:SkylineInsightsApiQueryLastTime = $null $global:DefaultSkylineConnection } catch { Write-Error ("Failure connecting to $skylineAPI. Posted $loginBody " + $_) } # end try/catch block } Function Disconnect-SkylineInsights { <# .NOTES =========================================================================== Created by: Brian Wuchner Date: February 21, 2022 Blog: www.enterpriseadmins.org Twitter: @bwuch =========================================================================== .SYNOPSIS Use this function to disconnect from Skyline Insights API .DESCRIPTION This function will allow you to disconnect from a Skyline Insights API. The global variable will be set with the Servername & Header value for use by other functions. .EXAMPLE PS C:\> Disconnect-SkylineInsights This will remove a connection to Skyline Insights. #> if ($global:DefaultSkylineConnection) { $global:DefaultSkylineConnection = $null } else { Write-Error 'Could not find an existing connection to SkylineInsights API.' } } Function Invoke-SkylineInsightsApi { <# .NOTES =========================================================================== Created by: Brian Wuchner Date: February 21, 2022 Blog: www.enterpriseadmins.org Twitter: @bwuch =========================================================================== .SYNOPSIS Use this function to post a query to the Skyline Insights API. .DESCRIPTION This function will allow you to query the Skyline Insights API. Proper headers will be formatted and posted if a DefaultSkylineConnection is present. This is primarily a helper function used by other functions included in the module. It is exported in the module manifest to be used for any custom queries. .EXAMPLE PS C:\> Invoke-SkylineInsightsApi -queryBody '{formatted-query-string-converted-to-json}' #> param( [Parameter(Mandatory=$true)][string]$queryBody, [Parameter(DontShow=$true)][int]$sleepTimerMs=501 ) if ( !$global:DefaultSkylineConnection ) { Write-Error 'You are not currently connected to any servers. Please connect first using Connect-SkylineInsights.' return; } write-debug "Querybody: $queryBody" try { if ($global:SkylineInsightsApiQueryLastTime) { $timeSinceLastQuery = (New-TimeSpan $global:SkylineInsightsApiQueryLastTime (Get-Date)).TotalMilliseconds if ($timeSinceLastQuery -lt $sleepTimerMs) { Write-Debug "Waiting $($sleepTimerMs-$timeSinceLastQuery)ms to prevent HTTP 429 TOO_MANY_REQUESTS error" Start-Sleep -Milliseconds ($sleepTimerMs-$timeSinceLastQuery) } } $restCall = invoke-restmethod -method post -Uri $($global:DefaultSkylineConnection.SkylineAPI) -Headers @{Authorization = "Bearer $($global:DefaultSkylineConnection.ConnectionDetail.access_token)"} -body $queryBody -ContentType "application/json" $global:SkylineInsightsApiQueryCount++ $global:SkylineInsightsApiQueryLastTime = Get-Date if ($restCall.errors) { Write-Error $restCall.errors.Message } return $restCall } catch { $incomingError = $_ try { # are nested try/catch blocks the powershell equilivent of vbscript On Error Resume Next? $errorStatusAsJson = ($incomingError | ConvertFrom-Json).status if ($errorStatusAsJson -eq '429 TOO_MANY_REQUESTS') { write-error 'Encountered HTTP 429 TOO_MANY_REQUESTS error, consider increasing sleepTimerMs value.' start-sleep -Milliseconds (2*$sleepTimerMs) break } } catch { # this was the error from trying to cast the incoming error to Json } if (!$errorStatusAsJson) { write-error $incomingError } } } Function Get-SkylineFinding { <# .NOTES =========================================================================== Created by: Brian Wuchner Date: February 21, 2022 Blog: www.enterpriseadmins.org Twitter: @bwuch =========================================================================== .SYNOPSIS Use this function to query findings from the Skyline Insights API. .DESCRIPTION This function will allow you to query the Skyline Insights API for Findings. As described in the documentation, the maximum limit per page is 200 records. This function provides an optional pagesize parameter to request smaller batches, but by default assumes 200 records. .EXAMPLE PS C:\> Get-SkylineFinding #> [cmdletbinding()] param( [Parameter(ValueFromPipelineByPropertyName=$true)][string]$findingId, [Parameter(ValueFromPipelineByPropertyName=$true)][string[]]$products, [Parameter(ValueFromPipelineByPropertyName=$true)][ValidateSet('CRITICAL','MODERATE','TRIVIAL')][string]$severity, [Parameter(DontShow=$true)][ValidateRange(1,200)][int]$pagesize=200 ) begin { $queryBody = @" { activeFindings(limit: $pagesize, start: 0 filter: {}) { findings { findingId accountId findingDisplayName severity products findingDescription findingImpact recommendations kbLinkURLs recommendationsVCF kbLinkURLsVCF categoryName findingTypes firstObserved totalAffectedObjectsCount } totalRecords timeTaken } } "@ } process { if (!$products) { $products = 'NO_PRODUCT_FILTER'} foreach ($thisProduct in $products) { if ($findingId) { $filterString = "findingId: `"$findingId`"," } if ($thisProduct -ne 'NO_PRODUCT_FILTER') { $filterString += "product: `"$thisProduct`"," } # Try to get results the first time $results = @() $thisQueryBody = $queryBody -Replace 'filter: {}', "filter: { $filterString }" $thisIteration = 0 do { $thisQueryBody = $thisQueryBody -Replace 'start: 0', "start: $thisIteration" Write-Debug $thisQueryBody $thisResult = Invoke-SkylineInsightsApi -queryBody (@{'query' = $thisQueryBody} | ConvertTo-Json -Compress) $totalRecords = $thisResult.data.activeFindings.totalRecords if ($severity) { $thisResult.data.activeFindings.Findings | Where-Object {$_.severity -eq $severity} } else { $thisResult.data.activeFindings.Findings } $results += ($thisResult.data.activeFindings.Findings) $thisIteration += $pageSize } while ($results.count -lt $totalRecords ) # end do/while loop #return $results } } end { } } Function Get-SkylineAffectedObject { <# .NOTES =========================================================================== Created by: Brian Wuchner Date: February 21, 2022 Blog: www.enterpriseadmins.org Twitter: @bwuch =========================================================================== .SYNOPSIS Use this function to query affected objects from the Skyline Insights API. .DESCRIPTION This function will allow you to query the Skyline Insights API for affected objects. Input parameters are required for the findingId and product. Products can be provided as an object (from Get-SkylineFinding) or a single product can be specified by name (or delimited list). As described in the documentation, the maximum limit per page is 200 records. This function provides an optional pagesize parameter to request smaller batches, but by default assumes 200 records. .EXAMPLE PS C:\> Get-SkylineAffectedObject -findingId 'vSphere-Vmtoolsmemoryleak-KB#76163' -product 'core-vcenter01.lab.enterpriseadmins.org' This example uses the ByName parameter set to pass in specific findings/product and expects either a single product or a 'separator' delimited list .EXAMPLE PS C:\> Get-SkylineFinding | Select-Object -First 2 | Get-SkylineAffectedObject This example uses the ByObject parameter set to pass in products as an object from Get-SkylineFinding #> [cmdletbinding()] param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)][string]$findingId, [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)][string[]]$products, [Parameter(DontShow=$true)][ValidateRange(1,200)][int]$pagesize=200 ) begin { $queryBody = @" { activeFindings( filter: { findingId: "", product: "", }) { findings { totalAffectedObjectsCount affectedObjects(start: 0, limit: $pagesize) { sourceName objectName objectType version buildNumber solutionTags { type version } firstObserved } } totalRecords timeTaken } } "@ # Try to get results the first time } process { $thisQueryBody = $queryBody -Replace 'findingId: "",', "findingId: `"$findingId`"," foreach ( $thisProduct in $products ) { $thisIteration = 0 $results = @() # reset results variable between products do { $thisQueryBody = $thisQueryBody -Replace 'product: "",', "product: `"$thisProduct`"," $thisQueryBody = $thisQueryBody -Replace 'start: 0', "start: $thisIteration" Write-Debug $thisQueryBody $thisResult = Invoke-SkylineInsightsApi -queryBody (@{'query' = $thisQueryBody} | ConvertTo-Json -Compress) $totalRecords = $thisResult.data.activeFindings.Findings.totalAffectedObjectsCount $thisResult.data.activeFindings.Findings.affectedObjects | Select-Object @{N='findingId';E={$findingId}}, * $results += ($thisResult.data.activeFindings.Findings.affectedObjects) | Select-Object @{N='findingId';E={$findingId}}, * $thisIteration += $pagesize } while ($results.count -lt $totalRecords ) # end do/while loop } # end foreach product loop } } Function Format-SkylineResult { <# .NOTES =========================================================================== Created by: Brian Wuchner Date: February 21, 2022 Blog: www.enterpriseadmins.org Twitter: @bwuch =========================================================================== .SYNOPSIS Use this function to format results from the Skyline Insights API .DESCRIPTION This function will format the output from the Skyline Insights API. For example, Get-SkylineFinding and Get-SkylineAffectedObject will return some strings, date values as numbers, and object properties. This function will convert date numbers to powershell dates and objects to delimiter separated stings. This should help with exporting results to CSV files for example. .EXAMPLE PS C:\> Get-SkylineFinding | Format-SkylineResult | Export-Csv c:\temp\findings.csv -NoTypeInformation This will return Skyline Findings, format them as needed, and export results to a CSV file. #> param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)][PSCustomObject]$inputObject, [string]$separator = '; ' ) begin { $results = @() # To format the dates, we need to add the value returned by the API to the begining of time $startOfTime = Get-Date '1970-01-01' } process { if ( $inputObject.accountId ) { #This appears to be a Finding $results += $inputObject | Select-Object findingId, accountId, findingDisplayName, severity, @{N='product';E={[string]::join($separator, $_.products)}}, findingDescription, findingImpact, @{N='recommendations';E={[string]::Join($separator,$_.recommendations)}}, @{N='kbLinkURLs';E={[string]::Join($separator, $_.kbLinkURLs)}}, @{N='recommendationsVCF';E={[string]::Join($separator,$_.recommendationsVCF)}}, @{N='kbLinkURLsVCF';E={[string]::Join($separator, $_.kbLinkURLsVCF)}}, categoryName, @{N='findingTypes';E={[string]::Join($sep, $_.findingTypes)}}, @{N='firstObserved';E={ $startOfTime+[timespan]::FromMilliseconds($_.firstObserved) }}, totalAffectedObjectsCount } elseif ( $inputObject.objectName ) { #This appears to be an AffectedObject $results += $inputObject | Select-Object findingId, sourceName, objectName, objectType, version, buildNumber, @{N='solutionTags-Type';E={$_.solutionTags.type}}, @{N='solutionTags-Version';E={$_.solutionTags.version}}, @{N='firstObserved';E={ $startOfTime+[timespan]::FromMilliseconds($_.firstObserved) }} } else { write-warning "Unable to determine input object type." } # end inputobject evaluation } #end process end { return $results } } Function Start-SkylineInsightsApiExplorer { <# .NOTES =========================================================================== Created by: Brian Wuchner Date: February 21, 2022 Blog: www.enterpriseadmins.org Twitter: @bwuch =========================================================================== .SYNOPSIS Use this function to launch the Skyline Insights API in a browser. .DESCRIPTION This function will open the Skyline Insights API explorer in the default web browser and populate the clipboard with the necessary authorization header value to enable interactive queries. .EXAMPLE PS C:\> Start-SkylineInsightsApiExplorer #> if ( !$global:DefaultSkylineConnection ) { Write-Error 'You are not currently connected to any servers. Please connect first using Connect-SkylineInsights.' return; } "Default web browser will launch to the Skyline Insights API explorer. In the lower left select 'Request Headers' and paste the authorization/bearer token into the text box. `nNote: this script has updated your clipboard with the required auth token." "{`"Authorization`":`"Bearer $($global:DefaultSkylineConnection.ConnectionDetail.access_token)`"}" | Set-Clipboard Start-Process "https://$($global:DefaultSkylineConnection.Name)/public/api/docs" } |