Skip to content

Commit bf9a028

Browse files
committed
(MODULES-8610) Add start agent job task
This commit adds a Bolt task allowing users to start SQLServer agent jobs on remote SQL Instances. The user can choose to start jobs at any step, and they can choose to wait for the job to complete before returning, or to return data immediately and let the jobs run async.
1 parent 4104356 commit bf9a028

File tree

4 files changed

+346
-0
lines changed

4 files changed

+346
-0
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
66

77
## [Unreleased]
88

9+
## Added
10+
11+
- Start agent jobs Bolt task ([MODULES-8610](https://tickets.puppetlabs.com/browse/MODULES-8610))
12+
913
## [2.4.0] - 2019-03-12
1014

1115
### Added

README.md

+42
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,48 @@ Use this parameter — especially when using the `fuzzy_match` parameter — to
11821182
Set this to true to change the behavior of the `job_name` parameter so that only jobs with names exactly matching one of the provided job_names is returned in the result set.
11831183
This is an optional parameter which accepts either `true` or `false`. It's default value is false.
11841184

1185+
#### sqlserver::start_sqlagent_jobs
1186+
1187+
##### parameters
1188+
1189+
* **instance_name**
1190+
1191+
The name of the instance to start a job on. The value defaults to the default instance on the machine.
1192+
Refer to named instances by either the short name of the instance or by `<COMPUTERNAME>\<INSTANCE_NAME>`.
1193+
Specifying an instance name will access only that instance.
1194+
Pass the values `.`, `MSSQLSERVER`, the node name, or leave blank refer to only the default instance.
1195+
This is an optional parameter which accepts a single string or array of strings as input.
1196+
1197+
* **job_name**
1198+
1199+
The name of a job to start. By default job names must be exact.
1200+
See the `fuzzy_match` parameter below for pattern matching job names.
1201+
This parameter is required and will accept a string or an array of strings as input.
1202+
1203+
* **fuzzy_match**
1204+
1205+
Modifies the behavior of the `job_name` parameter so that the value given is a job name pattern to match.
1206+
Searches are done using the PowerShell `-match` operator.
1207+
For example, if the string `backup` is passed to the `job_name` parameter, while `fuzzy_match` is set to true, jobs such as `Daily Backup` and `System Backup` will be matched and started.
1208+
This is an optional parameter which accepts either `true` or `false`. Its default value is false.
1209+
1210+
* **step**
1211+
1212+
The step number you would like to execute. Leave blank to execute at the first step.
1213+
Keep in mind that these job step numbers refer to a zero based array of job step.
1214+
If you want to execute a job starting at step 5 you need to pass 4 to this parameter.
1215+
This is because contrary to the SQLServer user interface in many places, the programming objects
1216+
this task leverages are zero based.
1217+
1218+
* **wait**
1219+
1220+
This parameter tells the task to wait until a set of jobs has completed and then return job data.
1221+
All matched jobs are started concurrently and the task waits in a loop for all of them to complete.
1222+
If this parameter is not set to true, then matched jobs are started and the task returns job data indicating that the job is executing.
1223+
Keep in mind that jobs that complete rapidly may complete before the task has finished executing.
1224+
This could result in returned job information indicating the job is currently idle. Please inspect the `lastRunDate` property of the job and the steps to determine if the job was executed successfully.
1225+
This is a boolean parameter that defaults to false.
1226+
11851227
### Microsoft SQL Server terms
11861228

11871229
Terminology differs somewhat between various database systems; please refer to this list of terms for clarification.

tasks/start_sql_agent_job.json

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"puppet_task_version": 1,
3+
"supports_noop": false,
4+
"description": "Start SQL Agent jobs on a server. You can start at a specified job step number (zero based indexes), and you can either wait on the job to complete, or return immediately.",
5+
"parameters": {
6+
"instance_name": {
7+
"description": "The instance to start a job on.",
8+
"type": "Optional[Variant[Array[String],String]]"
9+
},
10+
"job_name": {
11+
"description": "The name of the job to start.",
12+
"type": "Optional[Variant[Array[String],String]]"
13+
},
14+
"fuzzy_match": {
15+
"description": "Turn the job_name parameter into a pattern to match using the PowerShell -match operator.",
16+
"type": "Optional[Boolean]"
17+
},
18+
"step": {
19+
"description": "The zero based index number of the jop step to start from. Defaults to zero.",
20+
"type": "Optional[Integer]"
21+
},
22+
"wait": {
23+
"description": "Wait for all jobs started to complete before returning data. Defaults to false such that the task will return immediately indicating only that the job was started.",
24+
"type": "Optional[Boolean]"
25+
}
26+
}
27+
}

tasks/start_sql_agent_job.ps1

+273
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
[CmdletBinding()]
2+
param (
3+
# Instance name to return jobs from
4+
[Parameter(Mandatory = $false)]
5+
[string[]]
6+
$instance_name,
7+
8+
# Name of job or match pattern
9+
[Parameter(Mandatory = $true)]
10+
[string[]]
11+
$job_name,
12+
13+
# Use -match operator name matches.
14+
[Parameter(Mandatory = $false)]
15+
[switch]
16+
$fuzzy_match,
17+
18+
# Job step to start execution from. Zero based indexes.
19+
[Parameter(Mandatory = $false)]
20+
[int]
21+
$step = 0,
22+
23+
# Wait on the job to complete before returning results
24+
[Parameter(Mandatory = $false)]
25+
[switch]
26+
$wait
27+
)
28+
29+
function Get-SQLInstances {
30+
param(
31+
[string[]]$instance_name
32+
)
33+
34+
$instancesHolder = New-Object System.Collections.Generic.List[System.Object]
35+
$stringsToReturn = New-Object System.Collections.Generic.List[System.Object]
36+
37+
# The default instance is referred to in its service name as MSSQLSERVER. This
38+
# leads many SQLSERVER people to refer to it as such. They will also connect
39+
# to it using just a '.'. None of these are it's real name. Its real instance
40+
# name is just the machine name. A named instances real name is the machine
41+
# name a '\' and the instance name. This little foreach ensures that we are
42+
# referring to these instances by their real names so that proper filtering
43+
# can be done.
44+
45+
foreach ($name in $instance_name) {
46+
switch ($name) {
47+
{($_ -eq 'MSSQLSERVER') -or ($_ -eq '.')} { [void]$instancesHolder.add($env:COMPUTERNAME) }
48+
{$_ -eq $env:COMPUTERNAME} { [void]$instancesHolder.add($_) }
49+
{$_ -notmatch '\\'} { [void]$instancesHolder.add("$env:COMPUTERNAME\$_") }
50+
default { [void]$instancesHolder.add($name) }
51+
}
52+
}
53+
54+
if ($instancesHolder.count -eq 0) {
55+
[void]$instancesHolder.add($env:computername)
56+
}
57+
58+
$instanceStrings = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server').InstalledInstances
59+
60+
# The registry key does not return the real instance names. Again we must
61+
# normalize these names into their real names so that comparisons can be done
62+
# properly.
63+
64+
foreach ($string in $instanceStrings) {
65+
switch ($string) {
66+
'MSSQLSERVER' { $string = $env:COMPUTERNAME }
67+
Default {$string = "$env:COMPUTERNAME\$string"}
68+
}
69+
70+
foreach ($instance in $instancesHolder) {
71+
if ($instance -eq $string) {
72+
[void]$stringsToReturn.add($string)
73+
}
74+
}
75+
}
76+
77+
if ($stringsToReturn.count -gt 0) {
78+
Write-Output $stringsToReturn
79+
}
80+
else {
81+
throw "No instances were found by the name(s) $instance_name"
82+
}
83+
}
84+
85+
function Get-ServerObject {
86+
param(
87+
[string]$instance
88+
)
89+
90+
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo")
91+
92+
Write-Output (New-Object Microsoft.SqlServer.Management.Smo.Server -ArgumentList $instance)
93+
}
94+
95+
function Select-Job {
96+
param(
97+
[PSObject]$job,
98+
[string[]]$jobsToMatch,
99+
[switch]$fuzzy_match
100+
)
101+
102+
103+
# This function takes a single SQLServer job object and compares it against
104+
# the list of names passed into the -job_name parameter of the script to
105+
# determine if this is a job the user is interested in seeing. If it does
106+
# not pass the filter represented by that parameter the job is discarded.
107+
108+
foreach ($paramJob in $jobsToMatch) {
109+
if (!$fuzzy_match) {
110+
if ($paramJob -eq $job.name) {
111+
Write-Output $job
112+
}
113+
}
114+
else {
115+
# Match is a regex operator, and it doesn't like the '\' in domain names.
116+
if ($job.name -match [regex]::escape($paramJob)) {
117+
Write-Output $job
118+
}
119+
}
120+
}
121+
}
122+
123+
function New-CustomJobObject {
124+
param(
125+
[psobject]$job
126+
)
127+
128+
$customObject = @{
129+
name = $job.name
130+
description = $job.description
131+
enabled = $job.isEnabled
132+
ownerLoginName = $job.ownerLoginName
133+
instance = $job.parent.name
134+
lastRunDate = $job.lastRunDate
135+
lastRunOutcome = [string]$job.lastRunOutcome
136+
currentRunStatus = [string]$job.currentRunStatus
137+
currentRunStep = $job.currentRunStep
138+
startStepID = $job.startStepID
139+
currentRunRetryAttempt = $job.currentRunRetryAttempt
140+
nextRunDate = $job.nextRunDate
141+
dateCreated = $job.dateCreated
142+
dateLastModified = $job.dateLastModified
143+
emailLevel = $job.emailLevel
144+
operatorToEmail = $job.operatorToEmail
145+
operatorToNetSend = $job.operatorToNetSend
146+
operatorToPage = $job.operatorToPage
147+
category = $job.category
148+
steps = New-Object System.Collections.Generic.List[System.Object]
149+
}
150+
151+
foreach ($step in $job.jobSteps) {
152+
$step = @{
153+
name = $step.name
154+
type = [string]$step.subsystem
155+
databaseName = $step.DatabaseName
156+
lastRunDate = $step.lastRunDate
157+
lastRunDurationSeconds = $step.LastRunDuration
158+
lastRunOutcome = [string]$step.LastRunOutcome
159+
lastRunRetries = $step.lastRunRetries
160+
onFailAction = [string]$step.onFailAction
161+
onFailStep = $step.onFailStep
162+
onSuccessAction = [string]$step.onSuccessAction
163+
onSuccessStep = $step.onSuccessStep
164+
retryAttempts = $step.retryAttempts
165+
retryIntervalMinutes = $step.retryInterval
166+
}
167+
[void]$customObject.steps.add($step)
168+
}
169+
170+
Write-Output $customObject
171+
}
172+
173+
174+
$errorReturn = @{
175+
_error = @{
176+
msg = ''
177+
kind = 'puppetlabs.task/task-error'
178+
details = @{
179+
detailedInfo = ''
180+
exitcode = 1
181+
}
182+
}
183+
}
184+
185+
$return = @{jobs = New-Object System.Collections.Generic.List[System.Object]}
186+
187+
$jobs = New-Object System.Collections.Generic.List[System.Object]
188+
$finishedJobs = New-Object System.Collections.Generic.List[System.Object]
189+
190+
try {
191+
$SQLInstances = Get-SQLInstances -instance_name $instance_name
192+
}
193+
catch {
194+
$errorReturn._error.msg = 'Cannot detect SQL instance names.'
195+
$errorReturn._error.details.detailedInfo = $_
196+
return $errorReturn | ConvertTo-JSON
197+
}
198+
199+
foreach ($instance in $SQLInstances) {
200+
try {
201+
$sqlServer = Get-ServerObject -instance $instance
202+
}
203+
catch {
204+
$errorReturn._error.msg = "Cannot connect to SQL Instance: $instance"
205+
$errorReturn._error.details.detailedInfo = $_
206+
return $errorReturn | ConvertTo-JSON
207+
}
208+
209+
foreach ($currentJob in $sqlserver.jobserver.jobs) {
210+
if ($MyInvocation.BoundParameters.ContainsKey('job_name')) {
211+
if ($selectedJob = (Select-Job -job $currentJob -jobsToMatch $job_name -fuzzy_match:$fuzzy_match)) {
212+
[void]$jobs.add($selectedJob)
213+
$jobName = $selectedJob.jobsteps[$step].name
214+
if([string]::IsNullOrEmpty($jobName)){
215+
$errorReturn._error.msg = `
216+
("No job step found at index {0}. There are {1} steps in the job `"{2}`". Remember that these are zero based array indexes." `
217+
-f $step, $selectedJob.jobSteps.count, $selectedJob.name)
218+
$errorReturn._error.details.detailedInfo = $_
219+
return $errorReturn | ConvertTo-JSON
220+
}
221+
$selectedJob.start($jobName)
222+
# It takes the server a little time to spin up the job. If we don't do this here
223+
# then the -wait parameter may not work later.
224+
Start-Sleep -Milliseconds 300
225+
}
226+
}
227+
else {
228+
[void]$jobs.add($currentJob)
229+
$jobName = $selectedJob.jobsteps[$step].name
230+
if([string]::IsNullOrEmpty($jobName)){
231+
$errorReturn._error.msg = `
232+
("No job step found at index {0}. There are {1} steps in the job `"{2}`". Remember that these are zero based array indexes." `
233+
-f $step, $selectedJob.jobSteps.count, $selectedJob.name)
234+
$errorReturn._error.details.detailedInfo = $_
235+
return $errorReturn | ConvertTo-JSON
236+
}
237+
$selectedJob.start($jobName)
238+
Start-Sleep -Milliseconds 300
239+
}
240+
}
241+
}
242+
243+
do {
244+
$done = $true
245+
$finishedJobs.Clear()
246+
foreach ($job in $jobs) {
247+
$job.refresh()
248+
if ([string]$job.currentRunStatus -ne 'idle') {
249+
$done = $false
250+
}
251+
[void]$finishedJobs.add((New-CustomJobObject -job $job))
252+
}
253+
} while ($wait -and !$done)
254+
255+
$return.jobs = $finishedJobs
256+
257+
return ($return | ConvertTo-JSON -Depth 99)
258+
259+
<#
260+
.SYNOPSIS
261+
This script connects to a SQL instance running on a machine and starts agent
262+
jobs.
263+
.DESCRIPTION
264+
This script will connect to SQL instances running on a machine and start agent
265+
jobs. It allows you to start the job at a specific step. Job steps are
266+
specified by integer index numbers and indexes are zero based. You can either
267+
wait for the job to complete, or you can let the task finish and return only
268+
information indicating the the job is now running. It will always return data
269+
in JSON format.
270+
.PARAMETER instance_name
271+
The name of the instance running on a machine that you would like to connect to.
272+
Leave blank to get the default instance MSSQLSERVER.
273+
#>

0 commit comments

Comments
 (0)