Merge pull request #351 from Icinga:feature/fix_file_writer_corruption

Fix: File writer could cause corruption on parallel read/write

In some cases it can happen, that files are being read and written in parallel, which might cause the file of being corrupted.

This will resolve the issue, as in case of file locks we now do longer modify the file, but wait for the lock being released.
This commit is contained in:
Lord Hepipud 2021-08-21 13:29:40 +02:00 committed by GitHub
commit 3375d78ba2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 172 additions and 96 deletions

View file

@ -23,6 +23,7 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic
* [#335](https://github.com/Icinga/icinga-powershell-framework/pull/335) Fixes Icinga Director Self-Service Zones and CA config for legacy installation wizard
* [#343](https://github.com/Icinga/icinga-powershell-framework/pull/343) Fixes freeze within Icinga Management Console, in case commands which previously existed were removed/renamed or the user applied an invalid configuration with unknown commands as install file or install command
* [#345](https://github.com/Icinga/icinga-powershell-framework/pull/345) Fixes Framework environment variables like `$IcingaEnums` not working with v1.6.0
* [#351](https://github.com/Icinga/icinga-powershell-framework/pull/351) Fixes file writer which could cause corruption on parallel read/write events on the same file
### Enhancements

View file

@ -11,7 +11,7 @@
FunctionsToExport = @( '*' )
CmdletsToExport = @( '*' )
VariablesToExport = '*'
AliasesToExport = @( 'icinga' )
AliasesToExport = @( '*' )
PrivateData = @{
PSData = @{
Tags = @( 'icinga', 'icinga2', 'IcingaPowerShellFramework', 'IcingaPowerShell', 'IcingaforWindows', 'IcingaWindows')

View file

@ -119,7 +119,7 @@ function Write-IcingaFrameworkCodeCache()
$CacheContent += "`r`n";
}
$CacheContent += "Export-ModuleMember -Function @( '*' )";
$CacheContent += "Export-ModuleMember -Function @( '*' ) -Alias @( '*' ) -Variable @( '*' )";
Set-Content -Path $CacheFile -Value $CacheContent;
}
@ -157,7 +157,7 @@ function Publish-IcingaEventlogDocumentation()
if ([string]::IsNullOrEmpty($OutFile)) {
Write-Output $DocContent;
} else {
Set-Content -Path $OutFile -Value $DocContent;
Write-IcingaFileSecure -File $OutFile -Value $DocContent;
}
}

View file

@ -17,22 +17,24 @@ function Read-IcingaPowerShellConfig()
{
$ConfigDir = Get-IcingaPowerShellConfigDir;
$ConfigFile = Join-Path -Path $ConfigDir -ChildPath 'config.json';
$ConfigObject = (New-Object -TypeName PSObject);
[string]$Content = Read-IcingaFileSecure -File $ConfigFile -ExitOnReadError;
if ($global:IcingaDaemonData.FrameworkRunningAsDaemon) {
if ($global:IcingaDaemonData.ContainsKey('Config')) {
return $global:IcingaDaemonData.Config;
if ([string]::IsNullOrEmpty($Content) -eq $FALSE) {
try {
$ConfigObject = (ConvertFrom-Json -InputObject $Content -ErrorAction Stop);
} catch {
New-Item -ItemType Directory -Path (Join-Path -Path $ConfigDir -ChildPath 'corrupt') -ErrorAction SilentlyContinue;
$NewConfigFile = Join-Path -Path $ConfigDir -ChildPath ([string]::Format('corrupt/config_broken_{0}.json', (Get-Date -Format "yyyy-MM-dd-HH-mm-ss-ffff")));
Move-Item -Path $ConfigFile -Destination $NewConfigFile -ErrorAction SilentlyContinue;
New-Item -ItemType File -Path $ConfigFile -ErrorAction SilentlyContinue;
Write-IcingaEventMessage -EventId 1100 -Namespace 'Framework' -Objects $ConfigFile, $Content;
Write-IcingaConsoleError -Message 'Your configuration file "{0}" was corrupt and could not be read. It was moved to "{1}" for review and a new plain file has been created' -Objects $ConfigFile, $NewConfigFile;
$ConfigObject = (New-Object -TypeName PSObject);
}
}
if (-Not (Test-Path $ConfigFile)) {
return (New-Object -TypeName PSObject);
}
[string]$Content = Read-IcingaFileContent -File $ConfigFile;
if ([string]::IsNullOrEmpty($Content)) {
return (New-Object -TypeName PSObject);
}
return (ConvertFrom-Json -InputObject $Content);
return $ConfigObject;
}

View file

@ -25,16 +25,10 @@ function Write-IcingaPowerShellConfig()
$ConfigFile = Join-Path -Path $ConfigDir -ChildPath 'config.json';
if (-Not (Test-Path $ConfigDir)) {
New-Item -Path $ConfigDir -ItemType Directory | Out-Null;
New-Item -Path $ConfigDir -ItemType Directory -ErrorAction SilentlyContinue | Out-Null;
}
$Content = ConvertTo-Json -InputObject $Config -Depth 100;
Set-Content -Path $ConfigFile -Value $Content;
if ($global:IcingaDaemonData.FrameworkRunningAsDaemon) {
if ($global:IcingaDaemonData.ContainsKey('Config')) {
$global:IcingaDaemonData.Config = $Config;
}
}
Write-IcingaFileSecure -File $ConfigFile -Value $Content;
}

View file

@ -40,7 +40,7 @@ function Get-IcingaCacheData()
return $null;
}
$Content = Read-IcingaFileContent -File $CacheFile;
$Content = Read-IcingaFileSecure -File $CacheFile;
if ([string]::IsNullOrEmpty($Content)) {
return $null;

View file

@ -53,17 +53,12 @@ function Set-IcingaCacheData()
$KeyName = $Value
};
} else {
if ($cacheData.PSobject.Properties.Name -ne $KeyName) {
if ($cacheData.PSObject.Properties.Name -ne $KeyName) {
$cacheData | Add-Member -MemberType NoteProperty -Name $KeyName -Value $Value -Force;
} else {
$cacheData.$KeyName = $Value;
}
}
try {
Set-Content -Path $CacheFile -Value (ConvertTo-Json -InputObject $cacheData -Depth 100) | Out-Null;
} catch {
Exit-IcingaThrowException -InputString $_.Exception -CustomMessage (Get-IcingaCacheDir) -StringPattern 'System.UnauthorizedAccessException' -ExceptionType 'Permission' -ExceptionThrown $IcingaExceptions.Permission.CacheFolder;
Exit-IcingaThrowException -CustomMessage $_.Exception -ExceptionType 'Unhandled' -Force;
}
Write-IcingaFileSecure -File $CacheFile -Value (ConvertTo-Json -InputObject $cacheData -Depth 100);
}

View file

@ -35,6 +35,6 @@ function Find-IcingaAgentObjects()
if ([string]::IsNullOrEmpty($OutFile)) {
Write-Output $Result;
} else {
Set-Content -Path $OutFile -Value $Result;
Write-IcingaFileSecure -File $OutFile -Value $Result;
}
}

View file

@ -279,7 +279,7 @@ function Copy-IcingaAgentCACertificate()
$Index,
$response.RawContent.Length - $Index
);
Set-Content -Path (Join-Path $Desination -ChildPath 'ca.crt') -Value $CAContent;
Write-IcingaFileSecure -File (Join-Path $Desination -ChildPath 'ca.crt') -Value $CAContent;
Write-IcingaConsoleNotice ([string]::Format('Downloaded ca.crt from "{0}" to "{1}', $CAPath, $Desination))
} catch {
Write-IcingaConsoleError 'Failed to load any provided ca.crt ressource';

View file

@ -29,7 +29,7 @@ function Set-IcingaAgentNodeName()
$ConfigContent = $NewConfigContent;
}
Set-Content -Path $ConstantsConf -Value $ConfigContent;
Write-IcingaFileSecure -File $ConstantsConf -Value $ConfigContent;
Write-IcingaConsoleNotice ([string]::Format('Your hostname was successfully changed to "{0}"', $Hostname));
}

