|
| 1 | +function Copy-DbaAgentJobStep { |
| 2 | + <# |
| 3 | + .SYNOPSIS |
| 4 | + Copies job steps from one SQL Server Agent job to another, preserving job history by synchronizing steps without dropping the job itself. |
| 5 | +
|
| 6 | + .DESCRIPTION |
| 7 | + Synchronizes SQL Server Agent job steps between instances by copying step definitions from source jobs to destination jobs. Unlike Copy-DbaAgentJob with -Force, this command preserves job execution history because it only drops and recreates individual steps rather than the entire job. This is essential for maintaining historical job execution data in Always On Availability Group scenarios, disaster recovery environments, or when deploying step modifications across multiple servers. |
| 8 | +
|
| 9 | + The function removes all existing steps from the destination job before copying source steps, ensuring a clean synchronization. Job metadata like ownership, schedules, and alerts remain unchanged on the destination. |
| 10 | +
|
| 11 | + .PARAMETER Source |
| 12 | + Source SQL Server instance containing the jobs with steps to copy. You must have sysadmin access and server version must be SQL Server 2000 or higher. |
| 13 | + Use this when copying job steps from a specific instance rather than piping job objects with InputObject. |
| 14 | +
|
| 15 | + .PARAMETER SourceSqlCredential |
| 16 | + Alternative credentials for connecting to the source SQL Server instance. Accepts PowerShell credentials (Get-Credential). |
| 17 | + Use this when the source server requires different authentication than your current Windows session, such as SQL authentication or cross-domain scenarios. |
| 18 | + Windows Authentication, SQL Server Authentication, Active Directory - Password, and Active Directory - Integrated are all supported. |
| 19 | +
|
| 20 | + .PARAMETER Destination |
| 21 | + Destination SQL Server instance(s) where job steps will be synchronized. You must have sysadmin access and the server must be SQL Server 2000 or higher. |
| 22 | + Supports multiple destinations to copy job steps to multiple servers simultaneously, such as syncing all AG replicas or DR servers. |
| 23 | +
|
| 24 | + .PARAMETER DestinationSqlCredential |
| 25 | + Alternative credentials for connecting to the destination SQL Server instance. Accepts PowerShell credentials (Get-Credential). |
| 26 | + Use this when the destination server requires different authentication than your current Windows session, such as SQL authentication or cross-domain scenarios. |
| 27 | + Windows Authentication, SQL Server Authentication, Active Directory - Password, and Active Directory - Integrated are all supported. |
| 28 | +
|
| 29 | + .PARAMETER Job |
| 30 | + Specifies which SQL Agent jobs to process by name. Accepts wildcards and multiple job names. |
| 31 | + Use this to synchronize steps for specific jobs, such as copying modified steps from a primary AG replica to secondary replicas. |
| 32 | + If unspecified, all jobs will be processed. |
| 33 | +
|
| 34 | + .PARAMETER ExcludeJob |
| 35 | + Specifies which SQL Agent jobs to skip during the copy operation. Accepts wildcards and multiple job names. |
| 36 | + Use this to exclude specific jobs from bulk operations, such as skipping environment-specific jobs that shouldn't be synchronized. |
| 37 | +
|
| 38 | + .PARAMETER Step |
| 39 | + Specifies which job steps to copy by name. If not specified, all steps are copied. |
| 40 | + Use this to synchronize specific steps rather than all steps from a job. |
| 41 | +
|
| 42 | + .PARAMETER InputObject |
| 43 | + Accepts SQL Agent job objects from the pipeline, typically from Get-DbaAgentJob. |
| 44 | + Use this to copy steps for pre-filtered jobs or when combining with other job management cmdlets for complex workflows. |
| 45 | +
|
| 46 | + .PARAMETER WhatIf |
| 47 | + If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. |
| 48 | +
|
| 49 | + .PARAMETER Confirm |
| 50 | + If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. |
| 51 | +
|
| 52 | + .PARAMETER EnableException |
| 53 | + By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. |
| 54 | + This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. |
| 55 | + Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. |
| 56 | +
|
| 57 | + .NOTES |
| 58 | + Tags: Migration, Agent, Job |
| 59 | + Author: the dbatools team + Claude |
| 60 | +
|
| 61 | + Website: https://dbatools.io |
| 62 | + Copyright: (c) 2018 by dbatools, licensed under MIT |
| 63 | + License: MIT https://opensource.org/licenses/MIT |
| 64 | +
|
| 65 | + .LINK |
| 66 | + https://dbatools.io/Copy-DbaAgentJobStep |
| 67 | +
|
| 68 | + .EXAMPLE |
| 69 | + PS C:\> Copy-DbaAgentJobStep -Source PrimaryAG -Destination SecondaryAG1, SecondaryAG2 -Job "MaintenanceJob" |
| 70 | +
|
| 71 | + Copies all job steps from the "MaintenanceJob" on PrimaryAG to the same job on SecondaryAG1 and SecondaryAG2, preserving job history on the destination servers. |
| 72 | +
|
| 73 | + .EXAMPLE |
| 74 | + PS C:\> Get-DbaAgentJob -SqlInstance PrimaryAG -Job "BackupJob" | Copy-DbaAgentJobStep -Destination SecondaryAG1 |
| 75 | +
|
| 76 | + Retrieves the BackupJob from PrimaryAG and synchronizes its steps to the same job on SecondaryAG1 using pipeline input. |
| 77 | +
|
| 78 | + .EXAMPLE |
| 79 | + PS C:\> Copy-DbaAgentJobStep -Source sqlserver2014a -Destination sqlcluster -Job "DataETL" -SourceSqlCredential $cred |
| 80 | +
|
| 81 | + Copies job steps for the "DataETL" job from sqlserver2014a to sqlcluster, using SQL credentials for the source server and Windows credentials for the destination. |
| 82 | +
|
| 83 | + .EXAMPLE |
| 84 | + PS C:\> Copy-DbaAgentJobStep -Source Primary -Destination Replica1, Replica2, Replica3 |
| 85 | +
|
| 86 | + Synchronizes all job steps from Primary to multiple AG replicas, ensuring all replicas have identical job step definitions while preserving their individual job execution histories. |
| 87 | + #> |
| 88 | + [CmdletBinding(DefaultParameterSetName = "Default", SupportsShouldProcess, ConfirmImpact = "Medium")] |
| 89 | + param ( |
| 90 | + [DbaInstanceParameter]$Source, |
| 91 | + [PSCredential]$SourceSqlCredential, |
| 92 | + [parameter(Mandatory)] |
| 93 | + [DbaInstanceParameter[]]$Destination, |
| 94 | + [PSCredential]$DestinationSqlCredential, |
| 95 | + [object[]]$Job, |
| 96 | + [object[]]$ExcludeJob, |
| 97 | + [string[]]$Step, |
| 98 | + [parameter(ValueFromPipeline)] |
| 99 | + [Microsoft.SqlServer.Management.Smo.Agent.Job[]]$InputObject, |
| 100 | + [switch]$EnableException |
| 101 | + ) |
| 102 | + begin { |
| 103 | + if ($Source) { |
| 104 | + try { |
| 105 | + $splatGetJob = @{ |
| 106 | + SqlInstance = $Source |
| 107 | + SqlCredential = $SourceSqlCredential |
| 108 | + } |
| 109 | + if (Test-Bound "Job") { |
| 110 | + $splatGetJob["Job"] = $Job |
| 111 | + } |
| 112 | + if (Test-Bound "ExcludeJob") { |
| 113 | + $splatGetJob["ExcludeJob"] = $ExcludeJob |
| 114 | + } |
| 115 | + $InputObject = Get-DbaAgentJob @splatGetJob |
| 116 | + } catch { |
| 117 | + Stop-Function -Message "Error occurred while establishing connection to $Source" -Category ConnectionError -ErrorRecord $_ -Target $Source |
| 118 | + return |
| 119 | + } |
| 120 | + } |
| 121 | + } |
| 122 | + process { |
| 123 | + if (Test-FunctionInterrupt) { return } |
| 124 | + foreach ($destinstance in $Destination) { |
| 125 | + try { |
| 126 | + $destServer = Connect-DbaInstance -SqlInstance $destinstance -SqlCredential $DestinationSqlCredential |
| 127 | + } catch { |
| 128 | + Stop-Function -Message "Failure" -Category ConnectionError -ErrorRecord $_ -Target $destinstance -Continue |
| 129 | + } |
| 130 | + $destJobs = $destServer.JobServer.Jobs |
| 131 | + |
| 132 | + foreach ($sourceJob in $InputObject) { |
| 133 | + $jobName = $sourceJob.Name |
| 134 | + $sourceserver = $sourceJob.Parent.Parent |
| 135 | + |
| 136 | + $copyJobStepStatus = [PSCustomObject]@{ |
| 137 | + SourceServer = $sourceserver.Name |
| 138 | + DestinationServer = $destServer.Name |
| 139 | + Name = $jobName |
| 140 | + Type = "Agent Job Steps" |
| 141 | + Status = $null |
| 142 | + Notes = $null |
| 143 | + DateTime = [DbaDateTime](Get-Date) |
| 144 | + } |
| 145 | + |
| 146 | + if ((Test-Bound "Job") -and $jobName -notin $Job) { |
| 147 | + Write-Message -Level Verbose -Message "Job [$jobName] filtered. Skipping." |
| 148 | + continue |
| 149 | + } |
| 150 | + if ((Test-Bound "ExcludeJob") -and $jobName -in $ExcludeJob) { |
| 151 | + Write-Message -Level Verbose -Message "Job [$jobName] excluded. Skipping." |
| 152 | + continue |
| 153 | + } |
| 154 | + Write-Message -Message "Working on job: $jobName" -Level Verbose |
| 155 | + |
| 156 | + if ($destJobs.name -notcontains $sourceJob.name) { |
| 157 | + if ($Pscmdlet.ShouldProcess($destinstance, "Job $jobName does not exist on destination. Skipping step synchronization.")) { |
| 158 | + $copyJobStepStatus.Status = "Skipped" |
| 159 | + $copyJobStepStatus.Notes = "Job does not exist on destination. Use Copy-DbaAgentJob to create it first." |
| 160 | + $copyJobStepStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject |
| 161 | + Write-Message -Level Warning -Message "Job $jobName does not exist on destination $destinstance. Use Copy-DbaAgentJob to create it first." |
| 162 | + } |
| 163 | + continue |
| 164 | + } |
| 165 | + |
| 166 | + # Filter source steps if Step parameter is specified |
| 167 | + $sourceSteps = $sourceJob.JobSteps |
| 168 | + if (Test-Bound "Step") { |
| 169 | + $sourceSteps = $sourceSteps | Where-Object Name -in $Step |
| 170 | + if (-not $sourceSteps) { |
| 171 | + Write-Message -Level Warning -Message "No matching steps found in job $jobName for specified step names: $($Step -join ', ')" |
| 172 | + continue |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + if ($Pscmdlet.ShouldProcess($destinstance, "Synchronizing steps for job $jobName")) { |
| 177 | + try { |
| 178 | + $destJob = $destServer.JobServer.Jobs[$jobName] |
| 179 | + |
| 180 | + # Remove existing steps - copy to array first to avoid collection modification during enumeration |
| 181 | + $stepsToRemove = @($destJob.JobSteps | ForEach-Object { $_ }) |
| 182 | + if (Test-Bound "Step") { |
| 183 | + $stepsToRemove = $stepsToRemove | Where-Object Name -in $Step |
| 184 | + } |
| 185 | + |
| 186 | + Write-Message -Message "Removing $($stepsToRemove.Count) existing step(s) from $jobName on $destinstance" -Level Verbose |
| 187 | + foreach ($stepToRemove in $stepsToRemove) { |
| 188 | + Write-Message -Message "Removing step $($stepToRemove.Name) from $jobName on $destinstance" -Level Verbose |
| 189 | + $stepToRemove.Drop() |
| 190 | + } |
| 191 | + $destJob.JobSteps.Refresh() |
| 192 | + |
| 193 | + Write-Message -Message "Copying $($sourceSteps.Count) step(s) from $jobName to $destinstance" -Level Verbose |
| 194 | + foreach ($sourceStep in $sourceSteps) { |
| 195 | + Write-Message -Message "Creating step $($sourceStep.Name) in $jobName on $destinstance" -Level Verbose |
| 196 | + $sql = $sourceStep.Script() | Out-String |
| 197 | + # Replace @job_id with @job_name since the destination job has a different GUID |
| 198 | + $sql = $sql -replace "@job_id=N'[0-9a-fA-F-]+'", "@job_name=N'$($jobName -replace "'", "''")'" |
| 199 | + Write-Message -Message $sql -Level Debug |
| 200 | + $destServer.Query($sql) |
| 201 | + } |
| 202 | + |
| 203 | + $destJob.JobSteps.Refresh() |
| 204 | + $copyJobStepStatus.Status = "Successful" |
| 205 | + $copyJobStepStatus.Notes = "Synchronized $($sourceSteps.Count) job step(s)" |
| 206 | + $copyJobStepStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject |
| 207 | + } catch { |
| 208 | + $copyJobStepStatus.Status = "Failed" |
| 209 | + $copyJobStepStatus.Notes = (Get-ErrorMessage -Record $_) |
| 210 | + $copyJobStepStatus | Select-DefaultView -Property DateTime, SourceServer, DestinationServer, Name, Type, Status, Notes -TypeName MigrationObject |
| 211 | + Stop-Function -Message "Failed to synchronize steps for job $jobName on $destinstance" -ErrorRecord $_ -Target $destinstance -Continue |
| 212 | + } |
| 213 | + } |
| 214 | + } |
| 215 | + } |
| 216 | + } |
| 217 | +} |
0 commit comments