Public/New-Runbook.ps1
|
function New-Runbook { <# .SYNOPSIS Creates a new runbook from a built-in template or generates one using AI from ITSM ticket history. .DESCRIPTION Provides two methods for creating runbooks: 1. From Template: Copies a built-in template to the output location with optional customization. 2. From Ticket History: Queries an ITSM provider (ServiceNow, Jira, or CSV) for tickets related to a CI, then uses AI to analyze patterns and generate a YAML runbook for common issues. Generated runbooks follow the standard decision-tree YAML format used by Invoke-Runbook. .PARAMETER Name Name for the new runbook (used as the filename without extension). .PARAMETER FromTemplate Name of a built-in template to use as the base (e.g., 'high-cpu', 'disk-space'). .PARAMETER FromTicketHistory Switch to generate a runbook from ITSM ticket history using AI. .PARAMETER ITSMProvider ITSM provider type: ServiceNow, Jira, or CSV. .PARAMETER ITSMEndpoint URL or path to the ITSM source (API endpoint or CSV file path). .PARAMETER ITSMCredential PSCredential for authenticating with the ITSM provider. .PARAMETER CIName Configuration Item name to generate the runbook for. .PARAMETER Provider AI provider for runbook generation: Anthropic, OpenAI, Ollama, or Custom. .PARAMETER ApiKey API key for the AI provider. .PARAMETER Model Specific model to use with the AI provider. .PARAMETER OutputPath Directory where the generated YAML file will be saved. Defaults to the engine runbooks directory. .EXAMPLE New-Runbook -Name 'custom-cpu' -FromTemplate 'high-cpu' -OutputPath 'C:\Runbooks' Create a new runbook based on the high-cpu template. .EXAMPLE New-Runbook -Name 'webserver-issues' -FromTicketHistory -ITSMProvider ServiceNow -ITSMEndpoint 'https://instance.service-now.com' -ITSMCredential $cred -CIName 'WEB01' -Provider OpenAI -ApiKey $key Generate a runbook from ServiceNow tickets about WEB01 using OpenAI. .EXAMPLE New-Runbook -Name 'db-recovery' -FromTicketHistory -ITSMProvider CSV -ITSMEndpoint 'C:\tickets.csv' -CIName 'SQL01' -Provider Ollama Generate a runbook from a CSV file of tickets using a local Ollama model. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$Name, [Parameter(ParameterSetName = 'Template')] [string]$FromTemplate, [Parameter(ParameterSetName = 'TicketHistory')] [switch]$FromTicketHistory, [Parameter(ParameterSetName = 'TicketHistory')] [ValidateSet('ServiceNow', 'Jira', 'CSV')] [string]$ITSMProvider = 'ServiceNow', [Parameter(ParameterSetName = 'TicketHistory')] [string]$ITSMEndpoint, [Parameter(ParameterSetName = 'TicketHistory')] [PSCredential]$ITSMCredential, [Parameter(ParameterSetName = 'TicketHistory')] [string]$CIName, [Parameter(ParameterSetName = 'TicketHistory')] [ValidateSet('Anthropic', 'OpenAI', 'Ollama', 'Custom')] [string]$Provider = 'OpenAI', [Parameter(ParameterSetName = 'TicketHistory')] [string]$ApiKey, [Parameter(ParameterSetName = 'TicketHistory')] [string]$Model, [Parameter()] [string]$OutputPath ) # Determine output directory if (-not $OutputPath) { $OutputPath = Join-Path $env:USERPROFILE '.runbookengine\runbooks' } if (-not (Test-Path $OutputPath)) { New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null } $outputFile = Join-Path $OutputPath "$Name.yml" if ($FromTemplate) { # Copy from built-in template $templateDir = Join-Path $PSScriptRoot '..\Templates' $templateFile = Join-Path $templateDir "$FromTemplate.yml" if (-not (Test-Path $templateFile)) { $templateFile = Join-Path $templateDir "$FromTemplate.yaml" } if (-not (Test-Path $templateFile)) { # List available templates $available = Get-ChildItem -Path $templateDir -Filter '*.yml' -ErrorAction SilentlyContinue | ForEach-Object { $_.BaseName } throw "Template '$FromTemplate' not found. Available templates: $($available -join ', ')" } Copy-Item -Path $templateFile -Destination $outputFile -Force # Update the name in the copied file $content = Get-Content -Path $outputFile -Raw $content = $content -replace '^name:.*$', "name: $Name" -replace "(?m)^name:.*$", "name: $Name" Set-Content -Path $outputFile -Value $content -Encoding UTF8 Write-Host "Runbook created from template '$FromTemplate': $outputFile" -ForegroundColor Green return $outputFile } if ($FromTicketHistory) { if (-not $CIName) { throw "CIName is required when generating from ticket history." } Write-Host "Fetching ticket history for CI: $CIName..." -ForegroundColor Cyan # Fetch tickets from ITSM $tickets = @() switch ($ITSMProvider) { 'ServiceNow' { if (-not $ITSMEndpoint) { throw "ITSMEndpoint is required for ServiceNow." } if (-not $ITSMCredential) { throw "ITSMCredential is required for ServiceNow." } $headers = @{ 'Accept' = 'application/json' 'Content-Type' = 'application/json' } $encodedCI = [System.Uri]::EscapeDataString($CIName) $uri = "$($ITSMEndpoint.TrimEnd('/'))/api/now/table/incident?sysparm_query=cmdb_ci.name=$encodedCI&sysparm_limit=200&sysparm_fields=number,short_description,description,category,subcategory,priority,state,resolved_at,resolution_code,close_notes" try { $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers ` -Credential $ITSMCredential -ErrorAction Stop $tickets = $response.result | ForEach-Object { [PSCustomObject]@{ Number = $_.number Summary = $_.short_description Description = $_.description Category = $_.category SubCategory = $_.subcategory Priority = $_.priority State = $_.state Resolution = $_.close_notes } } } catch { throw "Failed to fetch tickets from ServiceNow: $_" } } 'Jira' { if (-not $ITSMEndpoint) { throw "ITSMEndpoint is required for Jira." } if (-not $ITSMCredential) { throw "ITSMCredential is required for Jira." } $username = $ITSMCredential.UserName $password = $ITSMCredential.GetNetworkCredential().Password $base64Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${username}:${password}")) $headers = @{ 'Authorization' = "Basic $base64Auth" 'Content-Type' = 'application/json' } $jql = [System.Uri]::EscapeDataString("labels = `"$CIName`" OR summary ~ `"$CIName`" ORDER BY created DESC") $uri = "$($ITSMEndpoint.TrimEnd('/'))/rest/api/2/search?jql=$jql&maxResults=200&fields=key,summary,description,priority,status,resolution,comment" try { $response = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers -ErrorAction Stop $tickets = $response.issues | ForEach-Object { [PSCustomObject]@{ Number = $_.key Summary = $_.fields.summary Description = $_.fields.description Category = 'Incident' SubCategory = '' Priority = $_.fields.priority.name State = $_.fields.status.name Resolution = if ($_.fields.resolution) { $_.fields.resolution.name } else { '' } } } } catch { throw "Failed to fetch tickets from Jira: $_" } } 'CSV' { if (-not $ITSMEndpoint) { throw "ITSMEndpoint (CSV file path) is required." } if (-not (Test-Path $ITSMEndpoint)) { throw "CSV file not found: $ITSMEndpoint" } try { $csvData = Import-Csv -Path $ITSMEndpoint -ErrorAction Stop # Filter for the CI if there's a CI or ComputerName column $ciColumn = $csvData[0].PSObject.Properties.Name | Where-Object { $_ -in @('CI', 'CIName', 'ComputerName', 'Server', 'Asset') } | Select-Object -First 1 if ($ciColumn) { $csvData = $csvData | Where-Object { $_.$ciColumn -like "*$CIName*" } } $tickets = $csvData | ForEach-Object { [PSCustomObject]@{ Number = if ($_.Number) { $_.Number } elseif ($_.TicketId) { $_.TicketId } else { $_.Key } Summary = if ($_.Summary) { $_.Summary } elseif ($_.Title) { $_.Title } else { $_.short_description } Description = if ($_.Description) { $_.Description } else { '' } Category = if ($_.Category) { $_.Category } else { 'Unknown' } SubCategory = if ($_.SubCategory) { $_.SubCategory } else { '' } Priority = if ($_.Priority) { $_.Priority } else { 'Medium' } State = if ($_.State) { $_.State } elseif ($_.Status) { $_.Status } else { 'Closed' } Resolution = if ($_.Resolution) { $_.Resolution } elseif ($_.close_notes) { $_.close_notes } else { '' } } } } catch { throw "Failed to parse CSV file: $_" } } } if ($tickets.Count -eq 0) { Write-Warning "No tickets found for CI '$CIName'. Creating a basic runbook template." $yamlContent = @" name: $Name version: "1.0" description: Auto-generated runbook for $CIName trigger: metric: manual threshold: 0 parameters: - name: ComputerName required: true steps: - id: initial_check action: script description: Check current system status script: | Get-CimInstance Win32_OperatingSystem -ComputerName `$ComputerName | Select-Object CSName, LastBootUpTime, FreePhysicalMemory, TotalVisibleMemorySize outputs: - system_status - id: review action: notify description: Manual review required message: "Review system status for `$ComputerName and determine next steps." include_data: system_status "@ Set-Content -Path $outputFile -Value $yamlContent -Encoding UTF8 Write-Host "Basic runbook created: $outputFile" -ForegroundColor Yellow return $outputFile } Write-Host "Found $($tickets.Count) tickets. Analyzing with AI..." -ForegroundColor Cyan # Prepare ticket summary for AI $ticketSummary = $tickets | ForEach-Object { "Ticket: $($_.Number)`nSummary: $($_.Summary)`nCategory: $($_.Category)`nPriority: $($_.Priority)`nResolution: $($_.Resolution)`n---" } $ticketText = $ticketSummary -join "`n" # Trim if too long if ($ticketText.Length -gt 12000) { $ticketText = $ticketText.Substring(0, 12000) + "`n... (truncated)" } $aiPrompt = @" Analyze the following IT support tickets for the server/CI named "$CIName" and generate a YAML runbook that addresses the most common issues found in these tickets. The runbook should follow this exact YAML structure: - name: descriptive name - version: "1.0" - description: what this runbook handles - trigger: metric, threshold, duration_minutes - parameters: list with name, required, default - steps: list where each step has id, action (script/decision/integration/notify/escalate), description - script steps: include a PowerShell script field and optionally outputs, verify - decision steps: include condition, if_true, if_false - notify steps: include message - escalate steps: include priority, message For script steps, use real PowerShell commands that would work on Windows servers. Use `$ComputerName as the target variable. Identify the top 3-5 most common issue patterns from the tickets and build a decision tree that: 1. Diagnoses the issue 2. Checks for common causes 3. Attempts automated remediation where safe 4. Escalates when automation cannot resolve TICKET DATA: $ticketText Generate ONLY the YAML content, no explanations or markdown code fences. "@ $systemPrompt = 'You are an expert infrastructure automation engineer. Generate precise, production-ready YAML runbooks for Windows server remediation. Output only valid YAML with no markdown formatting.' try { $aiParams = @{ Prompt = $aiPrompt Provider = $Provider SystemPrompt = $systemPrompt } if ($ApiKey) { $aiParams['ApiKey'] = $ApiKey } if ($Model) { $aiParams['Model'] = $Model } $generatedYaml = Invoke-AICompletion @aiParams # Clean up AI response - remove any markdown fences $generatedYaml = $generatedYaml -replace '```yaml\s*', '' -replace '```\s*', '' $generatedYaml = $generatedYaml.Trim() # Basic validation if ($generatedYaml -notmatch 'name:' -or $generatedYaml -notmatch 'steps:') { Write-Warning "AI response does not appear to be valid runbook YAML. Saving raw output." } Set-Content -Path $outputFile -Value $generatedYaml -Encoding UTF8 Write-Host "AI-generated runbook created: $outputFile" -ForegroundColor Green Write-Host "Review the generated runbook before use. Verify all scripts and conditions." -ForegroundColor Yellow } catch { throw "Failed to generate runbook via AI: $_" } return $outputFile } # If neither template nor ticket history, create a blank runbook $blankYaml = @" name: $Name version: "1.0" description: Custom runbook - add description here trigger: metric: manual threshold: 0 parameters: - name: ComputerName required: true steps: - id: step_1 action: script description: First diagnostic step script: | Write-Output "Running diagnostics on `$ComputerName" outputs: - diagnostic_result - id: evaluate action: decision description: Evaluate diagnostic results condition: "`$diagnostic_result -ne `$null" if_true: remediate if_false: escalate - id: remediate action: script description: Apply remediation requires_approval: true blast_radius: single_service script: | Write-Output "Remediating issue on `$ComputerName" - id: escalate action: escalate description: Escalate to admin priority: medium message: "Issue on `$ComputerName requires manual intervention" "@ Set-Content -Path $outputFile -Value $blankYaml -Encoding UTF8 Write-Host "Blank runbook created: $outputFile" -ForegroundColor Green Write-Host "Edit the runbook to add your custom logic." -ForegroundColor Yellow return $outputFile } |