AutomationUtils.psm1
<# #> # Global settings $ErrorActionPreference = "Stop" $InformationPreference = "Continue" Set-StrictMode -Version 2 # Global variables $script:Notifiers = @{} $script:Automations = @{} # Classes Class AutomationUtilsCapture { [System.Collections.Generic.List[System.Object]]$Content AutomationUtilsCapture() { $this.Content = [System.Collections.Generic.List[System.Object]]::New() } [string]ToString() { return ($this.Content | Out-String) } } Class AutomationUtilsNotification { [string]$Title = "" [string]$Body = "" [string]$Source = "" AutomationUtilsNotification() { } } Class AutomationUtilsNotificationBatch { [string]$Name [System.Collections.Generic.List[AutomationUtilsNotification]]$Notifications AutomationUtilsNotificationBatch([string]$Name) { $this.Notifications = New-Object 'System.Collections.Generic.List[AutomationUtilsNotification]' $this.Name = $Name } } # Functions <# #> Function Select-ForType { [CmdletBinding()] param( [Parameter(Mandatory=$true,ValueFromPipeline)] [AllowNull()] $Object, [Parameter(Mandatory=$true)] [ValidateNotNull()] [string]$Type, [Parameter(Mandatory=$false)] [ValidateNotNull()] [switch]$Derived = $false, [Parameter(Mandatory=$false)] [ValidateNotNull()] [ScriptBlock]$Begin = $null, [Parameter(Mandatory=$false)] [ValidateNotNull()] [ScriptBlock]$Process = $null, [Parameter(Mandatory=$false)] [ValidateNotNull()] [ScriptBlock]$End = $null ) begin { if ($null -ne $Begin) { & $Begin } $objects = New-Object 'System.Collections.Generic.List[System.Object]' } process { # Make sure we have a type object $checkType = [Type]$Type # Stop here on null if ($null -eq $Object) { $Object return } # Compare the input object type if ($Object.GetType() -eq $checkType -or ($Derived -and $checkType.IsAssignableFrom($Object.GetType()))) { # Only store this object if we have an End script to pass it to if ($null -ne $End) { $objects.Add($Object) } # Run the process script, if defined if ($null -ne $Process) { ForEach-Object -InputObject $Object -Process $Process } } else { $Object } } end { if ($null -ne $End) { ForEach-Object -InputObject $objects -Process $End } } } <# #> Function Reset-LogFile { [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$LogPath, [Parameter(Mandatory=$false)] [int]$PreserveCount = 5, [Parameter(Mandatory=$false)] [int]$RotateSizeKB = 0 ) process { # Check if the target is a directory if (Test-Path -PathType Container $LogPath) { Write-Error "Target is a directory" } # Create the log file, if it doesn't exist if (!(Test-Path $LogPath)) { Write-Verbose "Log Path doesn't exist. Attempting to create." if ($PSCmdlet.ShouldProcess($LogPath, "Create Log")) { New-Item -Type File $LogPath -EA SilentlyContinue | Out-Null } else { return } } # Get the attributes of the target log file $logInfo = Get-Item $LogPath $logSize = ($logInfo.Length/1024) Write-Verbose "Current log file size: $logSize KB" # Check if the log file needs rotation if ($logSize -lt $RotateSizeKB) { Write-Verbose "No rotation required" return } # Log file is over the threshold, so need to rotate or truncate Write-Verbose "Rotation required due to log size" Write-Verbose "PreserveCount: $PreserveCount" # Shuffle all of the logs along [int]$count = $PreserveCount while ($count -gt 0) { # If count is 1, we're working on the active log if ($count -le 1) { $source = $LogPath } else { $source = ("{0}.{1}" -f $LogPath, ($count-1)) } $destination = ("{0}.{1}" -f $LogPath, $count) # Check if there is an actual log to move and rename if (Test-Path -Path $source) { Write-Verbose "Need to rotate $source" if ($PSCmdlet.ShouldProcess($source, "Rotate")) { Move-Item -Path $source -Destination $destination -Force } } $count-- } # Create the log path, if it doesn't exist (i.e. was renamed/rotated) if (!(Test-Path $LogPath)) { if ($PSCmdlet.ShouldProcess($LogPath, "Create Log")) { New-Item -Type File $LogPath -EA SilentlyContinue | Out-Null } else { return } } # Clear the content of the log path (only applies if no rotation was done # due to 0 PreserveCount, but the log is over the RotateSizeKB maximum) if ($PSCmdlet.ShouldProcess($LogPath, "Truncate")) { Clear-Content -Path $LogPath -Force } } } <# #> Function Format-AsLog { [CmdletBinding()] param( [Parameter(Mandatory=$true,ValueFromPipeline)] $Input ) process { # Nothing to do for null if ($null -eq $Input) { return } $timestamp = [DateTime]::Now.ToString("yyyyMMdd HH:mm") @($Input) | Select-ForType -Type 'System.String' -Derived -Process { ("{0} (INFO): {1}" -f $timestamp, $_.ToString()) } | Select-ForType -Type 'System.Management.Automation.InformationRecord' -Derived -Process { ("{0} (INFO): {1}" -f $timestamp, $_.ToString()) } | Select-ForType -Type 'System.Management.Automation.VerboseRecord' -Derived -Process { ("{0} (VERBOSE): {1}" -f $timestamp, $_.ToString()) } | Select-ForType -Type 'System.Management.Automation.ErrorRecord' -Derived -Process { ("{0} (ERROR): {1}" -f $timestamp, $_.ToString()) $_ | Out-String -Stream | ForEach-Object { ("{0} (ERROR): {1}" -f $timestamp, $_.ToString()) } } | Select-ForType -Type 'System.Management.Automation.DebugRecord' -Derived -Process { ("{0} (DEBUG): {1}" -f $timestamp, $_.ToString()) } | Select-ForType -Type 'System.Management.Automation.WarningRecord' -Derived -Process { ("{0} (WARNING): {1}" -f $timestamp, $_.ToString()) } } } Function Invoke-ScriptRepeat { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNull()] [ScriptBlock]$ScriptBlock, [Parameter(Mandatory=$false)] [ValidateNotNull()] [int]$Iterations = -1, [Parameter(Mandatory=$false)] [ValidateSet("Start", "Finish")] [string]$WaitFrom = "Start", [Parameter(Mandatory=$false)] [ValidateNotNull()] [int]$WaitSeconds = 0, [Parameter(Mandatory=$false)] [switch]$CatchError = $false ) process { $count = $iterations # Iterations: # 1+ - Run this many times # 0 - Don't run at all # -1 - Run indefinitely while ($count -gt 0) { # Only decrement count if it is greater than zero # Count could be -1, which implies infinite runs # If count is already zero, don't decrement or it # will run indefinitely if ($count -gt 0) { $count-- } # Capture start of script run $start = [DateTime]::Now Write-Verbose ("Start Time: " + $start.ToString("yyyyMMdd HH:mm:ss")) # Execute script - Output will be streamed back to the caller if ($CatchError) { try { & $ScriptBlock *>&1 } catch { # Catch the error and pass it along in the pipeline Write-Verbose "Error received from script block" $_ } } else { & $ScriptBlock *>&1 } # Capture finish time for script run $finish = [DateTime]::Now Write-Verbose ("Finish Time: " + $finish.ToString("yyyyMMdd HH:mm:ss")) # Check if we have runs remaining and determine how # to wait if ($count -ne 0) { # Calculate the wait time $relative = $finish if ($WaitFrom -eq "Start") { $relative = $start } # Determine the wait time in seconds $wait = ($relative.AddSeconds($WaitSeconds) - [DateTime]::Now).TotalSeconds Write-Verbose "Next iteration in $wait seconds" if ($wait -gt 0) { # Wait until we should run again Write-Verbose "Starting sleep" Start-Sleep -Seconds $wait } } } } } <# #> Function New-Capture { [CmdletBinding()] param() process { [AutomationUtilsCapture]::New() } } <# #> Function Copy-ToCapture { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNull()] [AutomationUtilsCapture]$Capture, [Parameter(Mandatory=$true,ValueFromPipeline)] [AllowNull()] $Object ) process { $Capture.Content.Add($Object) $Object } } <# #> Function Invoke-CaptureScript { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNull()] [AutomationUtilsCapture]$Capture, [Parameter(Mandatory=$true)] [ValidateNotNull()] [ScriptBlock]$ScriptBlock ) process { & $ScriptBlock *>&1 | Copy-ToCapture -Capture $capture } } Function New-Notification { [CmdletBinding(DefaultParameterSetName="Body")] param( [Parameter(Mandatory=$true, ParameterSetName="Body")] [Parameter(Mandatory=$true, ParameterSetName="Script")] [ValidateNotNullOrEmpty()] [string]$Title, [Parameter(Mandatory=$true, ParameterSetName="Body")] [ValidateNotNullOrEmpty()] [string]$Body = $null, [Parameter(Mandatory=$true, ParameterSetName="Script")] [ValidateNotNull()] [ScriptBlock]$ScriptBlock = $null, [Parameter(Mandatory=$false, ParameterSetName="Body")] [Parameter(Mandatory=$false, ParameterSetName="Script")] [ValidateNotNullOrEmpty()] [string]$Source = "" ) process { $notification = [AutomationUtilsNotification]::New() $notification.Title = $Title $notification.Source = $Source switch ($PSCmdlet.ParameterSetName) { "Body" { $notification.Body = $Body } "Script" { $capture = New-Capture Invoke-CaptureScript -Capture $capture -ScriptBlock $ScriptBlock $notification.Body = $capture.ToString() } default { Write-Error "Unknown parameter set name" } } $notification } } Function Send-Notifications { [CmdletBinding()] param( [Parameter(Mandatory=$true,ValueFromPipeline)] [AllowNull()] $Notification, [Parameter(Mandatory=$false)] [ValidateNotNull()] [switch]$Pass, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Name ) begin { $batch = [AutomationUtilsNotificationBatch]::New($Name) } process { # Check if we have a notification object to record if ($null -ne $Notification -and ([AutomationUtilsNotification].IsAssignableFrom($Notification.GetType()))) { $batch.Notifications.Add($Notification) return } # If requested, pass the object on in the pipeline, rather than error if ($Pass) { $Notification return } # Not a notification object and we're not requested to pass it on, so error Write-Error "Non-Notification passed to Send-Notifications and 'Pass' not enabled" } end { # Check if we have some notifications to send if (($batch.Notifications | Measure-Object).Count -lt 1) { # Nothing to do return } # Iterate by Notifier as each notifier should receive a batch of the notifications # to allow it to choose whether to batch send or send individually $script:Notifiers.Keys | ForEach-Object { $notifierName = $_ $notifier = $script:Notifiers[$notifierName] Write-Verbose ("Sending notification via " + $notifierName) ForEach-Object -InputObject $batch -Process $notifier } } } Function Register-Notifier { [CmdletBinding()] param( [Parameter(Mandatory=$false)] [ValidateNotNullOrEmpty()] [string]$Name="default", [Parameter(Mandatory=$true)] [ValidateNotNull()] [ScriptBlock]$ScriptBlock ) process { $script:Notifiers[$Name] = $ScriptBlock } } Function Register-Automation { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Name, [Parameter(Mandatory=$true)] [ValidateNotNull()] [ScriptBlock]$ScriptBlock ) process { $script:Automations[$Name] = $ScriptBlock } } Function Invoke-Automation { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string]$Name, [Parameter(Mandatory=$false)] [ValidateNotNull()] [HashTable]$Config = @{} ) process { # Make sure this automation exists if (!$script:Automations.ContainsKey($Name)) { Write-Error "Automation `"$Name`" does not exist" } $automation = $script:Automations[$Name] # Run the automation & $automation @Config *>&1 | Select-ForType -Type AutomationUtilsNotification -Derived -Process { $_.Source = $Name $_ } } } |