Merge pull request #546 from bwuch/bwuch-SkylineInsights

Timeout upates and bugfixes.
This commit is contained in:
kamennikolov
2022-05-26 09:37:56 +03:00
committed by GitHub
3 changed files with 594 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<Configuration>
<ViewDefinitions>
<View>
<Name>SkylineConnection</Name>
<ViewSelectedBy>
<TypeName>SkylineConnection</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Width>30</Width>
<Label>Name</Label>
</TableColumnHeader>
<TableColumnHeader>
<Width>30</Width>
<Label>APIKey</Label>
</TableColumnHeader>
<TableColumnHeader>
<Label>CSPName</Label>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>APIKey</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>CSPName</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>

View File

@@ -0,0 +1,128 @@
<#
Copyright 2021 VMware, Inc.
SPDX-License-Identifier: BSD-2-Clause
#>
#
# Module manifest for module 'VMware.Skyline.InsightsApi'
#
# Generated by: Brian Wuchner
#
# Generated on: 2/21/2022
#
@{
# Script module or binary module file associated with this manifest.
RootModule = 'VMware.Skyline.InsightsApi.psm1'
# Version number of this module.
ModuleVersion = '1.0.0'
# Supported PSEditions
# CompatiblePSEditions = @()
# ID used to uniquely identify this module
GUID = '4dfcb1e5-69b9-405d-aecd-06119ec12649'
# Author of this module
Author = 'Brian Wuchner'
# Company or vendor of this module
CompanyName = 'VMware'
# Copyright statement for this module
Copyright = '(c) VMware. All rights reserved.'
# Description of the functionality provided by this module
Description = 'Community sourced PowerShell wrapper module for the Skyline Insights API.'
# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '4.0'
# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''
# Minimum version of the Windows PowerShell host required by this module
# PowerShellHostVersion = ''
# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# DotNetFrameworkVersion = ''
# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# CLRVersion = ''
# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to importing this module
# RequiredModules = @()
# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()
# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()
# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()
# Format files (.ps1xml) to be loaded when importing this module
FormatsToProcess = @('VMware.Skyline.InsightsApi.Format.ps1xml')
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = @('Connect-SkylineInsights','Disconnect-SkylineInsights','Invoke-SkylineInsightsApi','Get-SkylineFinding',
'Get-SkylineAffectedObject','Format-SkylineResult','Start-SkylineInsightsApiExplorer')
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = @()
# Variables to export from this module
VariablesToExport = '*'
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = @()
# DSC resources to export from this module
# DscResourcesToExport = @()
# List of all modules packaged with this module
# ModuleList = @()
# List of all files packaged with this module
# FileList = @()
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{
PSData = @{
# Tags applied to this module. These help with module discovery in online galleries.
# Tags = @()
# A URL to the license for this module.
# LicenseUri = ''
# A URL to the main website for this project.
# ProjectUri = ''
# A URL to an icon representing this module.
# IconUri = ''
# ReleaseNotes of this module
# ReleaseNotes = ''
} # End of PSData hashtable
} # End of PrivateData hashtable
# HelpInfo URI of this module
# HelpInfoURI = ''
# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''
}

View File

@@ -0,0 +1,425 @@
<#
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"
}