diff --git a/doc/100-General/10-Changelog.md b/doc/100-General/10-Changelog.md index 2b786a2d..6ecfa3c6 100644 --- a/doc/100-General/10-Changelog.md +++ b/doc/100-General/10-Changelog.md @@ -20,6 +20,7 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic * [#672](https://github.com/Icinga/icinga-powershell-framework/pull/issues) Fixes Icinga for Windows REST-Api to fully read client data, even when they client is sending the packets on a very slow basis, preventing the API trying to process an incomplete request * [#707](https://github.com/Icinga/icinga-powershell-framework/pull/707) Fixes size of the `Icinga for Windows` eventlog by setting it to `20MiB`, allowing to store more events before they are overwritten * [#708](https://github.com/Icinga/icinga-powershell-framework/pull/708) Fixes the order for updating components with `Update-Icinga`, to ensure the `framework` is always updated first before all other components +* [#709](https://github.com/Icinga/icinga-powershell-framework/pull/709) Fixes error handling for Icinga for Windows repositories by providing more details about occurring errors as well as properly checking the JSON-File for the repository and providing more details about JSON errors * [#710](https://github.com/Icinga/icinga-powershell-framework/pull/710) Fixes various console errors while running Icinga for Windows outside of an administrative shell * [#713](https://github.com/Icinga/icinga-powershell-framework/pull/713) Fixes Icinga for Windows REST-Api which fails during certificate auth handling while running as `NT Authority\NetworkService` * [#714](https://github.com/Icinga/icinga-powershell-framework/pull/714) Fixes missing service environment information during initial setup of Icinga for Windows v1.12 on some systems diff --git a/lib/core/framework/New-IcingaEnvironmentVariable.psm1 b/lib/core/framework/New-IcingaEnvironmentVariable.psm1 index 2355ec93..80ad5cb6 100644 --- a/lib/core/framework/New-IcingaEnvironmentVariable.psm1 +++ b/lib/core/framework/New-IcingaEnvironmentVariable.psm1 @@ -27,6 +27,12 @@ function New-IcingaEnvironmentVariable() $Global:Icinga.Private.Add('Documentation', @{ }); $Global:Icinga.Private.Add('Timers', @{ }); $Global:Icinga.Private.Add('ProgressStatus', @{ }); + $Global:Icinga.Private.Add( + 'RepositoryStatus', + @{ + 'FailedRepositories' = @{ }; + } + ); $Global:Icinga.Private.Add( 'Scheduler', diff --git a/lib/core/repository/Add-IcingaRepositoryErrorState.psm1 b/lib/core/repository/Add-IcingaRepositoryErrorState.psm1 new file mode 100644 index 00000000..ca48efe6 --- /dev/null +++ b/lib/core/repository/Add-IcingaRepositoryErrorState.psm1 @@ -0,0 +1,32 @@ +function Add-IcingaRepositoryErrorState() +{ + param ( + [string]$Repository = $null + ); + + if ([string]::IsNullOrEmpty($Repository)) { + return; + } + + if ($Global:Icinga -eq $null) { + return; + } + + if ($Global:Icinga.Contains('Private') -eq $FALSE) { + return; + } + + if ($Global:Icinga.Private.Contains('RepositoryStatus') -eq $FALSE) { + return; + } + + if ($Global:Icinga.Private.RepositoryStatus.Contains('FailedRepositories') -eq $FALSE) { + return; + } + + if ($Global:Icinga.Private.RepositoryStatus.FailedRepositories.ContainsKey($Repository)) { + return; + } + + $Global:Icinga.Private.RepositoryStatus.FailedRepositories.Add($Repository, $TRUE); +} diff --git a/lib/core/repository/Clear-IcingaRepositoryErrorState.psm1 b/lib/core/repository/Clear-IcingaRepositoryErrorState.psm1 new file mode 100644 index 00000000..3fd048e0 --- /dev/null +++ b/lib/core/repository/Clear-IcingaRepositoryErrorState.psm1 @@ -0,0 +1,16 @@ +function Clear-IcingaRepositoryErrorState() +{ + if ($Global:Icinga -eq $null) { + return; + } + + if ($Global:Icinga.Contains('Private') -eq $FALSE) { + return; + } + + if ($Global:Icinga.Private.Contains('RepositoryStatus') -eq $FALSE) { + return; + } + + $Global:Icinga.Private.RepositoryStatus.FailedRepositories = @{ }; +} diff --git a/lib/core/repository/Get-IcingaComponentList.psm1 b/lib/core/repository/Get-IcingaComponentList.psm1 index 6322b921..92ca3c06 100644 --- a/lib/core/repository/Get-IcingaComponentList.psm1 +++ b/lib/core/repository/Get-IcingaComponentList.psm1 @@ -15,6 +15,9 @@ function Get-IcingaComponentList() $SearchList | Add-Member -MemberType NoteProperty -Name 'Repos' -Value @(); $SearchList | Add-Member -MemberType NoteProperty -Name 'Components' -Value @{ }; + # Ensure our error list is cleared at this point + Clear-IcingaRepositoryErrorState; + foreach ($entry in $Repositories) { $RepoContent = Read-IcingaRepositoryFile -Name $entry.Name; diff --git a/lib/core/repository/Get-IcingaInstallation.psm1 b/lib/core/repository/Get-IcingaInstallation.psm1 index 799a9487..f9420230 100644 --- a/lib/core/repository/Get-IcingaInstallation.psm1 +++ b/lib/core/repository/Get-IcingaInstallation.psm1 @@ -7,6 +7,9 @@ function Get-IcingaInstallation() Set-IcingaServiceEnvironment; + # Ensure our error list is cleared at this point + Clear-IcingaRepositoryErrorState; + [hashtable]$InstalledComponents = @{ }; $PowerShellModules = Get-Module -ListAvailable; diff --git a/lib/core/repository/Install-IcingaComponent.psm1 b/lib/core/repository/Install-IcingaComponent.psm1 index ece2f219..c86bc09c 100644 --- a/lib/core/repository/Install-IcingaComponent.psm1 +++ b/lib/core/repository/Install-IcingaComponent.psm1 @@ -1,12 +1,13 @@ function Install-IcingaComponent() { param ( - [string]$Name = $null, - [string]$Version = $null, - [switch]$Release = $FALSE, - [switch]$Snapshot = $FALSE, - [switch]$Confirm = $FALSE, - [switch]$Force = $FALSE + [string]$Name = $null, + [string]$Version = $null, + [switch]$Release = $FALSE, + [switch]$Snapshot = $FALSE, + [switch]$Confirm = $FALSE, + [switch]$Force = $FALSE, + [switch]$KeepRepoErrors = $FALSE ); if ([string]::IsNullOrEmpty($Name)) { @@ -14,6 +15,10 @@ function Install-IcingaComponent() return; } + if ($KeepRepoErrors -eq $FALSE) { + Clear-IcingaRepositoryErrorState; + } + # Branch snapshot versions will have '/' inside their name if ($Name.Contains('/') -And $Snapshot) { $Name = $Name.Split('/')[0]; diff --git a/lib/core/repository/Read-IcingaRepositoryFile.psm1 b/lib/core/repository/Read-IcingaRepositoryFile.psm1 index 4ae3619b..f813b9ca 100644 --- a/lib/core/repository/Read-IcingaRepositoryFile.psm1 +++ b/lib/core/repository/Read-IcingaRepositoryFile.psm1 @@ -1,8 +1,9 @@ function Read-IcingaRepositoryFile() { param ( - [string]$Name = $null, - [switch]$TryAlternate = $FALSE + [string]$Name = $null, + [switch]$TryAlternate = $FALSE, + [switch]$PrintRetryMsg = $FALSE ); if ([string]::IsNullOrEmpty($Name)) { @@ -10,6 +11,10 @@ function Read-IcingaRepositoryFile() return $null; } + if ((Test-IcingaRepositoryErrorState -Repository $Name) -And $TryAlternate -eq $FALSE) { + return $null; + } + $Name = $Name.Replace('.', '-'); $Repository = Get-IcingaPowerShellConfig -Path ([string]::Format('Framework.Repository.Repositories.{0}', $Name)); @@ -22,6 +27,10 @@ function Read-IcingaRepositoryFile() $RepoPath = $null; $Content = $null; + if ($PrintRetryMsg) { + Write-IcingaConsoleNotice 'Unable to fetch Icinga for Windows repository information for repository "{0}" from provided location. Trying different lookup by adding "ifw.repo.json" to the end of the remote path.' -Objects $Name; + } + if ([string]::IsNullOrEmpty($Repository.LocalPath) -eq $FALSE -And (Test-Path -Path $Repository.LocalPath)) { $RepoPath = $Repository.LocalPath; } elseif ([string]::IsNullOrEmpty($Repository.RemotePath) -eq $FALSE -And (Test-Path -Path $Repository.RemotePath)) { @@ -29,17 +38,19 @@ function Read-IcingaRepositoryFile() } if ([string]::IsNullOrEmpty($RepoPath) -eq $FALSE -And (Test-Path -Path $RepoPath)) { + + if ($TryAlternate) { + $RepoPath = Join-Path $RepoPath -ChildPath 'ifw.repo.json'; + } + if ([IO.Path]::GetExtension($RepoPath).ToLower() -ne '.json' -And $TryAlternate -eq $FALSE) { return (Read-IcingaRepositoryFile -Name $Name -TryAlternate); - } elseif ([IO.Path]::GetExtension($RepoPath).ToLower() -ne '.json' -And $TryAlternat) { + } elseif ([IO.Path]::GetExtension($RepoPath).ToLower() -ne '.json' -And $TryAlternate) { Write-IcingaConsoleError 'Unable to read repository file from "{0}" for repository "{1}". No "ifw.repo.json" was found at defined location' -Objects $RepoPath, $Name; + Add-IcingaRepositoryErrorState -Repository $Name; return $null; } - if ($TryAlternate) { - $RepoPath = Join-Path $RepoPath -ChildPath 'ifw.repo.json'; - } - $Content = Get-Content -Path $RepoPath -Raw; } else { try { @@ -52,19 +63,26 @@ function Read-IcingaRepositoryFile() $WebContent = Invoke-IcingaWebRequest -UseBasicParsing -Uri $RepoPath; if ($null -ne $WebContent) { - if ($WebContent.RawContent.Contains('application/octet-stream')) { - $Content = [System.Text.Encoding]::UTF8.GetString($WebContent.Content) + if ((Test-PSCustomObjectMember -PSObject $WebContent -Name 'RawContent') -Or (Test-PSCustomObjectMember -PSObject $WebContent -Name 'Content')) { + if ((Test-PSCustomObjectMember -PSObject $WebContent -Name 'RawContent') -And $WebContent.RawContent.Contains('application/octet-stream')) { + $Content = [System.Text.Encoding]::UTF8.GetString($WebContent.Content) + } else { + $Content = $WebContent.Content; + } } else { - $Content = $WebContent.Content; + if ($TryAlternate -eq $FALSE) { + return (Read-IcingaRepositoryFile -Name $Name -TryAlternate -PrintRetryMsg); + } + $Content = $null; } } else { if ($TryAlternate -eq $FALSE) { - return (Read-IcingaRepositoryFile -Name $Name -TryAlternate); + return (Read-IcingaRepositoryFile -Name $Name -TryAlternate -PrintRetryMsg); } } } catch { if ($TryAlternate -eq $FALSE) { - return (Read-IcingaRepositoryFile -Name $Name -TryAlternate); + return (Read-IcingaRepositoryFile -Name $Name -TryAlternate -PrintRetryMsg); } else { Write-IcingaConsoleError 'Unable to resolve repository URL "{0}" for repository "{1}": {2}' -Objects $Repository.RemotePath, $Name, $_.Exception.Message; return $null; @@ -74,15 +92,21 @@ function Read-IcingaRepositoryFile() if ($null -eq $Content) { Write-IcingaConsoleError 'Unable to fetch data for repository "{0}" from any configured location' -Objects $Name; + Add-IcingaRepositoryErrorState -Repository $Name; return $null; } - try { + $RepositoryObject = $null; + + if (Test-IcingaJSONObject -InputObject $Content) { $RepositoryObject = ConvertFrom-Json -InputObject $Content -ErrorAction Stop; - } catch { + } else { + Write-IcingaConsoleError 'Failed to convert retreived content from repository "{0}" with location "{1}" to JSON' -Objects $Name, $Repository.RemotePath if ($TryAlternate -eq $FALSE) { - return (Read-IcingaRepositoryFile -Name $Name -TryAlternate); + return (Read-IcingaRepositoryFile -Name $Name -TryAlternate -PrintRetryMsg); } + + Add-IcingaRepositoryErrorState -Repository $Name; } return $RepositoryObject; diff --git a/lib/core/repository/Search-IcingaRepository.psm1 b/lib/core/repository/Search-IcingaRepository.psm1 index 6b2d995a..2bb44c32 100644 --- a/lib/core/repository/Search-IcingaRepository.psm1 +++ b/lib/core/repository/Search-IcingaRepository.psm1 @@ -30,6 +30,9 @@ function Search-IcingaRepository() $SearchList = New-Object -TypeName PSObject; $SearchList | Add-Member -MemberType NoteProperty -Name 'Repos' -Value @(); + # Ensure our error list is cleared at this point + Clear-IcingaRepositoryErrorState; + foreach ($entry in $Repositories) { $RepoContent = Read-IcingaRepositoryFile -Name $entry.Name; diff --git a/lib/core/repository/Test-IcingaJSONObject.psm1 b/lib/core/repository/Test-IcingaJSONObject.psm1 new file mode 100644 index 00000000..d06eb83e --- /dev/null +++ b/lib/core/repository/Test-IcingaJSONObject.psm1 @@ -0,0 +1,51 @@ +function Test-IcingaJSONObject() +{ + param ( + [string]$InputObject = $null + ); + + if ([string]::IsNullOrEmpty($InputObject)) { + return $FALSE; + } + + try { + $JSONContent = ConvertFrom-Json -InputObject $InputObject -ErrorAction Stop; + return $TRUE; + } catch { + [string]$ErrMsg = $_.Exception.Message; + + if ($ErrMsg.Contains('(') -And $ErrMsg.Contains(')')) { + try { + [int]$ErrLocation = $ErrMsg.Substring($ErrMsg.IndexOf('(') + 1, $ErrMsg.IndexOf(')') - $ErrMsg.IndexOf('(') - 1) - 1; + [string]$ExceptionMsg = $ErrMsg.Substring(0, $ErrMsg.IndexOf(')') + 1); + [string]$ErrOutput = $InputObject.Substring(0, $ErrLocation); + [array]$ErrArray = $ErrOutput.Split("`n"); + [string]$Indentation = ''; + [string]$ErrLine = ''; + + [int]$tmp = 0; + foreach ($entry in $ErrArray) { + $tmp += 1; + } + + foreach ($character in ([string]($ErrArray[$ErrArray.Count - 2])).ToCharArray()) { + if ([string]::IsNullOrEmpty($character) -Or $character -eq ' ') { + $Indentation += ' '; + } else { + $ErrLine += '^'; + } + } + + $ErrOutput = [string]::Format('{0}{1}{2}{3}', $ErrOutput, (New-IcingaNewLine), $Indentation, $ErrLine); + + Write-IcingaConsoleError 'Failed to parse JSON object. Exception: {0}{1}{2}' -Objects $ExceptionMsg, (New-IcingaNewLine), $ErrOutput; + return $FALSE; + } catch { + Write-IcingaConsoleError 'Failed to parse JSON object: {0}' -Objects $ErrMsg; + return $FALSE; + } + } + } + + return $TRUE; +} diff --git a/lib/core/repository/Test-IcingaRepositoryErrorState.psm1 b/lib/core/repository/Test-IcingaRepositoryErrorState.psm1 new file mode 100644 index 00000000..a9483a8a --- /dev/null +++ b/lib/core/repository/Test-IcingaRepositoryErrorState.psm1 @@ -0,0 +1,32 @@ +function Test-IcingaRepositoryErrorState() +{ + param ( + [string]$Repository + ); + + if ([string]::IsNullOrEmpty($Repository)) { + return $FALSE; + } + + if ($Global:Icinga -eq $null) { + return $FALSE; + } + + if ($Global:Icinga.Contains('Private') -eq $FALSE) { + return $FALSE; + } + + if ($Global:Icinga.Private.Contains('RepositoryStatus') -eq $FALSE) { + return $FALSE; + } + + if ($Global:Icinga.Private.RepositoryStatus.Contains('FailedRepositories') -eq $FALSE) { + return $FALSE; + } + + if ($Global:Icinga.Private.RepositoryStatus.FailedRepositories.ContainsKey($Repository) -eq $FALSE) { + return $FALSE; + } + + return $TRUE; +} diff --git a/lib/core/repository/Update-Icinga.psm1 b/lib/core/repository/Update-Icinga.psm1 index 1cb881ef..1b1e2fcd 100644 --- a/lib/core/repository/Update-Icinga.psm1 +++ b/lib/core/repository/Update-Icinga.psm1 @@ -73,7 +73,7 @@ function Update-Icinga() $UpdateJEA = $TRUE; } - Install-IcingaComponent -Name $entry -Version $NewVersion -Release:$Release -Snapshot:$Snapshot -Confirm:$Confirm -Force:$Force; + Install-IcingaComponent -Name $entry -Version $NewVersion -Release:$Release -Snapshot:$Snapshot -Confirm:$Confirm -Force:$Force -KeepRepoErrors; } # Update JEA profile if JEA is enabled once the update is complete diff --git a/lib/core/tools/Test-PSCustomObjectMember.psm1 b/lib/core/tools/Test-PSCustomObjectMember.psm1 index a69af038..67c5ea0e 100644 --- a/lib/core/tools/Test-PSCustomObjectMember.psm1 +++ b/lib/core/tools/Test-PSCustomObjectMember.psm1 @@ -1,6 +1,6 @@ function Test-PSCustomObjectMember() { - param( + param ( $PSObject, $Name ); @@ -9,5 +9,11 @@ function Test-PSCustomObjectMember() return $FALSE; } + # Lets make sure we also test for hashtables in case our object is a hashtable + # instead of a PSCustomObject + if ($PSObject -Is [hashtable]) { + return ([bool]($PSObject.ContainsKey($Name))); + } + return ([bool]($PSObject.PSObject.Properties.Name -eq $Name)); }