From 98de20725e612ac5cae352de28f4ffbeda117832 Mon Sep 17 00:00:00 2001 From: Lord Hepipud Date: Tue, 22 Apr 2025 12:04:41 +0200 Subject: [PATCH] Fixes Icinga for Windows freezing on empty config.json --- cache/framework_cache.psm1 | 34310 +++++++++++++++- doc/100-General/10-Changelog.md | 1 + .../installer/tools/Get-FileEncoding.psm1 | 15 +- 3 files changed, 34297 insertions(+), 29 deletions(-) diff --git a/cache/framework_cache.psm1 b/cache/framework_cache.psm1 index ddd9019..cc54fc8 100644 --- a/cache/framework_cache.psm1 +++ b/cache/framework_cache.psm1 @@ -1,41 +1,34299 @@ -<# - ### Note ### - - This file is shipping plain with Icinga for Windows for each version. - Once the module is loaded, this content will entirely be replaced with - all modules and components shipped by the Icinga PowerShell Framework. - - Manually enabling the feature is no longer required. +<# +.SYNOPSIS + Tests if a Cmdlet for a given endpoint is configured as whitelist or blacklist. + This function will return True if the command is whitelisted and False if it is + blacklisted. If the Cmdlet is not added anywhere, the function will return False as well. +.DESCRIPTION + Tests if a Cmdlet for a given endpoint is configured as whitelist or blacklist. + This function will return True if the command is whitelisted and False if it is + blacklisted. If the Cmdlet is not added anywhere, the function will return False as well. +.FUNCTIONALITY + Tests if a Cmdlet is allowed to be executed over the REST-Api +.EXAMPLE + PS>Test-IcingaRESTApiCommand -Command 'Invoke-IcingaCheckCPU' -Endpoint 'checker'; +.LINK + https://github.com/Icinga/icinga-powershell-framework #> -# Ensure we only load this module once -if ($null -ne $Global:Icinga -And $Global:Icinga.ContainsKey('CacheBuilding') -And $Global:Icinga['CacheBuilding']) { +function Show-IcingaRESTApiCommands() +{ + $Commands = Get-IcingaPowerShellConfig -Path 'RESTApi.Commands'; + + foreach ($property in $Commands.PSObject.Properties) { + $Whitelisted = 'None'; + $Blacklisted = 'None'; + + if (Test-IcingaPowerShellConfigItem -ConfigObject $property.Value -ConfigKey 'Whitelist') { + if ($property.Value.Whitelist.Count -ne 0) { + $Whitelisted = [string]::Join(', ', ($property.Value.Whitelist)); + } + } + if (Test-IcingaPowerShellConfigItem -ConfigObject $property.Value -ConfigKey 'Blacklist') { + if ($property.Value.Blacklist.Count -ne 0) { + $Blacklisted = [string]::Join(', ', ($property.Value.Blacklist)); + } + } + Write-IcingaConsolePlain -Message 'API Endpoint "{0}"' -Objects $property.Name; + Write-IcingaConsolePlain -Message '################'; + Write-IcingaConsolePlain -Message ''; + Write-IcingaConsolePlain -Message 'Whitelisted: {0}' -Objects $Whitelisted; + Write-IcingaConsolePlain -Message ''; + Write-IcingaConsolePlain -Message 'Blacklisted: {0}' -Objects $Blacklisted; + Write-IcingaConsolePlain -Message ''; + } +} +<# +.SYNOPSIS + Removes a Cmdlet from an REST-Api endpoints whitelist or blacklist. +.DESCRIPTION + Removes a Cmdlet from an REST-Api endpoints whitelist or blacklist. +.FUNCTIONALITY + Removes Cmdlets for REST-Api endpoints +.EXAMPLE + PS>Remove-IcingaRESTApiCommand -Command 'Invoke-IcingaCheck*' -Endpoint 'checker'; +.EXAMPLE + PS>Add-IcingaRESTApiCommand -Command 'Invoke-IcingaCheck*' -Endpoint 'checker' -Blacklist; +.LINK + https://github.com/Icinga/icinga-powershell-restapi +#> + +function Remove-IcingaRESTApiCommand() +{ + param ( + [string]$Command = '', + [string]$Endpoint = '', + [switch]$Blacklist = $FALSE + ); + + if ([string]::IsNullOrEmpty($Command) -Or [string]::IsNullOrEmpty($Endpoint)) { + return; + } + + $Commands = $null; + $ConfigPath = ([string]::Format('RESTApi.Commands.{0}.Whitelist', $Endpoint)); + [array]$Values = @(); + + if ($Blacklist) { + $ConfigPath = ([string]::Format('RESTApi.Commands.{0}.Blacklist', $Endpoint)); + } + + $Commands = Get-IcingaPowerShellConfig -Path $ConfigPath; + + if ($null -eq $Commands) { + return; + } + + foreach ($element in $Commands) { + if ($element.ToLower() -ne $Command.ToLower()) { + $Values += $element; + } + } + + Set-IcingaPowerShellConfig -Path $ConfigPath -Value $Values; +} +function New-IcingaForWindowsRESTEnvironment() +{ + param ( + [int]$ThreadCount = 5 + ); + + $Global:Icinga.Public.Daemons.Add( + 'RESTApi', + @{ + 'ApiRequests' = @{ }; + 'ApiCallThreadAssignment' = @{ }; + 'TotalThreads' = $ThreadCount; + 'LastThreadId' = 0; + # This will add another hashtable to our previous + # IcingaPowerShellRestApi hashtable to store actual + # endpoint configurations for the API + 'RegisteredEndpoints' = @{ }; + # This will add another hashtable to our previous + # IcingaPowerShellRestApi hashtable to store actual + # command aliases for execution for the API + 'CommandAliases' = @{ }; + # This will add another hashtable to our previous + # IcingaPowerShellRestApi hashtable to store actual + # command aliases for execution for the API + 'ClientBlacklist' = @{ }; + } + ); + + Add-IcingaThreadPool -Name 'RESTApiPool' -MaxInstances ($ThreadCount * 2); +} +<# +.SYNOPSIS + Tests if a Cmdlet for a given endpoint is configured as whitelist or blacklist. + This function will return True if the command is whitelisted and False if it is + blacklisted. If the Cmdlet is not added anywhere, the function will return False as well. +.DESCRIPTION + Tests if a Cmdlet for a given endpoint is configured as whitelist or blacklist. + This function will return True if the command is whitelisted and False if it is + blacklisted. If the Cmdlet is not added anywhere, the function will return False as well. +.FUNCTIONALITY + Tests if a Cmdlet is allowed to be executed over the REST-Api +.EXAMPLE + PS>Test-IcingaRESTApiCommand -Command 'Invoke-IcingaCheckCPU' -Endpoint 'checker'; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Test-IcingaRESTApiCommand() +{ + param ( + [string]$Command = '', + [string]$Endpoint = '' + ); + + if ([string]::IsNullOrEmpty($Command) -Or [string]::IsNullOrEmpty($Endpoint)) { + return $FALSE; + } + + $WhiteList = Get-IcingaPowerShellConfig -Path ([string]::Format('RESTApi.Commands.{0}.Whitelist', $Endpoint)); + $Blacklist = Get-IcingaPowerShellConfig -Path ([string]::Format('RESTApi.Commands.{0}.Blacklist', $Endpoint)); + + foreach ($entry in $Blacklist) { + if ($Command.ToLower() -like $entry.ToLower()) { + return $FALSE; + } + } + + foreach ($entry in $WhiteList) { + if ($Command.ToLower() -like $entry.ToLower()) { + return $TRUE; + } + } + + # If the command is not configured, always return false + return $FALSE; +} +<# +.SYNOPSIS + Adds a Cmdlet to an REST-Api endpoint which is either whitelisted or blacklisted. + Whitelisted Cmdlets can be executed over API endpoints, blacklisted not. + Use '*' for wildcard matches +.DESCRIPTION + Adds a Cmdlet to an REST-Api endpoint which is either whitelisted or blacklisted. + Whitelisted Cmdlets can be executed over API endpoints, blacklisted not. + Use '*' for wildcard matches +.FUNCTIONALITY + Enables or disables Cmdlets for REST-Api endpoints +.EXAMPLE + PS>Add-IcingaRESTApiCommand -Command 'Invoke-IcingaCheck*' -Endpoint 'checker'; +.EXAMPLE + PS>Add-IcingaRESTApiCommand -Command 'Invoke-IcingaCheck*' -Endpoint 'checker' -Blacklist; +.LINK + https://github.com/Icinga/icinga-powershell-restapi +#> + +function Add-IcingaRESTApiCommand() +{ + param ( + [string]$Command = '', + [string]$Endpoint = '', + [switch]$Blacklist = $FALSE + ); + + if ([string]::IsNullOrEmpty($Command) -Or [string]::IsNullOrEmpty($Endpoint)) { + return; + } + + $Commands = $null; + $ConfigPath = ([string]::Format('RESTApi.Commands.{0}.Whitelist', $Endpoint)); + [array]$Values = @(); + + if ($Blacklist) { + $ConfigPath = ([string]::Format('RESTApi.Commands.{0}.Blacklist', $Endpoint)); + } + + $Commands = Get-IcingaPowerShellConfig -Path $ConfigPath; + + if ((Test-IcingaPowerShellConfigItem -ConfigObject $Commands -ConfigKey $Command)) { + return; + } + + if ($null -ne $Commands) { + $Values = $Commands; + } + + $Values += $Command; + + Set-IcingaPowerShellConfig -Path $ConfigPath -Value $Values; +} +function New-IcingaForWindowsRESTApi() +{ + # Allow us to parse the framework global data to this thread + param ( + [string]$Address = '', + $Port, + $CertFile = $null, + $CertThumbprint = $null, + [PSCustomObject]$CertFilter = $null, + $RequireAuth + ); + + $RESTEndpoints = Invoke-IcingaNamespaceCmdlets -Command 'Register-IcingaRESTAPIEndpoint*'; + Write-IcingaDebugMessage -Message ( + [string]::Format( + 'Loading configuration for REST-Endpoints{0}{1}', + (New-IcingaNewLine), + ($RESTEndpoints | Out-String) + ) + ); + + Write-IcingaDebugMessage -Message ($Global:Icinga.Public.Daemons.RESTApi | Out-String); + + foreach ($entry in $RESTEndpoints.Values) { + [bool]$Success = Add-IcingaHashtableItem ` + -Hashtable $Global:Icinga.Public.Daemons.RESTApi.RegisteredEndpoints ` + -Key $entry.Alias ` + -Value $entry.Command; + + if ($Success -eq $FALSE) { + Write-IcingaEventMessage ` + -EventId 2100 ` + -Namespace 'RESTApi' ` + -Objects ([string]::Format('Adding duplicated REST endpoint "{0}" with command "{1}', $entry.Alias, $entry.Command)), $RESTEndpoints; + } + } + + $CommandAliases = Invoke-IcingaNamespaceCmdlets -Command 'Register-IcingaRESTApiCommandAliases*'; + + foreach ($entry in $CommandAliases.Values) { + foreach ($component in $entry.Keys) { + [bool]$Success = Add-IcingaHashtableItem ` + -Hashtable $Global:Icinga.Public.Daemons.RESTApi.CommandAliases ` + -Key $component ` + -Value $entry[$component]; + + if ($Success -eq $FALSE) { + Write-IcingaEventMessage ` + -EventId 2101 ` + -Namespace 'RESTApi' ` + -Objects ([string]::Format('Adding duplicated REST command aliases "{0}" for namespace "{1}', $entry[$component], $component)), $CommandAliases; + } + } + } + + Write-IcingaDebugMessage -Message ($Global:Icinga.Public.Daemons.RESTApi.RegisteredEndpoints | Out-String); + + $Global:Icinga.Public.SSL.CertFile = $CertFile; + $Global:Icinga.Public.SSL.CertThumbprint = $CertThumbprint; + $Global:Icinga.Public.SSL.CertFilter = $CertFilter; + + while ($TRUE) { + if ($null -eq $Global:Icinga.Public.SSL.Certificate) { + $Global:Icinga.Public.SSL.Certificate = Get-IcingaForWindowsCertificate; + } + + if ($null -ne $Global:Icinga.Public.SSL.Certificate) { + break; + } + + # Wait 1 minutes and try again + Write-IcingaEventMessage -EventId 2002 -Namespace 'RESTApi' -Objects ($Global:Icinga.Public.SSL.Certificate | Out-String), $Global:Icinga.Public.SSL.CertFile, $Global:Icinga.Public.SSL.CertThumbprint, ($Global:Icinga.Public.SSL.CertFilter | Out-String); + Start-Sleep -Seconds 60; + } + + # Create a background thread to renew the certificate on a regular basis + Start-IcingaForWindowsCertificateThreadTask; + + $Socket = New-IcingaTCPSocket -Address $Address -Port $Port -Start; + + # Keep our code executed as long as the PowerShell service is + # being executed. This is required to ensure we will execute + # the code frequently instead of only once + while ($TRUE) { + + # Force Icinga for Windows Garbage Collection + Optimize-IcingaForWindowsMemory -ClearErrorStack -SmartGC; + + $Connection = Open-IcingaTCPClientConnection ` + -Client (New-IcingaTCPClient -Socket $Socket) ` + -Certificate $Global:Icinga.Public.SSL.Certificate; + + if ($Connection.Client -eq $null -Or $Connection.Stream -eq $null) { + Close-IcingaTCPConnection -Connection $Connection; + $Connection = $null; + continue; + } + + if (Test-IcingaRESTClientBlacklisted -Client $Connection.Client -ClientList $Global:Icinga.Public.Daemons.RESTApi.ClientBlacklist) { + Write-IcingaDebugMessage -Message 'A remote client which is trying to connect was blacklisted' -Objects $Connection.Client.Client; + Close-IcingaTCPConnection -Connection $Connection; + $Connection = $null; + continue; + } + + if ((Test-IcingaRESTClientConnection -Connection $Connection) -eq $FALSE) { + $Connection = $null; + continue; + } + + # API not yet ready + if ($Global:Icinga.Public.Daemons.RESTApi.ApiRequests.Count -eq 0) { + Close-IcingaTCPConnection -Connection $Connection; + $Connection = $null; + continue; + } + + try { + $NextRESTApiThreadId = (Get-IcingaNextRESTApiThreadId); + + Write-IcingaDebugMessage -Message 'Scheduling Icinga for Windows API request' -Objects 'REST-Thread Id', $NextRESTApiThreadId; + + if ($Global:Icinga.Public.Daemons.RESTApi.ApiRequests.ContainsKey($NextRESTApiThreadId) -eq $FALSE) { + # Ensure we allow API calls to be executed even in case not all threads are loaded + # This will increase responsiveness of the API + $NextRESTApiThreadId = 0; + } + + $Global:Icinga.Public.Daemons.RESTApi.ApiRequests.$NextRESTApiThreadId.Add($Connection); + } catch { + Write-IcingaEventMessage -Namespace 'RESTApi' -EvenId 2050 -ExceptionObject $_; + } + } +} +<# +.SYNOPSIS + A background daemon for Icinga for Windows, providing a REST-Api interface over secure + TLS (https) connections. As certificates either custom ones are used or by default the + Icinga Agent certificates +.DESCRIPTION + This background daemon provides a REST-Api interface by default on Port 5668 over + TLS connections. No unencrypted http requests are allowed. As certificate it will + either use the default Icinga Agent certificates (default config) or custom generated + ones, specified as arguments while installing the daemon. + + In addition you can enable authentication, which allows local users or domain users. + + By default 5 concurrent threads will perform the work on API requests, can how ever be + reduced or increased depending on configuration. + + More Information on + https://icinga.com/docs/icinga-for-windows/latest/restapi +.PARAMETER Address + Allows to specify on which Address a socket should be created on. Defaults to loopback if empty +.PARAMETER Port + The Port the REST-Api will listen on. Defaults to 5668 +.PARAMETER CertFile + Use this to define a path on your local disk of you are using custom certificates. + Supported files are: .pfx, .crt + + By default, the local Icinga Agent certificates are used +.PARAMETER CertThumbprint + Provide a thumbprint of a certificate stored within the local Windows Cert store + + By default, the local Icinga Agent certificates are used +.PARAMETER RequireAuth + Enable authentication which will add basic auth prompt for any request on the API. + For authentication you can either use local Windows accounts or domain accounts +.PARAMETER ConcurrentThreads + Defines on how many threads are started to process API requests. Defaults to 5 +.PARAMETER Timeout + Not for use on this module directly, but allows other modules and features to properly + get an idea after which time interval connections are terminated +.LINK + https://github.com/Icinga/icinga-powershell-restapi +.NOTES +#> + +function Start-IcingaWindowsRESTApi() +{ + param ( + [string]$Address = '', + [int]$Port = 5668, + [string]$CertFile = $null, + [string]$CertThumbprint = $null, + [PSCustomObject]$CertFilter = $null, + [bool]$RequireAuth = $FALSE, + [int]$ConcurrentThreads = 5, + [int]$Timeout = 120 + ); + + New-IcingaForWindowsRESTEnvironment -ThreadCount $ConcurrentThreads; + + # Now create a new thread for our REST-Api, assign a name and parse all required arguments to it. + # Last but not least start it directly + New-IcingaThreadInstance ` + -Name 'Main' ` + -ThreadPool (Get-IcingaThreadPool -Name 'RESTApiPool') ` + -Command 'New-IcingaForWindowsRESTApi' ` + -CmdParameters @{ + 'Address' = $Address; + 'Port' = $Port; + 'CertFile' = $CertFile; + 'CertThumbprint' = $CertThumbprint; + 'CertFilter' = $CertFilter; + 'RequireAuth' = $RequireAuth; + } ` + -Start; + + $ThreadId = 0; + + while ($ConcurrentThreads -gt 0) { + $ConcurrentThreads = $ConcurrentThreads - 1; + $RESTThreadQueue = New-Object System.Collections.Concurrent.BlockingCollection[PSObject] ` + -ArgumentList (New-Object System.Collections.Concurrent.ConcurrentQueue[PSObject]); + $Global:Icinga.Public.Daemons.RESTApi.ApiRequests.Add($ThreadId, $RESTThreadQueue); + Start-IcingaForWindowsRESTThread -ThreadId $ThreadId -RequireAuth:$RequireAuth; + $ThreadId += 1; + + Start-Sleep -Seconds 1; + } +} +function New-IcingaForWindowsRESTThread() +{ + param ( + $RequireAuth, + $ThreadId + ); + + # Initialise our performance counter categories + Show-IcingaPerformanceCounterCategories | Out-Null; + + while ($TRUE) { + + try { + if ($Global:Icinga.Public.Daemons.RESTApi.ApiRequests.ContainsKey($ThreadId) -eq $FALSE) { + Start-Sleep -Milliseconds 10; + continue; + } + + Write-IcingaDebugMessage -Message 'Icinga for Windows REST-Api thread is processing a request' -Objects 'REST-Thread Id', $ThreadId; + + # block sleeping until content available + $Connection = $Global:Icinga.Public.Daemons.RESTApi.ApiRequests.$ThreadId.Take(); + + # Set our thread being active before we start reading the TCP stream, as we otherwise might find ourself + # in a process were we block our API entirely, because all reqests are scheduled to one single thread + Set-IcingaForWindowsThreadAlive -ThreadName $Global:Icinga.Protected.ThreadName -Active -Timeout 6 -TerminateAction @{ 'Command' = 'Close-IcingaTCPConnection'; 'Arguments' = @{ 'Connection' = $Connection } }; + + # Read the received message from the stream by using our smart functions + [string]$RestMessage = Read-IcingaTCPStream -Client $Connection.Client -Stream $Connection.Stream; + # Now properly translate the entire rest message to a parsable hashtable + $RESTRequest = Read-IcingaRESTMessage -RestMessage $RestMessage -Connection $Connection; + + # Once we read all of our messages, reset the thread alive with the default timeout + Set-IcingaForWindowsThreadAlive -ThreadName $Global:Icinga.Protected.ThreadName -Active -TerminateAction @{ 'Command' = 'Close-IcingaTCPConnection'; 'Arguments' = @{ 'Connection' = $Connection } }; + + if ($null -ne $RESTRequest) { + + # Check if we require to authenticate the user + if ($RequireAuth) { + # If no authentication header is provided we should show the prompt + if ([string]::IsNullOrEmpty($RESTRequest.Header.Authorization)) { + # In case we do not send an authentication header increase the blacklist counter + # to ensure we are not spammed and "attacked" by a client with useless requests + Add-IcingaRESTClientBlacklistCount ` + -Client $Connection.Client ` + -ClientList $Global:Icinga.Public.Daemons.RESTApi.ClientBlacklist; + # Send the authentication prompt + Send-IcingaWebAuthMessage -Connection $Connection; + # Close the connection + Close-IcingaTCPConnection -Connection $Connection; + $Connection = $null; + continue; + } + + $Credentials = Convert-Base64ToCredentials -AuthString $RESTRequest.Header.Authorization; + [bool]$LoginSuccess = Test-IcingaRESTCredentials -UserName $Credentials.user -Password $Credentials.password -Domain $Credentials.domain; + $Credentials = $null; + + # Handle login failures + if ($LoginSuccess -eq $FALSE) { + # Failed attempts should increase the blacklist counter + Add-IcingaRESTClientBlacklistCount ` + -Client $Connection.Client ` + -ClientList $Global:Icinga.Public.Daemons.RESTApi.ClientBlacklist; + # Re-send the authentication prompt + Send-IcingaWebAuthMessage -Connection $Connection; + # Close the connection + Close-IcingaTCPConnection -Connection $Connection; + $Connection = $null; + continue; + } + } + + # We should remove clients from the blacklist who are sending valid requests + Remove-IcingaRESTClientBlacklist -Client $Connection.Client -ClientList $Global:Icinga.Public.Daemons.RESTApi.ClientBlacklist; + switch (Get-IcingaRESTPathElement -Request $RESTRequest -Index 0) { + 'v1' { + Invoke-IcingaRESTAPIv1Calls -Request $RESTRequest -Connection $Connection; + break; + }; + default { + Write-IcingaDebugMessage -Message ('Invalid API call - no version specified' + ($RESTRequest.RequestPath | Out-String)); + Send-IcingaTCPClientMessage -Message ( + New-IcingaTCPClientRESTMessage ` + -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Not Found') ` + -ContentBody 'Invalid API call received. No version specified.' + ) -Stream $Connection.Stream; + }; + } + + # set our thread no longer be active. We do this, because below there is no way we can + # actually get stuck on a endless loop, caused by external modules + Set-IcingaForWindowsThreadAlive -ThreadName $Global:Icinga.Protected.ThreadName; + } + } catch { + $ExMsg = $_.Exception.Message; + + Send-IcingaTCPClientMessage -Message ( + New-IcingaTCPClientRESTMessage ` + -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Internal Server Error') ` + -ContentBody $ExMsg + ) -Stream $Connection.Stream; + + Write-IcingaEventMessage -Namespace 'RESTApi' -EventId 2051 -ExceptionObject $_; + } + + # Finally close the clients connection as we are done here and + # ensure this thread will close by simply leaving the function + Close-IcingaTCPConnection -Connection $Connection; + $Connection = $null; + + # Force Icinga for Windows Garbage Collection + Optimize-IcingaForWindowsMemory -ClearErrorStack -SmartGC; + } +} +function Get-IcingaNextRESTApiThreadId() +{ + # Improve our thread management by distributing new REST requests to a non-active thread + [array]$ConfiguredThreads = $Global:Icinga.Public.ThreadAliveHousekeeping.Keys; + + Write-IcingaDebugMessage -Message 'Distributing Icinga for Windows REST-Api calls to one of those threads' -Objects 'REST-Thread Ids', ($ConfiguredThreads | Out-String); + + foreach ($thread in $ConfiguredThreads) { + if ($thread.ToLower() -NotLike 'Start-IcingaForWindowsRESTThread::New-IcingaForWindowsRESTThread::CheckThread::*') { + continue; + } + + $ThreadConfig = $Global:Icinga.Public.ThreadAliveHousekeeping[$thread]; + + # If our thread is busy, skip this one and check for another one + if ($ThreadConfig.Active) { + continue; + } + + $ThreadIndex = $thread.Replace('Start-IcingaForWindowsRESTThread::New-IcingaForWindowsRESTThread::CheckThread::', ''); + + if (Test-Numeric $ThreadIndex) { + $Global:Icinga.Public.Daemons.RESTApi.LastThreadId = [int]$ThreadIndex; + return ([int]$ThreadIndex) + } + } + + # In case we are not having any spare thread left, distribute the thread to the next thread in our list + [int]$ConcurrentThreads = $Global:Icinga.Public.Daemons.RESTApi.TotalThreads - 1; + [int]$LastThreadId = $Global:Icinga.Public.Daemons.RESTApi.LastThreadId + 1; + + if ($LastThreadId -gt $ConcurrentThreads) { + $LastThreadId = 0; + } + + $Global:Icinga.Public.Daemons.RESTApi.LastThreadId = $LastThreadId; + + return $LastThreadId; +} +function Start-IcingaForWindowsCertificateThreadTask() +{ + New-IcingaThreadInstance ` + -Name 'CertificateRenewThread' ` + -ThreadPool (New-IcingaThreadPool -MaxInstances 1) ` + -Command 'New-IcingaForWindowsCertificateThreadTaskInstance' ` + -Start; +} +function Start-IcingaForWindowsRESTThread() +{ + param ( + [int]$ThreadId = 0, + [switch]$RequireAuth = $FALSE + ); + + # Now create a new thread, assign a name and parse all required arguments to it. + # Last but not least start it directly + New-IcingaThreadInstance ` + -Name 'CheckThread' ` + -ThreadPool (New-IcingaThreadPool -MaxInstances 1) ` + -Command 'New-IcingaForWindowsRESTThread' ` + -CmdParameters @{ + 'RequireAuth' = $RequireAuth; + 'ThreadId' = $ThreadId; + } ` + -Start ` + -CheckAliveState; +} +function New-IcingaForWindowsCertificateThreadTaskInstance() +{ + $IcingaHostname = Get-IcingaHostname -ReadConstants; + + while ($TRUE) { + # Check every 10 minutes if our certificate is present and update it in case it is + # missing or updates have happened + + $NewIcingaForWindowsCertificate = Get-IcingaForWindowsCertificate; + + if ($null -ne $NewIcingaForWindowsCertificate) { + if ($NewIcingaForWindowsCertificate.Issuer.ToLower() -eq ([string]::Format('cn={0}', $IcingaHostname).ToLower())) { + Write-IcingaEventMessage -EventId 1506 -Namespace 'Framework'; + } else { + if ($Global:Icinga.Public.SSL.Certificate.GetCertHashString() -ne $NewIcingaForWindowsCertificate.GetCertHashString()) { + $Global:Icinga.Public.SSL.Certificate = $NewIcingaForWindowsCertificate; + Write-IcingaEventMessage -EventId 2004 -Namespace 'RESTApi'; + } + } + } + + Start-Sleep -Seconds (60 * 10); + } +} +function Register-IcingaEventLogMessagesRESTApi() +{ + return @{ + 'RESTApi' = @{ + 2000 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Failed to start REST-Api daemon, as no valid provided SSL and Icinga 2 Agent certificate was found'; + 'Details' = 'While starting the Icinga for Windows REST-Api daemon, no valid certificate was found for usage. You can either share a valid certificate by defining the full path with `-CertFile` to a .crt, .cert or .pfx file, by using `-CertThumbprint` to lookup a certificate inside the Microsoft cert store and by default the Icinga 2 Agent certificates. Please note that only Icinga 2 Agent version 2.8.0 or later are supported'; + 'EventId' = 2000; + }; + 2001 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Failed to start REST-Api daemon in JEA context'; + 'Details' = 'Icinga for Windows is being used inside a JEA context as service with the REST-Api daemon. To establish a secure TLS socket, it is required to create certificates in advance for the socket to bind on with "Start-IcingaWindowsScheduledTaskRenewCertificate". The REST-Api daemon will now exit.'; + 'EventId' = 2001; + }; + 2002 = @{ + 'EntryType' = 'Warning'; + 'Message' = 'Icinga for Windows certificate not ready'; + 'Details' = 'The Icinga for Windows REST-Api was not able to fetch the Icinga Agent or icingaforwindows.pfx certificate file. You can manually enforce the certificate creation of the icingaforwindows.pfx by using the command "Start-IcingaWindowsScheduledTaskRenewCertificate". Once successful, this message should disappear and the REST-Api start in case you are running inside a JEA-Context. If you are not using JEA, the Icinga Agent certificate has to be present and signed by the Icinga CA. You can test if a certificate is present by using "Get-IcingaSSLCertForSocket". This should return a certificate object with the subject "CN=", while "" should match your hostname or object name in Icinga. This check is queued every 5 minutes and should vanish once everything works fine.'; + 'EventId' = 2002; + }; + 2003 = @{ + 'EntryType' = 'Warning'; + 'Message' = 'Icinga for Windows certificate was not found'; + 'Details' = 'The Icinga for Windows "icingaforwindows.pfx" file was not found on the system while the REST-Api is running. Please ensure the certificate is created shortly, as the daemon will no longer work once it will be restarted or the certificate is due for renewal. Please run "Start-IcingaWindowsScheduledTaskRenewCertificate" to re-create the certificate on your machine.' + 'EventId' = 2003; + }; + 2004 = @{ + 'EntryType' = 'Information'; + 'Message' = 'Icinga for Windows certificate was renewed'; + 'Details' = 'The Icinga for Windows certificate has been modified and was updated inside the Icinga for Windows REST-Api daemon.' + 'EventId' = 2004; + }; + 2050 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Failed to parse received REST-Api call'; + 'Details' = 'An API call send to the daemon could not be processed and caused an exception. Further details about the cause of this error can be found below.'; + 'EventId' = 2050; + }; + 2051 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Failed to execute API call'; + 'Details' = 'An API call could not be processed due to an internal exception.'; + 'EventId' = 2051; + }; + 2052 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Internal exception on calling API command'; + 'Details' = 'An internal command assigned to an API request could not be executed and caused an exception.'; + 'EventId' = 2052; + }; + 2100 = @{ + 'EntryType' = 'Warning'; + 'Message' = 'Failed to add namespace configuration for executed commands, as previous commands are reporting identical namespace identifiers'; + 'Details' = 'This warning occurs while the REST-Api is trying to auto-load different resources automatically to provide for example inventory information or any other auto-loaded configurations. Please review your installed modules, check the detailed description which modules and Cmdlets caused this conflict and either resolve it or get in contact with the corresponding developers.'; + 'EventId' = 2100; + }; + 2101 = @{ + 'EntryType' = 'Warning'; + 'Message' = 'Failed to add namespace configuration for command aliases, as an identical namespace was already added'; + 'Details' = 'This warning occurs while the REST-Api is trying to auto-load different resources automatically to provide for example command aliases for providing inventory information or any other auto-loaded configurations. Please review your installed modules, check the detailed description which modules and Cmdlets caused this conflict and either resolve it or get in contact with the corresponding developers.'; + 'EventId' = 2101; + }; + } + }; +} +function Remove-IcingaRESTClientBlacklist() +{ + param ( + [System.Net.Sockets.TcpClient]$Client = $null, + $ClientList = $null + ); + + if ($null -eq $Client) { + return; + } + + [string]$Endpoint = Get-IcingaTCPClientRemoteEndpoint -Client $Client; + [string]$IpAddress = $Endpoint.Split(':')[0]; + + Remove-IcingaHashtableItem ` + -Hashtable $ClientList ` + -Key $IpAddress; +} +function Invoke-IcingaRESTAPIv1Calls() +{ + param ( + [Hashtable]$Request = @{ }, + [Hashtable]$Connection = @{ } + ); + + [string]$ModuleToLoad = Get-IcingaRESTPathElement -Request $Request -Index 1; + + if ([string]::IsNullOrEmpty($ModuleToLoad)) { + Send-IcingaTCPClientMessage -Message ( + New-IcingaTCPClientRESTMessage ` + -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.Ok) ` + -ContentBody @{ + 'Endpoints' = @( + $Global:Icinga.Public.Daemons.RESTApi.RegisteredEndpoints.Keys + ) + } + ) -Stream $Connection.Stream; + return; + } + + if ($Global:Icinga.Public.Daemons.RESTApi.RegisteredEndpoints.ContainsKey($ModuleToLoad) -eq $FALSE) { + Send-IcingaTCPClientMessage -Message ( + New-IcingaTCPClientRESTMessage ` + -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Not Found') ` + -ContentBody 'There was no module found which is registered for this endpoint name.' + ) -Stream $Connection.Stream; + return; + } + + [string]$Command = $Global:Icinga.Public.Daemons.RESTApi.RegisteredEndpoints[$ModuleToLoad]; + + Write-IcingaDebugMessage -Message 'Executing REST-Module' -Objects $Command; + + if ($null -eq (Get-Command $Command -ErrorAction SilentlyContinue)) { + Send-IcingaTCPClientMessage -Message ( + New-IcingaTCPClientRESTMessage ` + -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Internal Server Error') ` + -ContentBody 'This API endpoint is registered, but the PowerShell Cmdlet this module is referencing too does not exist. Please check if the module was installed correctly and contact the developer if you require assistance to resolve this issue.' + ) -Stream $Connection.Stream; + return; + } + + [hashtable]$CommandArguments = @{ + '-Request' = $Request; + '-Connection' = $Connection; + '-ApiVersion' = 'v1'; + }; + + & $Command @CommandArguments | Out-Null; +} +function Test-IcingaRESTClientBlacklisted() +{ + param ( + [System.Net.Sockets.TcpClient]$Client = $null, + $ClientList = $null + ); + + if ($null -eq $Client) { + return $FALSE; + } + + [string]$Endpoint = Get-IcingaTCPClientRemoteEndpoint -Client $Client; + [string]$IpAddress = $Endpoint.Split(':')[0]; + [int]$Value = Get-IcingaHashtableItem ` + -Hashtable $ClientList ` + -Key $IpAddress ` + -NullValue 0; + + # After 9 invalid attempts we will blacklist the client + if ($Value -gt 9) { + Write-IcingaDebugMessage -Message 'Client is blacklisted' -Objects $IpAddress, $Value, $Client.Client; + return $TRUE; + } + + return $FALSE; +} +function Test-IcingaRESTClientConnection() +{ + param( + [Hashtable]$Connection = @{ } + ); + + # If we couldn't establish a proper SSL stream, close the connection + # immediately without opening a client connection thread + if ($null -eq $Connection.Stream) { + Add-IcingaRESTClientBlacklistCount ` + -Client $Connection.Client ` + -ClientList $Global:Icinga.Public.Daemons.RESTApi.ClientBlacklist; + Write-IcingaEventMessage -EventId 1501 -Namespace 'Framework' -Objects $Connection.Client.Client; + Close-IcingaTCPConnection -Connection $Connection; + $Connection = $null; + return $FALSE; + } + + Write-IcingaDebugMessage 'Client connection has passed Test'; + + return $TRUE; +} +function Add-IcingaRESTClientBlacklistCount() +{ + param ( + [System.Net.Sockets.TcpClient]$Client = $null, + $ClientList = $null + ); + + if ($null -eq $Client) { + return; + } + + [string]$Endpoint = Get-IcingaTCPClientRemoteEndpoint -Client $Client; + [string]$IpAddress = $Endpoint.Split(':')[0]; + [int]$Value = Get-IcingaHashtableItem ` + -Hashtable $ClientList ` + -Key $IpAddress ` + -NullValue 0; + + Add-IcingaHashtableItem ` + -Hashtable $ClientList ` + -Key $IpAddress ` + -Value ($Value + 1) ` + -Override | Out-Null; +} +function Show-IcingaRegisteredServiceChecks() +{ + [array]$ServiceSummary = @( + 'List of configured background service checks on this system:', + '=> https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/06-Collect-Metrics-over-Time/', + '' + ); + + [hashtable]$ServiceList = Get-IcingaRegisteredServiceChecks; + + foreach ($serviceId in $ServiceList.Keys) { + $serviceDetails = $ServiceList[$serviceId]; + + $ServiceSummary += $serviceDetails.CheckCommand; + $ServiceSummary += '-----------'; + + [int]$MaxLength = (Get-IcingaMaxTextLength -TextArray $serviceDetails.Keys) - 1; + [array]$ServiceData = @(); + + foreach ($serviceArguments in $serviceDetails.Keys) { + $serviceValue = $serviceDetails[$serviceArguments]; + $PrintName = Add-IcingaWhiteSpaceToString -Text $serviceArguments -Length $MaxLength; + if ($serviceValue -Is [array]) { + $serviceValue = [string]::Join(', ', $serviceValue); + } elseif ($serviceValue -Is [PSCustomObject]) { + $serviceValue = ConvertTo-IcingaCommandArgumentString -Command $serviceDetails.CheckCommand -CommandArguments $serviceValue; + } + $ServiceData += [string]::Format('{0} => {1}', $PrintName, $serviceValue); + } + + $ServiceSummary += $ServiceData | Sort-Object; + $ServiceSummary += ''; + } + + if ($ServiceList.Count -eq 0) { + $ServiceSummary += 'No background service checks configured'; + $ServiceSummary += ''; + } + + Write-Output $ServiceSummary; +} +function Register-IcingaServiceCheck() +{ + param( + [string]$CheckCommand, + [hashtable]$Arguments, + [int]$Interval = 60, + [array]$TimeIndexes = @() + ); + + if ([string]::IsNullOrEmpty($CheckCommand)) { + throw 'Please specify a CheckCommand'; + } + + $Hash = Get-StringSha1 ([string]::Format('{0} {1}', $CheckCommand, ($Arguments | Out-String))); + $Path = [string]::Format('BackgroundDaemon.RegisteredServices.{0}', $Hash); + + Set-IcingaPowerShellConfig -Path ([string]::Format('{0}.CheckCommand', $Path)) -Value $CheckCommand; + Set-IcingaPowerShellConfig -Path ([string]::Format('{0}.Arguments', $Path)) -Value $Arguments; + Set-IcingaPowerShellConfig -Path ([string]::Format('{0}.Interval', $Path)) -Value $Interval; + Set-IcingaPowerShellConfig -Path ([string]::Format('{0}.TimeIndexes', $Path)) -Value $TimeIndexes; + + Write-IcingaConsoleNotice 'Icinga background service check has been added'; +} +function Unregister-IcingaServiceCheck() +{ + param( + [string]$ServiceId + ); + + if ([string]::IsNullOrEmpty($ServiceId)) { + throw 'Please specify a Service Id'; + } + + $Path = [string]::Format('BackgroundDaemon.RegisteredServices.{0}', $ServiceId); + + Remove-IcingaPowerShellConfig -Path $Path; + + Write-IcingaConsoleNotice 'Icinga background service check has been removed'; +} +function Read-IcingaCheckResultStore() +{ + param ( + $CheckCommand + ); + + $LoadedCacheData = Get-IcingaCacheData -Space 'sc_daemon' -CacheStore 'checkresult_store' -KeyName $CheckCommand; + + if ($null -ne $LoadedCacheData) { + foreach ($entry in $LoadedCacheData.PSObject.Properties) { + $Global:Icinga.Private.Scheduler.CheckData[$CheckCommand]['results'].Add( + $entry.name, + @{ } + ); + foreach ($item in $entry.Value.PSObject.Properties) { + $Global:Icinga.Private.Scheduler.CheckData[$CheckCommand]['results'][$entry.name].Add( + $item.Name, + $item.Value + ); + } + } + } +} +function Clear-IcingaServiceCheckDaemonEnvironment() +{ + $Global:Icinga.Private.Daemons.ServiceCheck.PassedTime = 0; + $Global:Icinga.Private.Daemons.ServiceCheck.SortedResult.Clear(); + $Global:Icinga.Private.Daemons.ServiceCheck.PerformanceCache.Clear(); +} +function Get-IcingaRegisteredServiceChecks() +{ + $Services = Get-IcingaPowerShellConfig -Path 'BackgroundDaemon.RegisteredServices'; + [hashtable]$Output = @{ }; + + foreach ($service in $Services.PSObject.Properties) { + $Content = @{ + 'Id' = $service.Name; + 'CheckCommand' = $service.Value.CheckCommand; + 'Arguments' = $service.Value.Arguments; + 'Interval' = $service.Value.Interval; + 'TimeIndexes' = $service.Value.TimeIndexes; + }; + + $Output.Add($service.Name, $Content); + } + + return $Output; +} +function New-IcingaServiceCheckDaemonEnvironment() +{ + param ( + $CheckCommand, + $Arguments, + $TimeIndexes + ); + + if ($Global:Icinga.Public.Daemons.ServiceCheck.PerformanceDataCache.ContainsKey($CheckCommand) -eq $FALSE) { + $Global:Icinga.Public.Daemons.ServiceCheck.PerformanceDataCache.Add( + $CheckCommand, '' + ); + } + + $Global:Icinga.Private.Daemons.Add( + 'ServiceCheck', + @{ + 'PassedTime' = 0; + 'SortedResult' = $null; + 'PerformanceCache' = @{ }; + 'AverageCalculation' = @{ }; + 'MaxTime' = 0; + 'MaxTimeInSeconds' = 0; + } + ); + + foreach ($index in $TimeIndexes) { + # Only allow numeric index values + if ((Test-Numeric (ConvertTo-Seconds $index)) -eq $FALSE) { + Write-IcingaEventMessage -EventId 1450 -Namespace 'Framework' -Objects $CheckCommand, ($Arguments | Out-String), ($TimeIndexes | Out-String), $index; + continue; + } + if ($Global:Icinga.Private.Daemons.ServiceCheck.AverageCalculation.ContainsKey([string]$index) -eq $FALSE) { + $Global:Icinga.Private.Daemons.ServiceCheck.AverageCalculation.Add( + [string]$index, + @{ + 'Interval' = ([int]($index -Replace '[^0-9]', '')); + 'RawInterval' = $index; + 'Time' = (ConvertTo-Seconds $index); + 'Sum' = 0; + 'Count' = 0; + } + ); + } + if ($Global:Icinga.Private.Daemons.ServiceCheck.MaxTime -le (ConvertTo-Seconds $index)) { + $Global:Icinga.Private.Daemons.ServiceCheck.MaxTime = (ConvertTo-Seconds $index); + } + } + + $Global:Icinga.Private.Daemons.ServiceCheck.MaxTimeInSeconds = $Global:Icinga.Private.Daemons.ServiceCheck.MaxTime; + + if ($Global:Icinga.Private.Scheduler.CheckData.ContainsKey($CheckCommand) -eq $FALSE) { + $Global:Icinga.Private.Scheduler.CheckData.Add( + $CheckCommand, + @{ + 'results' = @{ }; + 'average' = (New-Object -TypeName PSObject); + } + ); + } +} +function Set-IcingaRegisteredServiceCheckConfig() +{ + param( + [string]$ServiceId, + [hashtable]$Arguments = $null, + $Interval = $null, + [array]$TimeIndexes = $null + ); + + $Services = Get-IcingaRegisteredServiceChecks; + + if ($Services.ContainsKey($ServiceId) -eq $FALSE) { + Write-IcingaConsoleError 'Service Id was not found'; + return; + } + + [bool]$Modified = $FALSE; + $Path = [string]::Format('BackgroundDaemon.RegisteredServices.{0}', $ServiceId); + + if ($null -ne $Arguments) { + Set-IcingaPowerShellConfig -Path ([string]::Format('{0}.Arguments', $Path)) -Value $Arguments; + $Modified = $TRUE; + } + if ($null -ne $Interval) { + Set-IcingaPowerShellConfig -Path ([string]::Format('{0}.Interval', $Path)) -Value $Interval; + $Modified = $TRUE; + } + if ($null -ne $TimeIndexes) { + Set-IcingaPowerShellConfig -Path ([string]::Format('{0}.TimeIndexes', $Path)) -Value $TimeIndexes; + $Modified = $TRUE; + } + + if ($Modified) { + Write-IcingaConsoleNotice 'Service configuration was successfully updated'; + } else { + Write-IcingaConsoleWarning 'No arguments were specified to update the service configuration'; + } +} +<# +.SYNOPSIS + A background daemon executing registered service checks in the background to fetch + metrics for certain checks over time. Time frames are configurable individual +.DESCRIPTION + This background daemon will execute checks registered with "Register-IcingaServiceCheck" + for the given time interval and store the collected metrics for a defined period of time + inside a JSON file. Check values collected by this daemon are then automatically added + to regular check executions for additional performance metrics. + + Example: Register-IcingaServiceCheck -CheckCommand 'Invoke-IcingaCheckCPU' -Interval 30 -TimeIndexes 1,3,5,15; + + This will execute the CPU check every 30 seconds and calculate the average of 1, 3, 5 and 15 minutes + + More Information on + https://icinga.com/docs/icinga-for-windows/latest/doc/service/02-Register-Daemons/ + https://icinga.com/docs/icinga-for-windows/latest/doc/service/10-Register-Service-Checks/ +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function Start-IcingaServiceCheckDaemon() +{ + New-IcingaThreadInstance ` + -Name 'Main' ` + -ThreadPool (Get-IcingaThreadPool -Name 'MainPool') ` + -Command 'Add-IcingaServiceCheckDaemon' ` + -Start; +} +function Add-IcingaServiceCheckDaemon() +{ + Add-IcingaThreadPool -Name 'ServiceCheckPool' -MaxInstances 100; + + [hashtable]$KnownChecks = @{ }; + + $Global:Icinga.Public.Daemons.Add( + 'ServiceCheck', + @{ + 'PerformanceDataCache' = @{ } + } + ); + + while ($TRUE) { + + # Load the configuration file to be aware of service check changes + $RegisteredServices = Get-IcingaRegisteredServiceChecks; + + # Debugging message + Write-IcingaDebugMessage 'Found these service checks to load within service check daemon' -Objects ($RegisteredServices.Keys | Out-String); + + # Loop all found background services and create a new thread for each check + foreach ($service in $RegisteredServices.Keys) { + [string]$ThreadName = [string]::Format('Start-IcingaServiceCheckTask::Add-IcingaServiceCheckTask::{0}::0', $service); + + # In case the check is already being executed, continue + if ((Test-IcingaThread -Thread $ThreadName)) { + continue; + } + + # Get all possible arguments for this check + [hashtable]$ServiceArgs = @{ }; + + if ($null -ne $RegisteredServices[$service].Arguments) { + foreach ($property in $RegisteredServices[$service].Arguments.PSObject.Properties) { + if ($ServiceArgs.ContainsKey($property.Name)) { + continue; + } + + $ServiceArgs.Add($property.Name, $property.Value) + } + } + + # Add the check to our cache, ensuring that we are aware of modifications during runtime + if ($KnownChecks.ContainsKey($service) -eq $FALSE) { + $KnownChecks.Add($service, $TRUE); + } else { + $KnownChecks[$service] = $TRUE; + } + + # Start a new thread for executing the check + Start-IcingaServiceCheckTask ` + -CheckId $service ` + -CheckCommand $RegisteredServices[$service].CheckCommand ` + -Arguments $ServiceArgs ` + -Interval $RegisteredServices[$service].Interval ` + -TimeIndexes $RegisteredServices[$service].TimeIndexes; + } + + # Cleanup + [array]$KnownCheckIds = $KnownChecks.Keys; + + foreach ($entry in $KnownCheckIds) { + if ($RegisteredServices.ContainsKey($entry)) { + continue; + } + + $KnownChecks[$entry] = $FALSE; + } + + foreach ($entry in $KnownCheckIds) { + if ($KnownChecks[$entry] -eq $FALSE) { + $KnownChecks.Remove($entry); + + [string]$ThreadName = [string]::Format('Start-IcingaServiceCheckTask::Add-IcingaServiceCheckTask::{0}::0', $entry); + + Remove-IcingaThread -Thread $ThreadName; + } + } + + # Force Icinga for Windows Garbage Collection + Optimize-IcingaForWindowsMemory -SmartGC; + + Start-Sleep -Seconds 10; + } +} +function Start-IcingaServiceCheckTask() +{ + param ( + $CheckId, + $CheckCommand, + $Arguments, + $Interval, + $TimeIndexes + ); + + New-IcingaThreadInstance -Name $CheckId -ThreadPool (Get-IcingaThreadPool -Name 'ServiceCheckPool') -Command 'Add-IcingaServiceCheckTask' -CmdParameters @{ + 'CheckCommand' = $CheckCommand; + 'Arguments' = $Arguments; + 'Interval' = $Interval; + 'TimeIndexes' = $TimeIndexes + 'CheckId' = $CheckId; + } -Start; +} +function Add-IcingaServiceCheckTask() +{ + param ( + $CheckCommand, + $Arguments, + $Interval, + $TimeIndexes, + $CheckId + ); + + # $Global:Icinga.Private.Daemons.ServiceCheck + New-IcingaServiceCheckDaemonEnvironment ` + -CheckCommand $CheckCommand ` + -Arguments $Arguments ` + -TimeIndexes $TimeIndexes; + + # Read our check result store data from disk for this service check + Read-IcingaCheckResultStore -CheckCommand $CheckCommand; + + $MetricCacheFile = Join-Path -Path (Join-Path -Path (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'service_check_cache') -ChildPath 'metrics') -ChildPath ([string]::Format('{0}.xml', $CheckCommand)); + [int]$CheckInterval = ConvertTo-Seconds $Interval; + [hashtable]$CheckDataCache = @{ }; + [array]$PerfDataEntries = @(); + [bool]$ForceExecution = $FALSE; + + if (Test-Path -Path $MetricCacheFile) { + $CheckDataCache = [System.Management.Automation.PSSerializer]::Deserialize((Get-Content -Path $MetricCacheFile -Raw -Encoding UTF8)); + } + + # In case we run this code for the first time for a CheckCommand and no data is available, ensure we run the check immediately + # This will ensure our plugins will always return proper values and not throw unknowns, in case the execution is set to a higher interval + if ($null -eq $CheckDataCache -Or $CheckDataCache.Count -eq 0) { + $ForceExecution = $TRUE; + } + + while ($TRUE) { + if ($ForceExecution -eq $FALSE -And $Global:Icinga.Private.Daemons.ServiceCheck.PassedTime -lt $CheckInterval) { + $Global:Icinga.Private.Daemons.ServiceCheck.PassedTime += 1; + Start-Sleep -Seconds 1; + + continue; + } + + $ForceExecution = $FALSE; + $Global:Icinga.Private.Daemons.ServiceCheck.PassedTime = 0; + + # Clear possible previous performance data from the daemon cache + $Global:Icinga.Private.Scheduler.PerfDataWriter.Daemon.Clear(); + + # Execute our check with possible arguments + try { + & $CheckCommand @Arguments | Out-Null; + } catch { + Write-IcingaEventMessage -EventId 1451 -Namespace 'Framework' -ExceptionObject $_ -Objects $CheckCommand, ($Arguments | Out-String), (Get-IcingaInternalPluginOutput); + + Clear-IcingaCheckSchedulerEnvironment; + + # Force Icinga for Windows Garbage Collection + Optimize-IcingaForWindowsMemory -ClearErrorStack -SmartGC; + + continue; + } + + $UnixTime = Get-IcingaUnixTime; + + foreach ($PerfLabel in $Global:Icinga.Private.Scheduler.PerfDataWriter.Daemon.Keys) { + $PerfValue = $Global:Icinga.Private.Scheduler.PerfDataWriter.Daemon[$PerfLabel].Value; + $PerfUnit = $Global:Icinga.Private.Scheduler.PerfDataWriter.Daemon[$PerfLabel].Unit; + + if ($CheckDataCache.ContainsKey($PerfLabel) -eq $FALSE) { + $CheckDataCache.Add($PerfLabel, (New-Object System.Collections.ArrayList)); + } + + $CheckDataCache[$PerfLabel].Add( + @{ + 'Time' = $UnixTime; + 'Value' = $PerfValue; + 'Unit' = $PerfUnit; + } + ) | Out-Null; + + [int]$IndexCount = $CheckDataCache[$PerfLabel].Count; + [int]$RemoveIndex = 0; + for ($i = 0; $i -lt $IndexCount; $i++) { + # In case we store more values than we require for our max time range, remove the oldest one + if (($UnixTime - $Global:Icinga.Private.Daemons.ServiceCheck.MaxTimeInSeconds) -gt [int]($CheckDataCache[$PerfLabel][$i].Time)) { + $RemoveIndex += 1; + continue; + } + + # Calculate the average value for our performance data based on the remaining data + foreach ($calc in $Global:Icinga.Private.Daemons.ServiceCheck.AverageCalculation.Keys) { + if (($UnixTime - $Global:Icinga.Private.Daemons.ServiceCheck.AverageCalculation[$calc].Time) -le [int]($CheckDataCache[$PerfLabel][$i].Time)) { + $Global:Icinga.Private.Daemons.ServiceCheck.AverageCalculation[$calc].Sum += $CheckDataCache[$PerfLabel][$i].Value; + $Global:Icinga.Private.Daemons.ServiceCheck.AverageCalculation[$calc].Count += 1; + } + } + } + + # Remove older entries more efficiently. As we store the data in an ArrayList, the oldest entries are at the beginning + # Therefore we can just remove a range of entries from the beginning of the list or clear the list if we need to remove all entries + if ($RemoveIndex -gt 0) { + if ($RemoveIndex -ge $IndexCount) { + $CheckDataCache[$PerfLabel].Clear() | Out-Null; + } else { + $CheckDataCache[$PerfLabel].RemoveRange(0, $RemoveIndex) | Out-Null; + } + $RemoveIndex = 0; + } + + # Now calculate the average values for our performance data + foreach ($calc in $Global:Icinga.Private.Daemons.ServiceCheck.AverageCalculation.Keys) { + if ($Global:Icinga.Private.Daemons.ServiceCheck.AverageCalculation[$calc].Count -ne 0) { + $AverageValue = ($Global:Icinga.Private.Daemons.ServiceCheck.AverageCalculation[$calc].Sum / $Global:Icinga.Private.Daemons.ServiceCheck.AverageCalculation[$calc].Count); + [string]$MetricMultiName = [string]::Format('{0}::Interval{1}={2}{3}', $PerfLabel, $Global:Icinga.Private.Daemons.ServiceCheck.AverageCalculation[$calc].Time, (Format-IcingaPerfDataValue $AverageValue), $PerfUnit); + # Write our performance data label + $PerfDataEntries += $MetricMultiName; + } + + $Global:Icinga.Private.Daemons.ServiceCheck.AverageCalculation[$calc].Sum = 0; + $Global:Icinga.Private.Daemons.ServiceCheck.AverageCalculation[$calc].Count = 0; + } + } + + $Global:Icinga.Public.Daemons.ServiceCheck.PerformanceDataCache[$CheckCommand] = $PerfDataEntries -Join ' '; + $PerfDataEntries = @(); + + $PerformanceLabelFile = Join-Path -Path (Join-Path -Path (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'service_check_cache') -ChildPath 'performance_labels') -ChildPath ([string]::Format('{0}.db', $CheckCommand)); + $CheckCacheXMLObj = [System.Management.Automation.PSSerializer]::Serialize($CheckDataCache); + + if ((Test-Path -Path $PerformanceLabelFile) -eq $FALSE) { + New-Item -Path $PerformanceLabelFile -ItemType File -Force | Out-Null; + } + if ((Test-Path -Path $MetricCacheFile) -eq $FALSE) { + New-Item -Path $MetricCacheFile -ItemType File -Force | Out-Null; + } + + Set-Content -Path $PerformanceLabelFile -Value $Global:Icinga.Public.Daemons.ServiceCheck.PerformanceDataCache[$CheckCommand] -Force -Encoding UTF8; + Set-Content -Path $MetricCacheFile -Value $CheckCacheXMLObj -Force -Encoding UTF8; + } +} +function Invoke-IcingaApiChecksRESTCall() +{ + param ( + [Hashtable]$Request = @{ }, + [Hashtable]$Connection = @{ }, + [string]$ApiVersion = $null + ); + + [Hashtable]$ContentResponse = @{ }; + + # Short our call + $CheckerAliases = $Global:Icinga.Public.Daemons.RESTApi.CommandAliases.checker; + $CheckConfig = $Request.Body; + [int]$ExitCode = 3; #Unknown + [string]$CheckResult = ''; + [string]$InternalError = ''; + + # Check if there are an inventory aliases configured + # This should be maintained by the developer and not occur + # anyway + if ($null -eq $CheckerAliases) { + $CheckerAliases = @{ }; + } + + if ((Get-IcingaRESTHeaderValue -Request $Request -Header 'Content-Type') -ne 'application/json' -And $Request.Method -eq 'POST') { + Send-IcingaTCPClientMessage -Message ( + New-IcingaTCPClientRESTMessage ` + -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Bad Request') ` + -ContentBody 'This API endpoint does only accept "application/json" as content type over POST.' + ) -Stream $Connection.Stream; + + return; + } + + # Our namespace to include inventory packages is 'include' over the api + # Everything else will be dropped for the moment + if ($Request.RequestArguments.ContainsKey('list')) { + + Add-IcingaHashtableItem ` + -Hashtable $ContentResponse ` + -Key 'Commands' ` + -Value $CheckerAliases | Out-Null; + + } elseif ($Request.RequestArguments.ContainsKey('command')) { + [string]$ExecuteCommand = $null; + + foreach ($element in $CheckerAliases.Keys) { + if ($Request.RequestArguments.command -Contains $element) { + $ExecuteCommand = $CheckerAliases[$element]; + # We only support to execute one check per call + # No need to loop through everything + break; + } + } + + if ([string]::IsNullOrEmpty($ExecuteCommand)) { + [string]$ExecuteCommand = $Request.RequestArguments.command; + } + + if ((Test-IcingaFunction -Name $ExecuteCommand) -eq $FALSE) { + + Add-IcingaHashtableItem ` + -Hashtable $ContentResponse ` + -Key $ExecuteCommand ` + -Value @{ + 'exitcode' = 3; + 'checkresult' = [string]::Format('[UNKNOWN] Icinga plugin not found exception: Command "{0}" is not present on the system{1}{1}The command "{0}" you are trying to execute over the REST-Api endpoint "apichecks" is not available on the system.', $ExecuteCommand, (New-IcingaNewLine)); + 'perfdata' = @(); + } | Out-Null; + + Send-IcingaTCPClientMessage -Message ( + New-IcingaTCPClientRESTMessage ` + -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Not Found') ` + -ContentBody $ContentResponse + ) -Stream $Connection.Stream; + + return; + } + + if ((Test-IcingaRESTApiCommand -Command $ExecuteCommand -Endpoint 'apichecks') -eq $FALSE) { + + Add-IcingaHashtableItem ` + -Hashtable $ContentResponse ` + -Key $ExecuteCommand ` + -Value @{ + 'exitcode' = 3; + 'checkresult' = [string]::Format('[UNKNOWN] Icinga Permission error was thrown: Permission denied for command "{0}"{1}{1}The command "{0}" you are trying to execute over the REST-Api endpoint "apichecks" is not whitelisted for remote execution.', $ExecuteCommand, (New-IcingaNewLine)); + 'perfdata' = @(); + } | Out-Null; + + Send-IcingaTCPClientMessage -Message ( + New-IcingaTCPClientRESTMessage ` + -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Forbidden') ` + -ContentBody $ContentResponse + ) -Stream $Connection.Stream; + + return; + } + + Write-IcingaDebugMessage -Message ('Executing API check for command: ' + $ExecuteCommand); + + if ([string]::IsNullOrEmpty($CheckConfig) -eq $FALSE -And $Request.Method -eq 'POST') { + # Convert our JSON config for checks to a PSCustomObject + $PSArguments = ConvertFrom-Json -InputObject $CheckConfig; + + # Read the command definition and arguments type, to ensure we properly handle SecureStrings + $CommandHelp = Get-Help -Name $ExecuteCommand -Full; + $CommandDetails = @{ }; + + foreach ($parameter in $CommandHelp.parameters.parameter) { + $CommandDetails.Add($parameter.Name, $parameter.Type.Name); + } + + # For executing the checks, we will require the data as + # hashtable, so declare it here + [hashtable]$Arguments = @{ }; + + # Now convert our custom object by Key<->Value to + # a valid hashtable, allowing us to parse arguments + # to our check command + $PSArguments.PSObject.Properties | ForEach-Object { + $CmdArgValue = $_.Value; + [string]$CmdArgName = $_.Name; + + # Ensure we can use both, `-MyArgument` and `MyArgument` as valid input + if ($CmdArgName[0] -eq '-') { + $CmdArgName = $CmdArgName.Substring(1, $CmdArgName.Length - 1); + } + + # Ensure we convert strings to SecureString, in case the plugin argument requires it + if ($CommandDetails.ContainsKey($CmdArgName) -And $CommandDetails[$CmdArgName] -Like 'SecureString') { + $CmdArgValue = ConvertTo-IcingaSecureString -String $_.Value; + } + + Add-IcingaHashtableItem ` + -Hashtable $Arguments ` + -Key $CmdArgName ` + -Value $CmdArgValue | Out-Null; + }; + + try { + [int]$ExitCode = & $ExecuteCommand @Arguments; + } catch { + [int]$ExitCode = 3; + $InternalError = $_.Exception.Message; + } + } elseif ($Request.Method -eq 'GET') { + try { + [int]$ExitCode = & $ExecuteCommand; + } catch { + [int]$ExitCode = 3; + $InternalError = $_.Exception.Message; + } + } else { + Send-IcingaTCPClientMessage -Message ( + New-IcingaTCPClientRESTMessage ` + -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.Ok) ` + -ContentBody @{ 'message' = 'This API endpoint does only accept GET and POST methods for requests.' } + ) -Stream $Connection.Stream; + + return; + } + + # Once the check is executed, the plugin output and the performance data are stored + # within a special cache map we can use for accessing + if ([string]::IsNullOrEmpty($InternalError)) { + $CheckResult = Get-IcingaCheckSchedulerPluginOutput; + } else { + if ($InternalError.Contains('[UNKNOWN]') -eq $FALSE) { + # Ensure we format the error message more user friendly + $CheckResult = [string]::Format('[UNKNOWN] Icinga Plugin execution error was thrown during API request:{0}{0}{1}', (New-IcingaNewLine), $InternalError); + } else { + $CheckResult = $InternalError; + } + } + [array]$PerfData = Get-IcingaCheckSchedulerPerfData; + + # Ensure our PerfData variable is always an array + if ($null -eq $PerfData -Or $PerfData.Count -eq 0) { + [array]$PerfData = @(); + } + + # Free our memory again + Clear-IcingaCheckSchedulerEnvironment -ClearCheckData; + + Write-IcingaDebugMessage -Message 'Check Executed. Result below' -Objects $ExecuteCommand, $CheckResult, $PerfData, $ExitCode; + + Add-IcingaHashtableItem ` + -Hashtable $ContentResponse ` + -Key $ExecuteCommand ` + -Value @{ + 'exitcode' = $ExitCode; + 'checkresult' = $CheckResult; + 'perfdata' = $PerfData; + } | Out-Null; + } + + if ($ContentResponse.Count -eq 0) { + $ContentResponse.Add( + 'message', + 'Welcome to the Icinga for Windows API checker. To execute checks, please use the command parameter. For providing arguments, you will have to submit a post with JSON encoded arguments. Example: /v1/checker?command=Invoke-IcingaCheckCPU' + ); + } + + # Send the response to the client + Send-IcingaTCPClientMessage -Message ( + New-IcingaTCPClientRESTMessage ` + -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.Ok) ` + -ContentBody $ContentResponse + ) -Stream $Connection.Stream; +} +<# +.SYNOPSIS + Registers an endpoint called 'checker' in our REST-Api +.DESCRIPTION + This module will provide an endpoint for the Icinga for Windows REST-Api + https://github.com/Icinga/icinga-powershell-restapi + + The endpoint itself is called 'checker' and will map available Cmdlets for + check plugins +.FUNCTIONALITY + This module will depend on the Icinga Plugins as well as the Framework and + the REST-Api. It will provide a wrapper to execute Icinga Plugins over the + REST-Api and fetch the plugin output +.OUTPUTS + $NULL +.LINK + https://github.com/Icinga/icinga-powershell-apichecks +.NOTES +#> +function Register-IcingaRESTAPIEndpointAPIChecks() +{ + return @{ + 'Alias' = 'checker'; + 'Command' = 'Invoke-IcingaApiChecksRESTCall'; + }; +} + +<# +.SYNOPSIS + Registers our command aliases for mapping the 'command' argument + from our REST-Call securely to our PowerShell Cmdlets +.DESCRIPTION + This module will provide an endpoint for the Icinga for Windows REST-Api + https://github.com/Icinga/icinga-powershell-restapi + By using the references, we can register endpoints and aliases to fetch + informations +.EXAMPLE + https://example.com/v1/checker?command=cpu +.FUNCTIONALITY + This module will depend on the Icinga Plugins as well as the Framework and + the REST-Api. It will provide a wrapper to execute Icinga Plugins over the + REST-Api and fetch the plugin output +.OUTPUTS + $NULL +.LINK + https://github.com/Icinga/icinga-powershell-apichecks +.NOTES +#> +function Register-IcingaRESTApiCommandAliasesApiChecks() +{ + [array]$CmdList = Get-Command -Name 'Invoke-IcingaCheck*'; + [hashtable]$CmdAlias = @{ + 'checker' = @{ } + }; + + $FrameworkRoot = Get-IcingaForWindowsRootPath; + + foreach ($entry in $CmdList) { + # Only include checks being installed on the same location as our Framework + if ($entry.Module.Path.Contains($FrameworkRoot) -eq $FALSE) { + continue; + } + + $CmdAliasName = $entry.Name.Replace('Invoke-IcingaCheck', ''); + + if ($CmdAlias.checker.ContainsKey($CmdAliasName)) { + continue; + } + + $CmdAlias.checker.Add($CmdAliasName, $Entry.Name); + } + + return $CmdAlias; +} +<# +.SYNOPSIS + Register the current host within the Icinga Director by using the + Self-Service API and return the host key +.DESCRIPTION + This function will register the current host within the Icinga Director in case + it is not already registered and returns the host key for storing it on disk + to allow the host to fetch detailed configurations like zones and endpoints +.FUNCTIONALITY + Register a host within the Icinga Director by using the Self-Service API +.EXAMPLE + PS>Register-IcingaDirectorSelfServiceHost -DirectorUrl 'https://example.com/icingaweb2/director -Hostname 'examplehost' -ApiKey 457g6b98054v76vb5490ß276bv0457v6054b76 -Endpoint 'icinga.example.com'; +.PARAMETER DirectorUrl + The URL pointing directly to the Icinga Web 2 Director module +.PARAMETER Hostname + The name of the current host to register within the Icinga Director +.PARAMETER ApiKey + The template key to authenticate against the Self-Service API +.PARAMETER Endpoint + The IP or FQDN to one of the parent Icinga 2 nodes this Agent will connect to + for determining which network interface shall be used by Icinga for connecting + and to apply hostalive/ping checks to +.INPUTS + System.String +.OUTPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Register-IcingaDirectorSelfServiceHost() +{ + param( + $DirectorUrl, + $Hostname, + $ApiKey = $null, + [string]$Endpoint = $null + ); + + if ([string]::IsNullOrEmpty($DirectorUrl)) { + throw 'Please enter a valid Url to your Icinga Director'; + } + + if ([string]::IsNullOrEmpty($Hostname)) { + throw 'Please enter the hostname to use'; + } + + if ([string]::IsNullOrEmpty($ApiKey)) { + throw 'Please enter the API key of the template you wish to use'; + } + + Set-IcingaTLSVersion; + $ProgressPreference = "SilentlyContinue"; + $DirectorConfigJson = $null; + + if ([string]::IsNullOrEmpty($Endpoint)) { + if ($DirectorUrl.Contains('https://') -Or $DirectorUrl.Contains('http://')) { + $Endpoint = $DirectorUrl.Split('/')[2]; + } else { + $Endpoint = $DirectorUrl.Split('/')[0]; + } + } + + $Interface = Get-IcingaNetworkInterface $Endpoint; + + if ($null -eq $Global:Icinga.InstallWizard.DirectorSelfServiceConfig) { + $DirectorConfigJson = [string]::Format('{{ "address": "{0}" }}', $Interface); + } else { + try { + $DirectorConfigJson = ConvertTo-Json -InputObject $Global:Icinga.InstallWizard.DirectorSelfServiceConfig -Compress -ErrorAction Stop; + $DirectorConfigJson = $DirectorConfigJson.Replace('$ifw.hostaddress$', $Interface); + $DirectorConfigJson = $DirectorConfigJson.Replace('$ifw.hostname.tolower$', (Get-IcingaHostname -AutoUseHostname 1 -LowerCase 1)); + $DirectorConfigJson = $DirectorConfigJson.Replace('$ifw.hostname.toupper$', (Get-IcingaHostname -AutoUseHostname 1 -UpperCase 1)); + $DirectorConfigJson = $DirectorConfigJson.Replace('$ifw.hostname$', (Get-IcingaHostname -AutoUseHostname 1)); + $DirectorConfigJson = $DirectorConfigJson.Replace('$ifw.hostfqdn.tolower$', (Get-IcingaHostname -AutoUseFQDN 1 -LowerCase 1)); + $DirectorConfigJson = $DirectorConfigJson.Replace('$ifw.hostfqdn.toupper$', (Get-IcingaHostname -AutoUseFQDN 1 -UpperCase 1)); + $DirectorConfigJson = $DirectorConfigJson.Replace('$ifw.hostfqdn$', (Get-IcingaHostname -AutoUseFQDN 1)); + } catch { + # Fallback to default + Write-IcingaConsosoleError 'Failed to deserialize your custom Icinga Director Self-Service configuration. Using Icinga for Windows defaults.' + $DirectorConfigJson = [string]::Format('{{ "address": "{0}" }}', $Interface); + } + } + + $EndpointUrl = Join-WebPath -Path $DirectorUrl -ChildPath ([string]::Format('/self-service/register-host?name={0}&key={1}', $Hostname, $ApiKey)); + + $response = Invoke-IcingaWebRequest -Uri $EndpointUrl -UseBasicParsing -Headers @{ 'accept' = 'application/json'; 'X-Director-Accept' = 'application/json' } -Method 'POST' -Body $DirectorConfigJson -NoErrorMessage; + + if ($response.StatusCode -ne 200) { + $ErrorMessage = ''; + switch ($response.StatusCode) { + 400 { + Write-IcingaConsoleWarning 'Failed to register host inside Icinga Director. The host is probably already registered.' + return $null; + }; + 404 { + $ErrorMessage = 'Failed to register host with the given API key "{0}" inside Icinga Director. Please ensure the template key you are using is correct and the template is set as "Icinga2 Agent" object. Non-Agent templates will not work over the Self-Service API.'; + break; + }; + 901 { + $ErrorMessage = 'Failed to register host over Self-Service API inside Icinga Director because of SSL/TLS error. Please ensure the certificate is valid and use "Enable-IcingaUntrustedCertificateValidation" for self-signed certificates or install the certificate on this machine.'; + break; + } + Default { + $ErrorMessage = ([string]::Format('Failed to register host inside Icinga Director because of unhandled exception: {0}', $response.StatusCode)); + break; + }; + } + + Write-IcingaConsoleError $ErrorMessage -Objects $ApiKey; + throw $ErrorMessage; + } + + $JsonContent = ConvertFrom-Json -InputObject $response.Content; + + if (Test-PSCustomObjectMember -PSObject $JsonContent -Name 'error') { + if ($JsonContent.error -like '*already been registered*') { + return $null; + } + + throw 'Icinga Director Self-Service has thrown an error: ' + $JsonContent.error; + } + + Set-IcingaPowerShellConfig -Path 'IcingaDirector.SelfService.ApiKey' -Value $JsonContent; + + Write-IcingaConsoleNotice 'Host was successfully registered within Icinga Director'; + + return $JsonContent; +} +<# +.SYNOPSIS + Will fetch the current host configuration or general configuration depending + if a host or template key is specified from the Icinga Director Self-Service API +.DESCRIPTION + Use the Self-Service API of the Icinga Director to connect to it and fetch the + configuration to apply for this host. The configuration itself is differentiated + if a template or the specific host key is used +.FUNCTIONALITY + Fetches host or general configuration form the Icinga Director Self-Service API +.EXAMPLE + PS>Get-IcingaDirectorSelfServiceConfig -DirectorUrl 'https://example.com/icingaweb2/director -ApiKey 457g6b98054v76vb5490ß276bv0457v6054b76; +.PARAMETER DirectorUrl + The URL pointing directly to the Icinga Web 2 Director module +.PARAMETER ApiKey + Either the template or host key to authenticate against the Self-Service API +.INPUTS + System.String +.OUTPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaDirectorSelfServiceConfig() +{ + param( + $DirectorUrl, + $ApiKey = $null + ); + + if ([string]::IsNullOrEmpty($DirectorUrl)) { + throw 'Please enter a valid Url to your Icinga Director'; + } + + if ([string]::IsNullOrEmpty($ApiKey)) { + throw 'Please enter either a template or your host key. If this message persists, ensure your host is not having a template key assigned already. If so, you can try dropping it within the Icinga Director.'; + } + + Set-IcingaTLSVersion; + $ProgressPreference = "SilentlyContinue"; + + $EndpointUrl = Join-WebPath -Path $DirectorUrl -ChildPath ([string]::Format('/self-service/powershell-parameters?key={0}', $ApiKey)); + + $response = Invoke-IcingaWebRequest -Uri $EndpointUrl -UseBasicParsing -Headers @{ 'accept' = 'application/json'; 'X-Director-Accept' = 'application/json' } -Method 'POST' -NoErrorMessage; + + if ($response.StatusCode -ne 200) { + $ErrorMessage = ''; + switch ($response.StatusCode) { + 403 { + $ErrorMessage = 'Failed to fetch configuration for host over Self-Service API with key "{0}". This error mostly occurs in case the host object itself is not defined as "Icinga2 Agent" object inside the Icinga Director.'; + break; + }; + 404 { + $ErrorMessage = 'Failed to fetch configuration for host over Self-Service API with key "{0}". Probably the assigned host/template key is not valid or your Icinga Director Url is invalid "{1}".'; + break; + }; + 901 { + $ErrorMessage = 'Failed to fetch host/template configuration from Icinga Director Self-Service API because of SSL/TLS error. Please ensure the certificate is valid and use "Enable-IcingaUntrustedCertificateValidation" for self-signed certificates or install the certificate on this machine.'; + break; + } + Default { + $ErrorMessage = ([string]::Format('Failed to fetch host/template configuration from Icinga Director Self-Service API because of unhandled exception: {0}', $response.StatusCode)); + break; + }; + } + + Write-IcingaConsoleError $ErrorMessage -Objects $ApiKey, $DirectorUrl; + throw $ErrorMessage; + } + + $JsonContent = ConvertFrom-Json -InputObject $response.Content; + + if (Test-PSCustomObjectMember -PSObject $JsonContent -Name 'error') { + throw 'Icinga Director Self-Service has thrown an error: ' + $JsonContent.error; + } + + $JsonContent = Add-PSCustomObjectMember -Object $JsonContent -Key 'IcingaMaster' -Value $response.BaseResponse.ResponseUri.Host; + + return $JsonContent; +} +<# +.SYNOPSIS + Will fetch the ticket for certificate signing by using the Icinga Director + Self-Service API +.DESCRIPTION + Use the Self-Service API of the Icinga Director to connect to it and fetch the + ticket to sign Icinga 2 certificate requests +.FUNCTIONALITY + Fetches the ticket for certificate signing form the Icinga Director Self-Service API +.EXAMPLE + PS>Get-IcingaDirectorSelfServiceTicket -DirectorUrl 'https://example.com/icingaweb2/director -ApiKey 457g6b98054v76vb5490ß276bv0457v6054b76; +.PARAMETER DirectorUrl + The URL pointing directly to the Icinga Web 2 Director module +.PARAMETER ApiKey + The host key to authenticate against the Self-Service API +.INPUTS + System.String +.OUTPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaDirectorSelfServiceTicket() +{ + param ( + $DirectorUrl, + $ApiKey = $null + ); + + if ([string]::IsNullOrEmpty($DirectorUrl)) { + Write-IcingaConsoleError 'Unable to fetch host ticket. No Director url has been specified'; + return; + } + + if ([string]::IsNullOrEmpty($ApiKey)) { + Write-IcingaConsoleError 'Unable to fetch host ticket. No API key has been specified'; + return; + } + + Set-IcingaTLSVersion; + + [string]$url = Join-WebPath -Path $DirectorUrl -ChildPath ([string]::Format('/self-service/ticket?key={0}', $ApiKey)); + + $response = Invoke-IcingaWebRequest -Uri $url -UseBasicParsing -Headers @{ 'accept' = 'application/json'; 'X-Director-Accept' = 'application/json' } -Method 'POST' -NoErrorMessage; + + if ($response.StatusCode -ne 200) { + $ErrorMessage = ''; + switch ($response.StatusCode) { + 404 { + $ErrorMessage = ([string]::Format('Failed to fetch certificate ticket for this host over Self-Service API. Please check that your Icinga Director Url "{1}" is valid and the provided API key "{0}" belongs to a Icinga host object.', $DirectorUrl, $ApiKey)); + break; + }; + 500 { + $ErrorMessage = 'Failed to fetch certificate ticket for this host over Self-Service API. Please check that your Icinga CA is running, you have configured a TicketSalt and that your Icinga Director has enough permissions to communicate with the Icinga 2 API for generating tickets.'; + break; + }; + 901 { + $ErrorMessage = 'Failed to fetch certificate ticket for this host over Self-Service API because of SSL/TLS error. Please ensure the certificate is valid and use "Enable-IcingaUntrustedCertificateValidation" for self-signed certificates or install the certificate on this machine.'; + break; + } + Default { + $ErrorMessage = ([string]::Format('Failed to fetch certificate ticket from Icinga Director because of unhandled exception: {0}', $response.StatusCode)); + break; + }; + } + + Write-IcingaConsoleError $ErrorMessage -Objects $ApiKey, $DirectorUrl; + throw $ErrorMessage; + } + + $JsonContent = ConvertFrom-Json -InputObject $response.Content; + + return $JsonContent; +} +function Push-IcingaRepository() +{ + param ( + [string]$Name = $null, + [switch]$Silent = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + if ($Silent -eq $FALSE) { + Write-IcingaConsoleError 'You have to provide a name for the repository'; + } + return; + } + + $Name = $Name.Replace('.', '-'); + + $CurrentRepositories = Get-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories'; + + if ($null -eq $CurrentRepositories) { + if ($Silent -eq $FALSE) { + Write-IcingaConsoleNotice 'You have no repositories configured yet.'; + } + return; + } + + [array]$RepoList = Get-IcingaRepositories; + [int]$Index = 0; + + foreach ($repo in $RepoList) { + if ($repo.Name -eq $Name) { + continue; + } + + $CurrentRepositories.($repo.Name).Order = [int]$Index; + $Index += 1; + } + + $CurrentRepositories.$Name.Order = [int]$Index; + + if ($Silent -eq $FALSE) { + Write-IcingaConsoleNotice 'The repository "{0}" was put at the top of the repository list' -Objects $Name; + } + + Set-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories' -Value $CurrentRepositories; +} +function Get-IcingaInstallation() +{ + param ( + [switch]$Release = $FALSE, + [switch]$Snapshot = $FALSE + ) + + Set-IcingaServiceEnvironment; + + # Ensure our error list is cleared at this point + Clear-IcingaRepositoryErrorState; + + [hashtable]$InstalledComponents = @{ }; + + $PowerShellModules = Get-Module -ListAvailable; + + foreach ($entry in $PowerShellModules) { + $RootPath = (Get-IcingaForWindowsRootPath); + + if ($entry.Path -NotLike "$RootPath*") { + continue; + } + + if ($entry.Name -Like 'icinga-powershell-*') { + $ComponentName = $entry.Name.Replace('icinga-powershell-', ''); + $InstallPackage = (Get-IcingaRepositoryPackage -Name $ComponentName -Release:$Release -Snapshot:$Snapshot); + $LatestVersion = ''; + $CurrentVersion = ([string]((Get-Module -ListAvailable -Name $entry.Name -ErrorAction SilentlyContinue) | Sort-Object Version -Descending | Select-Object Version -First 1).Version); + + if ($InstallPackage.HasPackage) { + [string]$LatestVersion = $InstallPackage.Package.Version; + } + + Add-IcingaHashtableItem ` + -Hashtable $InstalledComponents ` + -Key $ComponentName ` + -Value @{ + 'Path' = (Join-Path -Path $RootPath -ChildPath $entry.Name); + 'CurrentVersion' = $CurrentVersion; + 'LatestVersion' = $LatestVersion; + 'LockedVersion' = (Get-IcingaComponentLock -Name $ComponentName); + } | Out-Null; + } + } + + $IcingaForWindowsService = $Global:Icinga.Protected.Environment.'PowerShell Service'; + + if ($null -ne $IcingaForWindowsService -And $IcingaForWindowsService.Present) { + $ServicePath = Get-IcingaForWindowsServiceData; + + if ($InstalledComponents.ContainsKey('service')) { + $InstalledComponents.Remove('service'); + } + + $InstallPackage = (Get-IcingaRepositoryPackage -Name 'service' -Release:$Release -Snapshot:$Snapshot); + $LatestVersion = ''; + $CurrentVersion = ([string]((Read-IcingaServicePackage -File $ServicePath.FullPath).ProductVersion)); + + if ($InstallPackage.HasPackage) { + [string]$LatestVersion = $InstallPackage.Package.Version; + } + + $InstalledComponents.Add( + 'service', + @{ + 'Path' = $ServicePath.Directory; + 'CurrentVersion' = $CurrentVersion; + 'LatestVersion' = $LatestVersion; + 'LockedVersion' = (Get-IcingaComponentLock -Name 'service'); + } + ) + } + + $IcingaAgent = Get-IcingaAgentInstallation; + + if ($InstalledComponents.ContainsKey('agent')) { + $InstalledComponents.Remove('agent'); + } + + if ($IcingaAgent.Installed) { + + $InstallPackage = (Get-IcingaRepositoryPackage -Name 'agent' -Release:$Release -Snapshot:$Snapshot); + $LatestVersion = ''; + $CurrentVersion = ([string]$IcingaAgent.Version.Full); + + if ($InstallPackage.HasPackage) { + $LatestVersion = $InstallPackage.Package.Version; + } + + $InstalledComponents.Add( + 'agent', + @{ + 'Path' = $IcingaAgent.RootDir; + 'CurrentVersion' = $CurrentVersion; + 'LatestVersion' = $LatestVersion; + 'LockedVersion' = (Get-IcingaComponentLock -Name 'agent'); + } + ) + } + + return $InstalledComponents; +} +function Update-IcingaRepository() +{ + param ( + [string]$Name = $null, + [string]$Path = $null, + [string]$RemotePath = $null, + [switch]$CreateNew = $FALSE, + [switch]$ForceTrust = $FALSE + ); + + $CurrentRepositories = Get-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories'; + + [array]$ConfigCount = $CurrentRepositories.PSObject.Properties.Count; + + if (($null -eq $CurrentRepositories -Or $ConfigCount.Count -eq 0) -And $CreateNew -eq $FALSE) { + Write-IcingaConsoleNotice 'There are no repositories configured yet. You can create a custom repository with "New-IcingaRepository" or clone an existing one with "Sync-IcingaForWindowsRepository"'; + return; + } + + if ([string]::IsNullOrEmpty($Name) -eq $FALSE) { + + $Name = $Name.Replace('.', '-'); + + if ((Test-IcingaPowerShellConfigItem -ConfigObject $CurrentRepositories -ConfigKey $Name) -eq $FALSE -And $CreateNew -eq $FALSE) { + Write-IcingaConsoleError 'A repository with the given name "{0}" does not exist. Use "New-IcingaRepository" or "Sync-IcingaForWindowsRepository" to create a new one.' -Objects $Name; + return; + } + } + + foreach ($definedRepo in $CurrentRepositories.PSObject.Properties) { + + if ([string]::IsNullOrEmpty($Name) -eq $FALSE -And $definedRepo.Name.ToLower() -ne $Name.ToLower()) { + continue; + } + + if ($definedRepo.Value.Enabled -eq $FALSE) { + Write-IcingaConsoleNotice 'Skipping disabled repository "{0}"' -Objects $definedRepo.Name; + continue; + } + + if ([string]::IsNullOrEmpty($definedRepo.Value.CloneSource) -eq $FALSE) { + continue; + } + + if ([string]::IsNullOrEmpty($definedRepo.Value.LocalPath)) { + continue; + } + + if ((Test-Path $definedRepo.Value.LocalPath) -eq $FALSE) { + if ($CreateNew) { + return $null; + } + continue; + } + + $Path = Join-Path -Path $definedRepo.Value.LocalPath -ChildPath '\'; + + if ($CreateNew -eq $FALSE) { + Write-IcingaConsoleNotice 'Updating Icinga for Windows repository "{0}"' -Objects $definedRepo.Name; + } + + # Ensure we use the default RemotePath within our IMC config, unless we specify a different path during update + [string]$SetRemotePath = $definedRepo.Value.RemotePath; + + if ([string]::IsNullOrEmpty($RemotePath) -eq $FALSE) { + Set-IcingaPowerShellConfig -Path ([string]::Format('Framework.Repository.Repositories.{0}.RemotePath', $definedRepo.Name)) -Value $RemotePath; + $SetRemotePath = $RemotePath; + } + + $IcingaRepository = New-IcingaRepositoryFile -Path $Path -RemotePath $SetRemotePath -Name $Name; + + if ($CreateNew) { + return $IcingaRepository; + } + } + + # Always sync repositories at the end, in case we updated a local repository and cloned it to somewhere else + foreach ($definedRepo in $CurrentRepositories.PSObject.Properties) { + + if ([string]::IsNullOrEmpty($Name) -eq $FALSE -And $definedRepo.Name.ToLower() -ne $Name.ToLower()) { + continue; + } + + if ($definedRepo.Value.Enabled -eq $FALSE) { + continue; + } + + if ([string]::IsNullOrEmpty($definedRepo.Value.LocalPath)) { + continue; + } + + # Ensure we use the default RemotePath within our IMC config, unless we specify a different path during update + [string]$SetRemotePath = $definedRepo.Value.RemotePath; + + if ([string]::IsNullOrEmpty($RemotePath) -eq $FALSE) { + Set-IcingaPowerShellConfig -Path ([string]::Format('Framework.Repository.Repositories.{0}.RemotePath', $definedRepo.Name)) -Value $RemotePath; + $SetRemotePath = $RemotePath; + } + + Write-IcingaConsoleDebug 'Syncing repository "{0}"' -Objects $definedRepo.Name; + + if ([string]::IsNullOrEmpty($definedRepo.Value.CloneSource) -eq $FALSE) { + Sync-IcingaRepository ` + -Name $definedRepo.Name ` + -Path $definedRepo.Value.LocalPath ` + -RemotePath $SetRemotePath ` + -Source $definedRepo.Value.CloneSource ` + -UseSCP:$definedRepo.Value.UseSCP ` + -Force ` + -ForceTrust:$ForceTrust; + + return; + } + } + + Write-IcingaConsoleNotice 'All Icinga for Windows repositories were successfully updated'; +} +function Get-IcingaComponentList() +{ + param ( + [switch]$Snapshot = $FALSE + ); + + $Repositories = Get-IcingaRepositories -ExcludeDisabled; + [Version]$LatestVersion = $null; + [string]$SourcePath = $null; + [bool]$FoundPackage = $FALSE; + [array]$Output = @(); + [bool]$FoundPackage = $FALSE; + + $SearchList = New-Object -TypeName PSObject; + $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; + + if ($null -eq $RepoContent) { + continue; + } + + if ((Test-IcingaPowerShellConfigItem -ConfigObject $RepoContent -ConfigKey 'Packages') -eq $FALSE) { + continue; + } + + foreach ($repoEntry in $RepoContent.Packages.PSObject.Properties.Name) { + + if ($repoEntry.ToLower() -eq 'kickstart') { + continue; + } + + $RepoData = New-Object -TypeName PSObject; + $RepoData | Add-Member -MemberType NoteProperty -Name 'Name' -Value $entry.Name; + $RepoData | Add-Member -MemberType NoteProperty -Name 'RemoteSource' -Value $RepoContent.Info.RemoteSource; + $RepoData | Add-Member -MemberType NoteProperty -Name 'ComponentName' -Value $repoEntry; + $RepoData | Add-Member -MemberType NoteProperty -Name 'Packages' -Value @(); + + foreach ($package in $RepoContent.Packages.$repoEntry) { + + $ComponentData = New-Object -TypeName PSObject; + $ComponentData | Add-Member -MemberType NoteProperty -Name 'Version' -Value $package.Version; + $ComponentData | Add-Member -MemberType NoteProperty -Name 'Location' -Value $package.Location; + $ComponentData | Add-Member -MemberType NoteProperty -Name 'Snapshot' -Value $package.Snapshot; + + if ($package.Snapshot -And $Snapshot -eq $FALSE) { + continue; + } + + if ($package.Snapshot -eq $FALSE -And $Snapshot) { + continue; + } + + if ($Snapshot -eq $FALSE -And (Test-Numeric $package.Version.Replace('.', '')) -eq $FALSE) { + continue; + } + + if ($Snapshot -And (Test-Numeric $package.Version.Replace('.', '')) -eq $FALSE) { + # Branch snapshot + [string]$SnapshotPackageName = ([string]::Format('{0}/{1}', $repoEntry, $package.Version)); + if ($SearchList.Components.ContainsKey($SnapshotPackageName) -eq $FALSE) { + $SearchList.Components.Add($SnapshotPackageName, $package.Version); + } + + continue; + } + + if ($SearchList.Components.ContainsKey($repoEntry) -eq $FALSE) { + $SearchList.Components.Add($repoEntry, $package.Version); + } + + if ((Test-Numeric $package.Version.Replace('.', '')) -And [version]($SearchList.Components[$repoEntry]) -lt [version]$package.Version) { + $SearchList.Components[$repoEntry] = $package.Version; + } + + $RepoData.Packages += $ComponentData; + } + + $SearchList.Repos += $RepoData; + } + } + + return $SearchList; +} +function Pop-IcingaRepository() +{ + param ( + [string]$Name = $null + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to provide a name for the repository'; + return; + } + + $Name = $Name.Replace('.', '-'); + + $CurrentRepositories = Get-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories'; + + if ($null -eq $CurrentRepositories) { + Write-IcingaConsoleNotice 'You have no repositories configured yet.'; + return; + } + + [array]$RepoList = Get-IcingaRepositories; + [int]$Index = $RepoList.Count - 1; + + foreach ($repo in $RepoList) { + if ($repo.Name -eq $Name) { + continue; + } + + $CurrentRepositories.($repo.Name).Order = [int]$Index; + $Index -= 1; + } + + $CurrentRepositories.$Name.Order = [int]$Index; + + Write-IcingaConsoleNotice 'The repository "{0}" was put at the bottom of the repository list' -Objects $Name; + + Set-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories' -Value $CurrentRepositories; +} +function Remove-IcingaRepository() +{ + param ( + [string]$Name = $null + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to provide a name for the repository to remove'; + return; + } + + $Name = $Name.Replace('.', '-'); + + $CurrentRepositories = Get-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories'; + + if ((Test-IcingaPowerShellConfigItem -ConfigObject $CurrentRepositories -ConfigKey $Name) -eq $FALSE) { + Write-IcingaConsoleError 'A repository with the name "{0}" is not configured' -Objects $Name; + return; + } + + Push-IcingaRepository -Name $Name -Silent; + + Remove-IcingaPowerShellConfig -Path ( + [string]::Format( + 'Framework.Repository.Repositories.{0}', + $Name + ) + ); + + Write-IcingaConsoleNotice 'The repository with the name "{0}" was successfully removed' -Objects $Name; +} +function Search-IcingaRepository() +{ + param ( + [string]$Name = $null, + [string]$Version = $null, + [switch]$Release = $FALSE, + [switch]$Snapshot = $FALSE + ); + + if ($Version -eq 'release') { + $Version = $null; + } + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to provide a component name'; + return; + } + + if ($Release -eq $FALSE -And $Snapshot -eq $FALSE) { + $Release = $TRUE; + } + + $Repositories = Get-IcingaRepositories -ExcludeDisabled; + [Version]$LatestVersion = $null; + [string]$SourcePath = $null; + [bool]$FoundPackage = $FALSE; + [array]$Output = @(); + [bool]$FoundPackage = $FALSE; + + $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; + + if ($null -eq $RepoContent) { + continue; + } + + if ((Test-IcingaPowerShellConfigItem -ConfigObject $RepoContent -ConfigKey 'Packages') -eq $FALSE) { + continue; + } + + foreach ($repoEntry in $RepoContent.Packages.PSObject.Properties.Name) { + + if ($repoEntry -NotLike $Name -Or $Name.ToLower() -eq 'kickstart') { + continue; + } + + $RepoData = New-Object -TypeName PSObject; + $RepoData | Add-Member -MemberType NoteProperty -Name 'Name' -Value $entry.Name; + $RepoData | Add-Member -MemberType NoteProperty -Name 'RemoteSource' -Value $RepoContent.Info.RemoteSource; + $RepoData | Add-Member -MemberType NoteProperty -Name 'ComponentName' -Value $repoEntry; + $RepoData | Add-Member -MemberType NoteProperty -Name 'Packages' -Value @(); + + foreach ($package in $RepoContent.Packages.$repoEntry) { + + $ComponentData = New-Object -TypeName PSObject; + $ComponentData | Add-Member -MemberType NoteProperty -Name 'Version' -Value $package.Version; + $ComponentData | Add-Member -MemberType NoteProperty -Name 'Location' -Value $package.Location; + $ComponentData | Add-Member -MemberType NoteProperty -Name 'Snapshot' -Value $package.Snapshot; + + if ($Snapshot -And $package.Snapshot -eq $TRUE -And [string]::IsNullOrEmpty($Version)) { + $RepoData.Packages += $ComponentData; + continue; + } + + if ($Snapshot -And $package.Snapshot -eq $TRUE -And [string]::IsNullOrEmpty($Version) -eq $FALSE -And [version]$package.Version -eq [version]$Version) { + $RepoData.Packages += $ComponentData; + continue; + } + + if ($Release -And [string]::IsNullOrEmpty($Version) -And $package.Snapshot -eq $FALSE) { + $RepoData.Packages += $ComponentData; + continue; + } + + if ([string]::IsNullOrEmpty($Version) -eq $FALSE -And $Release -eq $FALSE -And $Snapshot -eq $FALSE -And $package.Snapshot -eq $FALSE) { + $RepoData.Packages += $ComponentData; + continue; + } + } + + if ($RepoData.Packages.Count -ne 0) { + $FoundPackage = $TRUE; + } + + $SearchList.Repos += $RepoData; + } + } + + if ($FoundPackage -eq $FALSE) { + $SearchVersion = 'release'; + if ([string]::IsNullOrEmpty($Version) -eq $FALSE) { + $SearchVersion = $Version; + } + if ($Release) { + Write-IcingaConsoleNotice 'The component "{0}" was not found on stable channel with version "{1}"' -Objects $Name, $SearchVersion; + } + if ($Snapshot) { + Write-IcingaConsoleNotice 'The component "{0}" was not found on snapshot channel with version "{1}"' -Objects $Name, $SearchVersion; + } + return; + } + + foreach ($repo in $SearchList.Repos) { + if ($repo.Packages.Count -eq 0) { + continue; + } + + $Output += $repo.Name; + $Output += '-----------'; + $Output += [string]::Format('Source => {0}', $repo.RemoteSource); + $Output += ''; + $Output += $repo.ComponentName; + + [array]$VersionList = $repo.Packages | Sort-Object { $_.Version } -Descending; + + foreach ($componentData in $VersionList) { + $Output += [string]::Format('{0} => {1}', $componentData.Version, $componentData.Location); + } + $Output += ''; + } + + Write-Host ($Output | Out-String); +} +function Show-IcingaRepository() +{ + [hashtable]$Repositories = @{ }; + [array]$RepoSummary = @( + 'List of configured repositories on this system. The list order matches the apply order:', + '' + ); + [array]$RepoList = Get-IcingaRepositories; + + foreach ($repo in $RepoList) { + + $RepoSummary += $repo.Name; + $RepoSummary += '-----------'; + + [int]$MaxLength = Get-IcingaMaxTextLength -TextArray $repo.Value.PSObject.Properties.Name; + [array]$RepoData = @(); + + foreach ($repoConfig in $repo.Value.PSObject.Properties) { + $PrintName = Add-IcingaWhiteSpaceToString -Text $repoConfig.Name -Length $MaxLength; + $RepoData += [string]::Format('{0} => {1}', $PrintName, $repoConfig.Value); + } + + $RepoSummary += $RepoData | Sort-Object; + $RepoSummary += ''; + } + + if ($RepoList.Count -eq 0) { + $RepoSummary += 'No repositories configured'; + } + + Write-Output $RepoSummary; +} +function Read-IcingaRepositoryFile() +{ + param ( + [string]$Name = $null, + [switch]$TryAlternate = $FALSE, + [switch]$PrintRetryMsg = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to provide a name for the repository'; + 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)); + + if ($null -eq $Repository) { + Write-IcingaConsoleError 'A repository with the given name "{0}" does not exist. Use "New-IcingaRepository" or "Sync-IcingaForWindowsRepository" to create a new one.' -Objects $Name; + return $null; + } + + $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)) { + $RepoPath = $Repository.RemotePath; + } + + 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 $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; + } + + $Content = Get-Content -Path $RepoPath -Raw; + } else { + try { + $RepoPath = $Repository.RemotePath; + + if ($TryAlternate) { + $RepoPath = (Join-WebPath -Path $Repository.RemotePath -ChildPath 'ifw.repo.json'); + } + + $WebContent = Invoke-IcingaWebRequest -UseBasicParsing -Uri $RepoPath; + + if ($null -ne $WebContent) { + 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 { + if ($TryAlternate -eq $FALSE) { + return (Read-IcingaRepositoryFile -Name $Name -TryAlternate -PrintRetryMsg); + } + $Content = $null; + } + } else { + if ($TryAlternate -eq $FALSE) { + return (Read-IcingaRepositoryFile -Name $Name -TryAlternate -PrintRetryMsg); + } + } + } catch { + if ($TryAlternate -eq $FALSE) { + 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; + } + } + } + + 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; + } + + $RepositoryObject = $null; + + if (Test-IcingaJSONObject -InputObject $Content) { + $RepositoryObject = ConvertFrom-Json -InputObject $Content -ErrorAction Stop; + } 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 -PrintRetryMsg); + } + + Add-IcingaRepositoryErrorState -Repository $Name; + } + + return $RepositoryObject; +} +function Get-IcingaRepositories() +{ + param ( + [switch]$ExcludeDisabled = $FALSE + ); + + $CurrentRepositories = Get-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories'; + [array]$RepoList = $CurrentRepositories.PSObject.Properties | Sort-Object { $_.Value.Order } -Descending; + + if ($ExcludeDisabled -eq $FALSE) { + return $RepoList; + } + + [array]$ActiveRepos = @(); + + foreach ($repo in $RepoList) { + if ($repo.Value.Enabled -eq $FALSE) { + continue; + } + + $ActiveRepos += $repo; + } + + return $ActiveRepos; +} +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 $FALSE; +} +function Show-Icinga() +{ + param ( + [switch]$SkipHeader = $FALSE + ); + + $IcingaInstallation = Get-IcingaInstallation -Release; + [array]$Output = @( 'Icinga for Windows environment:' ); + [array]$VersionList = @(); + [int]$MaxComponentLength = Get-IcingaMaxTextLength -TextArray $IcingaInstallation.Keys; + + foreach ($entry in $IcingaInstallation.Keys) { + $LockVersion = Get-IcingaComponentLock -Name $entry; + + if ($null -eq $LockVersion) { + $VersionList += [string]$IcingaInstallation[$entry].CurrentVersion; + continue; + } + + $VersionList += ([string]::Format('{0}*', $IcingaInstallation[$entry].CurrentVersion)); + } + + [int]$MaxVersionLength = Get-IcingaMaxTextLength -TextArray $VersionList; + [string]$ComponentHeader = Add-IcingaWhiteSpaceToString -Text 'Component' -Length $MaxComponentLength; + [string]$ComponentLine = Add-IcingaWhiteSpaceToString -Text '---' -Length $MaxComponentLength; + $Output += '-----------'; + $Output += ''; + + if ($SkipHeader) { + [array]$Output = @(); + } + + $IcingaForWindowsService = Get-IcingaForWindowsServiceData; + $IcingaAgentService = Get-IcingaAgentInstallation; + $WindowsInformation = Get-IcingaWindowsInformation Win32_OperatingSystem | Select-Object Version, BuildNumber, Caption; + $DefinedServiceUser = Get-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser'; + $JEAContext = Get-IcingaJEAContext; + $JEASessionFile = Get-IcingaJEASessionFile; + $IcingaForWindowsCert = Get-IcingaForWindowsCertificate; + $ServicePid = Get-IcingaForWindowsServicePid; + $JEAServicePid = Get-IcingaJEAServicePid; + + if ([string]::IsNullOrEmpty($DefinedServiceUser)) { + $DefinedServiceUser = ''; + } + if ([string]::IsNullOrEmpty($JEAContext)) { + $JEAContext = ''; + } + if ([string]::IsNullOrEmpty($JEASessionFile)) { + $JEASessionFile = ''; + } + if ([string]::IsNullOrEmpty($ServicePid)) { + $ServicePid = ''; + } + if ([string]::IsNullOrEmpty($JEAServicePid)) { + $JEAServicePid = ''; + } + + $Output += ''; + $Output += 'Environment configuration:'; + $Output += ''; + $Output += ([string]::Format('PowerShell Root => {0}', (Get-IcingaForWindowsRootPath))); + $Output += ([string]::Format('Icinga for Windows Service Path => {0}', $IcingaForWindowsService.Directory)); + $Output += ([string]::Format('Icinga for Windows Service User => {0}', $IcingaForWindowsService.User)); + $Output += ([string]::Format('Icinga for Windows Service Pid => {0}', $ServicePid)); + $Output += ([string]::Format('Icinga for Windows JEA Pid => {0}', $JEAServicePid)); + $Output += ([string]::Format('Icinga Agent Path => {0}', $IcingaAgentService.RootDir)); + $Output += ([string]::Format('Icinga Agent User => {0}', $IcingaAgentService.User)); + $Output += ([string]::Format('Defined Default User => {0}', $DefinedServiceUser)); + $Output += ([string]::Format('Icinga Managed User => {0}', (Test-IcingaManagedUser -IcingaUser (Get-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser')))); + $Output += ([string]::Format('PowerShell Version => {0}', $PSVersionTable.PSVersion.ToString())); + $Output += ([string]::Format('Operating System => {0}', $WindowsInformation.Caption)); + $Output += ([string]::Format('Operating System Version => {0}', $WindowsInformation.Version)); + $Output += ([string]::Format('JEA Context => {0}', $JEAContext)); + $Output += ([string]::Format('JEA Session File => {0}', $JEASessionFile)); + $Output += ([string]::Format('Api Check Forwarder => {0}', (Get-IcingaFrameworkApiChecks))); + $Output += ([string]::Format('Debug Mode => {0}', (Get-IcingaFrameworkDebugMode))); + $Output += ''; + $Output += 'Icinga for Windows Certificate:'; + $Output += ''; + if ($null -eq $IcingaForWindowsCert -Or [string]::IsNullOrEmpty($IcingaForWindowsCert)) { + $Output += 'Not installed'; + } else { + $Output += ([string]::Format('Issuer => {0}', ($IcingaForWindowsCert.Issuer))); + $Output += ([string]::Format('Subject => {0}', ($IcingaForWindowsCert.Subject))); + } + + $Output += ''; + + $Output += (Show-IcingaRegisteredBackgroundDaemons); + $Output += (Show-IcingaRegisteredServiceChecks); + $Output += (Show-IcingaRepository); + + $Output += 'Installed components on this system:'; + $Output += ''; + $Output += [string]::Format('{0} {1} Available', $ComponentHeader, ((Add-IcingaWhiteSpaceToString -Text 'Version' -Length $MaxVersionLength))); + $Output += [string]::Format('{0} {1} ---', $ComponentLine, ((Add-IcingaWhiteSpaceToString -Text '---' -Length $MaxVersionLength))); + + $IcingaInstallation = $IcingaInstallation.GetEnumerator() | Sort-Object -Property Name; + + foreach ($component in $IcingaInstallation) { + $Data = $component.Value; + $LatestVersion = $Data.LatestVersion; + $CurrentVersion = $Data.CurrentVersion; + + if ([string]::IsNullOrEmpty($Data.LockedVersion) -eq $FALSE) { + if ($Data.LockedVersion -eq $Data.CurrentVersion) { + $CurrentVersion = [string]::Format('{0}*', $CurrentVersion); + } else { + $LatestVersion = [string]::Format('{0}*', $Data.LockedVersion); + } + } + + [string]$ComponentName = Add-IcingaWhiteSpaceToString -Text $component.Name -Length $MaxComponentLength; + $Output += [string]::Format('{0} {1} {2}', $ComponentName, (Add-IcingaWhiteSpaceToString -Text $CurrentVersion -Length $MaxVersionLength), $LatestVersion); + } + + $Output += ''; + $Output += 'Available versions flagged with "*" mean that this component is locked to this version'; + + Write-Output $Output; +} +function Read-IcingaServicePackage() +{ + param ( + [string]$File = $null + ); + + if ([string]::IsNullOrEmpty($File) -Or (Test-Path $File) -eq $FALSE) { + Write-IcingaConsoleError 'The provided file "{0}" does not exist' -Objects $File; + return $null; + } + + if ((Test-IcingaAddTypeExist 'System.IO.Compression.FileSystem') -eq $FALSE) { + Add-Type -Assembly 'System.IO.Compression.FileSystem'; + } + + if ([IO.Path]::GetExtension($File) -ne '.zip' -And [IO.Path]::GetExtension($File) -ne '.exe') { + Write-IcingaConsoleError 'Your service binary must be inside a .zip file or directly given on the "-File" argument. Extension "{0}" given.' -Objects ([IO.Path]::GetExtension($File)); + return $null; + } + + [hashtable]$BinaryData = @{ + 'CompanyName' = ''; + 'FileVersion' = ''; + 'ProductVersion' = ''; + 'ComponentName' = 'service'; + } + + try { + $ZipPackage = $null; + + if ([IO.Path]::GetExtension($File) -eq '.zip') { + $ZipPackage = [System.IO.Compression.ZipFile]::OpenRead($File); + + foreach ($entry in $ZipPackage.Entries) { + if ([IO.Path]::GetExtension($entry.FullName) -ne '.exe') { + continue; + } + + $ServiceTempDir = New-IcingaTemporaryDirectory; + $BinaryFile = (Join-Path -Path $ServiceTempDir -ChildPath $entry.Name); + [System.IO.Compression.ZipFileExtensions]::ExtractToFile( + $entry, + (Join-Path -Path $ServiceTempDir -ChildPath $entry.Name), + $TRUE + ); + + $ServiceBin = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($BinaryFile); + + if ($ServiceBin.CompanyName -ne 'Icinga GmbH') { + Remove-Item -Path $ServiceTempDir -Recurse -Force; + continue; + } + + $BinaryData.CompanyName = $ServiceBin.CompanyName; + $BinaryData.ProductVersion = ([version]($ServiceBin.ProductVersion)).ToString(3); + $BinaryData.FileVersion = ([version]($ServiceBin.FileVersion)).ToString(3); + break; + } + + $ZipPackage.Dispose(); + } elseif ([IO.Path]::GetExtension($File) -eq '.exe') { + $ServiceBin = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($File); + + if ($ServiceBin.CompanyName -ne 'Icinga GmbH') { + return $null; + } + + $BinaryData.CompanyName = $ServiceBin.CompanyName; + $BinaryData.ProductVersion = ([version]($ServiceBin.ProductVersion)).ToString(3); + $BinaryData.FileVersion = ([version]($ServiceBin.FileVersion)).ToString(3); + } else { + return $null; + } + + if ([string]::IsNullOrEmpty($BinaryData.ProductVersion)) { + return $null; + } + + return $BinaryData; + } catch { + $ExMsg = $_.Exception.Message; + Write-IcingaConsoleError 'Failed to read package content and/or binary file: {0}' -Objects $ExMsg; + } finally { + if ($null -ne $ZipPackage) { + $ZipPackage.Dispose(); + } + } + + return $null; +} +function New-IcingaRepositoryFile() +{ + param ( + [string]$Path = $null, + [string]$RemotePath = $null, + [string]$Name = '', + [switch]$SnapshotFile = $FALSE + ); + + $RepoFile = 'ifw.repo.json'; + $RepoPath = Join-Path -Path $Path -ChildPath $RepoFile; + + $IcingaRepository = New-Object -TypeName PSObject; + $IcingaRepository | Add-Member -MemberType NoteProperty -Name 'Info' -Value (New-Object -TypeName PSObject); + + # Info + $IcingaRepository.Info | Add-Member -MemberType NoteProperty -Name 'Name' -Value $Name; + $IcingaRepository.Info | Add-Member -MemberType NoteProperty -Name 'LocalSource' -Value $Path; + $IcingaRepository.Info | Add-Member -MemberType NoteProperty -Name 'RemoteSource' -Value $RemotePath; + $IcingaRepository.Info | Add-Member -MemberType NoteProperty -Name 'Created' -Value ((Get-Date).ToUniversalTime().ToString('yyyy\/MM\/dd HH:mm:ss')); + $IcingaRepository.Info | Add-Member -MemberType NoteProperty -Name 'Updated' -Value $IcingaRepository.Info.Created; + $IcingaRepository.Info | Add-Member -MemberType NoteProperty -Name 'RepoHash' -Value $null; + + # Packages + $IcingaRepository | Add-Member -MemberType NoteProperty -Name 'Packages' -Value (New-Object -TypeName PSObject); + + $RepositoryFolder = Get-ChildItem -Path $Path -Recurse -Include '*.msi', '*.zip'; + + New-IcingaProgressStatus -Name 'Updating Repository' -Message ([string]::Format('Update Icinga for Windows repository ({0}). Processed files', $Name)) -MaxValue $RepositoryFolder.Count -Details; + + foreach ($entry in $RepositoryFolder) { + $RepoFilePath = $entry.FullName.Replace($Path, ''); + $FileHash = Get-FileHash -Path $entry.FullName -Algorithm SHA256; + $ComponentName = ''; + + Write-IcingaProgressStatus -Name 'Updating Repository'; + + $IcingaForWindowsPackage = New-Object -TypeName PSObject; + $IcingaForWindowsPackage | Add-Member -MemberType NoteProperty -Name 'Hash' -Value $FileHash.Hash; + $IcingaForWindowsPackage | Add-Member -MemberType NoteProperty -Name 'Location' -Value $RepoFilePath; + $IcingaForWindowsPackage | Add-Member -MemberType NoteProperty -Name 'RelativePath' -Value $TRUE; + + if ([IO.Path]::GetExtension($entry.Name) -eq '.zip') { + $IcingaPackage = Read-IcingaPackageManifest -File $entry.FullName; + $IcingaService = $null; + $Version = $null; + + if ($null -ne $IcingaPackage) { + $PackageVersion = $IcingaPackage.ModuleVersion; + $ComponentName = $IcingaPackage.ComponentName; + } else { + $IcingaService = Read-IcingaServicePackage -File $entry.FullName; + } + if ($null -ne $IcingaService) { + $PackageVersion = $IcingaService.ProductVersion; + $ComponentName = $IcingaService.ComponentName; + } + + [bool]$IsSnapshot = $FALSE; + + if ($entry.FullName.ToLower() -like '*\master.zip' -Or $SnapshotFile) { + $IsSnapshot = $TRUE; + } + + if ([string]::IsNullOrEmpty($ComponentName) -eq $FALSE) { + $IcingaForWindowsPackage | Add-Member -MemberType NoteProperty -Name 'Version' -Value $PackageVersion; + $IcingaForWindowsPackage | Add-Member -MemberType NoteProperty -Name 'Snapshot' -Value $IsSnapshot; + $IcingaForWindowsPackage | Add-Member -MemberType NoteProperty -Name 'Architecture' -Value 'Multi'; + } + } elseif ([IO.Path]::GetExtension($entry.Name) -eq '.msi') { + $IcingaPackage = Read-IcingaMSIMetadata -File $entry.FullName; + + if ([string]::IsNullOrEmpty($IcingaPackage.ProductName) -eq $FALSE -And $IcingaPackage.ProductName -eq 'Icinga 2') { + $IcingaForWindowsPackage | Add-Member -MemberType NoteProperty -Name 'Version' -Value $IcingaPackage.ProductVersion; + $IcingaForWindowsPackage | Add-Member -MemberType NoteProperty -Name 'Snapshot' -Value $IcingaPackage.Snapshot; + $IcingaForWindowsPackage | Add-Member -MemberType NoteProperty -Name 'Architecture' -Value $IcingaPackage.Architecture; + $ComponentName = 'agent'; + } + } + + if ([string]::IsNullOrEmpty($ComponentName)) { + continue; + } + + if (Test-IcingaPowerShellConfigItem -ConfigObject $IcingaRepository.Packages -ConfigKey $ComponentName) { + $IcingaRepository.Packages.$ComponentName += $IcingaForWindowsPackage; + } else { + $IcingaRepository.Packages | Add-Member -MemberType NoteProperty -Name $ComponentName -Value @(); + $IcingaRepository.Packages.$ComponentName += $IcingaForWindowsPackage; + } + + $IcingaRepository.Info.RepoHash = Get-IcingaRepositoryHash -Path $Path; + } + + Complete-IcingaProgressStatus -Name 'Updating Repository'; + + Write-IcingaFileSecure -File $RepoPath -Value (ConvertTo-Json -InputObject $IcingaRepository -Depth 100); + + return $IcingaRepository; +} +function Read-IcingaPackageManifest() +{ + param ( + [string]$File = $null + ); + + if ([string]::IsNullOrEmpty($File) -Or (Test-Path $File) -eq $FALSE) { + Write-IcingaConsoleError 'The provided file "{0}" does not exist' -Objects $File; + return $null; + } + + if ((Test-IcingaAddTypeExist 'System.IO.Compression.FileSystem') -eq $FALSE) { + Add-Type -Assembly 'System.IO.Compression.FileSystem'; + } + + if ([IO.Path]::GetExtension($File) -ne '.zip' -And [IO.Path]::GetExtension($File) -ne '.psd1') { + Write-IcingaConsoleError 'Your Icinga for Windows manifest must be inside a .zip file or directly given on the "-File" argument. Extension "{0}" given.' -Objects ([IO.Path]::GetExtension($File)); + return $null; + } + + try { + $ZipPackage = $null; + + if ([IO.Path]::GetExtension($File) -eq '.zip') { + $ZipPackage = [System.IO.Compression.ZipFile]::OpenRead($File); + $PackageManifest = $null; + $FileName = $null; + + foreach ($entry in $ZipPackage.Entries) { + if ([IO.Path]::GetExtension($entry.FullName) -ne '.psd1') { + continue; + } + + $FileName = $entry.Name.Replace('.psd1', ''); + $FilePath = $entry.FullName.Replace($entry.Name, ''); + $FileStream = $entry.Open(); + $FileReader = New-Object 'System.IO.StreamReader'($FileStream); + $PackageManifestContent = $FileReader.ReadToEnd(); + $FileReader.Dispose(); + + [ScriptBlock]$PackageScript = [ScriptBlock]::Create('return ' + $PackageManifestContent); + $PackageManifest = (& $PackageScript); + + if ($null -eq $PackageManifest -Or $PackageManifest.Count -eq 0) { + continue; + } + + if ($PackageManifest.ContainsKey('PrivateData') -eq $FALSE -Or $PackageManifest.ContainsKey('ModuleVersion') -eq $FALSE) { + continue; + } + + break; + } + + $ZipPackage.Dispose(); + } elseif ([IO.Path]::GetExtension($File) -eq '.psd1') { + $FileName = (Get-Item -Path $File).Name.Replace('.psd1', ''); + $PackageManifestContent = Get-Content -Path $File -Raw; + [ScriptBlock]$PackageScript = [ScriptBlock]::Create('return ' + $PackageManifestContent); + $PackageManifest = (& $PackageScript); + } else { + return $null; + } + + if ($null -eq $PackageManifest) { + return $null; + } + + $PackageManifest.Add('ComponentName', ''); + + if ([string]::IsNullOrEmpty($FileName) -eq $FALSE) { + if ($FileName.Contains('icinga-powershell-*')) { + $PackageManifest.ComponentName = $FileName.Replace('icinga-powershell-', ''); + } else { + if ($PackageManifest.ContainsKey('PrivateData') -And $PackageManifest.PrivateData.ContainsKey('Name') -And $PackageManifest.PrivateData.ContainsKey('Type')) { + if ($PackageManifest.PrivateData.Name -eq 'Icinga for Windows' -And $PackageManifest.PrivateData.Type -eq 'framework') { + $PackageManifest.ComponentName = 'framework'; + } else { + $PackageManifest.ComponentName = ($PackageManifest.PrivateData.Name -Replace 'Windows' -Replace '\W').ToLower(); + } + } + } + } + + return $PackageManifest; + } catch { + $ExMsg = $_.Exception.Message; + Write-IcingaConsoleError 'Failed to read package content and/or manifest file: {0}' -Objects $ExMsg; + } finally { + if ($null -ne $ZipPackage) { + $ZipPackage.Dispose(); + } + } + + return $null; +} +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); +} +function Install-IcingaComponent() +{ + param ( + [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)) { + Write-IcingaConsoleError 'You have to provide a component name'; + return; + } + + if ($KeepRepoErrors -eq $FALSE) { + Clear-IcingaRepositoryErrorState; + } + + # Branch snapshot versions will have '/' inside their name + if ($Name.Contains('/') -And $Snapshot) { + $Name = $Name.Split('/')[0]; + } + + Set-IcingaServiceEnvironment; + Set-IcingaTLSVersion; + + if ($Version.ToLower() -eq 'release' -Or $Version.ToLower() -eq 'latest') { + $Version = $null; + } + + if ($Release -eq $TRUE -And $Snapshot -eq $TRUE) { + Write-IcingaConsoleError 'You can only select either "Release" or "Snapshot" channel for package installation'; + return; + } + + if ($Release -eq $FALSE -And $Snapshot -eq $FALSE) { + $Release = $TRUE; + } + + $LockedVersion = Get-IcingaComponentLock -Name $Name; + + if ($null -ne $LockedVersion) { + $Version = $LockedVersion; + Write-IcingaConsoleNotice 'Component "{0}" is locked to version "{1}"' -Objects $Name, $LockedVersion; + } + + $PackageContent = Get-IcingaRepositoryPackage -Name $Name -Version $Version -Release:$Release -Snapshot:$Snapshot; + $InstallPackage = $PackageContent.Package; + $SourceRepo = $PackageContent.Source; + $RepoName = $PackageContent.Repository; + + if ($PackageContent.HasPackage -eq $FALSE) { + $SearchVersion = 'release'; + if ([string]::IsNullOrEmpty($Version) -eq $FALSE) { + $SearchVersion = $Version; + } + if ($Release) { + Write-IcingaConsoleError 'The component "{0}" was not found on stable channel with version "{1}"' -Objects $Name, $SearchVersion; + return; + } + if ($Snapshot) { + Write-IcingaConsoleError 'The component "{0}" was not found on snapshot channel with version "{1}"' -Objects $Name, $SearchVersion; + return; + } + return; + } + + $FileSource = $InstallPackage.Location; + + if ($InstallPackage.RelativePath -eq $TRUE) { + $FileSource = Join-WebPath -Path ($SourceRepo.Info.RemoteSource.Replace('\', '/')) -ChildPath ($InstallPackage.Location.Replace('\', '/')); + } + + if ($Confirm -eq $FALSE) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt ([string]::Format('Do you want to install component "{0}" from source "{1}" ({2})?', $Name.ToLower(), $RepoName, $FileSource)) -Default 'y').result -ne 1) { + return; + } + } + + $FileName = $FileSource.SubString($FileSource.LastIndexOf('/') + 1, $FileSource.Length - $FileSource.LastIndexOf('/') - 1); + $DownloadDirectory = New-IcingaTemporaryDirectory; + $DownloadDestination = (Join-Path -Path $DownloadDirectory -ChildPath $FileName); + + Write-IcingaConsoleNotice ([string]::Format('Downloading "{0}" from "{1}"', $Name.ToLower(), $FileSource)); + + if ((Invoke-IcingaWebRequest -UseBasicParsing -Uri $FileSource -OutFile $DownloadDestination).HasErrors) { + Write-IcingaConsoleError ([string]::Format('Failed to download "{0}" from "{1}" into "{2}". Starting cleanup process', $Name.ToLower(), $FileSource, $DownloadDestination)); + Start-Sleep -Seconds 2; + Remove-ItemSecure -Path $DownloadDirectory -Recurse -Force -Retries 5 | Out-Null; + + return; + } + + $FileHash = (Get-FileHash -Path $DownloadDestination -Algorithm SHA256).Hash; + + if ([string]::IsNullOrEmpty($InstallPackage.Hash) -eq $FALSE -And (Get-FileHash -Path $DownloadDestination -Algorithm SHA256).Hash -ne $InstallPackage.Hash) { + Write-IcingaConsoleError ([string]::Format('File validation failed. The stored hash inside the repository "{0}" is not matching the file hash "{1}"', $InstallPackage.Hash, $FileHash)); + return; + } + + if ([IO.Path]::GetExtension($FileName) -eq '.zip') { + <# + Handles installation of Icinga for Windows packages and Icinga for Windows service + #> + + Expand-IcingaZipArchive -Path $DownloadDestination -Destination $DownloadDirectory | Out-Null; + Start-Sleep -Seconds 2; + Remove-Item -Path $DownloadDestination -Force; + + $FolderContent = Get-ChildItem -Path $DownloadDirectory -Recurse -Include '*.psd1'; + + <# + Handles installation of Icinga for Windows packages + #> + if ($null -ne $FolderContent -And $FolderContent.Count -ne 0) { + $ManifestFile = $null; + $PackageName = $null; + $PackageRoot = $null; + + foreach ($manifest in $FolderContent) { + $ManifestFile = Read-IcingaPackageManifest -File $manifest.FullName; + + if ($null -ne $ManifestFile) { + $PackageName = $manifest.Name.Replace('.psd1', ''); + $PackageRoot = $manifest.FullName.SubString(0, $manifest.FullName.LastIndexOf('\')); + $PackageRoot = Join-Path -Path $PackageRoot -ChildPath '\*' + break; + } + } + + if ($null -eq $ManifestFile) { + Write-IcingaConsoleError ([string]::Format('Unable to read manifest for package "{0}". Aborting installation', $Name.ToLower())); + Start-Sleep -Seconds 2; + Remove-ItemSecure -Path $DownloadDirectory -Recurse -Force -Retries 5 | Out-Null; + return; + } + + $ComponentFolder = Join-Path -Path (Get-IcingaForWindowsRootPath) -ChildPath $PackageName; + $ModuleData = (Get-Module -ListAvailable -Name $PackageName -ErrorAction SilentlyContinue) | Sort-Object Version -Descending | Select-Object Version -First 1; + [string]$InstallVersion = $null; + $ServiceStatus = $null; + $AgentStatus = $null; + + if ($null -ne $ModuleData) { + [string]$InstallVersion = $ModuleData.Version; + } + + if ($ManifestFile.ModuleVersion -eq $InstallVersion -And $Force -eq $FALSE) { + Write-IcingaConsoleWarning ([string]::Format('The package "{0}" with version "{1}" is already installed. Use "-Force" to re-install the component', $Name.ToLower(), $ManifestFile.ModuleVersion)); + Start-Sleep -Seconds 2; + Remove-ItemSecure -Path $DownloadDirectory -Recurse -Force -Retries 5 | Out-Null; + return; + } + + # These update steps only apply for the framework + if ($Name.ToLower() -eq 'framework') { + Remove-IcingaFrameworkDependencyFile; + $ServiceStatus = (Get-Service 'icingapowershell' -ErrorAction SilentlyContinue).Status; + $AgentStatus = (Get-Service 'icinga2' -ErrorAction SilentlyContinue).Status; + + if ($ServiceStatus -eq 'Running') { + Write-IcingaConsoleNotice 'Stopping Icinga for Windows service'; + Stop-IcingaForWindows; + Start-Sleep -Seconds 1; + } + if ($AgentStatus -eq 'Running') { + Write-IcingaConsoleNotice 'Stopping Icinga Agent service'; + Stop-IcingaService 'icinga2'; + Start-Sleep -Seconds 1; + } + + # Ensure we close the IMC in case we do some updates on the Framework + Set-IcingaForWindowsManagementConsoleUpdating; + } + + if ((Test-Path $ComponentFolder) -eq $FALSE) { + [void](New-Item -ItemType Directory -Path $ComponentFolder -Force); + } + + $ComponentFileContent = Get-ChildItem -Path $ComponentFolder; + + foreach ($entry in $ComponentFileContent) { + if (($entry.Name -eq 'cache' -Or $entry.Name -eq 'config' -Or $entry.Name -eq 'certificate') -And $Name.ToLower() -eq 'framework') { + continue; + } + + [void](Remove-ItemSecure -Path $entry.FullName -Recurse -Force); + } + + Copy-ItemSecure -Path $PackageRoot -Destination $ComponentFolder -Recurse -Force | Out-Null; + + Write-IcingaConsoleNotice 'Installing version "{0}" of component "{1}"' -Objects $ManifestFile.ModuleVersion, $Name.ToLower(); + + Unblock-IcingaPowerShellFiles -Path $ComponentFolder; + + if ($Name.ToLower() -eq 'framework') { + if (Test-IcingaFunction 'Write-IcingaFrameworkCodeCache') { + Write-IcingaFrameworkCodeCache; + } + + Import-Module -Name $ComponentFolder -Force; + Import-Module -Name $ComponentFolder -Force -Global; + + # Apply migration tasks + Use-Icinga; + + if ($ServiceStatus -eq 'Running') { + Write-IcingaConsoleNotice 'Starting Icinga for Windows service'; + Start-IcingaService 'icingapowershell'; + } + if ($AgentStatus -eq 'Running') { + Write-IcingaConsoleNotice 'Starting Icinga Agent service'; + Start-IcingaService 'icinga2'; + } + } else { + try { + Import-Module -Name $ComponentFolder -Force -ErrorAction Stop; + Import-Module -Name $ComponentFolder -Force -Global -ErrorAction Stop; + + Write-IcingaConsoleNotice 'Installation of component "{0}" with version "{1}" was successful. Open a new PowerShell to apply the changes' -Objects $Name.ToLower(), $ManifestFile.ModuleVersion; + } catch { + Write-IcingaConsoleError 'Component "{0}" has been installed with version "{1}", but while importing the component an exception was thrown: {2}' -Objects $Name.ToLower(), $ManifestFile.ModuleVersion, $_.Exception.Message; + } + } + + # This will ensure that Framework functions will always win over third party functions, overwriting functionality + # of the Framework, which might cause problems during installation otherwise + Import-Module (Join-Path -Path (Get-IcingaForWindowsRootPath) -ChildPath 'icinga-powershell-framework') -Force; + Import-Module (Join-Path -Path (Get-IcingaForWindowsRootPath) -ChildPath 'icinga-powershell-framework') -Global -Force; + } else { + <# + Handles installation of Icinga for Windows service + #> + + $FolderContent = Get-ChildItem -Path $DownloadDirectory -Recurse -Include 'icinga-service.exe'; + + if ($Name.ToLower() -eq 'service') { + + $ConfigDirectory = Get-IcingaPowerShellConfig -Path 'Framework.Icinga.IcingaForWindowsService'; + $ConfigUser = Get-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser'; + $ServiceData = Get-IcingaForWindowsServiceData; + $ServiceDirectory = $ServiceData.Directory; + $ServiceUser = $ServiceData.User; + [int]$Success = -1; + + if ([string]::IsNullOrEmpty($ConfigDirectory) -eq $FALSE) { + $ServiceDirectory = $ConfigDirectory; + } + + if ([string]::IsNullOrEmpty($ConfigUser) -eq $FALSE) { + $ServiceUser = $ConfigUser; + } else { + Set-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser' -Value $ServiceUser; + } + + foreach ($binary in $FolderContent) { + + if ((Test-IcingaZipBinaryChecksum -Path $binary.FullName) -eq $FALSE) { + Write-IcingaConsoleError 'The checksum for the given service binary does not match'; + continue; + } + + if ((Test-Path $ServiceDirectory) -eq $FALSE) { + [void](New-Item -ItemType Directory -Path $ServiceDirectory -Force); + } + + $UpdateBin = Join-Path -Path $ServiceDirectory -ChildPath 'icinga-service.exe.update'; + $ServiceBin = Join-Path -Path $ServiceDirectory -ChildPath 'icinga-service.exe'; + + # Service is already installed + if (Test-Path $ServiceBin) { + $InstalledService = Read-IcingaServicePackage -File $ServiceBin; + $NewService = Read-IcingaServicePackage -File $binary.FullName; + + if ($InstalledService.ProductVersion -eq $NewService.ProductVersion -And $null -ne $InstalledService -And $null -ne $NewService -And $Force -eq $FALSE) { + $Success = 0; + break; + } + } + + Write-IcingaConsoleNotice 'Installing component "service" into "{0}"' -Objects $ServiceDirectory; + + Copy-ItemSecure -Path $binary.FullName -Destination $UpdateBin -Force | Out-Null; + + [void](Install-IcingaForWindowsService -Path $ServiceBin -User $ServiceUser -Password (Get-IcingaInternalPowerShellServicePassword)); + Update-IcingaServiceUser; + Set-IcingaInternalPowerShellServicePassword -Password $null; + $Success = 1; + break; + } + + if ($Success -eq 0) { + Write-IcingaConsoleWarning ([string]::Format('The package "service" with version "{0}" is already installed. Use "-Force" to re-install the component', $InstalledService.ProductVersion)); + Remove-ItemSecure -Path $DownloadDirectory -Recurse -Force -Retries 5 | Out-Null; + + return; + } + + if ($Success -eq 1) { + Remove-ItemSecure -Path $DownloadDirectory -Recurse -Force -Retries 5 | Out-Null; + Write-IcingaConsoleNotice 'Installation of component "service" was successful'; + + return; + } + + Write-IcingaConsoleError 'Failed to install component "service". Either the package did not include a service binary or the checksum of the binary did not match'; + Start-Sleep -Seconds 2; + Remove-ItemSecure -Path $DownloadDirectory -Recurse -Force -Retries 5 | Out-Null; + return; + } else { + Write-IcingaConsoleError 'There was no manifest file found inside the package'; + Remove-ItemSecure -Path $DownloadDirectory -Recurse -Force; + return; + } + } + } elseif ([IO.Path]::GetExtension($FileName) -eq '.msi') { + + <# + Handles installation of Icinga Agent MSI Packages + #> + + $IcingaData = Get-IcingaAgentInstallation; + $InstalledVersion = Get-IcingaAgentVersion; + $InstallTarget = $IcingaData.RootDir; + $InstallDir = Get-IcingaPowerShellConfig -Path 'Framework.Icinga.AgentLocation'; + $ConfigUser = Get-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser'; + $ServiceUser = $IcingaData.User; + + if ([string]::IsNullOrEmpty($InstallDir) -eq $FALSE) { + if ((Test-Path $InstallDir) -eq $FALSE) { + [void](New-Item -Path $InstallDir -ItemType Directory -Force); + } + $InstallTarget = $InstallDir; + } + + if ([string]::IsNullOrEmpty($ConfigUser) -eq $FALSE) { + $ServiceUser = $ConfigUser; + } else { + Set-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser' -Value $ServiceUser; + } + + Set-IcingaPowerShellConfig -Path 'Framework.Icinga.AgentLocation' -Value $InstallTarget; + + [string]$InstallFolderMsg = $InstallTarget; + + if ([string]::IsNullOrEmpty($InstallTarget) -eq $FALSE) { + $InstallTarget = [string]::Format(' INSTALL_ROOT="{0}"', $InstallTarget); + } else { + $InstallTarget = ''; + if ($IcingaData.Architecture -eq 'x86') { + $InstallFolderMsg = Join-Path -Path ${env:ProgramFiles(x86)} -ChildPath 'icinga2'; + } else { + $InstallFolderMsg = Join-Path -Path $env:ProgramFiles -ChildPath 'icinga2'; + } + } + + $MSIData = & powershell.exe -Command { + Use-Icinga -Minimal; + + $DownloadDestination = $args[0]; + return (Read-IcingaMSIMetadata -File $DownloadDestination); + } -Args $DownloadDestination; + + if ($InstalledVersion.Full -eq $MSIData.ProductVersion -And $Force -eq $FALSE) { + Write-IcingaConsoleWarning 'The package "agent" with version "{0}" is already installed. Use "-Force" to re-install the component' -Objects $InstalledVersion.Full; + Remove-ItemSecure -Path $DownloadDirectory -Recurse -Force -Retries 5 | Out-Null; + + return; + } + + Write-IcingaConsoleNotice 'Installing component "agent" with version "{0}" into "{1}"' -Objects $MSIData.ProductVersion, $InstallFolderMsg; + + if ($IcingaData.Installed) { + if ((Uninstall-IcingaAgent) -eq $FALSE) { + return; + } + } + + $InstallProcess = & powershell.exe -Command { + Use-Icinga -Minimal; + + $DownloadDestination = $args[0]; + $InstallTarget = $args[1]; + $InstallProcess = Start-IcingaProcess -Executable 'MsiExec.exe' -Arguments ([string]::Format('/quiet /norestart /i "{0}" {1}', $DownloadDestination, $InstallTarget)) -FlushNewLines; + + Start-Sleep -Seconds 2; + Optimize-IcingaForWindowsMemory; + + return $InstallProcess; + } -Args $DownloadDestination, $InstallTarget; + + if ($InstallProcess.ExitCode -ne 0) { + Write-IcingaConsoleError -Message 'Failed to install component "agent": {0}{1}' -Objects $InstallProcess.Message, $InstallProcess.Error; + return; + } + + $Global:Icinga.Protected.Environment.'Icinga Service'.Present = $TRUE; + $Global:Icinga.Protected.Environment.'Icinga Service'.User = $ServiceUser; + + Reset-IcingaAgentConfigFile; + Move-IcingaAgentDefaultConfig; + + Set-IcingaServiceUser -User $ServiceUser -SetPermission | Out-Null; + Update-IcingaServiceUser; + + Write-IcingaConsoleNotice 'Installation of component "agent" with version "{0}" was successful.' -Objects $MSIData.ProductVersion; + } else { + Write-IcingaConsoleError ([string]::Format('Unsupported file extension "{0}" found for package "{1}". Aborting installation', ([IO.Path]::GetExtension($FileName)), $Name.ToLower())); + } + + Remove-ItemSecure -Path $DownloadDirectory -Recurse -Force -Retries 5 | Out-Null; +} +function Get-IcingaForWindowsServiceData() +{ + $IcingaForWindowsService = $Global:Icinga.Protected.Environment.'PowerShell Service'; + + [hashtable]$ServiceData = @{ + 'Binary' = ''; + 'Directory' = ''; + 'FullPath' = ''; + 'User' = ''; + } + + if ($null -ne $IcingaForWindowsService -And ([string]::IsNullOrEmpty($IcingaForWindowsService.ServicePath)) -eq $FALSE) { + $ServicePath = $IcingaForWindowsService.ServicePath; + $ServicePath = $ServicePath.SubString(0, $ServicePath.IndexOf('.exe') + 4); + $ServicePath = $ServicePath.Replace('\"', '').Replace('"', ''); + $ServiceData.Binary = $ServicePath.SubString($ServicePath.LastIndexOf('\') + 1, $ServicePath.Length - $ServicePath.LastIndexOf('\') - 1); + $ServiceData.FullPath = $ServicePath; + $ServiceData.Directory = $ServicePath.Substring(0, $ServicePath.LastIndexOf('\') + 1); + $ServiceData.User = $IcingaForWindowsService.User; + + return $ServiceData; + } + + $ServiceData.Directory = (Join-Path -Path $env:ProgramFiles -ChildPath 'icinga-framework-service'); + $ServiceData.User = 'NT Authority\NetworkService'; + + return $ServiceData; +} +function Get-IcingaRepositoryPackage() +{ + param ( + [string]$Name, + [string]$Version = $null, + [switch]$Release = $FALSE, + [switch]$Snapshot = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to provide a component name'; + + return @{ + 'HasPackage' = $FALSE; + 'Package' = $null; + 'Source' = $null; + 'Repository' = $null; + }; + } + + if ($Name.ToLower() -eq 'kickstart') { + return @{ + 'HasPackage' = $FALSE; + 'Package' = $null; + 'Source' = $null; + 'Repository' = $null; + }; + } + + $Repositories = Get-IcingaRepositories -ExcludeDisabled; + [Version]$LatestVersion = $null; + $InstallPackage = $null; + $SourceRepo = $null; + $RepoName = $null; + [bool]$HasRepo = $FALSE; + [bool]$Isx86 = [bool]([IntPtr]::Size -eq 4); + + foreach ($entry in $Repositories) { + $RepoContent = Read-IcingaRepositoryFile -Name $entry.Name; + [bool]$FoundPackage = $FALSE; + + if ($null -eq $RepoContent) { + continue; + } + + if ((Test-IcingaPowerShellConfigItem -ConfigObject $RepoContent -ConfigKey 'Packages') -eq $FALSE) { + continue; + } + + if ((Test-IcingaPowerShellConfigItem -ConfigObject $RepoContent.Packages -ConfigKey $Name) -eq $FALSE) { + continue; + } + + foreach ($package in $RepoContent.Packages.$Name) { + + if ($Snapshot -And $package.Snapshot -eq $FALSE) { + continue; + } + + if ($Release -And $package.Snapshot -eq $TRUE) { + continue; + } + + if ($package.Architecture -ne 'Multi' -And $package.Architecture -eq 'x86' -And $Isx86 -eq $FALSE) { + continue; + } + + if ($Snapshot -And [string]::IsNullOrEmpty($Version) -eq $FALSE -And (Test-Numeric $package.Version.Replace('.', '')) -eq $FALSE -And (Test-Numeric $Version) -eq $FALSE -And $package.Version -eq $Version) { + $InstallPackage = $package; + $HasRepo = $TRUE; + $SourceRepo = $RepoContent; + $RepoName = $entry.Name; + break; + } + + if ((Test-Numeric $package.Version.Replace('.', '')) -eq $FALSE -Or ((Test-Numeric $Version.Replace('.', '')) -eq $FALSE -And [string]::IsNullOrEmpty($Version) -eq $FALSE) ) { + continue; + } + + if (([string]::IsNullOrEmpty($Version) -And ($null -eq $LatestVersion -Or $LatestVersion -lt $package.Version))) { + [Version]$LatestVersion = [Version]$package.Version; + $InstallPackage = $package; + $HasRepo = $TRUE; + $SourceRepo = $RepoContent; + $RepoName = $entry.Name; + continue; + } + + if ([string]::IsNullOrEmpty($Version) -eq $FALSE -And [version]$package.Version -eq [version]$Version) { + $InstallPackage = $package; + $FoundPackage = $TRUE; + $HasRepo = $TRUE; + $SourceRepo = $RepoContent; + $RepoName = $entry.Name; + break; + } + } + + if ($FoundPackage) { + break; + } + } + + return @{ + 'HasPackage' = $HasRepo; + 'Package' = $InstallPackage; + 'Source' = $SourceRepo; + 'Repository' = $RepoName; + }; +} +function Disable-IcingaRepository() +{ + param ( + [string]$Name = $null + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to provide a name for the repository'; + return; + } + + $Name = $Name.Replace('.', '-'); + + $CurrentRepositories = Get-IcingaPowerShellConfig -Path ([string]::Format('Framework.Repository.Repositories.{0}', $Name)); + + if ($null -eq $CurrentRepositories) { + Write-IcingaConsoleError 'A repository with the name "{0}" is not configured' -Objects $Name; + return; + } + + if ($CurrentRepositories.Enabled -eq $FALSE) { + Write-IcingaConsoleNotice 'The repository "{0}" is already disabled' -Objects $Name; + return; + } + + Set-IcingaPowerShellConfig -Path ([string]::Format('Framework.Repository.Repositories.{0}.Enabled', $Name)) -Value $FALSE; + + Write-IcingaConsoleNotice 'The repository "{0}" was successfully disabled' -Objects $Name; +} +function Add-IcingaRepository() +{ + param ( + [string]$Name = $null, + [string]$RemotePath = $null, + [switch]$Force = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to provide a name for the repository'; + return; + } + + $Name = $Name.Replace('.', '-'); + + if ([string]::IsNullOrEmpty($RemotePath)) { + Write-IcingaConsoleError 'You have to provide a remote path for the repository'; + return; + } + + $CurrentRepositories = Get-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories'; + + if ($null -eq $CurrentRepositories) { + $CurrentRepositories = New-Object -TypeName PSObject; + } + + $RepoExists = Test-IcingaPowerShellConfigItem -ConfigObject $CurrentRepositories -ConfigKey $Name; + + if ($RepoExists -And $Force -eq $FALSE) { + Write-IcingaConsoleError 'A repository with the given name "{0}" does already exist. Use "-Force" to overwrite the current repository' -Objects $Name; + return; + } elseif ($RepoExists -And $Force) { + Write-IcingaConsoleNotice 'Repository "{0}" is already registered. Forcing override of data.' -Objects $Name; + + foreach ($entry in $CurrentRepositories.PSObject.Properties) { + if ($Name.ToLower() -eq $entry.Name.ToLower()) { + $entry.Value.RemotePath = $RemotePath; + break; + } + } + + Set-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories' -Value $CurrentRepositories; + + return; + } + + [array]$RepoCount = $CurrentRepositories.PSObject.Properties.Count; + + $CurrentRepositories | Add-Member -MemberType NoteProperty -Name $Name -Value (New-Object -TypeName PSObject); + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'LocalPath' -Value $null; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'RemotePath' -Value $RemotePath; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'CloneSource' -Value $null; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'UseSCP' -Value $FALSE; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'Order' -Value $RepoCount.Count; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'Enabled' -Value $True; + + Set-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories' -Value $CurrentRepositories; + Push-IcingaRepository -Name $Name -Silent; + + Write-IcingaConsoleNotice 'Remote repository "{0}" was successfully added' -Objects $Name; +} +function Lock-IcingaComponent() +{ + param ( + [string]$Name = $null, + [string]$Version = $null + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to specify the component to lock'; + return; + } + + $Name = $Name.ToLower(); + if ([string]::IsNullOrEmpty($Version)) { + if ($Name -eq 'agent') { + $Version = (Get-IcingaAgentVersion).Full; + } else { + $ModuleData = Get-Module -ListAvailable -Name ([string]::Format('icinga-powershell-{0}', $Name)) -ErrorAction SilentlyContinue; + + if ($null -eq $ModuleData) { + $ModuleData = Get-Module -ListAvailable -Name "*$Name*" -ErrorAction SilentlyContinue; + } + + if ($null -ne $ModuleData) { + $Version = $ModuleData.Version.ToString(); + $Name = (Read-IcingaPackageManifest -File $ModuleData.Path).ComponentName; + } + } + } + + if ([string]::IsNullOrEmpty($Version)) { + Write-IcingaConsoleError 'Pinning the current version of component "{0}" is not possible, as it seems to be not installed. Please install the component first or manually specify version with "-Version"'; + return; + } + + $LockedComponents = Get-IcingaPowerShellConfig -Path 'Framework.Repository.ComponentLock'; + + if ($null -eq $LockedComponents) { + $LockedComponents = New-Object -TypeName PSObject; + } + + if (Test-IcingaPowerShellConfigItem -ConfigObject $LockedComponents -ConfigKey $Name) { + $LockedComponents.$Name = $Version; + } else { + $LockedComponents | Add-Member -MemberType NoteProperty -Name $Name -Value $Version; + } + + Write-IcingaConsoleNotice 'Locking of component "{0}" to version "{1}" successful. You can release the lock with "Unlock-IcingaComponent -Name {2}{0}{2}"' -Objects $Name, $Version, "'"; + + Set-IcingaPowerShellConfig -Path 'Framework.Repository.ComponentLock' -Value $LockedComponents; +} +function Get-IcingaComponentLock() +{ + param ( + [string]$Name = $null + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to specify the component to get the lock version'; + return; + } + + $LockedComponents = Get-IcingaPowerShellConfig -Path 'Framework.Repository.ComponentLock'; + + if ($null -eq $LockedComponents) { + return $null; + } + + if (Test-IcingaPowerShellConfigItem -ConfigObject $LockedComponents -ConfigKey $Name) { + return $LockedComponents.$Name; + } + + return $null; +} +function Enable-IcingaRepository() +{ + param ( + [string]$Name = $null + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to provide a name for the repository'; + return; + } + + $Name = $Name.Replace('.', '-'); + + $CurrentRepositories = Get-IcingaPowerShellConfig -Path ([string]::Format('Framework.Repository.Repositories.{0}', $Name)); + + if ($null -eq $CurrentRepositories) { + Write-IcingaConsoleError 'A repository with the name "{0}" is not configured' -Objects $Name; + return; + } + + if ($CurrentRepositories.Enabled -eq $TRUE) { + Write-IcingaConsoleNotice 'The repository "{0}" is already enabled' -Objects $Name; + return; + } + + Set-IcingaPowerShellConfig -Path ([string]::Format('Framework.Repository.Repositories.{0}.Enabled', $Name)) -Value $TRUE; + + Write-IcingaConsoleNotice 'The repository "{0}" was successfully enabled' -Objects $Name; +} +function Test-IcingaValidJSON() +{ + param ( + [string]$String = '', + [string]$File = '' + ); + + if ([string]::IsNullOrEmpty($File) -eq $FALSE) { + if ((Test-Path $File) -eq $FALSE) { + return $FALSE; + } + + $String = Get-Content -Path $File -Raw; + } + try { + # Test the conversion to JSON and return false on failure and true on success + ConvertFrom-Json -InputObject $String -ErrorAction Stop | Out-Null; + } catch { + return $FALSE; + } + + return $TRUE; +} +function Unlock-IcingaComponent() +{ + param ( + [string]$Name = $null + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to specify the component to unlock'; + return; + } + + $LockedComponents = Get-IcingaPowerShellConfig -Path 'Framework.Repository.ComponentLock'; + + if ($null -eq $LockedComponents) { + Write-IcingaConsoleNotice 'You have currently no components which are locked configured'; + return; + } + + if (Test-IcingaPowerShellConfigItem -ConfigObject $LockedComponents -ConfigKey $Name) { + Remove-IcingaPowerShellConfig -Path ([string]::Format('Framework.Repository.ComponentLock.{0}', $Name)); + Write-IcingaConsoleNotice 'Unlocking of component "{0}" was successful.' -Objects $Name; + } else { + Write-IcingaConsoleNotice 'The component "{0}" is not locked on this system' -Objects $Name; + } +} +function New-IcingaRepository() +{ + param ( + [string]$Name = $null, + [string]$Path = $null, + [string]$RemotePath = $null, + [switch]$Force = $FALSE, + [switch]$Snapshot = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to provide a name for the repository'; + return; + } + + $Name = $Name.Replace('.', '-'); + + if ([string]::IsNullOrEmpty($Path) -Or (Test-Path $Path) -eq $FALSE) { + Write-IcingaConsoleError 'The provided path "{0}" does not exist' -Objects $Path; + return; + } + + if ([string]::IsNullOrEmpty($RemotePath)) { + Write-IcingaConsoleWarning 'No explicit remote path has been defined. Using local path "{0}" as remote path' -Objects $Path; + $RemotePath = $Path; + } + + $CurrentRepositories = Get-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories'; + + if ($null -eq $CurrentRepositories) { + $CurrentRepositories = New-Object -TypeName PSObject; + } + + if (Test-IcingaPowerShellConfigItem -ConfigObject $CurrentRepositories -ConfigKey $Name) { + Write-IcingaConsoleError 'A repository with the given name "{0}" does already exist. Use "Update-IcingaRepository -Name {1}{0}{1}" to update it.' -Objects $Name, "'"; + return; + } + + $IcingaRepository = New-IcingaRepositoryFile -Path $Path -RemotePath $RemotePath -Name $Name -SnapshotFile:$Snapshot; + + [array]$ConfigCount = $IcingaRepository.Packages.PSObject.Properties.Count; + + if ($ConfigCount.Count -eq 0) { + Write-IcingaConsoleWarning 'Created empty repository at location "{0}"' -Objects $Path; + } + [array]$RepoCount = $CurrentRepositories.PSObject.Properties.Count; + + $CurrentRepositories | Add-Member -MemberType NoteProperty -Name $Name -Value (New-Object -TypeName PSObject); + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'LocalPath' -Value $Path; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'RemotePath' -Value $RemotePath; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'CloneSource' -Value $null; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'UseSCP' -Value $FALSE; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'Order' -Value $RepoCount.Count; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'Enabled' -Value $True; + + Set-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories' -Value $CurrentRepositories; +} +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 = @{ }; +} +function Get-IcingaRepositoryHash() +{ + param ( + [string]$Path + ); + + if ([string]::IsNullOrEmpty($Path) -Or (Test-Path $Path) -eq $FALSE) { + Write-IcingaConsoleError 'The provided path "{0}" does not exist' -Objects $Path; + return; + } + + $RepositoryFolder = Get-ChildItem -Path $Path -Recurse -Include '*.zip', '*.msi'; + $FileHashes = New-Object -TypeName 'System.Text.StringBuilder'; + + foreach ($entry in $RepositoryFolder) { + $FileHash = (Get-FileHash -Path $entry.FullName -Algorithm SHA256).Hash; + + if ([string]::IsNullOrEmpty($FileHash)) { + continue; + } + + if ($FileHashes.Length -ne 0) { + $FileHashes.Append('+') | Out-Null; + } + + $FileHashes.Append($FileHash) | Out-Null; + } + + $HashAlgorithm = [System.Security.Cryptography.HashAlgorithm]::Create('SHA256'); + $BinaryHash = $HashAlgorithm.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($FileHashes.ToString())) + + return [System.BitConverter]::ToString($BinaryHash).Replace('-', ''); +} +function Read-IcingaMSIMetadata() +{ + param ( + [string]$File = $null + ); + + if ([string]::IsNullOrEmpty($File) -Or (Test-Path $File) -eq $FALSE) { + Write-IcingaConsoleError 'The provided file "{0}" does not exist' -Objects $File; + return $null; + } + + if ([IO.Path]::GetExtension($File) -ne '.msi') { + Write-IcingaConsoleError 'This Cmdlet is only supporting files with .msi extension. Extension "{0}" given.' -Objects ([IO.Path]::GetExtension($File)); + return $null; + } + + $AgentFile = Get-Item $File; + $MSIPackageData = @{ + 'ProductCode' = ''; + 'ProductVersion' = ''; + 'ProductName' = ''; + } + + [array]$MSIObjects = $MSIPackageData.Keys; + + try { + $InstallerInstance = New-Object -ComObject 'WindowsInstaller.Installer'; + #$MSIPackage = $InstallerInstance.OpenDatabase($File, 0); # Not Working on Windows 2012 R2 + $MSIPackage = $InstallerInstance.GetType().InvokeMember('OpenDatabase', 'InvokeMethod', $Null, $InstallerInstance, @($File, 0)); + + foreach ($PackageInfo in $MSIObjects) { + $MSIQuery = [string]::Format( + "SELECT `Value` FROM `Property` WHERE `Property` = '{0}'", + $PackageInfo + ); + #$MSIDb = $MSIPackage.OpenView($MSIQuery); # Not Working on Windows 2012 R2 + $MSIDb = $MSIPackage.GetType().InvokeMember('OpenView', 'InvokeMethod', $Null, $MSIPackage, $MSIQuery); + + if ($null -eq $MSIDb) { + continue; + } + + #$MSIDb.Execute(); # Not Working on Windows 2012 R2 + $MSIDb.GetType().InvokeMember('Execute', 'InvokeMethod', $Null, $MSIDb, $Null); + #$MSITable = $MSIDb.Fetch(); # Not Working on Windows 2012 R2 + $MSITable = $MSIDb.GetType().InvokeMember('Fetch' , 'InvokeMethod', $Null, $MSIDb, $Null); + + if ($null -eq $MSITable) { + continue; + } + + $MSIPackageData[$PackageInfo] = $MSITable.GetType().InvokeMember('StringData', 'GetProperty', $null, $MSITable, 1); + + #$MSIDb.Close(); # Not Working on Windows 2012 R2 + $MSIDb.GetType().InvokeMember('Close', 'InvokeMethod', $null, $MSIDb, $null); + [void]([System.Runtime.InteropServices.Marshal]::ReleaseComObject($MSIDb)); + $MSIDb = $null; + } + + [void]([System.Runtime.InteropServices.Marshal]::ReleaseComObject($MSIPackage)); + [void]([System.Runtime.InteropServices.Marshal]::ReleaseComObject($InstallerInstance)); + $MSIPackage = $null; + $InstallerInstance = $null; + + if ($AgentFile.Name.Contains('x86_64')) { + $MSIPackageData.Add('Architecture', 'x64') + } else { + $MSIPackageData.Add('Architecture', 'x86') + } + + [Version]$PackageVersion = $MSIPackageData.ProductVersion; + if ($PackageVersion.Revision -eq -1) { + $MSIPackageData.Add('Snapshot', $False); + } else { + $MSIPackageData.Add('Snapshot', $True); + } + + return $MSIPackageData; + } catch { + Write-IcingaConsoleError 'Failed to query MSI package information for package "{0}". Exception: {1}' -Objects $File, $_.Exception.Message; + } + + return $null; +} +function Sync-IcingaRepository() +{ + param ( + [string]$Name = $null, + [string]$Path = $null, + [string]$RemotePath = $null, + [string]$Source = $null, + [switch]$UseSCP = $FALSE, + [switch]$Force = $FALSE, + [switch]$ForceTrust = $FALSE, + [switch]$SkipSCPMkdir = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to provide a name for the repository'; + return; + } + + $Name = $Name.Replace('.', '-'); + + if ($UseSCP -And $null -eq (Get-Command 'scp' -ErrorAction SilentlyContinue) -And $null -eq (Get-Command 'ssh' -ErrorAction SilentlyContinue)) { + Write-IcingaConsoleWarning 'You cannot use SCP on this system, as SCP and/or SSH seem not to be installed'; + return; + } + + if ($UseSCP -And $Path.Contains(':') -eq $FALSE -And $Path.Contains('@') -eq $FALSE) { + Write-IcingaConsoleWarning 'You have to add host and username to your "-Path" argument. Example: "icinga@icinga.example.com:/var/www/icingarepo/" '; + return; + } + + if ([string]::IsNullOrEmpty($Path) -Or (Test-Path $Path) -eq $FALSE -And $UseSCP -eq $FALSE) { + Write-IcingaConsoleWarning 'The provided path "{0}" does not exist and will be created' -Objects $Path; + } + + if ([string]::IsNullOrEmpty($RemotePath)) { + Write-IcingaConsoleWarning 'No explicit remote path has been defined. Using local path "{0}" as remote path' -Objects $Path; + $RemotePath = $Path; + } + + if ([string]::IsNullOrEmpty($Source)) { + Write-IcingaConsoleError 'You have to specify a source to sync from'; + return; + } + + $CurrentRepositories = Get-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories'; + + if ($null -eq $CurrentRepositories) { + $CurrentRepositories = New-Object -TypeName PSObject; + } + + if ((Test-IcingaPowerShellConfigItem -ConfigObject $CurrentRepositories -ConfigKey $Name) -And $Force -eq $FALSE) { + Write-IcingaConsoleError 'A repository with the given name "{0}" does already exist. Use "Update-IcingaRepository -Name {1}{0}{1}" to update it.' -Objects $Name, "'"; + return; + } + + if ((Test-Path $Path) -eq $FALSE -And $UseSCP -eq $FALSE) { + $FolderCreated = New-Item -Path $Path -ItemType Directory -Force -ErrorAction SilentlyContinue; + + if ($null -eq $FolderCreated) { + Write-IcingaConsoleError 'Unable to create repository folder at location "{0}". Please verify that you have permissions to write into the location and try again or create the folder manually' -Objects $Path; + return; + } + } + + $RepoFile = $null; + $SSHAuth = $null; + $RemovePath = $null; + + if (Test-Path $Source) { + $CopySource = Join-Path -Path $Source -ChildPath '\*'; + } else { + $CopySource = $Source; + } + + if ($UseSCP -eq $FALSE) { + $Path = Join-Path -Path $Path -ChildPath '\'; + $RemovePath = Join-Path -Path $Path -ChildPath '\*'; + } else { + $SSHIndex = $Path.IndexOf(':'); + $SSHAuth = $Path.Substring(0, $SSHIndex); + $Path = $Path.Substring($SSHIndex + 1, $Path.Length - $SSHIndex - 1); + + if ($Path[-1] -eq '/') { + $RemovePath = [string]::Format('{0}*', $Path); + } else { + $RemovePath = [string]::Format('{0}/*', $Path); + } + } + + # All cloning will be done into a local file first + $TmpDir = New-IcingaTemporaryDirectory; + $RepoFile = (Join-Path -Path $TmpDir -ChildPath 'ifw.repo.json'); + [bool]$HasNonRelative = $FALSE; + + if (Test-Path $CopySource) { # Sync source is local path + $Success = Copy-ItemSecure -Path $CopySource -Destination $TmpDir -Recurse -Force; + } else { # Sync Source is web path + $ProgressPreference = "SilentlyContinue"; + + $Result = Invoke-IcingaWebRequest -UseBasicParsing -Uri $Source -OutFile $RepoFile; + + if ($Result.HasError -Or (Test-IcingaValidJSON -File $RepoFile) -eq $FALSE) { + + $Result = Invoke-IcingaWebRequest -UseBasicParsing -Uri (Join-WebPath -Path $Source -ChildPath 'ifw.repo.json') -OutFile $RepoFile; + + if ($Result.HasError -Or (Test-IcingaValidJSON -File $RepoFile) -eq $FALSE) { + Write-IcingaConsoleError 'Unable to download repository file from "{0}". Exception: "{1}"' -Objects $Source, $_.Exception.Message; + $Success = Remove-Item -Path $TmpDir -Recurse -Force; + return; + } + } + + $RepoContent = Get-Content -Path $RepoFile -Raw; + $JsonRepo = ConvertFrom-Json -InputObject $RepoContent; + + foreach ($component in $JsonRepo.Packages.PSObject.Properties.Name) { + $IfWPackage = $JsonRepo.Packages.$component + + New-IcingaProgressStatus -Name 'Sync Repository' -Message ([string]::Format('Syncing Icinga for Windows repository {0} ({1}). Downloaded {2} packages', $Name, $JsonRepo.Info.RemoteSource, $component)) -MaxValue $IfWPackage.Count -Details; + + foreach ($package in $IfWPackage) { + $DownloadLink = $package.Location; + $TargetLocation = $TmpDir; + + Write-IcingaProgressStatus -Name 'Sync Repository'; + + if ($package.RelativePath -eq $TRUE) { + $DownloadLink = Join-WebPath -Path $JsonRepo.Info.RemoteSource -ChildPath $package.Location; + $TargetLocation = Join-Path -Path $TmpDir -ChildPath $package.Location; + + [void]( + New-Item ` + -ItemType Directory ` + -Path ( + $TargetLocation.SubString( + 0, + $TargetLocation.LastIndexOf('\') + ) + ) ` + -Force + ); + } else { + $HasNonRelative = $TRUE; + $FileName = $package.Location.Replace('/', '\'); + $Index = $FileName.LastIndexOf('\'); + $FileName = $FileName.SubString($Index, $FileName.Length - $Index); + $TargetLocation = Join-Path -Path $TmpDir -ChildPath $component; + [void](New-Item -ItemType Directory -Path $TargetLocation -Force); + $TargetLocation = Join-Path -Path $TargetLocation -ChildPath $FileName; + } + + try { + Write-IcingaConsoleDebug 'Syncing repository component "{0}" as file "{1}" into temp directory' -Objects $component, $package.Location; + Invoke-IcingaWebRequest -UseBasicParsing -Uri $DownloadLink -OutFile $TargetLocation | Out-Null; + } catch { + Write-IcingaConsoleError 'Failed to download repository component "{0}". Exception: "{1}"' -Objects $DownloadLink, $_.Exception.Message; + continue; + } + } + + Complete-IcingaProgressStatus -Name 'Sync Repository'; + } + } + + [string]$CopySource = [string]::Format('{0}\*', $TmpDir); + + if ((Test-Path $RepoFile) -eq $FALSE) { + Write-IcingaConsoleError 'The files from this repository were cloned but no repository file was found. Deleting temporary files'; + $Success = Remove-Item -Path $TmpDir -Recurse -Force; + return; + } + + $RepoContent = Get-Content -Path $RepoFile -Raw; + $JsonRepo = ConvertFrom-Json -InputObject $RepoContent; + + if ($null -eq $JsonRepo) { + Write-IcingaConsoleError 'The repository file was found but it is either damaged or empty. Deleting temporary files'; + $Success = Remove-Item -Path $TmpDir -Recurse -Force; + return; + } + + $EnableRepo = $TRUE; + + if ($ForceTrust -eq $FALSE -And $UseSCP -eq $FALSE) { + if ($null -eq $JsonRepo.Info.RepoHash -Or [string]::IsNullOrEmpty($JsonRepo.Info.RepoHash)) { + Write-IcingaConsoleWarning 'The cloned repository file hash cannot be verified, as it is not present inside the repository file. The repository will be added, but disabled for security reasons. Review the content first and ensure you trust the source before enabling it.'; + $EnableRepo = $FALSE; + } elseif ($JsonRepo.Info.RepoHash -ne (Get-IcingaRepositoryHash -Path $TmpDir)) { + $Success = Remove-Item -Path $TmpDir -Recurse -Force; + Write-IcingaConsoleError 'The repository hash for the cloned repository is not matching the file hash of the files inside. Removing repository data'; + return; + } + } + + if ($HasNonRelative) { + [void](New-IcingaRepositoryFile -Path $TmpDir -RemotePath $RemotePath -Name $Name); + $RepoContent = Get-Content -Path $RepoFile -Raw; + $JsonRepo = ConvertFrom-Json -InputObject $RepoContent; + Start-Sleep -Seconds 2; + } + + if ((Test-PSCustomObjectMember -PSObject $JsonRepo.Info -Name 'Name') -eq $FALSE) { + $JsonRepo.Info | Add-Member -MemberType NoteProperty -Name 'Name' -Value $Name; + } else { + $JsonRepo.Info.Name = $Name; + } + + $JsonRepo.Info.RepoHash = Get-IcingaRepositoryHash -Path $TmpDir; + $JsonRepo.Info.LocalSource = $Path; + $JsonRepo.Info.RemoteSource = $RemotePath; + $JsonRepo.Info.Updated = ((Get-Date).ToUniversalTime().ToString('yyyy\/MM\/dd HH:mm:ss')); + + Write-IcingaFileSecure -File $RepoFile -Value (ConvertTo-Json -InputObject $JsonRepo -Depth 100); + + if ($UseSCP -eq $FALSE) { # Windows target + $Success = Remove-Item -Path $RemovePath -Recurse -Force; + $Success = Copy-ItemSecure -Path $CopySource -Destination $Path -Recurse -Force; + + if ($Success -eq $FALSE) { + Write-IcingaConsoleError 'Unable to sync repository from location "{0}" to destination "{1}". Please verify that you have permissions to write into the location and try again or create the folder manually' -Objects $TmpDir, $Path; + $Success = Remove-Item -Path $TmpDir -Recurse -Force; + return; + } + } else { # Linux target + + if ($SkipSCPMkdir -eq $FALSE) { + Write-IcingaConsoleNotice 'Creating directory over SSH for host and user "{0}" and path "{1}"' -Objects $SSHAuth, $Path; + + $Result = Start-IcingaProcess -Executable 'ssh' -Arguments ([string]::Format('{0} mkdir -p "{1}"', $SSHAuth, $Path)); + if ($Result.ExitCode -ne 0) { + # TODO: Add link to setup docs + Write-IcingaConsoleError 'SSH Error on directory creation: {0}' -Objects $Result.Error; + $Success = Remove-Item -Path $TmpDir -Recurse -Force; + return; + } + } + + Write-IcingaConsoleNotice 'Removing old repository files from "{0}"' -Objects $Path; + + $Result = Start-IcingaProcess -Executable 'ssh' -Arguments ([string]::Format('{0} rm -Rf "{1}"', $SSHAuth, $RemovePath)); + + if ($Result.ExitCode -ne 0) { + Write-IcingaConsoleError 'SSH Error on removing old repository data: {0}' -Objects $Result.Error; + $Success = Remove-Item -Path $TmpDir -Recurse -Force; + return; + } + + Write-IcingaConsoleNotice 'Syncing new repository files to "{0}"' -Objects $Path; + + $SSHRemotePath = ([string]::Format('{0}:{1}', $SSHAuth, $Path)); + + & scp -r "$CopySource" "$SSHRemotePath"; + + if ($LASTEXITCODE -ne 0) { + Write-IcingaConsoleError 'SCP Error while copying repository files'; + $Success = Remove-Item -Path $TmpDir -Recurse -Force; + return; + } + + $Path = $SSHRemotePath; + } + + $Success = Remove-Item -Path $TmpDir -Recurse -Force; + + if ((Test-IcingaPowerShellConfigItem -ConfigObject $CurrentRepositories -ConfigKey $Name) -And $Force -eq $TRUE) { + $CurrentRepositories.$Name.Enabled = $EnableRepo; + Set-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories' -Value $CurrentRepositories; + Write-IcingaConsoleNotice 'Re-syncing of repository "{0}" was successful' -Objects $Name; + return; + } + + Write-IcingaConsoleNotice 'The repository was synced successfully. Use "Update-IcingaRepository" to sync possible changes from the source repository.'; + + [array]$RepoCount = $CurrentRepositories.PSObject.Properties.Count; + + $CurrentRepositories | Add-Member -MemberType NoteProperty -Name $Name -Value (New-Object -TypeName PSObject); + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'LocalPath' -Value $Path; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'RemotePath' -Value $RemotePath; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'CloneSource' -Value $Source; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'UseSCP' -Value ([bool]$UseSCP); + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'Order' -Value $RepoCount.Count; + $CurrentRepositories.$Name | Add-Member -MemberType NoteProperty -Name 'Enabled' -Value $EnableRepo; + + Set-IcingaPowerShellConfig -Path 'Framework.Repository.Repositories' -Value $CurrentRepositories; + return; } +function Test-IcingaRepositoryErrorState() +{ + param ( + [string]$Repository + ); -if ($null -eq $Global:Icinga) { - $Global:Icinga = @{ }; + 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; +} +function Uninstall-IcingaComponent() +{ + param ( + [string]$Name = '', + [switch]$RemovePackageFiles = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to specify a component name to uninstall'; + return $FALSE; + } + + Set-IcingaServiceEnvironment; + + if ($Name.ToLower() -eq 'agent') { + return (Uninstall-IcingaAgent -RemoveDataFolder:$RemovePackageFiles); + } + + if ($Name.ToLower() -eq 'service') { + return (Uninstall-IcingaForWindowsService -RemoveFiles:$RemovePackageFiles); + } + + $ModuleBase = Get-IcingaForWindowsRootPath; + $UninstallComponent = [string]::Format('icinga-powershell-{0}', $Name); + $UninstallPath = Join-Path -Path $ModuleBase -ChildPath $UninstallComponent; + + if ((Test-Path $UninstallPath) -eq $FALSE) { + Write-IcingaConsoleNotice -Message 'The Icinga for Windows component "{0}" at "{1}" could not ne found.' -Objects $UninstallComponent, $UninstallPath; + return $FALSE; + } + + # Set our current location to the PowerShell modules folder, to prevent possible folder lock during uninstallation + Set-IcingaPSLocation -Path $UninstallPath; + + Write-IcingaConsoleNotice -Message 'Uninstalling Icinga for Windows component "{0}" from "{1}"' -Objects $UninstallComponent, $UninstallPath; + if (Remove-ItemSecure -Path $UninstallPath -Recurse -Force) { + Write-IcingaConsoleNotice -Message 'Successfully removed Icinga for Windows component "{0}" from "{1}"' -Objects $UninstallComponent, $UninstallPath; + if ($UninstallComponent -ne 'icinga-powershell-framework') { + Remove-Module $UninstallComponent -Force -ErrorAction SilentlyContinue; + # In case we are not removing the framework itself, set the location to the Icinga for Windows Folder + Set-IcingaPSLocation -Path $UninstallPath; + } + return $TRUE; + } else { + Write-IcingaConsoleError -Message 'Unable to uninstall Icinga for Windows component "{0}" from "{1}"' -Objects $UninstallComponent, $UninstallPath; + } + + return $FALSE; +} +function Update-Icinga() +{ + param ( + [string]$Name = $null, + [string]$Version = 'release', + [switch]$Release = $FALSE, + [switch]$Snapshot = $FALSE, + [switch]$Confirm = $FALSE, + [switch]$Force = $FALSE + ); + + if ($Release -eq $FALSE -And $Snapshot -eq $FALSE) { + $Release = $TRUE; + } + + $CurrentInstallation = Get-IcingaInstallation -Release:$Release -Snapshot:$Snapshot; + [bool]$UpdateJEA = $FALSE; + [array]$ComponentsList = @(); + [bool]$IsInstalled = $FALSE; + + # We need to make sure that the framework is always installed first as component + # to prevent possible race-conditions during update, in case we update plugins + # before the framework. For plugins this applies as well, as other components + # could use them as depdency + if ($CurrentInstallation.ContainsKey('framework')) { + $ComponentsList += 'framework'; + } + if ($CurrentInstallation.ContainsKey('plugins')) { + $ComponentsList += 'plugins'; + } + + # Add all other components, but skip the framework in this case + foreach ($entry in $CurrentInstallation.Keys) { + if ($entry -eq 'framework' -Or $entry -eq 'plugins') { + continue; + } + $ComponentsList += $entry; + } + + # Now process with your installation + foreach ($entry in $ComponentsList) { + $Component = $CurrentInstallation[$entry]; + + if ([string]::IsNullOrEmpty($Name) -eq $FALSE -And $Name -ne $entry) { + continue; + } + + $IsInstalled = $TRUE; + + $NewVersion = $Component.LatestVersion; + + if ([string]::IsNullOrEmpty($Version) -eq $FALSE) { + # Ensure we are backwards compatible with Icinga for Windows v1.11.0 which broke the version update feature + if ($Version.ToLower() -ne 'release' -And $Version.ToLower() -ne 'latest') { + $NewVersion = $Version; + } + } + + if ([string]::IsNullOrEmpty($NewVersion)) { + Write-IcingaConsoleNotice 'No update package found for component "{0}"' -Objects $entry; + continue; + } + + $LockedVersion = Get-IcingaComponentLock -Name $entry; + + if ($null -ne $LockedVersion) { + $NewVersion = $LockedVersion; + } + + if ([Version]$NewVersion -le [Version]$Component.CurrentVersion -And $Force -eq $FALSE) { + Write-IcingaConsoleNotice 'The installed version "{0}" of component "{1}" is identical or lower than the new version "{2}". Use "-Force" to install anyway' -Objects $Component.CurrentVersion, $entry, $NewVersion; + continue; + } + + if ($entry.ToLower() -ne 'agent' -And $entry.ToLower() -ne 'service') { + $UpdateJEA = $TRUE; + } + + Install-IcingaComponent -Name $entry -Version $NewVersion -Release:$Release -Snapshot:$Snapshot -Confirm:$Confirm -Force:$Force -KeepRepoErrors; + } + + if ($IsInstalled -eq $FALSE) { + Write-IcingaConsoleError 'Failed to update the component "{0}", as it is not installed' -Objects $Name; + } + + # Update JEA profile if JEA is enabled once the update is complete + if ([string]::IsNullOrEmpty((Get-IcingaJEAContext)) -eq $FALSE -And $UpdateJEA) { + Update-IcingaJEAProfile; + Restart-IcingaForWindows; + } +} +function Test-PSCustomObjectMember() +{ + param ( + $PSObject, + $Name + ); + + if ($null -eq $PSObject) { + 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)); +} +<# +.SYNOPSIS + Removes the dll folder from the cache and deletes + all pre-compiled libraries by Icinga for Windows +.DESCRIPTION + Removes the dll folder from the cache and deletes + all pre-compiled libraries by Icinga for Windows +.EXAMPLE + PS> Clear-IcingaAddTypeLib +#> +function Clear-IcingaAddTypeLib() +{ + [string]$DLLFolder = (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'dll'); + + if ((Test-Path $DLLFolder) -eq $FALSE) { + Write-IcingaConsoleNotice 'The dll folder does not exist'; + return; + } + + if (Remove-ItemSecure -Path $DLLFolder -Recurse -Force) { + Write-IcingaConsoleNotice 'The dll cache folder was successfully removed'; + } else { + Write-IcingaConsoleError 'Failed to remove dll cache folder. Make sure it is not used at the moment and try again' + } +} +<# +.SYNOPSIS + Compiles Add-Type calls as DLL inside our cache/lib folder +.DESCRIPTION + Allows to compile DLLs for .NET related code required for plugins + or certain tasks on the Windows machine, not natively supported by + PowerShell. + + All DLL files are compiled within the cache/lib folder within the Framework + and loaded on demand in case required. Once loaded within a shell session, + there the function will simply do nothing +.PARAMETER TypeDefinition + The code to compile a DLL for. Like Add-Type, the code has to start with @" + at the beginning and end with "@ at the very beginning of a new line without + spaces or tabs +.PARAMETER TypeName + The name of the DLL and function being generated. The '.dll' name is NOT required +.PARAMETER Force + Allows to force create the library again and load it inside the shell +.EXAMPLE + $TypeDefinition = @" + /* + Your code + */ +"@ + Add-IcingaAddTypeLib -TypeDefinition $TypeDefinition -TypeName 'Example'; +#> +function Add-IcingaAddTypeLib() +{ + param ( + $TypeDefinition = $null, + [string]$TypeName = '', + [switch]$Force = $FALSE + ); + + # Do nothing if TypeDefinition is null + if ($null -eq $TypeDefinition) { + Write-IcingaConsoleError -Message 'Failed to add type with name "{0}". The TypeDefinition is empty' -Objects $TypeName; + return; + } + + # If no name is set, return an error as we require the name for identification + if ([string]::IsNullOrEmpty($TypeName)) { + Write-IcingaConsoleError -Message 'Failed to add type, as no name is specified'; + return; + } + + # If the type does already exist within our shell, to not load it again + if ((Test-IcingaAddTypeExist -Type $TypeName) -And $Force -eq $FALSE) { + return; + } + + # Get our DLL folder + [string]$DLLFolder = (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'dll'); + + if ((Test-Path $DLLFolder) -eq $FALSE) { + New-Item -Path $DLLFolder -ItemType 'Directory' | Out-Null; + } + + # Update the TypeName to include .dll ending + if ($TypeName.Contains('.dll') -eq $FALSE) { + $TypeName = [string]::Format('{0}.dll', $TypeName); + } + + # Create the full path to our file + [string]$DLLPath = Join-Path -Path $DLLFolder -ChildPath $TypeName; + + # If the DLL already exist, load the DLL from disk + if ((Test-Path $DLLPath) -And $Force -eq $FALSE) { + Add-Type -Path $DLLPath; + return; + } + + # If the DLL does not exist or we use -Force, create it + Add-Type -TypeDefinition $TypeDefinition -OutputType 'Library' -OutputAssembly $DLLPath; + # Load the newly created DLL + Add-Type -Path $DLLPath; +} +function Write-IcingaConsoleHeader() +{ + param ( + [array]$HeaderLines = @() + ); + + [array]$ParsedHeaders = @(); + [int]$MaxHeaderLength = 0; + [int]$TableHeaderCount = 0; + [array]$TableHeader = @(); + [array]$SeverityData = @(); + + Import-LocalizedData ` + -BaseDirectory (Get-IcingaFrameworkRootPath) ` + -FileName 'icinga-powershell-framework.psd1' ` + -BindingVariable IcingaFrameworkData; + + foreach ($line in $HeaderLines) { + $line = $line.Replace('$FrameworkVersion', $IcingaFrameworkData.PrivateData.Version); + $line = $line.Replace('$Copyright', $IcingaFrameworkData.Copyright); + $line = $line.Replace('$UserDomain', $env:USERDOMAIN); + $line = $line.Replace('$Username', $env:USERNAME); + + $ParsedHeaders += $line; + } + + foreach ($line in $ParsedHeaders) { + if ($line.Contains('[Notice]') -Or $line.Contains('[Warning]') -Or $line.Contains('Error')) { + continue + } + + if ($MaxHeaderLength -lt $line.Length) { + $MaxHeaderLength = $line.Length + } + } + + $TableHeaderCount = $MaxHeaderLength + 6; + + while ($TableHeaderCount -ne 0) { + $TableHeader += '*'; + $TableHeaderCount -= 1; + } + + $TableHeaderCount = $MaxHeaderLength + 6; + + Write-IcingaConsolePlain ([string]::Join('', $TableHeader)); + + foreach ($line in $ParsedHeaders) { + [array]$LeftSpacing = @(); + [array]$RightSpacing = @(); + + if ($line.Length -lt $MaxHeaderLength) { + $Spacing = [math]::floor(($MaxHeaderLength - $line.Length) / 2); + + while ($Spacing -gt 0) { + $LeftSpacing += ' '; + $RightSpacing += ' '; + $Spacing -= 1; + } + + if ($TableHeaderCount -gt ($line.Length + $LeftSpacing.Count + $RightSpacing.Count + 6)) { + [int]$RightOffset = $TableHeaderCount - ($line.Length + $LeftSpacing.Count + $RightSpacing.Count + 6) + while ($RightOffset -gt 0) { + $RightSpacing += ' '; + $RightOffset -= 1; + } + } + } + + if ($line.Contains('[Notice]') -Or $line.Contains('[Warning]') -Or $line.Contains('Error')) { + $SeverityData += $line; + continue; + } + + $HeaderMessage = [string]::Format('**{1} {0} {2}**', $line, ([string]::Join('', $LeftSpacing)), ([string]::Join('', $RightSpacing))); + + Write-IcingaConsolePlain -Message $HeaderMessage; + } + + Write-IcingaConsolePlain ([string]::Join('', $TableHeader)); + + if ($SeverityData.Count -ne 0) { + Write-IcingaConsolePlain -Message ''; + + foreach ($entry in $SeverityData) { + if (Write-IcingaConsoleTextColorSplit -Pattern '[Warning]' -Message $entry -ForeColor 'DarkYellow') { + continue; + } + + if (Write-IcingaConsoleTextColorSplit -Pattern '[Error]' -Message $entry -ForeColor 'Red') { + continue; + } + + if (Write-IcingaConsoleTextColorSplit -Pattern '[Notice]' -Message $entry -ForeColor 'Green') { + continue; + } + } + } +} +<# +.SYNOPSIS + Converts strings and all objects within an array from Default PowerShell encoding + to UTF8 +.DESCRIPTION + Converts strings and all objects within an array from Default PowerShell encoding + to UTF8 +.PARAMETER InputObject + A string or array object to convert +.EXAMPLE + PS> [array]$ConvertedArgs = ConvertTo-IcingaUTF8Arguments -Arguments $args; +#> + +function ConvertTo-IcingaUTF8Value() +{ + param ( + $InputObject = $null + ); + + if ($null -eq $InputObject) { + return $InputObject; + } + + if ($InputObject -Is [string]) { + # If german umlauts are contained, do not convert the value + # Fixing issues for running checks locally on CLI vs. Icinga Agent + if ($InputObject -Match "[äöüÄÖÜß]") { + return $InputObject; + } + + $InputInBytes = [System.Text.Encoding]::Default.GetBytes($InputObject); + + return ([string]([System.Text.Encoding]::UTF8.GetString($InputInBytes))); + } elseif ($InputObject -Is [array]) { + [array]$ArrayObject = @(); + + foreach ($entry in $InputObject) { + if ($entry -Is [array]) { + $ArrayObject += , (ConvertTo-IcingaUTF8Value -InputObject $entry); + } else { + $ArrayObject += ConvertTo-IcingaUTF8Value -InputObject $entry; + } + } + + return $ArrayObject; + } + + # If we are not a string or a array, just return the object + return $InputObject; +} +<# +.SYNOPSIS +Colored console output + +.DESCRIPTION +Print colored text on the console. +To specify the color you have to use color tags + +.PARAMETER Text +The text to print + +.EXAMPLE +Write-ColoredOutput -Text "uncolored textblue textuncolored textgreen text" +#> +function Write-ColoredOutput() { + param ( + [string]$Text = '' + ); + + <# + Define opening and closing tag and build regex + Example text + #> + $colorTagOpen = '<'; + $colorTagClose = '>'; + $regexEndTag = ''; + $regexStartTag = [string]::Format('{0}[A-Za-z]+{1}', $colorTagOpen, $colorTagClose); + $textParts = [regex]::Split($Text, "($regexStartTag.*?$regexEndTag)"); + + + # Loop over all parts + foreach ($part in $textParts) { + + # Check if current part is color tagged + if ($part -match "^$regexStartTag.*?$regexEndTag$") { + + # Get color tag + $colorTag = [regex]::Matches($cuttedPart, $regexStartTag).Value; + + # Get color out of color tag + $color = $colorTag.substring($colorTagOpen.Length, $colorTag.Length - ($colorTagOpen.Length + $colorTagClose.Length)); + + # Cut opening tag + $finalPart = $cuttedPart.substring($colorTag.Length, $cuttedPart.length - $colorTag.Length); + + # Cut closing tag + $cuttedPart = $part.substring(0, $part.Length - $regexEndTag.Length); + + <# + Try colored printing. If color does not exist, + catch runtime error and simply print normal + #> + try { + Write-Host -NoNewline -ForegroundColor $color $finalPart; + } catch { + Write-Host -NoNewline $part; + } + + continue; + } + + # Print non tagged, uncolored part + Write-Host -NoNewline $part; + } +} +<# +.SYNOPSIS +Converts a time string to a Unix timestamp. + +.DESCRIPTION +Converts a given time string like "2025-04-10 20:00:00" to a Unix timestamp. +The function returns 0 if the input is null or empty. +If the input cannot be parsed, it returns -1. +The function uses the UTC time zone for the conversion. + +.PARAMETER TimeString +The time string to convert. It should be in a format that can be parsed by [datetime]::Parse, like "2025-04-10 20:00:00" + +.EXAMPLE +$UnixTime = ConvertTo-IcingaUnixTime -TimeString "2025-04-10 20:00:00" +$UnixTime +#> + +function ConvertTo-IcingaUnixTime() +{ + param ( + [string]$TimeString = $null + ); + + if ([string]::IsNullOrEmpty($TimeString)) { + return 0; + } + + try { + return [decimal][double]::Parse( + (Get-Date -UFormat %s -Date (Get-Date -Date $TimeString).ToUniversalTime()) + ); + } catch { + return -1; + } + + return 0; +} +function Write-IcingaConsoleTextColorSplit() +{ + param ( + [string]$Pattern = '', + [string]$Message = '', + [ValidateSet('Default', 'Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White')] + [string]$ForeColor = 'Default' + ); + + if ($Message.Contains($Pattern)) { + [int]$RemoveLength = $Pattern.Length; + [string]$SeverityMsg = $Pattern; + Write-IcingaConsolePlain -Message ($Message.Substring(0, $Message.IndexOf($Pattern))) -NoNewLine; + + Write-IcingaConsolePlain -Message $SeverityMsg -ForeColor $ForeColor -NoNewLine; + + Write-IcingaConsolePlain -Message ($Message.Substring($Message.IndexOf($Pattern) + $RemoveLength, $Message.Length - $Message.IndexOf($Pattern) - $RemoveLength)); + + return $TRUE; + } + + return $FALSE; +} +function Test-IcingaForWindowsCmdletLoader() +{ + param ( + [string]$Path = '' + ); + + if ([string]::IsNullOrEmpty($Path)) { + return $FALSE; + } + + if ((Test-Path -Path $Path) -eq $FALSE) { + return $FALSE; + } + + $FrameworkRootDir = [string]::Format('{0}*', (Get-IcingaForWindowsRootPath)); + + if ($Path -NotLike $FrameworkRootDir) { + return $FALSE; + } + + return $TRUE; +} +<# +.SYNOPSIS + Creates a basic auth header for web requests in case the Get-Credential + method is not supported or working properly +.DESCRIPTION + Creates a basic auth header for web requests in case the Get-Credential + method is not supported or working properly +.FUNCTIONALITY + Creates a hashtable with a basic authorization header as Base64 encoded +.EXAMPLE + PS>New-IcingaBasicAuthHeader -Username 'example_user' -Password $SecurePasswordString; +.EXAMPLE + PS>New-IcingaBasicAuthHeader -Username 'example_user' -Password (Read-Host -Prompt 'Please enter your password' -AsSecureString); +.EXAMPLE + PS>New-IcingaBasicAuthHeader -Username 'example_user' -Password (ConvertTo-IcingaSecureString 'my_secret_password'); +.PARAMETER Username + The user we will use to authenticate for +.PARAMETER Password + The password for the user provided as SecureString +.INPUTS + System.String +.OUTPUTS + System.Hashtable +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function New-IcingaBasicAuthHeader() +{ + param( + [string]$Username = $null, + [SecureString]$Password = $null + ); + + if ($null -eq $Password -or [string]::IsNullOrEmpty($Username)) { + Write-IcingaConsoleWarning 'Please specify your username and password to continue'; + return @{ }; + } + + $Credentials = [System.Convert]::ToBase64String( + [System.Text.Encoding]::ASCII.GetBytes( + [string]::Format( + '{0}:{1}', + $Username, + (ConvertFrom-IcingaSecureString $Password) + ) + ) + ); + + return @{ + 'Authorization' = [string]::Format('Basic {0}', $Credentials) + }; +} +function Test-IcingaFunction() +{ + param( + [string]$Name + ); + + if ([string]::IsNullOrEmpty($Name)) { + return $FALSE; + } + + if (Get-Command $Name -ErrorAction SilentlyContinue) { + return $TRUE; + } + + return $FALSE; +} +<# +.SYNOPSIS + Compares an InputObject of type [array] or [string] to a specified + include and exclude array filter, returning either if the filter applies + as true/false or an object of type [array] for filtered InputObject values +.DESCRIPTION + Compares an InputObject of type [array] or [string] to a specified + include and exclude array filter, returning either if the filter applies + as true/false or an object of type [array] for filtered InputObject values. + + The function is designed to work as a general filter approach for check plugins as + example, to ensure filtering certain values is easy. After comparing the include + and exclude filter, the function will return True if the input object can be included + and will return False in case it should not be included. + + For [array] objects, the function will return a filtered [array] object on which all + values which should be included and excluded were evaluated and only the remaining ones + are returned. +.PARAMETER InputObject + The object to compare the filter against. This can be of type [array] or [string]. + Using an [array] object will return a filtered [array] result, while a [string] value + will return a [bool], if the filter can be applied or not +.PARAMETER Include + An [array] of values to compare the InputObject against and include only certain input +.PARAMETER Exclude + An [array] of values to compare the InputObject against and exclude only certain input +.EXAMPLE + PS> Test-IcingaArrayFilter -InputObject @('icinga2', 'icingapowershell', 'winrm') -Exclude @('icinga2'); + + icingapowershell + winrm +.EXAMPLE + PS> Test-IcingaArrayFilter -InputObject @('icinga2', 'icingapowershell', 'winrm') -Exclude @('*icinga*'); + + winrm +.EXAMPLE + PS> Test-IcingaArrayFilter -InputObject 'icinga2' -Include @('*icinga*', 'winrm'); + + True +.EXAMPLE + PS> Test-IcingaArrayFilter -InputObject 'icinga2' -Include @('*icinga*', 'winrm') -Exclude @('*icinga*'); + + False +#> + +function Test-IcingaArrayFilter() +{ + param ( + $InputObject = $null, + [array]$Include = @(), + [array]$Exclude = @() + ); + + [bool]$ReturnArray = $FALSE; + + if ($InputObject -Is [array]) { + $ReturnArray = $TRUE; + } + + [array]$FilteredArray = @(); + + if ($null -eq $InputObject) { + if ($ReturnArray) { + return $InputObject; + } + + return $FALSE; + } + + if ($Include.Count -eq 0 -And $Exclude.Count -eq 0) { + if ($ReturnArray) { + return $InputObject; + } + + return $TRUE; + } + + [bool]$IncludeFound = $FALSE; + + if ($ReturnArray) { + # Handles if our input object is an array + # Will return an array object instead of boolean + foreach ($input in $InputObject) { + if ((Test-IcingaArrayFilter -InputObject $input -Include $Include -Exclude $Exclude)) { + $FilteredArray += $input; + } + } + + return $FilteredArray; + } else { + try { + foreach ($entry in $Exclude) { + if (([string]$InputObject).ToLower() -Like ([string]$entry).ToLower()) { + return $FALSE; + } + } + + if ($Include.Count -eq 0) { + return $TRUE; + } + + foreach ($entry in $Include) { + if (([string]$InputObject).ToLower() -Like ([string]$entry).ToLower()) { + $IncludeFound = $TRUE; + break; + } + } + } catch { + Exit-IcingaThrowException -InputString $_.Exception.Message -StringPattern 'wildcard' -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.RegexError; + Exit-IcingaThrowException -CustomMessage $_.Exception.Message -ExceptionType 'Input' -ExceptionThrown $_.Exception.Message; + return $FALSE; + } + + return $IncludeFound; + } + + return $IncludeFound; +} +<# +.SYNOPSIS + Used to convert both IPv4 addresses and IPv6 addresses to binary. +.DESCRIPTION + ConvertTo-IcingaIPBinaryString returns a binary string based on the given IPv4 address or IPv6 address. + + More Information on https://github.com/Icinga/icinga-powershell-framework +.FUNCTIONALITY + This module is intended to be used to convert an IPv4 address or IPv6 address to binary string. +.PARAMETER IP + Used to specify an IPv4 address or IPv6 address. +.INPUTS + System.String +.OUTPUTS + System.String + +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function ConvertTo-IcingaIPBinaryString() +{ + param ( + $IP + ); + + if ($IP -like '*.*') { + $IP = ConvertTo-IcingaIPv4BinaryString -IP $IP; + } elseif ($IP -like '*:*') { + $IP = ConvertTo-IcingaIPv6BinaryString -IP $IP; + } else { + return 'Invalid IP was provided!'; + } + + return $IP; +} +function Add-IcingaHashtableItem() +{ + param( + $Hashtable, + $Key, + $Value, + [switch]$Override + ); + + if ($null -eq $Hashtable) { + return $FALSE; + } + + if ($Hashtable.ContainsKey($Key) -eq $FALSE) { + $Hashtable.Add($Key, $Value); + return $TRUE; + } else { + if ($Override) { + $Hashtable.Remove($Key); + $Hashtable.Add($Key, $Value); + return $TRUE; + } + } + return $FALSE; +} +<# +.SYNOPSIS + Converts an Icinga plugin value to a human-readable string. + +.DESCRIPTION + The Convert-IcingaPluginValueToString function is used to convert an Icinga plugin value to a human-readable string. It supports various units and can handle percentage values. + +.PARAMETER Value + The value to be converted. + +.PARAMETER BaseValue + The base value used for percentage calculations. + +.PARAMETER Unit + The unit of the value. + +.PARAMETER OriginalUnit + The original unit of the value. + +.PARAMETER UsePercent + Specifies whether to treat the value as a percentage. + +.PARAMETER IsThreshold + Specifies whether the value is a threshold. + +.OUTPUTS + System.String + Returns the converted value as a human-readable string. + +.EXAMPLE + Convert-IcingaPluginValueToString -Value 1024 -Unit 'KiB' + Converts the value 1024 with the unit 'KiB' to a human-readable string. + +.EXAMPLE + Convert-IcingaPluginValueToString -Value 50 -BaseValue 100 -UsePercent + Converts the value 50 as a percentage of the base value 100 to a human-readable string. + +.NOTES + This function is part of the Icinga PowerShell Framework module. +#> +function Convert-IcingaPluginValueToString() +{ + param ( + $Value = $null, + $BaseValue = $null, + [string]$Unit = '', + [string]$OriginalUnit = '', + [switch]$UsePercent = $FALSE, + [switch]$IsThreshold = $FALSE + ); + + $AdjustedValue = $Value; + $PercentValue = $null; + $HumanReadableValue = $null; + + if ([string]::IsNullOrEmpty($OriginalUnit)) { + $OriginalUnit = $Unit; + } + + try { + $AdjustedValue = ([math]::Round([decimal]$Value, 6)) + } catch { + $AdjustedValue = $Value; + } + + if ($UsePercent -And ($null -eq $BaseValue -Or $BaseValue -eq 0)) { + return ([string]::Format('{0}{1}', ([string]$AdjustedValue).Replace(',', '.'), $Unit)); + } elseif ($UsePercent) { + $Unit = $OriginalUnit; + if ($IsThreshold) { + $PercentValue = [math]::Round($Value, 2); + $AdjustedValue = [math]::Round(($BaseValue / 100) * $Value, 2); + } else { + $PercentValue = [math]::Round(($Value / $BaseValue) * 100, 2); + } + } + + switch ($OriginalUnit) { + { ($_ -eq "Kbit") -or ($_ -eq "Mbit") -or ($_ -eq "Gbit") -or ($_ -eq "Tbit") -or ($_ -eq "Pbit") -or ($_ -eq "Ebit") -or ($_ -eq "Zbit") -or ($_ -eq "Ybit") } { + $TransferSpeed = Get-IcingaNetworkInterfaceUnits -Value $AdjustedValue -Unit $Unit; + $HumanReadableValue = ([string]::Format('{0}{1}', $TransferSpeed.LinkSpeed, $TransferSpeed.Unit)).Replace(',', '.'); + break; + }; + { ($_ -eq "B") -or ($_ -eq "KiB") -or ($_ -eq "MiB") -or ($_ -eq "GiB") -or ($_ -eq "TiB") -or ($_ -eq "PiB") -or ($_ -eq "EiB") -or ($_ -eq "ZiB") -or ($_ -eq "YiB") } { + $HumanReadableValue = (ConvertTo-BytesNextUnit -Value $AdjustedValue -Unit $Unit -Units @('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB')).Replace(',', '.'); + break; + }; + { ($_ -eq "KB") -or ($_ -eq "MB") -or ($_ -eq "GB") -or ($_ -eq "TB") -or ($_ -eq "PB") -or ($_ -eq "EB") -or ($_ -eq "ZB") -or ($_ -eq "YB") } { + $HumanReadableValue = (ConvertTo-BytesNextUnit -Value $AdjustedValue -Unit $Unit -Units @('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')).Replace(',', '.'); + break; + }; + 's' { + $HumanReadableValue = (ConvertFrom-TimeSpan -Seconds $AdjustedValue).Replace(',', '.'); + break; + }; + } + + if ($null -eq $HumanReadableValue) { + $HumanReadableValue = ([string]::Format('{0}{1}', ([string]$AdjustedValue).Replace(',', '.'), $Unit)); + } + + # In case the value provided is a percentage value, we need to adjust the output so it doesn't make sense to add the percentage value again for this case + if ($UsePercent -And $Unit -ne '%') { + $HumanReadableValue = [string]::Format('{0} ({1}%)', $HumanReadableValue, $PercentValue); + } + + return $HumanReadableValue; +} +function Get-IcingaMaxTextLength() +{ + param ( + [array]$TextArray = '' + ); + + [int]$MaxLength = 0; + + foreach ($text in $TextArray) { + if ($MaxLength -lt $text.Length) { + $MaxLength = $text.Length; + } + } + + return $MaxLength; +} +function Test-AdministrativeShell() +{ + $WindowsPrincipcal = New-Object System.Security.Principal.WindowsPrincipal( + [System.Security.Principal.WindowsIdentity]::GetCurrent() + ); + + if ($WindowsPrincipcal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) { + return $TRUE; + } + return $FALSE; +} +function Format-IcingaPerfDataValue() +{ + param( + $PerfValue + ); + + if ((Test-Numeric $PerfValue) -eq $FALSE) { + return $PerfValue; + } + + # Convert our value to a string and replace ',' with a '.' to allow Icinga to parse the output + # In addition, round every output to 6 digits + return (([string]([math]::round([decimal]$PerfValue, 6))).Replace(',', '.')); +} +<# +.SYNOPSIS + Splits a username containing a domain into a hashtable to easily use both values independently. + If no domain is specified the hostname will used as "local domain" +.DESCRIPTION + Splits a username containing a domain into a hashtable to easily use both values independently. + If no domain is specified the hostname will used as "local domain" +.PARAMETER User + A user object either containing only the user or domain information +.EXAMPLE + PS>Split-IcingaUserDomain -User 'icinga'; + + Name Value + ---- ----- + User icinga + Domain icinga-win +.EXAMPLE + PS>Split-IcingaUserDomain -User 'ICINGADOMAIN\icinga'; + + Name Value + ---- ----- + User icinga + Domain ICINGADOMAIN +.EXAMPLE + PS>Split-IcingaUserDomain -User 'icinga@ICINGADOMAIN'; + + Name Value + ---- ----- + User icinga + Domain ICINGADOMAIN +.EXAMPLE + PS>Split-IcingaUserDomain -User '.\icinga'; + + Name Value + ---- ----- + User icinga + Domain icinga-win +.INPUTS + System.String +.OUTPUTS + System.Hashtable +#> + +function Split-IcingaUserDomain() +{ + param ( + $User + ); + + if ([string]::IsNullOrEmpty($User)) { + Write-IcingaConsoleError 'Please enter a valid username'; + return ''; + } + + [array]$UserData = @(); + + if ($User.Contains('\')) { + $UserData = $User.Split('\'); + } elseif ($User.Contains('@')) { + [array]$Split = $User.Split('@'); + $UserData = @( + $Split[1], + $Split[0] + ); + } else { + $UserData = @( + (Get-IcingaNetbiosName), + $User + ); + } + + if ([string]::IsNullOrEmpty($UserData[0]) -Or $UserData[0] -eq '.' -Or $UserData[0] -eq 'BUILTIN') { + $UserData[0] = (Get-IcingaNetbiosName); + } + + return @{ + 'Domain' = $UserData[0]; + 'User' = $UserData[1]; + }; +} +<# +.SYNOPSIS +Calculates the offset in seconds between the current Unix time and a specified Unix time or time string. + +.DESCRIPTION +The `Get-IcingaUnixTimeOffsetNow` function computes the difference in seconds between the current Unix time and a provided Unix time or time string. If no valid input is provided, the function returns 0. + +.PARAMETER TimeString +A string representing a specific time. This string will be converted to Unix time using the `ConvertTo-IcingaUnixTime` function. + +.PARAMETER UnixTime +A decimal value representing a specific Unix time. If provided, the offset will be calculated using this value. + +.RETURNS +The offset in seconds as a decimal value. If no valid input is provided or the conversion fails, the function returns 0. + +.EXAMPLE +PS> Get-IcingaUnixTimeOffsetNow -TimeString "2025-04-20 10:00:00" +Calculates the offset in seconds between the current Unix time and the specified time string. + +.EXAMPLE +PS> Get-IcingaUnixTimeOffsetNow -UnixTime 1672531200 +Calculates the offset in seconds between the current Unix time and the specified Unix time. + +.NOTES +This function depends on the `ConvertTo-IcingaUnixTime` and `Get-IcingaUnixTime` functions to perform time conversions and retrieve the current Unix time. +#> + +function Get-IcingaUnixTimeOffsetNow() +{ + param ( + [string]$TimeString = '', + [decimal]$UnixTime = 0 + ); + + if ([string]::IsNullOrEmpty($TimeString) -And $UnixTime -eq 0) { + return 0; + } + + if ([string]::IsNullOrEmpty($TimeString) -eq $FALSE) { + $UnixTime = ConvertTo-IcingaUnixTime -TimeString $TimeString; + + if ($UnixTime -le 0) { + return 0; + } + } + + $CurrentUnixTime = Get-IcingaUnixTime; + + return ($CurrentUnixTime - $UnixTime); +} +function New-IcingaTemporaryDirectory() +{ + [string]$TmpDirectory = ''; + [string]$DirectoryPath = ''; + + while ($TRUE) { + $TmpDirectory = [string]::Format('tmp_icinga{0}.d', (Get-Random)); + $DirectoryPath = Join-Path $Env:TMP -ChildPath $TmpDirectory; + + if ((Test-Path $DirectoryPath) -eq $FALSE) { + break; + } + } + + return (New-Item -Path $DirectoryPath -ItemType Directory); +} +function Add-IcingaArrayListItem() +{ + param ( + [System.Collections.ArrayList]$Array, + $Element + ); + + if ($null -eq $Array -Or $null -eq $Element) { + return; + } + + $Array.Add($Element) | Out-Null; +} +<# +.SYNOPSIS + Exports command as JSON for icinga director + +.DESCRIPTION + Get-IcingaCheckCommandConfig returns a JSON-file of one or all 'Invoke-IcingaCheck'-Commands, which can be imported via Icinga-Director + When no single command is specified all commands will be exported, and vice versa. + + More Information on https://github.com/Icinga/icinga-powershell-framework + +.FUNCTIONALITY + This module is intended to be used to export one or all PowerShell-Modules with the namespace 'Invoke-IcingaCheck'. + The JSON-Export, which will be generated through this module is structured like an Icinga-Director-JSON-Export, so it can be imported via the Icinga-Director the same way. + +.EXAMPLE + PS>Get-IcingaCheckCommandConfig + Check Command JSON for the following commands: + - 'Invoke-IcingaCheckBiosSerial' + - 'Invoke-IcingaCheckCPU' + - 'Invoke-IcingaCheckProcessCount' + - 'Invoke-IcingaCheckService' + - 'Invoke-IcingaCheckUpdates' + - 'Invoke-IcingaCheckUptime' + - 'Invoke-IcingaCheckUsedPartitionSpace' + - 'Invoke-IcingaCheckUsers' +############################################################ + +.EXAMPLE + Get-IcingaCheckCommandConfig -OutDirectory 'C:\Users\icinga\config-exports' + The following commands were exported: + - 'Invoke-IcingaCheckBiosSerial' + - 'Invoke-IcingaCheckCPU' + - 'Invoke-IcingaCheckProcessCount' + - 'Invoke-IcingaCheckService' + - 'Invoke-IcingaCheckUpdates' + - 'Invoke-IcingaCheckUptime' + - 'Invoke-IcingaCheckUsedPartitionSpace' + - 'Invoke-IcingaCheckUsers' + JSON export created in 'C:\Users\icinga\config-exports\PowerShell_CheckCommands_09-13-2019-10-55-1989.json' + +.EXAMPLE + Get-IcingaCheckCommandConfig Invoke-IcingaCheckBiosSerial, Invoke-IcingaCheckCPU -OutDirectory 'C:\Users\icinga\config-exports' + The following commands were exported: + - 'Invoke-IcingaCheckBiosSerial' + - 'Invoke-IcingaCheckCPU' + JSON export created in 'C:\Users\icinga\config-exports\PowerShell_CheckCommands_09-13-2019-10-58-5342.json' + +.PARAMETER CheckName + Used to specify an array of commands which should be exported. + Separated with ',' + +.PARAMETER FileName + Define a custom file name for the exported `.json`/`.conf` file + +.PARAMETER IcingaConfig + Will switch the configuration generator to write plain Icinga 2 `.conf` + files instead of Icinga Director Basket `.json` files. + Does only affect the written files, not the Cmdlet's String output. + +.INPUTS + System.Array + +.OUTPUTS + System.String + +.LINK + https://github.com/Icinga/icinga-powershell-framework + +.NOTES +#> + +[string]$PsBaseCArg = [string]::Format('try {{ Use-Icinga -Minimal; }} catch {{ Write-Output {0}The Icinga PowerShell Framework is either not installed on the system or not configured properly. Please check https://icinga.com/docs/windows for further details{0}; Write-Output {0}Error:{0} $$($$_.Exception.Message)Components:`r`n$$( Get-Module -ListAvailable {0}icinga-powershell-*{0} )`r`n{0}Module-Path:{0}`r`n$$($$Env:PSModulePath); exit 3; }}; Exit-IcingaExecutePlugin -Command {0}$ifw_api_command${0} ', "'"); + +function Get-IcingaCheckCommandConfig() +{ + param( + [array]$CheckName, + [string]$OutDirectory = '', + [string]$Filename, + [switch]$IcingaConfig + ); + + [array]$BlacklistedArguments = @( + 'ThresholdInterval' + ); + + # Check whether all Checks will be exported or just the ones specified + if ([string]::IsNullOrEmpty($CheckName) -eq $true) { + $CheckName = (Get-Command Invoke-IcingaCheck*).Name + } + + [int]$FieldID = 4; # Starts at '4', because 0-3 are reserved for 'Verbose', 'NoPerfData', ExecutionPolicy and a placeholder + [hashtable]$Basket = @{ }; + + # Define basic hashtable structure by adding fields: "Datafield", "DataList", "Command" + $Basket.Add('Datafield', @{ }); + $Basket.Add('DataList', @{ }); + $Basket.Add('Command', @{ }); + + # At first generate a base Check-Command we can use as import source for all other commands + $Basket.Command.Add( + 'PowerShell Base', + @{ + 'arguments' = @{ + '-NoProfile' = @{ + 'order' = '-3'; + 'skip_key' = $TRUE; + 'value' = '-NoProfile'; + }; + '-NoLogo' = @{ + 'order' = '-2'; + 'skip_key' = $TRUE; + 'value' = '-NoLogo'; + }; + '-ExecutionPolicy' = @{ + 'order' = '-1'; + 'value' = '$IcingaPowerShellBase_String_ExecutionPolicy$'; + }; + '-C' = @{ + 'order' = '0'; + 'value' = $PsBaseCArg; + }; + }; + 'command' = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'; + 'disabled' = $FALSE; + 'fields' = @( + @{ + 'datafield_id' = 2; + 'is_required' = 'n'; + 'var_filter' = $NULL; + }; + ); + 'imports' = @(); + 'is_string' = $NULL; + 'methods_execute' = 'PluginCheck'; + 'object_name' = 'PowerShell Base'; + 'object_type' = 'object'; + 'timeout' = '180'; + 'vars' = @{ + 'IcingaPowerShellBase_String_ExecutionPolicy' = 'ByPass'; + }; + 'zone' = $NULL; + } + ); + + + Add-PowerShellDataList -Name 'PowerShell ExecutionPolicies' -Basket $Basket -Arguments @( 'AllSigned', 'Bypass', 'Default', 'RemoteSigned', 'Restricted', 'Undefined', 'Unrestricted' ); + + $Basket.Datafield.Add( + '2', @{ + 'varname' = 'IcingaPowerShellBase_String_ExecutionPolicy'; + 'caption' = 'PowerShell Execution Policy'; + 'description' = 'Defines with which Execution Policy the PowerShell is started'; + 'datatype' = 'Icinga\Module\Director\DataType\DataTypeDatalist'; + 'format' = $NULL; + 'originalId' = '2'; + } + ); + + $Basket.Datafield['2'].Add( + 'settings', @{ + 'datalist' = 'PowerShell ExecutionPolicies'; + 'data_type' = 'string'; + 'behavior' = 'strict'; + } + ); + + $ThresholdIntervalArg = New-Object -TypeName PSObject; + $ThresholdIntervalArg | Add-Member -MemberType NoteProperty -Name 'type' -Value (New-Object -TypeName PSObject); + $ThresholdIntervalArg | Add-Member -MemberType NoteProperty -Name 'Description' -Value (New-Object -TypeName PSObject); + $ThresholdIntervalArg | Add-Member -MemberType NoteProperty -Name 'position' -Value 99; + $ThresholdIntervalArg | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'ThresholdInterval'; + $ThresholdIntervalArg | Add-Member -MemberType NoteProperty -Name 'required' -Value $FALSE; + $ThresholdIntervalArg.type | Add-Member -MemberType NoteProperty -Name 'name' -Value 'String'; + $ThresholdIntervalArg.Description | Add-Member -MemberType NoteProperty -Name 'Text' -Value 'Change the value your defined threshold checks against from the current value to a collected time threshold of the Icinga for Windows daemon, as described here: https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/06-Collect-Metrics-over-Time/ An example for this argument would be 1m or 15m which will use the average of 1m or 15m for monitoring.'; + + # Loop through ${CheckName}, to get information on every command specified/all commands. + foreach ($check in $CheckName) { + + [string]$check = [string]$check; + + # Get necessary syntax-information and more through cmdlet "Get-Help" + $Data = (Get-Help $check); + $ParameterList = (Get-Command -Name $check).Parameters; + $CheckParamList = @( $ThresholdIntervalArg ); + $PluginNameSpace = $check.Replace('Invoke-', ''); + + foreach ($entry in $Data.parameters.parameter) { + foreach ($BlackListArg in $BlacklistedArguments) { + if ($BlackListArg.ToLower() -eq $entry.Name.ToLower()) { + Write-IcingaConsoleError -Message 'The argument "{0}" for check command "{1}" is not allowed, as this is reserved as Framework constant argument and can not be used.' -Objects $BlackListArg, $check; + return; + } + } + $CheckParamList += (Convert-IcingaCheckArgumentToPSObject -Parameter $entry -CheckCommand $check); + } + + foreach ($arg in $ParameterList.Keys) { + foreach ($entry in $CheckParamList) { + if ($entry.Name -eq $arg) { + $entry.Attributes.ValidValues = $ParameterList[$arg].Attributes.ValidValues; + break; + } + } + } + + # Add command Structure + $Basket.Command.Add( + $check, @{ + 'arguments' = @{ } + 'fields' = @(); + 'imports' = @( 'PowerShell Base' ); + 'object_name' = $check; + 'object_type' = 'object'; + 'vars' = @{ + 'ifw_api_command' = $check; + 'ifw_api_arguments' = @{ }; + }; + } + ); + + # Loop through parameters of a given command + foreach ($parameter in $CheckParamList) { + + $IsDataList = $FALSE; + + # IsNumeric-Check on position to determine the order-value + If (Test-Numeric($parameter.position) -eq $TRUE) { + [string]$Order = [int]$parameter.position + 1; + } else { + [string]$Order = 99 + } + + $IcingaCustomVariable = [string]::Format('${0}_{1}_{2}$', $PluginNameSpace, (Get-Culture).TextInfo.ToTitleCase($parameter.type.name), $parameter.Name); + + if ($IcingaCustomVariable.Length -gt 66) { + Write-IcingaConsoleError 'The generated custom variable name for the argument "{0}" and plugin "{1}" is too long. Custom variables are generated by combining the check function name, the datatype of the argument as well as the argument name itself. Please shorten your argument name and/or the check function name. The maximum size of generated custom variables is 64 digits. Current argument size: "{2}", generated custom variable name: "{3}"' -Objects $parameter.Name, $check, ($IcingaCustomVariable.Length - 2), $IcingaCustomVariable.Replace('$', ''); + return; + } + + # Todo: Should we improve this? Actually the handling would be identical, we just need to assign + # the proper field for this + if ($IcingaCustomVariable -like '*_Int32_Verbose$' -Or $IcingaCustomVariable -like '*_Int_Verbose$' -Or $IcingaCustomVariable -like '*_Object_Verbose$') { + $IcingaCustomVariable = [string]::Format('${0}_Int_Verbose$', $PluginNameSpace); + } + + # Add arguments to a given command + if ($parameter.type.name -eq 'SwitchParameter') { + $Basket.Command[$check].arguments.Add( + [string]::Format('-{0}', $parameter.Name), @{ + 'set_if' = $IcingaCustomVariable; + 'set_if_format' = 'string'; + 'order' = $Order; + } + ); + + $Basket.Command[$check].vars.Add($IcingaCustomVariable.Replace('$', ''), $FALSE); + + } elseif ($parameter.type.name -eq 'Array') { + # Conditional whether type of parameter is array + $Basket.Command[$check].arguments.Add( + [string]::Format('-{0}', $parameter.Name), @{ + 'value' = @{ + 'type' = 'Function'; + 'body' = [string]::Format( + 'var arr = macro("{0}");{1}{1}if (len(arr) == 0) {2}{1} return "@()";{1}{3}{1}{1}var psarr = arr.map({1} x => if (typeof(x) == String) {2}{1} var argLen = len(x);{1} if (argLen != 0 && x.substr(0,1) == "{4}" && x.substr(argLen - 1, argLen) == "{4}") {2}{1} x;{1} {3} else {2}{1} "{4}" + x + "{4}";{1} {3}{1} {3} else {2}{1} x;{1} {3}{1}).join(",");{1}{1}return "@(" + psarr + ")";', + $IcingaCustomVariable, + "`r`n", + '{', + '}', + "'" + ); + } + 'order' = $Order; + } + ); + } elseif ($parameter.type.name -eq 'String') { + # Conditional whether type of parameter is String + $Basket.Command[$check].arguments.Add( + [string]::Format('-{0}', $parameter.Name), @{ + 'value' = @{ + 'type' = 'Function'; + 'body' = [string]::Format( + 'var str = macro("{0}");{1}var argLen = len(str);{1}{1}if (argLen == 0) {2}{1} return;{1}{3}{1}{1}if (argLen != 0 && str.substr(0,1) == "{4}" && str.substr(argLen - 1, argLen) == "{4}") {2}{1} return str;{1}{3}{1}{1}return ("{4}" + str + "{4}");', + $IcingaCustomVariable, + "`r`n", + '{', + '}', + "'" + ); + } + 'set_if' = [string]::Format('var str = macro("{0}"); if (len(str) == 0) {{ return false; }}; return true;', $IcingaCustomVariable); + 'set_if_format' = 'expression'; + 'order' = $Order; + } + ); + } elseif ($parameter.type.name -eq 'SecureString') { + # Convert out input string as SecureString + $Basket.Command[$check].arguments.Add( + [string]::Format('-{0}', $parameter.Name), @{ + 'value' = ( + [string]::Format( + "(ConvertTo-IcingaSecureString '{0}')", + $IcingaCustomVariable + ) + ) + 'order' = $Order; + } + ); + } else { + # Default to Object + $Basket.Command[$check].arguments.Add( + [string]::Format('-{0}', $parameter.Name), @{ + 'value' = $IcingaCustomVariable; + 'order' = $Order; + } + ); + } + + if ($parameter.type.name -eq 'SwitchParameter') { + $Basket.Command[$check].vars.ifw_api_arguments.Add([string]::Format('{0}', $parameter.Name), @{ + 'set_if' = $IcingaCustomVariable; + }); + } else { + $Basket.Command[$check].vars.ifw_api_arguments.Add([string]::Format('{0}', $parameter.Name), @{ + 'value' = $IcingaCustomVariable; + }); + } + + # Determine wether a parameter is required based on given syntax-information + if ($parameter.required -eq $TRUE) { + $Required = 'y'; + } else { + $Required = 'n'; + } + + $IcingaCustomVariable = [string]::Format('{0}_{1}_{2}', $PluginNameSpace, (Get-Culture).TextInfo.ToTitleCase($parameter.type.name), $parameter.Name); + + # Todo: Should we improve this? Actually the handling would be identical, we just need to assign + # the proper field for this + if ($IcingaCustomVariable -like '*_Int32_Verbose' -Or $IcingaCustomVariable -like '*_Int_Verbose' -Or $IcingaCustomVariable -like '*_Object_Verbose') { + $IcingaCustomVariable = [string]::Format('{0}_Int_Verbose', $PluginNameSpace); + } + + [bool]$ArgumentKnown = $FALSE; + + foreach ($argument in $Basket.Datafield.Keys) { + if ($Basket.Datafield[$argument].varname -eq $IcingaCustomVariable) { + $ArgumentKnown = $TRUE; + break; + } + } + + if ($ArgumentKnown) { + continue; + } + + $DataListName = [string]::Format('{0} {1}', $PluginNameSpace, $parameter.Name); + + if ($null -ne $parameter.Attributes.ValidValues) { + $IcingaDataType = 'Datalist'; + Add-PowerShellDataList -Name $DataListName -Basket $Basket -Arguments $parameter.Attributes.ValidValues; + $IsDataList = $TRUE; + } elseif ($parameter.type.name -eq 'SwitchParameter') { + $IcingaDataType = 'Boolean'; + } elseif ($parameter.type.name -eq 'Object') { + $IcingaDataType = 'String'; + } elseif ($parameter.type.name -eq 'Array') { + $IcingaDataType = 'Array'; + } elseif ($parameter.type.name -eq 'Int' -Or $parameter.type.name -eq 'Int32') { + $IcingaDataType = 'Number'; + } else { + $IcingaDataType = 'String'; + } + + $IcingaDataType = [string]::Format('Icinga\Module\Director\DataType\DataType{0}', $IcingaDataType) + + if ($Basket.Datafield.Values.varname -ne $IcingaCustomVariable) { + $DataFieldDescription = $parameter.Description.Text; + + # Remove HTML code tags and replace them with markdown tags + if ([string]::IsNullOrEmpty($DataFieldDescription) -eq $FALSE) { + $DataFieldDescription = $DataFieldDescription.Replace('
', '```');
+                    $DataFieldDescription = $DataFieldDescription.Replace('
', '```'); + } + + $Basket.Datafield.Add( + [string]$FieldID, @{ + 'varname' = $IcingaCustomVariable; + 'caption' = $parameter.Name; + 'description' = $DataFieldDescription; + 'datatype' = $IcingaDataType; + 'format' = $NULL; + 'originalId' = [string]$FieldID; + } + ); + + if ($IsDataList) { + [string]$DataListDataType = 'string'; + + if ($parameter.type.name -eq 'Array') { + $DataListDataType = 'array'; + } + + $Basket.Datafield[[string]$FieldID].Add( + 'settings', @{ + 'datalist' = $DataListName; + 'data_type' = $DataListDataType; + 'behavior' = 'strict'; + } + ); + } else { + $CustomVarVisibility = 'visible'; + + if ($parameter.type.name -eq 'SecureString') { + $CustomVarVisibility = 'hidden'; + } + + $Basket.Datafield[[string]$FieldID].Add( + 'settings', @{ + 'visibility' = $CustomVarVisibility; + } + ); + } + + # Increment FieldID, so unique datafields are added. + [int]$FieldID = [int]$FieldID + 1; + } + + # Increment FieldNumeration, so unique fields for a given command are added. + [int]$FieldNumeration = [int]$FieldNumeration + 1; + } + } + + foreach ($check in $CheckName) { + [int]$FieldNumeration = 0; + [string]$check = [string]$check; + + $Data = (Get-Help $check) + $PluginNameSpace = $check.Replace('Invoke-', ''); + $CheckParamList = @( $ThresholdIntervalArg ); + + foreach ($entry in $Data.parameters.parameter) { + $CheckParamList += (Convert-IcingaCheckArgumentToPSObject -Parameter $entry -CheckCommand $check); + } + + foreach ($parameter in $CheckParamList) { + $IcingaCustomVariable = [string]::Format('{0}_{1}_{2}', $PluginNameSpace, (Get-Culture).TextInfo.ToTitleCase($parameter.type.name), $parameter.Name); + + # Todo: Should we improve this? Actually the handling would be identical, we just need to assign + # the proper field for this + if ($IcingaCustomVariable -like '*_Int32_Verbose' -Or $IcingaCustomVariable -like '*_Int_Verbose' -Or $IcingaCustomVariable -like '*_Object_Verbose') { + $IcingaCustomVariable = [string]::Format('{0}_Int_Verbose', $PluginNameSpace); + } + + foreach ($DataFieldID in $Basket.Datafield.Keys) { + [string]$varname = $Basket.Datafield[$DataFieldID].varname; + if ([string]$varname -eq [string]$IcingaCustomVariable) { + $Basket.Command[$check].fields += @{ + 'datafield_id' = [int]$DataFieldID; + 'is_required' = $Required; + 'var_filter' = $NULL; + }; + } + } + } + } + + [string]$FileType = '.json'; + if ($IcingaConfig) { + $FileType = '.conf'; + } + + if ([string]::IsNullOrEmpty($Filename)) { + $TimeStamp = (Get-Date -Format "MM-dd-yyyy-HH-mm-ffff"); + $FileName = [string]::Format("PowerShell_CheckCommands_{0}{1}", $TimeStamp, $FileType); + } else { + if ($Filename.Contains($FileType) -eq $FALSE) { + $Filename = [string]::Format('{0}{1}', $Filename, $FileType); + } + } + + # Generate JSON Output from Hashtable + $output = ConvertTo-Json -Depth 100 $Basket -Compress; + + # Determine whether json output via powershell or in file (based on param -OutDirectory) + if ([string]::IsNullOrEmpty($OutDirectory) -eq $false) { + $ConfigDirectory = $OutDirectory; + $OutDirectory = (Join-Path -Path $OutDirectory -ChildPath $FileName); + if ((Test-Path($OutDirectory)) -eq $false) { + New-Item -Path $OutDirectory -ItemType File -Force | Out-Null; + } + + if ((Test-Path($OutDirectory)) -eq $false) { + throw 'Failed to create specified directory. Please try again or use a different target location.'; + } + + if ($IcingaConfig) { + Write-IcingaPlainConfigurationFiles -Content $Basket -OutDirectory $ConfigDirectory -FileName $FileName; + } else { + Write-IcingaFileSecure -File $OutDirectory -Value $output; + } + + # Output-Text + Write-IcingaConsoleNotice "The following commands were exported:" + foreach ($check in $CheckName) { + Write-IcingaConsoleNotice "- '$check'"; + } + Write-IcingaConsoleNotice "JSON export created in '${OutDirectory}'" + Write-IcingaConsoleWarning 'By using this generated check command configuration you will require the Icinga PowerShell Framework 1.4.0 or later to be installed on ALL monitored machines!'; + return; + } + + Write-IcingaConsoleNotice "Check Command JSON for the following commands:" + foreach ($check in $CheckName) { + Write-IcingaConsoleNotice "- '$check'" + } + Write-IcingaConsoleWarning 'By using this generated check command configuration you will require the Icinga PowerShell Framework 1.4.0 or later to be installed on ALL monitored machines!'; + Write-IcingaConsoleNotice '############################################################'; + + return $output; } -if ($Global:Icinga.ContainsKey('CacheBuilding') -eq $FALSE) { - $Global:Icinga.Add('CacheBuilding', $TRUE); -} else { - $Global:Icinga.CacheBuilding = $TRUE; +function Write-IcingaPlainConfigurationFiles() +{ + param ( + $Content, + $OutDirectory, + $FileName + ); + + $ConfigDirectory = $OutDirectory; + $OutDirectory = (Join-Path -Path $OutDirectory -ChildPath $FileName); + + $IcingaConfig = ''; + + foreach ($entry in $Content.Command.Keys) { + $CheckCommand = $Content.Command[$entry]; + + # Skip PowerShell base, this is written at the end in a separate file + if ($CheckCommand.object_name -eq 'PowerShell Base') { + continue; + } + + # Create the CheckCommand object + $IcingaConfig += [string]::Format('object CheckCommand "{0}" {{{1}', $CheckCommand.object_name, (New-IcingaNewLine)); + + # Import all defined import templates + foreach ($import in $CheckCommand.imports) { + $IcingaConfig += [string]::Format(' import "{0}"{1}', $import, (New-IcingaNewLine)); + } + $IcingaConfig += New-IcingaNewLine; + + if ($CheckCommand.arguments.Count -ne 0) { + # Arguments for the configuration + $IcingaConfig += ' arguments += {' + $IcingaConfig += New-IcingaNewLine; + + foreach ($argument in $CheckCommand.arguments.Keys) { + $CheckArgument = $CheckCommand.arguments[$argument]; + + # Each single argument, like "-Verbosity" = { + $IcingaConfig += [string]::Format(' "{0}" = {{{1}', $argument, (New-IcingaNewLine)); + + foreach ($argconfig in $CheckArgument.Keys) { + $Value = ''; + + if ($argconfig -eq 'set_if_format') { + continue; + } + + # Order is numeric -> no "" required + if ($argconfig -eq 'order') { + $StringFormater = ' {0} = {1}{2}'; + } elseif ($argconfig -eq 'set_if' -And $CheckArgument[$argconfig] -Like '*var str = macro*') { + $StringFormater = ' {0} = {{{{{2} {1}{2} }}}}{2}'; + } else { + # All other entries should be handled as strings and contain "" + $StringFormater = ' {0} = "{1}"{2}' + } + + # In case it is a hashtable, this is most likely a DSL function + # We have to render it differently to also match the intends + if ($CheckArgument[$argconfig] -is [Hashtable]) { + $Value = $CheckArgument[$argconfig].body; + $DSLArray = $Value.Split("`r`n"); + $Value = ''; + foreach ($item in $DSLArray) { + if ([string]::IsNullOrEmpty($item)) { + continue; + } + $Value += [string]::Format(' {0}{1}', $item, (New-IcingaNewLine)); + } + $Value = $Value.Substring(0, $Value.Length - 2); + $StringFormater =' {0} = {{{{{2}{1}{2} }}}}{2}' + } else { + # All other values besides DSL + $Value = $CheckArgument[$argconfig]; + } + + # Read description from our variables + if ($argconfig -eq 'value') { + foreach ($item in $Content.DataField.Keys) { + $DataField = $Content.DataField[$item]; + + if ($Value.Contains($DataField.varname)) { + if ([string]::IsNullOrEmpty($DataField.description)) { + break; + } + $Description = $DataField.description.Replace("`r`n", ' '); + $Description = $Description.Replace("\", '\\'); + $Description = $Description.Replace("`n", ' '); + $Description = $Description.Replace("`r", ' '); + $Description = $Description.Replace('"', "'"); + $IcingaConfig += [string]::Format(' description = "{0}"{1}', $Description, (New-IcingaNewLine)); + break; + } + } + } + + # Write the argument to your CheckCommand + $IcingaConfig += [string]::Format($StringFormater, $argconfig, $Value, (New-IcingaNewLine)); + } + + # Close this specific argument + $IcingaConfig += ' }' + $IcingaConfig += New-IcingaNewLine; + } + + $IcingaConfig = $IcingaConfig.Substring(0, $IcingaConfig.Length - 2); + + # Close all arguments content + $IcingaConfig += New-IcingaNewLine; + $IcingaConfig += ' }' + } + + # In case we pre-define custom variables, we should add them here + if ($CheckCommand.vars.Count -ne 0) { + $IcingaConfig += New-IcingaNewLine; + [bool]$AddNewLine = $FALSE; + + foreach ($var in $CheckCommand.vars.Keys) { + if ($CheckCommand.vars[$var] -Is [Hashtable]) { + if ($AddNewLine) { + $IcingaConfig += New-IcingaNewLine; + } + [string]$HashtableArguments = ''; + [bool]$AddConfigNewLine = $FALSE; + foreach ($item in $CheckCommand.vars[$var].Keys) { + if ($AddConfigNewLine) { + $HashtableArguments += New-IcingaNewLine; + } + $HashtableArguments += [string]::Format(' "{0}" = {{{1}', $item, (New-IcingaNewLine)); + + if ($CheckCommand.vars[$var][$item] -Is [Hashtable]) { + foreach ($icingaconf in $CheckCommand.vars[$var][$item].Keys) { + [string]$Value = $CheckCommand.vars[$var][$item][$icingaconf]; + $HashtableArguments += [string]::Format(' {0} = "{1}"{2}', $icingaconf, $Value, (New-IcingaNewLine)); + } + } else { + [string]$Value = $CheckCommand.vars[$var][$item]; + $HashtableArguments += [string]::Format(' value = "{0}"{1}', $Value, (New-IcingaNewLine)); + } + $HashtableArguments += ' }'; + $AddConfigNewLine = $TRUE; + } + + $IcingaConfig += [string]::Format(' vars.{0} = {{{1}', $var, (New-IcingaNewLine)); + $IcingaConfig += [string]::Format('{0}{1}', $HashtableArguments, (New-IcingaNewLine)); + $IcingaConfig += ' }'; + $AddNewLine = $TRUE; + } else { + if ($AddNewLine) { + $IcingaConfig += New-IcingaNewLine; + $AddNewLine = $FALSE; + } + [string]$Value = $CheckCommand.vars[$var]; + if ($CheckCommand.vars[$var] -Is [bool]) { + $IcingaConfig += [string]::Format(' vars.{0} = {1}{2}', $var, $Value.ToLower(), (New-IcingaNewLine)); + } else { + $IcingaConfig += [string]::Format(' vars.{0} = "{1}"{2}', $var, $Value.ToLower(), (New-IcingaNewLine)); + } + } + } + } else { + $IcingaConfig += New-IcingaNewLine; + } + + # Close the CheckCommand object + $IcingaConfig += '}'; + if ($Content.Command.Count -gt 2) { + $IcingaConfig += New-IcingaNewLine; + $IcingaConfig += New-IcingaNewLine; + } + } + + # Write the PowerShell Base command to a separate file for Icinga 2 configuration + [string]$PowerShellBase = [string]::Format('object CheckCommand "PowerShell Base" {{{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' import "plugin-check-command"{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' command = [{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' ]{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' timeout = 3m{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' arguments += {{{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' "-C" = {{{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' order = 0{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' value = "{0}"{1}', $PsBaseCArg, (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' }}{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' "-ExecutionPolicy" = {{{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' order = -1{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' value = "$IcingaPowerShellBase_String_ExecutionPolicy$"{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' }}{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' "-NoLogo" = {{{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' order = -2{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' set_if = "1"{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' }}{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' "-NoProfile" = {{{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' order = -3{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' set_if = "1"{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' }}{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' }}{0}', (New-IcingaNewLine)); + $PowerShellBase += [string]::Format(' vars.IcingaPowerShellBase_String_ExecutionPolicy = "ByPass"{0}', (New-IcingaNewLine)); + $PowerShellBase += '}'; + + Write-IcingaFileSecure -File (Join-Path -Path $ConfigDirectory -ChildPath 'PowerShell_Base.conf') -Value $PowerShellBase; + Write-IcingaFileSecure -File $OutDirectory -Value $IcingaConfig; } -# Ensures that VS Code is not generating the cache file -if ($null -ne $env:TERM_PROGRAM) { - Write-IcingaFrameworkCodeCache -DeveloperMode; - return; +function Add-PowerShellDataList() +{ + param( + $Name, + $Basket, + $Arguments + ); + + $Basket.DataList.Add( + $Name, @{ + 'list_name' = $Name; + 'owner' = $env:username; + 'originalId' = '2'; + 'entries' = @(); + } + ); + + foreach ($entry in $Arguments) { + if ([string]::IsNullOrEmpty($entry)) { + Write-IcingaConsoleWarning ` + -Message 'The plugin argument "{0}" contains the illegal ValidateSet $null which will not be rendered. Please remove it from the arguments list of "{1}"' ` + -Objects $Name, $Arguments; + + continue; + } + $Basket.DataList[$Name]['entries'] += @{ + 'entry_name' = $entry; + 'entry_value' = $entry; + 'format' = 'string'; + 'allowed_roles' = $NULL; + }; + } +} +<# +.SYNOPSIS + Converts unit to seconds. +.DESCRIPTION + This module converts a given time unit to seconds. + e.g hours to seconds. + + More Information on https://github.com/Icinga/icinga-powershell-framework + +.PARAMETER Value + Specify unit to be converted to seconds. Allowed units: ms, s, m, h, d, w, M, y + ms = miliseconds; s = seconds; m = minutes; h = hours; d = days; w = weeks; M = months; y = years; + + Like 20d for 20 days. +.EXAMPLE + PS> ConvertTo-Seconds 30d + 2592000 +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function ConvertTo-Seconds() +{ + param( + [string]$Value + ); + + if ([string]::IsNullOrEmpty($Value)) { + return $Value; + } + + [string]$NumberPart = ''; + [string]$UnitPart = ''; + [bool]$Negate = $FALSE; + [bool]$hasUnit = $FALSE; + + foreach ($char in $Value.ToCharArray()) { + if ((Test-Numeric $char)) { + $NumberPart += $char; + } else { + if ($char -eq '-') { + $Negate = $TRUE; + } elseif ($char -eq '.' -Or $char -eq ',') { + $NumberPart += '.'; + } else { + $UnitPart += $char; + $hasUnit = $TRUE; + } + } + } + + if (-Not $hasUnit -Or (Test-Numeric $NumberPart) -eq $FALSE) { + return $Value; + } + + [single]$ValueSplitted = $NumberPart; + $result = 0; + + if ($Negate) { + $ValueSplitted *= -1; + } + + [string]$errorMsg = ( + [string]::Format('Invalid unit type "{0}" specified for convertion. Allowed units: ms, s, m, h, d, w, M, y', $UnitPart) + ); + + if ($UnitPart -Match 'ms') { + $result = ($ValueSplitted / [math]::Pow(10, 3)); + } else { + if ($UnitPart.Length -gt 1) { + return $Value; + } + + switch ([int][char]$UnitPart) { + { 115 -contains $_ } { $result = $ValueSplitted; break; } # s + { 109 -contains $_ } { $result = $ValueSplitted * 60; break; } # m + { 104 -contains $_ } { $result = $ValueSplitted * 3600; break; } # h + { 100 -contains $_ } { $result = $ValueSplitted * 86400; break; } # d + { 119 -contains $_ } { $result = $ValueSplitted * 604800; break; } # w + { 77 -contains $_ } { $result = $ValueSplitted * 2592000; break; } # M + { 121 -contains $_ } { $result = $ValueSplitted * 31536000; break; } # y + default { + Throw $errorMsg; + break; + } + } + } + + return $result; } -Write-IcingaFrameworkCodeCache; +function ConvertTo-SecondsFromIcingaThresholds() +{ + param( + [string]$Threshold + ); -Import-Module icinga-powershell-framework -Global -Force; -Import-Module icinga-powershell-framework -Force; + [array]$Content = $Threshold.Split(':'); + [array]$NewContent = @(); -if ($null -ne $env:TERM_PROGRAM -Or $Global:Icinga.Protected.DeveloperMode) { - Copy-IcingaFrameworkCacheTemplate; + foreach ($entry in $Content) { + $NewContent += (Get-IcingaThresholdsAsSeconds -Value $entry) + } + + [string]$Value = [string]::Join(':', $NewContent); + + if ([string]::IsNullOrEmpty($Value) -eq $FALSE -And $Value.Contains(':') -eq $FALSE) { + return [convert]::ToDouble($Value); + } + + return $Value; } -$Global:Icinga.CacheBuilding = $FALSE; +function Get-IcingaThresholdsAsSeconds() +{ + param( + [string]$Value + ); + + if ($Value.Contains('~')) { + $Value = $Value.Replace('~', ''); + return [string]::Format('~{0}', (ConvertTo-Seconds $Value)); + } elseif ($Value.Contains('@')) { + $Value = $Value.Replace('@', ''); + return [string]::Format('@{0}', (ConvertTo-Seconds $Value)); + } + + return (ConvertTo-Seconds $Value); +} + +Export-ModuleMember -Function @( 'ConvertTo-Seconds', 'ConvertTo-SecondsFromIcingaThresholds' ); +<# +.SYNOPSIS + Securely allows to sort arrays for containing objects like hashtables, + which is not simply done by using Sort-Object, but requires custom expressions + (ScriptBlocks) to deal with the sorting. +.DESCRIPTION + Securely allows to sort arrays for containing objects like hashtables, + which is not simply done by using Sort-Object, but requires custom expressions + (ScriptBlocks) to deal with the sorting. + + Icinga for Windows does not allow ScriptBlocks inside external modules while using + JEA profiles. Sorting expressions are ScriptBlocks which are not allowed in JEA + context. To ensure module developers can still sort objects from inside arrays, + this function allows to parse the InputObject as array and adding the name of the + member which should be sorted. In addition it allows configuration if the result + should be sorted Descending or Ascending (default) +.PARAMETER InputObject + An array containing all our objects. Defined as Pipeline input +.PARAMETER MemberName + The member name from within your array objects to sort the result for +.PARAMETER Descending + Set to sort the output result Descending +.EXAMPLE + PS> ConvertTo-IcingaSecureSortedArray -InputObject $MyArray -MemberName 'CreationTime'; +.EXAMPLE + PS> ConvertTo-IcingaSecureSortedArray -InputObject $MyArray -MemberName 'CreationTime' -Descending; +.EXAMPLE + PS> $MyArray | ConvertTo-IcingaSecureSortedArray -MemberName 'CreationTime' -Descending; +#> +function ConvertTo-IcingaSecureSortedArray() +{ + param ( + [Parameter(ValueFromPipeline = $TRUE)] + [array]$InputObject = @(), + [string]$MemberName = '', + [switch]$Descending = $FALSE + ); + + Begin { + [array]$SortedArray = @(); + } + + Process { + if ([string]::IsNullOrEmpty($MemberName)) { + return $InputObject; + } + + foreach ($entry in $InputObject) { + $SortedArray += $entry; + } + } + + End { + return ($SortedArray | Sort-Object -Property @{ Expression = { $_.$MemberName }; Descending = $Descending }); + } +} +<# +.SYNOPSIS + Used to convert an IPv6 address to binary. +.DESCRIPTION + ConvertTo-IcingaIPv6 returns a binary string based on the given IPv6 address. + + More Information on https://github.com/Icinga/icinga-powershell-framework +.FUNCTIONALITY + This module is intended to be used to convert an IPv6 address to binary string. Its recommended to use ConvertTo-IcingaIPBinaryString as a smart function instead. +.PARAMETER IP + Used to specify an IPv6 address. +.INPUTS + System.String +.OUTPUTS + System.String + +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function ConvertTo-IcingaIPv6BinaryString() +{ + param( + [string]$IP + ); + [string]$IP = Expand-IcingaIPv6String $IP; + [array]$IPArr = $IP.Split(':'); + + $IPArr = $IPArr.ToCharArray(); + $IP = $IPArr | ForEach-Object { + [System.Convert]::ToString("0x$_", 2).PadLeft(4, '0'); + } + $IP = $IP -join ''; + $IP = $IP -replace '\s', ''; + + return @{ + 'value' = $IP; + 'name' = 'IPv6' + } +} +function Split-IcingaCheckCommandArgs() +{ + [array]$arguments = @(); + foreach ($arg in $args) { + $arguments += $arg; + } + + return $arguments; +} +function Get-IcingaFileHash() +{ + param ( + [string]$Path, + [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5')] + [string]$Algorithm = 'SHA256' + ); + + if ([string]::IsNullOrEmpty($Path) -Or ((Test-Path -Path $Path) -eq $FALSE)) { + Write-IcingaConsoleError 'Your path is either not specified or does not exist'; + return $null; + } + + $FileHasher = New-Object "System.Security.Cryptography.${Algorithm}CryptoServiceProvider"; + + if ($null -eq $FileHasher) { + Write-IcingaConsoleError 'Unable to create cryptography objects for algorithm "{0}"' -Objects $Algorithm; + return $null; + } + + # Read the file specified in $FilePath as a Byte array + [System.IO.Stream]$FileStream = [System.IO.File]::OpenRead($Path) + [Byte[]]$FileHash = $FileHasher.ComputeHash($FileStream) + [string]$HashString = [BitConverter]::ToString($FileHash).Replace('-', ''); + $RetValue = New-Object -TypeName PSObject; + + $RetValue | Add-Member -MemberType NoteProperty -Name 'Algorithm' -Value $Algorithm.ToUpper(); + $RetValue | Add-Member -MemberType NoteProperty -Name 'Hash' -Value $HashString; + $RetValue | Add-Member -MemberType NoteProperty -Name 'Path' -Value $Path; + + return $RetValue; +} +<# +.SYNOPSIS + Compares two numeric input values and returns the lower or higher value +.DESCRIPTION + Compares two numeric numbers and returns either the higher or lower value + depending on the configuration of the argument + + More Information on https://github.com/Icinga/icinga-powershell-framework +.FUNCTIONALITY + Compares two numeric input values and returns the lower or higher value +.PARAMETER Value + The input value to check for +.PARAMETER Compare + The value to compare against +.PARAMETER Minimum + Configures the command to return the lower number of both inputs +.PARAMETER Maximum + Configures the command to return the higher number of both inputs +.EXAMPLE + PS> Get-IcingaValue -Value 10 -Compare 12 -Minimum; +.EXAMPLE + PS> Get-IcingaValue -Value 10 -Compare 12 -Maximum; +.INPUTS + System.Integer +.OUTPUTS + System.Integer +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function Get-IcingaValue() +{ + param( + $Value, + $Compare, + [switch]$Minimum = $FALSE, + [switch]$Maximum = $FALSE + ); + + # If none of both is set, return the current value + if (-Not $Minimum -And -Not $Maximum) { + return $Value; + } + + # Return the lower value + if ($Minimum) { + # If the value is greater or equal the compared value, return the compared one + if ($Value -ge $Compare) { + return $Compare; + } + + # Otherwise return the value itself + return $Value; + } + + # Return the higher value + if ($Maximum) { + # If the value is greater or equal the compared one, return the value + if ($Value -ge $Compare) { + return $Value; + } + + # Otherwise return the compared value + return $Compare; + } + + # Shouldnt happen anyway + return $Value; +} +function Get-IcingaUnixTime() +{ + param( + [switch]$Milliseconds = $FALSE + ); + + if ($Milliseconds) { + return ([int64](([DateTime]::UtcNow) - (Get-Date '1/1/1970')).TotalMilliseconds / 1000); + } + + return [int][double]::Parse( + (Get-Date -UFormat %s -Date (Get-Date).ToUniversalTime()) + ); +} +function Add-PSCustomObjectMember() +{ + param ( + $Object, + $Key, + $Value + ); + + if ($null -eq $Object) { + return $Object; + } + + $Object | Add-Member -MemberType NoteProperty -Name $Key -Value $Value; + + return $Object; +} +function Get-IPConfigFromString() +{ + param( + [string]$IPConfig + ); + + if ($IPConfig.Contains(':') -and ($IPConfig.Contains('[') -eq $FALSE -And $IPConfig.Contains(']') -eq $FALSE)) { + throw 'Invalid IP-Address format. For IPv6 and/or port configuration, the syntax must be like [ip]:port'; + } + + if ($IPConfig.Contains('[') -eq $FALSE) { + return @{ + 'address' = $IPConfig; + 'port' = $null + }; + } + + if ($IPConfig.Contains('[') -eq $FALSE -or $IPConfig.Contains(']') -eq $FALSE) { + throw 'Invalid IP-Address format. It must match the following [ip]:port'; + } + + $StartBracket = $IPConfig.IndexOf('[') + 1; + $EndBracket = $IPConfig.IndexOf(']') - 1; + $PortDelimeter = $IPConfig.LastIndexOf(':') + 1; + + $Port = ''; + $IP = $IPConfig.Substring($StartBracket, $EndBracket); + + if ($PortDelimeter -ne 0 -And $PortDelimeter -ge $EndBracket) { + $Port = $IPConfig.Substring($PortDelimeter, $IPConfig.Length - $PortDelimeter); + } + + return @{ + 'address' = $IP; + 'port' = $Port + }; +} +function Remove-IcingaDirectorSelfServiceKey() +{ + $Path = 'IcingaDirector.SelfService.ApiKey'; + $Value = Get-IcingaPowerShellConfig $Path; + if ($null -ne $Value) { + Remove-IcingaPowerShellConfig 'IcingaDirector.SelfService.ApiKey'; + $Value = Get-IcingaPowerShellConfig $Path; + if ($null -eq $Value) { + Write-IcingaConsoleNotice 'Icinga Director Self-Service Api key was successfully removed. Please dont forget to drop it within the Icinga Director as well'; + } + } else { + Write-IcingaConsoleWarning 'There is no Self-Service Api key configured on this system'; + } +} +<# +.SYNOPSIS + Used to Expand an IPv6 address. +.DESCRIPTION + Expand-IcingaIPv6String returns the expanded version of an IPv6 address. + + More Information on https://github.com/Icinga/icinga-powershell-framework +.FUNCTIONALITY + This module is intended to be used to expand an IPv6 address. +.EXAMPLE + PS> Expand-IcingaIPv6String ffe8::71:ab: + FFE8:0000:0000:0000:0000:0071:00AB:0000 +.PARAMETER IP + Used to specify an IPv6 address. +.INPUTS + System.String +.OUTPUTS + System.String + +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function Expand-IcingaIPv6String() +{ + param ( + [String]$IP + ); + + $Counter = 0; + $RelV = -1; + + for ($Index = 0; $Index -lt $IP.Length; $Index++) { + if ($IP[$Index] -eq ':') { + $Counter++; + if (($Index - 1) -ge 0 -and $IP[$Index - 1] -eq ':') { + $RelV = $Index; + } + } + } + + if ($RelV -lt 0 -and $Counter -ne 7) { + Write-IcingaConsoleError "Invalid IP was provided!"; + return $null; + } + + if ($Counter -lt 7) { + $IP = $IP.Substring(0, $RelV) + (':'*(7 - $Counter)) + $IP.Substring($RelV); + } + + $Result = @(); + + foreach ($SIP in $IP -split ':') { + $Value = 0; + [int]::TryParse( + $SIP, + [System.Globalization.NumberStyles]::HexNumber, + [System.Globalization.CultureInfo]::InvariantCulture, + [Ref]$Value + ) | Out-Null; + $Result += ('{0:X4}' -f $Value); + } + $Result = $Result -join ':'; + + return $Result; +} +function ConvertTo-BytesNextUnit() +{ + param ( + [string]$Value = $null, + [string]$Unit = $null, + [array]$Units = @('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB') + ); + + [string]$UnitValue = [string]::Format('{0}{1}', $Value, $Unit); + + while ($TRUE) { + $Unit = Get-IcingaNextUnitIteration -Unit $Unit -Units $Units; + [decimal]$NewValue = (Convert-Bytes -Value $UnitValue -Unit $Unit).Value; + if ($NewValue -ge 1.0) { + if ($Unit -eq $RetUnit) { + break; + } + $RetValue = [math]::Round([decimal]$NewValue, 2); + $RetUnit = $Unit; + } else { + if ([string]::IsNullOrEmpty($RetUnit)) { + $RetValue = $Value; + $RetUnit = 'B'; + } + break; + } + } + + return ([string]::Format('{0}{1}', $RetValue, $RetUnit)); +} +function ConvertFrom-IcingaSecureString() +{ + param([SecureString]$SecureString); + + if ($SecureString -eq $null) { + return ''; + } + + [IntPtr]$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) + [string]$String = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) + + return $String; +} +function ConvertTo-IcingaNumericTimeIndex() +{ + param ( + [int]$TimeValue = 0 + ); + + if ($TimeValue -lt 60) { + return ([string]::Format('{0}s', $TimeValue)); + } + + [decimal]$Minutes = $TimeValue / 60; + [decimal]$Seconds = $Minutes - [math]::Truncate($Minutes); + [decimal]$Minutes = [math]::Truncate($Minutes); + [decimal]$Seconds = [math]::Round(60 * $Seconds, 0); + + $TimeIndex = New-Object -TypeName 'System.Text.StringBuilder'; + $TimeIndex.Append([string]::Format('{0}m', $Minutes)) | Out-Null; + + if ($Seconds -ne 0) { + $TimeIndex.Append([string]::Format('{0}s', $Seconds)) | Out-Null; + } + + return $TimeIndex.ToString(); +} +function Get-IcingaPSObjectProperties() +{ + param( + $Object = $null, + [array]$Include = @(), + [array]$Exclude = @() + ); + + [hashtable]$RetValue = @{ }; + + if ($null -eq $Object) { + return $RetValue; + } + + foreach ($property in $Object.PSObject.Properties) { + [string]$DataType = $property.TypeNameOfValue; + + if ($Include.Count -ne 0 -And -Not ($Include -Contains $property.Name)) { + continue; + } + + if ($Exclude.Count -ne 0 -And $Exclude -Contains $property.Name) { + continue; + } + + if ($DataType.Contains('string') -or $DataType.Contains('int') -Or $DataType.Contains('bool')) { + $RetValue.Add( + $property.Name, + $property.Value + ); + } else { + try { + $RetValue.Add( + $property.Name, + (Get-IcingaPSObjectProperties -Object $property.Value) + ); + } catch { + $RetValue.Add( + $property.Name, + ([string]$property.Value) + ); + } + + } + } + + return $RetValue; +} +function Show-IcingaDirecorSelfServiceKey() +{ + $Path = 'IcingaDirector.SelfService.ApiKey'; + $Value = Get-IcingaPowerShellConfig $Path; + + if ($null -ne $Value) { + Write-IcingaConsoleNotice ([string]::Format('Self-Service Key: "{0}"', $Value)); + } else { + Write-IcingaConsoleWarning 'There is no Self-Service Api key configured on this system'; + } +} +function Get-IcingaServices() +{ + param ( + [array]$Service, + [array]$Exclude = @() + ); + + $ServiceInformation = Get-Service; + $ServiceWmiInfo = $null; + + if ($Service.Count -eq 0) { + $ServiceWmiInfo = Get-IcingaWindowsInformation Win32_Service; + } else { + try { + $ServiceWmiInfo = Get-IcingaWindowsInformation Win32_Service | + ForEach-Object { + foreach ($svc in $Service) { + if ($_.Name -Like $svc) { + return $_; + } + } + } | Select-Object StartName, Name, ExitCode, StartMode, PathName; + } catch { + Exit-IcingaThrowException -InputString $_.Exception.Message -StringPattern 'wildcard' -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.RegexError; + Exit-IcingaThrowException -CustomMessage $_.Exception.Message -ExceptionType 'Input' -ExceptionThrown $_.Exception.Message; + return $null; + } + } + + if ($null -eq $ServiceInformation) { + return $null; + } + + [hashtable]$ServiceData = @{ }; + + foreach ($si in $ServiceInformation) { + + [array]$DependentServices = $null; + [array]$DependingServices = $null; + $ServiceExitCode = 0; + [string]$ServiceUser = ''; + [string]$ServicePath = ''; + [int]$StartModeId = 5; + [string]$StartMode = 'Unknown'; + + if ((Test-IcingaArrayFilter -InputObject $si.ServiceName -Include $Service -Exclude $Exclude) -eq $FALSE) { + continue; + } + + foreach ($wmiService in $ServiceWmiInfo) { + if ($wmiService.Name -eq $si.ServiceName) { + $ServiceUser = $wmiService.StartName; + $ServicePath = $wmiService.PathName; + $ServiceExitCode = $wmiService.ExitCode; + if ([string]::IsNullOrEmpty($wmiService.StartMode) -eq $FALSE) { + $StartModeId = ([int]$IcingaEnums.ServiceWmiStartupType[$wmiService.StartMode]); + $StartMode = $IcingaEnums.ServiceStartupTypeName[$StartModeId]; + } + break; + } + } + + #Dependent / Child + foreach ($dependency in $si.DependentServices) { + if ($null -eq $DependentServices) { + $DependentServices = @(); + } + $DependentServices += $dependency.Name; + } + + #Depends / Parent + foreach ($dependency in $si.ServicesDependedOn) { + if ($null -eq $DependingServices) { + $DependingServices = @(); + } + $DependingServices += $dependency.Name; + } + + $ServiceData.Add( + $si.Name, @{ + 'metadata' = @{ + 'DisplayName' = $si.DisplayName; + 'ServiceName' = $si.ServiceName; + 'Site' = $si.Site; + 'Container' = $si.Container; + 'ServiceHandle' = $si.ServiceHandle; + 'Dependent' = $DependentServices; + 'Depends' = $DependingServices; + }; + 'configuration' = @{ + 'CanPauseAndContinue' = $si.CanPauseAndContinue; + 'CanShutdown' = $si.CanShutdown; + 'CanStop' = $si.CanStop; + 'Status' = @{ + 'raw' = [int]$si.Status; + 'value' = $si.Status; + }; + 'ServiceType' = @{ + 'raw' = [int]$si.ServiceType; + 'value' = $si.ServiceType; + }; + 'ServiceHandle' = $si.ServiceHandle; + 'StartType' = @{ + 'raw' = $StartModeId; + 'value' = $StartMode; + }; + 'ServiceUser' = $ServiceUser; + 'ServicePath' = $ServicePath; + 'ExitCode' = $ServiceExitCode; + } + } + ); + } + return $ServiceData; +} +<# +.SYNOPSIS + Returns interface ip address, which will be used for the host object within the icinga director. +.DESCRIPTION + Get-IcingaNetworkInterface returns the ip address of the interface, which will be used for the host object within the icinga director. + + More Information on https://github.com/Icinga/icinga-powershell-framework +.FUNCTIONALITY + This module is intended to be used to determine the interface ip address, during kickstart wizard, but will also function standalone. +.EXAMPLE + PS> Get-IcingaNetworkInterface 'icinga.com' + 192.168.243.88 +.EXAMPLE + PS> Get-IcingaNetworkInterface '8.8.8.8' + 192.168.243.88 +.PARAMETER IP + Used to specify either an IPv4, IPv6 address or an FQDN. +.INPUTS + System.String +.OUTPUTS + System.String + +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function Get-IcingaNetworkInterface() +{ + param( + [string]$IP + ); + + if ([string]::IsNullOrEmpty($IP)) { + Write-IcingaConsoleError 'Please specify a valid IP-Address or FQDN'; + return $null; + } + + # Ensure that we can still process on older Windows system where + # Get-NetRoute ist not available + if ((Test-IcingaFunction 'Get-NetRoute') -eq $FALSE) { + Write-IcingaConsoleWarning 'Your Windows system does not support "Get-NetRoute". A fallback solution is used to fetch the IP of the first Network Interface routing through 0.0.0.0' + return (Get-IcingaNetworkRoute).Interface; + } + + try { + [array]$IP = ([System.Net.Dns]::GetHostAddresses($IP)).IPAddressToString; + } catch { + Write-IcingaConsoleError 'Invalid IP was provided!'; + return $null; + } + + $IPBinStringMaster = ConvertTo-IcingaIPBinaryString -IP $IP; + + [hashtable]$InterfaceData = @{ }; + + $InterfaceInfo = Get-NetRoute; + $Counter = 0; + + foreach ( $Info in $InterfaceInfo ) { + $Counter++; + + $Divide = $Info.DestinationPrefix; + $IP, $Mask = $Divide.Split('/'); + + foreach ($destinationIP in $IPBinStringMaster) { + [string]$Key = ''; + [string]$MaskKey = ''; + <# IPv4 #> + if ($destinationIP.name -eq 'IPv4') { + if ($IP -like '*.*') { + if ([int]$Mask -lt 10) { + $MaskKey = [string]::Format('00{0}', $Mask); + } else { + $MaskKey = [string]::Format('0{0}', $Mask); + } + } + } + <# IPv6 #> + if ($destinationIP.name -eq 'IPv6') { + if ($IP -like '*:*') { + if ([int]$Mask -lt 10) { + $MaskKey = [string]::Format('00{0}', $Mask); + } elseif ([int]$Mask -lt 100) { + $MaskKey = [string]::Format('0{0}', $Mask); + } else { + $MaskKey = $Mask; + } + } + } + + $Key = [string]::Format('{0}-{1}', $MaskKey, $Counter); + + if ($InterfaceData.ContainsKey($Key)) { + continue; + } + + $InterfaceData.Add( + $Key, @{ + 'Binary IP String' = (ConvertTo-IcingaIPBinaryString -IP $IP).value; + 'Mask' = $Mask; + 'Interface' = $Info.ifIndex; + } + ); + } + } + + $InterfaceDataOrdered = $InterfaceData.GetEnumerator() | Sort-Object -Property Name -Descending; + $ExternalInterfaces = @{ }; + + foreach ( $Route in $InterfaceDataOrdered ) { + foreach ($destinationIP in $IPBinStringMaster) { + [string]$RegexPattern = [string]::Format("^.{{{0}}}", $Route.Value.Mask); + [string]$ToBeMatched = $Route.Value."Binary IP String"; + if ($null -eq $ToBeMatched) { + continue; + } + + $Match1=[regex]::Matches($ToBeMatched, $RegexPattern).Value; + $Match2=[regex]::Matches($destinationIP.Value, $RegexPattern).Value; + + If ($Match1 -like $Match2) { + $ExternalInterface = ((Get-NetIPAddress -InterfaceIndex $Route.Value.Interface -AddressFamily $destinationIP.Name -ErrorAction SilentlyContinue).IPAddress); + + # If no interface was found -> skip this entry + if ($null -eq $ExternalInterface) { + continue; + } + + if ($ExternalInterfaces.ContainsKey($ExternalInterface)) { + $ExternalInterfaces[$ExternalInterface].count += 1; + } else { + $ExternalInterfaces.Add( + $ExternalInterface, + @{ + 'count' = 1 + } + ); + } + } + } + } + + if ($ExternalInterfaces.Count -eq 0) { + foreach ($destinationIP in $IPBinStringMaster) { + $ExternalInterface = ((Get-NetIPAddress -InterfaceIndex (Get-NetRoute | Where-Object -Property DestinationPrefix -Like '0.0.0.0/0')[0].IfIndex -AddressFamily $destinationIP.name).IPAddress).split('%')[0]; + if ($ExternalInterfaces.ContainsKey($ExternalInterface)) { + $ExternalInterfaces[$ExternalInterface].count += 1; + } else { + $ExternalInterfaces.Add( + $ExternalInterface, + @{ + 'count' = 1 + } + ); + } + } + } + + $InternalCount = 0; + [array]$UseInterface = @(); + foreach ($interface in $ExternalInterfaces.Keys) { + $currentCount = $ExternalInterfaces[$interface].count; + if ($currentCount -gt $InternalCount) { + $InternalCount = $currentCount; + $UseInterface += $interface; + } + } + + # In case we found multiple interfaces, fallback to our + # 'route print' function and return this interface instead + if ($UseInterface.Count -ne 1) { + return (Get-IcingaNetworkRoute).Interface; + } + + return $UseInterface[0]; +} +function New-IcingaTemporaryFile() +{ + [string]$TmpFile = ''; + [string]$FilePath = ''; + + while ($TRUE) { + $TmpFile = [string]::Format('tmp_icinga{0}.tmp', (Get-Random)); + $FilePath = Join-Path $Env:TMP -ChildPath $TmpFile; + + if ((Test-Path $FilePath) -eq $FALSE) { + break; + } + } + + return (New-Item -Path $FilePath -ItemType File); +} +function Get-IcingaUserSID() +{ + param( + [string]$User + ); + + if ([string]::IsNullOrEmpty($User)) { + return $null; + } + + if ($User -eq 'LocalSystem' -Or $User -eq '.\LocalSystem') { + $User = 'NT Authority\SYSTEM'; + } + + $UserData = Split-IcingaUserDomain -User $User; + + try { + $NTUser = New-Object System.Security.Principal.NTAccount($UserData.Domain, $UserData.User); + $SecurityData = $NTUser.Translate([System.Security.Principal.SecurityIdentifier]); + } catch { + try { + # Try again but this time with our domain + $UserData.Domain = (Get-IcingaWindowsInformation -ClassName Win32_ComputerSystem).Domain; + $NTUser = New-Object System.Security.Principal.NTAccount($UserData.Domain, $UserData.User); + $SecurityData = $NTUser.Translate([System.Security.Principal.SecurityIdentifier]); + } catch { + return $null; + } + } + + if ($null -eq $SecurityData) { + return $null; + } + + return $SecurityData.Value; +} +function ConvertTo-ByteIEC() +{ + param( + [single]$Value, + [string]$Unit + ); + + switch ($Unit) { + { 'B', 'Byte' -contains $_ } { $result = $Value; $boolOption = $true; } + { 'KiB', 'Kibibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 10)); $boolOption = $true; } + { 'MiB', 'Mebibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 20)); $boolOption = $true; } + { 'GiB', 'Gibibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 30)); $boolOption = $true; } + { 'TiB', 'Tebibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 40)); $boolOption = $true; } + { 'PiB', 'Pebibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 50)); $boolOption = $true; } + default { + if (-Not $boolOption) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; + } + } + } + + return $result; +} + +function ConvertTo-Kibibyte() +{ + param( + [single]$Value, + [string]$Unit + ); + + switch ($Unit) { + { 'B', 'Byte' -contains $_ } { $result = ($Value / [math]::Pow(2, 10)); $boolOption = $true; } + { 'KiB', 'Kibibyte' -contains $_ } { $result = $Value; $boolOption = $true; } + { 'MiB', 'Mebibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 10)); $boolOption = $true; } + { 'GiB', 'Gibibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 20)); $boolOption = $true; } + { 'TiB', 'Tebibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 30)); $boolOption = $true; } + { 'PiB', 'Pebibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 40)); $boolOption = $true; } + default { + if (-Not $boolOption) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; + } + } + } + + return $result; +} + +function ConvertTo-Mebibyte() +{ + param( + [single]$Value, + [string]$Unit + ); + + switch ($Unit) { + { 'B', 'Byte' -contains $_ } { $result = ($Value / [math]::Pow(2, 20)); $boolOption = $true; } + { 'KiB', 'Kibibyte' -contains $_ } { $result = ($Value / [math]::Pow(2, 10)); $boolOption = $true; } + { 'MiB', 'Mebibyte' -contains $_ } { $result = $Value; $boolOption = $true; } + { 'GiB', 'Gibibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 10)); $boolOption = $true; } + { 'TiB', 'Tebibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 20)); $boolOption = $true; } + { 'PiB', 'Pebibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 30)); $boolOption = $true; } + default { + if (-Not $boolOption) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; + } + } + } + + return $result; +} + +function ConvertTo-Gibibyte() +{ + param( + [single]$Value, + [string]$Unit + ); + + switch ($Unit) { + { 'B', 'Byte' -contains $_ } { $result = ($Value / [math]::Pow(2, 30)); $boolOption = $true; } + { 'KiB', 'Kibibyte' -contains $_ } { $result = ($Value / [math]::Pow(2, 20)); $boolOption = $true; } + { 'MiB', 'Mebibyte' -contains $_ } { $result = ($Value / [math]::Pow(2, 10)); $boolOption = $true; } + { 'GiB', 'Gibibyte' -contains $_ } { $result = $Value; $boolOption = $true; } + { 'TiB', 'Tebibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 10)); $boolOption = $true; } + { 'PiB', 'Pebibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 20)); $boolOption = $true; } + default { + if (-Not $boolOption) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; + } + } + } + + return $result; +} + +function ConvertTo-Tebibyte() +{ + param( + [single]$Value, + [string]$Unit + ); + + switch ($Unit) { + { 'B', 'Byte' -contains $_ } { $result = ($Value / [math]::Pow(2, 40)); $boolOption = $true; } + { 'KiB', 'Kibibyte' -contains $_ } { $result = ($Value / [math]::Pow(2, 30)); $boolOption = $true; } + { 'MiB', 'Mebibyte' -contains $_ } { $result = ($Value / [math]::Pow(2, 20)); $boolOption = $true; } + { 'GiB', 'Gibibyte' -contains $_ } { $result = ($Value / [math]::Pow(2, 10)); $boolOption = $true; } + { 'TiB', 'Tebibyte' -contains $_ } { $result = $Value; $boolOption = $true; } + { 'PiB', 'Pebibyte' -contains $_ } { $result = ($Value * [math]::Pow(2, 10)); $boolOption = $true; } + default { + if (-Not $boolOption) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; + } + } + } + + return $result; +} + +function ConvertTo-Pebibyte() +{ + param( + [single]$Value, + [string]$Unit + ); + + switch ($Unit) { + { 'B', 'Byte' -contains $_ } { $result = ($Value / [math]::Pow(2, 50)); $boolOption = $true; } + { 'KiB', 'Kibibyte' -contains $_ } { $result = ($Value / [math]::Pow(2, 40)); $boolOption = $true; } + { 'MiB', 'Mebibyte' -contains $_ } { $result = ($Value / [math]::Pow(2, 30)); $boolOption = $true; } + { 'GiB', 'Gibibyte' -contains $_ } { $result = ($Value / [math]::Pow(2, 20)); $boolOption = $true; } + { 'TiB', 'Tebibyte' -contains $_ } { $result = ($Value / [math]::Pow(2, 10)); $boolOption = $true; } + { 'PiB', 'Pebibyte' -contains $_ } { $result = $Value; $boolOption = $true; } + default { + if (-Not $boolOption) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; + } + } + } + + return $result; +} +<# +.SYNOPSIS + Compare-IcingaUnixTimeWithDateTime compares a DateTime-Object with the current DateTime and returns the offset between these values as Integer +.DESCRIPTION + Compare-IcingaUnixTimeWithDateTime compares a DateTime-Object with the current DateTime and returns the offset between these values as Integer +.PARAMETER DateTime + DateTime object you want to compare with the Universal Time +.INPUTS + System.DateTime +.OUTPUTS + System.Int64 +#> +function Compare-IcingaUnixTimeWithDateTime() { + param ( + [datetime]$DateTime + ); + + # This is when the computer starts counting time + $UnixEpochStart = (New-Object DateTime 1970, 1, 1, 0, 0, 0, ([DateTimeKind]::Utc)); + # We convert the creation and current time to seconds + $CreationTime = [long][System.Math]::Floor((($DateTime.ToUniversalTime() - $UnixEpochStart).Ticks / [timespan]::TicksPerSecond)); + $CurrentTime = Get-IcingaUnixTime; + + # To find out, from the snapshot creation time to the current time, how many seconds are, + # you have to subtract from the (Current Time in s) the (Creation Time in s) + return ($CurrentTime - $CreationTime); +} +function ConvertFrom-TimeSpan() +{ + param ( + $Seconds = 0 + ); + + if (([string]$Seconds).Contains(',') -Or (Test-Numeric $Seconds)) { + [decimal]$Seconds = [decimal]([string]$Seconds).Replace(',', '.'); + } + + $Sign = ''; + if ($Seconds -lt 0) { + $Seconds = [math]::Abs($Seconds); + $Sign = '-'; + } + + if ((Test-Numeric $Seconds) -eq $FALSE) { + return $Seconds; + } + + $TimeSpan = [TimeSpan]::FromSeconds($Seconds); + + if ($TimeSpan.TotalDays -ge 1.0) { + return ( + [string]::Format( + '{0}{1}d', + $Sign, + ([math]::Round($TimeSpan.TotalDays, 2)) + ) + ); + } + if ($TimeSpan.TotalHours -ge 1.0) { + return ( + [string]::Format( + '{0}{1}h', + $Sign, + ([math]::Round($TimeSpan.TotalHours, 2)) + ) + ); + } + if ($TimeSpan.TotalMinutes -ge 1.0) { + return ( + [string]::Format( + '{0}{1}m', + $Sign, + ([math]::Round($TimeSpan.TotalMinutes, 2)) + ) + ); + } + if ($TimeSpan.TotalSeconds -ge 1.0) { + return ( + [string]::Format( + '{0}{1}s', + $Sign, + ([math]::Round($TimeSpan.TotalSeconds, 2)) + ) + ); + } + if ($TimeSpan.TotalMilliseconds -ge 1.0) { + return ( + [string]::Format( + '{0}{1}ms', + $Sign, + $TimeSpan.TotalMilliseconds + ) + ); + } + + if ($Seconds -lt 0.001) { + return ([string]::Format('{0}{1}us', $Sign, ([math]::Ceiling([decimal]($Seconds*[math]::Pow(10, 6)))))); + } + + return ([string]::Format('{0}s', $Seconds)); +} +function Get-IcingaNetworkInterfaceUnits() +{ + param ( + [decimal]$Value = 0, + [string]$Unit = '' + ); + + [hashtable]$InterfaceData = @{ + 'RawValue' = $Value; + 'LinkSpeed' = 0; + 'Unit' = 'Mbit' + }; + + if ([string]::IsNullOrEmpty($Unit) -eq $FALSE) { + $InterfaceData.LinkSpeed = $Value; + $InterfaceData.Unit = $Unit; + + return $InterfaceData; + } + + [decimal]$result = ($Value / [Math]::Pow(10, 6)); + + if ($result -ge 1000) { + $InterfaceData.LinkSpeed = [decimal]($result / 1000); + $InterfaceData.Unit = 'Gbit'; + } else { + $InterfaceData.LinkSpeed = $result; + $InterfaceData.Unit = 'Mbit'; + } + + return $InterfaceData; +} +<# +.SYNOPSIS + Calls the PowerShell Garbage-Collector to force freeing memory + in a more aggressive way. Also fixes an issue on older PowerShell + versions on which the Garbage-Collector is not clearing memory + properly +.DESCRIPTION + Calls the PowerShell Garbage-Collector to force freeing memory + in a more aggressive way. Also fixes an issue on older PowerShell + versions on which the Garbage-Collector is not clearing memory + properly +.PARAMETER ClearErrorStack + Also clears the current error stack to free additional memory +.PARAMETER SmartGC + Ensures that memory is not flushed whenever this function is called, but instead + every 30 attempts this function is called to reduce CPU load. Only works for + PowerShell sessions with "$Global:Icinga.Protected.ThreadName" being set +.EXAMPLE + Optimize-IcingaForWindowsMemory; +.EXAMPLE + Optimize-IcingaForWindowsMemory -ClearErrorStack; +.NOTES + Clears memory used by PowerShell in a more aggressive way +#> +function Optimize-IcingaForWindowsMemory() +{ + param ( + [switch]$ClearErrorStack = $FALSE, + [switch]$SmartGC = $FALSE + ); + + if ([string]::IsNullOrEmpty($Global:Icinga.Protected.ThreadName) -eq $FALSE -And $SmartGC) { + if ($Global:Icinga.Protected.GarbageCollector.ContainsKey($Global:Icinga.Protected.ThreadName) -eq $FALSE) { + $Global:Icinga.Protected.GarbageCollector.Add($Global:Icinga.Protected.ThreadName, 0); + + return; + } else { + $Global:Icinga.Protected.GarbageCollector[$Global:Icinga.Protected.ThreadName] += 1; + } + + if ($Global:Icinga.Protected.GarbageCollector[$Global:Icinga.Protected.ThreadName] -le 30) { + return; + } + + $Global:Icinga.Protected.GarbageCollector[$Global:Icinga.Protected.ThreadName] = 0; + } + + # Clear all errors within our error stack + if ($ClearErrorStack) { + $Error.Clear(); + } + + # Force the full collection of memory being used + [decimal]$MemoryConsumption = [System.GC]::GetTotalMemory('forcefullcollection'); + # Collect all generations of objects inside our memory and force the flushing of + # objects + [System.GC]::Collect([System.GC]::MaxGeneration, [System.GCCollectionMode]::Forced); + # Collect the remaining memory for a before/after delta + [decimal]$CollectedMemory = [System.GC]::GetTotalMemory('forcefullcollection'); + # Just for debugging + [decimal]$MemoryDelta = $MemoryConsumption - $CollectedMemory; + [bool]$Negate = $FALSE; + + if ($MemoryDelta -lt 0) { + $MemoryDelta = $MemoryDelta * -1; + $Negate = $TRUE; + } + + $MemoryConsumptionMiB = Convert-Bytes -Value $MemoryConsumption -Unit 'MiB'; + $CollectedMemoryMiB = Convert-Bytes -Value $CollectedMemory -Unit 'MiB'; + $MemoryDeltaMiB = Convert-Bytes -Value $MemoryDelta -Unit 'MiB'; + + # Print an optional debug message, allowing us to analyze memory behavior over time + Write-IcingaDebugMessage ` + -Message 'Calling Icinga for Windows Garbage-Collector. Memory overview below (Before, After, Flushed/Added)' ` + -Objects @( + ([string]::Format('{0} MiB', $MemoryConsumptionMiB.Value)), + ([string]::Format('{0} MiB', $CollectedMemoryMiB.Value)), + ([string]::Format('{1}{0} MiB', $MemoryDeltaMiB.Value, (&{ if ($Negate) { return '-'; } else { return ''; } }))) + ); +} +function ConvertTo-JsonUTF8Bytes() +{ + [CmdletBinding()] + param ( + [parameter(Mandatory = $TRUE, ValueFromPipeline = $TRUE)] + $InputObject = $null, + [int]$Depth = 10, + [switch]$Compress = $FALSE + ); + + $JsonBody = ConvertTo-Json -InputObject $InputObject -Depth 100 -Compress; + $UTF8Bytes = ([System.Text.Encoding]::UTF8.GetBytes($JsonBody)); + + # Do not remove the "," as we require to force our PowerShell to handle our return value + # as proper collection + return , $UTF8Bytes; +} +<# +.SYNOPSIS + Helper function to convert values to integer if possible +.DESCRIPTION + Converts an input value to integer if possible in any way. Otherwise it will return the object unmodified + + More Information on https://github.com/Icinga/icinga-powershell-framework +.FUNCTIONALITY + Converts an input value to integer if possible in any way. Otherwise it will return the object unmodified +.PARAMETER Value + Any value/object is analysed and if possible converted to an integer +.INPUTS + System.Object +.OUTPUTS + System.Integer + +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function ConvertTo-Integer() +{ + param ( + $Value, + [switch]$NullAsEmpty + ); + + if ($null -eq $Value) { + if ($NullAsEmpty) { + return ''; + } + + return 0; + } + + if ([string]::IsNullOrEmpty($Value)) { + if ($NullAsEmpty) { + return ''; + } + + return 0; + } + + if ((Test-Numeric $Value)) { + return $Value; + } + + $Type = $value.GetType().Name; + + if ($Type -eq 'GpoBoolean' -Or $Type -eq 'Boolean' -Or $Type -eq 'SwitchParameter') { + return [int]$Value; + } + + if ($Type -eq 'String') { + if ($Value.ToLower() -eq 'true' -Or $Value.ToLower() -eq 'yes' -Or $Value.ToLower() -eq 'y') { + return 1; + } + if ($Value.ToLower() -eq 'false' -Or $Value.ToLower() -eq 'no' -Or $Value.ToLower() -eq 'n') { + return 0; + } + } + + return $Value; +} +function Get-IcingaUsernameFromSID() +{ + param ( + [string]$SID + ); + + if ([string]::IsNullOrEmpty($SID)) { + Write-IcingaConsoleError 'You have to specify a SID'; + return $null; + } + + $UserData = New-Object System.Security.Principal.SecurityIdentifier $SID; + $UserObject = $UserData.Translate([System.Security.Principal.NTAccount]); + + return $UserObject.Value; +} +function Get-UnitPrefixSI() +{ + param( + [single]$Value + ); + + If ( $Value / [math]::Pow(10, 15) -ge 1 ) { + return 'PB' + } elseif ( $Value / [math]::Pow(10, 12) -ge 1 ) { + return 'TB' + } elseif ( $Value / [math]::Pow(10, 9) -ge 1 ) { + return 'GB' + } elseif ( $Value / [math]::Pow(10, 6) -ge 1 ) { + return 'MB' + } elseif ( $Value / [math]::Pow(10, 3) -ge 1 ) { + return 'KB' + } else { + return 'B' + } +} +<# +.SYNOPSIS + Tests for binary operators with -band if a specific Value contains binary + operators within a Compare array. In addition you can use a Namespace + argument to provide a hashtable in which your key values are included to + reduce the amount of code to write +.DESCRIPTION + Tests for binary operators with -band if a specific Value contains binary + operators within a Compare array. In addition you can use a Namespace + argument to provide a hashtable in which your key values are included to + reduce the amount of code to write +.EXAMPLE + PS>Test-IcingaBinaryOperator -Value Ok -Compare EmptyClass, InvalidNameSpace, PermissionError, Ok -Namespace $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo; + True +.EXAMPLE + PS>Test-IcingaBinaryOperator -Value 2 -Compare 1,4,8,16,32,64,128,256; + False +.PARAMETER Value + The value to check if it is included within the compare argument. This can either be + the name of the key for a Namespace or a numeric value +.PARAMETER Compare + An array of values to compare for and check if the value matches with the -band operator + The array can either contain the key names of your Namespace, numeric values or both combined +.PARAMETER Namespace + A hashtable object containing values you want to compare for. By providing a hashtable here + you can use the key names for each value on the Value and Compare argument +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Test-IcingaBinaryOperator() +{ + param ( + $Value = $null, + [array]$Compare = @(), + [hashtable]$Namespace = $null + ); + + [long]$BinaryValue = 0; + + foreach ($entry in $Compare) { + if ($null -ne $Namespace) { + if ($Namespace.ContainsKey($entry)) { + $BinaryValue += $Namespace[$entry]; + } else { + if (Test-Numeric $entry) { + $BinaryValue += $entry; + } + } + } else { + $BinaryValue += $entry; + } + } + + if ($null -ne $Value -and (Test-Numeric $Value)) { + if (($Value -band $BinaryValue) -eq $Value) { + return $TRUE; + } + } + + if ($null -ne $Namespace -and $Namespace.ContainsKey($Value)) { + if (($Namespace[$Value] -band $BinaryValue) -eq $Namespace[$Value]) { + return $TRUE; + } + } + + return $FALSE; +} +function Convert-IcingaCheckArgumentToPSObject() +{ + param ( + $Parameter = $null, + $CheckCommand = $null + ); + + $ParamValue = New-Object -TypeName PSObject; + + if ($null -eq $parameter) { + return $ParamValue; + } + + $ParameterName = $Parameter.name; + + if ([string]::IsNullOrEmpty($CheckCommand) -eq $FALSE) { + $CmdData = Get-Command $CheckCommand; + if ($CmdData.Parameters.ContainsKey($ParameterName)) { + if ($CmdData.Parameters[$ParameterName].Aliases.Count -ne 0) { + $ParameterName = $CmdData.Parameters[$ParameterName].Aliases[0]; + } + } + } + + $ParamValue | Add-Member -MemberType NoteProperty -Name 'type' -Value (New-Object -TypeName PSObject); + $ParamValue | Add-Member -MemberType NoteProperty -Name 'Description' -Value (New-Object -TypeName PSObject); + $ParamValue | Add-Member -MemberType NoteProperty -Name 'Attributes' -Value (New-Object -TypeName PSObject); + $ParamValue | Add-Member -MemberType NoteProperty -Name 'position' -Value $Parameter.position; + $ParamValue | Add-Member -MemberType NoteProperty -Name 'Name' -Value $ParameterName; + $ParamValue | Add-Member -MemberType NoteProperty -Name 'required' -Value $Parameter.required; + $ParamValue.type | Add-Member -MemberType NoteProperty -Name 'name' -Value $Parameter.type.name; + $ParamValue.Description | Add-Member -MemberType NoteProperty -Name 'Text' -Value $Parameter.Description.Text; + $ParamValue.Attributes | Add-Member -MemberType NoteProperty -Name 'ValidValues' -Value $null; + + return $ParamValue; +} +<# +.SYNOPSIS + Converts any kind of Icinga threshold with provided units + to the lowest base of the unit which makes sense. It does + support the Icinga plugin language, like ~:30, @10:40, 15:30, + ... + + The conversion does currently support the following units: + + Size: B, KB, MB, GB, TB, PT, KiB, MiB, GiB, TiB, PiB + Time: ms, s, m, h, d w, M, y +.DESCRIPTION + Converts any kind of Icinga threshold with provided units + to the lowest base of the unit. It does support the Icinga + plugin language, like ~:30, @10:40, 15:30, ... + + You can also provide date time values in the format of "yyyy/MM/dd HH:mm:ss" + and use Icinga for Windows plugin thresholds in combination. You have to escape + the ':' inside the date time value with a '`' to ensure the correct conversion. + + Example: + 2024/08/19 12`:42`:00 + + The conversion does currently support the following units: + + Size: B, KB, MB, GB, TB, PT, KiB, MiB, GiB, TiB, PiB + Time: ms, s, m, h, d w, M, y +.FUNCTIONALITY + Converts values with units to the lowest unit of this category. + Accepts Icinga Thresholds. +.EXAMPLE + PS>Convert-IcingaPluginThresholds -Threshold '20d'; + + Name Value + ---- ----- + EndRange + Unit s + StartRange + Threshold 1728000 + Mode 0 + Raw 20d + IsDateTime False + Value 1728000 +.EXAMPLE + PS>Convert-IcingaPluginThresholds -Threshold '5GB'; + + Name Value + ---- ----- + EndRange + Unit B + StartRange + Threshold 5000000000 + Mode 0 + Raw 5GB + IsDateTime False + Value 5000000000 +.EXAMPLE + PS>Convert-IcingaPluginThresholds -Threshold '10MB:20MB'; + + Name Value + ---- ----- + EndRange 20000000 + Unit B + StartRange 10000000 + Threshold 10000000:20000000 + Mode 3 + Raw 10MB:20MB + IsDateTime False + Value +.EXAMPLE + PS>Convert-IcingaPluginThresholds -Threshold '10m:1h'; + + Name Value + ---- ----- + EndRange 3600 + Unit s + StartRange 600 + Threshold 600:3600 + Mode 3 + Raw 10m:1h + Value +.EXAMPLE + PS>Convert-IcingaPluginThresholds -Threshold '@10m:1h'; + + Name Value + ---- ----- + EndRange 3600 + Unit s + StartRange 600 + Threshold @600:3600 + Mode 4 + Raw @10m:1h + IsDateTime False + Value +.EXAMPLE + PS>Convert-IcingaPluginThresholds -Threshold '~:1M'; + + Name Value + ---- ----- + EndRange + Unit s + StartRange + Threshold ~:2592000 + Mode 2 + Raw ~:1M + IsDateTime False + Value 2592000 +.EXAMPLE + PS>Convert-IcingaPluginThresholds -Threshold '@2024/08/19 12`:42`:00:2024/08/19 12`:42`:00'; + Name Value + ---- ----- + EndRange 133685377200000000 + Unit s + StartRange 133685377200000000 + Threshold @133685377200000000:133685377200000000 + Mode 4 + Raw @2024/08/19 12`:42`:00:2024/08/19 12`:42`:00 + IsDateTime True + Value +.INPUTS + System.String +.OUTPUTS + System.Hashtable +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Convert-IcingaPluginThresholds() +{ + param ( + [string]$Threshold = $null + ); + + [hashtable]$RetValue = @{ + 'Raw' = $Threshold; + 'Unit' = ''; + 'Threshold' = $null; + 'Value' = $null; + 'StartRange' = $null; + 'EndRange' = $null; + 'Mode' = $IcingaEnums.IcingaThresholdMethod.Default; + 'IsDateTime' = $FALSE; + }; + + if ([string]::IsNullOrEmpty($Threshold)) { + return $RetValue; + } + + # Always ensure we are using correct digits + $Threshold = $Threshold.Replace(',', '.'); + [array]$Content = @(); + + if ($Threshold.Contains(':')) { + # If we have more than one ':' inside our string, lets check if this is a date time value + # In case it is convert it properly to a FileTime we can work with later on + if ([Regex]::Matches($Threshold, '`:').Count -gt 1) { + [bool]$HasTilde = $FALSE; + [bool]$HasAt = $FALSE; + + if ($Threshold.Contains('@')) { + $HasAt = $TRUE; + } elseif ($Threshold.Contains('~')) { + $HasTilde = $TRUE; + } + + $Threshold = $Threshold.Replace('`:', '!').Replace('~', '').Replace('@', ''); + [array]$DatimeValueArray = $Threshold.Split(':'); + + try { + [array]$DateTimeValue = @(); + if ([string]::IsNullOrEmpty($DatimeValueArray[0]) -eq $FALSE) { + [array]$DateTimeValue += ([DateTime]::ParseExact($DatimeValueArray[0].Replace('!', ':'), 'yyyy\/MM\/dd HH:mm:ss', $null)).ToFileTime(); + } + if ([string]::IsNullOrEmpty($DatimeValueArray[1]) -eq $FALSE) { + [array]$DateTimeValue += ([DateTime]::ParseExact($DatimeValueArray[1].Replace('!', ':'), 'yyyy\/MM\/dd HH:mm:ss', $null)).ToFileTime(); + } + + if ($DateTimeValue.Count -gt 1) { + $Threshold = [string]::Join(':', $DateTimeValue); + $RetValue.Mode = $IcingaEnums.IcingaThresholdMethod.Between; + + if ($HasAt) { + $Threshold = [string]::Format('@{0}', $Threshold); + } + } elseif ($DatimeValueArray.Count -gt 1) { + if ($HasTilde) { + $Threshold = [string]::Format('~:{0}', $DateTimeValue[0]); + } else { + $Threshold = [string]::Format('{0}:', $DateTimeValue[0]); + } + } else { + $Threshold = $DateTimeValue[0]; + } + $RetValue.Unit = 's'; + $RetValue.IsDateTime = $TRUE; + } catch { + $RetValue.Threshold = $Threshold.Replace('!', '`:'); + Exit-IcingaThrowException -CustomMessage ([string]::Format('Could not convert the provided threshold value {0} to a valid Icinga for Windows range', $RetValue.Raw)) -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.InvalidThresholdValue -Force; + return $RetValue; + } + } else { + $RetValue.Mode = $IcingaEnums.IcingaThresholdMethod.Between; + } + + $Content = $Threshold.Split(':'); + if ($Content.Count -eq 2 -And ([string]::IsNullOrEmpty($Content[1]))) { + $RetValue.Mode = $IcingaEnums.IcingaThresholdMethod.Lower; + } + } else { + $Content += $Threshold; + } + + [array]$ConvertedValue = @(); + + foreach ($ThresholdValue in $Content) { + + [bool]$HasTilde = $FALSE; + [bool]$HasAt = $FALSE; + [bool]$Negate = $FALSE; + $Value = ''; + $WorkUnit = ''; + + if ($ThresholdValue.Contains('~')) { + $ThresholdValue = $ThresholdValue.Replace('~', ''); + $HasTilde = $TRUE; + $RetValue.Mode = $IcingaEnums.IcingaThresholdMethod.Greater; + } elseif ($ThresholdValue.Contains('@')) { + $HasAt = $TRUE; + $ThresholdValue = $ThresholdValue.Replace('@', ''); + $RetValue.Mode = $IcingaEnums.IcingaThresholdMethod.Outside; + } + + if ($ThresholdValue[0] -eq '-' -And $ThresholdValue.Length -ge 1) { + $Negate = $TRUE; + $ThresholdValue = $ThresholdValue.Substring(1, $ThresholdValue.Length - 1); + } + + if (($ThresholdValue -Match "(^-?[0-9]+)((\.|\,)[0-9]+)?(B|KB|MB|GB|TB|PT|KiB|MiB|GiB|TiB|PiB)$")) { + $WorkUnit = 'B'; + if ([string]::IsNullOrEmpty($RetValue.Unit) -eq $FALSE -And $RetValue.Unit -ne $WorkUnit) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.MultipleUnitUsage -Force; + } + $Value = (Convert-Bytes -Value $ThresholdValue -Unit $WorkUnit).Value; + $RetValue.Unit = $WorkUnit; + } elseif (($ThresholdValue -Match "(^-?[0-9]+)((\.|\,)[0-9]+)?(ms|s|m|h|d|w|M|y)$")) { + $WorkUnit = 's'; + if ([string]::IsNullOrEmpty($RetValue.Unit) -eq $FALSE -And $RetValue.Unit -ne $WorkUnit) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.MultipleUnitUsage -Force; + } + $Value = (ConvertTo-Seconds -Value $ThresholdValue); + $RetValue.Unit = $WorkUnit; + } elseif (($ThresholdValue -Match "(^[\d\.]*) ?(%)")) { + $WorkUnit = '%'; + $Value = ([string]$ThresholdValue).Replace(' ', '').Replace('%', ''); + $RetValue.Unit = $WorkUnit; + } elseif (($ThresholdValue -Match "(^-?[0-9]+)((\.|\,)[0-9]+)?(Kbit|Mbit|Gbit|Tbit|Pbit|Ebit|Zbit|Ybit)$")) { + $Value = $Matches[1]; + $RetValue.Unit = $Matches[4]; + } else { + # Load all other units/values generically + [string]$StrNumeric = ''; + [bool]$FirstChar = $TRUE; + [bool]$Delimiter = $FALSE; + foreach ($entry in ([string]($ThresholdValue)).ToCharArray()) { + if ((Test-Numeric $entry) -Or ($entry -eq '.' -And $Delimiter -eq $FALSE)) { + $StrNumeric += $entry; + $FirstChar = $FALSE; + if ($entry -eq '.') { + $Delimiter = $TRUE; + } + } else { + if ([string]::IsNullOrEmpty($RetValue.Unit) -And $FirstChar -eq $FALSE) { + $RetValue.Unit = $entry; + } else { + $StrNumeric = ''; + $RetValue.Unit = ''; + break; + } + } + } + if ([string]::IsNullOrEmpty($StrNumeric)) { + $Value = $ThresholdValue; + } else { + $Value = [decimal]$StrNumeric; + } + } + + if ((Test-Numeric $Value) -And $Negate) { + $Value = $Value * -1; + } elseif ($Negate) { + $Value = [string]::Format('-{0}', $Value); + } + + if ($HasTilde) { + $ConvertedValue += [string]::Format('~{0}', $Value); + } elseif ($HasAt) { + $ConvertedValue += [string]::Format('@{0}', $Value); + } else { + $ConvertedValue += $Value; + } + } + + [string]$Value = [string]::Join(':', $ConvertedValue); + + switch ($RetValue.Mode) { + $IcingaEnums.IcingaThresholdMethod.Default { + $RetValue.Value = $ConvertedValue[0]; + + if ([string]::IsNullOrEmpty($RetValue.Value)) { + Exit-IcingaThrowException -CustomMessage ([string]::Format('Could not convert the provided threshold value {0} to a valid Icinga for Windows range', $RetValue.Raw)) -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.InvalidThresholdValue -Force; + return $RetValue; + } + break; + }; + $IcingaEnums.IcingaThresholdMethod.Lower { + $RetValue.Value = $ConvertedValue[0]; + + if ([string]::IsNullOrEmpty($RetValue.Value)) { + Exit-IcingaThrowException -CustomMessage ([string]::Format('Could not convert the provided threshold value {0} to a valid Icinga for Windows range', $RetValue.Raw)) -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.InvalidThresholdValue -Force; + return $RetValue; + } + break; + }; + $IcingaEnums.IcingaThresholdMethod.Greater { + $RetValue.Value = $ConvertedValue[1]; + + if ([string]::IsNullOrEmpty($RetValue.Value)) { + Exit-IcingaThrowException -CustomMessage ([string]::Format('Could not convert the provided threshold value {0} to a valid Icinga for Windows range', $RetValue.Raw)) -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.InvalidThresholdValue -Force; + return $RetValue; + } + break; + }; + $IcingaEnums.IcingaThresholdMethod.Between { + $RetValue.StartRange = [decimal]$ConvertedValue[0]; + $RetValue.EndRange = [decimal]$ConvertedValue[1]; + + if ([string]::IsNullOrEmpty($RetValue.StartRange) -Or [string]::IsNullOrEmpty($RetValue.EndRange)) { + Exit-IcingaThrowException -CustomMessage ([string]::Format('Could not convert the provided threshold value {0} to a valid Icinga for Windows range', $RetValue.Raw)) -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.InvalidThresholdValue -Force; + return $RetValue; + } + break; + }; + $IcingaEnums.IcingaThresholdMethod.Outside { + $RetValue.StartRange = [decimal]($ConvertedValue[0].Replace('@', '')); + $RetValue.EndRange = [decimal]$ConvertedValue[1]; + + if ([string]::IsNullOrEmpty($RetValue.StartRange) -Or [string]::IsNullOrEmpty($RetValue.EndRange)) { + Exit-IcingaThrowException -CustomMessage ([string]::Format('Could not convert the provided threshold value {0} to a valid Icinga for Windows range', ($RetValue.Raw))) -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.InvalidThresholdValue -Force; + return $RetValue; + } + break; + }; + } + + if ([string]::IsNullOrEmpty($Value) -eq $FALSE -And $Value.Contains(':') -eq $FALSE) { + if ((Test-Numeric $Value)) { + $RetValue.Threshold = [decimal]$Value; + $RetValue.Value = [decimal]$Value; + return $RetValue; + } + } + + # Always ensure we are using correct digits + $Value = ([string]$Value).Replace(',', '.'); + $RetValue.Threshold = $Value; + + return $RetValue; +} +function Write-IcingaFileSecure() +{ + param ( + [string]$File, + $Value + ); + + if ([string]::IsNullOrEmpty($File)) { + return; + } + + if ($null -eq $Value) { + $Value = ''; + } elseif ($Value.GetType().Name.ToLower() -eq 'hashtable') { + $Value = ConvertTo-Json -InputObject $Value -Depth 100; + } elseif (([string]($Value.GetType().BaseType)).ToLower() -eq 'array') { + $Value = $Value | Out-String; + } elseif ($Value.GetType().Name.ToLower() -eq 'pscustomobject') { + $Value = ConvertTo-Json -InputObject $Value -Depth 100; + } else { + $Value = $Value | Out-String; + } + + if ((Test-Path $File) -eq $FALSE) { + try { + New-Item -ItemType File -Path $File -ErrorAction Stop | Out-Null; + } catch { + Exit-IcingaThrowException -InputString $_.Exception -CustomMessage $File -StringPattern 'System.UnauthorizedAccessException' -ExceptionType 'Permission' -ExceptionThrown $IcingaExceptions.Permission.CacheFolder; + Exit-IcingaThrowException -CustomMessage $_.Exception -ExceptionType 'Unhandled' -Force; + } + } + + [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; + } +} +function ConvertFrom-JsonUTF8() +{ + [CmdletBinding()] + param ( + [parameter(Mandatory = $TRUE, ValueFromPipeline = $TRUE)] + $InputObject = $null + ); + + # We need to properly encode our String to UTF8 + $ContentBytes = [System.Text.Encoding]::Default.GetBytes($InputObject); + $UTF8String = [System.Text.Encoding]::UTF8.GetString($ContentBytes); + + # Return the correct encoded JSON + return (ConvertFrom-Json -InputObject $UTF8String); +} +function Get-IcingaHashtableItem() +{ + param( + $Hashtable, + $Key, + $NullValue = $null + ); + + if ($null -eq $Hashtable) { + return $NullValue; + } + + if ($Hashtable.ContainsKey($Key) -eq $FALSE) { + return $NullValue; + } + + return $Hashtable[$Key]; +} +function Show-IcingaEventLogAnalysis() +{ + param ( + [string]$LogName = 'Application' + ); + + Write-IcingaConsoleNotice 'Analysing EventLog "{0}"...' -Objects $LogName; + + Start-IcingaTimer 'EventLog Analyser'; + + try { + [array]$BasicLogArray = Get-WinEvent -ListLog $LogName -ErrorAction Stop; + $BasicLogData = $BasicLogArray[0]; + + if ($null -ne $BasicLogArray) { + $BasicLogArray.Dispose(); + $BasicLogArray = $null; + } + } catch { + Write-IcingaConsoleError 'Failed to fetch data for EventLog "{0}". Probably this log does not exist.' -Objects $LogName; + return; + } + + Write-IcingaConsoleNotice 'Logging Mode: {0}' -Objects $BasicLogData.LogMode; + Write-IcingaConsoleNotice 'Maximum Size: {0} GB' -Objects ([math]::Round((Convert-Bytes -Value $BasicLogData.MaximumSizeInBytes -Unit 'GB').value, 2)); + Write-IcingaConsoleNotice 'Current Entries: {0}' -Objects $BasicLogData.RecordCount; + + [hashtable]$LogAnalysis = @{ + 'Day' = @{ + 'Entries' = @{ }; + 'Count' = 0; + 'Average' = 0; + 'Maximum' = 0; + }; + 'Hour' = @{ + 'Entries' = @{ }; + 'Count' = 0; + 'Average' = 0; + 'Maximum' = 0; + }; + 'Minute' = @{ + 'Entries' = @{ }; + 'Count' = 0; + 'Average' = 0; + 'Maximum' = 0; + }; + }; + + $LogData = Get-WinEvent -LogName $LogName; + [string]$NewestEntry = $null; + [string]$OldestEntry = $null; + + foreach ($entry in $LogData) { + [string]$DayOfLogging = $entry.TimeCreated.ToString('yyyy\/MM\/dd'); + [string]$HourOfLogging = $entry.TimeCreated.ToString('yyyy\/MM\/dd-HH'); + [string]$MinuteOfLogging = $entry.TimeCreated.ToString('yyyy\/MM\/dd-HH-mm'); + + $OldestEntry = $entry.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'); + + if ([string]::IsNullOrEmpty($NewestEntry)) { + $NewestEntry = $OldestEntry; + } + + if ($LogAnalysis.Day.Entries.ContainsKey($DayOfLogging) -eq $FALSE) { + $LogAnalysis.Day.Entries.Add($DayOfLogging, 0); + } + + if ($LogAnalysis.Hour.Entries.ContainsKey($HourOfLogging) -eq $FALSE) { + $LogAnalysis.Hour.Entries.Add($HourOfLogging, 0); + } + + if ($LogAnalysis.Minute.Entries.ContainsKey($MinuteOfLogging) -eq $FALSE) { + $LogAnalysis.Minute.Entries.Add($MinuteOfLogging, 0); + } + + $LogAnalysis.Day.Entries[$DayOfLogging] += 1; + $LogAnalysis.Hour.Entries[$HourOfLogging] += 1; + $LogAnalysis.Minute.Entries[$MinuteOfLogging] += 1; + + $LogAnalysis.Day.Count += 1; + $LogAnalysis.Hour.Count += 1; + $LogAnalysis.Minute.Count += 1; + + $LogAnalysis.Day.Average = [math]::Ceiling($LogAnalysis.Day.Count / $LogAnalysis.Day.Entries.Count); + $LogAnalysis.Hour.Average = [math]::Ceiling($LogAnalysis.Hour.Count / $LogAnalysis.Hour.Entries.Count); + $LogAnalysis.Minute.Average = [math]::Ceiling($LogAnalysis.Minute.Count / $LogAnalysis.Minute.Entries.Count); + } + + if ($null -ne $LogData) { + $LogData.Dispose(); + $LogData = $null; + } + + foreach ($value in $LogAnalysis.Day.Entries.Values) { + $LogAnalysis.Day.Maximum = Get-IcingaValue -Value $value -Compare $LogAnalysis.Day.Maximum -Maximum; + } + foreach ($value in $LogAnalysis.Hour.Entries.Values) { + $LogAnalysis.Hour.Maximum = Get-IcingaValue -Value $value -Compare $LogAnalysis.Hour.Maximum -Maximum; + } + foreach ($value in $LogAnalysis.Minute.Entries.Values) { + $LogAnalysis.Minute.Maximum = Get-IcingaValue -Value $value -Compare $LogAnalysis.Minute.Maximum -Maximum; + } + Stop-IcingaTimer 'EventLog Analyser'; + + Write-IcingaConsoleNotice 'Average Logs per Day: {0}' -Objects $LogAnalysis.Day.Average; + Write-IcingaConsoleNotice 'Average Logs per Hour: {0}' -Objects $LogAnalysis.Hour.Average; + Write-IcingaConsoleNotice 'Average Logs per Minute: {0}' -Objects $LogAnalysis.Minute.Average; + Write-IcingaConsoleNotice 'Maximum Logs per Day: {0}' -Objects $LogAnalysis.Day.Maximum; + Write-IcingaConsoleNotice 'Maximum Logs per Hour: {0}' -Objects $LogAnalysis.Hour.Maximum; + Write-IcingaConsoleNotice 'Maximum Logs per Minute: {0}' -Objects $LogAnalysis.Minute.Maximum; + Write-IcingaConsoleNotice 'Newest entry timestamp: {0}' -Objects $NewestEntry; + Write-IcingaConsoleNotice 'Oldest entry timestamp: {0}' -Objects $OldestEntry; + Write-IcingaConsoleNotice 'Analysing Time: {0}s' -Objects ([math]::Round((Get-IcingaTimer 'EventLog Analyser').Elapsed.TotalSeconds, 2)); +} +function ConvertTo-IcingaCommandArgumentString() +{ + param ( + [string]$Command = '', + $CommandArguments = $null + ); + + [hashtable]$Arguments = @{ }; + + if ($CommandArguments -Is [PSCustomObject]) { + foreach ($entry in $CommandArguments.PSObject.Properties) { + $Arguments.Add($entry.Name, $entry.Value); + } + } elseif ($CommandArguments -Is [hashtable]) { + $Arguments = $CommandArguments; + } else { + return ''; + } + + foreach ($cmdArg in $Arguments.Keys) { + $PrintValue = $Arguments[$cmdArg]; + [string]$StringArg = ([string]$cmdArg).Replace('-', ''); + + if ($PrintValue.GetType().Name -eq 'Boolean') { + if ((Get-Command $Command).Parameters.$StringArg.ParameterType.Name -eq 'SwitchParameter') { + $PrintValue = ''; + } else { + if ($PrintValue) { + $PrintValue = '$TRUE'; + } else { + $PrintValue = '$FALSE'; + } + } + } elseif ($PrintValue.GetType().Name -eq 'String') { + $PrintValue = (ConvertFrom-IcingaArrayToString -Array $PrintValue -AddQuotes -UseSingleQuotes); + } elseif ($PrintValue.GetType().Name -eq 'Object[]') { + $PrintValue = (ConvertFrom-IcingaArrayToString -Array $PrintValue -AddQuotes -UseSingleQuotes); + } + if ([string]::IsNullOrEmpty($PrintValue)) { + $PrintArguments += ([string]::Format('{0} ', $cmdArg)); + } else { + $PrintArguments += ([string]::Format('{0} {1} ', $cmdArg, $PrintValue)); + } + } + + while ($PrintArguments[-1] -eq ' ') { + $PrintArguments = $PrintArguments.SubString(0, $PrintArguments.Length - 1); + } + + return $PrintArguments; +} +<# +.SYNOPSIS + Tests whether a value is numeric +.DESCRIPTION + This module tests whether a value is numeric + + More Information on https://github.com/Icinga/icinga-powershell-framework +.EXAMPLE + PS> Test-Numeric 32 + True +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> +function Test-Numeric ($number) { + return $number -Match "^-?[0-9]\d*(\.\d+)?$"; +} +<# +.SYNOPSIS + Tests if a Add-Type function is already installed inside the current + PowerShell session +.DESCRIPTION + Tests if a Add-Type function is already installed inside the current + PowerShell session +.PARAMETER Type + The name of the function being added +.EXAMPLE + Test-IcingaAddTypeExis -Type 'IcingaDiskAttributes'; +#> +function Test-IcingaAddTypeExist() +{ + param ( + [string]$Type = $null + ); + + if ([string]::IsNullOrEmpty($Type)) { + return $FALSE; + } + + [hashtable]$LoadedTypes = Get-IcingaPrivateEnvironmentVariable -Name 'AddTypeFunctions'; + + if ($null -eq $LoadedTypes) { + $LoadedTypes = @{ }; + } + + if ($LoadedTypes.ContainsKey($Type)) { + return $TRUE; + } + + foreach ($entry in [System.AppDomain]::CurrentDomain.GetAssemblies()) { + if ($entry.GetTypes() -Match $Type) { + $LoadedTypes.Add($Type, $TRUE); + + Set-IcingaPrivateEnvironmentVariable -Name 'AddTypeFunctions' -Value $LoadedTypes; + + return $TRUE; + } + } + + return $FALSE; +} +function New-StringTree() +{ + param( + [int]$Spacing + ) + + if ($Spacing -eq 0) { + return ''; + } + + [string]$spaces = '\_ '; + + while ($Spacing -gt 1) { + $Spacing -= 1; + $spaces = ' ' + $spaces; + } + + return $spaces; +} +function Get-UnitPrefixIEC() +{ + param( + [single]$Value + ); + + If ( $Value / [math]::Pow(2, 50) -ge 1 ) { + return 'PiB' + } elseif ( $Value / [math]::Pow(2, 40) -ge 1 ) { + return 'TiB' + } elseif ( $Value / [math]::Pow(2, 30) -ge 1 ) { + return 'GiB' + } elseif ( $Value / [math]::Pow(2, 20) -ge 1 ) { + return 'MiB' + } elseif ( $Value / [math]::Pow(2, 10) -ge 1 ) { + return 'KiB' + } else { + return 'B' + } +} + +function Add-IcingaWhiteSpaceToString() +{ + param ( + [string]$Text = '', + [int]$Length = 0 + ); + + [int]$LengthOffset = $Length - $Text.Length; + + while ($LengthOffset -ge 0) { + $Text += ' '; + $LengthOffset -= 1; + } + + return $Text; +} +<# +.SYNOPSIS + Fixes the current encoding hell for arguments by taking every argument + parsed from Icinga and converting it from PowerShell native encoding + to UTF8 +.DESCRIPTION + Fixes the current encoding hell for arguments by taking every argument + parsed from Icinga and converting it from PowerShell native encoding + to UTF8 +.PARAMETER Arguments + The array of arguments for re-encoding. By default, this could be $args + for calls from Exit-IcingaExecutePlugin +.EXAMPLE + PS> [hashtable]$ConvertedArgs = ConvertTo-IcingaPowerShellArguments -Command $CheckCommand -Arguments $args; +#> + +function ConvertTo-IcingaPowerShellArguments() +{ + param ( + [string]$Command = '', + [array]$Arguments = @() + ); + + if ([string]::IsNullOrEmpty($Command)) { + return @{ }; + } + + $CmdData = Get-Command $Command -ErrorAction SilentlyContinue; + [array]$CmdAllowedArgs = $CmdData.Parameters.Keys; + + # Ensure we do not cause exceptions along the border in case the plugin is not installed + if ($null -eq $CmdAllowedArgs) { + return @{ }; + } else { + # We need to manually add the "-ThresholdInterval" argument, as this is an artificial + # one we do need to take care off + $CmdAllowedArgs += 'ThresholdInterval'; + } + + # Ensure we not only add the parameter name to our allow list but also possible aliases + foreach ($entry in $CmdData.Parameters.Keys) { + if ($CmdData.Parameters[$entry].Aliases.Count -eq 0) { + continue; + } + + foreach ($cmdAlias in $CmdData.Parameters[$entry].Aliases) { + if ($CmdAllowedArgs -NotContains $cmdAlias) { + $CmdAllowedArgs += $cmdAlias; + } + } + } + + [hashtable]$IcingaArguments = @{ }; + [int]$ArgumentIndex = 0; + + while ($ArgumentIndex -lt $Arguments.Count) { + # Check if the current position is a string + if ($Arguments[$ArgumentIndex] -IsNot [string]) { + # Continue if we are not a string (argument) + $ArgumentIndex += 1; + continue; + } + + # check_by_icingaforwindows arguments -> not required for any plugin execution + if ($Arguments[$ArgumentIndex] -eq '-IcingaForWindowsRemoteExecution' -Or $Arguments[$ArgumentIndex] -eq '-IcingaForWindowsJEARemoteExecution') { + $ArgumentIndex += 1; + continue; + } + + # Check if our string value is a argument contained inside the command being executed + if ($CmdAllowedArgs -Contains ($Arguments[$ArgumentIndex].SubString(1, $Arguments[$ArgumentIndex].Length - 1)) -eq $FALSE) { + # Continue if we are not an argument + $ArgumentIndex += 1; + continue; + } + + # First convert our argument + [string]$Argument = ConvertTo-IcingaUTF8Value -InputObject $Arguments[$ArgumentIndex]; + # Cut the first '-' + $Argument = $Argument.Substring(1, $Argument.Length - 1); + + # Check if there is anything beyond this argument, if not + # -> We are a switch argument, adding TRUE; + if (($ArgumentIndex + 1) -ge $Arguments.Count) { + if ($IcingaArguments.ContainsKey($Argument) -eq $FALSE) { + $IcingaArguments.Add($Argument, $TRUE); + } + $ArgumentIndex += 1; + continue; + } + + # Check if our next value in the array is a string + if ($Arguments[$ArgumentIndex + 1] -Is [string]) { + [string]$NextValue = $Arguments[$ArgumentIndex + 1]; + + # If our next value on the index is an argument in our command + # -> The current argument seems to be a switch argument + if ($CmdAllowedArgs -Contains ($NextValue.SubString(1, $NextValue.Length - 1))) { + if ($IcingaArguments.ContainsKey($Argument) -eq $FALSE) { + $IcingaArguments.Add($Argument, $TRUE); + } + $ArgumentIndex += 1; + continue; + } + + # It could be that we parse strings without quotation which is broken because on how + # Icinga is actually writing the arguments, let's fix this by building the string ourselves + [int]$ReadStringIndex = $ArgumentIndex; + $StringValue = New-Object -TypeName 'System.Text.StringBuilder'; + while ($TRUE) { + # Check if we read beyond our array + if (($ReadStringIndex + 1) -ge $Arguments.Count) { + break; + } + + # Check if the next element is no longer a string element + if ($Arguments[$ReadStringIndex + 1] -IsNot [string]) { + break; + } + + [string]$NextValue = $Arguments[$ReadStringIndex + 1]; + + # Check the next string element and evaluate if it is an argument for our command + if ($CmdAllowedArgs -Contains ($NextValue.SubString(1, $NextValue.Length - 1))) { + break; + } + + # If we already added elements to our string builder before, add a whitespace + if ($StringValue.Length -ne 0) { + $StringValue.Append(' ') | Out-Null; + } + + # Append our string value to the string builder + $StringValue.Append($NextValue) | Out-Null; + $ReadStringIndex += 1; + } + + # Add our argument with the string builder value, in case we had something to add there + if ($StringValue.Length -ne 0) { + if ($IcingaArguments.ContainsKey($Argument) -eq $FALSE) { + $IcingaArguments.Add($Argument, (ConvertTo-IcingaUTF8Value -InputObject $StringValue.ToString())); + } + $ArgumentIndex += 1; + continue; + } + } + + # All Remaining values + + # If we are an array object, handle empty arrays + if ($Arguments[$ArgumentIndex + 1] -Is [array]) { + if ($null -eq $Arguments[$ArgumentIndex + 1] -Or ($Arguments[$ArgumentIndex + 1]).Count -eq 0) { + if ($IcingaArguments.ContainsKey($Argument) -eq $FALSE) { + $IcingaArguments.Add($Argument, @()); + } + $ArgumentIndex += 1; + continue; + } + } + + if ($IcingaArguments.ContainsKey($Argument) -eq $FALSE) { + # Add everything else + $IcingaArguments.Add( + $Argument, + (ConvertTo-IcingaUTF8Value -InputObject $Arguments[$ArgumentIndex + 1]) + ); + } + + $ArgumentIndex += 1; + } + + return $IcingaArguments; +} +<# +.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; + [bool]$UTF8BOM = $FALSE; + + if ((Get-FileEncoding -Path $File) -eq 'UTF8-BOM') { + $UTF8BOM = $TRUE; + } + + $UTF8Encoding = New-Object System.Text.UTF8Encoding $UTF8BOM; + $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'; +<# + # Helper class allowing to easily convert strings into SecureStrings + # and vice-versa + #> +function ConvertTo-IcingaSecureString() +{ + param ( + [string]$String + ); + + if ([string]::IsNullOrEmpty($String)) { + return $null; + } + + return (ConvertTo-SecureString -AsPlainText $string -Force); +} +function Format-IcingaPerfDataLabel() +{ + param( + $PerfData, + [switch]$MultiOutput = $FALSE + ); + + if ($MultiOutput) { + return (($PerfData) -Replace '[\W]', ''); + } + + $Output = ((($PerfData) -Replace ' ', '_') -Replace '[\W]', ''); + + while ($Output.Contains('__')) { + $Output = $Output.Replace('__', '_'); + } + + # Remove all special characters and spaces on label names + return $Output; +} +<# +.SYNOPSIS + Used to convert an IPv4 address to binary. +.DESCRIPTION + ConvertTo-IcingaIPv6 returns a binary string based on the given IPv4 address. + + More Information on https://github.com/Icinga/icinga-powershell-framework +.FUNCTIONALITY + This module is intended to be used to convert an IPv4 address to binary string. Its recommended to use ConvertTo-IcingaIPBinaryString as a smart function instead. +.PARAMETER IP + Used to specify an IPv4 address. +.INPUTS + System.String +.OUTPUTS + System.String + +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function ConvertTo-IcingaIPv4BinaryString() +{ + param( + [string]$IP + ); + + try { + $IP = $IP -split '\.' | ForEach-Object { + [System.Convert]::ToString($_, 2).PadLeft(8, '0'); + } + $IP = $IP -join ''; + $IP = $IP -replace '\s', ''; + } catch { + # Todo: Should we handle errors? It might happen due to faulty routes or unhandled route config + # we throw errors which should have no effect at all + return $null; + } + + return @{ + 'value' = $IP; + 'name' = 'IPv4' + } +} +<# +.SYNOPSIS + Fetch the used interface for our Windows System +.DESCRIPTION + Newer Windows systems provide a Cmdlet 'Get-NetRoute' for fetching the + network route configurations. Older systems however do not provide this + and to ensure some sort of backwards compatibility, we will have a look + on our route configuration and return the first valid interface found +.FUNCTIONALITY + This Cmdlet will return first valid IP for our interface +.EXAMPLE + PS>Get-IcingaNetworkRoute +.OUTPUTS + System.Array +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function Get-IcingaNetworkRoute() +{ + $RouteConfig = (&route print | Where-Object { + $_.TrimStart() -Like "0.0.0.0*"; + }).Split() | Where-Object { + return $_; + }; + + $Interface = @{ + 'Destination' = $RouteConfig[0]; + 'Netmask' = $RouteConfig[1]; + 'Gateway' = $RouteConfig[2]; + 'Interface' = $RouteConfig[3]; + 'Metric' = $RouteConfig[4]; + } + + return $Interface; +} +<# +.SYNOPSIS + Converts unit sizes to byte. +.DESCRIPTION + This module converts a given unit size to byte. + e.g Kilobyte to Byte. + + More Information on https://github.com/Icinga/icinga-powershell-framework +.EXAMPLE + PS> ConvertTo-Byte -Unit TB 200 + 200000000000000 +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function ConvertTo-ByteSI() +{ + param( + [single]$Value, + [string]$Unit + ); + + switch ($Unit) { + { 'B', 'Byte' -contains $_ } { $result = $Value; $boolOption = $true; } + { 'KB', 'Kilobyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 3)); $boolOption = $true; } + { 'MB', 'Megabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 6)); $boolOption = $true; } + { 'GB', 'Gigabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 9)); $boolOption = $true; } + { 'TB', 'Terabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 12)); $boolOption = $true; } + { 'PB', 'Petabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 15)); $boolOption = $true; } + default { + if (-Not $boolOption) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; + } + } + } + + return $result; +} + +<# +.SYNOPSIS + Converts unit sizes to Kilobyte. +.DESCRIPTION + This module converts a given unit size to Kilobyte. + e.g byte to Kilobyte. + + More Information on https://github.com/Icinga/icinga-powershell-framework +.EXAMPLE + PS> ConvertTo-Kilobyte -Unit TB 200 + 200000000000 +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function ConvertTo-Kilobyte() +{ + param( + [single]$Value, + [string]$Unit + ); + + switch ($Unit) { + { 'B', 'Byte' -contains $_ } { $result = ($Value / [math]::Pow(10, 3)); $boolOption = $true; } + { 'KB', 'Kilobyte' -contains $_ } { $result = $Value; $boolOption = $true; } + { 'MB', 'Megabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 3)); $boolOption = $true; } + { 'GB', 'Gigabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 6)); $boolOption = $true; } + { 'TB', 'Terabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 9)); $boolOption = $true; } + { 'PB', 'Petabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 12)); $boolOption = $true; } + default { + if (-Not $boolOption) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; + } + } + } + + return $result; +} + +<# +.SYNOPSIS + Converts unit sizes to Megabyte. +.DESCRIPTION + This module converts a given unit size to Megabyte. + e.g byte to Megabyte. + + More Information on https://github.com/Icinga/icinga-powershell-framework +.EXAMPLE + PS> ConvertTo-Kilobyte -Unit TB 200 + 200000000 +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function ConvertTo-Megabyte() +{ + param( + [single]$Value, + [string]$Unit + ); + + switch ($Unit) { + { 'B', 'Byte' -contains $_ } { $result = ($Value / [math]::Pow(10, 6)); $boolOption = $true; } + { 'KB', 'Kilobyte' -contains $_ } { $result = ($Value / [math]::Pow(10, 3)); $boolOption = $true; } + { 'MB', 'Megabyte' -contains $_ } { $result = $Value; $boolOption = $true; } + { 'GB', 'Gigabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 3)); $boolOption = $true; } + { 'TB', 'Terabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 6)); $boolOption = $true; } + { 'PB', 'Petabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 9)); $boolOption = $true; } + default { + if (-Not $boolOption) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; + } + } + } + + return $result; +} + +<# +.SYNOPSIS + Converts unit sizes to Gigabyte. +.DESCRIPTION + This module converts a given unit size to Gigabyte. + e.g byte to Gigabyte. + + More Information on https://github.com/Icinga/icinga-powershell-framework +.EXAMPLE + PS> ConvertTo-Gigabyte -Unit TB 200 + 200000 +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function ConvertTo-Gigabyte() +{ + param( + [single]$Value, + [string]$Unit + ); + + switch ($Unit) { + { 'B', 'Byte' -contains $_ } { $result = ($Value / [math]::Pow(10, 9)); $boolOption = $true; } + { 'KB', 'Kilobyte' -contains $_ } { $result = ($Value / [math]::Pow(10, 6)); $boolOption = $true; } + { 'MB', 'Megabyte' -contains $_ } { $result = ($Value / [math]::Pow(10, 3)); $boolOption = $true; } + { 'GB', 'Gigabyte' -contains $_ } { $result = $Value; $boolOption = $true; } + { 'TB', 'Terabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 3)); $boolOption = $true; } + { 'PB', 'Petabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 6)); $boolOption = $true; } + default { + if (-Not $boolOption) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; + } + } + } + + return $result; +} + +<# +.SYNOPSIS + Converts unit sizes to Terabyte. +.DESCRIPTION + This module converts a given unit size to Terabyte. + e.g byte to Terabyte. + + More Information on https://github.com/Icinga/icinga-powershell-framework +.EXAMPLE + PS> ConvertTo-Terabyte -Unit GB 2000000 + 2000 +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function ConvertTo-Terabyte() +{ + param( + [single]$Value, + [string]$Unit + ); + + switch ($Unit) { + { 'B', 'Byte' -contains $_ } { $result = ($Value / [math]::Pow(10, 12)); $boolOption = $true; } + { 'KB', 'Kilobyte' -contains $_ } { $result = ($Value / [math]::Pow(10, 9)); $boolOption = $true; } + { 'MB', 'Megabyte' -contains $_ } { $result = ($Value / [math]::Pow(10, 6)); $boolOption = $true; } + { 'GB', 'Gigabyte' -contains $_ } { $result = ($Value / [math]::Pow(10, 3)); $boolOption = $true; } + { 'TB', 'Terabyte' -contains $_ } { $result = $Value; $boolOption = $true; } + { 'PB', 'Petabyte' -contains $_ } { $result = ($Value * [math]::Pow(10, 3)); $boolOption = $true; } + default { + if (-Not $boolOption) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; + } + } + } + + return $result; +} + +<# +.SYNOPSIS + Converts unit sizes to Petabyte. +.DESCRIPTION + This module converts a given unit size to Petabyte. + e.g byte to Petabyte. + + More Information on https://github.com/Icinga/icinga-powershell-framework +.EXAMPLE + PS> ConvertTo-Petabyte -Unit GB 2000000 + 2 +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function ConvertTo-Petabyte() +{ + param( + [single]$Value, + [string]$Unit + ); + + switch ($Unit) { + { 'B', 'Byte' -contains $_ } { $result = ($Value / [math]::Pow(10, 15)); $boolOption = $true; } + { 'KB', 'Kilobyte' -contains $_ } { $result = ($Value / [math]::Pow(10, 12)); $boolOption = $true; } + { 'MB', 'Megabyte' -contains $_ } { $result = ($Value / [math]::Pow(10, 9)); $boolOption = $true; } + { 'GB', 'Gigabyte' -contains $_ } { $result = ($Value / [math]::Pow(10, 6)); $boolOption = $true; } + { 'TB', 'Terabyte' -contains $_ } { $result = ($Value / [math]::Pow(10, 3)); $boolOption = $true; } + { 'PB', 'Petabyte' -contains $_ } { $result = $Value; $boolOption = $true; } + default { + if (-Not $boolOption) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; + } + } + } + + return $result; +} +function ConvertFrom-Percent() +{ + param ( + $Value = $null, + $Percent = $null, + [int]$Digits = 0 + ); + + if ($null -eq $Value -Or $null -eq $Percent) { + return 0; + } + + return ([math]::Round(($Value / 100 * $Percent), $Digits)); +} +function ConvertFrom-IcingaArrayToString() +{ + param ( + [array]$Array = @(), + [switch]$AddQuotes = $FALSE, + [switch]$UseSingleQuotes = $FALSE, + [switch]$SecureContent = $FALSE + ); + + if ($null -eq $Array -Or $Array.Count -eq 0) { + if ($AddQuotes) { + if ($UseSingleQuotes) { + return "''"; + } + + return '""'; + } + + return ''; + } + + [array]$NewArray = @(); + + if ($AddQuotes) { + foreach ($entry in $Array) { + if ($SecureContent) { + $entry = '***'; + } + if ($UseSingleQuotes) { + $NewArray += ([string]::Format("'{0}'", $entry)); + } else { + $NewArray += ([string]::Format('"{0}"', $entry)); + } + } + } else { + if ($SecureContent) { + foreach ($entry in $Array) { + $NewArray += '***'; + } + } else { + $NewArray = $Array; + } + } + + return ([string]::Join(', ', $NewArray)); +} +function Pop-IcingaArrayListItem() +{ + param( + [System.Collections.ArrayList]$Array + ); + + if ($null -eq $Array) { + return $null; + } + + if ($Array.Count -eq 0) { + return $null; + } + + $Content = $Array[0]; + $Array.RemoveAt(0); + + return $Content; +} +function Get-StringSha1() +{ + param ( + [string]$Content + ); + + $CryptoAlgorithm = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider; + $ContentHash = [System.Text.Encoding]::UTF8.GetBytes($Content); + $ContentBytes = $CryptoAlgorithm.ComputeHash($ContentHash); + $OutputHash = ''; + + foreach ($byte in $ContentBytes) { + $OutputHash += $byte.ToString() + } + + return $OutputHash; +} +function Convert-Bytes() +{ + param( + [string]$Value, + [string]$Unit + ); + + # Ensure we always use proper formatting of values + $Value = $Value.Replace(',', '.'); + + If (($Value -Match "(^-?[0-9]+)((\.|\,)[0-9]+)?(B|KB|MB|GB|TB|PT|KiB|MiB|GiB|TiB|PiB)$") -eq $FALSE) { + $Value = [string]::Format('{0}B', $Value); + } + + If (($Value -Match "(^-?[0-9]+)((\.|\,)[0-9]+)?(B|KB|MB|GB|TB|PT|KiB|MiB|GiB|TiB|PiB)$")) { + [single]$CurrentValue = $Matches[1]; + [string]$CurrentUnit = $Matches[4]; + + switch ($CurrentUnit) { + { 'KiB', 'MiB', 'GiB', 'TiB', 'PiB' -contains $_ } { $CurrentValue = ConvertTo-ByteIEC $CurrentValue $CurrentUnit; $boolOption = $true; } + { 'KB', 'MB', 'GB', 'TB', 'PB' -contains $_ } { $CurrentValue = ConvertTo-ByteSI $CurrentValue $CurrentUnit; $boolOption = $true; } + } + + switch ($Unit) { + { 'B' -contains $_ } { $FinalValue = $CurrentValue; $boolOption = $true; } + { 'KB' -contains $_ } { $FinalValue = ConvertTo-Kilobyte $CurrentValue -Unit B; $boolOption = $true; } + { 'MB' -contains $_ } { $FinalValue = ConvertTo-Megabyte $CurrentValue -Unit B; $boolOption = $true; } + { 'GB' -contains $_ } { $FinalValue = ConvertTo-Gigabyte $CurrentValue -Unit B; $boolOption = $true; } + { 'TB' -contains $_ } { $FinalValue = ConvertTo-Terabyte $CurrentValue -Unit B; $boolOption = $true; } + { 'PB' -contains $_ } { $FinalValue = ConvertTo-Petabyte $CurrentValue -Unit B; $boolOption = $true; } + { 'KiB' -contains $_ } { $FinalValue = ConvertTo-Kibibyte $CurrentValue -Unit B; $boolOption = $true; } + { 'MiB' -contains $_ } { $FinalValue = ConvertTo-Mebibyte $CurrentValue -Unit B; $boolOption = $true; } + { 'GiB' -contains $_ } { $FinalValue = ConvertTo-Gibibyte $CurrentValue -Unit B; $boolOption = $true; } + { 'TiB' -contains $_ } { $FinalValue = ConvertTo-Tebibyte $CurrentValue -Unit B; $boolOption = $true; } + { 'PiB' -contains $_ } { $FinalValue = ConvertTo-Petabyte $CurrentValue -Unit B; $boolOption = $true; } + + default { + if (-Not $boolOption) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; + } + } + } + return @{'value' = ([decimal]$FinalValue); 'pastunit' = $CurrentUnit; 'endunit' = $Unit }; + } + + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.ConversionUnitMissing -Force; +} +function Format-IcingaDigitCount() +{ + param( + [string]$Value, + [int]$Digits, + [string]$Symbol = 0 + ); + + if ([string]::IsNullOrEmpty($Value)) { + return $Value; + } + + $CurrentLength = $Value.Length; + if ($CurrentLength -ge $Digits) { + return $Value; + } + + while ($Value.Length -lt $Digits) { + $Value = [string]::Format('{0}{1}', $Symbol, $Value); + } + + return $Value; +} +function Set-IcingaPSLocation() +{ + param ( + [string]$Path = (Get-Location) + ); + + if ([string]::IsNullOrEmpty($Path)) { + return; + } + + if ((Test-Path $Path) -eq $FALSE) { + return; + } + + [string]$IfWRootPath = Get-IcingaForWindowsRootPath; + [string]$CurrentPath = Get-Location; + + if ($CurrentPath -Like ([string]::Format('{0}*', $Path))) { + Set-Location -Path $IfWRootPath; + } +} +function Remove-IcingaHashtableItem() +{ + param( + $Hashtable, + $Key + ); + + if ($null -eq $Hashtable) { + return; + } + + if ($Hashtable.ContainsKey($Key)) { + $Hashtable.Remove($Key); + } +} +function New-IcingaNewLine() +{ + return "`r`n"; +} +function New-IcingaVersionObject() +{ + param ( + [array]$Version + ); + + if ($null -eq $Version -Or $Version.Count -gt 4) { + return (New-Object System.Version); + } + + return (New-Object System.Version $Version); +} +function Start-IcingaProcess() +{ + param( + [string]$Executable, + [string]$Arguments, + [switch]$FlushNewLines + ); + + $processData = New-Object System.Diagnostics.ProcessStartInfo; + $processData.FileName = $Executable; + $processData.RedirectStandardError = $true; + $processData.RedirectStandardOutput = $true; + $processData.UseShellExecute = $false; + $processData.Arguments = $Arguments; + + $process = New-Object System.Diagnostics.Process; + $process.StartInfo = $processData; + $process.Start() | Out-Null; + + $stdout = $process.StandardOutput.ReadToEnd(); + $stderr = $process.StandardError.ReadToEnd(); + $process.WaitForExit(); + + if ($flushNewLines) { + $stdout = $stdout.Replace("`n", '').Replace("`r", ''); + $stderr = $stderr.Replace("`n", '').Replace("`r", ''); + } else { + if ($stdout.Contains("`n")) { + $stdout = $stdout.Substring(0, $stdout.LastIndexOf("`n")); + } + } + return @{ + 'Message' = $stdout; + 'Error' = $stderr; + 'ExitCode' = $process.ExitCode; + }; +} +function Test-IcingaDecimal() +{ + param ( + $Value = $null + ); + + [hashtable]$RetValue = @{ + 'Value' = $Value; + 'Decimal' = $FALSE; + }; + + if ($null -eq $Value -Or [string]::IsNullOrEmpty($Value)) { + return $RetValue; + } + + $TmpValue = ([string]$Value).Replace(',', '.'); + + if ((Test-Numeric $TmpValue) -eq $FALSE) { + return $RetValue; + } + + $RetValue.Value = [decimal]$TmpValue; + $RetValue.Decimal = $TRUE; + + return $RetValue; +} +<# +.SYNOPSIS + Converts Icinga Network configuration from FQDN to IP +.DESCRIPTION + This Cmdlet will convert a given Icinga Endpoint configuration based + on a FQDN to a IPv4 based configuration and returns nothing of the + FQDN could not be resolved +.FUNCTIONALITY + Converts Icinga Network configuration from FQDN to IP +.EXAMPLE + PS>Convert-IcingaEndpointsToIPv4 -NetworkConfig @( '[icinga2.example.com]:5665' ); +.PARAMETER NetworkConfig + An array of Icinga endpoint or single network configuration, like '[icinga2.example.com]:5665' + which will be converted to IP based configuration +.INPUTS + System.Array +.OUTPUTS + System.Hashtable +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> +function Convert-IcingaEndpointsToIPv4() +{ + param ( + [array]$NetworkConfig + ); + + [array]$ResolvedNetwork = @(); + [array]$UnresolvedNetwork = @(); + [bool]$HasUnresolved = $FALSE; + [string]$Domain = $ENV:UserDNSDomain; + + foreach ($entry in $NetworkConfig) { + $Network = Get-IPConfigFromString -IPConfig $entry; + try { + $ResolvedIP = [System.Net.Dns]::GetHostAddresses($Network.address); + $ResolvedNetwork += $entry.Replace($Network.address, $ResolvedIP); + } catch { + # Once we failed in first place, try to lookup the "FQDN" with our host domain + # we are in. Might resolve some issues if our DNS is not knowing the plain + # hostname and untable to resolve it + try { + $ResolvedIP = [System.Net.Dns]::GetHostAddresses( + [string]::Format( + '{0}.{1}', + $Network.address, + $Domain + ) + ); + $ResolvedNetwork += $entry.Replace($Network.address, $ResolvedIP); + } catch { + $UnresolvedNetwork += $Network.address; + $HasUnresolved = $TRUE; + } + } + } + + return @{ + 'Network' = $ResolvedNetwork; + 'HasErrors' = $HasUnresolved; + 'Unresolved' = $UnresolvedNetwork; + }; +} +function Join-WebPath() +{ + param( + [string]$Path, + [string]$ChildPath + ); + + if ([string]::IsNullOrEmpty($Path) -Or [string]::IsNullOrEmpty($ChildPath)) { + return $Path; + } + + [int]$Length = $Path.Length; + [int]$Slash = $Path.LastIndexOf('/') + 1; + + if ($Length -eq $Slash) { + $Path = $Path.Substring(0, $Path.Length - 1); + } + + if ($ChildPath[0] -eq '/') { + return ([string]::Format('{0}{1}', $Path, $ChildPath)); + } + + return ([string]::Format('{0}/{1}', $Path, $ChildPath)); +} +function New-IcingaCheckCommand() +{ + param( + [string]$Name = '', + [array]$Arguments = @( + 'Warning', + 'Critical', + '[switch]NoPerfData', + '[int]Verbose' + ) + ); + + if ([string]::IsNullOrEmpty($Name) -eq $TRUE) { + throw 'Please specify a command name'; + } + + if ($Name -match 'Invoke' -or $Name -match 'IcingaCheck') { + throw 'Please specify a command name only without PowerShell Cmdlet naming'; + } + + [string]$CommandName = [string]::Format( + 'Invoke-IcingaCheck{0}', + (Get-Culture).TextInfo.ToTitleCase($Name.ToLower()) + ); + + [string]$CommandFile = [string]::Format( + 'icinga-powershell-{0}.psm1', + $Name.ToLower() + ); + [string]$PSDFile = [string]::Format( + 'icinga-powershell-{0}.psd1', + $Name.ToLower() + ); + + [string]$ModuleFolder = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath ( + [string]::Format('icinga-powershell-{0}', $Name.ToLower()) + ); + [string]$ScriptFile = Join-Path -Path $ModuleFolder -ChildPath $CommandFile; + [string]$PSDFile = Join-Path -Path $ModuleFolder -ChildPath $PSDFile; + + if ((Test-Path $ModuleFolder) -eq $TRUE) { + throw 'This module folder does already exist.'; + } + + if ((Test-Path $ScriptFile) -eq $TRUE) { + throw 'This check command does already exist.'; + } + + New-Item -Path $ModuleFolder -ItemType Directory | Out-Null; + + Add-Content -Path $ScriptFile -Value ''; + Add-Content -Path $ScriptFile -Value "function $CommandName()"; + Add-Content -Path $ScriptFile -Value "{"; + + if ($Arguments.Count -ne 0) { + Add-Content -Path $ScriptFile -Value " param("; + [int]$index = $Arguments.Count - 1; + foreach ($argument in $Arguments) { + + if ($argument.Contains('$') -eq $FALSE) { + if ($argument.Contains(']') -eq $TRUE) { + $splittedArguments = $argument.Split(']'); + $argument = [string]::Format('{0}]${1}', $splittedArguments[0], $splittedArguments[1]); + } else { + $argument = [string]::Format('${0}', $argument); + } + } + + if ($index -ne 0) { + [string]$content = [string]::Format('{0},', $argument); + } else { + [string]$content = [string]::Format('{0}', $argument); + } + Add-Content -Path $ScriptFile -Value " $content"; + + $index -= 1; + } + Add-Content -Path $ScriptFile -Value " );"; + } + + Add-Content -Path $ScriptFile -Value ""; + Add-Content -Path $ScriptFile -Value ' <# Icinga Basic Check-Plugin Template. Below you will find an example structure. #>'; + Add-Content -Path $ScriptFile -Value ([string]::Format(' $CheckPackage = New-IcingaCheckPackage -Name {0}New Package{0} -OperatorAnd -Verbose $Verbose;', "'")); + Add-Content -Path $ScriptFile -Value ([string]::Format(' $IcingaCheck = New-IcingaCheck -Name {0}New Check{0} -Value 10 -Unit {0}%{0}', "'")); + Add-Content -Path $ScriptFile -Value ([string]::Format(' $IcingaCheck.WarnOutOfRange($Warning).CritOutOfRange($Critical) | Out-Null;', "'")); + Add-Content -Path $ScriptFile -Value ([string]::Format(' $CheckPackage.AddCheck($IcingaCheck);', "'")); + Add-Content -Path $ScriptFile -Value ""; + Add-Content -Path $ScriptFile -Value ([string]::Format(' return (New-IcingaCheckresult -Check $CheckPackage -NoPerfData $NoPerfData -Compile);', "'")); + + Add-Content -Path $ScriptFile -Value "}"; + + Write-IcingaConsoleNotice ([string]::Format('The Check-Command "{0}" was successfully added.', $CommandName)); + + # Try to open the default Editor for the new Cmdlet + $DefaultEditor = (Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.psm1\OpenWithList' -Name a).a; + $DefaultEditor = $DefaultEditor.Replace('.exe', ''); + + New-ModuleManifest ` + -Path $PSDFile ` + -ModuleToProcess $CommandFile ` + -RequiredModules @('icinga-powershell-framework') ` + -FunctionsToExport @('*') ` + -CmdletsToExport @('*') ` + -VariablesToExport '*' | Out-Null; + + Unblock-IcingaPowerShellFiles -Path $ModuleFolder; + + Import-Module $ScriptFile -Global; + + if ([string]::IsNullOrEmpty($DefaultEditor) -eq $FALSE -And ($null -eq (Get-Command $DefaultEditor -ErrorAction SilentlyContinue)) -And ((Test-Path $DefaultEditor) -eq $FALSE)) { + Write-IcingaConsoleWarning 'No default editor for .psm1 files found. Specify a default editor to automatically open the newly generated check plugin.'; + return; + } + + & $DefaultEditor "$ScriptFile"; +} +function Get-IcingaNextUnitIteration() +{ + param ( + [string]$Unit = '', + [array]$Units = @() + ); + + [bool]$Found = $FALSE; + + foreach ($entry in $Units) { + if ($Found) { + return $entry; + } + if ($entry -eq $Unit) { + $Found = $TRUE; + } + } + + return ''; +} +<# +.SYNOPSIS + Sets nummeric values to be negative +.DESCRIPTION + This module sets a numeric value to be negative. + e.g 12 to -12 + + More Information on https://github.com/Icinga/icinga-powershell-framework +.EXAMPLE + PS> Set-NumericNegative 32 + -32 +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + + +function Set-NumericNegative() +{ + param( + $Value + ); + + $Value = $Value * -1; + + return $Value; +}<# +.SYNOPSIS + Reads data from a cache file of the Framework and returns its content +.DESCRIPTION + Allows a developer to read data from certain cache files to either speed up + loading procedures, to store content to not lose data on restarts of a daemon + or to build data tables over time +.FUNCTIONALITY + Returns cached data for specific content +.EXAMPLE + PS>Get-IcingaCacheData -Space 'sc_daemon' -CacheStore 'checkresult_store' -KeyName 'Invoke-IcingaCheckCPU'; +.PARAMETER Space + The individual space to read from. This is targeted to a folder the cache data is written to under icinga-powershell-framework/cache/ +.PARAMETER CacheStore + This is targeted to a sub-folder under icinga-powershell-framework/cache// +.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 + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> +function Get-IcingaCacheData() +{ + param( + [string]$Space, + [string]$CacheStore, + [string]$KeyName, + [switch]$TempFile = $FALSE, + [switch]$AsObject = $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; + } + + $Content = Read-IcingaFileSecure -File $CacheFile; + + if ([string]::IsNullOrEmpty($Content)) { + return $null; + } + + try { + $cacheData = ConvertFrom-Json -InputObject ([string]$Content); + } catch { + Write-IcingaEventMessage -EventId 1104 -Namespace 'Framework' -ExceptionObject $_ -Objects $CacheFile; + return $null; + } + + if ($AsObject -Or [string]::IsNullOrEmpty($KeyName)) { + return $cacheData; + } else { + return $cacheData.$KeyName; + } +} +function Test-IcingaCacheDataTempFile() +{ + param ( + [string]$Space, + [string]$CacheStore, + [string]$KeyName + ); + + # Once the file is written successully, validate it is fine + $tmpContent = Get-IcingaCacheData -Space $Space -CacheStore $CacheStore -KeyName $KeyName -TempFile; + + if ($null -eq $tmpContent) { + # File is corrupt or empty + return $FALSE; + } + + return $TRUE; +} +<# +.SYNOPSIS + Writes data to a cache file for the Framework +.DESCRIPTION + Allows a developer to write data to certain cache files to either speed up + loading procedures, to store content to not lose data on restarts of a daemon + or to build data tables over time +.FUNCTIONALITY + Writes data for specific value to a cache file +.EXAMPLE + PS>Set-IcingaCacheData -Space 'sc_daemon' -CacheStore 'checkresult_store' -KeyName 'Invoke-IcingaCheckCPU' -Value @{ 'CachedData' = 'MyValue' }; +.PARAMETER Space + The individual space to write to. This is targeted to a folder the cache data is written to under icinga-powershell-framework/cache/ +.PARAMETER CacheStore + This is targeted to a sub-folder under icinga-powershell-framework/cache// +.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 Value + The actual value to store within the cache file. This can be any kind of value, as long as it is convertable to JSON +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +.NOTES +#> + +function Set-IcingaCacheData() +{ + param ( + [string]$Space, + [string]$CacheStore, + [string]$KeyName, + $Value + ); + + $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 -KeyName $KeyName)) { + Copy-IcingaCacheTempFile -CacheFile $CacheFile -CacheTmpFile $CacheTmpFile; + } + + if ((Test-Path $CacheFile)) { + $cacheData = Get-IcingaCacheData -Space $Space -CacheStore $CacheStore -KeyName $KeyName -AsObject; + } else { + try { + 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; + } + } + + if ($null -eq $cacheData -or $cacheData.Count -eq 0) { + $cacheData = @{ + $KeyName = $Value + }; + } else { + if ($cacheData.PSObject.Properties.Name -ne $KeyName) { + $cacheData | Add-Member -MemberType NoteProperty -Name $KeyName -Value $Value -Force; + } else { + $cacheData.$KeyName = $Value; + } + } + + # 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 -KeyName $KeyName) -eq $FALSE) { + Remove-ItemSecure -Path $CacheTmpFile -Retries 5 -Force | Out-Null; + return; + } + + Copy-IcingaCacheTempFile -CacheFile $CacheFile -CacheTmpFile $CacheTmpFile; +} +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; +} +function New-IcingaProgressStatus() +{ + param ( + [string]$Name = '', + [int]$CurrentValue = 0, + [int]$MaxValue = 1, + [string]$Message = 'Processing Icinga for Windows', + [string]$Status = '{0}% Complete', + [switch]$Details = $FALSE, + [switch]$PrintErrors = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError -Message 'Failed to create new progress status. You have to specify a name.' -DropMessage:$(-Not $PrintErrors); + return; + } + + if ($MaxValue -le 0) { + Write-IcingaConsoleError -Message 'Failed to create new progress status. The maximum value has to be larger than 0.' -DropMessage:$(-Not $PrintErrors); + return; + } + + if ($Global:Icinga.Private.ProgressStatus.ContainsKey($Name)) { + Write-IcingaConsoleError -Message 'Failed to create new progress status. A progress status with this name is already active. Use "Complete-IcingaProgressStatus" to remove it.' -DropMessage:$(-Not $PrintErrors); + return; + } + + $Global:Icinga.Private.ProgressStatus.Add( + $Name, + @{ + 'CurrentValue' = $CurrentValue; + 'MaxValue' = $MaxValue; + 'Message' = $Message; + 'Status' = $Status; + 'Details' = ([bool]$Details); + } + ); + + $ProgressPreference = 'Continue'; +} +function Complete-IcingaProgressStatus() +{ + param ( + [string]$Name = '' + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError -Message 'Failed to complete progress status. You have to specify a name.'; + return; + } + + if ($Global:Icinga.Private.ProgressStatus.ContainsKey($Name) -eq $FALSE) { + return; + } + + if ($Global:Icinga.Private.ProgressStatus[$Name].Details) { + $Message = [string]::Format('{0}: {1}/{2}', $Global:Icinga.Private.ProgressStatus[$Name].Message, $Global:Icinga.Private.ProgressStatus[$Name].MaxValue, $Global:Icinga.Private.ProgressStatus[$Name].MaxValue); + } else { + $Message = $Global:Icinga.Private.ProgressStatus[$Name].Message; + } + + $ProgressPreference = 'Continue'; + Write-Progress -Activity $Message -Status ([string]::Format($Global:Icinga.Private.ProgressStatus[$Name].Status, 100)) -PercentComplete 100 -Completed; + + $Global:Icinga.Private.ProgressStatus.Remove($Name); + + $ProgressPreference = 'SilentlyContinue'; +} +function Write-IcingaProgressStatus() +{ + param ( + [string]$Name = '', + [int]$AddValue = 1, + [switch]$PrintErrors = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError -Message 'Failed to write progress status. You have to specify a name.' -DropMessage:$(-Not $PrintErrors); + return; + } + + if ($Global:Icinga.Private.ProgressStatus.ContainsKey($Name) -eq $FALSE) { + Write-IcingaConsoleError -Message 'Failed to write progress status. A progress status with the name "{0}" does not exist. You have to create it first with "New-IcingaProgressStatus".' -Objects $Name -DropMessage:$(-Not $PrintErrors); + return; + } + + $Global:Icinga.Private.ProgressStatus[$Name].CurrentValue += $AddValue; + + $ProgressValue = [math]::Round($Global:Icinga.Private.ProgressStatus[$Name].CurrentValue / $Global:Icinga.Private.ProgressStatus[$Name].MaxValue * 100, 0); + + if ($Global:Icinga.Private.ProgressStatus[$Name].Details) { + $Message = [string]::Format('{0}: {1}/{2}', $Global:Icinga.Private.ProgressStatus[$Name].Message, $Global:Icinga.Private.ProgressStatus[$Name].CurrentValue, $Global:Icinga.Private.ProgressStatus[$Name].MaxValue); + } else { + $Message = $Global:Icinga.Private.ProgressStatus[$Name].Message; + } + + if ($ProgressValue -ge 100) { + Complete-IcingaProgressStatus -Name $Name; + return; + } + + $ProgressPreference = 'Continue'; + + Write-Progress -Activity $Message -Status ([string]::Format($Global:Icinga.Private.ProgressStatus[$Name].Status, $ProgressValue)) -PercentComplete $ProgressValue; +} +function Set-IcingaForWindowsServiceJEAProfile() +{ + [string]$JeaProfile = Get-IcingaJEAContext; + $IcingaForWindowsService = Get-IcingaForWindowsServiceData; + + if ([string]::IsNullOrEmpty($IcingaForWindowsService.FullPath) -Or (Test-Path $IcingaForWindowsService.FullPath) -eq $FALSE) { + return; + } + + [string]$PreparedServicePath = [string]::Format( + '\"{0}\" \"{1}\" \"{2}\"', + $IcingaForWindowsService.FullPath, + (Get-IcingaPowerShellModuleFile), + $JeaProfile + ); + + $Result = Start-IcingaProcess -Executable 'sc.exe' -Arguments ([string]::Format('config icingapowershell binPath= "{0}"', $PreparedServicePath)); + + if ($Result.ExitCode -ne 0) { + Write-IcingaConsoleError 'Failed to update Icinga for Windows service for JEA profile "{0}": {1}{2}' -Objects $JeaProfile, $ResolveStatus.Message, $ResolveStatus.Error; + } else { + Write-IcingaConsoleNotice 'Icinga for Windows service JEA handling has been configured successfully to profile "{0}"' -Objects $JeaProfile; + } +} +function Deny-IcingaJEACommand() +{ + param ( + [string]$Command = $null, + [string]$FileComments = $null + ); + + if ([string]::IsNullOrEmpty($Command) -eq $FALSE) { + # Ensure certain commands are not added to the JEA profile + switch ($Command.ToLower()) { + 'Register-ScheduledTask'.ToLower() { + return $TRUE; + }; + 'Start-ScheduledTask'.ToLower() { + return $TRUE; + }; + 'Unregister-ScheduledTask'.ToLower() { + return $TRUE; + }; + 'New-ScheduledTaskAction'.ToLower() { + return $TRUE; + }; + 'Invoke-IcingaWindowsScheduledTask'.ToLower() { + return $TRUE; + }; + 'Start-IcingaWindowsScheduledTaskRenewCertificate'.ToLower() { + return $TRUE; + }; + 'Register-IcingaWindowsScheduledTaskRenewCertificate'.ToLower() { + return $TRUE; + }; + 'Stop-Process'.ToLower() { + return $TRUE; + }; + 'Remove-EventLog'.ToLower() { + return $TRUE; + }; + 'Unregister-IcingaEventLog'.ToLower() { + return $TRUE; + }; + 'Remove-Item'.ToLower() { + return $TRUE; + }; + 'Remove-ItemSecure'.ToLower() { + return $TRUE; + }; + 'Stop-Service'.ToLower() { + return $TRUE; + }; + 'Restart-Service'.ToLower() { + return $TRUE; + }; + 'Copy-ItemSecure'.ToLower() { + return $TRUE; + }; + 'Copy-Item'.ToLower() { + return $TRUE; + }; + 'Move-Item'.ToLower() { + return $TRUE; + }; + 'Restart-IcingaService'.ToLower() { + return $TRUE; + }; + 'Restart-IcingaForWindows'.ToLower() { + return $TRUE; + }; + 'Stop-IcingaForWindows'.ToLower() { + return $TRUE; + }; + 'Stop-IcingaService'.ToLower() { + return $TRUE; + }; + 'Restart-IcingaService'.ToLower() { + return $TRUE; + }; + 'Restart-IcingaForWindows'.ToLower() { + return $TRUE; + }; + 'Remove-IcingaPowerShellConfig'.ToLower() { + return $TRUE; + }; + 'Add-Content'.ToLower() { + return $TRUE; + }; + } + } + + if ([string]::IsNullOrEmpty($FileComments) -eq $FALSE) { + if ($FileComments.ToLower().Contains('ignorejea')) { + return $TRUE; + } + } + + return $FALSE; +} +function Remove-IcingaFrameworkDependencyFile() +{ + $DependencyFile = Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'framework_dependencies.json'; + + if (-Not (Test-Path $DependencyFile)) { + return; + } + + Remove-ItemSecure -Path $DependencyFile -Force | Out-Null; +} +function Uninstall-IcingaJEAProfile() +{ + $JeaProfile = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'IcingaForWindows.pssc'; + $JeaProfileRessource = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'RoleCapabilities\IcingaForWindows.psrc'; + + if (Test-Path $JeaProfile) { + Write-IcingaConsoleNotice 'Removing JEA profile'; + Remove-Item $JeaProfile -Force; + } + + if (Test-Path $JeaProfileRessource) { + Write-IcingaConsoleNotice 'Removing JEA profile ressource'; + Remove-Item $JeaProfileRessource -Force; + } + + Write-IcingaConsoleNotice 'Removing JEA profile registration'; + Unregister-PSSessionConfiguration -Name 'IcingaForWindows' -Force -ErrorAction SilentlyContinue; + + Set-IcingaPowerShellConfig -Path 'Framework.JEAProfile' -Value ''; +} +function Read-IcingaPowerShellModuleFile() +{ + param ( + [string]$File, + [string]$FileContent = '' + ); + + if (([string]::IsNullOrEmpty($File) -Or (Test-Path -Path $File) -eq $FALSE) -And [string]::IsNullOrEmpty($FileContent)) { + return ''; + } + + if ([string]::IsNullOrEmpty($FileContent)) { + $FileContent = Read-IcingaFileSecure -File $File; + } + + $PSParser = [System.Management.Automation.PSParser]::Tokenize($FileContent, [ref]$null); + [array]$Comments = @(); + [array]$RegexFilter = @(); + [string]$RegexPattern = ''; + [array]$CommandList = @(); + [array]$FunctionList = @(); + [array]$VariableList = @(); + [array]$AliasList = @(); + [array]$ExportFunctionList = @(); + [array]$ExportCmdletList = @(); + [hashtable]$CmdCache = @{ }; + [hashtable]$FncCache = @{ }; + [int]$Index = 0; + [bool]$ThreadCommand = $FALSE; + [bool]$ThreadFetchNext = $FALSE; + [bool]$ShellCommand = $FALSE; + [bool]$ShellGroupStart = $FALSE; + [bool]$BeginExportMember = $FALSE; + [bool]$BeginExportFunc = $FALSE; + [bool]$BeginExportCmdlet = $FALSE; + [bool]$BeginExportVar = $FALSE; + [bool]$BeginExportAlias = $FALSE; + [bool]$BeginReadExport = $FALSE; + + foreach ($entry in $PSParser) { + if ($entry.Type -eq 'Comment') { + $Comments += Select-Object -InputObject $entry -ExpandProperty 'Content'; + } elseif ($entry.Type -eq 'Command') { + if ($CmdCache.ContainsKey($entry.Content) -eq $FALSE) { + $CommandList += [string]$entry.Content; + $CmdCache.Add($entry.Content, 0); + } + + if ($entry.Content.ToLower() -eq 'export-modulemember') { + $BeginExportMember = $TRUE; + } + + # We need to include commands we call with New-IcingaThreadInstance e.g. + # => New-IcingaThreadInstance -Name "Main" -ThreadPool (Get-IcingaThreadPool -Name 'MainPool') -Command 'Add-IcingaForWindowsDaemon' -Start; + if ($entry.Content.ToLower() -eq 'new-icingathreadinstance') { + $ThreadCommand = $TRUE; + } + } elseif ($entry.Type -eq 'CommandArgument') { + if ($PSParser[$index - 1].Type -eq 'Keyword' -And $PSParser[$index - 1].Content.ToLower() -eq 'function') { + if ($FncCache.ContainsKey($entry.Content) -eq $FALSE) { + $FunctionList += [string]$entry.Content; + $FncCache.Add($entry.Content, 0); + } + } + } elseif ($entry.Type -eq 'Member' -And $entry.Content.ToLower() -eq 'addcommand') { + # In case we have objects that use .AddCommand() we should add these to our function list e.g. + # => [void]$Shell.AddCommand('Set-IcingaEnvironmentGlobal'); + $ShellCommand = $TRUE; + } + + if ($BeginExportMember) { + if ($entry.Type -eq 'NewLine' -Or ($entry.Type -eq 'StatementSeparator' -And $entry.Content -eq ';')) { + $BeginExportMember = $FALSE; + } + + if ($BeginExportVar -Or $BeginExportAlias -Or $BeginExportFunc -Or $BeginExportCmdlet) { + if ($BeginReadExport) { + if ($entry.Type -ne 'String' -And ($entry.Type -ne 'Operator' -And $entry.Content -ne ',')) { + $BeginReadExport = $FALSE; + $BeginExportVar = $FALSE; + $BeginExportAlias = $FALSE; + $BeginExportFunc = $FALSE; + $BeginExportCmdlet = $FALSE; + } + } + if ($entry.Type -eq 'String') { + $BeginReadExport = $TRUE; + if ($BeginExportVar) { + $VariableList += $entry.Content; + } + if ($BeginExportAlias) { + $AliasList += $entry.Content; + } + if ($BeginExportFunc) { + $ExportFunctionList += $entry.Content; + } + if ($BeginExportCmdlet) { + $ExportCmdletList += $entry.Content; + } + } + } + # Read -Variable argument + if ($entry.Type -eq 'CommandParameter' -And $entry.Content.ToLower() -eq '-variable') { + $BeginExportVar = $TRUE; + } + if ($entry.Type -eq 'CommandParameter' -And $entry.Content.ToLower() -eq '-alias') { + $BeginExportAlias = $TRUE; + } + if ($entry.Type -eq 'CommandParameter' -And $entry.Content.ToLower() -eq '-function') { + $BeginExportFunc = $TRUE; + } + if ($entry.Type -eq 'CommandParameter' -And $entry.Content.ToLower() -eq '-cmdlet') { + $BeginExportCmdlet = $TRUE; + } + } + + # If we reached -Command for New-IcingaThreadInstance, check for the String element and add its value to our function list e.g. + # => Add-IcingaForWindowsDaemon + if ($ThreadFetchNext) { + if ($entry.Type -eq 'String') { + if (Test-IcingaFunction $entry.Content) { + if ($FncCache.ContainsKey($entry.Content) -eq $FALSE) { + $FunctionList += [string]$entry.Content; + $FncCache.Add($entry.Content, 0); + } + } + } + $ThreadFetchNext = $FALSE; + } + + # If we found the command New-IcingaThreadInstance inside ths script, loop until we reach -Command + if ($ThreadCommand) { + if ($entry.Type -eq 'CommandParameter' -And $entry.Content.ToLower() -eq '-command') { + $ThreadFetchNext = $TRUE; + $ThreadCommand = $FALSE; + } + } + + # If we reached the string content of our .AddCommand() object. add its value to our function list e.g. + # => Set-IcingaEnvironmentGlobal + if ($ShellGroupStart) { + if ($entry.Type -eq 'String') { + if (Test-IcingaFunction $entry.Content) { + if ($FncCache.ContainsKey($entry.Content) -eq $FALSE) { + $FunctionList += [string]$entry.Content; + $FncCache.Add($entry.Content, 0); + } + } + + $ShellGroupStart = $FALSE; + } + } + + # If we found an .AddArgument() member, continue until our group starts with ( + if ($ShellCommand) { + if ($entry.Type -eq 'GroupStart' -And $entry.Content.ToLower() -eq '(') { + $ShellCommand = $FALSE; + $ShellGroupStart = $TRUE; + } + } + + $Index += 1; + } + + foreach ($entry in $Comments) { + $RegexFilter += [regex]::Escape($entry); + } + + $RegexPattern = [string]::Join('|', $RegexFilter); + + return @{ + 'NormalisedContent' = ($FileContent -Replace $RegexPattern -Split '\r?\n' -NotMatch '^\s*$'); + 'RawContent' = $FileContent; + 'CommandList' = $CommandList; + 'FunctionList' = $FunctionList; + 'VariableList' = $VariableList; + 'AliasList' = $AliasList; + 'ExportFunction' = $ExportFunctionList; + 'ExportCmdlet' = $ExportCmdletList; + 'Comments' = $Comments; + }; +} +function Get-IcingaJEASessionFile() +{ + [string]$Path = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'RoleCapabilities\IcingaForWindows.psrc'; + + if (Test-Path -Path $Path) { + return $Path; + } + + return ''; +} +function Get-IcingaCommandDependency() +{ + param ( + $DependencyList = (New-Object PSCustomObject), + [hashtable]$CompiledList = @{ }, + [string]$CmdName = '', + [string]$CmdType = '' + ); + + # Function, Cmdlet, Alias, Modules, Application + if ([string]::IsNullOrEmpty($CmdType)) { + return $CompiledList; + } + + if (Deny-IcingaJEACommand -Command $CmdName) { + return $CompiledList; + } + + # Create the list container for our object type if not existing + # => Function, Cmdlet, Alias, Modules, Application + if ($CompiledList.ContainsKey($CmdType) -eq $FALSE) { + $CompiledList.Add($CmdType, @{ }); + } + + # e.g. Invoke-IcingaCheckCPU + if ($CompiledList[$CmdType].ContainsKey($CmdName)) { + $CompiledList[$CmdType][$CmdName] += 1; + + return $CompiledList; + } + + # Add the command this function is called with + $CompiledList[$CmdType].Add($CmdName, 0); + + # The command is not known in our Framework dependency list -> could be a native Windows command + if ((Test-PSCustomObjectMember -PSObject $DependencyList -Name $CmdName) -eq $FALSE) { + return $CompiledList; + } + + # Loop our entire dependency list for every single command + foreach ($CmdList in $DependencyList.$CmdName.PSObject.Properties.Name) { + # $Cmd => The list of child commands + # $CmdList => Function, Cmdlet, Alias, Modules, Application + $Cmd = $DependencyList.$CmdName.$CmdList; + + # Create the list container for our object type if not existing + # => Function, Cmdlet, Alias, Modules, Application + if ($CompiledList.ContainsKey($CmdList) -eq $FALSE) { + $CompiledList.Add($CmdList, @{ }); + } + + # Loop all commands within our child list for this command + foreach ($entry in $Cmd.PSObject.Properties.Name) { + + # $entry => The command name e.g. Write-IcingaConsolePlain + if ($CompiledList[$CmdList].ContainsKey($entry) -eq $FALSE) { + $CompiledList = Get-IcingaCommandDependency ` + -DependencyList $DependencyList ` + -CompiledList $CompiledList ` + -CmdName $entry ` + -CmdType $CmdList; + } else { + $CompiledList[$CmdList][$entry] += 1; + } + } + } + + return $CompiledList; +} +function Get-IcingaJEAConfiguration() +{ + param ( + [switch]$RebuildFramework = $FALSE, + [switch]$AllowScriptBlocks = $FALSE + ); + + # Prepare all variables and content we require for building the profile + $CommandList = Get-Command; + $PowerShellModules = Get-ChildItem -Path (Get-IcingaForWindowsRootPath) -Filter 'icinga-powershell-*'; + [array]$BlockedModules = @(); + $DependencyList = New-Object PSCustomObject; + [hashtable]$UsedCmdlets = @{ + 'Alias' = @{ }; + 'Cmdlet' = @{ }; + 'Function' = @{ }; + 'Modules' = ([System.Collections.ArrayList]@()); + }; + $ModuleContent = ''; + [bool]$DependencyCache = $FALSE; + + if ((Test-Path (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'framework_dependencies.json')) -And $RebuildFramework -eq $FALSE) { + $DependencyList = ConvertFrom-Json -InputObject (Read-IcingaFileSecure -File (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'framework_dependencies.json')); + $DependencyCache = $TRUE; + } + + # Lookup all PowerShell modules installed for Icinga for Windows inside the same folder as the Framework + # and fetch each single module file to list the used Cmdlets and Functions + # Add each file content to a big string file for better parsing + + New-IcingaProgressStatus -Name 'Icinga for Windows Components' -Message 'Fetching Icinga for Windows Components' -MaxValue $PowerShellModules.Count -Details; + foreach ($module in $PowerShellModules) { + Write-IcingaProgressStatus -Name 'Icinga for Windows Components'; + if ($module.Name.ToLower() -eq 'icinga-powershell-framework') { + continue; + } + + if ($UsedCmdlets.Modules -NotContains $module.Name) { + $UsedCmdlets.Modules.Add($module.Name) | Out-Null; + } + + $ModuleFiles = Get-ChildItem -Path $module.FullName -Recurse -Include '*.psm1'; + $ModuleFileContent = ''; + + foreach ($PSFile in $ModuleFiles) { + if ($PSFile.Name.ToLower() -eq ([string]::Format('{0}.ifw_compilation.psm1', $module.Name))) { + continue; + } + + $DeserializedFile = Read-IcingaPowerShellModuleFile -File $PSFile.FullName; + $RawModuleContent = $DeserializedFile.NormalisedContent; + + if ([string]::IsNullOrEmpty($RawModuleContent)) { + continue; + } + + $ModuleFileContent += $RawModuleContent; + $ModuleFileContent += "`r`n"; + $ModuleFileContent += "`r`n"; + + $SourceCode = $RawModuleContent.ToLower().Replace(' ', ''); + $SourceCode = $SourceCode.Replace("`r`n", ''); + $SourceCode = $SourceCode.Replace("`n", ''); + + # Lookup the entire command list and compare the source code behind if it contains any [ScriptBlocks] or Add-Types + # [ScriptBlocks] are forbidden and modules containing them will not be added, while Add-Type will print a warning + if ($null -ne (Select-String -InputObject $ModuleFileContent -Pattern '[scriptblock]' -SimpleMatch) -Or $null -ne (Select-String -InputObject $SourceCode -Pattern '={' -SimpleMatch) -Or $null -ne (Select-String -InputObject $SourceCode -Pattern 'return{' -SimpleMatch) -Or $null -ne (Select-String -InputObject $SourceCode -Pattern ';{' -SimpleMatch)) { + if ($AllowScriptBlocks -eq $FALSE) { + Write-IcingaConsoleError 'Unable to include module "{0}" into JEA profile. The file "{1}" is using one or more [ScriptBlock] variables which are forbidden in JEA context.' -Objects $module.Name, $PSFile.FullName; + $UsedCmdlets.Modules.RemoveAt($UsedCmdlets.Modules.IndexOf($module.Name)); + $BlockedModules += $module.Name; + $ModuleFileContent = ''; + break; + } else { + Write-IcingaConsoleWarning 'Module "{0}" is containing [ScriptBlock] like content inside file "{1}". Please validate the file before running it inside JEA context.' -Objects $module.Name, $PSFile.FullName; + } + } + + if ($null -ne (Select-String -InputObject $SourceCode -Pattern 'add-type' -SimpleMatch) -Or $null -ne (Select-String -InputObject $SourceCode -Pattern 'add-icingaaddtypelib' -SimpleMatch) -Or $null -ne (Select-String -InputObject $SourceCode -Pattern 'typedefinition@"' -SimpleMatch) -Or $null -ne (Select-String -InputObject $SourceCode -Pattern '@"' -SimpleMatch)) { + Write-IcingaConsoleWarning 'The module "{0}" is using "Add-Type" or "Add-IcingaAddTypeLib" definitions for file "{1}". Ensure you validate the code before trusting this publisher.' -Objects $module.Name, $PSFile.FullName; + } + } + + $ModuleContent += $ModuleFileContent; + } + + Complete-IcingaProgressStatus -Name 'Icinga for Windows Components'; + + if ($DependencyCache -eq $FALSE) { + # Now lets lookup every single Framework file and get all used Cmdlets and Functions so we know our dependencies + $FrameworkFiles = Get-ChildItem -Path (Get-IcingaFrameworkRootPath) -Recurse -Filter '*.psm1'; + + New-IcingaProgressStatus -Name 'Icinga for Windows Files' -Message 'Compiling Icinga PowerShell Framework Dependency List' -MaxValue $FrameworkFiles.Count -Details; + + foreach ($ModuleFile in $FrameworkFiles) { + Write-IcingaProgressStatus -Name 'Icinga for Windows Files'; + + # Just ignore our cache file + if ($ModuleFile.FullName -eq (Get-IcingaFrameworkCodeCacheFile)) { + continue; + } + + $DeserializedFile = Read-IcingaPowerShellModuleFile -File $ModuleFile.FullName; + + if (Deny-IcingaJEACommand -FileComments $DeserializedFile.Comments) { + continue; + } + + foreach ($FoundFunction in $DeserializedFile.FunctionList) { + $DependencyList = Get-IcingaFrameworkDependency ` + -Command $FoundFunction ` + -DependencyList $DependencyList; + } + } + + Complete-IcingaProgressStatus -Name 'Icinga for Windows Files'; + + Write-IcingaFileSecure -File (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'framework_dependencies.json') -Value $DependencyList; + } + + $UsedCmdlets.Modules.Add('icinga-powershell-framework') | Out-Null; + + # Check all our configured background daemons and ensure we get all Cmdlets and Functions including the dependency list + $BackgroundDaemons = (Get-IcingaBackgroundDaemons).Keys; + + New-IcingaProgressStatus -Name 'Compiling Icinga for Windows Daemons' -Message 'Compiling Background Daemon Dependency List' -MaxValue $BackgroundDaemons.Count -Details; + + foreach ($daemon in $BackgroundDaemons) { + Write-IcingaProgressStatus -Name 'Compiling Icinga for Windows Daemons'; + + $DaemonCmd = (Get-Command $daemon); + + if ($BlockedModules -Contains $DaemonCmd.Source) { + continue; + } + + $ModuleContent += [string]::Format('function {0} {{{1}{2}{1}}}', $daemon, "`r`n", $DaemonCmd.ScriptBlock.ToString()); + + [string]$CommandType = ([string]$DaemonCmd.CommandType).Replace(' ', ''); + + $UsedCmdlets = Get-IcingaCommandDependency ` + -DependencyList $DependencyList ` + -CompiledList $UsedCmdlets ` + -CmdName $DaemonCmd.Name ` + -CmdType $CommandType; + } + + Complete-IcingaProgressStatus -Name 'Compiling Icinga for Windows Daemons'; + + # We need to add this function which is not used anywhere else and should still add the entire dependency tree + $UsedCmdlets = Get-IcingaCommandDependency ` + -DependencyList $DependencyList ` + -CompiledList $UsedCmdlets ` + -CmdName 'Exit-IcingaExecutePlugin' ` + -CmdType 'Function'; + + # We need to add this function for our background daemon we start with 'Start-IcingaForWindowsDaemon', + # as these functions are called outside the JEA context + $UsedCmdlets = Get-IcingaCommandDependency ` + -DependencyList $DependencyList ` + -CompiledList $UsedCmdlets ` + -CmdName 'Start-IcingaPowerShellDaemon' ` + -CmdType 'Function'; + + $UsedCmdlets = Get-IcingaCommandDependency ` + -DependencyList $DependencyList ` + -CompiledList $UsedCmdlets ` + -CmdName 'Start-IcingaForWindowsDaemon' ` + -CmdType 'Function'; + + # Fixes error if only the Icinga PowerShell Framework is installed, which then causes JEA to fail entirely because of this missing Cmdlet + $UsedCmdlets = Get-IcingaCommandDependency ` + -DependencyList $DependencyList ` + -CompiledList $UsedCmdlets ` + -CmdName 'Select-Object' ` + -CmdType 'Cmdlet'; + + # Finally loop through all commands again and build our JEA command list + $DeserializedFile = Read-IcingaPowerShellModuleFile -FileContent $ModuleContent; + [array]$JeaCmds = $DeserializedFile.CommandList + $DeserializedFile.FunctionList; + + New-IcingaProgressStatus -Name 'Compiling JEA' -Message 'Compiling JEA Profile Catalog' -MaxValue $JeaCmds.Count -Details; + + foreach ($cmd in $JeaCmds) { + Write-IcingaProgressStatus -Name 'Compiling JEA'; + $CmdData = Get-Command $cmd -ErrorAction SilentlyContinue; + + if ($null -eq $CmdData) { + continue; + } + + $CommandType = ([string]$CmdData.CommandType).Replace(' ', ''); + + if (Deny-IcingaJEACommand -Command $cmd) { + continue; + } + + $UsedCmdlets = Get-IcingaCommandDependency ` + -DependencyList $DependencyList ` + -CompiledList $UsedCmdlets ` + -CmdName $cmd ` + -CmdType $CommandType; + } + + Complete-IcingaProgressStatus -Name 'Compiling JEA'; + + Disable-IcingaProgressPreference; + + return $UsedCmdlets; +} +function Get-IcingaJEAServicePid() +{ + [string]$JeaPidFile = (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'jea.pid'); + [string]$JeaPid = Read-IcingaFileSecure -File $JeaPidFile; + + if ([string]::IsNullOrEmpty($JeaPid) -eq $FALSE) { + $JeaPid = $JeaPid.Replace("`r`n", '').Replace("`n", '').Replace(' ', ''); + } + + if ([string]::IsNullOrEmpty($JeaPid) -Or $JeaPid -eq '0' -Or $JeaPid -eq 0) { + return $null; + } + + return $JeaPid; +} +function Install-IcingaJEAProfile() +{ + param ( + [string]$IcingaUser = (Get-IcingaServiceUser), + [switch]$ConstrainedLanguage = $FALSE, + [switch]$TestEnv = $FALSE, + [switch]$RebuildFramework = $FALSE, + [switch]$AllowScriptBlocks = $FALSE + ); + + if ($PSVersionTable.PSVersion -lt '5.0.0.0') { + Write-IcingaConsoleError 'You cannot use JEA profiles on your system, as your installed PowerShell version "{0}" is lower than minimum required version "5.0"' -Objects $PSVersionTable.PSVersion; + return; + } + + $IcingaUserInfo = Split-IcingaUserDomain -User $IcingaUser; + + # Max length for the user name + if ($IcingaUserInfo.User.Length -gt 20) { + Write-IcingaConsoleError 'The specified user name "{0}" is too long. The maximum character limit is 20 digits.' -Objects $IcingaUserInfo.User; + + return; + } + + Write-IcingaConsoleNotice 'Writing Icinga for Windows environment information as JEA profile'; + Write-IcingaJEAProfile -RebuildFramework:$RebuildFramework -AllowScriptBlocks:$AllowScriptBlocks; + Write-IcingaConsoleNotice 'Registering Icinga for Windows JEA profile'; + Register-IcingaJEAProfile -IcingaUser $IcingaUser -TestEnv:$TestEnv -ConstrainedLanguage:$ConstrainedLanguage; + # We need to run the task renewal with our scheduled task to fix errors while using WinRM / SSH + Start-IcingaWindowsScheduledTaskRenewCertificate; +} + +Set-Alias -Name 'Update-IcingaJEAProfile' -Value 'Install-IcingaJEAProfile'; +function Write-IcingaJEAProfile() +{ + param ( + [switch]$RebuildFramework = $FALSE, + [switch]$AllowScriptBlocks = $FALSE + ); + + [hashtable]$JeaConfig = Get-IcingaJEAConfiguration -RebuildFramework:$RebuildFramework -AllowScriptBlocks:$AllowScriptBlocks; + $JeaFile = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'templates\IcingaForWindows.psrc.template'; + $JeaString = Get-Content $JeaFile; + $NewJeaFile = ''; + + foreach ($line in $JeaString) { + if ($line -like '*ModulesToImport*') { + $NewJeaFile += [string]::Format(' ModulesToImport = {0}{1}', (ConvertFrom-IcingaArrayToString -Array $JeaConfig.Modules -AddQuotes), "`n"); + continue; + } + if ($line -like '*VisibleCmdlets*') { + $NewJeaFile += [string]::Format(' VisibleCmdlets = {0}{1}', (ConvertFrom-IcingaArrayToString -Array $JeaConfig.Cmdlet.Keys -AddQuotes), "`n"); + continue; + } + if ($line -like '*VisibleFunctions*') { + $NewJeaFile += [string]::Format(' VisibleFunctions = {0}{1}', (ConvertFrom-IcingaArrayToString -Array $JeaConfig.Function.Keys -AddQuotes), "`n"); + continue; + } + $NewJeaFile += $line + "`n"; + } + + Set-Content -Path (Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'RoleCapabilities\IcingaForWindows.psrc') -Value $NewJeaFile; +} +function Get-IcingaFrameworkDependency() +{ + param ( + [string]$Command = $null, + $DependencyList = (New-Object PSCustomObject) + ); + + if (Test-PSCustomObjectMember -PSObject $DependencyList -Name $Command) { + return $DependencyList; + } + + $DependencyList | Add-Member -MemberType NoteProperty -Name ($Command) -Value (New-Object PSCustomObject); + + $CommandConfig = (Get-Command $Command); + $ModuleContent = $CommandConfig.ScriptBlock.ToString(); + $DeserializedFile = Read-IcingaPowerShellModuleFile -FileContent $ModuleContent; + [array]$CheckCmd = $DeserializedFile.CommandList + $DeserializedFile.FunctionList; + + if (Deny-IcingaJEACommand -Command $Command -FileComment $DeserializedFile.Comment) { + return $DependencyList; + } + + foreach ($cmd in $CheckCmd) { + if ($cmd -eq $Command) { + continue; + } + + $CommandConfig = Get-Command $cmd -ErrorAction SilentlyContinue; + + if ($null -eq $CommandConfig) { + continue; + } + + [string]$CommandType = ([string]$CommandConfig.CommandType).Replace(' ', ''); + + if ((Test-PSCustomObjectMember -PSObject ($DependencyList.$Command) -Name $CommandType) -eq $FALSE) { + $DependencyList.$Command | Add-Member -MemberType NoteProperty -Name ($CommandType) -Value (New-Object PSCustomObject); + } + + if ((Test-PSCustomObjectMember -PSObject ($DependencyList.$Command.$CommandType) -Name $cmd) -eq $FALSE) { + $DependencyList.$Command.$CommandType | Add-Member -MemberType NoteProperty -Name ($cmd) -Value 0; + } + + $DependencyList.$Command.$CommandType.($cmd) += 1; + } + + return $DependencyList; +} +function Get-IcingaJEAContext() +{ + return (Get-IcingaPowerShellConfig -Path 'Framework.JEAProfile'); +} +function Test-IcingaPowerShellCommandInCode() +{ + param ( + [string]$Code = '', + [string]$Command = '' + ); + + if ([string]::IsNullOrEmpty($Code) -Or [string]::IsNullOrEmpty($Command)) { + return $FALSE; + } + + [string]$SearchCmdSpace = [string]::Format('{0} ', $Command); + [string]$SearchCmdColon = [string]::Format('{0};', $Command); + [string]$SearchCmdCBClose = [string]::Format('{0})', $Command); + [string]$SearchCmdCBOpen = [string]::Format('{0}(', $Command); + [string]$SearchCmdSB = [string]::Format('{0}]', $Command); + [string]$SearchCmdBrace = [string]::Format('{0}}}', $Command); + [string]$SearchCmdSQ = [string]::Format("{0}'", $Command); + [string]$SearchCmdRN = [string]::Format('{0}{1}', $Command, "`r`n"); + [string]$SearchCmdNL = [string]::Format('{0}{1}', $Command, "`n"); + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdSpace -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdColon -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdCBOpen -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdCBClose -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdSB -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdBrace -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdSQ -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdRN -SimpleMatch)) { + return $TRUE; + } + + if ($null -ne (Select-String -InputObject $ModuleContent -Pattern $SearchCmdNL -SimpleMatch)) { + return $TRUE; + } + + return $FALSE; +} +function Register-IcingaJEAProfile() +{ + param ( + [string]$IcingaUser = (Get-IcingaServiceUser), + [switch]$ConstrainedLanguage = $FALSE, + [switch]$TestEnv = $FALSE + ); + + $JeaTemplate = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'templates\IcingaForWindows.pssc.template'; + $JeaProfile = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'IcingaForWindows.pssc'; + $JeaContent = Get-Content -Path $JeaTemplate -Raw; + $JeaName = 'IcingaForWindows'; + + if ($TestEnv) { + $IcingaUser = $ENV:USERNAME; + $JeaProfile = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'IcingaForWindowsTest.pssc'; + $JeaName = 'IcingaForWindowsTest'; + } + + if ([string]::IsNullOrEmpty($IcingaUser)) { + Write-IcingaConsoleError 'No user found to set the JEA profile to. By default the Icinga Agent user is used for this'; + return; + } + + $LanguageMode = 'FullLanguage'; + + if ($ConstrainedLanguage) { + $LanguageMode = 'ConstrainedLanguage'; + } + + $UserSID = Get-IcingaUserSID -User $IcingaUser; + $IcingaUser = Get-IcingaUsernameFromSID -SID $UserSID; + $JeaContent = $JeaContent.Replace('$ICINGAFORWINDOWSJEAUSER$', $IcingaUser); + $JeaContent = $JeaContent.Replace('$POWERSHELLLANGUAGEMODE$', $LanguageMode); + + Set-Content -Path $JeaProfile -Value $JeaContent; + + $Result = Register-PSSessionConfiguration -Name $JeaName -Path $JeaProfile -Force; + + if ($TestEnv -eq $FALSE) { + Set-IcingaPowerShellConfig -Path 'Framework.JEAProfile' -Value 'IcingaForWindows'; + } + + if ($null -ne $Result) { + Write-IcingaConsoleNotice 'JEA Profile "{0}" was successfully installed' -Objects $Result.Name; + } else { + Write-IcingaConsoleNotice 'Failed to install JEA profile'; + } +} +function Test-IcingaJEAServiceRunning() +{ + param ( + [string]$JeaPid = $null + ); + + if ([string]::IsNullOrEmpty($JeaPid)) { + [string]$JeaPid = Get-IcingaJEAServicePid; + } + + if ([string]::IsNullOrEmpty($JeaPid)) { + return $FALSE; + } + + if ($JeaPid -eq '0' -Or $JeaPid -eq 0) { + return $FALSE; + } + + $JeaPowerShellProcess = Get-Process -Id $JeaPid -ErrorAction SilentlyContinue; + if ($null -eq $JeaPowerShellProcess) { + return $FALSE; + } + + if ($JeaPowerShellProcess.ProcessName -ne 'wsmprovhost') { + return $FALSE; + } + + return $TRUE; +} +<# +.SYNOPSIS + Enables the feature to forward all executed checks to an internal + installed API to run them within a daemon +.DESCRIPTION + Enables the feature to forward all executed checks to an internal + installed API to run them within a daemon +.FUNCTIONALITY + Enables the Icinga for Windows Api checks forwarder +.EXAMPLE + PS>Enable-IcingaFrameworkApiChecks; +.LINK + https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/30-API-Check-Forwarder/ +#> + +function Enable-IcingaFrameworkApiChecks() +{ + Set-IcingaPowerShellConfig -Path 'Framework.ApiChecks' -Value $TRUE; +} +<# +.SYNOPSIS + Allows to test if console output can be written or not for this PowerShell session +.DESCRIPTION + Allows to test if console output can be written or not for this PowerShell session +.FUNCTIONALITY + Allows to test if console output can be written or not for this PowerShell session +.EXAMPLE + PS>Enable-IcingaFrameworkConsoleOutput; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Test-IcingaFrameworkConsoleOutput() +{ + if ($null -eq $global:Icinga) { + return $TRUE; + } + + if ($global:Icinga.ContainsKey('DisableConsoleOutput') -eq $FALSE) { + return $TRUE; + } + + return (-Not ($global:Icinga.DisableConsoleOutput)); +} +<# +.SYNOPSIS + Get the current debug mode configuration of the Framework +.DESCRIPTION + Get the current debug mode configuration of the Framework +.FUNCTIONALITY + Get the current debug mode configuration of the Framework +.EXAMPLE + PS>Get-IcingaFrameworkDebugMode; +.LINK + https://github.com/Icinga/icinga-powershell-framework +.OUTPUTS + System.Boolean +#> + +function Get-IcingaFrameworkDebugMode() +{ + $DebugMode = Get-IcingaPowerShellConfig -Path 'Framework.DebugMode'; + + if ($null -eq $DebugMode) { + return $FALSE; + } + + return $DebugMode; +} +function Invoke-IcingaNamespaceCmdlets() +{ + param ( + [string]$Command + ); + + [Hashtable]$CommandConfig = @{ }; + + if ($Command.Contains('*') -eq $FALSE) { + $Command = [string]::Format('{0}*', $Command); + } + + $CommandList = Get-Command $Command; + + foreach ($Cmdlet in $CommandList) { + try { + $CommandName = $Cmdlet.Name; + $Content = (& $CommandName); + + Add-IcingaHashtableItem ` + -Hashtable $CommandConfig ` + -Key $Cmdlet.Name ` + -Value $Content | Out-Null; + } catch { + Write-IcingaEventMessage -EventId 1103 -Namespace 'Framework' -ExceptionObject $_ -Objects $CommandName; + } + } + + return $CommandConfig; +} +<# +.SYNOPSIS + Downloads a ZIP-Archive for the Icinga for Windows Service Binary + and installs it into a specified directory +.DESCRIPTION + Wizard function to download the Icinga for Windows Service binary from + a public ressource or from a local webstore / webshare and extract + the ZIP-Archive into a target destination +.FUNCTIONALITY + Downloads and unzips the Icinga for Windows service binary ZIP-Archive +.EXAMPLE + PS>Get-IcingaFrameworkServiceBinary -FrameworkServiceUrl 'https://github.com/Icinga/icinga-powershell-service/releases/download/v1.0.0/icinga-service-v1.0.0.zip' -ServiceDirectory 'C:\Program Files\icinga-framework-service'; +.EXAMPLE + PS>Get-IcingaFrameworkServiceBinary -FrameworkServiceUrl 'C:/users/public/icinga-service-v1.0.0.zip' -ServiceDirectory 'C:\Program Files\icinga-framework-service'; +.PARAMETER FrameworkServiceUrl + The URL / Source for downloading the ZIP-Archive from. +.PARAMETER Destination + The target destination to extract the ZIP-Archive to and to place the service binary +.INPUTS + System.String +.OUTPUTS + System.Hashtable +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaFrameworkServiceBinary() +{ + param( + [string]$FrameworkServiceUrl, + [string]$ServiceDirectory, + [switch]$Release = $FALSE + ); + + Set-IcingaTLSVersion; + $ProgressPreference = "SilentlyContinue"; + + if ([string]::IsNullOrEmpty($FrameworkServiceUrl) -Or $Release) { + if ($Release -Or (Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you provide a custom source of the service binary?' -Default 'n').result -eq 1) { + $LatestRelease = (Invoke-IcingaWebRequest -Uri 'https://github.com/Icinga/icinga-powershell-service/releases/latest' -UseBasicParsing).BaseResponse.ResponseUri.AbsoluteUri; + $FrameworkServiceUrl = $LatestRelease.Replace('/tag/', '/download/'); + $Tag = $FrameworkServiceUrl.Split('/')[-1]; + $FrameworkServiceUrl = [string]::Format('{0}/icinga-service-{1}.zip', $FrameworkServiceUrl, $Tag); + } else { + $FrameworkServiceUrl = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please enter the full path to your service binary repository' -Default 'v').answer; + } + } + + if ([string]::IsNullOrEmpty($FrameworkServiceUrl)) { + Write-IcingaConsoleError 'No Url to download the Icinga Service Binary from has been specified. Please try again.'; + return Get-IcingaFrameworkServiceBinary; + } + + if ([string]::IsNullOrEmpty($ServiceDirectory)) { + $ServiceDirectory = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please enter the path you wish to install the service to' -Default 'v' -DefaultInput 'C:\Program Files\icinga-framework-service\').answer; + } + + if ((Test-Path $ServiceDirectory) -eq $FALSE) { + New-Item -Path $ServiceDirectory -Force -ItemType Directory | Out-Null; + } + + $TmpDirectory = New-IcingaTemporaryDirectory; + if (Test-Path $FrameworkServiceUrl) { + $ZipArchive = Join-Path -Path $TmpDirectory -ChildPath ($FrameworkServiceUrl.Replace('/', '\').Split('\')[-1]); + } else { + $ZipArchive = Join-Path -Path $TmpDirectory -ChildPath ($FrameworkServiceUrl.Split('/')[-1]); + } + + $TmpServiceBin = Join-Path -Path $TmpDirectory -ChildPath 'icinga-service.exe'; + $UpdateBin = Join-Path -Path $ServiceDirectory -ChildPath 'icinga-service.exe.update'; + $ServiceBin = Join-Path -Path $ServiceDirectory -ChildPath 'icinga-service.exe'; + + if ((Invoke-IcingaWebRequest -Uri $FrameworkServiceUrl -UseBasicParsing -OutFile $ZipArchive).HasErrors) { + Write-IcingaConsoleError -Message 'Failed to download the Icinga Service Binary from "{0}". Please try again.' -Objects $FrameworkServiceUrl; + return Get-IcingaFrameworkServiceBinary; + } + + if ((Expand-IcingaZipArchive -Path $ZipArchive -Destination $TmpDirectory) -eq $FALSE) { + throw 'Failed to expand the downloaded ZIP archive'; + } + + if ((Test-IcingaZipBinaryChecksum -Path $TmpServiceBin) -eq $FALSE) { + throw 'The checksum of the downloaded file and the required MD5 hash are not matching'; + } + + Copy-ItemSecure -Path $TmpServiceBin -Destination $UpdateBin -Force | Out-Null; + Start-Sleep -Seconds 1; + Remove-ItemSecure -Path $TmpDirectory -Recurse -Force | Out-Null; + + return @{ + 'FrameworkServiceUrl' = $FrameworkServiceUrl; + 'ServiceDirectory' = $ServiceDirectory; + 'ServiceBin' = $ServiceBin; + }; +} +<# +.SYNOPSIS + Fetches a Stopwatch system object by a given name if initialised with Start-IcingaTimer +.DESCRIPTION + Fetches a Stopwatch system object by a given name if initialised with Start-IcingaTimer +.FUNCTIONALITY + Fetches a Stopwatch system object by a given name if initialised with Start-IcingaTimer +.EXAMPLE + PS>Get-IcingaTimer; +.EXAMPLE + PS>Get-IcingaTimer -Name 'My Test Timer'; +.PARAMETER Name + The name of a custom identifier to run mutliple timers at once +.INPUTS + System.String +.OUTPUTS + System.Diagnostics.Stopwatch +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaTimer() +{ + param ( + [string]$Name = 'DefaultTimer' + ); + + $TimerData = Get-IcingaHashtableItem -Key $Name -Hashtable $Global:Icinga.Private.Timers; + + if ($null -eq $TimerData) { + return $null; + } + + return $TimerData.Timer; +} +function Publish-IcingaPluginDocumentation() +{ + param ( + [string]$ModulePath + ); + + if ([string]::IsNullOrEmpty($ModulePath) -Or (Test-Path $ModulePath) -eq $FALSE) { + Write-IcingaConsoleError -Message 'Either your provided path "{0}" is empty or does not exist' -Objects $ModulePath; + return; + } + + [string]$PluginDir = Join-Path -Path $ModulePath -ChildPath 'plugins'; + [string]$DocDir = Join-Path -Path $ModulePath -ChildPath 'doc'; + [string]$PluginDocFile = Join-Path -Path $ModulePath -ChildPath 'doc/10-Icinga-Plugins.md'; + [string]$PluginDocDir = Join-Path -Path $ModulePath -ChildPath 'doc/plugins'; + + New-IcingaDocumentObject -Name 'Plugins Base' -Path $PluginDocFile; + + if ((Test-Path $PluginDocDir) -eq $FALSE) { + New-Item -Path $PluginDocDir -ItemType Directory -Force | Out-Null; + } + + $MDFiles = Get-ChildItem -Path $PluginDocDir; + [int]$FileCount = $MDFiles.Count; + [string]$FileCountStr = ''; + + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '# Icinga Plugins'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content ''; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content 'Below you will find a documentation for every single available plugin provided by this repository. Most of the plugins allow the usage of default Icinga threshold range handling, which is defined as follows:'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content ''; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '| Argument | Throws error on | Ok range |'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '| --- | --- | --- |'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '| 20 | < 0 or > 20 | 0 .. 20 |'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '| 20: | < 20 | between 20 .. ∞ |'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '| ~:20 | > 20 | between -∞ .. 20 |'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '| 30:40 | < 30 or > 40 | between {30 .. 40} |'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '| `@30:40 | ≥ 30 and ≤ 40 | outside -∞ .. 29 and 41 .. ∞ |'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content ''; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content 'Please ensure that you will escape the `@` if you are configuring it on the Icinga side. To do so, you will simply have to write an *\`* before the `@` symbol: \``@`'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content ''; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content 'To test thresholds with different input values, you can use the Framework Cmdlet `Get-IcingaHelpThresholds`.'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content ''; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content 'Each plugin ships with a constant Framework argument `-ThresholdInterval`. This can be used to modify the value your thresholds are compared against from the current, fetched value to one collected over time by the Icinga for Windows daemon. In case you [Collect Metrics Over Time](https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/06-Collect-Metrics-over-Time/) for specific time intervals, you can for example set the argument to `15m` to get the average value of 15m as base for your monitoring values. Please note that in this example, you will require to have collected the `15m` average for `Invoke-IcingaCheckCPU`.'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content ''; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '```powershell'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content 'icinga> icinga { Invoke-IcingaCheckCPU -Warning 20 -Critical 40 -Core _Total -ThresholdInterval 15m }' + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '' + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '[WARNING] CPU Load: [WARNING] Core Total (29,14817700%)' + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '\_ [WARNING] Core Total: 29,14817700% is greater than threshold 20% (15m avg.)'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content "| 'core_total_1'=31.545677%;;;0;100 'core_total_15'=29.148177%;20;40;0;100 'core_total_5'=28.827410%;;;0;100 'core_total_20'=30.032942%;;;0;100 'core_total_3'=27.731669%;;;0;100 'core_total'=33.87817%;;;0;100"; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '```'; + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '' + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '| Plugin Name | Description |' + Add-IcingaDocumentContent -Name 'Plugins Base' -Content '| --- | --- |' + + $AvailablePlugins = Get-ChildItem -Path $PluginDir -Recurse -Filter *.psm1; + foreach ($plugin in $AvailablePlugins) { + [string]$PluginName = $plugin.Name.Replace('.psm1', ''); + [string]$PluginDocName = ''; + [string]$PluginSynopsis = '-'; + $PluginDetails = Get-Help -Name $PluginName -Full; + + if ($null -ne $PluginDetails -And [string]::IsNullOrEmpty($PluginDetails.Synopsis) -eq $FALSE) { + $PluginSynopsis = $PluginDetails.Synopsis.Replace("`r`n", ' '); + $PluginSynopsis = $PluginSynopsis.Replace("`r", ' '); + $PluginSynopsis = $PluginSynopsis.Replace("`n", ' '); + } + + foreach ($DocFile in $MDFiles) { + $DocFileName = $DocFile.Name; + if ($DocFileName -Like "*$PluginName.md") { + $PluginDocName = $DocFile.Name; + break; + } + } + + if ([string]::IsNullOrEmpty($PluginDocName)) { + $FileCount += 1; + if ($FileCount -lt 10) { + $FileCountStr = [string]::Format('0{0}', $FileCount); + } else { + $FileCountStr = $FileCount; + } + + $PluginDocName = [string]::Format('{0}-{1}.md', $FileCountStr, $PluginName); + } + [string]$PluginDescriptionFile = Join-Path -Path $PluginDocDir -ChildPath $PluginDocName; + + New-IcingaDocumentObject -Name $PluginName -Path $PluginDescriptionFile; + + Add-IcingaDocumentContent -Name 'Plugins Base' -Content ([string]::Format( + '| [{0}](plugins/{1}) | {2} |', + $PluginName, + $PluginDocName, + $PluginSynopsis + )); + + $PluginHelp = Get-Help $PluginName -Full; + + Add-IcingaDocumentContent -Name $PluginName -Content ([string]::Format('# {0}', $PluginHelp.Name)); + Add-IcingaDocumentContent -Name $PluginName -Content ''; + Add-IcingaDocumentContent -Name $PluginName -Content '## Description'; + Add-IcingaDocumentContent -Name $PluginName -Content ''; + Add-IcingaDocumentContent -Name $PluginName -Content $PluginHelp.details.description.Text; + Add-IcingaDocumentContent -Name $PluginName -Content ''; + Add-IcingaDocumentContent -Name $PluginName -Content $PluginHelp.description.Text; + Add-IcingaDocumentContent -Name $PluginName -Content ''; + Add-IcingaDocumentContent -Name $PluginName -Content '## Permissions'; + Add-IcingaDocumentContent -Name $PluginName -Content ''; + + if ([string]::IsNullOrEmpty($PluginHelp.Role)) { + Add-IcingaDocumentContent -Name $PluginName -Content 'No special permissions required.'; + } else { + Add-IcingaDocumentContent -Name $PluginName -Content 'To execute this plugin you will require to grant the following user permissions.'; + Add-IcingaDocumentContent -Name $PluginName -Content ''; + Add-IcingaDocumentContent -Name $PluginName -Content $PluginHelp.Role; + } + + if ($null -ne $PluginHelp.parameters.parameter) { + Add-IcingaDocumentContent -Name $PluginName -Content ''; + Add-IcingaDocumentContent -Name $PluginName -Content '## Arguments'; + Add-IcingaDocumentContent -Name $PluginName -Content ''; + Add-IcingaDocumentContent -Name $PluginName -Content '| Argument | Type | Required | Default | Description |'; + Add-IcingaDocumentContent -Name $PluginName -Content '| --- | --- | --- | --- | --- |'; + + foreach ($parameter in $PluginHelp.parameters.parameter) { + [string]$ParamDescription = $parameter.description.Text; + if ([string]::IsNullOrEmpty($ParamDescription) -eq $FALSE) { + $ReplacementString = '
'; + + $ParamDescription = $ParamDescription.Replace("`r`n", $ReplacementString); + $ParamDescription = $ParamDescription.Replace("`r", $ReplacementString); + $ParamDescription = $ParamDescription.Replace("`n", $ReplacementString); + + if ($ParamDescription.Contains('|')) { + $ParamDescription = $ParamDescription.Replace('|', '|'); + } + } + + [string]$TableContent = [string]::Format( + '| {0} | {1} | {2} | {3} | {4} |', + $parameter.name, + $parameter.type.name, + $parameter.required, + $parameter.defaultValue, + $ParamDescription + ); + Add-IcingaDocumentContent -Name $PluginName -Content $TableContent; + } + + Add-IcingaDocumentContent -Name $PluginName -Content '| ThresholdInterval | String | | | Change the value your defined threshold checks against from the current value to a collected time threshold of the Icinga for Windows daemon, as described [here](https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/06-Collect-Metrics-over-Time/). An example for this argument would be 1m or 15m which will use the average of 1m or 15m for monitoring. |'; + } + + if ($null -ne $PluginHelp.examples) { + [int]$ExampleIndex = 1; + Add-IcingaDocumentContent -Name $PluginName -Content ''; + Add-IcingaDocumentContent -Name $PluginName -Content '## Examples'; + Add-IcingaDocumentContent -Name $PluginName -Content ''; + + foreach ($example in $PluginHelp.examples.example) { + [string]$ExampleDescription = $example.remarks.Text; + if ([string]::IsNullOrEmpty($ExampleDescription) -eq $FALSE) { + } + + Add-IcingaDocumentContent -Name $PluginName -Content ([string]::Format('### Example Command {0}', $ExampleIndex)); + Add-IcingaDocumentContent -Name $PluginName -Content ''; + Add-IcingaDocumentContent -Name $PluginName -Content '```powershell'; + Add-IcingaDocumentContent -Name $PluginName -Content $example.code; + Add-IcingaDocumentContent -Name $PluginName -Content '```'; + Add-IcingaDocumentContent -Name $PluginName -Content ''; + Add-IcingaDocumentContent -Name $PluginName -Content ([string]::Format('### Example Output {0}', $ExampleIndex)); + Add-IcingaDocumentContent -Name $PluginName -Content ''; + Add-IcingaDocumentContent -Name $PluginName -Content '```powershell'; + Add-IcingaDocumentContent -Name $PluginName -Content $ExampleDescription; + Add-IcingaDocumentContent -Name $PluginName -Content '```'; + Add-IcingaDocumentContent -Name $PluginName -Content ''; + + $ExampleIndex += 1; + } + + Write-IcingaDocumentFile $PluginName -ClearCache; + } + } + + Write-IcingaDocumentFile 'Plugins Base' -ClearCache; +} +<# +.SYNOPSIS + Wrapper for Stop-Service which catches errors and prints proper output messages +.DESCRIPTION + Stops a service if it is installed and prints console messages if a stop + was triggered or the service is not installed +.FUNCTIONALITY + Wrapper for Stop-Service which catches errors and prints proper output messages +.EXAMPLE + PS>Stop-IcingaService -Service 'icinga2'; +.PARAMETER Service + The name of the service to be stopped +.INPUTS + System.String +.OUTPUTS + Null +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Stop-IcingaService() +{ + param( + $Service + ); + + if (Get-Service "$Service" -ErrorAction SilentlyContinue) { + Write-IcingaConsoleNotice -Message 'Stopping service "{0}"' -Objects $Service; + + & powershell.exe -Command { + Use-Icinga -Minimal; + + $Service = $args[0]; + try { + Stop-Service "$Service" -ErrorAction Stop; + Start-Sleep -Seconds 2; + Optimize-IcingaForWindowsMemory; + } catch { + Write-IcingaConsoleError -Message 'Failed to stop service "{0}". Error: {1}' -Objects $Service, $_.Exception.Message; + } + } -Args $Service; + } else { + Write-IcingaConsoleWarning -Message 'The service "{0}" is not installed' -Objects $Service; + } + + Optimize-IcingaForWindowsMemory; +} +<# +.SYNOPSIS + Allows to disable any console output for this PowerShell session +.DESCRIPTION + Allows to disable any console output for this PowerShell session +.FUNCTIONALITY + Allows to disable any console output for this PowerShell session +.EXAMPLE + PS>Disable-IcingaFrameworkConsoleOutput; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Disable-IcingaFrameworkConsoleOutput() +{ + if ($null -eq $global:Icinga) { + $global:Icinga = @{ }; + } + + if ($global:Icinga.ContainsKey('DisableConsoleOutput') -eq $FALSE) { + $global:Icinga.Add('DisableConsoleOutput', $TRUE); + } else { + $global:Icinga.DisableConsoleOutput = $TRUE; + } +} +<# +.SYNOPSIS + Installs the Icinga PowerShell Services as a Windows service +.DESCRIPTION + Uses the Icinga Service binary which is already installed on the system to register + it as a Windows service and sets the proper user for it +.FUNCTIONALITY + Installs the Icinga PowerShell Services as a Windows service +.EXAMPLE + PS>Install-IcingaForWindowsService -Path C:\Program Files\icinga-service\icinga-service.exe; +.EXAMPLE + PS>Install-IcingaForWindowsService -Path C:\Program Files\icinga-service\icinga-service.exe -User 'NT Authority\NetworkService'; +.PARAMETER Path + The location on where the service binary executable is found +.PARAMETER User + The service user the service is running with +.PARAMETER Password + If the specified service user is requiring a password for registering you can provide it here as secure string +.INPUTS + System.String +.OUTPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Install-IcingaForWindowsService() +{ + param( + $Path, + $User = 'NT Authority\NetworkService', + [SecureString]$Password + ); + + if ([string]::IsNullOrEmpty($Path)) { + Write-IcingaConsoleWarning 'No path specified for Framework service. Service will not be installed'; + return; + } + + $UpdateFile = [string]::Format('{0}.update', $Path); + $IfWService = $Global:Icinga.Protected.Environment.'PowerShell Service'; + + if ((Test-Path $UpdateFile)) { + + Write-IcingaConsoleNotice 'Updating Icinga PowerShell Service binary'; + + if ($IfWService.Status -eq 'Running') { + Write-IcingaConsoleNotice 'Stopping Icinga PowerShell service'; + Stop-IcingaForWindows; + Start-Sleep -Seconds 1; + } + + if (Test-Path $Path) { + Remove-ItemSecure -Path $Path -Force | Out-Null; + } + Copy-ItemSecure -Path $UpdateFile -Destination $Path -Force | Out-Null; + Remove-ItemSecure -Path $UpdateFile -Force | Out-Null; + } + + if ((Test-Path $Path) -eq $FALSE) { + throw 'Please specify the path directly to the service binary'; + } + + $Path = [string]::Format( + '\"{0}\" \"{1}\"', + $Path, + (Get-IcingaPowerShellModuleFile) + ); + + if ($IfWService.Present -eq $FALSE) { + $ServiceCreation = Start-IcingaProcess -Executable 'sc.exe' -Arguments ([string]::Format('create icingapowershell binPath= "{0}" DisplayName= "Icinga PowerShell Service" start= auto', $Path)); + $Global:Icinga.Protected.Environment.'PowerShell Service'.Present = $TRUE; + $Global:Icinga.Protected.Environment.'PowerShell Service'.User = $User; + $Global:Icinga.Protected.Environment.'PowerShell Service'.ServicePath = $Path; + + if ($ServiceCreation.ExitCode -ne 0) { + throw ([string]::Format('Failed to install Icinga PowerShell Service: {0}{1}', $ServiceCreation.Message, $ServiceCreation.Error)); + } + } else { + $ServiceUpdate = Start-IcingaProcess -Executable 'sc.exe' -Arguments ([string]::Format('config icingapowershell binPath= "{0}"', $Path)); + + if ($ServiceUpdate.ExitCode -ne 0) { + throw ([string]::Format('Failed to update config for Icinga PowerShell Service: {0}{1}', $ServiceUpdate.Message, $ServiceUpdate.Error)); + } + + $Global:Icinga.Protected.Environment.'PowerShell Service'.ServicePath = $Path; + } + + # This is just a hotfix to ensure we setup the service properly before assigning it to + # a proper user, like 'NT Authority\NetworkService'. For some reason the NetworkService + # will not start without this workaround. + # Todo: Figure out the reason and fix it properly + Set-IcingaServiceUser -User 'LocalSystem' -Service 'icingapowershell' | Out-Null; + Restart-IcingaForWindows; + Start-Sleep -Seconds 1; + Stop-IcingaForWindows; + Start-Sleep -Seconds 1; + + if ($IfWService.Status -eq 'Running') { + Write-IcingaConsoleNotice 'Starting Icinga PowerShell service'; + Start-IcingaService 'icingapowershell'; + Start-Sleep -Seconds 1; + } + + return (Set-IcingaServiceUser -User $User -Password $Password -Service 'icingapowershell'); +} + +Set-Alias -Name 'Install-IcingaFrameworkService' -Value 'Install-IcingaForWindowsService'; +<# +.SYNOPSIS + Wrapper for Restart-Service which catches errors and prints proper output messages +.DESCRIPTION + Restarts a service if it is installed and prints console messages if a restart + was triggered or the service is not installed +.FUNCTIONALITY + Wrapper for restart service which catches errors and prints proper output messages +.EXAMPLE + PS>Restart-IcingaService -Service 'icinga2'; +.PARAMETER Service + The name of the service to be restarted +.INPUTS + System.String +.OUTPUTS + Null +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Restart-IcingaService() +{ + param ( + $Service + ); + + if (Get-Service "$Service" -ErrorAction SilentlyContinue) { + Write-IcingaConsoleNotice ([string]::Format('Restarting service "{0}"', $Service)); + + & powershell.exe -Command { + Use-Icinga -Minimal; + + $Service = $args[0]; + try { + Restart-Service "$Service" -ErrorAction Stop; + Start-Sleep -Seconds 2; + Optimize-IcingaForWindowsMemory; + } catch { + Write-IcingaConsoleError -Message 'Failed to restart service "{0}". Error: {1}' -Objects $Service, $_.Exception.Message; + } + } -Args $Service; + } else { + Write-IcingaConsoleWarning -Message 'The service "{0}" is not installed' -Objects $Service; + } + + Optimize-IcingaForWindowsMemory; +} +<# +.SYNOPSIS + Fetches the current enable/disable state of the feature + for executing checks of the internal REST-Api +.DESCRIPTION + Fetches the current enable/disable state of the feature + for executing checks of the internal REST-Api +.FUNCTIONALITY + Get the current API check execution configuration of the + Icinga PowerShell Framework +.EXAMPLE + PS>Get-IcingaFrameworkApiChecks; +.LINK + https://github.com/Icinga/icinga-powershell-framework +.OUTPUTS + System.Boolean +#> + +function Get-IcingaFrameworkApiChecks() +{ + $ApiChecks = Get-IcingaPowerShellConfig -Path 'Framework.ApiChecks'; + + if ($null -eq $ApiChecks) { + return $FALSE; + } + + return $ApiChecks; +} +<# +.SYNOPSIS + Enables the debug mode of the Framework to print additional details into + the Windows Event Log with Id 1000 +.DESCRIPTION + Enables the debug mode of the Framework to print additional details into + the Windows Event Log with Id 1000 +.FUNCTIONALITY + Enables the Icinga for Windows Debug-Log +.EXAMPLE + PS>Enable-IcingaFrameworkDebugMode; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Enable-IcingaFrameworkDebugMode() +{ + $Global:Icinga.Protected.DebugMode = $TRUE; + Set-IcingaPowerShellConfig -Path 'Framework.DebugMode' -Value $TRUE; +} +<# +.SYNOPSIS + Uninstalls the Icinga PowerShell Service as a Windows Service +.DESCRIPTION + Uninstalls the Icinga PowerShell Service as a Windows Service. The service binary + will be left on the system. +.FUNCTIONALITY + Uninstalls the Icinga PowerShell Service as a Windows Service +.EXAMPLE + PS>Uninstall-IcingaForWindowsService; +.INPUTS + System.String +.OUTPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Uninstall-IcingaForWindowsService() +{ + param ( + [switch]$RemoveFiles = $FALSE + ); + + Set-IcingaServiceEnvironment; + + $ServiceData = Get-IcingaForWindowsServiceData; + + Stop-IcingaForWindows; + Start-Sleep -Seconds 1; + + $ServiceCreation = Start-IcingaProcess -Executable 'sc.exe' -Arguments 'delete icingapowershell'; + + switch ($ServiceCreation.ExitCode) { + 0 { + Write-IcingaConsoleNotice 'Icinga PowerShell Service was successfully removed'; + $Global:Icinga.Protected.Environment.'PowerShell Service'.Present = $FALSE; + } + 1060 { + Write-IcingaConsoleWarning 'The Icinga PowerShell Service is not installed'; + } + Default { + throw ([string]::Format('Failed to install Icinga PowerShell Service: {0}{1}', $ServiceCreation.Message, $ServiceCreation.Error)); + } + } + + if ($RemoveFiles -eq $FALSE) { + return $TRUE; + } + + if ([string]::IsNullOrEmpty($ServiceData.Directory) -Or (Test-Path $ServiceData.Directory) -eq $FALSE) { + return $TRUE; + } + + $ServiceFolderContent = Get-ChildItem -Path $ServiceData.Directory; + + foreach ($entry in $ServiceFolderContent) { + if ($entry.Name -eq 'icinga-service.exe' -Or $entry.Name -eq 'icinga-service.exe.md5' -Or $entry.Name -eq 'icinga-service.exe.update') { + Remove-Item $entry.FullName -Force; + Write-IcingaConsoleNotice 'Removing file "{0}"' -Objects $entry.FullName; + } + } + + $ServiceFolderContent = Get-ChildItem -Path $ServiceData.Directory; + + if ($ServiceFolderContent.Count -eq 0) { + Remove-Item $ServiceData.Directory; + Write-IcingaConsoleNotice 'Removing directory "{0}"' -Objects $ServiceData.Directory; + } else { + Write-IcingaConsoleWarning 'Unable to remove folder "{0}", because there are still files inside.' -Objects $ServiceData.Directory; + } + + return $TRUE; +} + +Set-Alias -Name 'Uninstall-IcingaFrameworkService' -Value 'Uninstall-IcingaForWindowsService'; +<# +.SYNOPSIS + Fetches plugins within the namespace `Invoke-IcingaCheck*` for a given + component name or the direct path and creates Icinga Director as well as + Icinga 2 configuration files. + + The configuration files are printed within a `config` folder of the + specific module and split into `director` and `icinga` +.DESCRIPTION + etches plugins within the namespace `Invoke-IcingaCheck*` for a given + component name or the direct path and creates Icinga Director as well as + Icinga 2 configuration files. + + The configuration files are printed within a `config` folder of the + specific module and split into `director` and `icinga` +.FUNCTIONALITY + Creates Icinga 2 and Icinga Director configuration files for plugins +.EXAMPLE + PS>Publish-IcingaPluginConfiguration -ComponentName 'plugins'; +.EXAMPLE + PS>Publish-IcingaPluginConfiguration -ComponentPath 'C:\Program Files\WindowsPowerShell\modules\icinga-powershell-plugins'; +.PARAMETER ComponentName + The name of the component to lookup for plugins and write configuration for. + The leading icinga-powershell- is not required and you should simply use the name, + like 'plugins' or 'mssql' +.PARAMETER ComponentPath + The path to the root directory of a PowerShell Plugin repository, like + 'C:\Program Files\WindowsPowerShell\modules\icinga-powershell-plugins' +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Publish-IcingaPluginConfiguration() +{ + param ( + [string]$ComponentName, + [string]$ComponentPath + ); + + if ([string]::IsNullOrEmpty($ComponentName) -And [string]::IsNullOrEmpty($ComponentPath)) { + Write-IcingaConsoleError 'Please specify either a component name like "plugins" or set the component path to the root folder if a component, like "C:\Program Files\WindowsPowerShell\modules\icinga-powershell\plugins".'; + return; + } + + if ([string]::IsNullOrEmpty($ComponentPath)) { + $ComponentPath = Join-Path -Path (Get-IcingaForWindowsRootPath) -ChildPath ([string]::Format('icinga-powershell-{0}', $ComponentName)); + } + + if ((Test-Path $ComponentPath) -eq $FALSE) { + Write-IcingaConsoleError 'The path "{0}" for the Icinga for Windows component is not valid' -Objects $ComponentPath; + return; + } + + try { + Import-Module $ComponentPath -Global -Force -ErrorAction Stop; + } catch { + [string]$Message = $_.Exception.Message; + Write-IcingaConsoleError 'Failed to import the module on path "{0}". Please verify that this is a valid PowerShell module root folder. Exception: {1}{2}' -Objects $ComponentPath, (New-IcingaNewLine), $Message; + return; + } + + $CheckCommands = Get-Command -ListImported -Name 'Invoke-IcingaCheck*' -ErrorAction SilentlyContinue; + + if ($null -eq $CheckCommands) { + Write-IcingaConsoleError 'No Icinga CheckCommands were configured for module "{0}". Please verify that this is a valid PowerShell module root folder. Exception: {1}{2}' -Objects $ComponentPath, (New-IcingaNewLine), $Message; + return; + } + + [array]$CheckList = @(); + + [string]$BasketConfigDir = Join-Path -Path $ComponentPath -ChildPath 'config\director'; + [string]$IcingaConfigDir = Join-Path -Path $ComponentPath -ChildPath 'config\icinga'; + + if ((Test-Path $BasketConfigDir)) { + Remove-Item -Path $BasketConfigDir -Recurse -Force | Out-Null; + } + if ((Test-Path $IcingaConfigDir)) { + Remove-Item -Path $IcingaConfigDir -Recurse -Force | Out-Null; + } + + if ((Test-Path $BasketConfigDir) -eq $FALSE) { + New-Item -Path $BasketConfigDir -ItemType Directory | Out-Null; + } + if ((Test-Path $IcingaConfigDir) -eq $FALSE) { + New-Item -Path $IcingaConfigDir -ItemType Directory | Out-Null; + } + + foreach ($check in $CheckCommands) { + [string]$CheckPath = $check.Module.ModuleBase; + + if ($CheckPath.Contains($ComponentPath) -eq $FALSE) { + continue; + } + + $CheckList += [string]$check; + Get-IcingaCheckCommandConfig -CheckName $check -OutDirectory $BasketConfigDir -FileName $check; + Get-IcingaCheckCommandConfig -CheckName $check -OutDirectory $IcingaConfigDir -FileName $check -IcingaConfig; + } + + if ($CheckList.Count -eq 0) { + Write-IcingaConsoleNotice 'The module "{0}" is not containing any plugins' -Objects $ComponentName; + return; + } + + Get-IcingaCheckCommandConfig -CheckName $CheckList -OutDirectory $BasketConfigDir -FileName ([string]::Format('{0}_Bundle', (Get-Culture).TextInfo.ToTitleCase($ComponentName))); + Get-IcingaCheckCommandConfig -CheckName $CheckList -OutDirectory $IcingaConfigDir -FileName ([string]::Format('{0}_Bundle', (Get-Culture).TextInfo.ToTitleCase($ComponentName))) -IcingaConfig; +} +function Test-IcingaForWindowsService() +{ + param ( + [switch]$ResolveProblems = $FALSE + ); + + Set-IcingaServiceEnvironment -Force; + + $ServiceData = Get-IcingaForWindowsServiceData; + $ServiceConfig = $Global:Icinga.Protected.Environment.'PowerShell Service'; + [bool]$Passed = $TRUE; + + if ($null -eq $ServiceConfig) { + Write-IcingaConsoleNotice 'Icinga for Windows service "icingapowershell" is not installed'; + return $Passed; + } + + [string]$PreparedServicePath = [string]::Format( + '\"{0}\" \"{1}\"', + $ServiceData.FullPath, + (Get-IcingaPowerShellModuleFile) + ); + [string]$ServicePath = $ServiceConfig.ServicePath.SubString(0, $ServiceConfig.ServicePath.IndexOf(' "')); + + if ($ServicePath.Contains('"')) { + Write-IcingaTestOutput -Severity 'Passed' -Message 'Your service installation is not affected by IWKB000009'; + } else { + if ($ResolveProblems) { + Write-IcingaTestOutput -Severity 'Warning' -Message 'Your service installation is affected by IWKB000009. Trying to resolve the problem.'; + $ResolveStatus = Start-IcingaProcess -Executable 'sc.exe' -Arguments ([string]::Format('config icingapowershell binPath= "{0}"', $PreparedServicePath)); + + if ($ResolveStatus.ExitCode -ne 0) { + Write-IcingaConsoleError 'Failed to resolve problems for service "icingapowershell": {0}{1}' -Objects $ResolveStatus.Message, $ResolveStatus.Error; + $Passed = $FALSE; + } else { + Write-IcingaTestOutput -Severity 'Passed' -Message 'Your service installation is no longer affected by IWKB000009'; + } + } else { + Write-IcingaTestOutput -Severity 'Failed' -Message 'Your service installation is affected by IWKB000009. Please have a look on https://icinga.com/docs/icinga-for-windows/latest/doc/knowledgebase/IWKB000009/ for further details. Run this Cmdlet with "-ResolveProblems" to fix it'; + $Passed = $FALSE; + } + } + + if ($ServiceConfig.ServicePath.Contains('.psm1')) { + if ($ResolveProblems) { + Write-IcingaTestOutput -Severity 'Warning' -Message 'Your service installation is referring to "icinga-powershell-framework.psm1" for module imports. Trying to resolve the problem.'; + $ResolveStatus = Start-IcingaProcess -Executable 'sc.exe' -Arguments ([string]::Format('config icingapowershell binPath= "{0}"', $PreparedServicePath)); + + if ($ResolveStatus.ExitCode -ne 0) { + Write-IcingaConsoleError 'Failed to resolve problems for service "icingapowershell": {0}{1}' -Objects $ResolveStatus.Message, $ResolveStatus.Error; + $Passed = $FALSE; + } else { + Write-IcingaTestOutput -Severity 'Passed' -Message 'Your service installation is now properly referring to "icinga-powershell-framework.psd1" for module imports.'; + } + } else { + Write-IcingaTestOutput -Severity 'Failed' -Message 'Your service installation is referring "icinga-powershell-framework.psm1". This is deprecated and has to be changed to "icinga-powershell-framework.psd1". Run this Cmdlet with "-ResolveProblems" to fix it.'; + $Passed = $FALSE; + } + } else { + Write-IcingaTestOutput -Severity 'Passed' -Message 'Your service installation is properly referring to "icinga-powershell-framework.psd1" for module imports.'; + } + + return $Passed; +} +function Test-IcingaForWindowsMigration() +{ + param ( + [Version]$MigrationVersion = $null + ); + + if ($null -eq $MigrationVersion) { + return $FALSE; + } + + [string]$CurrentFrameworkRoot = Get-IcingaFrameworkRootPath; + [array]$ListOfFrameworks = (Get-Module -ListAvailable -Name icinga-powershell-framework); + [Version]$CurrentFrameworkVersion = $ListOfFrameworks[0].Version; + [string]$MigrationConfigPath = [string]::Format('Framework.Migrations.{0}', $MigrationVersion.ToString().Replace('.', '')); + $VersionMigrationApplied = Get-IcingaPowerShellConfig -Path $MigrationConfigPath; + + if ($ListOfFrameworks.Count -gt 1) { + Write-IcingaConsoleWarning -Message 'Found multiple installations of the module "icinga-powershell-framework". Please check the list below and cleanup your installation to ensure system integrity' + foreach ($entry in $ListOfFrameworks) { + Write-Host ([string]::Format(' => Path "{0}" with version "{1}"', $entry.ModuleBase, $entry.Version)); + + # Ensure we use the correct version of the framework loaded within this session + if ($CurrentFrameworkRoot -eq $entry.ModuleBase) { + $CurrentFrameworkVersion = $entry.Version; + } + } + + Write-IcingaConsoleWarning -Message 'This instance of Icinga for Windows will run with Framework version "{0}"' -Objects $CurrentFrameworkVersion.ToString(); + } + + # Migration for this version is already applied + if ($VersionMigrationApplied) { + return $FALSE; + } + + if ($CurrentFrameworkVersion -ge $MigrationVersion) { + return $TRUE; + } + + return $FALSE; +} +<# +.SYNOPSIS + Compares a binary within a .zip file to a included .md5 to ensure + the checksum is matching +.DESCRIPTION + Compares a possible included .md5 checksum file with the provided binary + to ensure they are identical +.FUNCTIONALITY + Compares a binary within a .zip file to a included .md5 to ensure + the checksum is matching. +.EXAMPLE + PS>Test-IcingaZipBinaryChecksum -Path 'C:\Program Files\icinga-service\icinga-service.exe'; +.PARAMETER Path + Path to the binary to be checked for. A Corresponding .md5 file with the + extension added on the file is required, like icinga-service.exe.md5 +.INPUTS + System.String +.OUTPUTS + Null +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Test-IcingaZipBinaryChecksum() +{ + param( + $Path + ); + + $MD5Path = [string]::Format('{0}.md5', $Path); + + if ((Test-Path $MD5Path) -eq $FALSE) { + return $TRUE; + } + + [string]$MD5Checksum = Get-Content $MD5Path; + $MD5Checksum = ($MD5Checksum.Split(' ')[0]).ToLower(); + + $FileHash = ((Get-IcingaFileHash $Path -Algorithm MD5).Hash).ToLower(); + + if ($MD5Checksum -ne $FileHash) { + return $FALSE; + } + + return $TRUE; +} +<# +.SYNOPSIS + Fetch the raw output values for a check command for each single object + processed by New-IcingaCheck +.DESCRIPTION + Fetch the raw output values for a check command for each single object + processed by New-IcingaCheck +.FUNCTIONALITY + Fetch the raw output values for a check command for each single object + processed by New-IcingaCheck +.OUTPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaCheckSchedulerCheckData() +{ + return $global:Icinga.Private.Scheduler.CheckData; +} +function Set-IcingaForWindowsMigration() +{ + param ( + [Version]$MigrationVersion = $null + ); + + if ($null -eq $MigrationVersion) { + return; + } + + [string]$MigrationConfigPath = [string]::Format('Framework.Migrations.{0}', $MigrationVersion.ToString().Replace('.', '')); + + Set-IcingaPowerShellConfig -Path $MigrationConfigPath -Value $TRUE | Out-Null +} +<# +.SYNOPSIS + Returns the spent time since Start-IcingaTimer was executed in seconds for + a specific timer name +.DESCRIPTION + Returns the spent time since Start-IcingaTimer was executed in seconds for + a specific timer name +.FUNCTIONALITY + Returns the spent time since Start-IcingaTimer was executed in seconds for + a specific timer name +.EXAMPLE + PS>Show-IcingaTimer; +.EXAMPLE + PS>Show-IcingaTimer -Name 'My Test Timer'; +.PARAMETER Name + The name of a custom identifier to run multiple timers at once +.INPUTS + System.String +.OUTPUTS + Single +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Show-IcingaTimer() +{ + param ( + [string]$Name = 'DefaultTimer', + [switch]$ShowAll = $FALSE + ); + + $TimerObject = Get-IcingaTimer -Name $Name; + + if (-Not $ShowAll) { + if ($null -eq $TimerObject) { + Write-IcingaConsoleNotice 'A timer with the name "{0}" does not exist' -Objects $Name; + return; + } + + $TimerOutput = New-Object -TypeName PSObject; + $TimerOutput | Add-Member -MemberType NoteProperty -Name 'Timer Name' -Value $Name; + $TimerOutput | Add-Member -MemberType NoteProperty -Name 'Elapsed Seconds' -Value $TimerObject.Elapsed.TotalSeconds; + + $TimerOutput | Format-Table -AutoSize; + } else { + $TimerObjects = Get-IcingaHashtableItem -Key 'Timers' -Hashtable $Global:Icinga.Private; + + [array]$MultiOutput = @(); + + foreach ($TimerName in $TimerObjects.Keys) { + $TimerObject = $TimerObjects[$TimerName].Timer; + + $TimerOutput = New-Object -TypeName PSObject; + $TimerOutput | Add-Member -MemberType NoteProperty -Name 'Timer Name' -Value $TimerName; + $TimerOutput | Add-Member -MemberType NoteProperty -Name 'Elapsed Seconds' -Value $TimerObject.Elapsed.TotalSeconds; + $MultiOutput += $TimerOutput; + } + + $MultiOutput | Format-Table -AutoSize; + } +} +<# +.SYNOPSIS + Update the current version of the PowerShell Framework with a newer or older one +.DESCRIPTION + Allows you to specify a download url or being asked by a wizard on where a update for + the PowerShell framework can be fetched from and applies the up- or downgrade +.FUNCTIONALITY + Update the current version of the PowerShell Framework with a newer or older one +.EXAMPLE + PS>Install-IcingaFrameworkUpdate; +.EXAMPLE + PS>Install-IcingaFrameworkUpdate -FrameworkUrl 'C:/icinga/framework.zip'; +.EXAMPLE + PS>Install-IcingaFrameworkUpdate -FrameworkUrl 'https://github.com/Icinga/icinga-powershell-framework/archive/v1.0.2.zip'; +.PARAMETER FrameworkUrl + The url to a remote or local resource pointing directly to a .zip file containing the required files for updating +.INPUTS + System.String +.OUTPUTS + Null +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Install-IcingaFrameworkUpdate() +{ + param( + [string]$FrameworkUrl + ); + + $RepositoryName = 'icinga-powershell-framework'; + $Archive = Get-IcingaPowerShellModuleArchive -DownloadUrl $FrameworkUrl -ModuleName 'Icinga Framework' -Repository $RepositoryName; + + if ($Archive.Installed -eq $FALSE) { + return @{ + 'PluginUrl' = $Archive.DownloadUrl + }; + } + + Write-IcingaConsoleNotice ([string]::Format('Installing module into "{0}"', ($Archive.Directory))); + Expand-IcingaZipArchive -Path $Archive.Archive -Destination $Archive.Directory | Out-Null; + + $FolderContent = Get-ChildItem -Path $Archive.Directory; + $ModuleContent = $Archive.Directory; + + foreach ($entry in $FolderContent) { + if ($entry -like ([string]::Format('{0}*', $RepositoryName))) { + $ModuleContent = Join-Path -Path $ModuleContent -ChildPath $entry; + break; + } + } + + Write-IcingaConsoleNotice ([string]::Format('Using content of folder "{0}" for updates', $ModuleContent)); + + $ServiceStatus = (Get-Service 'icingapowershell' -ErrorAction SilentlyContinue).Status; + $AgentStatus = (Get-Service 'icinga2' -ErrorAction SilentlyContinue).Status; + + if ($ServiceStatus -eq 'Running') { + Write-IcingaConsoleNotice 'Stopping Icinga PowerShell service'; + Stop-IcingaForWindows; + Start-Sleep -Seconds 1; + } + if ($AgentStatus -eq 'Running') { + Write-IcingaConsoleNotice 'Stopping Icinga Agent service'; + Stop-IcingaService 'icinga2'; + Start-Sleep -Seconds 1; + } + + $ModuleDirectory = (Join-Path -Path $Archive.ModuleRoot -ChildPath $RepositoryName); + + if ((Test-Path $ModuleDirectory) -eq $FALSE) { + Write-IcingaConsoleError 'Failed to update the component. Module Root-Directory was not found'; + return; + } + + $Files = Get-ChildItem $ModuleDirectory -File '*'; + + Write-IcingaConsoleNotice 'Removing files from framework'; + + foreach ($ModuleFile in $Files) { + Remove-ItemSecure -Path $ModuleFile.FullName -Force | Out-Null; + } + + Remove-ItemSecure -Path (Join-Path $ModuleDirectory -ChildPath 'doc') -Recurse -Force | Out-Null; + Remove-ItemSecure -Path (Join-Path $ModuleDirectory -ChildPath 'lib') -Recurse -Force | Out-Null; + + Write-IcingaConsoleNotice 'Copying new files to framework'; + Copy-ItemSecure -Path (Join-Path $ModuleContent -ChildPath 'doc') -Destination $ModuleDirectory -Recurse -Force | Out-Null; + Copy-ItemSecure -Path (Join-Path $ModuleContent -ChildPath 'lib') -Destination $ModuleDirectory -Recurse -Force | Out-Null; + Copy-ItemSecure -Path (Join-Path $ModuleContent -ChildPath 'manifests') -Destination $ModuleDirectory -Recurse -Force | Out-Null; + Copy-ItemSecure -Path (Join-Path -Path $ModuleContent -ChildPath '/*') -Destination $ModuleDirectory -Recurse -Force | Out-Null; + + Unblock-IcingaPowerShellFiles -Path $ModuleDirectory; + + Write-IcingaConsoleNotice 'Cleaning temporary content'; + Start-Sleep -Seconds 1; + Remove-ItemSecure -Path $Archive.Directory -Recurse -Force | Out-Null; + + Write-IcingaConsoleNotice 'Updating Framework cache file'; + if (Test-IcingaFunction 'Write-IcingaFrameworkCodeCache') { + Write-IcingaFrameworkCodeCache; + } + + Import-Module -Name $ModuleDirectory -Force; + + # Apply migration tasks + Use-Icinga; + + if ([string]::IsNullOrEmpty((Get-IcingaJEAContext)) -eq $FALSE) { + Remove-IcingaFrameworkDependencyFile; + Write-IcingaConsoleNotice 'Updating Icinga JEA profile'; + & powershell.exe -Command { Use-Icinga -Minimal; Install-IcingaJEAProfile; } | Out-Null; + } + + Write-IcingaConsoleNotice 'Framework update has been completed. Please start a new PowerShell instance now to complete the update'; + + Test-IcingaForWindowsService -ResolveProblems | Out-Null; + Test-IcingaAgent; + + if ($ServiceStatus -eq 'Running') { + Write-IcingaConsoleNotice 'Starting Icinga PowerShell service'; + Start-IcingaService 'icingapowershell'; + } + if ($AgentStatus -eq 'Running') { + Write-IcingaConsoleNotice 'Starting Icinga Agent service'; + Start-IcingaService 'icinga2'; + } +} +<# +.SYNOPSIS + Wrapper for Start-Service which catches errors and prints proper output messages +.DESCRIPTION + Starts a service if it is installed and prints console messages if a start + was triggered or the service is not installed +.FUNCTIONALITY + Wrapper for Start-Service which catches errors and prints proper output messages +.EXAMPLE + PS>Start-IcingaService -Service 'icinga2'; +.PARAMETER Service + The name of the service to be started +.INPUTS + System.String +.OUTPUTS + Null +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Start-IcingaService() +{ + param( + $Service + ); + + if (Get-Service $Service -ErrorAction SilentlyContinue) { + Write-IcingaConsoleNotice -Message 'Starting service "{0}"' -Objects $Service; + + & powershell.exe -Command { + Use-Icinga -Minimal; + + $Service = $args[0]; + try { + Start-Service "$Service" -ErrorAction Stop; + Start-Sleep -Seconds 2; + Optimize-IcingaForWindowsMemory; + } catch { + Write-IcingaConsoleError -Message 'Failed to start service "{0}". Error: {1}' -Objects $Service, $_.Exception.Message; + } + } -Args $Service; + } else { + Write-IcingaConsoleWarning -Message 'The service "{0}" is not installed' -Objects $Service; + } + + Optimize-IcingaForWindowsMemory; +} +<# +.SYNOPSIS + Unblocks a folder with PowerShell module/script files to make them usable + on certain environments +.DESCRIPTION + Wrapper command to unblock recursively a certain folder for PowerShell script + and module files +.FUNCTIONALITY + Unblocks a folder with PowerShell module/script files to make them usable + on certain environments +.EXAMPLE + PS>Unblock-IcingaPowerShellFiles -Path 'C:\Program Files\WindowsPowerShell\Modules\my-module'; +.PARAMETER Path + The path to a PowerShell module folder or script file to unblock it +.INPUTS + System.String +.OUTPUTS + Null +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Unblock-IcingaPowerShellFiles() +{ + param( + $Path + ); + + if ([string]::IsNullOrEmpty($Path)) { + Write-IcingaConsoleError 'The specified directory was not found'; + return; + } + + Write-IcingaConsoleNotice 'Unblocking Icinga PowerShell Files'; + Get-ChildItem -Path $Path -Recurse | Unblock-File; +} +<# +.SYNOPSIS + Creates all environment variables for Icinga for Windows for the + PowerShell session +.DESCRIPTION + Creates all environment variables for Icinga for Windows for the + PowerShell session +.EXAMPLE + New-IcingaEnvironmentVariable; +#> + +function New-IcingaEnvironmentVariable() +{ + if ($null -eq $Global:Icinga) { + $Global:Icinga = @{ }; + } + + if ($Global:Icinga.ContainsKey('CacheBuilding') -eq $FALSE) { + $Global:Icinga.Add('CacheBuilding', $FALSE); + } + + # Session specific configuration for this shell + if ($Global:Icinga.ContainsKey('Private') -eq $FALSE) { + $Global:Icinga.Add('Private', @{ }); + + $Global:Icinga.Private.Add('Daemons', @{ }); + $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', + @{ + 'CheckCommand' = ''; + 'CheckData' = @{ }; + 'ThresholdCache' = @{ }; + 'CheckResults' = @(); + 'PerformanceData' = ''; + 'PluginException' = $null; + 'ExitCode' = $null; + 'PerfDataWriter' = @{ + 'Cache' = @{ }; + 'Storage' = (New-Object System.Text.StringBuilder); + 'Daemon' = @{ }; + 'MetricsOverTime' = ''; + } + } + ); + + $Global:Icinga.Private.Add( + 'PerformanceCounter', + @{ + 'Cache' = @{ }; + } + ); + } + + # Shared configuration for all threads + if ($Global:Icinga.ContainsKey('Public') -eq $FALSE) { + $Global:Icinga.Add('Public', [hashtable]::Synchronized(@{ })); + + $Global:Icinga.Public.Add('Daemons', @{ }); + $Global:Icinga.Public.Add('Threads', @{ }); + $Global:Icinga.Public.Add('ThreadPools', @{ }); + $Global:Icinga.Public.Add('ThreadAliveHousekeeping', @{ }); + } + + # Session specific configuration which should never be modified by users! + if ($Global:Icinga.ContainsKey('Protected') -eq $FALSE) { + $Global:Icinga.Add('Protected', @{ }); + + $Global:Icinga.Protected.Add('DeveloperMode', $FALSE); + $Global:Icinga.Protected.Add('DebugMode', $FALSE); + $Global:Icinga.Protected.Add('JEAContext', $FALSE); + $Global:Icinga.Protected.Add('RunAsDaemon', $FALSE); + $Global:Icinga.Protected.Add('Minimal', $FALSE); + $Global:Icinga.Protected.Add('ThreadName', ''); + $Global:Icinga.Protected.Add('GarbageCollector', @{ }); + $Global:Icinga.Protected.Add( + 'Environment', @{ + 'Icinga Service' = @{ + 'Status' = ''; + 'Present' = $FALSE; + 'Name' = 'icinga2'; + 'DisplayName' = 'icinga2'; + 'User' = 'NT Authority\NetworkService'; + 'ServicePath' = ''; + }; + 'PowerShell Service' = @{ + 'Status' = ''; + 'Present' = $FALSE; + 'Name' = 'icingapowershell'; + 'DisplayName' = 'icingapowershell'; + 'User' = 'NT Authority\NetworkService'; + 'ServicePath' = ''; + }; + 'FetchedServices' = $FALSE; + } + ); + } +} +<# +.SYNOPSIS + Wrapper for Remove-Item to securely remove items allowing better handling for errors +.DESCRIPTION + Removes files and folders from disk and catches possible exceptions with proper return + values to handle errors better +.FUNCTIONALITY + Wrapper for Remove-Item to securely remove items allowing better handling for errors +.EXAMPLE + PS>Remove-ItemSecure -Path C:\icinga; +.EXAMPLE + PS>Remove-ItemSecure -Path C:\icinga -Recurse; +.EXAMPLE + PS>Remove-ItemSecure -Path C:\icinga -Recurse -Force; +.PARAMETER Path + The path to a file or folder you wish you delete +.PARAMETER Recurse + Removes sub-folders and sub-files for a given location +.PARAMETER Force + Tries to forcefully removes a files and folders if they are either being used or a folder is + still containing items +.PARAMETER Retries + In case the object/folder is not removed without errors, the function will re-try the attempt + as often as specified with a sleep of 2 seconds between attempts to avoid possible file locks + for other operations +.INPUTS + System.String +.OUTPUTS + System.Boolean +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Remove-ItemSecure() +{ + param ( + [string]$Path, + [switch]$Recurse = $FALSE, + [switch]$Force = $FALSE, + [int]$Retries = 1 + ); + + if ([string]::IsNullOrEmpty($Path) -Or (Test-Path $Path) -eq $FALSE) { + Write-IcingaConsoleError 'The provided path "{0}" does not exist' -Objects $Path; + return $FALSE; + } + + if ($Retries -le 0) { + $Retries = 1; + } + + $ExMsg = $null; + + while ($Retries -gt 0) { + try { + Remove-Item -Path $Path -Recurse:$Recurse -Force:$Force -ErrorAction Stop; + return $TRUE; + } catch { + $ExMsg = $_.Exception; + } + + Start-Sleep -Seconds 2; + Write-IcingaConsoleNotice -Message 'Waiting for lock on path "{0}" to be released before trying to remove it' -Objects $Path; + $Retries -= 1; + } + + Write-IcingaConsoleError 'Failed to remove items from path "{0}". Recurse is "{1}", Force is "{2}": "{3}"' -Objects $Path, $Recurse, $Force, $ExMsg; + + return $FALSE; +} +<# +.SYNOPSIS + Allows to enable any console output for this PowerShell session +.DESCRIPTION + Allows to enable any console output for this PowerShell session +.FUNCTIONALITY + Allows to enable any console output for this PowerShell session +.EXAMPLE + PS>Enable-IcingaFrameworkConsoleOutput; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Enable-IcingaFrameworkConsoleOutput() +{ + if ($null -eq $global:Icinga) { + $global:Icinga = @{ }; + } + + if ($global:Icinga.ContainsKey('DisableConsoleOutput') -eq $FALSE) { + $global:Icinga.Add('DisableConsoleOutput', $FALSE); + } else { + $global:Icinga.DisableConsoleOutput = $FALSE; + } +} +function Invoke-IcingaForWindowsMigration() +{ + $IcingaForWindowsService = Get-IcingaForWindowsServiceData; + + if (([string]::IsNullOrEmpty($IcingaForWindowsService.FullPath) -eq $FALSE -And (Test-Path $IcingaForWindowsService.FullPath))) { + $ServiceBinaryData = Read-IcingaServicePackage -File $IcingaForWindowsService.FullPath; + + if ($ServiceBinaryData.FileVersion -lt (New-IcingaVersionObject -Version '1.2.0')) { + Write-IcingaConsoleWarning -Message 'You are running a Icinga for Windows Service binary older than v1.2.0. You need to upgrade to v1.2.0 or later before you can use Icinga for Windows properly. You can update it with "Update-Icinga -Name service"'; + return; + } + } + + # Upgrade to v1.8.0 + if (Test-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.8.0')) { + $ServiceStatus = (Get-Service 'icingapowershell' -ErrorAction SilentlyContinue).Status; + + Write-IcingaConsoleNotice 'Applying pending migrations required for Icinga for Windows v1.8.0'; + if ($ServiceStatus -eq 'Running') { + Stop-IcingaForWindows; + } + + $ApiChecks = Get-IcingaPowerShellConfig -Path 'Framework.Experimental.UseApiChecks'; + + if ($null -ne $ApiChecks) { + Remove-IcingaPowerShellConfig -Path 'Framework.Experimental.UseApiChecks' | Out-Null; + Set-IcingaPowerShellConfig -Path 'Framework.ApiChecks' -Value $ApiChecks; + } + + # Remove all prior EventLog handler + Unregister-IcingaEventLog; + # Add new Icinga for Windows EventLog + Register-IcingaEventLog; + + Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.8.0'); + + # For upgrading to v1.8.0 it is required to restart the Icinga for Windows service + if ($ServiceStatus -eq 'Running') { + Restart-IcingaService -Service 'icingapowershell'; + } + } + + if (Test-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.10.0')) { + Write-IcingaConsoleNotice 'Applying pending migrations required for Icinga for Windows v1.10.0'; + + $ServiceStatus = (Get-Service 'icingapowershell' -ErrorAction SilentlyContinue).Status; + + if ($ServiceStatus -eq 'Running') { + Stop-IcingaForWindows; + } + + # Convert the time intervals for the background daemon services from the previous index handling + # 1, 3, 5, 15 as example to 1m, 3m, 5m, 15m + $BackgroundServices = Get-IcingaPowerShellConfig -Path 'BackgroundDaemon.RegisteredServices'; + + # Only run this migration in case background services are defined + if ($null -ne $BackgroundServices) { + foreach ($service in $BackgroundServices.PSObject.Properties) { + [array]$ConvertedTimeIndex = @(); + + foreach ($interval in $service.Value.TimeIndexes) { + if (Test-Numeric $interval) { + $ConvertedTimeIndex += [string]::Format('{0}m', $interval); + } else { + $ConvertedTimeIndex = $interval; + } + } + + $service.Value.TimeIndexes = $ConvertedTimeIndex; + } + + Set-IcingaPowerShellConfig -Path 'BackgroundDaemon.RegisteredServices' -Value $BackgroundServices; + } + + Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.10.0'); + + if ($ServiceStatus -eq 'Running') { + Restart-IcingaForWindows; + } + } + + if (Test-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.10.1')) { + Write-IcingaConsoleNotice 'Applying pending migrations required for Icinga for Windows v1.10.1'; + + # Fix Icinga for Windows v1.10.0 broken background service registration + if ($null -eq (Get-IcingaPowerShellConfig -Path 'BackgroundDaemon.RegisteredServices')) { + Remove-IcingaPowerShellConfig -Path 'BackgroundDaemon.RegisteredServices'; + } + + 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; + # Start the task to ensure the certificate is generated + Start-IcingaWindowsScheduledTaskRenewCertificate; + + Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.12.0'); + } + + if (Test-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.12.1')) { + Write-IcingaConsoleNotice 'Applying pending migrations required for Icinga for Windows v1.12.1'; + + # Fixes the size of the Icinga for Windows Eventlog, allowing more logs to be collected + # before older ones are faded out + Register-IcingaEventLog; + + # Fixes user environment which is now set to LocalSystem, allowing configurations over WinRM and SSH + Register-IcingaWindowsScheduledTaskRenewCertificate -Force; + + Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.12.1'); + } + + if (Test-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.12.2')) { + Write-IcingaConsoleNotice 'Applying pending migrations required for Icinga for Windows v1.12.2'; + + # Revokes certificate handling to run as local Administrators group with highest privileges instead of LocalSystem + Register-IcingaWindowsScheduledTaskRenewCertificate -Force; + Start-Sleep -Seconds 1; + # Enforce the certificate creation to update broken certificates + Start-IcingaWindowsScheduledTaskRenewCertificate; + # Restart the Icinga for Windows service + Start-Sleep -Seconds 2; + Restart-IcingaForWindows; + + Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.12.2'); + } + + if (Test-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.12.3')) { + Write-IcingaConsoleNotice 'Applying pending migrations required for Icinga for Windows v1.12.3'; + + # Updates certificate renew task to properly handle changes in the certificate renewal process + Register-IcingaWindowsScheduledTaskRenewCertificate -Force; + Start-Sleep -Seconds 1; + # Enforce the certificate creation to update broken certificates + Start-IcingaWindowsScheduledTaskRenewCertificate; + # Restart the Icinga for Windows service + Start-Sleep -Seconds 2; + Restart-IcingaForWindows; + + Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.12.3'); + } + + if (Test-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.13.0')) { + Write-IcingaConsoleNotice 'Applying pending migrations required for Icinga for Windows v1.13.0'; + + # Updates certificate renew task to handle changes made which now stores the Icinga CA inside the cert store + Start-IcingaWindowsScheduledTaskRenewCertificate; + # Ensure the Icinga Agent is not spamming the Application log by default + Write-IcingaAgentEventLogConfig -Severity 'warning'; + # Set our newly added process update task + Register-IcingaWindowsScheduledTaskProcessPriority -Force; + + Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.13.0'); + } + + if (Test-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.13.0.1')) { + Write-IcingaConsoleNotice 'Applying pending migrations required for Icinga for Windows v1.13.0.1'; + + # Set our newly added process update task + Register-IcingaWindowsScheduledTaskProcessPriority -Force; + + Set-IcingaForWindowsMigration -MigrationVersion (New-IcingaVersionObject -Version '1.13.0.1'); + } +} +<# +.SYNOPSIS + Disables the debug mode of the Framework +.DESCRIPTION + Disables the debug mode of the Framework +.FUNCTIONALITY + Disables the Icinga for Windows Debug-Log +.EXAMPLE + PS>Disable-IcingaFrameworkDebugMode; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Disable-IcingaFrameworkDebugMode() +{ + $Global:Icinga.Protected.DebugMode = $FALSE; + Set-IcingaPowerShellConfig -Path 'Framework.DebugMode' -Value $FALSE; +} +<# +.SYNOPSIS + Function to fetch the last executed plugin output from an internal memory + cache in case the Framework is running as daemon. +.DESCRIPTION + While running the Framework as daemon, checkresults for plugins are not + printed into the console but written into an internal memory cache. Once + a plugin was executed, use this function to fetch the plugin output +.FUNCTIONALITY + Returns the last checkresult output for executed plugins while the + Framework is running as daemon +.OUTPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaCheckSchedulerPluginOutput() +{ + if ($Global:Icinga.Private.Scheduler.CheckResults.Count -eq 0) { + return @(); + } + + $CheckResult = [string]::Join("`r`n", $Global:Icinga.Private.Scheduler.CheckResults); + [array]$Global:Icinga.Private.Scheduler.CheckResults = @(); + + return $CheckResult; +} +<# +.SYNOPSIS + Function to fetch the last executed plugin performance data + from an internal memory cache in case the Framework is running as daemon. +.DESCRIPTION + While running the Framework as daemon, check results for plugins are not + printed into the console but written into an internal memory cache. Once + a plugin was executed, use this function to fetch the plugin performance data +.FUNCTIONALITY + Returns the last performance data output for executed plugins while the + Framework is running as daemon +.OUTPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaCheckSchedulerPerfData() +{ + [string]$PerfData = $Global:Icinga.Private.Scheduler.PerformanceData; + [string]$Global:Icinga.Private.Scheduler.PerformanceData = ''; + + # Ensure we clear our PerfDataWriter cache and storage to have a clean base state for the next plugin execution + $Global:Icinga.Private.Scheduler.PerfDataWriter.Cache.Clear() | Out-Null; + $Global:Icinga.Private.Scheduler.PerfDataWriter.Storage.Clear() | Out-Null; + + return $PerfData; +} +<# +.SYNOPSIS + Start a new timer for a given name and stores it within the $globals section + of the Icinga PowerShell Framework +.DESCRIPTION + Start a new timer for a given name and stores it within the $globals section + of the Icinga PowerShell Framework +.FUNCTIONALITY + Start a new timer for a given name and stores it within the $globals section + of the Icinga PowerShell Framework +.EXAMPLE + PS>Start-IcingaTimer; +.EXAMPLE + PS>Start-IcingaTimer -Name 'My Test Timer'; +.PARAMETER Name + The name of a custom identifier to run mutliple timers at once +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Start-IcingaTimer() +{ + param ( + [string]$Name = 'DefaultTimer' + ); + + if ((Test-IcingaTimer -Name $Name)) { + Write-IcingaConsoleNotice 'The timer with the name "{0}" is already active' -Objects $Name; + return; + } + + # Load the library first + [System.Reflection.Assembly]::LoadWithPartialName("System.Diagnostics") | Out-Null; + $TimerObject = New-Object System.Diagnostics.Stopwatch; + $TimerObject.Start(); + + Add-IcingaHashtableItem -Key $Name -Value ( + @{ + 'Active' = $TRUE; + 'Timer' = $TimerObject; + } + ) -Hashtable $Global:Icinga.Private.Timers -Override | Out-Null; +} +<# +.SYNOPSIS + Sets a private variable for the Icinga for Windows environment + to use within the current PowerShell Session +.DESCRIPTION + Sets a private variable for the Icinga for Windows environment + to use within the current PowerShell Session +.PARAMETER Name + The name of the variable +.PARAMETER Value + The value the variable will be assigned with +.EXAMPLE + Set-IcingaPrivateEnvironmentVariable -Name 'AddTypeFunctions' -Value @{ 'IcingaDiskAttributes', $TRUE }; +#> + +function Set-IcingaPrivateEnvironmentVariable() +{ + param ( + [string]$Name, + $Value + ); + + if ([string]::IsNullOrEmpty($Name)) { + return; + } + + if ($global:Icinga.Private.ContainsKey($Name) -eq $FALSE) { + $global:Icinga.Private.Add($Name, $Value); + return; + } + + $global:Icinga.Private[$Name] = $Value; +} +<# +.SYNOPSIS + Disables the feature to forward all executed checks to an internal + installed API to run them within a daemon +.DESCRIPTION + Disables the feature to forward all executed checks to an internal + installed API to run them within a daemon +.FUNCTIONALITY + Disables the Icinga for Windows Api checks forwarded +.EXAMPLE + PS>Disable-IcingaFrameworkApiChecks; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Disable-IcingaFrameworkApiChecks() +{ + Set-IcingaPowerShellConfig -Path 'Framework.ApiChecks' -Value $FALSE; +} +<# +.SYNOPSIS + Extracts a ZIP-Archive to a certain location +.DESCRIPTION + Unzips a ZIP-Archive on to a certain location +.FUNCTIONALITY + Unzips a ZIP-Archive on to a certain location +.EXAMPLE + PS>Expand-IcingaZipArchive -Path 'C:\users\public\test.zip' -Destination 'C:\users\public\'; +.PARAMETER Path + The location of your ZIP-Archive +.PARAMETER Destination + The target destination to extract the ZIP-Archive to +.INPUTS + System.String +.OUTPUTS + System.Boolean +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Expand-IcingaZipArchive() +{ + param( + $Path, + $Destination + ); + + if ((Test-Path $Path) -eq $FALSE -Or (Test-Path $Destination) -eq $FALSE) { + Write-IcingaConsoleError 'The path to the zip archive or the destination path do not exist'; + return $FALSE; + } + + Add-Type -AssemblyName System.IO.Compression.FileSystem; + + try { + [System.IO.Compression.ZipFile]::ExtractToDirectory($Path, $Destination); + return $TRUE; + } catch { + throw $_.Exception; + } + + return $FALSE; +} +<# +.SYNOPSIS + Installs a PowerShell Module within the 'icinga-powershell-' namespace + from GitHub or custom locations and installs it into the module directory + the Framework itself is installed to +.DESCRIPTION + Installs a PowerShell Module within the 'icinga-powershell-' namespace + from GitHub or custom locations and installs it into the module directory + the Framework itself is installed to +.FUNCTIONALITY + Download and install a PowerShell module from the 'icinga-powershell-' namespace +.EXAMPLE + PS>Install-IcingaFrameworkComponent -Name 'plugins' -Release; +.EXAMPLE + PS>Install-IcingaFrameworkComponent -Name 'plugins' -Release -DryRun; +.PARAMETER Name + The name of the module to install. The namespace 'icinga-powershell-' is added + by the function automatically +.PARAMETER GitHubUser + Overwrite the default GitHub user for a different one to download modules from +.PARAMETER Url + Specify a direct Url to a ZIP-Archive for external or local web ressources or + local network shares +.PARAMETER Release + Download the latest Release version from a GitHub source +.PARAMETER Snapshot + Download the latest master branch from a GitHub source +.PARAMETER DryRun + Only fetch possible Urls and return the result. No download or installation + will be done +.INPUTS + System.String +.OUTPUTS + System.Hashtable +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Install-IcingaFrameworkComponent() +{ + param( + [string]$Name, + [string]$GitHubUser = 'Icinga', + [string]$Url, + [switch]$Release = $FALSE, + [switch]$Snapshot = $FALSE, + [switch]$DryRun = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + throw 'Please specify a component name to install from a GitHub/Local space'; + } + + Set-IcingaTLSVersion; + + $TextInfo = (Get-Culture).TextInfo; + $ComponentName = $TextInfo.ToTitleCase($Name); + $RepositoryName = [string]::Format('icinga-powershell-{0}', $Name); + $Archive = Get-IcingaPowerShellModuleArchive ` + -DownloadUrl $Url ` + -GitHubUser $GitHubUser ` + -ModuleName ( + [string]::Format( + 'Icinga {0}', $ComponentName + ) + ) ` + -Repository $RepositoryName ` + -Release $Release ` + -Snapshot $Snapshot ` + -DryRun $DryRun; + + if ($Archive.Installed -eq $FALSE -Or $DryRun) { + return @{ + 'RepoUrl' = $Archive.DownloadUrl + }; + } + + Write-IcingaConsoleNotice ([string]::Format('Installing module into "{0}"', ($Archive.Directory))); + Expand-IcingaZipArchive -Path $Archive.Archive -Destination $Archive.Directory | Out-Null; + + $FolderContent = Get-ChildItem -Path $Archive.Directory; + $ModuleContent = $Archive.Directory; + + foreach ($entry in $FolderContent) { + if ($entry -like ([string]::Format('{0}*', $RepositoryName))) { + $ModuleContent = Join-Path -Path $ModuleContent -ChildPath $entry; + break; + } + } + + Write-IcingaConsoleNotice ([string]::Format('Using content of folder "{0}" for updates', $ModuleContent)); + + $PluginDirectory = (Join-Path -Path $Archive.ModuleRoot -ChildPath $RepositoryName); + + if ((Test-Path $PluginDirectory) -eq $FALSE) { + Write-IcingaConsoleNotice ([string]::Format('{0} Module Directory "{1}" is not present. Creating Directory', $ComponentName, $PluginDirectory)); + New-Item -Path $PluginDirectory -ItemType Directory | Out-Null; + } + + Write-IcingaConsoleNotice ([string]::Format('Copying files to {0}', $ComponentName)); + Copy-ItemSecure -Path (Join-Path -Path $ModuleContent -ChildPath '/*') -Destination $PluginDirectory -Recurse -Force | Out-Null; + + Write-IcingaConsoleNotice 'Cleaning temporary content'; + Start-Sleep -Seconds 1; + Remove-ItemSecure -Path $Archive.Directory -Recurse -Force | Out-Null; + + Unblock-IcingaPowerShellFiles -Path $PluginDirectory; + + # In case the plugins are not installed before, load the framework again to + # include the plugins + Use-Icinga; + + if ([string]::IsNullOrEmpty((Get-IcingaJEAContext)) -eq $FALSE) { + Write-IcingaConsoleNotice 'Updating Icinga JEA profile'; + & powershell.exe -Command { Use-Icinga -Minimal; Install-IcingaJEAProfile; } | Out-Null; + } + + # Unload the module if it was loaded before + Remove-Module $PluginDirectory -Force -ErrorAction SilentlyContinue; + # Now import the module + Import-Module $PluginDirectory; + + Write-IcingaConsoleNotice ([string]::Format('Icinga {0} update has been completed. Please start a new PowerShell to apply it', $ComponentName)); + + return @{ + 'RepoUrl' = $Archive.DownloadUrl + }; +} +<# +.SYNOPSIS + Clears the entire check scheduler cache environment and frees memory as + well as cleaning the stack +.DESCRIPTION + Clears the entire check scheduler cache environment and frees memory as + well as cleaning the stack +.FUNCTIONALITY + Clears the entire check scheduler cache environment and frees memory as + well as cleaning the stack +.OUTPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Clear-IcingaCheckSchedulerEnvironment() +{ + param ( + [switch]$ClearCheckData = $FALSE + ); + + Get-IcingaCheckSchedulerPluginOutput | Out-Null; + Get-IcingaCheckSchedulerPerfData | Out-Null; + + if ($ClearCheckData) { + Clear-IcingaCheckSchedulerCheckData; + } + + $Global:Icinga.Private.Scheduler.PluginException = $null; + $Global:Icinga.Private.Scheduler.CheckResults = $null; + $Global:Icinga.Private.Scheduler.ExitCode = $null; +} +<# +.SYNOPSIS + Clear all cached values for all check commands executed by this thread. + This is mandatory as we might run into a memory leak otherwise! +.DESCRIPTION + Clear all cached values for all check commands executed by this thread. + This is mandatory as we might run into a memory leak otherwise! +.FUNCTIONALITY + Clear all cached values for all check commands executed by this thread. + This is mandatory as we might run into a memory leak otherwise! +.OUTPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Clear-IcingaCheckSchedulerCheckData() +{ + $global:Icinga.Private.Scheduler.CheckData.Clear(); +} +<# +.SYNOPSIS + Tests if a specific timer object is already present and started with Start-IcingaTimer +.DESCRIPTION + Tests if a specific timer object is already present and started with Start-IcingaTimer +.FUNCTIONALITY + Tests if a specific timer object is already present and started with Start-IcingaTimer +.EXAMPLE + PS>Test-IcingaTimer; +.EXAMPLE + PS>Test-IcingaTimer -Name 'My Test Timer'; +.PARAMETER Name + The name of a custom identifier to run mutliple timers at once +.INPUTS + System.String +.OUTPUTS + Boolean +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Test-IcingaTimer() +{ + param ( + [string]$Name = 'DefaultTimer' + ); + + $TimerData = Get-IcingaHashtableItem -Key $Name -Hashtable $Global:Icinga.Private.Timers; + + if ($null -eq $TimerData) { + return $FALSE; + } + + return $TimerData.Active; +} +<# +.SYNOPSIS + A more secure way to copy items from one location to another including error handling +.DESCRIPTION + Wrapper for the Copy-Item Cmdlet to more securely copy items with error + handling to prevent interuptions during actions +.FUNCTIONALITY + Copies items from a source to a destination location +.EXAMPLE + PS>Copy-ItemSecure -Path 'C:\users\public\test.txt' -Destination 'C:\users\public\text2.txt'; +.EXAMPLE + PS>Copy-ItemSecure -Path 'C:\users\public\testfolder\' -Destination 'C:\users\public\testfolder2\' -Recurse; +.PARAMETER Path + The location you wish to copy from. Can either be a file or a directory +.PARAMETER Destination + The target destination to copy to. Can either be a file or a directory +.PARAMETER Recurse + Include possible sub-folders +.PARAMETER Force + Overwrite already existing files/folders +.INPUTS + System.String +.OUTPUTS + System.Boolean +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> +function Copy-ItemSecure() +{ + param( + [string]$Path, + [string]$Destination, + [switch]$Recurse, + [switch]$Force + ); + + if ((Test-Path $Path) -eq $FALSE) { + return $FALSE; + } + + try { + if ($Recurse -And $Force) { + Copy-Item -Path $Path -Destination $Destination -Recurse -Force; + } elseif ($Recurse -And -Not $Force) { + Copy-Item -Path $Path -Destination $Destination -Recurse; + } elseif (-Not $Recurse -And $Force) { + Copy-Item -Path $Path -Destination $Destination -Force; + } else { + Copy-Item -Path $Path -Destination $Destination; + } + return $TRUE; + } catch { + Write-IcingaConsoleError -Message 'Failed to copy items from path "{0}" to "{1}": {2}' -Objects $Path, $Destination, $_.Exception; + } + return $FALSE; +} +<# +.SYNOPSIS + Get the version of an installed PowerShell Module +.DESCRIPTION + Get the version of an installed PowerShell Module +.FUNCTIONALITY + Get the version of an installed PowerShell Module +.EXAMPLE + PS>Get-IcingaPowerShellModuleVersion -ModuleName 'icinga-powershell-framework'; +.EXAMPLE + PS>Get-IcingaPowerShellModuleVersion -ModuleName 'icinga-powershell-plugins'; +.PARAMETER ModuleName + The PowerShell module to fetch the installed version from +.INPUTS + System.String +.OUTPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaPowerShellModuleVersion() +{ + param( + $ModuleName + ); + + $ModuleDetails = Get-Module -ListAvailable $ModuleName; + + if ($null -eq $ModuleDetails) { + return $null; + } + + return $ModuleDetails.PrivateData.Version; +} +<# +.SYNOPSIS + Download a PowerShell Module from a custom source or from GitHub + by providing a repository and the user space +.DESCRIPTION + Download a PowerShell Module from a custom source or from GitHub + by providing a repository and the user space +.FUNCTIONALITY + Download and install a PowerShell module from a custom or GitHub source +.EXAMPLE + PS>Get-IcingaPowerShellModuleArchive -ModuleName 'Plugins' -Repository 'icinga-powershell-plugins' -Release 1; +.EXAMPLE + PS>Get-IcingaPowerShellModuleArchive -ModuleName 'Plugins' -Repository 'icinga-powershell-plugins' -Release 1 -DryRun 1; +.PARAMETER DownloadUrl + The Url to a ZIP-Archive to download from (skips the wizard) +.PARAMETER ModuleName + The name which is used inside output messages +.PARAMETER Repository + The repository to download the ZIP-Archive from +.PARAMETER GitHubUser + The user from which a repository is downloaded from +.PARAMETER Release + Download the latest release +.PARAMETER Snapshot + Download the latest package from the master branch +.PARAMETER DryRun + Only return the finished build Url including the version to install but + do not modify the system in any way +.INPUTS + System.String +.OUTPUTS + System.Hashtable +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaPowerShellModuleArchive() +{ + param( + [string]$DownloadUrl = '', + [string]$ModuleName = '', + [string]$Repository = '', + [string]$GitHubUser = 'Icinga', + [bool]$Release = $FALSE, + [bool]$Snapshot = $FALSE, + [bool]$DryRun = $FALSE + ); + + Set-IcingaTLSVersion; + $ProgressPreference = "SilentlyContinue"; + $Tag = 'master'; + [bool]$SkipRepo = $FALSE; + + if ($Release -Or $Snapshot) { + $SkipRepo = $TRUE; + } + + # Fix TLS errors while connecting to GitHub with old PowerShell versions + [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11"; + + if ([string]::IsNullOrEmpty($DownloadUrl)) { + if ($SkipRepo -Or (Get-IcingaAgentInstallerAnswerInput -Prompt ([string]::Format('Do you provide a custom repository for "{0}"?', $ModuleName)) -Default 'n').result -eq 1) { + if ($Release -eq $FALSE -And $Snapshot -eq $FALSE) { + $branch = (Get-IcingaAgentInstallerAnswerInput -Prompt ([string]::Format('Which version of the "{0}" do you want to install? (release/snapshot)', $ModuleName)) -Default 'v' -DefaultInput 'release').answer; + } elseif ($Release) { + $branch = 'release'; + } else { + $branch = 'snapshot' + } + if ($branch.ToLower() -eq 'snapshot') { + $DownloadUrl = [string]::Format('https://github.com/{0}/{1}/archive/master.zip', $GitHubUser, $Repository); + } else { + $WebResponse = Invoke-IcingaWebRequest -Uri 'https://github.com/{0}/{1}/releases/latest' -Objects $GitHubUser, $Repository -UseBasicParsing; + + if ($null -eq $WebResponse.HasErrors -Or $WebResponse.HasErrors -eq $FALSE) { + $LatestRelease = $WebResponse.BaseResponse.ResponseUri.AbsoluteUri; + $DownloadUrl = $LatestRelease.Replace('/releases/tag/', '/archive/'); + $Tag = $DownloadUrl.Split('/')[-1]; + } else { + Write-IcingaConsoleError -Message 'Failed to fetch latest release for "{0}" from GitHub. Either the module or the GitHub account do not exist' -Objects $ModuleName; + } + + $DownloadUrl = [string]::Format('{0}/{1}.zip', $DownloadUrl, $Tag); + + $CurrentVersion = Get-IcingaPowerShellModuleVersion $Repository; + + if ($null -ne $CurrentVersion -And $CurrentVersion -eq $Tag) { + Write-IcingaConsoleNotice -Message 'Your "{0}" is already up-to-date' -Objects $ModuleName; + return @{ + 'DownloadUrl' = $DownloadUrl; + 'Version' = $Tag; + 'Directory' = ''; + 'Archive' = ''; + 'ModuleRoot' = (Get-IcingaForWindowsRootPath); + 'Installed' = $FALSE; + }; + } + } + } else { + $DownloadUrl = (Get-IcingaAgentInstallerAnswerInput -Prompt ([string]::Format('Please enter the full path of the custom repository for the "{0}" (location of zip file)', $ModuleName)) -Default 'v').answer; + } + } + + if ($DryRun) { + return @{ + 'DownloadUrl' = $DownloadUrl; + 'Version' = $Tag; + 'Directory' = ''; + 'Archive' = ''; + 'ModuleRoot' = (Get-IcingaForWindowsRootPath); + 'Installed' = $FALSE; + }; + } + + $DownloadDirectory = New-IcingaTemporaryDirectory; + $DownloadDestination = (Join-Path -Path $DownloadDirectory -ChildPath ([string]::Format('{0}.zip', $Repository))); + Write-IcingaConsoleNotice ([string]::Format('Downloading "{0}" into "{1}"', $ModuleName, $DownloadDirectory)); + + if ((Invoke-IcingaWebRequest -UseBasicParsing -Uri $DownloadUrl -OutFile $DownloadDestination).HasErrors) { + Write-IcingaConsoleError ([string]::Format('Failed to download "{0}" into "{1}". Starting cleanup process', $ModuleName, $DownloadDirectory)); + Start-Sleep -Seconds 2; + Remove-ItemSecure -Path $DownloadDirectory -Recurse -Force -Retries 5 | Out-Null; + + Write-IcingaConsoleNotice 'Starting to re-run the download wizard'; + + return Get-IcingaPowerShellModuleArchive -ModuleName $ModuleName -Repository $Repository; + } + + return @{ + 'DownloadUrl' = $DownloadUrl; + 'Version' = $Tag; + 'Directory' = $DownloadDirectory; + 'Archive' = $DownloadDestination; + 'ModuleRoot' = (Get-IcingaForWindowsRootPath); + 'Installed' = $TRUE; + }; +} +<# +.SYNOPSIS + Uninstalls a specific module within the icinga-powershell-* namespace + inside your PowerShell module folder +.DESCRIPTION + Uninstalls a specific module within the icinga-powershell-* namespace + inside your PowerShell module folder +.FUNCTIONALITY + Uninstalls a specific module within the icinga-powershell-* namespace + inside your PowerShell module folder +.PARAMETER Name + The component you want to uninstall, like 'plugins' or 'mssql' +.INPUTS + System.String +.OUTPUTS + Null +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Uninstall-IcingaFrameworkComponent() +{ + param ( + [string]$Name = '' + ); + + $ModuleBase = Get-IcingaForWindowsRootPath; + $UninstallComponent = [string]::Format('icinga-powershell-{0}', $Name); + $UninstallPath = Join-Path -Path $ModuleBase -ChildPath $UninstallComponent; + + if ((Test-Path $UninstallPath) -eq $FALSE) { + Write-IcingaConsoleNotice -Message 'The Icinga for Windows component "{0}" at "{1}" could not ne found.' -Objects $UninstallComponent, $UninstallPath; + return $FALSE; + } + + Write-IcingaConsoleNotice -Message 'Uninstalling Icinga for Windows component "{0}" from "{1}"' -Objects $UninstallComponent, $UninstallPath; + if (Remove-ItemSecure -Path $UninstallPath -Recurse -Force) { + Write-IcingaConsoleNotice -Message 'Successfully removed Icinga for Windows component "{0}" from "{1}"' -Objects $UninstallComponent, $UninstallPath; + if ($UninstallComponent -ne 'icinga-powershell-framework') { + Remove-Module $UninstallComponent -Force -ErrorAction SilentlyContinue; + } + return $TRUE; + } else { + Write-IcingaConsoleError -Message 'Unable to uninstall Icinga for Windows component "{0}" from "{1}"' -Objects $UninstallComponent, $UninstallPath; + } + + return $FALSE; +} +function Read-IcingaWindowsEventLog() +{ + param ( + [string]$LogName = 'Application', + [array]$Source = @(), + [array]$Include = @(), + [array]$Exclude = @(), + [int]$MaxEntries = 500 + ); + + if ([string]::IsNullOrEmpty($LogName)) { + Write-IcingaConsoleError 'You have to specify a log to read from'; + return; + } + + $LastEvent = $null; + $LastMessage = $null; + $LastId = $null; + $MaxEvents = 40000; + + while ($TRUE) { + [array]$IcingaEvents = Get-WinEvent -LogName $LogName -MaxEvents $MaxEvents -ErrorAction SilentlyContinue; + [int]$CurrentIndex = $MaxEntries; + [array]$CollectedEvents = @(); + + foreach ($event in $IcingaEvents) { + + if ($CurrentIndex -eq 0) { + break; + } + + if ($Source.Count -ne 0 -And $Source -NotContains $event.ProviderName) { + continue; + } + + $CurrentIndex -= 1; + + if ($null -ne $LastEvent -And $event.TimeCreated -lt $LastEvent) { + $MaxEvents = 500; + break; + } + + if ($event.TimeCreated -eq $LastEvent -And (Get-StringSha1 -Content $event.Message) -eq $LastMessage -And $event.Id -eq $LastId) { + $MaxEvents = 500; + break; + } + + if ((Test-IcingaArrayFilter -InputObject $event.Message -Include $Include -Exclude $Exclude) -eq $FALSE) { + continue; + } + + $CollectedEvents += $event; + } + + if ($null -ne $IcingaEvents) { + $IcingaEvents.Dispose(); + $IcingaEvents = $null; + } + + $CollectedEvents = $CollectedEvents | Sort-Object { $_.TimeCreated }; + + foreach ($event in $CollectedEvents) { + + $ForeColor = 'White'; + + if ($event.Level -eq 3) { # Warning + $ForeColor = 'DarkYellow'; + } elseif ($event.Level -eq 2) { # Error + $ForeColor = 'Red'; + } + + $LastMessage = (Get-StringSha1 -Content $event.Message); + $LastId = $event.Id; + $LastEvent = [DateTime]$event.TimeCreated; + + Write-IcingaConsolePlain -Message '[{0}] {1}' -Objects $event.TimeCreated, $event.Message -ForeColor $ForeColor; + } + + $CollectedEvents = $null; + + Start-Sleep -Seconds 1; + # Force Icinga for Windows Garbage Collection + Optimize-IcingaForWindowsMemory -ClearErrorStack; + } +} +<# +.SYNOPSIS + Installs the Icinga Plugins PowerShell module from a remote or local source +.DESCRIPTION + Installs the Icinga PowerShell Plugins from a remote or local source into the + PowerShell module folder and makes them available for usage with Icinga 2 or + other components. +.FUNCTIONALITY + Installs the Icinga Plugins PowerShell module from a remote or local source +.EXAMPLE + PS>Install-IcingaFrameworkPlugins; +.EXAMPLE + PS>Install-IcingaFrameworkPlugins -PluginsUrl 'C:/icinga/icinga-plugins.zip'; +.EXAMPLE + PS>Install-IcingaFrameworkPlugins -PluginsUrl 'https://github.com/Icinga/icinga-powershell-plugins/archive/v1.0.0.zip'; +.PARAMETER PluginsUrl + The URL pointing either to a local or remote ressource to download the plugins from. This requires to be the + full path to the .zip file to download. +.INPUTS + System.String +.OUTPUTS + System.Hashtable +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Install-IcingaFrameworkPlugins() +{ + param( + [string]$PluginsUrl + ); + + [Hashtable]$Result = Install-IcingaFrameworkComponent ` + -Name 'plugins' ` + -GitHubUser 'Icinga' ` + -Url $PluginsUrl; + + return @{ + 'PluginUrl' = $Result.RepoUrl; + }; +} +function Read-IcingaForWindowsLog() +{ + param ( + [array]$Source = @(), + [array]$Include = @(), + [array]$Exclude = @() + ); + + Read-IcingaWindowsEventLog -LogName 'Icinga for Windows' -Source $Source -MaxEntries 500 -Include $Include -Exclude $Exclude; +} +<# +.SYNOPSIS + Uninstalls every PowerShell module within the icinga-powershell-* namespace + including the Icinga Agent with all components (like certificates) as well as + the Icinga for Windows service and the Icinga PowerShell Framework. +.DESCRIPTION + Uninstalls every PowerShell module within the icinga-powershell-* namespace + including the Icinga Agent with all components (like certificates) as well as + the Icinga for Windows service and the Icinga PowerShell Framework. +.FUNCTIONALITY + Uninstalls every PowerShell module within the icinga-powershell-* namespace + including the Icinga Agent with all components (like certificates) as well as + the Icinga for Windows service and the Icinga PowerShell Framework. +.PARAMETER IcingaUser + In case the Icinga Security profile was installed with a defined user any other than + "icinga", you require to specify the user to remove it entirely +.PARAMETER Force + Suppress the question if you are sure to uninstall everything +.PARAMETER ComponentsOnly + Only uninstalls components like Icinga Agent, plugins, and so on and keeps the Framework +.INPUTS + System.String +.OUTPUTS + Null +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Uninstall-IcingaForWindows() +{ + param ( + $IcingaUser = 'icinga', + [switch]$Force = $FALSE, + [switch]$ComponentsOnly = $FALSE + ); + + $ModuleList = Get-Module 'icinga-powershell-*' -ListAvailable; + [string]$Modules = [string]::Join(', ', $ModuleList.Name); + + if ($Force -eq $FALSE) { + Write-IcingaConsoleWarning -Message 'You are about to uninstall the Icinga Agent with all components (including certificates) and all Icinga for Windows Components: {0}{1}Are you sure you want to proceed? (y/N)' -Objects $Modules, (New-IcingaNewLine); + $Input = Read-Host 'Confirm uninstall'; + if ($input -ne 'y') { + return; + } + } + + Set-IcingaServiceEnvironment; + Set-IcingaPSLocation; + + Write-IcingaConsoleNotice 'Uninstalling Icinga for Windows from this host'; + Write-IcingaConsoleNotice 'Uninstalling Icinga Security configuration if applied'; + 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; + # Ensure we close the IMC in case being open and we uninstall the Framework + Set-IcingaForWindowsManagementConsoleClosing; + } + Write-IcingaConsoleNotice 'Uninstalling Icinga for Windows service'; + Uninstall-IcingaForWindowsService -RemoveFiles | Out-Null; + + $HasErrors = $FALSE; + + foreach ($module in $ModuleList.Name) { + [string]$ModuleName = $module.Replace('icinga-powershell-', '').ToLower(); + + if ($ModuleName -eq 'framework' -And $ComponentsOnly) { + continue; + } + + if ((Uninstall-IcingaFrameworkComponent -Name $ModuleName)) { + continue; + } + + $HasErrors = $TRUE; + } + + if ($ComponentsOnly -eq $FALSE) { + Remove-Module 'icinga-powershell-framework' -Force -ErrorAction SilentlyContinue; + } + + if ($HasErrors) { + Write-Host 'Not all components could be removed. Please ensure no other PowerShell/Application is currently open and accessing Icinga for Windows files'; + } else { + Write-Host 'Icinga for Windows was removed from this host.'; + } +} +<# +.SYNOPSIS + Reads a private environment variable from Icinga for Windows + of the current PowerShell session +.DESCRIPTION + Reads a private environment variable from Icinga for Windows + of the current PowerShell session +.PARAMETER Name + The name of the variable to load the content from +.EXAMPLE + Get-IcingaPrivateEnvironmentVariable -Name 'AddTypeFunctions'; +.NOTES +General notes +#> +function Get-IcingaPrivateEnvironmentVariable() +{ + param ( + [string]$Name + ); + + if ([string]::IsNullOrEmpty($Name)) { + return $null; + } + + if ($global:Icinga.Private.ContainsKey($Name) -eq $FALSE) { + return $null; + } + + return $global:Icinga.Private[$Name]; +} +function Invoke-IcingaInternalServiceCall() +{ + param ( + [string]$Command = '', + [hashtable]$Arguments = @{ }, + [switch]$NoExit = $FALSE + ); + + # If our Framework is running as daemon, never call our api + if ($Global:Icinga.Protected.RunAsDaemon) { + return $NULL; + } + + # If the API forward feature is disabled, do nothing + if ((Get-IcingaFrameworkApiChecks) -eq $FALSE) { + return $NULL; + } + + # Test our Icinga for Windows service. If the service is not installed or not running, execute the plugin locally + $IcingaForWindowsService = (Get-Service 'icingapowershell' -ErrorAction SilentlyContinue); + + if ($null -eq $IcingaForWindowsService -Or $IcingaForWindowsService.Status -ne 'Running') { + return $NULL; + } + + # In case the REST-Api module ist not configured, do nothing + $BackgroundDaemons = Get-IcingaBackgroundDaemons; + + if ($null -eq $BackgroundDaemons -Or $BackgroundDaemons.ContainsKey('Start-IcingaWindowsRESTApi') -eq $FALSE) { + return $NULL; + } + + $RestApiPort = 5668; + [int]$Timeout = 120; + $Daemon = $BackgroundDaemons['Start-IcingaWindowsRESTApi']; + + # Fetch our deamon configuration + if ($Daemon.ContainsKey('-Port')) { + $RestApiPort = $Daemon['-Port']; + } elseif ($Daemon.ContainsKey('Port')) { + $RestApiPort = $Daemon['Port']; + } + if ($Daemon.ContainsKey('-Timeout')) { + $Timeout = $Daemon['-Timeout']; + } elseif ($Daemon.ContainsKey('Timeout')) { + $Timeout = $Daemon['Timeout']; + } + + # In case we are using SecureStrings for credentials, we have to convert them back to regular strings + # before pushing them to the REST-Api + [array]$CommandArguments = $Arguments.Keys; + + foreach ($arg in $CommandArguments) { + $Value = $Arguments[$arg]; + + if ($Value -Is [SecureString]) { + $Arguments[$arg] = ConvertFrom-IcingaSecureString -SecureString $Value; + } + } + + Set-IcingaTLSVersion; + Enable-IcingaUntrustedCertificateValidation -SuppressMessages; + + # For security reasons, we will not log the arguments in case of an error, only in debug mode + $ErrorArguments = ''; + if ($Global:Icinga.Protected.DebugMode) { + $ErrorArguments = $Arguments; + } + + # Now queue the check inside our REST-Api + try { + $ApiResult = Invoke-WebRequest -Method POST -UseBasicParsing -Uri ([string]::Format('https://localhost:{0}/v1/checker?command={1}', $RestApiPort, $Command)) -Body (ConvertTo-JsonUTF8Bytes -InputObject $Arguments -Depth 100 -Compress) -ContentType 'application/json' -TimeoutSec $Timeout; + } catch { + # Fallback to execute plugin locally + Write-IcingaEventMessage -Namespace 'Framework' -EventId 1553 -ExceptionObject $_ -Objects $Command, $ErrorArguments; + return $NULL; + } + + # Resolve our result from the API + $IcingaResult = ConvertFrom-JsonUTF8 -InputObject $ApiResult.Content; + $IcingaCR = ''; + + # In case we didn't receive a check result, fallback to local execution + if ([string]::IsNullOrEmpty($IcingaResult.$Command.checkresult)) { + Write-IcingaEventMessage -Namespace 'Framework' -EventId 1553 -Objects 'The check result for the executed command was empty', $Command, $ErrorArguments; + return $NULL; + } + + if ([string]::IsNullOrEmpty($IcingaResult.$Command.exitcode)) { + Write-IcingaEventMessage -Namespace 'Framework' -EventId 1553 -Objects 'The check result for the executed command was empty', $Command, $ErrorArguments; + return $NULL; + } + + $IcingaCR = ($IcingaResult.$Command.checkresult.Replace("`r`n", "`n")); + + if ($IcingaResult.$Command.perfdata.Count -ne 0) { + $IcingaCR = [string]::Format('{0}{1}| {2}', $IcingaCR, "`r`n", ([string]::Join('', $IcingaResult.$Command.perfdata))); + } + + if ($NoExit) { + Set-IcingaInternalPluginExitCode -ExitCode $IcingaResult.$Command.exitcode; + + return $IcingaCR; + } + + # Print our response and exit with the provide exit code + Write-IcingaConsolePlain $IcingaCR; + exit $IcingaResult.$Command.exitcode; +} +<# +.SYNOPSIS + Stops a timer object started with Start-IcingaTimer for a specific + named timer +.DESCRIPTION + Stops a timer object started with Start-IcingaTimer for a specific + named timer +.FUNCTIONALITY + Stops a timer object started with Start-IcingaTimer for a specific + named timer +.EXAMPLE + PS>Stop-IcingaTimer; +.EXAMPLE + PS>Stop-IcingaTimer -Name 'My Test Timer'; +.PARAMETER Name + The name of a custom identifier to run mutliple timers at once +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Stop-IcingaTimer() +{ + param ( + [string]$Name = 'DefaultTimer' + ); + + $TimerObject = Get-IcingaTimer -Name $Name; + + if ($null -eq $TimerObject) { + return; + } + + if ($TimerObject.IsRunning) { + $TimerObject.Stop(); + } + Add-IcingaHashtableItem -Key $Name -Value ( + @{ + 'Active' = $FALSE; + 'Timer' = $TimerObject; + } + ) -Hashtable $Global:Icinga.Private.Timers -Override | Out-Null; +} +function Disable-IcingaInterceptCounter() +{ + [array]$InterceptCounterList = @( + 'HKLM:\SYSTEM\CurrentControlSet\Services\Intercept CSM Filters\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\Intercept Injector\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\Intercept SyncAction Processing\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\InterceptCountersManager\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\InterceptCountersManager\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\Backup Exec\Performance' + ); + + foreach ($counter in $InterceptCounterList) { + if (Test-Path $counter) { + Write-IcingaConsoleNotice 'Disabling SCOM intercept counter "{0}"' -Objects $counter + + $CounterState = Get-ItemProperty -Path $counter -Name 'Disable Performance Counters' -ErrorAction SilentlyContinue; + + if ($null -eq $CounterState) { + New-ItemProperty -Path $counter -Name 'Disable Performance Counters' -Value 1; + continue; + } + + Set-ItemProperty -Path $counter -Name 'Disable Performance Counters' -Value 1; + } else { + Write-IcingaConsoleNotice 'SCOM intercept counter "{0}" not installed on the system' -Objects $counter + } + } +} +function Test-IcingaInterceptCounter() +{ + Write-IcingaConsoleNotice 'Testing for Microsoft SCOM Intercept Counters'; + + [bool]$TestResult = $TRUE; + [array]$InterceptCounterList = @( + 'HKLM:\SYSTEM\CurrentControlSet\Services\Intercept CSM Filters\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\Intercept Injector\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\Intercept SyncAction Processing\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\InterceptCountersManager\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\InterceptCountersManager\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\Backup Exec\Performance' + ); + + foreach ($counter in $InterceptCounterList) { + if (Test-Path -Path $counter) { + $CounterState = Get-ItemProperty -Path $counter -Name 'Disable Performance Counters' -ErrorAction SilentlyContinue; + + if ($null -eq $CounterState -Or $CounterState.'Disable Performance Counters' -eq 0) { + Write-IcingaTestOutput -Severity 'Failed' -Message ([string]::Format('Entry "{0}" is present on the system and the intercept counter is NOT disabled', $counter)); + $TestResult = $FALSE; + continue; + } + + Write-IcingaTestOutput -Severity 'Passed' -Message ([string]::Format('Entry "{0}" is present on the system and the intercept counter is disabled', $counter)); + } else { + Write-IcingaTestOutput -Severity 'Passed' -Message ([string]::Format('Entry "{0}" is not present on the system', $counter)); + } + } + + if ($TestResult -eq $FALSE) { + Write-IcingaTestOutput -Severity 'Failed' -Message 'One or more intercept counters exist on this system which are not disabled. Please take a look at https://icinga.com/docs/icinga-for-windows/latest/doc/knowledgebase/IWKB000016/ for further details'; + } else { + Write-IcingaTestOutput -Severity 'Passed' -Message 'There are either no intercept counters installed on your system or they are disabled. Monitoring of Performance Counters should work fine'; + } +} +function Enable-IcingaInterceptCounter() +{ + [array]$InterceptCounterList = @( + 'HKLM:\SYSTEM\CurrentControlSet\Services\Intercept CSM Filters\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\Intercept Injector\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\Intercept SyncAction Processing\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\InterceptCountersManager\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\InterceptCountersManager\Performance', + 'HKLM:\SYSTEM\CurrentControlSet\Services\Backup Exec\Performance' + ); + + foreach ($counter in $InterceptCounterList) { + if (Test-Path $counter) { + Write-IcingaConsoleNotice 'Enabling SCOM intercept counter "{0}"' -Objects $counter + + $CounterState = Get-ItemProperty -Path $counter -Name 'Disable Performance Counters' -ErrorAction SilentlyContinue; + + if ($null -eq $CounterState) { + continue; + } + + Set-ItemProperty -Path $counter -Name 'Disable Performance Counters' -Value 0; + } else { + Write-IcingaConsoleNotice 'SCOM intercept counter "{0}" not installed on the system' -Objects $counter + } + } +} +function New-IcingaDocumentObject() +{ + param ( + [string]$Name = $null, + [string]$Path = $null, + [switch]$Force = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to specify an internal name for the documentation object'; + return; + } + + if ([string]::IsNullOrEmpty($Path)) { + Write-IcingaConsoleError 'You have to specify a path on where the document should be written to'; + return; + } + + if ($Global:Icinga.Private.Documentation.ContainsKey($Name)) { + if ($Force -eq $FALSE) { + Write-IcingaConsoleError 'A documentation object with the name "{0}" does already exist in memory. Use -Force to overwrite it' -Objects $Name; + return; + } + + $Global:Icinga.Private.Documentation[$Name] = @{ + 'Path' = $Path; + 'Content' = (New-Object -TypeName 'System.Text.StringBuilder'); + } + } else { + $Global:Icinga.Private.Documentation.Add( + $Name, + @{ + 'Path' = $Path; + 'Content' = (New-Object -TypeName 'System.Text.StringBuilder'); + } + ); + } +} +function Write-IcingaDocumentFile() +{ + param ( + [string]$Name = $null, + [switch]$ClearCache = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to specify an internal name for the documentation object'; + return; + } + + Write-IcingaFileSecure -File $Global:Icinga.Private.Documentation[$Name]['Path'] -Value $Global:Icinga.Private.Documentation[$Name]['Content'].ToString(); + + if ($ClearCache) { + $Global:Icinga.Private.Documentation.Remove($Name); + } +} +function Publish-IcingaEventLogDocumentation() +{ + param( + [string]$Namespace, + [string]$OutFile + ); + + [string]$DocContent = [string]::Format( + '# {0} EventLog Documentation', + $Namespace + ); + $DocContent += New-IcingaNewLine; + $DocContent += New-IcingaNewLine; + $DocContent += "Below you will find a list of EventId's which are exported by this module. The short and detailed message are both written directly into the EventLog. This documentation shall simply provide a summary of available EventId's"; + + $SortedArray = $IcingaEventLogEnums[$Namespace].Keys.GetEnumerator() | Sort-Object; + + foreach ($entry in $SortedArray) { + $entry = $IcingaEventLogEnums[$Namespace][$entry]; + + $DocContent = [string]::Format( + '{0}{2}{2}## Event Id {1}{2}{2}| Category | Short Message | Detailed Message |{2}| --- | --- | --- |{2}| {3} | {4} | {5} |', + $DocContent, + $entry.EventId, + (New-IcingaNewLine), + $entry.EntryType, + $entry.Message, + $entry.Details + ); + } + + if ([string]::IsNullOrEmpty($OutFile)) { + Write-Output $DocContent; + } else { + Write-IcingaFileSecure -File $OutFile -Value $DocContent; + } +} +function Add-IcingaDocumentContent() +{ + param ( + [string]$Name = $null, + [string]$Content = '', + [switch]$NoNewLine = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'You have to specify an internal name for the documentation object'; + return; + } + + if ($Global:Icinga.Private.Documentation.ContainsKey($Name) -eq $false) { + Write-IcingaConsoleError 'A documentation object with the name "{0}" does not exist' -Objects $Name; + return; + } + + if ($NoNewLine) { + $Global:Icinga.Private.Documentation[$Name]['Content'].Append($Content) | Out-Null; + } else { + $Global:Icinga.Private.Documentation[$Name]['Content'].AppendLine($Content) | Out-Null; + } +} +<# +.SYNOPSIS + A installation wizard that will guide you through the entire installation and + configuration of Icinga for Windows including the Icinga Agent +.DESCRIPTION + A installation wizard that will guide you through the entire installation and + configuration of Icinga for Windows including the Icinga Agent +.FUNCTIONALITY + Makes initial installation and first configuration of Icinga for Windows and + the Icinga Agent very easy +.PARAMETER Hostname + Set a specific hostname to the system and do not lookup anything automatically +.PARAMETER AutoUseFQDN + Tells the wizard if you want to use the FQDN for the Icinga Agent or not. + Set it to 1 to use FQDN and 0 to do not use it. Leave it empty to be prompted the wizard question. + Ignores `AutoUseHostname` +.PARAMETER AutoUseHostname + Tells the wizard if you want to use the hostname for the Icinga Agent or not. + Set it to 1 to use hostname only 0 to not use it. Leave it empty to be prompted the wizard question. + Overwritten by `AutoUseFQDN` +.PARAMETER LowerCase + Tells the wizard if the provided hostname should be converted to lower case characters. + Set it to 1 to lower case the name and 0 to do nothing. Leave it empty to be prompted the wizard question +.PARAMETER UpperCase + Tells the wizard if the provided hostname should be converted to upper case characters. + Set it to 1 to upper case the name and 0 to do nothing. Leave it empty to be prompted the wizard question +.PARAMETER AddDirectorGlobal + Tells the wizard to add the `director-global` zone to your Icinga Agent configuration. + Set it to 1 to add it and 0 to not add it. Leave it empty to be prompted the wizard question +.PARAMETER AddGlobalTemplates + Tells the wizard to add the `global-templates` zone to your Icinga Agent configuration. + Set it to 1 to add it and 0 to not add it. Leave it empty to be prompted the wizard question +.PARAMETER PackageSource + Tells the wizard from which source we can download the Icinga Agent from. Use https://packages.icinga.com/windows/ if you can reach the internet + Set the source to either web, local or network share. Leave it empty to be prompted the wizard question +.PARAMETER AgentVersion + Tells the wizard which Icinga Agent version to install. You can provide latest, snapshot or a specific version like 2.11.6 + Set the value to one mentioned above. Leave it empty to be prompted the wizard question +.PARAMETER InstallDir + Tells the wizard which directory the Icinga Agent will be installed into. Default is `C:\Program Files\ICINGA2` + Set the value to one mentioned above. +.PARAMETER AllowVersionChanges + Tells the wizard if the Icinga Agent should be updated/downgraded in case the current/target version are not matching + Should be equal to `UpdateAgent` + Set it to 1 to allow updates/downgrades 0 to not allow it. Leave it empty to be prompted the wizard question +.PARAMETER UpdateAgent + Tells the wizard if the Icinga Agent should be updated/downgraded in case the current/target version are not matching + Should be equal to `AllowVersionChanges` + Set it to 1 to allow updates/downgrades 0 to not allow it. Leave it empty to be prompted the wizard question +.PARAMETER AddFirewallRule + Tells the wizard if the used Icinga Agent port should be opened for incoming traffic on the Windows Firewall + Set it to 1 to set the firewall rule 0 to do nothing. Leave it empty to be prompted the wizard question +.PARAMETER AcceptConnections + Tells the wizard if the Icinga Agent is accepting incoming connections. + Might require `AddFirewallRule` being enabled in case this value is set to 1 + Set it to 1 to accept connections 0 to not accept them. Leave it empty to be prompted the wizard question +.PARAMETER Endpoints + Tells the wizard which endpoints this Icinga Agent has as parent. Example: master-icinga1, master-icinga2 + Set all parent endpoint names in a comma separated list. Leave it empty to be prompted the wizard question +.PARAMETER EndpointConnections + Tells the wizard the connection configuration for provided endpoints. The order of this argument has to match + the endpoint configuration on the `Endpoints` argument. Example: [master-icinga1.example.com]:5665, 192.168.0.5 + Set all parent endpoint connections as comma separated list. Leave it empty to be prompted the wizard question +.PARAMETER ConvertEndpointIPConfig + Tells the wizard if FQDN for parent connections should be looked up and resolved to IP addresses. Example: example.com => 93.184.216.34 + Set it to 1 to lookup the up and 0 to do nothing. Leave it empty to be prompted the wizard question +.PARAMETER ParentZone + Tells the wizard which parent zone name to use for Icinga Agent configuration. + Set it to the name of the parent zone. Leave it empty to be prompted the wizard question +.PARAMETER GlobalZones + Tells the wizard to add additional global zones to your configuration. You can provide a comma separated list for this + Add additional global zones as comma separated list, use @() to not add anything. Leave it empty to be prompted the wizard question +.PARAMETER CAEndpoint + Tells the wizard which address/fqdn to use for Icinga Agent certificate signing. + Set the IP/FQDN of your CA Server/Icinga parent node or leave it empty if no connection is possible +.PARAMETER CAPort + Tells the wizard which port to use for Icinga Agent certificate signing. + Set the port of your CA Server/Icinga parent node or leave it empty if no connection is possible +.PARAMETER Ticket + Tells the wizard which ticket to use for Icinga Agent certificate signing. + Set the ticket of your certificate request for this host or leave it empty if no ticket is available. + If you leave this argument empty, you will have to set `-EmptyTicket` to 1 and otherwise to 0 +.PARAMETER EmptyTicket + Tells the wizard to use a provided `-Ticket` or skip it. If `-Ticket` is empty you do not want to use it, + set this argument to 1. If you set `-Ticket` with a ticket to use, set this argument to 0 + Leave it empty to be prompted the wizard question +.PARAMETER CAFile + Tells the wizard if the Icinga CA Server ca.crt shall be used for signing certificate request. + You can specify a web, local or network share as source to lookup the `ca.crt`. + If this argument is set to be empty, ensure to also set `-EmptyCA` to 1 +.PARAMETER EmptyCA + Tells the wizard if the argument `-CAFile` is set or not. Set this argument to 1 if `-CAFile` is + set and to 0 if `-CAFile` is not used +.PARAMETER RunInstaller + Tells the wizard to skip the question if the configuration is correct and skips the question if you + want to execute the wizard. +.PARAMETER Reconfigure + Tells the wizard to execute all arguments again and configure certificates any thing else even if no + change to the Icinga Agent was made. This is mostly required if you run the wizard again with the same + Icinga Agent version being installed and available +.PARAMETER ServiceUser + Tells the wizard which service user should be used for the Icinga Agent and PowerShell Service. + Add 'NT Authority\NetworkService' to use the default one or specify a custom user + Leave it empty to be prompted the wizard question. +.PARAMETER ServicePass + Tells the wizard to use a special password for service users in case you are running a custom user + instead of local service accounts. +.PARAMETER InstallFrameworkService + Tells the wizard if you want to install the Icinga PowerShell service + Set it to 1 to install it and 0 to not install it. Leave it empty to be prompted the wizard question +.PARAMETER FrameworkServiceUrl + Tells the wizard where to download the Icinga PowerShell Service binary from. Example: https://github.com/Icinga/icinga-powershell-service/releases/download/v1.1.0/icinga-service-v1.1.0.zip + This argument is only required if `-InstallFrameworkService` is set to 1 +.PARAMETER ServiceDirectory + Tells the wizard where to install the Icinga PowerShell Service binary into. Use `C:\Program Files\icinga-framework-service` as default + This argument is only required if `-InstallFrameworkService` is set to 1 +.PARAMETER ServiceBin + Tells the wizard the exact path of the service binary. Must match the path of `-ServiceDirectory` and add the binary at the end. Use `C:\Program Files\icinga-framework-service\icinga-service.exe` as default + This argument is only required if `-InstallFrameworkService` is set to 1 +.PARAMETER UseDirectorSelfService + Tells the wizard to use the Icinga Director Self-Service API. + Set it to 1 to use the SelfService and 0 to not use it. Leave it empty to prompt the wizard question +.PARAMETER SkipDirectorQuestion + Tells the wizard to skip all related Icinga Director questions. + Set it to 1 to skip possible questions and 0 if you continue with Icinga Director. Leave it empty to prompt the wizard question +.PARAMETER DirectorUrl + Tells the wizard which URL to use for the Icinga Director. Only required if `-UseDirectorSelfService` is set to 1 + Specify the URL targeting your Icinga Director. Leave it empty to prompt the wizard question +.PARAMETER SelfServiceAPIKey + Tells the wizard which SelfService API key to use for registering this host. In case wizard already run once, + this argument is always overwritten by the local stored argument. Only required if `-UseDirectorSelfService` is set to 1 + Specify the SelfService API key being used for configuration. Leave it empty to prompt the wizard question in case the registration was not yet done for this host +.PARAMETER OverrideDirectorVars + Tells the wizard of variables shipped by the Icinga Director SelfService should be overwritten. Only required if `-UseDirectorSelfService` is set to 1 + Set it to 1 to override arguments and 0 to do nothing. Leave it empty to prompt the wizard question +.PARAMETER InstallFrameworkPlugins + Tells the wizard if you want to install the Icinga PowerShell Plugins + Set it to 1 to install them and 0 to not install them. Leave it empty to be prompted the wizard question +.PARAMETER PluginsUrl + Tells the wizard where to download the Icinga PowerShell Plugins from. Example: https://github.com/Icinga/icinga-powershell-plugins/archive/v1.2.0.zip + This argument is only required if `-InstallFrameworkPlugins` is set to 1 +.INPUTS + System.String +.OUTPUTS + System.Boolean +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Start-IcingaAgentInstallWizard() +{ + param( + [string]$Hostname, + $AutoUseFQDN, + $AutoUseHostname, + $LowerCase, + $UpperCase, + $AddDirectorGlobal = $null, + $AddGlobalTemplates = $null, + [string]$PackageSource, + [string]$AgentVersion, + [string]$InstallDir, + $AllowVersionChanges, + $UpdateAgent = $null, + $AddFirewallRule = $null, + $AcceptConnections = $null, + [array]$Endpoints = @(), + [array]$EndpointConnections = @(), + $ConvertEndpointIPConfig = $null, + [string]$ParentZone, + [array]$GlobalZones = $null, + [string]$CAEndpoint, + $CAPort = $null, + [string]$Ticket, + $EmptyTicket, + [string]$CAFile = $null, + $EmptyCA = $null, + [switch]$RunInstaller, + [switch]$Reconfigure, + [string]$ServiceUser, + [securestring]$ServicePass = $null, + $InstallFrameworkService = $null, + $FrameworkServiceUrl = $null, + $ServiceDirectory = $null, + $ServiceBin = $null, + $UseDirectorSelfService = $null, + [bool]$SkipDirectorQuestion = $FALSE, + [string]$DirectorUrl, + [string]$SelfServiceAPIKey = $null, + $OverrideDirectorVars = $null, + $InstallFrameworkPlugins = $null, + $PluginsUrl = $null + ); + + Write-IcingaDeprecated -Function 'Start-IcingaAgentInstallWizard'; + + [array]$InstallerArguments = @(); + [array]$GlobalZoneConfig = @(); + + if ($SkipDirectorQuestion -eq $FALSE) { + if ($null -eq $UseDirectorSelfService) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to use the Icinga Director Self-Service API?' -Default 'y').result -eq 1) { + $UseDirectorSelfService = $TRUE; + } else { + $UseDirectorSelfService = $FALSE; + $InstallerArguments += '-UseDirectorSelfService 0'; + } + } + if ($UseDirectorSelfService) { + + $InstallerArguments += '-UseDirectorSelfService 1'; + $DirectorArgs = Start-IcingaAgentDirectorWizard ` + -DirectorUrl $DirectorUrl ` + -SelfServiceAPIKey $SelfServiceAPIKey ` + -OverrideDirectorVars $OverrideDirectorVars ` + -Hostname $Hostname ` + -RunInstaller $RunInstaller; + + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'DirectorUrl' -Value $DirectorUrl -InstallerArguments $InstallerArguments; + $DirectorUrl = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'SelfServiceAPIKey' -Value $SelfServiceAPIKey -InstallerArguments $InstallerArguments -Default $null; + + if ([string]::IsNullOrEmpty($Result.Value) -eq $FALSE) { + $SelfServiceAPIKey = $Result.Value; + $InstallerArguments = $Result.Args; + } + + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'Ticket' -Value $Ticket -InstallerArguments $InstallerArguments; + $Ticket = $Result.Value; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'PackageSource' -Value $PackageSource -InstallerArguments $InstallerArguments; + $PackageSource = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'AgentVersion' -Value $AgentVersion -InstallerArguments $InstallerArguments; + $AgentVersion = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'InstallDir' -Value $InstallDir -InstallerArguments $InstallerArguments; + $InstallDir = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'CAPort' -Value $CAPort -InstallerArguments $InstallerArguments; + $CAPort = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'AllowVersionChanges' -Value $AllowVersionChanges -InstallerArguments $InstallerArguments; + $AllowVersionChanges = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'GlobalZones' -Value $GlobalZones -InstallerArguments $InstallerArguments; + $GlobalZones = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'ParentZone' -Value $ParentZone -InstallerArguments $InstallerArguments; + $ParentZone = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'CAEndpoint' -Value $CAEndpoint -InstallerArguments $InstallerArguments; + $CAEndpoint = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'Endpoints' -Value $Endpoints -InstallerArguments $InstallerArguments; + $Endpoints = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'AddFirewallRule' -Value $AddFirewallRule -InstallerArguments $InstallerArguments; + $AddFirewallRule = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'AcceptConnections' -Value $AcceptConnections -InstallerArguments $InstallerArguments; + $AcceptConnections = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'ServiceUser' -Value $ServiceUser -InstallerArguments $InstallerArguments; + $ServiceUser = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'UpdateAgent' -Value $UpdateAgent -InstallerArguments $InstallerArguments; + $UpdateAgent = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'AddDirectorGlobal' -Value $AddDirectorGlobal -InstallerArguments $InstallerArguments; + $AddDirectorGlobal = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'AddGlobalTemplates' -Value $AddGlobalTemplates -InstallerArguments $InstallerArguments; + $AddGlobalTemplates = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'LowerCase' -Value $LowerCase -Default $FALSE -InstallerArguments $InstallerArguments; + $LowerCase = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'UpperCase' -Value $UpperCase -Default $FALSE -InstallerArguments $InstallerArguments; + $UpperCase = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'AutoUseFQDN' -Value $AutoUseFQDN -Default $FALSE -InstallerArguments $InstallerArguments; + $AutoUseFQDN = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'AutoUseHostname' -Value $AutoUseHostname -Default $FALSE -InstallerArguments $InstallerArguments; + $AutoUseHostname = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'EndpointConnections' -Value $EndpointConnections -InstallerArguments $InstallerArguments; + $EndpointConnections = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'OverrideDirectorVars' -Value $OverrideDirectorVars -InstallerArguments $InstallerArguments; + $OverrideDirectorVars = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'InstallFrameworkService' -Value $InstallFrameworkService -InstallerArguments $InstallerArguments; + $InstallFrameworkService = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'ServiceDirectory' -Value $ServiceDirectory -InstallerArguments $InstallerArguments; + $ServiceDirectory = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'FrameworkServiceUrl' -Value $FrameworkServiceUrl -InstallerArguments $InstallerArguments; + $FrameworkServiceUrl = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'InstallFrameworkPlugins' -Value $InstallFrameworkPlugins -InstallerArguments $InstallerArguments; + $InstallFrameworkPlugins = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'PluginsUrl' -Value $PluginsUrl -InstallerArguments $InstallerArguments; + $PluginsUrl = $Result.Value; + $InstallerArguments = $Result.Args; + $Result = Set-IcingaWizardArgument -DirectorArgs $DirectorArgs -WizardArg 'ConvertEndpointIPConfig' -Value $ConvertEndpointIPConfig -InstallerArguments $InstallerArguments; + $ConvertEndpointIPConfig = $Result.Value; + $InstallerArguments = $Result.Args; + } + } + + # 'latest' is deprecated starting with 1.1.0 + if ($AgentVersion -eq 'latest') { + $AgentVersion = 'release'; + Write-IcingaConsoleWarning -Message 'The value "latest" for the argument "AgentVersion" is deprecated. Please use the value "release" in the future!'; + } + + if ([string]::IsNullOrEmpty($Hostname) -And $null -eq $AutoUseFQDN -And $null -eq $AutoUseHostname) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to specify the hostname manually?' -Default 'n').result -eq 1) { + $HostFQDN = Get-IcingaHostname -AutoUseFQDN 1 -AutoUseHostname 0 -LowerCase 1 -UpperCase 0; + if ((Get-IcingaAgentInstallerAnswerInput -Prompt ([string]::Format('Do you want to automatically fetch the hostname as FQDN? (Result: "{0}")', $HostFQDN)) -Default 'y').result -eq 1) { + $InstallerArguments += '-AutoUseFQDN 1'; + $InstallerArguments += '-AutoUseHostname 0'; + $AutoUseFQDN = $TRUE; + $AutoUseHostname = $FALSE; + } else { + $InstallerArguments += '-AutoUseFQDN 0'; + $InstallerArguments += '-AutoUseHostname 1'; + $AutoUseFQDN = $FALSE; + $AutoUseHostname = $TRUE; + } + $Hostname = Get-IcingaHostname -AutoUseFQDN $AutoUseFQDN -AutoUseHostname $AutoUseHostname -LowerCase 1 -UpperCase 0; + if ((Get-IcingaAgentInstallerAnswerInput -Prompt ([string]::Format('Do you want to convert the hostname into lower case characters? (Result: "{0}")', $Hostname)) -Default 'y').result -eq 1) { + $InstallerArguments += '-LowerCase 1'; + $InstallerArguments += '-UpperCase 0'; + $LowerCase = $TRUE; + $UpperCase = $FALSE; + } else { + $Hostname = Get-IcingaHostname -AutoUseFQDN $AutoUseFQDN -AutoUseHostname $AutoUseHostname -LowerCase 0 -UpperCase 1; + if ((Get-IcingaAgentInstallerAnswerInput -Prompt ([string]::Format('Do you want to convert the hostname into upper case characters? (Result: "{0}")', $Hostname)) -Default 'y').result -eq 1) { + $InstallerArguments += '-LowerCase 0'; + $InstallerArguments += '-UpperCase 1'; + $LowerCase = $FALSE; + $UpperCase = $TRUE; + } else { + $InstallerArguments += '-LowerCase 0'; + $InstallerArguments += '-UpperCase 0'; + $LowerCase = $FALSE; + $UpperCase = $FALSE; + } + } + $Hostname = Get-IcingaHostname -AutoUseFQDN $AutoUseFQDN -AutoUseHostname $AutoUseHostname -LowerCase $LowerCase -UpperCase $UpperCase; + } else { + $Hostname = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please specify the hostname to use' -Default 'v').answer; + } + } else { + if ($AutoUseFQDN -Or $AutoUseHostname) { + $Hostname = Get-IcingaHostname -AutoUseFQDN $AutoUseFQDN -AutoUseHostname $AutoUseHostname -LowerCase $LowerCase -UpperCase $UpperCase; + } + } + + Write-IcingaConsoleNotice ([string]::Format('Using hostname "{0}" for the Icinga Agent configuration', $Hostname)); + + $IcingaAgent = Get-IcingaAgentInstallation; + if ($IcingaAgent.Installed -eq $FALSE) { + if ([string]::IsNullOrEmpty($PackageSource)) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to install the Icinga Agent now?' -Default 'y').result -eq 1) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to use a different package source? (Defaults: "https://packages.icinga.com/windows/")' -Default 'n').result -eq 0) { + $PackageSource = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please specify your package source' -Default 'v').answer; + $InstallerArguments += "-PackageSource '$PackageSource'"; + } else { + $PackageSource = 'https://packages.icinga.com/windows/' + $InstallerArguments += "-PackageSource '$PackageSource'"; + } + + Write-IcingaConsoleNotice ([string]::Format('Using package source "{0}" for the Icinga Agent package', $PackageSource)); + $AllowVersionChanges = $TRUE; + $InstallerArguments += '-AllowVersionChanges 1'; + + if ([string]::IsNullOrEmpty($AgentVersion)) { + $AgentVersion = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please specify the version you want to install ("release", "snapshot" or a specific version like "2.11.3")' -Default 'v' -DefaultInput 'release').answer; + $InstallerArguments += "-AgentVersion '$AgentVersion'"; + + Write-IcingaConsoleNotice ([string]::Format('Installing Icinga version: "{0}"', $AgentVersion)); + } + } else { + $AllowVersionChanges = $FALSE; + $InstallerArguments += '-AllowVersionChanges 0'; + $InstallerArguments += "-AgentVersion '$AgentVersion'"; + $AgentVersion = ''; + } + } + } else { + if ($null -eq $UpdateAgent) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'The Icinga Agent is already installed. Would you like to update it?' -Default 'y').result -eq 1) { + $UpdateAgent = 1; + $AllowVersionChanges = $TRUE; + $InstallerArguments += '-AllowVersionChanges 1'; + } else { + $UpdateAgent = 0; + $AllowVersionChanges = $FALSE; + $InstallerArguments += '-AllowVersionChanges 0'; + } + $InstallerArguments += "-UpdateAgent $UpdateAgent"; + } + + if ($UpdateAgent -eq 1) { + if ([string]::IsNullOrEmpty($AgentVersion)) { + $AgentVersion = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please specify the version you want to install ("release", "snapshot" or a specific version like "2.11.3")' -Default 'v' -DefaultInput 'release').answer; + $InstallerArguments += "-AgentVersion '$AgentVersion'"; + + Write-IcingaConsoleNotice ([string]::Format('Updating/Downgrading Icinga 2 Agent to version: "{0}"', $AgentVersion)); + } + + if ([string]::IsNullOrEmpty($PackageSource)) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to use a different package source then "https://packages.icinga.com/windows/" ?' -Default 'n').result -eq 0) { + $PackageSource = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please specify your package source' -Default 'v').answer; + $InstallerArguments += "-PackageSource '$PackageSource'"; + } else { + $PackageSource = 'https://packages.icinga.com/windows/' + $InstallerArguments += "-PackageSource '$PackageSource'"; + } + } + } + } + + if ($Endpoints.Count -eq 0) { + $ArrayString = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please specify the parent node(s) separated by "," (Examples: "master1, master2" or "master1.example.com, master2.example.com")' -Default 'v').answer; + $Endpoints = ($ArrayString.Replace(' ', '')).Split(','); + $InstallerArguments += ("-Endpoints " + ([string]::Join(',', $Endpoints))); + } + + if ($null -eq $CAPort) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Are you using another port than 5665 for Icinga communication?' -Default 'n').result -eq 0) { + $CAPort = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please enter the port for Icinga communication' -Default 'v' -DefaultInput '5665').answer; + $InstallerArguments += "-CAPort $CAPort"; + } else { + $InstallerArguments += "-CAPort 5665"; + $CAPort = 5665; + } + } + + [bool]$CanConnectToParent = $FALSE; + + if ($null -eq $AcceptConnections) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt "Is this Agent able to connect to its parent node(s)?" -Default 'y').result -eq 1) { + $CanConnectToParent = $TRUE; + $AcceptConnections = 0; + $InstallerArguments += ("-AcceptConnections 0"); + } else { + $AcceptConnections = 1; + $InstallerArguments += ("-AcceptConnections 1"); + } + } else { + if ((Test-IcingaWizardArgument -Argument 'AcceptConnections') -eq $FALSE) { + $InstallerArguments += ([string]::Format('-AcceptConnections {0}', [int]$AcceptConnections)); + } + + if ($AcceptConnections -eq $FALSE) { + $CanConnectToParent = $TRUE; + } + } + + if ($null -eq $AddFirewallRule -And $CanConnectToParent -eq $FALSE) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt ([string]::Format('Do you want to open the Windows Firewall for incoming traffic on Port "{0}"?', $CAPort)) -Default 'y').result -eq 1) { + $InstallerArguments += "-AddFirewallRule 1"; + $AddFirewallRule = $TRUE; + } else { + $InstallerArguments += "-AddFirewallRule 0"; + $AddFirewallRule = $FALSE; + } + } else { + if ($CanConnectToParent -eq $TRUE) { + $InstallerArguments += "-AddFirewallRule 0"; + $AddFirewallRule = $FALSE; + } + } + + if ($null -eq $ConvertEndpointIPConfig -And $CanConnectToParent -eq $TRUE) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to convert parent node(s) connection data to IP addresses?' -Default 'y').result -eq 1) { + $InstallerArguments += "-ConvertEndpointIPConfig 1"; + $ConvertEndpointIPConfig = $TRUE; + if ($EndpointConnections.Count -eq 0) { + $EndpointsConversion = Convert-IcingaEndpointsToIPv4 -NetworkConfig $Endpoints.Split(','); + } else { + $EndpointsConversion = Convert-IcingaEndpointsToIPv4 -NetworkConfig $EndpointConnections; + } + if ($EndpointsConversion.HasErrors) { + Write-IcingaConsoleWarning -Message 'Not all of your endpoint connection data could be resolved. These endpoints were dropped: {0}' -Objects ([string]::Join(', ', $EndpointsConversion.Unresolved)); + } + $EndpointConnections = $EndpointsConversion.Network; + } else { + $InstallerArguments += "-ConvertEndpointIPConfig 0"; + $ConvertEndpointIPConfig = $FALSE; + } + } + + if ($EndpointConnections.Count -eq 0 -And $AcceptConnections -eq 0) { + $NetworkDefault = ''; + foreach ($Endpoint in $Endpoints) { + $NetworkDefault += [string]::Format('[{0}]:{1},', $Endpoint, $CAPort); + } + if ([string]::IsNullOrEmpty($NetworkDefault) -eq $FALSE) { + $NetworkDefault = $NetworkDefault.Substring(0, $NetworkDefault.Length - 1); + } + $ArrayString = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please specify the network destinations this Agent will connect to separated by "," (Examples: 192.168.0.1, [192.168.0.2]:5665, [icinga2.example.com]:5665)' -Default 'v' -DefaultInput $NetworkDefault).answer; + $EndpointConnections = ($ArrayString.Replace(' ', '')).Split(','); + + if ($ConvertEndpointIPConfig) { + $EndpointsConversion = Convert-IcingaEndpointsToIPv4 -NetworkConfig $EndpointConnections.Split(','); + if ($EndpointsConversion.HasErrors -eq $FALSE) { + $EndpointConnections = $EndpointsConversion.Network; + } + } + $InstallerArguments += ("-EndpointConnections " + ([string]::Join(',', $EndpointConnections))); + } elseif ($EndpointConnections.Count -ne 0 -And $AcceptConnections -eq 0 -And $ConvertEndpointIPConfig) { + $EndpointsConversion = Convert-IcingaEndpointsToIPv4 -NetworkConfig $EndpointConnections; + if ($EndpointsConversion.HasErrors) { + Write-IcingaConsoleWarning -Message 'Not all of your endpoint connection data could be resolved. These endpoints were dropped: {0}' -Objects ([string]::Join(', ', $EndpointsConversion.Unresolved)); + } + $EndpointConnections = $EndpointsConversion.Network; + } + + if ([string]::IsNullOrEmpty($ParentZone)) { + $ParentZone = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please specify the parent zone this Agent will connect to' -Default 'v' -DefaultInput 'master').answer; + $InstallerArguments += "-ParentZone $ParentZone"; + } + + if ($null -eq $AddDirectorGlobal) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to add the global zone "director-global"?' -Default 'y').result -eq 1) { + $AddDirectorGlobal = $TRUE; + $InstallerArguments += ("-AddDirectorGlobal 1"); + } else { + $AddDirectorGlobal = $FALSE; + $InstallerArguments += ("-AddDirectorGlobal 0"); + } + } + + if ($AddDirectorGlobal) { + $GlobalZoneConfig += 'director-global'; + } + + if ($null -eq $AddGlobalTemplates) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to add the global zone "global-templates"?' -Default 'y').result -eq 1) { + $AddGlobalTemplates = $TRUE; + $InstallerArguments += ("-AddGlobalTemplates 1"); + } else { + $AddGlobalTemplates = $FALSE; + $InstallerArguments += ("-AddGlobalTemplates 0"); + } + } + + if ($AddGlobalTemplates) { + $GlobalZoneConfig += 'global-templates'; + } + + if ($null -eq $GlobalZones) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to add custom global zones?' -Default 'n').result -eq 0) { + $ArrayString = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please specify your additional zones separated by "," (Example: "global-zone1, global-zone2")' -Default 'v').answer; + if ([string]::IsNullOrEmpty($ArrayString) -eq $FALSE) { + $GlobalZones = ($ArrayString.Replace(' ', '')).Split(',') + $GlobalZoneConfig += $GlobalZones; + $InstallerArguments += ("-GlobalZones " + ([string]::Join(',', $GlobalZones))); + } else { + $GlobalZones = @(); + $InstallerArguments += ("-GlobalZones @()"); + } + } else { + $GlobalZones = @(); + $InstallerArguments += ("-GlobalZones @()"); + } + } else { + $GlobalZoneConfig += $GlobalZones; + } + + if ($CanConnectToParent) { + if ([string]::IsNullOrEmpty($CAEndpoint)) { + $CAEndpoint = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please enter the connection data of the parent node that handles certificate requests' -Default 'v' -DefaultInput (Get-IPConfigFromString $EndpointConnections[0]).address).answer; + $InstallerArguments += "-CAEndpoint $CAEndpoint"; + } + if ([string]::IsNullOrEmpty($Ticket) -And $null -eq $EmptyTicket) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you have a PKI Ticket to sign your certificate request?' -Default 'y').result -eq 1) { + $Ticket = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please enter your PKI Ticket' -Default 'v').answer; + if ([string]::IsNullOrEmpty($Ticket)) { + $InstallerArguments += "-EmptyTicket 1" + } else { + $InstallerArguments += "-EmptyTicket 0" + } + $InstallerArguments += "-Ticket '$Ticket'"; + } else { + $InstallerArguments += "-Ticket ''"; + $InstallerArguments += "-EmptyTicket 1" + } + } else { + if ([string]::IsNullOrEmpty($Ticket)) { + $InstallerArguments += "-Ticket ''"; + } else { + $InstallerArguments += "-Ticket '$Ticket'"; + } + if ($null -eq $EmptyTicket) { + $InstallerArguments += "-EmptyTicket 1" + } else { + $InstallerArguments += "-EmptyTicket $EmptyTicket" + } + } + } else { + if ([string]::IsNullOrEmpty($CAFile) -And $null -eq $EmptyCA) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Is your public Icinga 2 CA (ca.crt) available on a local, network or web share?' -Default 'y').result -eq 1) { + $CAFile = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please provide the full path to your ca.crt file (Examples: "C:\icinga2\ca.crt", "https://icinga.example.com/ca.crt"' -Default 'v').answer; + if ([string]::IsNullOrEmpty($CAFile)) { + $InstallerArguments += "-EmptyCA 1" + } else { + $InstallerArguments += "-EmptyCA 0" + } + $InstallerArguments += "-CAFile '$CAFile'"; + } else { + $InstallerArguments += "-CAFile ''"; + $InstallerArguments += "-EmptyCA 1"; + $EmptyCA = $TRUE; + } + } else { + if ([string]::IsNullOrEmpty($CAFile)) { + $InstallerArguments += "-CAFile ''"; + } else { + $InstallerArguments += "-CAFile '$CAFile'"; + } + if ($null -eq $EmptyCA) { + $InstallerArguments += "-EmptyCA 1" + } else { + $InstallerArguments += "-EmptyCA $EmptyCA" + } + } + } + + if ([string]::IsNullOrEmpty($ServiceUser)) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to change the user of the Icinga Agent service? (Defaults: "NT Authority\NetworkService")' -Default 'n').result -eq 0) { + $ServiceUser = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please enter a custom user for the Icinga Agent service' -Default 'v' -DefaultInput 'NT Authority\NetworkService').answer; + $InstallerArguments += "-ServiceUser $ServiceUser"; + if ($null -eq $ServicePass) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Does your Icinga Agent service user require a password to login? (Not required for System users)' -Default 'y').result -eq 1) { + $ServicePass = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please enter the password for your service user' -Secure -Default 'v').answer; + $InstallerArguments += "-ServicePass $ServicePass"; + } else { + $ServicePass = $null + $InstallerArguments += '-ServicePass $null'; + } + } + } else { + $InstallerArguments += "-ServiceUser 'NT Authority\NetworkService'"; + $ServiceUser = 'NT Authority\NetworkService'; + } + } + + if ($null -eq $InstallFrameworkPlugins) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to install the Icinga PowerShell Plugins?' -Default 'y').result -eq 1) { + $result = Install-IcingaFrameworkPlugins -PluginsUrl $PluginsUrl; + $PluginsUrl = $result.PluginUrl; + $InstallerArguments += "-InstallFrameworkPlugins 1"; + $InstallerArguments += "-PluginsUrl '$PluginsUrl'"; + } else { + $InstallerArguments += "-InstallFrameworkPlugins 0"; + } + } elseif ($InstallFrameworkPlugins -eq 1) { + $result = Install-IcingaFrameworkPlugins -PluginsUrl $PluginsUrl; + $InstallerArguments += "-InstallFrameworkPlugins 1"; + $InstallerArguments += "-PluginsUrl '$PluginsUrl'"; + } else { + if ((Test-IcingaWizardArgument -Argument 'InstallFrameworkPlugins') -eq $FALSE) { + $InstallerArguments += "-InstallFrameworkPlugins 0"; + } + } + + if ($null -eq $InstallFrameworkService) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to install the Icinga PowerShell Framework as a service?' -Default 'y').result -eq 1) { + $result = Get-IcingaFrameworkServiceBinary; + $InstallerArguments += "-InstallFrameworkService 1"; + $InstallerArguments += [string]::Format("-FrameworkServiceUrl '{0}'", $result.FrameworkServiceUrl); + $InstallerArguments += [string]::Format("-ServiceDirectory '{0}'", $result.ServiceDirectory); + $InstallerArguments += [string]::Format("-ServiceBin '{0}'", $result.ServiceBin); + $ServiceBin = $result.ServiceBin; + } else { + $InstallerArguments += "-InstallFrameworkService 0"; + } + } elseif ($InstallFrameworkService -eq $TRUE) { + $result = Get-IcingaFrameworkServiceBinary -FrameworkServiceUrl $FrameworkServiceUrl -ServiceDirectory $ServiceDirectory; + $ServiceBin = $result.ServiceBin; + } else { + $InstallerArguments += "-InstallFrameworkService 0"; + } + + if ($InstallerArguments.Count -ne 0) { + $InstallerArguments += "-RunInstaller"; + Write-IcingaConsoleNotice 'The wizard is complete. These are the configured settings:'; + + Write-IcingaConsolePlain '========'; + Write-IcingaConsolePlain ($InstallerArguments | Out-String); + Write-IcingaConsolePlain '========'; + + if (-Not $RunInstaller) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Is this configuration correct?' -Default 'y').result -eq 1) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to run the installer now? (Otherwise only the configuration command will be printed)' -Default 'y').result -eq 1) { + Write-IcingaConsoleNotice 'To execute your Icinga Agent installation based on your answers again on this or another machine, simply run this command:'; + + $RunInstaller = $TRUE; + } else { + Write-IcingaConsoleNotice 'To execute your Icinga Agent installation based on your answers, simply run this command:'; + } + } else { + Write-IcingaConsoleNotice 'Please run the wizard again to modify your answers or modify the command below:'; + } + } + Get-IcingaAgentInstallCommand -InstallerArguments $InstallerArguments -PrintConsole; + } + + if ($RunInstaller) { + if ((Test-IcingaAgentNETFrameworkDependency) -eq $FALSE) { + Write-IcingaConsoleError -Message 'You cannot install the Icinga Agent on this system as the required .NET Framework version is not installed. Please install .NET Framework 4.6.0 or later and use the above provided install arguments to try again.' + return; + } + + if ((Install-IcingaAgent -Version $AgentVersion -Source $PackageSource -AllowUpdates $AllowVersionChanges -InstallDir $InstallDir) -Or $Reconfigure) { + Reset-IcingaAgentConfigFile; + Move-IcingaAgentDefaultConfig; + Set-IcingaAgentNodeName -Hostname $Hostname; + Set-IcingaServiceUser -User $ServiceUser -Password $ServicePass -SetPermission | Out-Null; + if ($InstallFrameworkService) { + Install-IcingaForWindowsService -Path $ServiceBin -User $ServiceUser -Password $ServicePass | Out-Null; + } + Register-IcingaBackgroundDaemon -Command 'Start-IcingaServiceCheckDaemon'; + Install-IcingaAgentBaseFeatures; + $CertsInstalled = Install-IcingaAgentCertificates -Hostname $Hostname -Endpoint $CAEndpoint -Port $CAPort -CACert $CAFile -Ticket $Ticket; + Write-IcingaAgentApiConfig -Port $CAPort; + if ($EmptyCA -eq $TRUE -And $CertsInstalled -eq $FALSE) { + Disable-IcingaAgentFeature 'api'; + Write-IcingaConsoleWarning ` + -Message '{0}{1}{2}{3}{4}' ` + -Objects ( + 'Your Icinga Agent API feature has been disabled. Please provide either your ca.crt ', + 'or connect to a parent node for certificate requests. You can run "Install-IcingaAgentCertificates" ', + 'with your configuration to properly create the host certificate and a valid certificate request. ', + 'After this you can enable the API feature by using "Enable-IcingaAgentFeature api" and restart the ', + 'Icinga Agent service "Restart-IcingaService icinga2"' + ); + } + Write-IcingaAgentZonesConfig -Endpoints $Endpoints -EndpointConnections $EndpointConnections -ParentZone $ParentZone -GlobalZones $GlobalZoneConfig -Hostname $Hostname; + if ($AddFirewallRule) { + # First cleanup the system by removing all old Firewalls + Enable-IcingaFirewall -IcingaPort $CAPort -Force; + } + # Ensure the Icinga Agent is not spamming the Application log by default + Write-IcingaAgentEventLogConfig -Severity 'warning'; + Test-IcingaAgent; + if ($InstallFrameworkService) { + Restart-IcingaForWindows; + } + Restart-IcingaService 'icinga2'; + } + } +} + +function Add-InstallerArgument() +{ + param( + $InstallerArguments, + [string]$Key, + $Value, + [switch]$ReturnValue + ); + + [bool]$IsArray = $Value -is [array]; + + # Check for arrays + if ($IsArray) { + [array]$NewArray = @(); + foreach ($entry in $Value) { + $NewArray += Add-InstallerArgument -Value $entry -ReturnValue; + } + + if ($ReturnValue) { + return ([string]::Join(',', $NewArray)); + } + + $InstallerArguments += [string]::Format( + '-{0} {1}', + $Key, + [string]::Join(',', $NewArray) + ); + + return $InstallerArguments; + } + + # Check for integers + if (Test-Numeric $Value) { + if ($ReturnValue) { + return $Value; + } + + $InstallerArguments += [string]::Format( + '-{0} {1}', + $Key, + $Value + ); + + return $InstallerArguments; + } + + # Check for integer conversion + $IntValue = ConvertTo-Integer -Value $Value; + if ([string]$Value -ne [string]$IntValue) { + if ($ReturnValue) { + return $IntValue; + } + + $InstallerArguments += [string]::Format( + '-{0} {1}', + $Key, + $IntValue + ); + + return $InstallerArguments; + } + + $Type = $Value.GetType().Name; + $NewValue = $null; + + if ($Type -eq 'String') { + $NewValue = [string]::Format( + "'{0}'", + $Value + ); + + if ($ReturnValue) { + return $NewValue; + } + + $InstallerArguments += [string]::Format( + '-{0} {1}', + $Key, + $NewValue + ); + + return $InstallerArguments; + } +} + +function Test-IcingaWizardArgument() +{ + param( + [string]$Argument + ); + + foreach ($entry in $InstallerArguments) { + if ($entry -like [string]::Format('-{0} *', $Argument)) { + return $TRUE; + } + } + + return $FALSE; +} + +function Set-IcingaWizardArgument() +{ + param( + [hashtable]$DirectorArgs, + [string]$WizardArg, + $Value, + $Default = $null, + $InstallerArguments + ); + + if ($DirectorArgs.Overrides.ContainsKey($WizardArg)) { + + $InstallerArguments = Add-InstallerArgument ` + -InstallerArguments $InstallerArguments ` + -Key $WizardArg ` + -Value $DirectorArgs.Overrides[$WizardArg]; + + return @{ + 'Value' = $DirectorArgs.Overrides[$WizardArg]; + 'Args' = $InstallerArguments; + }; + } + + $RetValue = $null; + + if ($DirectorArgs.Arguments.ContainsKey($WizardArg)) { + $RetValue = $DirectorArgs.Arguments[$WizardArg]; + } else { + + if ($null -ne $Value -And [string]::IsNullOrEmpty($Value) -eq $FALSE) { + $InstallerArguments = Add-InstallerArgument ` + -InstallerArguments $InstallerArguments ` + -Key $WizardArg ` + -Value $Value; + + return @{ + 'Value' = $Value; + 'Args' = $InstallerArguments; + }; + } else { + return @{ + 'Value' = $Default; + 'Args' = $InstallerArguments; + }; + } + } + + if ([string]::IsNullOrEmpty($Value) -eq $FALSE) { + + $InstallerArguments = Add-InstallerArgument ` + -InstallerArguments $InstallerArguments ` + -Key $WizardArg ` + -Value $Value; + + return @{ + 'Value' = $Value; + 'Args' = $InstallerArguments; + }; + } + + return @{ + 'Value' = $RetValue; + 'Args' = $InstallerArguments; + }; +} +function Get-IcingaAgentInstallCommand() +{ + param( + $InstallerArguments, + [switch]$PrintConsole + ); + + [string]$Installer = ( + [string]::Format( + 'Start-IcingaAgentInstallWizard {0}', + ([string]::Join(' ', $InstallerArguments)) + ) + ); + + if ($PrintConsole) { + Write-IcingaConsolePlain '====' + Write-IcingaConsolePlain $Installer; + Write-IcingaConsolePlain '====' + } else { + return $Installer; + } +} +<# +.SYNOPSIS + Checks for old configurations provided by the old PowerShell module + and restores the original configuration file +.DESCRIPTION + Restores the original Icinga 2 configuration by replacing the existing + configuration created by the old PowerShell module with the plain one + from the Icinga 2 backup file +.FUNCTIONALITY + Restores original Icinga 2 configuration icinga2.conf +.EXAMPLE + PS>Reset-IcingaAgentConfigFile; +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Reset-IcingaAgentConfigFile() +{ + $ConfigDir = Get-IcingaAgentConfigDirectory; + $OldConfig = Join-Path -Path $ConfigDir -ChildPath 'icinga2.conf'; + $OldConfigBackup = Join-Path -Path $ConfigDir -ChildPath 'icinga2.conf.old.module'; + $OriginalConfig = Join-Path -Path $ConfigDir -ChildPath 'icinga2.confdirector.bak'; + + if ((Test-Path $OriginalConfig)) { + Write-IcingaConsoleWarning 'Found icinga2.conf backup file created by old PowerShell module. Restoring original configuration'; + + Move-Item -Path $OldConfig -Destination $OldConfigBackup; + Move-Item -Path $OriginalConfig -Destination $OldConfig; + } +} +function Show-IcingaAgentObjects() +{ + $Binary = Get-IcingaAgentBinary; + $Output = Start-IcingaProcess -Executable $Binary -Arguments 'object list'; + + if ($Output.ExitCode -ne 0) { + Write-IcingaConsoleError ([string]::Format('Failed to fetch Icinga Agent objects list: {0}{1}', $Output.Message, $Output.Error)); + return $null; + } + + return $Output.Message; +} +function Compare-IcingaVersions() +{ + param( + $CurrentVersion, + $RequiredVersion + ); + + if ([string]::IsNullOrEmpty($RequiredVersion)) { + return $FALSE; + } + + $RequiredVersion = Split-IcingaVersion -Version $RequiredVersion; + + if ([string]::IsNullOrEmpty($CurrentVersion) -eq $FALSE) { + $CurrentVersion = Split-IcingaVersion -Version $CurrentVersion; + } else { + $CurrentVersion = Get-IcingaAgentVersion; + } + + if ($requiredVersion.Major -gt $currentVersion.Major) { + return $FALSE; + } + + if ($requiredVersion.Minor -gt $currentVersion.Minor) { + return $FALSE; + } + + if ($requiredVersion.Minor -ge $currentVersion.Minor -And $requiredVersion.Fixes -gt $currentVersion.Fixes) { + return $FALSE; + } + + return $TRUE; +} +<# +.SYNOPSIS + Clears the entire content of the Icinga Agent API directory located + at Program Data\icinga2\var\lib\icinga2\api\ +.DESCRIPTION + Clears the entire content of the Icinga Agent API directory located + at Program Data\icinga2\var\lib\icinga2\api\ +.FUNCTIONALITY + Clears the entire content of the Icinga Agent API directory located + at Program Data\icinga2\var\lib\icinga2\api\ +.EXAMPLE + PS>Clear-IcingaAgentApiDirectory; +.EXAMPLE + PS>Clear-IcingaAgentApiDirectory -Force; +.PARAMETER Force + In case the Icinga Agent service is running while executing the command, + the force argument will ensure the service is stopped before the API + directory is flushed and restarted afterwards +.INPUTS + System.Boolean +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Clear-IcingaAgentApiDirectory() +{ + param ( + [switch]$Force = $FALSE + ); + + $IcingaService = $Global:Icinga.Protected.Environment.'Icinga Service'; + $ApiDirectory = (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\api\'); + + if ((Test-Path $ApiDirectory) -eq $FALSE) { + Write-IcingaConsoleError 'The Icinga Agent API directory is not present on this system. Please check if the Icinga Agent is installed'; + return; + } + + if ($IcingaService.Status -eq 'Running' -And $Force -eq $FALSE) { + Write-IcingaConsoleError 'The API directory can not be deleted while the Icinga Agent is running. Use the "-Force" argument to stop the service, flush the directory and restart the service again.'; + return; + } + + if ($IcingaService.Status -eq 'Running') { + Stop-IcingaService icinga2; + Start-Sleep -Seconds 1; + } + + Write-IcingaConsoleNotice 'Flushing Icinga Agent API directory'; + Remove-ItemSecure -Path (Join-Path -Path $ApiDirectory -ChildPath '*') -Recurse -Force | Out-Null; + Start-Sleep -Seconds 1; + + if ($IcingaService.Status -eq 'Running') { + Start-IcingaService icinga2; + } +} +function Split-IcingaVersion() +{ + param( + [string]$Version + ); + + # TODO: Allow developers to adjust their code from mayor to major naming + # for the next releases and remove the mayor naming in the future + if ([string]::IsNullOrEmpty($Version)) { + return @{ + 'Full' = ''; + 'Mayor' = $null; + 'Major' = $null; + 'Minor' = $null; + 'Fixes' = $null; + 'Snapshot' = $null; + } + } + + [array]$IcingaVersion = $Version.Split('.'); + $Snapshot = $null; + + if ([string]::IsNullOrEmpty($IcingaVersion[3]) -eq $FALSE) { + $Snapshot = [int]$IcingaVersion[3]; + } + + return @{ + 'Full' = $Version; + 'Mayor' = [int]$IcingaVersion[0]; + 'Major' = [int]$IcingaVersion[0]; + 'Minor' = [int]$IcingaVersion[1]; + 'Fixes' = [int]$IcingaVersion[2]; + 'Snapshot' = $Snapshot; + } +} +function Enable-IcingaAgentFeature() +{ + param( + [string]$Feature + ); + + if ([string]::IsNullOrEmpty($Feature)) { + throw 'Please specify a valid feature'; + } + + if ((Test-IcingaAgentFeatureEnabled -Feature $Feature)) { + Write-IcingaConsoleNotice ([string]::Format('This feature is already enabled [{0}]', $Feature)); + return; + } + + $Binary = Get-IcingaAgentBinary; + $Process = Start-IcingaProcess -Executable $Binary -Arguments ([string]::Format('feature enable {0}', $Feature)); + + if ($Process.ExitCode -ne 0) { + throw ([string]::Format('Failed to enable Icinga Feature: {0}', $Process.Message)); + } + + Write-IcingaConsoleNotice ([string]::Format('Feature "{0}" was successfully enabled', $Feature)); +} +function Start-IcingaAgentDirectorWizard() +{ + param( + [string]$DirectorUrl, + [string]$SelfServiceAPIKey = $null, + $OverrideDirectorVars = $null, + [bool]$RunInstaller = $FALSE, + [switch]$ForceTemplateKey = $FALSE, + [string]$Hostname = $null + ); + + [hashtable]$DirectorOverrideArgs = @{ } + if ([string]::IsNullOrEmpty($DirectorUrl)) { + $DirectorUrl = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please specify the Url pointing to your Icinga Director (Example: "https://example.com/icingaweb2/director")' -Default 'v').answer; + } + + [bool]$HostKnown = $FALSE; + [string]$TemplateKey = $SelfServiceAPIKey; + + if ($null -eq $OverrideDirectorVars -And $RunInstaller -eq $FALSE) { + if ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Do you want to manually override arguments provided by the Director API?' -Default 'n').result -eq 0) { + $OverrideDirectorVars = $TRUE; + } else { + $OverrideDirectorVars = $FALSE; + } + } + + $LocalAPIKey = Get-IcingaPowerShellConfig -Path 'IcingaDirector.SelfService.ApiKey'; + + if ($ForceTemplateKey) { + if ($SelfServiceAPIKey -eq $LocalAPIKey) { + $ForceTemplateKey = $FALSE; + } + } + + if ($ForceTemplateKey -eq $FALSE) { + if ([string]::IsNullOrEmpty($LocalAPIKey)) { + $LegacyTokenPath = Join-Path -Path Get-IcingaAgentConfigDirectory -ChildPath 'icingadirector.token'; + if (Test-Path $LegacyTokenPath) { + $SelfServiceAPIKey = Get-Content -Path $LegacyTokenPath; + Set-IcingaPowerShellConfig -Path 'IcingaDirector.SelfService.ApiKey' -Value $SelfServiceAPIKey; + } else { + $ForceTemplateKey = $TRUE; + } + } else { + $SelfServiceAPIKey = $LocalAPIKey; + } + } + + if ([string]::IsNullOrEmpty($SelfServiceAPIKey)) { + $SelfServiceAPIKey = (Get-IcingaAgentInstallerAnswerInput -Prompt 'Please enter your Self-Service API key' -Default 'v').answer; + } else { + if ($ForceTemplateKey -eq $FALSE) { + $HostKnown = $TRUE; + } + } + + if ([string]::IsNullOrEmpty($LocalAPIKey) -eq $FALSE -And $LocalAPIKey -ne $TemplateKey -And $ForceTemplateKey -eq $FALSE) { + try { + $Arguments = Get-IcingaDirectorSelfServiceConfig -DirectorUrl $DirectorUrl -ApiKey $LocalAPIKey; + } catch { + Write-IcingaConsoleError 'Your local stored host key is no longer valid. Using provided template key'; + + return Start-IcingaAgentDirectorWizard ` + -DirectorUrl $DirectorUrl ` + -SelfServiceAPIKey $TemplateKey ` + -OverrideDirectorVars $OverrideDirectorVars ` + -Hostname $Hostname ` + -ForceTemplateKey; + } + } else { + try { + $Arguments = Get-IcingaDirectorSelfServiceConfig -DirectorUrl $DirectorUrl -ApiKey $SelfServiceAPIKey; + } catch { + Write-IcingaConsoleError ([string]::Format('Failed to connect to your Icinga Director at "{0}". Please try again', $DirectorUrl)); + + return Start-IcingaAgentDirectorWizard ` + -SelfServiceAPIKey ((Get-IcingaAgentInstallerAnswerInput -Prompt 'Please re-enter your SelfService API Key for the Host-Template in case the key is no longer assigned to your host' -Default 'v' -DefaultInput $SelfServiceAPIKey).answer) ` + -OverrideDirectorVars $OverrideDirectorVars -Hostname $Hostname; + } + } + + $Arguments = Convert-IcingaDirectorSelfServiceArguments -JsonInput $Arguments; + + if ($OverrideDirectorVars -eq $TRUE -And -Not $RunInstaller) { + $DirectorOverrideArgs = Start-IcingaDirectorAPIArgumentOverride -Arguments $Arguments; + foreach ($entry in $DirectorOverrideArgs.Keys) { + if ($Arguments.ContainsKey($entry)) { + $Arguments[$entry] = $DirectorOverrideArgs[$entry]; + } + } + } + + if($Hostname -ne $null){ + $Arguments.Add('Hostname', $Hostname); + } + + if ($HostKnown -eq $FALSE) { + while ($TRUE) { + [bool]$RegisterFailed = $FALSE; + try { + $SelfServiceAPIKey = Register-IcingaDirectorSelfServiceHost -DirectorUrl $DirectorUrl -ApiKey $SelfServiceAPIKey -Hostname (Get-IcingaHostname @Arguments) -Endpoint $Arguments.IcingaMaster; + + if ([string]::IsNullOrEmpty($SelfServiceAPIKey) -eq $FALSE) { + break; + } else { + $RegisterFailed = $TRUE; + } + } catch { + $RegisterFailed = $TRUE; + } + + if ($RegisterFailed) { + $SelfServiceAPIKey = (Get-IcingaAgentInstallerAnswerInput -Prompt ([string]::Format('Failed to register host within Icinga Director. Full error: "{0}". Please re-enter your SelfService API Key. If this prompt continues ensure you are using an Agent template or drop your host key at "Hosts -> {1} -> Agent"', $_.Exception.Message, (Get-IcingaHostname @Arguments))) -Default 'v' -DefaultInput $SelfServiceAPIKey).answer; + } + } + + # Host is already registered + if ($null -eq $SelfServiceAPIKey) { + Write-IcingaConsoleError 'The wizard is unable to complete as this host is already registered but the local API key is not stored within the config' + return; + } + + $Arguments = Get-IcingaDirectorSelfServiceConfig -DirectorUrl $DirectorUrl -ApiKey $SelfServiceAPIKey; + $Arguments = Convert-IcingaDirectorSelfServiceArguments -JsonInput $Arguments; + if ($OverrideDirectorVars -eq $TRUE -And -Not $RunInstaller) { + $DirectorOverrideArgs = Start-IcingaDirectorAPIArgumentOverride -Arguments $Arguments; + foreach ($entry in $DirectorOverrideArgs.Keys) { + if ($Arguments.ContainsKey($entry)) { + $Arguments[$entry] = $DirectorOverrideArgs[$entry]; + } + } + } + } + + $IcingaTicket = Get-IcingaDirectorSelfServiceTicket -DirectorUrl $DirectorUrl -ApiKey $SelfServiceAPIKey; + + $DirectorOverrideArgs.Add( + 'DirectorUrl', $DirectorUrl + ); + $DirectorOverrideArgs.Add( + 'Ticket', $IcingaTicket + ); + $DirectorOverrideArgs.Add( + 'OverrideDirectorVars', 0 + ); + + if ([string]::IsNullOrEmpty($TemplateKey) -eq $FALSE) { + $DirectorOverrideArgs.Add( + 'SelfServiceAPIKey', $TemplateKey + ); + } + + return @{ + 'Arguments' = $Arguments; + 'Overrides' = $DirectorOverrideArgs; + }; +} + +function Start-IcingaDirectorAPIArgumentOverride() +{ + param( + $Arguments + ); + + $NewArguments = @{ }; + Write-IcingaConsoleNotice 'Please follow the wizard and manually override all entries you intend to'; + Write-IcingaConsoleNotice '===='; + + foreach ($entry in $Arguments.Keys) { + $value = (Get-IcingaAgentInstallerAnswerInput -Prompt ([string]::Format('Please enter the new value for the argument "{0}"', $entry)) -Default 'v' -DefaultInput $Arguments[$entry]).answer; + if ($Arguments[$entry] -is [array] -Or ($value -is [string] -And $value.Contains(','))) { + if ([string]::IsNullOrEmpty($value) -eq $FALSE) { + while ($value.Contains(', ')) { + $value = $value.Replace(', ', ','); + } + [array]$tmpArray = $value.Split(','); + if ($null -ne (Compare-Object -ReferenceObject $Arguments[$entry] -DifferenceObject $tmpArray)) { + $NewArguments.Add( + $entry, + $tmpArray + ); + } + } + continue; + } elseif ($Arguments[$entry] -is [bool]) { + if ($value -eq 'true' -or $value -eq 'y' -or $value -eq '1' -or $value -eq 'yes' -or $value -eq 1) { + $value = 1; + } else { + $value = 0; + } + } + + if ($Arguments[$entry] -ne $value) { + $NewArguments.Add($entry, $value); + } + } + + return $NewArguments; +} + +Export-ModuleMember -Function @( 'Start-IcingaAgentDirectorWizard' ); +function Convert-IcingaDirectorSelfServiceArguments() +{ + param( + $JsonInput + ); + + if ($null -eq $JsonInput) { + return @{ }; + } + + [hashtable]$DirectorArguments = @{ + PackageSource = $JsonInput.download_url; + AgentVersion = $JsonInput.agent_version; + CAPort = $JsonInput.agent_listen_port; + AllowVersionChanges = $JsonInput.allow_updates; + GlobalZones = $JsonInput.global_zones; + ParentZone = $JsonInput.parent_zone; + CAEndpoint = $JsonInput.IcingaMaster; + Endpoints = $JsonInput.parent_endpoints; + AddFirewallRule = $JsonInput.agent_add_firewall_rule; + AcceptConnections = $JsonInput.agent_add_firewall_rule; + ServiceUser = $JsonInput.icinga_service_user; + IcingaMaster = $JsonInput.IcingaMaster; + InstallFrameworkService = $JsonInput.install_framework_service; + ServiceDirectory = $JsonInput.framework_service_directory; + FrameworkServiceUrl = $JsonInput.framework_service_url; + InstallFrameworkPlugins = $JsonInput.install_framework_plugins; + PluginsUrl = $JsonInput.framework_plugins_url; + ConvertEndpointIPConfig = $JsonInput.resolve_parent_host; + UpdateAgent = $TRUE; + AddDirectorGlobal = $FALSE; + AddGlobalTemplates = $FALSE; + RunInstaller = $TRUE; + }; + + # Use NetworkService as default if nothing was transmitted by Director + if ([string]::IsNullOrEmpty($DirectorArguments['ServiceUser'])) { + $DirectorArguments['ServiceUser'] = 'NT Authority\NetworkService'; + } + + if ($JsonInput.transform_hostname -eq 1) { + $DirectorArguments.Add( + 'LowerCase', $TRUE + ); + } + + if ($JsonInput.transform_hostname -eq 2) { + $DirectorArguments.Add( + 'UpperCase', $TRUE + ); + } + + if ($JsonInput.fetch_agent_fqdn) { + $DirectorArguments.Add( + 'AutoUseFQDN', $TRUE + ); + } elseif ($JsonInput.fetch_agent_name) { + $DirectorArguments.Add( + 'AutoUseHostname', $TRUE + ); + } + + $NetworkDefault = ''; + foreach ($Endpoint in $JsonInput.endpoints_config) { + $NetworkDefault += [string]::Format('[{0}]:{1},', ($Endpoint.Split(';')[0]), $JsonInput.agent_listen_port); + } + if ([string]::IsNullOrEmpty($NetworkDefault) -eq $FALSE) { + $NetworkDefault = $NetworkDefault.Substring(0, $NetworkDefault.Length - 1).Split(','); + $DirectorArguments.Add( + 'EndpointConnections', $NetworkDefault + ); + + $EndpointConnections = $NetworkDefault; + } + + return $DirectorArguments; +} +function Disable-IcingaAgentFeature() +{ + param( + [string]$Feature + ); + + if ([string]::IsNullOrEmpty($Feature)) { + throw 'Please specify a valid feature'; + } + + if ((Test-IcingaAgentFeatureEnabled -Feature $Feature) -eq $FALSE) { + Write-IcingaConsoleNotice ([string]::Format('This feature is already disabled [{0}]', $Feature)); + return; + } + + $Binary = Get-IcingaAgentBinary; + $Process = Start-IcingaProcess -Executable $Binary -Arguments ([string]::Format('feature disable {0}', $Feature)); + + if ($Process.ExitCode -ne 0) { + throw ([string]::Format('Failed to disable Icinga Feature: {0}', $Process.Message)); + } + + Write-IcingaConsoleNotice ([string]::Format('Feature "{0}" was successfully disabled', $Feature)); +} +function Move-IcingaAgentDefaultConfig() +{ + $ConfigDir = Get-IcingaAgentConfigDirectory; + $BackupFile = Join-Path -Path $ConfigDir -ChildPath 'ps_backup\backup_executed.key'; + + if ((Test-Path $BackupFile)) { + Write-IcingaConsoleNotice 'A backup of your default configuration is not required. A backup was already made'; + return; + } + + New-Item (Join-Path -Path $ConfigDir -ChildPath 'ps_backup') -ItemType Directory | Out-Null; + + Move-Item -Path (Join-Path -Path $ConfigDir -ChildPath 'conf.d') -Destination (Join-Path -Path $ConfigDir -ChildPath 'ps_backup\conf.d'); + Move-Item -Path (Join-Path -Path $ConfigDir -ChildPath 'zones.conf') -Destination (Join-Path -Path $ConfigDir -ChildPath 'ps_backup\zones.conf'); + Copy-Item -Path (Join-Path -Path $ConfigDir -ChildPath 'constants.conf') -Destination (Join-Path -Path $ConfigDir -ChildPath 'ps_backup\constants.conf'); + Copy-Item -Path (Join-Path -Path $ConfigDir -ChildPath 'features-available') -Destination (Join-Path -Path $ConfigDir -ChildPath 'ps_backup\features-available'); + + New-Item (Join-Path -Path $ConfigDir -ChildPath 'conf.d') -ItemType Directory | Out-Null; + New-Item (Join-Path -Path $ConfigDir -ChildPath 'zones.conf') -ItemType File | Out-Null; + New-Item -Path $BackupFile -ItemType File | Out-Null; + + Write-IcingaConsoleNotice 'Successfully backed up Icinga 2 Agent default config'; +} +function Set-IcingaAcl() +{ + param( + [string]$Directory, + [string]$IcingaUser = (Get-IcingaServiceUser), + [switch]$Remove = $FALSE + ); + + if (-Not (Test-Path $Directory)) { + Write-IcingaConsoleWarning 'Unable to set ACL for directory "{0}". Directory does not exist' -Objects $Directory; + return; + } + + if ($IcingaUser.ToLower() -eq 'nt authority\system' -Or $IcingaUser.ToLower() -like '*localsystem') { + return; + } + + $DirectoryAcl = (Get-Item -Path $Directory).GetAccessControl('Access'); + $DirectoryAccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule( + $IcingaUser, + 'Modify', + 'ContainerInherit,ObjectInherit', + 'None', + 'Allow' + ); + + if ($Remove -eq $FALSE) { + $DirectoryAcl.SetAccessRule($DirectoryAccessRule); + } else { + foreach ($entry in $DirectoryAcl.Access) { + if (([string]($entry.IdentityReference)).ToLower() -like [string]::Format('*\{0}', $IcingaUser.ToLower())) { + $DirectoryAcl.RemoveAccessRuleSpecific($entry); + } + } + } + + Set-Acl -Path $Directory -AclObject $DirectoryAcl; + + if ($Remove -eq $FALSE) { + Test-IcingaAcl -Directory $Directory -WriteOutput | Out-Null; + } +} +function Set-IcingaServiceEnvironment() +{ + param ( + [switch]$Force = $FALSE + ); + + # Don't do anything if we are not inside an administrative shell + if ((Test-AdministrativeShell) -eq $FALSE) { + return; + } + + # Ensure we build our internal environment variables based on each version + # This is just required to prevent possible issues during upgrades from one version to another + if ($Global:Icinga.Protected.ContainsKey('Environment') -eq $FALSE) { + $Global:Icinga.Protected.Add( + 'Environment', + @{ } + ); + } + + if ($Global:Icinga.Protected.ContainsKey('Environment') -eq $FALSE) { + $Global:Icinga.Protected.Add( + 'Environment', + @{ } + ); + } + + if ($Global:Icinga.Protected.Environment.ContainsKey('FetchedServices') -eq $FALSE) { + $Global:Icinga.Protected.Environment.Add( + 'FetchedServices', $FALSE + ); + } + + if ($Global:Icinga.Protected.Environment.ContainsKey('Icinga Service') -eq $FALSE) { + $Global:Icinga.Protected.Environment.Add( + 'Icinga Service', + @{ + 'Status' = ''; + 'Present' = $FALSE; + 'Name' = 'icinga2'; + 'DisplayName' = 'icinga2'; + 'User' = 'NT Authority\NetworkService'; + 'ServicePath' = ''; + } + ); + } + + if ($Global:Icinga.Protected.Environment.ContainsKey('PowerShell Service') -eq $FALSE) { + $Global:Icinga.Protected.Environment.Add( + 'PowerShell Service', + @{ + 'Status' = ''; + 'Present' = $FALSE; + 'Name' = 'icingapowershell'; + 'DisplayName' = 'icingapowershell'; + 'User' = 'NT Authority\NetworkService'; + 'ServicePath' = ''; + } + ); + } + + if ($Global:Icinga.Protected.Environment.FetchedServices -And $Force -eq $FALSE) { + return; + } + + # Use scheduled tasks to fetch our current service configuration for faster load times afterwards + $IcingaService = Invoke-IcingaWindowsScheduledTask -JobType GetWindowsService -ObjectName 'icinga2'; + $PowerShellService = Invoke-IcingaWindowsScheduledTask -JobType GetWindowsService -ObjectName 'icingapowershell'; + + if ($null -ne $IcingaService -And $null -ne $IcingaService.Service) { + $Global:Icinga.Protected.Environment.'Icinga Service' = $IcingaService.Service; + } + + if ($null -ne $PowerShellService -And $null -ne $PowerShellService.Service) { + $Global:Icinga.Protected.Environment.'PowerShell Service' = $PowerShellService.Service; + } + + # In case the services are not present, ensure defaults are always set + if ($Global:Icinga.Protected.Environment.'Icinga Service'.User -eq 'Unknown') { + $Global:Icinga.Protected.Environment.'Icinga Service'.User = 'NT Authority\NetworkService'; + } + if ($Global:Icinga.Protected.Environment.'PowerShell Service'.User -eq 'Unknown') { + $Global:Icinga.Protected.Environment.'PowerShell Service'.User = 'NT Authority\NetworkService'; + } + + $Global:Icinga.Protected.Environment.FetchedServices = $TRUE; +} +function Set-IcingaServiceUser() +{ + param ( + [string]$User = 'NT Authority\NetworkService', + [securestring]$Password, + [string]$Service = 'icinga2', + [switch]$SetPermission = $FALSE + ); + + if ([string]::IsNullOrEmpty($User)) { + Write-IcingaConsoleError -Message 'Please specify a username to modify the service user'; + return $FALSE; + } + + switch ($Service.ToLower()) { + 'icinga2' { + if ($Global:Icinga.Protected.Environment.'Icinga Service'.Present -eq $FALSE) { + Write-IcingaConsoleDebug -Message 'Trying to update user for service "icinga2" while the service is not installed yet'; + return $FALSE; + } + break; + }; + 'icingapowershell' { + if ($Global:Icinga.Protected.Environment.'PowerShell Service'.Present -eq $FALSE) { + Write-IcingaConsoleDebug -Message 'Trying to update user for service "icingapowershell" while the service is not installed yet'; + return $FALSE; + } + break; + }; + default { + if ($null -eq (Get-Service $Service -ErrorAction SilentlyContinue)) { + return $FALSE; + } + }; + } + + if ($User.Contains('@')) { + $UserData = $User.Split('@'); + $User = [string]::Format('{0}\{1}', $UserData[1], $UserData[0]); + } elseif ($User.Contains('\') -eq $FALSE) { + $User = [string]::Format('.\{0}', $User); + } + + $ArgString = 'config {0} obj= "{1}" password= "{2}"'; + if ($null -eq $Password) { + $ArgString = 'config {0} obj= "{1}"{2}'; + } + + $Output = Start-IcingaProcess ` + -Executable 'sc.exe' ` + -Arguments ([string]::Format($ArgString, $Service, $User, (ConvertFrom-IcingaSecureString $Password))) ` + -FlushNewLines $TRUE; + + if ($Output.ExitCode -eq 0) { + + switch ($Service.ToLower()) { + 'icinga2' { + $Global:Icinga.Protected.Environment.'Icinga Service'.User = $User; + break; + }; + 'icingapowershell' { + $Global:Icinga.Protected.Environment.'PowerShell Service'.User = $User; + break; + }; + } + + if ($SetPermission) { + Set-IcingaAgentServicePermission | Out-Null; + Set-IcingaUserPermissions -IcingaUser $User; + } + + Write-IcingaConsoleNotice 'Service User "{0}" for service "{1}" successfully updated' -Objects $User, $Service; + return $TRUE; + } else { + Write-IcingaConsoleError ([string]::Format('Failed to update the service user: {0}', $Output.Message)); + return $FALSE; + } +} + +Set-Alias -Name 'Set-IcingaAgentServiceUser' -Value 'Set-IcingaServiceUser'; +function Set-IcingaUserPermissions() +{ + param ( + [string]$IcingaUser = (Get-IcingaServiceUser), + [switch]$Remove = $FALSE + ); + + Set-IcingaAcl -Directory "$Env:ProgramData\icinga2\etc" -IcingaUser $IcingaUser -Remove:$Remove; + Set-IcingaAcl -Directory "$Env:ProgramData\icinga2\var" -IcingaUser $IcingaUser -Remove:$Remove; + Set-IcingaAcl -Directory (Get-IcingaCacheDir) -IcingaUser $IcingaUser -Remove:$Remove; + Set-IcingaAcl -Directory (Get-IcingaPowerShellConfigDir) -IcingaUser $IcingaUser -Remove:$Remove; + Set-IcingaAcl -Directory (Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'certificate') -IcingaUser $IcingaUser -Remove:$Remove; +} +function Set-IcingaAgentNodeName() +{ + param( + $Hostname + ); + + if ([string]::IsNullOrEmpty($Hostname)) { + throw 'You have to specify a hostname in order to change the Icinga Agent NodeName'; + } + + $ConfigDir = Get-IcingaAgentConfigDirectory; + $ConstantsConf = Join-Path -Path $ConfigDir -ChildPath 'constants.conf'; + + $ConfigContent = Get-Content -Path $ConstantsConf; + + if ($ConfigContent.Contains('//const NodeName = "localhost"')) { + $ConfigContent = $ConfigContent.Replace( + '//const NodeName = "localhost"', + [string]::Format('const NodeName = "{0}"', $Hostname) + ); + } else { + [string]$NewConfigContent = ''; + foreach ($line in $ConfigContent) { + if ($line.Contains('const NodeName =')) { + $line = [string]::Format('const NodeName = "{0}"', $Hostname); + } + $NewConfigContent = [string]::Format('{0}{1}{2}', $NewConfigContent, $line, "`r`n"); + } + $ConfigContent = $NewConfigContent; + } + + Write-IcingaFileSecure -File $ConstantsConf -Value $ConfigContent; + + Write-IcingaConsoleNotice ([string]::Format('Your hostname was successfully changed to "{0}"', $Hostname)); +} +function Set-IcingaAgentServicePermission() +{ + if (Test-IcingaAgentServicePermission -Silent) { + Write-IcingaConsoleNotice 'The Icinga Service User already has permission to run as service'; + return; + } + + $SystemPermissions = New-IcingaTemporaryFile; + $ServiceUser = Get-IcingaServiceUser; + $ServiceUserSID = Get-IcingaUserSID $ServiceUser; + $SystemContent = Get-IcingaAgentServicePermission; + $NewSystemContent = @(); + + if ([string]::IsNullOrEmpty($ServiceUser)) { + Write-IcingaTestOutput -Severity 'Failed' -Message 'There is no user assigned to the Icinga 2 service or the service is not yet installed'; + return $FALSE; + } + + foreach ($line in $SystemContent) { + if ($line -like '*SeServiceLogonRight*') { + $line = [string]::Format('{0},*{1}', $line, $ServiceUserSID); + } + + $NewSystemContent += $line; + } + + Write-IcingaFileSecure -File "$SystemPermissions.inf" -Value $NewSystemContent; + + $SystemOutput = Start-IcingaProcess -Executable 'secedit.exe' -Arguments ([string]::Format('/import /cfg "{0}.inf" /db "{0}.sdb"', $SystemPermissions)); + + if ($SystemOutput.ExitCode -ne 0) { + throw ([string]::Format('Unable to import system permission information: {0}', $SystemOutput.Message)); + return $null; + } + + $SystemOutput = Start-IcingaProcess -Executable 'secedit.exe' -Arguments ([string]::Format('/configure /cfg "{0}.inf" /db "{0}.sdb"', $SystemPermissions)); + + if ($SystemOutput.ExitCode -ne 0) { + throw ([string]::Format('Unable to configure system permission information: {0}', $SystemOutput.Message)); + return $null; + } + + Remove-Item $SystemPermissions*; + + Test-IcingaAgentServicePermission | Out-Null; +} +function Write-IcingaAgentZonesConfig() +{ + param( + [array]$Endpoints = @(), + [array]$EndpointConnections = @(), + [string]$ParentZone = '', + [array]$GlobalZones = @(), + [string]$Hostname = '' + ); + + if ($Endpoints.Count -eq 0) { + throw 'Please properly specify your endpoint names'; + } + + if ([string]::IsNullOrEmpty($ParentZone)) { + throw 'Please specify a parent zone this agent shall connect to / receives connections from'; + } + + if ([string]::IsNullOrEmpty($Hostname)) { + throw 'Please specify hostname for this agent configuration'; + } + + [int]$Index = 0; + [string]$ZonesConf = ''; + + $ZonesConf = [string]::Format('{0}object Endpoint "{1}" {2}{3}', $ZonesConf, $Hostname, '{', "`r`n"); + $ZonesConf = [string]::Format('{0}{1}{2}{2}', $ZonesConf, '}', "`r`n"); + + foreach ($endpoint in $Endpoints) { + $ZonesConf = [string]::Format('{0}object Endpoint "{1}" {2}{3}', $ZonesConf, $endpoint, '{', "`r`n"); + if ($EndpointConnections.Count -ne 0) { + $ConnectionConfig = Get-IPConfigFromString -IPConfig ($EndpointConnections[$Index]); + $ZonesConf = [string]::Format('{0} host = "{1}";{2}', $ZonesConf, $ConnectionConfig.address, "`r`n"); + if ([string]::IsNullOrEmpty($ConnectionConfig.port) -eq $FALSE) { + $ZonesConf = [string]::Format('{0} port = "{1}";{2}', $ZonesConf, $ConnectionConfig.port, "`r`n"); + } + } + $ZonesConf = [string]::Format('{0}{1}{2}{2}', $ZonesConf, '}', "`r`n"); + $Index += 1; + } + + [string]$EndpointString = ''; + foreach ($endpoint in $Endpoints) { + $EndpointString = [string]::Format( + '{0}"{1}", ', + $EndpointString, + $endpoint + ); + } + $EndpointString = $EndpointString.Substring(0, $EndpointString.Length - 2); + + $ZonesConf = [string]::Format('{0}object Zone "{1}" {2}{3}', $ZonesConf, $ParentZone, '{', "`r`n"); + $ZonesConf = [string]::Format('{0} endpoints = [ {1} ];{2}', $ZonesConf, $EndpointString, "`r`n"); + $ZonesConf = [string]::Format('{0}{1}{2}{2}', $ZonesConf, '}', "`r`n"); + + $ZonesConf = [string]::Format('{0}object Zone "{1}" {2}{3}', $ZonesConf, $Hostname, '{', "`r`n"); + $ZonesConf = [string]::Format('{0} parent = "{1}";{2}', $ZonesConf, $ParentZone, "`r`n"); + $ZonesConf = [string]::Format('{0} endpoints = [ "{1}" ];{2}', $ZonesConf, $Hostname, "`r`n"); + $ZonesConf = [string]::Format('{0}{1}{2}{2}', $ZonesConf, '}', "`r`n"); + + foreach ($zone in $GlobalZones) { + $ZonesConf = [string]::Format('{0}object Zone "{1}" {2}{3}', $ZonesConf, $zone, '{', "`r`n"); + $ZonesConf = [string]::Format('{0} global = true;{1}', $ZonesConf, "`r`n"); + $ZonesConf = [string]::Format('{0}{1}{2}{2}', $ZonesConf, '}', "`r`n"); + } + + $ZonesConf = $ZonesConf.Substring(0, $ZonesConf.Length - 4); + + Write-IcingaFileSecure -File (Join-Path -Path (Get-IcingaAgentConfigDirectory) -ChildPath 'zones.conf') -Value $ZonesConf; + Write-IcingaConsoleNotice 'Icinga Agent zones.conf has been written successfully'; +} +<# +.SYNOPSIS + Writes the Icinga Agent Event Log configuration. + +.DESCRIPTION + The Write-IcingaAgentEventLogConfig function is used to write the configuration for the Icinga Agent Event Log. It creates a configuration file with the specified severity level for the Windows Event Log Logger. + +.PARAMETER Severity + Specifies the severity level for the Windows Event Log Logger. Valid values are 'debug', 'notice', 'information', 'warning', and 'critical'. The default value is 'information'. + +.EXAMPLE + Write-IcingaAgentEventLogConfig -Severity 'warning' + This example writes the Icinga Agent Event Log configuration with the severity level set to 'warning'. + +.NOTES + Please make sure to restart the Icinga Agent after applying any changes to the configuration. +#> + +function Write-IcingaAgentEventLogConfig() +{ + param ( + [ValidateSet('debug', 'notice', 'information', 'warning', 'critical')] + [string]$Severity = 'information' + ); + + [string]$FilePath = (Join-Path -Path (Get-IcingaAgentConfigDirectory) -ChildPath 'features-available\windowseventlog.conf'); + + if ((Test-Path -Path $FilePath) -eq $FALSE) { + Write-IcingaConsoleNotice 'Windows Eventlog configuration for the Icinga Agent was not written. Either the Icinga Agent is not installed or your installed version does not support this feature'; + return; + } + + $EventLogConf = New-Object System.Text.StringBuilder; + + $EventLogConf.AppendLine('/**') | Out-Null; + $EventLogConf.AppendLine(' * The WindowsEventLogLogger type writes log information to the Windows Event Log.') | Out-Null; + $EventLogConf.AppendLine(' */') | Out-Null; + $EventLogConf.AppendLine('') | Out-Null; + $EventLogConf.AppendLine('object WindowsEventLogLogger "windowseventlog" {') | Out-Null; + $EventLogConf.AppendLine([string]::Format(' severity = "{0}"', $Severity)) | Out-Null; + $EventLogConf.Append('}') | Out-Null; + + Write-IcingaFileSecure -File $FilePath -Value $EventLogConf.ToString(); + Write-IcingaConsoleNotice 'Windows Eventlog configuration has been written successfully to use severity level: {0} - Please restart the Icinga Agent to apply this change' -Objects $Severity; +} +function Write-IcingaAgentApiConfig() +{ + param ( + [int]$Port = 5665, + [string]$CipherList = $null + ); + + [string]$ApiConf = ''; + + $ApiConf = [string]::Format('{0}object ApiListener "api" {1}{2}', $ApiConf, '{', "`r`n"); + $ApiConf = [string]::Format('{0} accept_commands = true;{1}', $ApiConf, "`r`n"); + $ApiConf = [string]::Format('{0} accept_config = true;{1}', $ApiConf, "`r`n"); + $ApiConf = [string]::Format('{0} bind_host = "::";{1}', $ApiConf, "`r`n"); + $ApiConf = [string]::Format('{0} bind_port = {1};{2}', $ApiConf, $Port, "`r`n"); + if ([string]::IsNullOrEmpty($CipherList) -eq $FALSE) { + $ApiConf = [string]::Format('{0} cipher_list = "{1}";{2}', $ApiConf, $CipherList, "`r`n"); + } + $ApiConf = [string]::Format('{0}{1}{2}{2}', $ApiConf, '}', "`r`n"); + + $ApiConf = $ApiConf.Substring(0, $ApiConf.Length - 4); + + Write-IcingaFileSecure -File (Join-Path -Path (Get-IcingaAgentConfigDirectory) -ChildPath 'features-available\api.conf') -Value $ApiConf; + Write-IcingaConsoleNotice 'Api configuration has been written successfully'; +} +function Write-IcingaTestOutput() +{ + param( + [ValidateSet('Passed', 'Warning', 'Failed')] + $Severity, + $Message, + [switch]$DropMessage = $FALSE + ); + + if ($DropMessage) { + return; + } + + $Color = 'Green'; + + Switch ($Severity) { + 'Passed' { + $Color = 'Green'; + break; + }; + 'Warning' { + $Color = 'Yellow'; + break; + }; + 'Failed' { + $Color = 'Red'; + break; + }; + } + + Write-Host '[' -NoNewline; + Write-Host $Severity -ForegroundColor $Color -NoNewline; + Write-Host ']:' $Message; +} +function Write-IcingaAgentObjectList() +{ + param( + [string]$Path + ); + + if ([string]::IsNullOrEmpty($Path)) { + throw 'Please specify a path to write the Icinga objects to'; + } + + $ObjectList = Get-IcingaAgentObjectList; + + Write-IcingaFileSecure -File $Path -Value $ObjectList; +} +function Test-IcingaStateFile() +{ + param ( + [switch]$WriteOutput = $FALSE + ); + + $IcingaAgentData = Get-IcingaAgentInstallation; + [string]$StateFilePath = Join-Path -Path $ENV:ProgramData -ChildPath 'icinga2\var\lib\icinga2\icinga2.state'; + + if ((Test-Path $StateFilePath) -eq $FALSE) { + Write-IcingaTestOutput -Severity 'Passed' -Message 'The Icinga Agent state file does not exist' -DropMessage:(-Not $WriteOutput); + return $TRUE; + } + + $Success = Read-IcingaStateFile; + + if ($Success) { + Write-IcingaTestOutput -Severity 'Passed' -Message 'The Icinga Agent state file is healthy' -DropMessage:(-Not $WriteOutput); + return $TRUE; + } else { + Write-IcingaTestOutput -Severity 'Failed' -Message 'The Icinga Agent state file is corrupt. Use the "Repair-IcingaStateFile" command to repair the file or "Read-IcingaStateFile -WriteOutput" for further details' -DropMessage:(-Not $WriteOutput); + } + + return $FALSE; +} +<# +.SYNOPSIS + Test if .NET Framework 4.6.0 or above is installed which is required by + the Icinga Agent. Returns either true or false - depending on if the + .NET Framework 4.6.0 or above is installed or not +.DESCRIPTION + Test if .NET Framework 4.6.0 or above is installed which is required by + the Icinga Agent. Returns either true or false - depending on if the + .NET Framework 4.6.0 or above is installed or not +.FUNCTIONALITY + Test if .NET Framework 4.6.0 or above is installed +.EXAMPLE + PS>Test-IcingaAgentNETFrameworkDependency; +.OUTPUTS + System.Boolean +.LINK + https://github.com/Icinga/icinga-powershell-framework + https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed +#> + +function Test-IcingaAgentNETFrameworkDependency() +{ + $RegistryContent = Get-ItemProperty -Path 'HKLM:SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full' -ErrorAction SilentlyContinue; + + # We require at least .NET Framework 4.6.0 to be installed on the system + # Version on Windows 10: 393295 + # Version on any other system: 393297 + # We do only require to check for the Windows 10 version, as the other Windows verions + # do not cause an issue there then because of how the next versions are iterated + + if ($null -eq $RegistryContent -Or $RegistryContent.Release -lt 393295) { + if ($null -eq $RegistryContent) { + $RegistryContent = @{ + 'Version' = 'Unknown' + }; + } + Write-IcingaConsoleError ` + -Message 'To install the Icinga Agent you will require .NET Framework 4.6.0 or later to be installed on the system. Current installed version: {0}' ` + -Objects $RegistryContent.Version; + + return $FALSE; + } + + Write-IcingaConsoleNotice ` + -Message 'Found installed .NET Framework version {0}' ` + -Objects $RegistryContent.Version; + + return $TRUE; +} +function Test-IcingaAcl() +{ + param( + [string]$Directory, + [switch]$WriteOutput, + [string]$ServiceUser = (Get-IcingaServiceUser) + ); + + if ([string]::IsNullOrEmpty($Directory) -Or -Not (Test-Path $Directory)) { + Write-IcingaConsoleWarning 'The specified directory "{0}" was not found' -Objects $Directory; + return $FALSE; + } + + if ($ServiceUser.ToLower() -eq 'nt authority\system' -Or $ServiceUser.ToLower() -like '*localsystem') { + Write-IcingaTestOutput -Severity 'Passed' -Message ([string]::Format('Directory "{0}" is fully accessible by "NT Authority\SYSTEM"', $Directory)); + return $TRUE; + } + + $FolderACL = Get-Acl $Directory; + $UserFound = $FALSE; + $HasAccess = $FALSE; + $ServiceUserSID = Get-IcingaUserSID $ServiceUser; + + foreach ($user in $FolderACL.Access) { + # Not only check here for the exact name but also for included strings like NT AU or NT-AU or even further later on + # As the Get-Acl Cmdlet will translate usernames into the own language, resultng in 'NT AUTHORITY\NetworkService' being translated + # to 'NT-AUTORITÄT\Netzwerkdienst' for example + $UserSID = $null; + try { + $UserSID = Get-IcingaUserSID $user.IdentityReference; + } catch { + $UserSID = $null; + } + + if ($ServiceUserSID -eq $UserSID) { + $UserFound = $TRUE; + if (($user.FileSystemRights -Like '*Modify*' -And $user.FileSystemRights -Like '*Synchronize*') -Or $user.FileSystemRights -like '*FullControl*') { + $HasAccess = $TRUE; + } + } + } + + if ($WriteOutput) { + [string]$messageFormat = 'Directory "{0}" {1} by the Icinga Service User "{2}"'; + if ($UserFound) { + if ($HasAccess) { + Write-IcingaTestOutput -Severity 'Passed' -Message ([string]::Format($messageFormat, $Directory, 'is accessible and writable', $ServiceUser)); + } else { + Write-IcingaTestOutput -Severity 'Failed' -Message ([string]::Format($messageFormat, $Directory, 'is accessible but NOT writable', $ServiceUser)); + Write-IcingaConsolePlain "\_ Please run the following command to fix this issue: Set-IcingaAcl -Directory '$Directory'"; + } + } else { + Write-IcingaTestOutput -Severity 'Failed' -Message ([string]::Format($messageFormat, $Directory, 'is not accessible', $ServiceUser)); + Write-IcingaConsolePlain "\_ Please run the following command to fix this issue: Set-IcingaAcl -Directory '$Directory'"; + } + } + + return $UserFound; +} +function Test-IcingaAgentServicePermission() +{ + param( + [switch]$Silent = $FALSE + ); + + $ServiceUser = Get-IcingaServiceUser; + $ServiceUserSID = Get-IcingaUserSID $ServiceUser; + $SystemContent = Get-IcingaAgentServicePermission; + [bool]$FoundSID = $FALSE; + + if ($ServiceUser -eq 'NT Authority\SYSTEM') { + Write-IcingaTestOutput -Severity 'Passed' -Message ([string]::Format('The specified user "{0}" is allowed to run as service', $ServiceUser)); + return $TRUE; + } + + if ([string]::IsNullOrEmpty($ServiceUser)) { + if (-Not $Silent) { + Write-IcingaTestOutput -Severity 'Failed' -Message 'There is no user assigned to the Icinga 2 service or the service is not yet installed'; + } + return $FALSE; + } + + foreach ($line in $SystemContent) { + if ($line -like '*SeServiceLogonRight*') { + $Index = $line.IndexOf('= ') + 2; + [string]$SIDs = $line.Substring($Index, $line.Length - $Index); + [array]$SIDArray = $SIDs.Split(','); + + foreach ($sid in $SIDArray) { + if ($sid -like "*$ServiceUserSID" -Or $sid -eq $ServiceUser) { + $FoundSID = $TRUE; + break; + } + } + } + if ($FoundSID) { + break; + } + } + + if (-Not $Silent) { + if ($FoundSID) { + Write-IcingaTestOutput -Severity 'Passed' -Message ([string]::Format('The specified user "{0}" is allowed to run as service', $ServiceUser)); + } else { + Write-IcingaTestOutput -Severity 'Failed' -Message ([string]::Format('The specified user "{0}" is not allowed to run as service', $ServiceUser)); + } + } + + return $FoundSID; +} +function Test-IcingaAgentConfig() +{ + param ( + [switch]$WriteStackTrace + ); + + $Binary = Get-IcingaAgentBinary; + $ConfigResult = Start-IcingaProcess -Executable $Binary -Arguments 'daemon -C'; + + if ($ConfigResult.ExitCode -eq 0) { + Write-IcingaTestOutput -Severity 'Passed' -Message 'Icinga Agent configuration is valid'; + return $TRUE; + } else { + Write-IcingaTestOutput -Severity 'Failed' -Message 'Icinga Agent configuration contains errors. Run this command for getting a detailed error report: "Test-IcingaAgentConfig -WriteStackTrace | Out-Null"'; + if ($WriteStackTrace) { + Write-IcingaConsolePlain $ConfigResult.Message; + } + return $FALSE; + } +} +function Test-IcingaForWindows() +{ + param ( + [switch]$ResolveProblems = $FALSE + ); + + Write-IcingaConsoleNotice 'Collecting Icinga for Windows environment information'; + Set-IcingaServiceEnvironment -Force; + + $IcingaAgentData = Get-IcingaAgentInstallation; + $IcingaService = $Global:Icinga.Protected.Environment.'Icinga Service'; + $IfWService = $Global:Icinga.Protected.Environment.'PowerShell Service'; + + if ($IcingaService.Present -eq $FALSE -And $IfWService.Present -eq $FALSE -And $IcingaAgentData.Installed -eq $FALSE) { + Write-IcingaConsoleNotice 'The Icinga Agent and Icinga for Windows service are not installed and the Icinga Agent is not present on the system'; + return; + }; + + if ($IcingaAgentData.Installed -And $IcingaService.Present -eq $FALSE) { + if ($ResolveProblems -eq $FALSE) { + Write-IcingaTestOutput -Severity Failed -Message 'The Icinga Agent service is not installed, while Icinga Agent itself is present on the system.'; + } else { + Write-IcingaConsoleNotice 'Fixing problems with Icinga Service not present for installed Icinga Agent'; + Repair-IcingaService -RootFolder $IcingaAgentData.RootDir; + } + } elseif ($IcingaAgentData.Installed -eq $FALSE -And $IcingaService.Present) { + Write-IcingaTestOutput -Severity Failed -Message 'The Icinga Agent service is installed, while Icinga Agent itself is not present on the system.'; + } elseif ($IcingaService.Present -eq $FALSE) { + Write-IcingaTestOutput -Severity Warning -Message 'The Icinga Agent service seems not to be installed'; + } else { + Write-IcingaTestOutput -Severity Passed -Message 'The Icinga Agent service and the Icinga Agent are installed on the system'; + } + + if ($IfWService.Present -eq $FALSE) { + Write-IcingaTestOutput -Severity Warning -Message 'The Icinga for Windows service seems not to be installed'; + } else { + Write-IcingaTestOutput -Severity Passed -Message 'The Icinga for Windows service is installed on the system'; + + if ([string]::IsNullOrEmpty($IfWService.ServicePath) -eq $FALSE) { + $IfWServicePath = $IfWService.ServicePath.Substring(1, $IfWService.ServicePath.IndexOf('" ') - 1); + if (Test-Path $IfWServicePath) { + Write-IcingaTestOutput -Severity Passed -Message ([string]::Format('The Icinga for Windows service binary does exist: "{0}"', $IfWServicePath)); + } else { + Write-IcingaTestOutput -Severity Failed -Message ([string]::Format('The Icinga for Windows service binary could not be found: "{0}"', $IfWServicePath)); + } + } else { + Write-IcingaTestOutput -Severity Failed -Message 'The Icinga for Windows service path seems to be not configured'; + } + } + + Test-IcingaForWindowsService -ResolveProblems:$ResolveProblems | Out-Null; + + if ($IcingaService.Present -And $IfWService.Present -And $IfWService.User.ToLower() -ne $IcingaService.User.ToLower()) { + Write-IcingaTestOutput -Severity Warning -Message ( + [string]::Format( + 'The Icinga Agent service user "{0}" is not matching the Icinga for Windows service user "{1}"', + $IcingaService.User, + $IfWService.User + ) + ); + } else { + Write-IcingaTestOutput -Severity Passed -Message ( + [string]::Format( + 'The Icinga Agent service user "{0}" is matching the Icinga for Windows service user "{1}"', + $IcingaService.User, + $IfWService.User + ) + ); + } + + if ((Test-IcingaAgentServicePermission) -eq $FALSE -And $ResolveProblems) { + Write-IcingaConsoleNotice 'Fixing problems with Icinga service user permissions'; + Set-IcingaAgentServicePermission; + } + + [array]$TestingDirectory = @( + (Join-Path -Path $Env:ProgramData -ChildPath '\icinga2\etc'), + (Join-Path -Path $Env:ProgramData -ChildPath '\icinga2\var'), + (Get-IcingaCacheDir), + (Get-IcingaPowerShellConfigDir), + (Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'certificate') + ); + + foreach ($entry in $TestingDirectory) { + if ((Test-IcingaAcl -Directory $entry -WriteOutput) -eq $FALSE -And $ResolveProblems) { + Write-IcingaConsoleNotice 'Fixing permission problem for Directory {0}' -Objects $entry; + Set-IcingaAcl -Directory $entry; + } + } + + if ((Test-IcingaStateFile -WriteOutput) -eq $FALSE -And $ResolveProblems) { + Write-IcingaConsoleNotice 'Fixing problems with Icinga State file'; + Repair-IcingaStateFile; + } + + if ($IcingaAgentData.Installed) { + Test-IcingaAgentConfig | Out-Null; + if (Test-IcingaAgentFeatureEnabled -Feature 'debuglog') { + Write-IcingaTestOutput -Severity 'Warning' -Message 'The debug log of the Icinga Agent is enabled. Please keep in mind to disable it once testing is done, as a huge amount of data is generated' + } else { + Write-IcingaTestOutput -Severity 'Passed' -Message 'Icinga Agent debug log is disabled' + } + } + + [hashtable]$DaemonList = Get-IcingaBackgroundDaemons; + + if ($DaemonList.ContainsKey('Start-IcingaWindowsRESTApi') -eq $FALSE) { + if ($ResolveProblems -eq $FALSE) { + Write-IcingaTestOutput -Severity Failed -Message 'The Icinga for Windows REST-Api is not configured to start with the daemon'; + } else { + Write-IcingaConsoleNotice 'Fixing problems with missing Icinga for Windows REST-Api configuration'; + Register-IcingaBackgroundDaemon -Command 'Start-IcingaWindowsRESTApi'; + Add-IcingaRESTApiCommand -Command 'Invoke-IcingaCheck*' -Endpoint 'apichecks'; + } + } else { + Write-IcingaTestOutput -Severity Passed -Message 'The Icinga for Windows REST-Api is configured to start with the daemon'; + } + + if ((Get-IcingaFrameworkApiChecks) -eq $FALSE) { + if ($ResolveProblems -eq $FALSE) { + Write-IcingaTestOutput -Severity Failed -Message 'The Icinga for Windows REST-Api is not configured to allow API checks'; + } else { + Write-IcingaConsoleNotice 'Fixing problems with missing Icinga for Windows API-Checks feature configuration'; + Enable-IcingaFrameworkApiChecks; + } + } else { + Write-IcingaTestOutput -Severity Passed -Message 'The Icinga for Windows REST-Api is configured to allow API checks'; + } + + $IfWCertificate = Get-IcingaForWindowsCertificate; + $Hostname = Get-IcingaHostname -ReadConstants; + + if ($null -eq $IfWCertificate) { + if ($ResolveProblems -eq $FALSE) { + Write-IcingaTestOutput -Severity Failed -Message 'The Icinga for Windows certificate is not installed on the system'; + } else { + Write-IcingaConsoleNotice 'Fixing problems with missing Icinga for Windows certificate'; + Start-IcingaWindowsScheduledTaskRenewCertificate; + $IfWCertificate = Get-IcingaForWindowsCertificate; + } + } + + if ($null -ne $IfWCertificate) { + if ($IfWCertificate.Issuer.ToLower() -eq ([string]::Format('cn={0}', $Hostname).ToLower())) { + Write-IcingaTestOutput -Severity Failed -Message 'The Icinga for Windows certificate seems to be not signed by our Icinga CA yet. Re-Creating the certificate might resolve this issue [IWKB000013]'; + if ($ResolveProblems) { + Write-IcingaConsoleNotice 'Fixing problems with missing not signed Icinga for Windows certificate by re-creating it from the Icinga Agent certificate'; + Start-IcingaWindowsScheduledTaskRenewCertificate; + Start-Sleep -Seconds 5; + $IfWCertificate = Get-IcingaForWindowsCertificate; + + if ($IfWCertificate.Issuer.ToLower() -eq ([string]::Format('cn={0}', $Hostname).ToLower())) { + Write-IcingaTestOutput -Severity Passed -Message 'The Icinga for Windows certificate is still not properly signed. Please have a look on your Icinga Agent side and validate the configuration is correct and the certificate request was processed [IWKB000013]'; + } else { + Write-IcingaTestOutput -Severity Passed -Message 'The Icinga for Windows certificate is installed on the system and possibly signed by a valid CA'; + } + } + } else { + Write-IcingaTestOutput -Severity Passed -Message 'The Icinga for Windows certificate is installed on the system and possibly signed by a valid CA'; + } + } + + $JEAContext = Get-IcingaJEAContext; + $JEASessionFile = Get-IcingaJEASessionFile; + $ServicePid = Get-IcingaForWindowsServicePid; + $JEAServicePid = Get-IcingaJEAServicePid; + + if ([string]::IsNullOrEmpty($JEAContext)) { + Write-IcingaTestOutput -Severity Warning -Message 'Icinga for Windows is configured without a JEA-Profile. It is highly recommended to use JEA for advanced security and easier permission handling'; + } else { + Write-IcingaTestOutput -Severity Passed -Message 'Icinga for Windows is configured with a JEA-Profile'; + } + + if ($IfWService.Status -eq 'Running') { + if ([string]::IsNullOrEmpty($JEAContext) -eq $FALSE -And [string]::IsNullOrEmpty($JEAServicePid)) { + if ($ResolveProblems -eq $FALSE) { + Write-IcingaTestOutput -Severity Failed -Message 'The Icinga for Windows service is running, but the JEA-Session is not working. Please validate the proper installation of JEA and try to rebuild the security profile.'; + } else { + Write-IcingaConsoleNotice 'Fixing problems with JEA session by updating the Icinga for Windows JEA-Profile'; + Update-IcingaJeaProfile -RebuildFramework; + } + } elseif ([string]::IsNullOrEmpty($JEAContext) -And [string]::IsNullOrEmpty($JEAServicePid)) { + Write-IcingaTestOutput -Severity Passed -Message 'The Icinga for Windows service is running'; + } else { + Write-IcingaTestOutput -Severity Passed -Message 'The Icinga for Windows service is running and the JEA-Service is running as well'; + } + } else { + Write-IcingaTestOutput -Severity Failed -Message 'The Icinga for Windows service is currently not running'; + } + + Set-IcingaTLSVersion; + + try { + $ApiResult = Invoke-IcingaForWindowsRESTApi; + Write-IcingaTestOutput -Severity Passed -Message 'The Icinga for Windows REST-Api responded successfully on this machine'; + } catch { + if ($IfWService.User.ToLower() -eq 'nt authority\networkservice') { + Write-IcingaTestOutput -Severity Failed -Message ([string]::Format('The Icinga for Windows REST-Api responded with an error on this machine, which is expected when using the default NetworkService account [IWKB000018]: "{0}"', $_.Exception.Message)); + } else { + if ($ResolveProblems -eq $FALSE) { + Write-IcingaTestOutput -Severity Failed -Message ([string]::Format('The Icinga for Windows REST-Api responded with an error on this machine: "{0}"', $_.Exception.Message)); + } else { + Write-IcingaConsoleNotice 'Fixing problems with Icinga for Windows REST-Api by restarting the Icinga for Windows service'; + Restart-IcingaForWindows; + } + } + } +} +function Test-IcingaAgentFeatureEnabled() +{ + param( + [string]$Feature + ); + + $Features = Get-IcingaAgentFeatures; + + if ($Features.Enabled -Contains $Feature) { + return $TRUE; + } + + return $FALSE; +} +function Test-IcingaAgent() +{ + $IcingaAgentData = Get-IcingaAgentInstallation; + $AgentServicePresent = Get-Service 'icinga2' -ErrorAction SilentlyContinue; + if ($IcingaAgentData.Installed -And $null -ne $AgentServicePresent) { + Write-IcingaTestOutput -Severity 'Passed' -Message 'Icinga Agent service is installed'; + } elseif ($IcingaAgentData.Installed -And $null -eq $AgentServicePresent) { + Write-IcingaTestOutput -Severity 'Failed' -Message 'Icinga Agent service is not installed'; + } elseif ($IcingaAgentData.Installed -eq $FALSE -And $null -ne $AgentServicePresent) { + Write-IcingaTestOutput -Severity 'Failed' -Message 'Icinga Agent service is still present, while Icinga Agent itself is not installed.'; + } elseif ($IcingaAgentData.Installed -eq $FALSE -And $null -eq $AgentServicePresent) { + Write-IcingaTestOutput -Severity 'Passed' -Message 'Icinga Agent is not installed and service is not present.'; + + return; + } + + Test-IcingaAgentServicePermission | Out-Null; + Test-IcingaAcl "$Env:ProgramData\icinga2\etc" -WriteOutput | Out-Null; + Test-IcingaAcl "$Env:ProgramData\icinga2\var" -WriteOutput | Out-Null; + Test-IcingaAcl (Get-IcingaCacheDir) -WriteOutput | Out-Null; + Test-IcingaAcl (Get-IcingaPowerShellConfigDir) -WriteOutput | Out-Null; + Test-IcingaAcl -Directory (Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'certificate') -WriteOutput | Out-Null; + Test-IcingaStateFile -WriteOutput | Out-Null; + + if ($IcingaAgentData.Installed) { + Test-IcingaAgentConfig | Out-Null; + if (Test-IcingaAgentFeatureEnabled -Feature 'debuglog') { + Write-IcingaTestOutput -Severity 'Warning' -Message 'The debug log of the Icinga Agent is enabled. Please keep in mind to disable it once testing is done, as a huge amount of data is generated' + } else { + Write-IcingaTestOutput -Severity 'Passed' -Message 'Icinga Agent debug log is disabled' + } + } +} +function Get-IcingaFirewallConfig() +{ + param( + [switch]$NoOutput + ); + + [bool]$LegacyFirewallPresent = $FALSE; + [bool]$IcingaFirewallPresent = $FALSE; + + $LegacyFirewall = Start-IcingaProcess -Executable 'netsh' -Arguments 'advfirewall firewall show rule name="Icinga 2 Agent Inbound by PS-Module"'; + + if ($LegacyFirewall.ExitCode -eq 0) { + if ($NoOutput -eq $FALSE) { + Write-IcingaConsoleWarning 'Legacy firewall configuration has been detected.'; + } + $LegacyFirewallPresent = $TRUE; + } + + $IcingaFirewall = Start-IcingaProcess -Executable 'netsh' -Arguments 'advfirewall firewall show rule name="Icinga Agent Inbound"'; + + if ($IcingaFirewall.ExitCode -eq 0) { + if ($NoOutput -eq $FALSE) { + Write-IcingaConsoleNotice 'Icinga firewall is present.'; + } + $IcingaFirewallPresent = $TRUE; + } else { + if ($NoOutput -eq $FALSE) { + Write-IcingaConsoleError 'Icinga firewall is not present'; + } + } + + return @{ + 'LegacyFirewall' = $LegacyFirewallPresent; + 'IcingaFirewall' = $IcingaFirewallPresent; + } +} +function Disable-IcingaFirewall() +{ + param( + [switch]$LegacyOnly + ); + + $FirewallConfig = Get-IcingaFirewallConfig -NoOutput; + + if ($FirewallConfig.LegacyFirewall) { + $Firewall = Start-IcingaProcess -Executable 'netsh' -Arguments 'advfirewall firewall delete rule name="Icinga 2 Agent Inbound by PS-Module"'; + if ($Firewall.ExitCode -ne 0) { + Write-IcingaConsoleError ([string]::Format('Failed to remove legacy firewall: {0}{1}', $Firewall.Message, $Firewall.Error)); + } else { + Write-IcingaConsoleNotice 'Successfully removed legacy firewall rule'; + } + } + + if ($LegacyOnly) { + return; + } + + if ($FirewallConfig.IcingaFirewall) { + $Firewall = Start-IcingaProcess -Executable 'netsh' -Arguments 'advfirewall firewall delete rule name="Icinga Agent Inbound"'; + if ($Firewall.ExitCode -ne 0) { + Write-IcingaConsoleError ([string]::Format('Failed to remove Icinga firewall: {0}{1}', $Firewall.Message, $Firewall.Error)); + } else { + Write-IcingaConsoleNotice 'Successfully removed Icinga firewall rule'; + } + } +} +function Enable-IcingaFirewall() +{ + param( + [int]$IcingaPort = 5665, + [switch]$Force + ); + + $FirewallConfig = Get-IcingaFirewallConfig -NoOutput; + + if ($FirewallConfig.IcingaFirewall -And $Force -eq $FALSE) { + Write-IcingaConsoleNotice 'Icinga Firewall is already enabled' + return; + } + + if ($Force) { + Disable-IcingaFirewall; + } + + $IcingaBinary = Get-IcingaAgentBinary; + [string]$FirewallRule = [string]::Format( + 'advfirewall firewall add rule dir=in action=allow program="{0}" name="{1}" description="{2}" enable=yes remoteip=any localip=any localport={3} protocol=tcp', + $IcingaBinary, + 'Icinga Agent Inbound', + 'Inbound Firewall Rule to allow Icinga 2 masters / satellites to connect to the Icinga 2 Agent installed on this system.', + $IcingaPort + ); + + $FirewallResult = Start-IcingaProcess -Executable 'netsh' -Arguments $FirewallRule; + + if ($FirewallResult.ExitCode -ne 0) { + Write-IcingaConsoleError ([string]::Format('Failed to open Icinga firewall for port "{0}": {1}[2}', $IcingaPort, $FirewallResult.Message, $FirewallResult.Error)); + } else { + Write-IcingaConsoleNotice ([string]::Format('Successfully enabled firewall for port "{0}"', $IcingaPort)); + } +} +function Find-IcingaAgentObjects() +{ + param( + $Find = @(), + $OutFile = $null + ); + + if ($Find.Length -eq 0) { + throw 'Please specify content you want to look for'; + } + + [array]$ObjectList = (Get-IcingaAgentObjectList).Split("`r`n"); + [int]$lineIndex = 0; + [array]$Result = @(); + + foreach ($line in $ObjectList) { + if ([string]::IsNullOrEmpty($line)) { + continue; + } + + foreach ($entry in $Find) { + if ($line -like $entry) { + [string]$ResultLine = [string]::Format( + 'Line #{0} => "{1}"', + $lineIndex, + $line + ); + $Result += $ResultLine; + } + } + + $lineIndex += 1; + } + + if ([string]::IsNullOrEmpty($OutFile)) { + Write-Output $Result; + } else { + Write-IcingaFileSecure -File $OutFile -Value $Result; + } +} +function Get-IcingaHostname() +{ + param( + [string]$Hostname, + [bool]$AutoUseFQDN = $FALSE, + [bool]$AutoUseHostname = $FALSE, + [bool]$UpperCase = $FALSE, + [bool]$LowerCase = $FALSE, + [switch]$ReadConstants = $FALSE + ); + + [string]$UseHostname = ''; + + if ($ReadConstants) { + if (Test-Path -Path (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\etc\icinga2\constants.conf')) { + # Read the constants conf + $FileContent = Get-Content -Path (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\etc\icinga2\constants.conf') -Encoding 'UTF8'; + + foreach ($line in $FileContent) { + if ($line.Contains('NodeName') -eq $FALSE) { + continue; + } + + if ($line.Contains('const') -eq $FALSE -Or $line.Contains('=') -eq $FALSE -Or $line.Contains('"') -eq $FALSE) { + continue; + } + + [int]$ValueIndex = $line.IndexOf('"') + 1; + + $UseHostname = $line.SubString($ValueIndex, $line.Length - $ValueIndex); + + if ($UseHostname[-1] -eq '"') { + $UseHostname = $UseHostname.Substring(0, $UseHostname.Length - 1); + } + + break; + } + + return $UseHostname + } + } + + if ([string]::IsNullOrEmpty($Hostname) -eq $FALSE) { + $UseHostname = $Hostname; + } elseif ($AutoUseFQDN) { + $UseHostname = [System.Net.Dns]::GetHostEntry("localhost").HostName; + } else { + $UseHostname = [System.Net.Dns]::GetHostName(); + } + + if ($UpperCase) { + $UseHostname = $UseHostname.ToUpper(); + } elseif ($LowerCase) { + $UseHostname = $UseHostname.ToLower(); + } + + return $UseHostname; +} +function Get-IcingaAgentConfigDirectory() +{ + return (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\etc\icinga2\') +} +function Get-IcingaAgentInstallation() +{ + [string]$architecture = ''; + if ([IntPtr]::Size -eq 4) { + $architecture = "x86"; + $regPath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'; + } else { + $architecture = "x86_64"; + $regPath = @('HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'); + } + + $RegistryData = Get-ItemProperty $regPath; + $IcingaData = $null; + foreach ($entry in $RegistryData) { + if ($entry.DisplayName -eq 'Icinga 2') { + $IcingaData = $entry; + break; + } + } + + $ServiceUser = Get-IcingaServiceUser; + + if ($null -eq $IcingaData) { + return @{ + 'Installed' = $FALSE; + 'RootDir' = ''; + 'Version' = (Split-IcingaVersion); + 'Architecture' = $architecture; + 'Uninstaller' = ''; + 'InstallDate' = ''; + 'User' = $ServiceUser; + }; + } + + return @{ + 'Installed' = $TRUE; + 'RootDir' = $IcingaData.InstallLocation; + 'Version' = (Split-IcingaVersion $IcingaData.DisplayVersion); + 'Architecture' = $architecture; + 'Uninstaller' = $IcingaData.UninstallString.Replace("MsiExec.exe ", ""); + 'InstallDate' = $IcingaData.InstallDate; + 'User' = $ServiceUser; + }; +} +function Get-IcingaAgentHostCertificate() +{ + if (-Not (Test-Path -Path (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs\'))) { + return @{ + 'CertFile' = ''; + 'Subject' = ''; + 'Thumbprint' = ''; + }; + } + + # Default for Icinga 2.8.0 and above + [string]$CertDirectory = (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs\*'); + $FolderContent = Get-ChildItem -Path $CertDirectory -Filter '*.crt' -Exclude 'ca.crt'; + $Hostname = Get-IcingaHostname -ReadConstants; + $CertPath = $null; + + foreach ($certFile in $FolderContent) { + if ($certFile.Name -like ([string]::Format('{0}.crt', $Hostname))) { + $CertPath = $certFile.FullName; + break; + } + } + + if ([string]::IsNullOrEmpty($CertPath)) { + return $null; + } + + $Certificate = New-Object Security.Cryptography.X509Certificates.X509Certificate2 $CertPath; + + if ($null -ne $Certificate) { + if ($Certificate.Issuer.ToLower() -eq ([string]::Format('cn={0}', $Hostname).ToLower())) { + Write-IcingaConsoleWarning ` + -Message 'The Icinga Agent certificate "{0}" seems not to be signed by our Icinga CA yet. Using this certificate for the REST-Api as example might not work. Please check the state of the certificate and complete the signing process if required. [IWKB000013]' ` + -Objects $CertPath; + + Write-IcingaEventMessage -EventId 1506 -Namespace 'Framework' -Objects $CertPath; + } + } + + return @{ + 'CertFile' = $CertPath; + 'Subject' = $Certificate.Subject; + 'Thumbprint' = $Certificate.Thumbprint; + }; +} +function Get-IcingaAgentVersion() +{ + $IcingaAgent = Get-IcingaAgentInstallation; + + return $IcingaAgent.Version; +} +function Get-IcingaAgentBinary() +{ + $IcingaRootDir = Get-IcingaAgentRootDirectory; + if ([string]::IsNullOrEmpty($IcingaRootDir)) { + throw 'The Icinga Agent seems not to be installed'; + } + + $IcingaBinary = (Join-Path -Path $IcingaRootDir -ChildPath '\sbin\icinga2.exe'); + + if ((Test-Path $IcingaBinary) -eq $FALSE) { + throw 'Icinga Agent binary could not be found'; + } + + return $IcingaBinary; +} +function Get-IcingaAgentFeatures() +{ + $Binary = Get-IcingaAgentBinary; + $ConfigResult = Start-IcingaProcess -Executable $Binary -Arguments 'feature list'; + + if ($ConfigResult.ExitCode -ne 0) { + return @{ + 'Enabled' = @(); + 'Disabled' = @(); + } + } + + $DisabledFeatures = ( + $ConfigResult.Message.SubString( + 0, + $ConfigResult.Message.IndexOf('Enabled features') + ) + ).Replace('Disabled features: ', '').Replace("`r`n", '').Replace("`r", '').Replace("`n", ''); + + $EnabledFeatures = ( + $ConfigResult.Message.SubString( + $ConfigResult.Message.IndexOf('Enabled features'), + $ConfigResult.Message.Length - $ConfigResult.Message.IndexOf('Enabled features') + ) + ).Replace('Enabled features: ', '').Replace("`r`n", '').Replace("`r", '').Replace("`n", ''); + + return @{ + 'Enabled' = ($EnabledFeatures.Split(' ')); + 'Disabled' = ($DisabledFeatures.Split(' ')); + } +} +function Get-IcingaAgentMSIPackage() +{ + param( + [string]$Source, + [string]$Version, + [switch]$SkipDownload + ); + + if ([string]::IsNullOrEmpty($Version)) { + throw 'Please specify a valid version: "release", "snapshot" or a specific version like "2.11.0"'; + } + + if ([string]::IsNullOrEmpty($Source)) { + throw 'Please specify a valid download URL, like "https://packages.icinga.com/windows/"'; + } + + Set-IcingaTLSVersion; + # Disable the progress bar for the WebRequest + $ProgressPreference = "SilentlyContinue"; + $Architecture = Get-IcingaAgentArchitecture; + $LastUpdate = $null; + $Version = $Version.ToLower(); + + if ($Version -eq 'snapshot' -Or $Version -eq 'release') { + if (Test-Path $Source) { + $Content = Get-ChildItem -Path $Source; + + foreach ($entry in $Content) { + # Only check for MSI packages + if ($entry.Extension.ToLower() -ne '.msi') { + continue; + } + + $PackageVersion = ''; + + if ($entry.Name.ToLower().Contains('-')) { + $PackageVersion = ($entry.Name.Split('-')[1]).Replace('v', ''); + } + + if ($Version -eq 'snapshot') { + if ($PackageVersion -eq 'snapshot') { + $UseVersion = 'snapshot'; + break; + } + continue; + } + + if ($PackageVersion -eq 'snapshot') { + continue; + } + + try { + if ($null -eq $UseVersion -Or [version]$PackageVersion -ge [version]$UseVersion) { + $UseVersion = $PackageVersion; + } + } catch { + # Nothing to catch specifically + } + } + } else { + $Content = (Invoke-IcingaWebRequest -Uri $Source -UseBasicParsing).RawContent.Split("`r`n"); + $UsePackage = $null; + $UseVersion = $null; + + foreach ($line in $Content) { + if ($line -like '*.msi*' -And $line -like "*$Architecture.msi*") { + $MSIPackage = $line.SubString( + $line.IndexOf('Icinga2-'), + $line.IndexOf('.msi') - $line.IndexOf('Icinga2-') + ); + $LastUpdate = $line.SubString( + $line.IndexOf('indexcollastmod">') + 17, + $line.Length - $line.IndexOf('indexcollastmod">') - 17 + ); + $LastUpdate = $LastUpdate.SubString(0, $LastUpdate.IndexOf(' ')); + $LastUpdate = $LastUpdate.Replace('-', ''); + $MSIPackage = [string]::Format('{0}.msi', $MSIPackage); + $PackageVersion = ($MSIPackage.Split('-')[1]).Replace('v', ''); + + if ($Version -eq 'snapshot') { + if ($PackageVersion -eq 'snapshot') { + $UseVersion = 'snapshot'; + break; + } + } elseif ($Version -eq 'release') { + if ($line -like '*snapshot*' -Or $line -like '*-rc*') { + continue; + } + + if ($null -eq $UseVersion -Or [version]$PackageVersion -ge [version]$UseVersion) { + $UseVersion = $PackageVersion; + } + } + } + } + } + if ($Version -eq 'snapshot') { + $UsePackage = [string]::Format('Icinga2-{0}-{1}.msi', $UseVersion, $Architecture); + } else { + $UsePackage = [string]::Format('Icinga2-v{0}-{1}.msi', $UseVersion, $Architecture); + } + } else { + $UsePackage = [string]::Format('Icinga2-v{0}-{1}.msi', $Version, $Architecture); + } + + if ($null -eq $UsePackage) { + throw 'No Icinga installation MSI package for your architecture could be found for the provided version and source'; + } + + if ($SkipDownload -eq $FALSE) { + $DownloadPath = Join-Path $Env:TEMP -ChildPath $UsePackage; + Write-IcingaConsoleNotice ([string]::Format('Downloading Icinga 2 Agent installer "{0}" into temp directory "{1}"', $UsePackage, $DownloadPath)); + Invoke-IcingaWebRequest -Uri (Join-WebPath -Path $Source -ChildPath $UsePackage) -OutFile $DownloadPath | Out-Null; + } + + return @{ + 'InstallerPath' = $DownloadPath; + 'Version' = ($UsePackage).Replace('Icinga2-v', '').Replace('Icinga2-', '').Replace([string]::Format('-{0}.msi', $Architecture), '') + 'LastUpdate' = $LastUpdate; + } +} +function Get-IcingaAgentObjectList() +{ + $Binary = Get-IcingaAgentBinary; + $ObjectList = Start-IcingaProcess -Executable $Binary -Arguments 'object list'; + + return $ObjectList.Message; +} +function Get-IcingaAgentLogDirectory() +{ + return (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\log\icinga2\') +} +function Get-IcingaNetbiosName() +{ + $ComputerData = Get-IcingaWindowsInformation Win32_ComputerSystem; + + return $ComputerData.Name; +} +function Get-IcingaAgentArchitecture() +{ + $IcingaAgent = Get-IcingaAgentInstallation; + + return $IcingaAgent.Architecture; +} +function Get-IcingaServiceUser() +{ + $IcingaService = $Global:Icinga.Protected.Environment.'Icinga Service'; + $IfWService = $Global:Icinga.Protected.Environment.'PowerShell Service'; + # Default User + $ServiceUser = 'NT Authority\NetworkService'; + + if ($null -eq $IcingaService -Or $null -eq $IfWService) { + Set-IcingaServiceEnvironment; + } + + if ($IcingaService.Present) { + $ServiceUser = $IcingaService.User.Replace('.\', ''); + if ($ServiceUser -eq 'LocalSystem') { + return 'NT Authority\SYSTEM'; + } + } elseif ($IfWService.Present) { + $ServiceUser = $IfWService.User.Replace('.\', ''); + if ($ServiceUser -eq 'LocalSystem') { + return 'NT Authority\SYSTEM'; + } + } + + return $ServiceUser; +} +function Get-IcingaAgentServicePermission() +{ + $SystemPermissions = New-IcingaTemporaryFile; + $SystemOutput = Start-IcingaProcess -Executable 'secedit.exe' -Arguments ([string]::Format('/export /cfg "{0}.inf"', $SystemPermissions)); + + if ($SystemOutput.ExitCode -ne 0) { + throw ([string]::Format('Unable to fetch system permission information: {0}', $SystemOutput.Message)); + return $null; + } + + $SystemContent = Get-Content "$SystemPermissions.inf"; + + Remove-Item $SystemPermissions*; + + return $SystemContent; +} +function Get-IcingaAgentRootDirectory() +{ + $IcingaAgent = Get-IcingaAgentInstallation; + if ($IcingaAgent.Installed -eq $FALSE) { + return ''; + } + + return $IcingaAgent.RootDir; +} +function Get-IcingaAgentInstallerAnswerInput() +{ + param( + $Prompt, + [ValidateSet("y", "n", "v")] + $Default, + $DefaultInput = '', + [switch]$Secure + ); + + $DefaultAnswer = ''; + + if ($Default -eq 'y') { + $DefaultAnswer = ' (Y/n)'; + } elseif ($Default -eq 'n') { + $DefaultAnswer = ' (y/N)'; + } elseif ($Default -eq 'v') { + if ([string]::IsNullOrEmpty($DefaultInput) -eq $FALSE) { + $DefaultAnswer = [string]::Format(' (Defaults: "{0}")', $DefaultInput); + } + } + + if (-Not $Secure) { + $answer = Read-Host -Prompt ([string]::Format('{0}{1}', $Prompt, $DefaultAnswer)); + } else { + $answer = Read-Host -Prompt ([string]::Format('{0}{1}', $Prompt, $DefaultAnswer)) -AsSecureString; + } + + if ($Default -ne 'v') { + $answer = $answer.ToLower(); + + $returnValue = 0; + if ([string]::IsNullOrEmpty($answer) -Or $answer -eq $Default) { + $returnValue = 1; + } else { + $returnValue = 0; + } + + return @{ + 'result' = $returnValue; + 'answer' = ''; + } + } + + if ([string]::IsNullOrEmpty($answer)) { + $answer = $DefaultInput; + } + + return @{ + 'result' = 2; + 'answer' = $answer; + } +} +function Read-IcingaStateFile() +{ + param ( + [switch]$WriteOutput = $FALSE + ); + + [string]$StateFilePath = Join-Path -Path $ENV:ProgramData -ChildPath 'icinga2\var\lib\icinga2\icinga2.state'; + + if ((Test-Path $StateFilePath) -eq $FALSE) { + return $TRUE; + } + + $StateFileContent = Get-Content -Path $StateFilePath -Encoding 'UTF8' -Raw; + $FileInformation = Get-Item -Path $StateFilePath; + + if ([string]::IsNullOrEmpty($StateFileContent)) { + return $FALSE; + } + + while ($TRUE) { + try { + if ([string]::IsNullOrEmpty($StateFileContent)) { + break; + } + + if ($StateFileContent.Contains(':') -eq $FALSE) { + Write-IcingaTestOutput -Severity 'Failed' -Message 'The start index of the Icinga Agent state file could not be found. The file seems to be corrupt.' -DropMessage:(-Not $WriteOutput); + return $FALSE; + } + + [int]$IndexOfJSON = $StateFileContent.IndexOf(':'); + [int]$StatementSize = $StateFileContent.SubString(0, $IndexOfJSON); + [string]$JSONString = $StateFileContent.Substring($IndexOfJSON + 1, $StatementSize); + [int]$TotalMsgLen = $IndexOfJSON + $StatementSize + 2; + $StateFileContent = $StateFileContent.Substring($TotalMsgLen, $StateFileContent.Length - $TotalMsgLen); + $JsonValid = ConvertFrom-Json -InputObject $JSONString -ErrorAction Stop; + } catch { + [string]$ErrMessage = [string]::Format('The Icinga Agent state file validation failed with an exception: "{0}"', $_.Exception.Message); + Write-IcingaTestOutput -Severity 'Failed' -Message $ErrMessage -DropMessage:(-Not $WriteOutput); + + return $FALSE; + } + } + + return $TRUE; +} +function Read-IcingaAgentLogFile() +{ + param ( + [array]$Include = @(), + [array]$Exclude = @() + ); + + if ((Test-IcingaAgentFeatureEnabled -Feature 'windowseventlog') -And ([version](Get-IcingaAgentVersion).Full) -ge (New-IcingaVersionObject -Version '2.13.0')) { + + # Icinga 2.13.0 and beyond will log directly into the EventLog + Read-IcingaWindowsEventLog -LogName 'Application' -Source 'Icinga 2' -MaxEntries 500 -Include $Include -Exclude $Exclude; + } else { + $Logfile = Join-Path -Path (Get-IcingaAgentLogDirectory) -ChildPath 'icinga2.log'; + if ((Test-Path $Logfile) -eq $FALSE) { + Write-IcingaConsoleError 'Icinga 2 logfile not present. Unable to load it'; + return; + } + + Get-Content -Path $Logfile -Tail 20 -Wait -Encoding 'UTF8'; + } +} +function Read-IcingaAgentDebugLogFile() +{ + $Logfile = Join-Path -Path (Get-IcingaAgentLogDirectory) -ChildPath 'debug.log'; + if ((Test-Path $Logfile) -eq $FALSE) { + Write-IcingaConsoleError 'Icinga 2 debug logfile not present. Unable to load it'; + return; + } + + Get-Content -Path $Logfile -Tail 20 -Wait -Encoding 'UTF8'; +} +function Uninstall-IcingaAgent() +{ + param ( + [switch]$RemoveDataFolder = $FALSE + ); + + $IcingaData = Get-IcingaAgentInstallation; + [string]$IcingaProgramData = Join-Path -Path $Env:ProgramData -ChildPath 'icinga2'; + + if ($IcingaData.Installed -eq $FALSE) { + Write-IcingaConsoleNotice 'Unable to uninstall the Icinga Agent. The Agent is not installed'; + if ($RemoveDataFolder) { + if (Test-Path $IcingaProgramData) { + Write-IcingaConsoleNotice -Message 'Removing Icinga Agent directory: "{0}"' -Objects $IcingaProgramData; + return ((Remove-ItemSecure -Path $IcingaProgramData -Recurse -Force) -eq $FALSE); + } else { + Write-IcingaConsoleNotice -Message 'Icinga Agent directory "{0}" does not exist' -Objects $IcingaProgramData; + } + } + return $FALSE; + } + + Stop-IcingaService -Service 'icinga2'; + + $Uninstaller = & powershell.exe -Command { + Use-Icinga -Minimal; + + $IcingaData = $args[0]; + $Uninstaller = Start-IcingaProcess -Executable 'MsiExec.exe' -Arguments ([string]::Format('{0} /q /norestart', $IcingaData.Uninstaller)) -FlushNewLine; + + Start-Sleep -Seconds 2; + Optimize-IcingaForWindowsMemory; + + return $Uninstaller; + } -Args $IcingaData; + + if ($Uninstaller.ExitCode -ne 0) { + Write-IcingaConsoleError ([string]::Format('Failed to remove Icinga Agent: {0}{1}', $Uninstaller.Message, $Uninstaller.Error)); + return $FALSE; + } + + $Global:Icinga.Protected.Environment.'Icinga Service'.Present = $FALSE; + + if ($RemoveDataFolder) { + Write-IcingaConsoleNotice -Message 'Removing Icinga Agent directory: "{0}"' -Objects $IcingaProgramData; + if ((Remove-ItemSecure -Path $IcingaProgramData -Recurse -Force) -eq $FALSE) { + return $FALSE; + } + } + + Write-IcingaConsoleNotice 'Icinga Agent was successfully removed'; + return $TRUE; +} +function Install-IcingaAgentBaseFeatures() +{ + Disable-IcingaAgentFeature -Feature 'checker'; + Disable-IcingaAgentFeature -Feature 'notification'; + Enable-IcingaAgentFeature -Feature 'api'; +} +<# +.SYNOPSIS + Installs the required certificates for the Icinga Agent including the entire + signing process either by using the CA-Proxy, the CA-Server directly or + by manually signing the request on the CA master +.DESCRIPTION + Installs the required certificates for the Icinga Agent including the entire + signing process either by using the CA-Proxy, the CA-Server directly or + by manually signing the request on the CA master +.FUNCTIONALITY + Creates, installs and signs required certificates for the Icinga Agent +.EXAMPLE + # Connect to the CA server with a ticket to fully complete the request + PS>Install-IcingaAgentCertificates -Hostname 'windows.example.com' -Endpoint 'icinga2.example.com' -Ticket 'my_secret_ticket'; +.EXAMPLE + # Connect to the CA server without a ticket, to create the sign request on the master + PS>Install-IcingaAgentCertificates -Hostname 'windows.example.com' -Endpoint 'icinga2.example.com'; +.EXAMPLE + # Uses the Icinga ca.crt from a local filesystem and prepares the Icinga Agent for receiving connections from the Master/Satellite for signing + PS>Install-IcingaAgentCertificates -Hostname 'windows.example.com' -CACert 'C:\users\public\icinga2\ca.crt'; +.EXAMPLE + # Uses the Icinga ca.crt from a web resource and prepares the Icinga Agent for receiving connections from the Master/Satellite for signing + PS>Install-IcingaAgentCertificates -Hostname 'windows.example.com' -CACert 'https://example.com/icinga2/ca.crt'; +.PARAMETER Hostname + The hostname of the local system. Has to match the object name within the Icinga configuration +.PARAMETER Endpoint + The address of either the Icinga CA master or a parent node of the Agent to transmit the request to the CA master +.PARAMETER Port + The port used for Icinga communication. Uses 5665 as default +.PARAMETER CACert + Allows to specify the path to the ca.crt from the Icinga CA master on a local, network or web share to allow certificate generation + in case the Icinga Agent is not able to connect to it's parent hosts +.PARAMETER Ticket + The ticket number for the signing request which is either generated by Icinga 2 or the Icinga Director +.PARAMETER Force + Ignores existing certificates and will force the creation, overriding existing certificates +.INPUTS + System.String +.OUTPUTS + System.Boolean +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Install-IcingaAgentCertificates() +{ + param( + [string]$Hostname, + [string]$Endpoint, + [int]$Port = 5665, + [string]$CACert, + [string]$Ticket, + [switch]$Force = $FALSE + ); + + if ([string]::IsNullOrEmpty($Hostname)) { + Write-IcingaConsoleError 'Failed to install Icinga Agent certificates. Please provide a hostname'; + return $FALSE; + } + + # Default for Icinga 2.8.0 and above + [string]$NewCertificateDirectory = (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs\'); + [string]$OldCertificateDirectory = (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\etc\icinga2\pki\'); + [string]$CertificateDirectory = $NewCertificateDirectory; + if ((Compare-IcingaVersions -RequiredVersion '2.8.0') -eq $FALSE) { + # Certificate path for versions older than 2.8.0 + $CertificateDirectory = $OldCertificateDirectory; + Move-IcingaAgentCertificates -Source $NewCertificateDirectory -Destination $OldCertificateDirectory; + } else { + Move-IcingaAgentCertificates -Source $OldCertificateDirectory -Destination $NewCertificateDirectory; + } + + if (-Not (Test-IcingaAgentCertificates -CertDirectory $CertificateDirectory -Hostname $Hostname -Force $Force)) { + Write-IcingaConsoleNotice ([string]::Format('Generating host certificates for host "{0}"', $Hostname)); + + $arguments = [string]::Format('pki new-cert --cn {0} --key {1}{0}.key --cert {1}{0}.crt', + $Hostname, + $CertificateDirectory + ); + + if ((Start-IcingaAgentCertificateProcess -Arguments $arguments) -eq $FALSE) { + Write-IcingaConsoleError 'Failed to generate host certificate'; + return $FALSE; + } + + # Once we generated new host certificates, we always require to sign them if possible + $Force = $TRUE; + } + + if ([string]::IsNullOrEmpty($Endpoint) -And [string]::IsNullOrEmpty($CACert)) { + Write-IcingaConsoleWarning 'Your host certificates were generated successfully. Please either specify an endpoint to connect to or provide the path to a valid ca.crt'; + return $FALSE; + } + + if (-Not [string]::IsNullOrEmpty($Endpoint)) { + + # In case we use a custom configuration for our CA endpoint server with address and port, ensure we establish + # a connection to this endpoint as well as the port + $ConnectionConfig = Get-IPConfigFromString -IPConfig $Endpoint; + $Endpoint = $ConnectionConfig.address; + if ([string]::IsNullOrEmpty($ConnectionConfig.port) -eq $FALSE) { + $Port = $ConnectionConfig.port; + } + + if (-Not (Test-IcingaAgentCertificates -CertDirectory $CertificateDirectory -Hostname $Hostname -TestTrustedParent -Force $Force)) { + + Write-IcingaConsoleNotice ([string]::Format('Fetching trusted master certificate from "{0}"', $Endpoint)); + + # Argument --key for save-cert is deprecated starting with Icinga 2.12.0 + if (Compare-IcingaVersions -RequiredVersion '2.12.0') { + $arguments = [string]::Format('pki save-cert --trustedcert {0}trusted-parent.crt --host {1} --port {2}', + $CertificateDirectory, + $Endpoint, + $Port + ); + } else { + $arguments = [string]::Format('pki save-cert --key {0}{1}.key --trustedcert {0}trusted-parent.crt --host {2} --port {3}', + $CertificateDirectory, + $Hostname, + $Endpoint, + $Port + ); + } + + if ((Start-IcingaAgentCertificateProcess -Arguments $arguments) -eq $FALSE) { + Write-IcingaConsoleError -Message 'Unable to connect to your provided Icinga CA. Please verify the entered configuration is correct.' ` + 'If you are not able to connect to your Icinga CA from this machine, you will have to provide the path' ` + 'to your Icinga ca.crt and use the CA-Proxy certificate handling.'; + return $FALSE; + } + } + + if (-Not (Test-IcingaAgentCertificates -CertDirectory $CertificateDirectory -Hostname $Hostname -TestCACert -Force $Force)) { + [string]$PKIRequest = 'pki request --host {0} --port {1} --ticket {4} --key {2}{3}.key --cert {2}{3}.crt --trustedcert {2}trusted-parent.crt --ca {2}ca.crt'; + + if ([string]::IsNullOrEmpty($Ticket)) { + $PKIRequest = 'pki request --host {0} --port {1} --key {2}{3}.key --cert {2}{3}.crt --trustedcert {2}trusted-parent.crt --ca {2}ca.crt'; + } + + $arguments = [string]::Format($PKIRequest, + $Endpoint, + $Port, + $CertificateDirectory, + $Hostname, + $Ticket + ); + + if ((Start-IcingaAgentCertificateProcess -Arguments $arguments) -eq $FALSE) { + Write-IcingaConsoleError 'Failed to sign Icinga certificate'; + return $FALSE; + } + + if ([string]::IsNullOrEmpty($Ticket)) { + Write-IcingaConsoleNotice 'Your certificates were generated successfully. Please sign the certificate now on your Icinga CA master. You can lookup open requests with "icinga2 ca list"'; + } else { + Write-IcingaConsoleNotice 'Icinga certificates successfully installed'; + } + } + + return $TRUE; + } elseif (-Not [string]::IsNullOrEmpty($CACert)) { + if (-Not (Copy-IcingaAgentCACertificate -CAPath $CACert -Destination $CertificateDirectory)) { + return $FALSE; + } + Write-IcingaConsoleNotice 'Host-Certificates and ca.crt are present. Please start your Icinga Agent now and manually sign your certificate request on your CA master. You can lookup open requests with "icinga2 ca list"'; + } + + return $TRUE; +} + +function Start-IcingaAgentCertificateProcess() +{ + param( + $Arguments + ); + + $Binary = Get-IcingaAgentBinary; + $Process = Start-IcingaProcess -Executable $Binary -Arguments $Arguments; + + if ($Process.ExitCode -ne 0) { + Write-IcingaConsoleError ([string]::Format('Failed to create certificate.{0}Arguments: {1}{0}Error:{2} {3}', "`r`n", $Arguments, $Process.Message, $Process.Error)); + return $FALSE; + } + + Write-IcingaConsoleNotice $Process.Message; + return $TRUE; +} + +function Move-IcingaAgentCertificates() +{ + param( + [string]$Source, + [string]$Destination + ); + + $SourceDir = Join-Path -Path $Source -ChildPath '\*'; + $TargetDir = Join-Path -Path $Destination -ChildPath '\'; + + Move-Item -Path $SourceDir -Destination $TargetDir; +} + +function Test-IcingaAgentCertificates() +{ + param( + [string]$CertDirectory, + [string]$Hostname, + [switch]$TestCACert, + [switch]$TestTrustedParent, + [bool]$Force + ); + + if ($Force) { + return $FALSE; + } + + if ($TestCACert) { + if (Test-Path (Join-Path -Path $CertDirectory -ChildPath 'ca.crt')) { + Write-IcingaConsoleNotice 'Your ca.crt is present. No generation or fetching required'; + return $TRUE; + } else { + Write-IcingaConsoleWarning 'Your ca.crt is not present. Manually copy or fetching from your Icinga CA is required.'; + return $FALSE; + } + } + + if ($TestTrustedParent) { + if (Test-Path (Join-Path -Path $CertDirectory -ChildPath 'trusted-parent.crt')) { + Write-IcingaConsoleNotice 'Your trusted-parent.crt is present. No fetching or generation required'; + return $TRUE; + } else { + Write-IcingaConsoleWarning 'Your trusted master certificate is not present. Fetching from your CA server is required'; + return $FALSE; + } + } + + if ((-Not (Test-Path ((Join-Path -Path $CertDirectory -ChildPath $Hostname) + '.key'))) ` + -Or -Not (Test-Path ((Join-Path -Path $CertDirectory -ChildPath $Hostname) + '.crt'))) { + return $FALSE; + } + + [string]$hostCRT = [string]::Format('{0}.crt', $Hostname); + [string]$hostKEY = [string]::Format('{0}.key', $Hostname); + [bool]$CertNameInvalid = $FALSE; + + $certificates = Get-ChildItem -Path $CertDirectory; + # Now loop each file and match their name with our hostname + foreach ($cert in $certificates) { + if ($cert.Name.toLower() -eq $hostCRT.toLower() -Or $cert.Name.toLower() -eq $hostKEY.toLower()) { + $file = $cert.Name.Replace('.key', '').Replace('.crt', ''); + if (-Not ($file -clike $Hostname)) { + Write-IcingaConsoleWarning ([string]::Format('Certificate file {0} is not matching the hostname {1}. Certificate generation is required.', $cert.Name, $Hostname)); + $CertNameInvalid = $TRUE; + break; + } + } + } + + if ($CertNameInvalid) { + Remove-Item -Path (Join-Path -Path $CertDirectory -ChildPath $hostCRT) -Force; + Remove-Item -Path (Join-Path -Path $CertDirectory -ChildPath $hostKEY) -Force; + return $FALSE; + } + + Write-IcingaConsoleNotice 'Icinga host certificates are present and valid. No generation required'; + + return $TRUE; +} + +function Copy-IcingaAgentCACertificate() +{ + param( + [string]$CAPath, + [string]$Destination + ); + + # Copy ca.crt from local path or network share to certificate path + if ((Test-Path $CAPath)) { + Copy-Item -Path $CAPath -Destination (Join-Path -Path $Destination -ChildPath 'ca.crt') | Out-Null; + Write-IcingaConsoleNotice ([string]::Format('Copied ca.crt from "{0}" to "{1}', $CAPath, $Destination)); + } else { + Set-IcingaTLSVersion; + # It could also be a web resource + try { + $response = Invoke-IcingaWebRequest $CAPath -UseBasicParsing; + [int]$Index = $response.RawContent.IndexOf("`r`n`r`n") + 4; + + [string]$CAContent = $response.RawContent.SubString( + $Index, + $response.RawContent.Length - $Index + ); + Write-IcingaFileSecure -File (Join-Path $Destination -ChildPath 'ca.crt') -Value $CAContent; + Write-IcingaConsoleNotice ([string]::Format('Downloaded ca.crt from "{0}" to "{1}', $CAPath, $Destination)) + } catch { + Write-IcingaConsoleError 'Failed to load any provided ca.crt resource'; + return $FALSE; + } + } + + return $TRUE; +} + +Export-ModuleMember -Function @('Install-IcingaAgentCertificates'); +function Install-IcingaAgent() +{ + param( + [string]$Version, + [string]$Source = 'https://packages.icinga.com/windows/', + [string]$InstallDir = '', + [bool]$AllowUpdates = $FALSE + ); + + if ([string]::IsNullOrEmpty($Version)) { + Write-IcingaConsoleError 'No Icinga Agent version specified. Skipping installation.'; + return $FALSE; + } + + if ($IcingaData.Installed -eq $TRUE -and $AllowUpdates -eq $FALSE) { + Write-IcingaConsoleWarning 'The Icinga Agent is already installed on this system. To perform updates or downgrades, please add the "-AllowUpdates" argument'; + return $FALSE; + } + + $IcingaData = Get-IcingaAgentInstallation; + $InstalledVersion = Get-IcingaAgentVersion; + $IcingaInstaller = Get-IcingaAgentMSIPackage -Source $Source -Version $Version -SkipDownload; + $InstallTarget = $IcingaData.RootDir; + + if ($Version -eq 'snapshot') { + if ($IcingaData.InstallDate -ge $IcingaInstaller.LastUpdate -And [string]::IsNullOrEmpty($InstalledVersion.Snapshot) -eq $FALSE) { + Write-IcingaConsoleNotice 'There is no new snapshot package available which requires to be installed.' + return $FALSE; + } + $IcingaInstaller.Version = 'snapshot'; + } elseif ($IcingaInstaller.Version -eq $InstalledVersion.Full) { + Write-IcingaConsoleNotice ( + [string]::Format( + 'No installation required. Your installed version [{0}] is matching the online version [{1}]', + $InstalledVersion.Full, + $IcingaInstaller.Version + ) + ); + return $FALSE; + } + + $IcingaInstaller = Get-IcingaAgentMSIPackage -Source $Source -Version $IcingaInstaller.Version; + + if ((Test-Path $IcingaInstaller.InstallerPath) -eq $FALSE) { + throw 'Failed to locate Icinga Agent installer file'; + } + + if ([string]::IsNullOrEmpty($InstallDir) -eq $FALSE) { + if ((Test-Path $InstallDir) -eq $FALSE) { + New-Item -Path $InstallDir -ItemType Directory -Force | Out-Null; + } + $InstallTarget = $InstallDir; + } + + [string]$InstallFolderMsg = $InstallTarget; + + if ([string]::IsNullOrEmpty($InstallTarget) -eq $FALSE) { + $InstallTarget = [string]::Format(' INSTALL_ROOT="{0}"', $InstallTarget); + } else { + $InstallTarget = ''; + if ($IcingaData.Architecture -eq 'x86') { + $InstallFolderMsg = Join-Path -Path ${env:ProgramFiles(x86)} -ChildPath 'ICINGA2'; + } else { + $InstallFolderMsg = Join-Path -Path $env:ProgramFiles -ChildPath 'ICINGA2'; + } + } + + Write-IcingaConsoleNotice ([string]::Format('Installing new Icinga Agent version into "{0}"', $InstallFolderMsg)); + + if ($IcingaData.Installed) { + if ((Uninstall-IcingaAgent) -eq $FALSE) { + return $FALSE; + } + } + + $InstallProcess = & powershell.exe -Command { + Use-Icinga -Minimal; + + $IcingaInstaller = $args[0]; + $InstallTarget = $args[1]; + $InstallProcess = Start-IcingaProcess -Executable 'MsiExec.exe' -Arguments ([string]::Format('/quiet /norestart /i "{0}" {1}', $IcingaInstaller.InstallerPath, $InstallTarget)) -FlushNewLines; + + Start-Sleep -Seconds 2; + Optimize-IcingaForWindowsMemory; + + return $InstallProcess; + } -Args $IcingaInstaller, $InstallTarget; + + if ($InstallProcess.ExitCode -ne 0) { + Write-IcingaConsoleError -Message 'Failed to install Icinga 2 Agent: {0}{1}' -Objects $InstallProcess.Message, $InstallProcess.Error; + return $FALSE; + } + + Write-IcingaConsoleNotice 'Icinga Agent was successfully installed'; + return $TRUE; +} +<# +.SYNOPSIS + Repairs the Icinga Agent service installation in case the service is no longer present on the system + caused by update failures +.DESCRIPTION + Repairs the Icinga Agent service installation in case the service is no longer present on the system + caused by update failures +.PARAMETER RootFolder + Specifies the root folder of the Icinga Agent installation, in case a custom location is used for installation + Default locations are detected automatically +.EXAMPLE + PS> Repair-IcingaService; +.EXAMPLE + PS> Repair-IcingaService -RootFolder 'D:\Programs\icinga2'; +.NOTES + https://icinga.com/docs/icinga-for-windows/latest/doc/knowledgebase/IWKB000011/ +#> +function Repair-IcingaService() +{ + param ( + [string]$RootFolder = '' + ); + + if ($Global:Icinga.Protected.Environment.'Icinga Service'.Present) { + Write-IcingaConsoleNotice -Message 'The Icinga Agent service is already installed. If you received the error "The specified service has been marked for deletion", please have a look at https://icinga.com/docs/icinga-for-windows/latest/doc/knowledgebase/IWKB000011/' + return; + } + + [string]$IcingaBinaryPath = 'sbin\icinga2.exe'; + [string]$IcingaServicePath = ''; + + if ([string]::IsNullOrEmpty($RootFolder) -eq $FALSE) { + $IcingaServicePath = Join-Path -Path $RootFolder -ChildPath $IcingaBinaryPath; + + if ((Test-Path $IcingaServicePath) -eq $FALSE) { + Write-IcingaConsoleError ` + -Message 'The Icinga Agent could not be found at the location "{0}". Please specify only the Icinga Agent root path with "-RootFolder"' ` + -Objects $IcingaServicePath; + + return; + } + } else { + $IcingaServicePath = Join-Path -Path $Env:ProgramFiles -ChildPath ([string]::Format('ICINGA2\{0}', $IcingaBinaryPath)); + + if ((Test-Path $IcingaServicePath) -eq $FALSE) { + $IcingaServicePath = Join-Path -Path ${Env:ProgramFiles(x86)} -ChildPath ([string]::Format('ICINGA2\{0}', $IcingaBinaryPath)); + + if ((Test-Path $IcingaServicePath) -eq $FALSE) { + Write-IcingaConsoleError ` + -Message 'The Icinga Agent could not be found at the default locations "{0}" or "{1}". If you installed the Icinga Agent into a customer directory, please specify the root folder with "-RootFolder"' ` + -Objects $Env:ProgramFiles, ${Env:ProgramFiles(x86)}; + + return; + } + } + } + + Write-IcingaConsoleNotice ` + -Message 'Repairing Icinga Agent service with location "{0}"' ` + -Objects $IcingaServicePath; + + $IcingaServicePath = [string]::Format('\"{0}\" --scm \"daemon\"', $IcingaServicePath); + $IcingaService = Start-IcingaProcess -Executable 'sc.exe' -Arguments ([string]::Format('create icinga2 binPath= "{0}" DisplayName= "Icinga 2" start= auto', $IcingaServicePath)); + + if ($IcingaService.ExitCode -ne 0) { + Write-IcingaConsoleError ` + -Message 'Failed to install Icinga Agent service: {0}{1}' ` + -Objects $IcingaService.Message, $IcingaService.Error; + + return; + } + + $Global:Icinga.Protected.Environment.'Icinga Service'.Present = $TRUE; + + $IcingaData = Get-IcingaAgentInstallation; + $ConfigUser = Get-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser'; + $ServiceUser = $IcingaData.User; + + if ([string]::IsNullOrEmpty($ConfigUser) -eq $FALSE) { + $ServiceUser = $ConfigUser; + } + + Set-IcingaServiceUser -User $ServiceUser -SetPermission | Out-Null; + Update-IcingaServiceUser; + + Write-IcingaConsoleNotice -Message 'Icinga Agent service was successfully repaired. You can start it now with "Start-Service icinga2"'; +} +function Repair-IcingaStateFile() +{ + param ( + [switch]$Force + ); + + [string]$StateFilePath = Join-Path -Path $ENV:ProgramData -ChildPath 'icinga2\var\lib\icinga2\icinga2.state*'; + + if ((Test-IcingaStateFile) -And $Force -eq $FALSE) { + Write-IcingaConsoleNotice -Message 'The Icinga Agent state file seems to be okay'; + return; + } + + $Success = Remove-ItemSecure -Path $StateFilePath -Force -Retries 5; + + if ($Success) { + Write-IcingaConsoleNotice -Message 'The corrupted Icinga Agent State files have been removed'; + } else { + Write-IcingaConsoleError -Message 'Failed to remove the corrupted Icinga Agent state files'; + } +} +function Get-IcingaPerformanceCounterDetails() +{ + param ( + [string]$Counter = $null + ); + + [hashtable]$RetValue = @{ + 'RawCounter' = $Counter; + 'HasValue' = $TRUE; + 'HasInstance' = $FALSE; + 'Category' = ''; + 'Instance' = ''; + 'Counter' = ''; + 'CounterInstance' = ''; + } + + if ([string]::IsNullOrEmpty($Counter)) { + $RetValue.HasValue = $FALSE; + + return $RetValue; + } + + [array]$CounterElements = $Counter.Split('\'); + [string]$Instance = ''; + [string]$Category = ''; + [bool]$HasInstance = $FALSE; + + if ($CounterElements[1].Contains('(') -And $CounterElements[1].Contains(')')) { + $HasInstance = $TRUE; + [int]$StartIndex = $CounterElements[1].IndexOf('(') + 1; + [int]$EndIndex = $CounterElements[1].Length - $StartIndex - 1; + $Instance = $CounterElements[1].Substring($StartIndex, $EndIndex); + $RetValue.HasInstance = $HasInstance; + $Category = $CounterElements[1].Substring(0, $CounterElements[1].IndexOf('(')); + $RetValue.CounterInstance = [string]::Format('{0}_{1}', $Instance, $CounterElements[2]); + } else { + $Category = $CounterElements[1]; + } + + $RetValue.Category = $Category; + $RetValue.Instance = $Instance; + $RetValue.Counter = $CounterElements[2]; + + return $RetValue; +} +<# +.SYNOPSIS + Test if a certain Performance Counter category exist on the systems and returns + either true or false depending on the state +.DESCRIPTION + Test if a certain Performance Counter category exist on the systems and returns + either true or false depending on the state +.FUNCTIONALITY + Test if a certain Performance Counter category exist on the systems and returns + either true or false depending on the state +.EXAMPLE + PS>Test-IcingaPerformanceCounterCategory -Category 'Processor'; + + True +.PARAMETER Category + The name of the category to test for +.INPUTS + System.String +.OUTPUTS + System.Boolean +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Test-IcingaPerformanceCounterCategory() +{ + param ( + [string]$Category + ); + + if ([string]::IsNullOrEmpty($Category)) { + return $FALSE; + } + + try { + $Counter = New-Object System.Diagnostics.PerformanceCounterCategory($Category); + + if ($null -eq $Counter -Or [string]::IsNullOrEmpty($Counter.CategoryType)) { + return $FALSE; + } + } catch { + return $FALSE; + } + + return $TRUE; +} +<# +.SYNOPSIS + Will provide a virtual object, containing an array of Performance Counters. + The object has the following members: + * Name + * Value +.DESCRIPTION + Will provide a virtual object, containing an array of Performance Counters. + The object has the following members: + * Name + * Value +.FUNCTIONALITY + Will provide a virtual object, containing an array of Performance Counters. + The object has the following members: + * Name + * Value +.EXAMPLE + PS>New-IcingaPerformanceCounterResult -FullName '\Processor(*)\% processor time' -PerformanceCounters $PerformanceCounters; +.PARAMETER FullName + The full path to the Performance Counter +.PARAMETER PerformanceCounters + A list of all instances/counters for the given Performance Counter +.INPUTS + System.String +.OUTPUTS + System.PSObject +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function New-IcingaPerformanceCounterResult() +{ + param( + [string]$FullName = '', + [array]$PerformanceCounters = @() + ); + + $pc_instance = New-Object -TypeName PSObject; + $pc_instance | Add-Member -MemberType NoteProperty -Name 'FullName' -Value $FullName; + $pc_instance | Add-Member -MemberType NoteProperty -Name 'Counters' -Value $PerformanceCounters; + + $pc_instance | Add-Member -MemberType ScriptMethod -Name 'Name' -Value { + return $this.FullName; + } + + $pc_instance | Add-Member -MemberType ScriptMethod -Name 'Value' -Value { + [hashtable]$CounterResults = @{ }; + + foreach ($counter in $this.Counters) { + $CounterResults.Add($counter.Name(), $counter.Value()); + } + + return $CounterResults; + } + + return $pc_instance; +} +<# +.SYNOPSIS + Creates counter objects and sub-instances from a given Performance Counter + Will return either a New-IcingaPerformanceCounterObject or New-IcingaPerformanceCounterResult + which both contain the same members, allowing for dynamically use of objects +.DESCRIPTION + Creates counter objects and sub-instances from a given Performance Counter + Will return either a New-IcingaPerformanceCounterObject or New-IcingaPerformanceCounterResult + which both contain the same members, allowing for dynamically use of objects +.FUNCTIONALITY + Creates counter objects and sub-instances from a given Performance Counter + Will return either a New-IcingaPerformanceCounterObject or New-IcingaPerformanceCounterResult + which both contain the same members, allowing for dynamically use of objects +.EXAMPLE + PS>New-IcingaPerformanceCounter -Counter '\Processor(*)\% processor time'; + + FullName Counters + -------- -------- + \Processor(*)\% processor time {@{FullName=\Processor(2)\% processor time; Category=Processor; Instance=2; Counter=%... +.EXAMPLE + PS>New-IcingaPerformanceCounter -Counter '\Processor(*)\% processor time' -SkipWait; +.PARAMETER Counter + The path to the Performance Counter to fetch data for +.PARAMETER SkipWait + Set this if no sleep is intended for initialising the counter. This can be useful + if multiple counters are fetched during one call with this function if the sleep + is done afterwards manually. A sleep is set to 500ms to ensure counter data is + valid and contains an offset from previous/current values +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function New-IcingaPerformanceCounter() +{ + param( + [string]$Counter = '', + [boolean]$SkipWait = $FALSE + ); + + # Simply use the counter name, like + # \Paging File(_total)\% Usage + if ([string]::IsNullOrEmpty($Counter) -eq $TRUE) { + return (New-IcingaPerformanceCounterNullObject -FullName $Counter -ErrorMessage 'Failed to initialise counter, as no counter was specified.'); + } + + [array]$CounterArray = $Counter.Split('\'); + [string]$UseCounterCategory = ''; + [string]$UseCounterName = ''; + [string]$UseCounterInstance = ''; + + # If we add the counter as it should be + # \Paging File(_total)\% Usage + # the first array element will be an empty string we can skip + # Otherwise the name was wrong and we should not continue + if (-Not [string]::IsNullOrEmpty($CounterArray[0])) { + return (New-IcingaPerformanceCounterNullObject -FullName $Counter -ErrorMessage ([string]::Format('Failed to deserialize counter "{0}". It seems the leading "\" is missing.', $Counter))); + } + + # In case our Performance Counter is containing instances, we should split + # The content and read the instance and counter category out + if ($CounterArray[1].Contains('(')) { + [array]$TmpCounter = $CounterArray[1].Split('('); + $UseCounterCategory = $TmpCounter[0]; + $UseCounterInstance = $TmpCounter[1].Replace(')', ''); + } else { + # Otherwise we only require the category + $UseCounterCategory = $CounterArray[1]; + } + + # At last get the actual counter containing our values + $UseCounterName = $CounterArray[2]; + + # Now as we know how the counter path is constructed and has been split into + # the different values, we need to know how to handle the instances of the counter + + # If we specify a instance with (*) we want the module to automatically fetch all + # instances for this counter. This will result in an New-IcingaPerformanceCounterResult + # which contains the parent name including counters for all instances that + # have been found + if ($UseCounterInstance -eq '*') { + # In case we already loaded the counters once, return the finished array + $CachedCounter = Get-IcingaPerformanceCounterCacheItem -Counter $Counter; + + if ($null -ne $CachedCounter) { + return (New-IcingaPerformanceCounterResult -FullName $Counter -PerformanceCounters $CachedCounter); + } + + # If we need to build the array, load all instances from the counters and + # create single performance counters and add them to a custom array and + # later to a custom object + try { + [array]$AllCountersInstances = @(); + $CounterInstances = New-Object System.Diagnostics.PerformanceCounterCategory($UseCounterCategory); + foreach ($instance in $CounterInstances.GetInstanceNames()) { + [string]$NewCounterName = $Counter.Replace('*', $instance); + $NewCounter = New-IcingaPerformanceCounterObject -FullName $NewCounterName -Category $UseCounterCategory -Counter $UseCounterName -Instance $instance -SkipWait $TRUE; + $AllCountersInstances += $NewCounter; + } + } catch { + # Throw an exception in case our permissions are not enough to fetch performance counter + Exit-IcingaThrowException -InputString $_.Exception -StringPattern 'System.UnauthorizedAccessException' -ExceptionType 'Permission' -ExceptionThrown $IcingaExceptions.Permission.PerformanceCounter; + Exit-IcingaThrowException -InputString $_.Exception -StringPattern 'System.InvalidOperationException' -ExceptionType 'Input' -CustomMessage $Counter -ExceptionThrown $IcingaExceptions.Inputs.PerformanceCounter; + Exit-IcingaThrowException -InputString $_.Exception -StringPattern '' -ExceptionType 'Unhandled'; + # Shouldn't actually get down here anyways + return (New-IcingaPerformanceCounterNullObject -FullName $Counter -ErrorMessage ([string]::Format('Failed to deserialize instances for counter "{0}". Exception: "{1}".', $Counter, $_.Exception.Message))); + } + + # If we load multiple instances, we should add a global wait here instead of a wait for each single instance + # This will speed up CPU loading for example with plenty of cores available + if ($SkipWait -eq $FALSE) { + Start-Sleep -Milliseconds 500; + } + + # Add the parent counter including the array of Performance Counters to our + # caching mechanism and return the New-IcingaPerformanceCounterResult object for usage + # within the monitoring modules + Add-IcingaPerformanceCounterCache -Counter $Counter -Instances $AllCountersInstances; + return (New-IcingaPerformanceCounterResult -FullName $Counter -PerformanceCounters $AllCountersInstances); + } else { + # This part will handle the counters without any instances as well as + # specifically assigned instances, like (_Total) CPU usage. + + # In case we already have the counter within our cache, return the + # cached informations + $CachedCounter = Get-IcingaPerformanceCounterCacheItem -Counter $Counter; + + if ($null -ne $CachedCounter) { + return $CachedCounter; + } + + # If the cache is not present yet, create the Performance Counter object, + # and add it to our cache + $NewCounter = New-IcingaPerformanceCounterObject -FullName $Counter -Category $UseCounterCategory -Counter $UseCounterName -Instance $UseCounterInstance -SkipWait $SkipWait; + Add-IcingaPerformanceCounterCache -Counter $Counter -Instances $NewCounter; + } + + # This function will always return non-instance counters or + # specifically defined instance counters. Performance Counter Arrays + # are returned within their function. This is just to ensure that the + # function looks finished from developer point of view + return (Get-IcingaPerformanceCounterCacheItem -Counter $Counter); +} +<# +.SYNOPSIS + Displays all available instances for a provided Performance Counter +.DESCRIPTION + Displays all available instances for a provided Performance Counter +.FUNCTIONALITY + Displays all available instances for a provided Performance Counter +.PARAMETER Counter + The name of the Performance Counter to fetch data for +.EXAMPLE + PS>Show-IcingaPerformanceCounterInstances -Counter '\Processor(*)\% processor time'; + + Name Value + ---- ----- + _Total \Processor(_Total)\% processor time + 0 \Processor(0)\% processor time + 1 \Processor(1)\% processor time + 2 \Processor(2)\% processor time + 3 \Processor(3)\% processor time + ... +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Show-IcingaPerformanceCounterInstances() +{ + param ( + [string]$Counter + ); + + [hashtable]$Instances = @{ }; + + if ([string]::IsNullOrEmpty($Counter)) { + Write-IcingaConsoleError 'Please enter a Performance Counter'; + return; + } + + $PerfCounter = New-IcingaPerformanceCounter -Counter $Counter -SkipWait $TRUE; + + foreach ($entry in $PerfCounter.Counters) { + $Instances.Add( + $entry.Instance, + ($Counter.Replace('(*)', ([string]::Format('({0})', $entry.Instance)))) + ); + } + + if ($Instances.Count -eq 0) { + Write-IcingaConsoleNotice ` + -Message 'No instances were found for Performance Counter "{0}". Please ensure the provided counter has instances and you are using "*" for the instance name.' ` + -Objects $Counter; + + return; + } + + return ( + $Instances.GetEnumerator() | Sort-Object Name + ); +} +<# +.SYNOPSIS + Will use an array of provided Performance Counter and sort the input by + a given counter category. In this case we can fetch all Processor instances + and receive values for each core which can then be accessed from a hashtable + with an ready query. Allows to modify output in addition +.DESCRIPTION + Will use an array of provided Performance Counter and sort the input by + a given counter category. In this case we can fetch all Processor instances + and receive values for each core which can then be accessed from a hashtable + with an ready query. Allows to modify output in addition +.FUNCTIONALITY + Will use an array of provided Performance Counter and sort the input by + a given counter category. In this case we can fetch all Processor instances + and receive values for each core which can then be accessed from a hashtable + with an ready query. Allows to modify output in addition +.EXAMPLE + PS>New-IcingaPerformanceCounterStructure -CounterCategory 'Processor' -PerformanceCounterHash (New-IcingaPerformanceCounterArray '\Processor(*)\% processor time'); + + Name Value + ---- ----- + 7 {% processor time} + 3 {% processor time} + 4 {% processor time} + _Total {% processor time} + 2 {% processor time} + 1 {% processor time} + 0 {% processor time} + 6 {% processor time} + 5 {% processor time} +.EXAMPLE + PS>New-IcingaPerformanceCounterStructure -CounterCategory 'Processor' -PerformanceCounterHash (New-IcingaPerformanceCounterArray '\Processor(*)\% processor time') -InstanceNameCleanupArray '_'; + + Name Value + ---- ----- + 7 {% processor time} + Total {} + 3 {% processor time} + 4 {% processor time} + 2 {% processor time} + 1 {% processor time} + 0 {% processor time} + 6 {% processor time} + 5 {% processor time} +.PARAMETER CounterCategory + The name of the category the sort algorithm will fetch the instances from for sorting +.PARAMETER PerformanceCounterHash + An array of Performance Counter objects provided by 'New-IcingaPerformanceCounterArray' to sort for +.PARAMETER InstanceNameCleanupArray + An array which will be used to remove string content from the sorted instances keys. For example '_' will change + '_Total' to 'Total'. Replacements are done in the order added to this array +.INPUTS + System.String +.OUTPUTS + System.Hashtable +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> +function New-IcingaPerformanceCounterStructure() +{ + param( + [string]$CounterCategory = '', + [hashtable]$PerformanceCounterHash = @{ }, + [array]$InstanceNameCleanupArray = @() + ) + + # The storage variables we require to store our data + [array]$AvailableInstances = @(); + [hashtable]$StructuredCounterData = @{ }; + + # With this little trick we can fetch all instances we have and get their unique name + $CounterInstances = New-Object System.Diagnostics.PerformanceCounterCategory($CounterCategory); + foreach ($instance in $CounterInstances.GetInstanceNames()) { + # For some counters we require to apply a 'cleanup' for the instance name + # Example Disks: Some disks are stored with the name + # 'HarddiskVolume1' + # To be able to map the volume correctly to disks, we require to remove + # 'HarddiskVolume' so only '1' will remain, which allows us to map the + # volume correctly afterwards + [string]$CleanInstanceName = $instance; + foreach ($cleanup in $InstanceNameCleanupArray) { + $CleanInstanceName = $CleanInstanceName.Replace($cleanup, ''); + } + $AvailableInstances += $CleanInstanceName; + } + + # Now let the real magic begin. + + # At first we will loop all instances of our Performance Counters, which means all + # instances we have found above. We build a new hashtable then to list the instances + # by their individual name and all corresponding counters as children + # This allows us a structured output with all data for each instance + foreach ($instance in $AvailableInstances) { + + # First build a hashtable for each instance to add data to later + $StructuredCounterData.Add($instance, @{ }); + + # Now we need to loop all return values from our Performance Counters + foreach ($InterfaceCounter in $PerformanceCounterHash.Keys) { + # As we just looped the parent counter (Instance *), we now need to + # loop the actual counters for each instance + foreach ($interface in $PerformanceCounterHash[$InterfaceCounter]) { + # Finally let's loop through all the results which contain the values + # to build our new, structured hashtable + foreach ($entry in $interface.Keys) { + # Match the counters based on our current parent index + # (the instance name we want to add the values as children). + if ($entry.Contains('(' + $instance + ')')) { + # To ensure we don't transmit the entire counter name, + # we only want to include the name of the actual counter. + # There is no need to return + # \Network Interface(Desktopadapter Intel[R] Gigabit CT)\Bytes Received/sec + # the naming + # Bytes Received/sec + # is enough + [array]$TmpOutput = $entry.Split('\'); + [string]$OutputName = $TmpOutput[$TmpOutput.Count - 1]; + + # Now add the actual value to our parent instance with the + # improved value name, including the sample and counter value data + $StructuredCounterData[$instance].Add($OutputName, $interface[$entry]); + } + } + } + } + } + + return $StructuredCounterData; +} +<# +.SYNOPSIS + This will create a Performance Counter object in case a counter instance + does not exist, but still returning default members to allow us to smoothly + execute our code +.DESCRIPTION + This will create a Performance Counter object in case a counter instance + does not exist, but still returning default members to allow us to smoothly + execute our code +.FUNCTIONALITY + This will create a Performance Counter object in case a counter instance + does not exist, but still returning default members to allow us to smoothly + execute our code +.EXAMPLE + PS>New-IcingaPerformanceCounterNullObject '\Processor(20)\%processor time' -ErrorMessage 'This counter with instance 20 does not exist'; + + FullName ErrorMessage + -------- ------------ + \Processor(20)\%processor time This counter with instance 20 does not exist +.PARAMETER FullName + The full path/name of the Performance Counter which does not exist +.PARAMETER ErrorMessage + The error message which is included within the 'error' member of the Performance Counter +.INPUTS + System.String +.OUTPUTS + System.PSObject +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function New-IcingaPerformanceCounterNullObject() +{ + param( + [string]$FullName = '', + [string]$ErrorMessage = '' + ); + + $pc_instance = New-Object -TypeName PSObject; + $pc_instance | Add-Member -MemberType NoteProperty -Name 'FullName' -Value $FullName; + $pc_instance | Add-Member -MemberType NoteProperty -Name 'ErrorMessage' -Value $ErrorMessage; + + $pc_instance | Add-Member -MemberType ScriptMethod -Name 'Name' -Value { + return $this.FullName; + } + + $pc_instance | Add-Member -MemberType ScriptMethod -Name 'Value' -Value { + [hashtable]$ErrorMessage = @{ }; + + $ErrorMessage.Add('value', $null); + $ErrorMessage.Add('sample', $null); + $ErrorMessage.Add('help', $null); + $ErrorMessage.Add('type', $null); + $ErrorMessage.Add('error', $this.ErrorMessage); + + return $ErrorMessage; + } + + return $pc_instance; +} +<# +.SYNOPSIS + Creates a new Performance Counter object based on given input filters. + Returns a PSObject with custom members to access the data of the counter +.DESCRIPTION + Creates a new Performance Counter object based on given input filters. + Returns a PSObject with custom members to access the data of the counter +.FUNCTIONALITY + Creates a new Performance Counter object based on given input filters. + Returns a PSObject with custom members to access the data of the counter +.EXAMPLE + PS>New-IcingaPerformanceCounterObject -FullName '\Processor(*)\% processor time' -Category 'Processor' -Instance '*' -Counter '% processor time'; + + Category : Processor + Instance : * + Counter : % processor time + PerfCounter : System.Diagnostics.PerformanceCounter + SkipWait : False +.EXAMPLE + PS>New-IcingaPerformanceCounterObject -FullName '\Processor(*)\% processor time' -Category 'Processor' -Instance '*' -Counter '% processor time' -SkipWait; + + Category : Processor + Instance : * + Counter : % processor time + PerfCounter : System.Diagnostics.PerformanceCounter + SkipWait : True +.PARAMETER FullName + The full path to the Performance Counter +.PARAMETER Category + The name of the category of the Performance Counter +.PARAMETER Instance + The instance of the Performance Counter +.PARAMETER Counter + The actual name of the counter to fetch +.PARAMETER SkipWait + Set this if no sleep is intended for initialising the counter. This can be useful + if multiple counters are fetched during one call with this function if the sleep + is done afterwards manually. A sleep is set to 500ms to ensure counter data is + valid and contains an offset from previous/current values +.INPUTS + System.String +.OUTPUTS + System.PSObject +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function New-IcingaPerformanceCounterObject() +{ + param( + [string]$FullName = '', + [string]$Category = '', + [string]$Instance = '', + [string]$Counter = '', + [boolean]$SkipWait = $FALSE + ); + + $pc_instance = New-Object -TypeName PSObject; + $pc_instance | Add-Member -MemberType NoteProperty -Name 'FullName' -Value $FullName; + $pc_instance | Add-Member -MemberType NoteProperty -Name 'Category' -Value $Category; + $pc_instance | Add-Member -MemberType NoteProperty -Name 'Instance' -Value $Instance; + $pc_instance | Add-Member -MemberType NoteProperty -Name 'Counter' -Value $Counter; + $pc_instance | Add-Member -MemberType NoteProperty -Name 'PerfCounter' -Value $Counter; + $pc_instance | Add-Member -MemberType NoteProperty -Name 'SkipWait' -Value $SkipWait; + + $pc_instance | Add-Member -MemberType ScriptMethod -Name 'Init' -Value { + + Write-IcingaConsoleDebug ` + -Message 'Creating new Counter for Category "{0}" with Instance "{1}" and Counter "{2}". Full Name "{3}"' ` + -Objects $this.Category, $this.Instance, $this.Counter, $this.FullName; + + # Create the Performance Counter object we want to access + $this.PerfCounter = New-Object System.Diagnostics.PerformanceCounter; + $this.PerfCounter.CategoryName = $this.Category; + $this.PerfCounter.CounterName = $this.Counter; + + # Only add an instance in case it is defined + if ([string]::IsNullOrEmpty($this.Instance) -eq $FALSE) { + $this.PerfCounter.InstanceName = $this.Instance + } + + # Initialise the counter + try { + $this.PerfCounter.NextValue() | Out-Null; + $this.PerfCounter.NextSample() | Out-Null; + } catch { + # Nothing to do here, will be handled later + } + + <# + # For some counters we require to wait a small amount of time to receive proper data + # Other counters do not need these informations and we do also not require to wait + # for every counter we use, once the counter is initialised within our environment. + # This will allow us to skip the sleep to speed up loading counters + #> + if ($this.SkipWait -eq $FALSE) { + Start-Sleep -Milliseconds 500; + } + } + + # Return the name of the counter as string + $pc_instance | Add-Member -MemberType ScriptMethod -Name 'Name' -Value { + return $this.FullName; + } + + <# + # Return a hashtable containing the counter value including the + # Sample values for the counter itself. In case we run into an error, + # keep the counter construct but add an error message in addition. + #> + $pc_instance | Add-Member -MemberType ScriptMethod -Name 'Value' -Value { + [hashtable]$CounterData = @{ }; + + $CounterValue = $this.PerfCounter.NextValue(); + + if ($null -eq $CounterValue) { + $CounterValue = 0; + } + + try { + [string]$CounterType = $this.PerfCounter.CounterType; + $CounterData.Add('value', ([math]::Round([decimal]$CounterValue, 6))); + $CounterData.Add('sample', $this.PerfCounter.NextSample()); + $CounterData.Add('help', $this.PerfCounter.CounterHelp); + $CounterData.Add('type', $CounterType); + $CounterData.Add('error', $null); + } catch { + $CounterData = @{ }; + $CounterData.Add('value', 0); # Set the value to 0 in case of an error + $CounterData.Add('sample', 0); + $CounterData.Add('help', $null); + $CounterData.Add('type', $null); + $CounterData.Add('error', $_.Exception.Message); + } + + return $CounterData; + } + + # Initialise the entire counter and internal handlers + $pc_instance.Init(); + + # Return this custom object + return $pc_instance; +} +<# +.SYNOPSIS + Prints a list of all available Performance Counters for a specified category +.DESCRIPTION + Prints a list of all available Performance Counters for a specified category +.FUNCTIONALITY + Prints a list of all available Performance Counters for a specified category +.EXAMPLE + PS>Show-IcingaPerformanceCounters -CounterCategory 'Processor'; + + \Processor(*)\dpcs queued/sec + \Processor(*)\% c1 time + \Processor(*)\% idle time + \Processor(*)\c3 transitions/sec + \Processor(*)\% c2 time + \Processor(*)\% dpc time + \Processor(*)\% privileged time +.PARAMETER CounterCategory + The name of the category to fetch available counters for +.INPUTS + System.String +.OUTPUTS + System.Array +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> +function Show-IcingaPerformanceCounters() +{ + param ( + [string]$CounterCategory + ); + + [hashtable]$counters = @{ }; + + if ([string]::IsNullOrEmpty($CounterCategory)) { + $counters.Add('error', 'Please specify a counter category'); + return $counters; + } + + try { + # At first create our Performance Counter object for the category we specified + $Category = New-Object System.Diagnostics.PerformanceCounterCategory($CounterCategory); + + # Now loop through all keys to find the name of available counters + foreach ($counter in $Category.ReadCategory().Keys) { + [string]$CounterInstanceAddition = ''; + + # As counters might also have instances (like interfaces, disks, paging file), we should + # try to load them as well + foreach ($instance in $Category.ReadCategory()[$counter].Keys) { + # If we do not match this magic string, we have multiple instances we can access + # to get informations for different disks, volumes and interfaces for example + if ($instance -ne 'systemdiagnosticsperfcounterlibsingleinstance') { + # Re-Write the name we return of the counter to something we can use directly + # within our modules to load data from. A returned counter will look like this + # for example: + # \PhysicalDisk(*)\avg. disk bytes/read + [string]$UsableCounterName = [string]::Format('\{0}(*)\{1}', $CounterCategory, $counter); + if ($counters.ContainsKey($UsableCounterName) -eq $TRUE) { + $counters[$UsableCounterName] += $Category.ReadCategory()[$counter][$instance]; + } else { + $counters.Add($UsableCounterName, @( $Category.ReadCategory()[$counter][$instance] )); + } + } else { + # For counters with no instances, we still require to return a re-build Performance Counter + # output, to make later usage in our modules very easy. This can look like this: + # \System\system up time + [string]$UsableCounterName = [string]::Format('\{0}\{1}', $CounterCategory, $counter); + $counters.Add($UsableCounterName, $null); + } + } + }; + } catch { + # In case we run into an error, return an error message + $counters.Add('error', $_.Exception.Message); + } + + return $counters.Keys; +} +<# +.SYNOPSIS + Accepts a list of Performance Counters which will all be fetched at once and + returned as a hashtable object. No additional configuration is required. +.DESCRIPTION + Accepts a list of Performance Counters which will all be fetched at once and + returned as a hashtable object. No additional configuration is required. +.FUNCTIONALITY + Accepts a list of Performance Counters which will all be fetched at once and + returned as a hashtable object. No additional configuration is required. +.EXAMPLE + PS>New-IcingaPerformanceCounterArray -CounterArray '\Processor(*)\% processor time', '\Memory\committed bytes'; + + Name Value + ---- ----- + \Processor(*)\% processor time {\Processor(7)\% processor time, \Processor(6)\% processor time, \Processor(0)\% proc... + \Memory\committed bytes {error, sample, type, value...} +.PARAMETER CounterArray + An array of Performance Counters which will all be fetched at once +.INPUTS + System.String +.OUTPUTS + System.Hashtable +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function New-IcingaPerformanceCounterArray() +{ + param( + [array]$CounterArray = @() + ) + + [hashtable]$CounterResult = @{ }; + [bool]$RequireSleep = $TRUE; + foreach ($counter in $CounterArray) { + # We want to speed up things with loading, so we will check if a specified + # Counter is already cached within our hashtable. If it is not, we sleep + # at the end of the function the required 500ms and don't have to wait + # NumOfCounters * 500 milliseconds for the first runs. This will speed + # up the general loading of counters and will not require some fancy + # pre-caching / configuration handler + $CachedCounter = Get-IcingaPerformanceCounterCacheItem -Counter $Counter; + + # Remove this for now to ensure our CPU metrics will not be cached + # and represent correct values, not exceeding 200% and beyond + #if ($null -ne $CachedCounter) { + # $RequireSleep = $FALSE; + #} + + $obj = New-IcingaPerformanceCounter -Counter $counter -SkipWait $TRUE; + if ($CounterResult.ContainsKey($obj.Name()) -eq $FALSE) { + $CounterResult.Add($obj.Name(), $obj.Value()); + } + } + + # TODO: Add a cache for our Performance Counters to only fetch them once + # for each session to speed up the loading. This cold be something like + # this: + # + # $Global:Icinga.Private.PerformanceCounter.Cache += $CounterResult; + + # Above we initialise ever single counter and we only require a sleep once + # in case a new, yet unknown counter was added + if ($RequireSleep) { + Start-Sleep -Milliseconds 500; + + # Agreed, this is some sort of code duplication but it wouldn't make + # any sense to create a own function for this. Why are we doing + # this anyway? + # Simple: In case we found counters which have yet not been initialised + # we did this above. Now we have waited 500 ms to receive proper + # values from these counters. As the previous generated result + # might have contained counters with 0 results, we will now + # check all counters again to receive the proper values. + # Agreed, might sound like a overhead, but the impact only + # applies to the first call of the module with the counters. + # This 'duplication' however decreased the execution from + # certain modules from 25s to 1s on the first run. Every + # additional run is then being executed within 0.x s + # which sounds like a very good performance and solution + $CounterResult = @{ }; + foreach ($counter in $CounterArray) { + $obj = New-IcingaPerformanceCounter -Counter $counter -SkipWait $TRUE; + if ($CounterResult.ContainsKey($obj.Name()) -eq $FALSE) { + $CounterResult.Add($obj.Name(), $obj.Value()); + } + } + } + + return $CounterResult; +} +<# +.SYNOPSIS + Prints the description of a Performance Counter if available on the system +.DESCRIPTION + Prints the description of a Performance Counter if available on the system +.FUNCTIONALITY + Prints the description of a Performance Counter if available on the system +.EXAMPLE + PS>Show-IcingaPerformanceCounterHelp '\Processor(*)\% processor time'; + + % Processor Time is the percentage of elapsed time that the processor spends to execute a non-Idle thread. It is calculated by measuring the percentage of time that the processor spends executing the idle thread and then subtracting that value from 100%. (Each processor has an idle thread that consumes cycles when no other threads are ready to run). This counter is the primary indicator of processor activity, and displays the average percentage of busy time observed during the sample interval. It should be noted that the accounting calculation of whether the processor is idle is performed at an internal sampling interval of the system clock (10ms). On todays fast processors, % Processor Time can therefore underestimate the processor utilization as the processor may be spending a lot of time servicing threads between the system clock sampling interval. Workload based timer applications are one example of applications which are more likely to be measured inaccurately as timers are signaled just after the sample is taken. +.EXAMPLE + PS>Show-IcingaPerformanceCounterHelp '\Memory\system code total bytes'; + + System Code Total Bytes is the size, in bytes, of the pageable operating system code currently mapped into the system virtual address space. This value is calculated by summing the bytes in Ntoskrnl.exe, Hal.dll, the boot drivers, and file systems loaded by Ntldr/osloader. This counter does not include code that must remain in physical memory and cannot be written to disk. This counter displays the last observed value only; it is not an average. +.PARAMETER Counter + The full path to the Performance Counter to lookup. +.INPUTS + System.String +.OUTPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> +function Show-IcingaPerformanceCounterHelp() +{ + param ( + [string]$Counter = '' + ); + + if ([string]::IsNullOrEmpty($Counter)) { + Write-IcingaConsoleError 'Please enter a Performance Counter'; + return; + } + + # Create a Performance Counter array which is easier to access later on and skip possible waits + $PerfCounter = New-IcingaPerformanceCounterArray -Counter $Counter -SkipWait $TRUE; + [string]$HelpText = ''; + [string]$ErrorText = ''; + + if ($PerfCounter.ContainsKey($Counter)) { + # Handle counters without instances, like '\Memory\system code total bytes' + $HelpText = $PerfCounter[$Counter].help; + $ErrorText = $PerfCounter[$Counter].error; + + if ([string]::IsNullOrEmpty($HelpText)) { + # Handle counters with instances, like '\Processor(*)\% processor time' + $CounterObject = $PerfCounter[$Counter].GetEnumerator() | Select-Object -First 1; + $CounterData = $CounterObject.Value; + $HelpText = $CounterData.help; + if ([string]::IsNullOrEmpty($ErrorText)) { + $ErrorText = $CounterData.error; + } + } + } + + if ([string]::IsNullOrEmpty($HelpText) -eq $FALSE) { + return $HelpText; + } + + Write-IcingaConsoleError ` + -Message 'Help context for the Performance Counter "{0}" could not be loaded or was not found. Error context if available: "{1}"' ` + -Objects $Counter, $ErrorText; +} +<# +.SYNOPSIS + Adds counter instances or single counter objects to an internal cache + by a given counter name or full path +.DESCRIPTION + Adds counter instances or single counter objects to an internal cache + by a given counter name or full path +.FUNCTIONALITY + Adds counter instances or single counter objects to an internal cache + by a given counter name or full path +.EXAMPLE + PS>Add-IcingaPerformanceCounterCache -Counter '\Processor(*)\% processor time' -Instances $CounterInstances; +.PARAMETER Counter + The path to the counter to store data for +.PARAMETER Instances + The value to store for a specific path to a counter +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Add-IcingaPerformanceCounterCache() +{ + param ( + $Counter, + $Instances + ); + + if ($Global:Icinga.Private.PerformanceCounter.Cache.ContainsKey($Counter)) { + $Global:Icinga.Private.PerformanceCounter.Cache[$Counter] = $Instances; + } else { + $Global:Icinga.Private.PerformanceCounter.Cache.Add( + $Counter, $Instances + ); + } +} +<# +.SYNOPSIS + Fetches all available Performance Counter caregories on the system by using the + registry and returns the entire content as array. Allows to filter for certain + categories only +.DESCRIPTION + Fetches all available Performance Counter caregories on the system by using the + registry and returns the entire content as array. Allows to filter for certain + categories only +.FUNCTIONALITY + Fetches all available Performance Counter caregories on the system by using the + registry and returns the entire content as array. Allows to filter for certain + categories only +.EXAMPLE + PS>Show-IcingaPerformanceCounterCategories; + + System + Memory + Browser + Cache + Process + Thread + PhysicalDisk + ... +.EXAMPLE + PS>Show-IcingaPerformanceCounterCategories -Filter 'Processor'; + + Processor +.EXAMPLE + PS>Show-IcingaPerformanceCounterCategories -Filter 'Processor', 'Memory'; + + Memory + Processor +.PARAMETER Filter + A array of counter categories to filter for. Supports wildcard search +.INPUTS + System.String +.OUTPUTS + System.Array +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Show-IcingaPerformanceCounterCategories() +{ + param ( + [array]$Filter = @() + ); + + [array]$Counters = @(); + [array]$FilteredCounters = @(); + # Load our cache if it does exist yet + $PerfCounterCache = Get-IcingaPerformanceCounterCacheItem 'Icinga:CachedCounterList'; + + # Create a cache for all available performance counter categories on the system + if ($null -eq $PerfCounterCache -or $PerfCounterCache.Count -eq 0) { + # Fetch the categories from the registry + $PerfCounterCache = Get-ItemProperty ` + -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\009' ` + -Name 'counter' | Select-Object -ExpandProperty Counter; + + # Now lets loop our registry data and fetch only for counter categories + # Ignore everything else and drop the information + foreach ($counter in $PerfCounterCache) { + # First filter out the ID's of the performance counter + if (-Not ($counter -match "^[\d\.]+$") -And [string]::IsNullOrEmpty($counter) -eq $FALSE) { + # Now check if the value we got is a counter category + if ([System.Diagnostics.PerformanceCounterCategory]::Exists($counter) -eq $TRUE) { + $Counters += $counter; + } + } + } + + # Set our cache to the current list of categories + Add-IcingaPerformanceCounterCache -Counter 'Icinga:CachedCounterList' -Instances $Counters; + $PerfCounterCache = $Counters; + } + + # In case we have no filter applied, simply return the entire list + if ($Filter.Count -eq 0) { + return $PerfCounterCache; + } + + # In case we do, check each counter category against our filter element + foreach ($counter in $PerfCounterCache) { + foreach ($element in $Filter) { + if ($counter -like $element) { + $FilteredCounters += $counter; + } + } + } + + return $FilteredCounters; +} +<# +.SYNOPSIS + Fetches stored data for a given performance counter path. Returns + $null if no values are assigned +.DESCRIPTION + Fetches stored data for a given performance counter path. Returns + $null if no values are assigned +.FUNCTIONALITY + Fetches stored data for a given performance counter path. Returns + $null if no values are assigned +.EXAMPLE + PS>Get-IcingaPerformanceCounterCacheItem -Counter '\Processor(*)\% processor time'; +.PARAMETER Counter + The path to the counter to fetch data for +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaPerformanceCounterCacheItem() +{ + param ( + $Counter + ); + + if ([string]::IsNullOrEmpty($Counter)) { + return $null; + } + + if ($Global:Icinga.Private.PerformanceCounter.Cache.ContainsKey($Counter)) { + return $Global:Icinga.Private.PerformanceCounter.Cache[$Counter]; + } + + return $null; +} +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)); + $TaskSettings = New-ScheduledTaskSettingsSet -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries -StartWhenAvailable; + # We need to schedule this task as LocalSystem to ensure we can fetch the information while connected over WinRM/SSH + # We require high admin privilleges anyway, therefor this shouldn't hurt + Register-ScheduledTask -User 'System' -TaskName $TaskName -Action $WinAction -TaskPath $TaskPath -Settings $TaskSettings -Force | Out-Null; + + Start-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath; + + Wait-IcingaWindowsScheduledTask; + + [string]$TaskOutput = Read-IcingaFileSecure -File $TmpFile; + $TaskData = ConvertFrom-Json $TaskOutput; + + return $TaskData; +} +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 -User 'System' -TaskName $TaskName -Action $WinAction -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-IcingaForWindows; }}', "'", $TmpFile.FullName, $FilePath)) + Register-ScheduledTask -User 'System' -TaskName $TaskName -Action $WinAction -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; +} +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 -ErrorAction SilentlyContinue; + + if ($null -eq $TaskStatus) { + return; + } + + 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; + } +} +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; +} +function Register-IcingaWindowsScheduledTaskProcessPriority() +{ + param ( + [switch]$Force = $FALSE + ); + + [string]$TaskName = 'Set Process Priority'; + [string]$TaskPath = '\Icinga\Icinga for Windows\'; + + $SetProcessPriorityTask = Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction SilentlyContinue; + + if ($null -ne $SetProcessPriorityTask -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\SetProcessPriority.ps1'; + $TaskAction = New-ScheduledTaskAction -Execute 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -Argument ([string]::Format("-WindowStyle Hidden -Command &{{ & '{0}' }}", $ScriptPath)); + $TaskPrincipal = New-ScheduledTaskPrincipal -UserId 'S-1-5-18' -RunLevel 'Highest' -LogonType ServiceAccount; + $TaskSettings = New-ScheduledTaskSettingsSet -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries -StartWhenAvailable; + + Register-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -Force -Principal $TaskPrincipal -Action $TaskAction -Settings $TaskSettings | Out-Null; + + Write-IcingaConsoleNotice -Message 'The task "{0}" has been successfully registered at location "{1}".' -Objects $TaskName, $TaskPath; +} +function Start-IcingaWindowsScheduledTaskProcessPriority() +{ + [string]$TaskName = 'Set Process Priority'; + [string]$TaskPath = '\Icinga\Icinga for Windows\'; + + $SetProcessPriorityTask = Get-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -ErrorAction SilentlyContinue; + + if ($null -eq $SetProcessPriorityTask) { + Write-IcingaConsoleNotice -Message 'The "{0}" task is not present on this system.' -Objects $TaskName; + return; + } + + Start-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath; +} +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; +} +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 -UserId 'S-1-5-18' -RunLevel 'Highest' -LogonType ServiceAccount; + $TaskSettings = New-ScheduledTaskSettingsSet -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries -StartWhenAvailable; + + Register-ScheduledTask -TaskName $TaskName -TaskPath $TaskPath -Force -Principal $TaskPrincipal -Action $TaskAction -Trigger $TaskTrigger -Settings $TaskSettings | Out-Null; + + Write-IcingaConsoleNotice -Message 'The task "{0}" has been successfully registered at location "{1}".' -Objects $TaskName, $TaskPath; +} +function Write-IcingaForWindowsComponentCompilationFile() +{ + param ( + [string]$ScriptRootPath = '', + [string]$CompiledFilePath = '' + ); + + # Store our current shell location + [string]$OldLocation = Get-Location; + # Get the current location and leave this folder + Set-Location -Path $ScriptRootPath; + Set-Location -Path '..'; + + # Store the location of the current file + + # Now as we are inside the module root, get the name of the module and the path + [string]$ModulePath = Get-Location; + [string]$ModuleName = $ModulePath.Split('\')[-1]; + + # Fetch all '.psm1' files from this module content + [array]$ModuleFiles = Get-ChildItem -Path $ModulePath -Recurse -Filter '*.psm1'; + # Get all public functions + [array]$FunctionList = @(); + [array]$VariableList = @(); + [array]$AliasList = @(); + [array]$CmdletList = @(); + # Variable to store all of our module files + [string]$CompiledModule = ''; + + foreach ($entry in $ModuleFiles) { + # Ensure the compilation file never includes itself + if ($entry.FullName -eq $CompiledFilePath) { + continue; + } + + $FunctionList += Get-IcingaForWindowsComponentPublicFunctions -FileObject $entry -ModuleName $ModuleName; + $FileConfig = (Read-IcingaPowerShellModuleFile -File $entry.FullName); + $VariableList += $FileConfig.VariableList; + $AliasList += $FileConfig.AliasList; + $CmdletList += $FileConfig.ExportCmdlet; + $CompiledModule += (Get-Content -Path $entry.FullName -Raw -Encoding 'UTF8'); + $CompiledModule += "`r`n"; + } + + if ((Test-Path -Path $CompiledFilePath) -eq $FALSE) { + New-Item -Path $CompiledFilePath -ItemType File -Force | Out-Null; + } + + $CompiledModule += "`r`n"; + $CompiledModule += [string]::Format( + "Export-ModuleMember -Cmdlet @( {0} ) -Function @( {1} ) -Variable @( {2} ) -Alias @( {3} );", + ((ConvertFrom-IcingaArrayToString -Array ($CmdletList | Select-Object -Unique) -AddQuotes -UseSingleQuotes)), + ((ConvertFrom-IcingaArrayToString -Array ($FunctionList | Select-Object -Unique) -AddQuotes -UseSingleQuotes)), + ((ConvertFrom-IcingaArrayToString -Array ($VariableList | Select-Object -Unique) -AddQuotes -UseSingleQuotes)), + ((ConvertFrom-IcingaArrayToString -Array ($AliasList | Select-Object -Unique) -AddQuotes -UseSingleQuotes)) + ); + + Set-Content -Path $CompiledFilePath -Value $CompiledModule -Encoding 'UTF8'; + + Import-Module -Name $ModulePath -Force; + Import-Module -Name $ModulePath -Force -Global; + + # Set our location back to the previous folder + Set-Location -Path $OldLocation; +} +<# +.SYNOPSIS + Test an Icinga for Windows component and validate the functionality + including the code styling +.DESCRIPTION + Test an Icinga for Windows component and validate the functionality + including the code styling +.PARAMETER Name + The name of the Icinga for Windows component and module +.PARAMETER ShowIssues + Prints a list of all code styling issues found within the module + for resolving them +.EXAMPLE + Test-IcingaForWindowsComponent -Name 'framework' -ShowIssues; +#> +function Test-IcingaForWindowsComponent() +{ + param ( + [string]$Name, + [switch]$ShowIssues = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'Please specify the name of the component you want to test'; + return; + } + + [string]$ModuleName = [string]::Format('icinga-powershell-{0}', $Name.ToLower()); + [string]$ModuleRoot = Get-IcingaForWindowsRootPath; + [string]$ModuleDir = Join-Path -Path $ModuleRoot -ChildPath $ModuleName; + [string]$ScriptAnalyzer = Join-Path -Path $ModuleDir -ChildPath 'PSScriptAnalyzerSettings.psd1'; + [string]$ModuleManifest = (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('{0}.psd1', $ModuleName))) + + if ((Test-Path $ModuleDir) -eq $FALSE) { + Write-IcingaConsoleError 'A component with the name "{0}" does not exist. Use "New-IcingaForWindowsComponent" to create a new one or verify that the provided name is correct.' -Objects $Name; + return; + } + + if ($null -ne (Get-Command -Name 'Invoke-ScriptAnalyzer' -ErrorAction SilentlyContinue)) { + $ScriptAnalyzerContent = Get-Content -Raw -Path $ScriptAnalyzer; + [ScriptBlock]$ScriptAnalyzerScriptBlock = [ScriptBlock]::Create('return ' + $ScriptAnalyzerContent); + $ScriptAnalyzerData = (& $ScriptAnalyzerScriptBlock); + + $ScriptAnalyzerData.Add('IncludeRule', $ScriptAnalyzerData.IncludeRules); + $ScriptAnalyzerData.Add('ExcludeRule', $ScriptAnalyzerData.ExcludeRules); + $ScriptAnalyzerData.Remove('Rules'); + $ScriptAnalyzerData.Remove('IncludeRules'); + $ScriptAnalyzerData.Remove('ExcludeRules'); + + $Result = Invoke-ScriptAnalyzer -Path $ModuleDir -Recurse -ReportSummary @ScriptAnalyzerData; + + if ($Result.Count -ne 0) { + if ($ShowIssues -eq $FALSE) { + Write-IcingaConsoleWarning 'Your script analyzer has found issues inside this module. Use "-ShowIssues" to print each of them, allowing you to fix them.'; + } else { + Write-Host ($Result | Out-String); + } + } else { + Write-IcingaConsoleNotice 'Your module does not have any code styling errors'; + } + } else { + Write-IcingaConsoleWarning 'The PowerShell ScriptAnalyzer is not installed on this system. Validating the module is not possible.'; + } + + try { + Import-Module -Name $ModuleDir -Force -ErrorAction Stop; + + Write-IcingaConsoleNotice 'Your module was successfully loaded. No errors were detected'; + } + catch { + $ErrorScriptName = $_.InvocationInfo.ScriptName; + $ErrorScriptLine = $_.InvocationInfo.Line; + $PositionMessage = $_.InvocationInfo.PositionMessage; + + if (([string]$_.FullyQualifiedErrorId).Contains('System.IO.FileLoadException')) { + $ErrorScriptName = $ModuleManifest; + $ErrorScriptLine = 'Check your NestedModule argument to verify that all included modules are present and valid' + } + + Write-IcingaConsoleError ` + -Message 'Failed to import the module "{0}": {1}{2}{2}Module File: {3}{2}Error Line: {4}{2}Position: {5}{2}{2}Full Error: {6}' ` + -Objects @( + $ModuleName, + $_.FullyQualifiedErrorId, + (New-IcingaNewLine), + $ErrorScriptName, + $ErrorScriptLine, + $PositionMessage, + $_.Exception.Message + ); + } +} +<# +.SYNOPSIS + Updates an Icinga for Windows component by updating the NestedModules entry + of the manifest including documentation and provides an optional .zip file + for usage within the Icinga for Windows repositories +.DESCRIPTION + Updates an Icinga for Windows component by updating the NestedModules entry + of the manifest including documentation and provides an optional .zip file + for usage within the Icinga for Windows repositories +.PARAMETER Name + The name of the Icinga for Windows component and module +.PARAMETER ReleasePackagePath + The path on where the .zip file for release will be created at. + Defaults to the current users home folder +.PARAMETER NoOutput + Use this flag to disable console outputs for the progress, required by other + functions using this function +.PARAMETER CreateReleasePackage + If set, the function will create a .zip file for this specific module which + can be used within the Icinga for Windows repository manager. + Use -ReleasePackagePath to override the default location on where the + package will be created in. Default location is the current users + home folder +#> +function Publish-IcingaForWindowsComponent() +{ + param ( + [string]$Name, + [string]$ReleasePackagePath = '', + [switch]$NoOutput = $FALSE, + [switch]$CreateReleasePackage = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'Please specify the name of the component you want to publish'; + return; + } + + [string]$ModuleName = [string]::Format('icinga-powershell-{0}', $Name.ToLower()); + [string]$ModuleRoot = Get-IcingaForWindowsRootPath; + [string]$ModuleDir = Join-Path -Path $ModuleRoot -ChildPath $ModuleName; + [string]$ModuleManifest = (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('{0}.psd1', $ModuleName))) + + if ((Test-Path $ModuleDir) -eq $FALSE) { + Write-IcingaConsoleError 'A component with the name "{0}" does not exist. Use "New-IcingaForWindowsComponent" to create a new one or verify that the provided name is correct.' -Objects $Name; + return; + } + + $ComponentType = ''; + $ManifestScript = ''; + $ManifestContent = Get-Content -Path $ModuleManifest; + + foreach ($entry in $ManifestContent) { + [string]$LineContent = [string]$entry; + if ($LineContent.Contains('#')) { + $LineContent = $LineContent.Substring(0, $LineContent.IndexOf('#')); + } + + if ([string]::IsNullOrEmpty($LineContent) -Or $LineContent -eq "`r`n" -Or $LineContent -eq "`n" -Or [string]::IsNullOrEmpty($LineContent.Replace(' ', ''))) { + continue; + } + + $ManifestScript += $LineContent; + $ManifestScript += "`r`n"; + } + + [ScriptBlock]$ManifestScriptBlock = [ScriptBlock]::Create('return ' + $ManifestScript); + $ModuleManifestData = (& $ManifestScriptBlock); + $ModuleFiles = Get-ChildItem -Path $ModuleDir -Recurse -Filter '*.psm1'; + [array]$FunctionList = @(); + [array]$CmdletList = @(); + [array]$VariableList = @(); + [array]$AliasList = @(); + [string]$CompiledFolder = (Join-Path -Path $ModuleDir -ChildPath 'compiled'); + [string]$CompiledFile = [string]::Format('{0}.ifw_compilation.psm1', $ModuleName.ToLower()); + [string]$CompiledFileInclude = [string]::Format('.\compiled\{0}', $CompiledFile); + [string]$CompiledFilePath = (Join-Path -Path $CompiledFolder -ChildPath $CompiledFile); + + foreach ($entry in $ModuleFiles) { + # Ensure the compilation file never includes itself + if ($entry.FullName -eq $CompiledFilePath) { + continue; + } + + $FunctionList += Get-IcingaForWindowsComponentPublicFunctions -FileObject $entry -ModuleName $ModuleName; + $FileConfig = (Read-IcingaPowerShellModuleFile -File $entry.FullName); + $VariableList += $FileConfig.VariableList; + $AliasList += $FileConfig.AliasList; + $CmdletList += $FileConfig.ExportCmdlet; + } + + if ((Test-Path -Path $CompiledFolder) -eq $FALSE) { + New-Item -Path $CompiledFolder -ItemType Directory -Force | Out-Null; + } + + Copy-ItemSecure -Path (Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'templates\compilation.psm1.template') -Destination $CompiledFilePath -Force | Out-Null; + + if ($NoOutput) { + Disable-IcingaFrameworkConsoleOutput; + } + + Write-IcingaForWindowsComponentManifest -Name $Name -ModuleList @( $CompiledFileInclude ) -FunctionList $FunctionList -VariableList $VariableList -AliasList $AliasList -CmdletList $CmdletList; + + if ($ModuleManifestData.PrivateData.Type -eq 'plugins') { + Publish-IcingaPluginConfiguration -ComponentName $Name; + Publish-IcingaPluginDocumentation -ModulePath $ModuleDir; + } + + Copy-ItemSecure -Path (Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'templates\compilation.psm1.template') -Destination $CompiledFilePath -Force | Out-Null; + + if ($CreateReleasePackage) { + if ([string]::IsNullOrEmpty($ReleasePackagePath)) { + $ReleasePackagePath = Join-Path -Path $ENV:HOMEDRIVE -ChildPath $ENV:HOMEPATH; + $ReleasePackagePath = Join-Path -Path $ReleasePackagePath -ChildPath ([string]::Format('ifw_releases\{0}', $ModuleName)) + } + + if ((Test-Path $ReleasePackagePath) -eq $FALSE) { + New-Item -ItemType Directory -Path $ReleasePackagePath | Out-Null; + } + + if ((Test-Path $ReleasePackagePath) -eq $FALSE) { + Write-IcingaConsoleError 'Failed to create path "{0}" for providing .zip archive for module "{1}"' -Objects $ReleasePackagePath, $ModuleName; + return; + } + + if ((Test-IcingaAddTypeExist 'System.IO.Compression.FileSystem') -eq $FALSE) { + Add-Type -Assembly 'System.IO.Compression.FileSystem'; + } + + [string]$ZipName = [string]::Format( + '{0}-{1}.zip', $ModuleName, $ModuleManifestData.PrivateData.Version + ); + + $ZipFile = Join-Path -Path $ReleasePackagePath -ChildPath $ZipName; + + if (Test-Path $ZipFile) { + Remove-ItemSecure -Path $ZipFile -Force | Out-Null; + } + + $ReleaseTmpDir = New-IcingaTemporaryDirectory; + Copy-ItemSecure -Path $ModuleDir -Destination $ReleaseTmpDir -Recurse -Force | Out-Null; + + $ReleaseTmpContent = Get-ChildItem -Path (Join-Path -Path $ReleaseTmpDir -ChildPath $ModuleName) -Recurse; + + foreach ($entry in $ReleaseTmpContent) { + if ((Test-Path $entry.FullName) -eq $FALSE) { + continue; + } + + if ($entry.Name[0] -eq '.') { + Remove-ItemSecure -Path $entry.FullName -Recurse -Force | Out-Null; + } + } + + [System.IO.Compression.ZipFile]::CreateFromDirectory( + $ReleaseTmpDir, + $ZipFile, + [System.IO.Compression.CompressionLevel]::Optimal, + $FALSE + ); + + Remove-ItemSecure -Path $ReleaseTmpDir -Force -Recurse | Out-Null; + + if (Test-Path $ZipFile) { + Write-IcingaConsoleNotice 'Published module with version "{0}" at "{1}"' -Objects $ModuleManifestData.PrivateData.Version, $ZipFile; + } else { + Write-IcingaConsoleError 'Failed to publish module "{0}" at "{1}". Please verify you have enough permissions to access this location and try again' -Objects $ModuleName, $ZipFile; + } + + } + + Write-IcingaConsoleNotice 'Component "{0}" has been updated as module "icinga-powershell-{1}" at location "{2}" successfully' -Objects $Name, $Name.ToLower(), $ModuleDir; + + Enable-IcingaFrameworkConsoleOutput; +} +<# +.SYNOPSIS + Opens any Icinga for Windows component in the defined editor +.DESCRIPTION + Opens any Icinga for Windows component in the defined editor +.PARAMETER Name + The name of the Icinga for Windows component +.PARAMETER Editor + Defines which editor should be used +.EXAMPLE + Open-IcingaForWindowsComponentInEditor -Name 'framework' -Editor 'code'; +#> +function Open-IcingaForWindowsComponentInEditor() +{ + param ( + [string]$Name, + [ValidateSet('code')] + [string]$Editor = 'code' + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'Please specify the name of the component you want to open'; + return; + } + + [string]$ModuleName = [string]::Format('icinga-powershell-{0}', $Name.ToLower()); + [string]$ModuleRoot = Get-IcingaForWindowsRootPath; + [string]$ModuleDir = Join-Path -Path $ModuleRoot -ChildPath $ModuleName; + [string]$ModuleManifest = (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('{0}.psd1', $ModuleName))) + + if ((Test-Path $ModuleDir) -eq $FALSE) { + Write-IcingaConsoleError 'A component with the name "{0}" does not exist. Use "New-IcingaForWindowsComponent" to create a new one or verify that the provided name is correct.' -Objects $Name; + return; + } + + [bool]$EditorInstalled = $FALSE; + [string]$EditorName = 'Unspecified'; + + switch ($Editor) { + 'code' { + if ($null -ne (Get-Command 'code.cmd' -ErrorAction SilentlyContinue)) { + $EditorInstalled = $TRUE; + } + $EditorName = 'Visual Studio Code'; + break; + }; + # TODO: Add more editors + } + + if ($EditorInstalled -eq $FALSE) { + Write-IcingaConsoleError 'Unable to open module "{0}" with {1}. Either the binary was not found or {1} is not installed' -Objects $ModuleName, $EditorName; + return; + } + + switch ($Editor) { + 'code' { + & code --new-window "$ModuleDir"; + + break; + } + # TODO: Add more editors + } +} +function Update-IcingaForWindowsManifestArray() +{ + param ( + [string]$ManifestFile = '', + [array]$ArrayVariableValues = @(), + [string]$ArrayVariableName = '' + ); + + if ([string]::IsNullOrEmpty($ArrayVariableName)) { + return; + } + + # Remove duplicate entries + $ArrayVariableValues = $ArrayVariableValues | Select-Object -Unique; + + [array]$ManifestContent = Get-Content -Path $ManifestFile -ErrorAction SilentlyContinue; + + if ($null -eq $ManifestContent -Or $ManifestContent.Count -eq 0) { + Write-IcingaConsoleWarning 'The manifest file "{0}" could not be loaded for updating array element "{1}2' -Objects $ManifestFile, $ArrayVariableName; + return; + } + + $ContentString = New-Object -TypeName 'System.Text.StringBuilder'; + [bool]$UpdatedArrayContent = $FALSE; + + foreach ($entry in $ManifestContent) { + [string]$ManifestLine = $entry; + + if ($UpdatedArrayContent -And $entry -Like '*)*') { + $UpdatedArrayContent = $FALSE; + continue; + } + + if ($UpdatedArrayContent) { + continue; + } + + if ($entry -Like ([string]::Format('*{0}*', $ArrayVariableName.ToLower()))) { + if ($entry -NotLike '*)*') { + $UpdatedArrayContent = $TRUE; + } + $ContentString.AppendLine(([string]::Format(' {0} = @(', $ArrayVariableName))) | Out-Null; + + if ($ArrayVariableValues.Count -ne 0) { + [array]$NestedModules = (ConvertFrom-IcingaArrayToString -Array $ArrayVariableValues -AddQuotes -UseSingleQuotes).Split(','); + [int]$ModuleIndex = 0; + foreach ($module in $NestedModules) { + if ([string]::IsNullOrEmpty($module)) { + continue; + } + + $ModuleIndex += 1; + + if ($ModuleIndex -ne $NestedModules.Count) { + if ($ModuleIndex -eq 1) { + $ManifestLine = [string]::Format(' {0},', $module); + } else { + $ManifestLine = [string]::Format(' {0},', $module); + } + } else { + if ($ModuleIndex -eq 1) { + $ManifestLine = [string]::Format(' {0}', $module); + } else { + $ManifestLine = [string]::Format(' {0}', $module); + } + } + + $ContentString.AppendLine($ManifestLine) | Out-Null; + } + } + + $ContentString.AppendLine(' )') | Out-Null; + continue; + } + + if ([string]::IsNullOrEmpty($ManifestLine.Replace(' ', '')) -Or $ManifestLine -eq "`r`n" -Or $ManifestLine -eq "`n") { + continue; + } + + $ContentString.AppendLine($ManifestLine) | Out-Null; + } + + Write-IcingaFileSecure -File $ManifestFile -Value $ContentString.ToString(); +} +function Get-IcingaForWindowsComponentPublicFunctions() +{ + param ( + $FileObject = $null, + [string]$ModuleName = '' + ); + + [array]$ExportFunctions = @(); + + # First first if we are inside a public space + if ((Test-IcingaForWindowsComponentPublicFunctions -FileObject $FileObject -ModuleName $ModuleName) -eq $FALSE) { + $FileData = (Read-IcingaPowerShellModuleFile -File $FileObject.FullName); + + foreach ($entry in $FileData.FunctionList) { + if ($entry.Contains('Global:')) { + $ExportFunctions += $entry.Replace('Global:', ''); + } + } + + $ExportFunctions += $FileData.ExportFunction; + + return $ExportFunctions; + } + + $FileData = (Read-IcingaPowerShellModuleFile -File $FileObject.FullName); + $ExportFunctions += $FileData.FunctionList; + $ExportFunctions += $FileData.ExportFunction; + + # If we are, add all functions we found + return $ExportFunctions; +} +<# +.SYNOPSIS + Updates a Icinga for Windows manifest file by updating NestedModules for + easier usage +.DESCRIPTION + Updates a Icinga for Windows manifest file by updating NestedModules for + easier usage +.PARAMETER Name + The name of the Icinga for Windows component to edit +.PARAMETER ModuleConfig + Configuration parsed as hashtable to update our manifest template with proper data +.PARAMETER ModuleList + An array of PowerShell module files within module to update the NestedModule entry with +#> +function Write-IcingaForWindowsComponentManifest() +{ + param ( + [string]$Name, + [hashtable]$ModuleConfig = @{ }, + [array]$ModuleList = @(), + [array]$FunctionList = @(), + [array]$CmdletList = @(), + [array]$VariableList = @(), + [array]$AliasList = @() + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'Please specify a name for writing the component manifest'; + return; + } + + $PSDefaultParameterValues = @{ '*:Encoding' = 'utf8' }; + + [string]$ModuleName = [string]::Format('icinga-powershell-{0}', $Name.ToLower()); + [string]$ModuleRoot = Get-IcingaForWindowsRootPath; + [string]$ModuleDir = Join-Path -Path $ModuleRoot -ChildPath $ModuleName; + [string]$ManifestFileData = Read-IcingaFileSecure -File (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('{0}.psd1', $ModuleName))); + $ContentString = New-Object -TypeName 'System.Text.StringBuilder'; + + if ([string]::IsNullOrEmpty($ManifestFileData)) { + Write-IcingaConsoleWarning 'The manifest file of module "{0}" could not be loaded' -Objects $ModuleName; + return; + } + + $ManifestFileData = $ManifestFileData.Substring($ManifestFileData.IndexOf('@'), $ManifestFileData.Length - $ManifestFileData.IndexOf('@')); + + if ($null -ne $ModuleConfig -And $ModuleConfig.Count -ne 0) { + foreach ($entry in $ModuleConfig.Keys) { + $Value = $ModuleConfig[$entry]; + + if ($entry -Like '$TAGS$') { + $Value = (ConvertFrom-IcingaArrayToString -Array $Value -AddQuotes -UseSingleQuotes) + } elseif ($entry -Like '$REQUIREDMODULES$') { + [int]$CurrentIndex = 0; + foreach ($module in $Value) { + $CurrentIndex += 1; + $ContentString.Append('@{ ') | Out-Null; + + foreach ($dependency in $module.Keys) { + $DependencyValue = $module[$dependency]; + + $ContentString.Append([string]::Format("{0} = '{1}'; ", $dependency, $DependencyValue)) | Out-Null; + } + $ContentString.Append('}') | Out-Null; + + if ($CurrentIndex -ne $Value.Count) { + $ContentString.Append(",`r`n ") | Out-Null; + } + } + + $Value = $ContentString.ToString(); + } + + $ManifestFileData = $ManifestFileData.Replace($entry, $Value); + } + + Write-IcingaFileSecure -File (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('{0}.psd1', $ModuleName))) -Value $ManifestFileData; + } + + $ContentString.Clear() | Out-Null; + + [string]$ManifestFile = (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('{0}.psd1', $ModuleName))); + + Update-IcingaForWindowsManifestArray -ArrayVariableName 'NestedModules' -ArrayVariableValues $ModuleList -ManifestFile $ManifestFile; + Update-IcingaForWindowsManifestArray -ArrayVariableName 'FunctionsToExport' -ArrayVariableValues $FunctionList -ManifestFile $ManifestFile; + Update-IcingaForWindowsManifestArray -ArrayVariableName 'CmdletsToExport' -ArrayVariableValues $CmdletList -ManifestFile $ManifestFile; + Update-IcingaForWindowsManifestArray -ArrayVariableName 'VariablesToExport' -ArrayVariableValues $VariableList -ManifestFile $ManifestFile; + Update-IcingaForWindowsManifestArray -ArrayVariableName 'AliasesToExport' -ArrayVariableValues $AliasList -ManifestFile $ManifestFile; + + Import-Module -Name $ManifestFile -Force; + Import-Module -Name $ManifestFile -Force -Global; +} +<# +.SYNOPSIS + Creates an empty new Icinga for Windows module +.DESCRIPTION + Creates an empty new Icinga for Windows module +.PARAMETER Name + The name of the Icinga for Windows component and module +.PARAMETER Author + The author of the module as string +.PARAMETER CompanyName + The company this module belongs to as string +.PARAMETER Copyright + The copyright owner of this module as string +.PARAMETER ModuleVersion + The version of this module as 3 digit version number, eg '1.0.0' +.PARAMETER Description + The description of what this module does as string +.PARAMETER RequiredModules + The required modules this module depends on, as an array of hashtables e.g. + @( @{ ModuleName = 'icinga-powershell-framework'; ModuleVersion = '1.7.0' } ) +.PARAMETER Tags + An array of string tags and keywords, for finding this module on the installation list +.PARAMETER ProjectUri + The url of the project as string +.PARAMETER LicenseUri + The url to the license as string +.PARAMETER ComponentType + Defines on how Icinga for Windows will load this module. Valid options are + 'plugins', 'apiendpoint', 'daemon', 'library' +.PARAMETER OpenInEditor + Will open the newly created module within the editor +#> +function New-IcingaForWindowsComponent() +{ + param ( + [string]$Name, + [string]$Author = $env:USERNAME, + [string]$CompanyName = '', + [string]$Copyright = ([string]::Format('(c) {0} {1} | GPL v2.0', [DateTime]::Now.Year, $env:USERNAME)), + [Version]$ModuleVersion = '1.0.0', + [string]$Description = '', + [array]$RequiredModules = @( @{ ModuleName = 'icinga-powershell-framework'; ModuleVersion = '1.7.0' } ), + [string[]]$Tags = $Name, + [string]$ProjectUri = '', + [string]$LicenseUri = '', + [ValidateSet('plugins', 'apiendpoint', 'daemon', 'library')] + [string]$ComponentType = 'plugins', + [switch]$OpenInEditor = $FALSE + ); + + if ([string]::IsNullOrEmpty($Name)) { + Write-IcingaConsoleError 'Please specify a name for your new component'; + return; + } + + [string]$ModuleName = [string]::Format('icinga-powershell-{0}', $Name.ToLower()); + [string]$ModuleRoot = Get-IcingaForWindowsRootPath; + [string]$ModuleDir = Join-Path -Path $ModuleRoot -ChildPath $ModuleName; + [string]$DaemonFunction = ''; + [string]$EndpointName = ''; + + if (Test-Path $ModuleDir) { + Write-IcingaConsoleError 'A component with this name does already exist. Use "Publish-IcingaForWindowsComponent" to apply changes'; + return; + } + + $TextInfo = (Get-Culture).TextInfo; + + New-Item -ItemType Directory -Path $ModuleDir | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'doc') | Out-Null; + New-Item -ItemType File -Path (Join-Path -Path $ModuleDir -ChildPath 'README.md') | Out-Null; + New-Item -ItemType File -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('{0}.psm1', $ModuleName))) | Out-Null; + + $ModuleMainFile = Join-Path -Path $ModuleDir -ChildPath ([string]::Format('{0}.psm1', $ModuleName)); + + Set-Content -Path $ModuleMainFile -Value ([string]::Format('function Import-IcingaPowerShellComponent{0}()', $TextInfo.ToTitleCase($Name))); + Add-Content -Path $ModuleMainFile -Value '{'; + Add-Content -Path $ModuleMainFile -Value ''; + Add-Content -Path $ModuleMainFile -Value '}'; + + switch ($ComponentType) { + 'plugins' { + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'plugins') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'provider') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'provider\public') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'provider\private') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\public') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\private') | Out-Null; + + break; + }; + 'apiendpoint' { + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'endpoint') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\public') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\private') | Out-Null; + + [string]$RegisterFunction = ([string]::Format('Register-IcingaRESTAPIEndpoint{0}', $TextInfo.ToTitleCase($Name.ToLower()))); + [string]$RegisterFunctionFile = (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('endpoint\{0}.psm1', $RegisterFunction))); + [string]$InvokeFunction = ([string]::Format('Invoke-IcingaForWindowsApiRESTCall{0}', $TextInfo.ToTitleCase($Name.ToLower()))); + [string]$InvokeFunctionFile = (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('endpoint\{0}.psm1', $InvokeFunction))); + + Set-Content -Path $RegisterFunctionFile -Value ([string]::Format('function {0}()', $RegisterFunction)); + Add-Content -Path $RegisterFunctionFile -Value '{'; + Add-Content -Path $RegisterFunctionFile -Value ' # Ensure that we can call our API with a specific endpoint'; + Add-Content -Path $RegisterFunctionFile -Value ' return @{'; + Add-Content -Path $RegisterFunctionFile -Value ([string]::Format(" 'Alias' = '{0}';", $Name.ToLower())); + Add-Content -Path $RegisterFunctionFile -Value ([string]::Format(" 'Command' = '{0}';", $InvokeFunction)); + Add-Content -Path $RegisterFunctionFile -Value ' }'; + Add-Content -Path $RegisterFunctionFile -Value '}'; + + Set-Content -Path $InvokeFunctionFile -Value ([string]::Format('function {0}()', $InvokeFunction)); + Add-Content -Path $InvokeFunctionFile -Value '{'; + Add-Content -Path $InvokeFunctionFile -Value ' # Do not modify the param section'; + Add-Content -Path $InvokeFunctionFile -Value ' param ('; + Add-Content -Path $InvokeFunctionFile -Value ' [Hashtable]$Request = @{},'; + Add-Content -Path $InvokeFunctionFile -Value ' [Hashtable]$Connection = @{},'; + Add-Content -Path $InvokeFunctionFile -Value ' [string]$ApiVersion = $null'; + Add-Content -Path $InvokeFunctionFile -Value ' );' + Add-Content -Path $InvokeFunctionFile -Value ''; + Add-Content -Path $InvokeFunctionFile -Value ' # This is the main function for your API endpoint.'; + Add-Content -Path $InvokeFunctionFile -Value ' # Also check the developer guide for further details: https://icinga.com/docs/icinga-for-windows/latest/doc/900-Developer-Guide/12-Custom-API-Endpoints/'; + Add-Content -Path $InvokeFunctionFile -Value ''; + Add-Content -Path $InvokeFunctionFile -Value ' # Send a success message once you connect to the endpoint as example'; + Add-Content -Path $InvokeFunctionFile -Value ' Send-IcingaTCPClientMessage -Message ('; + Add-Content -Path $InvokeFunctionFile -Value ' New-IcingaTCPClientRESTMessage `'; + Add-Content -Path $InvokeFunctionFile -Value ' -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.Ok) `'; + Add-Content -Path $InvokeFunctionFile -Value " -ContentBody 'Api endpoint is installed'"; + Add-Content -Path $InvokeFunctionFile -Value ' ) -Stream $Connection.Stream;'; + Add-Content -Path $InvokeFunctionFile -Value '}'; + + $EndpointName = $Name.ToLower(); + + break; + }; + 'daemon' { + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'daemon') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\public') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\private') | Out-Null; + New-Item ` + -ItemType File ` + -Path (Join-Path -Path (Join-Path -Path $ModuleDir -ChildPath 'daemon') -ChildPath ([string]::Format('Start-IcingaForWindowsDaemon{0}.psm1', $TextInfo.ToTitleCase($Name.ToLower())))) | Out-Null; + + $DaemonFunction = ([string]::Format('Start-IcingaForWindowsDaemon{0}', $TextInfo.ToTitleCase($Name.ToLower()))); + $DaemonEntry = ([string]::Format('Add-IcingaIcingaForWindowsDaemonEntry{0}', $TextInfo.ToTitleCase($Name.ToLower()))); + + Set-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonFunction))) -Value ([string]::Format('function {0}()', $DaemonFunction)); + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonFunction))) -Value '{'; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonFunction))) -Value ' # This is the entry point for Icinga for Windows. Use this function for registering your background daemon'; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonFunction))) -Value ' # Also check the developer guide for further details: https://icinga.com/docs/icinga-for-windows/latest/doc/900-Developer-Guide/10-Custom-Daemons/'; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonFunction))) -Value ''; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonFunction))) -Value ' New-IcingaThreadInstance `'; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonFunction))) -Value ( + [string]::Format( + ' -Name {1}IcingaForWindows_Daemon_{0}{1} `', + $TextInfo.ToTitleCase($Name.ToLower()), + "'" + ) + ); + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonFunction))) -Value ( + [string]::Format( + ' -ThreadPool (Get-IcingaThreadPool -Name {0}MainPool{0}) `', + "'" + ) + ); + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonFunction))) -Value ( + [string]::Format( + ' -Command {0}{1}{0} `', + "'", + $DaemonEntry + ) + ); + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonFunction))) -Value ( + [string]::Format( + ' -CmdParameters @{{ }} `', + "'" + ) + ); + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonFunction))) -Value ' -Start;'; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonFunction))) -Value '}'; + + Set-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ([string]::Format('function {0}()', $DaemonEntry)); + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value '{'; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ''; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ' # This is your main daemon function. Add your code inside the WHILE() loop which is executed once the daemon is loaded.'; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ' # Also check the developer guide for further details: https://icinga.com/docs/icinga-for-windows/latest/doc/900-Developer-Guide/10-Custom-Daemons/'; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ''; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ' while ($TRUE) {'; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ' # Add your daemon code within this loop'; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ' Start-Sleep -Seconds 1;'; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ' }'; + Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value '}'; + + break; + }; + 'library' { + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\public') | Out-Null; + New-Item -ItemType Directory -Path (Join-Path -Path $ModuleDir -ChildPath 'lib\private') | Out-Null; + + break; + } + } + + Copy-ItemSecure ` + -Path (Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'templates\Manifest.psd1.template') ` + -Destination (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('{0}.psd1', $ModuleName))) ` + -Force | Out-Null; + + Write-IcingaForWindowsComponentManifest -Name $Name -ModuleConfig @{ + '$MODULENAME$' = ([string]::Format('Windows {0}', $Name)); + '$GUID$' = (New-Guid); + '$AUTHOR$' = $Author; + '$COMPANYNAME$' = $CompanyName; + '$COPYRIGHT$' = $Copyright; + '$MODULEVERSION$' = $ModuleVersion.ToString(); + '$VMODULEVERSION$' = ([string]::Format('v{0}', $ModuleVersion.ToString())); + '$DESCRIPTION$' = $Description; + '$REQUIREDMODULES$' = $RequiredModules; + '$NESTEDMODULES$' = ''; + '$FUNCTIONSTOEXPORT$' = ''; + '$VARIABLESTOEXPORT$' = ''; + '$ALIASESTOEXPORT$' = ''; + '$TAGS$' = $Tags; + '$PROJECTURI$' = $ProjectUri; + '$LICENSEURI$' = $LicenseUri; + '$COMPONENTTYPE$' = $ComponentType; + '$DAEMONFUNCTION$' = $DaemonFunction; + '$APIENDPOINT$' = $EndpointName; + }; + + Set-Content ` + -Path (Join-Path -Path $ModuleDir -ChildPath 'README.md') ` + -Value ([string]::Format('# Icinga for Windows - {0}', $Name)); + + + Add-Content ` + -Path (Join-Path -Path $ModuleDir -ChildPath 'README.md') ` + -Value ''; + + Add-Content ` + -Path (Join-Path -Path $ModuleDir -ChildPath 'README.md') ` + -Value ([string]::Format('This is the auto generated readme for the Icinga for Windows Module `icinga-powershell-{0}`', $Name.ToLower())); + + Add-Content ` + -Path (Join-Path -Path $ModuleDir -ChildPath 'README.md') ` + -Value ''; + + Add-Content ` + -Path (Join-Path -Path $ModuleDir -ChildPath 'README.md') ` + -Value 'You can start to modify this readme including writing your PowerShell code. Use "Publish-IcingaForWindowsComponent" to generate Icinga 2/Icinga Director configuration files for plugins, auto-generate your configuration and update your module manifest to include all module files you created into the base module.'; + + Add-Content ` + -Path (Join-Path -Path $ModuleDir -ChildPath 'README.md') ` + -Value ''; + + Copy-ItemSecure ` + -Path (Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'templates\PSScriptAnalyzerSettings.psd1.template') ` + -Destination (Join-Path -Path $ModuleDir -ChildPath 'PSScriptAnalyzerSettings.psd1') ` + -Force | Out-Null; + + Write-IcingaConsoleNotice 'New component "{0}" has been created as module "icinga-powershell-{1}" at location "{2}"' -Objects $Name, $Name.ToLower(), $ModuleDir; + Publish-IcingaForWindowsComponent -Name $Name -NoOutput; + + Import-Module $ModuleDir -Force; + Import-Module $ModuleDir -Global -Force; + + if ($OpenInEditor) { + Open-IcingaForWindowsComponentInEditor -Name $Name; + } +} +function Test-IcingaForWindowsComponentPublicFunctions() +{ + param ( + $FileObject = $null, + [string]$ModuleName = '' + ); + + if ($null -eq $FileObject -Or [string]::IsNullOrEmpty($ModuleName)) { + return $FALSE; + } + + # If we load the main .psm1 file of this module, add all functions inside to the public space + if ($FileObject.Name -eq ([string]::Format('{0}.psm1', $ModuleName))) { + return $TRUE; + } + + [int]$RelativPathStartIndex = $FileObject.FullName.IndexOf($ModuleName) + $ModuleName.Length; + $ModuleFileRelativePath = $FileObject.FullName.SubString($RelativPathStartIndex, $FileObject.FullName.Length - $RelativPathStartIndex); + + if ($ModuleFileRelativePath.Contains('\public\') -Or $ModuleFileRelativePath.Contains('\plugins\') -Or $ModuleFileRelativePath.Contains('\endpoint\') -Or $ModuleFileRelativePath.Contains('\daemon\')) { + return $TRUE; + } + + return $FALSE; +} +function New-IcingaWindowsUserPassword() +{ + if ($null -eq $Global:Icinga) { + $Global:Icinga = @{ + 'ServiceUserPassword' = $null + }; + } + + if ($Global:Icinga.ContainsKey('ServiceUserPassword') -eq $FALSE) { + $Global:Icinga.Add('ServiceUserPassword', $null); + } + + [SecureString]$Password = ConvertTo-IcingaSecureString -String (Get-IcingaRandomChars -Count 60); + $Global:Icinga.ServiceUserPassword = $Password; + + return $Password; +} +function Update-IcingaServiceUser() +{ + $IcingaUser = Get-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser'; + + if ([string]::IsNullOrEmpty($IcingaUser)) { + return; + } + + if ((Test-IcingaManagedUser -IcingaUser $IcingaUser) -eq $FALSE) { + return; + } + + $UserConfig = Get-IcingaWindowsUserConfig -UserName $IcingaUser; + $User = New-IcingaWindowsUser -IcingaUser $UserConfig.Name; + + Set-IcingaServiceUser -User $IcingaUser -Password $Global:Icinga.ServiceUserPassword -Service 'icinga2' | Out-Null; + Set-IcingaServiceUser -User $IcingaUser -Password $Global:Icinga.ServiceUserPassword -Service 'icingapowershell' | Out-Null; + + Restart-IcingaService 'icinga2'; + Restart-IcingaForWindows; +} +function Get-IcingaRandomChars() +{ + param ( + [int]$Count = 10, + [string]$Symbols = 'abcdefghiklmnoprstuvwxyzABCDEFGHKLMNOPRSTUVWXYZ1234567890!§$%()=?}][{@#*+' + ); + + $RandomChars = ''; + + if ([string]::IsNullOrEmpty($Symbols)) { + return $RandomChars; + } + + while ($Count -gt 0) { + + [int]$SymbolLength = $Symbols.Length; + $RandomValue = Get-Random -Minimum 0 -Maximum ($SymbolLength - 1); + $RandomChars += $Symbols[$RandomValue]; + $Count -= 1; + } + + return $RandomChars; +} +function Uninstall-IcingaSecurity() +{ + param ( + $IcingaUser = 'icinga' + ); + + Uninstall-IcingaServiceUser -IcingaUser $IcingaUser; + Uninstall-IcingaJEAProfile; +} +function Remove-IcingaWindowsUser() +{ + param ( + $IcingaUser = 'icinga' + ); + + $UserConfig = Get-IcingaWindowsUserConfig -UserName $IcingaUser; + + if ($UserConfig.UserExist -eq $FALSE -Or $UserConfig.IcingaManagedUser -eq $FALSE) { + if ($UserConfig.UserExist -eq $FALSE) { + Write-IcingaConsoleNotice 'The user "{0}" is not present on this system' -Objects $IcingaUser; + } elseif ($UserConfig.IcingaManagedUser -eq $FALSE) { + Write-IcingaConsoleNotice 'The user "{0}" was not created by Icinga for Windows. Unable to remove user' -Objects $IcingaUser; + } + + return @{ + 'User' = $IcingaUser; + 'SID' = $SID; + }; + } + + $Result = Start-IcingaProcess -Executable 'net' -Arguments ([string]::Format('user "{0}" /DELETE', $UserConfig.Name)); + + if ($Result.ExitCode -ne 0) { + Write-IcingaConsoleError 'Failed to delete user "{0}": {1}' -Objects $IcingaUser, $Result.Error; + } else { + # Delete Home Directory + $HomePath = Join-Path -Path ($ENV:HOMEDRIVE) -ChildPath (Join-Path -Path '\Users\' -ChildPath $IcingaUser); + Remove-ItemSecure -Path $HomePath -Recurse -Force | Out-Null; + } + + return @{ + 'User' = $UserConfig.Caption; + 'SID' = $UserConfig.SID; + }; +} +function Restart-Icinga() +{ + Stop-Icinga; + Start-Icinga; +} +function Get-IcingaWindowsUserMetadata() +{ + return @{ + 'Description' = 'Dedicated user for Icinga for Windows with limited privileges. The user is only allowed to be used as service user, while local login or RDP sessions are disabled. For monitoring, this user requires a valid JEA profile.'; + 'FullName' = 'Icinga for Windows Monitoring User'; + }; +} +function Install-IcingaSecurity() +{ + param ( + [string]$IcingaUser = 'icinga', + [switch]$RebuildFramework = $FALSE, + [switch]$AllowScriptBlocks = $FALSE, + [switch]$ConstrainedLanguage = $FALSE + ); + + if ($PSVersionTable.PSVersion -lt (New-IcingaVersionObject -Version 5, 0)) { + Write-IcingaConsoleError 'You cannot use JEA profiles on your system, as your installed PowerShell version "{0}" is lower than minimum required version "5.0"' -Objects $PSVersionTable.PSVersion; + return; + } + + if ((Test-AdministrativeShell) -eq $FALSE) { + Write-IcingaConsoleError -Message 'This command can only be executed from an administrative shell'; + return; + } + + $IcingaUserInfo = Split-IcingaUserDomain -User $IcingaUser; + + # Max length for the user name + if ($IcingaUserInfo.User.Length -gt 20) { + Write-IcingaConsoleError 'The specified user name "{0}" is too long. The maximum character limit is 20 digits.' -Objects $IcingaUserInfo.User; + + return; + } + + Install-IcingaServiceUser -IcingaUser $IcingaUser; + Install-IcingaJEAProfile -IcingaUser $IcingaUser -RebuildFramework:$RebuildFramework -AllowScriptBlocks:$AllowScriptBlocks -ConstrainedLanguage:$ConstrainedLanguage; + + Restart-IcingaForWindows; +} +function Clear-IcingaWindowsUserPassword() +{ + if ($null -eq $Global:Icinga) { + return; + } + + if ($Global:Icinga.ContainsKey('ServiceUserPassword') -eq $FALSE) { + return; + } + + $Global:Icinga.ServiceUserPassword = $null; +} +function Start-IcingaForWindows() +{ + Start-IcingaService -Service 'icingapowershell'; + # Update the process priority after each restart + Start-IcingaWindowsScheduledTaskProcessPriority; +} +function Install-IcingaServiceUser() +{ + param ( + $IcingaUser = 'icinga' + ); + + if ([string]::IsNullOrEmpty($IcingaUser)) { + Write-IcingaConsoleError 'The provided user cannot be empty.'; + return; + } + + Write-IcingaConsoleNotice 'Installing user "{0}"' -Objects $IcingaUser; + + $User = New-IcingaWindowsUser -IcingaUser $IcingaUser; + + Start-Sleep -Seconds 2; + + Set-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser' -Value $User.User; + + Set-IcingaServiceUser -User $IcingaUser -Password $Global:Icinga.ServiceUserPassword -Service 'icinga2' | Out-Null; + Set-IcingaServiceUser -User $IcingaUser -Password $Global:Icinga.ServiceUserPassword -Service 'icingapowershell' | Out-Null; + + Update-IcingaWindowsUserPermission -SID $User.SID; + + Set-IcingaUserPermissions -IcingaUser $IcingaUser; + + Restart-IcingaService 'icinga2'; + Restart-IcingaForWindows; + + Clear-IcingaWindowsUserPassword; + + Write-IcingaConsoleNotice 'User "{0}" including permissions was successfully installed on this host' -Objects $IcingaUser; +} +function New-IcingaWindowsUser() +{ + param ( + $IcingaUser = 'icinga' + ); + + if ((Test-AdministrativeShell) -eq $FALSE) { + Write-IcingaConsoleError 'For this command you require to run an Admin shell'; + + return @{ + 'User' = $null; + 'SID' = $null; + }; + } + + $IcingaUserInfo = Split-IcingaUserDomain -User $IcingaUser; + + # Max length for the user name + if ($IcingaUserInfo.User.Length -gt 20) { + Write-IcingaConsoleError 'The specified user name "{0}" is too long. The maximum character limit is 20 digits.' -Objects $IcingaUserInfo.User; + + return @{ + 'User' = $null; + 'SID' = $null; + }; + } + + $UserMetadata = Get-IcingaWindowsUserMetadata; + $UserConfig = Get-IcingaWindowsUserConfig -UserName $IcingaUser; + + # In case the user exist, we can check if it is a managed user for modifying the login password + if ($UserConfig.UserExist) { + + # User already exist -> override password - but only if the user is entirely managed by Icinga + if ($UserConfig.IcingaManagedUser) { + # In case the password set fails, we need to try again + [int]$Attempts = 0; + [bool]$Success = $FALSE; + + while ($Attempts -lt 10) { + $Result = Start-IcingaProcess -Executable 'net' -Arguments ([string]::Format('user "{0}" "{1}"', $IcingaUser, (ConvertFrom-IcingaSecureString -SecureString (New-IcingaWindowsUserPassword)))); + + if ($Result.ExitCode -eq 0) { + $Success = $TRUE; + break; + } + + $Attempts += 1; + } + + if ($Success -eq $FALSE) { + Write-IcingaConsoleError 'Failed to update password for user "{0}": {1}' -Objects $IcingaUser, $Result.Error; + + return @{ + 'User' = $UserConfig.Caption; + 'SID' = $UserConfig.SID; + }; + } + Write-IcingaConsoleNotice 'User updated successfully.'; + } else { + Write-IcingaConsoleWarning 'User "{0}" is not managed by Icinga for Windows. No changes were made.' -Objects $IcingaUser; + } + + return @{ + 'User' = $UserConfig.Caption; + 'SID' = $UserConfig.SID; + }; + } + + # Access our local Account Database + $AccountDB = [ADSI]"WinNT://$Env:COMPUTERNAME,Computer"; + $IcingaUserObject = $AccountDB.Create("User", $IcingaUser); + $IcingaUserObject.SetPassword((ConvertFrom-IcingaSecureString -SecureString (New-IcingaWindowsUserPassword))); + $IcingaUserObject.SetInfo(); + $IcingaUserObject.FullName = $UserMetadata.FullName; + $IcingaUserObject.SetInfo(); + $IcingaUserObject.Description = $UserMetadata.Description; + $IcingaUserObject.SetInfo(); + $IcingaUserObject.UserFlags = 65600; + $IcingaUserObject.SetInfo(); + + # Add to local user group + <# This is not required, but let's leave it here for possible later lookup on how this works + $SIDLocalGroup = New-Object System.Security.Principal.SecurityIdentifier ("S-1-5-32-545"); + $LocalGroup = ($SIDLocalGroup.Translate([System.Security.Principal.NTAccount])).Value.Split('\')[1]; + + $LocalUserGroup = [ADSI]"WinNT://$Env:COMPUTERNAME/$LocalGroup,group"; + $LocalUserGroup.Add("WinNT://$Env:COMPUTERNAME/$IcingaUser,user") + #> + + $UserConfig = Get-IcingaWindowsUserConfig -UserName $IcingaUser; + + Write-IcingaConsoleNotice 'User was successfully created.'; + + return @{ + 'User' = $UserConfig.Caption; + 'SID' = $UserConfig.SID; + }; +} +function Stop-IcingaJEAProcess() +{ + param ( + [string]$JeaPid = $null + ); + + if ([string]::IsNullOrEmpty($JeaPid)) { + [string]$JeaPid = Get-IcingaJEAServicePid; + } + + if ([string]::IsNullOrEmpty($JeaPid)) { + return; + } + + if ($JeaPid -eq '0' -Or $JeaPid -eq 0) { + return; + } + + $JeaPowerShellProcess = Get-Process -Id $JeaPid -ErrorAction SilentlyContinue; + if ($null -eq $JeaPowerShellProcess) { + return; + } + + if ($JeaPowerShellProcess.ProcessName -ne 'wsmprovhost') { + return; + } + + try { + Stop-Process -Id $JeaPid -Force -ErrorAction Stop; + } catch { + Write-IcingaConsoleError 'Unable to stop the JEA process "wsmprovhost" caused by the following error: "{0}".' -Objects $_.Exception.Message; + } +} +function Restart-IcingaForWindows() +{ + Stop-IcingaForWindows; + Start-IcingaForWindows; +} + +Set-Alias -Name 'Restart-IcingaWindowsService' -Value 'Restart-IcingaForWindows'; +function Start-Icinga() +{ + Start-IcingaService -Service 'icinga2'; + Start-IcingaForWindows; +} +function Stop-IcingaForWindows() +{ + [string]$JeaPid = Get-IcingaJEAServicePid; + + Stop-IcingaService -Service 'icingapowershell'; + + if ((Test-IcingaJEAServiceRunning -JeaPid $JeaPid)) { + Stop-Process -Id $JeaPid -Force; + } +} + +Set-Alias -Name 'Stop-IcingaWindowsService' -Value 'Stop-IcingaForWindows'; +<# +.SYNOPSIS + Will return certain configuration values for specified users by + using the username or SID by doing a local lookup with Get-LocalUser, + in case the Cmdlet is installed +.DESCRIPTION + Will return certain configuration values for specified users by + using the username or SID by doing a local lookup with Get-LocalUser, + in case the Cmdlet is installed. + + Allows to test if a user does exist and if the user is managed by + Icinga for Windows. + + In case both, -UserName and -SID are used, the -SID argument will always be + prioritized and therefor only one argument should be used at the same time. +.PARAMETER UserName + The local username you want to fetch config from +.PARAMETER SID + The SID of a local user you want to fetch config from. This argument + will always be prioritized, even when -UserName is set +.EXAMPLE + PS> Get-IcingaWindowsUserConfig -UserName 'icinga'; +.EXAMPLE + PS> Get-IcingaWindowsUserConfig -SID 'S-1-5-21-1004336348-1177238915-682003330-512'; +#> +function Get-IcingaWindowsUserConfig() +{ + param ( + [string]$UserName = '', + [string]$SID = '' + ); + + if ([string]::IsNullOrEmpty($SID) -And [string]::IsNullOrEmpty($UserName) -eq $FALSE) { + $SID = Get-IcingaUserSID -User $UserName; + } + + $UserConfig = @{ + 'SID' = ''; + 'Name' = ''; + 'FullName' = ''; + 'Caption' = ''; + 'Domain' = (Get-IcingaNetbiosName); + 'Description' = ''; + 'IcingaManagedUser' = $FALSE; + 'UserExist' = $FALSE; + }; + + if ([string]::IsNullOrEmpty($SID) -And [string]::IsNullOrEmpty($UserName)) { + return $UserConfig; + } + + # If we are not running PowerShell 5.0 or later, 'Get-LocalUser' will not be available + # which should always result in "false" for the managed user + if ((Test-IcingaFunction 'Get-LocalUser') -eq $FALSE) { + return $UserConfig; + } + + $UserMetadata = Get-IcingaWindowsUserMetadata; + + try { + $UserData = Get-LocalUser -SID $SID -ErrorAction Stop; + } catch { + return $UserConfig; + } + + $UserConfig.SID = $UserData.SID.Value; + $UserConfig.Name = $UserData.Name; + $UserConfig.FullName = $UserData.FullName; + $UserConfig.Caption = [string]::Format('{0}\{1}', $UserConfig.Domain, $UserData.Name); + $UserConfig.Description = $UserData.Description; + + if ($UserConfig.FullName -eq $UserMetadata.FullName -And $UserConfig.Description -eq $UserMetadata.Description) { + $UserConfig.IcingaManagedUser = $TRUE; + } + + $UserConfig.UserExist = $TRUE; + + return $UserConfig; +} +function Stop-Icinga() +{ + Stop-IcingaService -Service 'icinga2'; + Stop-IcingaForWindows; +} +function Disable-IcingaServiceRecovery() +{ + if ($null -ne (Get-Service 'icinga2' -ErrorAction SilentlyContinue)) { + $ServiceStatus = Start-IcingaProcess -Executable 'sc.exe' -Arguments 'failure icinga2 reset=0 actions=none/0/none/0/none/0'; + + if ($ServiceStatus.ExitCode -ne 0) { + Write-IcingaConsoleError -Message 'Failed to disable recover settings for service "icinga2": {0} {1}' -Objects $ServiceStatus.Message, $ServiceStatus.Error; + } else { + Write-IcingaConsoleNotice -Message 'Successfully disabled service recovery for service "icinga2"'; + } + } + + if ($null -ne (Get-Service 'icingapowershell' -ErrorAction SilentlyContinue)) { + $ServiceStatus = Start-IcingaProcess -Executable 'sc.exe' -Arguments 'failure icingapowershell reset=0 actions=none/0/none/0/none/0'; + + if ($ServiceStatus.ExitCode -ne 0) { + Write-IcingaConsoleError -Message 'Failed to disable recover settings for service "icingapowershell": {0} {1}' -Objects $ServiceStatus.Message, $ServiceStatus.Error; + } else { + Write-IcingaConsoleNotice -Message 'Successfully disabled service recovery for service "icingapowershell"'; + } + } +} +function Uninstall-IcingaServiceUser() +{ + param ( + $IcingaUser = 'icinga' + ); + + if ([string]::IsNullOrEmpty($IcingaUser)) { + Write-IcingaConsoleError 'The provided user cannot be empty.'; + return; + } + + Write-IcingaConsoleNotice 'Uninstalling user "{0}"' -Objects $IcingaUser; + + Stop-IcingaService 'icinga2'; + Stop-IcingaForWindows; + + Set-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser' -Value ''; + + Set-IcingaServiceUser -User 'NT Authority\NetworkService' -Service 'icinga2' | Out-Null; + Set-IcingaServiceUser -User 'NT Authority\NetworkService' -Service 'icingapowershell' | Out-Null; + + Set-IcingaUserPermissions -IcingaUser $IcingaUser -Remove; + + $UserConfig = Remove-IcingaWindowsUser -IcingaUser $IcingaUser; + + if ($null -ne $UserConfig -And ([string]::IsNullOrEmpty($UserConfig.SID) -eq $FALSE)) { + Update-IcingaWindowsUserPermission -SID $UserConfig.SID -Remove; + } + + Restart-IcingaService 'icinga2'; + Restart-IcingaForWindows; + + Write-IcingaConsoleNotice 'User "{0}" including permissions was removed from this host' -Objects $IcingaUser; +} +function Update-IcingaWindowsUserPermission() +{ + param ( + [string]$SID = '', + [switch]$Remove = $FALSE + ); + + if ([string]::IsNullOrEmpty($SID)) { + Write-IcingaConsoleError 'You have to specify the SID of the user to set the security profile to'; + return; + } + + if ($SID.Length -le 16) { + Write-IcingaConsoleWarning 'It seems the provided SID "{0}" is a system SID. Skipping permission update' -Objects $SID; + return; + } + + if ((Test-IcingaManagedUser -SID $SID) -eq $FALSE) { + Write-IcingaConsoleWarning 'This user is not managed by Icinga directly. Skipping permission update'; + return; + } + + $UpdatedProfile = New-IcingaTemporaryFile; + $SystemOutput = Start-IcingaProcess -Executable 'secedit.exe' -Arguments ([string]::Format('/export /cfg "{0}.inf"', $UpdatedProfile)); + $NewSecurityProfile = @(); + + if ($SystemOutput.ExitCode -ne 0) { + throw ([string]::Format('Unable to fetch security profile: {0}', $SystemOutput.Message)); + return; + } + + $SecurityProfile = ''; + + if ($Remove -eq $FALSE) { + $SecurityProfile = Get-Content "$UpdatedProfile.inf"; + + foreach ($line in $SecurityProfile) { + if ($line -like '*SeServiceLogonRight*') { + $line = [string]::Format('{0},*{1}', $line, $SID); + } + if ($line -like '*SeDenyNetworkLogonRight*') { + $line = [string]::Format('{0},*{1}', $line, $SID); + } + if ($line -like '*SeDenyInteractiveLogonRight*') { + $line = [string]::Format('{0},*{1}', $line, $SID); + } + + $NewSecurityProfile += $line; + } + } else { + $SecurityProfile = Get-Content "$UpdatedProfile.inf" -Raw; + $SecurityProfile = $SecurityProfile.Replace([string]::Format(',*{0}', $SID), ''); + $SecurityProfile = $SecurityProfile.Replace([string]::Format('*{0},', $SID), ''); + $NewSecurityProfile = $SecurityProfile; + } + + Set-Content -Path "$UpdatedProfile.inf" -Value $NewSecurityProfile; + + $SystemOutput = Start-IcingaProcess -Executable 'secedit.exe' -Arguments ([string]::Format('/import /cfg "{0}.inf" /db "{0}.sdb"', $UpdatedProfile)); + + if ($SystemOutput.ExitCode -ne 0) { + throw ([string]::Format('Unable to import security profile: {0}', $SystemOutput.Message)); + return; + } + + $SystemOutput = Start-IcingaProcess -Executable 'secedit.exe' -Arguments ([string]::Format('/configure /cfg "{0}.inf" /db "{0}.sdb"', $UpdatedProfile)); + + if ($SystemOutput.ExitCode -ne 0) { + throw ([string]::Format('Unable to configure security profile: {0}', $SystemOutput.Message)); + return; + } + + Remove-Item $UpdatedProfile*; +} +function Enable-IcingaServiceRecovery() +{ + param ( + [int]$IntervalInSeconds = 120 + ); + + # The interval in milliseconds to restart actions to happen for the service + $IntervalInMilliseconds = $IntervalInSeconds * 1000; + + if ($null -ne (Get-Service 'icinga2' -ErrorAction SilentlyContinue)) { + $ServiceStatus = Start-IcingaProcess -Executable 'sc.exe' -Arguments ([string]::Format('failure icinga2 reset=0 actions=restart/{0}/restart/{0}/restart/{0}', $IntervalInMilliseconds)); + + if ($ServiceStatus.ExitCode -ne 0) { + Write-IcingaConsoleError -Message 'Failed to enable recover settings for service "icinga2": {0} {1}' -Objects $ServiceStatus.Message, $ServiceStatus.Error; + } else { + Write-IcingaConsoleNotice -Message 'Successfully enabled service recovery for service "icinga2"'; + } + } + + if ($null -ne (Get-Service 'icingapowershell' -ErrorAction SilentlyContinue)) { + $ServiceStatus = Start-IcingaProcess -Executable 'sc.exe' -Arguments ([string]::Format('failure icingapowershell reset=0 actions=restart/{0}/restart/{0}/restart/{0}', $IntervalInMilliseconds)); + + if ($ServiceStatus.ExitCode -ne 0) { + Write-IcingaConsoleError -Message 'Failed to enable recover settings for service "icingapowershell": {0} {1}' -Objects $ServiceStatus.Message, $ServiceStatus.Error; + } else { + Write-IcingaConsoleNotice -Message 'Successfully enabled service recovery for service "icingapowershell"'; + } + } +} +function Test-IcingaManagedUser() +{ + param ( + [string]$IcingaUser = '', + [string]$SID = '' + ); + + $UserConfig = Get-IcingaWindowsUserConfig -UserName $IcingaUser -SID $SID; + + return $UserConfig.IcingaManagedUser; +} +function Start-IcingaForWindowsInstallation() +{ + param ( + [switch]$Automated + ); + + if ($global:Icinga.InstallWizard.DirectorInstallError -eq $FALSE -And (Get-IcingaFrameworkDebugMode) -eq $FALSE) { + Clear-CLIConsole; + } + + Write-IcingaConsoleNotice 'Starting Icinga for Windows installation'; + + Set-IcingaServiceEnvironment; + + if ($global:Icinga.InstallWizard.DirectorInstallError) { + Write-IcingaConsoleError 'Failed to start Icinga for Windows installation, caused by an error while communicating with Icinga Director: {0}' -Objects $global:Icinga.InstallWizard.DirectorError; + throw $global:Icinga.InstallWizard.DirectorError; + return; + } + + $ConnectionType = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectConnection'; + $HostnameType = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectHostname'; + $CustomHostname = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallationMenuEnterCustomHostname'; + $FirewallType = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectOpenWindowsFirewall'; + + # Certificate handler + $CertificateType = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectCertificate'; + $CertificateForceGen = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectForceCertificateGeneration'; + $CertificateTicket = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaTicket'; + $CertificateCAFile = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaCAFile'; + + # Icinga Agent + $AgentVersion = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallationMenuEnterIcingaAgentVersion'; + $InstallIcingaAgent = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectInstallIcingaAgent'; + $AgentInstallDir = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallationMenuEnterIcingaAgentDirectory'; + $ServiceUser = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallationMenuEnterIcingaAgentUser'; + $ServicePassword = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallationMenuEnterIcingaAgentServicePassword'; + + # Icinga for Windows Service + $InstallPSService = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectInstallIcingaForWindowsService'; + $WindowsServiceDir = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallationMenuEnterWindowsServiceDirectory'; + + # Icinga for Windows Plugins + $InstallPluginChoice = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectInstallIcingaPlugins'; + + # Global Zones + $GlobalZonesType = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectGlobalZones'; + $GlobalZonesCustom = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallationMenuEnterCustomGlobalZones'; + + # Icinga Endpoint Configuration + $IcingaZone = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentZone'; + $IcingaEndpoints = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentNodes'; + $IcingaPort = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallationMenuEnterIcingaPort'; + $IcingaCAServer = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallationMenuEnterIcingaCAServer'; + + # Repository + $IcingaStableRepo = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallationMenuStableRepository'; + + # JEA Profile + $InstallJEAProfile = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectInstallJEAProfile'; + + # Api Checks + $InstallApiChecks = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectInstallApiChecks'; + + # Service Recovery + $ServiceRecovery = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectServiceRecovery'; + + $Hostname = ''; + $GlobalZones = @(); + $IcingaParentAddresses = @(); + $ServicePackageSource = '' + $ServiceSourceGitHub = $FALSE; + $InstallAgent = $TRUE; + $InstallService = $TRUE; + $InstallPlugins = $TRUE; + $PluginPackageRelease = $FALSE; + $PluginPackageSnapshot = $FALSE; + $ForceCertificateGen = $FALSE; + [bool]$InstallJEA = $FALSE; + [bool]$InstallRESTApi = $FALSE; + + if ([string]::IsNullOrEmpty($IcingaStableRepo) -eq $FALSE) { + Add-IcingaRepository -Name 'Icinga Stable' -RemotePath $IcingaStableRepo -Force; + } + + foreach ($endpoint in $IcingaEndpoints) { + $EndpointAddress = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses' -Parent $endpoint; + + $IcingaParentAddresses += $EndpointAddress; + } + + Set-IcingaPowerShellConfig -Path 'Framework.Icinga.ServiceUser' -Value $ServiceUser; + + switch ($HostnameType) { + '0' { + $Hostname = (Get-IcingaHostname -AutoUseFQDN 1); + break; + }; + '1' { + $Hostname = (Get-IcingaHostname -AutoUseFQDN 1 -LowerCase 1); + break; + }; + '2' { + $Hostname = (Get-IcingaHostname -AutoUseFQDN 1 -UpperCase 1); + break; + }; + '3' { + $Hostname = (Get-IcingaHostname -AutoUseHostname 1); + break; + }; + '4' { + $Hostname = (Get-IcingaHostname -AutoUseHostname 1 -LowerCase 1); + break; + }; + '5' { + $Hostname = (Get-IcingaHostname -AutoUseHostname 1 -UpperCase 1); + break; + }; + '6' { + $Hostname = $CustomHostname; + break; + } + } + + switch ($GlobalZonesType) { + '0' { + $GlobalZones += 'director-global'; + $GlobalZones += 'global-templates'; + break; + }; + '1' { + $GlobalZones += 'director-global'; + break; + } + '2' { + $GlobalZones += 'global-templates'; + break; + } + } + + foreach ($zone in $GlobalZonesCustom) { + if ([string]::IsNullOrEmpty($zone) -eq $FALSE) { + if ($GlobalZones -Contains $zone) { + continue; + } + + $GlobalZones += $zone; + } + } + + switch ($InstallIcingaAgent) { + '0' { + # Install Icinga Agent from packages.icinga.com + $InstallAgent = $TRUE; + break; + }; + '1' { + # Do not install Icinga Agent + $InstallAgent = $FALSE; + break; + } + } + + switch ($InstallPSService) { + '0' { + # Install Icinga for Windows Service + $InstallService = $TRUE; + break; + }; + '1' { + # Do not install Icinga for Windows service + $InstallService = $FALSE; + break; + } + } + + switch ($InstallPluginChoice) { + '0' { + # Download stable release + $PluginPackageRelease = $TRUE; + break; + }; + '1' { + # Do not install plugins + $InstallPlugins = $FALSE; + break; + } + } + + if ($CertificateForceGen -eq 1) { + $ForceCertificateGen = $TRUE; + } + + if ($InstallAgent) { + Set-IcingaPowerShellConfig -Path 'Framework.Icinga.AgentLocation' -Value $AgentInstallDir; + Install-IcingaComponent -Name 'agent' -Version $AgentVersion -Confirm -Release; + } + + # Only continue this, if our installation was successful + if ((Get-IcingaAgentInstallation).Installed) { + Set-IcingaAgentNodeName -Hostname $Hostname; + Set-IcingaServiceUser -User $ServiceUser -Password (ConvertTo-IcingaSecureString $ServicePassword) -SetPermission | Out-Null; + Set-IcingaUserPermissions -IcingaUser $ServiceUser; + Install-IcingaAgentBaseFeatures; + Write-IcingaAgentApiConfig -Port $IcingaPort; + # Ensure the Icinga Agent is not spamming the Application log by default + Write-IcingaAgentEventLogConfig -Severity 'warning'; + + # Fixes an issue with the local Icinga for Windows listen port and the defined ports for communicating with the Icinga Parent/CA Nodes + # This will check if we provided a custom port for the endpoints and use this one instead of the configured listen port if Icinga for Windows + $IcingaCAPort = $IcingaPort; + + if ($null -ne $IcingaParentAddresses -And $IcingaParentAddresses.Count -ne 0) { + $ConnectionConfig = Get-IPConfigFromString -IPConfig ($IcingaParentAddresses[0]); + if ($null -ne $ConnectionConfig -And $null -ne $ConnectionConfig.Port -And [string]::IsNullOrEmpty($ConnectionConfig.Port) -eq $FALSE) { + $IcingaCAPort = $ConnectionConfig.Port; + } + } + + if ((Install-IcingaAgentCertificates -Hostname $Hostname -Endpoint $IcingaCAServer -Port $IcingaCAPort -CACert $CertificateCAFile -Ticket $CertificateTicket -Force:$ForceCertificateGen) -eq $FALSE) { + Disable-IcingaAgentFeature 'api'; + Write-IcingaConsoleWarning ` + -Message '{0}{1}{2}{3}{4}' ` + -Objects ( + 'Your Icinga Agent API feature has been disabled. Please provide either your ca.crt ', + 'or connect to a parent node for certificate requests. You can run "Install-IcingaAgentCertificates" ', + 'with your configuration to properly create the host certificate and a valid certificate request. ', + 'After this you can enable the API feature by using "Enable-IcingaAgentFeature api" and restart the ', + 'Icinga Agent service "Restart-IcingaService icinga2"' + ); + } + + Write-IcingaAgentZonesConfig -Endpoints $IcingaEndpoints -EndpointConnections $IcingaParentAddresses -ParentZone $IcingaZone -GlobalZones $GlobalZones -Hostname $Hostname; + } + + if ($InstallService) { + Set-IcingaPowerShellConfig -Path 'Framework.Icinga.IcingaForWindowsService' -Value $WindowsServiceDir; + Set-IcingaInternalPowerShellServicePassword -Password (ConvertTo-IcingaSecureString $ServicePassword); + + Install-IcingaComponent -Name 'service' -Release -Confirm; + Register-IcingaBackgroundDaemon -Command 'Start-IcingaServiceCheckDaemon'; + } + + if ($InstallPlugins) { + Install-IcingaComponent -Name 'plugins' -Release:$PluginPackageRelease -Snapshot:$PluginPackageSnapshot -Confirm; + } + + switch ($FirewallType) { + '0' { + # Open Windows Firewall + Enable-IcingaFirewall -IcingaPort $IcingaPort -Force; + break; + }; + '1' { + # Close Windows Firewall + Disable-IcingaFirewall; + break; + } + } + + Write-IcingaFrameworkCodeCache; + Test-IcingaAgent; + + if ((Get-IcingaAgentInstallation).Installed) { + Restart-IcingaService 'icinga2'; + } + + if ($InstallService) { + Restart-IcingaForWindows; + } + + switch ($InstallApiChecks) { + '0' { + Disable-IcingaFrameworkApiChecks; + break; + }; + '1' { + Register-IcingaBackgroundDaemon -Command 'Start-IcingaWindowsRESTApi'; + Add-IcingaRESTApiCommand -Command 'Invoke-IcingaCheck*' -Endpoint 'apichecks'; + Enable-IcingaFrameworkApiChecks; + $InstallRESTApi = $TRUE; + if ($InstallService) { + Restart-IcingaForWindows; + } else { + Write-IcingaConsoleWarning -Message 'You have selected to install the Api-Check feature and all required configurations were made. The Icinga for Windows service is however not marked for installation, which will cause this feature to not work.'; + } + break; + }; + } + + switch ($ServiceRecovery) { + '0' { + Disable-IcingaServiceRecovery; + break; + }; + '1' { + Enable-IcingaServiceRecovery; + break; + }; + } + + # 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; + $InstallJEA = $TRUE; + break; + }; + '1' { + Install-IcingaSecurity; + $InstallJEA = $TRUE; + break; + }; + '2' { + # Do not install JEA profile + }; + } + + # Always install the Icinga for Windows certificate + # We need to run the task renewal with our scheduled task to fix errors while using WinRM / SSH + Start-IcingaWindowsScheduledTaskRenewCertificate; + Restart-IcingaForWindows; + + # Update configuration and clear swap + $ConfigSwap = Get-IcingaPowerShellConfig -Path 'Framework.Config.Swap'; + Set-IcingaPowerShellConfig -Path 'Framework.Config.Swap' -Value $null; + Set-IcingaPowerShellConfig -Path 'Framework.Config.Live' -Value $ConfigSwap; + $global:Icinga.InstallWizard.Config = @{ }; + Set-IcingaPowerShellConfig -Path 'Framework.Installed' -Value $TRUE; + + if ($Automated -eq $FALSE) { + Write-IcingaConsoleNotice 'Icinga for Windows is installed. Returning to main menu in 5 seconds' + Start-Sleep -Seconds 5; + } + + $global:Icinga.InstallWizard.NextCommand = 'Install-Icinga'; + $global:Icinga.InstallWizard.NextArguments = @{ }; +} +function Install-Icinga() +{ + param ( + [string]$InstallCommand = $null, + [string]$InstallFile = '', + [switch]$NoSSLValidation = $FALSE + ); + + if ($null -eq $global:Icinga) { + $global:Icinga = @{ }; + } + + # Always ensure we use the proper TLS Version + Set-IcingaTLSVersion; + + # Ignore SSL validation in case we set the flag + if ($NoSSLValidation) { + Enable-IcingaUntrustedCertificateValidation; + } + + if ($global:Icinga.ContainsKey('InstallWizard') -eq $FALSE) { + $global:Icinga.Add( + 'InstallWizard', @{ + 'AdminShell' = (Test-AdministrativeShell); + 'LastInput' = ''; + 'LastNotice' = ''; + 'LastWarning' = @(); + 'LastError' = @(); + 'DirectorError' = ''; + 'HeaderPreview' = ''; + 'DirectorSelfServiceConfig' = $null; + 'DirectorSelfService' = $FALSE; + 'DirectorRegisteredHost' = $FALSE; + 'DirectorInstallError' = $FALSE; + 'LastParent' = [System.Collections.ArrayList]@(); + 'LastValues' = @(); + 'DisabledEntries' = @{ }; + 'Config' = @{ }; + 'ConfigSwap' = @{ }; + 'ParentConfig' = $null; + 'Menu' = 'Install-Icinga'; + 'NextCommand' = ''; + 'NextArguments' = $null; + 'HeaderSelection' = $null; + 'DisplayAdvanced' = $FALSE; + 'ShowAdvanced' = $FALSE; + 'ShowHelp' = $FALSE; + 'ShowCommand' = $FALSE; + 'DeleteValues' = $FALSE; + 'HeaderPrint' = $FALSE; + 'JumpToSummary' = $FALSE; + 'Closing' = $FALSE; + } + ); + } else { + $Global:Icinga.InstallWizard.LastWarning.Clear(); + $Global:Icinga.InstallWizard.LastError.Clear(); + } + + if ([string]::IsNullOrEmpty($InstallFile) -eq $FALSE) { + $InstallCommand = Read-IcingaFileSecure -File $InstallFile -ExitOnReadError; + } + + # Use our install command to configure everything + if ([string]::IsNullOrEmpty($InstallCommand) -eq $FALSE) { + + try { + $JsonInstallCmd = ConvertFrom-Json -InputObject $InstallCommand -ErrorAction Stop; + } catch { + Write-IcingaConsoleError 'Failed to deserialize the provided JSON from file or command: {0}' -Objects $_.Exception.Message; + return; + } + + # Add our "old" swap internally + $OldConfigSwap = Get-IcingaPowerShellConfig -Path 'Framework.Config.Swap'; + Disable-IcingaFrameworkConsoleOutput; + + [hashtable]$IcingaConfiguration = Convert-IcingaForwindowsManagementConsoleJSONConfig -Config $JsonInstallCmd; + + # First run our configuration values + $Success = Invoke-IcingaForWindowsManagementConsoleCustomConfig -IcingaConfiguration $IcingaConfiguration; + + if ($Success -eq $FALSE) { + return; + } + + # In case we use the director, we require to first fetch all basic values from the Self-Service API then + # require to register the host to fet the remaining content + if ($IcingaConfiguration.ContainsKey('IfW-DirectorSelfServiceKey') -And $IcingaConfiguration.ContainsKey('IfW-DirectorUrl')) { + Enable-IcingaFrameworkConsoleOutput; + Resolve-IcingaForWindowsManagementConsoleInstallationDirectorTemplate; + Resolve-IcingaForWindowsManagementConsoleInstallationDirectorTemplate -Register; + Disable-IcingaFrameworkConsoleOutput; + } else { + # Now load all remaining values we haven't set and define the defaults + Add-IcingaForWindowsInstallationAdvancedEntries; + } + + # Now apply our configuration again to ensure the defaults are overwritten again + # Suite a mess, but we can improve this later + $Success = Invoke-IcingaForWindowsManagementConsoleCustomConfig -IcingaConfiguration $IcingaConfiguration; + + if ($Success -eq $FALSE) { + return; + } + + Enable-IcingaFrameworkConsoleOutput; + + Start-IcingaForWindowsInstallation -Automated; + + # Set our "old" swap live again. By doing so, we can still continue our old + # configuration + Set-IcingaPowerShellConfig -Path 'Framework.Config.Swap' -Value $OldConfigSwap; + + return; + } + + while ($TRUE) { + + # Do nothing else anymore in case we are closing the management console + if ($global:Icinga.InstallWizard.Closing) { + break; + } + + $FrameworkInstalled = Get-IcingaPowerShellConfig -Path 'Framework.Installed'; + + if ($null -eq $FrameworkInstalled) { + $FrameworkInstalled = $FALSE; + } + + if ([string]::IsNullOrEmpty($global:Icinga.InstallWizard.NextCommand) -Or $global:Icinga.InstallWizard.NextCommand -eq 'Install-Icinga') { + Show-IcingaForWindowsInstallerMenu ` + -Header 'What do you want to do?' ` + -Entries @( + @{ + 'Caption' = 'Base Installation'; + 'Command' = 'Show-IcingaForWindowsInstallerMenuInstallWindows'; + 'Help' = 'Allows you to install Icinga for Windows with all required components and options.' + }, + @{ + 'Caption' = 'Component Manager'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleComponentManager'; + 'Help' = 'Allows you to manage components for Icinga for Windows.'; + 'AdminMenu' = $TRUE; + }, + @{ + 'Caption' = 'Settings'; + 'Command' = 'Show-IcingaForWindowsMenuManage'; + 'Help' = 'Allows you to modify your current Icinga for Windows installation.'; + }, + @{ + 'Caption' = 'Overview'; + 'Command' = 'Show-IcingaForWindowsMenuListEnvironment'; + 'Help' = 'Shows you an overview of your current Icinga for Windows installation, including installed components and system informations.'; + }, + @{ + 'Caption' = 'Icinga Shell'; + 'Command' = 'Invoke-IcingaForWindowsMenuStartIcingaShell'; + 'Help' = 'Starts an interactive PowerShell with all loaded Icinga for Windows components.'; + } + ) ` + -DefaultIndex 0; + } else { + $NextArguments = $global:Icinga.InstallWizard.NextArguments; + + if ($global:Icinga.InstallWizard.NextCommand.Contains(':')) { + $NextArguments = @{ + 'Value' = ($global:Icinga.InstallWizard.NextCommand.Split(':')[1]); + }; + $global:Icinga.InstallWizard.NextCommand = $global:Icinga.InstallWizard.NextCommand.Split(':')[0]; + } + + try { + if ($null -ne $NextArguments -And $NextArguments.Count -ne 0) { + & $global:Icinga.InstallWizard.NextCommand @NextArguments; + } else { + & $global:Icinga.InstallWizard.NextCommand; + } + } catch { + $ErrMsg = $_.Exception.Message; + + $global:Icinga.InstallWizard.LastError += [string]::Format('Failed to enter menu "{0}". Error "{1}', $global:Icinga.InstallWizard.NextCommand, $ErrMsg); + $global:Icinga.InstallWizard.NextCommand = 'Install-Icinga'; + $global:Icinga.InstallWizard.NextArguments = @{ }; + } + } + } + + if ($null -ne (Get-Command -Name 'Set-IcingaForWindowsManagementConsoleClosing' -ErrorAction SilentlyContinue)) { + Set-IcingaForWindowsManagementConsoleClosing -Completed; + } +} +function Get-IcingaForWindowsInstallerValuesFromStep() +{ + param ( + [string]$InstallerStep, + [string]$Parent + ); + + [array]$Values = @(); + + $Step = Get-IcingaForWindowsManagementConsoleMenu; + + if ([string]::IsNullOrEmpty($InstallerStep) -eq $FALSE) { + $Step = Get-IcingaForWindowsManagementConsoleAlias -Command $InstallerStep; + + if ([string]::IsNullOrEmpty($Parent) -eq $FALSE) { + $Step = [string]::Format('{0}:{1}', $Step, $Parent); + } + } + + if ($global:Icinga.InstallWizard.Config.ContainsKey($Step) -eq $FALSE) { + return @(); + } + + if ($null -eq $global:Icinga.InstallWizard.Config[$Step].Values) { + return @(); + } + + return [array]($global:Icinga.InstallWizard.Config[$Step].Values); +} +function Clear-CLIConsole() +{ + try { + Clear-Host -ErrorAction Stop; + } catch { + # Nothing to do + } +} +function Add-IcingaForWindowsManagementConsoleLastParent() +{ + $Menu = Get-IcingaForWindowsManagementConsoleAlias -Command (Get-IcingaForWindowsManagementConsoleMenu); + + if ($Menu -eq (Get-IcingaForWindowsInstallerLastParent)) { + return; + } + + # Do not add Yes/No Dialog to the list + if ($Menu -eq 'Show-IcingaWindowsManagementConsoleYesNoDialog') { + return; + } + + $global:Icinga.InstallWizard.LastParent.Add($Menu) | Out-Null; +} +function Add-IcingaForWindowsInstallerDisabledEntry() +{ + param ( + [string]$Name = '', + [string]$Reason = '' + ); + + if ([string]::IsNullOrEmpty($Reason)) { + $Reason = 'Generic disable message'; + } + + if ($Global:Icinga.InstallWizard.DisabledEntries.ContainsKey($Name)) { + $Global:Icinga.InstallWizard.DisabledEntries[$Name] = $Reason; + return; + } + + $Global:Icinga.InstallWizard.DisabledEntries.Add($Name, $Reason); +} +function Get-IcingaForWindowsInstallerDisabledEntry() +{ + param ( + [string]$Name = '' + ); + + if ($Global:Icinga.InstallWizard.DisabledEntries.ContainsKey($Name)) { + return ($Global:Icinga.InstallWizard.DisabledEntries[$Name]); + } + + return ''; +} +function Write-IcingaManagementConsoleCommand() +{ + param ( + $Entry = $null, + $Values = @() + ); + + if ($null -eq $Entry -Or $Entry.ContainsKey('Action') -eq $FALSE) { + return ''; + } + + [string]$PrintArguments = ''; + [string]$PrintCommand = ''; + [hashtable]$DefinedArgs = @{ }; + + if ($entry.Action.ContainsKey('Arguments') -And $entry.Action.Arguments.ContainsKey('-Command')) { + $PrintCommand = $Entry.Action.Arguments['-Command']; + if ($Entry.Action.Arguments.ContainsKey('-CmdArguments')) { + $DefinedArgs = $Entry.Action.Arguments['-CmdArguments']; + } + } elseif ($entry.Action.ContainsKey('Command')) { + $PrintCommand = $entry.Action.Command; + if ($Entry.Action.ContainsKey('Arguments')) { + $DefinedArgs = $Entry.Action.Arguments; + } + } + + $PrintArguments = ConvertTo-IcingaCommandArgumentString -Command $PrintCommand -CommandArguments $DefinedArgs; + $PrintArguments = $PrintArguments.Replace('$DefaultValues$', ((ConvertFrom-IcingaArrayToString -Array $Values -AddQuotes))); + + if ([string]::IsNullOrEmpty($PrintArguments) -eq $FALSE) { + $PrintArguments = [string]::Format(' {0}', $PrintArguments); + } + + Write-IcingaConsolePlain ([string]::Format('PS> {0}{1};', $PrintCommand, $PrintArguments)) -ForeColor Magenta; + Write-IcingaConsolePlain ''; +} +function Get-IcingaInternalPowerShellServicePassword() +{ + if ($null -eq $global:Icinga -Or $Global:Icinga.ContainsKey('InstallerServicePassword') -eq $FALSE) { + return $null; + } + + return $Global:Icinga.InstallerServicePassword; +} +function Write-IcingaforWindowsManagementConsoleConfigSwap() +{ + param ( + $Config = @{ } + ); + + [hashtable]$NewConfig = @{ }; + + # Remove passwords - do not store them inside our local config file + foreach ($entry in $Config.Keys) { + $Value = $Config[$entry]; + + $NewConfig.Add($entry, @{ }); + + foreach ($configElement in $Value.Keys) { + $confValue = $Value[$configElement]; + + if ($Value.Password -eq $TRUE -And $configElement -eq 'Values') { + $NewConfig[$entry].Add( + $configElement, + @( '***' ) + ); + } else { + $NewConfig[$entry].Add( + $configElement, + $confValue + ); + } + } + } + + Set-IcingaPowerShellConfig -Path 'Framework.Config.Swap' -Value $NewConfig; +} +function Remove-IcingaForWindowsInstallerConfigEntry() +{ + param ( + [string]$Menu, + [string]$Parent + ); + + $Menu = Get-IcingaForWindowsManagementConsoleAlias -Command $Menu; + + if ([string]::IsNullOrEmpty($Parent) -eq $FALSE) { + $Menu = [string]::Format('{0}:{1}', $Menu, $Parent); + } + + if ($global:Icinga.InstallWizard.Config.ContainsKey($Menu)) { + $global:Icinga.InstallWizard.Config.Remove($Menu); + } + + Write-IcingaforWindowsManagementConsoleConfigSwap -Config $global:Icinga.InstallWizard.Config; +} +function Test-IcingaForWindowsManagementConsoleUpdating() +{ + $UpdateFile = Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'framework.update'; + + return (Test-Path -Path $UpdateFile); +} +function Set-IcingaForWindowsManagementConsoleClosing() +{ + param ( + [switch]$Completed = $FALSE + ); + + if ($null -eq $Global:Icinga) { + return; + } + + if ($Global:Icinga.ContainsKey('InstallWizard') -eq $FALSE) { + return; + } + + if ($Global:Icinga.InstallWizard.ContainsKey('Closing') -eq $FALSE) { + return; + } + + $global:Icinga.InstallWizard.Closing = (-Not ([bool]$Completed)); +} +function Get-IcingaForWindowsInstallerLastParent() +{ + if ($global:Icinga.InstallWizard.LastParent.Count -ne 0) { + $Parent = $global:Icinga.InstallWizard.LastParent[-1]; + return $Parent; + } + + return $null; +} +function Reset-IcingaForWindowsManagementConsoleInstallationDirectorConfigModifyState() +{ + foreach ($entry in $Global:Icinga.InstallWizard.Config.Keys) { + + if ($entry -eq 'IfW-DirectorUrl' -Or $entry -eq 'IfW-DirectorSelfServiceKey') { + continue; + } + + $Global:Icinga.InstallWizard.Config[$entry].Modified = $FALSE; + } +} +function Set-IcingaForWindowsInstallerValuesFromStep() +{ + param ( + [string]$InstallerStep, + [string]$Parent, + [array]$Values = @() + ); + + $Step = Get-IcingaForWindowsManagementConsoleMenu; + + if ([string]::IsNullOrEmpty($InstallerStep) -eq $FALSE) { + $Step = Get-IcingaForWindowsManagementConsoleAlias -Command $InstallerStep; + + if ([string]::IsNullOrEmpty($Parent) -eq $FALSE) { + $Step = [string]::Format('{0}:{1}', $Step, $Parent); + } + } + + $global:Icinga.InstallWizard.Config[$Step].Values = $Values; +} +function Set-IcingaForWindowsManagementConsoleUpdating() +{ + param ( + [switch]$Completed = $FALSE + ); + + Set-IcingaForWindowsManagementConsoleClosing; + + $UpdateFile = Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'framework.update'; + if ($Completed) { + if (Test-IcingaForWindowsManagementConsoleUpdating) { + Remove-ItemSecure -Path $UpdateFile -Force -Retries 5 | Out-Null; + } + } else { + New-Item -Path $UpdateFile -ItemType File -Force | Out-Null; + } +} +function Convert-IcingaForwindowsManagementConsoleJSONConfig() +{ + param ( + $Config + ); + + [int]$Index = 0; + $MaxIndex = $Config.PSObject.Properties.Count; + [string]$Menu = ''; + [hashtable]$ConvertedConfig = @{ }; + + while ($Index -lt $MaxIndex.Count) { + foreach ($entry in $Config.PSObject.Properties) { + + if ($index -eq [int]$entry.Value.Index) { + $ConvertedConfig.Add( + $entry.Name, + @{ + 'Selection' = $entry.Value.Selection; + 'Values' = $entry.Value.Values; + 'Index' = $index; + 'Parent' = $entry.Value.Parent; + 'ParentEntry' = $entry.Value.ParentEntry; + 'Hidden' = $entry.Value.Hidden; + 'Password' = $entry.Value.Password; + 'Advanced' = $entry.Value.Advanced; + 'Modified' = $entry.Value.Modified; + } + ); + + if ($entry.Value.Advanced -eq $FALSE) { + $global:Icinga.InstallWizard.LastParent.Add($entry.Name) | Out-Null; + } + } + } + $Index += 1; + } + + return $ConvertedConfig; +} +function Invoke-IcingaForWindowsManagementConsoleCustomConfig() +{ + param ( + [hashtable]$IcingaConfiguration = @{ } + ); + + foreach ($cmd in $IcingaConfiguration.Keys) { + $cmdConfig = $IcingaConfiguration[$cmd]; + + if ($cmd.Contains(':')) { + continue; + } + + $cmdArguments = @{ + 'Automated' = $TRUE; + } + + if ($cmdConfig.ContainsKey('Values') -And $null -ne $cmdConfig.Values) { + $cmdArguments.Add('Value', $cmdConfig.Values); + } + if ($cmdConfig.ContainsKey('Selection') -And $null -ne $cmdConfig.Selection) { + $cmdArguments.Add('DefaultInput', $cmdConfig.Selection) + } + + try { + &$cmd @cmdArguments; + } catch { + Enable-IcingaFrameworkConsoleOutput; + Write-IcingaConsoleError 'Failed to apply installation configuration of command "{0}" and argument list{1}because of the following error: "{2}"' -Objects $cmd, ($cmdArguments | Out-String), $_.Exception.Message; + return $FALSE; + } + } + + return $TRUE; +} +function Add-IcingaForWindowsInstallerConfigEntry() +{ + param ( + [string]$Selection = $null, + [array]$Values = @(), + [switch]$Hidden = $FALSE, + [switch]$PasswordInput = $FALSE, + [switch]$OverwriteValues = $FALSE, + [string]$OverwriteMenu = '', + [string]$OverwriteParent = '', + [switch]$Advanced = $FALSE, + [switch]$NoConfigSwap = $FALSE + ); + + if ([string]::IsNullOrEmpty($OverwriteMenu) -eq $FALSE) { + $Step = $OverwriteMenu; + } else { + $Step = Get-IcingaForWindowsManagementConsoleMenu; + } + if ([string]::IsNullOrEmpty($OverwriteParent) -eq $FALSE) { + $Parent = $OverwriteParent; + } else { + $Parent = $global:Icinga.InstallWizard.ParentConfig; + } + + $ConfigIndex = $global:Icinga.InstallWizard.Config.Count; + $ParentEntry = $null; + + $Parent = Get-IcingaForWindowsManagementConsoleAlias -Command $Parent; + $Step = Get-IcingaForWindowsManagementConsoleAlias -Command $Step; + + if ([string]::IsNullOrEmpty($Parent) -eq $FALSE) { + $ParentEntry = $Parent.Split(':')[1]; + $Parent = $Parent.Split(':')[0]; + $Step = [string]::Format('{0}:{1}', $Step, $ParentEntry); + } + + if ($global:Icinga.InstallWizard.Config.ContainsKey($Step) -eq $FALSE) { + $global:Icinga.InstallWizard.Config.Add( + $Step, + @{ + 'Selection' = $Selection; + 'Values' = $Values + 'Index' = $ConfigIndex; + 'Parent' = $Parent; + 'ParentEntry' = $ParentEntry; + 'Hidden' = [bool]$Hidden; + 'Password' = [bool]$PasswordInput; + 'Advanced' = [bool]$Advanced; + 'Modified' = ($Advanced -eq $FALSE); + } + ); + } else { + $global:Icinga.InstallWizard.Config[$Step].Selection = $Selection; + $global:Icinga.InstallWizard.Config[$Step].Values = $Values; + $global:Icinga.InstallWizard.Config[$Step].Modified = $TRUE; + } + + if ($NoConfigSwap -eq $FALSE) { + Write-IcingaforWindowsManagementConsoleConfigSwap -Config $global:Icinga.InstallWizard.Config; + } +} +function Remove-IcingaForWindowsInstallerLastParent() +{ + if ($global:Icinga.InstallWizard.LastParent.Count -ne 0) { + $global:Icinga.InstallWizard.LastParent.RemoveAt($global:Icinga.InstallWizard.LastParent.Count - 1); + } +} +function Get-IcingaForWindowsManagementConsoleMenu() +{ + if ($null -eq $global:Icinga -Or $null -eq $global:Icinga.InstallWizard) { + return ''; + } + + if ([string]::IsNullOrEmpty($global:Icinga.InstallWizard.Menu) -Or $global:Icinga.InstallWizard.Menu -eq 'break') { + return ''; + } + + return (Get-IcingaForWindowsManagementConsoleAlias -Command $global:Icinga.InstallWizard.Menu); +} +function Set-IcingaForWindowsManagementConsoleMenu() +{ + param ( + [string]$Menu + ); + + if ([string]::IsNullOrEmpty($Menu) -Or $Menu -eq 'break') { + return; + } + + $global:Icinga.InstallWizard.Menu = (Get-IcingaForWindowsManagementConsoleAlias -Command $Menu); +} +function Get-IcingaForWindowsInstallerStepSelection() +{ + param ( + [string]$InstallerStep + ); + + if ([string]::IsNullOrEmpty($InstallerStep)) { + $InstallerStep = Get-IcingaForWindowsManagementConsoleMenu; + } else { + $InstallerStep = Get-IcingaForWindowsManagementConsoleAlias -Command $InstallerStep; + } + + if ($global:Icinga.InstallWizard.Config.ContainsKey($InstallerStep)) { + return $global:Icinga.InstallWizard.Config[$InstallerStep].Selection; + } + + return $null; +} +function Set-IcingaInternalPowerShellServicePassword() +{ + param ( + [SecureString]$Password = $null + ); + + if ($null -eq $global:Icinga) { + $Global:Icinga = @{ + 'InstallerServicePassword' = $Password; + } + + return; + } + + if ($Global:Icinga.ContainsKey('InstallerServicePassword') -eq $FALSE) { + $Global:Icinga.Add( + 'InstallerServicePassword', + $Password + ) + + return; + } + + $Global:Icinga.InstallerServicePassword = $Password; +} +<# +.SYNOPSIS + Determines the encoding of a file. + +.DESCRIPTION + The Get-FileEncoding function determines the encoding of a file by examining the file's byte order mark (BOM) or by checking if the file is ASCII or UTF8 without BOM. + +.PARAMETER Path + Specifies the path of the file to check. + +.EXAMPLE + Get-FileEncoding -Path "C:\path\to\file.txt" + Returns the encoding of the specified file. + +.OUTPUTS + System.String + The function returns a string representing the encoding of the file. Possible values are: + - UTF8-BOM: UTF-8 encoding with a byte order mark (BOM). + - Unicode: UTF-16 encoding (little-endian). + - BigEndianUnicode: UTF-16 encoding (big-endian). + - UTF7: UTF-7 encoding. + - UTF32: UTF-32 encoding. + - UTF8: UTF-8 encoding without a byte order mark (BOM). + - ASCII: ASCII encoding. + +.NOTES + This function requires PowerShell version 3.0 or later. +#> + +function Get-FileEncoding() +{ + param ( + [string]$Path = '' + ); + + if ([string]::IsNullOrEmpty($Path) -Or (Test-Path -Path $Path) -eq $FALSE) { + Write-IcingaConsoleError 'The specified file "{0}" was not found' -Objects $Path; + return $null; + } + + $FileStream = [System.IO.File]::OpenRead($Path); + $Bytes = New-Object Byte[] 4; + $FileStream.Read($Bytes, 0, 4) | Out-Null; + $FileStream.Close(); + + if ($Bytes[0] -eq 0xef -and $Bytes[1] -eq 0xbb -and $Bytes[2] -eq 0xbf) { + return 'UTF8-BOM'; + } elseif ($Bytes[0] -eq 0xff -and $Bytes[1] -eq 0xfe) { + return 'Unicode'; + } elseif ($Bytes[0] -eq 0xfe -and $Bytes[1] -eq 0xff) { + return 'BigEndianUnicode'; + } elseif ($Bytes[0] -eq 0x2b -and $Bytes[1] -eq 0x2f -and $Bytes[2] -eq 0x76) { + return 'UTF7'; + } elseif ($Bytes[0] -eq 0xff -and $Bytes[1] -eq 0xfe -and $Bytes[2] -eq 0x00 -and $Bytes[3] -eq 0x00) { + return 'UTF32'; + } else { + # Check if the file is ASCII or UTF8 without BOM + $Content = Get-Content -Encoding String -Path $Path -ErrorAction SilentlyContinue; + + # In case the file is empty, we assume it's UTF8 + if ([string]::IsNullOrEmpty($Content)) { + return 'UTF8'; + } + + $Bytes = [System.Text.Encoding]::UTF8.GetBytes($content); + + # Check each byte to see if it's outside the ASCII range + foreach ($byte in $Bytes) { + if ($byte -gt 127) { + return 'UTF8'; + } + } + } + + # This is the default encoding, as UTF8 without BOM could be valid ASCII + return 'ASCII'; +} +function Get-IcingaForWindowsManagementConsoleAlias() +{ + param ( + [string]$Command + ); + + if ([string]::IsNullOrEmpty($Command)) { + return ''; + } + + $ParentEntry = $null; + + if ($Command.Contains(':')) { + $KeyValue = $Command.Split(':'); + $Command = $KeyValue[0]; + $ParentEntry = $KeyValue[1]; + } + + $CommandAlias = Get-Alias -Definition $Command -ErrorAction SilentlyContinue; + + if ($null -ne $CommandAlias) { + $Command = $CommandAlias.Name; + } + + if ([string]::IsNullOrEmpty($ParentEntry) -eq $FALSE) { + $Command = [string]::Format('{0}:{1}', $Command, $ParentEntry); + } + + return $Command; +} +function Get-IcingaForWindowsManagementConsoleConfigurationString() +{ + param ( + [switch]$Compress = $FALSE + ); + + [hashtable]$Configuration = @{ }; + + foreach ($entry in $Global:Icinga.InstallWizard.Config.Keys) { + $Value = $Global:Icinga.InstallWizard.Config[$entry]; + + # Only print arguments that contain changes + if ($Value.Modified -eq $FALSE) { + continue; + } + + $Command = $entry; + $Parent = $null; + + # Handle configurations with parent dependencies + if ($entry.Contains(':')) { + $KeyValue = $entry.Split(':'); + $Command = $KeyValue[0]; + $Parent = $KeyValue[1]; + } + + if ($Configuration.ContainsKey($Command) -eq $FALSE) { + $Configuration.Add($Command, @{ }); + } + + # No parent exist, just add the values + if ([string]::IsNullOrEmpty($Parent)) { + if ($null -ne $Value.Values -And $Value.Values.Count -ne 0) { + [array]$ConfigValues = @(); + + foreach ($element in $Value.Values) { + if ([string]::IsNullOrEmpty($element) -eq $FALSE) { + $ConfigValues += $element; + } + } + + if ($ConfigValues.Count -ne 0) { + $Configuration[$Command].Add( + 'Values', $ConfigValues + ); + } + } + } else { + # Handle parent references + [hashtable]$ParentConfig = @{ }; + + if ($Configuration[$Command].ContainsKey('Values')) { + $ParentConfig = $Configuration[$Command].Values; + } + + $ParentConfig.Add( + $Value.ParentEntry, + $Value.Values + ); + + $Configuration[$Command].Values = $ParentConfig; + } + + if ($Configuration[$Command].ContainsKey('Selection')) { + continue; + } + + if ([string]::IsNullOrEmpty($Value.Selection) -eq $FALSE -And $Value.Selection -ne 'c') { + $Configuration[$Command].Add( + 'Selection', $Value.Selection + ); + } + + if ($Configuration[$Command].Count -eq 0) { + $Configuration.Remove($Command); + } + } + + return ($Configuration | ConvertTo-Json -Depth 100 -Compress:$Compress); +} +function Show-IcingaForWindowsInstallerMenu() +{ + param ( + [string]$Header, + [array]$Entries, + [array]$DefaultValues = @(), + [string]$DefaultIndex = $null, + [string]$ParentConfig = $null, + [switch]$AddConfig = $FALSE, + [switch]$PasswordInput = $FALSE, + [switch]$ContinueFirstValue = $FALSE, + [switch]$MandatoryValue = $FALSE, + [int]$ConfigLimit = -1, + [switch]$JumpToSummary = $FALSE, + [string]$ContinueFunction = $null, + [switch]$ConfigElement = $FALSE, + [switch]$HiddenConfigElement = $FALSE, + [switch]$ReadOnly = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE, + [switch]$PlainTextOutput = $FALSE, + [switch]$NoConfigSwap = $FALSE + ); + + if ($global:Icinga.InstallWizard.DirectorInstallError -eq $FALSE -And (Test-IcingaForWindowsInstallationHeaderPrint) -eq $FALSE -And (Get-IcingaFrameworkDebugMode) -eq $FALSE) { + Clear-CLIConsole; + } + + $PSCallStack = Get-PSCallStack; + $LastArguments = $null; + $LastCommand = $null; + + if ($PSCallStack.Count -gt 1) { + $LastCommand = $PSCallStack[1].Command; + $LastArguments = $PSCallStack[1].InvocationInfo.BoundParameters; + + # Only keep internal values as long as we are navigating within the same menu + if ($global:Icinga.InstallWizard.Menu -ne $LastCommand) { + $global:Icinga.InstallWizard.LastValues = @(); + } + + # Prevent from adding ourself because of stack calls. + # This should always be the "real" last command + if ($LastCommand -ne 'Show-IcingaForWindowsInstallerMenu') { + $global:Icinga.InstallWizard.Menu = $LastCommand; + } else { + $LastCommand = Get-IcingaForWindowsManagementConsoleMenu; + } + } + + $SelectionForCurrentMenu = Get-IcingaForWindowsInstallerStepSelection -InstallerStep (Get-IcingaForWindowsManagementConsoleMenu); + [bool]$EntryModified = $FALSE; + [int]$EntryIndex = 0; + [hashtable]$KnownIndexes = @{ }; + $LastParent = Get-IcingaForWindowsInstallerLastParent; + [array]$StoredValues = (Get-IcingaForWindowsInstallerValuesFromStep); + $global:Icinga.InstallWizard.ParentConfig = $ParentConfig; + $global:Icinga.InstallWizard.LastInput = $null; + + if ($LastParent -eq (Get-IcingaForWindowsManagementConsoleAlias -Command $LastCommand)) { + Remove-IcingaForWindowsInstallerLastParent; + $LastParent = Get-IcingaForWindowsInstallerLastParent; + } + + if (Test-IcingaForWindowsInstallationJumpToSummary) { + $SelectionForCurrentMenu = $null; + } + + if (($StoredValues.Count -eq 0 -And $DefaultValues.Count -ne 0) -Or $Automated) { + $StoredValues = $DefaultValues; + } + + if ($global:Icinga.InstallWizard.DeleteValues) { + $StoredValues = @(); + $global:Icinga.InstallWizard.DeleteValues = $FALSE; + } + + if ((Test-IcingaForWindowsInstallationHeaderPrint) -eq $FALSE) { + + $ConsoleHeaderLines = @( + 'Icinga for Windows Management Console', + 'Copyright $Copyright', + 'User environment $UserDomain\$Username', + 'Icinga PowerShell Framework $FrameworkVersion' + ); + + if ($global:Icinga.InstallWizard.AdminShell -eq $FALSE) { + $ConsoleHeaderLines += '[Warning]: Run this shell with administrative privileges to unlock all features' + } + + if ($PSVersionTable.PSVersion -lt '5.0.0.0') { + $ConsoleHeaderLines += ([string]::Format('[Warning]: Update to PowerShell version >=5.0 from currently {0} to unlock all features (like JEA)', $PSVersionTable.PSVersion.ToString(2))); + } + + Write-IcingaConsoleHeader -HeaderLines $ConsoleHeaderLines; + + Write-IcingaConsolePlain ''; + Write-IcingaConsolePlain $Header; + + Write-IcingaConsolePlain ''; + } + + foreach ($entry in $Entries) { + if ([string]::IsNullOrEmpty($entry.Caption) -eq $FALSE) { + [string]$CaptionIndention = ''; + if ($Entries.Count -ge 10 -And $EntryIndex -lt 10) { + $CaptionIndention = ' '; + } + + $Header = ([string]::Format('{0}[{1}] {2}', $CaptionIndention, $EntryIndex, $entry.Caption)); + $FontColor = 'Default'; + + if ((Test-IcingaForWindowsInstallationHeaderPrint) -eq $FALSE) { + # Highlight the default index in a different color + if ($DefaultIndex -eq $EntryIndex) { + $FontColor = 'Cyan'; + } + + # In case a entry is disabled, highlight it differently + if ($null -ne $entry.Disabled -And $entry.Disabled -eq $TRUE) { + $FontColor = 'DarkGray'; + } + + if ($Global:Icinga.InstallWizard.AdminShell -eq $FALSE -And $null -ne $entry.AdminMenu -And $entry.AdminMenu -eq $TRUE) { + $FontColor = 'DarkGray'; + } + + # Mark our previous selection in another color for better highlighting + if ($null -ne $SelectionForCurrentMenu -And $SelectionForCurrentMenu -eq $EntryIndex) { + $FontColor = 'Green'; + } + + Write-IcingaConsolePlain $Header -ForeColor $FontColor; + + if ($global:Icinga.InstallWizard.ShowCommand) { + Write-IcingaManagementConsoleCommand -Entry $entry -Values $StoredValues; + } + + if ($global:Icinga.InstallWizard.ShowHelp -And ([string]::IsNullOrEmpty($entry.Help)) -eq $FALSE) { + Write-IcingaConsolePlain ''; + Write-IcingaConsolePlain $entry.Help -ForeColor Magenta; + Write-IcingaConsolePlain ''; + } + } else { + if ((Get-IcingaForWindowsInstallationHeaderSelection) -eq $EntryIndex) { + $global:Icinga.InstallWizard.HeaderPreview = $entry.Caption; + return; + } + } + } + + $KnownIndexes.Add([string]$EntryIndex, $TRUE); + $EntryIndex += 1; + } + + if ((Test-IcingaForWindowsInstallationHeaderPrint)) { + return; + } + + if ($StoredValues.Count -ne 0) { + if ($PlainTextOutput) { + Write-IcingaConsolePlain (ConvertFrom-IcingaArrayToString -Array $StoredValues) -ForeColor Cyan; + } else { + if ($PasswordInput -eq $FALSE) { + Write-IcingaConsolePlain ([string]::Format(' {0}', (ConvertFrom-IcingaArrayToString -Array $StoredValues -AddQuotes))) -ForeColor Cyan; + } else { + Write-IcingaConsolePlain ([string]::Format(' {0}', (ConvertFrom-IcingaArrayToString -Array $StoredValues -AddQuotes -SecureContent))) -ForeColor Cyan; + } + } + } + + if ($AddConfig) { + if ($global:Icinga.InstallWizard.ShowCommand) { + Write-IcingaManagementConsoleCommand -Entry $Entries[0] -Values $StoredValues; + } + + if ($global:Icinga.InstallWizard.ShowHelp -And ([string]::IsNullOrEmpty($Entries[0].Help)) -eq $FALSE) { + Write-IcingaConsolePlain ''; + Write-IcingaConsolePlain $entry.Help -ForeColor Magenta; + } + } + + Write-IcingaConsolePlain ''; + Write-IcingaConsolePlain '[x] Exit' -NoNewLine; + + if ($global:Icinga.InstallWizard.DisplayAdvanced) { + if ($global:Icinga.InstallWizard.ShowAdvanced -eq $FALSE) { + Write-IcingaConsolePlain ' [a] Advanced' -NoNewLine; + } else { + Write-IcingaConsolePlain ' [a] Hide Advanced' -NoNewLine -ForeColor Green; + } + } + + Write-IcingaConsolePlain ' [c] Continue' -NoNewLine; + + if ($AddConfig -And $ReadOnly -eq $FALSE) { + Write-IcingaConsolePlain ' [d] Delete' -NoNewLine; + } + + if ($global:Icinga.InstallWizard.ShowHelp -eq $FALSE) { + Write-IcingaConsolePlain ' [h] Help' -NoNewLine; + } else { + Write-IcingaConsolePlain ' [h] Hide Help' -NoNewLine -ForeColor Green; + } + + if ($global:Icinga.InstallWizard.ShowCommand -eq $FALSE) { + Write-IcingaConsolePlain ' [l] Commands' -NoNewLine; + } else { + Write-IcingaConsolePlain ' [l] Hide Commands' -NoNewLine -ForeColor Green; + } + + Write-IcingaConsolePlain ' [m] Main' -NoNewLine; + + if ([string]::IsNullOrEmpty($LastParent) -eq $FALSE -Or $global:Icinga.InstallWizard.LastParent.Count -gt 1) { + Write-IcingaConsolePlain ' [p] Previous'; + } else { + Write-IcingaConsolePlain ''; + } + + $Prompt = 'Input'; + $CountPrompt = ([string]::Format('({0}/{1})', $StoredValues.Count, $ConfigLimit)); + if ($ConfigLimit -eq -1) { + $CountPrompt = ([string]::Format('({0} values)', $StoredValues.Count)); + } + + if ($AddConfig) { + $Prompt = ([string]::Format('Input {0}', $CountPrompt)); + # In case we reached the maximum entries, set c as default input for easier handling + if (($ConfigLimit -le $StoredValues.Count) -Or ($ContinueFirstValue -eq $TRUE -And $StoredValues.Count -ge 1)) { + $DefaultIndex = 'c'; + } + } + + if ([string]::IsNullOrEmpty($DefaultIndex) -eq $FALSE) { + if ((Test-Numeric $DefaultIndex)) { + $Prompt = [string]::Format('Input (Default {0} and c)', $DefaultIndex); + } else { + $Prompt = [string]::Format('Input (Default {0})', $DefaultIndex); + } + if ($AddConfig) { + $Prompt = [string]::Format('{0} {1}', $Prompt, $CountPrompt); + } + } + + Write-IcingaConsolePlain ''; + + if ($Global:Icinga.InstallWizard.LastError.Count -ne 0) { + foreach ($entry in $global:Icinga.InstallWizard.LastError) { + if ([string]::IsNullOrEmpty($entry)) { + continue; + } + Write-IcingaConsoleError -Message $entry; + } + $Global:Icinga.InstallWizard.LastError.Clear(); + Write-IcingaConsolePlain ''; + } + if ($Global:Icinga.InstallWizard.LastWarning.Count -ne 0) { + foreach ($entry in $Global:Icinga.InstallWizard.LastWarning) { + if ([string]::IsNullOrEmpty($entry)) { + continue; + } + Write-IcingaConsoleWarning -Message $entry; + } + $Global:Icinga.InstallWizard.LastWarning.Clear(); + Write-IcingaConsolePlain ''; + } + + if ([string]::IsNullOrEmpty($Global:Icinga.InstallWizard.LastNotice) -eq $FALSE) { + Write-IcingaConsoleNotice ($Global:Icinga.InstallWizard.LastNotice); + $Global:Icinga.InstallWizard.LastNotice = ''; + Write-IcingaConsolePlain ''; + } + + if ($Automated -eq $FALSE) { + $Result = Read-Host -Prompt $Prompt -AsSecureString:$PasswordInput; + + # Translate the value back to check what we used for input. We are not going to share + # the content however + if ($PasswordInput) { + $Result = ConvertFrom-IcingaSecureString -SecureString $Result; + } + + if ([string]::IsNullOrEmpty($Result) -And [string]::IsNullOrEmpty($DefaultIndex) -eq $FALSE) { + $Result = $DefaultIndex; + } + } else { + if ([string]::IsNullOrEmpty($DefaultIndex) -eq $FALSE) { + $Result = $DefaultIndex; + } + } + + $global:Icinga.InstallWizard.NextCommand = $LastCommand; + $global:Icinga.InstallWizard.NextArguments = $LastArguments; + $global:Icinga.InstallWizard.LastInput = $Result; + + switch ($Result) { + 'x' { + Clear-CLIConsole; + $global:Icinga.InstallWizard.Closing = $TRUE; + return; + }; + 'a' { + $global:Icinga.InstallWizard.ShowAdvanced = (-Not ($global:Icinga.InstallWizard.ShowAdvanced)); + return; + }; + 'h' { + $global:Icinga.InstallWizard.ShowHelp = (-Not ($global:Icinga.InstallWizard.ShowHelp)); + return; + }; + 'l' { + $global:Icinga.InstallWizard.ShowCommand = (-Not ($global:Icinga.InstallWizard.ShowCommand)); + return; + } + 'm' { + $global:Icinga.InstallWizard.NextCommand = $null; + $global:Icinga.InstallWizard.NextArguments = $null; + return; + } + 'p' { + if ([string]::IsNullOrEmpty($LastParent) -eq $FALSE) { + Remove-IcingaForWindowsInstallerLastParent; + + $global:Icinga.InstallWizard.NextCommand = $LastParent; + $global:Icinga.InstallWizard.NextArguments = $null; + return; + } + + $global:Icinga.InstallWizard.LastError += 'You cannot move to the previous menu from here.'; + if ($global:Icinga.InstallWizard.LastParent.Count -eq 0) { + $global:Icinga.InstallWizard.NextCommand = $null; + $global:Icinga.InstallWizard.NextArguments = $null; + return; + } + + return; + }; + 'd' { + if ($ReadOnly -eq $FALSE) { + $StoredValues = @(); + Clear-IcingaForWindowsInstallerValuesFromStep + $global:Icinga.InstallWizard.DeleteValues = $TRUE; + $global:Icinga.InstallWizard.LastValues = @(); + } + + return; + }; + 'c' { + if ($MandatoryValue -And $StoredValues.Count -eq 0) { + $global:Icinga.InstallWizard.LastError += 'You need to add at least one value!'; + + return; + } + + if ($AddConfig -eq $FALSE) { + $Result = $DefaultIndex; + $global:Icinga.InstallWizard.LastInput = $Result; + } + + $global:Icinga.InstallWizard.LastValues = $StoredValues; + + break; + }; + default { + if ($AddConfig) { + + if ($ConfigLimit -eq -1 -Or $ConfigLimit -gt $StoredValues.Count) { + if ([string]::IsNullOrEmpty($Result) -eq $FALSE) { + + $StoredValues += $Result; + if ($ConfigElement) { + Add-IcingaForWindowsInstallerConfigEntry -Values $StoredValues -Hidden:$HiddenConfigElement -PasswordInput:$PasswordInput -Advanced:$Advanced; + } + + $global:Icinga.InstallWizard.LastValues = $StoredValues; + } else { + if ($DefaultValues.Count -ne 0) { + $global:Icinga.InstallWizard.LastNotice = 'Empty values are not allowed! Resetting to default.'; + } else { + $global:Icinga.InstallWizard.LastError += 'You cannot add an empty value!'; + } + } + } else { + $global:Icinga.InstallWizard.LastError += [string]::Format('You can only add {0} value(s)', $ConfigLimit); + } + + return; + } + if ((Test-Numeric $Result) -eq $FALSE -Or $KnownIndexes.ContainsKey([string]$Result) -eq $FALSE) { + $global:Icinga.InstallWizard.LastError += [string]::Format('Invalid selection has been made: {0}', $Result); + + return; + } + + break; + }; + } + + [bool]$AdminMenu = $FALSE; + [bool]$DisabledMenu = $FALSE; + [string]$DisabledReason = ''; + $NextMenu = $null; + $NextArguments = @{ }; + $ActionCmd = $null; + $ActionArgs = $null; + + if ([string]::IsNullOrEmpty($Result) -eq $FALSE) { + if ($Result -eq 'c') { + if ([string]::IsNullOrEmpty($ContinueFunction) -eq $FALSE) { + $NextMenu = $ContinueFunction; + } else { + $NextMenu = $Entries[0].Command; + if ($null -ne $Entries[0].Disabled) { + $DisabledMenu = $Entries[0].Disabled; + if ($null -ne $Entries[0].DisabledReason) { + $DisabledReason = $Entries[0].DisabledReason; + } + } + + if ($null -ne $Entries[0].AdminMenu) { + $AdminMenu = $Entries[0].AdminMenu; + } + } + $ActionCmd = $Entries[0].Action.Command; + $ActionArgs = $Entries[0].Action.Arguments; + } else { + $NextMenu = $Entries[$Result].Command; + if ($null -ne $Entries[$Result].Disabled) { + $DisabledMenu = $Entries[$Result].Disabled; + if ($null -ne $Entries[$Result].DisabledReason) { + $DisabledReason = $Entries[$Result].DisabledReason; + } + } + + if ($null -ne $Entries[$Result].AdminMenu) { + $AdminMenu = $Entries[$Result].AdminMenu; + } + + if ($Entries[$Result].ContainsKey('Arguments')) { + $NextArguments = $Entries[$Result].Arguments; + } + $ActionCmd = $Entries[$Result].Action.Command; + $ActionArgs = $Entries[$Result].Action.Arguments; + } + } + + if ($AdminMenu -And (Test-AdministrativeShell) -eq $FALSE) { + $Global:Icinga.InstallWizard.LastNotice = [string]::Format('This menu is not enabled: [{0}] => You require to run this shell in administrative mode', $Result); + + return; + } + + if ($DisabledMenu) { + if ([string]::IsNullOrEmpty($DisabledReason) -eq $FALSE) { + $DisabledReason = [string]::Format(' => Reason: {0}', $DisabledReason); + } + $global:Icinga.InstallWizard.LastNotice = [string]::Format('This menu is not enabled: [{0}]{1}', $Result, $DisabledReason); + + return; + } + + if ([string]::IsNullOrEmpty($NextMenu)) { + $global:Icinga.InstallWizard.LastNotice = [string]::Format('This menu is not yet implemented: {0}', $Result); + + return; + } + + if ($Advanced -eq $FALSE) { + Add-IcingaForWindowsManagementConsoleLastParent; + } + + if ($JumpToSummary) { + $NextMenu = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + } + + if ($ConfigElement) { + Add-IcingaForWindowsInstallerConfigEntry ` + -InstallerStep (Get-IcingaForWindowsManagementConsoleMenu) ` + -Selection $Result ` + -Values $StoredValues ` + -Hidden:$HiddenConfigElement ` + -PasswordInput:$PasswordInput ` + -Advanced:$Advanced ` + -NoConfigSwap:$NoConfigSwap; + } + + # Reset Help View + $global:Icinga.InstallWizard.ShowHelp = $FALSE; + $global:Icinga.InstallWizard.ShowCommand = $FALSE; + + if ($NextMenu -eq 'break') { + return; + } + + $global:Icinga.InstallWizard.NextCommand = $NextMenu; + $global:Icinga.InstallWizard.NextArguments = $NextArguments; + + if ($Automated) { + return; + } + + # In case a action is defined, execute the given action + if ([string]::IsNullOrEmpty($ActionCmd) -eq $FALSE) { + if ($null -eq $ActionArgs -Or $ActionArgs.Count -eq 0) { + $ActionArgs = @{ }; + } else { + while ($TRUE) { + [bool]$ModifiedAllArgs = $TRUE; + foreach ($entry in $ActionArgs.Keys) { + if ($ActionArgs[$entry].GetType().Name -ne 'Boolean' -And $ActionArgs[$entry] -eq '$DefaultValues$') { + $ActionArgs[$entry] = $StoredValues; + $ModifiedAllArgs = $FALSE; + break; + } + } + + if ($ModifiedAllArgs) { + break; + } + } + } + + & $ActionCmd @ActionArgs | Out-Null; + } +} +function Clear-IcingaForWindowsInstallerValuesFromStep() +{ + $Step = Get-IcingaForWindowsManagementConsoleMenu; + + if ($global:Icinga.InstallWizard.Config.ContainsKey($Step) -eq $FALSE) { + return; + } + + if ($null -eq $global:Icinga.InstallWizard.Config[$Step].Values) { + return; + } + + $global:Icinga.InstallWizard.Config[$Step].Values = @(); +} +function Clear-IcingaForWindowsManagementConsolePaginationCache() +{ + $global:Icinga.InstallWizard.LastParent.Clear(); +} +function Test-IcingaForWindowsManagementConsoleDelete() +{ + if ([string]::IsNullOrEmpty($global:Icinga.InstallWizard.LastInput)) { + return $FALSE; + } + + if ($global:Icinga.InstallWizard.LastInput -eq 'd') { + return $TRUE; + } + + return $FALSE; +} +function Test-IcingaForWindowsManagementConsoleMenu() +{ + if ([string]::IsNullOrEmpty($global:Icinga.InstallWizard.LastInput)) { + return $FALSE; + } + + if ($global:Icinga.InstallWizard.LastInput -eq 'm') { + return $TRUE; + } + + return $FALSE; +} +function Show-IcingaWindowsManagementConsoleYesNoDialog() +{ + param ( + [string]$Caption = '', + [string]$Command = '', + [hashtable]$CmdArguments = @{ }, + [array]$Value = @(), + [string]$DefaultInput = '0', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + $LastParent = Get-IcingaForWindowsInstallerLastParent; + + Show-IcingaForWindowsInstallerMenu ` + -Header ([string]::Format('Are you sure you want to perform this action: "{0}"?', $Caption)) ` + -Entries @( + @{ + 'Caption' = 'No'; + 'Command' = $LastParent; + 'Help' = 'Do not apply the last action and return without doing anything'; + }, + @{ + 'Caption' = 'Yes'; + 'Command' = $LastParent; + 'Help' = "Apply the action and confirm it's execution"; + } + ) ` + -DefaultIndex $DefaultInput; + + if ((Get-IcingaForWindowsManagementConsoleLastInput) -eq '1') { + if ($null -eq $CmdArguments -Or $CmdArguments.Count -eq 0) { + & $Command | Out-Null; + } else { + & $Command @CmdArguments | Out-Null; + } + $global:Icinga.InstallWizard.LastNotice = [string]::Format('Action "{0}" has been executed', $Caption); + } +} +function Test-IcingaForWindowsManagementConsoleHelp() +{ + if ([string]::IsNullOrEmpty($global:Icinga.InstallWizard.LastInput)) { + return $FALSE; + } + + if ($global:Icinga.InstallWizard.LastInput -eq 'h') { + return $TRUE; + } + + return $FALSE; +} +function Get-IcingaForWindowsManagementConsoleLastInput() +{ + return $global:Icinga.InstallWizard.LastInput; +} +function Test-IcingaForWindowsManagementConsoleExit() +{ + if ([string]::IsNullOrEmpty($global:Icinga.InstallWizard.LastInput)) { + return $FALSE; + } + + if ($global:Icinga.InstallWizard.LastInput -eq 'x') { + return $TRUE; + } + + return $FALSE; +} +function Test-IcingaForWindowsManagementConsoleContinue() +{ + if ([string]::IsNullOrEmpty($global:Icinga.InstallWizard.LastInput)) { + return $FALSE; + } + + if ($global:Icinga.InstallWizard.LastInput -eq 'c') { + return $TRUE; + } + + return $FALSE; +} +function Test-IcingaForWindowsManagementConsolePrevious() +{ + if ([string]::IsNullOrEmpty($global:Icinga.InstallWizard.LastInput)) { + return $FALSE; + } + + if ($global:Icinga.InstallWizard.LastInput -eq 'p') { + return $TRUE; + } + + return $FALSE; +} +function Enable-IcingaForWindowsInstallationJumpToSummary() +{ + $global:Icinga.InstallWizard.JumpToSummary = $TRUE; +} +function Disable-IcingaForWindowsInstallationHeaderPrint() +{ + $global:Icinga.InstallWizard.HeaderPrint = $FALSE; +} +function Test-IcingaForWindowsInstallationHeaderPrint() +{ + return $global:Icinga.InstallWizard.HeaderPrint; +} +function Get-IcingaForWindowsInstallationHeaderSelection() +{ + return $global:Icinga.InstallWizard.HeaderSelection; +} +function Set-IcingaForWindowsInstallationHeaderSelection() +{ + param ( + [string]$Selection = $null + ); + + $global:Icinga.InstallWizard.HeaderSelection = $Selection; +} +function Test-IcingaForWindowsInstallationJumpToSummary() +{ + return $global:Icinga.InstallWizard.JumpToSummary; +} +function Enable-IcingaForWindowsInstallationHeaderPrint() +{ + $global:Icinga.InstallWizard.HeaderPrint = $TRUE; +} +function Disable-IcingaForWindowsInstallationJumpToSummary() +{ + $global:Icinga.InstallWizard.JumpToSummary = $FALSE; +} +function Test-IcingaForWindowsInstallerParentEndpoints() +{ + $Selection = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectConnection'; + + # Agents connects, therefor validate this setting. 1 only accepts connections from parent + if ($Selection -ne 1) { + $Values = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentNodes'; + $NetworkMap = @{ }; + [bool]$HasErrors = $FALSE; + + foreach ($endpoint in $Values) { + $Address = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses' -Parent $endpoint; + $TestAddress = $Address; + if ($null -eq $Address -Or $Address.Count -eq 0) { + $TestAddress = $endpoint; + } + + $Resolved = Convert-IcingaEndpointsToIPv4 -NetworkConfig $TestAddress; + + if ($Resolved.HasErrors) { + $Address = $endpoint; + $HasErrors = $TRUE; + } else { + $Address = $Resolved.Network[0]; + } + + $NetworkMap.Add( + $endpoint, + @{ + 'Endpoint' = $endpoint; + 'Address' = $Address; + 'Error' = $Resolved.HasErrors; + } + ); + + Add-IcingaForWindowsInstallerConfigEntry -Selection 'c' -Values $Address -OverwriteValues ` + -OverwriteMenu 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses' ` + -OverwriteParent ([string]::Format('Show-IcingaForWindowsInstallerMenuEnterIcingaParentNodes:{0}', $endpoint)); + } + + if ($HasErrors) { + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses'; + $global:Icinga.InstallWizard.NextArguments = @{ 'Value' = $NetworkMap }; + return; + } + } + + $global:Icinga.InstallWizard.NextCommand = 'Add-IcingaForWindowsInstallationAdvancedEntries'; +} +function Add-IcingaForWindowsInstallationAdvancedEntries() +{ + $ConnectionConfiguration = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectConnection'; + + $OpenFirewall = '1'; # Do not open firewall + if ($ConnectionConfiguration -ne '0') { + $OpenFirewall = '0'; + } + + Disable-IcingaFrameworkConsoleOutput; + + Show-IcingaForWindowsInstallationMenuEnterIcingaPort -Automated -Advanced; + Show-IcingaForWindowsInstallerMenuSelectOpenWindowsFirewall -DefaultInput $OpenFirewall -Automated -Advanced; + # Only apply the certificate menu in case it was not selected previously, if + # we choose IfW-Connection 1 for example, which tells the Parent to connect to Agent only + if ($null -eq (Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectCertificate')) { + Show-IcingaForWindowsInstallerMenuSelectCertificate -Automated -Advanced; + } + Show-IcingaForWindowsInstallerMenuSelectForceCertificateGeneration -Automated -Advanced; + Show-IcingaForWindowsInstallerMenuSelectGlobalZones -Automated -Advanced; + Show-IcingaForWindowsInstallationMenuEnterCustomGlobalZones -Automated -Advanced; + Show-IcingaForWindowsInstallationMenuEnterIcingaAgentVersion -Automated -Advanced; + Show-IcingaForWindowsInstallerMenuSelectInstallIcingaAgent -Automated -Advanced; + Show-IcingaForWindowsInstallationMenuEnterIcingaAgentDirectory -Automated -Advanced; + Show-IcingaForWindowsInstallationMenuEnterIcingaAgentUser -Automated -Advanced; + Show-IcingaForWindowsInstallerMenuSelectInstallIcingaPlugins -Automated -Advanced; + Show-IcingaForWindowsInstallerMenuSelectInstallIcingaForWindowsService -Automated -Advanced; + Show-IcingaForWindowsInstallationMenuEnterWindowsServiceDirectory -Automated -Advanced; + Show-IcingaForWindowsInstallationMenuStableRepository -Automated -Advanced; + Show-IcingaForWindowsInstallerMenuSelectInstallJEAProfile -Automated -Advanced; + Show-IcingaForWindowsInstallationMenuEnterIcingaCAServer -Automated -Advanced; + Show-IcingaForWindowsInstallerMenuSelectInstallApiChecks -Automated -Advanced; + Show-IcingaForWindowsInstallerMenuSelectServiceRecovery -Automated -Advanced; + Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorSelfServiceConfig -Automated -Advanced; + + Enable-IcingaFrameworkConsoleOutput; + + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsInstallerConfigurationSummary'; +} +function Show-IcingaForWindowsInstallationMenuEnterCustomGlobalZones() +{ + param ( + [array]$Value = @( '' ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please add all your global zones you want to add:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'If you have configured custom global zones you require on this Windows host, please add all of them in this list. Default zones like "director-global" and "global-templates" should not be configured here'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -DefaultValues @( $Value ) ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-CustomZones' -Value 'Show-IcingaForWindowsInstallationMenuEnterCustomGlobalZones'; +function Show-IcingaForWindowsInstallerMenuSelectGlobalZones() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '0', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Which default Icinga global zones do you want to add?' ` + -Entries @( + @{ + 'Caption' = 'Add "director-global" and "global-templates" zones'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Adds both global zones, "director-global" and "global-templates" as the default installer would. Depending on your environment these might be mandatory.'; + }, + @{ + 'Caption' = 'Add "director-global" zone'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Only add the global zone "director-global" to your configuration which might be required if you are using the Icinga Director, depending on your configuration.'; + }, + @{ + 'Caption' = 'Add "global-templates" zone'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Only add the global zone "global-templates" to your configuration'; + }, + @{ + 'Caption' = 'Add no default global zone'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Do not add any default global zones to your configuration'; + } + ) ` + -DefaultIndex $DefaultInput ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-GlobalZones' -Value 'Show-IcingaForWindowsInstallerMenuSelectGlobalZones'; +function Show-IcingaForWindowsInstallerMenuSelectCertificate() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '0', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'How do you want to create the Icinga certificate?' ` + -Entries @( + @{ + 'Caption' = 'Sign certificate manually on the Icinga CA master'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'This option will not require you to provide additional details for certificate generation and only require a connection to/from this host. You will have to sign the certificate manually on the Icinga CA master with "icinga2 ca sign "'; + }, + @{ + 'Caption' = 'Sign certificate with a ticket'; + 'Command' = 'Show-IcingaForWindowsInstallerMenuEnterIcingaTicket'; + 'Help' = 'By selecting this option, this host will connect to a parent Icinga node and sign the certificate with a ticket you have to provide in the next step'; + }, + @{ + 'Caption' = 'Sign certificate with local ca.crt'; + 'Command' = 'Show-IcingaForWindowsInstallerMenuEnterIcingaCAFile'; + 'Help' = 'This will allow you to sign the certificate for this host directly on this machine. For this you will have to store your Icinga ca.crt somewhere accessible to this system. In the next step you are asked to provide the path to the location of your ca.crt'; + } + ) ` + -DefaultIndex $DefaultInput ` + -JumpToSummary:$FALSE ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; + + # Make sure we delete configuration no longer required + switch (Get-IcingaForWindowsManagementConsoleLastInput) { + '0' { + Remove-IcingaForWindowsInstallerConfigEntry -Menu 'Show-IcingaForWindowsInstallerMenuEnterIcingaTicket'; + Remove-IcingaForWindowsInstallerConfigEntry -Menu 'Show-IcingaForWindowsInstallerMenuEnterIcingaCAFile'; + break; + }; + '1' { + Remove-IcingaForWindowsInstallerConfigEntry -Menu 'Show-IcingaForWindowsInstallerMenuEnterIcingaCAFile'; + break; + }; + '2' { + Remove-IcingaForWindowsInstallerConfigEntry -Menu 'Show-IcingaForWindowsInstallerMenuEnterIcingaTicket'; + break; + }; + } + + # By Default, we are not jumping to the Summary on this menu but will require this in case + # we choose CAFile selection and are on the summary page, as then we do not want to be prompted + # for the Hostname again and require to tell the CA menu, that we should directly move to the + # summary page again + $LastInput = Get-IcingaForWindowsManagementConsoleLastInput; + + if ($LastInput -eq '2' -And $JumpToSummary) { + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsInstallerMenuEnterIcingaCAFile'; + $global:Icinga.InstallWizard.NextArguments = @{ 'JumpToSummary' = $TRUE; }; + } +} + +Set-Alias -Name 'IfW-Certificate' -Value 'Show-IcingaForWindowsInstallerMenuSelectCertificate'; +function Show-IcingaForWindowsInstallerMenuEnterIcingaTicket() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + $Advanced = $TRUE; + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter your ticket for signing the Icinga certificate:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'The ticket required for signing your local Icinga certificate. You can get the ticket from the Icinga Director for this host or from your Icinga CA master by running "icinga2 pki ticket --cn "'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues @( $Value ) ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-Ticket' -Value 'Show-IcingaForWindowsInstallerMenuEnterIcingaTicket'; +function Show-IcingaForWindowsInstallerMenuEnterIcingaCAFile() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + $Advanced = $TRUE; + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter the path to your ca.crt file. This can be a local, network share or web address:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'To sign certificates locally you can copy the Icinga CA master "ca.crt" file (normally located at "/var/lib/icinga2/ca") to a location you can access from this host. Enter the full path on where you stored the "ca.crt" file. You can provide a local path "C:\users\public\ca.crt", a network share "\\share.example.com\icinga\ca.crt" or a web address "https://example.com/icinga/ca.crt"'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues @( $Value ) ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; + + # By default, we are never prompt to enter the CA target path, unless we are connecting + # from Parent->Agent, which is option 1 von IfW-Connection + # In case we run this configuration, we are forwarded from that menu to here and require + # to enter the hostname in addition + if ((Test-IcingaForWindowsManagementConsoleContinue) -And $JumpToSummary -eq $FALSE) { + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsInstallerMenuSelectHostname'; + } +} + +Set-Alias -Name 'IfW-CAFile' -Value 'Show-IcingaForWindowsInstallerMenuEnterIcingaCAFile'; +function Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorSelfServiceConfig() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + # Ensure we simply set the global variable for the Config in case we run in automation mode + if ($Automated) { + if ($null -ne $Value -And $null -ne $Value[0]) { + $Global:Icinga.InstallWizard.DirectorSelfServiceConfig = ConvertFrom-Json -InputObject $Value[0] -ErrorAction Stop; + return; + } + } + + # Set the default if no value ist set + if ($Value -IsNot [array] -Or $null -eq $Value -or $Value.Count -eq 0) { + $Value.Clear(); + if ($null -eq $Global:Icinga.InstallWizard.DirectorSelfServiceConfig) { + $Value += '{ "address": "$ifw.hostaddress$" }'; + } else { + $Value += ConvertTo-Json -InputObject $Global:Icinga.InstallWizard.DirectorSelfServiceConfig -Depth 100 -Compress; + } + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'You can update the Icinga Director Self-Service config in this section. USE WITH CARE!' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'This is the configuration JSON-Object for the Icinga Director Self-Service API. You can set a custom IP-Address or define the display name of an object with "display_name" as key. Use this methid with caution! Not all configuration elements in general possible are accessible by using the Self-Service keys.'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues @( $Value ) ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; + + # Fetch the current JSON-String inserted by the user + [string]$ConfigString = Get-IcingaForWindowsInstallerValuesFromStep; + + if ([string]::IsNullOrEmpty($ConfigString) -eq $FALSE) { + try { + # Validate that our JSON is correct + $Global:Icinga.InstallWizard.DirectorSelfServiceConfig = ConvertFrom-Json -InputObject $ConfigString -ErrorAction Stop; + } catch { + # Set some defaults to ensure we don't break the installer + Write-IcingaConsoleError ([string]::Format('The provided Icinga Director Self Service configuration "{0}" does not appear to be a valid JSON-String. E.g.: {{ "address": "$ifw.hostaddress$" }} without leading and ending "" before and after {{ }}', $ConfigString)); + $Global:Icinga.InstallWizard.DirectorSelfServiceConfig = $null; + Set-IcingaForWindowsInstallerValuesFromStep -Values @( '{ }' ); + } + } else { + $Global:Icinga.InstallWizard.DirectorSelfServiceConfig = $null; + } +} + +Set-Alias -Name 'IfW-DirectorSelfServiceConfig' -Value 'Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorSelfServiceConfig'; +function Show-IcingaForWindowsManagementConsoleInstallationDirectorRegisterHost() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '0', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Do you want to register the host right now inside the Icinga Director? This will show missing configurations.' ` + -Entries @( + @{ + 'Caption' = 'Do not register host inside Icinga Director'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'If you do not want to modify extended properties for this host and use default values from the Icinga Director, based on the Self-Service API configuration, use this option and complete the installation process afterwards.'; + }, + @{ + 'Caption' = 'Register host inside Icinga Director'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'You can select this option to register the host within the Icinga Director right now, unlocking more advanced configurations for this host like "Parent Zone", "Parent Nodes" and "Parent Node Addresses". Please note that the installation process will fail if you continue the installer, if you do not register it.'; + 'Action' = @{ + 'Command' = 'Resolve-IcingaForWindowsManagementConsoleInstallationDirectorTemplate'; + 'Arguments' = @{ + '-Register' = $TRUE; + } + } + } + ) ` + -DefaultIndex $DefaultInput ` + -JumpToSummary:$FALSE ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-DirectorRegisterHost' -Value 'Show-IcingaForWindowsManagementConsoleInstallationDirectorRegisterHost'; +function Resolve-IcingaForWindowsManagementConsoleInstallationDirectorTemplate() +{ + param ( + [switch]$Register = $FALSE + ); + + $DirectorUrl = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorUrl'; + $SelfServiceKey = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorSelfServiceKey'; + $HostRegisterSetting = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectHostname'; + $CustomHostname = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallationMenuEnterCustomHostname'; + $UsedEnteredKey = $SelfServiceKey; + + # Once we run this menu, we require to reset everything to have a proper state + if ($Register -eq $FALSE) { + $global:Icinga.InstallWizard.Config = @{ }; + + Add-IcingaForWindowsInstallerConfigEntry -Selection 'c' -Values $DirectorUrl -OverwriteValues -OverwriteMenu 'Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorUrl'; + Add-IcingaForWindowsInstallerConfigEntry -Selection 'c' -Values $SelfServiceKey -OverwriteValues -OverwriteMenu 'Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorSelfServiceKey'; + + if ($null -ne $HostRegisterSetting) { + Add-IcingaForWindowsInstallerConfigEntry -Selection $HostRegisterSetting -OverwriteValues -OverwriteMenu 'Show-IcingaForWindowsInstallerMenuSelectHostname' + if ($HostRegisterSetting -eq '6') { + Add-IcingaForWindowsInstallerConfigEntry -Selection 'c' -Values $CustomHostname -OverwriteValues -OverwriteMenu 'Show-IcingaForWindowsInstallationMenuEnterCustomHostname'; + } + } + } else { + $Global:Icinga.InstallWizard.DirectorRegisteredHost = $TRUE; + $HostnameType = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectHostname'; + $CustomHostname = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallationMenuEnterCustomHostname'; + $Hostname = ''; + + switch ($HostnameType) { + '0' { + $Hostname = (Get-IcingaHostname -AutoUseFQDN 1); + break; + }; + '1' { + $Hostname = (Get-IcingaHostname -AutoUseFQDN 1 -LowerCase 1); + break; + }; + '2' { + $Hostname = (Get-IcingaHostname -AutoUseFQDN 1 -UpperCase 1); + break; + }; + '3' { + $Hostname = (Get-IcingaHostname -AutoUseHostname 1); + break; + }; + '4' { + $Hostname = (Get-IcingaHostname -AutoUseHostname 1 -LowerCase 1); + break; + }; + '5' { + $Hostname = (Get-IcingaHostname -AutoUseHostname 1 -UpperCase 1); + break; + }; + '6' { + $Hostname = $CustomHostname; + break; + } + } + + [bool]$RegisterFailed = $FALSE; + + try { + $SelfServiceKey = Register-IcingaDirectorSelfServiceHost -DirectorUrl $DirectorUrl -ApiKey $SelfServiceKey -Hostname $Hostname; + if ([string]::IsNullOrEmpty($SelfServiceKey) -eq $FALSE) { + $UsedEnteredKey = $SelfServiceKey; + } else { + $RegisterFailed = $TRUE; + } + } catch { + $RegisterFailed = $TRUE; + } + + if ($RegisterFailed) { + Write-IcingaConsoleNotice 'Host seems already to be registered within Icinga Director. Trying local Api key if present' + $SelfServiceKey = Get-IcingaPowerShellConfig -Path 'IcingaDirector.SelfService.ApiKey'; + + if ([string]::IsNullOrEmpty($SelfServiceKey)) { + Write-IcingaConsoleNotice 'No local Api key was found and using your provided template key failed. Please ensure the host is not already registered and drop the set Self-Service key within the Icinga Director for this host.' + } + } + + Add-IcingaForWindowsInstallerConfigEntry -Selection 'c' -Values $UsedEnteredKey -OverwriteValues -OverwriteMenu 'Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorSelfServiceKey'; + } + + try { + $DirectorConfig = Get-IcingaDirectorSelfServiceConfig -DirectorUrl $DirectorUrl -ApiKey $SelfServiceKey; + } catch { + Set-IcingaForWindowsManagementConsoleMenu 'Show-IcingaForWindowsInstallerConfigurationSummary'; + $global:Icinga.InstallWizard.LastError += 'Failed to fetch host configuration with the given Director Url and Self-Service key. Please ensure the template key is correct and in case a previous host key was used, that it matches the one configured within the Icinga Director. In case this form was loaded previously with a key, it might be that the host key is no longer valid and requires to be dropped. In addition please ensure that this host can connect to the Icinga Director and the SSL certificate is trusted. Otherwise run "Enable-IcingaUntrustedCertificateValidation" before starting the management console. Otherwise modify the "DirectorSelfServiceKey" configuration element above with the correct key and try again.'; + $global:Icinga.InstallWizard.DirectorError = $global:Icinga.InstallWizard.LastError; + $global:Icinga.InstallWizard.DirectorInstallError = $TRUE; + return; + } + + $global:Icinga.InstallWizard.DirectorInstallError = $FALSE; + $global:Icinga.InstallWizard.DirectorError = ''; + + # No we need to identify which host selection is matching our config + $HostnameSelection = 1; + $InstallPluginsSelection = 0; + $InstallServiceSelection = 0; + $WindowsFirewallSelection = 1; + $AgentVersion = 'release'; + $InstallIcingaAgent = 0; + + $ServiceUserName = $DirectorConfig.icinga_service_user; + $AgentPackageSelection = 1; #Always use custom source + $IcingaPort = $DirectorConfig.agent_listen_port; + $GlobalZones = @(); + $IcingaParents = @(); + $IcingaParentAddresses = New-Object PSCustomObject; + $ParentZone = ''; + $MasterAddress = ''; + $Ticket = ''; + $DirectorHostRegister = (Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsManagementConsoleInstallationDirectorRegisterHost'); + + if ([string]::IsNullOrEmpty($ServiceUserName)) { + $ServiceUserName = 'NT Authority\NetworkService'; + } + + if ($null -eq $DirectorHostRegister) { + $DirectorHostRegister = 0; + } + + if (Test-IcingaPowerShellConfigItem -ConfigObject $DirectorConfig -ConfigKey 'agent_version') { + $AgentVersion = $DirectorConfig.agent_version; + } else { + $InstallIcingaAgent = 1; + } + + if ($DirectorUrl.ToLower().Contains('https://') -Or $DirectorUrl.ToLower().Contains('http://')) { + $MasterAddress = $DirectorUrl.Split('/')[2]; + } else { + $MasterAddress = $DirectorUrl.Split('/')[0]; + } + + if ($Register) { + if ($null -ne $DirectorConfig.agent_add_firewall_rule -And $DirectorConfig.agent_add_firewall_rule) { + # Open Windows Firewall + $WindowsFirewallSelection = 0; + } + + if ($null -ne $DirectorConfig.global_zones) { + $GlobalZones = $DirectorConfig.global_zones; + } + + if ($null -ne $DirectorConfig.parent_endpoints) { + $IcingaParents = $DirectorConfig.parent_endpoints; + } + + if ($null -ne $DirectorConfig.endpoints_config) { + [int]$Index = 0; + foreach ($entry in $DirectorConfig.endpoints_config) { + $IcingaParentAddresses | Add-Member -MemberType NoteProperty -Name ($IcingaParents[$Index]) -Value (($entry.Split(';')[0])); + $Index += 1; + } + } + + if ($null -ne $DirectorConfig.parent_zone) { + $ParentZone = $DirectorConfig.parent_zone; + } + + $Ticket = Get-IcingaDirectorSelfServiceTicket -DirectorUrl $DirectorUrl -ApiKey $SelfServiceKey; + } + + if ($DirectorConfig.fetch_agent_fqdn) { + switch ($DirectorConfig.transform_hostname) { + '0' { + # FQDN as it is + $HostnameSelection = 0; + break; + }; + '1' { + # FQDN to lowercase + $HostnameSelection = 1; + break; + }; + '2' { + # FQDN to uppercase + $HostnameSelection = 2; + break; + } + } + } elseif ($DirectorConfig.fetch_agent_name) { + switch ($DirectorConfig.transform_hostname) { + '0' { + # Hostname as it is + $HostnameSelection = 3; + break; + }; + '1' { + # Hostname to lowercase + $HostnameSelection = 4; + break; + }; + '2' { + # Hostname to uppercase + $HostnameSelection = 5; + break; + } + } + } + + if ($DirectorConfig.install_framework_service -eq 0) { + # Do not install + $InstallServiceSelection = 1; + } else { + # Install the service from our repository + $InstallServiceSelection = 0; + } + + if ($DirectorConfig.install_framework_plugins -eq 0) { + # Do not install + $InstallPluginsSelection = 1; + } else { + # Install the plugins from our repository + $InstallPluginsSelection = 0; + } + + Disable-IcingaFrameworkConsoleOutput; + + $HostRegisterSetting = Get-IcingaForWindowsInstallerStepSelection -InstallerStep 'Show-IcingaForWindowsInstallerMenuSelectHostname'; + + if ($null -ne $HostRegisterSetting -And $HostnameSelection -ne $HostRegisterSetting) { + $HostnameSelection = $HostRegisterSetting; + } + + Show-IcingaForWindowsInstallerMenuSelectHostname -DefaultInput $HostnameSelection -Automated; + + if ($HostnameSelection -eq '6') { + Show-IcingaForWindowsInstallationMenuEnterCustomHostname -Value $CustomHostname -Automated; + } + + Add-IcingaForWindowsInstallationAdvancedEntries; + Disable-IcingaFrameworkConsoleOutput; + + Show-IcingaForWindowsInstallerMenuSelectInstallIcingaPlugins -DefaultInput $InstallPluginsSelection -Value @() -Automated; + Show-IcingaForWindowsInstallerMenuSelectInstallIcingaForWindowsService -DefaultInput $InstallServiceSelection -Value @() -Automated; + Show-IcingaForWindowsInstallerMenuSelectOpenWindowsFirewall -DefaultInput $WindowsFirewallSelection -Value @() -Automated; + + if ($Register) { + Show-IcingaForWindowsInstallationMenuEnterCustomGlobalZones -Value $GlobalZones -Automated; + Show-IcingaForWindowsInstallerMenuEnterIcingaParentNodes -Value $IcingaParents -Automated; + Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses -Value $IcingaParentAddresses -Automated; + Show-IcingaForWindowsInstallerMenuEnterIcingaParentZone -Value $ParentZone -Automated; + } + + Show-IcingaForWindowsInstallationMenuEnterIcingaCAServer -Automated -Value $MasterAddress; + Show-IcingaForWindowsInstallationMenuEnterIcingaAgentUser -Automated -Value $ServiceUserName; + + Show-IcingaForWindowsInstallerMenuSelectCertificate -Automated -DefaultInput '1'; + Show-IcingaForWindowsInstallerMenuEnterIcingaTicket -Automated -Value $Ticket; + + Show-IcingaForWindowsInstallerMenuSelectInstallIcingaAgent -Automated -DefaultInput $InstallIcingaAgent; + Show-IcingaForWindowsInstallationMenuEnterIcingaAgentVersion -Automated -Value $AgentVersion; + Show-IcingaForWindowsManagementConsoleInstallationDirectorRegisterHost -DefaultInput $DirectorHostRegister -Automated; + + Enable-IcingaFrameworkConsoleOutput; + Reset-IcingaForWindowsManagementConsoleInstallationDirectorConfigModifyState; +} +function Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorSelfServiceKey() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + if ($null -eq $Value -or $Value.Count -eq 0) { + $LocalApiKey = Get-IcingaPowerShellConfig -Path 'IcingaDirector.SelfService.ApiKey'; + if ([string]::IsNullOrEmpty($LocalApiKey) -eq $FALSE) { + $Value += $LocalApiKey; + } + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter the Self-Service API key for the Host-Template to use:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'This is the Self-Service API for the host template to use. To get this, you will have to set the host template to be an "Icinga 2 Agent" template inside the Icinga Director. Afterwards you see an "Agent" tab on the top right navigation, providing you with the key. In case you entered this menu for the first time and see a key already present, this means the installer already run once and therefor you will be presented with your host key. If a host is already present within the Icinga Director, you can also use the "Agent" tab to get the key of this host directly to enter here'; + 'Action' = @{ + 'Command' = 'Resolve-IcingaForWindowsManagementConsoleInstallationDirectorTemplate'; + 'Arguments' = @{ + '-Register' = $FALSE; + } + } + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues $Value ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-DirectorSelfServiceKey' -Value 'Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorSelfServiceKey'; +function Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorUrl() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + $Global:Icinga.InstallWizard.DirectorSelfService = $TRUE; + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter the URL pointing to your Icinga Director module:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorSelfServiceKey'; + 'Help' = 'The Icinga Web 2 url pointing directly to the root of the Icinga Director module. Example: "https://example.com/icingaweb2/director" or "https://icinga.example.com/director"'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues $Value ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-DirectorUrl' -Value 'Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorUrl'; +function Show-IcingaForWindowsInstallerMenuSelectInstallJEAProfile() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '2', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + if ($PSVersionTable.PSVersion -lt (New-IcingaVersionObject -Version 5, 0)) { + Add-IcingaForWindowsInstallerDisabledEntry -Name 'IfW-InstallJEAProfile' -Reason ([string]::Format('PowerShell version "{0}" is lower than 5.0', $PSVersionTable.PSVersion.ToString(2))); + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please select if you want to install the JEA profile for the assigned service user or to create a managed user' ` + -Entries @( + @{ + 'Caption' = 'Install JEA Profile'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Installs the Icinga for Windows JEA profile for the specified service user'; + }, + @{ + 'Caption' = 'Install JEA Profile with managed user "icinga"'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Installs the Icinga for Windows JEA profile with a newly created, managed user "icinga". This will override your service and service password configuration'; + }, + @{ + 'Caption' = 'Do not install JEA Profile'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Do not install the Icinga for Windows JEA profile'; + } + ) ` + -DefaultIndex $DefaultInput ` + -JumpToSummary:$FALSE ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-InstallJEAProfile' -Value 'Show-IcingaForWindowsInstallerMenuSelectInstallJEAProfile'; +function Show-IcingaForWindowsInstallerMenuSelectInstallApiChecks() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '1', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please select if you want to enable the Api-Checks feature' ` + -Entries @( + @{ + 'Caption' = 'Do not install Api-Checks feature'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Does neither register the REST-Api background daemon nor enables the Api-Check feature'; + }, + @{ + 'Caption' = 'Install Api-Checks feature'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Enables the Icinga for Windows REST-Api background daemon and enables the Api-Check feature, which results in every check executed by "Exit-IcingaExecutePlugin" to be forwarded to the internal API. This will provide a huge performance boost for plugin execution. Also enables all checks for the namespace "Invoke-IcingaCheck*" to be executed over the Api. The REST-Api is only configured to run on localhost. Requires the Icinga for Windows service to be installed.'; + } + ) ` + -DefaultIndex $DefaultInput ` + -JumpToSummary:$FALSE ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-InstallApiChecks' -Value 'Show-IcingaForWindowsInstallerMenuSelectInstallApiChecks'; +function Show-IcingaForWindowsInstallerMenuSelectServiceRecovery() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '1', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please select if you want to enable or disable automatic service recovery' ` + -Entries @( + @{ + 'Caption' = 'Disable automatic service recovery'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Enables automatic service recovery for the Icinga Agent and Icinga for Windows service, in case the server terminates itself because of errors'; + }, + @{ + 'Caption' = 'Enable automatic service recovery'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Disables automatic service recovery for the Icinga Agent and Icinga for Windows service, in case the server terminates itself because of errors'; + } + ) ` + -DefaultIndex $DefaultInput ` + -JumpToSummary:$FALSE ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-ServiceRecovery' -Value 'Show-IcingaForWindowsInstallerMenuSelectServiceRecovery'; +function Show-IcingaForWindowsInstallationMenuStableRepository() +{ + param ( + [array]$Value = @( 'https://packages.icinga.com/IcingaForWindows/stable/ifw.repo.json' ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter the path or Url for your stable Icinga repository:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'This is the stable repository from where all packages of Icinga for Windows are downloaded and installed from. Defaults to "https://packages.icinga.com/IcingaForWindows/stable/ifw.repo.json"'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues @( $Value ) ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-StableRepository' -Value 'Show-IcingaForWindowsInstallationMenuStableRepository'; +function Show-IcingaForWindowsInstallerConfigurationSummary() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + [array]$Entries = @(); + [int]$CurrentIndex = 0; + [array]$KeyValues = @(); + + foreach ($entry in $global:Icinga.InstallWizard.Config.Keys) { + if ($entry.Contains(':')) { + $KeySplit = $entry.Split(':'); + $KeyValues += [string]::Format('{0} for "{1}"', ($KeySplit[0]), ($KeySplit[1])); + } else { + $KeyValues += $entry; + } + } + + [int]$MaxEntryLength = (Get-IcingaMaxTextLength -TextArray $KeyValues) - 4; + + Enable-IcingaForWindowsInstallationHeaderPrint; + + while ($TRUE) { + if ($CurrentIndex -gt $global:Icinga.InstallWizard.Config.Count) { + break; + } + + foreach ($entry in $global:Icinga.InstallWizard.Config.Keys) { + $ConfigEntry = $global:Icinga.InstallWizard.Config[$entry]; + + if ($ConfigEntry.Index -ne $CurrentIndex) { + continue; + } + + if ($ConfigEntry.Hidden) { + continue; + } + + if ($ConfigEntry.Advanced -And $global:Icinga.InstallWizard.ShowAdvanced -eq $FALSE) { + continue; + } + + $EntryValue = $ConfigEntry.Selection; + if ($null -ne $ConfigEntry.Values -And $ConfigEntry.Count -ne 0) { + if ($ConfigEntry.Password) { + $EntryValue = ConvertFrom-IcingaArrayToString -Array $ConfigEntry.Values -AddQuotes -SecureContent; + } else { + $EntryValue = ConvertFrom-IcingaArrayToString -Array $ConfigEntry.Values -AddQuotes; + } + } + + [string]$Caption = '' + $PrintName = $entry; + $RealCommand = $entry; + $ChildElement = ''; + + if ($RealCommand.Contains(':')) { + $RealCommand = $entry.Split(':')[0]; + $ChildElement = $entry.Split(':')[1]; + } + + if ($entry.Contains(':')) { + $PrintName = [string]::Format('{0} for "{1}"', $RealCommand, $ChildElement); + } else { + $PrintName = $RealCommand; + } + + $PrintName = $PrintName.Replace('IfW-', ''); + $PrintName = Add-IcingaWhiteSpaceToString -Text $PrintName -Length $MaxEntryLength; + + if (Test-Numeric ($ConfigEntry.Selection)) { + Set-IcingaForWindowsInstallationHeaderSelection -Selection $ConfigEntry.Selection; + + try { + &$RealCommand; + } catch { + $ErrMsg = [string]::Format('Failed to apply configuration item "{0}". This could happen in case elements were removed or renamed between Framework versions: {1}', $RealCommand, $_.Exception.Message); + $global:Icinga.InstallWizard.LastError += $ErrMsg; + continue; + } + + $Caption = ([string]::Format('{0}=> {1}', $PrintName, $global:Icinga.InstallWizard.HeaderPreview)); + } else { + $Caption = ([string]::Format('{0}=> {1}', $PrintName, $EntryValue)); + } + + [bool]$EntryDisabled = $FALSE; + [string]$EntryDisabledReason = Get-IcingaForWindowsInstallerDisabledEntry -Name $RealCommand; + + if ([string]::IsNullOrEmpty($EntryDisabledReason) -eq $FALSE) { + $EntryDisabled = $TRUE; + $Caption = ([string]::Format('{0}=> Disabled: {1}', $PrintName, $EntryDisabledReason)); + } + + $Entries += @{ + 'Caption' = $Caption; + 'Command' = $entry; + 'Arguments' = @{ '-JumpToSummary' = $TRUE }; + 'Help' = ''; + 'Disabled' = $EntryDisabled; + 'DisabledReason' = $EntryDisabledReason + } + + $global:Icinga.InstallWizard.HeaderPreview = ''; + } + + $CurrentIndex += 1; + } + + Disable-IcingaForWindowsInstallationHeaderPrint; + Enable-IcingaForWindowsInstallationJumpToSummary; + + $global:Icinga.InstallWizard.DisplayAdvanced = $TRUE; + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please validate your configuration. Installation or answer file/command export are available on next step:' ` + -Entries $Entries ` + -DefaultIndex 'c' ` + -ContinueFunction 'Show-IcingaForWindowsInstallerMenuFinishInstaller' ` + -ConfigElement ` + -Hidden; + + Disable-IcingaForWindowsInstallationJumpToSummary; + $global:Icinga.InstallWizard.DisplayAdvanced = $FALSE; +} + +Set-Alias -Name 'IfW-ConfigurationSummary' -Value 'Show-IcingaForWindowsInstallerConfigurationSummary'; +function Show-IcingaForWindowsInstallerMenuContinueConfiguration() +{ + $SwapConfig = Get-IcingaPowerShellConfig -Path 'Framework.Config.Swap'; + $global:Icinga.InstallWizard.Config = Convert-IcingaForwindowsManagementConsoleJSONConfig -Config $SwapConfig; + [string]$Menu = Get-IcingaForWindowsInstallerLastParent; + + # We don't need the last entry, as this will be added anyways because we are + # starting right from there and it will be added anyway + Remove-IcingaForWindowsInstallerLastParent; + + if ($Menu.Contains(':')) { + $Menu = Get-IcingaForWindowsInstallerLastParent; + } + + $global:Icinga.InstallWizard.NextCommand = $Menu; +} +function Show-IcingaForWindowsInstallerMenuInstallWindows() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '0', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + if ($null -eq (Get-IcingaPowerShellConfig -Path 'Framework.Config.Swap') -And $null -eq (Get-IcingaPowerShellConfig -Path 'Framework.Config.Live')) { + Show-IcingaForWindowsInstallerMenuSelectConnection; + return; + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Choose the configuration type:' ` + -Entries @( + @{ + 'Caption' = 'New configuration'; + 'Command' = 'Show-IcingaForWindowsInstallerMenuNewConfiguration'; + 'Help' = 'Start a new configuration and truncate all information stored on the current swap file. This will only modify your production if you hit "Start installation" at the end'; + }, + @{ + 'Caption' = 'Continue configuration'; + 'Command' = 'Show-IcingaForWindowsInstallerMenuContinueConfiguration'; + 'Help' = 'Continue with the previous configuration swap file.'; + 'Disabled' = ([bool]($null -eq (Get-IcingaPowerShellConfig -Path 'Framework.Config.Swap'))); + }, + @{ + 'Caption' = 'Reconfigure environment'; + 'Command' = 'Invoke-IcingaForWindowsManagementConsoleReconfigureAgent'; + 'Help' = 'Load the current configuration of Icinga for Windows to modify it.'; + 'Disabled' = ([bool]($null -eq (Get-IcingaPowerShellConfig -Path 'Framework.Config.Live'))); + } + ) ` + -DefaultIndex $DefaultInput ` + -Automated:$Automated ` + -Advanced:$Advanced; +} +function Show-IcingaForWindowsManagementConsoleInstallationFileExport() +{ + $FilePath = $ENV:USERPROFILE; + + if ($null -ne $global:Icinga.InstallWizard.LastValues -And $global:Icinga.InstallWizard.LastValues.Count -ne 0) { + $FilePath = $global:Icinga.InstallWizard.LastValues[0]; + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Where do you want to export the answer file to? The filename "IfW_answer.json" is added automatically.' ` + -Entries @( + @{ + 'Caption' = ''; + 'Command' = 'Export-IcingaForWindowsManagementConsoleInstallationAnswerFile'; + 'Help' = 'This will all you to export the answer file with the given configuration. You can install Icinga for Windows with this file by using the command "Install-Icinga -InstallFile ".'; + } + ) ` + -AddConfig ` + -DefaultValues @( $FilePath ) ` + -ConfigLimit 1 ` + -DefaultIndex 'c' ` + -MandatoryValue ` + -Hidden; +} +function Show-IcingaForWindowsManagementConsoleInstallationConfigString() +{ + [string]$ConfigurationString = [string]::Format( + "Install-Icinga -InstallCommand '{0}'", + (Get-IcingaForWindowsManagementConsoleConfigurationString -Compress) + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Here is your configuration command for Icinga for Windows:' ` + -Entries @( + @{ + 'Caption' = ''; + 'Command' = 'Install-Icinga'; + 'Help' = 'This command provides a list of settings you entered or modified during the process. In case values are not modified, they do not show up here and are left as default. You can run this entire command on a different Windows host to apply the same configuration'; + 'Action' = @{ + 'Command' = 'Clear-IcingaForWindowsManagementConsolePaginationCache'; + } + } + ) ` + -AddConfig ` + -DefaultValues @( $ConfigurationString ) ` + -ConfigLimit 1 ` + -DefaultIndex 'c' ` + -ReadOnly ` + -PlainTextOutput ` + -Hidden; +} +function Show-IcingaForWindowsInstallerMenuFinishInstaller() +{ + if ($global:Icinga.InstallWizard.DirectorSelfService -eq $TRUE -And $global:Icinga.InstallWizard.DirectorRegisteredHost -eq $FALSE) { + $global:Icinga.InstallWizard.LastNotice = 'You are using the Icinga Director Self-Service API but have not registered the host inside the Self-Service API on the previous menu'; + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'How you do want to proceed:' ` + -Entries @( + @{ + 'Caption' = 'Start installation'; + 'Command' = 'Start-IcingaForWindowsInstallation'; + 'Help' = 'Apply the just configured configuration and install components as selected'; + 'Disabled' = ($global:Icinga.InstallWizard.DirectorSelfService -eq $TRUE -And $global:Icinga.InstallWizard.DirectorRegisteredHost -eq $FALSE); + 'DisabledReason' = 'You are using the Icinga Director Self-Service API but have not registered the host inside the Self-Service API on the previous menu'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Clear-IcingaForWindowsManagementConsolePaginationCache'; + } + }, + @{ + 'Caption' = 'Export answer file'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleInstallationFileExport'; + 'Help' = 'Allows you to export a JSON file containing all settings configured during this step and use it on another system'; + }, + @{ + 'Caption' = 'Print installation command'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleInstallationConfigString'; + 'Help' = 'Allows you to export a simple configuration command you can run on another system. Similar to the "Export answer file" option, but does not require to distribute files'; + }, + @{ + 'Caption' = 'Save current configuration and go to main menu'; + 'Command' = 'Install-Icinga'; + 'Help' = 'Keep the current configuration as "swap" and exit to the main menu'; + 'Action' = @{ + 'Command' = 'Clear-IcingaForWindowsManagementConsolePaginationCache'; + } + } + ) ` + -DefaultIndex 0 ` + -Hidden; +} + +Set-Alias -Name 'IfW-FinishInstaller' -Value 'Show-IcingaForWindowsInstallerMenuFinishInstaller'; +function Export-IcingaForWindowsManagementConsoleInstallationAnswerFile() +{ + $FilePath = ''; + $Value = $global:Icinga.InstallWizard.LastValues; + + if ($null -ne $Value -And $Value.Count -ne 0) { + $FilePath = $Value[0] + } + + if (Test-Path ($FilePath)) { + 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; + } else { + $global:Icinga.InstallWizard.LastError += ([string]::Format('The provided path to store the answer file is invalid: "{0}"', $FilePath)); + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsManagementConsoleInstallationFileExport'; + } +} +function Show-IcingaForWindowsInstallerMenuNewConfiguration() +{ + $global:Icinga.InstallWizard.Config = @{ }; + Show-IcingaForWindowsInstallerMenuSelectConnection; +} +function Show-IcingaForWindowsInstallerMenuEnterPluginsPackageSource() +{ + param ( + [array]$Value = @( ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter the full path to the Icinga PowerShell Plugins .zip file:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Allows you to specify a full custom path on where your Icinga plugins .zip file is located at. You can specify a local path "C:\icinga\plugins\icinga-powershell-plugins.zip", a network path "\\example.com\software\icinga\plugins\icinga-powershell-plugins.zip" or a web path "https://example.com/icinga/windows/plugins/icinga-powershell-plugins.zip". Please note that only .zip packages downloaded from "https://github.com/icinga/icinga-powershell-plugins/releases" will work. You can get the packages from there and place them on your custom location'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues $Value ` + -ContinueFirstValue ` + -MandatoryValue ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-PluginPackageSource' -Value 'Show-IcingaForWindowsInstallerMenuEnterPluginsPackageSource'; +function Show-IcingaForWindowsInstallerMenuSelectInstallIcingaPlugins() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '0', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please select where your Icinga plugins are downloaded from:' ` + -Entries @( + @{ + 'Caption' = 'Install plugins'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Installs the Icinga Plugins from the defined stable repository'; + }, + @{ + 'Caption' = 'Do not install plugins'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Select this if you do not want to install the plugins for the moment'; + } + ) ` + -DefaultIndex $DefaultInput ` + -JumpToSummary:$FALSE ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-InstallPlugins' -Value 'Show-IcingaForWindowsInstallerMenuSelectInstallIcingaPlugins'; +function Show-IcingaForWindowsInstallerMenuSelectInstallIcingaAgent() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '0', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please select if the Icinga Agent should be installed' ` + -Entries @( + @{ + 'Caption' = 'Install Icinga Agent'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Downloads the Icinga Agent from the specified stable repository and installs it'; + }, + @{ + 'Caption' = 'Do not install Icinga Agent'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Do not install the Icinga Agent on this system'; + } + ) ` + -DefaultIndex $DefaultInput ` + -JumpToSummary:$FALSE ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-InstallAgent' -Value 'Show-IcingaForWindowsInstallerMenuSelectInstallIcingaAgent'; +function Show-IcingaForWindowsInstallationMenuEnterIcingaAgentServicePassword() +{ + param ( + [array]$Value = @( ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter the password for your service. Not required for system users!' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Allows you to provide a password for a service user account. This is only required for custom users, like a local or domain user. The default user does not require a password and should be left empty in this case'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -PasswordInput ` + -DefaultValues @( $Value ) ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-ServicePassword' -Value 'Show-IcingaForWindowsInstallationMenuEnterIcingaAgentServicePassword'; +function Show-IcingaForWindowsInstallationMenuEnterIcingaAgentVersion() +{ + param ( + [array]$Value = @( 'release' ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please specify the version of the Icinga Agent you want to install:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Allows you to define which Icinga Agent version is installed on this system. The installer will search for the .MSI package for the specified version on the source location. You can either use "release" to install the highest version found, use "snapshot" to install snapshot packages or specify a direct version like "2.12.3"'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues @( $Value ) ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-AgentVersion' -Value 'Show-IcingaForWindowsInstallationMenuEnterIcingaAgentVersion'; +function Show-IcingaForWindowsInstallationMenuEnterIcingaAgentUser() +{ + param ( + [array]$Value = @( 'NT Authority\NetworkService' ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please define the user the Icinga Agent service should run with:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallationMenuEnterIcingaAgentServicePassword'; + 'Help' = 'Allows you to override the default user the Icinga Agent is running with as service. In case a password is required, you can add it in the next step'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues @( $Value ) ` + -MandatoryValue ` + -JumpToSummary:$FALSE ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; + + # Remove a defined password in case we are running system services + [string]$ServiceUser = Get-IcingaForWindowsInstallerValuesFromStep; + + if ([string]::IsNullOrEmpty($ServiceUser) -eq $FALSE) { + $ServiceUser = $ServiceUser.ToLower(); + } else { + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsInstallationMenuEnterIcingaAgentUser'; + return; + } + + if ($ServiceUser -eq 'networkservice' -Or $ServiceUser -eq 'nt authority\networkservice' -Or $ServiceUser -eq 'localsystem' -Or $ServiceUser -eq 'nt authority\localservice' -Or $ServiceUser -eq 'localservice') { + Remove-IcingaForWindowsInstallerConfigEntry -Menu 'Show-IcingaForWindowsInstallationMenuEnterIcingaAgentServicePassword'; + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + $global:Icinga.InstallWizard.NextArguments = @{ }; + } else { + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsInstallationMenuEnterIcingaAgentServicePassword'; + } +} + +Set-Alias -Name 'IfW-AgentUser' -Value 'Show-IcingaForWindowsInstallationMenuEnterIcingaAgentUser'; +function Show-IcingaForWindowsInstallationMenuEnterIcingaAgentDirectory() +{ + param ( + [array]$Value = @( (Join-Path -Path $Env:ProgramFiles -ChildPath 'ICINGA2') ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Enter the path where to install the Icinga Agent into:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Allows you to override the location on where the Icinga Agent will be installed into'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues @( $Value ) ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-AgentDirectory' -Value 'Show-IcingaForWindowsInstallationMenuEnterIcingaAgentDirectory'; +function Show-IcingaForWindowsInstallerMenuSelectInstallIcingaForWindowsService() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '0', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please select of you want to install the Icinga for Windows service:' ` + -Entries @( + @{ + 'Caption' = 'Install Icinga for Windows Service'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Installs the Icinga for Windows service from the provided stable repository'; + }, + @{ + 'Caption' = 'Do not install Icinga for Windows service'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Select this if you do not want to install the Icinga for Windows service'; + } + ) ` + -DefaultIndex $DefaultInput ` + -JumpToSummary:$FALSE ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-InstallService' -Value 'Show-IcingaForWindowsInstallerMenuSelectInstallIcingaForWindowsService'; +function Show-IcingaForWindowsInstallationMenuEnterWindowsServiceDirectory() +{ + param ( + [array]$Value = @( (Join-Path -Path $Env:ProgramFiles -ChildPath 'icinga-framework-service') ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Enter the path where to install the Icinga for Windows service binary into:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'If you want to run a background PowerShell daemon, you will require a binary starting the shell as service. This is the permanent location for the binary, as the Icinga for Windows service is registered with this binary to run PowerShell as background daemon'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues @( $Value ) ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-WindowsServiceDirectory' -Value 'Show-IcingaForWindowsInstallationMenuEnterWindowsServiceDirectory'; +function Show-IcingaForWindowsInstallerMenuEnterWindowsServicePackageSource() +{ + param ( + [array]$Value = @( ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter the full path to the Icinga PowerShell Service .zip file:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Allows you to specify a full custom path on where your Icinga service package .zip file is located at. You can specify a local path "C:\icinga\service\icinga-service.zip", a network path "\\example.com\software\icinga\service\icinga-service.zip" or a web path "https://example.com/icinga/windows/service/icinga-service.zip". Please note that only the custom release .zip packages downloaded from "https://github.com/Icinga/icinga-powershell-service/releases" will work. You can get the packages from there and place them on your custom location'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues $Value ` + -ContinueFirstValue ` + -MandatoryValue ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-WindowsServicePackageSource' -Value 'Show-IcingaForWindowsInstallerMenuEnterWindowsServicePackageSource'; +function Show-IcingaForWindowsInstallerMenuSelectOpenWindowsFirewall() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '1', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please select if your Windows Firewall should be opened for the Icinga port:' ` + -Entries @( + @{ + 'Caption' = 'Open Windows Firewall for incoming Icinga connections'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'This will open the Windows Firewall for the configured Icinga Port, to allow incoming communication from Icinga parent node(s)'; + }, + @{ + 'Caption' = 'Do not open Windows Firewall for incoming Icinga connections'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'Do not open the Windows firewall for any incoming Icinga connections. Please note that in case your Icinga Agent is configured for "Connecting from parent system" you will not be able to establish a communication unless the connection type is changed or the port is opened'; + } + ) ` + -DefaultIndex $DefaultInput ` + -JumpToSummary:$FALSE ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-WindowsFirewall' -Value 'Show-IcingaForWindowsInstallerMenuSelectOpenWindowsFirewall'; +function Show-IcingaForWindowsInstallationMenuEnterCustomHostname() +{ + param ( + [array]$Value = @( (Get-IcingaHostname -AutoUseHostname 1 -LowerCase 1) ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter your hostname:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentZone'; + 'Help' = 'A custom hostname being used for generating certificates and for usage within Icinga'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues $Value ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-CustomHostname' -Value 'Show-IcingaForWindowsInstallationMenuEnterCustomHostname'; +function Show-IcingaForWindowsInstallerMenuEnterIcingaParentNodes() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + $Endpoints = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentNodes'; + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter your parent Icinga node name(s):' ` + -Entries @( + @{ + 'Command' = 'Test-IcingaForWindowsInstallerParentEndpoints'; + 'Help' = 'These are the object names for your parent Icinga endpoints as defined within the zones.conf. If you are running multiple Icinga instances within the same zone, you require to add both of them'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 2 ` + -DefaultValues $Value ` + -ContinueFirstValue ` + -MandatoryValue ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; + + # In case we delete our parent config, ensure we also delete our endpoint addresses + if (Test-IcingaForWindowsManagementConsoleDelete) { + foreach ($endpoint in $Endpoints) { + Remove-IcingaForWindowsInstallerConfigEntry -Menu 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses' -Parent $endpoint; + } + } +} + +Set-Alias -Name 'IfW-ParentNodes' -Value 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentNodes'; +function Show-IcingaForWindowsInstallerMenuSelectForceCertificateGeneration() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '0', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Do you want to force the creation of possible existing Icinga Agent certificates?' ` + -Entries @( + @{ + 'Caption' = 'Do not enforce certificate creation'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'In case certificates for the Icinga Agent for the matching hostname do already exist, they will not be re-created.' + }, + @{ + 'Caption' = 'Enforce certificate creation'; + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'This will always create the Icinga Agent certificates and create a new certificate request, even when certificates do already exist.'; + } + ) ` + -DefaultIndex $DefaultInput ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-ForceCertificateCreation' -Value 'Show-IcingaForWindowsInstallerMenuSelectForceCertificateGeneration'; +function Show-IcingaForWindowsInstallerMenuSelectConnection() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '0', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + $Global:Icinga.InstallWizard.DirectorSelfService = $FALSE; + + Show-IcingaForWindowsInstallerMenu ` + -Header 'How do you want to configure your local Icinga Agent?' ` + -Entries @( + @{ + 'Caption' = 'Connecting from this system'; + 'Command' = 'Show-IcingaForWindowsInstallerMenuSelectHostname'; + 'Help' = 'Choose this option if your Icinga Agent should only connect to a parent Icinga node. This is the easiest configuration as certificate generation is done automatically.' + }, + @{ + 'Caption' = 'Connecting from parent system'; + 'Command' = 'Show-IcingaForWindowsInstallerMenuEnterIcingaCAFile'; + 'Help' = 'Choose this option if the Icinga Agent should not or cannot connect to a parent Icinga node and only connections from a Master/Satellite are possible. This will open the Windows firewall for the chosen Icinga protocol port (default 5665). Certificate generation requires to specify the location of the Icinga ca.crt, which is required to complete the setup process.'; + }, + @{ + 'Caption' = 'Connecting from both systems'; + 'Command' = 'Show-IcingaForWindowsInstallerMenuSelectHostname'; + 'Help' = 'Choose this if connections from a parent Icinga node are possible and the Icinga Agent should connect to a parent node. This will open the Windows firewall for the chosen Icinga protocol port (default 5665).'; + }, + @{ + 'Caption' = 'Icinga Director Self-Service API'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleInstallationEnterDirectorUrl'; + 'Help' = 'Choose this option if you can connect to the Icinga Director from this host. You will be asked for the Icinga Director Url and a Self-Service API key. The entire configuration for this host is then fetched from the Icinga Director.'; + } + ) ` + -DefaultIndex $DefaultInput ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; + + # If we choose option 1 "Connecting from parent system", we require to ask the user for the + # location of the ca.crt, as otherwise the api feature will not be enabled. This section will + # ensure we are forwarded to the proper menu later on and certain options are defined for our + # certificate handling + $LastInput = Get-IcingaForWindowsManagementConsoleLastInput; + + if ([string]::IsNullOrEmpty($LastInput) -eq $FALSE -and $LastInput -ne '1') { + # Remove the set hostname in case we choose a different option + Add-IcingaForWindowsInstallerConfigEntry -Selection '0' -OverwriteMenu 'Show-IcingaForWindowsInstallerMenuSelectCertificate' -Advanced; + Remove-IcingaForWindowsInstallerConfigEntry -Menu 'Show-IcingaForWindowsInstallerMenuEnterIcingaCAFile'; + } elseif ($LastInput -eq '1') { + Add-IcingaForWindowsInstallerConfigEntry -Selection '2' -OverwriteMenu 'Show-IcingaForWindowsInstallerMenuSelectCertificate' -Advanced; + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsInstallerMenuEnterIcingaCAFile'; + $global:Icinga.InstallWizard.NextArguments = @{ 'JumpToSummary' = $JumpToSummary; }; + } +} + +Set-Alias -Name 'IfW-Connection' -Value 'Show-IcingaForWindowsInstallerMenuSelectConnection'; +function Show-IcingaForWindowsInstallerMenuEnterIcingaParentZone() +{ + param ( + [array]$Value = @( 'master' ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter your parent Icinga zone:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentNodes'; + 'Help' = 'The object name of the zone of the parent Icinga node(s) you want to communicate with, as defined within the zones.conf'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues $Value ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-ParentZone' -Value 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentZone'; +function Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = $null, + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + if ($Value.Count -ne 0) { + + while ($TRUE) { + # Use the installer file/command for automation + if ($Value[0].GetType().Name.ToLower() -eq 'pscustomobject') { + + # This is just to handle automated installation by using a file or the install command + # We use a hashtable here as well, but reduce complexity and remove network checks + + foreach ($endpoint in $Value[0].PSObject.Properties) { + Add-IcingaForWindowsInstallerConfigEntry -Selection 'c' -Values $endpoint.Value ` + -OverwriteValues ` + -OverwriteMenu 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses' ` + -OverwriteParent ([string]::Format('Show-IcingaForWindowsInstallerMenuEnterIcingaParentNodes:{0}', $endpoint.Name)); + } + + return; + + } elseif ($Value[0].GetType().Name.ToLower() -eq 'hashtable') { # We will be forwarded this from Test-IcingaForWindowsInstallerParentEndpoints + + $NetworkMap = $Value[0]; + [int]$AddressIndex = 0; + + foreach ($entry in $NetworkMap.Keys) { + $EndpointConfig = $NetworkMap[$entry]; + + if ($EndpointConfig.Error -eq $FALSE) { + $AddressIndex += 1; + continue; + } + + $global:Icinga.InstallWizard.LastError += ([string]::Format('Failed to resolve the address for the following endpoint: {0}', $EndpointConfig.Endpoint)); + + $Address = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses' -Parent $EndpointConfig.Endpoint; + + if ($null -eq $Address -Or $Address.Count -eq 0) { + $Address = @( $EndpointConfig.Address ); + } + + Show-IcingaForWindowsInstallerMenu ` + -Header ([string]::Format('Please enter the connection data for endpoint: "{0}"', $EndpointConfig.Endpoint)) ` + -Entries @( + @{ + 'Command' = 'break'; + 'Help' = 'The address to communicate with your parent Icinga node. It is highly recommended to use an IP address instead of a FQDN'; + } + ) ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues $Address ` + -MandatoryValue ` + -ParentConfig ([string]::Format('Show-IcingaForWindowsInstallerMenuEnterIcingaParentNodes:{0}', $EndpointConfig.Endpoint)) ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; + + $NewAddress = $Address; + $NewValue = $Value; + + if ((Test-IcingaForWindowsManagementConsoleContinue)) { + $ParentAddress = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses' -Parent $EndpointConfig.Endpoint; + $NetworkTest = Convert-IcingaEndpointsToIPv4 -NetworkConfig $ParentAddress; + $NewAddress = $ParentAddress; + + if ($NetworkTest.HasErrors -eq $FALSE) { + Add-IcingaForWindowsInstallerConfigEntry -Selection 'c' -Values ($NetworkTest.Network[0]) -OverwriteValues -OverwriteMenu 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses'; + $AddressIndex += 1; + $NewValue[0][$entry].Error = $FALSE; + continue; + } + } + + Set-IcingaForWindowsManagementConsoleMenu -Menu 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses'; + + $NewValue[0][$entry].Address = $NewAddress; + + $global:Icinga.InstallWizard.NextArguments = @{ + 'Value' = $NewValue; + 'DefaultInput' = $DefaultInput; + 'JumpToSummary' = $JumpToSummary; + 'Automated' = $Automated; + 'Advanced' = $Advanced; + }; + + return; + } + + $global:Icinga.InstallWizard.NextCommand = 'Add-IcingaForWindowsInstallationAdvancedEntries'; + return; + } elseif ($Value[0].GetType().Name.ToLower() -eq 'string') { + $Address = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses' -Parent $Value[0]; + + Show-IcingaForWindowsInstallerMenu ` + -Header ([string]::Format('Please enter the connection data for endpoint: "{0}"', $Value[0])) ` + -Entries @( + @{ + 'Command' = 'break'; + 'Help' = 'The address to communicate with your parent Icinga node. It is highly recommended to use an IP address instead of a FQDN'; + } + ) ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues $Address ` + -MandatoryValue ` + -ParentConfig ([string]::Format('Show-IcingaForWindowsInstallerMenuEnterIcingaParentNodes:{0}', $Value[0])) ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; + + if ((Test-IcingaForWindowsManagementConsoleContinue)) { + $ParentAddress = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses' -Parent $Value[0]; + $NetworkTest = Convert-IcingaEndpointsToIPv4 -NetworkConfig $ParentAddress; + + if ($NetworkTest.HasErrors -eq $FALSE) { + Add-IcingaForWindowsInstallerConfigEntry -Selection 'c' -Values ($NetworkTest.Network[0]) -OverwriteValues -OverwriteMenu 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses'; + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + return; + } + } + } + + if ((Test-IcingaForWindowsManagementConsoleExit) -Or (Test-IcingaForWindowsManagementConsoleMenu) -Or (Test-IcingaForWindowsManagementConsolePrevious)) { + return; + } + + if ((Test-IcingaForWindowsManagementConsoleDelete)) { + continue; + } + } + + # Just to ensure we never are "trapped" in a endless loop + if ($Automated) { + break; + } + } + + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsInstallerConfigurationSummary'; +} + +Set-Alias -Name 'IfW-ParentAddress' -Value 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses'; +function Show-IcingaForWindowsInstallerMenuSelectHostname() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = '1', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'How is your host object named in Icinga?' ` + -Entries @( + @{ + 'Caption' = ([string]::Format('"{0}": FQDN (current)', (Get-IcingaHostname -AutoUseFQDN 1))); + 'Command' = 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentZone'; + 'Help' = 'This will use the current FQDN of your host and not modify the name at all'; + }, + @{ + 'Caption' = ([string]::Format('"{0}": FQDN (lowercase)', (Get-IcingaHostname -AutoUseFQDN 1 -LowerCase 1))); + 'Command' = 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentZone'; + 'Help' = 'This will use the current FQDN of your host and modify all characters to lowercase'; + }, + @{ + 'Caption' = ([string]::Format('"{0}": FQDN (uppercase)', (Get-IcingaHostname -AutoUseFQDN 1 -UpperCase 1))); + 'Command' = 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentZone'; + 'Help' = 'This will use the current FQDN of your host and modify all characters to uppercase'; + }, + @{ + 'Caption' = ([string]::Format('"{0}": Hostname (current)', (Get-IcingaHostname -AutoUseHostname 1))); + 'Command' = 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentZone'; + 'Help' = 'This will use the hostname only without FQDN extension without modification'; + }, + @{ + 'Caption' = ([string]::Format('"{0}": Hostname (lowercase)', (Get-IcingaHostname -AutoUseHostname 1 -LowerCase 1))); + 'Command' = 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentZone'; + 'Help' = 'This will use the hostname only without FQDN extension and modify all characters to lowercase'; + }, + @{ + 'Caption' = ([string]::Format('"{0}": Hostname (uppercase)', (Get-IcingaHostname -AutoUseHostname 1 -UpperCase 1))); + 'Command' = 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentZone'; + 'Help' = 'This will use the hostname only without FQDN extension and modify all characters to uppercase'; + }, + @{ + 'Caption' = 'Set custom Hostname'; + 'Command' = 'Show-IcingaForWindowsInstallationMenuEnterCustomHostname'; + 'Help' = 'Allows you to set a custom hostname'; + } + ) ` + -DefaultIndex $DefaultInput ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; + + $LastInput = Get-IcingaForWindowsManagementConsoleLastInput; + + if ([string]::IsNullOrEmpty($LastInput) -eq $FALSE -and $LastInput -ne '6') { + # Remove the set hostname in case we choose a different option + Remove-IcingaForWindowsInstallerConfigEntry -Menu 'Show-IcingaForWindowsInstallationMenuEnterCustomHostname'; + } elseif ($LastInput -eq '6' -And $JumpToSummary) { + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsInstallationMenuEnterCustomHostname'; + $global:Icinga.InstallWizard.NextArguments = @{ 'JumpToSummary' = $TRUE; }; + } +} + +Set-Alias -Name 'IfW-Hostname' -Value 'Show-IcingaForWindowsInstallerMenuSelectHostname'; +function Show-IcingaForWindowsInstallationMenuEnterIcingaPort() +{ + param ( + [array]$Value = @( 5665 ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter your parent Icinga communication port:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'This is the port Icinga will use for communicating with all parent nodes and for which the firewall must be opened, depending on your communication configuration. Defaults to 5665'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues @( $Value ) ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-Port' -Value 'Show-IcingaForWindowsInstallationMenuEnterIcingaPort'; +function Show-IcingaForWindowsInstallationMenuEnterIcingaCAServer() +{ + param ( + [array]$Value = @(), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + if ($null -eq $Value -or $Value.Count -eq 0) { + $IcingaEndpoints = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentNodes'; + + foreach ($endpoint in $IcingaEndpoints) { + $EndpointAddress = Get-IcingaForWindowsInstallerValuesFromStep -InstallerStep 'Show-IcingaForWindowsInstallerMenuEnterIcingaParentAddresses' -Parent $endpoint; + + if ($null -ne $EndpointAddress -And [string]::IsNullOrEmpty($EndpointAddress) -eq $FALSE) { + $Value += $EndpointAddress; + break; + } + } + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter your Icinga CA server:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsInstallerConfigurationSummary'; + 'Help' = 'This is the Icinga endpoint to connect to for signing your certificates. This can be a satellite, as requests will be forwarded to your CA server.'; + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues @( $Value ) ` + -MandatoryValue ` + -JumpToSummary:$JumpToSummary ` + -ConfigElement ` + -Automated:$Automated ` + -Advanced:$Advanced; +} + +Set-Alias -Name 'IfW-CAServer' -Value 'Show-IcingaForWindowsInstallationMenuEnterIcingaCAServer'; +function Show-IcingaForWindowsMenuManageTroubleshooting() +{ + $IcingaAgentService = Get-Service 'icinga2' -ErrorAction SilentlyContinue; + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Troubleshooting options for problems:' ` + -Entries @( + @{ + 'Caption' = 'Flush Icinga Agent API directory (Restarts service)'; + 'Command' = 'Show-IcingaForWindowsMenuManageTroubleshooting'; + 'Help' = 'Allows you to flush the Icinga Agent API directory for cleanup. This will restart the Icinga Agent service'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Flush Icinga Agent API Directory (Restarts Service)'; + '-Command' = 'Clear-IcingaAgentApiDirectory'; + '-CmdArguments' = @{ + '-Force' = $TRUE; + } + } + } + }, + @{ + 'Caption' = 'Update Icinga for Windows cache'; + 'Command' = 'Show-IcingaForWindowsMenuManageTroubleshooting'; + 'Help' = 'Updates the Icinga for Windows Code Cache by re-compiling every module to ensure nothing is missing and up-to-date'; + 'Action' = @{ + 'Command' = 'Write-IcingaFrameworkCodeCache'; + } + }, + @{ + 'Caption' = 'Install Icinga for Windows certificate'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Uses the Icinga Agent certificate on this system to create a certificate for Icinga for Windows, which is required inside a JEA context in case the REST-Api feature is used, as the background daemon will be unable to perform certain actions requires for using the certificate otherwise'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Install Icinga for Windows certificate'; + '-Command' = 'Start-IcingaWindowsScheduledTaskRenewCertificate'; + } + } + }, + @{ + 'Caption' = 'Repair Icinga Agent service'; + 'Command' = 'Show-IcingaForWindowsMenuManageTroubleshooting'; + 'Help' = 'Allows to repair the Icinga Agent service in case it was removed or broke during installation/upgrade'; + 'Disabled' = ($null -ne $IcingaAgentService); + 'DisabledReason' = 'The Icinga Agent service is already present'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Repair-IcingaService'; + } + }, + @{ + 'Caption' = 'Repair Icinga Agent state file'; + 'Command' = 'Show-IcingaForWindowsMenuManageTroubleshooting'; + 'Help' = 'Allows to repair the Icinga Agent state file, in case the file is corrupt'; + 'Disabled' = (Test-IcingaStateFile); + 'DisabledReason' = 'The Icinga Agent state file is healthy'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Repair-IcingaStateFile'; + } + }, + @{ + 'Caption' = 'Allow untrusted certificate communication (This session)'; + 'Command' = 'Show-IcingaForWindowsMenuManageTroubleshooting'; + 'Help' = 'Enables the Icinga untrusted certificate validation, allowing you to communicate with web servers which ships with a self-signed certificate not installed on this system. This applies only to this PowerShell session and is not permanent. Might be helpful in case you want to connect to the Icinga Director and the SSL is not trusted by this host'; + 'Disabled' = $FALSE + 'Action' = @{ + 'Command' = 'Enable-IcingaUntrustedCertificateValidation'; + } + } + ); +} +function Show-IcingaForWindowsMenuManageViewLogs() +{ + Show-IcingaForWindowsInstallerMenu ` + -Header 'View all related logs:' ` + -Entries @( + @{ + 'Caption' = 'View Icinga Agent Main Log'; + 'Command' = 'Show-IcingaForWindowsMenuManageViewLogs'; + 'Help' = 'Allows to view the Icinga Agent main log in case the "mainlog" feature of the Icinga Agent is enabled'; + 'Disabled' = ((-Not (Test-IcingaAgentFeatureEnabled -Feature 'mainlog') -And -Not (Test-IcingaAgentFeatureEnabled -Feature 'windowseventlog'))); + 'DisabledReason' = 'It seems like neither the "mainlog" nor the "windowseventlog" feature of the Icinga Agent is enabled'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Start-Process'; + 'Arguments' = @{ '-FilePath' = 'powershell.exe'; '-ArgumentList' = "-Command `"&{ icinga { Read-IcingaAgentLogFile; }; }`"" }; + } + }, + @{ + 'Caption' = 'View Icinga Agent Debug Log'; + 'Command' = 'Show-IcingaForWindowsMenuManageViewLogs'; + 'Help' = 'Allows to read the Icinga Agent debug log in case the "debuglog" feature of the Icinga Agent is enabled'; + 'Disabled' = (-Not (Test-IcingaAgentFeatureEnabled -Feature 'debuglog')); + 'DisabledReason' = 'The "debuglog" feature of the Icinga Agent is not enabled'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Start-Process'; + 'Arguments' = @{ '-FilePath' = 'powershell.exe'; '-ArgumentList' = "-Command `"&{ icinga { Read-IcingaAgentDebugLogFile; }; }`"" }; + } + }, + @{ + 'Caption' = 'View Icinga for Windows EventLog'; + 'Command' = 'Show-IcingaForWindowsMenuManageViewLogs'; + 'Help' = 'Allows to read the Icinga for Windows from the EventLog'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Start-Process'; + 'Arguments' = @{ '-FilePath' = 'powershell.exe'; '-ArgumentList' = "-Command `"&{ icinga { Read-IcingaForWindowsLog; }; }`"" }; + } + }, + @{ + 'Caption' = 'View Icinga for Windows Debug EventLog'; + 'Command' = 'Show-IcingaForWindowsMenuManageViewLogs'; + 'Help' = 'Allows to read the Icinga for Windows EventLog, filtered by debug messages'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Start-Process'; + 'Arguments' = @{ '-FilePath' = 'powershell.exe'; '-ArgumentList' = "-Command `"&{ icinga { Read-IcingaWindowsEventLog -LogName 'Icinga for Windows' -Source 'IfW::Debug'; }; }`"" }; + } + } + ); +} +function Show-IcingaForWindowsMenuManageIcingaForWindowsServices() +{ + $IcingaAgentService = Get-Service 'icinga2' -ErrorAction SilentlyContinue; + $IcingaAgentStatus = 'Not Installed'; + $IcingaForWindowsService = Get-Service 'icingapowershell' -ErrorAction SilentlyContinue; + $IcingaForWindowsStatus = 'Not Installed'; + + if ($null -ne $IcingaAgentService) { + $IcingaAgentStatus = $IcingaAgentService.Status; + } + + if ($null -ne $IcingaForWindowsService) { + $IcingaForWindowsStatus = $IcingaForWindowsService.Status; + } + + Show-IcingaForWindowsInstallerMenu ` + -Header ([string]::Format('Manage Services:{0}=> Icinga Agent Service: {1}{0}=> Icinga for Windows Service: {2}', (New-IcingaNewLine), $IcingaAgentStatus, $IcingaForWindowsStatus)) ` + -Entries @( + @{ + 'Caption' = 'Start Icinga Agent Service'; + 'Command' = 'Show-IcingaForWindowsMenuManageIcingaForWindowsServices'; + 'Help' = 'Allows you to start the Icinga Agent if the service is not running'; + 'Disabled' = ($null -eq $IcingaAgentService -Or $IcingaAgentStatus -eq 'Running'); + 'DisabledReason' = 'The Icinga Agent service is either not installed or the service is already running'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Start-IcingaService'; + 'Arguments' = @{ '-Service' = 'icinga2'; }; + } + }, + @{ + 'Caption' = 'Stop Icinga Agent Service'; + 'Command' = 'Show-IcingaForWindowsMenuManageIcingaForWindowsServices'; + 'Help' = 'Allows you to stop the Icinga Agent if the service is not running'; + 'Disabled' = ($null -eq $IcingaAgentService -Or $IcingaAgentStatus -ne 'Running'); + 'DisabledReason' = 'The Icinga Agent service is either not installed or the service is not running'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Stop-IcingaService'; + 'Arguments' = @{ '-Service' = 'icinga2'; }; + } + }, + @{ + 'Caption' = 'Restart Icinga Agent Service'; + 'Command' = 'Show-IcingaForWindowsMenuManageIcingaForWindowsServices'; + 'Help' = 'Allows you to restart the Icinga Agent if the service is installed'; + 'Disabled' = ($null -eq $IcingaAgentService); + 'DisabledReason' = 'The Icinga Agent service is not installed'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Restart-IcingaService'; + 'Arguments' = @{ '-Service' = 'icinga2'; }; + } + }, + @{ + 'Caption' = 'Repair Icinga Agent Service'; + 'Command' = 'Show-IcingaForWindowsMenuManageIcingaForWindowsServices'; + 'Help' = 'Allows to repair the Icinga Agent service in case it was removed or broke during installation/upgrade'; + 'Disabled' = ($null -ne $IcingaAgentService); + 'DisabledReason' = 'The Icinga Agent service is already present'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Repair-IcingaService'; + } + }, + @{ + 'Caption' = 'Start Icinga for Windows Service'; + 'Command' = 'Show-IcingaForWindowsMenuManageIcingaForWindowsServices'; + 'Help' = 'Allows you to start the Icinga for Windows Service if the service is not running'; + 'Disabled' = ($null -eq $IcingaForWindowsService -Or $IcingaForWindowsStatus -eq 'Running'); + 'DisabledReason' = 'The Icinga for Windows service is either not installed or already running'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Start-IcingaService'; + 'Arguments' = @{ '-Service' = 'icingapowershell'; }; + } + }, + @{ + 'Caption' = 'Stop Icinga for Windows Service'; + 'Command' = 'Show-IcingaForWindowsMenuManageIcingaForWindowsServices'; + 'Help' = 'Allows you to stop the Icinga for Windows Service if the service is not running'; + 'Disabled' = ($null -eq $IcingaForWindowsService -Or $IcingaForWindowsStatus -ne 'Running'); + 'DisabledReason' = 'The Icinga for Windows service is either not installed or not running'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Stop-IcingaForWindows'; + } + }, + @{ + 'Caption' = 'Restart Icinga for Windows Service'; + 'Command' = 'Show-IcingaForWindowsMenuManageIcingaForWindowsServices'; + 'Help' = 'Allows you to restart the Icinga for Windows Service if the service is installed'; + 'Disabled' = ($null -eq $IcingaForWindowsService); + 'DisabledReason' = 'The Icinga for Windows service is not installed'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Restart-IcingaForWindows'; + } + }, + @{ + 'Caption' = 'Enable recovery settings for services'; + 'Command' = 'Show-IcingaForWindowsMenuManageIcingaForWindowsServices'; + 'Help' = 'Enables automatic service recovery for the Icinga Agent and Icinga for Windows service, in case the server terminates itself because of errors'; + 'Disabled' = ($null -eq $IcingaForWindowsService -And $null -eq $IcingaAgentService); + 'DisabledReason' = 'Neither the Icinga Agent nor the Icinga for Windows service are installed'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Enable-IcingaServiceRecovery'; + } + }, + @{ + 'Caption' = 'Disable recovery settings for services'; + 'Command' = 'Show-IcingaForWindowsMenuManageIcingaForWindowsServices'; + 'Help' = 'Disables automatic service recovery for the Icinga Agent and Icinga for Windows service, in case the server terminates itself because of errors'; + 'Disabled' = ($null -eq $IcingaForWindowsService -And $null -eq $IcingaAgentService); + 'DisabledReason' = 'Neither the Icinga Agent nor the Icinga for Windows service are installed'; + 'AdminMenu' = $TRUE; + 'Action' = @{ + 'Command' = 'Disable-IcingaServiceRecovery'; + } + } + ); +} +function Invoke-IcingaForWindowsManagementConsoleToggleFrameworkDebug() +{ + if (Get-IcingaFrameworkDebugMode) { + Disable-IcingaFrameworkDebugMode; + } else { + Enable-IcingaFrameworkDebugMode; + } +} +function Invoke-IcingaForWindowsManagementConsoleToggleFrameworkApiChecks() +{ + if (Get-IcingaFrameworkApiChecks) { + Disable-IcingaFrameworkApiChecks; + } else { + if ((Get-IcingaBackgroundDaemons).ContainsKey('Start-IcingaWindowsRESTApi') -eq $FALSE) { + Register-IcingaBackgroundDaemon -Command 'Start-IcingaWindowsRESTApi'; + Add-IcingaRESTApiCommand -Command 'Invoke-IcingaCheck*' -Endpoint 'apichecks'; + } + + # We need to run the task renewal with our scheduled task to fix errors while using WinRM / SSH + Start-IcingaWindowsScheduledTaskRenewCertificate; + Enable-IcingaFrameworkApiChecks; + } + + Restart-IcingaForWindows; +} +function Show-IcingaForWindowsManagementConsoleFrameworkExperimental() +{ + [array]$Entries = @(); + + if ($Entries.Count -ne 0) { + Show-IcingaForWindowsInstallerMenu ` + -Header 'Manage Icinga for Windows experimental features. Not recommended for production!' ` + -Entries $Entries; + } else { + Show-IcingaForWindowsInstallerMenu ` + -Header 'No experimental features for Icinga for Windows available'; + } +} +function Show-IcingaForWindowsManagementConsoleManageIcingaForWindowsFeatures() +{ + $FrameworkDebug = Get-IcingaFrameworkDebugMode; + $ApiChecks = Get-IcingaFrameworkApiChecks; + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Manage Icinga for Windows features:' ` + -Entries @( + @{ + 'Caption' = ([string]::Format('Api-Check Forwarder: {0}', (& { if ($ApiChecks) { 'Enabled' } else { 'Disabled' } } ))); + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageIcingaForWindowsFeatures'; + 'Help' = 'In case enabled, all check commands executed by "Exit-IcingaExecutePlugin" (Icinga default) are forwarded to an internal REST-Api and executed from within the Icinga for Windows background daemon. Requires the Icinga for Windows background daemon'; + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Invoke-IcingaForWindowsManagementConsoleToggleFrameworkApiChecks'; + 'Arguments' = @{ }; + } + }, + @{ + 'Caption' = ([string]::Format('Debug Mode: {0}', (& { if ($FrameworkDebug) { 'Enabled' } else { 'Disabled' } } ))); + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageIcingaForWindowsFeatures'; + 'Help' = 'Disable or enable the Icinga for Windows debug mode'; + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Invoke-IcingaForWindowsManagementConsoleToggleFrameworkDebug'; + } + }, + @{ + 'Caption' = 'Experimental'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleFrameworkExperimental'; + 'Help' = 'Allows you to manage experimental features for Icinga for Windows'; + 'Disabled' = $FALSE + } + ); +} +function Show-IcingaForWindowsManagementConsolePopIcingaRepository() +{ + [array]$Repositories = @(); + [array]$RepoList = Get-IcingaRepositories; + [int]$MaxLength = Get-IcingaMaxTextLength -TextArray $RepoList.Name; + + foreach ($repo in $RepoList) { + + $PrintName = Add-IcingaWhiteSpaceToString -Text $repo.Name -Length $MaxLength; + $PrintName = [string]::Format('{0}=> {1}', $PrintName, $repo.Value.RemotePath); + + $Repositories += @{ + 'Caption' = $PrintName; + 'Command' = 'Show-IcingaForWindowsManagementConsolePopIcingaRepository'; + 'Help' = ''; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = ([string]::Format('Move Icinga Repository "{0}" to bottom', $repo.Name)); + '-Command' = 'Pop-IcingaRepository'; + '-CmdArguments' = @{ + '-Name' = $repo.Name; + } + } + } + } + } + + if ($Repositories.Count -ne 0) { + Show-IcingaForWindowsInstallerMenu ` + -Header 'Select a repository to move it to the bottom of the list' ` + -Entries $Repositories; + } else { + Show-IcingaForWindowsInstallerMenu ` + -Header 'There are no local configured Icinga Repositories' + } +} +function Show-IcingaForWindowsManagementConsoleManageIcingaRepositories() +{ + Show-IcingaForWindowsInstallerMenu ` + -Header 'Manage Icinga for Windows Repositories:' ` + -Entries @( + @{ + 'Caption' = 'Set Icinga Repository "Icinga Stable"'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleSetIcingaStableRepositories'; + 'Help' = 'Allows to set the repository URL for the "Icinga Stable" repository and will override it, if it already exist'; + }, + @{ + 'Caption' = 'Set Icinga Repository "Icinga Snapshot"'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleSetIcingaSnapshotRepositories'; + 'Help' = 'Allows to set the repository URL for the "Icinga Snapshot" repository and will override it, if it already exist'; + }, + @{ + 'Caption' = 'Show Icinga Repository list'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleIcingaRepositoriesList'; + 'Help' = 'Shows a list of all defined Icinga Repositories on this machine'; + }, + @{ + 'Caption' = 'Move Icinga Repository to top'; + 'Command' = 'Show-IcingaForWindowsManagementConsolePushIcingaRepository'; + 'Help' = 'Allows you to move certain repositories on the system to the top of the list, which will then be applied first'; + }, + @{ + 'Caption' = 'Move Icinga Repository to bottom'; + 'Command' = 'Show-IcingaForWindowsManagementConsolePopIcingaRepository'; + 'Help' = 'Allows you to move certain repositories on the system to the bottom of the list, which will then be applied last'; + }, + @{ + 'Caption' = 'Enable Icinga Repository'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleEnableIcingaRepository'; + 'Help' = 'Allows you to enable certain repositories on the system'; + }, + @{ + 'Caption' = 'Disable Icinga Repository'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleDisableIcingaRepository'; + 'Help' = 'Allows you to disable certain repositories on the system'; + }, + @{ + 'Caption' = 'Remove Icinga Repository'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleRemoveIcingaRepository'; + 'Help' = 'Allows you to remove certain repositories from the system'; + } + ); +} +function Show-IcingaForWindowsManagementConsoleRemoveIcingaRepository() +{ + [array]$Repositories = @(); + [array]$RepoList = Get-IcingaRepositories; + [int]$MaxLength = Get-IcingaMaxTextLength -TextArray $RepoList.Name; + + foreach ($repo in $RepoList) { + + $PrintName = Add-IcingaWhiteSpaceToString -Text $repo.Name -Length $MaxLength; + $PrintName = [string]::Format('{0}=> {1}', $PrintName, $repo.Value.RemotePath); + + $Repositories += @{ + 'Caption' = $PrintName; + 'Command' = 'Show-IcingaForWindowsManagementConsoleRemoveIcingaRepository'; + 'Help' = ''; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = ([string]::Format('Remove Icinga Repository "{0}"', $repo.Name)); + '-Command' = 'Remove-IcingaRepository'; + '-CmdArguments' = @{ + '-Name' = $repo.Name; + } + } + } + } + } + + if ($Repositories.Count -ne 0) { + Show-IcingaForWindowsInstallerMenu ` + -Header 'Select a repository to remove it from the system' ` + -Entries $Repositories; + } else { + Show-IcingaForWindowsInstallerMenu ` + -Header 'There are no local configured Icinga Repositories' + } +} +function Show-IcingaForWindowsManagementConsoleEnableIcingaRepository() +{ + [array]$Repositories = @(); + [array]$RepoList = Get-IcingaRepositories; + [int]$MaxLength = Get-IcingaMaxTextLength -TextArray $RepoList.Name; + + foreach ($repo in $RepoList) { + + if ($repo.Value.Enabled) { + continue; + } + + $PrintName = Add-IcingaWhiteSpaceToString -Text $repo.Name -Length $MaxLength; + $PrintName = [string]::Format('{0}=> {1}', $PrintName, $repo.Value.RemotePath); + + $Repositories += @{ + 'Caption' = $PrintName; + 'Command' = 'Show-IcingaForWindowsManagementConsoleEnableIcingaRepository'; + 'Help' = ''; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = ([string]::Format('Enable Icinga Repository "{0}"', $repo.Name)); + '-Command' = 'Enable-IcingaRepository'; + '-CmdArguments' = @{ + '-Name' = $repo.Name; + } + } + } + } + } + + if ($Repositories.Count -ne 0) { + Show-IcingaForWindowsInstallerMenu ` + -Header 'Select a repository to enable it from the system' ` + -Entries $Repositories; + } else { + Show-IcingaForWindowsInstallerMenu ` + -Header 'There are no local configured Icinga Repositories' + } +} +function Show-IcingaForWindowsManagementConsoleSetIcingaStableRepositories() +{ + param ( + [array]$Value = @( 'https://packages.icinga.com/IcingaForWindows/stable/ifw.repo.json' ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + $CurrentRepositories = Get-IcingaRepositories; + + foreach ($entry in $CurrentRepositories) { + if ($entry.Name -eq 'Icinga Stable') { + $Value = $entry.Value.RemotePath; + break; + } + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter the URL/Path for the location of your "Icinga Stable" Repository:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageIcingaRepositories'; + 'Help' = 'Sets the current repository for Icinga for Windows as "Icinga Stable"'; + 'Action' = @{ + 'Command' = 'Add-IcingaRepository'; + 'Arguments' = @{ + '-Name' = 'Icinga Stable'; + '-RemotePath' = '$DefaultValues$'; + '-Force' = $TRUE; + } + } + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues $Value ` + -ContinueFirstValue ` + -MandatoryValue ` + -ConfigElement ` + -HiddenConfigElement ` + -Advanced ` + -NoConfigSwap; +} +function Show-IcingaForWindowsManagementConsoleDisableIcingaRepository() +{ + [array]$Repositories = @(); + [array]$RepoList = Get-IcingaRepositories -ExcludeDisabled; + [int]$MaxLength = Get-IcingaMaxTextLength -TextArray $RepoList.Name; + + foreach ($repo in $RepoList) { + + $PrintName = Add-IcingaWhiteSpaceToString -Text $repo.Name -Length $MaxLength; + $PrintName = [string]::Format('{0}=> {1}', $PrintName, $repo.Value.RemotePath); + + $Repositories += @{ + 'Caption' = $PrintName; + 'Command' = 'Show-IcingaForWindowsManagementConsoleDisableIcingaRepository'; + 'Help' = ''; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = ([string]::Format('Disable Icinga Repository "{0}"', $repo.Name)); + '-Command' = 'Disable-IcingaRepository'; + '-CmdArguments' = @{ + '-Name' = $repo.Name; + } + } + } + } + } + + if ($Repositories.Count -ne 0) { + Show-IcingaForWindowsInstallerMenu ` + -Header 'Select a repository to disable it from the system' ` + -Entries $Repositories; + } else { + Show-IcingaForWindowsInstallerMenu ` + -Header 'There are no local configured Icinga Repositories' + } +} +function Show-IcingaForWindowsManagementConsoleSetIcingaSnapshotRepositories() +{ + param ( + [array]$Value = @( 'https://packages.icinga.com/IcingaForWindows/snapshot/ifw.repo.json' ), + [string]$DefaultInput = 'c', + [switch]$JumpToSummary = $FALSE, + [switch]$Automated = $FALSE, + [switch]$Advanced = $FALSE + ); + + $CurrentRepositories = Get-IcingaRepositories; + + foreach ($entry in $CurrentRepositories) { + if ($entry.Name -eq 'Icinga Snapshot') { + $Value = $entry.Value.RemotePath; + break; + } + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Please enter the URL/Path for the location of your "Icinga Snapshot" Repository:' ` + -Entries @( + @{ + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageIcingaRepositories'; + 'Help' = 'Sets the current repository for Icinga for Windows as "Icinga Snapshot"'; + 'Action' = @{ + 'Command' = 'Add-IcingaRepository'; + 'Arguments' = @{ + '-Name' = 'Icinga Snapshot'; + '-RemotePath' = '$DefaultValues$'; + '-Force' = $TRUE; + } + } + } + ) ` + -DefaultIndex $DefaultInput ` + -AddConfig ` + -ConfigLimit 1 ` + -DefaultValues $Value ` + -ContinueFirstValue ` + -MandatoryValue ` + -ConfigElement ` + -HiddenConfigElement ` + -Advanced ` + -NoConfigSwap; +} +function Show-IcingaForWindowsManagementConsolePushIcingaRepository() +{ + [array]$Repositories = @(); + [array]$RepoList = Get-IcingaRepositories; + [int]$MaxLength = Get-IcingaMaxTextLength -TextArray $RepoList.Name; + + foreach ($repo in $RepoList) { + + $PrintName = Add-IcingaWhiteSpaceToString -Text $repo.Name -Length $MaxLength; + $PrintName = [string]::Format('{0}=> {1}', $PrintName, $repo.Value.RemotePath); + + $Repositories += @{ + 'Caption' = $PrintName; + 'Command' = 'Show-IcingaForWindowsManagementConsolePushIcingaRepository'; + 'Help' = ''; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = ([string]::Format('Move Icinga Repository "{0}" to top', $repo.Name)); + '-Command' = 'Push-IcingaRepository'; + '-CmdArguments' = @{ + '-Name' = $repo.Name; + } + } + } + } + } + + if ($Repositories.Count -ne 0) { + Show-IcingaForWindowsInstallerMenu ` + -Header 'Select a repository to move it to the top of the list' ` + -Entries $Repositories; + } else { + Show-IcingaForWindowsInstallerMenu ` + -Header 'There are no local configured Icinga Repositories' + } +} +function Show-IcingaForWindowsManagementConsoleIcingaRepositoriesList() +{ + [array]$Repositories = @(); + [array]$RepoList = Get-IcingaRepositories; + [int]$MaxLength = Get-IcingaMaxTextLength -TextArray $RepoList.Name; + + foreach ($repo in $RepoList) { + + $PrintName = Add-IcingaWhiteSpaceToString -Text $repo.Name -Length $MaxLength; + $PrintName = [string]::Format('{0}=> {1}', $PrintName, $repo.Value.RemotePath); + + $Repositories += @{ + 'Caption' = $PrintName; + 'Command' = 'Show-IcingaForWindowsManagementConsoleIcingaRepositoriesList'; + 'Help' = ''; + 'Disabled' = (-Not $repo.Value.Enabled); + } + } + + if ($Repositories.Count -ne 0) { + Show-IcingaForWindowsInstallerMenu ` + -Header 'List of local configured Icinga Repositories' ` + -Entries $Repositories; + } else { + Show-IcingaForWindowsInstallerMenu ` + -Header 'There are no local configured Icinga Repositories' + } +} +function Show-IcingaForWindowsManagementConsoleUnregisterBackgroundDaemons() +{ + [array]$RegisteredDaemons = @(); + + $BackgroundDaemons = Get-IcingaBackgroundDaemons; + + foreach ($daemon in $BackgroundDaemons.Keys) { + $DaemonValue = $BackgroundDaemons[$daemon]; + $HelpObject = Get-Help $daemon -Full -ErrorAction SilentlyContinue; + $HelpText = ''; + $Caption = [string]::Format('Unregister background daemon "{0}"', $daemon); + + if ($null -ne $HelpObject) { + $HelpText = $HelpObject.Description.Text; + } + + $RegisteredDaemons += @{ + 'Caption' = $Caption; + 'Command' = 'Show-IcingaForWindowsManagementConsoleUnregisterBackgroundDaemons'; + 'Help' = $HelpText; + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = $Caption; + '-Command' = 'Unregister-IcingaBackgroundDaemon'; + '-CmdArguments' = @{ + '-BackgroundDaemon' = $daemon; + } + } + } + } + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Unregister Icinga for Windows background daemon:' ` + -Entries $RegisteredDaemons; +} +function Show-IcingaForWindowsManagementConsoleRegisterBackgroundDaemons() +{ + [array]$AvailableDaemons = @(); + $ModuleList = Get-Module 'icinga-powershell-*' -ListAvailable; + + $AvailableDaemons += @{ + 'Caption' = 'Register background daemon "Start-IcingaServiceCheckDaemon"'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleRegisterBackgroundDaemons'; + 'Help' = ((Get-Help 'Start-IcingaServiceCheckDaemon' -Full).Description.Text); + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Register background daemon "Start-IcingaServiceCheckDaemon"'; + '-Command' = 'Register-IcingaBackgroundDaemon'; + '-CmdArguments' = @{ + '-Command' = 'Start-IcingaServiceCheckDaemon'; + } + } + } + } + + foreach ($module in $ModuleList) { + + $ModuleInfo = $null; + + Import-LocalizedData -BaseDirectory (Join-Path -Path (Get-IcingaForWindowsRootPath) -ChildPath $module.Name) -FileName ([string]::Format('{0}.psd1', $module.Name)) -BindingVariable ModuleInfo -ErrorAction SilentlyContinue; + + if ($null -eq $ModuleInfo -Or $null -eq $ModuleInfo.PrivateData -Or $null -eq $ModuleInfo.PrivateData.Type -Or ([string]::IsNullOrEmpty($ModuleInfo.PrivateData.Type)) -Or $ModuleInfo.PrivateData.Type -ne 'daemon' -Or $null -eq $ModuleInfo.PrivateData.Function -Or ([string]::IsNullOrEmpty($ModuleInfo.PrivateData.Function))) { + continue; + } + + $HelpObject = Get-Help ($ModuleInfo.PrivateData.Function) -Full -ErrorAction SilentlyContinue; + $HelpText = ''; + $Caption = [string]::Format('Register background daemon "{0}"', ($ModuleInfo.PrivateData.Function)); + + if ($null -ne $HelpObject) { + $HelpText = $HelpObject.Description.Text; + } + + $AvailableDaemons += @{ + 'Caption' = $Caption; + 'Command' = 'Show-IcingaForWindowsManagementConsoleRegisterBackgroundDaemons'; + 'Help' = $HelpText; + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = $Caption; + '-Command' = 'Register-IcingaBackgroundDaemon'; + '-CmdArguments' = @{ + '-Command' = $ModuleInfo.PrivateData.Function; + } + } + } + } + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Register Icinga for Windows background daemon:' ` + -Entries $AvailableDaemons; +} +function Show-IcingaForWindowsManagementConsoleManageBackgroundDaemons() +{ + Show-IcingaForWindowsInstallerMenu ` + -Header 'Manage the Icinga for Windows background daemons:' ` + -Entries @( + @{ + 'Caption' = 'Register background daemon'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleRegisterBackgroundDaemons'; + 'Help' = 'Allows you to register a new background daemon for Icinga for Windows'; + 'Disabled' = $FALSE; + }, + @{ + 'Caption' = 'Unregister background daemon'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleUnregisterBackgroundDaemons'; + 'Help' = 'Remove registered Icinga for Windows background daemons'; + 'Disabled' = $FALSE; + } + ); +} +function Show-IcingaForWindowsManagementConsoleManageJEA() +{ + [bool]$JEAContext = (-Not [string]::IsNullOrEmpty((Get-IcingaJEAContext))); + $ServiceData = Get-IcingaAgentInstallation; + $ServiceUser = 'No service installed'; + + if ($ServiceData.Installed -eq $FALSE) { + $ServiceData = $Global:Icinga.Protected.Environment.'PowerShell Service'; + if ($null -ne $ServiceData) { + $ServiceUser = $ServiceData.User; + } + } else { + $ServiceUser = $ServiceData.User; + } + + Show-IcingaForWindowsInstallerMenu ` + -Header ([string]::Format('Manage Icinga for Windows JEA configuration:{0}=> Current User: {1}{0}=> JEA installed: {2}', (New-IcingaNewLine), $ServiceUser, $JEAContext)) ` + -DefaultIndex '0' ` + -Entries @( + @{ + 'Caption' = 'Install JEA profile with managed user "icinga"'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Will create a managed user called "icinga", updates the services "icinga2" and "icingapowershell" with the new user and creates a JEA profile based on installed modules'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Install JEA profile with managed user "icinga"'; + '-Command' = 'Install-IcingaSecurity'; + '-CmdArguments' = @{ + '-RebuildFramework' = $TRUE; + } + } + } + }, + @{ + 'Caption' = 'Install JEA profile without managed user'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Installs the JEA profile for Icinga for Windows for the current user assigned to the "icinga2" service'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Install JEA profile without managed user'; + '-Command' = 'Install-IcingaJEAProfile'; + '-CmdArguments' = @{ + '-RebuildFramework' = $TRUE; + } + } + } + }, + @{ + 'Caption' = 'Update JEA profile'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Update JEA profile'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Update JEA profile'; + '-Command' = 'Install-IcingaJEAProfile'; + '-CmdArguments' = @{ + '-RebuildFramework' = $TRUE; + } + } + } + }, + <#@{ + 'Caption' = 'Install JEA profile with "ConstrainedLanguage" and managed user "icinga"'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Will create a managed user called "icinga", updates the services "icinga2" and "icingapowershell" with the new user and creates a JEA profile with "ConstrainedLanguage" based on installed modules. This is NOT recommended in case you are using the Icinga for Windows service, as it will not work in "ConstrainedLanguage" mode'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Install JEA profile with "ConstrainedLanguage" and managed user "icinga"'; + '-Command' = 'Install-IcingaSecurity'; + '-CmdArguments' = @{ + '-ConstrainedLanguage' = $TRUE; + } + } + } + }, + @{ + 'Caption' = 'Install/Update JEA profile with "ConstrainedLanguage"'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Installs or updates the JEA profile for Icinga for Windows for the current user assigned to the "icinga2" service with "ConstrainedLanguage". This is NOT recommended in case you are using the Icinga for Windows service, as it will not work in "ConstrainedLanguage" mode'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Install/Update JEA profile with "ConstrainedLanguage"'; + '-Command' = 'Install-IcingaJEAProfile'; + '-CmdArguments' = @{ + '-ConstrainedLanguage' = $TRUE; + } + } + } + },#> + @{ + 'Caption' = 'Uninstall JEA profile and managed user "icinga"'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'This will uninstall the JEA profile and remove the managed user "icinga" from the system. For the services the default user "NT Authority\NetworkService" will be applied'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Uninstall JEA profile and managed user "icinga"'; + '-Command' = 'Uninstall-IcingaSecurity'; + } + } + }, + @{ + 'Caption' = 'Uninstall JEA profile without managed user'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Uninstalls the JEA profile from this system'; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Uninstall JEA profile without managed user'; + '-Command' = 'Uninstall-IcingaJEAProfile'; + } + } + } + ); +} +function Show-IcingaForWindowsManagementConsoleComponentManager() +{ + Show-IcingaForWindowsInstallerMenu ` + -Header 'Choose what you want to do with components' ` + @{ + 'Caption' = 'Install'; + 'Command' = 'Show-IcingaForWindowsMenuInstallComponents'; + 'Help' = 'Allows you to install new components for Icinga for Windows from your repositories.'; + }, + @{ + 'Caption' = 'Install from Snapshot'; + 'Command' = 'Show-IcingaForWindowsMenuInstallComponentsSnapshot'; + 'Help' = 'Allows you to install new components for Icinga for Windows from your repositories.'; + }, + @{ + 'Caption' = 'Update'; + 'Command' = 'Show-IcingaForWindowsMenuUpdateComponents'; + 'Help' = 'Allows you to modify your current Icinga for Windows installation.'; + }, + @{ + 'Caption' = 'Update from Snapshot'; + 'Command' = 'Show-IcingaForWindowsMenuUpdateComponentsSnapshot'; + 'Help' = 'Allows you to modify your current Icinga for Windows installation.'; + }, + @{ + 'Caption' = 'Remove'; + 'Command' = 'Show-IcingaForWindowsMenuRemoveComponents'; + 'Help' = 'Allows you to modify your current Icinga for Windows installation.'; + } +} +function Show-IcingaForWindowsMenuUpdateComponents() +{ + $IcingaInstallation = Get-IcingaInstallation -Release; + [int]$MaxComponentLength = Get-IcingaMaxTextLength -TextArray $IcingaInstallation.Keys; + [array]$UpdateList = @(); + + foreach ($entry in $IcingaInstallation.Keys) { + $Component = $IcingaInstallation[$entry]; + + $LatestVersion = $Component.LatestVersion; + if ([string]::IsNullOrEmpty($Component.LockedVersion) -eq $FALSE) { + if ([Version]$Component.CurrentVersion -ge [Version]$Component.LockedVersion) { + continue; + } + $LatestVersion = [string]::Format('{0}*', $Component.LockedVersion); + } + + if ([string]::IsNullOrEmpty($LatestVersion)) { + continue; + } + + if ([Version]$Component.CurrentVersion -eq [Version]$LatestVersion) { + continue; + } + + $UpdateList += @{ + 'Caption' = ([string]::Format('{0} [{1}] => [{2}]', (Add-IcingaWhiteSpaceToString -Text $entry -Length $MaxComponentLength), $Component.CurrentVersion, $LatestVersion)); + 'Command' = 'Show-IcingaForWindowsMenuUpdateComponents'; + 'Help' = ([string]::Format('This will update "{0}" from current version "{1}" to stable version "{2}"', $entry, $Component.CurrentVersion, $LatestVersion)); + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = ([string]::Format('Update "{0}" from version "{1}" to stable version "{2}"', $entry, $Component.CurrentVersion, $LatestVersion)); + '-Command' = 'Update-Icinga'; + '-CmdArguments' = @{ + '-Name' = $entry; + '-Release' = $TRUE; + '-Confirm' = $TRUE; + } + } + } + } + } + + if ($UpdateList.Count -ne 0) { + $UpdateList += @{ + 'Caption' = 'Update entire environment'; + 'Command' = 'Show-IcingaForWindowsMenuUpdateComponents'; + 'Help' = 'This will update all components listed above to the mentioned stable version' + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Update entire Icinga for Windows environment'; + '-Command' = 'Update-Icinga'; + '-CmdArguments' = @{ + '-Release' = $TRUE; + '-Confirm' = $TRUE; + } + } + } + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Updates Icinga for Windows components. Select an entry to continue:' ` + -Entries $UpdateList; + } else { + Show-IcingaForWindowsInstallerMenu ` + -Header 'There are no updates pending for your environment' + } +} + +function Show-IcingaForWindowsMenuInstallComponentsSnapshot() +{ + $IcingaInstallation = Get-IcingaComponentList -Snapshot; + $CurrentComponents = Get-IcingaInstallation -Snapshot; + [int]$MaxComponentLength = Get-IcingaMaxTextLength -TextArray $IcingaInstallation.Components.Keys; + [array]$InstallList = @(); + + foreach ($entry in $IcingaInstallation.Components.Keys) { + $LatestVersion = $IcingaInstallation.Components[$entry]; + $LockedVersion = Get-IcingaComponentLock -Name $entry; + $VersionText = $LatestVersion; + + # Only show not installed components + if ($CurrentComponents.ContainsKey($entry)) { + continue; + } + + if ($null -ne $LockedVersion) { + $VersionText = [string]::Format('{0}*', $LockedVersion); + $LatestVersion = $LockedVersion; + } + + $InstallList += @{ + 'Caption' = ([string]::Format('{0} [{1}]', (Add-IcingaWhiteSpaceToString -Text $entry -Length $MaxComponentLength), $VersionText)); + 'Command' = 'Show-IcingaForWindowsMenuInstallComponents'; + 'Help' = ([string]::Format('This will install the component "{0}" with version "{1}"', $entry, $VersionText)); + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = ([string]::Format('Install component "{0}" with version "{1}"', $entry, $VersionText)); + '-Command' = 'Install-IcingaComponent'; + '-CmdArguments' = @{ + '-Name' = $entry; + '-Version' = $LatestVersion; + '-Snapshot' = $TRUE; + '-Force' = $TRUE; + '-Confirm' = $TRUE; + } + } + } + } + } + + if ($InstallList.Count -ne 0) { + Show-IcingaForWindowsInstallerMenu ` + -Header 'Install Icinga for Windows components. Select an entry to continue:' ` + -Entries $InstallList; + } else { + Show-IcingaForWindowsInstallerMenu ` + -Header 'There are no packages found for installation' + } +} +function Show-IcingaForWindowsMenuInstallComponents() +{ + $IcingaInstallation = Get-IcingaComponentList; + $CurrentComponents = Get-IcingaInstallation -Release; + [int]$MaxComponentLength = Get-IcingaMaxTextLength -TextArray $IcingaInstallation.Components.Keys; + [array]$InstallList = @(); + + foreach ($entry in $IcingaInstallation.Components.Keys) { + $LatestVersion = $IcingaInstallation.Components[$entry]; + $LockedVersion = Get-IcingaComponentLock -Name $entry; + $VersionText = $LatestVersion; + + # Only show not installed components + if ($CurrentComponents.ContainsKey($entry)) { + continue; + } + + if ($null -ne $LockedVersion) { + $VersionText = [string]::Format('{0}*', $LockedVersion); + $LatestVersion = $LockedVersion; + } + + $InstallList += @{ + 'Caption' = ([string]::Format('{0} [{1}]', (Add-IcingaWhiteSpaceToString -Text $entry -Length $MaxComponentLength), $VersionText)); + 'Command' = 'Show-IcingaForWindowsMenuInstallComponents'; + 'Help' = ([string]::Format('This will install the component "{0}" with version "{1}"', $entry, $VersionText)); + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = ([string]::Format('Install component "{0}" with version "{1}"', $entry, $VersionText)); + '-Command' = 'Install-IcingaComponent'; + '-CmdArguments' = @{ + '-Name' = $entry; + '-Version' = $LatestVersion; + '-Release' = $TRUE; + '-Confirm' = $TRUE; + } + } + } + } + } + + if ($InstallList.Count -ne 0) { + Show-IcingaForWindowsInstallerMenu ` + -Header 'Install Icinga for Windows components. Select an entry to continue:' ` + -Entries $InstallList; + } else { + Show-IcingaForWindowsInstallerMenu ` + -Header 'There are no packages found for installation' + } +} +function Show-IcingaForWindowsMenuRemoveComponents() +{ + [array]$UninstallFeatures = @(); + $AgentInstalled = (Get-IcingaAgentInstallation).Installed; + $PowerShellServiceInstalled = Get-Service -Name 'icingapowershell' -ErrorAction SilentlyContinue; + $IcingaWindowsServiceData = Get-IcingaForWindowsServiceData; + $ModuleList = Get-Module 'icinga-powershell-*' -ListAvailable; + + $UninstallFeatures += @{ + 'Caption' = 'Uninstall Icinga Agent'; + 'Command' = 'Show-IcingaForWindowsMenuRemoveComponents'; + 'Help' = 'Will remove the Icinga Agent from this system. Please note that this option will leave content inside "ProgramData", like certificates, alone' + 'Disabled' = (-Not $AgentInstalled); + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Uninstall Icinga Agent'; + '-Command' = 'Uninstall-IcingaComponent'; + '-CmdArguments' = @{ + '-Name' = 'agent'; + } + } + } + } + + $UninstallFeatures += @{ + 'Caption' = 'Uninstall Icinga Agent including ProgramData'; + 'Command' = 'Show-IcingaForWindowsMenuRemoveComponents'; + 'Help' = 'Will remove the Icinga Agent from this system. Please note that this option will leave content inside "ProgramData", like certificates, alone' + 'Disabled' = (-Not (Test-Path -Path (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2'))); + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Uninstall Icinga Agent including ProgramData';; + '-Command' = 'Uninstall-IcingaComponent'; + '-CmdArguments' = @{ + '-Name' = 'agent'; + '-RemovePackageFiles' = $TRUE; + } + } + } + } + + $UninstallFeatures += @{ + 'Caption' = 'Uninstall Icinga for Windows Service'; + 'Command' = 'Show-IcingaForWindowsMenuRemoveComponents'; + 'Help' = 'This will remove the icingapowershell service for Icinga for Windows if installed' + 'Disabled' = ($null -eq $PowerShellServiceInstalled); + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Uninstall Icinga for Windows service'; + '-Command' = 'Uninstall-IcingaComponent'; + '-CmdArguments' = @{ + '-Name' = 'service'; + } + } + } + } + + $UninstallFeatures += @{ + 'Caption' = 'Uninstall Icinga for Windows Service including binary'; + 'Command' = 'Show-IcingaForWindowsMenuRemoveComponents'; + 'Help' = 'This will remove the icingapowershell service for Icinga for Windows if installed and the service binary including the folder, if empty afterwards' + 'Disabled' = (-Not (Test-Path $IcingaWindowsServiceData.Directory)); + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Uninstall Icinga for Windows Service including binary'; + '-Command' = 'Uninstall-IcingaComponent'; + '-CmdArguments' = @{ + '-Name' = 'service'; + '-RemovePackageFiles' = $TRUE; + } + } + } + } + + foreach ($module in $ModuleList) { + $ComponentName = $module.Name.Replace('icinga-powershell-', ''); + $Caption = ([string]::Format('Uninstall "{0}"', $ComponentName)); + + if ($ComponentName -eq 'framework' -Or $ComponentName -eq 'service' -Or $ComponentName -eq 'agent') { + continue; + } + + $UninstallFeatures += @{ + 'Caption' = $Caption; + 'Command' = 'Show-IcingaForWindowsMenuRemoveComponents'; + 'Help' = ([string]::Format('This will remove the Icinga for Windows component "{0}" from this host', $ComponentName)); + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = $Caption; + '-Command' = 'Uninstall-IcingaComponent'; + '-CmdArguments' = @{ + '-Name' = $ComponentName; + } + } + } + } + } + + $UninstallFeatures += @{ + 'Caption' = 'Uninstall all components'; + 'Command' = 'Show-IcingaForWindowsMenuRemoveComponents'; + 'Help' = 'This will remove all current installed components of Icinga for Windows, but will keep the Framework installed' + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Uninstall all components'; + '-Command' = 'Uninstall-IcingaForWindows'; + '-CmdArguments' = @{ + '-Force' = $TRUE; + '-ComponentsOnly' = $TRUE; + } + } + } + } + + $UninstallFeatures += @{ + 'Caption' = 'Uninstall Icinga for Windows'; + 'Command' = 'Show-IcingaForWindowsMenuRemoveComponents'; + 'Help' = 'This will remove everything installed and configured by Icinga for Windows on this machine' + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Uninstall Icinga for Windows'; + '-Command' = 'Uninstall-IcingaForWindows'; + '-CmdArguments' = @{ + '-Force' = $TRUE; + } + } + } + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Uninstall Icinga for Windows components. Select an entry to continue:' ` + -Entries $UninstallFeatures; +} +function Show-IcingaForWindowsMenuUpdateComponentsSnapshot() +{ + $IcingaInstallation = Get-IcingaInstallation -Snapshot; + [int]$MaxComponentLength = Get-IcingaMaxTextLength -TextArray $IcingaInstallation.Keys; + [array]$UpdateList = @(); + + foreach ($entry in $IcingaInstallation.Keys) { + $Component = $IcingaInstallation[$entry]; + + $LatestVersion = $Component.LatestVersion; + if ([string]::IsNullOrEmpty($Component.LockedVersion) -eq $FALSE) { + if ([Version]$Component.CurrentVersion -ge [Version]$Component.LockedVersion) { + continue; + } + $LatestVersion = [string]::Format('{0}*', $Component.LockedVersion); + } + + if ([string]::IsNullOrEmpty($LatestVersion)) { + continue; + } + + $UpdateList += @{ + 'Caption' = ([string]::Format('{0} [{1}] => [snapshot]', (Add-IcingaWhiteSpaceToString -Text $entry -Length $MaxComponentLength), $Component.CurrentVersion)); + 'Command' = 'Show-IcingaForWindowsMenuUpdateComponents'; + 'Help' = ([string]::Format('This will update "{0}" from current version "{1}" to latest snapshot version', $entry, $Component.CurrentVersion)); + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = ([string]::Format('Update "{0}" from version "{1}" to latest snapshot version', $entry, $Component.CurrentVersion)); + '-Command' = 'Update-Icinga'; + '-CmdArguments' = @{ + '-Name' = $entry; + '-Snapshot' = $TRUE; + '-Confirm' = $TRUE; + '-Force' = $TRUE; + } + } + } + } + } + + if ($UpdateList.Count -ne 0) { + $UpdateList += @{ + 'Caption' = 'Update entire environment'; + 'Command' = 'Show-IcingaForWindowsMenuUpdateComponents'; + 'Help' = 'This will update all components listed above to the mentioned snapshot version' + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Show-IcingaWindowsManagementConsoleYesNoDialog'; + 'Arguments' = @{ + '-Caption' = 'Update entire Icinga for Windows environment'; + '-Command' = 'Update-Icinga'; + '-CmdArguments' = @{ + '-Snapshot' = $TRUE; + '-Confirm' = $TRUE; + '-Force' = $TRUE; + } + } + } + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Updates Icinga for Windows components. Select an entry to continue:' ` + -Entries $UpdateList; + } else { + Show-IcingaForWindowsInstallerMenu ` + -Header 'There are no updates pending for your environment' + } +} +function Show-IcingaForWindowsMenuManage() +{ + $AgentInstalled = (Get-IcingaAgentInstallation).Installed; + $JEADisabled = $FALSE; + + if ($PSVersionTable.PSVersion -lt (New-IcingaVersionObject -Version 5, 0)) { + $JEADisabled = $TRUE; + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Icinga for Windows Settings:' ` + -Entries @( + @{ + 'Caption' = 'Services'; + 'Command' = 'Show-IcingaForWindowsMenuManageIcingaForWindowsServices'; + 'Help' = 'Allows you to manage the Icinga Agent and Icinga for Windows service'; + 'Disabled' = (-Not $AgentInstalled -And -Not ([bool](Get-Service 'icingapowershell' -ErrorAction SilentlyContinue))); + 'AdminMenu' = $TRUE; + }, + @{ + 'Caption' = 'Icinga Agent Features'; + 'Command' = 'Show-IcingaForWindowsMenuManageIcingaAgentFeatures'; + 'Help' = 'Allows you to install Icinga for Windows with all required components and options'; + 'Disabled' = (-Not $AgentInstalled); + 'AdminMenu' = $TRUE; + }, + @{ + 'Caption' = 'Background Daemons'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageBackgroundDaemons'; + 'Help' = 'Allows you to manage Icinga for Windows background daemons'; + 'Disabled' = ($null -eq (Get-Service 'icingapowershell' -ErrorAction SilentlyContinue)); + 'DisabledReason' = 'Icinga for Windows service is not installed'; + }, + @{ + 'Caption' = 'Repositories'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageIcingaRepositories'; + 'Help' = 'Allows you to manage Icinga for Windows repositories'; + }, + @{ + 'Caption' = 'JEA'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageJEA'; + 'Help' = 'Allows you to manage Icinga for Windows JEA profile'; + 'Disabled' = $JEADisabled; + 'DisabledReason' = ([string]::Format('PowerShell version "{0}" is lower than 5.0 or you are not inside an administrative shell', $PSVersionTable.PSVersion.ToString(2))); + 'AdminMenu' = $TRUE; + }, + @{ + 'Caption' = 'Logging'; + 'Command' = 'Show-IcingaForWindowsMenuManageViewLogs'; + 'Help' = 'View different logs'; + }, + @{ + 'Caption' = 'Icinga for Windows Features'; + 'Command' = 'Show-IcingaForWindowsManagementConsoleManageIcingaForWindowsFeatures'; + 'Help' = 'Allows you to modify certain settings for Icinga for Windows'; + }, + @{ + 'Caption' = 'Troubleshooting'; + 'Command' = 'Show-IcingaForWindowsMenuManageTroubleshooting'; + 'Help' = 'Resolve problems with your Icinga for Windows environment with pre-defined actions'; + } + <#, + @{ + 'Caption' = 'Health Check'; + 'Command' = ''; + 'Help' = 'Check the current health and status information of your installation'; + }#> + ); +} +function Show-IcingaForWindowsMenuListEnvironment() +{ + Show-IcingaForWindowsInstallerMenu ` + -Header 'Icinga for Windows environment overview:' ` + -Entries @( + @{ + 'Caption' = ''; + 'Command' = 'Install-Icinga'; + 'Help' = 'A summary of your current Icinga for Windows installation'; + } + ) ` + -AddConfig ` + -DefaultValues @(([string]::Join("`n", (Show-Icinga -SkipHeader)))) ` + -ConfigLimit 1 ` + -DefaultIndex 'c' ` + -ReadOnly ` + -PlainTextOutput ` + -Hidden; +} +function Invoke-IcingaForWindowsMenuStartIcingaShell() +{ + Clear-CLIConsole; + $global:Icinga.InstallWizard.Closing = $TRUE; + Invoke-IcingaCommand -Shell; +} +function Show-IcingaForWindowsMenuManageIcingaAgentFeatures() +{ + $Features = Get-IcingaAgentFeatures; + + [array]$FeatureList = @(); + + foreach ($entry in $Features.Enabled) { + + if ([string]::IsNullOrEmpty($entry)) { + continue; + } + + [string]$Caption = [string]::Format('{0}: Enabled', $entry); + + $FeatureList += @{ + 'Caption' = $Caption; + 'Command' = 'Show-IcingaForWindowsMenuManageIcingaAgentFeatures'; + 'Help' = ([string]::Format('The feature "{0}" is currently enabled. Select this entry to disable it.', $entry)); + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Disable-IcingaAgentFeature'; + 'Arguments' = @{ + '-Feature' = $entry; + } + } + } + } + + foreach ($entry in $Features.Disabled) { + + if ([string]::IsNullOrEmpty($entry)) { + continue; + } + + [string]$Caption = [string]::Format('{0}: Disabled', $entry); + + $FeatureList += @{ + 'Caption' = $Caption; + 'Command' = 'Show-IcingaForWindowsMenuManageIcingaAgentFeatures'; + 'Help' = ([string]::Format('The feature "{0}" is currently disabled. Select this entry to enable it.', $entry)); + 'Disabled' = $FALSE; + 'Action' = @{ + 'Command' = 'Enable-IcingaAgentFeature'; + 'Arguments' = @{ + '-Feature' = $entry; + } + } + } + } + + Show-IcingaForWindowsInstallerMenu ` + -Header 'Manage Icinga Agent Features. Select an entry and hit enter to Disable/Enable them:' ` + -Entries $FeatureList; +} +function Invoke-IcingaForWindowsManagementConsoleReconfigureAgent() +{ + $LiveConfig = Get-IcingaPowerShellConfig -Path 'Framework.Config.Live'; + + if ($null -eq $LiveConfig) { + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsInstallerMenuInstallWindows'; + $global:Icinga.InstallWizard.LastError += 'Unable to load any previous live configuration. Reconfiguring not possible.'; + return; + } + + $global:Icinga.InstallWizard.Config = Convert-IcingaForwindowsManagementConsoleJSONConfig -Config $LiveConfig; + $global:Icinga.InstallWizard.NextCommand = 'Show-IcingaForWindowsInstallerConfigurationSummary'; +} +function Remove-IcingaThread() +{ + param( + [string]$Thread = '' + ); + + if ([string]::IsNullOrEmpty($Thread)) { + return; + } + + Stop-IcingaThread -Thread $Thread; + + if ($Global:Icinga.Public.Threads.ContainsKey($Thread)) { + $Global:Icinga.Public.Threads[$Thread].Shell.Dispose(); + $Global:Icinga.Public.Threads.Remove($Thread); + + Write-IcingaDebugMessage 'Removing Thread: {0}' -Objects $Thread; + } +} +function Restart-IcingaThread() +{ + param( + [string]$Thread + ); + + if ([string]::IsNullOrEmpty($Thread)) { + return; + } + + Stop-IcingaThread $Thread; + Start-IcingaThread $Thread; +} +function Set-IcingaEnvironmentGlobal() +{ + param ( + $GlobalEnvironment = $null + ); + + if ($null -eq $GlobalEnvironment -Or $null -eq $Global:Icinga) { + return; + } + + if ($Global:Icinga.ContainsKey('Public') -eq $FALSE) { + return; + } + + $Global:Icinga.Public = $GlobalEnvironment; +} +function New-IcingaThreadPool() +{ + param( + [int]$MinInstances = 1, + [int]$MaxInstances = 5 + ); + + $SessionConfiguration = $null; + $SessionFile = Get-IcingaJEASessionFile; + + if ([string]::IsNullOrEmpty((Get-IcingaJEAContext))) { + $SessionConfiguration = [System.Management.Automation.RunSpaces.InitialSessionState]::CreateDefault(); + } else { + if ([string]::IsNullOrEmpty($SessionFile)) { + Write-IcingaEventMessage -EventId 1502 -Namespace 'Framework'; + return $null; + } + $SessionConfiguration = [System.Management.Automation.Runspaces.InitialSessionState]::CreateFromSessionConfigurationFile($SessionFile); + } + + $RunSpaces = [RunSpaceFactory]::CreateRunSpacePool( + $MinInstances, + $MaxInstances, + $SessionConfiguration, + $host + ) + + $RunSpaces.Open(); + + return $RunSpaces; +} +function New-IcingaThreadHash() +{ + param( + [string]$ShellScript, + [array]$Arguments + ); + + [string]$ScriptString = ''; + [string]$ArgString = ($Arguments | Out-String); + if ($null -ne $ShellScript) { + $ScriptString = $ShellScript.ToString(); + } + return (Get-StringSha1 -Content ($ScriptString + $ArgString + (Get-Date -Format "MM-dd-yyyy-HH-mm-ffff"))); +} +function Set-IcingaEnvironmentThreadName() +{ + param ( + [string]$ThreadName = '' + ); + + $Global:Icinga.Protected.ThreadName = $ThreadName; +} +function Stop-IcingaThread() +{ + param ( + [string]$Thread = '' + ); + + if ([string]::IsNullOrEmpty($Thread)) { + return; + } + + if ($Global:Icinga.Public.Threads.ContainsKey($Thread)) { + if ($Global:Icinga.Public.Threads[$Thread].Started -eq $TRUE) { + $Global:Icinga.Public.Threads[$Thread].Shell.Stop(); + $Global:Icinga.Public.Threads[$Thread].Handle = $null; + $Global:Icinga.Public.Threads[$Thread].Started = $FALSE; + } + } +} +function Add-IcingaThreadPool() +{ + param ( + [string]$Name = '', + [int]$MinInstances = 1, + [int]$MaxInstances = 5 + ); + + if ($Global:Icinga.Public.ThreadPools.ContainsKey($Name)) { + return; + } + + $Global:Icinga.Public.ThreadPools.Add( + $Name, + (New-IcingaThreadPool -MinInstances $MinInstances -MaxInstances $MaxInstances) + ); +} +function Get-IcingaThreadPool() +{ + param ( + [string]$Name + ); + + if ($Global:Icinga.Public.ThreadPools.ContainsKey($Name)) { + return $Global:Icinga.Public.ThreadPools[$Name]; + } + + Write-IcingaEventMessage -Namespace 'Framework' -EventId 1401 -Objects $Name; + + return (New-IcingaThreadPool); +} +function Start-IcingaThread() +{ + param( + [string]$Thread + ); + + if ([string]::IsNullOrEmpty($Thread)) { + return; + } + + if ($Global:Icinga.Public.Threads.ContainsKey($Thread)) { + if ($Global:Icinga.Public.Threads[$Thread].Started -eq $FALSE) { + $Global:Icinga.Public.Threads[$Thread].Handle = $Global:Icinga.Public.Threads[$Thread].Shell.BeginInvoke(); + $Global:Icinga.Public.Threads[$Thread].Started = $TRUE; + } + } +} +function Test-IcingaThread() +{ + param( + [string]$Thread + ); + + if ([string]::IsNullOrEmpty($Thread)) { + return $FALSE; + } + + return $Global:Icinga.Public.Threads.ContainsKey($Thread); +} +function New-IcingaThreadInstance() +{ + param ( + [string]$Name = '', + [string]$ThreadName = $null, + $ThreadPool = $null, + [ScriptBlock]$ScriptBlock = $null, + [string]$Command = '', + [hashtable]$CmdParameters = @{ }, + [array]$Arguments = @(), + [Switch]$Start = $FALSE, + [switch]$CheckAliveState = $FALSE + ); + + if ([string]::IsNullOrEmpty($ThreadName)) { + $CallStack = Get-PSCallStack; + $SourceCommand = $CallStack[1].Command; + + if ([string]::IsNullOrEmpty($Name)) { + $Name = New-IcingaThreadHash -ShellScript $ScriptBlock -Arguments $Arguments; + } + + $ThreadName = [string]::Format('{0}::{1}::{2}::0', $SourceCommand, $Command, $Name); + + [int]$ThreadIndex = 0; + + while ($TRUE) { + + if ($Global:Icinga.Public.Threads.ContainsKey($ThreadName) -eq $FALSE) { + break; + } + + $ThreadIndex += 1; + $ThreadName = [string]::Format('{0}::{1}::{2}::{3}', $SourceCommand, $Command, $Name, $ThreadIndex); + } + } + + Write-IcingaDebugMessage -Message ( + [string]::Format( + 'Creating new thread instance {0}{1}Arguments:{1}{2}', + $ThreadName, + "`r`n", + ($Arguments | Out-String) + ) + ); + + $Shell = [PowerShell]::Create(); + $Shell.RunSpacePool = $ThreadPool; + [string]$CodeHash = ''; + + if ([string]::IsNullOrEmpty($Command) -eq $FALSE) { + + # Initialize the Icinga for Windows environment in each thread + [void]$Shell.AddCommand('Use-Icinga'); + [void]$Shell.AddParameter('-LibOnly', $TRUE); + [void]$Shell.AddParameter('-Daemon', $TRUE); + + # Share our public data between all threads + if ($null -ne $Global:Icinga -And $Global:Icinga.ContainsKey('Public')) { + [void]$Shell.AddCommand('Set-IcingaEnvironmentGlobal'); + [void]$Shell.AddParameter('GlobalEnvironment', $Global:Icinga.Public); + } + + # Set the JEA context for all threads + if ($null -ne $Global:Icinga -And $Global:Icinga.ContainsKey('Protected') -And $Global:Icinga.Protected.ContainsKey('JEAContext')) { + [void]$Shell.AddCommand('Set-IcingaEnvironmentJEA'); + [void]$Shell.AddParameter('JeaEnabled', $Global:Icinga.Protected.JEAContext); + } + + [void]$Shell.AddCommand('Set-IcingaEnvironmentThreadName'); + [void]$Shell.AddParameter('ThreadName', $ThreadName); + + [void]$Shell.AddCommand($Command); + + $CodeHash = $Command; + + foreach ($cmd in $CmdParameters.Keys) { + $Value = $CmdParameters[$cmd]; + + Write-IcingaDebugMessage -Message 'Adding new argument to thread command' -Objects $cmd, $value, $Command; + + [void]$Shell.AddParameter($cmd, $value); + + $Arguments += $cmd; + $Arguments += $value; + } + } + + if ($null -ne $ScriptBlock) { + Write-IcingaDeprecated -Function 'New-IcingaThreadInstance' -Argument 'ScriptBlock'; + $CodeHash = $ScriptBlock; + + [void]$Shell.AddScript($ScriptBlock); + foreach ($argument in $Arguments) { + [void]$Shell.AddArgument($argument); + } + } + + $Thread = New-Object PSObject; + Add-Member -InputObject $Thread -MemberType NoteProperty -Name Shell -Value $Shell; + + if ($Start) { + Write-IcingaDebugMessage -Message 'Starting shell instance' -Objects $Command, $Shell, $Thread; + try { + $ShellData = $Shell.BeginInvoke(); + } catch { + Write-IcingaDebugMessage -Message 'Failed to start Icinga thread instance' -Objects $Command, $_.Exception.Message; + } + Add-Member -InputObject $Thread -MemberType NoteProperty -Name Handle -Value ($ShellData); + Add-Member -InputObject $Thread -MemberType NoteProperty -Name Started -Value $TRUE; + } else { + Add-Member -InputObject $Thread -MemberType NoteProperty -Name Handle -Value $null; + Add-Member -InputObject $Thread -MemberType NoteProperty -Name Started -Value $FALSE; + } + + $Global:Icinga.Public.Threads.Add($ThreadName, $Thread); + + if ($CheckAliveState) { + Set-IcingaForWindowsThreadAlive ` + -ThreadName $ThreadName ` + -ThreadCmd $Command ` + -ThreadArgs $CmdParameters ` + -ThreadPool $ThreadPool; + } +} +function Set-IcingaEnvironmentJEA() +{ + param ( + [bool]$JeaEnabled = $FALSE + ); + + if ($null -ne $Global:Icinga -And $Global:Icinga.ContainsKey('Protected') -And $Global:Icinga.Protected.ContainsKey('JEAContext')) { + $Global:Icinga.Protected.JEAContext = $JeaEnabled; + } +} +<# + This code is broken and does not work. The idea is, that we create + a log entry within the Windows EventLog with a folder structure + Icinga + |_ Icinga for Windows + |_ Admin + |_ Debug + |_ Icinga Agent + |_ Admin + |_ Debug + + But it doesn't work. Ideas welcome. The entries are created, but the structure + is not represented +#> + +<# +function Register-IcingaForWindowsEventLogFolder() +{ + param ( + [string]$RootFolder = 'Icinga', + [string]$Application = 'Icinga for Windows', + [string]$Folder = '' + ); + + if ([string]::IsNullOrEmpty($Folder)) { + Write-IcingaConsoleError -Message 'You have to specify a folder name'; + return; + } + + # Base config + [string]$IcingaGUID = '{d59d4eba-fc0e-413e-b245-c53d259428c7}' + [string]$LogRoot = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT'; + [string]$ApplicationLog = 'HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\Application'; + [string]$LogChannel = [string]::Format('{0}\Channels', $LogRoot); + [string]$LogPublisher = [string]::Format('{0}\Publishers\{1}', $LogRoot, $IcingaGUID); + [string]$FolderPath = [string]::Format('{0}-{1}', $RootFolder, $Application); + [string]$LogFolderName = [string]::Format('{0}/{1}', $FolderPath, $Folder); + [string]$ChannelReference = [string]::Format('{0}\ChannelReference', $LogPublisher); + [string]$ChannelEntry = [string]::Format('{0}\{1}', $LogChannel, $LogFolderName); + [string]$ApplicationEntry = [string]::Format('{0}\{1}', $ApplicationLog, $FolderPath); + [string]$LogFile = [string]::Format('{0}\System32\Winevt\Logs\{1}%4{2}.evtx', $Env:SystemRoot, $FolderPath, $Folder); + [int]$FolderCount = 1; + + if (Test-Path $ChannelEntry) { + Write-Host 'This log does already exist'; + return; + } + + # Create the file to log into and the registry key for pointing to our GUID + if ((Test-Path $ApplicationEntry) -eq $FALSE) { + New-Item -Path $ApplicationEntry | Out-Null; + New-ItemProperty -Path $ApplicationEntry -Name 'ProviderGuid' -PropertyType 'String' -Value $IcingaGUID | Out-Null; + New-ItemProperty -Path $ApplicationEntry -Name 'File' -PropertyType 'ExpandString' -Value $LogFile | Out-Null; + } + + # Create the channel data + # HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Channels + $HKLMRoot = Get-Item -Path 'HKLM:\'; + $HKLMRoot = $HKLMRoot.OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Channels', $TRUE); + $HKLMRoot.CreateSubKey($LogFolderName) | Out-Null; + $HKLMRoot.Close(); + + New-ItemProperty -Path $ChannelEntry -Name 'OwningPublisher' -PropertyType 'String' -Value $IcingaGUID | Out-Null; + New-ItemProperty -Path $ChannelEntry -Name 'Enabled' -PropertyType 'DWord' -Value 1 | Out-Null; + New-ItemProperty -Path $ChannelEntry -Name 'Type' -PropertyType 'DWord' -Value 0 | Out-Null; + New-ItemProperty -Path $ChannelEntry -Name 'Isolation' -PropertyType 'DWord' -Value 0 | Out-Null; + + # Create the publisher data + if ((Test-Path $LogPublisher) -eq $FALSE) { + # HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Publishers\{d59d4eba-fc0e-413e-b245-c53d259428c7} + New-Item -Path $LogPublisher -Value $FolderPath | Out-Null; + New-ItemProperty -Path $LogPublisher -Name 'Enabled' -PropertyType 'DWord' -Value 1 | Out-Null; + # HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Publishers\{d59d4eba-fc0e-413e-b245-c53d259428c7}\ChannelReference + New-Item -Path $ChannelReference | Out-Null; + + # Add Count + New-ItemProperty -Path $ChannelReference -Name 'Count' -PropertyType 'DWord' -Value $FolderCount | Out-Null; + } else { + [int]$FolderCount = (Get-ItemProperty -Path $ChannelReference -Name 'Count').Count + 1; + } + + # At first, get all elements from the folder + $RegisteredFolders = Get-ChildItem $ChannelReference; + + foreach ($knownFolder in $RegisteredFolders) { + # Full path to our registry sub folder + $FolderProperty = Get-ItemProperty -Path $knownFolder.PSPath; + + if ($FolderProperty.'(default)' -eq $LogFolderName) { + Write-IcingaConsoleWarning -Message 'The EventLog folder "{0}" does already exist' -Objects $LogFolderName; + return; + } + } + + [string]$NewFolderLocation = [string]::Format('{0}\{1}', $ChannelReference, ($FolderCount - 1)); + + New-Item -Path $NewFolderLocation -Value $LogFolderName | Out-Null; + New-ItemProperty -Path $NewFolderLocation -Name 'Flags' -PropertyType 'DWord' -Value 0 | Out-Null; + New-ItemProperty -Path $NewFolderLocation -Name 'Id' -PropertyType 'DWord' -Value 16 | Out-Null; + + # Update Count + Set-ItemProperty -Path $ChannelReference -Name 'Count' -Value $FolderCount | Out-Null; +} +#> +<# +.SYNOPSIS + Default Cmdlet for printing debug messages to console +.DESCRIPTION + Default Cmdlet for printing debug messages to console +.FUNCTIONALITY + Default Cmdlet for printing debug messages to console +.EXAMPLE + PS>Write-IcingaConsoleDebug -Message 'Test message: {0}' -Objects 'Hello World'; +.PARAMETER Message + The message to print with {x} placeholdes replaced by content inside the Objects array. Replace x with the + number of the index from the objects array +.PARAMETER Objects + An array of objects being added to a provided message. The index of the array position has to refer to the + message locations. +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Write-IcingaConsoleDebug() +{ + param ( + [string]$Message, + [array]$Objects, + [switch]$DropMessage = $FALSE + ); + + if ((Get-IcingaFrameworkDebugMode) -eq $FALSE) { + return; + } + + Write-IcingaConsoleOutput ` + -Message $Message ` + -Objects $Objects ` + -ForeColor 'Blue' ` + -Severity 'Debug' ` + -DropMessage:$DropMessage; +} +<# +.SYNOPSIS + Standardise console output and make handling of object conversion easier into messages + by using this standard function for displaying severity and log entries +.DESCRIPTION + Standardised function to output console messages controlled by the arguments provided + for coloring, displaying severity and add objects into output messages +.FUNCTIONALITY + Standardise console output and make handling of object conversion easier into messages + by using this standard function for displaying severity and log entries +.EXAMPLE + PS>Write-IcingaConsoleOutput -Message 'Test message: {0}' -Objects 'Hello World' -ForeColor 'Green' -Severity 'Test'; +.PARAMETER Message + The message to print with {x} placeholdes replaced by content inside the Objects array. Replace x with the + number of the index from the objects array +.PARAMETER Objects + An array of objects being added to a provided message. The index of the array position has to refer to the + message locations. +.PARAMETER ForeColor + The color the severity name will be displayed in +.PARAMETER Severity + The severity being displayed before the actual message. Leave empty to skip. +.PARAMETER NoNewLine + Will ensure that no new line is added at the end of the message, allowing to + write different messages with different function calls without line breaks +.PARAMETER DropMessage + Will not write the message to the console and simply drop it +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Write-IcingaConsoleOutput() +{ + param ( + [string]$Message, + [array]$Objects, + [ValidateSet('Default', 'Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White')] + [string]$ForeColor = 'Default', + [string]$Severity = 'Notice', + [switch]$NoNewLine = $FALSE, + [switch]$DropMessage = $FALSE + ); + + if ($DropMessage) { + return; + } + + if ((Test-IcingaFrameworkConsoleOutput) -eq $FALSE) { + return; + } + + # Never write console output in case the Framework is running as daemon + if ($Global:Icinga.Protected.RunAsDaemon -eq $TRUE) { + return; + } + + $OutputMessage = $Message; + [int]$Index = 0; + + foreach ($entry in $Objects) { + + $OutputMessage = $OutputMessage.Replace( + [string]::Format('{0}{1}{2}', '{', $Index, '}'), + $entry + ); + $Index++; + } + + if ($Global:Icinga.ContainsKey('InstallWizard') -And [string]::IsNullOrEmpty($OutputMessage) -eq $FALSE) { + if ($Severity -eq 'Error') { + if ($Global:Icinga.InstallWizard.LastError -NotContains $OutputMessage) { + $Global:Icinga.InstallWizard.LastError += $OutputMessage; + } + } + if ($Severity -eq 'Warning') { + if ($Global:Icinga.InstallWizard.LastWarning -NotContains $OutputMessage) { + $Global:Icinga.InstallWizard.LastWarning += $OutputMessage; + } + } + } + + if ([string]::IsNullOrEmpty($Severity) -eq $FALSE) { + Write-Host '[' -NoNewline; + Write-Host $Severity -NoNewline -ForegroundColor $ForeColor; + Write-Host ']: ' -NoNewline; + Write-Host $OutputMessage; + + return; + } + + if ($ForeColor -eq 'Default') { + Write-Host $OutputMessage -NoNewline:$NoNewLine; + } else { + Write-Host $OutputMessage -ForegroundColor $ForeColor -NoNewline:$NoNewLine; + } +} +<# +.SYNOPSIS + Default Cmdlet for printing notice messages to console +.DESCRIPTION + Default Cmdlet for printing notice messages to console +.FUNCTIONALITY + Default Cmdlet for printing notice messages to console +.EXAMPLE + PS>Write-IcingaConsoleNotice -Message 'Test message: {0}' -Objects 'Hello World'; +.PARAMETER Message + The message to print with {x} placeholdes replaced by content inside the Objects array. Replace x with the + number of the index from the objects array +.PARAMETER Objects + An array of objects being added to a provided message. The index of the array position has to refer to the + message locations. +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Write-IcingaConsoleNotice() +{ + param ( + [string]$Message, + [array]$Objects, + [switch]$DropMessage = $FALSE + ); + + Write-IcingaConsoleOutput ` + -Message $Message ` + -Objects $Objects ` + -ForeColor 'Green' ` + -Severity 'Notice' ` + -DropMessage:$DropMessage; +} +function Unregister-IcingaEventLog() +{ + # Icinga for Windows v1.8.0 or later + Remove-EventLog -LogName 'Icinga for Windows' -ErrorAction SilentlyContinue; + + # Older versions + Remove-EventLog -Source 'Icinga for Windows' -ErrorAction SilentlyContinue; + # Icinga for Windows v1.8.0 or later - required a second time to ensure + # everything is removed for legacy versions + Remove-EventLog -LogName 'Icinga for Windows' -ErrorAction SilentlyContinue; +} +function Write-IcingaDeprecated() +{ + param ( + [string]$Function, + [string]$Argument + ); + + if ([string]::IsNullOrEmpty($Function)) { + return; + } + + $Message = 'The called function or method "{0}" is deprecated. Please update your component or contact the developer to update the component accordingly.'; + + if ([string]::IsNullOrEmpty($Argument) -eq $FALSE) { + $Message = 'The function or method "{0}" is called with deprecated argument "{1}". Please update your component or contact the developer to update the component accordingly.'; + } + + Write-IcingaConsoleOutput ` + -Message $Message ` + -Objects $Function, $Argument ` + -ForeColor 'Cyan' ` + -Severity 'Deprecated'; + + Write-IcingaEventMessage -EventId 1001 -Namespace 'Framework' -Objects ` + ([string]::Format('Command or Method: {0}', $Function)), + ([string]::Format('Argument: {0}', $Argument)), + (Get-PSCallStack); +} +<# +.SYNOPSIS + Default Cmdlet for printing error messages to console +.DESCRIPTION + Default Cmdlet for printing error messages to console +.FUNCTIONALITY + Default Cmdlet for printing error messages to console +.EXAMPLE + PS>Write-IcingaConsoleError -Message 'Test message: {0}' -Objects 'Hello World'; +.PARAMETER Message + The message to print with {x} placeholder replaced by content inside the Objects array. Replace x with the + number of the index from the objects array +.PARAMETER Objects + An array of objects being added to a provided message. The index of the array position has to refer to the + message locations. +.PARAMETER DropMessage + Allows to programmatically drop a message in case it is not required without dealing with If-Blocks +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Write-IcingaConsoleError() +{ + param ( + [string]$Message, + [array]$Objects, + [switch]$DropMessage = $FALSE + ); + + Write-IcingaConsoleOutput ` + -Message $Message ` + -Objects $Objects ` + -ForeColor 'Red' ` + -Severity 'Error' ` + -DropMessage:$DropMessage; +} +<# +.SYNOPSIS + Default Cmdlet for printing plain messages to console +.DESCRIPTION + Default Cmdlet for printing plain messages to console +.FUNCTIONALITY + Default Cmdlet for printing plain messages to console +.EXAMPLE + PS>Write-IcingaConsolePlain -Message 'Test message: {0}' -Objects 'Hello World'; +.PARAMETER Message + The message to print with {x} placeholdes replaced by content inside the Objects array. Replace x with the + number of the index from the objects array +.PARAMETER Objects + An array of objects being added to a provided message. The index of the array position has to refer to the + message locations. +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Write-IcingaConsolePlain() +{ + param ( + [string]$Message, + [array]$Objects, + [ValidateSet('Default', 'Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 'DarkRed', 'DarkMagenta', 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 'Red', 'Magenta', 'Yellow', 'White')] + [string]$ForeColor = 'Default', + [switch]$NoNewLine = $FALSE, + [switch]$DropMessage = $FALSE + ); + + Write-IcingaConsoleOutput ` + -Message $Message ` + -Objects $Objects ` + -ForeColor $ForeColor ` + -Severity $null ` + -NoNewLine:$NoNewLine ` + -DropMessage:$DropMessage; +} +function Write-IcingaErrorMessage() +{ + param( + [int]$EventId = 0, + [string]$Message = $null + ); + + if ($EventId -eq 0 -Or [string]::IsNullOrEmpty($Message)) { + return; + } + + Write-EventLog -LogName 'Icinga for Windows' -Source 'Icinga for Windows' -EntryType Error -EventId $EventId -Message $Message; +} +function Write-IcingaEventMessage() +{ + param ( + [int]$EventId = 0, + [string]$Namespace = $null, + [array]$Objects = @(), + $ExceptionObject = $null + ); + + if ($EventId -eq 0 -Or [string]::IsNullOrEmpty($Namespace)) { + return; + } + + [string]$EntryType = $IcingaEventLogEnums[$Namespace][$EventId].EntryType; + [string]$Message = $IcingaEventLogEnums[$Namespace][$EventId].Message; + [string]$Details = $IcingaEventLogEnums[$Namespace][$EventId].Details; + + if ([string]::IsNullOrEmpty($Details)) { + $Details = ''; + } + if ([string]::IsNullOrEmpty($Message)) { + $Message = ''; + } + + [string]$ObjectDump = ''; + + if ($Objects.Count -eq 0) { + $ObjectDump = [string]::Format( + '{0}No additional object details provided.', + (New-IcingaNewLine) + ); + } + + foreach ($entry in $Objects) { + $ObjectDump = [string]::Format( + '{0}{1}', + $ObjectDump, + ($entry | Out-String) + ); + } + + [string]$EventLogMessage = [string]::Format( + '{0}{1}{1}{2}{3}{1}{1}Object details:{1}{4}', + $Message, + (New-IcingaNewLine), + $Details, + (Get-IcingaExceptionString -ExceptionObject $ExceptionObject), + $ObjectDump + ); + + if ($null -eq $EntryType -Or $null -eq $Message) { + return; + } + + [int]$MaxEventLogMessageSize = 30000; + + if ($EventLogMessage.Length -ge $MaxEventLogMessageSize) { + while ($EventLogMessage.Length -ge $MaxEventLogMessageSize) { + $CutMessage = $EventLogMessage.Substring(0, $MaxEventLogMessageSize); + Write-EventLog ` + -LogName 'Icinga for Windows' ` + -Source ([string]::Format('IfW::{0}', $Namespace)) ` + -EntryType $EntryType ` + -EventId $EventId ` + -Message $CutMessage; + + $EventLogMessage = $EventLogMessage.Substring($MaxEventLogMessageSize, $EventLogMessage.Length - $MaxEventLogMessageSize); + } + } + + if ([string]::IsNullOrEmpty($EventLogMessage)) { + return; + } + + Write-EventLog ` + -LogName 'Icinga for Windows' ` + -Source ([string]::Format('IfW::{0}', $Namespace)) ` + -EntryType $EntryType ` + -EventId $EventId ` + -Message $EventLogMessage; +} +function Get-IcingaExceptionString() +{ + param ( + $ExceptionObject = $null + ); + + if ($null -eq $ExceptionObject) { + return ''; + } + + $ExceptionStack = New-Object -TypeName 'System.Text.StringBuilder'; + + $ExceptionStack.AppendLine('') | Out-Null; + $ExceptionStack.AppendLine('') | Out-Null; + $ExceptionStack.AppendLine('Icinga for Windows exception report:') | Out-Null; + + if ([string]::IsNullOrEmpty($ExceptionObject.Exception.Message) -eq $FALSE) { + $ExceptionStack.AppendLine('') | Out-Null; + $ExceptionStack.AppendLine('Exception Message:') | Out-Null; + $ExceptionStack.AppendLine($ExceptionObject.Exception.Message) | Out-Null; + } + if ([string]::IsNullOrEmpty($ExceptionObject.InvocationInfo.InvocationName) -eq $FALSE) { + $ExceptionStack.AppendLine('') | Out-Null; + $ExceptionStack.AppendLine('Invocation Name:') | Out-Null; + $ExceptionStack.AppendLine($ExceptionObject.InvocationInfo.InvocationName) | Out-Null; + } + if ([string]::IsNullOrEmpty($ExceptionObject.InvocationInfo.CommandOrigin) -eq $FALSE) { + $ExceptionStack.AppendLine('') | Out-Null; + $ExceptionStack.AppendLine('Command Origin:') | Out-Null; + $ExceptionStack.AppendLine($ExceptionObject.InvocationInfo.CommandOrigin) | Out-Null; + } + if ([string]::IsNullOrEmpty($ExceptionObject.InvocationInfo.ScriptLineNumber) -eq $FALSE) { + $ExceptionStack.AppendLine('') | Out-Null; + $ExceptionStack.AppendLine('Script Line Number:') | Out-Null; + $ExceptionStack.AppendLine($ExceptionObject.InvocationInfo.ScriptLineNumber) | Out-Null; + } + if ([string]::IsNullOrEmpty($ExceptionObject.InvocationInfo.PositionMessage) -eq $FALSE) { + $ExceptionStack.AppendLine('') | Out-Null; + $ExceptionStack.AppendLine('Exact Position:') | Out-Null; + $ExceptionStack.AppendLine($ExceptionObject.InvocationInfo.PositionMessage) | Out-Null; + } + if ([string]::IsNullOrEmpty($ExceptionObject.Exception.StackTrace) -eq $FALSE) { + $ExceptionStack.AppendLine('') | Out-Null; + $ExceptionStack.AppendLine('StackTrace:') | Out-Null; + $ExceptionStack.AppendLine($ExceptionObject.Exception.StackTrace) | Out-Null; + } + + $CallStack = Get-PSCallStack; + + $ExceptionStack.AppendLine('') | Out-Null; + $ExceptionStack.AppendLine('Call Stack:') | Out-Null; + if ($CallStack.Count -gt 11) { + $ExceptionStack.AppendLine(($CallStack[0..10] | Out-String)) | Out-Null; + } else { + $ExceptionStack.AppendLine(($CallStack | Out-String)) | Out-Null; + } + $ExceptionStack.Remove($ExceptionStack.Length - 8, 8) | Out-Null; + + return $ExceptionStack.ToString(); +} +<# + # This script will provide 'Enums' we can use for proper + # error handling and to provide more detailed descriptions + # + # Example usage: + # $IcingaEventLogEnums[2000] + #> +if ($null -eq $IcingaEventLogEnums -Or $IcingaEventLogEnums.ContainsKey('Framework') -eq $FALSE) { + [hashtable]$IcingaEventLogEnums += @{ + 'Framework' = @{ + 1001 = @{ + 'EntryType' = 'Warning'; + 'Message' = 'Icinga for Windows deprecation warning'; + 'Details' = 'Icinga for Windows or one of its components executed a function or method, which is flagged as deprecated. Please modify your code or contact the responsible developer to update the component to no longer user this deprecated function or method.'; + 'EventId' = 1001; + }; + 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; + }; + 1103 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Failed to load Icinga for Windows namespace content'; + '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'; + 'Details' = 'Icinga for Windows could not find the Function or Cmdlet for the specified background daemon. The daemon was not loaded.'; + 'EventId' = 1400; + }; + 1401 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Icinga for Windows thread pool not found'; + 'Details' = 'Icinga for Windows was unable to find a specified thread pool with [Get-IcingaThreadPool] for a background daemon. To keep the daemon running, it defaulted to a basic pool but this issue should be addressed. The name of the inquired pool is:'; + 'EventId' = 1401; + }; + 1450 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Icinga for Windows service check daemon invalid index'; + 'Details' = 'Icinga for Windows is unable to process the provided time index for a background service check task, as a given index is not numeric'; + 'EventId' = 1450; + }; + 1451 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Icinga for Windows service check daemon exception on plugin execution'; + 'Details' = 'Icinga for Windows failed to execute a plugin within the background service check daemon with an exception'; + 'EventId' = 1451; + }; + 1452 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Icinga for Windows service check daemon failed with exception'; + 'Details' = 'Icinga for Windows failed to properly execute a task within the background service check daemon with a given exception'; + 'EventId' = 1452; + }; + 1500 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Failed to securely establish a communication between this server and the client'; + 'Details' = 'A client connection could not be established to this server. This issue is mostly caused by using Self-Signed/Icinga 2 Agent certificates for the server and the client not trusting the certificate. To resolve this issue, either use trusted certificates signed by your trusted CA or setup the client to accept untrusted certificates'; + 'EventId' = 1500; + }; + 1501 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Client connection was interrupted because of invalid SSL stream'; + 'Details' = 'A client connection was terminated by the Framework because no secure SSL handshake could be established. This issue in general is followed by EventId 1500.'; + 'EventId' = 1501; + }; + 1502 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Unable to create PowerShell RunSpace in JEA context'; + 'Details' = 'A PowerShell RunSpace for background threads could not be created, as the required Icinga for Windows session configuration file could not be found. Use "Install-IcingaJEAProfile" to resolve this problem.'; + 'EventId' = 1502; + }; + 1503 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Unable to start Icinga for Windows service'; + 'Details' = 'Unable to start Icinga for Windows service, as the JEA session created by the service is still active. Run "Restart-IcingaForWindows" to restart the Icinga for Windows service, while running in JEA context to prevent this issue.'; + 'EventId' = 1503; + }; + 1504 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Icinga for Windows JEA context vanished'; + 'Details' = 'The Icinga for Windows JEA session is no longer available. It might have either crashed or get terminated by user actions, like restarting the WinRM service.'; + 'EventId' = 1504; + }; + 1505 = @{ + 'EntryType' = 'Warning'; + 'Message' = 'Icinga for Windows JEA context not available'; + 'Details' = 'The Icinga for Windows JEA session is no longer available and is attempted to be restarted on the system. This could have either happened due to a crash or a user action, like restarting the WinRM service.'; + 'EventId' = 1505; + }; + 1506 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Icinga Agent certificate not signed by Icinga CA'; + 'Details' = 'The local Icinga Agent certificate seems not to be signed by our Icinga CA yet. Using this certificate for the REST-Api as example might not work yet. Please check the state of the certificate and complete the signing process if required [IWKB000013]'; + 'EventId' = 1506; + }; + 1507 = @{ + 'EntryType' = 'Error'; + 'Message' = 'An internal threading error occurred. A frozen thread was detected'; + 'Details' = 'One of the internal Icinga for Windows threads was being active but not responding for at least 5 minutes. The frozen thread has been terminated and restarted.'; + 'EventId' = 1507; + }; + 1508 = @{ + 'EntryType' = 'Warning'; + 'Message' = 'Icinga for Windows certificate is pending for signing'; + 'Details' = 'The Icinga for Windows certificate is pending to be signed. This happens if the Icinga Agent certificate is not yet signed by the Icinga CA. The creation will pause for some minutes and try again.'; + 'EventId' = 1508; + }; + 1509 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Icinga CA certificate could not be imported to the AuthRoot store'; + 'Details' = 'The Icinga CA certificate could not be imported to the LocalMachine\AuthRoot store on this device. This will cause all local PowerShell requests to communicate with the API to be untrusted, requring to enable untrusted certificate request as otherwise the communication will fail.'; + 'EventId' = 1509; + }; + 1550 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Unsupported web authentication used'; + 'Details' = 'A web client tried to authenticate with an unsupported authorization method.'; + 'EventId' = 1550; + }; + 1551 = @{ + 'EntryType' = 'Warning'; + 'Message' = 'Invalid authentication credentials provided'; + 'Details' = 'A web request for a client was rejected because of invalid formatted base64 encoded credentials.'; + 'EventId' = 1551; + }; + 1552 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Failed to parse use credentials from base64 encoding'; + 'Details' = 'Provided user credentials encoded as base64 could not be converted to domain, user and password objects.'; + 'EventId' = 1552; + }; + 1553 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Failed to query Icinga check over internal REST-Api check handler'; + 'Details' = 'A service check could not be executed by using the internal REST-Api check handler. The check either ran into a timeout or could not be processed. Maybe the check was not registered to be allowed for being executed. Further details can be found below.'; + 'EventId' = 1553; + }; + 1560 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Failed to test user login as no Principal Context could be established'; + 'Details' = 'A web client trying to authenticate failed as no Principal Context for the provided domain could be established.'; + 'EventId' = 1560; + }; + 1561 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Failed to authenticate user with given credentials'; + 'Details' = 'A web client trying to authenticate failed as the provided user credentials could not be verified.'; + 'EventId' = 1561; + }; + 1600 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Exception on function calls in JEA context'; + 'Details' = 'An exception occurred while executing Icinga for Windows code inside a JEA context.'; + 'EventId' = 1600; + }; + 1700 = @{ + 'EntryType' = 'Error'; + 'Message' = 'Icinga Agent hostname is not set inside the constants.conf'; + 'Details' = 'Your current Icinga Agent configuration is missing a crutial configuration part inside your constants.conf, which should set the current configured hostname as NodeName. As long as this configuration is missing, certain Icinga for Windows feature might only partially work. Re-Installing Icinga for Windows might resolve this issue.'; + 'EventId' = 1700; + }; + } + }; +} + +Export-ModuleMember -Variable @( 'IcingaEventLogEnums' ); +function Register-IcingaEventLog() +{ + param ( + [string]$LogName = $null + ); + + if ((Test-AdministrativeShell) -eq $FALSE) { + return; + } + + if ([string]::IsNullOrEmpty($LogName)) { + New-EventLog -LogName 'Icinga for Windows' -Source 'IfW::Framework' -ErrorAction SilentlyContinue; + New-EventLog -LogName 'Icinga for Windows' -Source 'IfW::Service' -ErrorAction SilentlyContinue; + New-EventLog -LogName 'Icinga for Windows' -Source 'IfW::Debug' -ErrorAction SilentlyContinue; + } else { + $LogName = [string]::Format('IfW::{0}', $LogName); + + New-EventLog -LogName 'Icinga for Windows' -Source $LogName -ErrorAction SilentlyContinue; + } + + $IfWEventLog = Get-WinEvent -ListLog 'Icinga for Windows'; + + # In case the value is already set, nothing to do + if ($IfWEventLog.MaximumSizeInBytes -ge 20971520) { + return; + } + + # Set the size to 20MiB + $IfWEventLog.MaximumSizeInBytes = 20971520; + $IfWEventLog.SaveChanges(); +} +function Write-IcingaDebugMessage() +{ + param ( + [string]$Message, + [array]$Objects = @(), + $ExceptionObject = $null + ); + + if ([string]::IsNullOrEmpty($Message)) { + return; + } + + if ($Global:Icinga.Protected.DebugMode -eq $FALSE) { + return; + } + + [array]$DebugContent = @($Message); + $DebugContent += $Objects; + + Write-IcingaEventMessage -EventId 1000 -Namespace 'Debug' -ExceptionObject $ExceptionObject -Objects $DebugContent; + + $DebugContent = $null; +} +<# + # This script will provide 'Enums' we can use for proper + # error handling and to provide more detailed descriptions + # + # Example usage: + # $IcingaEventLogEnums[2000] + #> +if ($null -eq $IcingaEventLogEnums -Or $IcingaEventLogEnums.ContainsKey('Debug') -eq $FALSE) { + [hashtable]$IcingaEventLogEnums += @{ + 'Debug' = @{ + 1000 = @{ + 'EntryType' = 'Information'; + 'Message' = 'Generic debug message issued by the Framework or its components'; + '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; + }; + } + }; +} + +Export-ModuleMember -Variable @( 'IcingaEventLogEnums' ); +<# +.SYNOPSIS + Default Cmdlet for printing warning messages to console +.DESCRIPTION + Default Cmdlet for printing warning messages to console +.FUNCTIONALITY + Default Cmdlet for printing warning messages to console +.EXAMPLE + PS>Write-IcingaConsoleWarning -Message 'Test message: {0}' -Objects 'Hello World'; +.PARAMETER Message + The message to print with {x} placeholdes replaced by content inside the Objects array. Replace x with the + number of the index from the objects array +.PARAMETER Objects + An array of objects being added to a provided message. The index of the array position has to refer to the + message locations. +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Write-IcingaConsoleWarning() +{ + param ( + [string]$Message, + [array]$Objects, + [switch]$DropMessage = $FALSE + ); + + Write-IcingaConsoleOutput ` + -Message $Message ` + -Objects $Objects ` + -ForeColor 'DarkYellow' ` + -Severity 'Warning' ` + -DropMessage:$DropMessage; +} +<# +.SYNOPSIS + Writes a given config object to disk +.DESCRIPTION + Writes a given config object to disk +.FUNCTIONALITY + Writes a given config object to disk +.EXAMPLE + PS>Write-IcingaPowerShellConfig -Config $PSObject; +.PARAMETER Config + A PSObject containing the entire configuration to write +.INPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Write-IcingaPowerShellConfig() +{ + param ( + $Config + ); + + $ConfigDir = Get-IcingaPowerShellConfigDir; + $ConfigFile = Join-Path -Path $ConfigDir -ChildPath 'config.json'; + + if (-Not (Test-Path $ConfigDir)) { + New-Item -Path $ConfigDir -ItemType Directory -ErrorAction SilentlyContinue | Out-Null; + } + + $Content = ConvertTo-Json -InputObject $Config -Depth 100; + + Write-IcingaFileSecure -File $ConfigFile -Value $Content; +} +<# +.SYNOPSIS + Test if a config entry on an object is already present +.DESCRIPTION + Test if a config entry on an object is already present +.FUNCTIONALITY + Test if a config entry on an object is already present +.EXAMPLE + PS>Test-IcingaPowerShellConfigItem -ConfigObject $PSObject -ConfigKey 'keyname'; +.PARAMETER ConfigObject + The custom config object to check for +.PARAMETER ConfigKey + The key which is checked +.INPUTS + System.String +.OUTPUTS + System.Boolean +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Test-IcingaPowerShellConfigItem() +{ + param ( + $ConfigObject, + $ConfigKey + ); + + if ($null -eq $ConfigObject -Or [string]::IsNullOrEmpty($ConfigKey)) { + return $FALSE; + } + + foreach ($entry in $ConfigObject.PSObject.Properties) { + if ($entry.Name.ToLower() -eq $ConfigKey.ToLower()) { + return $TRUE; + } + } + + return $FALSE; +} +<# +.SYNOPSIS + Creates a new config entry with given arguments +.DESCRIPTION + Creates a new config entry with given arguments +.FUNCTIONALITY + Creates a new config entry with given arguments +.EXAMPLE + PS>New-IcingaPowerShellConfigItem -ConfigObject $PSObject -ConfigKey 'keyname' -ConfigValue 'keyvalue'; +.PARAMETER ConfigObject + The custom config object to modify +.PARAMETER ConfigKey + The key which is added to the config object +.PARAMETER ConfigValue + The value written for the ConfigKey +.INPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function New-IcingaPowerShellConfigItem() +{ + param( + $ConfigObject, + [string]$ConfigKey, + $ConfigValue = $null + ); + + if ($null -eq $ConfigValue) { + $ConfigValue = (New-Object -TypeName PSObject); + } + + $ConfigObject | Add-Member -MemberType NoteProperty -Name $ConfigKey -Value $ConfigValue; +} +<# +.SYNOPSIS + Returns the configuration for a provided config path +.DESCRIPTION + Returns the configuration for a provided config path +.FUNCTIONALITY + Returns the configuration for a provided config path +.EXAMPLE + PS>Get-IcingaPowerShellConfig -Path 'framework.daemons'; +.PARAMETER Path + The path to the config item to check for +.INPUTS + System.String +.OUTPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaPowerShellConfig() +{ + param ( + $Path = '' + ); + + $Config = Read-IcingaPowerShellConfig; + $PathArray = $Path.Split('.'); + $ConfigObject = $Config; + + foreach ($entry in $PathArray) { + if (-Not (Test-IcingaPowerShellConfigItem -ConfigObject $ConfigObject -ConfigKey $entry)) { + return $null; + } + + $ConfigObject = $ConfigObject.$entry; + } + + return $ConfigObject; + + <# + # Alternate config parser. Might come handy in the future, requires to redesign + # Set-IcingaPowerShellConfig including all calls for full coverage + $Config = Read-IcingaPowerShellConfig; + $PathArray = $Path.Split('.'); + $ConfigObject = $Config; + [int]$Index = 0; + $entry = $PathArray[$Index]; + + while ($Index -lt $PathArray.Count) { + if (-Not (Test-IcingaPowerShellConfigItem -ConfigObject $ConfigObject -ConfigKey $entry) -And $Index -lt $PathArray.Count) { + $Index += 1; + $entry = [string]::Format('{0}.{1}', $entry, $PathArray[$Index]); + + continue; + } elseif (-Not (Test-IcingaPowerShellConfigItem -ConfigObject $ConfigObject -ConfigKey $entry) -And $Index -ge $PathArray.Count) { + return $null; + } + + $ConfigObject = $ConfigObject.$entry; + $Index += 1; + $entry = $PathArray[$Index]; + } + + return $ConfigObject; + #> +} +<# +.SYNOPSIS + Sets a config entry for a given path to a certain value +.DESCRIPTION + Sets a config entry for a given path to a certain value +.FUNCTIONALITY + Sets a config entry for a given path to a certain value +.EXAMPLE + PS>Set-IcingaPowerShellConfig -Path 'framework.daemons.servicecheck' -Value $DaemonConfig; +.PARAMETER Path + The path to the config item to be set +.PARAMETER Value + The value to be set for a specific config path +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Set-IcingaPowerShellConfig() +{ + param( + $Path = '', + $Value = $null + ); + + $Config = Read-IcingaPowerShellConfig; + $PathArray = $Path.Split('.'); + $ConfigObject = $Config; + [int]$Index = $PathArray.Count; + $InputValue = $null; + foreach ($entry in $PathArray) { + if ($index -eq 1) { + $InputValue = $Value; + } + if (-Not (Test-IcingaPowerShellConfigItem -ConfigObject $ConfigObject -ConfigKey $entry)) { + New-IcingaPowerShellConfigItem -ConfigObject $ConfigObject -ConfigKey $entry -ConfigValue $InputValue; + } + + if ($index -eq 1) { + $ConfigObject.$entry = $Value; + break; + } + + $ConfigObject = $ConfigObject.$entry; + $index -= 1; + } + + Write-IcingaPowerShellConfig $Config; +} +<# +.SYNOPSIS + Removes a config entry from a given path +.DESCRIPTION + Removes a config entry from a given path +.FUNCTIONALITY + Removes a config entry from a given path +.EXAMPLE + PS>Remove-IcingaPowerShellConfig -Path 'framework.daemons'; +.PARAMETER Path + The path to the config item to remove +.INPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Remove-IcingaPowerShellConfig() +{ + param( + $Path = '' + ); + + if ([string]::IsNullOrEmpty($Path)) { + throw 'Please specify a valid path to an object'; + } + + $Config = Read-IcingaPowerShellConfig; + $PathArray = $Path.Split('.'); + $ConfigObject = $Config; + [int]$Index = $PathArray.Count; + + foreach ($entry in $PathArray) { + + if (-Not (Test-IcingaPowerShellConfigItem -ConfigObject $ConfigObject -ConfigKey $entry)) { + return $null; + } + + if ($index -eq 1) { + $ConfigObject.PSObject.Properties.Remove($entry); + break; + } + + $ConfigObject = $ConfigObject.$entry; + $Index -= 1; + } + + Write-IcingaPowerShellConfig $Config; +} +<# +.SYNOPSIS + Returns the amount of items for a config item +.DESCRIPTION + Returns the amount of items for a config item +.FUNCTIONALITY + Returns the amount of items for a config item +.EXAMPLE + PS>Get-IcingaConfigTreeCount -Path 'framework.daemons'; +.PARAMETER Path + The path to the config item to check for +.INPUTS + System.String +.OUTPUTS + System.Integer +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaConfigTreeCount() +{ + param( + $Path = '' + ); + + $Config = Read-IcingaPowerShellConfig; + $PathArray = $Path.Split('.'); + $ConfigObject = $Config; + [int]$Count = 0; + + foreach ($entry in $PathArray) { + if (-Not (Test-IcingaPowerShellConfigItem -ConfigObject $ConfigObject -ConfigKey $entry)) { + continue; + } + + $ConfigObject = $ConfigObject.$entry; + } + + foreach ($config in $ConfigObject.PSObject.Properties) { + $Count += 1; + } + + return $Count; +} +<# +.SYNOPSIS + Reads the entire configuration and returns it as custom object +.DESCRIPTION + Reads the entire configuration and returns it as custom object +.FUNCTIONALITY + Reads the entire configuration and returns it as custom object +.EXAMPLE + PS>Read-IcingaPowerShellConfig; +.OUTPUTS + System.Object +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +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 ([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' -ExceptionObject $_ -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); + } + } + + return $ConfigObject; +} +<# +.SYNOPSIS + Tests if a specific WMI class including the Namespace can be accessed and returns status codes for possible errors/exceptions that might occur. + Returns binary operator values for easier comparison. In case no errors occurred it will return $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo.Ok +.DESCRIPTION + Tests if a specific WMI class including the Namespace can be accessed and returns status codes for possible errors/exceptions that might occur. + Returns binary operator values for easier comparison. In case no errors occurred it will return $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo.Ok +.ROLE + ### WMI Permissions + + No special permissions required as this Cmdlet will validate all input data and reports back the result. +.OUTPUTS + Name Value + ---- ----- + Ok 1 + EmptyClass 2 + PermissionError 4 + ObjectNotFound 8 + InvalidNameSpace 16 + UnhandledException 32 + NotSpecified 64 + CimNotInstalled 128 +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> +function Test-IcingaWindowsInformation() +{ + param ( + [string]$ClassName, + [string]$NameSpace = 'Root\Cimv2' + ); + + if ([string]::IsNullOrEmpty($ClassName)) { + return $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo.EmptyClass; + } + + # Check with Get-CimClass for the specified WMI class and in the specified namespace default root\cimv2 + if ((Test-IcingaFunction 'Get-CimInstance') -eq $FALSE ) { + return $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo.CimNotInstalled; + } + + # We clear all previous errors so that we can catch the last error message from this try/catch in the plugins. + $Error.Clear(); + + try { + Get-CimInstance -ClassName $ClassName -Namespace $NameSpace -ErrorAction Stop | Out-Null; + } catch { + + Write-IcingaConsoleDebug ` + -Message "WMIClass: '{0}' : Namespace : {1} {2} {3}" ` + -Objects $ClassName, $NameSpace, (New-IcingaNewLine), $_.Exception.Message; + + if ($_.CategoryInfo.Category -like 'MetadataError') { + return $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo.InvalidNameSpace; + } + + if ($_.CategoryInfo.Category -like 'ObjectNotFound') { + return $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo.ObjectNotFound; + } + + if ($_.CategoryInfo.Category -like 'NotSpecified') { + return $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo.NotSpecified; + } + + if ($_.CategoryInfo.Category -like 'PermissionDenied') { + return $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo.PermissionError; + } + + return $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo.UnhandledException; + } + + return $TestIcingaWindowsInfoEnums.TestIcingaWindowsInfo.Ok; +} +<# +.SYNOPSIS + Removes a user from a specific Wmi namespace +.DESCRIPTION + Removes a user from a specific Wmi namespace +.PARAMETER User + The user to set permissions for. Can either be a local or domain user +.PARAMETER Namespace + The Wmi namespace to grant permissions for. Required namespaces are listed within each plugin documentation +.INPUTS + System.String +.OUTPUTS + System.Boolean +#> + +function Remove-IcingaWmiPermissions() +{ + param ( + [string]$User, + [string]$Namespace + ); + + if ([string]::IsNullOrEmpty($User)) { + Write-IcingaConsoleError 'Please enter a valid username'; + return $FALSE; + } + + if ([string]::IsNullOrEmpty($Namespace)) { + Write-IcingaConsoleError 'You have to specify a Wmi namespace to grant permissions for'; + return $FALSE; + } + + $WmiSecurity = Get-IcingaWmiSecurityData -User $User -Namespace $Namespace; + + if ($null -eq $WmiSecurity) { + return $FALSE; + } + + [System.Management.ManagementBaseObject[]]$RebasedDACL = @() + [bool]$UserPresent = $FALSE; + + foreach ($entry in $WmiSecurity.WmiAcl.DACL) { + if ($entry.Trustee.SidString -ne $WmiSecurity.UserSID) { + $RebasedDACL += $entry.PSObject.immediateBaseObject; + } else { + $UserPresent = $TRUE; + } + } + + if ($UserPresent -eq $FALSE) { + Write-IcingaConsoleNotice 'User "{0}" is not configured for namespace "{1}"' -Objects $User, $Namespace; + return $TRUE; + } + + $WmiSecurity.WmiAcl.DACL = $RebasedDACL.PSObject.immediateBaseObject; + + $WmiSecurity.WmiArguments.Name = 'SetSecurityDescriptor'; + $WmiSecurity.WmiArguments.Add('ArgumentList', $WmiSecurity.WmiAcl.PSObject.immediateBaseObject); + $WmiArguments = $WmiSecurity.WmiArguments + + $WmiSecurityData = Invoke-WmiMethod @WmiArguments; + if ($WmiSecurityData.ReturnValue -ne 0) { + Write-IcingaConsoleError 'Failed to set Wmi security descriptor information with error {0}' -Objects $WmiSecurityData.ReturnValue; + return $FALSE; + } + + Write-IcingaConsoleNotice 'Removed user "{0}" from Namespace "{1}" successfully' -Objects $User, $Namespace; + + return $TRUE; +} +<# +.SYNOPSIS + Allows to query Wmi information by either using Wmi directly or Cim. This provides a save handling + to call Wmi classes, as we are catching possible errors including missing permissions for better + and improved error output during plugin execution. +.DESCRIPTION + Allows to query Wmi information by either using Wmi directly or Cim. This provides a save handling + to call Wmi classes, as we are catching possible errors including missing permissions for better + and improved error output during plugin execution. +.PARAMETER ClassName + The Wmi class to fetch information from +.PARAMETER Filter + Allows to filter only for specific Wmi information. The syntax is identical to Get-WmiObject and Get-CimInstance +.PARAMETER Namespace + The Wmi namespace to lookup additional information. The syntax is identical to Get-WmiObject and Get-CimInstance +.PARAMETER ForceWMI + Forces the usage of `Get-WmiObject` instead of `Get-CimInstance` +.EXAMPLE + PS>Get-IcingaWindowsInformation -ClassName Win32_Service; +.EXAMPLE + PS>Get-IcingaWindowsInformation -ClassName Win32_Service -ForceWMI; +.EXAMPLE + PS>Get-IcingaWindowsInformation -ClassName MSFT_NetAdapter -NameSpace 'root\StandardCimv2'; +.EXAMPLE + PS>Get-IcingaWindowsInformation Win32_LogicalDisk -Filter 'DriveType = 3'; +.INPUTS + System.String +.OUTPUTS + System.Boolean +#> + +function Get-IcingaWindowsInformation() +{ + param ( + [string]$ClassName, + $Filter, + $Namespace, + [switch]$ForceWMI = $FALSE + ); + + $Arguments = @{ + 'ClassName' = $ClassName; + } + + if ([string]::IsNullOrEmpty($Filter) -eq $FALSE) { + $Arguments.Add( + 'Filter', $Filter + ); + } + if ([string]::IsNullOrEmpty($Namespace) -eq $FALSE) { + $Arguments.Add( + 'Namespace', $Namespace + ); + } + + if ($ForceWMI -eq $FALSE -And (Get-Command 'Get-CimInstance' -ErrorAction SilentlyContinue)) { + try { + $CimData = (Get-CimInstance @Arguments -ErrorAction Stop); + + Write-IcingaDebugMessage 'Debug output for "Get-IcingaWindowsInformation::Get-CimInstance"' -Objects $ClassName, $Filter, $Namespace, ($CimData | Out-String); + + return $CimData; + } catch { + $ErrorName = $_.Exception.NativeErrorCode; + $ErrorMessage = $_.Exception.Message; + $ErrorCode = $_.Exception.StatusCode; + + if ([string]::IsNullOrEmpty($Namespace)) { + $Namespace = 'root/cimv2'; + } + + switch ($ErrorCode) { + # Permission error + 2 { + Exit-IcingaThrowException -ExceptionType 'Permission' -ExceptionThrown $IcingaExceptions.Permission.CimInstance -CustomMessage ([string]::Format('Class: "{0}", Namespace: "{1}"', $ClassName, $Namespace)) -Force; + }; + # InvalidClass + 5 { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.CimClassNameUnknown -CustomMessage $ClassName -Force; + }; + # All other errors + default { + Exit-IcingaThrowException -ExceptionType 'Custom' -InputString $ErrorMessage -CustomMessage ([string]::Format('CimInstanceUnhandledError: Class "{0}": Error "{1}": Id "{2}"', $ClassName, $ErrorName, $ErrorCode)) -Force; + } + } + } + } + + if ((Get-Command 'Get-WmiObject' -ErrorAction SilentlyContinue)) { + try { + $WmiData = (Get-WmiObject @Arguments -ErrorAction Stop); + + Write-IcingaDebugMessage 'Debug output for "Get-IcingaWindowsInformation::Get-WmiObject"' -Objects $ClassName, $Filter, $Namespace, ($WmiData | Out-String); + + return $WmiData; + } catch { + $ErrorName = $_.CategoryInfo.Category; + $ErrorMessage = $_.Exception.Message; + $ErrorCode = ($_.Exception.HResult -band 0xFFFF); + + if ([string]::IsNullOrEmpty($Namespace)) { + $Namespace = 'root/cimv2'; + } + + switch ($ErrorName) { + # Permission error + 'InvalidOperation' { + Exit-IcingaThrowException -ExceptionType 'Permission' -ExceptionThrown $IcingaExceptions.Permission.WMIObject -CustomMessage ([string]::Format('Class: "{0}", Namespace: "{1}"', $ClassName, $Namespace)) -Force; + }; + # Invalid Class + 'InvalidType' { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Inputs.WmiObjectClassUnknown -CustomMessage $ClassName -Force; + }; + # All other errors + default { + Exit-IcingaThrowException -ExceptionType 'Custom' -InputString $ErrorMessage -CustomMessage ([string]::Format('WmiObjectUnhandledError: Class "{0}": Error "{1}": Id "{2}"', $ClassName, $ErrorName, $ErrorCode)) -Force; + } + } + } + } + + # Exception + Exit-IcingaThrowException -ExceptionType 'Custom' -InputString 'Failed to fetch Windows information by using CimInstance or WmiObject. Both commands are not present on the system.' -CustomMessage ([string]::Format('CimWmiUnhandledError: Class "{0}"', $ClassName)) -Force; +} +<# + # WMI WBEM_SECURITY_FLAGS + # https://docs.microsoft.com/en-us/windows/win32/api/wbemcli/ne-wbemcli-wbem_security_flags + # https://docs.microsoft.com/en-us/windows/win32/secauthz/standard-access-rights +#> + +[hashtable]$SecurityFlags = @{ + 'WBEM_Enable' = 1; + 'WBEM_Method_Execute' = 2; + 'WBEM_Full_Write_Rep' = 4; + 'WBEM_Partial_Write_Rep' = 8; + 'WBEM_Write_Provider' = 0x10; + 'WBEM_Remote_Access' = 0x20; + 'WBEM_Right_Subscribe' = 0x40; + 'WBEM_Right_Publish' = 0x80; + 'Read_Control' = 0x20000; + 'Write_DAC' = 0x40000; +}; + +[hashtable]$SecurityDescription = @{ + 1 = 'Enables the account and grants the user read permissions. This is a default access right for all users and corresponds to the Enable Account permission on the Security tab of the WMI Control. For more information, see Setting Namespace Security with the WMI Control.'; + 2 = 'Allows the execution of methods. Providers can perform additional access checks. This is a default access right for all users and corresponds to the Execute Methods permission on the Security tab of the WMI Control.'; + 4 = 'Allows a user account to write to classes in the WMI repository as well as instances. A user cannot write to system classes. Only members of the Administrators group have this permission. WBEM_FULL_WRITE_REP corresponds to the Full Write permission on the Security tab of the WMI Control.'; + 8 = 'Allows you to write data to instances only, not classes. A user cannot write classes to the WMI repository. Only members of the Administrators group have this right. WBEM_PARTIAL_WRITE_REP corresponds to the Partial Write permission on the Security tab of the WMI Control.'; + 0x10 = 'Allows writing classes and instances to providers. Note that providers can do additional access checks when impersonating a user. This is a default access right for all users and corresponds to the Provider Write permission on the Security tab of the WMI Control.'; + 0x20 = 'Allows a user account to remotely perform any operations allowed by the permissions described above. Only members of the Administrators group have this right. WBEM_REMOTE_ACCESS corresponds to the Remote Enable permission on the Security tab of the WMI Control.'; + 0x40 = 'Specifies that a consumer can subscribe to the events delivered to a sink. Used in IWbemEventSink::SetSinkSecurity.'; + 0x80 = 'Specifies that the account can publish events to the instance of __EventFilter that defines the event filter for a permanent consumer. Available in wbemcli.h.'; + 0x20000 = 'The right to read the information in the objects security descriptor, not including the information in the system access control list (SACL).'; + 0x40000 = 'The right to modify the discretionary access control list (DACL) in the objects security descriptor.'; +}; + +[hashtable]$SecurityNames = @{ + 'Enable' = 'WBEM_Enable'; + 'MethodExecute' = 'WBEM_Method_Execute'; + 'FullWrite' = 'WBEM_Full_Write_Rep'; + 'PartialWrite' = 'WBEM_Partial_Write_Rep'; + 'ProviderWrite' = 'WBEM_Write_Provider'; + 'RemoteAccess' = 'WBEM_Remote_Access'; + 'Subscribe' = 'WBEM_Right_Subscribe'; + 'Publish' = 'WBEM_Right_Publish'; + 'ReadSecurity' = 'Read_Control'; + 'WriteSecurity' = 'Write_DAC'; +}; + +[hashtable]$AceFlags = @{ + 'Access_Allowed' = 0x0; + 'Access_Denied' = 0x1; + 'Container_Inherit' = 0x2; +} + +[hashtable]$IcingaWBEM = @{ + SecurityFlags = $SecurityFlags; + SecurityDescription = $SecurityDescription + SecurityNames = $SecurityNames; + AceFlags = $AceFlags; +} + +Export-ModuleMember -Variable @( 'IcingaWBEM' ); +<# +.SYNOPSIS + Tests the current set permissions for a user on a specific namespace and returns true if the + current configuration is matching the intended configuration and returns false if either no + permissions are set yet or the intended configuration is not matching the current configuration +.DESCRIPTION + Tests the current set permissions for a user on a specific namespace and returns true if the + current configuration is matching the intended configuration and returns false if either no + permissions are set yet or the intended configuration is not matching the current configuration +.PARAMETER User + The user to set permissions for. Can either be a local or domain user +.PARAMETER Namespace + The Wmi namespace to grant permissions for. Required namespaces are listed within each plugin documentation +.PARAMETER Enable + Enables the account and grants the user read permissions. This is a default access right for all users and corresponds to the Enable Account permission on the Security tab of the WMI Control. For more information, see Setting Namespace Security with the WMI Control. +.PARAMETER RemoteAccess + Allows a user account to remotely perform any operations allowed by the permissions described above. Only members of the Administrators group have this right. WBEM_REMOTE_ACCESS corresponds to the Remote Enable permission on the Security tab of the WMI Control. +.PARAMETER Recurse + Applies a container inherit flag and grants permission not only on the specific Wmi tree but also objects within this namespace (recommended) +.PARAMETER DenyAccess + Blocks the user from having access to this Wmi and or sub namespace tree. +.PARAMETER Flags + Allows to specify additional flags for permission granting: PartialWrite, Subscribe, ProviderWrite,ReadSecurity, WriteSecurity, Publish, MethodExecute, FullWrite +.INPUTS + System.String +.OUTPUTS + System.Boolean +#> + +function Test-IcingaWmiPermissions() +{ + param ( + [string]$User, + [string]$Namespace, + [switch]$Enable, + [switch]$RemoteAccess, + [switch]$Recurse, + [switch]$DenyAccess, + [array]$Flags + ); + + if ([string]::IsNullOrEmpty($User)) { + Write-IcingaConsoleError 'Please enter a valid username'; + return $FALSE; + } + + if ([string]::IsNullOrEmpty($Namespace)) { + Write-IcingaConsoleError 'You have to specify a Wmi namespace to grant permissions for'; + return $FALSE; + } + + [int]$PermissionMask = [int]$PermissionMask = New-IcingaWmiPermissionMask -Enable:$Enable -RemoteAccess:$RemoteAccess -Flags $Flags; + + if ($PermissionMask -eq 0) { + Write-IcingaConsoleError 'You have to specify permissions to grant for a specific user'; + return $FALSE; + } + + $WmiSecurity = Get-IcingaWmiSecurityData -User $User -Namespace $Namespace; + + if ($null -eq $WmiSecurity) { + return $FALSE; + } + + [System.Management.ManagementBaseObject]$UserACL = $null; + + foreach ($entry in $WmiSecurity.WmiAcl.DACL) { + if ($entry.Trustee.SidString -eq $WmiSecurity.UserSID) { + $UserACL = $entry.PSObject.immediateBaseObject; + break; + } + } + + # No permissions granted for this user + if ($null -eq $UserACL) { + return $FALSE; + } + + [bool]$RecurseMatch = $TRUE; + + if ($Recurse -And $UserACL.AceFlags -ne $IcingaWBEM.AceFlags.Container_Inherit) { + $RecurseMatch = $FALSE; + } elseif ($Recurse -eq $FALSE -And $UserACL.AceFlags -ne 0) { + $RecurseMatch = $FALSE; + } + + if ($UserACL.AccessMask -ne $PermissionMask -Or $RecurseMatch -eq $FALSE) { + return $FALSE; + } + + return $TRUE; +} +<# +.SYNOPSIS + Generates a permission mask based on the set and provided flags which are used + for adding/testing Wmi permissions +.DESCRIPTION + Generates a permission mask based on the set and provided flags which are used + for adding/testing Wmi permissions +.PARAMETER Enable + Enables the account and grants the user read permissions. This is a default access right for all users and corresponds to the Enable Account permission on the Security tab of the WMI Control. For more information, see Setting Namespace Security with the WMI Control. +.PARAMETER RemoteAccess + Allows a user account to remotely perform any operations allowed by the permissions described above. Only members of the Administrators group have this right. WBEM_REMOTE_ACCESS corresponds to the Remote Enable permission on the Security tab of the WMI Control. +.PARAMETER Flags + Allows to specify additional flags for permssion granting: PartialWrite, Subscribe, ProviderWrite,ReadSecurity, WriteSecurity, Publish, MethodExecute, FullWrite +.INPUTS + System.String +.OUTPUTS + System.Int +#> + +function New-IcingaWmiPermissionMask() +{ + param ( + [switch]$Enable, + [switch]$RemoteAccess, + [array]$Flags + ); + + [int]$PermissionMask = 0; + + if ($Enable) { + $PermissionMask += $IcingaWBEM.SecurityFlags.WBEM_Enable; + } + if ($RemoteAccess) { + $PermissionMask += $IcingaWBEM.SecurityFlags.WBEM_Remote_Access; + } + + foreach ($flag in $Flags) { + if ($flag -like 'Enable' -And $Enable) { + continue; + } + if ($flag -like 'RemoteAccess' -And $RemoteAccess) { + continue; + } + + if ($IcingaWBEM.SecurityNames.ContainsKey($flag) -eq $FALSE) { + Write-IcingaConsoleError 'Invalid Security flag "{0}" . Supported flags: {1}' -Objects $flag, $IcingaWBEM.SecurityNames.Keys; + return $FALSE; + } + + $PermissionMask += $IcingaWBEM.SecurityFlags[$IcingaWBEM.SecurityNames[$flag]]; + } + + return $PermissionMask; +} +<# +.SYNOPSIS + Sets permissions for a specific Wmi namespace for a user. You can grant basic permissions based + on the arguments available and grant additional ones with the `-Flags` argument. +.DESCRIPTION + Sets permissions for a specific Wmi namespace for a user. You can grant basic permissions based + on the arguments available and grant additional ones with the `-Flags` argument. +.PARAMETER User + The user to set permissions for. Can either be a local or domain user +.PARAMETER Namespace + The Wmi namespace to grant permissions for. Required namespaces are listed within each plugin documentation +.PARAMETER Enable + Enables the account and grants the user read permissions. This is a default access right for all users and corresponds to the Enable Account permission on the Security tab of the WMI Control. For more information, see Setting Namespace Security with the WMI Control. +.PARAMETER RemoteAccess + Allows a user account to remotely perform any operations allowed by the permissions described above. Only members of the Administrators group have this right. WBEM_REMOTE_ACCESS corresponds to the Remote Enable permission on the Security tab of the WMI Control. +.PARAMETER Recurse + Applies a container inherit flag and grants permission not only on the specific Wmi tree but also objects within this namespace (recommended) +.PARAMETER DenyAccess + Blocks the user from having access to this Wmi and or subnamespace tree. +.PARAMETER Flags + Allows to specify additional flags for permssion granting: PartialWrite, Subscribe, ProviderWrite,ReadSecurity, WriteSecurity, Publish, MethodExecute, FullWrite +.EXAMPLE + PS>Add-IcingaWmiPermissions -Namespace 'root\cimv2' -User icinga -Enable; +.EXAMPLE + PS>Add-IcingaWmiPermissions -Namespace 'root\cimv2' -User 'ICINGADOMAIN\icinga' -Enable -RemoteAccess; +.EXAMPLE + PS>Add-IcingaWmiPermissions -Namespace 'root\cimv2' -User 'ICINGADOMAIN\icinga' -Enable -RemoteAccess -Recurse; +.EXAMPLE + PS>Add-IcingaWmiPermissions -Namespace 'root\cimv2' -User 'ICINGADOMAIN\icinga' -Enable -RemoteAccess -Flags 'ReadSecurity', 'MethodExecute' -Recurse; +.INPUTS + System.String +.OUTPUTS + System.Boolean +#> + +function Add-IcingaWmiPermissions() +{ + param ( + [string]$User, + [string]$Namespace, + [switch]$Enable, + [switch]$RemoteAccess, + [switch]$Recurse, + [switch]$DenyAccess, + [array]$Flags + ); + + if ([string]::IsNullOrEmpty($User)) { + Write-IcingaConsoleError 'Please enter a valid username'; + return $FALSE; + } + + if ([string]::IsNullOrEmpty($Namespace)) { + Write-IcingaConsoleError 'You have to specify a Wmi namespace to grant permissions for'; + return $FALSE; + } + + [int]$PermissionMask = New-IcingaWmiPermissionMask -Enable:$Enable -RemoteAccess:$RemoteAccess -Flags $Flags; + + if ($PermissionMask -eq 0) { + Write-IcingaConsoleError 'You have to specify permissions to grant for a specific user'; + return $FALSE; + } + + if (Test-IcingaWmiPermissions -User $User -Namespace $Namespace -Enable:$Enable -RemoteAccess:$RemoteAccess -Recurse:$Recurse -DenyAccess:$DenyAccess -Flags $Flags) { + Write-IcingaConsoleNotice 'Wmi permissions for user "{0}" are already set.' -Objects $User; + return $TRUE; + } else { + Write-IcingaConsoleNotice 'Removing possible existing configuration for this user before continuing'; + Remove-IcingaWmiPermissions -User $User -Namespace $Namespace | Out-Null; + } + + $WmiSecurity = Get-IcingaWmiSecurityData -User $User -Namespace $Namespace; + + if ($null -eq $WmiSecurity) { + return $FALSE; + } + + $WmiAce = (New-Object System.Management.ManagementClass("Win32_Ace")).CreateInstance(); + $WmiAce.AccessMask = $PermissionMask; + + if ($Recurse) { + $WmiAce.AceFlags = $IcingaWBEM.AceFlags.Container_Inherit; + } else { + $WmiAce.AceFlags = 0; + } + + $WmiTrustee = (New-Object System.Management.ManagementClass("Win32_Trustee")).CreateInstance(); + $WmiTrustee.SidString = Get-IcingaUserSID -User $User; + $WmiAce.Trustee = $WmiTrustee + + if ($DenyAccess) { + $WmiAce.AceType = $IcingaWBEM.AceFlags.Access_Denied; + } else { + $WmiAce.AceType = $IcingaWBEM.AceFlags.Access_Allowed; + } + + $WmiSecurity.WmiAcl.DACL += $WmiAce.PSObject.immediateBaseObject; + + $WmiSecurity.WmiArguments.Name = 'SetSecurityDescriptor'; + $WmiSecurity.WmiArguments.Add('ArgumentList', $WmiSecurity.WmiAcl.PSObject.immediateBaseObject); + $WmiArguments = $WmiSecurity.WmiArguments; + + $WmiSecurityData = Invoke-WmiMethod @WmiArguments; + if ($WmiSecurityData.ReturnValue -ne 0) { + Write-IcingaConsoleError 'Failed to set Wmi security descriptor information with error {0}' -Objects $WmiSecurityData.ReturnValue; + return $FALSE; + } + + Write-IcingaConsoleNotice 'Wmi permissions for Namespace "{0}" and user "{1}" was set successfully' -Objects $Namespace, $User; + + return $TRUE; +} +<# +.SYNOPSIS + Returns several information about the Wmi namespace and the provided user data to + work with them while adding/testing/removing Wmi permissions +.DESCRIPTION + Returns several information about the Wmi namespace and the provided user data to + work with them while adding/testing/removing Wmi permissions +.PARAMETER User + The user to set permissions for. Can either be a local or domain user +.PARAMETER Namespace + The Wmi namespace to grant permissions for. Required namespaces are listed within each plugin documentation +.INPUTS + System.String +.OUTPUTS + System.Hashtable +#> + +function Get-IcingaWmiSecurityData() +{ + param ( + [string]$User, + [string]$Namespace + ); + + [hashtable]$WmiArguments = @{ + 'Name' = 'GetSecurityDescriptor'; + 'Namespace' = $Namespace; + 'Path' = "__systemsecurity=@"; + } + + $WmiSecurityData = Invoke-WmiMethod @WmiArguments; + + if ($WmiSecurityData.ReturnValue -ne 0) { + Write-IcingaConsoleError 'Fetching Wmi security descriptor information failed with error {0}' -Objects $WmiSecurityData.ReturnValue; + return $null; + } + + $UserData = Split-IcingaUserDomain -User $User; + $UserSID = Get-IcingaUserSID -User $User; + $WmiAcl = $WmiSecurityData.Descriptor; + + if ([string]::IsNullOrEmpty($UserSID)) { + Write-IcingaConsoleError 'Unable to load the SID for user "{0}"' -Objects $User; + return $null; + } + + return @{ + 'WmiArguments' = $WmiArguments; + 'UserData' = $UserData; + 'UserSID' = $UserSID; + 'WmiAcl' = $WmiAcl; + } +} +<# +.SYNOPSIS + Sets the configuration for a proxy server which is used by all Invoke-WebRequests + to ensure internet connections are working correctly. +.DESCRIPTION + Sets the configuration for a proxy server which is used by all Invoke-WebRequests + to ensure internet connections are working correctly. +.FUNCTIONALITY + Sets the configuration for a proxy server which is used by all Invoke-WebRequests + to ensure internet connections are working correctly. +.EXAMPLE + PS>Set-IcingaFrameworkProxyServer -Server 'http://example.com:8080'; +.PARAMETER Server + The server with the port for the Proxy. Example: 'http://example.com:8080' +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Set-IcingaFrameworkProxyServer() +{ + param ( + [string]$Server = '' + ); + + Set-IcingaPowerShellConfig -Path 'Framework.Proxy.Server' -Value $Server; + + Write-IcingaConsoleNotice 'The Proxy server for the PowerShell Framework has been set to "{0}"' -Objects $Server; +} +<# +.SYNOPSIS + Sets the allowed TLS version for communicating with endpoints to TLS 1.2 and 1.1 +.DESCRIPTION + Sets the allowed TLS version for communicating with endpoints to TLS 1.2 and 1.1 +.FUNCTIONALITY + Uses the [Net.ServicePointManager] to set the SecurityProtocol to TLS 1.2 and 1.1 +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Set-IcingaTLSVersion() +{ + [Net.ServicePointManager]::SecurityProtocol = "tls12, tls11"; +} +<# +.SYNOPSIS + Fetches the configuration of the configured Proxy server for the Framework, in + case it is set +.DESCRIPTION + etches the configuration of the configured Proxy server for the Framework, in + case it is set +.FUNCTIONALITY + etches the configuration of the configured Proxy server for the Framework, in + case it is set +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaFrameworkProxyServer() +{ + return (Get-IcingaPowerShellConfig -Path 'Framework.Proxy.Server'); +} +<# +.SYNOPSIS + Disables the progress bar during file downloads or while loading certain modules. + This will increase the speed of certain tasks, for example file downloads +.DESCRIPTION + Disables the progress bar during file downloads or while loading certain modules. + This will increase the speed of certain tasks, for example file downloads +.FUNCTIONALITY + Sets the $ProgressPreference to 'SilentlyContinue' +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Disable-IcingaProgressPreference() +{ + $global:ProgressPreference = "SilentlyContinue"; +} +<# +.SYNOPSIS + A wrapper function for Invoke-WebRequest to allow easier proxy support and + to catch errors more directly. +.DESCRIPTION + A wrapper function for Invoke-WebRequest to allow easier proxy support and + to catch errors more directly. +.FUNCTIONALITY + Uses Invoke-WebRequest to fetch information and returns the same output, but + with direct error handling and global proxy support by configuration +.EXAMPLE + PS>Invoke-IcingaWebRequest -Uri 'https://icinga.com'; +.EXAMPLE + PS>Invoke-IcingaWebRequest -Uri 'https://icinga.com' -UseBasicParsing; +.EXAMPLE + PS>Invoke-IcingaWebRequest -Uri 'https://{0}.com' -UseBasicParsing -Objects 'icinga'; +.EXAMPLE + PS>Invoke-IcingaWebRequest -Uri 'https://{0}.com' -UseBasicParsing -Objects 'icinga' -Headers @{ 'accept' = 'application/json' }; +.PARAMETER Uri + The Uri for the web request +.PARAMETER Body + Specifies the body of the request. The body is the content of the request that follows the headers. You can + also pipe a body value to Invoke-WebRequest. + + The Body parameter can be used to specify a list of query parameters or specify the content of the response. + + When the input is a GET request and the body is an IDictionary (typically, a hash table), the body is added + to the URI as query parameters. For other GET requests, the body is set as the value of the request body in + the standard name=value format. + + When the body is a form, or it is the output of an Invoke-WebRequest call, Windows PowerShell sets the + request content to the form fields. +.PARAMETER Headers + Web headers send with the request as hashtable +.PARAMETER Method + The request method to send to the destination. + Allowed values: 'Get', 'Post', 'Put', 'Trace', 'Patch', 'Options', 'Merge', 'Head', 'Default', 'Delete' +.PARAMETER OutFile + Specifies the output file for which this cmdlet saves the response body. Enter a path and file name. If you omit the path, the default is the current location. +.PARAMETER UseBasicParsing + Indicates that the cmdlet uses the response object for HTML content without Document Object Model (DOM) + parsing. + + This parameter is required when Internet Explorer is not installed on the computers, such as on a Server + Core installation of a Windows Server operating system. +.PARAMETER Objects + Use placeholders within the `-Uri` argument, like {0} and replace them with array elements of this argument. + The index entry of {0} has to match the order of this argument. +.PARAMETER NoErrorMessage + Will not print any error message caused by invalid requests or errors +.INPUTS + System.String +.OUTPUTS + System.Boolean +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Invoke-IcingaWebRequest() +{ + param ( + [string]$Uri = '', + $Body, + [hashtable]$Headers, + [ValidateSet('Get', 'Post', 'Put', 'Trace', 'Patch', 'Options', 'Merge', 'Head', 'Default', 'Delete')] + [string]$Method = 'Get', + [string]$OutFile, + [switch]$UseBasicParsing, + [array]$Objects = @(), + [switch]$NoErrorMessage = $FALSE + ); + + [int]$Index = 0; + foreach ($entry in $Objects) { + + $Uri = $Uri.Replace( + [string]::Format('{0}{1}{2}', '{', $Index, '}'), + $entry + ); + $Index++; + } + + # If our URI is a local path or a file share path, always ensure to use the correct Windows directory + # handling with '\' instead of '/' + if ([string]::IsNullOrEmpty($Uri) -eq $FALSE -And (Test-Path $Uri)) { + $Uri = $Uri.Replace('/', '\'); + } + + $WebArguments = @{ + 'Uri' = $Uri; + 'Method' = $Method; + } + + if ($Method -ne 'Get' -And $null -ne $Body -and [string]::IsNullOrEmpty($Body) -eq $FALSE) { + $WebArguments.Add('Body', $Body); + } + + if ($Headers.Count -ne 0) { + $WebArguments.Add('Headers', $Headers); + } + + if ([string]::IsNullOrEmpty($OutFile) -eq $FALSE) { + $WebArguments.Add('OutFile', $OutFile); + } + + $ProxyServer = Get-IcingaFrameworkProxyServer; + + if ([string]::IsNullOrEmpty($ProxyServer) -eq $FALSE) { + $WebArguments.Add('Proxy', $ProxyServer); + } + + Set-IcingaTLSVersion; + Disable-IcingaProgressPreference; + + $ErrorStatus = 900; + + try { + $Response = Invoke-WebRequest -UseBasicParsing:$UseBasicParsing @WebArguments; + } catch { + [string]$ErrorId = ([string]$_.FullyQualifiedErrorId).Split(',')[0]; + [string]$Message = $_.Exception.Message; + + if ([string]::IsNullOrEmpty($_.Exception.Response.StatusCode.Value__) -eq $FALSE) { + $ErrorStatus = $_.Exception.Response.StatusCode.Value__; + } + + if ($_.Exception.ToString().Contains('TlsStream')) { + $ErrorId = 'System.Net.TlsStream.Exception'; + } + + switch ($ErrorId) { + 'System.UriFormatException' { + Write-IcingaConsoleError 'The provided Url "{0}" is not a valid format. Response: "{1}"' -Objects $Uri, $Message -DropMessage:$NoErrorMessage; + break; + }; + 'WebCmdletWebResponseException' { + Write-IcingaConsoleError 'The remote host "{0}" send an exception response "{1}": "{2}"' -Objects $Uri, $ErrorStatus, $Message -DropMessage:$NoErrorMessage; + break; + }; + 'System.InvalidOperationException' { + Write-IcingaConsoleError 'Failed to query host "{0}". Possible this is caused by an invalid Proxy Server configuration: "{1}". Response: "{2}"' -Objects $Uri, $ProxyServer, $Message -DropMessage:$NoErrorMessage; + break; + }; + 'System.Net.TlsStream.Exception' { + Write-IcingaConsoleError 'Failed to establish secure SSL/TLS connection to "{0}". Please ensure the certificate is valid and trusted and use "Set-IcingaTLSVersion" on older Windows machines. If you are using self-signed certificates, install them locally or use "Enable-IcingaUntrustedCertitifacateValidation". Error Message: "{1}"' -Objects $Uri, $Message -DropMessage:$NoErrorMessage; + $ErrorStatus = 901; + break; + } + Default { + Write-IcingaConsoleError 'Unhandled exception for Url "{0}" with error id "{1}":{2}{2}{3}{2}Response: "{4}"' -Objects $Uri, $ErrorId, (New-IcingaNewLine), $Message -DropMessage:$NoErrorMessage; + break; + }; + } + + # Return some sort of objects which are often used to ensure we at least have some out-of-the-box compatibility + return @{ + 'HasErrors' = $TRUE; + 'BaseResponse' = @{ + 'ResponseUri' = @{ + 'AbsoluteUri' = $Uri; + }; + }; + 'StatusCode' = $ErrorStatus; + }; + } + + return $Response; +} +function Get-IcingaProviderFilterData() +{ + param ( + [string]$ProviderName = '', + [hashtable]$ProviderFilter = @{ } + ); + + [hashtable]$FilterResult = @{ }; + + foreach ($filterObject in $ProviderFilter.Keys) { + if ($filterObject.ToLower() -ne $ProviderName.ToLower()) { + continue; + } + + if ($FilterResult.ContainsKey($filterObject) -eq $FALSE) { + $FilterResult.Add( + $filterObject, + (New-IcingaProviderFilterObject -ProviderName $ProviderName -HashtableFilter $ProviderFilter[$filterObject]) + ); + } + } + + return $FilterResult; +} +function New-IcingaProviderFilterObject() +{ + param ( + [string]$ProviderName = '', + [hashtable]$HashtableFilter = @{ } + ); + + if ([string]::IsNullOrEmpty($ProviderName)) { + return @{ }; + } + + [array]$ProviderFilterCmdlet = Get-Command ([string]::Format('New-IcingaProviderFilterData{0}', $ProviderName)) -ErrorAction SilentlyContinue; + + if ($null -eq $ProviderFilterCmdlet -Or $ProviderFilterCmdlet.Count -eq 0) { + return @{ }; + } + + if ((Test-IcingaForWindowsCmdletLoader -Path $ProviderFilterCmdlet[0].Module.ModuleBase) -eq $FALSE) { + return @{ }; + } + + $FilterResult = & $ProviderFilterCmdlet[0].Name @HashtableFilter; + + [string]$ObjectName = $ProviderName; + $CmdHelp = Get-Help ($ProviderFilterCmdlet[0].Name) -ErrorAction SilentlyContinue; + + if ($null -ne $CmdHelp) { + if ([string]::IsNullOrEmpty($CmdHelp.Role) -eq $FALSE) { + [string]$ObjectName = [string]($CmdHelp.Role); + } + } + + $CmdHelp = $null; + $ProviderFilterCmdlet = $null; + + return @{ + $ObjectName = $FilterResult; + }; +} +function New-IcingaProviderObject() +{ + param ( + [string]$Name = 'Undefined' + ); + + $ProviderObject = New-Object PSCustomObject; + $ProviderObject | Add-Member -MemberType NoteProperty -Name 'Name' -Value $Name; + $ProviderObject | Add-Member -MemberType NoteProperty -Name 'FeatureInstalled' -Value $TRUE; + $ProviderObject | Add-Member -MemberType NoteProperty -Name 'Metadata' -Value (New-Object PSCustomObject); + $ProviderObject | Add-Member -MemberType NoteProperty -Name 'Metrics' -Value (New-Object PSCustomObject); + $ProviderObject | Add-Member -MemberType NoteProperty -Name 'MetricsOverTime' -Value (New-Object PSCustomObject); + $ProviderObject.MetricsOverTime | Add-Member -MemberType NoteProperty -Name 'MetricContainer' -Value (New-Object PSCustomObject); + $ProviderObject.MetricsOverTime | Add-Member -MemberType NoteProperty -Name 'Cache' -Value (New-Object PSCustomObject); + $ProviderObject.MetricsOverTime | Add-Member -MemberType NoteProperty -Name 'Compiled' -Value (New-Object PSCustomObject); + + return $ProviderObject; +} +function Get-IcingaProviderElement() +{ + param ( + $Object = $null + ); + + if ($null -eq $Object) { + return @(); + } + + try { + return $Object.PSObject.Properties + } catch { + return @(); + } + + return @(); +} +function Get-IcingaProviderData() +{ + param ( + [array]$Name = '', + [array]$IncludeFilter = @(), + [array]$ExcludeFilter = @(), + [switch]$IncludeDetails = $FALSE + ); + + [hashtable]$ProviderData = @{ }; + + foreach ($entry in $Name) { + [array]$ProviderDataList = Get-Command -Name ([string]::Format('Get-IcingaProviderDataValues{0}', $entry)) -ErrorAction SilentlyContinue; + + if ($null -eq $ProviderDataList -Or $ProviderDataList.Count -eq 0) { + $ProviderData.Add($entry, 'Provider not Found'); + continue; + } + + if ($ProviderDataList.Count -gt 1) { + $ProviderData.Add($entry, 'Provider name not unique enough'); + continue; + } + + if ((Test-IcingaForWindowsCmdletLoader -Path $ProviderDataList.Module.ModuleBase) -eq $FALSE) { + $ProviderData.Add($entry, 'Security violation. Provider not installed at Framework location'); + continue; + } + + $ProviderCmd = $ProviderDataList[0]; + $ProviderContent = (& $ProviderCmd -IncludeDetails:$IncludeDetails -IncludeFilter $IncludeFilter -ExcludeFilter $ExcludeFilter); + + if ($ProviderData.ContainsKey($ProviderContent.Name) -eq $FALSE) { + $ProviderData.Add($ProviderContent.Name, $ProviderContent); + } else { + $ProviderData[$ProviderContent.Name] = $ProviderContent; + } + } + + return $ProviderData; +} +<# +.ROLE + Query +#> + +function New-IcingaProviderFilterDataCpu() +{ + param ( + [switch]$Limit100Percent = $FALSE, + [switch]$IncludeDetails = $FALSE + ); + + $CpuData = New-IcingaProviderObject -Name 'Cpu'; + $CpuCounter = New-IcingaPerformanceCounterArray '\Processor Information(*)\% Processor Utility'; + $CounterStructure = New-IcingaPerformanceCounterStructure -CounterCategory 'Processor Information' -PerformanceCounterHash $CpuCounter; + [int]$TotalCpuThreads = 0; + [decimal]$TotalCpuLoad = 0; + [int]$SocketCount = 0; + [hashtable]$SocketList = @{ }; + + foreach ($currentcore in $CounterStructure.Keys) { + [string]$Socket = $currentcore.Split(',')[0]; + [string]$CoreId = $currentcore.Split(',')[1]; + [string]$SocketName = [string]::Format('Socket #{0}', $Socket); + + if ($Socket -eq '_Total' -Or $CoreId -eq '_Total') { + continue; + } + + if ($SocketList.ContainsKey($SocketName) -eq $FALSE) { + $SocketList.Add( + $SocketName, + @{ + 'ThreadCount' = 0; + 'TotalLoad' = 0; + } + ); + } + + if ((Test-PSCustomObjectMember -PSObject $CpuData.Metrics -Name $SocketName) -eq $FALSE) { + $CpuData.Metrics | Add-Member -MemberType NoteProperty -Name $SocketName -Value (New-Object PSCustomObject); + } + + [decimal]$CoreLoad = $CounterStructure[$currentcore]['% Processor Utility'].value; + + if ($Limit100Percent) { + if ($CoreLoad -gt 100) { + $CoreLoad = 100; + } + } + + $CpuData.Metrics.$SocketName | Add-Member -MemberType NoteProperty -Name $CoreId -Value $CoreLoad; + $CpuData.MetricsOverTime.MetricContainer | Add-Member -MemberType NoteProperty -Name $SocketName -Value $null -Force; + + $SocketList[$SocketName].ThreadCount += 1; + $SocketList[$SocketName].TotalLoad += $CoreLoad; + } + + $CpuData.Metadata | Add-Member -MemberType NoteProperty -Name 'Sockets' -Value (New-Object PSCustomObject); + + foreach ($entry in $SocketList.Keys) { + $SocketList[$entry].TotalLoad = $SocketList[$entry].TotalLoad / $SocketList[$entry].ThreadCount; + $TotalCpuLoad += $SocketList[$entry].TotalLoad; + $TotalCpuThreads += $SocketList[$entry].ThreadCount; + $SocketCount += 1; + + $CpuData.Metadata.Sockets | Add-Member -MemberType NoteProperty -Name $entry -Value (New-Object PSCustomObject); + $CpuData.Metadata.Sockets.$entry | Add-Member -MemberType NoteProperty -Name 'Threads' -Value $SocketList[$entry].ThreadCount; + $CpuData.Metrics.$entry | Add-Member -MemberType NoteProperty -Name 'Total' -Value $SocketList[$entry].TotalLoad; + } + + $CpuData.Metadata | Add-Member -MemberType NoteProperty -Name 'TotalLoad' -Value ($TotalCpuLoad / $SocketCount); + $CpuData.Metadata | Add-Member -MemberType NoteProperty -Name 'TotalLoadSum' -Value $TotalCpuLoad; + $CpuData.Metadata | Add-Member -MemberType NoteProperty -Name 'TotalThreads' -Value $TotalCpuThreads; + $CpuData.Metadata | Add-Member -MemberType NoteProperty -Name 'CoreDigits' -Value ([string]$TotalCpuThreads).Length; + $CpuData.Metadata | Add-Member -MemberType NoteProperty -Name 'CoreDetails' -Value (New-Object PSCustomObject); + + if ($IncludeDetails) { + [array]$CPUCIMData = Get-IcingaWindowsInformation Win32_Processor; + + foreach ($cpu in $CPUCIMData) { + if ((Test-PSCustomObjectMember -PSObject $CpuData.Metadata.CoreDetails -Name $cpu.DeviceID) -eq $FALSE) { + $CpuData.Metadata.CoreDetails | Add-Member -MemberType NoteProperty -Name $cpu.DeviceID -Value (New-Object PSCustomObject); + } + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'Architecture' -Value $cpu.Architecture; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'ProcessorType' -Value $cpu.ProcessorType; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'StatusInfo' -Value $cpu.StatusInfo; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'Family' -Value $cpu.Family; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'CurrentVoltage' -Value $cpu.CurrentVoltage; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'L3CacheSize' -Value $cpu.L3CacheSize; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'L2CacheSize' -Value $cpu.L2CacheSize; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'L2CacheSpeed' -Value $cpu.L2CacheSpeed; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'VoltageCaps' -Value $cpu.VoltageCaps; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'CurrentClockSpeed' -Value $cpu.CurrentClockSpeed; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'Caption' -Value $cpu.Caption; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'Name' -Value $cpu.Name; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'SerialNumber' -Value $cpu.SerialNumber; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'Manufacturer' -Value $cpu.Manufacturer; + $CpuData.Metadata.CoreDetails.($cpu.DeviceID) | Add-Member -MemberType NoteProperty -Name 'AddressWidth' -Value $cpu.AddressWidth; + } + } + + return $CpuData; +} +function Get-IcingaProviderDataValuesCpu() +{ + param ( + [array]$IncludeFilter = @(), + [array]$ExcludeFilter = @(), + [hashtable]$ProviderFilter = @{ }, + [switch]$IncludeDetails = $FALSE + ); + + $CpuData = New-IcingaProviderObject -Name 'Cpu'; + [hashtable]$FilterObject = Get-IcingaProviderFilterData -ProviderName 'Cpu' -ProviderFilter $ProviderFilter; + + $CpuData.Metrics = $FilterObject.Cpu.Query.Metrics; + $CpuData.MetricsOverTime = $FilterObject.Cpu.Query.MetricsOverTime; + $CpuData.Metadata = $FilterObject.Cpu.Query.Metadata; + + $FilterObject = $null; + + return $CpuData; +} +function Get-IcingaProviderDataValuesHyperV() +{ + param ( + [array]$IncludeFilter = @(), + [array]$ExcludeFilter = @(), + [switch]$IncludeDetails = $FALSE + ); + + $HyperVData = New-IcingaProviderObject -Name 'Hyper-V'; + + # Check if the Hyper-V is installed. If not, we will simply return an empty object + if ($null -eq (Get-Service -Name 'vmms' -ErrorAction SilentlyContinue)) { + $HyperVData.FeatureInstalled = $FALSE; + + return $HyperVData; + } + + $HyperVData.Metrics | Add-Member -MemberType NoteProperty -Name 'ClusterData' -Value (New-Object PSCustomObject); + $HyperVData.Metrics | Add-Member -MemberType NoteProperty -Name 'BlackoutTimes' -Value (New-Object PSCustomObject); + $HyperVData.Metrics.BlackoutTimes | Add-Member -MemberType NoteProperty -Name 'Information' -Value (New-Object PSCustomObject); + $HyperVData.Metrics.BlackoutTimes | Add-Member -MemberType NoteProperty -Name 'Warning' -Value (New-Object PSCustomObject); + $HyperVData.Metrics.ClusterData | Add-Member -MemberType NoteProperty -Name 'NodeCount' -Value 1; # We always have at least 1 node + $HyperVData.Metrics.ClusterData | Add-Member -MemberType NoteProperty -Name 'VMList' -Value (New-Object PSCustomObject); + $HyperVData.Metrics.ClusterData.VMList | Add-Member -MemberType NoteProperty -Name 'Duplicates' -Value (New-Object PSCustomObject); + $HyperVData.Metrics.ClusterData.VMList | Add-Member -MemberType NoteProperty -Name 'VMs' -Value (New-Object PSCustomObject); + + try { + if (Test-IcingaFunction 'Get-ClusterNode') { + $ClusterInformation = Get-ClusterNode -Cluster '.' -ErrorAction Stop; + + $HyperVData.Metrics.ClusterData.NodeCount = $ClusterInformation.Count; + } + + [array]$VMRessources = @(); + + if (Test-IcingaFunction 'Get-ClusterResource') { + [array]$VMRessources = Get-ClusterResource -Cluster '.' -ErrorAction Stop | Where-Object ResourceType -EQ 'Virtual Machine'; + } else { + [array]$VMRessources = Get-VM -ErrorAction Stop; + } + + if ($null -ne $VMRessources -And $VMRessources.Count -ne 0) { + foreach ($VMRessource in $VMRessources) { + if ((Test-PSCustomObjectMember -PSObject $HyperVData.Metrics.ClusterData.VMList.VMs -Name $VMRessource.Name) -eq $FALSE) { + $HyperVData.Metrics.ClusterData.VMList.VMs | Add-Member -MemberType NoteProperty -Name $VMRessource.Name -Value 1; + } else { + $HyperVData.Metrics.ClusterData.VMList.VMs.($VMRessource.Name) += 1; + + if ((Test-PSCustomObjectMember -PSObject $HyperVData.Metrics.ClusterData.VMList.Duplicates -Name $VMRessource.Name) -eq $FALSE) { + $HyperVData.Metrics.ClusterData.VMList.Duplicates | Add-Member -MemberType NoteProperty -Name $VMRessource.Name -Value 0; + } + $HyperVData.Metrics.ClusterData.VMList.Duplicates.($VMRessource.Name) = $HyperVData.Metrics.ClusterData.VMList.VMs.($VMRessource.Name); + } + } + } + + # Blackout Times + # => Info + [array]$InformationBlackoutTimes = Get-WinEvent -FilterHashtable @{ 'LogName'='Microsoft-Windows-Hyper-V-VMMS-Admin'; 'Id' = '20415'; } -MaxEvents 300 -ErrorAction SilentlyContinue; + + if ($null -ne $InformationBlackoutTimes -Or $InformationBlackoutTimes.Count -ne 0) { + foreach ($event in $InformationBlackoutTimes) { + $XMLEventData = ([xml]$event.ToXml()).Event; + + if ((Test-PSCustomObjectMember -PSObject $HyperVData.Metrics.BlackoutTimes.Information -Name $XMLEventData.UserData.VmlEventLog.Parameter0) -eq $FALSE) { + $EventObject = New-Object PSCustomObject; + $EventObject | Add-Member -MemberType NoteProperty -Name 'Timestamp' -Value $event.TimeCreated; + $EventObject | Add-Member -MemberType NoteProperty -Name 'BlackoutTime' -Value $XMLEventData.UserData.VmlEventLog.Parameter2; + + $HyperVData.Metrics.BlackoutTimes.Information | Add-Member -MemberType NoteProperty -Name $XMLEventData.UserData.VmlEventLog.Parameter0 -Value $EventObject; + } + } + } + + # Blackout Times + # => Warning + [array]$WarningBlackoutTimes = Get-WinEvent -FilterHashtable @{ 'LogName'='Microsoft-Windows-Hyper-V-VMMS-Admin'; 'Id' = '20417'; } -MaxEvents 300 -ErrorAction SilentlyContinue; + + if ($null -ne $WarningBlackoutTimes -Or $WarningBlackoutTimes.Count -ne 0) { + foreach ($event in $WarningBlackoutTimes) { + $XMLEventData = ([xml]$event.ToXml()).Event; + + if ((Test-PSCustomObjectMember -PSObject $HyperVData.Metrics.BlackoutTimes.Warning -Name $XMLEventData.UserData.VmlEventLog.Parameter0) -eq $FALSE) { + $EventObject = New-Object PSCustomObject; + $EventObject | Add-Member -MemberType NoteProperty -Name 'Timestamp' -Value $event.TimeCreated; + $EventObject | Add-Member -MemberType NoteProperty -Name 'BlackoutTime' -Value $XMLEventData.UserData.VmlEventLog.Parameter2; + + [bool]$IsAcknowledged = $FALSE; + + foreach ($InfoBlackoutTime in $HyperVData.Metrics.BlackoutTimes.Information.PSObject.Properties.Name) { + if ($InfoBlackoutTime -eq $XMLEventData.UserData.VmlEventLog.Parameter0) { + if($HyperVData.Metrics.BlackoutTimes.Information.$InfoBlackoutTime.Timestamp -gt $event.TimeCreated) { + $IsAcknowledged = $TRUE; + break; + } + } + } + + if ($IsAcknowledged) { + continue; + } + + $HyperVData.Metrics.BlackoutTimes.Warning | Add-Member -MemberType NoteProperty -Name $XMLEventData.UserData.VmlEventLog.Parameter0 -Value $EventObject; + } + } + } + + if ($null -ne $InformationBlackoutTimes) { + $InformationBlackoutTimes.Dispose(); + $InformationBlackoutTimes = $null; + } + + if ($null -ne $WarningBlackoutTimes) { + $WarningBlackoutTimes.Dispose(); + $WarningBlackoutTimes = $null; + } + } catch { + Exit-IcingaThrowException -ExceptionType 'Custom' -CustomMessage 'Hyper-V Error' -ExceptionThrown $_.Exception.Message -Force; + } + + return $HyperVData; +} +function Get-IcingaProviderDataValuesProcess() +{ + param ( + [array]$IncludeFilter = @(), + [array]$ExcludeFilter = @(), + [switch]$IncludeDetails = $FALSE + ); + + # Fetch all required process information + $ProviderName = 'Process' + $ProcessData = New-IcingaProviderObject -Name $ProviderName; + [array]$ProcessInformation = Get-IcingaWindowsInformation Win32_Process; + [array]$ProcessPerfDataList = Get-IcingaWindowsInformation Win32_PerfFormattedData_PerfProc_Process; + [hashtable]$CPUProcessCache = @{ }; + [hashtable]$MEMProcessCache = @{ }; + + # Read our process data and build our metrics object + foreach ($entry in $ProcessInformation) { + [string]$ProcessName = $entry.Name.Replace('.exe', ''); + [int]$ProcessId = [int]$entry.ProcessId; + + if ((Test-IcingaArrayFilter -InputObject $ProcessName -Include $IncludeFilter -Exclude $ExcludeFilter) -eq $FALSE) { + continue; + } + + if ((Test-PSCustomObjectMember -PSObject $ProcessData.Metrics -Name $ProcessName) -eq $FALSE) { + $ProcessData.Metrics | Add-Member -MemberType NoteProperty -Name $ProcessName -Value (New-Object PSCustomObject); + $ProcessData.Metrics.$ProcessName | Add-Member -MemberType NoteProperty -Name 'List' -Value (New-Object PSCustomObject); + $ProcessData.Metrics.$ProcessName | Add-Member -MemberType NoteProperty -Name 'Total' -Value (New-Object PSCustomObject); + $ProcessData.Metrics.$ProcessName.Total | Add-Member -MemberType NoteProperty -Name 'PageFileUsage' -Value 0; + $ProcessData.Metrics.$ProcessName.Total | Add-Member -MemberType NoteProperty -Name 'ThreadCount' -Value 0; + $ProcessData.Metrics.$ProcessName.Total | Add-Member -MemberType NoteProperty -Name 'WorkingSetSize' -Value 0; + $ProcessData.Metrics.$ProcessName.Total | Add-Member -MemberType NoteProperty -Name 'ProcessCount' -Value 0; + $ProcessData.Metrics.$ProcessName.Total | Add-Member -MemberType NoteProperty -Name 'CpuUsage' -Value 0; + $ProcessData.Metrics.$ProcessName.Total | Add-Member -MemberType NoteProperty -Name 'MemoryUsage' -Value 0; + } + + # Detail for each single Process + $ProcessData.Metrics.$ProcessName.List | Add-Member -MemberType NoteProperty -Name $ProcessId -Value (New-Object PSCustomObject); + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'Name' -Value $ProcessName; + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'ProcessName' -Value ([string]$entry.Name); + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'ProcessId' -Value $ProcessId; + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'Priority' -Value ([int]$entry.Priority); + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'PageFileUsage' -Value ([decimal]$entry.PageFileUsage); + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'ThreadCount' -Value ([int]$entry.ThreadCount); + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'KernelModeTime' -Value ([decimal]$entry.KernelModeTime); + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'UserModeTime' -Value ([decimal]$entry.UserModeTime); + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'WorkingSetSize' -Value ([decimal]$entry.WorkingSetSize); + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'CommandLine' -Value ([string]$entry.CommandLine); + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'CpuUsage' -Value 0; + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'MemoryUsage' -Value 0; + + # Total data for all processes with a given name + $ProcessData.Metrics.$ProcessName.Total.PageFileUsage += ([decimal]$entry.PageFileUsage); + $ProcessData.Metrics.$ProcessName.Total.ThreadCount += ([int]$entry.ThreadCount); + $ProcessData.Metrics.$ProcessName.Total.WorkingSetSize += ([decimal]$entry.WorkingSetSize); + $ProcessData.Metrics.$ProcessName.Total.ProcessCount += 1; + } + + # Process all process performance metrics and add memory and cpu usage to the correct process id + foreach ($entry in $ProcessPerfDataList) { + [string]$ProcessName = $entry.Name; + [int]$ProcessId = [int]$entry.IDProcess; + + if ($ProcessName.Contains('#')) { + $ProcessName = $ProcessName.Substring(0, $ProcessName.IndexOf('#')); + } + + if ((Test-IcingaArrayFilter -InputObject $ProcessName -Include $IncludeFilter -Exclude $ExcludeFilter) -eq $FALSE) { + continue; + } + + if ((Test-PSCustomObjectMember -PSObject $ProcessData.Metrics -Name $ProcessName) -eq $FALSE) { + continue; + } + + # Add a cache for our Process Data with all CPU loads for every single Process Id + $CPUProcessCache.Add([string]::Format('{0}|{1}', $ProcessName, [string]$ProcessId), [int]$entry.PercentProcessorTime); + $MEMProcessCache.Add([string]::Format('{0}|{1}', $ProcessName, [string]$ProcessId), [decimal]$entry.WorkingSetPrivate); + + # Just in case a process id is not present, we should ensure to add it to prevent exceptions + if ((Test-PSCustomObjectMember -PSObject $ProcessData.Metrics.$ProcessName.List -Name $ProcessId) -eq $FALSE) { + $ProcessData.Metrics.$ProcessName.List | Add-Member -MemberType NoteProperty -Name $ProcessId -Value (New-Object PSCustomObject); + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'CpuUsage' -Value 0; + $ProcessData.Metrics.$ProcessName.List.$ProcessId | Add-Member -MemberType NoteProperty -Name 'MemoryUsage' -Value 0; + } + + $ProcessData.Metrics.$ProcessName.List.$ProcessId.CpuUsage = [int]$entry.PercentProcessorTime; + $ProcessData.Metrics.$ProcessName.List.$ProcessId.MemoryUsage = [decimal]$entry.WorkingSetPrivate; + $ProcessData.Metrics.$ProcessName.Total.CpuUsage += [int]$entry.PercentProcessorTime; + $ProcessData.Metrics.$ProcessName.Total.MemoryUsage += [decimal]$entry.WorkingSetPrivate; + } + + # Generaet a "hot" object for our 10 most CPU and Memory consuming process objects + $ProcessData.Metadata | Add-Member -MemberType NoteProperty -Name 'Hot' -Value (New-Object PSCustomObject); + $ProcessData.Metadata.Hot | Add-Member -MemberType NoteProperty -Name 'Cpu' -Value (New-Object PSCustomObject); + $ProcessData.Metadata.Hot | Add-Member -MemberType NoteProperty -Name 'Memory' -Value (New-Object PSCustomObject); + + [array]$TopCPUUsage = $CPUProcessCache.GetEnumerator() | Sort-Object Value -Descending; + [array]$TopMEMUsage = $MEMProcessCache.GetEnumerator() | Sort-Object Value -Descending; + [int]$IterationIndex = 0; + + while ($IterationIndex -lt 10) { + if ($TopCPUUsage.Count -gt 0 -And (($TopCPUUsage.Count - 1) -ge $IterationIndex )) { + + if ($TopCPUUsage.Count -gt 1) { + [string]$CPUProcessName = $TopCPUUsage.Key[$IterationIndex].Split('|')[0]; + [int]$CPUProcessId = $TopCPUUsage.Key[$IterationIndex].Split('|')[1]; + [int]$CPUUsage = $TopCPUUsage.Value[$IterationIndex]; + } else { + [string]$CPUProcessName = $TopCPUUsage.Key.Split('|')[0]; + [int]$CPUProcessId = $TopCPUUsage.Key.Split('|')[1]; + [int]$CPUUsage = $TopCPUUsage.Value; + } + + if ($TopCPUUsage.Value[$IterationIndex] -gt 0) { + $ProcessData.Metadata.Hot.Cpu | Add-Member -MemberType NoteProperty -Name $CPUProcessId -Value (New-Object PSCustomObject); + $ProcessData.Metadata.Hot.Cpu.$CPUProcessId | Add-Member -MemberType NoteProperty -Name 'Name' -Value $CPUProcessName; + $ProcessData.Metadata.Hot.Cpu.$CPUProcessId | Add-Member -MemberType NoteProperty -Name 'ProcessId' -Value $CPUProcessId; + $ProcessData.Metadata.Hot.Cpu.$CPUProcessId | Add-Member -MemberType NoteProperty -Name 'CpuUsage' -Value $CPUUsage; + } + } + + if ($TopMEMUsage.Count -gt 0 -And (($TopMEMUsage.Count - 1) -ge $IterationIndex )) { + if ($TopMEMUsage.Count -gt 1) { + [string]$MEMProcessName = $TopMEMUsage.Key[$IterationIndex].Split('|')[0]; + [int]$MEPProcessId = $TopMEMUsage.Key[$IterationIndex].Split('|')[1]; + [decimal]$MemoryUsage = $TopMEMUsage.Value[$IterationIndex]; + } else { + [string]$MEMProcessName = $TopMEMUsage.Key.Split('|')[0]; + [int]$MEPProcessId = $TopMEMUsage.Key.Split('|')[1]; + [int]$MemoryUsage = $TopMEMUsage.Value; + } + + if ($TopMEMUsage.Value[$IterationIndex] -gt 0) { + $ProcessData.Metadata.Hot.Memory | Add-Member -MemberType NoteProperty -Name $MEPProcessId -Value (New-Object PSCustomObject); + $ProcessData.Metadata.Hot.Memory.$MEPProcessId | Add-Member -MemberType NoteProperty -Name 'Name' -Value $MEMProcessName; + $ProcessData.Metadata.Hot.Memory.$MEPProcessId | Add-Member -MemberType NoteProperty -Name 'ProcessId' -Value $MEPProcessId; + $ProcessData.Metadata.Hot.Memory.$MEPProcessId | Add-Member -MemberType NoteProperty -Name 'MemoryUsage' -Value $MemoryUsage; + } + } + + $IterationIndex += 1; + } + + $CPUProcessCache = $null; + $MEMProcessCache = $null; + $ProcessInformation = $null; + $ProcessPerfDataList = $null; + $TopCPUUsage = $null; + $TopMEMUsage = $null; + + return $ProcessData; +} +function Get-IcingaProviderDataValuesEventlog() +{ + param ( + [array]$IncludeFilter = @(), + [array]$ExcludeFilter = @(), + [hashtable]$ProviderFilter = @{ }, + [switch]$IncludeDetails = $FALSE + ); + + $EventlogData = New-IcingaProviderObject -Name 'Eventlog'; + [hashtable]$FilterObject = Get-IcingaProviderFilterData -ProviderName 'Eventlog' -ProviderFilter $ProviderFilter; + + $EventLogData.Metrics | Add-Member -MemberType NoteProperty -Name 'List' -Value $FilterObject.EventLog.Query.List; + $EventLogData.Metrics | Add-Member -MemberType NoteProperty -Name 'Events' -Value $FilterObject.EventLog.Query.Events; + $EventLogData.Metrics | Add-Member -MemberType NoteProperty -Name 'Problems' -Value $FilterObject.EventLog.Query.Problems; + $EventLogData.Metrics | Add-Member -MemberType NoteProperty -Name 'HasEvents' -Value $FilterObject.EventLog.Query.HasEvents; + + $FilterObject = $null; + + return $EventlogData; +} +function Add-IcingaProviderEventlogFilterData() +{ + param( + [string]$EventFilter = '', + $StringBuilderObject = $null + ); + + if ($null -eq $StringBuilderObject -Or $StringBuilderObject.Length -eq 0) { + return $EventFilter; + } + + [string]$NewStringEntry = $StringBuilderObject.ToString(); + + $StringBuilderObject.Clear(); + $StringBuilderObject = $null; + + if ([string]::IsNullOrEmpty($NewStringEntry)) { + return $EventFilter; + } + + if ([string]::IsNullOrEmpty($EventFilter)) { + return $NewStringEntry; + } + + [string]$EventFilter = [string]::Format('{0} and {1}', $EventFilter, $NewStringEntry); + + return $EventFilter; +} +<# +.ROLE + Query +#> + +function New-IcingaProviderFilterDataEventlog() +{ + param( + [string]$LogName = '', + [array]$IncludeEventId = @(), + [array]$ExcludeEventId = @(), + [array]$IncludeUsername = @(), + [array]$ExcludeUsername = @(), + [array]$IncludeEntryType = @(), + [array]$ExcludeEntryType = @(), + [array]$IncludeMessage = @(), + [array]$ExcludeMessage = @(), + [array]$IncludeSource = @(), + [array]$ExcludeSource = @(), + [array]$ProblemId = @(), + [array]$AcknowledgeId = @(), + [string]$EventsAfter = $null, + [string]$EventsBefore = $null, + [int]$MaxEntries = 40000, + [switch]$DisableTimeCache = $FALSE + ); + + if ($ProblemId.Count -ne $AcknowledgeId.Count) { + Exit-IcingaThrowException -ExceptionType 'Input' -ExceptionThrown $IcingaExceptions.Configuration.PluginArgumentAsymmetry -ExceptionList $IcingaPluginExceptions -CustomMessage ([string]::Format('ProblemId count: {0}, AcknowledgeId count: {1}', $ProblemId.Count, $AcknowledgeId.Count)) -Force; + } + + [string]$EventLogFilter = ''; + $EventIdFilter = New-Object -TypeName 'System.Text.StringBuilder'; + $EventIdInternalFilter = New-Object -TypeName 'System.Text.StringBuilder'; + $EntryTypeFilter = New-Object -TypeName 'System.Text.StringBuilder'; + $SourceFilter = New-Object -TypeName 'System.Text.StringBuilder'; + $UserFilter = New-Object -TypeName 'System.Text.StringBuilder'; + $TimeFilter = New-Object -TypeName 'System.Text.StringBuilder'; + $EventAfterFilter = $null; + $EventBeforeFilter = $null; + $EventsAfter = (Convert-IcingaPluginThresholds -Threshold $EventsAfter).Value; + $EventsBefore = (Convert-IcingaPluginThresholds -Threshold $EventsBefore).Value; + [string]$CheckHash = (Get-StringSha1 ($LogName + $IncludeEventId + $ExcludeEventId + $IncludeUsername + $ExcludeUsername + $IncludeEntryType + $ExcludeEntryType + $IncludeMessage + $ExcludeMessage + $ProblemId + $AcknowledgeId)) + '.lastcheck'; + [hashtable]$ProblemList = @{ }; + [hashtable]$ResolveList = @{ }; + [int]$IndexOfEntries = 0; + + foreach ($entry in $ProblemId) { + Add-IcingaHashtableItem -Hashtable $ProblemList -Key ([string]$entry) -Value @{ + 'NewestEntry' = ''; + 'Message' = ''; + 'Count' = 0; + 'ResolvedId' = $AcknowledgeId[$IndexOfEntries]; + 'IsProblem' = $TRUE; + } | Out-Null; + } + + # Reset the Id's + [int]$IndexOfEntries = 0; + + foreach ($entry in $AcknowledgeId) { + Add-IcingaHashtableItem -Hashtable $ResolveList -Key ([string]$entry) -Value @{ + 'NewestEntry' = ''; + 'Count' = 0; + 'ProblemId' = $ProblemId[$IndexOfEntries]; + } | Out-Null; + } + + if ([string]::IsNullOrEmpty($EventsAfter) -and $DisableTimeCache -eq $FALSE) { + $time = Get-IcingaCacheData -Space 'provider' -CacheStore 'eventlog' -KeyName $CheckHash; + Set-IcingaCacheData -Space 'provider' -CacheStore 'eventlog' -KeyName $CheckHash -Value ((Get-Date).ToFileTime()); + + if ($null -ne $time) { + $EventAfterFilter = ([datetime]::FromFileTime($time)).ToString("yyyy-MM-dd HH:mm:ss"); + } + } + + # In case we are not having cached time execution and not have not overwritten the timestamp, only fetch values from 2 hours in the past + if ([string]::IsNullOrEmpty($EventAfterFilter)) { + if ([string]::IsNullOrEmpty($EventsAfter)) { + [string]$EventAfterFilter = ([datetime]::Now.Subtract([TimeSpan]::FromHours(2))).ToString("yyyy-MM-dd HH:mm:ss"); + } else { + if ((Test-Numeric $EventsAfter)) { + [string]$EventAfterFilter = ([datetime]::Now.Subtract([TimeSpan]::FromSeconds($EventsAfter))).ToString('yyyy\/MM\/dd HH:mm:ss'); + } else { + [string]$EventAfterFilter = $EventsAfter; + } + } + } + + if ([string]::IsNullOrEmpty($EventsBefore) -eq $FALSE) { + if ((Test-Numeric $EventsBefore)) { + [string]$EventBeforeFilter = ([datetime]::Now.Subtract([TimeSpan]::FromSeconds($EventsBefore))).ToString("yyyy-MM-dd HH:mm:ss"); + } else { + [string]$EventBeforeFilter = $EventsBefore; + } + } else { + [string]$EventBeforeFilter = ([datetime]::FromFileTime(((Get-Date).ToFileTime()))).ToString("yyyy-MM-dd HH:mm:ss"); + } + + # The filter string can be used to query Event Log entries based on the specified Event IDs, to filter correctly + # between included and excluded Event IDs to ensure that includes are separated with OR while excludes + # are added with AND to ensure that the filter string is correctly constructed. + if ($IncludeEventId.Count -ne 0 -Or $ExcludeEventId.Count -ne 0) { + $EventIdInternalFilter.Append('(') | Out-Null; + + foreach ($entry in $IncludeEventId) { + if ($EventIdFilter.Length -ne 0) { + $EventIdFilter.Append( + ([string]::Format(' or EventID={0}', $entry)) + ) | Out-Null; + } else { + $EventIdFilter.Append( + ([string]::Format('( EventID={0}', $entry)) + ) | Out-Null; + } + } + + if ($EventIdFilter.Length -ne 0) { + $EventIdFilter.Append(' )'); + } + + foreach ($entry in $ExcludeEventId) { + if ($EventIdFilter.Length -ne 0) { + $EventIdFilter.Append( + ([string]::Format(' and EventID!={0}', $entry)) + ) | Out-Null; + } else { + $EventIdFilter.Append( + ([string]::Format('EventID!={0}', $entry)) + ) | Out-Null; + } + } + + $EventIdInternalFilter.Append($EventIdFilter.ToString()) | Out-Null; + $EventIdInternalFilter.Append(')') | Out-Null; + } + + foreach ($entry in $IncludeEntryType) { + [string]$EntryId = $ProviderEnums.EventLogSeverity[$entry]; + if ($EntryTypeFilter.Length -ne 0) { + $EntryTypeFilter.Append( + ([string]::Format(' and Level={0}', $EntryId)) + ) | Out-Null; + } else { + $EntryTypeFilter.Append( + ([string]::Format('Level={0}', $EntryId)) + ) | Out-Null; + } + } + + foreach ($entry in $ExcludeEntryType) { + [string]$EntryId = $ProviderEnums.EventLogSeverity[$entry]; + if ($EntryTypeFilter.Length -ne 0) { + $EntryTypeFilter.Append( + ([string]::Format(' and Level!={0}', $EntryId)) + ) | Out-Null; + } else { + $EntryTypeFilter.Append( + ([string]::Format('Level!={0}', $EntryId)) + ) | Out-Null; + } + } + + foreach ($entry in $IncludeSource) { + if ($SourceFilter.Length -ne 0) { + $SourceFilter.Append( + ([string]::Format(' and Provider[@Name="{0}"]', $entry)) + ) | Out-Null; + } else { + $SourceFilter.Append( + ([string]::Format('Provider[@Name="{0}"]', $entry)) + ) | Out-Null; + } + } + + foreach ($entry in $ExcludeSource) { + if ($SourceFilter.Length -ne 0) { + $SourceFilter.Append( + ([string]::Format(' and Provider[@Name!="{0}"]', $entry)) + ) | Out-Null; + } else { + $SourceFilter.Append( + ([string]::Format('Provider[@Name!="{0}"]', $entry)) + ) | Out-Null; + } + } + + foreach ($entry in $IncludeUsername) { + [string]$UserSID = (Get-IcingaUserSID -User $entry); + if ($UserFilter.Length -ne 0) { + $UserFilter.Append( + ([string]::Format(' and Security[@UserID="{0}', $UserSID)) + ) | Out-Null; + } else { + $UserFilter.Append( + ([string]::Format('Security[@UserID="{0}"]', $UserSID)) + ) | Out-Null; + } + } + + foreach ($entry in $ExcludeUsername) { + [string]$UserSID = (Get-IcingaUserSID -User $entry); + if ($UserFilter.Length -ne 0) { + $UserFilter.Append( + ([string]::Format(' and Security[@UserID!="{0}"]', $UserSID)) + ) | Out-Null; + } else { + $UserFilter.Append( + ([string]::Format('Security[@UserID!="{0}"]', $UserSID)) + ) | Out-Null; + } + } + + $TimeFilter.Append( + ([string]::Format('TimeCreated[@SystemTime>="{0}"]', (Get-Date $EventAfterFilter).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ssZ"))) + ) | Out-Null; + + $TimeFilter.Append( + ([string]::Format(' and TimeCreated[@SystemTime<="{0}"]', (Get-Date $EventBeforeFilter).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ssZ"))) + ) | Out-Null; + + [string]$EventLogFilter = Add-IcingaProviderEventlogFilterData -EventFilter $EventLogFilter -StringBuilderObject $EventIdInternalFilter; + [string]$EventLogFilter = Add-IcingaProviderEventlogFilterData -EventFilter $EventLogFilter -StringBuilderObject $EntryTypeFilter; + [string]$EventLogFilter = Add-IcingaProviderEventlogFilterData -EventFilter $EventLogFilter -StringBuilderObject $SourceFilter; + [string]$EventLogFilter = Add-IcingaProviderEventlogFilterData -EventFilter $EventLogFilter -StringBuilderObject $UserFilter; + [string]$EventLogFilter = Add-IcingaProviderEventlogFilterData -EventFilter $EventLogFilter -StringBuilderObject $TimeFilter; + + while ($EventLogFilter[0] -eq ' ') { + $EventLogFilter = $EventLogFilter.Substring(1, $EventLogFilter.Length - 1); + } + + [string]$EventLogFilter = [string]::Format('Event[System[{0}]]', $EventLogFilter); + + try { + $EventLogEntries = Get-WinEvent -LogName $LogName -MaxEvents $MaxEntries -FilterXPath $EventLogFilter -ErrorAction Stop; + } catch { + Exit-IcingaThrowException -InputString $_.FullyQualifiedErrorId -StringPattern 'ParameterArgumentValidationError' -ExceptionList $IcingaPluginExceptions -ExceptionType 'Input' -ExceptionThrown $IcingaPluginExceptions.Inputs.EventLogNegativeEntries; + Exit-IcingaThrowException -InputString $_.FullyQualifiedErrorId -StringPattern 'CannotConvertArgumentNoMessage' -ExceptionList $IcingaPluginExceptions -ExceptionType 'Input' -ExceptionThrown $IcingaPluginExceptions.Inputs.EventLogNoMessageEntries; + Exit-IcingaThrowException -InputString $_.FullyQualifiedErrorId -StringPattern 'NoMatchingLogsFound' -CustomMessage (-Join $LogName) -ExceptionList $IcingaPluginExceptions -ExceptionType 'Input' -ExceptionThrown $IcingaPluginExceptions.Inputs.EventLogLogName; + } + + $EventLogQueryData = New-Object PSCustomObject; + $EventLogQueryData | Add-Member -MemberType NoteProperty -Name 'List' -Value (New-Object PSCustomObject); + $EventLogQueryData | Add-Member -MemberType NoteProperty -Name 'Events' -Value (New-Object PSCustomObject); + $EventLogQueryData | Add-Member -MemberType NoteProperty -Name 'Problems' -Value (New-Object PSCustomObject); + $EventLogQueryData | Add-Member -MemberType NoteProperty -Name 'HasEvents' -Value $FALSE; + + foreach ($event in $EventLogEntries) { + # Filter out remaining message not matching our filter + if ((Test-IcingaArrayFilter -InputObject $event.Message -Include $IncludeMessage -Exclude $ExcludeMessage) -eq $FALSE) { + continue; + } + + if ($ProblemList.ContainsKey([string]$event.Id)) { + if ([string]::IsNullOrEmpty($ProblemList[([string]$event.Id)].NewestEntry)) { + $ProblemList[([string]$event.Id)].NewestEntry = ([string]($event.TimeCreated)); + $ProblemList[([string]$event.Id)].Message = [string]($event.Message); + } + $ProblemList[([string]$event.Id)].Count += 1; + } + if ($ResolveList.ContainsKey([string]$event.Id)) { + if ([string]::IsNullOrEmpty($ResolveList[([string]$event.Id)].NewestEntry)) { + $ResolveList[([string]$event.Id)].NewestEntry = ([string]($event.TimeCreated)); + } + } + + $EventLogQueryData.HasEvents = $TRUE; + + [string]$EventIdentifier = [string]::Format('{0}-{1}', + $event.Id, + $event.ProviderName + ); + + [string]$EventHash = Get-StringSha1 $EventIdentifier; + + if ((Test-PSCustomObjectMember -PSObject $EventLogQueryData.List -Name $EventHash) -eq $FALSE) { + [string]$EventMessage = [string]($event.Message); + if ([string]::IsNullOrEmpty($EventMessage)) { + $EventMessage = ''; + } + + $EventLogQueryData.List | Add-Member -MemberType NoteProperty -Name $EventHash -Value (New-Object PSCustomObject); + $EventLogQueryData.List.$EventHash | Add-Member -MemberType NoteProperty -Name 'NewestEntry' -Value ([string]($event.TimeCreated)); + $EventLogQueryData.List.$EventHash | Add-Member -MemberType NoteProperty -Name 'OldestEntry' -Value ([string]($event.TimeCreated)); + $EventLogQueryData.List.$EventHash | Add-Member -MemberType NoteProperty -Name 'EventId' -Value ([string]($event.Id)); + $EventLogQueryData.List.$EventHash | Add-Member -MemberType NoteProperty -Name 'Message' -Value $EventMessage; + $EventLogQueryData.List.$EventHash | Add-Member -MemberType NoteProperty -Name 'Severity' -Value $ProviderEnums.EventLogSeverityName[$event.Level]; + $EventLogQueryData.List.$EventHash | Add-Member -MemberType NoteProperty -Name 'Source' -Value ([string]($event.ProviderName)); + $EventLogQueryData.List.$EventHash | Add-Member -MemberType NoteProperty -Name 'Count' -Value 1; + + } else { + $EventLogQueryData.List.$EventHash.OldestEntry = ([string]($event.TimeCreated)); + $EventLogQueryData.List.$EventHash.Count += 1; + } + + if ((Test-PSCustomObjectMember -PSObject $EventLogQueryData.Events -Name $event.Id) -eq $FALSE) { + $EventLogQueryData.Events | Add-Member -MemberType NoteProperty -Name $event.Id -Value 1; + } else { + $EventLogQueryData.Events.($event.Id) += 1; + } + } + + if ($ProblemId.Count -ne 0) { + foreach ($problem in $ProblemList.Keys) { + [string]$ressolvedId = $ProblemList[$problem].ResolvedId; + $LastProblem = $null; + $LastResolved = $null; + + if ([string]::IsNullOrEmpty($ProblemList[$problem].NewestEntry) -eq $FALSE) { + $LastProblem = [DateTime]$ProblemList[$problem].NewestEntry; + } + if ([string]::IsNullOrEmpty($ResolveList[$ressolvedId].NewestEntry) -eq $FALSE) { + $LastResolved = [DateTime]$ResolveList[$ressolvedId].NewestEntry; + } + + if ($ResolveList.ContainsKey($ressolvedId)) { + if ($null -ne $LastProblem -And $null -ne $LastResolved -And $LastProblem -le $LastResolved) { + $ProblemList[$problem].$IsProblem = $FALSE; + } else { + $EventLogQueryData.Problems | Add-Member -MemberType NoteProperty -Name $problem -Value (New-Object PSCustomObject); + $EventLogQueryData.Problems.$problem | Add-Member -MemberType NoteProperty -Name 'EventId' -Value $problem; + $EventLogQueryData.Problems.$problem | Add-Member -MemberType NoteProperty -Name 'Count' -Value $ProblemList[$problem].Count; + $EventLogQueryData.Problems.$problem | Add-Member -MemberType NoteProperty -Name 'Message' -Value $ProblemList[$problem].Message; + $EventLogQueryData.Problems.$problem | Add-Member -MemberType NoteProperty -Name 'NewestEntry' -Value $ProblemList[$problem].NewestEntry; + } + } + } + } + + if ($null -ne $EventLogEntries) { + $EventLogEntries.Dispose(); + } + + $EventLogEntries = $null; + $EventLogFilter = $null; + $EventIdFilter = $null; + $EventIdInternalFilter = $null; + $EntryTypeFilter = $null; + $SourceFilter = $null; + $UserFilter = $null; + $TimeFilter = $null; + + return $EventLogQueryData; +} +function Test-IcingaCAInstalledToAuthRoot() +{ + $IcingaCAFile = Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs\ca.crt'; + + if ((Test-Path $IcingaCAFile) -eq $FALSE) { + return $FALSE; + } + + $IcingaCACert = New-Object Security.Cryptography.X509Certificates.X509Certificate2 $IcingaCAFile; + + [bool]$IcingaCAInstalled = $FALSE; + + Get-ChildItem -Recurse -Path 'Cert:\LocalMachine\AuthRoot\' | Where-Object { + if ($_.Thumbprint -eq $IcingaCACert.Thumbprint) { + $IcingaCAInstalled = $TRUE; + } + }; + + $IcingaCACert = $null; + + return $IcingaCAInstalled; +} +function Send-IcingaTCPClientMessage() +{ + param( + [Hashtable]$Message = @{ }, + [System.Net.Security.SslStream]$Stream = $null + ); + + if ($null -eq $Message -Or $Message.Count -eq 0 -Or $Message.length -eq 0) { + return; + } + + $Stream.Write($Message.message, 0, $Message.length); + $Stream.Flush(); +} +function Get-IcingaTCPClientRemoteEndpoint() +{ + param( + [System.Net.Sockets.TcpClient]$Client = $null + ); + + if ($null -eq $Client) { + return 'unknown'; + } + + return $Client.Client.RemoteEndPoint; +} +function Get-IcingaRESTPathElement() +{ + param( + [Hashtable]$Request = @{ }, + [int]$Index = 0 + ); + + if ($null -eq $Request -Or $Request.Count -eq 0) { + return ''; + } + + if ($Request.ContainsKey('RequestPath') -eq $FALSE) { + return ''; + } + + if (($Index + 1) -gt $Request.RequestPath.PathArray.Count) { + return ''; + } + + return $Request.RequestPath.PathArray[$Index]; +} +function Read-IcingaRESTMessage() +{ + param( + [string]$RestMessage = $null, + [hashtable]$Connection = $null + ); + + # Just in case we didn't receive anything - no need to + # parse through everything + if ([string]::IsNullOrEmpty($RestMessage)) { + return $null; + } + + Write-IcingaDebugMessage ( + [string]::Format( + 'Receiving client message{0}{0}{1}', + (New-IcingaNewLine), + $RestMessage + ) + ); + + [hashtable]$Request = @{ }; + $RestMessage -match '(.+) (.+) (.+)' | Out-Null; + + $Request.Add('Method', $Matches[1]); + $Request.Add('FullRequest', $Matches[2]); + $Request.Add('RequestPath', @{ }); + $Request.Add('RequestArguments', @{ }); + + #Path + $PathMatch = $Matches[2]; + $PathMatch -match '((\/[^\/\?]+)*)\??([^\/]*)' | Out-Null; + $Arguments = $Matches[3]; + $Request.RequestPath.Add('FullPath', $Matches[1]); + $Request.RequestPath.Add('PathArray', $Matches[1].TrimStart('/').Split('/')); + + $Matches = $null; + + # Arguments + $ArgumentsSplit = $Arguments.Split('&'); + $ArgumentsSplit+='\\\\\\\\\\\\=FIN'; + foreach ( $Argument in $ArgumentsSplit | Sort-Object -Descending) { + if ($Argument.Contains('=')) { + $Argument -match '(.+)=(.+)' | Out-Null; + If (($Matches[1] -ne $Current) -And ($NULL -ne $Current)) { + $Request.RequestArguments.Add( $Current, $ArgumentContent ); + [array]$ArgumentContent = $null; + } + $Current = $Matches[1]; + [array]$ArgumentContent += ($Matches[2]); + } else { + $Request.RequestArguments.Add( $Argument, $null ); + } + } + + # Header + $Request.Add( 'Header', @{ } ); + $SplitString = $RestMessage.Split("`r`n"); + + foreach ( $SingleString in $SplitString ) { + if ( ([string]::IsNullOrEmpty($SingleString) -eq $FALSE) -And ($SingleString -match '^{.+' -eq $FALSE) -And $SingleString.Contains(':') -eq $TRUE ) { + $SingleSplitString = $SingleString.Split(':', 2); + $Request.Header.Add( $SingleSplitString[0], $SingleSplitString[1].Trim()); + } + } + + $Request.Add('ContentLength', [int](Get-IcingaRESTHeaderValue -Header 'Content-Length' -Request $Request)); + + $Matches = $null; + + # Body + $RestMessage -match '(\{(.*\n)*}|\{.*\})' | Out-Null; + + # Store your messag body inside a stringbuilder object + $MessageBody = New-Object 'System.Text.StringBuilder'; + + # Our message is not complete + if ($null -eq $Matches) { + # Try to figure out if we received parts of your body from the already read message + $RestMsgArray = $RestMessage.Split("`r`n"); + [bool]$EmptyLine = $FALSE; + [bool]$BeginBody = $FALSE; + + foreach ($entry in $RestMsgArray) { + if ($BeginBody) { + $MessageBody.Append($entry) | Out-Null; + continue; + } + + if ([string]::IsNullOrEmpty($entry)) { + Write-IcingaDebugMessage ` + -Message 'Found end of header, lets check for body elements'; + + # In case we found two empty lines in a row, we found our body + if ($EmptyLine) { + $BeginBody = $TRUE; + continue; + } + + # A first empty line means we found a possible header end + $EmptyLine = $TRUE; + } else { + #Reset the empty line in case the next line contains content + $EmptyLine = $FALSE; + } + } + + Write-IcingaDebugMessage ` + -Message 'We partially received the body of the message, but it might be incomplete. Lets check. Read body length' ` + -Objects $MessageBody.Length, $MessageBody.ToString(); + } else { + # Our message is already complete + $Request.Add('Body', $Matches[1]); + } + + # We received a content length, but couldn't load the body. Some clients will send the body as separate message + # Lets try to read the body content + if ($null -ne $Connection) { + if ($Request.ContainsKey('ContentLength') -And $Request.ContentLength -gt 0 -And ($Request.ContainsKey('Body') -eq $FALSE -Or [string]::IsNullOrEmpty($Request.Body))) { + + Write-IcingaDebugMessage ` + -Message 'The message body was not send or incomplete. Received body size and content' ` + -Objects $MessageBody.Length, $MessageBody.ToString(); + + # In case we received party of the message earlier, read the remaining bytes from the message + # and append it to our body + $MessageBody.Append( + (Read-IcingaTCPStream -Client $Connection.Client -Stream $Connection.Stream -ReadLength ($Request.ContentLength - $MessageBody.Length)) + ) | Out-Null; + + # Add our fully read message to our body object + $Request.Body = $MessageBody.ToString(); + + # Some debug output + Write-IcingaDebugMessage -Message 'Full request dump' -Objects $Request; + Write-IcingaDebugMessage -Message 'Body Content' -Objects $Request.Body; + } + } + + return $Request; +} +function Get-IcingaRESTHeaderValue() +{ + param( + [hashtable]$Request = @{ }, + [string]$Header = $null + ); + + if ($null -eq $Request -or [string]::IsNullOrEmpty($Header) -Or $Request.Count -eq 0) { + return $null; + } + + if ($Request.Header.ContainsKey($Header) -eq $FALSE) { + return $null + } + + return $Request.Header[$Header]; +} +function ConvertTo-IcingaX509Certificate() +{ + param( + [string]$CertFile = $null, + [string]$OutFile = $null, + [switch]$Force = $FALSE + ); + + if ([string]::IsNullOrEmpty($CertFile)) { + throw 'Please specify a valid path to an existing certificate (.cer, .pem, .cert)'; + } + + if ((Test-Path $CertFile) -eq $FALSE) { + throw 'The provided path to your certificate was not valid'; + } + + # Use an empty password for converted certificates + $Password = $null; + # Use a target file to specify if we use temp files or not + $TargetFile = $OutFile; + # Temp Cert + [bool]$TempFile = $FALSE; + + # Create a temp file to store the certificate in + if ([string]::IsNullOrEmpty($OutFile)) { + # Create a temporary file for full path and name + $TargetFile = New-IcingaTemporaryFile; + # Get the actual path to work with + $TargetFile = $TargetFile.FullName; + # Set internally that we are using a temp file + $TempFile = $TRUE; + # Delete the file again + Remove-Item $TargetFile -Force -ErrorAction SilentlyContinue; + } + + # Convert our certificate if our target file does not exist + # it is a temp file or we force its creation + if (-Not (Test-Path $TargetFile) -Or $TempFile -Or $Force) { + Write-Output "$Password + $Password" | & 'C:\Windows\system32\certutil.exe' -mergepfx "$CertFile" "$TargetFile" | Set-Variable -Name 'CertUtilOutput'; + } + + Write-IcingaDebugMessage -Message ( + [string]::Format( + 'Certutil merge request has been completed. Certutil message:{0}{0}{1}', + (New-IcingaNewLine), + ($CertUtilOutput | Out-String) + ) + ); + + # If no target file exists afterwards (a valid PFX certificate) + # then throw an exception + if (-Not (Test-Path $TargetFile)) { + [string]$ErrMessage = [string]::Format('Unable to create the Icinga for Windows certificate file "icingaforwindows.pfx". Certutil output:{0}{1}', (New-IcingaNewLine), ($CertUtilOutput | Out-String)); + Write-IcingaConsoleError $ErrMessage; + throw $ErrMessage; + } + + # Now load the actual certificate from the path + $Certificate = New-Object Security.Cryptography.X509Certificates.X509Certificate2 $TargetFile; + # Delete the PFX-Certificate which will be present after certutil merge + if ($TempFile) { + Remove-Item $TargetFile -Force -ErrorAction SilentlyContinue; + } + + # Return the certificate + return $Certificate +} +function Disable-IcingaUntrustedCertificateValidation() +{ + try { + [System.Net.ServicePointManager]::CertificatePolicy = $null; + + Write-IcingaConsoleNotice 'Successfully disabled untrusted certificate validation for this shell instance'; + } catch { + Write-IcingaConsoleError ( + [string]::Format( + 'Failed to disable untrusted certificate policy: {0}', $_.Exception.Message + ) + ); + } +} +function Get-IcingaSSLCertForSocket() +{ + param( + [string]$CertFile = $null, + [string]$CertThumbprint = $null + ); + + # At first check if we assigned a cert file to use directly and check + # if it is there and either import a PFX or use our convert function + # to get a proper certificate + if ([string]::IsNullOrEmpty($CertFile) -eq $FALSE) { + if ((Test-Path $CertFile)) { + if ([IO.Path]::GetExtension($CertFile) -eq '.pfx') { + return (New-Object Security.Cryptography.X509Certificates.X509Certificate2 $CertFile); + } else { + return ConvertTo-IcingaX509Certificate -CertFile $CertFile; + } + } + } + + # We could also have assigned a Thumbprint to use from the + # Windows cert store. Try to look it up an return it if + # it is found + if ([string]::IsNullOrEmpty($CertThumbprint) -eq $FALSE) { + $Certificates = Get-ChildItem ` + -Path 'cert:\*' ` + -Recurse ` + -Include $CertThumbprint ` + -ErrorAction SilentlyContinue ` + -WarningAction SilentlyContinue; + + if ($Certificates.Count -ne 0) { + return $Certificates[0]; + } + } + + # If no cert file or thumbprint was specified or simply as fallback, + # we should use the Icinga 2 Agent certificates + $AgentCertificate = Get-IcingaAgentHostCertificate; + + # If Agent is not installed or certificates were not found, + # simply return null + if ($null -eq $AgentCertificate) { + return $null; + } + + return (ConvertTo-IcingaX509Certificate -CertFile $AgentCertificate.CertFile); +} +function Open-IcingaTCPClientConnection() +{ + param( + [System.Net.Sockets.TcpClient]$Client = $null, + [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate = $null + ); + + if ($null -eq $Client -Or $null -eq $Certificate) { + return $null; + } + + $Stream = New-IcingaSSLStream -Client $Client -Certificate $Certificate; + + return @{ + 'Client' = $Client; + 'Stream' = $Stream; + }; +} +function New-IcingaTCPSocket() +{ + param ( + [string]$Address = '', + [int]$Port = 0, + [switch]$Start = $FALSE + ); + + if ($Port -eq 0) { + throw 'Please specify a valid port to open a TCP socket for'; + } + + # Listen on localhost by default + $ListenAddress = New-Object System.Net.IPEndPoint([IPAddress]::Loopback, $Port); + + if ([string]::IsNullOrEmpty($Address) -eq $FALSE) { + $ListenAddress = New-Object System.Net.IPEndPoint([IPAddress]::Parse($Address), $Port); + } + + $TCPSocket = New-Object 'System.Net.Sockets.TcpListener' $ListenAddress; + + Write-IcingaDebugMessage -Message ( + [string]::Format( + 'Creating new TCP socket on Port {0}. Endpoint configuration {1}', + $Port, + $TCPSocket.LocalEndpoint + ) + ); + + if ($Start) { + Write-IcingaDebugMessage -Message ( + [string]::Format( + 'Starting TCP socket for endpoint {0}', + $TCPSocket.LocalEndpoint + ) + ); + $TCPSocket.Start(); + } + + return $TCPSocket; +} +function Close-IcingaTCPConnection() +{ + param( + [hashtable]$Connection = @{ } + ); + + if ($null -eq $Connection -Or $Connection.Count -eq 0) { + $Connection = $null; + + return; + } + + Write-IcingaDebugMessage -Message ( + [string]::Format( + 'Closing client connection for endpoint {0}', + (Get-IcingaTCPClientRemoteEndpoint -Client $Connection.Client) + ) + ); + + try { + if ($null -ne $Connection.Stream) { + $Connection.Stream.Close(); + $Connection.Stream.Dispose(); + $Connection.Stream = $null; + } + } catch { + $Connection.Stream = $null; + } + try { + if ($null -ne $Connection.Client) { + $Connection.Client.Close(); + $Connection.Client.Dispose(); + $Connection.Client = $null; + } + } catch { + $Connection.Client = $null; + } + + $Connection = $null; +} +<# + # This script will provide 'Enums' we can use within our module to + # easier access constants and to maintain a better overview of the + # entire components + #> +[hashtable]$HTTPResponseCode = @{ + 200 = 'Ok'; + 400 = 'Bad Request'; + 401 = 'Unauthorized'; + 403 = 'Forbidden'; + 404 = 'Not Found' + 500 = 'Internal Server Error'; + 504 = 'Gateway Timeout'; +}; + +[hashtable]$HTTPResponseType = @{ + 'Ok' = 200; + 'Bad Request' = 400; + 'Unauthorized' = 401; + 'Forbidden' = 403; + 'Not Found' = 404; + 'Internal Server Error' = 500; + 'Gateway Timeout' = 504; +}; + +<# + # Once we defined a new enum hashtable above, simply add it to this list + # to make it available within the entire module. + # + # Example usage: + # $IcingaHTTPEnums.HTTPResponseType.Ok + #> +[hashtable]$IcingaHTTPEnums = @{ + HTTPResponseCode = $HTTPResponseCode; + HTTPResponseType = $HTTPResponseType; +} + +Export-ModuleMember -Variable @( 'IcingaHTTPEnums' ); +<# +.SYNOPSIS + Sends a basic auth request back to the client +.DESCRIPTION + Sends a basic auth request back to the client +.FUNCTIONALITY + Sends a basic auth request back to the client +.EXAMPLE + PS>Send-IcingaWebAuthMessage -Connection $Connection; +.PARAMETER Connection + The connection data of the Framework containing the client and stream object +.INPUTS + System.Hashtable +.OUTPUTS + Null +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Send-IcingaWebAuthMessage() +{ + param ( + [Hashtable]$Connection = @{ } + ); + + Send-IcingaTCPClientMessage -Message ( + New-IcingaTCPClientRESTMessage ` + -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.Unauthorized) ` + -ContentBody 'Please provide your credentials for login.' ` + -BasicAuth + ) -Stream $Connection.Stream; +} +function New-IcingaSSLStream() +{ + param( + [System.Net.Sockets.TcpClient]$Client = $null, + [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate = $null + ); + + if ($null -eq $Client) { + return $null; + } + + [System.Net.Security.SslStream]$SSLStream = $null; + + try { + $SSLStream = New-Object System.Net.Security.SslStream($Client.GetStream(), $false); + $TLSProtocols = [System.Security.Authentication.SslProtocols]::Tls12 -bor [System.Security.Authentication.SslProtocols]::Tls13; + $SSLStream.AuthenticateAsServer($Certificate, $false, $TLSProtocols, $true) | Out-Null; + } catch { + if ($null -ne $SSLStream) { + $SSLStream.Close(); + $SSLStream.Dispose(); + $SSLStream = $null; + } + Write-IcingaEventMessage -EventId 1500 -Namespace 'Framework' -ExceptionObject $_ -Objects $Client.Client; + return $null; + } + + return $SSLStream; +} +function Read-IcingaTCPStream() +{ + param( + [System.Net.Sockets.TcpClient]$Client = @{ }, + [System.Net.Security.SslStream]$Stream = $null, + [int]$ReadLength = 0 + ); + + [bool]$UnknownMessageSize = $FALSE; + [int]$TimeoutInSeconds = 5; + + if ($ReadLength -eq 0) { + $ReadLength = $Client.ReceiveBufferSize; + $UnknownMessageSize = $TRUE; + } + + if ($null -eq $Stream) { + return $null; + } + + # Ensure that our stream will timeout after a while to not block + # the API permanently + $Stream.ReadTimeout = $TimeoutInSeconds * 1000; + # Get the maxium size of our buffer + [int]$MessageSize = 0; + [byte[]]$bytes = New-Object byte[] $ReadLength; + [int]$DebugReadLength = $ReadLength; + [int]$EmptyStreamCount = 0; + # Ensure we calculate our own timeouts for the API, in case some malicious actions take place + # and we only receive one byte each second for a large message. We should terminate the request + # in this case + $ReadTimeout = [DateTime]::Now; + + while ($ReadLength -ne 0) { + # Create a buffer element to store our message size into + [byte[]]$buffer = New-Object byte[] $ReadLength; + # Read the content of our SSL stream + $ReadBytes = $Stream.Read($buffer, 0, $ReadLength); + # Now lets copy all read bytes from our buffer to our bytes variable + # As we might read multiple times from our stream, we have to read + # the entire buffer from index 0 and copy our content to our bytes + # variable, while our starting index is always at the last message + # sizes end (Message size equals ReadBytes from the previous attempt) + # and we need to copy as much bytes to our new array, as we read + [array]::Copy($buffer, 0, $bytes, $MessageSize, $ReadBytes); + + $ReadLength -= $ReadBytes; + $MessageSize += $ReadBytes; + + # In case the client terminates the session, we might receive a bunch of invalid + # information. Just to make sure there is no other error happening, + # we should wait a little to check if the client is still present and + # trying to send data + if ($ReadBytes -eq 0) { + $EmptyStreamCount += 1; + Start-Sleep -Milliseconds 100; + } + + if ($EmptyStreamCount -gt 20) { + # This would mean we waited 2 seconds to receive actual data, the client decide not to do anything + # We should terminate this session + + Send-IcingaTCPClientMessage -Message ( + New-IcingaTCPClientRESTMessage ` + -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Internal Server Error') ` + -ContentBody @{ 'message' = 'The Icinga for Windows API received no data while reading the TCP stream. The session was terminated to protect the API.' } + ) -Stream $Stream; + + Close-IcingaTCPConnection -Connection @{ 'Client' = $Client; 'Stream' = $Stream; }; + + Write-IcingaDebugMessage ` + -Message 'Icinga for Windows API received multiple empty results and attempts from the client. Terminating session.' ` + -Objects $DebugReadLength, $ReadBytes, $ReadLength, $MessageSize; + + # Return an empty JSON + return '{ }'; + } + + if ((([DateTime]::Now) - $ReadTimeout).TotalSeconds -gt $TimeoutInSeconds -And $ReadLength -ne 0) { + # This would mean we waited to long to receive the entire message + # We should terminate this session, in case we have't just completed the read + # of our message + + Send-IcingaTCPClientMessage -Message ( + New-IcingaTCPClientRESTMessage ` + -HTTPResponse ($IcingaHTTPEnums.HTTPResponseType.'Gateway Timeout') ` + -ContentBody @{ 'message' = 'The Icinga for Windows API waited too long to receive the entire message from the client. The session was terminated to protect the API.' } + ) -Stream $Stream; + + Close-IcingaTCPConnection -Connection @{ 'Client' = $Client; 'Stream' = $Stream; }; + + Write-IcingaDebugMessage ` + -Message 'Icinga for Windows API waited too long to receive the entire message from the client. Terminating session.' ` + -Objects $DebugReadLength, $ReadBytes, $ReadLength, $MessageSize; + + # Return an empty JSON + return '{ }'; + } + + # In case our client did not set a defined message size, we just take the default from the Client + # In most cases, the client will default to 65536 as network buffer for this and we should just + # pretend the message was send properly. In most cases, the important request will go through + # as this will only happen to the first message. Afterwards we can use the HTTP headers to read + # the actual content size of the message + if ($UnknownMessageSize) { + Write-IcingaDebugMessage ` + -Message 'Message buffer for incoming network stream was 65536. Assuming we read all data' ` + -Objects $DebugReadLength, $ReadBytes, $ReadLength, $MessageSize; + + break; + } + + # Just some debug context, allowing us to check what is going on with the API + Write-IcingaDebugMessage ` + -Message 'Network stream output while parsing request. Request Size / Read Bytes / Remaining Size / Message Size' ` + -Objects $DebugReadLength, $ReadBytes, $ReadLength, $MessageSize; + } + + # Resize our array to the correct size, as we might have read + # 65536 bytes because of our client not sending the correct length + [byte[]]$resized = New-Object byte[] $MessageSize; + [array]::Copy($bytes, 0, $resized, 0, $MessageSize); + + Write-IcingaDebugMessage -Message 'Network Stream message size' -Objects $MessageSize; + Write-IcingaDebugMessage -Message 'Network Stream message in bytes' -Objects $resized; + + # Return our message content + return [System.Text.Encoding]::UTF8.GetString($resized); +} +function Get-IcingaForWindowsCertificate() +{ + [string]$CertThumbprint = $null; + [PSCustomObject]$CertFilter = $null; + [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate = $null; + + if ($Global:Icinga.Public.ContainsKey('SSL')) { + $CertThumbprint = $Global:Icinga.Public.SSL.CertThumbprint; + $CertFilter = $Global:Icinga.Public.SSL.CertFilter; + } + + if ([string]::IsNullOrEmpty($CertThumbprint) -eq $FALSE -Or $null -ne $CertFilter) { + [hashtable]$FilterArgs = @{ + '-Recurse' = $TRUE; + '-Path' = 'cert:\LocalMachine\*'; + } + + if ([string]::IsNullOrEmpty($CertThumbprint) -eq $FALSE) { + $FilterArgs.Add('-Include', $CertThumbprint); + } + + [array]$Certificates = Get-ChildItem @FilterArgs; + [DateTime]$CurrentTime = [DateTime]::Now; + + foreach ($cert in $Certificates) { + if ((Test-PSCustomObjectMember -PSObject $CertFilter -Name 'Subject') -And [string]::IsNullOrEmpty($CertFilter.Subject) -eq $FALSE) { + if ($cert.Subject.ToLower() -NotLike $CertFilter.Subject.ToLower()) { + continue; + } + } + + if ((Test-PSCustomObjectMember -PSObject $CertFilter -Name 'Issuer') -And [string]::IsNullOrEmpty($CertFilter.Issuer) -eq $FALSE) { + if ($cert.Issuer.ToLower() -NotLike $CertFilter.Issuer.ToLower()) { + continue; + } + } + + # Certificate expiration date + if ($cert.NotAfter -lt $CurrentTime) { + continue; + } + + # Certificate start date + if ($cert.NotBefore -gt $CurrentTime) { + continue; + } + + $Certificate = $cert; + break; + } + } else { + [string]$CertificateFolder = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'certificate'; + [string]$CertificateFile = Join-Path -Path $CertificateFolder -ChildPath 'icingaforwindows.pfx'; + + if (-Not (Test-Path $CertificateFile)) { + return $null; + } + + $Certificate = ( + New-Object Security.Cryptography.X509Certificates.X509Certificate2 $CertificateFile, '', ([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet) + ); + } + + return $Certificate; +} +function Test-IcingaForWindowsCertificate() +{ + $IfWCertificate = Get-IcingaForWindowsCertificate; + $Hostname = Get-IcingaHostname -ReadConstants; + + if ([string]::IsNullOrEmpty($Hostname)) { + Write-IcingaEventMessage -EventId 1700 -Namespace 'Framework'; + return $FALSE; + } + + if ($null -eq $IfWCertificate) { + return $FALSE; + } + + if ($IfWCertificate.Issuer.ToLower() -eq ([string]::Format('cn={0}', $Hostname).ToLower())) { + return $FALSE; + } + + return $TRUE; +} +function Enable-IcingaUntrustedCertificateValidation() +{ + param ( + [switch]$SuppressMessages = $FALSE + ); + + try { + # There is no other way as to use C# for this specific + # case to configure the certificate validation check + Add-Type @" + using System.Net; + using System.Security.Cryptography.X509Certificates; + + public class IcingaUntrustedCertificateValidation : ICertificatePolicy { + public bool CheckValidationResult(ServicePoint srvPoint, X509Certificate certificate, WebRequest request, int certificateProblem) { + return true; + } + } +"@ + + [System.Net.ServicePointManager]::CertificatePolicy = New-Object IcingaUntrustedCertificateValidation; + + if ($SuppressMessages -eq $FALSE) { + Write-IcingaConsoleNotice 'Successfully enabled untrusted certificate validation for this shell instance'; + } + } catch { + if ($SuppressMessages -eq $FALSE) { + Write-IcingaConsoleError -Message 'Failed to enable untrusted certificate policy: {0}' -Objects $_.Exception.Message; + } + } +} +function Invoke-IcingaForWindowsRESTApi() +{ + param ( + [string]$Uri = 'v1', + [ValidateSet('GET', 'POST')] + [string]$Method = 'GET', + [hashtable]$Body = @{ } + ); + + <# Disable this for now, as there is no way to properly handle this with Windows tool and localhost listener + if ((Test-IcingaCAInstalledToAuthRoot) -eq $FALSE) { + Write-IcingaConsoleError 'The Icinga CA certificate is not installed to the local machine certificate store. Please run the "Start-IcingaWindowsScheduledTaskRenewCertificate" command to fix this issue.'; + return $null; + } + #> + + Set-IcingaTLSVersion; + Enable-IcingaUntrustedCertificateValidation -SuppressMessages; + + $IcingaForWindowsCertificate = Get-IcingaForWindowsCertificate; + $RestApiPort = 5668; + [int]$Timeout = 20; + $BackgroundDaemons = Get-IcingaBackgroundDaemons; + + if ($null -ne $BackgroundDaemons -And $BackgroundDaemons.ContainsKey('Start-IcingaWindowsRESTApi')) { + $Daemon = $BackgroundDaemons['Start-IcingaWindowsRESTApi']; + + # Fetch our deamon configuration + if ($Daemon.ContainsKey('-Port')) { + $RestApiPort = $Daemon['-Port']; + } elseif ($Daemon.ContainsKey('Port')) { + $RestApiPort = $Daemon['Port']; + } + } + + [string]$WebBaseURL = [string]::Format('https://localhost:{0}', $RestApiPort); + + $WebBaseURL = Join-WebPath -Path $WebBaseURL -ChildPath $Uri; + + [hashtable]$Arguments = @{ + '-Method' = $Method; + '-UseBasicParsing' = $TRUE; + '-Uri' = $WebBaseURL; + '-ContentType' = 'application/json'; + '-TimeoutSec' = $Timeout; + '-Certificate' = $IcingaForWindowsCertificate; + '-ErrorAction' = 'Stop'; + }; + + if ($null -ne $Body -And $Body.Count -ne 0 -And $Method -eq 'POST') { + $Arguments.Add('-Body', (ConvertTo-JsonUTF8Bytes -InputObject $Body -Depth 100 -Compress)); + } + + return ( + Invoke-WebRequest @Arguments + ); +} +<# +.SYNOPSIS + Converts a provided Authorization header to credentials +.DESCRIPTION + Converts a provided Authorization header to credentials +.FUNCTIONALITY + Converts a provided Authorization header to credentials +.EXAMPLE + PS>Convert-Base64ToCredentials -AuthString "Basic =Bwebb474567b56756b..."; +.PARAMETER AuthString + The value of the Authorization header of the web request +.INPUTS + System.String +.OUTPUTS + System.Hashtable +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Convert-Base64ToCredentials() +{ + param ( + [String]$AuthString + ); + + [hashtable]$Credentials = @{ }; + + $AuthArray = $AuthString.Split(' '); + + switch ($AuthArray[0]) { + 'Basic' { + $AuthString = $AuthArray[1]; + }; + default { + Write-IcingaEventMessage -EventId 1550 -Namespace 'Framework' -Objects $AuthArray[0]; + return @{ }; + } + } + + # Convert the Base64 Secure String back to a normal string + [string]$AuthString = [System.Text.Encoding]::UTF8.GetString( + [System.Convert]::FromBase64String( + $AuthString + ) + ); + + # If no ':' is within the string, the credential data is not properly formatted + if ($AuthString.Contains(':') -eq $FALSE) { + Write-IcingaEventMessage -EventId 1551 -Namespace 'Framework'; + $AuthString = $null; + return $Credentials; + } + + try { + # Build our User Data and Password from the string + [string]$UserData = $AuthString.Substring( + 0, + $AuthString.IndexOf(':') + ); + $Credentials.Add( + 'password', + ( + ConvertTo-IcingaSecureString ` + $AuthString.Substring($AuthString.IndexOf(':') + 1, $AuthString.Length - $UserData.Length - 1) + ) + ); + + $AuthString = $null; + + # Extract a possible domain + if ($UserData.Contains('\')) { + # Split the auth string on the '\' + [array]$AuthData = $UserData.Split('\'); + # First value of the array is the Domain, second is the Username + $Credentials.Add('domain', $AuthData[0]); + $Credentials.Add( + 'user', + ( + ConvertTo-IcingaSecureString $AuthData[1] + ) + ); + $AuthData = $null; + } else { + $Credentials.Add('domain', $null); + $Credentials.Add( + 'user', + ( + ConvertTo-IcingaSecureString $UserData + ) + ); + } + + $UserData = $null; + } catch { + Write-IcingaEventMessage -EventId 1552 -Namespace 'Framework' -ExceptionObject $_; + return @{}; + } + + return $Credentials; +} +function Close-IcingaTCPSocket() +{ + param( + [System.Net.Sockets.TcpListener]$Socket = $null + ); + + if ($null -eq $Socket) { + return; + } + + Write-IcingaDebugMessage -Message ( + [string]::Format( + 'Closing TCP socket {0}', + $Socket.LocalEndpoint + ) + ); + + $Socket.Stop(); +} +function New-IcingaTCPClient() +{ + param( + [System.Net.Sockets.TcpListener]$Socket = $null + ); + + if ($null -eq $Socket) { + return $null; + } + + [System.Net.Sockets.TcpClient]$Client = $Socket.AcceptTcpClient(); + + Write-IcingaDebugMessage -Message ( + [string]::Format( + 'New incoming client connection for endpoint {0}', + (Get-IcingaTCPClientRemoteEndpoint -Client $Client) + ) + ); + + return $Client; +} +function New-IcingaTCPClientRESTMessage() +{ + param( + [Hashtable]$Headers = $null, + $ContentBody = $null, + [int]$HTTPResponse = 200, + [switch]$BasicAuth = $FALSE + ); + + [string]$ContentLength = ''; + [string]$HTMLContent = ''; + [string]$AuthHeader = ''; + + if ($null -ne $ContentBody) { + $json = ConvertTo-Json $ContentBody -Depth 100 -Compress; + $bytes = [System.Text.Encoding]::UTF8.GetBytes($json); + $HTMLContent = [System.Text.Encoding]::UTF8.GetString($bytes); + if ($bytes.Length -gt 0) { + $ContentLength = [string]::Format( + 'Content-Length: {0}{1}', + $bytes.Length, + (New-IcingaNewLine) + ); + } + } + + if ($BasicAuth) { + $AuthHeader = [string]::Format( + 'WWW-Authenticate: Basic realm="Icinga for Windows"{0}', + (New-IcingaNewLine) + ); + } + + $ResponseMessage = -Join( + [string]::Format( + 'HTTP/1.1 {0} {1}{2}', + $HTTPResponse, + $IcingaHTTPEnums.HTTPResponseCode[$HTTPResponse], + (New-IcingaNewLine) + ), + [string]::Format( + 'Server: IcingaForWindows/{0}{1}', + (Get-Module -Name icinga-powershell-framework).Version, + (New-IcingaNewLine) + ), + [string]::Format( + 'Content-Type: application/json{0}', + (New-IcingaNewLine) + ), + $AuthHeader, + $ContentLength, + (New-IcingaNewLine), + $HTMLContent + ); + + Write-IcingaDebugMessage -Message 'Sending message to client' -Objects $ResponseMessage; + + # Encode our message before sending it + $UTF8Message = [System.Text.Encoding]::UTF8.GetBytes($ResponseMessage); + + return @{ + 'message' = $UTF8Message; + 'length' = $UTF8Message.Length; + }; +} + +<# +.SYNOPSIS + Tests provided credentials against either the local machine or a domain controller +.DESCRIPTION + Tests provided credentials against either the local machine or a domain controller +.FUNCTIONALITY + Tests provided credentials against either the local machine or a domain controller +.EXAMPLE + PS>Test-IcingaRESTCredentials $UserName $SecureUser -Password $SecurePassword; +.EXAMPLE + PS>Test-IcingaRESTCredentials $UserName $SecureUser -Password $SecurePassword -Domain 'Example'; +.PARAMETER UserName + The username to use for login as SecureString +.PARAMETER Password + The password to use for login as SecureString +.PARAMETER Domain + The domain to use for login as string +.INPUTS + System.SecureString +.OUTPUTS + System.Boolean +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Test-IcingaRESTCredentials() +{ + param ( + [SecureString]$UserName, + [SecureString]$Password, + [String]$Domain + ); + + Add-Type -AssemblyName System.DirectoryServices.AccountManagement; + + # Base handling: We try to authenticate against a local user on the machine + [string]$AuthMethod = [System.DirectoryServices.AccountManagement.ContextType]::Machine; + [string]$AuthDomain = $env:COMPUTERNAME; + + # If we specify a domain, we should authenticate against our Domain + if ([string]::IsNullOrEmpty($Domain) -eq $FALSE) { + $AuthMethod = [System.DirectoryServices.AccountManagement.ContextType]::Domain; + $AuthDomain = $Domain; + } + + try { + # Create an Account Management object based on the above determined settings + $AccountService = New-Object System.DirectoryServices.AccountManagement.PrincipalContext( + $AuthMethod, + $AuthDomain + ); + } catch { + # Regardless of the error, print the message and return false to prevent further execution + Write-IcingaEventMessage -EventId 1560 -Namespace 'Framework' -ExceptionObject $_; + return $FALSE; + } + + # In case we couldn't setup the Account Service, always return false + if ($null -eq $AccountService) { + return $FALSE; + } + + try { + # Try to authenticate and either return true or false as integer + [bool]$AuthResult = [int]($AccountService.ValidateCredentials( + (ConvertFrom-IcingaSecureString $UserName), + (ConvertFrom-IcingaSecureString $Password) + ) + ); + + return $AuthResult; + } catch { + Write-IcingaEventMessage -EventId 1561 -Namespace 'Framework' -ExceptionObject $_; + } + + return $FALSE; +} +function Install-IcingaForWindowsCertificate() +{ + param ( + [string]$CertFile = '', + [string]$CertThumbprint = '' + ); + + [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate = $null; + [string]$CertificateFolder = Join-Path -Path (Get-IcingaFrameworkRootPath) -ChildPath 'certificate'; + [string]$CertificateFile = Join-Path -Path $CertificateFolder -ChildPath 'icingaforwindows.pfx'; + [bool]$FoundCertificate = $FALSE; + + if (-Not (Test-Path $CertificateFolder)) { + New-Item -ItemType Directory -Path $CertificateFolder -Force | Out-Null; + } + + if (-Not (Test-IcingaAcl -Directory $CertificateFolder)) { + Set-IcingaAcl -Directory $CertificateFolder; + } + + if (Test-Path $CertificateFile) { + Remove-ItemSecure -Path $CertificateFile -Force | Out-Null; + } + + if ([string]::IsNullOrEmpty($CertFile) -eq $FALSE) { + if ([IO.Path]::GetExtension($CertFile) -ne '.pfx') { + ConvertTo-IcingaX509Certificate -CertFile $CertFile -OutFile $CertificateFile -Force | Out-Null; + } else { + Copy-ItemSecure -Path $CertFile -Destination $CertificateFile -Force | Out-Null; + } + } + + # This is no longer supported as certificates will now be read from the cert store directly + # We just keep the argument for compatibility reasons + if ([string]::IsNullOrEmpty($CertThumbprint) -eq $FALSE) { + Write-IcingaDeprecated -Function 'Install-IcingaForWindowsCertificate' -Argument 'CertThumbprint'; + <#$Certificate = Get-ChildItem -Path 'cert:\*' -Include $CertThumbprint -Recurse + + if ($null -ne $Certificate) { + Export-Certificate -Cert $Certificate -FilePath $CertificateFile | Out-Null; + }#> + return; + } + + if ([string]::IsNullOrEmpty($CertFile) -And [string]::IsNullOrEmpty($CertThumbprint)) { + $IcingaHostCertificate = Get-IcingaAgentHostCertificate; + + if ([string]::IsNullOrEmpty($IcingaHostCertificate.CertFile) -eq $FALSE) { + ConvertTo-IcingaX509Certificate -CertFile $IcingaHostCertificate.CertFile -OutFile $CertificateFile -Force | Out-Null; + } + } + + if (Test-Path $CertificateFile) { + Write-IcingaConsoleNotice -Message 'Successfully installed Icinga for Windows certificate at "{0}"' -Objects $CertificateFile; + } else { + Write-IcingaConsoleError -Message 'Unable to install Icinga for Windows certificate, as with specified arguments and auto-lookup for Icinga Agent certificate, no certificate could be created' -Objects $CertificateFile; + } +} +function Import-IcingaCAToAuthRoot() +{ + $IcingaCAFile = Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs\ca.crt'; + + if ((Test-Path $IcingaCAFile) -eq $FALSE) { + return $FALSE; + } + + if (Test-IcingaCAInstalledToAuthRoot) { + return $TRUE; + } + + Import-Certificate -FilePath $IcingaCAFile -CertStoreLocation 'Cert:\LocalMachine\AuthRoot\' | Out-Null; + + return ( + Test-IcingaCAInstalledToAuthRoot + ); +} +<# +.SYNOPSIS + Securely reads data from enum providers and returns either a found value + from the provider or the given value, in case it was not found or does not + match our index +.DESCRIPTION + Securely reads data from enum providers and returns either a found value + from the provider or the given value, in case it was not found or does not + match our index +.FUNCTIONALITY + Securely reads data from enum providers and returns either a found value + from the provider or the given value, in case it was not found or does not + match our index +.EXAMPLE + PS> Get-IcingaProviderEnumData -Enum $ProviderEnums -Key 'DiskBusType' -Index 6; + PS> Fibre Channel +.PARAMETER Enum + The Icinga for Windows enum provider variable +.PARAMETER Key + The key of the index of our enum we want to access +.PARAMETER Index + They index key for your provided enum. Can either the a numeric value or string +.INPUTS + System.Object +.OUTPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Get-IcingaProviderEnumData() +{ + param ( + $Enum = $null, + $Key = $null, + $Index = '' + ); + + if ($null -eq $Enum -Or $null -eq $Key) { + return $Index; + } + + if ($Enum -IsNot [hashtable]) { + return $Index; + } + + if ($Enum.ContainsKey($Key) -eq $FALSE) { + return $Index; + } + + [array]$Keys = $Enum[$Key].Keys; + + if (Test-Numeric -number $Keys[0]) { + # Handle Providers with numeric indexes + if ((Test-Numeric -number $Index)) { + # Our index is numeric, return the value if it exists + if ($Enum[$Key].ContainsKey([int]$Index)) { + return $Enum[$Key][[int]$Index]; + } + } + } else { + # Handle Providers with string indexes + if ($Enum[$Key].ContainsKey([string]$Index)) { + return $Enum[$Key][[string]$Index]; + } + } + + # If above rules do not apply, simply return the index as it is + return $Index; +} +[hashtable]$TestIcingaWindowsInfo = @{ + 'Ok' = 1; + 'EmptyClass' = 2; + 'PermissionError' = 4; + 'ObjectNotFound' = 8; + 'InvalidNameSpace' = 16; + 'UnhandledException' = 32; + 'NotSpecified' = 64; + 'CimNotInstalled' = 128; +} + +[hashtable]$TestIcingaWindowsInfoText = @{ + 1 = 'Everything is fine.'; + 2 = 'No class specified to check'; + 4 = 'Unable to query data using the given WMI-Class. You are either missing permissions or the service is not running properly'; + 8 = 'The specified WMI Class could not be found in the specified NameSpace.'; + 16 = 'No namespace with the specified name could be found on this system.'; + 32 = 'Windows unhandled exception is thrown. Please enable frame DebugMode for information.'; + 64 = 'Either the service has been stopped or you are not authorized to access the service.'; + 128 = 'The Cmdlet Get-CimClass is not available on your system.'; +} + +[hashtable]$TestIcingaWindowsInfoExceptionType = @{ + 1 = 'OK'; + 2 = 'EmptyClass'; + 4 = 'PermissionError'; + 8 = 'ObjectNotFound'; + 16 = 'InvalidNameSpace'; + 32 = 'UnhandledException'; + 64 = 'NotSpecified'; + 128 = 'CimNotInstalled'; +} + +[hashtable]$TestIcingaWindowsInfoEnums = @{ + TestIcingaWindowsInfo = $TestIcingaWindowsInfo; + TestIcingaWindowsInfoText = $TestIcingaWindowsInfoText; + TestIcingaWindowsInfoExceptionType = $TestIcingaWindowsInfoExceptionType; +} + +Export-ModuleMember -Variable @( 'TestIcingaWindowsInfoEnums' ); +<# + # This script will provide 'Enums' we can use within our module to + # easier access constants and to maintain a better overview of the + # entire components + #> + +[hashtable]$IcingaExitCode = @{ + Ok = 0; + Warning = 1; + Critical = 2; + Unknown = 3; +}; + +[hashtable]$IcingaExitCodeText = @{ + 0 = '[OK]'; + 1 = '[WARNING]'; + 2 = '[CRITICAL]'; + 3 = '[UNKNOWN]'; +}; + +[hashtable]$IcingaExitCodeColor = @{ + 0 = 'Green'; + 1 = 'Yellow'; + 2 = 'Red'; + 3 = 'Magenta'; +}; + +[hashtable]$IcingaThresholdMethod = @{ + Default = 0; # 20 + Lower = 1; # 20: + LowerEqual = 2; + Greater = 3; # ~:20 + GreaterEqual = 4; + Between = 5; # 30:40 + Outside = 6; # @20:30 + Matches = 7; + NotMatches = 8; +}; + +[hashtable]$IcingaMeasurementUnits = @{ + 's' = 'seconds'; + 'ms' = 'milliseconds'; + 'us' = 'microseconds'; + '%' = 'percent'; + 'B' = 'bytes'; + 'KB' = 'Kilobytes'; + 'MB' = 'Megabytes'; + 'GB' = 'Gigabytes'; + 'TB' = 'Terabytes'; + 'c' = 'counter'; + 'Kbit' = 'Kilobit'; + 'Mbit' = 'Megabit'; + 'Gbit' = 'Gigabit'; + 'Tbit' = 'Terabit'; + 'Pbit' = 'Petabit'; + 'Ebit' = 'Exabit'; + 'Zbit' = 'Zettabit'; + 'Ybit' = 'Yottabit'; +}; + +<################################################################################################## +################# Service Enums ################################################################## +##################################################################################################> + +[hashtable]$ServiceStartupTypeName = @{ + 0 = 'Boot'; + 1 = 'System'; + 2 = 'Automatic'; + 3 = 'Manual'; + 4 = 'Disabled'; + 5 = 'Unknown'; # Custom +} + +[hashtable]$ServiceWmiStartupType = @{ + 'Boot' = 0; + 'System' = 1; + 'Auto' = 2; + 'Manual' = 3; + 'Disabled' = 4; + 'Unknown' = 5; # Custom +} + +<# + # Once we defined a new enum hashtable above, simply add it to this list + # to make it available within the entire module. + # + # Example usage: + # $IcingaEnums.IcingaExitCode.Ok + #> +if ($null -eq $IcingaEnums) { + [hashtable]$IcingaEnums = @{ + IcingaExitCode = $IcingaExitCode; + IcingaExitCodeText = $IcingaExitCodeText; + IcingaThresholdMethod = $IcingaThresholdMethod; + IcingaExitCodeColor = $IcingaExitCodeColor; + IcingaMeasurementUnits = $IcingaMeasurementUnits; + #services + ServiceStartupTypeName = $ServiceStartupTypeName; + ServiceWmiStartupType = $ServiceWmiStartupType; + } +} + +Export-ModuleMember -Variable @( 'IcingaEnums' ); +function Get-IcingaInternalPluginOutput() +{ + if ([string]::IsNullOrEmpty($Global:Icinga.Private.Scheduler.PluginException) -eq $FALSE) { + return $Global:Icinga.Private.Scheduler.PluginException; + } + + return $Global:Icinga.Private.Scheduler.CheckResults; +} +function New-IcingaCheckResult() +{ + param ( + $Check, + [bool]$NoPerfData = $FALSE, + [switch]$Compile = $FALSE + ); + + $IcingaCheckResult = New-Object -TypeName PSObject; + $IcingaCheckResult | Add-Member -MemberType NoteProperty -Name 'Check' -Value $Check; + $IcingaCheckResult | Add-Member -MemberType NoteProperty -Name 'NoPerfData' -Value $NoPerfData; + + $IcingaCheckResult | Add-Member -MemberType ScriptMethod -Name 'Compile' -Value { + # Always ensure our cache is cleared before compiling new check data + Get-IcingaCheckSchedulerPluginOutput | Out-Null; + Get-IcingaCheckSchedulerPerfData | Out-Null; + + if ($null -eq $this.Check) { + return $IcingaEnums.IcingaExitCode.Unknown; + } + + # Compile the check / package if not already done + $this.Check.Compile(); + + Write-IcingaPluginOutput -Output ($this.Check.__GetCheckOutput()); + + if ($this.NoPerfData -eq $FALSE) { + Write-IcingaPluginPerfData -IcingaCheck $this.Check; + } + + # Clear our metrics over time cache, as we need to load them again for the next + # plugin execution + $Global:Icinga.Private.Scheduler.PerfDataWriter.MetricsOverTime = ''; + + # Ensure we reset our internal cache once the plugin was executed + $CheckCommand = $this.Check.__GetCheckCommand(); + if ([string]::IsNullOrEmpty($CheckCommand) -eq $FALSE -And $Global:Icinga.Private.Scheduler.ThresholdCache.ContainsKey($CheckCommand)) { + $Global:Icinga.Private.Scheduler.ThresholdCache[$CheckCommand] = $null; + } + # Reset the current execution date + $Global:Icinga.CurrentDate = $null; + + $ExitCode = $this.Check.__GetCheckState(); + + Set-IcingaInternalPluginExitCode -ExitCode $ExitCode; + + $this.Check = $null; + + return $ExitCode; + } + + if ($Compile) { + $IcingaExitCode = $IcingaCheckResult.Compile(); + $IcingaCheckResult = $null; + $Check = $null; + + return $IcingaExitCode; + } + + return $IcingaCheckResult; +} +function Write-IcingaPluginResult() +{ + param ( + [string]$PluginOutput = '', + [string]$PluginPerfData = '' + ); + + [string]$CheckResult = $PluginOutput; + + if ([string]::IsNullOrEmpty($PluginPerfData) -eq $FALSE) { + $CheckResult = [string]::Format('{0}{1}| {2}', $CheckResult, "`r`n", $PluginPerfData); + } + + Write-IcingaConsolePlain $CheckResult; +} +function New-IcingaCheckBaseObject() +{ + $IcingaCheckBaseObject = New-Object -TypeName PSObject; + + $IcingaCheckBaseObject | Add-Member -MemberType NoteProperty -Name 'Name' -Value ''; + $IcingaCheckBaseObject | Add-Member -MemberType NoteProperty -Name 'Verbose' -Value 0; + $IcingaCheckBaseObject | Add-Member -MemberType NoteProperty -Name '__Hidden' -Value $FALSE; + $IcingaCheckBaseObject | Add-Member -MemberType NoteProperty -Name '__SkipSummary' -Value $FALSE; + $IcingaCheckBaseObject | Add-Member -MemberType NoteProperty -Name '__Parent' -Value $IcingaCheckBaseObject; + $IcingaCheckBaseObject | Add-Member -MemberType NoteProperty -Name '__Indention' -Value 0; + $IcingaCheckBaseObject | Add-Member -MemberType NoteProperty -Name '__ErrorMessage' -Value ''; + $IcingaCheckBaseObject | Add-Member -MemberType NoteProperty -Name '__CheckState' -Value $IcingaEnums.IcingaExitCode.Ok; + $IcingaCheckBaseObject | Add-Member -MemberType NoteProperty -Name '__CheckCommand' -Value ''; + $IcingaCheckBaseObject | Add-Member -MemberType NoteProperty -Name '__CheckOutput' -Value $null; + $IcingaCheckBaseObject | Add-Member -MemberType NoteProperty -Name '__ObjectType' -Value 'IcingaCheckBaseObject'; + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__SetCheckCommand' -Value { + $CallStack = Get-PSCallStack; + + foreach ($entry in $CallStack) { + [string]$CheckCommand = $entry.Command; + if ($CheckCommand.ToLower() -Like 'invoke-icingacheck*') { + $this.__CheckCommand = $CheckCommand; + $Global:Icinga.Private.Scheduler.CheckCommand = $CheckCommand; + break; + } + } + + if ([string]::IsNullOrEmpty($this.__CheckCommand)) { + return; + } + + if ($Global:Icinga.Private.Scheduler.ThresholdCache.ContainsKey($this.__CheckCommand) -eq $FALSE) { + $Global:Icinga.Private.Scheduler.ThresholdCache.Add($this.__CheckCommand, $null); + } + + if ($null -ne $Global:Icinga.Private.Scheduler.ThresholdCache[$this.__CheckCommand]) { + return; + } + + if ($Global:Icinga.Public.Daemons.ContainsKey('ServiceCheck') -And $Global:Icinga.Public.Daemons.ServiceCheck.PerformanceDataCache.ContainsKey($this.__CheckCommand) -And $Global:Icinga.Public.Daemons.ServiceCheck.PerformanceDataCache[$CheckCommand].Count -ne 0) { + $Global:Icinga.Private.Scheduler.ThresholdCache[$this.__CheckCommand] = $Global:Icinga.Public.Daemons.ServiceCheck.PerformanceDataCache[$CheckCommand]; + } else { + # Fallback in case the service is not registered within the daemon or we are running a plugin from without the daemon environment + $Global:Icinga.Private.Scheduler.ThresholdCache[$this.__CheckCommand] = (Get-IcingaCacheData -Space 'sc_daemon' -CacheStore 'checkresult' -KeyName $this.__CheckCommand); + } + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__SetParent' -Value { + param ($Parent); + + $this.__Parent = $Parent; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__GetParent' -Value { + return $this.__Parent; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__IsHidden' -Value { + return $this.__Hidden; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__SetHidden' -Value { + param ([bool]$Hidden); + + $this.__Hidden = $Hidden; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__NoHeaderReport' -Value { + return $this.__SkipSummary; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__SetNoHeaderReport' -Value { + param ([bool]$SkipSummary); + + $this.__SkipSummary = $SkipSummary; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__GetName' -Value { + return $this.Name; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__SetIndention' -Value { + param ($Indention); + + $this.__Indention = $Indention; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__GetIndention' -Value { + return $this.__Indention; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__NewIndention' -Value { + return ($this.__Indention + 1); + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__GetCheckState' -Value { + return $this.__CheckState; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__GetCheckCommand' -Value { + return $this.__CheckCommand; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Force -Name '__SetCheckOutput' -Value { + param ($PluginOutput); + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__GetCheckOutput' -Value { + + if ($this.__IsHidden()) { + return '' + }; + + if ($this._CanOutput() -eq $FALSE) { + return ''; + } + + return ( + [string]::Format( + '{0}{1}', + (New-StringTree -Spacing $this.__GetIndention()), + $this.__CheckOutput + ) + ); + } + + # Shared function + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name 'Compile' -Value { + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__SetVerbosity' -Value { + param ($Verbosity); + + $this.Verbose = $Verbosity; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__GetVerbosity' -Value { + return $this.Verbose; + } + + # Shared function + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__GetHeaderOutputValue' -Value { + return ''; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '_CanOutput' -Value { + # Always allow the output of the top parent elements + if ($this.__GetIndention() -eq 0) { + return $TRUE; + } + + switch ($this.Verbose) { + 0 { # Only print states not being OK + if ($this.__CheckState -ne $IcingaEnums.IcingaExitCode.Ok) { + return $TRUE; + } + + if ($this.__ObjectType -eq 'IcingaCheckPackage') { + return $this.__HasNotOkChecks(); + } + + return $FALSE; + }; + 1 { # Print states not being OK and all content of affected check packages + if ($this.__CheckState -ne $IcingaEnums.IcingaExitCode.Ok) { + return $TRUE; + } + + if ($this.__ObjectType -eq 'IcingaCheckPackage') { + return $this.__HasNotOkChecks(); + } + + if ($this.__GetParent().__ObjectType -eq 'IcingaCheckPackage') { + return $this.__GetParent().__HasNotOkChecks(); + } + + return $FALSE; + }; + } + + # For any other verbosity, print everything + return $TRUE; + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name '__ValidateThresholdInput' -Value { + # Shared function + } + + $IcingaCheckBaseObject | Add-Member -MemberType ScriptMethod -Name 'HasChecks' -Value { + # Shared function + } + + $IcingaCheckBaseObject.__SetCheckCommand(); + + return $IcingaCheckBaseObject; +} +function Write-IcingaPluginOutput() +{ + param ( + $Output + ); + + if ($Global:Icinga.Protected.RunAsDaemon -eq $FALSE -And $Global:Icinga.Protected.JEAContext -eq $FALSE) { + if ($Global:Icinga.Protected.Minimal) { + Clear-CLIConsole; + } + Write-IcingaConsolePlain $Output; + } else { + # New behavior with local thread separated results + [array]$Global:Icinga.Private.Scheduler.CheckResults += $Output; + } +} +function New-IcingaPerformanceDataEntry() +{ + param ( + $PerfDataObject, + $Label = $null, + $Value = $null, + $Warning = $null, + $Critical = $null, + [hashtable]$PerfData = @{ }, + [string]$Interval = '' + ); + + if ($null -eq $PerfDataObject) { + return $PerfData; + } + + [string]$MetricIndex = $PerfDataObject.index; + [string]$MetricName = $PerfDataObject.name; + [string]$LabelName = $PerfDataObject.label; + [string]$Template = $PerfDataObject.template; + [string]$PerfValue = $PerfDataObject.value; + [string]$WarningValue = $PerfDataObject.warning; + [string]$CriticalValue = $PerfDataObject.critical; + + if ([string]::IsNullOrEmpty($Label) -eq $FALSE) { + $LabelName = $Label; + $MetricInterval = $Label.Split('::')[-1]; + $MetricName = [string]::Format('{0}::{1}', $MetricName, $MetricInterval); + } + if ([string]::IsNullOrEmpty($Value) -eq $FALSE) { + $PerfValue = $Value; + } + + # Override our warning/critical values only if the label does not match. + # Eg. Core_1 not matching Core_1_5 - this is only required for time span checks + if ([string]::IsNullOrEmpty($Label) -eq $FALSE -And [string]::IsNullOrEmpty($Interval) -eq $FALSE -And $Label.Contains([string]::Format('::Interval{0}', $Interval))) { + $WarningValue = $Warning; + $CriticalValue = $Critical; + } + + $minimum = ''; + $maximum = ''; + + if ([string]::IsNullOrEmpty($PerfDataObject.minimum) -eq $FALSE) { + $minimum = [string]::Format(';{0}', $PerfDataObject.minimum); + } + if ([string]::IsNullOrEmpty($PerfDataObject.maximum) -eq $FALSE) { + $maximum = [string]::Format(';{0}', $PerfDataObject.maximum); + } + + [string]$MultiLabelName = ''; + $LabelName = [string]::Format('{0}::ifw_{1}::{2}', $MetricIndex, $Template, $MetricName).Replace('::::', '::'); + + if ($LabelName.Contains('::Interval') -eq $FALSE) { + if ($PerfData.ContainsKey($LabelName) -eq $FALSE) { + $PerfData.Add( + $LabelName, + @{ + 'Index' = ''; + 'Values' = @() + } + ); + } + } + + if ([string]::IsNullOrEmpty($LabelName) -eq $FALSE -And $LabelName.Contains('::Interval')) { + $IntervalName = $LabelName.Split('::')[-1]; + $LabelInterval = $IntervalName.Replace('Interval', ''); + $MetricName = $LabelName.Split('::')[4]; + $MultiLabelName = [string]::Format('{0}{1}', $MetricName, (ConvertTo-IcingaNumericTimeIndex -TimeValue $LabelInterval)); + $LabelName = [string]::Format('{0}::ifw_{1}::{2}', $MetricIndex, $Template, $MetricName); + } else { + $MultiLabelName = $LabelName; + } + + $PerfDataOutput = [string]::Format( + "'{0}'={1}{2};{3};{4}{5}{6}", + $MultiLabelName.ToLower(), + (Format-IcingaPerfDataValue $PerfValue), + $PerfDataObject.unit, + (Format-IcingaPerfDataValue $WarningValue), + (Format-IcingaPerfDataValue $CriticalValue), + (Format-IcingaPerfDataValue $minimum), + (Format-IcingaPerfDataValue $maximum) + ); + + if ($MultiLabelName.Contains('::ifw_')) { + $PerfData[$LabelName].Index = $PerfDataOutput; + } else { + $PerfData[$LabelName].Values += $PerfDataOutput; + } + + return $PerfData; +} +function Set-IcingaInternalPluginException() +{ + param ( + [string]$PluginException = '' + ); + + # Only catch the first exception + if ([string]::IsNullOrEmpty($Global:Icinga.Private.Scheduler.PluginException)) { + $Global:Icinga.Private.Scheduler.PluginException = $PluginException; + } +} +function Write-IcingaExecutePluginException() +{ + param ( + $Command = '', + $ErrorObject = $null, + $Arguments = @() + ); + + if ($null -eq $ErrorObject) { + return; + } + + $ExMsg = $ErrorObject.Exception.Message; + $StackTrace = $ErrorObject.ScriptStackTrace; + $ExErrorId = $ErrorObject.FullyQualifiedErrorId; + $ArgName = $ErrorObject.Exception.ParameterName; + $ListArgs = @(); + + foreach ($entry in $Arguments) { + if ($entry -eq '-IcingaForWindowsRemoteExecution' -Or $entry -eq '-IcingaForWindowsJEARemoteExecution') { + continue; + } + $ListArgs += $entry; + } + + if ($ExErrorId -Like "*ParameterArgumentTransformationError*" -And $ExMsg.Contains('System.Security.SecureString')) { + $ExMsg = [string]::Format( + 'Cannot bind parameter {0}. Cannot convert the provided value for argument "{0}" of type "System.String" to type "System.Security.SecureString".', + $ArgName + ); + + $Arguments.Clear(); + $ListArgs = 'Hidden for security reasons'; + } + + Write-IcingaConsolePlain '[UNKNOWN] Icinga Exception: {0}{1}{1}CheckCommand: {2}{1}Arguments: {3}{1}{1}StackTrace:{1}{4}' -Objects $ExMsg, (New-IcingaNewLine), $Command, $ListArgs, $StackTrace; + $Global:Icinga.Private.Scheduler.ExitCode = 3; +} +function New-IcingaCheck() +{ + param( + [string]$Name = '', + $Value = $null, + $BaseValue = $null, + $Unit = '', + $MetricIndex = 'default', + $MetricName = '', + $MetricTemplate = '', + [string]$Minimum = '', + [string]$Maximum = '', + $ObjectExists = -1, + $Translation = $null, + [string]$LabelName = $null, + [switch]$NoPerfData = $FALSE, + [switch]$NoHeaderReport = $FALSE + ); + + $IcingaCheck = New-IcingaCheckBaseObject; + + $IcingaCheck.Name = $Name; + $IcingaCheck.__ObjectType = 'IcingaCheck'; + + # Ensure we always set our current value to 0 in case it is null and we set a unit, to prevent conversion exceptions + if ([string]::IsNullOrEmpty($Unit) -eq $FALSE -And $null -eq $Value) { + $IcingaCheck | Add-Member -MemberType NoteProperty -Name 'Value' -Value 0; + } else { + $IcingaCheck | Add-Member -MemberType NoteProperty -Name 'Value' -Value $Value; + } + + $IcingaCheck | Add-Member -MemberType NoteProperty -Name 'BaseValue' -Value $BaseValue; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name 'Unit' -Value $Unit; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name 'MetricIndex' -Value $MetricIndex; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name 'MetricName' -Value $MetricName; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name 'MetricTemplate' -Value $MetricTemplate; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name 'Minimum' -Value $Minimum; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name 'Maximum' -Value $Maximum; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name 'ObjectExists' -Value $ObjectExists; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name 'Translation' -Value $Translation; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name 'LabelName' -Value $LabelName; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name 'NoPerfData' -Value ([bool]$NoPerfData); + $IcingaCheck | Add-Member -MemberType NoteProperty -Name '__WarningValue' -Value $null; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name '__CriticalValue' -Value $null; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name '__LockedState' -Value $FALSE; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name '__ThresholdObject' -Value $null; + $IcingaCheck | Add-Member -MemberType NoteProperty -Name '__TimeInterval' -Value $null; + + $IcingaCheck.__SetNoHeaderReport($NoHeaderReport); + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Force -Name 'Compile' -Value { + $this.__ValidateThresholdInput(); + if ($null -eq $this.__ThresholdObject) { + $this.__CreateDefaultThresholdObject(); + } + $this.__SetCheckOutput(); + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Force -Name '__SetInternalTimeInterval' -Value { + $CallStack = Get-PSCallStack; + [bool]$FoundInterval = $FALSE; + + foreach ($entry in $CallStack) { + if ($FoundInterval) { + break; + } + [string]$CheckCommand = $entry.Command; + if ($CheckCommand -eq $this.__CheckCommand) { + [string]$CheckArguments = $entry.Arguments.Replace('{', '').Replace('}', ''); + [array]$SplitArgs = $CheckArguments.Split(','); + + foreach ($SetArg in $SplitArgs) { + $SetArg = $SetArg.Replace(' ', ''); + if ($FoundInterval) { + $this.__TimeInterval = $SetArg; + break; + } + if ($SetArg -eq '-ThresholdInterval' -Or $SetArg -eq '-ThresholdInterval:') { + $FoundInterval = $TRUE; + continue; + } + } + break; + } + } + } + + # Override shared function + $IcingaCheck | Add-Member -MemberType ScriptMethod -Force -Name '__GetHeaderOutputValue' -Value { + if ($null -eq $this.__ThresholdObject) { + return '' + } + + if ([string]::IsNullOrEmpty($this.__ThresholdObject.HeaderValue)) { + return ''; + } + + return ( + [string]::Format( + ' ({0})', + $this.__ThresholdObject.HeaderValue + ) + ) + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Force -Name '__CreateDefaultThresholdObject' -Value { + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $this.__ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + $this.__SetCheckState($this.__ThresholdObject, $IcingaEnums.IcingaExitCode.Ok); + } + + # Override shared function + $IcingaCheck | Add-Member -MemberType ScriptMethod -Force -Name '__SetCheckOutput' -Value { + param ($PluginOutput); + + if ($this.__InLockState()) { + return; + } + + $PluginThresholds = ''; + $TimeSpan = ''; + $PluginThresholds = $this.__ThresholdObject.Message; + + if ([string]::IsNullOrEmpty($PluginOutput) -eq $FALSE) { + $PluginThresholds = $PluginOutput; + } + + <#if ($null -ne $this.__ThresholdObject -And [string]::IsNullOrEmpty($this.__ThresholdObject.TimeSpan) -eq $FALSE) { + $TimeSpan = [string]::Format( + '{0}({1}m avg.)', + (&{ if ([string]::IsNullOrEmpty($PluginThresholds)) { return ''; } else { return ' ' } }), + $this.__ThresholdObject.TimeSpanOutput + ); + }#> + + [bool]$AddColon = $TRUE; + + if ([string]::IsNullOrEmpty($this.Name) -eq $FALSE -And $this.Name[$this.Name.Length - 1] -eq ':') { + $AddColon = $FALSE; + } + + $this.__CheckOutput = [string]::Format( + '{0} {1}{2} {3}{4}', + $IcingaEnums.IcingaExitCodeText[$this.__CheckState], + $this.Name, + (&{ if ($AddColon) { return ':'; } else { return ''; } }), + $PluginThresholds, + $TimeSpan + ); + + $this.__SetPerformanceData(); + } + + # __GetTimeSpanThreshold(0, 'Core_30_20', 'Core_30') + $IcingaCheck | Add-Member -MemberType ScriptMethod -Force -Name '__GetTimeSpanThreshold' -Value { + param ($TimeSpanLabel, $Label, $MultiOutput); + + [hashtable]$TimeSpans = @{ + 'Warning' = ''; + 'Critical' = ''; + 'Interval' = ''; + } + + [string]$LabelName = (Format-IcingaPerfDataLabel -PerfData $this.Name -MultiOutput:$MultiOutput); + if ([string]::IsNullOrEmpty($this.LabelName) -eq $FALSE) { + $LabelName = $this.LabelName; + } + + if ($Label -ne $LabelName) { + return $TimeSpans; + } + + $TimeSpan = $TimeSpanLabel.Replace($Label, '').Replace('_', '').Replace('::Interval', '').Replace('::', ''); + + if ($null -ne $this.__WarningValue -And [string]::IsNullOrEmpty($this.__WarningValue.TimeSpan) -eq $FALSE -And $this.__WarningValue.TimeSpan -eq $TimeSpan) { + $TimeSpans.Warning = $this.__WarningValue.Threshold.Threshold; + } + if ($null -ne $this.__CriticalValue -And [string]::IsNullOrEmpty($this.__CriticalValue.TimeSpan) -eq $FALSE -And $this.__CriticalValue.TimeSpan -eq $TimeSpan) { + $TimeSpans.Critical = $this.__CriticalValue.Threshold.Threshold; + } + $TimeSpans.Interval = $TimeSpan; + + return $TimeSpans; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__GetWarningThresholdObject' -Value { + return $this.__WarningValue; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__GetCriticalThresholdObject' -Value { + return $this.__CriticalValue; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__CreatePerfDataLabel' -Value { + $PerfDataTemplate = ($this.__CheckCommand.Replace('Invoke-IcingaCheck', '')); + + if ([string]::IsNullOrEmpty($this.MetricTemplate) -eq $FALSE) { + $PerfDataTemplate = $this.MetricTemplate; + } + + [string]$PerfDataName = [string]::Format( + '{0}::ifw_{1}::{2}', + $this.MetricIndex, + $PerfDataTemplate.ToLower(), + $this.MetricName + ); + + return $PerfDataName; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__SetPerformanceData' -Value { + if ($null -eq $this.__ThresholdObject -Or $this.NoPerfData) { + return; + } + + [string]$LabelName = (Format-IcingaPerfDataLabel -PerfData $this.Name); + [string]$MultiLabelName = (Format-IcingaPerfDataLabel -PerfData $this.Name -MultiOutput); + $value = ConvertTo-Integer -Value $this.__ThresholdObject.Value; + $warning = ''; + $critical = ''; + + # Set our threshold to nothing if we use time spans, as it would cause performance metrics to + # contain warning/critical values for everything, which is not correct + if ([string]::IsNullOrEmpty($this.__WarningValue.TimeSpan)) { + $warning = ConvertTo-Integer -Value $this.__WarningValue.Threshold.Threshold -NullAsEmpty; + } + if ([string]::IsNullOrEmpty($this.__CriticalValue.TimeSpan)) { + $critical = ConvertTo-Integer -Value $this.__CriticalValue.Threshold.Threshold -NullAsEmpty; + } + + if ([string]::IsNullOrEmpty($this.LabelName) -eq $FALSE) { + $LabelName = $this.LabelName; + $MultiLabelName = $this.LabelName; + } + + if ([string]::IsNullOrEmpty($this.Minimum) -And [string]::IsNullOrEmpty($this.Maximum)) { + if ($this.Unit -eq '%') { + $this.Minimum = '0'; + $this.Maximum = '100'; + } elseif ($null -ne $this.BaseValue) { + $this.Minimum = '0'; + $this.Maximum = $this.__ThresholdObject.BaseValue; + } + + if ([string]::IsNullOrEmpty($this.Maximum) -eq $FALSE -And (Test-Numeric $this.Maximum) -And (Test-Numeric $this.Value) -And $this.Value -gt $this.Maximum) { + $this.Maximum = $this.__ThresholdObject.Value; + } + } + + [string]$PerfDataName = $this.__CreatePerfDataLabel(); + + # Ensure we only add a label with identical name once + if ($Global:Icinga.Private.Scheduler.PerfDataWriter.Cache.ContainsKey($PerfDataName) -eq $FALSE) { + $Global:Icinga.Private.Scheduler.PerfDataWriter.Cache.Add($PerfDataName, $TRUE); + } else { + return; + } + + [string]$PerfDataLabel = [string]::Format( + '{0}={1}{2};{3};{4};{5};{6}', + $PerfDataName.ToLower(), + (Format-IcingaPerfDataValue $value), + $this.__ThresholdObject.Unit, + (Format-IcingaPerfDataValue $warning), + (Format-IcingaPerfDataValue $critical), + (Format-IcingaPerfDataValue $this.Minimum), + (Format-IcingaPerfDataValue $this.Maximum) + ); + + # Add a space before adding another metric + if ($Global:Icinga.Private.Scheduler.PerfDataWriter.Storage.Length -ne 0) { + $Global:Icinga.Private.Scheduler.PerfDataWriter.Storage.Append(' ') | Out-Null; + } + + # This is just to make sure the background daemon has data to work with and also ensure we don't increase + # memory in case we don't have the daemon running + if ($Global:Icinga.Private.Scheduler.PerfDataWriter.Daemon.ContainsKey($PerfDataName) -eq $FALSE) { + $Global:Icinga.Private.Scheduler.PerfDataWriter.Daemon.Add( + $PerfDataName, + @{ + 'Value' = $value; + 'Unit' = $this.__ThresholdObject.PerfUnit + } + ); + } else { + $Global:Icinga.Private.Scheduler.PerfDataWriter.Daemon[$PerfDataName] = @{ + 'Value' = $value; + 'Unit' = $this.__ThresholdObject.PerfUnit + } + } + + $Global:Icinga.Private.Scheduler.PerfDataWriter.Storage.Append($PerfDataLabel) | Out-Null; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__ValidateObject' -Value { + if ($null -eq $this.ObjectExists) { + $this.SetUnknown() | Out-Null; + $this.__SetCheckOutput('The object does not exist'); + $this.__LockState(); + } + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__LockState' -Value { + $this.__LockedState = $TRUE; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__InLockState' -Value { + return $this.__LockedState; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__ValidateUnit' -Value { + if ([string]::IsNullOrEmpty($this.Unit) -eq $FALSE -And (-Not $IcingaEnums.IcingaMeasurementUnits.ContainsKey($this.Unit))) { + $this.SetUnknown(); + $this.__SetCheckOutput( + [string]::Format( + 'Usage of invalid plugin unit "{0}". Allowed units are: {1}', + $this.Unit, + (($IcingaEnums.IcingaMeasurementUnits.Keys | Sort-Object name) -Join ', ') + ) + ); + + $this.__LockState(); + $this.unit = $null; + } + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__ValidateMetricsName' -Value { + if ([string]::IsNullOrEmpty($this.MetricIndex)) { + Write-IcingaConsoleError -Message 'The metric index has no default value for the check object "{0}"' -Objects $this.Name; + } else { + $this.MetricIndex = Format-IcingaPerfDataLabel -PerfData $this.MetricIndex -MultiOutput; + } + + if ([string]::IsNullOrEmpty($this.MetricName)) { + $this.MetricName = Format-IcingaPerfDataLabel -PerfData $this.Name -MultiOutput; + } else { + $this.MetricName = Format-IcingaPerfDataLabel -PerfData $this.MetricName -MultiOutput; + } + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__ConvertMinMax' -Value { + if ([string]::IsNullOrEmpty($this.Unit) -eq $FALSE) { + if ([string]::IsNullOrEmpty($this.Minimum) -eq $FALSE) { + $this.Minimum = (Convert-IcingaPluginThresholds -Threshold ([string]::Format('{0}{1}', $this.Minimum, $this.Unit))).Value; + } + if ([string]::IsNullOrEmpty($this.Maximum) -eq $FALSE) { + $this.Maximum = (Convert-IcingaPluginThresholds -Threshold ([string]::Format('{0}{1}', $this.Maximum, $this.Unit))).Value; + } + } + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__SetCurrentExecutionTime' -Value { + if ($null -eq $global:Icinga) { + $global:Icinga = @{ }; + } + + if ($global:Icinga.ContainsKey('CurrentDate') -eq $FALSE) { + $global:Icinga.Add('CurrentDate', (Get-Date)); + return; + } + + if ($null -ne $global:Icinga.CurrentDate) { + return; + } + + $global:Icinga.CurrentDate = (Get-Date).ToUniversalTime(); + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__AddCheckDataToCache' -Value { + + # We only require this in case we are running as daemon + if ([string]::IsNullOrEmpty($this.__CheckCommand) -Or $Global:Icinga.Protected.RunAsDaemon -eq $FALSE) { + return; + } + + if ($global:Icinga.Private.Scheduler.CheckData.ContainsKey($this.__CheckCommand) -eq $FALSE) { + return; + } + + # Fix possible error for identical time stamps due to internal exceptions + # and check execution within the same time slot because of this + [string]$TimeIndex = Get-IcingaUnixTime; + [string]$LabelName = $this.Name; + + if ([string]::IsNullOrEmpty($this.LabelName) -eq $FALSE) { + $LabelName = $this.LabelName; + } + + Add-IcingaHashtableItem -Hashtable $global:Icinga.Private.Scheduler.CheckData[$this.__CheckCommand]['results'] -Key $LabelName -Value @{ } | Out-Null; + Add-IcingaHashtableItem -Hashtable $global:Icinga.Private.Scheduler.CheckData[$this.__CheckCommand]['results'][$LabelName] -Key $TimeIndex -Value $this.Value -Override | Out-Null; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'SetOk' -Value { + param ([string]$Message, [bool]$Lock); + + if ($this.__InLockState() -eq $FALSE) { + $this.__CheckState = $IcingaEnums.IcingaExitCode.Ok; + $this.__SetCheckOutput($Message); + } + + if ($Lock) { + $this.__LockState(); + } + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'SetWarning' -Value { + param ([string]$Message, [bool]$Lock); + + if ($this.__InLockState() -eq $FALSE) { + $this.__CheckState = $IcingaEnums.IcingaExitCode.Warning; + $this.__SetCheckOutput($Message); + } + + if ($Lock) { + $this.__LockState(); + } + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'SetCritical' -Value { + param ([string]$Message, [bool]$Lock); + + if ($this.__InLockState() -eq $FALSE) { + $this.__CheckState = $IcingaEnums.IcingaExitCode.Critical; + $this.__SetCheckOutput($Message); + } + + if ($Lock) { + $this.__LockState(); + } + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'SetUnknown' -Value { + param ([string]$Message, [bool]$Lock); + + if ($this.__InLockState() -eq $FALSE) { + $this.__CheckState = $IcingaEnums.IcingaExitCode.Unknown; + $this.__SetCheckOutput($Message); + } + + if ($Lock) { + $this.__LockState(); + } + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__SetCheckState' -Value { + param ($ThresholdObject, $State); + + if ($ThresholdObject.HasError) { + $this.SetUnknown() | Out-Null; + $this.__ThresholdObject = $ThresholdObject; + $this.__SetCheckOutput($this.__ThresholdObject.Message); + $this.__LockState(); + return; + } + + if ($this.__InLockState()) { + return; + } + + # In case no thresholds are set, always set the first value + if ($null -eq $this.__ThresholdObject) { + $this.__ThresholdObject = $ThresholdObject; + } + + if ($ThresholdObject.IsOk -eq $FALSE) { + if ($this.__CheckState -lt $State) { + $this.__CheckState = $State; + $this.__ThresholdObject = $ThresholdObject; + } + } + + $this.__SetCheckOutput(); + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name '__GetBaseThresholdArguments' -Value { + return @{ + '-InputValue' = $this.Value; + '-BaseValue' = $this.BaseValue; + '-Unit' = $this.Unit; + '-CheckName' = $this.__GetName(); + '-PerfDataLabel' = $this.__CreatePerfDataLabel(); + '-ThresholdCache' = (Get-IcingaThresholdCache -CheckCommand $this.__CheckCommand); + '-Translation' = $this.Translation; + '-TimeInterval' = $this.__TimeInterval; + '-NoPerfData' = $this.NoPerfData; + }; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'WarnOutOfRange' -Value { + param ($Threshold); + + if ($null -ne $Threshold -And $Threshold.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Threshold) { + $this.WarnOutOfRange($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Threshold', $Threshold); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__WarningValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Warning); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'WarnDateTime' -Value { + param ($Threshold); + + if ($null -ne $Threshold -And $Threshold.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Threshold) { + $this.WarnDateTime($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Threshold', $Threshold); + $ThresholdArguments.Add('-DateTime', $TRUE); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__WarningValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Warning); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'WarnIfLike' -Value { + param ($Threshold); + + if ($null -ne $Threshold -And $Threshold.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Threshold) { + $this.WarnIfLike($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Threshold', $Threshold); + $ThresholdArguments.Add('-Matches', $TRUE); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__WarningValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Warning); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'WarnIfNotLike' -Value { + param ($Threshold); + + if ($null -ne $Threshold -And $Threshold.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Threshold) { + $this.WarnIfNotLike($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Threshold', $Threshold); + $ThresholdArguments.Add('-NotMatches', $TRUE); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__WarningValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Warning); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'WarnIfMatch' -Value { + param ($Threshold); + + return $this.WarnIfLike($Threshold); + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'WarnIfNotMatch' -Value { + param ($Threshold); + + return $this.WarnIfNotLike($Threshold); + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'CritOutOfRange' -Value { + param ($Threshold); + + if ($null -ne $Threshold -And $Threshold.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Threshold) { + $this.CritOutOfRange($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Threshold', $Threshold); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__CriticalValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Critical); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'CritDateTime' -Value { + param ($Threshold); + + if ($null -ne $Threshold -And $Threshold.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Threshold) { + $this.CritDateTime($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Threshold', $Threshold); + $ThresholdArguments.Add('-DateTime', $TRUE); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__CriticalValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Critical); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'CritIfLike' -Value { + param ($Threshold); + + if ($null -ne $Threshold -And $Threshold.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Threshold) { + $this.CritIfLike($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Threshold', $Threshold); + $ThresholdArguments.Add('-Matches', $TRUE); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__CriticalValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Critical); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'CritIfNotLike' -Value { + param ($Threshold); + + if ($null -ne $Threshold -And $Threshold.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Threshold) { + $this.CritIfNotLike($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Threshold', $Threshold); + $ThresholdArguments.Add('-NotMatches', $TRUE); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__CriticalValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Critical); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'CritIfMatch' -Value { + param ($Threshold); + + return $this.CritIfLike($Threshold); + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'CritIfNotMatch' -Value { + param ($Threshold); + + return $this.CritIfNotLike($Threshold); + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'WarnIfBetweenAndEqual' -Value { + param ($Min, $Max); + + [string]$Threshold = [string]::Format('@{0}:{1}', $Min, $Max); + + return $this.WarnOutOfRange($Threshold); + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'CritIfBetweenAndEqual' -Value { + param ($Min, $Max); + + [string]$Threshold = [string]::Format('@{0}:{1}', $Min, $Max); + + return $this.CritOutOfRange($Threshold); + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'WarnIfLowerThan' -Value { + param ($Value); + + if ($null -ne $Value -And $Value.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Value) { + $this.WarnIfLowerThan($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [string]$Threshold = [string]::Format('{0}:', $Value); + + return $this.WarnOutOfRange($Threshold); + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'CritIfLowerThan' -Value { + param ($Value); + + if ($null -ne $Value -And $Value.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Value) { + $this.CritIfLowerThan($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [string]$Threshold = [string]::Format('{0}:', $Value); + + return $this.CritOutOfRange($Threshold); + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'WarnIfGreaterThan' -Value { + param ($Value); + + if ($null -ne $Value -And $Value.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Value) { + $this.WarnIfGreaterThan($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [string]$Threshold = [string]::Format('~:{0}', $Value); + + return $this.WarnOutOfRange($Threshold); + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'CritIfGreaterThan' -Value { + param ($Value); + + if ($null -ne $Value -And $Value.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Value) { + $this.CritIfGreaterThan($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [string]$Threshold = [string]::Format('~:{0}', $Value); + + return $this.CritOutOfRange($Threshold); + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'WarnIfBetween' -Value { + param ($Min, $Max); + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Minimum', $Min); + $ThresholdArguments.Add('-Maximum', $Max); + $ThresholdArguments.Add('-IsBetween', $TRUE); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__WarningValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Warning); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'CritIfBetween' -Value { + param ($Min, $Max); + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Minimum', $Min); + $ThresholdArguments.Add('-Maximum', $Max); + $ThresholdArguments.Add('-IsBetween', $TRUE); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__CriticalValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Critical); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'WarnIfLowerEqualThan' -Value { + param ($Threshold); + + if ($null -ne $Threshold -And $Threshold.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Threshold) { + $this.WarnIfLowerEqualThan($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Threshold', $Threshold); + $ThresholdArguments.Add('-IsLowerEqual', $TRUE); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__WarningValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Warning); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'CritIfLowerEqualThan' -Value { + param ($Threshold); + + if ($null -ne $Threshold -And $Threshold.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Threshold) { + $this.CritIfLowerEqualThan($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Threshold', $Threshold); + $ThresholdArguments.Add('-IsLowerEqual', $TRUE); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__CriticalValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Critical); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'WarnIfGreaterEqualThan' -Value { + param ($Threshold); + + if ($null -ne $Threshold -And $Threshold.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Threshold) { + $this.WarnIfGreaterEqualThan($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Threshold', $Threshold); + $ThresholdArguments.Add('-IsGreaterEqual', $TRUE); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__WarningValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Warning); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Name 'CritIfGreaterEqualThan' -Value { + param ($Threshold); + + if ($null -ne $Threshold -And $Threshold.GetType().BaseType.Name.ToLower() -eq 'array') { + foreach ($entry in $Threshold) { + $this.CritIfGreaterEqualThan($entry) | Out-Null; + + # Break on first value causing a warning + if ($ThresholdObject.InRange -eq $FALSE) { + break; + } + } + + return $this; + } + + [hashtable]$ThresholdArguments = $this.__GetBaseThresholdArguments(); + $ThresholdArguments.Add('-Threshold', $Threshold); + $ThresholdArguments.Add('-IsGreaterEqual', $TRUE); + + $ThresholdObject = Compare-IcingaPluginThresholds @ThresholdArguments; + + $this.__CriticalValue = $ThresholdObject; + $this.__SetCheckState($ThresholdObject, $IcingaEnums.IcingaExitCode.Critical); + + return $this; + } + + $IcingaCheck | Add-Member -MemberType ScriptMethod -Force -Name '__ValidateThresholdInput' -Value { + if ($null -eq $this.__WarningValue -Or $null -eq $this.__CriticalValue) { + return; + } + + [bool]$OutOfRange = $FALSE; + + # Both thresholds use the mode + if ($this.__WarningValue.Threshold.Mode -eq $this.__CriticalValue.Threshold.Mode) { + + #Handles 20 + if ($this.__WarningValue.Threshold.Mode -eq $IcingaEnums.IcingaThresholdMethod.Default -And $null -ne $this.__WarningValue.Threshold.Value -And $null -ne $this.__CriticalValue.Threshold.Value) { + if ($this.__WarningValue.Threshold.Value -gt $this.__CriticalValue.Threshold.Value) { + $OutOfRange = $TRUE; + } + } + + # Handles: 30:40 + if ($this.__WarningValue.Threshold.Mode -eq $IcingaEnums.IcingaThresholdMethod.Between) { + if ($this.__WarningValue.Threshold.StartRange -lt $this.__CriticalValue.Threshold.StartRange) { + $OutOfRange = $TRUE; + } + if ($this.__WarningValue.Threshold.EndRange -gt $this.__CriticalValue.Threshold.EndRange) { + $OutOfRange = $TRUE; + } + # Handles: @30:40 + } elseif ($this.__WarningValue.Threshold.Mode -eq $IcingaEnums.IcingaThresholdMethod.Outside) { + if ($this.__WarningValue.Threshold.StartRange -ge $this.__CriticalValue.Threshold.StartRange) { + $OutOfRange = $TRUE; + } + if ($this.__WarningValue.Threshold.EndRange -le $this.__CriticalValue.Threshold.EndRange) { + $OutOfRange = $TRUE; + } + } + + # Handles: 20: + if ($this.__WarningValue.Threshold.Mode -eq $IcingaEnums.IcingaThresholdMethod.Lower -And $null -ne $this.__WarningValue.Threshold.Value -And $null -ne $this.__CriticalValue.Threshold.Value) { + if ($this.__WarningValue.Threshold.Value -lt $this.__CriticalValue.Threshold.Value) { + $OutOfRange = $TRUE; + } + } + + # Handles: ~:20 + if ($this.__WarningValue.Threshold.Mode -eq $IcingaEnums.IcingaThresholdMethod.Greater -And $null -ne $this.__WarningValue.Threshold.Value -And $null -ne $this.__CriticalValue.Threshold.Value) { + if ($this.__WarningValue.Threshold.Value -gt $this.__CriticalValue.Threshold.Value) { + $OutOfRange = $TRUE; + } + } + } else { + # Todo: Implement handling for mixed modes + } + + if ($OutOfRange) { + $this.SetUnknown([string]::Format('Warning threshold range "{0}" is greater than Critical threshold range "{1}"', $this.__WarningValue.Threshold.Threshold, $this.__CriticalValue.Threshold.Threshold), $TRUE) | Out-Null; + } + } + + $IcingaCheck.__ValidateObject(); + $IcingaCheck.__ValidateUnit(); + $IcingaCheck.__ValidateMetricsName(); + $IcingaCheck.__SetCurrentExecutionTime(); + $IcingaCheck.__AddCheckDataToCache(); + $IcingaCheck.__SetInternalTimeInterval(); + $IcingaCheck.__ConvertMinMax(); + + return $IcingaCheck; +} +function ConvertTo-IcingaPluginOutputTranslation() +{ + param ( + $Value = $null, + [hashtable]$Translation = @{ } + ); + + if ($null -eq $Value) { + return 'Nothing'; + } + + if ($null -eq $Translation -Or $Translation.Count -eq 0) { + return $Value; + } + + [array]$TranslationKeys = $Translation.Keys; + [array]$TranslationValues = $Translation.Values; + [int]$Index = 0; + [bool]$FoundTranslation = $FALSE; + + foreach ($entry in $TranslationKeys) { + if (([string]($Value)).ToLower() -eq ([string]($entry)).ToLower()) { + $FoundTranslation = $TRUE; + break; + } + $Index += 1; + } + + if ($FoundTranslation -eq $FALSE) { + return $Value; + } + + return $TranslationValues[$Index]; +} +function Get-IcingaMetricsOverTimePerfData() +{ + param ( + [switch]$AddWhiteSpace = $FALSE + ); + + [string]$MetricsOverTime = ''; + [bool]$IsDaemonWorker = $FALSE; + + if ([string]::IsNullOrEmpty($Global:Icinga.Private.Scheduler.PerfDataWriter.MetricsOverTime) -eq $FALSE) { + if ($AddWhiteSpace) { + return (' ' + $Global:Icinga.Private.Scheduler.PerfDataWriter.MetricsOverTime); + } + + return $Global:Icinga.Private.Scheduler.PerfDataWriter.MetricsOverTime; + } + + if ($Global:Icinga.Public.Daemons.ContainsKey('ServiceCheck') -And $Global:Icinga.Public.Daemons.ServiceCheck.ContainsKey('PerformanceDataCache') -And $Global:Icinga.Public.Daemons.ServiceCheck.PerformanceDataCache.ContainsKey($Global:Icinga.Private.Scheduler.CheckCommand)) { + $IsDaemonWorker = $TRUE; + } + + if ($IsDaemonWorker) { + $MetricsOverTime = $Global:Icinga.Public.Daemons.ServiceCheck.PerformanceDataCache[$Global:Icinga.Private.Scheduler.CheckCommand]; + } else { + $PerformanceLabelFile = Join-Path -Path (Join-Path -Path (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'service_check_cache') -ChildPath 'performance_labels') -ChildPath ([string]::Format('{0}.db', $Global:Icinga.Private.Scheduler.CheckCommand)); + if (Test-Path -Path $PerformanceLabelFile) { + $MetricsOverTime = Get-Content -Path $PerformanceLabelFile -Raw; + } + } + + $Global:Icinga.Private.Scheduler.PerfDataWriter.MetricsOverTime = $MetricsOverTime; + + if ([string]::IsNullOrEmpty($MetricsOverTime) -eq $FALSE -And $AddWhiteSpace) { + $MetricsOverTime = ' ' + $MetricsOverTime; + } + + return $MetricsOverTime; +} +<# +.SYNOPSIS + Compares a value to a threshold and returns a result. + +.DESCRIPTION + The Compare-IcingaPluginValueToThreshold function compares a value to a threshold and returns a result indicating whether the value meets the threshold criteria. It supports various threshold methods such as default, lower, lower equal, greater, greater equal, between, outside, matches, and not matches. The function also handles percentage values and provides human-readable output. + +.PARAMETER Value + The value to compare against the threshold. + +.PARAMETER BaseValue + The base value used for percentage calculations. + +.PARAMETER Unit + The unit of measurement for the value. + +.PARAMETER Translation + The translation table for converting values to human-readable format. + +.PARAMETER Threshold + The threshold object containing the threshold criteria. + +.PARAMETER OverrideMode + The override mode for the threshold. + +.OUTPUTS + A hashtable containing the following properties: + - Message: The result message indicating whether the value meets the threshold criteria. + - IsOk: A boolean value indicating whether the value meets the threshold criteria. + - HasError: A boolean value indicating whether an error occurred during the comparison. + +.EXAMPLE + $threshold = @{ + EndRange = $null; + Unit = 'B'; + StartRange = $null; + Threshold = '30000000MB'; + Mode = 0; + Raw = '30MB'; + IsDateTime = $FALSE; + Value = 30000000; + } + Compare-IcingaPluginValueToThreshold -Value 450000000 -Unit 'B' -Threshold $threshold + + This example compares the value 15 to the threshold criteria specified in the $threshold object. The function returns a hashtable with the result message, IsOk, and HasError properties. + +.NOTES + This function is part of the Icinga PowerShell Framework module. + +.LINK + https://github.com/icinga/icinga-powershell-framework + +#> +function Compare-IcingaPluginValueToThreshold() +{ + param ( + $Value = $null, + $BaseValue = $null, + $Unit = $null, + $CheckUnit = $null, + $Translation = $null, + $Threshold = $null, + $OverrideMode = $null, + $MetricsOverTime = $null + ); + + [hashtable]$RetValue = @{ + 'Message' = ''; + 'IsOk' = $FALSE; + 'HasError' = $FALSE; + } + + # This will properly handle metrics over time + $MoTObject = ConvertTo-IcingaMetricsOverTime -MetricsOverTime $MetricsOverTime; + $OriginalValue = $Value; + if ($MoTObject.Error -eq $FALSE) { + if ($MoTObject.Apply) { + $Value = $MoTObject.Value; + } + } else { + $RetValue.Message = $MoTObject.Message; + $RetValue.HasError = $TRUE; + + return $RetValue; + } + + # The MoT message is by default empty and will do nothing. In case we use checks for + # Metrics over Time, this will return something like "(15m Avg.)" and expand it to the + # final output message + $MoTMessage = $MoTObject.Message; + $HumanReadableValue = $Value; + $PercentValue = $null; + $TranslatedValue = $Value; + [bool]$UsePercent = $FALSE; + + # Otherwise just convert the values to human readble values + $HumanReadableValue = ConvertTo-IcingaPluginOutputTranslation -Translation $Translation -Value $HumanReadableValue; + $HumanReadableValue = Convert-IcingaPluginValueToString -Value $HumanReadableValue -Unit $Unit -OriginalUnit $CheckUnit; + + if ($null -eq $Value -Or $null -eq $Threshold -Or [string]::IsNullOrEmpty($Threshold.Raw)) { + $RetValue.Message = $HumanReadableValue; + $RetValue.IsOk = $TRUE; + + return $RetValue; + } + + if (Test-Numeric $Value) { + [decimal]$Value = $Value; + } + + if ($null -eq $OverrideMode) { + $OverrideMode = $Threshold.Mode; + } + + if ($Threshold.Unit -eq '%' -And $null -eq $BaseValue) { + $RetValue.Message = 'This plugin threshold does not support percentage units'; + $RetValue.HasError = $TRUE; + + return $RetValue; + } + + # In case we have a percentage value, we need to adjust the value + if ($Threshold.Unit -eq '%' -And $null -ne $BaseValue -And $BaseValue -ne 0) { + $UsePercent = $TRUE; + $HumanReadableValue = Convert-IcingaPluginValueToString -Value $Value -BaseValue $BaseValue -Unit $Threshold.Unit -OriginalUnit $Unit -UsePercent:$UsePercent; + $Value = [math]::Round(($Value / $BaseValue) * 100, 2); + # Ensure that we properly set the threshold range or metrics we defined from percent to acutal values based on the BaseValue + # Otherwise performance metrics will not be properly reported, causing later issues for visualiation tools like Grafana + $Threshold = Convert-IcingaPluginThresholdsFromPercent -BaseValue $BaseValue -Threshold $Threshold; + } else { + $TranslatedValue = ConvertTo-IcingaPluginOutputTranslation -Translation $Translation -Value $Value; + } + + switch ($OverrideMode) { + $IcingaEnums.IcingaThresholdMethod.Default { + if ($Value -lt 0 -Or $Value -gt $Threshold.Value) { + if ($Value -lt 0) { + $RetValue.Message = [string]::Format('Value {0} is lower than 0{1}', $HumanReadableValue, $MoTMessage); + return $RetValue; + } + + if ($Value -gt $Threshold.Value) { + $RetValue.Message = [string]::Format('Value {0} is greater than threshold {1}{2}', $HumanReadableValue, (Convert-IcingaPluginValueToString -Value $Threshold.Value -BaseValue $BaseValue -Unit $Threshold.Unit -OriginalUnit $CheckUnit -UsePercent:$UsePercent -IsThreshold), $MoTMessage); + return $RetValue; + } + } + break; + }; + $IcingaEnums.IcingaThresholdMethod.Lower { + if ($Value -lt $Threshold.Value) { + $RetValue.Message = [string]::Format('Value {0} is lower than threshold {1}{2}', $HumanReadableValue, (Convert-IcingaPluginValueToString -Value $Threshold.Value -BaseValue $BaseValue -Unit $Threshold.Unit -OriginalUnit $CheckUnit -UsePercent:$UsePercent -IsThreshold), $MoTMessage); + return $RetValue; + } + break; + }; + $IcingaEnums.IcingaThresholdMethod.LowerEqual { + if ($Value -le $Threshold.Value) { + $RetValue.Message = [string]::Format('Value {0} is lower or equal than threshold {1}{2}', $HumanReadableValue, (Convert-IcingaPluginValueToString -Value $Threshold.Value -BaseValue $BaseValue -Unit $Threshold.Unit -OriginalUnit $CheckUnit -UsePercent:$UsePercent -IsThreshold), $MoTMessage); + return $RetValue; + } + break; + }; + $IcingaEnums.IcingaThresholdMethod.Greater { + if ($Value -gt $Threshold.Value) { + $RetValue.Message = [string]::Format('Value {0} is greater than threshold {1}{2}', $HumanReadableValue, (Convert-IcingaPluginValueToString -Value $Threshold.Value -BaseValue $BaseValue -Unit $Threshold.Unit -OriginalUnit $CheckUnit -UsePercent:$UsePercent -IsThreshold), $MoTMessage); + return $RetValue; + } + break; + }; + $IcingaEnums.IcingaThresholdMethod.GreaterEqual { + if ($Value -gt $Threshold.Value) { + $RetValue.Message = [string]::Format('Value {0} is greater or equal than threshold {1}{2}', $HumanReadableValue, (Convert-IcingaPluginValueToString -Value $Threshold.Value -BaseValue $BaseValue -Unit $Threshold.Unit -OriginalUnit $CheckUnit -UsePercent:$UsePercent -IsThreshold), $MoTMessage); + return $RetValue; + } + break; + }; + $IcingaEnums.IcingaThresholdMethod.Between { + if ($Value -lt $Threshold.StartRange -Or $Value -gt $Threshold.EndRange) { + $RetValue.Message = [string]::Format('Value {0} is not between thresholds <{1} or >{2}{3}', $HumanReadableValue, (Convert-IcingaPluginValueToString -Value $Threshold.StartRange -BaseValue $BaseValue -Unit $Threshold.Unit -OriginalUnit $CheckUnit -UsePercent:$UsePercent -IsThreshold), (Convert-IcingaPluginValueToString -Value $Threshold.EndRange -BaseValue $BaseValue -Unit $Threshold.Unit -OriginalUnit $Unit -UsePercent:$UsePercent -IsThreshold), $MoTMessage); + return $RetValue; + } + break; + }; + $IcingaEnums.IcingaThresholdMethod.Outside { + if ($Value -ge $Threshold.StartRange -And $Value -le $Threshold.EndRange) { + $RetValue.Message = [string]::Format('Value {0} is between thresholds >={1} and <={2}{3}', $HumanReadableValue, (Convert-IcingaPluginValueToString -Value $Threshold.StartRange -BaseValue $BaseValue -Unit $Threshold.Unit -OriginalUnit $CheckUnit -UsePercent:$UsePercent -IsThreshold), (Convert-IcingaPluginValueToString -Value $Threshold.EndRange -BaseValue $BaseValue -Unit $Threshold.Unit -OriginalUnit $Unit -UsePercent:$UsePercent -IsThreshold), $MoTMessage); + return $RetValue; + } + break; + }; + $IcingaEnums.IcingaThresholdMethod.Matches { + if ($Value -Like $Threshold.Value ) { + $RetValue.Message = [string]::Format('Value {0} is matching threshold {1}{2}', $TranslatedValue, (ConvertTo-IcingaPluginOutputTranslation -Translation $Translation -Value $Threshold.Value), $MoTMessage); + return $RetValue; + } + break; + }; + $IcingaEnums.IcingaThresholdMethod.NotMatches { + if ($Value -NotLike $Threshold.Value ) { + $RetValue.Message = [string]::Format('Value {0} is not matching threshold {1}{2}', $TranslatedValue, (ConvertTo-IcingaPluginOutputTranslation -Translation $Translation -Value $Threshold.Value), $MoTMessage); + return $RetValue; + } + break; + }; + } + + $RetValue.Message = $HumanReadableValue; + $RetValue.IsOk = $TRUE; + + return $RetValue; +} +function Set-IcingaInternalPluginExitCode() +{ + param ( + $ExitCode = 0 + ); + + # Only add the first exit code we should cover during one runtime + if ($null -eq $Global:Icinga.Private.Scheduler.ExitCode) { + $Global:Icinga.Private.Scheduler.ExitCode = $ExitCode; + } +} +function ConvertTo-IcingaMetricsOverTime() +{ + param ( + $MetricsOverTime = $null + ); + + $MoTObj = @{ + 'Value' = $null; + 'Message' = ''; + 'Apply' = $FALSE; + 'Error' = $FALSE; + } + + if ($MetricsOverTime -eq $null) { + return $MoTObj; + } + + if ([string]::IsNullOrEmpty($MetricsOverTime.Interval)) { + return $MoTObj; + } + + try { + [int]$IntervalInSeconds = ConvertTo-Seconds -Value $MetricsOverTime.Interval; + $MoTPerfData = Get-IcingaMetricsOverTimePerfData; + [array]$MoTToArray = $MoTPerfData.Split(' '); + [string]$SearchLabel = [string]::Format('{0}::Interval{1}', $MetricsOverTime.Label, $IntervalInSeconds); + [hashtable]$AvailableMoT = @{ }; + + foreach ($mot in $MoTToArray) { + if ([string]::IsNullOrEmpty($mot) -Or $mot.Contains('=') -eq $FALSE) { + continue; + } + + $MoTPerfData = $mot.Split('='); + $TimeIndexName = [string]::Format('{0}s', $MoTPerfData[0].Split('::')[-1].Replace('Interval', '')); + + if ($AvailableMoT.ContainsKey($TimeIndexName) -eq $FALSE) { + $AvailableMoT.Add($TimeIndexName, $TRUE); + } + + if ($MoTPerfData[0] -eq $SearchLabel) { + $MoTObj.Value = $MoTPerfData[1]; + $MoTObj.Apply = $TRUE; + $MoTObj.Message = [string]::Format(' ({0} Avg.)', $MetricsOverTime.Interval); + break; + } + } + + if ($MoTObj.Apply -eq $FALSE) { + $MoTObj.Message = [string]::Format('[Failed to parse metrics over time with -ThresholdInterval "{0}": No data found matching the requested time index. Available indexes: [{1}]]', $MetricsOverTime.Interval, ($AvailableMoT.Keys -Join ', ')); + $MoTObj.Error = $TRUE; + } + } catch { + $MoTObj.Message = [string]::Format('[Failed to parse metrics over time with -ThresholdInterval "{0}": {1}]', $MetricsOverTime.Interval, $_.Exception.Message); + $MoTObj.Error = $TRUE; + } + + return $MoTObj; +} +function Write-IcingaPluginPerfData() +{ + # We shouldn't write all Metrics over Time to Icinga, as this will just cause a massive + # overload of Performance Metrics written and processed. We leave this code for now + # allowing us to enable it later again or make it user configurable + #[string]$MetricsOverTime = Get-IcingaMetricsOverTimePerfData -AddWhiteSpace; + [string]$MetricsOverTime = ''; + + if ($Global:Icinga.Protected.RunAsDaemon -eq $FALSE -And $Global:Icinga.Protected.JEAContext -eq $FALSE) { + if ($Global:Icinga.Private.Scheduler.PerfDataWriter.Storage.Length -ne 0) { + Write-IcingaConsolePlain ([string]::Format('| {0}{1}', ($Global:Icinga.Private.Scheduler.PerfDataWriter.Storage.ToString()), $MetricsOverTime)); + } + } else { + $Global:Icinga.Private.Scheduler.PerformanceData = $Global:Icinga.Private.Scheduler.PerfDataWriter.Storage.ToString() + $MetricsOverTime; + } + + # Ensure we clear our cache after writing the data + $Global:Icinga.Private.Scheduler.PerfDataWriter.Cache.Clear() | Out-Null; + $Global:Icinga.Private.Scheduler.PerfDataWriter.Storage.Clear() | Out-Null; +} +<# +.SYNOPSIS + Converts Icinga plugin thresholds from percentage to actual values. + +.DESCRIPTION + The Convert-IcingaPluginThresholdsFromPercent function takes a base value and a threshold object and converts the threshold values from percentage to actual values. It supports various threshold modes such as Default, Lower, LowerEqual, Greater, GreaterEqual, Between, Outside, Matches, and NotMatches. + +.PARAMETER BaseValue + The base value used for calculating the threshold values. + +.PARAMETER Threshold + The threshold object containing the threshold mode and value. + +.OUTPUTS + The modified threshold object with the converted threshold values. + +.EXAMPLE + $baseValue = 274112400000 + $threshold = @{ + EndRange = 90; + Unit = '%'; + StartRange = 30; + Threshold = '@30:90'; + Mode = 6; + Raw = '@30%:90%'; + IsDateTime = $FALSE; + Value = $null; + } + + Convert-IcingaPluginThresholdsFromPercent -BaseValue $baseValue -Threshold $threshold + + This example converts the threshold value of 50% to the actual value based on the provided base value of 100. + +.NOTES + +#> +function Convert-IcingaPluginThresholdsFromPercent() +{ + param ( + $BaseValue = $null, + $Threshold = $null + ); + + if ($null -eq $Threshold -Or $null -eq $BaseValue -Or $BaseValue -eq 0) { + return $Threshold; + } + + switch ($Threshold.Mode) { + $IcingaEnums.IcingaThresholdMethod.Default { + $Threshold.Threshold = [math]::Round($BaseValue / 100 * $Threshold.Value, 0); + break; + }; + $IcingaEnums.IcingaThresholdMethod.Lower { + $Threshold.Threshold = [math]::Round($BaseValue / 100 * $Threshold.Value, 0); + break; + }; + $IcingaEnums.IcingaThresholdMethod.LowerEqual { + $Threshold.Threshold = [math]::Round($BaseValue / 100 * $Threshold.Value, 0); + break; + }; + $IcingaEnums.IcingaThresholdMethod.Greater { + $Threshold.Threshold = [math]::Round($BaseValue / 100 * $Threshold.Value, 0); + break; + }; + $IcingaEnums.IcingaThresholdMethod.GreaterEqual { + $Threshold.Threshold = [math]::Round($BaseValue / 100 * $Threshold.Value, 0); + break; + }; + $IcingaEnums.IcingaThresholdMethod.Between { + $StartRange = [math]::Round($BaseValue / 100 * $Threshold.StartRange, 0); + $EndRange = [math]::Round($BaseValue / 100 * $Threshold.EndRange, 0); + + $Threshold.Threshold = [string]::Format('{0}:{1}', $StartRange, $EndRange); + break; + }; + $IcingaEnums.IcingaThresholdMethod.Outside { + $StartRange = [math]::Round($BaseValue / 100 * $Threshold.StartRange, 0); + $EndRange = [math]::Round($BaseValue / 100 * $Threshold.EndRange, 0); + + $Threshold.Threshold = [string]::Format('@{0}:{1}', $StartRange, $EndRange); + break; + }; + $IcingaEnums.IcingaThresholdMethod.Matches { + break; + }; + $IcingaEnums.IcingaThresholdMethod.NotMatches { + break; + }; + } + + return $Threshold; +} +<# +.SYNOPSIS + Compares a value against specified thresholds and returns the result. + +.DESCRIPTION + The Compare-IcingaPluginThresholds function compares a given value against specified thresholds and returns an object containing the comparison result. It supports various comparison methods such as Matches, NotMatches, Between, LowerEqual, and GreaterEqual. If an error occurs during the comparison, the function returns an object with the error details. + +.PARAMETER Threshold + Specifies the threshold value or range to compare against. This can be a single value or a range specified in the format "min:max". If not provided, the function assumes no threshold. + +.PARAMETER InputValue + Specifies the value to compare against the threshold. + +.PARAMETER BaseValue + Specifies the base value for percentage calculations. If the unit is set to '%', the base value is used to calculate the percentage. If not provided, the function assumes no base value. + +.PARAMETER Matches + Indicates that the comparison should use the Matches method. This method checks if the input value matches the threshold. + +.PARAMETER NotMatches + Indicates that the comparison should use the NotMatches method. This method checks if the input value does not match the threshold. + +.PARAMETER DateTime + Specifies whether the input value is a DateTime object. If set to $true, the input value is treated as a DateTime object. + +.PARAMETER Unit + Specifies the unit of measurement for the values. This is used for percentage calculations. If not provided, the function assumes no unit. + +.PARAMETER ThresholdCache + Specifies a cache object to store threshold values for reuse. + +.PARAMETER CheckName + Specifies the name of the check being performed. + +.PARAMETER PerfDataLabel + Specifies the label for the performance data. + +.PARAMETER Translation + Specifies a hashtable for translating threshold values. + +.PARAMETER Minium + Specifies the minimum threshold value for comparison. This is used when the IsBetween method is used. + +.PARAMETER Maximum + Specifies the maximum threshold value for comparison. This is used when the IsBetween method is used. + +.PARAMETER IsBetween + Indicates that the comparison should use the IsBetween method. This method checks if the input value is between the minimum and maximum thresholds. + +.PARAMETER IsLowerEqual + Indicates that the comparison should use the IsLowerEqual method. This method checks if the input value is lower than or equal to the threshold. + +.PARAMETER IsGreaterEqual + Indicates that the comparison should use the IsGreaterEqual method. This method checks if the input value is greater than or equal to the threshold. + +.PARAMETER TimeInterval + Specifies the time interval for the comparison. This is used when the input value is a DateTime object. + +.OUTPUTS + The function returns an object containing the comparison result. The object has the following properties: + - Value: The input value. + - Unit: The unit of measurement. + - Message: The result message of the comparison. + - IsOK: Indicates whether the comparison result is OK. + - HasError: Indicates whether an error occurred during the comparison. + - Threshold: The threshold value or range used for comparison. + - Minimum: The minimum threshold value used for comparison. + - Maximum: The maximum threshold value used for comparison. + +.EXAMPLE + $threshold = "10:20" + $inputValue = 15 + $baseValue = 100 + $result = Compare-IcingaPluginThresholds -Threshold $threshold -InputValue $inputValue -BaseValue $baseValue + $result.IsOK + # Returns $true + +.NOTES + This function is part of the Icinga PowerShell Framework module. +#> +function Compare-IcingaPluginThresholds() +{ + param ( + [string]$Threshold = $null, + $InputValue = $null, + $BaseValue = $null, + [switch]$Matches = $FALSE, + [switch]$NotMatches = $FALSE, + [switch]$DateTime = $FALSE, + [string]$Unit = '', + $ThresholdCache = $null, + [string]$CheckName = '', + [string]$PerfDataLabel = '', + [hashtable]$Translation = @{ }, + $Minium = $null, + $Maximum = $null, + [switch]$IsBetween = $FALSE, + [switch]$IsLowerEqual = $FALSE, + [switch]$IsGreaterEqual = $FALSE, + [string]$TimeInterval = $null, + [switch]$NoPerfData = $FALSE + ); + + try { + # Fix possible numeric value comparison issues + $TestInput = Test-IcingaDecimal $InputValue; + $BaseInput = Test-IcingaDecimal $BaseValue; + $MoTData = @{ + 'Label' = $PerfDataLabel; + 'Interval' = $TimeInterval; + }; + + # Ensure we do not include our checks for which we do not write any performance data + # Metrics over time will not work for those, as the metrics are not stored. + # There just set the variable to null which means they won't be processed + if ($NoPerfData) { + $MoTData = $null; + } + + if ($TestInput.Decimal) { + [decimal]$InputValue = [decimal]$TestInput.Value; + } + if ($BaseInput.Decimal) { + [decimal]$BaseValue = [decimal]$BaseInput.Value; + } + + $IcingaThresholds = New-Object -TypeName PSObject; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'Value' -Value $InputValue; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'Unit' -Value $Unit; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'Message' -Value ''; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'IsOK' -Value $FALSE; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'HasError' -Value $FALSE; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'Threshold' -Value (Convert-IcingaPluginThresholds -Threshold $Threshold); + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'Minimum' -Value (Convert-IcingaPluginThresholds -Threshold $Minium); + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'Maximum' -Value (Convert-IcingaPluginThresholds -Threshold $Maximum); + + if ($TestInput.Decimal) { + $ConvertedValue = Convert-IcingaPluginThresholds -Threshold ([string]::Format('{0}{1}', $InputValue, $Unit)); + + if ((Test-IcingaDecimal -Value $ConvertedValue.Value).Decimal) { + $InputValue = [decimal]$ConvertedValue.Value; + $IcingaThresholds.Value = [decimal]$ConvertedValue.Value; + $IcingaThresholds.Unit = $ConvertedValue.Unit; + } + } + + if ($BaseInput.Decimal) { + $ConvertedBaseValue = Convert-IcingaPluginThresholds -Threshold ([string]::Format('{0}{1}', $BaseValue, $Unit)); + + if ((Test-IcingaDecimal -Value $ConvertedBaseValue.Value).Decimal) { + $BaseValue = [decimal]$ConvertedBaseValue.Value; + } + } + + # In case we are using % values, we should set the BaseValue always to 100 + if ($Unit -eq '%' -And $null -eq $BaseValue) { + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'BaseValue' -Value 100; + } else { + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'BaseValue' -Value $BaseValue; + } + + $CheckResult = $null; + + if ($Matches) { + $CheckResult = Compare-IcingaPluginValueToThreshold -Value $InputValue -BaseValue $IcingaThresholds.BaseValue -Threshold $IcingaThresholds.Threshold -Unit $IcingaThresholds.Unit -CheckUnit $Unit -Translation $Translation -OverrideMode $IcingaEnums.IcingaThresholdMethod.Matches -MetricsOverTime $MoTData; + } elseif ($NotMatches) { + $CheckResult = Compare-IcingaPluginValueToThreshold -Value $InputValue -BaseValue $IcingaThresholds.BaseValue -Threshold $IcingaThresholds.Threshold -Unit $IcingaThresholds.Unit -CheckUnit $Unit -Translation $Translation -OverrideMode $IcingaEnums.IcingaThresholdMethod.NotMatches -MetricsOverTime $MoTData; + } elseif ($IsBetween) { + $CheckResult = Compare-IcingaPluginValueToThreshold -Value $InputValue -BaseValue $IcingaThresholds.BaseValue -Threshold $IcingaThresholds.Threshold -Unit $IcingaThresholds.Unit -CheckUnit $Unit -Translation $Translation -OverrideMode $IcingaEnums.IcingaThresholdMethod.Between -MetricsOverTime $MoTData; + } elseif ($IsLowerEqual) { + $CheckResult = Compare-IcingaPluginValueToThreshold -Value $InputValue -BaseValue $IcingaThresholds.BaseValue -Threshold $IcingaThresholds.Threshold -Unit $IcingaThresholds.Unit -CheckUnit $Unit -Translation $Translation -OverrideMode $IcingaEnums.IcingaThresholdMethod.LowerEqual -MetricsOverTime $MoTData; + } elseif ($IsGreaterEqual) { + $CheckResult = Compare-IcingaPluginValueToThreshold -Value $InputValue -BaseValue $IcingaThresholds.BaseValue -Threshold $IcingaThresholds.Threshold -Unit $IcingaThresholds.Unit -CheckUnit $Unit -Translation $Translation -OverrideMode $IcingaEnums.IcingaThresholdMethod.GreaterEqual -MetricsOverTime $MoTData; + } else { + $CheckResult = Compare-IcingaPluginValueToThreshold -Value $InputValue -BaseValue $IcingaThresholds.BaseValue -Threshold $IcingaThresholds.Threshold -Unit $IcingaThresholds.Unit -CheckUnit $Unit -Translation $Translation -MetricsOverTime $MoTData; + } + + $IcingaThresholds.Message = $CheckResult.Message; + $IcingaThresholds.IsOK = $CheckResult.IsOK; + $IcingaThresholds.HasError = $CheckResult.HasError; + + return $IcingaThresholds; + } catch { + $IcingaThresholds = New-Object -TypeName PSObject; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'Value' -Value $InputValue; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'Unit' -Value $Unit; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'Message' -Value $_.Exception.Message; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'IsOK' -Value $FALSE; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'HasError' -Value $TRUE; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'Threshold' -Value $Threshold; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'Minimum' -Value $Minium; + $IcingaThresholds | Add-Member -MemberType NoteProperty -Name 'Maximum' -Value $Maximum; + + return $IcingaThresholds; + } + + return $null; +} +function Exit-IcingaExecutePlugin() +{ + param ( + [string]$Command = '' + ); + + # We need to fix the argument encoding hell + [hashtable]$ConvertedArgs = ConvertTo-IcingaPowerShellArguments -Command $Command -Arguments $args; + [string]$JEAProfile = Get-IcingaJEAContext; + [bool]$CheckByIcingaForWindows = $FALSE; + [bool]$CheckByJEAShell = $FALSE; + + if ($args -Contains '-IcingaForWindowsRemoteExecution') { + $CheckByIcingaForWindows = $TRUE; + } + if ($args -Contains '-IcingaForWindowsJEARemoteExecution') { + $CheckByJEAShell = $TRUE; + } + + # We use the plugin check_by_icingaforwindows.ps1 to execute + # checks from a Linux/Windows remote source + if ($CheckByIcingaForWindows) { + # First try to queue the check over the REST-Api + $CheckResult = Invoke-IcingaInternalServiceCall -Command $Command -Arguments $ConvertedArgs -NoExit; + + if ($null -ne $CheckResult) { + # Seems we got a result + Write-IcingaConsolePlain -Message $CheckResult; + + # Do not close the session, we need to read the ExitCode from Get-IcingaInternalPluginExitCode + # The plugin itself will terminate the session + return; + } + + # We couldn't use our Rest-Api and Api-Checks feature, then lets execute the plugin locally + # Set daemon true, because this will change internal handling for errors and plugin output + $Global:Icinga.Protected.RunAsDaemon = $TRUE; + + try { + # Execute our plugin + (& $Command @ConvertedArgs) | Out-Null; + } catch { + # Handle errors within our plugins + # If anything goes wrong handle the error very detailed + + $Global:Icinga.Protected.RunAsDaemon = $FALSE; + Write-IcingaExecutePluginException -Command $Command -ErrorObject $_ -Arguments $ConvertedArgs; + $ConvertedArgs.Clear(); + + # Do not close the session, we need to read the ExitCode from Get-IcingaInternalPluginExitCode + # The plugin itself will terminate the session + return; + } + + # Disable it again - we need to write data to our shell now. Not very intuitive, but it is the easiest + # solution to do it this way + $Global:Icinga.Protected.RunAsDaemon = $FALSE; + + # Now print the result to shell + Write-IcingaPluginResult -PluginOutput (Get-IcingaInternalPluginOutput) -PluginPerfData (Get-IcingaCheckSchedulerPerfData); + + # Do not close the session, we need to read the ExitCode from Get-IcingaInternalPluginExitCode + # The plugin itself will terminate the session + return; + } + + # Regardless of JEA enabled or disabled, forward all checks to the internal API + # and check if we get a result from there + Invoke-IcingaInternalServiceCall -Command $Command -Arguments $ConvertedArgs; + + try { + # If the plugin is not installed, throw a good exception + Exit-IcingaPluginNotInstalled -Command $Command; + + # In case we have JEA enabled on our system, this shell currently open most likely has no + # JEA configuration installed. This is because a JEA shell will not return an exit code and + # Icinga relies on that. Therefor we will try to open a new PowerShell with the JEA configuration + # assigned for Icinga for Windows, execute the plugins there and return the result + if ([string]::IsNullOrEmpty($JEAProfile) -eq $FALSE) { + $ErrorHandler = '' + $JEARun = ( + & powershell.exe -ConfigurationName $JEAProfile -NoLogo -NoProfile -Command { + # Load Icinga for Windows + Use-Icinga; + + # Enable our JEA context + $Global:Icinga.Protected.JEAContext = $TRUE; + + # Parse the arguments our previous shell received + $Command = $args[0]; + $Arguments = $args[1]; + $Output = ''; + + try { + # Try executing our checks, store the exit code and plugin output + $ExitCode = (& $Command @Arguments); + $Output = (Get-IcingaInternalPluginOutput); + $ExitCode = (Get-IcingaInternalPluginExitCode); + } catch { + # If we failed for some reason, print a detailed error and use exit code 3 to mark the check as unkown + $Output = [string]::Format('[UNKNOWN] Icinga Exception: Error while executing plugin in JEA context{0}{0}{1}', (New-IcingaNewLine), $_.Exception.Message); + $ExitCode = 3; + } + + # Return the result to our main PowerShell + return @{ + 'Output' = $Output; + 'PerfData' = (Get-IcingaCheckSchedulerPerfData) + 'ExitCode' = $ExitCode; + } + } -args $Command, $ConvertedArgs + ) 2>$ErrorHandler; + + # If we have an exit code larger or equal 0, the execution inside the JEA shell was successfully and we can share the result + # In case we had an error inside the JEA shell, it will returned here as well + if ($LASTEXITCODE -ge 0) { + Write-IcingaPluginResult -PluginOutput $JEARun.Output -PluginPerfData $JEARun.PerfData; + exit $JEARun.ExitCode; + } else { + # If for some reason the PowerShell could not be started within JEA context, we can throw an exception with exit code 3 + # to mark the check as unknown including our error message + Write-IcingaConsolePlain '[UNKNOWN] Icinga Exception: Unable to start the PowerShell.exe with the provided JEA profile "{0}" for CheckCommand: {1}' -Objects $JEAProfile, $Command; + exit 3; + } + } else { + # If we simply run the check without JEA context or from remote, we can just execute the plugin and + # exit with the exit code received from the result + exit (& $Command @ConvertedArgs); + } + } catch { + # If anything goes wrong handle the error + Write-IcingaExecutePluginException -Command $Command -ErrorObject $_ -Arguments $ConvertedArgs; + $ConvertedArgs.Clear(); + + exit 3; + } +} +function Get-IcingaThresholdCache() +{ + param ( + [string]$CheckCommand = $null + ); + + if ([string]::IsNullOrEmpty($CheckCommand)) { + return $null; + } + + if ($Global:Icinga.Private.Scheduler.ThresholdCache.ContainsKey($CheckCommand) -eq $FALSE) { + return $null; + } + + return $Global:Icinga.Private.Scheduler.ThresholdCache[$CheckCommand]; +} +function New-IcingaCheckPackage() +{ + param ( + [string]$Name = '', + [switch]$OperatorAnd = $FALSE, + [switch]$OperatorOr = $FALSE, + [switch]$OperatorNone = $FALSE, + [int]$OperatorMin = -1, + [int]$OperatorMax = -1, + [array]$Checks = @(), + [int]$Verbose = 0, + [switch]$IgnoreEmptyPackage = $FALSE, + [switch]$Hidden = $FALSE, + [switch]$AddSummaryHeader = $FALSE + ); + + $IcingaCheckPackage = New-IcingaCheckBaseObject; + + $IcingaCheckPackage.Name = $Name; + $IcingaCheckPackage.__ObjectType = 'IcingaCheckPackage'; + $IcingaCheckPackage.__SetHidden($Hidden); + $IcingaCheckPackage.__SetVerbosity($Verbose); + + $IcingaCheckPackage | Add-Member -MemberType NoteProperty -Name 'OperatorAnd' -Value $OperatorAnd; + $IcingaCheckPackage | Add-Member -MemberType NoteProperty -Name 'OperatorOr' -Value $OperatorOr; + $IcingaCheckPackage | Add-Member -MemberType NoteProperty -Name 'OperatorNone' -Value $OperatorNone; + $IcingaCheckPackage | Add-Member -MemberType NoteProperty -Name 'OperatorMin' -Value $OperatorMin; + $IcingaCheckPackage | Add-Member -MemberType NoteProperty -Name 'OperatorMax' -Value $OperatorMax; + $IcingaCheckPackage | Add-Member -MemberType NoteProperty -Name 'IgnoreEmptyPackage' -Value $IgnoreEmptyPackage; + $IcingaCheckPackage | Add-Member -MemberType NoteProperty -Name 'AddSummaryHeader' -Value $AddSummaryHeader; + $IcingaCheckPackage | Add-Member -MemberType NoteProperty -Name '__Checks' -Value @(); + $IcingaCheckPackage | Add-Member -MemberType NoteProperty -Name '__OkChecks' -Value @(); + $IcingaCheckPackage | Add-Member -MemberType NoteProperty -Name '__WarningChecks' -Value @(); + $IcingaCheckPackage | Add-Member -MemberType NoteProperty -Name '__CriticalChecks' -Value @(); + $IcingaCheckPackage | Add-Member -MemberType NoteProperty -Name '__UnknownChecks' -Value @(); + + $IcingaCheckPackage | Add-Member -MemberType ScriptMethod -Name 'ValidateOperators' -Value { + if ($this.OperatorMin -ne -1) { + return; + } + + if ($this.OperatorMax -ne -1) { + return; + } + + if ($this.OperatorNone -ne $FALSE) { + return; + } + + if ($this.OperatorOr -ne $FALSE) { + return; + } + + # If no operator is set, use And as default + $this.OperatorAnd = $TRUE; + } + + $IcingaCheckPackage | Add-Member -MemberType ScriptMethod -Name 'AddCheck' -Value { + param([array]$Checks); + + if ($null -eq $Checks -Or $Checks.Count -eq 0) { + return; + } + + foreach ($check in $Checks) { + $check.__SetIndention($this.__NewIndention()); + $check.__SetCheckOutput(); + $check.__SetVerbosity($this.__GetVerbosity()); + $check.__SetParent($this); + [array]$this.__Checks += $check; + } + } + + # Override shared function + $IcingaCheckPackage | Add-Member -MemberType ScriptMethod -Force -Name '__SetIndention' -Value { + param ($Indention); + + $this.__Indention = $Indention; + + foreach ($check in $this.__Checks) { + $check.__SetIndention($this.__NewIndention()); + } + } + + $IcingaCheckPackage | Add-Member -MemberType ScriptMethod -Name '__SetCheckState' -Value { + param ($State); + + if ($this.__GetCheckState() -lt $State) { + $this.__CheckState = $State; + $this.__SetCheckOutput(); + } + } + + # Override shared function + $IcingaCheckPackage | Add-Member -MemberType ScriptMethod -Force -Name '__SetCheckOutput' -Value { + param ($PluginOutput); + + $UnknownChecks = ''; + $CriticalChecks = ''; + $WarningChecks = ''; + $CheckSummary = New-Object -TypeName 'System.Text.StringBuilder'; + [bool]$HasContent = $FALSE; + + # Only apply this to the top parent package + if ($this.__GetIndention() -eq 0) { + if ($this.__UnknownChecks.Count -ne 0) { + $UnknownChecks = [string]::Format(' [UNKNOWN] {0}', ([string]::Join(', ', $this.__UnknownChecks))); + $CheckSummary.Append( + [string]::Format(' {0} Unknown', $this.__UnknownChecks.Count) + ) | Out-Null; + } + if ($this.__CriticalChecks.Count -ne 0) { + $CriticalChecks = [string]::Format(' [CRITICAL] {0}', ([string]::Join(', ', $this.__CriticalChecks))); + $CheckSummary.Append( + [string]::Format(' {0} Critical', $this.__CriticalChecks.Count) + ) | Out-Null; + } + if ($this.__WarningChecks.Count -ne 0) { + $WarningChecks = [string]::Format(' [WARNING] {0}', ([string]::Join(', ', $this.__WarningChecks))); + $CheckSummary.Append( + [string]::Format(' {0} Warning', $this.__WarningChecks.Count) + ) | Out-Null; + } + } + + if ($this.__OkChecks.Count -ne 0) { + $CheckSummary.Append( + [string]::Format(' {0} Ok', $this.__OkChecks.Count) + ) | Out-Null; + } + + if ($this.AddSummaryHeader -eq $FALSE) { + $CheckSummary.Clear() | Out-Null; + $CheckSummary.Append('') | Out-Null; + } elseif ($CheckSummary.Length -ne 0) { + $HasContent = $TRUE; + } + + if ([string]::IsNullOrEmpty($this.__ErrorMessage) -eq $FALSE) { + $HasContent = $TRUE; + } + + $this.__CheckOutput = [string]::Format( + '{0} {1}{2}{3}{4}{5}{6}{7}{8}', + $IcingaEnums.IcingaExitCodeText[$this.__GetCheckState()], + $this.Name, + (&{ if ($HasContent) { return ':'; } else { return ''; } }), + $CheckSummary.ToString(), + ([string]::Format('{0}{1}', (&{ if ($this.__ErrorMessage.Length -gt 1) { return ' '; } else { return ''; } }), $this.__ErrorMessage)), + $UnknownChecks, + $CriticalChecks, + $WarningChecks, + $this.__ShowPackageConfig() + ); + } + + $IcingaCheckPackage | Add-Member -Force -MemberType ScriptMethod -Name 'Compile' -Value { + $this.__OkChecks.Clear(); + $this.__WarningChecks.Clear(); + $this.__CriticalChecks.Clear(); + $this.__UnknownChecks.Clear(); + + $WorstState = $IcingaEnums.IcingaExitCode.Ok; + $BestState = $IcingaEnums.IcingaExitCode.Ok; + $NotOkChecks = 0; + $OkChecks = 0; + + if ($this.__Checks.Count -eq 0 -And $this.IgnoreEmptyPackage -eq $FALSE) { + $this.__ErrorMessage = 'No checks added to this package'; + $this.__SetCheckState($IcingaEnums.IcingaExitCode.Unknown); + $this.__SetCheckOutput(); + return; + } + + [array]$this.__Checks = ($this.__Checks | Sort-Object -Property Name); + + # Loop all checks to understand the content of result + foreach ($check in $this.__Checks) { + + $check.Compile(); + + if ($check.__IsHidden() -Or $check.__NoHeaderReport()) { + continue; + } + + if ($WorstState -lt $check.__GetCheckState()) { + $WorstState = $check.__GetCheckState(); + } + + if ($BestState -gt $check.__GetCheckState()) { + $BestState = $check.__GetCheckState(); + } + + [string]$CheckStateOutput = [string]::Format( + '{0}{1}', + $check.__GetName(), + $check.__GetHeaderOutputValue() + ); + + switch ($check.__GetCheckState()) { + $IcingaEnums.IcingaExitCode.Ok { + $this.__OkChecks += $CheckStateOutput; + $OkChecks += 1; + break; + }; + $IcingaEnums.IcingaExitCode.Warning { + $this.__WarningChecks += $CheckStateOutput; + $NotOkChecks += 1; + break; + }; + $IcingaEnums.IcingaExitCode.Critical { + $this.__CriticalChecks += $CheckStateOutput; + $NotOkChecks += 1; + break; + }; + $IcingaEnums.IcingaExitCode.Unknown { + $this.__UnknownChecks += $CheckStateOutput; + $NotOkChecks += 1; + break; + }; + } + } + + if ($this.OperatorAnd -And $NotOkChecks -ne 0) { + $this.__SetCheckState($WorstState); + } elseif ($this.OperatorOr -And $OkChecks -eq 0 ) { + $this.__SetCheckState($WorstState); + } elseif ($this.OperatorNone -And $OkChecks -ne 0 ) { + $this.__SetCheckState($WorstState); + } elseif ($this.OperatorMin -ne -1) { + if (-Not ($this.__Checks.Count -eq 0 -And $this.IgnoreEmptyPackage -eq $TRUE)) { + if ($this.OperatorMin -gt $this.__Checks.Count) { + $this.__SetCheckState($IcingaEnums.IcingaExitCode.Unknown); + $this.__ErrorMessage = [string]::Format('Minium check count ({0}) is larger than number of assigned checks ({1})', $this.OperatorMin, $this.__Checks.Count); + } elseif ($OkChecks -lt $this.OperatorMin) { + $this.__SetCheckState($WorstState); + $this.__ErrorMessage = ''; + } + } + } elseif ($this.OperatorMax -ne -1) { + if (-Not ($this.__Checks.Count -eq 0 -And $this.IgnoreEmptyPackage -eq $TRUE)) { + if ($this.OperatorMax -gt $this.__Checks.Count) { + $this.__SetCheckState($IcingaEnums.IcingaExitCode.Unknown); + $this.__ErrorMessage = [string]::Format('Maximum check count ({0}) is larger than number of assigned checks ({1})', $this.OperatorMax, $this.__Checks.Count); + } elseif ($OkChecks -gt $this.OperatorMax) { + $this.__SetCheckState($WorstState); + $this.__ErrorMessage = ''; + } + } + } + + $this.__SetCheckOutput(); + } + + # Override default behaviour from shared function + $IcingaCheckPackage | Add-Member -MemberType ScriptMethod -Force -Name '__SetVerbosity' -Value { + param ($Verbosity); + # Do nothing for check packages + } + + # Override default behaviour from shared function + $IcingaCheckPackage | Add-Member -MemberType ScriptMethod -Force -Name '__GetCheckOutput' -Value { + + if ($this.__IsHidden()) { + return '' + }; + + if ($this._CanOutput() -eq $FALSE) { + return ''; + } + + $CheckOutput = [string]::Format( + '{0}{1}', + (New-StringTree -Spacing $this.__GetIndention()), + $this.__CheckOutput + ); + + foreach ($check in $this.__Checks) { + if ($check.__IsHidden()) { + continue; + }; + + if ($check._CanOutput() -eq $FALSE) { + continue; + } + + $CheckOutput = [string]::Format( + '{0}{1}{2}', + $CheckOutput, + "`n", + $check.__GetCheckOutput() + ); + } + + return $CheckOutput; + } + + $IcingaCheckPackage | Add-Member -MemberType ScriptMethod -Force -Name '__HasNotOkChecks' -Value { + + if ($this.__WarningChecks.Count -ne 0) { + return $TRUE; + } + + if ($this.__CriticalChecks.Count -ne 0) { + return $TRUE; + } + + if ($this.__UnknownChecks.Count -ne 0) { + return $TRUE; + } + + return $FALSE; + } + + $IcingaCheckPackage | Add-Member -MemberType ScriptMethod -Force -Name '__ShowPackageConfig' -Value { + if ($this.__GetVerbosity() -lt 3) { + return ''; + } + + if ($this.OperatorAnd) { + return ' (All must be [OK])'; + } + if ($this.OperatorOr) { + return ' (Atleast one must be [OK])'; + } + if ($this.OperatorMin -ne -1) { + return ([string]::Format(' (Atleast {0} must be [OK])', $this.OperatorMin)); + } + if ($this.OperatorMax -ne -1) { + return ([string]::Format(' (Not more than {0} must be [OK])', $this.OperatorMax)); + } + + return ''; + } + + # __GetTimeSpanThreshold(0, 'Core_30_20', 'Core_30') + $IcingaCheckPackage | Add-Member -MemberType ScriptMethod -Force -Name '__GetTimeSpanThreshold' -Value { + param ($TimeSpanLabel, $Label, $MultiOutput); + + foreach ($check in $this.__Checks) { + $Result = $check.__GetTimeSpanThreshold($TimeSpanLabel, $Label, $MultiOutput); + + if ([string]::IsNullOrEmpty($Result.Interval) -eq $FALSE) { + return $Result; + } + } + + return @{ + 'Warning' = ''; + 'Critical' = ''; + 'Interval' = ''; + }; + } + + # Override shared function + $IcingaCheckPackage | Add-Member -MemberType ScriptMethod -Force -Name 'HasChecks' -Value { + if ($this.__Checks.Count -eq 0) { + return $FALSE; + } + + return $TRUE; + } + + $IcingaCheckPackage.ValidateOperators(); + $IcingaCheckPackage.AddCheck($Checks); + + return $IcingaCheckPackage; +} +function Get-IcingaInternalPluginExitCode() +{ + return $Global:Icinga.Private.Scheduler.ExitCode; +} +<# +.SYNOPSIS + This function returns the HRESULT unique value thrown by the last exception +.DESCRIPTION + This function returns the HRESULT unique value thrown by the last exception +.OUTPUTS + System.String +#> +function Get-IcingaLastExceptionId() +{ + if ([string]::IsNullOrEmpty($Error)) { + return ''; + } + + [string]$ExceptionId = ([string]($Error.FullyQualifiedErrorId)).Split(',')[0].Split(' ')[1]; + $Error.Clear(); + + return $ExceptionId; +} +<# +.SYNOPSIS + Tests if a provided command is available on the system and exists + the shell with an Unknown error and a message. Required to properly + handle Icinga checks and possible error displaying inside Icinga Web 2 +.DESCRIPTION + Tests if a provided command is available on the system and exists + the shell with an Unknown error and a message. Required to properly + handle Icinga checks and possible error displaying inside Icinga Web 2 +.FUNCTIONALITY + Tests if a provided command is available on the system and exists + the shell with an Unknown error and a message. Required to properly + handle Icinga checks and possible error displaying inside Icinga Web 2 +.EXAMPLE + PS>Exit-IcingaPluginNotInstalled -Command 'Invoke-IcingaCheckCPU'; +.PARAMETER Command + The name of the check command to test for +.INPUTS + System.String +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> + +function Exit-IcingaPluginNotInstalled() +{ + param ( + [string]$Command + ); + + if ([string]::IsNullOrEmpty($Command)) { + Exit-IcingaThrowException -CustomMessage 'Null-Command' -ExceptionType 'Configuration' -ExceptionThrown $IcingaExceptions.Configuration.PluginNotAssigned -Force; + } + + if ($null -eq (Get-Command $Command -ErrorAction SilentlyContinue)) { + Exit-IcingaThrowException -CustomMessage $Command -ExceptionType 'Configuration' -ExceptionThrown $IcingaExceptions.Configuration.PluginNotInstalled -Force; + } +} +function Exit-IcingaThrowException() +{ + param( + [string]$InputString, + [string]$StringPattern, + [string]$CustomMessage, + $ExceptionThrown, + [ValidateSet('Permission', 'Input', 'Configuration', 'Connection', 'Unhandled', 'Custom')] + [string]$ExceptionType = 'Unhandled', + [hashtable]$ExceptionList = @{ }, + [string]$KnowledgeBaseId, + [switch]$Force + ); + + if ($Force -eq $FALSE) { + if ($null -eq $InputString -Or [string]::IsNullOrEmpty($InputString)) { + return; + } + + if (-Not $InputString.Contains($StringPattern)) { + return; + } + } + + if ($null -eq $ExceptionList -Or $ExceptionList.Count -eq 0) { + $ExceptionList = $IcingaExceptions; + } + + $ExceptionMessageLib = $null; + $ExceptionTypeString = ''; + + switch ($ExceptionType) { + 'Permission' { + $ExceptionTypeString = 'Permission'; + $ExceptionMessageLib = $ExceptionList.Permission; + }; + 'Input' { + $ExceptionTypeString = 'Invalid Input'; + $ExceptionMessageLib = $ExceptionList.Inputs; + }; + 'Configuration' { + $ExceptionTypeString = 'Invalid Configuration'; + $ExceptionMessageLib = $ExceptionList.Configuration; + }; + 'Connection' { + $ExceptionTypeString = 'Connection error'; + $ExceptionMessageLib = $ExceptionList.Connection; + }; + 'Unhandled' { + $ExceptionTypeString = 'Unhandled'; + }; + 'Custom' { + $ExceptionTypeString = 'Custom'; + }; + } + + [string]$ExceptionName = ''; + [string]$ExceptionIWKB = $KnowledgeBaseId; + + if ($null -ne $ExceptionMessageLib) { + foreach ($definedError in $ExceptionMessageLib.Keys) { + if ($ExceptionMessageLib.$definedError -eq $ExceptionThrown) { + $ExceptionName = $definedError; + break; + } + } + } + if ($null -eq $ExceptionMessageLib -Or [string]::IsNullOrEmpty($ExceptionName)) { + $ExceptionName = [string]::Format('{0} Exception', $ExceptionTypeString); + if ([string]::IsNullOrEmpty($InputString)) { + $InputString = $ExceptionThrown; + } + $ExceptionThrown = [string]::Format( + '{0} exception occured:{1}{2}', + $ExceptionTypeString, + "`r`n", + $InputString + ); + } + + if ($ExceptionThrown -is [hashtable]) { + $ExceptionIWKB = $ExceptionThrown.IWKB; + $ExceptionThrown = $ExceptionThrown.Message; + } + + if ([string]::IsNullOrEmpty($ExceptionIWKB) -eq $FALSE) { + $ExceptionIWKB = [string]::Format( + '{0}{0}Further details can be found on the Icinga for Windows Knowledge base: https://icinga.com/docs/windows/latest/doc/knowledgebase/{1}', + (New-IcingaNewLine), + $ExceptionIWKB + ); + } + + $OutputMessage = '{0}: Icinga {6} Error was thrown: {4}: {5}{2}{2}{3}{1}'; + if ([string]::IsNullOrEmpty($CustomMessage) -eq $TRUE) { + $OutputMessage = '{0}: Icinga {6} Error was thrown: {4}{2}{2}{3}{5}{1}'; + } + + $OutputMessage = [string]::Format( + $OutputMessage, + $IcingaEnums.IcingaExitCodeText.($IcingaEnums.IcingaExitCode.Unknown), + $ExceptionIWKB, + (New-IcingaNewLine), + $ExceptionThrown, + $ExceptionName, + $CustomMessage, + $ExceptionTypeString + ); + + Set-IcingaInternalPluginExitCode -ExitCode $IcingaEnums.IcingaExitCode.Unknown; + Set-IcingaInternalPluginException -PluginException $OutputMessage; + + if ($Global:Icinga.Protected.RunAsDaemon -eq $TRUE -Or $Global:Icinga.Protected.JEAContext -eq $TRUE) { + throw $OutputMessage; + + # Just in case we don't end - shouldn't happen anyway + return; + } + + Write-IcingaConsolePlain $OutputMessage; + exit $IcingaEnums.IcingaExitCode.Unknown; +} +<# + # This script will provide 'Enums' we can use within our module to + # easier access constants and to maintain a better overview of the + # entire components + #> + +[hashtable]$Permission = @{ + PerformanceCounter = 'A Plugin failed to fetch Performance Counter information. This may be caused when the used Service User is not permitted to access these information. To fix this, please add the User the Icinga Agent is running on into the "Performance Monitor Users" group and restart the service.'; + CacheFolder = "A plugin failed to write new data into the configured cache directory. Please update the permissions of this folder to allow write access for the user the Icinga Service is running with or use another folder as cache directory."; + CimInstance = @{ + 'Message' = 'The user you are running this command as does not have permission to access the requested Cim-Object. To fix this, please add the user the Agent is running with to the "Remote Management Users" groups and grant access to the WMI branch for the Class/Namespace mentioned above and add the permission "Remote enable".'; + 'IWKB' = 'IWKB000001'; + }; + WMIObject = @{ + 'Message' = 'The user you are running this command as does not have permission to access the requested Wmi-Object. To fix this, please add the user the Agent is running with to the "Remote Management Users" groups and grant access to the WMI branch for the Class/Namespace mentioned above and add the permission "Remote enable".'; + 'IWKB' = 'IWKB000001'; + }; + WindowsUpdate = @{ + 'Message' = 'The user you are running this command as does not have permission to access the Windows Update ComObject "Microsoft.Update.Session".'; + 'IWKB' = 'IWKB000006'; + }; + WindowsAuthentication = "A plugin failed to authenticate against the authentication servers or the local machine with the given credentials"; +}; + +[hashtable]$Inputs = @{ + PerformanceCounter = 'A plugin failed to fetch Performance Counter information. Please ensure the counter is written properly and available on your system.'; + EventLogLogName = 'Failed to fetch EventLog information. Please specify a valid LogName.'; + EventLog = 'Failed to fetch EventLog information. Please check your inputs for EntryTypes and other categories and try again.'; + ConversionUnitMissing = 'Unable to parse input value. You have to add an unit to your input value. Example: "10GB". Allowed units are: "B, KB, MB, GB, TB, PB, KiB, MiB, GiB, TiB, PiB".'; + MultipleUnitUsage = 'Failed to convert your Icinga threshold units as you were trying to convert values with a different type of unit category. This feature only supports the conversion of one unit category. For example you can not convert 20MB:10d in the same call, as size and time units are not compatible.'; + CimClassNameUnknown = 'The provided class name you try to fetch with Get-CimInstance is not known on this system.'; + WmiObjectClassUnknown = 'The provided class name you try to fetch with Get-WmiObject is not known on this system.'; + MSSQLCredentialHandling = 'The connection to MSSQL was not possible because your login credential was not correct.'; + MSSQLCommandMissing = 'Failed to build a SQL query'; + RegexError = 'A request was not handled properly because a provided regex could not be interpreted. Please validate your regex and try again. In case you are trying to access a ressource containing [], you will have to escape each symbol by using `. Example: myservice`[`]'; + InvalidThresholdValue = 'A treshold value provided for the plugin was invalid and did not follow the Icinga for Windows threshold guidelines. Available formatting examples are: "20", "20:", "~:20", "20:30", "@20:30".'; +}; + +[hashtable]$Configuration = @{ + PluginArgumentConflict = 'Your plugin argument configuration is causing a conflict. Mostly this error is caused by mismatching configurations by enabling multiple switch arguments which are resulting in a conflicting configuration for the plugin.'; + PluginArgumentMissing = 'Your plugin argument configuration is missing mandatory arguments. This error is caused when mandatory or required arguments are missing from a plugin call and the operation is unable to process without them.'; + PluginArgumentAsymmetry = 'Your plugin argument configuration is causing an asymmetry. This error is caused by an uneven amount of arguments in your plugin call. Please ensure that your plugin call is properly configured and all arguments are set correctly.'; + PluginNotInstalled = 'The plugin assigned to this service check seems not to be installed on this machine. Please review your service check configuration for spelling errors and check if the plugin is installed and executable on this machine by PowerShell. You can ensure modules are available by manually importing them by their name with the following commands: Import-Module -Name "module name" -Force; Import-Module -Name "module name" -Global -Force;'; + PluginNotAssigned = 'Your check for this service could not be processed because it seems like no valid Cmdlet was assigned to the check command. Please review your check command to ensure that a valid Cmdlet is assigned and executed by a PowerShell call.'; + EventLogNotInstalled = 'Your Icinga PowerShell Framework has been executed by an unprivileged user before it was properly installed. The Windows EventLog application could not be registered because the current user has insufficient permissions. Please log into the machine and run "Use-Icinga" once from an administrative shell to complete the setup process. Once done this error should vanish.'; + PerfCounterCategoryMissing = 'The specified Performance Counter category was not found on this system. This could either be a configuration error on your local Windows machine or a wrong usage of the plugin. Please check on different Windows machines if this issue persis. In case it only occurs on certain machines it is likely that the counter is simply not present and the plugin can not be processed.'; +} + +[hashtable]$Connection = @{ + MSSQLConnectionError = 'Could not open a connection to SQL Server. This failure may be caused by the fact that under the default settings SQL Server does not allow remote connections or the host is unreachable.'; +} + +<# + # Once we defined a new enum hashtable above, simply add it to this list + # to make it available within the entire module. + # + # Example usage: + # $IcingaException.Inputs.PerformanceCounter + #> + +if ($null -eq $IcingaExceptions) { + [hashtable]$IcingaExceptions = @{ + Permission = $Permission; + Inputs = $Inputs; + Configuration = $Configuration; + Connection = $Connection; + } +} + +Export-ModuleMember -Variable @( 'IcingaExceptions' ); +function Exit-IcingaThrowCritical() +{ + param ( + [string]$Message = '', + [string]$FilterString = $null, + [string]$SearchString = $null, + [switch]$Force = $FALSE + ); + + if ($Force -eq $FALSE) { + if ([string]::IsNullOrEmpty($FilterString) -Or [string]::IsNullOrEmpty($SearchString)) { + return; + } + + if ($FilterString -NotLike "*$SearchString*") { + return; + } + } + + [string]$OutputMessage = [string]::Format( + '[CRITICAL] {0}', + $Message + ); + + Set-IcingaInternalPluginExitCode -ExitCode $IcingaEnums.IcingaExitCode.Critical; + Set-IcingaInternalPluginException -PluginException $OutputMessage; + + if ($Global:Icinga.Protected.RunAsDaemon -eq $TRUE -Or $Global:Icinga.Protected.JEAContext -eq $TRUE) { + throw $OutputMessage; + + # Just in case we don't end - shouldn't happen anyway + return; + } + + Write-IcingaConsolePlain $OutputMessage; + exit $IcingaEnums.IcingaExitCode.Critical; +} +function Register-IcingaBackgroundDaemon() +{ + param( + [string]$Command, + [hashtable]$Arguments + ); + + if ([string]::IsNullOrEmpty($Command)) { + throw 'Please specify a Cmdlet to run as Background Daemon'; + } + + if (-Not (Test-IcingaFunction $Command)) { + throw ([string]::Format('The Cmdlet "{0}" is not available in your session. Please restart the session and try again or verify your input', $Command)); + } + + $Path = [string]::Format('BackgroundDaemon.EnabledDaemons.{0}', $Command); + + Set-IcingaPowerShellConfig -Path ([string]::Format('{0}.Command', $Path)) -Value $Command; + Set-IcingaPowerShellConfig -Path ([string]::Format('{0}.Arguments', $Path)) -Value $Arguments; + + Write-IcingaConsoleNotice ([string]::Format('Background daemon Cmdlet "{0}" has been configured', $Command)); +} +function Set-IcingaForWindowsThreadAlive() +{ + param ( + [string]$ThreadName = '', + [string]$ThreadCmd = '', + $ThreadPool = $null, + [hashtable]$ThreadArgs = @{ }, + [switch]$Active = $FALSE, + [hashtable]$TerminateAction = @{ }, + [int]$Timeout = 300 + ); + + if ([string]::IsNullOrEmpty($ThreadName)) { + return; + } + + if ($Global:Icinga.Public.ThreadAliveHousekeeping.ContainsKey($ThreadName) -eq $FALSE) { + if ($null -eq $ThreadPool) { + return; + } + + $Global:Icinga.Public.ThreadAliveHousekeeping.Add( + $ThreadName, + @{ + 'LastSeen' = [DateTime]::Now; + 'Command' = $ThreadCmd; + 'Arguments' = $ThreadArgs; + 'ThreadPool' = $ThreadPool; + 'Active' = [bool]$Active; + 'TerminateAction' = $TerminateAction; + 'Timeout' = $Timeout; + } + ); + + return; + } + + $Global:Icinga.Public.ThreadAliveHousekeeping[$ThreadName].LastSeen = [DateTime]::Now; + $Global:Icinga.Public.ThreadAliveHousekeeping[$ThreadName].Active = [bool]$Active; + $Global:Icinga.Public.ThreadAliveHousekeeping[$ThreadName].TerminateAction = $TerminateAction; + $Global:Icinga.Public.ThreadAliveHousekeeping[$ThreadName].Timeout = $Timeout; +} +function Unregister-IcingaBackgroundDaemon() +{ + param( + [string]$BackgroundDaemon, + [hashtable]$Arguments + ); + + if ([string]::IsNullOrEmpty($BackgroundDaemon)) { + throw 'Please specify a Cmdlet to remove from running as Background Daemon'; + } + + $Path = [string]::Format('BackgroundDaemon.EnabledDaemons.{0}', $BackgroundDaemon); + + Remove-IcingaPowerShellConfig -Path $Path; + + Write-IcingaConsoleNotice 'Background daemon has been removed'; +} +function Get-IcingaForWindowsServicePid() +{ + [string]$PidFile = (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'service.pid'); + [string]$IfWPid = Read-IcingaFileSecure -File $PidFile; + + if ([string]::IsNullOrEmpty($IfWPid) -eq $FALSE) { + $IfWPid = $IfWPid.Replace("`r`n", '').Replace("`n", '').Replace(' ', ''); + } + + return $IfWPid; +} +function Suspend-IcingaForWindowsFrozenThreads() +{ + try { + [array]$ConfiguredThreads = $Global:Icinga.Public.ThreadAliveHousekeeping.Keys; + + foreach ($thread in $ConfiguredThreads) { + $ThreadConfig = $Global:Icinga.Public.ThreadAliveHousekeeping[$thread]; + + # Only check active threads + if ($ThreadConfig.Active -eq $FALSE) { + continue; + } + + # Check if the thread is active and not doing something for 5 minutes + if (([DateTime]::Now - $ThreadConfig.LastSeen).TotalSeconds -lt $ThreadConfig.Timeout) { + continue; + } + + # If it does, kill the thread + Remove-IcingaThread -Thread $thread; + + if ($ThreadConfig.TerminateAction.Count -ne 0) { + $TerminateArguments = @{ }; + if ($ThreadConfig.TerminateAction.ContainsKey('Arguments')) { + $TerminateArguments = $ThreadConfig.TerminateAction.Arguments; + } + + if ($ThreadConfig.TerminateAction.ContainsKey('Command')) { + $TerminateCmd = $ThreadConfig.TerminateAction.Command; + + if ([string]::IsNullOrEmpty($TerminateCmd) -eq $FALSE) { + & $TerminateCmd @TerminateArguments | Out-Null; + } + } + } + + # Now restart it + New-IcingaThreadInstance ` + -ThreadName $thread ` + -ThreadPool $ThreadConfig.ThreadPool ` + -Command $ThreadConfig.Command ` + -CmdParameters $ThreadConfig.Arguments ` + -Start ` + -CheckAliveState; + + Write-IcingaEventMessage -EventId 1507 -Namespace 'Framework' -Objects $thread; + } + } catch { + # Nothing to do here + } +} +function Get-IcingaBackgroundDaemons() +{ + $Daemons = Get-IcingaPowerShellConfig -Path 'BackgroundDaemon.EnabledDaemons'; + [hashtable]$Output = @{ }; + + if ($null -eq $Daemons) { + return $Output; + } + + foreach ($daemon in $Daemons.PSObject.Properties) { + $Arguments = @{ }; + + foreach ($argument in $daemon.Value.Arguments.PSObject.Properties) { + $Arguments.Add($argument.Name, $argument.Value); + } + + $Output.Add($daemon.Name, $Arguments); + } + + return $Output; +} +function Show-IcingaRegisteredBackgroundDaemons() +{ + [array]$DaemonSummary = @( + 'List of configured background daemons on this system:', + '' + ); + + [hashtable]$DaemonList = Get-IcingaBackgroundDaemons; + + foreach ($daemon in $DaemonList.Keys) { + + $DaemonSummary += $daemon; + $DaemonSummary += '-----------'; + $DaemonConfig = $DaemonList[$daemon]; + + [int]$MaxLength = (Get-IcingaMaxTextLength -TextArray $DaemonConfig.Keys) - 1; + [array]$DaemonData = @(); + + foreach ($daemonArgument in $DaemonConfig.Keys) { + $daemonValue = $DaemonConfig[$daemonArgument]; + $PrintName = Add-IcingaWhiteSpaceToString -Text $daemonArgument -Length $MaxLength; + $DaemonData += [string]::Format('{0} => {1}', $PrintName, $daemonValue); + } + + if ($DaemonConfig.Count -eq 0) { + $DaemonSummary += 'No arguments defined'; + } + + $DaemonSummary += $DaemonData | Sort-Object; + $DaemonSummary += ''; + } + + if ($DaemonList.Count -eq 0) { + $DaemonSummary += 'No background daemons configured'; + $DaemonSummary += ''; + } + + Write-Output $DaemonSummary; +} +function Start-IcingaPowerShellDaemon() +{ + param ( + [switch]$RunAsService = $FALSE, + [switch]$JEAContext = $FALSE, + [switch]$JEARestart = $FALSE + ); + + Start-IcingaForWindowsDaemon -RunAsService:$RunAsService -JEARestart:$JEARestart -JEAContext:$JEAContext; +} + +function Start-IcingaForWindowsDaemon() +{ + param ( + [switch]$RunAsService = $FALSE, + [switch]$JEAContext = $FALSE, + [switch]$JEARestart = $FALSE + ); + + $Global:Icinga.Protected.RunAsDaemon = [bool]$RunAsService; + $Global:Icinga.Protected.JEAContext = [bool]$JEAContext; + [string]$MainServicePidFile = (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'service.pid'); + [string]$JeaPidFile = (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'jea.pid'); + [string]$JeaProfile = Get-IcingaPowerShellConfig -Path 'Framework.JEAProfile'; + [string]$JeaPid = ''; + + if ((Test-IcingaJEAServiceRunning) -eq $FALSE) { + Write-IcingaFileSecure -File ($MainServicePidFile) -Value $PID; + + if ([string]::IsNullOrEmpty($JeaProfile)) { + Write-IcingaDebugMessage -Message 'Starting Icinga for Windows service without JEA context' -Objects $RunAsService, $JEARestart, $JeaProfile; + + # Todo: Add config for active background tasks. Set it to 20 for the moment + Add-IcingaThreadPool -Name 'MainPool' -MaxInstances 20; + $Global:Icinga.Public.Add( + 'SSL', + @{ + 'Certificate' = $null; + 'CertFile' = $null; + 'CertThumbprint' = $null; + 'CertFilter' = $null; + } + ); + + New-IcingaThreadInstance -Name "Main" -ThreadPool (Get-IcingaThreadPool -Name 'MainPool') -Command 'Add-IcingaForWindowsDaemon' -Start; + } else { + Write-IcingaDebugMessage -Message 'Starting Icinga for Windows service inside JEA context' -Objects $RunAsService, $JEARestart, $JeaProfile; + + & powershell.exe -NoProfile -NoLogo -ConfigurationName $JeaProfile -Command { + try { + Use-Icinga -Daemon; + + Write-IcingaFileSecure -File ($args[0]) -Value $PID; + + # Make sure a new JEA session is always updated with the correct process priority + Start-IcingaWindowsScheduledTaskProcessPriority; + + $Global:Icinga.Protected.JEAContext = $TRUE; + $Global:Icinga.Protected.RunAsDaemon = $TRUE; + # Todo: Add config for active background tasks. Set it to 20 for the moment + Add-IcingaThreadPool -Name 'MainPool' -MaxInstances 20; + + $Global:Icinga.Public.Add( + 'SSL', + @{ + 'Certificate' = $null; + 'CertFile' = $null; + 'CertThumbprint' = $null; + 'CertFilter' = $null; + } + ); + + New-IcingaThreadInstance -Name "Main" -ThreadPool (Get-IcingaThreadPool -Name 'MainPool') -Command 'Add-IcingaForWindowsDaemon' -Start; + + while ($TRUE) { + Start-Sleep -Seconds 100; + } + } catch { + Write-IcingaEventMessage -EventId 1600 -Namespace 'Framework' -ExceptionObject $_; + } + } -Args $JeaPidFile; + } + } + + if ($JEARestart) { + return; + } + + if ($RunAsService) { + [int]$JeaRestartCounter = 1; + $FailureTime = $null; + while ($TRUE) { + if ([string]::IsNullOrEmpty($JeaProfile) -eq $FALSE) { + if ([string]::IsNullOrEmpty($JeaPid)) { + [string]$JeaPid = Get-IcingaJEAServicePid; + } + + if ((Test-IcingaJEAServiceRunning -JeaPid $JeaPid) -eq $FALSE) { + if ($JeaRestartCounter -gt 5) { + Write-IcingaEventMessage -EventId 1504 -Namespace Framework; + exit 1; + } + + Write-IcingaFileSecure -File $JeaPidFile -Value ''; + $FailureTime = [DateTime]::Now; + Write-IcingaEventMessage -EventId 1505 -Namespace Framework -Objects ([string]::Format('{0}/5', $JeaRestartCounter)); + Start-IcingaForWindowsDaemon -RunAsService:$RunAsService -JEAContext:$JEAContext -JEARestart; + + if (([DateTime]::Now - $FailureTime).TotalSeconds -lt 180) { + $JeaRestartCounter += 1; + } else { + $JeaRestartCounter = 1; + } + + $JeaPid = ''; + } + + Start-Sleep -Seconds 5; + $JeaAliveCounter += 1; + + continue; + } + Start-Sleep -Seconds 100; + } + } +} +function Add-IcingaForWindowsDaemon() +{ + try { + $EnabledDaemons = Get-IcingaBackgroundDaemons; + + foreach ($daemon in $EnabledDaemons.Keys) { + Write-IcingaDebugMessage -Message 'Trying to enable background daemon' -Objects $daemon; + if (-Not (Test-IcingaFunction $daemon)) { + Write-IcingaEventMessage -EventId 1400 -Namespace 'Framework' $daemon; + continue; + } + + $daemonArgs = $EnabledDaemons[$daemon]; + Write-IcingaDebugMessage -Message 'Starting background daemon' -Objects $daemon, $daemonArgs; + + & $daemon @daemonArgs; + } + } catch { + # Todo: Add exception handling + } + + while ($TRUE) { + Start-Sleep -Seconds 10; + + # Handle possible threads being frozen + Suspend-IcingaForWindowsFrozenThreads; + + # Force Icinga for Windows Garbage Collection + Optimize-IcingaForWindowsMemory -ClearErrorStack -SmartGC; + } +} +<# +.SYNOPSIS + Executes a SQL query +.DESCRIPTION + This Cmdlet will send a SQL query to a given database and + execute the query and returns the output. +.FUNCTIONALITY + Executes a SQL query +.EXAMPLE + PS> Send-IcingaMSSQLCommand -SqlCommand $SqlCommand; +.PARAMETER SqlCommand + The SQL query which will be executed, e.g. $SqlCommand = New-IcingaMSSQLCommand +.INPUTS + System.Data.SqlClient.SqlCommand +.OUTPUTS + System.Data.DataSet +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> +function Send-IcingaMSSQLCommand() +{ + param ( + [System.Data.SqlClient.SqlCommand]$SqlCommand = $null + ); + + $Adapter = New-Object System.Data.SqlClient.SqlDataAdapter $SqlCommand; + + $Data = New-Object System.Data.DataSet; + $Adapter.Fill($Data) | Out-Null; + + return $Data.Tables; +} +function Get-IcingaMSSQLInstanceName() +{ + param ( + $SqlConnection = $null, + [string]$Username, + [securestring]$Password, + [string]$Address = "localhost", + [int]$Port = 1433, + [switch]$IntegratedSecurity = $FALSE, + [switch]$TestConnection = $FALSE + ); + + [bool]$NewSqlConnection = $FALSE; + + if ($null -eq $SqlConnection) { + $SqlConnection = Open-IcingaMSSQLConnection -Username $Username -Password $Password -Address $Address -IntegratedSecurity:$IntegratedSecurity -Port $Port -TestConnection:$TestConnection; + + if ($null -eq $SqlConnection) { + return 'Unknown'; + } + + $NewSqlConnection = $TRUE; + } + + $Query = 'SELECT @@servicename' + $SqlCommand = New-IcingaMSSQLCommand -SqlConnection $SqlConnection -SqlQuery $Query; + $InstanceName = (Send-IcingaMSSQLCommand -SqlCommand $SqlCommand).Column1; + + if ($NewSqlConnection -eq $TRUE) { + Close-IcingaMSSQLConnection -SqlConnection $SqlConnection; + } + + return $InstanceName; +} +<# +.SYNOPSIS + Opens a connection to a MSSQL server +.DESCRIPTION + This Cmdlet will open a connection to a MSSQL server + and returns that connection object. +.FUNCTIONALITY + Opens a connection to a MSSQL server +.EXAMPLE + PS>Open-IcingaMSSQLConnection -IntegratedSecurity -Address localhost; +.EXAMPLE + PS>Open-IcingaMSSQLConnection -Username Exampleuser -Password (ConvertTo-IcingaSecureString 'examplePassword') -Address 123.125.123.2; +.PARAMETER Username + The username for connecting to the MSSQL database +.PARAMETER Password + The password for connecting to the MSSQL database as secure string +.PARAMETER Address + The IP address or FQDN to the MSSQL server to connect to (default: localhost) +.PARAMETER Port + The port of the MSSQL server/instance to connect to with the provided credentials (default: 1433) +.PARAMETER SqlDatabase + The name of a specific database to connect to. Leave empty to connect "globaly" +.PARAMETER IntegratedSecurity + Allows this plugin to use the credentials of the current PowerShell session inherited by + the user the PowerShell is running with. If this is set and the user the PowerShell is + running with can access to the MSSQL database you will not require to provide username + and password +.PARAMETER TestConnection + Set this if you want to return $null on connection errors during MSSQL.open() instead of + exception messages. +.OUTPUTS + System.Data.SqlClient.SqlConnection +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> +function Open-IcingaMSSQLConnection() +{ + param ( + [string]$Username, + [securestring]$Password, + [string]$Address = "localhost", + [int]$Port = 1433, + [string]$SqlDatabase, + [switch]$IntegratedSecurity = $FALSE, + [switch]$TestConnection = $FALSE + ); + + if ($IntegratedSecurity -eq $FALSE) { + if ([string]::IsNullOrEmpty($Username)) { + Exit-IcingaThrowException ` + -ExceptionType 'Input' ` + -ExceptionThrown $IcingaExceptions.Inputs.MSSQLCredentialHandling ` + -CustomMessage '-Username not set and -IntegratedSecurity is false' ` + -Force; + } elseif ($null -eq $Password) { + Exit-IcingaThrowException ` + -ExceptionType 'Input' ` + -ExceptionThrown $IcingaExceptions.Inputs.MSSQLCredentialHandling ` + -CustomMessage '-Password not set and -IntegratedSecurity is false' ` + -Force; + } + + $Password.MakeReadOnly(); + $SqlCredential = New-Object System.Data.SqlClient.SqlCredential($Username, $Password); + } + + try { + $SqlConnection = New-Object System.Data.SqlClient.SqlConnection; + $SqlConnection.ConnectionString = "Server=$Address,$Port;"; + + if ([string]::IsNullOrEmpty($SqlDatabase) -eq $FALSE) { + $SqlConnection.ConnectionString += "Database=$SqlDatabase;"; + } + + if ($IntegratedSecurity -eq $TRUE) { + $SqlConnection.ConnectionString += "Integrated Security=True;"; + } + + $SqlConnection.Credential = $SqlCredential; + + Write-IcingaDebugMessage ` + -Message 'Open client connection for endpoint {0}' ` + -Objects $SqlConnection; + + $SqlConnection.Open(); + } catch { + + if ($TestConnection) { + return $null; + } + + if ([string]::IsNullOrEmpty($Username) -eq $FALSE) { + Exit-IcingaThrowException ` + -InputString $_.Exception.Message ` + -StringPattern $Username ` + -ExceptionType 'Input' ` + -ExceptionThrown $IcingaExceptions.Inputs.MSSQLCredentialHandling; + } + + Exit-IcingaThrowException ` + -InputString $_.Exception.Message ` + -StringPattern 'error: 40' ` + -ExceptionType 'Connection' ` + -ExceptionThrown $IcingaExceptions.Connection.MSSQLConnectionError; + + Exit-IcingaThrowException ` + -InputString $_.Exception.Message ` + -StringPattern 'error: 0' ` + -ExceptionType 'Connection' ` + -ExceptionThrown $IcingaExceptions.Connection.MSSQLConnectionError; + + Exit-IcingaThrowException ` + -InputString $_.Exception.Message ` + -StringPattern 'error: 25' ` + -ExceptionType 'Connection' ` + -ExceptionThrown $IcingaExceptions.Connection.MSSQLConnectionError; + # Last resort + Exit-IcingaThrowException ` + -InputString $_.Exception.Message ` + -ExceptionType 'Custom' ` + -Force; + } + + return $SqlConnection; +} +<# +.SYNOPSIS + Builds a SQL query +.DESCRIPTION + This Cmdlet will build a SQL query + and returns it as an string. +.FUNCTIONALITY + Build a SQL query +.EXAMPLE + PS>New-IcingaMSSQLCommand -SqlConnection $SqlConnection -SqlQuery "SELECT object_name FROM sys.dm_os_performance_counters"; +.PARAMETER SqlConnection + An open SQL connection object e.g. $SqlConnection = Open-IcingaMSSQLConnection -IntegratedSecurity; +.PARAMETER SqlQuery + A SQL query as string. +.INPUTS + System.Data.SqlClient.SqlConnection + System.String +.OUTPUTS + System.Data.SqlClient.SqlCommand +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> +function New-IcingaMSSQLCommand() +{ + param ( + [System.Data.SqlClient.SqlConnection]$SqlConnection = $null, + [string]$SqlQuery = $null + ); + + $SqlCommand = New-Object System.Data.SqlClient.SqlCommand; + $SqlCommand.Connection = $SqlConnection; + + if ($null -eq $SqlCommand.Connection) { + Exit-IcingaThrowException -ExceptionType 'Input' ` + -ExceptionThrown $IcingaExceptions.Inputs.MSSQLCommandMissing ` + -CustomMessage 'It seems the -SqlConnection is empty or invalid' ` + -Force; + } + + $SqlCommand.CommandText = $SqlQuery; + + return $SqlCommand; +} +<# +.SYNOPSIS + Closes a open connection to a MSSQL server +.DESCRIPTION + This Cmdlet will close an open connection to a MSSQL server. +.FUNCTIONALITY + Closes an open connection to a MSSQL server. +.EXAMPLE + PS>Close-IcingaMSSQLConnection $OpenMSSQLConnection; +.INPUTS + System.Data.SqlClient.SqlConnection +.OUTPUTS +.LINK + https://github.com/Icinga/icinga-powershell-framework +#> +function Close-IcingaMSSQLConnection() +{ + param ( + [System.Data.SqlClient.SqlConnection]$SqlConnection = $null + ); + + if ($null -eq $SqlConnection) { + return; + } + + Write-IcingaDebugMessage ` + -Message 'Closing client connection for endpoint {0}' ` + -Objects $SqlConnection; + + $SqlConnection.Close(); + $SqlConnection.Dispose(); + $SqlConnection = $null; +} +function Get-IcingaHelpThresholds() +{ + param ( + $Value, + $Warning, + $Critical + ); + + if ([string]::IsNullOrEmpty($Value) -eq $FALSE) { + $ExampleCheck = New-IcingaCheck -Name 'Example' -Value $Value; + $ExampleCheck.WarnOutOfRange($Warning).CritOutOfRange($Critical) | Out-Null; + + return (New-IcingaCheckResult -Check $ExampleCheck -Compile); + } + + Write-IcingaConsolePlain + ' + Icinga is providing a basic handling for thresholds to make it easier to check if certain values of metrics should rise an event or not. + By default, you are always fine to specify simple numeric values for thresholds throughout the entire Check-Plugins. + + ##################### + + -Warning 60 + -Critical 90 + + This example will always raise an event, in case the value is below 0. On the other hand, it will raise + Warning, if the value is above 60 and + Critical, if the value is above 90. + + Example: Get-IcingaHelpThresholds -Value 40 -Warning 60 -Critical 90; #This will return Ok + Get-IcingaHelpThresholds -Value 70 -Warning 60 -Critical 90; #This will return Warning + + There is however a smart way available, to check for ranges of metric values which are explained below. + + ##################### + + Between Range + -Warning "30:50" + + This configuration will check if a value is within the specified range. In this example it would return Ok, whenver the + value is >= 30 and <= 50 + + Example: Get-IcingaHelpThresholds -Value 40 -Warning "30:50" -Critical "10:70"; #This will return Ok + Get-IcingaHelpThresholds -Value 20 -Warning "30:50" -Critical "10:70"; #This will return Warning + Get-IcingaHelpThresholds -Value 5 -Warning "30:50" -Critical "10:70"; #This will return Critical + + ##################### + + Outside Range + -Warning "@40:70" + + The exact opposite of the between range. Simply write an @ before your range and it will return Ok only, if the value is + outside the range. In this case, it will only return Ok if the value is <= 40 and >= 70 + + Example: Get-IcingaHelpThresholds -Value 10 -Warning "@20:90" -Critical "@40:60"; #This will return Ok + Get-IcingaHelpThresholds -Value 20 -Warning "@20:90" -Critical "@40:60"; #This will return Warning + Get-IcingaHelpThresholds -Value 50 -Warning "@20:90" -Critical "@40:60"; #This will return Critical + + ##################### + + Above value + -Warning "50:" + + A threshold followed by a : will always return Ok in case the value is above the configured start value. In this case it will + always return Ok as long as the value itself is above 50 + + Example: Get-IcingaHelpThresholds -Value 100 -Warning "90:" -Critical "50:"; #This will return Ok + Get-IcingaHelpThresholds -Value 60 -Warning "90:" -Critical "50:"; #This will return Warning + Get-IcingaHelpThresholds -Value 10 -Warning "90:" -Critical "50:"; #This will return Critical + + ##################### + + Below value + -Warning "~:40" + + Like the above value, you can also configure a threshold to require to be lower then a certain value. In this example, every value + below 40 will return Ok + + Example: Get-IcingaHelpThresholds -Value 20 -Warning "~:40" -Critical "~:70"; #This will return Ok + Get-IcingaHelpThresholds -Value 60 -Warning "~:40" -Critical "~:70"; #This will return Warning + Get-IcingaHelpThresholds -Value 90 -Warning "~:40" -Critical "~:70"; #This will return Critical + + ##################### + + You can play around yourself with this by using this Cmdlet with different values and -Warning / -Critical thresholds: + + Get-IcingaHelpThresholds -Value -Warning -Critical ; + '; +} +Export-ModuleMember -Function @( '*' ) -Alias @( '*' ) -Variable @( '*' ) diff --git a/doc/100-General/10-Changelog.md b/doc/100-General/10-Changelog.md index a0578c6..39cdfc6 100644 --- a/doc/100-General/10-Changelog.md +++ b/doc/100-General/10-Changelog.md @@ -17,6 +17,7 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic ### Bugfixes +* [#785](https://github.com/Icinga/icinga-powershell-framework/issues/785) Fixes Icinga for Windows freezing during loading in case the `config.json` is empty * [#787](https://github.com/Icinga/icinga-powershell-framework/pull/787) Fixes the return value in case the `Agent` component could not be installed from `$FALSE` to `null` * [#796](https://github.com/Icinga/icinga-powershell-framework/issues/796) [#798](https://github.com/Icinga/icinga-powershell-framework/issues/798) Fixes an issue with the new check handling, which did not properly convert values from checks to the correct performance data values and base values in some cases * [#797](https://github.com/Icinga/icinga-powershell-framework/issues/797) Fixes plugins throwing `UNKNOWN` in case `-TresholdInterval` is used for Metrics over Time, when checks are newly registered and checked, before the first MoT is executed and collected diff --git a/lib/core/installer/tools/Get-FileEncoding.psm1 b/lib/core/installer/tools/Get-FileEncoding.psm1 index 1500a0c..670e985 100644 --- a/lib/core/installer/tools/Get-FileEncoding.psm1 +++ b/lib/core/installer/tools/Get-FileEncoding.psm1 @@ -38,7 +38,10 @@ function Get-FileEncoding() return $null; } - $Bytes = Get-Content -Encoding Byte -ReadCount 4 -TotalCount 4 -Path $Path; + $FileStream = [System.IO.File]::OpenRead($Path); + $Bytes = New-Object Byte[] 4; + $FileStream.Read($Bytes, 0, 4) | Out-Null; + $FileStream.Close(); if ($Bytes[0] -eq 0xef -and $Bytes[1] -eq 0xbb -and $Bytes[2] -eq 0xbf) { return 'UTF8-BOM'; @@ -52,8 +55,14 @@ function Get-FileEncoding() return 'UTF32'; } else { # Check if the file is ASCII or UTF8 without BOM - $Content = Get-Content -Encoding String -Path $Path; - $Bytes = [System.Text.Encoding]::UTF8.GetBytes($content); + $Content = Get-Content -Encoding String -Path $Path -ErrorAction SilentlyContinue; + + # In case the file is empty, we assume it's UTF8 + if ([string]::IsNullOrEmpty($Content)) { + return 'UTF8'; + } + + $Bytes = [System.Text.Encoding]::UTF8.GetBytes($content); # Check each byte to see if it's outside the ASCII range foreach ($byte in $Bytes) {