Skip to content

Commit b62b258

Browse files
authored
feat: add outputfile param for agent job steps (#329)
* feat: add outputfile param for agent job steps * tweaks * fix step fetching * whitespace * Update agent_job_step.ps1 * changelog
1 parent fae704b commit b62b258

6 files changed

Lines changed: 210 additions & 10 deletions

File tree

.github/copilot-instructions.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
Purpose
2+
3+
This repository is an Ansible Collection for managing SQL Server using the PowerShell dbatools module. Modules are implemented in PowerShell (*.ps1) with small Python documentation shims (*.py). The collection targets cross-platform control nodes (Linux/macOS/Windows) but talks to SQL Server (Windows-style paths, SMO) on Windows servers.
4+
5+
Primary languages & tools
6+
7+
- PowerShell (modules under `plugins/modules/*.ps1` and module_utils/*.psm1)
8+
- Python (module documentation files under `plugins/modules/*.py`)
9+
- YAML (integration tests under `tests/integration/targets/**`)
10+
- dbatools PowerShell module (runtime dependency)
11+
12+
High-level guidance for Copilot edits/suggestions
13+
14+
- Preserve idempotency. Modules must set `module.Result.changed` only when actual server-side state changes. Prefer reading server state with dbatools `Get-*` functions and comparing to the desired state.
15+
- Prefer using dbatools functions (Get-DbaAgentJob, Set-DbaAgentJob, Get-DbaAgentJobOutputFile, Set-DbaAgentJobOutputFile, etc.) rather than manipulating SMO internals directly.
16+
- Prefer using PSDefaultParameterValues when possible to avoid redundant parameter parsing.
17+
- Use the shared helpers in `plugins/module_utils/_SqlServerUtils.psm1` (e.g., `Get-LowlyDbaSqlServerAuthSpec`, `Get-SqlCredential`, `ConvertTo-SerializableObject`) instead of reimplementing them.
18+
- For cross-platform code running on Linux control nodes: avoid using Windows-only path APIs when comparing paths returned by SQL Server tools. Either let the server report the canonical value and compare server-before vs server-after, or perform simple string normalization that is safe on all hosts.
19+
- Respect `check_mode` by not performing actual changes and by returning what would be changed.
20+
- When calling dbatools setters that mutate SMO state, prefer to re-read the server-reported value (Get-*) and use that for the module `data` return and change detection. If SMO latency is a concern, implement a small retry (few attempts with short sleeps) when reading back the value — do not add long sleeps in tests.
21+
- When serializing SMO objects for `module.Result.data`, use `ConvertTo-SerializableObject` to avoid recursion and platform-specific type issues.
22+
23+
Code style & constraints
24+
25+
- Keep PowerShell code compatible with PowerShell Core and Windows PowerShell where practical.
26+
- Use Ansible's AnsibleModule patterns: module creation via `[Ansible.Basic.AnsibleModule]::Create($args, $spec, @(Get-LowlyDbaSqlServerAuthSpec))` and `ExitJson()` / `FailJson()` for outputs.
27+
- Avoid printing or writing secrets; mark password options `no_log` in specs.
28+
29+
Testing guidance
30+
31+
- Integration tests live in `tests/integration/targets/*`. Tests expect a Windows SQL Server target (CI uses ephemeral VMs/containers). Running tests locally requires a reachable SQL Server instance and credentials in `tests/integration/integration_config.yml`.
32+
- Add/adjust unit tests for any helper logic in `plugins/module_utils` if you change normalization/serialization.
33+
- Keep tests deterministic: avoid brittle timing assumptions; use short retries for SMO refresh instead of fixed long sleeps.
34+
35+
Documentation
36+
37+
- Create antsibull changelog frament files under `changelogs/fragments/` for any changes that affect module behavior or parameters.
38+
39+
PR reviewer checklist (help Copilot produce PRs that pass CI)
40+
41+
- Ensure modules are idempotent and `changed` is correct.
42+
- Ensure module `data` contains serializable values (callers expect JSON-friendly types).
43+
- Verify `Get-LowlyDbaSqlServerAuthSpec` returns an array fragment spec when used with `AnsibleModule.Create()`.
44+
- Confirm `no_log` for password fields in specs.
45+
- Run unit tests for changed helpers and run a sample integration playbook against a local test instance if possible.
46+
47+
Where to look in the codebase
48+
49+
- Module utils: `plugins/module_utils/_SqlServerUtils.psm1`
50+
- Modules: `plugins/modules/*.ps1` (see `agent_job.ps1`, `agent_job_step.ps1` as examples)
51+
- Tests: `tests/integration/targets/*/tasks/main.yml`
52+
53+
If you need to modify behavior:
54+
55+
- Prefer small, well-tested changes to helper functions. Centralized fixes in `module_utils` avoid repeating logic across many modules.
56+
- For path comparisons and idempotency: prefer server-side verification (Get after Set) or minimal normalization, rather than heavy OS-specific path APIs.
57+
58+
Contact
59+
60+
If you need clarification about how a module is expected to behave, read the module's docstring in the Python shim (`plugins/modules/*.py`) and the corresponding integration tests under `tests/integration/targets/*` to see expected inputs/outputs.

CHANGELOG.rst

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,27 @@ lowlydba.sqlserver Release Notes
44

55
.. contents:: Topics
66

7+
v2.7.0
8+
======
9+
10+
Release Summary
11+
---------------
12+
13+
Added output file support for SQL Agent job steps.
14+
15+
Minor Changes
16+
-------------
17+
18+
- agent_job_step - Added ``output_file`` parameter to specify the output file path for SQL Agent job steps (https://github.com/lowlydba/lowlydba.sqlserver/pull/329).
19+
720
v2.6.1
821
======
922

23+
Release Summary
24+
---------------
25+
26+
Testing updates for Ansible 2.19 compatibility.
27+
1028
Minor Changes
1129
-------------
1230

@@ -17,7 +35,7 @@ v2.6.0
1735
======
1836

1937
Release Summary
20-
--------------
38+
---------------
2139

2240
Added support for contained Availability Groups using dbatools 2.1.15 - thanks @DorBreger!
2341

changelogs/changelog.yaml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,8 @@ releases:
560560
changes:
561561
minor_changes:
562562
- Added support for contained Availability Groups using dbatools 2.1.15 (https://github.com/lowlydba/lowlydba.sqlserver/pull/249).
563-
release_summary: Added support for contained Availability Groups using dbatools 2.1.15 - thanks @DorBreger!
563+
release_summary: Added support for contained Availability Groups using dbatools
564+
2.1.15 - thanks @DorBreger!
564565
fragments:
565566
- noop.yml
566567
release_date: '2025-04-06'
@@ -573,3 +574,12 @@ releases:
573574
fragments:
574575
- 314-ansible-2-19-compatibility.yml
575576
release_date: '2025-05-03'
577+
2.7.0:
578+
changes:
579+
minor_changes:
580+
- agent_job_step - Added ``output_file`` parameter to specify the output file
581+
path for SQL Agent job steps (https://github.com/lowlydba/lowlydba.sqlserver/pull/329).
582+
release_summary: Added output file support for SQL Agent job steps.
583+
fragments:
584+
- 329-add-agent-outputfile.yml
585+
release_date: '2025-08-16'

plugins/modules/agent_job_step.ps1

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ $spec = @{
3131
on_fail_step_id = @{type = 'int'; required = $false; default = 0 }
3232
retry_attempts = @{type = 'int'; required = $false; default = 0 }
3333
retry_interval = @{type = 'int'; required = $false; default = 0 }
34+
output_file = @{type = 'str'; required = $false }
3435
state = @{type = 'str'; required = $false; default = 'present'; choices = @('present', 'absent') }
3536
}
3637
required_together = @(
@@ -55,6 +56,7 @@ $onFailAction = $module.Params.on_fail_action
5556
[nullable[int]]$onFailStepId = $module.Params.on_fail_step_id
5657
[int]$retryAttempts = $module.Params.retry_attempts
5758
[nullable[int]]$retryInterval = $module.Params.retry_interval
59+
$outputFile = $module.Params.output_file
5860
$state = $module.Params.state
5961
$checkMode = $module.CheckMode
6062
$module.Result.changed = $false
@@ -84,7 +86,7 @@ try {
8486
}
8587
elseif ($state -eq "present") {
8688
if (!($stepName) -or !($stepId)) {
87-
$module.FailJson("Step name must be specified when state=present.")
89+
$module.FailJson("Step name and step_id must be specified when state=present.")
8890
}
8991
$jobStepParams = @{
9092
SqlInstance = $sqlInstance
@@ -99,7 +101,6 @@ try {
99101
OnFailStepId = $onFailStepId
100102
RetryAttempts = $retryAttempts
101103
RetryInterval = $retryInterval
102-
WhatIf = $checkMode
103104
}
104105
if ($null -ne $command) {
105106
$jobStepParams.Add("Command", $command)
@@ -110,6 +111,30 @@ try {
110111
$jobStepParams.Add("StepId", $stepId)
111112
$output = New-DbaAgentJobStep @jobStepParams
112113
$module.Result.changed = $true
114+
115+
# Set output file if specified
116+
if ($null -ne $outputFile -and $outputFile -ne "") {
117+
$setOutputFileSplat = @{
118+
SqlInstance = $sqlInstance
119+
SqlCredential = $sqlCredential
120+
Job = $job
121+
Step = $stepName
122+
OutputFile = $outputFile
123+
}
124+
$outputFileResult = Set-DbaAgentJobOutputFile @setOutputFileSplat
125+
if ($null -ne $outputFileResult) {
126+
$module.Result.changed = $true
127+
# Add the OutputFileName property from the Set-DbaAgentJobOutputFile result
128+
if ($null -ne $outputFileResult.OutputFileName) {
129+
Add-Member -InputObject $output -MemberType NoteProperty -Name "OutputFileName" -Value $outputFileResult.OutputFileName -Force
130+
}
131+
}
132+
}
133+
134+
# Ensure OutputFileName property is always available for consistency
135+
if ($null -ne $output -and -not ($output.PSObject.Properties.Name -contains "OutputFileName")) {
136+
Add-Member -InputObject $output -MemberType NoteProperty -Name "OutputFileName" -Value $output.OutputFileName -Force
137+
}
113138
}
114139
# Update existing
115140
else {
@@ -137,9 +162,35 @@ try {
137162
"OnSuccessActionStep"
138163
"RetryAttempts"
139164
"RetryInterval"
165+
"OutputFileName"
140166
)
141167
$diff = Compare-Object -ReferenceObject $output -DifferenceObject $old -Property $compareProperty
142168
}
169+
170+
# Set output file if specified and different from current
171+
if ($null -ne $outputFile -and $outputFile -ne "" -and $existingJobStep.OutputFileName -ne $outputFile) {
172+
$setOutputFileSplat = @{
173+
SqlInstance = $sqlInstance
174+
SqlCredential = $sqlCredential
175+
Job = $job
176+
Step = $stepName # Use the new step name since Set-DbaAgentJobStep already renamed it
177+
OutputFile = $outputFile
178+
}
179+
$outputFileResult = Set-DbaAgentJobOutputFile @setOutputFileSplat
180+
if ($null -ne $outputFileResult) {
181+
$module.Result.changed = $true
182+
# Add the OutputFileName property from the Set-DbaAgentJobOutputFile result
183+
if ($null -ne $outputFileResult.OutputFileName) {
184+
Add-Member -InputObject $output -MemberType NoteProperty -Name "OutputFileName" -Value $outputFileResult.OutputFileName -Force
185+
}
186+
}
187+
}
188+
189+
# Ensure OutputFileName property is always available
190+
if ($null -ne $output -and -not ($output.PSObject.Properties.Name -contains "OutputFileName")) {
191+
Add-Member -InputObject $output -MemberType NoteProperty -Name "OutputFileName" -Value $output.OutputFileName -Force
192+
}
193+
143194
if ($diff -or $checkMode) {
144195
$module.Result.changed = $true
145196
}

plugins/modules/agent_job_step.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@
8484
required: false
8585
type: int
8686
default: 0
87+
output_file:
88+
description:
89+
- The full path to the output file for the job step.
90+
- This specifies where the output of the job step will be written.
91+
version_added: 2.7.0
92+
required: false
93+
type: str
8794
author: "John McCall (@lowlydba)"
8895
requirements:
8996
- L(dbatools,https://www.powershellgallery.com/packages/dbatools/) PowerShell module
@@ -108,6 +115,15 @@
108115
step_name: Step1
109116
step_id: 1
110117
command: "TRUNCATE TABLE dbo.TestData;"
118+
119+
- name: Create a job step with output file
120+
lowlydba.sqlserver.agent_job_step:
121+
sql_instance: sql-01.myco.io
122+
job: MyJob
123+
step_name: Step2
124+
step_id: 2
125+
command: "SELECT * FROM sys.databases;"
126+
output_file: "C:\\Logs\\MyJob_Step2.log"
111127
'''
112128

113129
RETURN = r'''

tests/integration/targets/agent_job_step/tasks/main.yml

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
- result.data.State == "Existing"
4242
- result is changed
4343

44-
- name: Create agent job step two in checkmode
44+
- name: Create agent job step two (in checkmode)
4545
lowlydba.sqlserver.agent_job_step:
4646
step_name: "{{ job_step2 }}"
4747
step_id: 2
@@ -51,7 +51,7 @@
5151
that:
5252
- result is changed
5353

54-
- name: Verify create agent job step two in checkmode works
54+
- name: Verify 'create agent job step two (in checkmode)' works
5555
lowlydba.sqlserver.agent_job_step:
5656
step_name: "{{ job_step2 }}"
5757
step_id: 2
@@ -88,7 +88,7 @@
8888
that:
8989
- result is not changed
9090

91-
- name: Change agent job step in checkmode
91+
- name: Change agent job step (in checkmode)
9292
lowlydba.sqlserver.agent_job_step:
9393
step_name: "{{ job_step3 }}"
9494
step_id: 3
@@ -99,7 +99,7 @@
9999
that:
100100
- result is changed
101101

102-
- name: Verify change agent job step in checkmode works
102+
- name: Verify 'change agent job step (in checkmode)' works
103103
lowlydba.sqlserver.agent_job_step:
104104
step_name: "{{ job_step3 }}"
105105
step_id: 3
@@ -129,6 +129,51 @@
129129
- result.data.State == "Existing"
130130
- result is not changed
131131

132+
- name: Create agent job step with output file
133+
lowlydba.sqlserver.agent_job_step:
134+
step_name: "Step with output file"
135+
step_id: 4
136+
command: "SELECT * FROM sys.databases;"
137+
output_file: "C:\\temp\\test_output.log"
138+
register: result
139+
- assert:
140+
that:
141+
- result.data != None
142+
- result.data.Name == "Step with output file"
143+
- result.data.ID == 4
144+
- result.data.OutputFileName == "C:\\temp\\test_output.log"
145+
- result is changed
146+
147+
- name: Update agent job step output file
148+
lowlydba.sqlserver.agent_job_step:
149+
step_name: "Step with output file"
150+
step_id: 4
151+
command: "SELECT * FROM sys.databases;"
152+
output_file: "C:\\temp\\test_output_updated.log"
153+
register: result
154+
- assert:
155+
that:
156+
- result.data != None
157+
- result.data.Name == "Step with output file"
158+
- result.data.ID == 4
159+
- result.data.OutputFileName == "C:\\temp\\test_output_updated.log"
160+
- result is changed
161+
162+
- name: Verify 'output file unchanged when same'
163+
lowlydba.sqlserver.agent_job_step:
164+
step_name: "Step with output file"
165+
step_id: 4
166+
command: "SELECT * FROM sys.databases;"
167+
output_file: "C:\\temp\\test_output_updated.log"
168+
register: result
169+
- assert:
170+
that:
171+
- result.data != None
172+
- result.data.Name == "Step with output file"
173+
- result.data.ID == 4
174+
- result.data.OutputFileName == "C:\\temp\\test_output_updated.log"
175+
- result is not changed
176+
132177
- name: Remove agent job step
133178
lowlydba.sqlserver.agent_job_step:
134179
step_name: "{{ job_step1 }}"
@@ -138,7 +183,7 @@
138183
that:
139184
- result is changed
140185

141-
- name: Remove agent job step in checkmode
186+
- name: Remove agent job step (in checkmode)
142187
lowlydba.sqlserver.agent_job_step:
143188
step_id: 2
144189
state: "absent"
@@ -148,7 +193,7 @@
148193
that:
149194
- result is changed
150195

151-
- name: Verify remove agent job step in checkmode works
196+
- name: Verify 'remove agent job step (in checkmode)' works
152197
lowlydba.sqlserver.agent_job_step:
153198
step_id: 2
154199
state: "absent"

0 commit comments

Comments
 (0)