Monitoring.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\Monitoring.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName Monitoring.Import.DoDotSource -Fallback $false if ($Monitoring_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf may not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName Monitoring.Import.IndividualFiles -Fallback $false if ($Monitoring_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) $correctPath = $Path -replace '\\/',([IO.Path]::DirectorySeparatorChar) if ($doDotSource) { . $correctPath } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($correctPath))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1" # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1" # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code <# This file loads the strings documents from the respective language folders. This allows localizing messages and errors. Load psd1 language files for each language you wish to support. Partial translations are acceptable - when missing a current language message, it will fallback to English or another available language. #> Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'Monitoring' -Language 'en-US' function Add-Workload { <# .SYNOPSIS Start a worker agent for gathering data from monitored targets. .DESCRIPTION Creates a new runspaces and adds it to the worker agent pool. Then adds a tracking item for tracking results with Receive-Workload. .PARAMETER WorkloadPackage A workload package, containing target and the related checks. .EXAMPLE PS C:\> Add-Workload -WorkloadPackage $workload Start a worker agent for gathering data from the monitored target specified in the workload package. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $WorkloadPackage ) begin { #region Main Invocation Scriptblock $scriptBlock = { param ( $WorkLoad ) $result = [pscustomobject]@{ PSTypeName = 'Monitoring.CheckResult' Success = $true Target = $WorkLoad.Target Checks = $WorkLoad.Checks Connected = $false Results = @{ } Errors = @() ErrorChecks = @() } #region Establish Connections try { $connections = Connect-MonTarget -Name $WorkLoad.Target.Name -ErrorAction Stop $result.Connected = $true } catch { $result.Errors += $_ $result.Success = $false return $result } #endregion Establish Connections #region Execute Scans foreach ($check in $WorkLoad.Checks) { try { $result.Results[$check.Name] = @{ Timestamp = (Get-Date) Result = ($check.Check.Invoke($WorkLoad.Target.Name, $connections) | Write-Output) Message = '' } } catch { $result.Results[$check.Name] = @{ Timestamp = (Get-Date) Result = $null Message = $_.Exception.Message } $result.Errors += $_ $result.ErrorChecks += $check $result.Success = $false } } #endregion Execute Scans #region Disconnect foreach ($capability in $WorkLoad.Target.Capability) { Disconnect-MonTarget -Capability $capability -Connection $connections -TargetName $WorkLoad.Target.Name -ErrorAction SilentlyContinue } #endregion Disconnect $result } #endregion Main Invocation Scriptblock } process { $powershell = [PowerShell]::Create().AddScript($scriptBlock).AddArgument($WorkloadPackage) $powershell.RunspacePool = $script:runspacePool $script:runspaces += [pscustomobject]@{ Workload = $WorkloadPackage Runspace = $powerShell.BeginInvoke() PowerShell = $powerShell StartTime = (Get-Date) Received = $false } } } function ConvertFrom-Base64 { <# .SYNOPSIS Converts a base64 encoded string back into its original form. .DESCRIPTION Converts a base64 encoded string back into its original form. .PARAMETER Text The base64 encoded string to convert .EXAMPLE PS C:\> 'RXhhbXBsZQ==' | ConvertFrom-Base64 Converts the encoded 'RXhhbXBsZQ==' string into its human readable form ('Example') #> [OutputType([System.String])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [string[]] $Text ) process { foreach ($entry in $Text) { [Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String($entry))) } } } function ConvertTo-Base64 { <# .SYNOPSIS Converts text to base 64 encoded string. .DESCRIPTION Converts text to base 64 encoded string. .PARAMETER Text The text to convert. .EXAMPLE PS C:\> 'Example' | ConvertTo-Base64 Converts the string 'Example' into its base64 representation. #> [OutputType([System.String])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, Mandatory = $true)] [string[]] $Text ) process { foreach ($entry in $Text) { [System.Convert]::ToBase64String(([Text.Encoding]::UTF8.GetBytes($entry))) } } } function Export-Config { <# .SYNOPSIS Persists configuration data controlling the module's execution. .DESCRIPTION Persists configuration data controlling the module's execution. .PARAMETER Type What kind of configuration data to export: - All : Everything (default) - Target : Configuration information about targets to service - Limit : The limits to use for determining alert states .PARAMETER TargetName Filter what targets should be affected. By default, all targets are exported. .EXAMPLE PS C:\> Export-Config Export all configuration data regarding all targets and limits. #> [CmdletBinding()] Param ( [ValidateSet('All', 'Target', 'Limit')] [string] $Type = 'All', [string] $TargetName = '*' ) begin { $activeSource = Get-PSFConfigValue -FullName 'Monitoring.Source.Config.Active' $sourceItem = $script:configSources[$activeSource] if (-not $sourceItem) { Stop-PSFFunction -String 'Import-Config.SourceNotFound' -StringValues $activeSource -EnableException $true -Cmdlet $PSCmdlet } } process { $params = $Type, $TargetName $ExecutionContext.InvokeCommand.InvokeScript($true, $sourceItem.ExportScript, $null, $params) } } function Export-Data { <# .SYNOPSIS Export the gathered data to cache. .DESCRIPTION Export the gathered data to cache. .PARAMETER TargetName Target name filter of what to export. .EXAMPLE PS C:\> Export-Data Export all gathered data to cache. #> [CmdletBinding()] param ( [string] $TargetName = '*' ) begin { $activeSource = Get-PSFConfigValue -FullName 'Monitoring.Source.Data.Active' $sourceItem = $script:dataSources[$activeSource] if (-not $sourceItem) { Stop-PSFFunction -String 'Import-Data.SourceNotFound' -StringValues $activeSource -EnableException $true -Cmdlet $PSCmdlet } } process { $ExecutionContext.InvokeCommand.InvokeScript($true, $sourceItem.ExportScript, $null, $TargetName) } } function Import-Config { <# .SYNOPSIS Imports configuration data from cache. .DESCRIPTION Imports configuration data from cache. .PARAMETER Type What kind of configuration data to import: - All : Everything (default) - Target : Configuration information about targets to service - Limit : The limits to use for determining alert states .PARAMETER TargetName Filter what targets should be affected. By default, all targets are imported. .EXAMPLE PS C:\> Import-Config Imports all configuration data #> [CmdletBinding()] param ( [ValidateSet('All', 'Target', 'Limit')] [string] $Type = 'All', [string] $TargetName = '*' ) begin { $activeSource = Get-PSFConfigValue -FullName 'Monitoring.Source.Config.Active' $sourceItem = $script:configSources[$activeSource] if (-not $sourceItem) { Stop-PSFFunction -String 'Import-Config.SourceNotFound' -StringValues $activeSource -EnableException $true -Cmdlet $PSCmdlet } } process { $params = $Type, $TargetName $ExecutionContext.InvokeCommand.InvokeScript($true, $sourceItem.ImportScript, $null, $params) } } function Import-Data { <# .SYNOPSIS Gathers target data from disk. .DESCRIPTION Gathers target data from disk. .PARAMETER TargetName The target for which to retrieve data. Defaults to all items. target data is stored on disk under base64-encoded filename for compatibility reasons. .EXAMPLE PS C:\> Import-Data Imports all cached data. #> [CmdletBinding()] param ( [string] $TargetName = '*' ) begin { $activeSource = Get-PSFConfigValue -FullName 'Monitoring.Source.Data.Active' $sourceItem = $script:dataSources[$activeSource] if (-not $sourceItem) { Stop-PSFFunction -String 'Import-Data.SourceNotFound' -StringValues $activeSource -EnableException $true -Cmdlet $PSCmdlet } } process { $ExecutionContext.InvokeCommand.InvokeScript($true, $sourceItem.ImportScript, $null, $TargetName) } } function Receive-Workload { <# .SYNOPSIS Waits for worker agents to finish and receives their results. .DESCRIPTION Waits for worker agents to finish and receives their results. Returns all result objects generated by each worker. Use of this commands assumes that: - Start-WorkloadManager was used to prepare worker agent execution - Add-Workload was used to queue data gathering agents for the affected targets. .EXAMPLE PS C:\> Receive-Workload Waits for worker agents to finish and receives their results. .NOTES Results are also stored in the module-scope $script:lastCheckResults variable. This is strictly for debugging purposes #> [CmdletBinding()] param ( ) process { while ($script:runspaces | Where-Object Received -EQ $false) { #region Collect Data from finished workers foreach ($runspaceContainer in ($script:runspaces | Where-Object { -not $_.Received -and $_.Runspace.IsCompleted })) { $resultObject = $runspaceContainer.PowerShell.EndInvoke($runspaceContainer.Runspace) $runspaceContainer.PowerShell.Dispose() $script:lastCheckResults += $resultObject if (-not $resultObject.Connected) { $runspaceContainer.Received = $true Write-PSFMessage -Level Warning -String 'Receive-Workload.ConnectFailed' -StringValues $resultObject.Target.Name, $resultObject.Errors.Exception.Message -Target $resultObject.Target.Name continue } Import-Data -TargetName $resultObject.Target.Name if (-not $script:data[$resultObject.Target.Name]) { $script:data[$resultObject.Target.Name] = @{ } } foreach ($key in $resultObject.Results.Keys) { if ($resultObject.ErrorChecks.Name -contains $key) { Write-PSFMessage -Level Warning -String 'Receive-Workload.CheckFailed' -StringValues $key, $resultObject.Target.Name, $resultObject.Results[$key].Message -Target $resultObject.Target.Name continue } $script:data[$resultObject.Target.Name][$key] = $resultObject.Results[$key] } Export-Data -TargetName $resultObject.Target.Name $runspaceContainer.Received = $true $resultObject } #endregion Collect Data from finished workers #region Terminate worker agents that timed out foreach ($runspaceContainer in ($script:runspaces | Where-Object { -not $_.Received -and ($_.StartTime.Add((Get-PSFConfigValue -FullName 'Monitoring.Runspace.ExecutionTimeout')) -lt (Get-Date)) })) { Write-PSFMessage -Level Warning -String 'Receive-Workload.RunspaceTimeout' -StringValues $runspaceContainer.Workload.Target.Name -Target $runspaceContainer.Workload.Target.Name $runspaceContainer.PowerShell.Dispose() $runspaceContainer.Received = $true } #endregion Terminate worker agents that timed out # Prevent max CPU load while runspaces are still busy Start-Sleep -Milliseconds 100 } } } function Start-WorkloadManager { <# .SYNOPSIS Generates a clean runspace pool for operating data gathering workloads. .DESCRIPTION Generates a clean runspace pool for operating data gathering workloads. .EXAMPLE PS C:\> Start-WorkloadManager Generates a clean runspace pool for operating data gathering workloads. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] Param ( ) begin { # Dispose of old runspace pools in case execution was interrupted if ($script:runspacePool -and -not $script:runspacePool.IsDisposed) { $script:runspacePool.Dispose() } } process { # Create a runspace pool with the same instance of the module imported $initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() $null = $initialSessionState.ImportPSModule("$($script:ModuleRoot)\Monitoring.psd1") $script:runspacePool = [RunspaceFactory]::CreateRunspacePool($initialSessionState) $null = $script:runspacePool.SetMinRunspaces(1) $null = $script:runspacePool.SetMaxRunspaces((Get-PSFConfigValue -FullName 'Monitoring.Runspace.MaxWorkerCount')) $script:runspacePool.Open() # Declare runtime variable storing the worker agent information $script:runspaces = @() # In-Memory Result Cache. For debugging purposes only. $script:lastCheckResults = @() } } function Stop-WorkloadManager { <# .SYNOPSIS Cleans up all leftovers from the worker agents. .DESCRIPTION Cleans up all leftovers from the worker agents. .EXAMPLE PS C:\> Stop-WorkloadManager Cleans up all leftovers from the worker agents. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] Param ( ) process { if ($script:runspacePool -and -not $script:runspacePool.IsDisposed) { $script:runspacePool.Dispose() } } } function Test-Overlap { <# .SYNOPSIS Matches N:N mappings for congruence. .DESCRIPTION Matches N:N mappings for congruence. Use this for comparing two arrays for overlap. This can be used for scenarios such as: - Whether n Items in Array One are equal to an Item in Array Two. - Whether n Items in Array One are similar to an Item in Array Two. This is especially designed to abstract filtering by multiple wildcard filters. .PARAMETER ReferenceObject The object(s) to compare .PARAMETER DifferenceObject The array of items to compare them to. .PARAMETER Property Compare a property, rather than the basic object. .PARAMETER Count The number of congruent items required for a successful result. Defaults to 1. .PARAMETER Operator How the comparison should be performed. Defaults to 'Equal' Supported Comparisons: Equal, Like, Match .EXAMPLE PS C:\> Test-Overlap -ReferenceObject $ReferenceObject -DifferenceObject $DifferenceObject Tests whether any item in the two arrays are equal. #> [OutputType([System.Boolean])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [AllowNull()] $ReferenceObject, [Parameter(Mandatory = $true)] [AllowNull()] $DifferenceObject, [string] $Property, [int] $Count = 1, [ValidateSet('Equal', 'Like', 'Match')] [string] $Operator = 'Equal' ) begin { $parameter = @{ IncludeEqual = $true ExcludeDifferent = $true } if ($Property) { $parameter['Property'] = $Property } } process { switch ($Operator) { 'Equal' { return (Compare-Object -ReferenceObject $ReferenceObject -DifferenceObject $DebugPreference @parameter | Measure-Object).Count -ge $Count } 'Like' { $numberFound = 0 foreach ($reference in $ReferenceObject) { foreach ($difference in $DifferenceObject) { if ($Property -and ($reference.$Property -like $difference.$Property)) { $numberFound++ } elseif (-not $Property -and ($reference -like $difference)) { $numberFound++ } if ($numberFound -ge $Count) { return $true } } } return $false } 'Match' { $numberFound = 0 foreach ($reference in $ReferenceObject) { foreach ($difference in $DifferenceObject) { if ($Property -and ($reference.$Property -match $difference.$Property)) { $numberFound++ } elseif (-not $Property -and ($reference -match $difference)) { $numberFound++ } if ($numberFound -ge $Count) { return $true } } } return $false } } } } function Get-MonCheck { <# .SYNOPSIS Returns registered checks. .DESCRIPTION Returns registered checks. .PARAMETER Tag The tag to filter by. .PARAMETER Name The name to filter by .EXAMPLE PS C:\> Get-MonCheck Returns all registered checks. #> [CmdletBinding()] Param ( [string[]] $Tag = '*', [string[]] $Name = '*' ) process { $checks = foreach ($checkItem in $script:checks.Values) { #region Filter by Name $foundName = $false foreach ($nameItem in $Name) { if ($checkItem.Name -like $nameItem) { $foundName = $true break } } if (-not $foundName) { continue } #endregion Filter by Name #region Filter by Tag $foundTag = $false foreach ($tagItem in $Tag) { if ($checkItem.Tag -like $tagItem) { $foundTag = $true break } } if (-not $foundTag) { continue } #endregion Filter by Tag $clonedCheck = $checkItem.Clone() $clonedCheck['PSTypeName'] = 'Monitoring.Check' [PSCustomObject]$clonedCheck } $checks | Sort-Object Name } } function Invoke-MonCheck { <# .SYNOPSIS Command that gathers data as configured. .DESCRIPTION The main data gathering command. Schedule this command as a scheduled task after setting up the targets, connection capabilities and checks. .PARAMETER Tag The tags to scan for. .PARAMETER TargetName The targets to scan. .PARAMETER Name The name of the checks to execute. .EXAMPLE PS C:\> Invoke-MonCheck Executes all checks. #> [CmdletBinding()] param ( [string[]] $Tag = '*', [string[]] $TargetName = '*', [string[]] $Name = '*' ) begin { Import-Config #region Auto Import Management Packs if (-not $script:triedAutoImport) { $script:triedAutoImport = $true #region Import Registered Management Pack Modules foreach ($moduleName in (Get-PSFConfigValue -FullName 'Monitoring.ManagementPack.Import')) { try { Write-PSFMessage -String 'Import.ManagementPack.Import' -StringValues $moduleName -ModuleName Monitoring Import-Module -Name $moduleName -Scope Global -ErrorAction Stop } catch { Write-PSFMessage -Level Warning -String 'Import.ManagementPack.Import.Failed' -StringValues $moduleName -ModuleName Monitoring } } #endregion Import Registered Management Pack Modules #region Import all detected Management Packs if enabled if (Get-PSFConfigValue -FullName 'Monitoring.ManagementPack.AutoLoad') { $psd1Files = Get-Item "C:\Program Files\WindowsPowerShell\Modules\*\*\*.psd1" $allManagementPackModules = foreach ($psd1File in $psd1Files) { $data = Import-PowerShellDataFile -Path $psd1File.FullName -ErrorAction Ignore if (-not $data) { continue } if (-not ($data.PrivateData.PSData.Tags -eq 'MonitoringManagementPack')) { continue } $data['FileName'] = $psd1File.BaseName $data['Path'] = $psd1File.FullName $data['ModuleVersion'] = [version]$data['ModuleVersion'] [pscustomobject]$data } $toImport = $allManagementPackModules | Group-Object FileName | ForEach-Object { $_.Group | Sort-Object ModuleVersion -Descending | Select-Object -First 1 -ExpandProperty Path } foreach ($moduleManifest in $toImport) { try { Write-PSFMessage -String 'Import.ManagementPack.Import' -StringValues $moduleManifest -ModuleName Monitoring Import-Module $moduleManifest -Scope Global -ErrorAction Stop } catch { Write-PSFMessage -Level Warning -String 'Import.ManagementPack.Import.Failed' -StringValues $moduleManifest -ModuleName Monitoring } } } #endregion Import all detected Management Packs if enabled } #endregion Auto Import Management Packs Start-WorkloadManager } process { $checks = Get-MonCheck -Tag $Tag -Name $Name $targets = Get-MonTarget -Name $TargetName -Tag $Tag foreach ($targetItem in $targets) { $workload = [pscustomobject]@{ PSTypeName = 'Monitoring.Workload' Target = $targetItem Checks = ($checks | Where-Object { Test-Overlap -ReferenceObject $targetItem -DifferenceObject $_ -Property Tag -Operator Like }) } if (-not $workload.Checks) { continue } Add-Workload -WorkloadPackage $workload } Receive-Workload } end { Stop-WorkloadManager } } function Register-MonCheck { <# .SYNOPSIS Register the logic used to scan a target for a given point of information. .DESCRIPTION Register a scriptblock that will be used to gather a piece of information from the target. This scriptblock will receive two arguments: - The name of the target - A hashtable of connections (as registered using Register-MonConnection and applied to the target by its Capability property) .PARAMETER Name The name of the check to register. Must be unique or it will overwrite the other check. .PARAMETER Check The logic to execute. This scriptblock will receive two arguments: - The name of the target - A hashtable of connections (as registered using Register-MonConnection and applied to the target by its Capability property) .PARAMETER Tag The tags this check applies to. Tags are arbitrary labels that group monitoring targets. A target has one or more tags, and all checks with a matching tag are applied to the target. .PARAMETER Description Adds a description to the check, explaining what this check does and requires. .PARAMETER Module The Management Pack Module that introduced the check. .PARAMETER RecommendedLimit Adds a recommended limit to the check. This is intended to offer the opportunity to give a sane default setting. Caveat: Under no circumstances is this an assumption that this limit is a good fit for every environment. Consider this a starting point if you are unsure, what your actual limits should be like. .PARAMETER RecommendedLimitOperator The operator to apply to the recommended limit. See the caveat on the parameter help for RecommendedLimit. .EXAMPLE PS C:\> Register-MonCheck -Name 'NTDS_DB_FreeDiskPercent' -Check $checkScript -Tag 'dc' -Description 'Returns the percent of free space on the disk hosting the NTDS Database.' Registers the logic stored in the $checkScript variable under the name 'NTDS_DB_FreeDiskPercent' and assigns it to the 'dc' label. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Name, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [System.Management.Automation.ScriptBlock] $Check, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $Tag, [string] $Description, [string] $Module, [object] $RecommendedLimit, [Monitoring.LimitOperator] $RecommendedLimitOperator = 'LessThan' ) process { $script:checks[$Name] = @{ Name = $Name Tag = $Tag Check = $Check Description = $Description Module = $Module RecommendedLimit = $RecommendedLimit RecommendedLimitOperator = $RecommendedLimitOperator } } } function Connect-MonTarget { <# .SYNOPSIS Establish a connection to the specified target. .DESCRIPTION Establish a connection to the specified target. All configured capabilities will be considered. .PARAMETER Name The name of the target to connect to. .EXAMPLE PS C:\> Connect-MonTarget -Name server.contoso.com Establishes a connection to server.contoso.com #> [OutputType([Hashtable])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name ) begin { $target = Get-MonTarget -Name $Name } process { [hashtable]$connections = @{ } foreach ($capability in $target.Capability) { if (-not $script:connectionTypes.ContainsKey($capability)) { Write-Error "Connection Type: $capability not found!" continue } try { $tempResult = $script:connectionTypes[$capability].Connect.Invoke($Name) | Select-Object -First 1 foreach ($key in $tempResult.Keys) { $connections[$key] = $tempResult[$key] } } catch { Write-Error "Failed to connect to $Name via $capability : $_" continue } } $connections } } function Disconnect-MonTarget { <# .SYNOPSIS Disconnects all sessions for a given target. .DESCRIPTION Disconnects all sessions for a given target. .PARAMETER Capability The capability for which to cancel the connection. .PARAMETER Connection The hashtable of connections generated from Connect-MonTarget .PARAMETER TargetName The target to disconnect from. .EXAMPLE PS C:\> Disconnect-MonTarget -Capability 'WinRM' -Connection $Connection -TargetName server.contoso.com Disconnects all WinRM related sessions for server.contoso.com #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Capability, [Parameter(Mandatory = $true)] $Connection, [string] $TargetName ) process { if (-not $script:connectionTypes.ContainsKey($Capability)) { Write-Error "Connection Type: $Capability not found!" return } try { $script:connectionTypes[$Capability].Disconnect.Invoke($Connection, $TargetName) } catch { Write-Error "Failed to disconnect from $Capability : $_" return } } } function Get-MonConnection { <# .SYNOPSIS Returns registered connections based on capability. .DESCRIPTION Returns registered connections based on capability. .PARAMETER Capability The capability for which to look for connections. .EXAMPLE PS C:\> Get-MonConnection -Capability WinRM Returns the registered connection logic for connecting via WinRM #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[]] $Capability ) process { foreach ($capabilityItem in $Capability) { if (-not $script:connectionTypes[$capabilityItem]) { Write-Error "Connection Capability $capabilityItem has no matching connection script!" continue } [pscustomobject]@{ PSTypeName = 'Monitoring.Connection' Name = $capabilityItem Connection = $script:connectionTypes[$capabilityItem] } } } } function Register-MonConnection { <# .SYNOPSIS Registers logic that connects to targets. .DESCRIPTION Registers logic that connects to targets. Use this to add capabilities to the module, that can then be used to connect to a target and be leveraged by checks. .PARAMETER Capability The name to assign to the capability. .PARAMETER ConnectionScript The script to connect to a target. Only receives the name of the target as argument. Must return a hashtable, either with a unique name and the connection object, or an empty hashtable. The hashtable may contain more than one entry and will be merged with other entires, if a target supports multiple capabilities. .PARAMETER DisconnectionScript The script to disconnect from a target. Receives two arguments: - A hashtable of connections - The name of the target The hastable in question contains ALL connections from all capabilities applicable to the target. .EXAMPLE PS C:\> Register-MonConnection -Capability 'WinRM' -ConnectionScript $connect -DisconnectionScript $disconnect Registers the WinRM capability with logic to connect and logic to disconnect. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Capability, [Parameter(Mandatory = $true)] [System.Management.Automation.ScriptBlock] $ConnectionScript, [System.Management.Automation.ScriptBlock] $DisconnectionScript = { } ) process { $script:connectionTypes[$Capability] = @{ Name = $Capability Connect = $ConnectionScript Disconnect = $DisconnectionScript } } } function Get-MonLimit { <# .SYNOPSIS Returns registered check limits. .DESCRIPTION Returns registered check limits. .PARAMETER TargetName The name of the target for which to check limits. .PARAMETER CheckName The name of the check to look for. .EXAMPLE PS C:\> Get-MonLimit Returns all limits registered. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] Param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Name')] [string[]] $TargetName = "*", [Parameter(ValueFromPipelineByPropertyName = $true)] [string[]] $CheckName = '*' ) begin { Import-Config } process { foreach ($targetItem in (Get-MonTarget -Name $TargetName)) { $script:Limits.$($targetItem.Name).Values | Where-Object { Test-Overlap -ReferenceObject $_.CheckName -DifferenceObject $CheckName -Operator Like } | ForEach-Object { $clonedTable = $_.Clone() $clonedTable['PSTypeName'] = 'Monitoring.Limit' [pscustomobject]$clonedTable } } } } function Set-MonLimit { <# .SYNOPSIS Applies a limit/threshold about what constitutes a warning/error. .DESCRIPTION Applies a limit/threshold about what constitutes a warning/error. .PARAMETER TargetName The name of the target to apply it to. The targets must already exist for this to be considered. By default, ALL targets are considered. .PARAMETER CheckName The check for which to apply a limit. The check does not have to exist before applying a limit. .PARAMETER ErrorLimit The threshold that needs to be crossed for the state to be considered in Error. .PARAMETER WarningLimit The threshold that needs to be crossed for the state to be considered in Warning. .PARAMETER Operator What operator to apply to the limit. For example, setting the Operator to 'GreaterThan' and the ErrorLimit to 80 would have all results greater than 80 be considered in error. .EXAMPLE PS C:\> Get-MonTarget -Tag DC | Set-MonLimit -CheckName 'LogDriveFreeSpacePercent' -ErrorLimit 10 -WarningLimit 20 -Operator LessThan Updates all targets of the type DC to new limit thresholds for the check LogDriveFreeSpacePercent: - Warning as soon as the result sinks below '20' - Error as soon as the result sinks below '10' #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Name')] [string[]] $TargetName = "*", [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $CheckName, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [object] $ErrorLimit, [Parameter(ValueFromPipelineByPropertyName = $true)] [object] $WarningLimit, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [Monitoring.LimitOperator] [string] $Operator ) process { foreach ($targetItem in (Get-MonTarget -Name $TargetName)) { Import-Config -TargetName $targetItem.Name -Type Limit if (-not $script:limits[$targetItem.Name]) { $script:limits[$targetItem.Name] = @{ } } $script:limits[$targetItem.Name][$CheckName] = @{ TargetName = $targetItem.Name CheckName = $CheckName ErrorLimit = $ErrorLimit WarningLimit = $WarningLimit Operator = $Operator } Export-Config -TargetName $targetItem.Name -Type Limit } } } function Test-MonHealth { <# .SYNOPSIS Returns cached data and compares it with configured alert limits (if present). .DESCRIPTION Returns cached data and compares it with configured alert limits (if present). .PARAMETER TargetName Filter by target. .PARAMETER Tag Filter by assigned tag to that target. .PARAMETER CheckName Filter by applied check. .EXAMPLE PS C:\> Test-MonHealth Returns all scanend data for all targets and all checks. #> [CmdletBinding()] param ( [string[]] $TargetName = '*', [string[]] $Tag = '*', [string[]] $CheckName = '*' ) begin { #region Utility Function function Add-Result { [CmdletBinding()] param ( [string] $Name, $Data, [hashtable] $Result, [string] $CheckName, [string] $TargetName, $WarningLimit, $ErrorLimit, [string] $Operator, [switch] $Finalize ) #region Finalize and return objects if ($Finalize) { foreach ($resultItem in $Result.Values) { #region Case: No data gathered yet if (-not $resultItem.Timestamp) { $resultItem.Status = 'Error' $resultItem continue } #endregion Case: No data gathered yet #region Case: Stale Data if ($resultItem.Timestamp.Add((Get-PSFConfigValue -FullName 'Monitoring.Data.StaleTimeout')) -lt (Get-Date)) { $resultItem.Status = 'Error' $resultItem continue } #endregion Case: Stale Data #region Case: Valid Data switch ($resultItem.Operator) { 'GreaterThan' { if ($resultItem.Value -gt $resultItem.WarningLimit) { $resultItem.Status = 'Warning' } if ($resultItem.Value -gt $resultItem.AlarmLimit) { $resultItem.Status = 'Error' } break } 'GreaterEqual' { if ($resultItem.Value -ge $resultItem.WarningLimit) { $resultItem.Status = 'Warning' } if ($resultItem.Value -ge $resultItem.AlarmLimit) { $resultItem.Status = 'Error' } break } 'Equal' { if ($resultItem.Value -eq $resultItem.WarningLimit) { $resultItem.Status = 'Warning' } if ($resultItem.Value -eq $resultItem.AlarmLimit) { $resultItem.Status = 'Error' } break } 'NotEqual' { if ($resultItem.Value -ne $resultItem.WarningLimit) { $resultItem.Status = 'Warning' } if ($resultItem.Value -ne $resultItem.AlarmLimit) { $resultItem.Status = 'Error' } break } 'LessEqual' { if ($resultItem.Value -le $resultItem.WarningLimit) { $resultItem.Status = 'Warning' } if ($resultItem.Value -le $resultItem.AlarmLimit) { $resultItem.Status = 'Error' } break } 'LessThan' { if ($resultItem.Value -lt $resultItem.WarningLimit) { $resultItem.Status = 'Warning' } if ($resultItem.Value -lt $resultItem.AlarmLimit) { $resultItem.Status = 'Error' } break } 'Like' { if ($resultItem.Value -like $resultItem.WarningLimit) { $resultItem.Status = 'Warning' } if ($resultItem.Value -like $resultItem.AlarmLimit) { $resultItem.Status = 'Error' } break } 'NotLike' { if ($resultItem.Value -notlike $resultItem.WarningLimit) { $resultItem.Status = 'Warning' } if ($resultItem.Value -notlike $resultItem.AlarmLimit) { $resultItem.Status = 'Error' } break } 'Match' { if ($resultItem.Value -match $resultItem.WarningLimit) { $resultItem.Status = 'Warning' } if ($resultItem.Value -match $resultItem.AlarmLimit) { $resultItem.Status = 'Error' } break } 'NotMatch' { if ($resultItem.Value -notmatch $resultItem.WarningLimit) { $resultItem.Status = 'Warning' } if ($resultItem.Value -notmatch $resultItem.AlarmLimit) { $resultItem.Status = 'Error' } break } default { $resultItem.Status = 'No Limit' } } $resultItem #endregion Case: Valid Data } } #endregion Finalize and return objects if (-not $Result[$Name]) { $Result[$Name] = [pscustomobject]@{ PSTypeName = 'Monitoring.HealthResult' Target = $TargetName Check = $CheckName Value = $Data.Result Timestamp = $Data.Timestamp Message = $Data.Message WarningLimit = $WarningLimit ErrorLimit = $ErrorLimit Operator = $Operator Status = 'Healthy' } } else { # Can only happen when processing limits that have matching data $Result[$Name].WarningLimit = $WarningLimit $Result[$Name].ErrorLimit = $ErrorLimit $Result[$Name].Operator = $Operator } } #endregion Utility Function Import-Config } process { foreach ($targetItem in (Get-MonTarget -Name $TargetName -Tag $Tag)) { Import-Data -TargetName $targetItem $result = @{ } foreach ($key in $script:data[$targetItem.Name].Keys) { if (-not (Test-Overlap -ReferenceObject $key -DifferenceObject $CheckName -Operator Like)) { continue } Add-Result -Name $key -Data $script:data[$targetItem.Name][$key] -Result $result -CheckName $key -TargetName $targetItem.Name } foreach ($limitItem in $script:limits[$targetItem.Name].Values) { if (-not (Test-Overlap -ReferenceObject $limitItem.CheckName -DifferenceObject $CheckName -Operator Like)) { continue } $paramAddResult = @{ Name = $limitItem.CheckName Result = $result CheckName = $key TargetName = $targetItem.Name WarningLimit = $limitItem.WarningLimit ErrorLimit = $limitItem.ErrorLimit Operator = $limitItem.Operator } Add-Result @paramAddResult } Add-Result -Result $result -Finalize } } } function Get-MonDatum { <# .SYNOPSIS Returns information on a single piece of scanned data. .DESCRIPTION Returns information on a single piece of scanned data. Returns an object with three properties: - Timestamp (When was the data last retrieved) - Result (What data was retrieved) - Message (Any error message) Any content in Message implies a failed result. If no data was found for the specified combination of target and check, the message property will list "No Data". .PARAMETER TargetName The name of the target to retrive data for. No wildcards. .PARAMETER CheckName The name of the check to retrive data for. No wildcards. .EXAMPLE PS C:\> Get-MonDatum -TargetName sever.contoso.com -CheckName NTDS.DBDiskFreeSpacePercent Returns the check result of NTDS.DBDiskFreeSpacePercent for sever.contoso.com #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $TargetName, [Parameter(Mandatory = $true)] [string] $CheckName ) begin { Import-Data -TargetName $TargetName } process { $datum = $script:data.$TargetName.$CheckName if ($datum) { $clonedDatum = $datum.Clone() $clonedDatum['PSTypeName'] = 'Monitoring.Datum' [pscustomobject]$clonedDatum } else { [pscustomobject]@{ PSTypeName = 'Monitoring.Datum' Timestamp = $null Result = $null Message = 'No Data' } } } } function Get-MonConfigSource { <# .SYNOPSIS Returns the registered config sources. .DESCRIPTION Returns the registered config sources. .PARAMETER Name The name of the source to return. .EXAMPLE PS C:\> Get-MonConfigSource Lists all config sources. #> [CmdletBinding()] param ( [string] $Name = '*' ) process { $script:configSources.Values | Where-Object Name -Like $Name | ForEach-Object { $clonedHashtable = $_.Clone() $clonedHashtable['PSTypeName'] = 'Monitoring.ConfigSource' [pscustomobject]$clonedHashtable } } } function Get-MonDataSource { <# .SYNOPSIS Returns the registered data sources. .DESCRIPTION Returns the registered data sources. .PARAMETER Name The name of the source to return. .EXAMPLE PS C:\> Get-MonDataSource Lists all data sources. #> [CmdletBinding()] param ( [string] $Name = '*' ) process { $script:dataSources.Values | Where-Object Name -Like $Name | ForEach-Object { $clonedHashtable = $_.Clone() $clonedHashtable['PSTypeName'] = 'Monitoring.DataSource' [pscustomobject]$clonedHashtable } } } function Register-MonConfigSource { <# .SYNOPSIS Registers a custom monitoring configuration source. .DESCRIPTION Registers a custom monitoring configuration source. Config sources are the configuration backend that define the data gathering behavior. This includes the targets to monitor and any limits to apply in scenarios where the limit configuration is stored in the module configuration itself. For example, the 'Path' config source that comes with the module (and is the default source) will store the configuration in file. This command makes the actual configuration management freely extensible. .PARAMETER Name The name of the source. Must be unique, otherwise the previous config source will be overwritten, .PARAMETER Description A description of the config source. .PARAMETER ImportScript The scriptblock to execute to read configuration from the source. .PARAMETER ExportScript The scriptblock to execute to write configuration to the source. .EXAMPLE PS C:\> Register-MonConfigSource -Name 'Path' -Description 'Uses the filesystem as data backend for monitoring configuration' -ImportScript $ImportScript -ExportScript $ExportScript Registers the "Path" config source. #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $Description, [Parameter(Mandatory = $true)] [scriptblock] $ImportScript, [Parameter(Mandatory = $true)] [scriptblock] $ExportScript ) process { $script:configSources[$Name] = @{ Name = $Name Description = $Description ImportScript = $ImportScript ExportScript = $ExportScript } } } function Register-MonDataSource { <# .SYNOPSIS Registers a custom monitoring data source. .DESCRIPTION Registers a custom monitoring data source. Data sources are the data backend that manage the data gathered by the checks. For example, the 'Path' data source that comes with the module (and is the default source) will store the data in file. This command makes the actual backend freely extensible. .PARAMETER Name The name of the source. Must be unique, otherwise the previous data source will be overwritten, .PARAMETER Description A description of the data source. .PARAMETER ImportScript The scriptblock to execute to read data from the source. .PARAMETER ExportScript The scriptblock to execute to write data to the source. .EXAMPLE PS C:\> Register-MonDataSource -Name 'Path' -Description 'Uses the filesystem as data backend for monitoring data' -ImportScript $ImportScript -ExportScript $ExportScript Registers the "Path" data source. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $Description, [Parameter(Mandatory = $true)] [scriptblock] $ImportScript, [Parameter(Mandatory = $true)] [scriptblock] $ExportScript ) process { $script:dataSources[$Name] = @{ Name = $Name Description = $Description ImportScript = $ImportScript ExportScript = $ExportScript } } } function Get-MonTarget { <# .SYNOPSIS Returns registered monitoring targets. .DESCRIPTION Returns registered monitoring targets. .PARAMETER Name The name of the target. .PARAMETER Tag The tags the tartget should have. .EXAMPLE PS C:\> Get-MonTarget Returns all targets #> [CmdletBinding()] Param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $Name = "*", [Parameter(ValueFromPipelineByPropertyName = $true)] [string[]] $Tag = "*" ) process { foreach ($targetItem in $script:configuration.Values) { $clonedItem = $targetItem.Clone() #region Filter by Name $foundName = $false foreach ($nameItem in $Name) { if ($clonedItem.Name -like $nameItem) { $foundName = $true break } } if (-not $foundName) { continue } #endregion Filter by Name #region Filter by Tag $foundTag = $false foreach ($tagItem in $Tag) { if ($clonedItem.Tag -like $tagItem) { $foundTag = $true break } } if (-not $foundTag) { continue } #endregion Filter by Tag $clonedItem['PSTypeName'] = 'Monitoring.Target' [pscustomobject]$clonedItem } } } function Remove-MonTarget { <# .SYNOPSIS Deletes a target. .DESCRIPTION Deletes a target. This will purge all configuration data from memory and file. Optionally, the data gathered from checks can be retained. .PARAMETER Name Name of the target to purge. .PARAMETER KeepData By default, all data pertaining a given target will be purged as well. Using this switch disables that behavior. .EXAMPLE PS C:\> Remove-MonTarget -Name 'server.contoso.com' Removes the target named server.contoso.com #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $Name, [switch] $KeepData ) process { foreach ($nameItem in $Name) { Import-Config -TargetName $nameItem if (-not $script:configuration[$nameItem]) { Write-Error "Unable to find target $nameItem" continue } $script:configuration.Remove($nameItem) if ($script:limits.ContainsKey($nameItem)) { $script:limits.Remove($nameItem) } Export-Config -TargetName $nameItem Import-Data -TargetName $nameItem if (-not $KeepData -and $script:data[$nameItem]) { $script:data.Remove($nameItem) Export-Data -TargetName $nameItem } } } } function Set-MonTarget { <# .SYNOPSIS Register a monitoring target. .DESCRIPTION Register a monitoring target. This is used for updating and maintaining a target system. .PARAMETER Name What to target the monitoring subject by. A unique label used to identify the resource. For computer management, this could be a DNS name. For monitoring SQL instances a connection string or instance name. .PARAMETER Tag What tag to assign to the target. Tags are monitoring groups. Checks are assigned to tags. For example, all checks assigned the tag 'DC' are applied to targets also tagged as 'DC'. At the same time, a target could also have the tag 'Server' and thus be subject to checks assigned to that tag. Assignments of tags are cummulative - applying new tags adds them to the object. .PARAMETER Capability Capabilities are used to determine what kind of connections can be established to this target. For example, adding a WinRM capability would tell the system the target can accept PowerShell Remoting and CIM over WinRM connections. Add supported capabilities by using the Register-MonConnection. Assignments of capabilities are cummulative - applying new capabilities adds them to the object. .EXAMPLE PS C:\> Set-MonTarget -Name 'server1.contoso.com' -Tag 'server', 'dc', 'server2019' -Capability WinRM, ldap Configures the server 'server1.contoso.com' as server, server2019 and dc. It also configures it to accept WinRM and ldap connections. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $Name, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $Tag, [PsfValidateSet(TabCompletion = 'Monitoring.Connection')] [PsfArgumentCompleter('Monitoring.Connection')] [string[]] $Capability ) process { foreach ($nameItem in $Name) { Import-Config -TargetName $nameItem #region Add to existing target if ($target = $script:configuration[$nameItem]) { foreach ($tagItem in $Tag) { if ($target.Tag -notcontains $tagItem) { $target.Tag + $tagItem } } foreach ($capabilityItem in $Capability) { if ($target.Capability -notcontains $capabilityItem) { $target.Capability + $capabilityItem } } } #endregion Add to existing target #region Create new target else { $script:configuration[$nameItem] = @{ Name = $nameItem Tag = $Tag Capability = $Capability } } #endregion Create new target Export-Config -TargetName $nameItem } } } <# This is an example configuration file By default, it is enough to have a single one of them, however if you have enough configuration settings to justify having multiple copies of it, feel totally free to split them into multiple files. #> <# # Example Configuration Set-PSFConfig -Module 'Monitoring' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" #> Set-PSFConfig -Module 'Monitoring' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'Monitoring' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." # Data Settings Set-PSFConfig -Module 'Monitoring' -Name 'Data.StaleTimeout' -Value (New-TimeSpan -Minutes 30) -Initialize -Validation 'timespan' -Description "The time limit after which data is by default considered to be stale." # Runspace Settings Set-PSFConfig -Module 'Monitoring' -Name 'Runspace.MaxWorkerCount' -Value 32 -Initialize -Validation 'integer' -Description "The maximum number of targets that can be processed in parallel." Set-PSFConfig -Module 'Monitoring' -Name 'Runspace.ExecutionTimeout' -Value (New-TimeSpan -Seconds 300) -Initialize -Validation 'timespan' -Description "The timeout of each worker runspace. If gathering data takes longer than this, data gathering is cancelled." # Source Settings Set-PSFConfig -Module 'Monitoring' -Name 'Source.Config.Active' -Value 'Path' -Initialize -Validation 'string' -Description 'Which data source is used for configuration storage and retrieval.' Set-PSFConfig -Module 'Monitoring' -Name 'Source.Data.Active' -Value 'Path' -Initialize -Validation 'string' -Description 'Which data source is used for data storage and retrieval.' # Management Pack Settings Set-PSFConfig -Module 'Monitoring' -Name 'ManagementPack.AutoLoad' -Value $false -Initialize -Validation 'bool' -Description 'Whether to automatically detect and load Management Pack Modules during module import.' Set-PSFConfig -Module 'Monitoring' -Name 'ManagementPack.Import' -Value @() -Initialize -Validation 'stringarray' -Description 'An explicit list of Management Pack Modules to import when importing this module.' <# Stored scriptblocks are available in [PsfValidateScript()] attributes. This makes it easier to centrally provide the same scriptblock multiple times, without having to maintain it in separate locations. It also prevents lengthy validation scriptblocks from making your parameter block hard to read. Set-PSFScriptblock -Name 'Monitoring.ScriptBlockName' -Scriptblock { } #> Register-PSFTeppScriptblock -Name Monitoring.Tags -ScriptBlock { (Get-MonCheck).Tag, (Get-MonTarget).Tag | Remove-PSFNUll -Enumerate | Select-Object -Unique | Sort-Object } Register-PSFTeppScriptblock -Name Monitoring.Target -ScriptBlock { (Get-MonTarget).Name } Register-PSFTeppScriptblock -Name Monitoring.Check -ScriptBlock { if ($fakeBoundParameter.TargetName) { $targetTags = (Get-MonTarget -Name $fakeBoundParameter.TargetName).Tag if ($targetTags) { return (Get-MonCheck -Tag $targetTags).Name } } (Get-MonCheck).Name } Register-PSFTeppScriptblock -Name Monitoring.Connection -ScriptBlock { (Get-MonConnection).Name } Register-PSFTeppArgumentCompleter -Command Get-MonCheck -Parameter Tag -Name 'Monitoring.Tags' Register-PSFTeppArgumentCompleter -Command Get-MonTarget -Parameter Tag -Name 'Monitoring.Tags' Register-PSFTeppArgumentCompleter -Command Invoke-MonCheck -Parameter Tag -Name 'Monitoring.Tags' Register-PSFTeppArgumentCompleter -Command Register-MonCheck -Parameter Tag -Name 'Monitoring.Tags' Register-PSFTeppArgumentCompleter -Command Set-MonTarget -Parameter Tag -Name 'Monitoring.Tags' Register-PSFTeppArgumentCompleter -Command Test-MonHealth -Parameter Tag -Name 'Monitoring.Tags' Register-PSFTeppArgumentCompleter -Command Disconnect-MonTarget -Parameter TargetName -Name 'Monitoring.Target' Register-PSFTeppArgumentCompleter -Command Get-MonDatum -Parameter TargetName -Name 'Monitoring.Target' Register-PSFTeppArgumentCompleter -Command Get-MonLimit -Parameter TargetName -Name 'Monitoring.Target' Register-PSFTeppArgumentCompleter -Command Get-MonTarget -Parameter Name -Name 'Monitoring.Target' Register-PSFTeppArgumentCompleter -Command Invoke-MonCheck -Parameter TargetName -Name 'Monitoring.Target' Register-PSFTeppArgumentCompleter -Command Remove-MonTarget -Parameter Name -Name 'Monitoring.Target' Register-PSFTeppArgumentCompleter -Command Set-MonLimit -Parameter TargetName -Name 'Monitoring.Target' Register-PSFTeppArgumentCompleter -Command Set-MonTarget -Parameter Name -Name 'Monitoring.Target' Register-PSFTeppArgumentCompleter -Command Test-MonHealth -Parameter TargetName -Name 'Monitoring.Target' Register-PSFTeppArgumentCompleter -Command Get-MonDatum -Parameter CheckName -Name 'Monitoring.Check' Register-PSFTeppArgumentCompleter -Command Get-MonLimit -Parameter CheckName -Name 'Monitoring.Check' Register-PSFTeppArgumentCompleter -Command Set-MonLimit -Parameter CheckName -Name 'Monitoring.Check' Register-PSFTeppArgumentCompleter -Command Test-MonHealth -Parameter CheckName -Name 'Monitoring.Check' New-PSFLicense -Product 'Monitoring' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2019-04-15") -Text @" Copyright (c) 2019 Friedrich Weinmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ #if (-not (Test-Path -Path $script:pathConfiguration)) { New-Item -Path $script:pathConfiguration -ItemType Directory -Force } #region Data & Config # DATA: Stores the data actually present on each target for each applicable check $script:data = @{ <# TargetName = @{ CheckName = @{ Timestamp = $null Result = $null Message = "" } } #> } # CONFIG: Stores configuration data, specifically that relating to targets $script:configuration = @{ <# TargetName = @{ Name = 'TargetName' Tag = 'tag1','tag2' Capability = 'WinRM' } #> } # CONFIG: Stores the limits specified by the monitoring system. $script:limits = @{ <# TargetName = @{ CheckName = @{ TargetName = 'TargetName' CheckName = 'CheckName' AlarmLimit = $null WarningLimit = $null Operator = 'GreaterThan|GreaterEqual|Equal|NotEqual|LesserEqual|LesserThan|Like|NotLike|Match|NotMatch' } } #> } #endregion Data & Config #region Runtime # RUNTIME : Stores the registered config source configuration $script:configSources = @{ <# SourceName = @{ Name = 'SourceName' ImportScript = { ... } ExportScript = { ... } } #> } # RUNTIME : Stores the registered data source configuration $script:dataSources = @{ <# SourceName = @{ Name = 'SourceName' ImportScript = { ... } ExportScript = { ... } } #> } # RUNTIME : Stores the various checks that have been registered $script:checks = @{ <# CheckName = @{ Name = 'CheckName' Tag = 'tag1' Check = { ... } } #> } # RUNTIME: Stores registered connection types $script:connectionTypes = @{ <# CapabilityName = @{ Name = 'CapabilityName' Connect = { ... } Disconnect = { ... } } #> } #endregion Runtime $script:triedAutoImport = $false Register-MonConnection -Capability ldap -ConnectionScript { param ( $TargetName ) # Nothing - it's a dummy connection @{ } } -DisconnectionScript { param ( $Connections, $TargetName ) # Nothing - it's a dummy connection } Register-MonConnection -Capability WinRM -ConnectionScript { param ( $TargetName ) @{ 'WinRM_PS' = (New-PSSession -ComputerName $TargetName) 'WinRM_CIM' = (New-CimSession -ComputerName $TargetName) } } -DisconnectionScript { param ( $Connections, $TargetName ) if ($Connections.WinRM_PS) { $Connections.WinRM_PS | Remove-PSSession } if ($Connections.WinRM_CIM) { $Connections.WinRM_CIM | Remove-CimSession } } #region Configurations $basePath = Join-PSFPath $env:APPDATA 'WindowsPowerShell' 'Monitoring' if (Test-PSFPowerShell -Elevated) { $basePath = Join-PSFPath $env:ProgramData 'WindowsPowerShell' 'Monitoring' } Set-PSFConfig -Module 'Monitoring' -Name 'Source.Path.Config' -Value (Join-Path -Path $basePath -ChildPath Config) -Initialize -Validation 'string' -Description 'The path where the "Path" data source stores its configuration data.' #endregion Configurations #region Create Configuration Source $scriptblockImport = { param ( [string] $Type, [string] $TargetName ) #region Import Targets if ($Type -match '^All$|^Target$') { foreach ($fileItem in (Get-Item "$(Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Config')\*.target")) { $baseName = $fileItem.BaseName | ConvertFrom-Base64 if (-not (($baseName -eq $TargetName) -or ($baseName -like $TargetName))) { continue } $data = Import-PSFClixml -Path $fileItem.FullName $script:configuration[$data.Target] = $data.Value } } #endregion Import Targets #region Import Limits if ($Type -match '^All$|^Limit$') { foreach ($fileItem in (Get-Item "$(Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Config')\*.limit")) { $baseName = $fileItem.BaseName | ConvertFrom-Base64 if (-not (($baseName -eq $TargetName) -or ($baseName -like $TargetName))) { continue } $data = Import-PSFClixml -Path $fileItem.FullName $script:limits[$data.Target] = $data.Value } } #endregion Import Limits } $scriptblockExport = { param ( [string] $Type, [string] $TargetName ) $pathConfiguration = Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Config' #region Export Targets if ($Type -match '^All$|^Target$') { $wasFound = $false foreach ($key in $script:configuration.Keys) { if (-not (($key -eq $TargetName) -or ($key -like $TargetName))) { continue } if ($key -eq $TargetName) { $wasFound = $true } $object = [pscustomobject]@{ Target = $key Value = $script:configuration[$key] } $object | Export-PSFClixml -Path (Join-Path -Path $pathConfiguration -ChildPath "$($key | ConvertTo-Base64).target") -Depth 5 } # Delete logic. Applies when using Remove-MonTarget if ((-not $wasFound) -and (Test-Path (Join-Path -Path $pathConfiguration -ChildPath "$($TargetName | ConvertTo-Base64).target"))) { Remove-Item -Path (Join-Path -Path $pathConfiguration -ChildPath "$($TargetName | ConvertTo-Base64).target") -Force } } #endregion Export Targets #region Export Limits if ($Type -match '^All$|^Limit$') { $wasFound = $false foreach ($key in $script:limits.Keys) { if (-not (($key -eq $TargetName) -or ($key -like $TargetName))) { continue } if ($key -eq $TargetName) { $wasFound = $true } $object = [pscustomobject]@{ Target = $key Value = $script:limits[$key] } $object | Export-PSFClixml -Path (Join-Path -Path $pathConfiguration -ChildPath "$($key | ConvertTo-Base64).limit") -Depth 5 } # Delete logic. Applies when using Remove-MonTarget if ((-not $wasFound) -and (Test-Path (Join-Path -Path $pathConfiguration -ChildPath "$($TargetName | ConvertTo-Base64).limit"))) { Remove-Item -Path (Join-Path -Path $pathConfiguration -ChildPath "$($TargetName | ConvertTo-Base64).limit") -Force } } #endregion Export Limits } $paramRegisterMonConfigSource = @{ Name = 'Path' Description = 'Uses the filesystem as data backend for monitoring configuration' ImportScript = $scriptblockImport ExportScript = $scriptblockExport } Register-MonConfigSource @paramRegisterMonConfigSource #endregion Create Configuration Source #region Ensure Path Exists $pathConfigCache = Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Config' if (-not (Test-Path -Path $pathConfigCache)) { $null = New-Item -Path $pathConfigCache -ItemType Directory -Force } #endregion Ensure Path Exists #region Configurations $basePath = Join-PSFPath $env:APPDATA 'WindowsPowerShell' 'Monitoring' if (Test-PSFPowerShell -Elevated) { $basePath = Join-PSFPath $env:ProgramData 'WindowsPowerShell' 'Monitoring' } Set-PSFConfig -Module 'Monitoring' -Name 'Source.Path.Data' -Value (Join-Path -Path $basePath -ChildPath Data) -Initialize -Validation 'string' -Description 'The path where the "Path" data source stores its scan content.' #endregion Configurations #region Create Data Source $scriptblockImport = { param ( [string] $TargetName = '*' ) $wasFound = $false foreach ($fileItem in (Get-ChildItem -Path (Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Data') -File)) { $baseName = $fileItem.BaseName | ConvertFrom-Base64 if ($baseName -eq $TargetName) { $wasFound = $true } if (($baseName -eq $TargetName) -or ($baseName -like $TargetName)) { $importedData = Import-PSFClixml -Path $fileItem.FullName $script:data[$importedData.Target] = $importedData.Content } } if (-not $TargetName.Contains("*") -and -not $wasFound) { $script:data[$TargetName] = @{ } } } $scriptblockExport = { param ( [string] $TargetName = '*' ) $pathDataCache = Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Data' $wasFound = $false foreach ($key in $script:data.Keys) { if (($key -ne $TargetName) -and ($key -notlike $TargetName)) { continue } if ($key -eq $TargetName) { $wasFound = $true } $object = [pscustomobject]@{ Target = $key Content = $script:data[$key] } $object | Export-PSFClixml -Path (Join-Path -Path $pathDataCache -ChildPath ($key | ConvertTo-Base64)) -Depth 5 } # Delete logic. Applies when using Remove-MonTarget if ((-not $wasFound) -and (Test-Path (Join-Path -Path $pathDataCache -ChildPath ($TargetName | ConvertTo-Base64)))) { Remove-Item -Path (Join-Path -Path $pathDataCache -ChildPath ($TargetName | ConvertTo-Base64)) -Force } } $paramRegisterMonDataSource = @{ Name = 'Path' Description = 'Uses the filesystem as data backend for monitoring data' ImportScript = $scriptblockImport ExportScript = $scriptblockExport } Register-MonDataSource @paramRegisterMonDataSource #endregion Create Data Source #region Ensure Path Exists $pathDataCache = Get-PSFConfigValue -FullName 'Monitoring.Source.Path.Data' if (-not (Test-Path -Path $pathDataCache)) { $null = New-Item -Path $pathDataCache -ItemType Directory -Force } #endregion Ensure Path Exists # Load the persisted target configuration Import-Config -Type Target #endregion Load compiled code |