diff --git a/doc/100-General/10-Changelog.md b/doc/100-General/10-Changelog.md index c1788b54..91e76303 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 ### Enhancements * [#679](https://github.com/Icinga/icinga-powershell-framework/pull/679) Adds a new data provider for fetching process information of Windows systems, while sorting all objects based on a process name and their process id +* [#688](https://github.com/Icinga/icinga-powershell-framework/pull/688) Adds new handling to add scheduled tasks in Windows for interacting with Icinga for Windows core functionality as well as an auto renewal task for the Icinga for Windows certificate generation ## 1.11.2 (tbd) diff --git a/jobs/GetWindowsService.ps1 b/jobs/GetWindowsService.ps1 new file mode 100644 index 00000000..ba647a26 --- /dev/null +++ b/jobs/GetWindowsService.ps1 @@ -0,0 +1,32 @@ +param ( + [string]$ServiceName = '', + [string]$TmpFilePath = '' +); + +Use-Icinga -Minimal; + +[string]$ErrMsg = ""; +[hashtable]$ServiceData = @{ + 'Status' = ''; + 'Present' = $FALSE; + 'Name' = 'Unknown'; + 'DisplayName' = 'Unknown'; +}; + +try { + $SvcData = Get-Service "$ServiceName" -ErrorAction Stop; + $ServiceData.Status = [string]$SvcData.Status; + $ServiceData.Name = $SvcData.Name; + $ServiceData.DisplayName = $SvcData.DisplayName; + $ServiceData.Present = $TRUE; +} catch { + $ErrMsg = [string]::Format('Failed to get data for service "{0}": {1}', $ServiceName, $_.Exception.Message); +} + +Write-IcingaFileSecure -File "$TmpFilePath" -Value ( + @{ + 'Service' = $ServiceData; + 'Message' = [string]::Format('Successfully fetched data for service "{0}"', $ServiceName); + 'ErrMsg' = $ErrMsg; + } | ConvertTo-Json -Depth 100 +); diff --git a/jobs/RenewCertificate.ps1 b/jobs/RenewCertificate.ps1 new file mode 100644 index 00000000..00da3b0c --- /dev/null +++ b/jobs/RenewCertificate.ps1 @@ -0,0 +1,33 @@ +Use-Icinga -Minimal; + +# This script will simply install the Icinga for Windows certificate everyime the +# scheduled task is running. This does not impact our system at all, because we +# can update the certificate at any time without having to worry about the state + +# To make the configuration of the task as easy as possible, we should fetch +# the current configuration of our REST-Api and check if we provide a custom +# certificate file or thumbprint. In case we do, ensure we use this certificate +# for the icingaforwindows.pfx creation instead of the auto lookup +[hashtable]$RegisteredBackgroundDaemons = Get-IcingaBackgroundDaemons; +[string]$CertificatePath = ''; +[string]$CertificateThumbprint = ''; + +if ($RegisteredBackgroundDaemons.ContainsKey('Start-IcingaWindowsRESTApi')) { + if ($RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi'].ContainsKey('CertFile')) { + $CertificatePath = $RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi']['CertFile']; + } + if ($RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi'].ContainsKey('-CertFile')) { + $CertificatePath = $RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi']['-CertFile']; + } + if ($RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi'].ContainsKey('CertThumbprint')) { + $CertificateThumbprint = $RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi']['CertThumbprint']; + } + if ($RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi'].ContainsKey('-CertThumbprint')) { + $CertificateThumbprint = $RegisteredBackgroundDaemons['Start-IcingaWindowsRESTApi']['-CertThumbprint']; + } +} + +Install-IcingaForWindowsCertificate -CertFile $CertificatePath -CertThumbprint $CertificateThumbprint; + +# Tell the Task-Scheduler that the script was executed fine +exit 0; diff --git a/jobs/RestartWindowsService.ps1 b/jobs/RestartWindowsService.ps1 new file mode 100644 index 00000000..9c66c491 --- /dev/null +++ b/jobs/RestartWindowsService.ps1 @@ -0,0 +1,27 @@ +param ( + [string]$ServiceName = '', + [string]$TmpFilePath = '' +); + +Use-Icinga -Minimal; + +[bool]$Success = $TRUE; +[string]$ErrMsg = ""; +[string]$Status = ''; + +try { + Restart-Service "$ServiceName" -ErrorAction Stop; + $Status = [string](Get-Service "$ServiceName").Status; +} catch { + $Success = $FALSE; + $ErrMsg = [string]::Format('Failed to restart service "{0}": {1}', $ServiceName, $_.Exception.Message); +} + +Write-IcingaFileSecure -File "$TmpFilePath" -Value ( + @{ + 'Success' = $Success; + 'Message' = [string]::Format('Service "{0}" successfully restarted', $ServiceName); + 'ErrMsg' = $ErrMsg; + 'Status' = $Status; + } | ConvertTo-Json -Depth 100 +); diff --git a/jobs/StartWindowsService.ps1 b/jobs/StartWindowsService.ps1 new file mode 100644 index 00000000..af3426a9 --- /dev/null +++ b/jobs/StartWindowsService.ps1 @@ -0,0 +1,27 @@ +param ( + [string]$ServiceName = '', + [string]$TmpFilePath = '' +); + +Use-Icinga -Minimal; + +[bool]$Success = $TRUE; +[string]$ErrMsg = ""; +[string]$Status = ''; + +try { + Start-Service "$ServiceName" -ErrorAction Stop; + $Status = [string](Get-Service "$ServiceName").Status; +} catch { + $Success = $FALSE; + $ErrMsg = [string]::Format('Failed to start service "{0}": {1}', $ServiceName, $_.Exception.Message); +} + +Write-IcingaFileSecure -File "$TmpFilePath" -Value ( + @{ + 'Success' = $Success; + 'Message' = [string]::Format('Service "{0}" successfully started', $ServiceName); + 'ErrMsg' = $ErrMsg; + 'Status' = $Status; + } | ConvertTo-Json -Depth 100 +); diff --git a/jobs/StopWindowsService.ps1 b/jobs/StopWindowsService.ps1 new file mode 100644 index 00000000..b0eae968 --- /dev/null +++ b/jobs/StopWindowsService.ps1 @@ -0,0 +1,27 @@ +param ( + [string]$ServiceName = '', + [string]$TmpFilePath = '' +); + +Use-Icinga -Minimal; + +[bool]$Success = $TRUE; +[string]$ErrMsg = ""; +[string]$Status = ''; + +try { + Stop-Service "$ServiceName" -ErrorAction Stop; + $Status = [string](Get-Service "$ServiceName").Status; +} catch { + $Success = $FALSE; + $ErrMsg = [string]::Format('Failed to stop service "{0}": {1}', $ServiceName, $_.Exception.Message); +} + +Write-IcingaFileSecure -File "$TmpFilePath" -Value ( + @{ + 'Success' = $Success; + 'Message' = [string]::Format('Service "{0}" successfully stopped', $ServiceName); + 'ErrMsg' = $ErrMsg; + 'Status' = $Status; + } | ConvertTo-Json -Depth 100 +); diff --git a/lib/core/framework/Invoke-IcingaForWindowsMigration.psm1 b/lib/core/framework/Invoke-IcingaForWindowsMigration.psm1 index facc4e2a..72896cb9 100644 --- a/lib/core/framework/Invoke-IcingaForWindowsMigration.psm1 +++ b/lib/core/framework/Invoke-IcingaForWindowsMigration.psm1 @@ -89,4 +89,13 @@ function Invoke-IcingaForWindowsMigration() Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.10.1'); } + + if (Test-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.12.0')) { + Write-IcingaConsoleNotice 'Applying pending migrations required for Icinga for Windows v1.12.0'; + + # Add a new scheduled task to automatically renew the Icinga for Windows certificate + Register-IcingaWindowsScheduledTaskRenewCertificate -Force; + + Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.12.0'); + } } diff --git a/lib/core/framework/Uninstall-IcingaForWindows.psm1 b/lib/core/framework/Uninstall-IcingaForWindows.psm1 index bcd4b5d3..e60b5c0a 100644 --- a/lib/core/framework/Uninstall-IcingaForWindows.psm1 +++ b/lib/core/framework/Uninstall-IcingaForWindows.psm1 @@ -52,6 +52,8 @@ function Uninstall-IcingaForWindows() Uninstall-IcingaSecurity -IcingaUser $IcingaUser; Write-IcingaConsoleNotice 'Uninstalling Icinga Agent'; Uninstall-IcingaAgent -RemoveDataFolder | Out-Null; + Write-IcingaConsoleNotice 'Uninstalling Certificate Renewal Task'; + Unregister-IcingaWindowsScheduledTaskRenewCertificate; if ($ComponentsOnly -eq $FALSE) { Write-IcingaConsoleNotice 'Uninstalling Icinga for Windows EventLog'; Unregister-IcingaEventLog; diff --git a/lib/core/installer/Start-IcingaForWindowsInstallation.psm1 b/lib/core/installer/Start-IcingaForWindowsInstallation.psm1 index 4ce51db6..5a7dfd71 100644 --- a/lib/core/installer/Start-IcingaForWindowsInstallation.psm1 +++ b/lib/core/installer/Start-IcingaForWindowsInstallation.psm1 @@ -282,6 +282,9 @@ function Start-IcingaForWindowsInstallation() }; } + # Ensure we add the scheduled task to renew the certificates for Icinga for Windows on a daily basis + Register-IcingaWindowsScheduledTaskRenewCertificate -Force; + switch ($InstallJEAProfile) { '0' { Install-IcingaJEAProfile; diff --git a/lib/core/wintasks/Invoke-IcingaWindowsScheduledTask.psm1 b/lib/core/wintasks/Invoke-IcingaWindowsScheduledTask.psm1 new file mode 100644 index 00000000..466b1acf --- /dev/null +++ b/lib/core/wintasks/Invoke-IcingaWindowsScheduledTask.psm1 @@ -0,0 +1,93 @@ +function Invoke-IcingaWindowsScheduledTask() +{ + param ( + [ValidateSet('UninstallAgent', 'UpgradeAgent', 'ReadMSIPackage', 'InstallJEA', 'StartWindowsService', 'StopWindowsService', 'RestartWindowsService', 'GetWindowsService')] + [string]$JobType = '', + [string]$FilePath = '', + [string]$TargetPath = '', + [string]$ObjectName = '' + ); + + if ((Test-AdministrativeShell) -eq $FALSE) { + Write-IcingaConsoleError 'You require to run this shell in administrative mode for the action "{0}" and object "{1}"' -Objects $JobType, $ObjectName; + return $null; + } + + [string]$TaskName = 'Management Task'; + [string]$TaskPath = '\Icinga\Icinga for Windows\'; + $TaskData = $null; + $TmpFile = New-IcingaTemporaryFile; + + if (Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction SilentlyContinue) { + Unregister-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -Confirm:$FALSE -ErrorAction SilentlyContinue | Out-Null; + } + + switch ($JobType) { + 'StartWindowsService' { + $TaskData = Invoke-IcingaWindowsServiceHandlerTask -ScriptPath 'jobs\StartWindowsService.ps1' -ServiceName $ObjectName -TmpFile $TmpFile.FullName -TaskName $TaskName -TaskPath $TaskPath; + }; + 'StopWindowsService' { + $TaskData = Invoke-IcingaWindowsServiceHandlerTask -ScriptPath 'jobs\StopWindowsService.ps1' -ServiceName $ObjectName -TmpFile $TmpFile.FullName -TaskName $TaskName -TaskPath $TaskPath; + }; + 'RestartWindowsService' { + $TaskData = Invoke-IcingaWindowsServiceHandlerTask -ScriptPath 'jobs\RestartWindowsService.ps1' -ServiceName $ObjectName -TmpFile $TmpFile.FullName -TaskName $TaskName -TaskPath $TaskPath; + }; + 'GetWindowsService' { + $TaskData = Invoke-IcingaWindowsServiceHandlerTask -ScriptPath 'jobs\GetWindowsService.ps1' -ServiceName $ObjectName -TmpFile $TmpFile.FullName -TaskName $TaskName -TaskPath $TaskPath; + }; + 'UninstallAgent' { + $WinAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ([string]::Format('-WindowStyle Hidden -Command &{{ Use-Icinga -Minimal; Write-IcingaFileSecure -File {0}{1}{0} -Value (Start-IcingaProcess -Executable {0}MsiExec.exe{0} -Arguments {0}"{2}" /q{0} -FlushNewLines | ConvertTo-Json -Depth 100); }}', "'", $TmpFile.FullName, $FilePath, $TargetPath)) + Register-ScheduledTask -TaskName $TaskName -Action $WinAction -RunLevel Highest -TaskPath $TaskPath | Out-Null; + + Start-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath; + + Wait-IcingaWindowsScheduledTask; + # Wait some time before continuing to ensure the service is properly removed + Start-Sleep -Seconds 2; + + [string]$TaskOutput = Read-IcingaFileSecure -File $TmpFile.FullName; + $TaskData = ConvertFrom-Json $TaskOutput; + }; + 'UpgradeAgent' { + + }; + 'ReadMSIPackage' { + if (Test-Path $FilePath) { + + $WinAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ([string]::Format('-WindowStyle Hidden -Command &{{ Use-Icinga -Minimal; Write-IcingaFileSecure -File {0}{1}{0} -Value (Read-IcingaMSIMetadata -File {0}{2}{0} | ConvertTo-Json -Depth 100); }}', "'", $TmpFile.FullName, $FilePath)) + Register-ScheduledTask -TaskName $TaskName -Action $WinAction -RunLevel Highest -TaskPath $TaskPath | Out-Null; + + Start-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath; + + Wait-IcingaWindowsScheduledTask; + + [string]$TaskOutput = Read-IcingaFileSecure -File $TmpFile.FullName; + $TaskData = ConvertFrom-Json $TaskOutput; + } else { + Write-IcingaConsoleError 'Unable to execute Job Type {0} because the specified file "{1}" does not exist' -Objects $JobType, $FilePath; + } + }; + 'InstallJEA' { + $WinAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ([string]::Format('-Command &{{ Use-Icinga -Minimal; Install-IcingaJEAProfile; Restart-IcingaWindowsService; }}', "'", $TmpFile.FullName, $FilePath)) + Register-ScheduledTask -TaskName $TaskName -Action $WinAction -RunLevel Highest -TaskPath $TaskPath | Out-Null; + Start-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath; + + Wait-IcingaWindowsScheduledTask; + + # No output data required for this task + }; + Default { + Write-IcingaConsoleError 'Unable to execute Job Type {0}. Undefined operation' -Objects $JobType; + }; + }; + + if (Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction SilentlyContinue) { + Unregister-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -Confirm:$FALSE -ErrorAction SilentlyContinue | Out-Null; + } + + if (Test-Path $TmpFile) { + Remove-Item -Path $TmpFile -Force; + } + + return $TaskData; +} diff --git a/lib/core/wintasks/Invoke-IcingaWindowsServiceHandlerTask.psm1 b/lib/core/wintasks/Invoke-IcingaWindowsServiceHandlerTask.psm1 new file mode 100644 index 00000000..692c917a --- /dev/null +++ b/lib/core/wintasks/Invoke-IcingaWindowsServiceHandlerTask.psm1 @@ -0,0 +1,33 @@ +function Invoke-IcingaWindowsServiceHandlerTask() +{ + param ( + [string]$ScriptPath = '', + [string]$ServiceName = '', + [string]$TmpFile = '', + [string]$TaskName = '', + [string]$TaskPath = '' + ); + + if ([string]::IsNullOrEmpty($ScriptPath)) { + return $null; + } + + $ScriptPath = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath $ScriptPath; + + if ((Test-Path $ScriptPath) -eq $FALSE) { + Write-IcingaConsoleError 'Unable to execute Job. The provided script path "{0}" does not exist' -Objects $ScriptPath; + return $null; + } + + $WinAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ([string]::Format("-WindowStyle Hidden -Command &{{ & '{0}' -ServiceName '{1}' -TmpFilePath '{2}' }}", $ScriptPath, $ServiceName, $TmpFile)); + Register-ScheduledTask -TaskName $TaskName -Action $WinAction -RunLevel Highest -TaskPath $TaskPath | Out-Null; + + Start-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath; + + Wait-IcingaWindowsScheduledTask; + + [string]$TaskOutput = Read-IcingaFileSecure -File $TmpFile; + $TaskData = ConvertFrom-Json $TaskOutput; + + return $TaskData; +} diff --git a/lib/core/wintasks/Wait-IcingaWindowsScheduledTask.psm1 b/lib/core/wintasks/Wait-IcingaWindowsScheduledTask.psm1 new file mode 100644 index 00000000..a3cba44e --- /dev/null +++ b/lib/core/wintasks/Wait-IcingaWindowsScheduledTask.psm1 @@ -0,0 +1,26 @@ +function Wait-IcingaWindowsScheduledTask() +{ + param ( + [string]$TaskName = 'Management Task', + [string]$TaskPath = '\Icinga\Icinga for Windows\', + [int]$Timeout = 180 + ); + + [int]$TimeoutTicks = $Timeout * 1000; + + while ($TimeoutTicks -gt 0) { + $TaskStatus = Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath; + if ($TaskStatus.State -eq 'Ready') { + break; + } + Start-Sleep -Milliseconds 500; + + $TimeoutTicks -= 500; + } + + if ($TimeoutTicks -le 0) { + Stop-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath | Out-Null; + + Write-IcingaConsoleError 'The scheduled task "{0}" at path "{1}" could not be executed within {2} seconds and run into a timeout' -Objects $TaskName, $TaskPath, $Timeout; + } +} diff --git a/lib/core/wintasks/daemon/Register-TaskRenewCertificate.psm1 b/lib/core/wintasks/daemon/Register-TaskRenewCertificate.psm1 new file mode 100644 index 00000000..539d477c --- /dev/null +++ b/lib/core/wintasks/daemon/Register-TaskRenewCertificate.psm1 @@ -0,0 +1,26 @@ +function Register-IcingaWindowsScheduledTaskRenewCertificate() +{ + param ( + [switch]$Force = $FALSE + ); + + [string]$TaskName = 'Renew Certificate'; + [string]$TaskPath = '\Icinga\Icinga for Windows\'; + + $RenewCertificateTask = Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction SilentlyContinue; + + if ($null -ne $RenewCertificateTask -And $Force -eq $FALSE) { + Write-IcingaConsoleWarning -Message 'The {0} task is already present. User -Force to enforce the re-creation' -Objects $TaskName; + return; + } + + $ScriptPath = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath '\jobs\RenewCertificate.ps1'; + $TaskTrigger = New-ScheduledTaskTrigger -Daily -DaysInterval 1 -At '1am'; + $TaskAction = New-ScheduledTaskAction -Execute 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -Argument ([string]::Format("-WindowStyle Hidden -Command &{{ & '{0}' }}", $ScriptPath)); + $TaskPrincipal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-544' -RunLevel 'Highest'; + $TaskSettings = New-ScheduledTaskSettingsSet -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries -StartWhenAvailable; + + Register-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -Force -Principal $TaskPrincipal -Action $TaskAction -Trigger $TaskTrigger -Settings $TaskSettings; + + Write-IcingaConsoleWarning -Message 'The task "{0}" has been successfully registered at location "{1}".' -Objects $TaskName, $TaskPath; +} diff --git a/lib/core/wintasks/daemon/Start-TaskRenewCertificate.psm1 b/lib/core/wintasks/daemon/Start-TaskRenewCertificate.psm1 new file mode 100644 index 00000000..d386ff60 --- /dev/null +++ b/lib/core/wintasks/daemon/Start-TaskRenewCertificate.psm1 @@ -0,0 +1,14 @@ +function Start-IcingaWindowsScheduledTaskRenewCertificate() +{ + [string]$TaskName = 'Renew Certificate'; + [string]$TaskPath = '\Icinga\Icinga for Windows\'; + + $RenewCertificateTask = Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction SilentlyContinue; + + if ($null -eq $RenewCertificateTask) { + Write-IcingaConsoleNotice -Message 'The "{0}" task is not present on this system.' -Objects $TaskName; + return; + } + + Start-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath; +} diff --git a/lib/core/wintasks/daemon/Unregister-TaskRenewCertificate.psm1 b/lib/core/wintasks/daemon/Unregister-TaskRenewCertificate.psm1 new file mode 100644 index 00000000..cfee6297 --- /dev/null +++ b/lib/core/wintasks/daemon/Unregister-TaskRenewCertificate.psm1 @@ -0,0 +1,16 @@ +function Unregister-IcingaWindowsScheduledTaskRenewCertificate() +{ + [string]$TaskName = 'Renew Certificate'; + [string]$TaskPath = '\Icinga\Icinga for Windows\'; + + $RenewCertificateTask = Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction SilentlyContinue; + + if ($null -eq $RenewCertificateTask) { + Write-IcingaConsoleNotice -Message 'The "{0}" task is not present on this system.' -Objects $TaskName; + return; + } + + Stop-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath | Out-Null; + Unregister-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -Confirm:$FALSE -ErrorAction SilentlyContinue | Out-Null; + Write-IcingaConsoleNotice -Message 'The "{0}" task was removed from the system.' -Objects $TaskName; +}