From 50a82d14e6d5dd6cd3f8f134113a02d71f5f969e Mon Sep 17 00:00:00 2001 From: Lord Hepipud Date: Sat, 27 Aug 2022 15:05:54 +0200 Subject: [PATCH] Improve cache file writer with more robust handling --- lib/core/cache/Copy-IcingaCacheTempFile.psm1 | 12 ++++++++++ lib/core/cache/Get-IcingaCacheData.psm1 | 18 +++++++++++++-- lib/core/cache/Set-IcingaCacheData.psm1 | 22 +++++++++++++++---- .../cache/Test-IcingaCacheDataTempFile.psm1 | 17 ++++++++++++++ lib/core/logging/Icinga_EventLog_Enums.psm1 | 6 +++++ 5 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 lib/core/cache/Copy-IcingaCacheTempFile.psm1 create mode 100644 lib/core/cache/Test-IcingaCacheDataTempFile.psm1 diff --git a/lib/core/cache/Copy-IcingaCacheTempFile.psm1 b/lib/core/cache/Copy-IcingaCacheTempFile.psm1 new file mode 100644 index 00000000..d1002f52 --- /dev/null +++ b/lib/core/cache/Copy-IcingaCacheTempFile.psm1 @@ -0,0 +1,12 @@ +function Copy-IcingaCacheTempFile() +{ + param ( + [string]$CacheFile = '', + [string]$CacheTmpFile = '' + ); + + # Copy the new file over the old one + Copy-ItemSecure -Path $CacheTmpFile -Destination $CacheFile -Force | Out-Null; + # Remove the old file + Remove-ItemSecure -Path $CacheTmpFile -Retries 5 -Force | Out-Null; +} diff --git a/lib/core/cache/Get-IcingaCacheData.psm1 b/lib/core/cache/Get-IcingaCacheData.psm1 index 1b779fdf..27004fb0 100644 --- a/lib/core/cache/Get-IcingaCacheData.psm1 +++ b/lib/core/cache/Get-IcingaCacheData.psm1 @@ -16,6 +16,9 @@ .PARAMETER KeyName This is the actual cache file located under icinga-powershell-framework/cache///.json Please note to only provide the name without the '.json' apendix. This is done by the module itself +.PARAMETER TempFile + To safely write data, by default Icinga for Windows will write all content into a .tmp file at the same location with the same name + before applying it to the proper file. Set this argument to read the content of a temp file instead .INPUTS System.String .OUTPUTS @@ -29,13 +32,19 @@ function Get-IcingaCacheData() param( [string]$Space, [string]$CacheStore, - [string]$KeyName + [string]$KeyName, + [switch]$TempFile = $FALSE ); $CacheFile = Join-Path -Path (Join-Path -Path (Join-Path -Path (Get-IcingaCacheDir) -ChildPath $Space) -ChildPath $CacheStore) -ChildPath ([string]::Format('{0}.json', $KeyName)); [string]$Content = ''; $cacheData = @{ }; + # Read a tmp file if present + if ($TempFile) { + $CacheFile = [string]::Format('{0}.tmp', $CacheFile); + } + if ((Test-Path $CacheFile) -eq $FALSE) { return $null; } @@ -46,7 +55,12 @@ function Get-IcingaCacheData() return $null; } - $cacheData = ConvertFrom-Json -InputObject ([string]$Content); + try { + $cacheData = ConvertFrom-Json -InputObject ([string]$Content); + } catch { + Write-IcingaEventMessage -EventId 1104 -Namespace 'Framework' -ExceptionObject $_ -Objects $CacheFile; + return $null; + } if ([string]::IsNullOrEmpty($KeyName)) { return $cacheData; diff --git a/lib/core/cache/Set-IcingaCacheData.psm1 b/lib/core/cache/Set-IcingaCacheData.psm1 index a6db9391..0abdb110 100644 --- a/lib/core/cache/Set-IcingaCacheData.psm1 +++ b/lib/core/cache/Set-IcingaCacheData.psm1 @@ -34,14 +34,19 @@ function Set-IcingaCacheData() $Value ); - $CacheFile = Join-Path -Path (Join-Path -Path (Join-Path -Path (Get-IcingaCacheDir) -ChildPath $Space) -ChildPath $CacheStore) -ChildPath ([string]::Format('{0}.json', $KeyName)); - $cacheData = @{ }; + $CacheFile = Join-Path -Path (Join-Path -Path (Join-Path -Path (Get-IcingaCacheDir) -ChildPath $Space) -ChildPath $CacheStore) -ChildPath ([string]::Format('{0}.json', $KeyName)); + $CacheTmpFile = [string]::Format('{0}.tmp', $CacheFile); + $cacheData = @{ }; + + if ((Test-IcingaCacheDataTempFile -Space $Space -CacheStore $CacheStore)) { + Copy-IcingaCacheTempFile -CacheFile $CacheFile -CacheTmpFile $CacheTmpFile; + } if ((Test-Path $CacheFile)) { $cacheData = Get-IcingaCacheData -Space $Space -CacheStore $CacheStore; } else { try { - New-Item -ItemType File -Path $CacheFile -Force -ErrorAction Stop | Out-Null; + New-Item -ItemType File -Path $CacheTmpFile -Force -ErrorAction Stop | Out-Null; } catch { Exit-IcingaThrowException -InputString $_.Exception -CustomMessage (Get-IcingaCacheDir) -StringPattern 'NewItemUnauthorizedAccessError' -ExceptionType 'Permission' -ExceptionThrown $IcingaExceptions.Permission.CacheFolder; Exit-IcingaThrowException -CustomMessage $_.Exception -ExceptionType 'Unhandled' -Force; @@ -60,5 +65,14 @@ function Set-IcingaCacheData() } } - Write-IcingaFileSecure -File $CacheFile -Value (ConvertTo-Json -InputObject $cacheData -Depth 100); + # First write all content to a tmp file at the same location, just with '.tmp' at the end + Write-IcingaFileSecure -File $CacheTmpFile -Value (ConvertTo-Json -InputObject $cacheData -Depth 100); + + # If something went wrong, remove the cache file again + if ((Test-IcingaCacheDataTempFile -Space $Space -CacheStore $CacheStore) -eq $FALSE) { + Remove-ItemSecure -Path $CacheTmpFile -Retries 5 -Force | Out-Null; + return; + } + + Copy-IcingaCacheTempFile -CacheFile $CacheFile -CacheTmpFile $CacheTmpFile; } diff --git a/lib/core/cache/Test-IcingaCacheDataTempFile.psm1 b/lib/core/cache/Test-IcingaCacheDataTempFile.psm1 new file mode 100644 index 00000000..6a0f612d --- /dev/null +++ b/lib/core/cache/Test-IcingaCacheDataTempFile.psm1 @@ -0,0 +1,17 @@ +function Test-IcingaCacheDataTempFile() +{ + param ( + [string]$Space, + [string]$CacheStore + ); + + # Once the file is written successully, validate it is fine + $tmpContent = Get-IcingaCacheData -Space $Space -CacheStore $CacheStore -TempFile; + + if ($null -eq $tmpContent) { + # File is corrupt or empty + return $FALSE; + } + + return $TRUE; +} diff --git a/lib/core/logging/Icinga_EventLog_Enums.psm1 b/lib/core/logging/Icinga_EventLog_Enums.psm1 index a0427380..abbbac69 100644 --- a/lib/core/logging/Icinga_EventLog_Enums.psm1 +++ b/lib/core/logging/Icinga_EventLog_Enums.psm1 @@ -38,6 +38,12 @@ if ($null -eq $IcingaEventLogEnums -Or $IcingaEventLogEnums.ContainsKey('Framewo 'Details' = 'Icinga for Windows was unable to run a specific command within the namespace content, to load additional extensions and component data into Icinga for Windows.'; 'EventId' = 1103; }; + 1104 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Unable to read Icinga for Windows cache file'; + 'Details' = 'Icinga for Windows could not read the specified cache file, as the content seems to be corrupt. This happens mostly in case of unexpected shutdowns or terminations during the write process.'; + 'EventId' = 1104; + }; 1400 = @{ 'EntryType' = 'Error'; 'Message' = 'Icinga for Windows background daemon not found';