View file

@ -24,7 +24,7 @@ function Set-IcingaAgentServicePermission()
$NewSystemContent += $line;
}
Set-Content -Path "$SystemPermissions.inf" -Value $NewSystemContent;
Write-IcingaFileSecure -File "$SystemPermissions.inf" -Value $NewSystemContent;
$SystemOutput = Start-IcingaProcess -Executable 'secedit.exe' -Arguments ([string]::Format('/import /cfg "{0}.inf" /db "{0}.sdb"', $SystemPermissions));

View file

@ -15,6 +15,6 @@ function Write-IcingaAgentApiConfig()
$ApiConf = $ApiConf.Substring(0, $ApiConf.Length - 4);
Set-Content -Path (Join-Path -Path (Get-IcingaAgentConfigDirectory) -ChildPath 'features-available\api.conf') -Value $ApiConf;
Write-IcingaFileSecure -File (Join-Path -Path (Get-IcingaAgentConfigDirectory) -ChildPath 'features-available\api.conf') -Value $ApiConf;
Write-IcingaConsoleNotice 'Api configuration has been written successfully';
}

View file

@ -10,5 +10,5 @@ function Write-IcingaAgentObjectList()
$ObjectList = Get-IcingaAgentObjectList;
Set-Content -Path $Path -Value $ObjectList;
Write-IcingaFileSecure -File $Path -Value $ObjectList;
}

