Assets/JobRunner.ps1
|
<#
.SYNOPSIS Master Wrapper for executing Ops jobs. .DESCRIPTION Runs PowerShell or Node.js scripts, handles logging, error catching, and alerting. .PARAMETER ScriptPath Full path to the script to execute. .PARAMETER JobName Name of the job for logging and alerting. .PARAMETER LogLevel Logging level (DEBUG, INFO, WARNING, ERROR). .PARAMETER ScriptArguments Arguments to pass to the script. .PARAMETER EmailRecipients Comma-separated list of email addresses for failure alerts. .PARAMETER AlertWebhookUrl Webhook URL for failure alerts (e.g., Slack, Teams). .PARAMETER RequiredSecrets List of secret names to inject as environment variables. #> param ( [Parameter(Mandatory = $true)] [string]$ScriptPath, [Parameter(Mandatory = $true)] [string]$JobName, [string]$LogLevel = "INFO", [string[]]$ScriptArguments = @(), [string[]]$EmailRecipients = @(), [string]$AlertWebhookUrl, [string[]]$RequiredSecrets = @() ) $ErrorActionPreference = "Stop" $env:OPS_LOG_LEVEL = $LogLevel # Event Log Source $EventSource = "WinBatchOrchestrator" if (-not ([System.Diagnostics.EventLog]::SourceExists($EventSource))) { # Fallback if source doesn't exist (requires admin to create) $EventSource = "Application" } # Import Utils $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path Import-Module (Join-Path $ScriptDir "OpsUtils.psm1") -Force $LogDir = "C:\Ops\Logs" if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir -Force | Out-Null } $Timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $LogFile = Join-Path $LogDir "$JobName-$Timestamp.log" $JsonLogFile = Join-Path $LogDir "$JobName.history.json" # Appending log for history $env:OPS_LOG_FILE = $JsonLogFile Start-Transcript -Path $LogFile -Append | Out-Null # Concurrency Locking $MutexName = "Global\OpsJob-$JobName" $Mutex = $null try { $Mutex = New-Object System.Threading.Mutex($false, $MutexName) } catch { Write-Warning "Could not create Mutex. Proceeding without concurrency check." } if ($null -ne $Mutex) { if (-not $Mutex.WaitOne(0, $false)) { Write-OpsLog -Message "Job $JobName is already running. Skipping execution." -Level "WARNING" Stop-Transcript | Out-Null exit 0 } } Write-OpsLog -Message "Starting Job: $JobName" -Level "INFO" Write-OpsLog -Message "Script: $ScriptPath" -Level "INFO" Write-EventLog -LogName Application -Source $EventSource -EntryType Information -EventId 100 -Message "Starting OpsJob: $JobName" $ExitCode = 0 try { # Secret Injection foreach ($SecretName in $RequiredSecrets) { $SecretPath = Join-Path "C:\Ops\Secrets" "$SecretName.xml" if (Test-Path $SecretPath) { try { $Cred = Import-Clixml -Path $SecretPath $EnvVarName = "SECRET_$($SecretName.ToUpper())" Set-Item -Path "env:$EnvVarName" -Value $Cred.GetNetworkCredential().Password Write-OpsLog -Message "Injected secret: $SecretName as $EnvVarName" -Level "DEBUG" } catch { Write-OpsLog -Message "Failed to load secret: $SecretName" -Level "WARNING" } } else { Write-OpsLog -Message "Secret not found: $SecretName" -Level "WARNING" } } if (-not (Test-Path $ScriptPath)) { throw "Script not found: $ScriptPath" } $Extension = [System.IO.Path]::GetExtension($ScriptPath).ToLower() if ($Extension -eq ".ps1") { Write-OpsLog -Message "Executing PowerShell script..." -Level "INFO" & $ScriptPath @ScriptArguments } elseif ($Extension -eq ".js") { Write-OpsLog -Message "Executing Node.js script..." -Level "INFO" # Node.js Dependency Check $ScriptDir = Split-Path -Parent $ScriptPath if (Test-Path (Join-Path $ScriptDir "package.json")) { if (-not (Test-Path (Join-Path $ScriptDir "node_modules"))) { Write-OpsLog -Message "Installing Node.js dependencies..." -Level "INFO" $InstallProcess = Start-Process -FilePath "npm" -ArgumentList "install --production" -WorkingDirectory $ScriptDir -PassThru -Wait -NoNewWindow if ($InstallProcess.ExitCode -ne 0) { Write-OpsLog -Message "npm install failed with code $($InstallProcess.ExitCode)" -Level "WARNING" } } } $NodeArgs = @($ScriptPath) + $ScriptArguments $Process = Start-Process -FilePath "node" -ArgumentList $NodeArgs -PassThru -Wait -NoNewWindow if ($Process.ExitCode -ne 0) { throw "Node.js script exited with code $($Process.ExitCode)" } } else { throw "Unsupported script extension: $Extension" } Write-OpsLog -Message "Job completed successfully." -Level "INFO" Write-EventLog -LogName Application -Source $EventSource -EntryType Information -EventId 101 -Message "OpsJob Success: $JobName" } catch { $ExitCode = 1 $ErrorMessage = $_.Exception.Message Write-OpsLog -Message "Job Failed: $ErrorMessage" -Level "ERROR" Write-EventLog -LogName Application -Source $EventSource -EntryType Error -EventId 102 -Message "OpsJob Failed: $JobName`nError: $ErrorMessage" # Email Alert if ($EmailRecipients.Count -gt 0) { Write-OpsLog -Message "Sending alert email..." -Level "INFO" $SmtpServer = "smtp.example.com" $From = "ops-alerts@example.com" $Subject = "FAILURE: Job $JobName" $Body = "Job $JobName failed.`n`nError: $ErrorMessage`n`nSee attached log." try { Send-MailMessage -To $EmailRecipients -From $From -Subject $Subject -Body $Body -SmtpServer $SmtpServer -Attachments $LogFile -ErrorAction Stop } catch { Write-OpsLog -Message "Failed to send email: $_" -Level "ERROR" } } # Webhook Alert if (-not [string]::IsNullOrWhiteSpace($AlertWebhookUrl)) { Write-OpsLog -Message "Sending webhook alert..." -Level "INFO" $Payload = @{ text = "FAILURE: Job $JobName" attachments = @(@{ color = "danger" title = "Job Failed: $JobName" text = "Error: $ErrorMessage" fields = @( @{ title = "Script"; value = $ScriptPath; short = $false } @{ title = "Log"; value = $LogFile; short = $false } ) }) } | ConvertTo-Json -Depth 5 try { Invoke-RestMethod -Uri $AlertWebhookUrl -Method Post -Body $Payload -ContentType "application/json" -ErrorAction Stop } catch { Write-OpsLog -Message "Failed to send webhook: $_" -Level "ERROR" } } } finally { if ($null -ne $Mutex) { $Mutex.ReleaseMutex() $Mutex.Dispose() } Stop-Transcript | Out-Null if ($ExitCode -ne 0) { exit $ExitCode } } |