Public/Get-ShiftHandoff.ps1
|
function Get-ShiftHandoff { <# .SYNOPSIS Generates a shift handoff report summarizing runbook activity, escalations, and pending approvals. .DESCRIPTION Collects information from the engine's execution history for a specified time window: - Which runbooks ran and their outcomes - What succeeded and what failed - Pending approval requests - Escalations that occurred - Notes and recommendations for the incoming shift Optionally generates an HTML report and sends it via email. .PARAMETER HoursBack Number of hours to look back for the report. Default is 8 (one shift). .PARAMETER OutputPath File path to save the HTML handoff report. .PARAMETER IncludeRunbookActivity Include runbook execution activity in the report. Default is true. .PARAMETER IncludeAlerts Include alerts and escalations. Default is true. .PARAMETER IncludePendingApprovals Include any pending approval requests. Default is true. .PARAMETER SendEmail Send the report via email. .PARAMETER SmtpServer SMTP server for email delivery. .PARAMETER EmailTo Recipient email address(es). .PARAMETER EmailFrom Sender email address. .PARAMETER SmtpCredential Credential for SMTP authentication. .PARAMETER UseSsl Use SSL for SMTP connection. .EXAMPLE Get-ShiftHandoff -HoursBack 8 -OutputPath 'C:\Reports\handoff.html' Generate an 8-hour shift handoff report and save as HTML. .EXAMPLE Get-ShiftHandoff -HoursBack 12 -SendEmail -SmtpServer 'mail.contoso.com' -EmailTo 'nightshift@contoso.com' -EmailFrom 'runbooks@contoso.com' Generate and email a 12-hour handoff report. .EXAMPLE Get-ShiftHandoff -IncludeRunbookActivity -IncludePendingApprovals Quick console summary of runbook activity and pending approvals. #> [CmdletBinding()] param( [Parameter()] [int]$HoursBack = 8, [Parameter()] [string]$OutputPath, [Parameter()] [bool]$IncludeRunbookActivity = $true, [Parameter()] [bool]$IncludeAlerts = $true, [Parameter()] [bool]$IncludePendingApprovals = $true, [Parameter()] [switch]$SendEmail, [Parameter()] [string]$SmtpServer, [Parameter()] [string[]]$EmailTo, [Parameter()] [string]$EmailFrom, [Parameter()] [PSCredential]$SmtpCredential, [Parameter()] [switch]$UseSsl ) $cutoffTime = (Get-Date).AddHours(-$HoursBack) $executionsPath = Join-Path $env:USERPROFILE '.runbookengine\executions' $runbookActivity = [System.Collections.Generic.List[object]]::new() $escalations = [System.Collections.Generic.List[object]]::new() $pendingApprovals = [System.Collections.Generic.List[object]]::new() $nextShiftNotes = [System.Collections.Generic.List[string]]::new() # Collect runbook activity if ($IncludeRunbookActivity -and (Test-Path $executionsPath)) { $execFiles = Get-ChildItem -Path $executionsPath -Filter '*.json' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -ge $cutoffTime } foreach ($file in $execFiles) { try { $exec = Get-Content -Path $file.FullName -Raw | ConvertFrom-Json -ErrorAction Stop $execTime = if ($exec.StartTime) { try { [DateTime]::Parse($exec.StartTime) } catch { $file.LastWriteTime } } else { $file.LastWriteTime } if ($execTime -ge $cutoffTime) { $computerName = if ($exec.ComputerName) { $exec.ComputerName } elseif ($exec.Parameters -and $exec.Parameters.ComputerName) { $exec.Parameters.ComputerName } else { 'Unknown' } $runbookActivity.Add([PSCustomObject]@{ ExecutionId = $exec.ExecutionId RunbookName = $exec.RunbookName ComputerName = $computerName Status = $exec.Status StartTime = $exec.StartTime Duration = $exec.Duration StepCount = if ($exec.StepResults) { @($exec.StepResults).Count } else { 0 } }) # Check for escalations if ($exec.Status -eq 'Escalated' -and $IncludeAlerts) { $escalationStep = $exec.StepResults | Where-Object { $_.Action -eq 'escalate' } | Select-Object -First 1 $escalations.Add([PSCustomObject]@{ ExecutionId = $exec.ExecutionId RunbookName = $exec.RunbookName ComputerName = $computerName Message = if ($escalationStep) { $escalationStep.Output } else { 'Escalated' } Priority = 'High' Time = $exec.StartTime }) $nextShiftNotes.Add("ESCALATION: $($exec.RunbookName) on $computerName was escalated. Review required.") } # Check for failures if ($exec.Status -eq 'Failed') { $failedStep = $exec.StepResults | Where-Object { $_.Status -eq 'Failed' } | Select-Object -First 1 $nextShiftNotes.Add("FAILED: $($exec.RunbookName) on $computerName failed at step '$($failedStep.StepId)'. May need manual intervention.") } # Check for pending approvals if ($IncludePendingApprovals -and $exec.ApprovalLog) { $pending = $exec.ApprovalLog | Where-Object { -not $_.Approved -and $_.Notes -match 'awaiting|pending' } foreach ($p in $pending) { $pendingApprovals.Add([PSCustomObject]@{ ExecutionId = $exec.ExecutionId RunbookName = $exec.RunbookName StepDescription = $p.StepDescription ComputerName = $computerName RequestedAt = $p.RequestedAt Method = $p.Method }) } } # Check for verification failures if ($exec.VerificationResults) { $unverified = $exec.VerificationResults | Where-Object { -not $_.Verified } foreach ($uv in $unverified) { $nextShiftNotes.Add("UNVERIFIED FIX: $($exec.RunbookName) step '$($uv.StepId)' on $computerName - fix could not be verified after $($uv.Attempts) attempts.") } } } } catch { Write-Verbose "Failed to parse execution file $($file.Name): $_" } } } # Summary statistics $totalRuns = $runbookActivity.Count $completedRuns = @($runbookActivity | Where-Object { $_.Status -eq 'Completed' }).Count $failedRuns = @($runbookActivity | Where-Object { $_.Status -eq 'Failed' }).Count $escalatedRuns = @($runbookActivity | Where-Object { $_.Status -eq 'Escalated' }).Count if ($totalRuns -gt 0) { $nextShiftNotes.Insert(0, "Summary: $totalRuns runbook executions in the last $HoursBack hours - $completedRuns completed, $failedRuns failed, $escalatedRuns escalated.") } else { $nextShiftNotes.Add("No runbook activity in the last $HoursBack hours.") } if ($pendingApprovals.Count -gt 0) { $nextShiftNotes.Add("ACTION REQUIRED: $($pendingApprovals.Count) pending approval(s) need attention.") } # Check correlations across recent activity $affectedServers = $runbookActivity | ForEach-Object { $_.ComputerName } | Select-Object -Unique foreach ($server in $affectedServers) { $serverRuns = @($runbookActivity | Where-Object { $_.ComputerName -eq $server }) if ($serverRuns.Count -ge 3) { $nextShiftNotes.Add("PATTERN: $server had $($serverRuns.Count) runbook executions. Multiple issues may indicate a systemic problem.") } } $handoffReport = [PSCustomObject]@{ GeneratedAt = (Get-Date).ToString('o') HoursBack = $HoursBack PeriodStart = $cutoffTime.ToString('o') PeriodEnd = (Get-Date).ToString('o') TotalExecutions = $totalRuns Completed = $completedRuns Failed = $failedRuns Escalated = $escalatedRuns RunbookActivity = $runbookActivity.ToArray() Escalations = $escalations.ToArray() PendingApprovals = $pendingApprovals.ToArray() NextShiftNotes = $nextShiftNotes.ToArray() AffectedServers = $affectedServers } # Generate HTML report if ($OutputPath) { $htmlPath = New-HtmlDashboard -ReportType 'ShiftHandoff' -Data $handoffReport -OutputPath $OutputPath Write-Host "Shift handoff report saved: $htmlPath" -ForegroundColor Green } # Send email if requested if ($SendEmail) { if (-not $SmtpServer) { throw "SmtpServer is required when using -SendEmail." } if (-not $EmailTo) { throw "EmailTo is required when using -SendEmail." } if (-not $EmailFrom) { $EmailFrom = "runbook-engine@$($env:USERDNSDOMAIN)" } $subject = "Shift Handoff Report - $(Get-Date -Format 'yyyy-MM-dd HH:mm') ($totalRuns runs, $failedRuns failed, $escalatedRuns escalated)" $emailBody = @" SHIFT HANDOFF REPORT ==================== Period: Last $HoursBack hours ($(($cutoffTime).ToString('HH:mm')) to $((Get-Date).ToString('HH:mm'))) Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') SUMMARY ------- Total Executions: $totalRuns Completed: $completedRuns Failed: $failedRuns Escalated: $escalatedRuns Pending Approvals: $($pendingApprovals.Count) NOTES FOR NEXT SHIFT -------------------- $($nextShiftNotes | ForEach-Object { "- $_" } | Out-String) RUNBOOK ACTIVITY ---------------- $($runbookActivity | Format-Table RunbookName, ComputerName, Status, Duration -AutoSize | Out-String) --- Generated by Infra-RunbookEngine "@ $mailParams = @{ From = $EmailFrom To = $EmailTo Subject = $subject Body = $emailBody SmtpServer = $SmtpServer } if ($SmtpCredential) { $mailParams['Credential'] = $SmtpCredential } if ($UseSsl) { $mailParams['UseSsl'] = $true } # If HTML report was generated, attach it if ($OutputPath -and (Test-Path $OutputPath)) { $mailParams['Attachments'] = $OutputPath } try { Send-MailMessage @mailParams -ErrorAction Stop Write-Host "Handoff report emailed to: $($EmailTo -join ', ')" -ForegroundColor Green } catch { Write-Warning "Failed to send handoff email: $_" } } return $handoffReport } |