View file

@ -66,6 +66,6 @@ function Write-IcingaAgentZonesConfig()
$ZonesConf = $ZonesConf.Substring(0, $ZonesConf.Length - 4);
Set-Content -Path (Join-Path -Path (Get-IcingaAgentConfigDirectory) -ChildPath 'zones.conf') -Value $ZonesConf;
Write-IcingaFileSecure -File (Join-Path -Path (Get-IcingaAgentConfigDirectory) -ChildPath 'zones.conf') -Value $ZonesConf;
Write-IcingaConsoleNotice 'Icinga Agent zones.conf has been written successfully';
}

View file

@ -41,7 +41,7 @@ function Install-Icinga()
}
if ([string]::IsNullOrEmpty($InstallFile) -eq $FALSE) {
$InstallCommand = Read-IcingaFileContent -File $InstallFile;
$InstallCommand = Read-IcingaFileSecure -File $InstallFile -ExitOnReadError;
}
# Use our install command to configure everything

View file

@ -8,7 +8,7 @@ function Export-IcingaForWindowsManagementConsoleInstallationAnswerFile()
}
if (Test-Path ($FilePath)) {
Set-Content -Path (Join-Path -Path $FilePath -ChildPath 'IfW_answer.json') -Value (Get-IcingaForWindowsManagementConsoleConfigurationString);
Write-IcingaFileSecure -File (Join-Path -Path $FilePath -ChildPath 'IfW_answer.json') -Value (Get-IcingaForWindowsManagementConsoleConfigurationString);
$global:Icinga.InstallWizard.NextCommand = 'Install-Icinga';
$global:Icinga.InstallWizard.LastNotice = ([string]::Format('Answer file "IfW_answer.json" successfully exported into "{0}"', $FilePath));
Clear-IcingaForWindowsManagementConsolePaginationCache;

View file

@ -14,6 +14,24 @@ if ($null -eq $IcingaEventLogEnums -Or $IcingaEventLogEnums.ContainsKey('Framewo
'Details' = 'The Framework or is components can issue generic debug message in case the debug log is enabled. Please ensure to disable it, if not used. You can do so with the command "Disable-IcingaFrameworkDebugMode"';
'EventId' = 1000;
};
1100 = @{
'EntryType' = 'Error';
'Message' = 'Corrupt Icinga for Windows configuration';
'Details' = 'Your Icinga for Windows configuration file was corrupt and could not be read successfully. A new configuration file was created and the old one renamed for review, to keep your settings available.';
'EventId' = 1100;
};
1101 = @{
'EntryType' = 'Warning';
'Message' = 'Unable to update Icinga for Windows file';
'Details' = 'Icinga for Windows could not update the specified file after several attempts, because another process is locking it. Modifications made on the file have not been persisted.';
'EventId' = 1101;
};
1102 = @{
'EntryType' = 'Warning';
'Message' = 'Unable to read Icinga for Windows content file';
'Details' = 'Icinga for Windows could not read the specified file after several attempts, because another process is locking the file. Icinga for Windows terminated itself to prevent damage to this file.';
'EventId' = 1102;
};
1500 = @{
'EntryType' = 'Error';
'Message' = 'Failed to securely establish a communication between this server and the client';

View file

@ -85,7 +85,7 @@ function New-IcingaRepositoryFile()
$IcingaRepository.Info.RepoHash = Get-IcingaRepositoryHash -Path $Path;
}
Set-Content -Path $RepoPath -Value (ConvertTo-Json -InputObject $IcingaRepository -Depth 100);
Write-IcingaFileSecure -File $RepoPath -Value (ConvertTo-Json -InputObject $IcingaRepository -Depth 100);
return $IcingaRepository;
}

View file

@ -194,7 +194,7 @@ function Sync-IcingaRepository()
$JsonRepo.Info.RemoteSource = $RemotePath;
$JsonRepo.Info.Updated = ((Get-Date).ToUniversalTime().ToString('yyyy\/MM\/dd HH:mm:ss'));
Set-Content -Path $RepoFile -Value (ConvertTo-Json -InputObject $JsonRepo -Depth 100);
Write-IcingaFileSecure -File $RepoFile -Value (ConvertTo-Json -InputObject $JsonRepo -Depth 100);
if ($UseSCP -eq $FALSE) { # Windows target
$Success = Remove-Item -Path $RemovePath -Recurse -Force;

View file

@ -408,7 +408,7 @@ function Get-IcingaCheckCommandConfig()
if ($IcingaConfig) {
Write-IcingaPlainConfigurationFiles -Content $Basket -OutDirectory $ConfigDirectory -FileName $FileName;
} else {
Set-Content -Path $OutDirectory -Value $output;
Write-IcingaFileSecure -File $OutDirectory -Value $output;
}
# Output-Text
@ -571,8 +571,8 @@ function Write-IcingaPlainConfigurationFiles()
$PowerShellBase += [string]::Format(' timeout = 3m{0}', (New-IcingaNewLine));
$PowerShellBase += '}';
Set-Content -Path (Join-Path -Path $ConfigDirectory -ChildPath 'PowerShell_Base.conf') -Value $PowerShellBase;
Set-Content -Path $OutDirectory -Value $IcingaConfig;
Write-IcingaFileSecure -File (Join-Path -Path $ConfigDirectory -ChildPath 'PowerShell_Base.conf') -Value $PowerShellBase;
Write-IcingaFileSecure -File $OutDirectory -Value $IcingaConfig;
}
function Add-PowerShellDataList()

View file

@ -1,44 +0,0 @@
<#
.SYNOPSIS
Reads content of a file in read-only mode, ensuring no data corruption is happening
.DESCRIPTION
Reads content of a file in read-only mode, ensuring no data corruption is happening
.FUNCTIONALITY
Reads content of a file in read-only mode, ensuring no data corruption is happening
.EXAMPLE
PS>Read-IcingaFileContent -File 'config.json';
.OUTPUTS
System.Object
.LINK
https://github.com/Icinga/icinga-powershell-framework
#>
function Read-IcingaFileContent()
{
param (
[string]$File
);
if ((Test-Path $File) -eq $FALSE) {
return $null;
}
[System.IO.FileStream]$FileStream = [System.IO.File]::Open(
$File,
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read,
[System.IO.FileShare]::Read
);
$ReadArray = New-Object Byte[] $FileStream.Length;
$UTF8Encoding = New-Object System.Text.UTF8Encoding $TRUE;
$FileContent = '';
while ($FileStream.Read($ReadArray, 0 , $ReadArray.Length)) {
$FileContent = [System.String]::Concat($FileContent, $UTF8Encoding.GetString($ReadArray));
}
$FileStream.Dispose();
return $FileContent;
}

View file

@ -0,0 +1,71 @@
<#
.SYNOPSIS
Reads content of a file in read-only mode, ensuring no data corruption is happening
.DESCRIPTION
Reads content of a file in read-only mode, ensuring no data corruption is happening
.FUNCTIONALITY
Reads content of a file in read-only mode, ensuring no data corruption is happening
.EXAMPLE
PS>Read-IcingaFileSecure -File 'config.json';
.EXAMPLE
PS>Read-IcingaFileSecure -File 'config.json' -ExitOnReadError;
.OUTPUTS
System.Object
.LINK
https://github.com/Icinga/icinga-powershell-framework
#>
function Read-IcingaFileSecure()
{
param (
[string]$File,
[switch]$ExitOnReadError = $FALSE
);
if ([string]::IsNullOrEmpty($File) -Or (Test-Path $File) -eq $FALSE) {
return $null;
}
[int]$WaitTicks = 0;
[bool]$ConfigRead = $FALSE;
# Lets wait 5 seconds before cancelling reading
while ($WaitTicks -lt (($WaitTicks + 1) * 50)) {
try {
[System.IO.FileStream]$FileStream = [System.IO.File]::Open(
$File,
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read,
[System.IO.FileShare]::Read
);
$ReadArray = New-Object Byte[] $FileStream.Length;
$UTF8Encoding = New-Object System.Text.UTF8Encoding $TRUE;
$FileContent = '';
while ($FileStream.Read($ReadArray, 0 , $ReadArray.Length)) {
$FileContent = [System.String]::Concat($FileContent, $UTF8Encoding.GetString($ReadArray));
}
$FileStream.Dispose();
$ConfigRead = $TRUE;
break;
} catch {
# File is still locked, wait for lock to vanish
}
$WaitTicks += 1;
Start-Sleep -Milliseconds 100;
}
if ($ConfigRead -eq $FALSE -And $ExitOnReadError) {
Write-IcingaEventMessage -EventId 1102 -Namespace 'Framework' -Objects $ConfigFile, $Content;
Write-IcingaConsoleWarning -Message 'Your file "{0}" could not be read, as another process is locking it. Icinga for Windows will terminate itself after 5 seconds to prevent damage to this file.' -Objects $File;
Start-Sleep -Seconds 5;
exit 3;
}
return $FileContent;
}
Set-Alias -Name 'Read-IcingaFileContent' -Value 'Read-IcingaFileSecure';

View file

@ -0,0 +1,39 @@
function Write-IcingaFileSecure()
{
param (
[string]$File,
$Value
);
[int]$WaitTicks = 0;
[bool]$FileUpdated = $FALSE;
# Lets wait 5 seconds before cancelling writing
while ($WaitTicks -lt (($WaitTicks + 1) * 50)) {
try {
[System.IO.FileStream]$FileStream = [System.IO.File]::Open(
$File,
[System.IO.FileMode]::Truncate,
[System.IO.FileAccess]::Write,
[System.IO.FileShare]::Read
);
$ContentBytes = [System.Text.Encoding]::UTF8.GetBytes($Value);
$FileStream.Write($ContentBytes, 0, $ContentBytes.Length);
$FileStream.Dispose();
$FileUpdated = $TRUE;
break;
} catch {
Exit-IcingaThrowException -InputString $_.Exception -CustomMessage $File -StringPattern 'System.UnauthorizedAccessException' -ExceptionType 'Permission' -ExceptionThrown $IcingaExceptions.Permission.CacheFolder;
# File is still locked, wait for lock to vanish
}
$WaitTicks += 1;
Start-Sleep -Milliseconds 100;
}
if ($FileUpdated -eq $FALSE) {
Write-IcingaEventMessage -EventId 1101 -Namespace 'Framework' -Objects $File, $Value;
Write-IcingaConsoleWarning -Message 'Your file "{0}" could not be updated with your changes, as another process is locking it.' -Objects $File;
}
}