diff --git a/CHANGELOG.md b/CHANGELOG.md index ff762546..c4108c37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Pester v5 support for agent checks. + ## [3.0.1-preview0025] - 2023-08-28 ### Added diff --git a/containers/JessAndBeard.psm1 b/containers/JessAndBeard.psm1 index 92167ee4..649b9b7b 100644 --- a/containers/JessAndBeard.psm1 +++ b/containers/JessAndBeard.psm1 @@ -2312,7 +2312,7 @@ Uh-Oh - The Tag filters between v4 and v5 are not the same somehow. For v4 We returned {0} and -For v5 we returned +for v4 we returned {1} " -f ($v4code.TagFilter | Out-String), ($v5code.Configuration.Filter.Tag.Value | Out-String) Write-PSFMessage -Message $Message -Level Warning @@ -2325,47 +2325,54 @@ The Tags are the same" $changedTags = @( @{ Name = 'TraceFlagsExpected' - RunChange = 3 # + or - the number of tests run for v5 - PassedChange = 3 # + or - the number of tests passed for v5 - FailedChange = 0 # + or - the number of tests failed for v5 - SkippedChange = 0 # + or - the number of tests skipped for v5 + RunChange = 3 # + or - the number of tests run for v4 + PassedChange = 3 # + or - the number of tests passed for v4 + FailedChange = 0 # + or - the number of tests failed for v4 + SkippedChange = 0 # + or - the number of tests skipped for v4 }, @{ Name = 'TraceFlagsNotExpected' - RunChange = 3 # + or - the number of tests for v5 - PassedChange = 3 # + or - the number of tests passed for v5 - FailedChange = 0 # + or - the number of tests failed for v5 - SkippedChange = 0 # + or - the number of tests skipped for v5 + RunChange = 3 # + or - the number of tests for v4 + PassedChange = 3 # + or - the number of tests passed for v4 + FailedChange = 0 # + or - the number of tests failed for v4 + SkippedChange = 0 # + or - the number of tests skipped for v4 }, @{ Name = 'XESessionRunningAllowed' - RunChange = -12 # + or - the number of tests for v5 - PassedChange = 0 # + or - the number of tests passed for v5 - FailedChange = -12 # + or - the number of tests failed for v5 - SkippedChange = 0 # + or - the number of tests skipped for v5 + RunChange = -12 # + or - the number of tests for v4 + PassedChange = 0 # + or - the number of tests passed for v4 + FailedChange = -12 # + or - the number of tests failed for v4 + SkippedChange = 0 # + or - the number of tests skipped for v4 }, @{ Name = 'LinkedServerConnection' - RunChange = -3 # + or - the number of tests for v5 - PassedChange = -3 # + or - the number of tests passed for v5 - FailedChange = 0 # + or - the number of tests failed for v5 - SkippedChange = 0 # + or - the number of tests skipped for v5 + RunChange = -3 # + or - the number of tests for v4 + PassedChange = -3 # + or - the number of tests passed for v4 + FailedChange = 0 # + or - the number of tests failed for v4 + SkippedChange = 0 # + or - the number of tests skipped for v4 }, @{ Name = 'SupportedBuild' - RunChange = -3 # + or - the number of tests run for v5 - PassedChange = -3 # + or - the number of tests passed for v5 - FailedChange = 0 # + or - the number of tests failed for v5 - SkippedChange = 0 # + or - the number of tests skipped for v5 + RunChange = -3 # + or - the number of tests run for v4 + PassedChange = -3 # + or - the number of tests passed for v4 + FailedChange = 0 # + or - the number of tests failed for v4 + SkippedChange = 0 # + or - the number of tests skipped for v4 }, @{ Name = 'GuestUserConnect' - RunChange = 0 # + or - the number of tests run for v5 - PassedChange = +2 # + or - the number of tests passed for v5 - FailedChange = -2 # + or - the number of tests failed for v5 - SkippedChange = 0 # + or - the number of tests skipped for v5 + RunChange = 0 # + or - the number of tests run for v4 + PassedChange = +2 # + or - the number of tests passed for v4 + FailedChange = -2 # + or - the number of tests failed for v4 + SkippedChange = 0 # + or - the number of tests skipped for v4 }, + @{ + Name = 'AgentServiceAccount' + RunChange = -3 # + or - the number of tests run for v4 + PassedChange = -5 # + or - the number of tests passed for v4 + FailedChange = -1 # + or - the number of tests failed for v4 + SkippedChange = +3 # + or - the number of tests skipped for v4 + }, @{ Name = 'SqlEngineServiceAccount' RunChange = -3 # + or - the number of tests run for v5 @@ -2407,7 +2414,7 @@ Uh-Oh - The total tests run between v4 and v5 are not the same somehow. For v4 We ran {0} tests and -For v5 we ran +for v4 we ran {1} tests The MOST COMMON REASON IS you have used Tags instead of Tag in your Describe block {2} " -f $v4code.TotalCount, $v5run, $messageAppend @@ -2427,7 +2434,7 @@ Uh-Oh - The total tests Passed between v4 and v5 are not the same somehow. For v4 We Passed {0} tests and -For v5 we Passed +for v4 we Passed {1} tests {2} @@ -2454,7 +2461,7 @@ Uh-Oh - The total tests Failed between v4 and v5 are not the same somehow. For v4 We Failed {0} tests and -For v5 we Failed +for v4 we Failed {1} tests " -f $v4code.FailedCount, $v5Failed, $messageAppend diff --git a/source/checks/Agentv5.Tests.ps1 b/source/checks/Agentv5.Tests.ps1 index 63b10584..9be16609 100644 --- a/source/checks/Agentv5.Tests.ps1 +++ b/source/checks/Agentv5.Tests.ps1 @@ -31,23 +31,28 @@ BeforeDiscovery { } } } + + #TODO : Clean this up Write-PSFMessage -Message "Instances = $($InstancesToTest.Name)" -Level Verbose + Set-PSFConfig -Module dbachecks -Name global.notcontactable -Value $NotContactable -} + # Get-DbcConfig is expensive so we call it once + $__dbcconfig = Get-DbcConfig +} -Describe "Database Mail XPs" -Tag DatabaseMailEnabled, CIS, security -ForEach $InstancesToTest { - $skip = Get-DbcConfigValue skip.agent.databasemailenabled +Describe "Database Mail XPs" -Tag DatabaseMailEnabled, CIS, security, Agent -ForEach $InstancesToTest { + $skip = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.databasemailenabled' }).Value Context "Testing Database Mail XPs on <_.Name>" { - It "Testing Database Mail XPs is set to <_.DatabaseMailEnabled> on <_.Name>" -Skip:$skip { + It "Testing Database Mail XPs is set to <_.ConfigValues.DatabaseMailEnabled> on <_.Name>" -Skip:$skip { $PSItem.DatabaseMailEnabled | Should -Be $PSItem.ConfigValues.DatabaseMailEnabled -Because 'The Database Mail XPs setting should be set correctly' } } } -Describe "SQL Agent Account" -Tag AgentServiceAccount, ServiceAccount -ForEach $InstancesToTest { - $skipServiceState = Get-DbcConfigValue skip.agent.servicestate - $skipServiceStartMode = Get-DbcConfigValue skip.agent.servicestartmode +Describe "SQL Agent Account" -Tag AgentServiceAccount, ServiceAccount, Agent -ForEach $InstancesToTest { + $skipServiceState = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.servicestate' }).Value + $skipServiceStartMode = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.servicestartmode' }).Value Context "Testing SQL Agent is running on <_.Name>" { It "SQL Agent should be running for <_.InstanceName> on <_.Name>" -Skip:$skipServiceState { @@ -66,374 +71,176 @@ Describe "SQL Agent Account" -Tag AgentServiceAccount, ServiceAccount -ForEach $ } } -Describe "DBA Operators" -Tag DbaOperator, Operator -ForEach $InstancesToTest { - $skipOperatorName = Get-DbcConfigValue skip.agent.operatorname - $skipOperatorEamil = Get-DbcConfigValue skip.agent.operatoremail +Describe "DBA Operator" -Tag DbaOperator, Operator, Agent -ForEach $InstancesToTest { + $skipOperatorName = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.dbaoperatorname' }).Value + $skipOperatorEmail = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.dbaoperatoremail' }).Value Context "Testing DBA Operators exists on <_.Name>" { - It "The Operator <_.ExpectedOperatorName> exists on <_.Name>" -Skip:$skipOperatorName -ForEach ($PSItem.Operator | Where-Object ExpectedOperatorName -ne 'null') { + It "The Operator <_.ExpectedOperatorName> exists on <_.Name>" -Skip:$skipOperatorName -ForEach ($PSItem.Operator | Where-Object ExpectedOperatorName -NE 'null') { $PSItem.ExpectedOperatorName | Should -BeIn $PSItem.ActualOperatorName -Because 'This Operator is expected to exist' } - It "The Operator email <_.ExpectedOperatorEmail> is correct on <_.Name>" -Skip:$skipOperatorEamil -ForEach ($PSItem.Operator | Where-Object ExpectedOperatorEmail -ne 'null') { + It "The Operator email <_.ExpectedOperatorEmail> is correct on <_.Name>" -Skip:$skipOperatorEmail -ForEach ($PSItem.Operator | Where-Object ExpectedOperatorEmail -NE 'null') { $PSItem.ExpectedOperatorEmail | Should -BeIn $PSItem.ActualOperatorEmail -Because 'This operator email is expected to exist' } } } -# Describe "Failsafe Operator" -Tags FailsafeOperator, Operator, $filename { -# if ($NotContactable -contains $psitem) { -# Context "Testing failsafe operator exists on $psitem" { -# It "Can't Connect to $Psitem" { -# $false | Should -BeTrue -Because "The instance should be available to be connected to!" -# } -# } -# } -# else { -# Context "Testing failsafe operator exists on $psitem" { -# $failsafeoperator = Get-DbcConfigValue agent.failsafeoperator -# It "The Failsafe Operator exists on $psitem" { -# (Connect-DbaInstance -SqlInstance $psitem).JobServer.AlertSystem.FailSafeOperator | Should -Be $failsafeoperator -Because 'The failsafe operator will ensure that any job failures will be notified to someone if not set explicitly' -# } -# } -# } -# } - -# Describe "Database Mail Profile" -Tags DatabaseMailProfile, $filename { -# if ($NotContactable -contains $psitem) { -# Context "Testing database mail profile is set on $psitem" { -# It "Can't Connect to $Psitem" { -# $false | Should -BeTrue -Because "The instance should be available to be connected to!" -# } -# } -# } -# else { -# Context "Testing database mail profile is set on $psitem" { -# $databasemailprofile = Get-DbcConfigValue agent.databasemailprofile -# It "The Database Mail profile $databasemailprofile exists on $psitem" { -# ((Get-DbaDbMailProfile -SqlInstance $InstanceSMO).Name -contains $databasemailprofile) | Should -Be $true -Because 'The database mail profile is required to send emails' -# } -# } -# } -# } - -# Describe "Agent Mail Profile" -Tags AgentMailProfile, $filename { -# if ($NotContactable -contains $psitem) { -# Context "Testing SQL Agent Alert System database mail profile is set on $psitem" { -# It "Can't Connect to $Psitem" { -# $false | Should -BeTrue -Because "The instance should be available to be connected to!" -# } -# } -# } -# else { -# Context "Testing SQL Agent Alert System database mail profile is set on $psitem" { -# $agentmailprofile = Get-DbcConfigValue agent.databasemailprofile -# It "The SQL Server Agent Alert System should have an enabled database mail profile on $psitem" { -# (Get-DbaAgentServer -SqlInstance $InstanceSMO).DatabaseMailProfile | Should -Be $agentmailprofile -Because 'The SQL Agent Alert System needs an enabled database mail profile to send alert emails' -# } -# } -# } -# } - -# Describe "Failed Jobs" -Tags FailedJob, $filename { - -# if ($NotContactable -contains $psitem) { -# Context "Checking for failed enabled jobs on $psitem" { -# It "Can't Connect to $Psitem" { -# $false | Should -BeTrue -Because "The instance should be available to be connected to!" -# } -# } -# } -# else { -# $maxdays = Get-DbcConfigValue agent.failedjob.since -# $startdate = (Get-Date).AddDays( - $maxdays) -# Context "Checking for failed enabled jobs since $startdate on $psitem" { -# $excludecancelled = Get-DbcConfigValue agent.failedjob.excludecancelled -# @(Get-DbaAgentJob -SqlInstance $psitem | Where-Object { $Psitem.IsEnabled -and ($psitem.LastRunDate -gt $startdate) }).ForEach{ -# if ($psitem.LastRunOutcome -eq "Unknown") { -# It -Skip "We chose to skip this as $psitem's last run outcome is unknown on $($psitem.SqlInstance)" { -# $psitem.LastRunOutcome | Should -Be "Succeeded" -Because 'All Agent Jobs should have succeed this one is unknown - you need to investigate the failed jobs' -# } -# } -# elseif (($psitem.LastRunOutcome -eq "Cancelled") -and ($excludecancelled -eq $true)) { -# It -Skip "We chose to skip this as $psitem's last run outcome is cancelled on $($psitem.SqlInstance)" { -# $psitem.LastRunOutcome | Should -Be "Succeeded" -Because 'All Agent Jobs should have succeed this one is unknown - you need to investigate the failed jobs' -# } -# } -# else { -# It "$psitem's last run outcome is $($psitem.LastRunOutcome) on $($psitem.SqlInstance)" { -# $psitem.LastRunOutcome | Should -Be "Succeeded" -Because 'All Agent Jobs should have succeed - you need to investigate the failed jobs' -# } -# } -# } -# } -# } -# } - -# Describe "Valid Job Owner" -Tags ValidJobOwner, $filename { -# [string[]]$targetowner = Get-DbcConfigValue agent.validjobowner.name - -# if ($NotContactable -contains $psitem) { -# Context "Testing job owners on $psitem" { -# It "Can't Connect to $Psitem" { -# $false | Should -BeTrue -Because "The instance should be available to be connected to!" -# } -# } -# } -# else { -# Context "Testing job owners on $psitem" { -# @(Get-DbaAgentJob -SqlInstance $psitem -EnableException:$false).ForEach{ -# It "Job $($psitem.Name) - owner $($psitem.OwnerLoginName) should be in this list ( $( [String]::Join(", ", $targetowner) ) ) on $($psitem.SqlInstance)" { -# $psitem.OwnerLoginName | Should -BeIn $TargetOwner -Because "The account that is the job owner is not what was expected" -# } -# } -# } -# } -# } -# Describe "Invalid Job Owner" -Tags InValidJobOwner, $filename { -# [string[]]$targetowner = Get-DbcConfigValue agent.invalidjobowner.name - -# if ($NotContactable -contains $psitem) { -# Context "Testing job owners on $psitem" { -# It "Can't Connect to $Psitem" { -# $false | Should -BeTrue -Because "The instance should be available to be connected to!" -# } -# } -# } -# else { -# Context "Testing job owners on $psitem" { -# @(Get-DbaAgentJob -SqlInstance $psitem -EnableException:$false).ForEach{ -# It "Job $($psitem.Name) - owner $($psitem.OwnerLoginName) should not be in this list ( $( [String]::Join(", ", $targetowner) ) ) on $($psitem.SqlInstance)" { -# $psitem.OwnerLoginName | Should -Not -BeIn $TargetOwner -Because "The account that is the job owner has been defined as not valid" -# } -# } -# } -# } -# } - -# Describe "Agent Alerts" -Tags AgentAlert, $filename { -# $severity = Get-DbcConfigValue agent.alert.Severity -# $messageid = Get-DbcConfigValue agent.alert.messageid -# $AgentAlertJob = Get-DbcConfigValue agent.alert.Job -# $AgentAlertNotification = Get-DbcConfigValue agent.alert.Notification -# $skip = Get-DbcConfigValue skip.agent.alert -# if ($NotContactable -contains $psitem) { -# Context "Testing Agent Alerts Severity exists on $psitem" { -# It "Can't Connect to $Psitem" { -# $false | Should -BeTrue -Because "The instance should be available to be connected to!" -# } -# } -# Context "Testing Agent Alerts MessageID exists on $psitem" { -# It "Can't Connect to $Psitem" { -# $false | Should -BeTrue -Because "The instance should be available to be connected to!" -# } -# } -# } -# else { -# $alerts = Get-DbaAgentAlert -SqlInstance $psitem -# Context "Testing Agent Alerts Severity exists on $psitem" { -# ForEach ($sev in $severity) { -# It "Severity $sev Alert should exist on $psitem" -Skip:$skip { -# ($alerts.Where{ $psitem.Severity -eq $sev }) | Should -be $true -Because "Recommended Agent Alerts to exists http://blog.extreme-advice.com/2013/01/29/list-of-errors-and-severity-level-in-sql-server-with-catalog-view-sysmessages/" -# } -# It "Severity $sev Alert should be enabled on $psitem" -Skip:$skip { -# ($alerts.Where{ $psitem.Severity -eq $sev }).IsEnabled | Should -be $true -Because "Configured alerts should be enabled" -# } -# if ($AgentAlertJob) { -# It "A job name for Severity $sev Alert on $psitem" -Skip:$skip { -# ($alerts.Where{ $psitem.Severity -eq $sev }).jobname -ne $null | Should -be $true -Because "Should notify by SQL Agent Job" -# } -# } -# if ($AgentAlertNotification) { -# It "Severity $sev Alert should have a notification on $psitem" -Skip:$skip { -# ($alerts.Where{ $psitem.Severity -eq $sev }).HasNotification -in 1, 2, 3, 4, 5, 6, 7 | Should -be $true -Because "Should notify by Agent notifications" -# } -# } -# } -# } -# Context "Testing Agent Alerts MessageID exists on $psitem" { -# ForEach ($mid in $messageid) { -# It "Message_ID $mid Alert should exist on $psitem" -Skip:$skip { -# ($alerts.Where{ $psitem.messageid -eq $mid }) | Should -be $true -Because "Recommended Agent Alerts to exists http://blog.extreme-advice.com/2013/01/29/list-of-errors-and-severity-level-in-sql-server-with-catalog-view-sysmessages/" -# } -# It "Message_ID $mid Alert should be enabled on $psitem" -Skip:$skip { -# ($alerts.Where{ $psitem.messageid -eq $mid }) | Should -be $true -Because "Configured alerts should be enabled" -# } -# if ($AgentAlertJob) { -# It "A Job name for Message_ID $mid Alert should be on $psitem" -Skip:$skip { -# ($alerts.Where{ $psitem.messageid -eq $mid }).jobname -ne $null | Should -be $true -Because "Should notify by SQL Agent Job" -# } -# } -# if ($AgentAlertNotification) { -# It "Message_ID $mid Alert should have a notification on $psitem" -Skip:$skip { -# ($alerts.Where{ $psitem.messageid -eq $mid }).HasNotification -in 1, 2, 3, 4, 5, 6, 7 | Should -be $true -Because "Should notify by Agent notifications" -# } -# } -# } -# } -# } -# } - -# Describe "Job History Configuration" -Tags JobHistory, $filename { -# if ($NotContactable -contains $psitem) { -# Context "Testing job history configuration on $psitem" { -# It "Can't Connect to $Psitem" { -# $false | Should -BeTrue -Because "The instance should be available to be connected to!" -# } -# } -# } -# else { -# Context "Testing job history configuration on $psitem" { -# [int]$minimumJobHistoryRows = Get-DbcConfigValue agent.history.maximumhistoryrows -# [int]$minimumJobHistoryRowsPerJob = Get-DbcConfigValue agent.history.maximumjobhistoryrows - -# $AgentServer = Get-DbaAgentServer -SqlInstance $psitem -EnableException:$false - -# if ($minimumJobHistoryRows -eq -1) { -# It "The maximum job history configuration should be set to disabled on $psitem" { -# Assert-JobHistoryRowsDisabled -AgentServer $AgentServer -minimumJobHistoryRows $minimumJobHistoryRows -# } -# } -# else { -# It "The maximum job history number of rows configuration should be greater or equal to $minimumJobHistoryRows on $psitem" { -# Assert-JobHistoryRows -AgentServer $AgentServer -minimumJobHistoryRows $minimumJobHistoryRows -# } -# It "The maximum job history rows per job configuration should be greater or equal to $minimumJobHistoryRowsPerJob on $psitem" { -# Assert-JobHistoryRowsPerJob -AgentServer $AgentServer -minimumJobHistoryRowsPerJob $minimumJobHistoryRowsPerJob -# } -# } -# } -# } -# } -# Describe "Long Running Agent Jobs" -Tags LongRunningJob, $filename { -# $skip = Get-DbcConfigValue skip.agent.longrunningjobs -# $runningjobpercentage = Get-DbcConfigValue agent.longrunningjob.percentage -# if (-not $skip) { -# $query = "SELECT -# JobName, -# AvgSec, -# start_execution_date as StartDate, -# RunningSeconds, -# RunningSeconds - AvgSec AS Diff -# FROM -# ( -# SELECT -# j.name AS JobName, -# start_execution_date, -# AVG(DATEDIFF(SECOND, 0, STUFF(STUFF(RIGHT('000000' -# + CONVERT(VARCHAR(6),jh.run_duration),6),5,0,':'),3,0,':'))) AS AvgSec, -# ja.start_execution_date as startdate, -# DATEDIFF(second, ja.start_execution_date, GetDate()) AS RunningSeconds -# FROM msdb.dbo.sysjobactivity ja -# JOIN msdb.dbo.sysjobs j -# ON ja.job_id = j.job_id -# JOIN msdb.dbo.sysjobhistory jh -# ON jh.job_id = j.job_id -# WHERE start_execution_date is not null -# AND stop_execution_date is null -# AND run_duration < 235959 -# AND run_duration >= 0 -# AND ja.start_execution_date > DATEADD(day,-1,GETDATE()) -# GROUP BY j.name,j.job_id,start_execution_date,stop_execution_date,ja.job_id -# ) AS t -# ORDER BY JobName;" -# $runningjobs = Invoke-DbaQuery -SqlInstance $PSItem -Database msdb -Query $query -# } -# if ($NotContactable -contains $psitem) { -# Context "Testing long running jobs on $psitem" { -# It "Can't Connect to $Psitem" { -# $false | Should -BeTrue -Because "The instance should be available to be connected to!" -# } -# } -# } -# else { -# Context "Testing long running jobs on $psitem" { -# if ($runningjobs) { -# foreach ($runningjob in $runningjobs | Where-Object { $_.AvgSec -ne 0 }) { -# It "Running job $($runningjob.JobName) duration should not be more than $runningjobpercentage % extra of the average run time on $psitem" -Skip:$skip { -# Assert-LongRunningJobs -runningjob $runningjob -runningjobpercentage $runningjobpercentage -# } -# } -# } -# else { -# It "There are no running jobs currently on $psitem" -Skip:$skip { -# $True | SHould -BeTrue -# } -# } -# } -# } -# } -# Describe "Last Agent Job Run" -Tags LastJobRunTime, $filename { -# $skip = Get-DbcConfigValue skip.agent.lastjobruntime -# $runningjobpercentage = Get-DbcConfigValue agent.lastjobruntime.percentage -# $maxdays = Get-DbcConfigValue agent.failedjob.since -# if (-not $skip) { -# $query = "IF OBJECT_ID('tempdb..#dbachecksLastRunTime') IS NOT NULL DROP Table #dbachecksLastRunTime -# SELECT * INTO #dbachecksLastRunTime -# FROM -# ( -# SELECT -# j.job_id, -# j.name AS JobName, -# DATEDIFF(SECOND, 0, STUFF(STUFF(RIGHT('000000' + CONVERT(VARCHAR(6),jh.run_duration),6),5,0,':'),3,0,':')) AS Duration -# FROM msdb.dbo.sysjobs j -# INNER JOIN -# ( -# SELECT job_id, instance_id = MAX(instance_id) -# FROM msdb.dbo.sysjobhistory -# GROUP BY job_id -# ) AS h -# ON j.job_id = h.job_id -# INNER JOIN -# msdb.dbo.sysjobhistory AS jh -# ON jh.job_id = h.job_id -# AND jh.instance_id = h.instance_id -# WHERE msdb.dbo.agent_datetime(jh.run_date, jh.run_time) > DATEADD(DAY,- $maxdays,GETDATE()) -# AND jh.step_id = 0 -# ) AS lrt - -# IF OBJECT_ID('tempdb..#dbachecksAverageRunTime') IS NOT NULL DROP Table #dbachecksAverageRunTime -# SELECT * INTO #dbachecksAverageRunTime -# FROM -# ( -# SELECT -# job_id, -# AVG(DATEDIFF(SECOND, 0, STUFF(STUFF(RIGHT('000000' + CONVERT(VARCHAR(6),run_duration),6),5,0,':'),3,0,':'))) AS AvgSec -# FROM msdb.dbo.sysjobhistory hist -# WHERE msdb.dbo.agent_datetime(run_date, run_time) > DATEADD(DAY,- $maxdays,GETDATE()) -# AND Step_id = 0 -# AND run_duration >= 0 -# GROUP BY job_id -# ) as art - -# SELECT -# JobName, -# Duration, -# AvgSec, -# Duration - AvgSec AS Diff -# FROM #dbachecksLastRunTime lastrun -# JOIN #dbachecksAverageRunTime avgrun -# ON lastrun.job_id = avgrun.job_id - -# DROP Table #dbachecksLastRunTime -# DROP Table #dbachecksAverageRunTime" -# $lastagentjobruns = Invoke-DbaQuery -SqlInstance $PSItem -Database msdb -Query $query -# Context "Testing last job run time on $psitem" { -# foreach ($lastagentjobrun in $lastagentjobruns | Where-Object { $_.AvgSec -ne 0 }) { -# It "Job $($lastagentjobrun.JobName) last run duration should be not be greater than $runningjobpercentage % extra of the average run time on $psitem" -Skip:$skip { -# Assert-LastJobRun -lastagentjobrun $lastagentjobrun -runningjobpercentage $runningjobpercentage -# } -# } -# } -# } -# else { -# Context "Testing last job run time on $psitem" { -# It "Job average run time on $psitem" -Skip { -# Assert-LastJobRun -lastagentjobrun $lastagentjobrun -runningjobpercentage $runningjobpercentage -# } -# } -# } -# } +Describe "Failsafe operator" -Tag FailsafeOperator, Operator, Agent -ForEach $InstancesToTest { + $skipFailsafeOperator = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.failsafeoperator' }).Value + + Context "Testing failsafe operator exists on <_.Name>" { + It "The failsafe operator <_.FailSafeOperator.ExpectedFailSafeOperator> exists on <_.Name>" -Skip:$skipFailsafeOperator { + $PSItem.FailSafeOperator.ActualFailSafeOperator | Should -Be $PSItem.FailSafeOperator.ExpectedFailSafeOperator -Because 'The failsafe operator will ensure that any job failures will be notified to someone if not set explicitly' + } + } +} + +Describe "Database Mail Profile" -Tag DatabaseMailProfile, Agent -ForEach $InstancesToTest { + $skipDatabaseMailProfile = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.databasemailprofile' }).Value + + Context "Testing Database Mail Profile exists on <_.Name>" { + It "The Database Mail profile <_.DatabaseMailProfile.ExpectedDatabaseMailProfile> exists on <_.Name>" -Skip:$skipDatabaseMailProfile { #-ForEach ($PSItem.DatabaseMailProfile | Where-Object ExpectedDatabaseMailProfile -NE 'null') { + $PSItem.DatabaseMailProfile.ActualDatabaseMailProfile | Should -BeIn $PSItem.DatabaseMailProfile.ExpectedDatabaseMailProfile -Because 'The database mail profile is required to send emails' + } + } +} + +Describe "Agent Mail Profile" -Tag AgentMailProfile, Agent -ForEach $InstancesToTest { + $skipAgentMailProfile = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.mailprofile' }).Value + + Context "Testing SQL Agent Alert System database mail profile is set on <_.Name>" { + It "The SQL Server Agent Alert System has the mail profile <_.AgentMailProfile.ExpectedAgentMailProfile> enabled as profile on <_.Name>." -Skip:$skipAgentMailProfile { #-ForEach ($PSItem.DatabaseMailProfile | Where-Object ExpectedDatabaseMailProfile -NE 'null') { + $PSItem.AgentMailProfile.ActualAgentMailProfile | Should -Be $PSItem.AgentMailProfile.ExpectedAgentMailProfile -Because 'The SQL Agent Alert System needs an enabled database mail profile to send alert emails' + } + } +} + +Describe "Valid Job Owner" -Tag ValidJobOwner, Agent -ForEach $InstancesToTest { + $skipAgentJobTargetOwner = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.jobowner' }).Value + + Context "Testing SQL Agent Job Owner on <_.Name>" { + It "The Job <_.JobName> has the Job Owner <_.ActualJobOwnerName> that should exist in this list ($([String]::Join(', ', "<_.ExpectedJobOwnerName>"))) on <_.InstanceName>" -Skip:$skipAgentJobTargetOwner -ForEach ($PSItem.JobOwner) { + $PSItem.ActualJobOwnerName | Should -BeIn $PSItem.ExpectedJobOwnerName -Because 'The account that is the job owner is not what was expected' + } + } +} + + +Describe "Invalid Job Owner" -Tag InvalidJobOwner, Agent -ForEach $InstancesToTest { + $skipAgentJobTargetInvalidOwner = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.invalidjobowner.name' }).Value + + Context "Testing Invalid SQL Agent Job Owner on <_.Name>" { + It "The Job <_.JobName> has the Job Owner <_.ActualJobOwnerName> that shouldn't exist in this list ($([String]::Join(', ', "<_.InvalidJobOwnerName>"))) on <_.InstanceName>" -Skip:$skipAgentJobTargetInvalidOwner -ForEach ($PSItem.InvalidJobOwner) { + $PSItem.ActualJobOwnerName | Should -Not -BeIn $PSItem.InvalidJobOwnerName -Because 'The account that is the job owner has been defined as not valid' + } + } +} +Describe "Last Agent Job Run" -Tag LastJobRunTime, Agent -ForEach $InstancesToTest { + $skipAgentJobLastRun = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.lastjobruntime' }).Value + Context "Testing last job run time on <_.Name>" { + It "Job <_.JobName> last run duration (<_.Duration> seconds) should not be greater than <_.ExpectedRunningJobPercentage>% extra of the average run time (<_.Average> seconds) on <_.InstanceName>" -Skip:$skipAgentJobLastRun -ForEach ($PSItem.LastJobRuns) { + $PSItem.ActualRunningJobPercentage | Should -BeLessThan $PSItem.ExpectedRunningJobPercentage -Because "The last run of job $($PSItem.JobName) was $($PSItem.Duration) seconds. This is more than the $($PSItem.ExpectedRunningJobPercentage)% specified as the maximum variance" + } + } +} + + +Describe "Long Running Agent Jobs" -Tag LongRunningJob, Agent -ForEach $InstancesToTest { + $skipAgentLongRunningJobs = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.longrunningjobs' }).Value + + Context "Testing long running jobs on <_.Name>" { + It "Running job <_.JobName> duration should not be more than <_.ExpectedLongRunningJobPercentage>% extra of the average run time (<_.Average> seconds) on <_.InstanceName>" -Skip:$skipAgentLongRunningJobs -ForEach ($PSItem.LongRunningJobs) { + $PSItem.ActualLongRunningJobPercentage | Should -BeLessThan $PSItem.ExpectedLongRunningJobPercentage -Because "The current running job $($PSItem.JobName) has been running for $($PSItem.Diff) seconds longer than the average run time. This is more than the $($PSItem.ExpectedLongRunningJobPercentage)% specified as the maximum" + } + } +} + + +Describe "SQL Agent Failed Jobs" -Tag FailedJob, Agent -ForEach $InstancesToTest { + $skipAgentFailedJobs = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.failedjobs' }).Value + $excludecancelled = ($__dbcconfig | Where-Object { $_.Name -eq 'agent.failedjob.excludecancelled' }).Value + + Context "Checking for failed enabled jobs since on <_.Name>" { + if (-not $skipAgentFailedJobs) { + It "We chose to skip this as <_.JobName>'s last run outcome is unknown on <_.InstanceName>" -Skip -ForEach ($PSItem.JobsFailed | Where-Object { $_.LastRunOutcome -eq "Unknown" }) { + $PSItem.LastRunOutcome | Should -Be $PSItem.ExpectedOutcome -Because 'All Agent Jobs should have succeed this one is unknown - you need to investigate the failed jobs' + } + It "You chose to skip this as <_.JobName>'s last run outcome is cancelled on <_.InstanceName>" -Skip -ForEach ($PSItem.JobsFailed | Where-Object { $_.LastRunOutcome -eq "Cancelled" -and ($excludecancelled -eq $true) }) { + $PSItem.LastRunOutcome | Should -Be $PSItem.ExpectedOutcome -Because 'All Agent Jobs should have succeed this one is Cancelled - you need to investigate the failed jobs' + } + } + It "Job <_.JobName> last run outcome is <_.LastRunOutcome> on <_.InstanceName>" -Skip:$skipAgentFailedJobs -ForEach ($PSItem.JobsFailed | Where-Object { $_.LastRunOutcome -notin ("Cancelled", "Unknown") }) { + $PSItem.LastRunOutcome | Should -Be $PSItem.ExpectedOutcome -Because "All Agent Jobs should have succeed - you need to investigate the failed jobs" + } + } +} + + + +Describe "Agent Alerts" -Tag AgentAlert, Agent -ForEach $InstancesToTest { + $skipAgentAlerts = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.alert' }).Value + $AgentAlertJob = ($__dbcconfig | Where-Object { $_.Name -eq 'agent.alert.Job' }).Value + $AgentAlertNotification = ($__dbcconfig | Where-Object { $_.Name -eq 'agent.alert.Notification' }).Value + + Context "Testing Agent Alerts Severity exists on <_.Name>" { + It "Severity <_.AgentAlertSeverity> Alert should exist on <_.InstanceName>" -Skip:$skipAgentAlerts -ForEach ($PSItem.AgentAlerts.Severities) { #| Where-Object { $_.Severity -NE $null }) { + $PSItem.Severity | Should -Be $PSItem.AgentAlertSeverity -Because "Recommended Agent Alerts to exists http://blog.extreme-advice.com/2013/01/29/list-of-errors-and-severity-level-in-sql-server-with-catalog-view-sysmessages/" + } + + It "Severity <_.AgentAlertSeverity> Alert should be enabled on <_.InstanceName>" -Skip:$skipAgentAlerts -ForEach ($PSItem.AgentAlerts.Severities) { + $PSItem.IsEnabled | Should -Be $true -Because "Configured alerts should be enabled" + } + if ($AgentAlertJob) { + It "A job name for Severity <_.AgentAlertSeverity> Alert on <_.InstanceName>" -Skip:$skipAgentAlerts -ForEach ($PSItem.AgentAlerts.Severities) { + $PSItem.JobName -ne $null | Should -Be $true -Because "Should notify by SQL Agent Job" + } + } + if ($AgentAlertNotification) { + It "Severity <_.AgentAlertSeverity> Alert should have a notification on <_.InstanceName>" -Skip:$skipAgentAlerts -ForEach ($PSItem.AgentAlerts.Severities) { + $PSItem.HasNotification -in 1, 2, 3, 4, 5, 6, 7 | Should -Be $true -Because "Should notify by Agent notifications" + } + } + + } + + Context "Testing Agent Alerts MessageID exists on <_.Name>" { + It "MessageID <_.AgentMessageID> Alert should exist on <_.InstanceName>" -Skip:$skipAgentAlerts -ForEach ($PSItem.AgentAlerts.MessageIDs) { + $PSItem.MessageID | Should -Be $PSItem.AgentMessageID -Because "Recommended Agent Alerts to exists http://blog.extreme-advice.com/2013/01/29/list-of-errors-and-severity-level-in-sql-server-with-catalog-view-sysmessages/" + } + It "MessageID <_.AgentMessageID> Alert should be enabled on <_.InstanceName>" -Skip:$skipAgentAlerts -ForEach ($PSItem.AgentAlerts.MessageIDs) { + $PSItem.IsEnabled | Should -Be $true -Because "Configured alerts should be enabled" + } + if ($AgentAlertJob) { + It "A job name for MessageID <_.AgentMessageID> on <_.InstanceName>" -Skip:$skipAgentAlerts -ForEach ($PSItem.AgentAlerts.MessageIDs) { + $PSItem.JobName -ne $null | Should -Be $true -Because "Should notify by SQL Agent Job" + } + } + if ($AgentAlertNotification) { + It "MessageID <_.AgentMessageID> Alert should have a notification on <_.InstanceName>" -Skip:$skipAgentAlerts -ForEach ($PSItem.AgentAlerts.MessageIDs) { + $PSItem.HasNotification -in 1, 2, 3, 4, 5, 6, 7 | Should -Be $true -Because "Should notify by Agent notifications" + } + } + } +} + +Describe "Job History Configuration" -Tag JobHistory, Agent -ForEach $InstancesToTest { + $skipAgentJobHistory = ($__dbcconfig | Where-Object { $_.Name -eq 'skip.agent.JobHistory' }).Value + [int]$minimumJobHistoryRows = ($__dbcconfig | Where-Object { $_.Name -eq 'agent.history.maximumhistoryrows' }).Value + + if ($minimumJobHistoryRows -eq -1) { + It "The maximum job history configuration should be set to disabled on <_.InstanceName>" -Skip:$skipAgentJobHistory -ForEach ($PSItem.JobHistory) { + $PSItem.CurrentMaximumHistoryRows | Should -Be $PSItem.ExpectedMaximumHistoryRows -Because "Maximum job history configuration should be disabled" + } + } else { + It "The maximum job history number of rows configuration should be greater or equal to <_.ExpectedMaximumHistoryRows> on <_.InstanceName>" -Skip:$skipAgentJobHistory -ForEach ($PSItem.JobHistory) { + $PSItem.CurrentMaximumHistoryRows | Should -BeGreaterOrEqual $PSItem.ExpectedMaximumHistoryRows -Because "We expect the maximum job history row configuration to be greater than the configured setting <_.ExpectedMaximumHistoryRows>" + } + It "The maximum job history rows per job configuration should be greater or equal to <_.ExpectedMaximumJobHistoryRows> on <_.InstanceName>" -Skip:$skipAgentJobHistory -ForEach ($PSItem.JobHistory) { + $PSItem.CurrentMaximumJobHistoryRows | Should -BeGreaterOrEqual $PSItem.ExpectedMaximumJobHistoryRows -Because "We expect the maximum job history row configuration per agent job to be greater than the configured setting <_.ExpectedMaximumJobHistoryRows>" + } + } +} \ No newline at end of file diff --git a/source/internal/configurations/configuration.ps1 b/source/internal/configurations/configuration.ps1 index baf3b028..f56e50bd 100644 --- a/source/internal/configurations/configuration.ps1 +++ b/source/internal/configurations/configuration.ps1 @@ -18,7 +18,7 @@ $EmailValidationSb = { } Register-PSFConfigValidation -Name validation.EmailValidation -ScriptBlock $EmailValidationSb -$__dbachecksNotv5 = 'ADUser', 'BuiltInAdmin', 'EngineServiceAdmin', 'FullTextServiceAdmin', 'LocalWindowsGroup', 'PublicPermission', 'SqlBrowserServiceAccount', 'TempDbConfiguration','CertificateExpiration', 'DatabaseExists', 'DatabaseGrowthEvent', 'DatabaseMailEnabled', 'DatabaseMailProfile', 'DatafileAutoGrowthType', 'DisabledIndex', 'DuplicateIndex', 'FileGroupBalanced', 'FKCKTrusted', 'FutureFileGrowth', 'IdentityUsage', 'LastDiffBackup', 'LastFullBackup', 'LastGoodCheckDb', 'LastLogBackup', 'LogfilePercentUsed', 'LogfileSize', 'MaxDopDatabase', 'OrphanedUser', 'SymmetricKeyEncryptionLevel', 'TestLastBackup', 'TestLastBackupVerifyOnly', 'UnusedIndex', 'DatabaseMailEnabled', 'AgentServiceAccount', 'DbaOperator', 'FailsafeOperator', 'DatabaseMailProfile', 'AgentMailProfile', 'FailedJob', 'ValidJobOwner', 'InValidJobOwner', 'AgentAlert', 'JobHistory', 'LongRunningJob', 'LastJobRunTime', 'PowerPlan', 'SPN', 'DiskCapacity', 'PingComputer', 'CPUPrioritisation', 'DiskAllocationUnit', 'NonStandardPort', 'ServerProtocol', 'OlaInstalled', 'SystemFull', 'UserFull', 'UserDiff', 'UserLog', 'CommandLog', 'SystemIntegrityCheck', 'UserIntegrityCheck', 'UserIndexOptimize', 'OutputFileCleanup', 'DeleteBackupHistory', 'PurgeJobHistory', 'DomainName', 'OrganizationalUnit', 'ClusterHealth', 'LogShippingPrimary', 'LogShippingSecondary' +$__dbachecksNotv5 = 'ADUser', 'BuiltInAdmin', 'EngineServiceAdmin', 'FullTextServiceAdmin', 'LocalWindowsGroup', 'PublicPermission', 'SqlBrowserServiceAccount', 'TempDbConfiguration'',CertificateExpiration', 'DatabaseExists', 'DatabaseGrowthEvent', 'DatafileAutoGrowthType', 'DisabledIndex', 'DuplicateIndex', 'FileGroupBalanced', 'FKCKTrusted', 'FutureFileGrowth', 'IdentityUsage', 'LastDiffBackup', 'LastFullBackup', 'LastGoodCheckDb', 'LastLogBackup', 'LogfilePercentUsed', 'LogfileSize', 'MaxDopDatabase', 'OrphanedUser', 'SymmetricKeyEncryptionLevel', 'TestLastBackup', 'TestLastBackupVerifyOnly', 'UnusedIndex', 'PowerPlan', 'SPN', 'DiskCapacity', 'PingComputer', 'CPUPrioritisation', 'DiskAllocationUnit', 'NonStandardPort', 'ServerProtocol', 'OlaInstalled', 'SystemFull', 'UserFull', 'UserDiff', 'UserLog', 'CommandLog', 'SystemIntegrityCheck', 'UserIntegrityCheck', 'UserIndexOptimize', 'OutputFileCleanup', 'DeleteBackupHistory', 'PurgeJobHistory', 'DomainName', 'OrganizationalUnit', 'ClusterHealth', 'LogShippingPrimary', 'LogShippingSecondary' Set-PSFConfig -Module dbachecks -Name checks.notv5ready -Value @($__dbachecksNotv5) -Initialize -Description "Checks that have not been converted to v5 yet" @@ -354,10 +354,18 @@ Set-PSFConfig -Module dbachecks -Name skip.hadr.listener.pingcheck -Validation b Set-PSFConfig -Module dbachecks -Name skip.agent.databasemailenabled -Validation bool -Value $false -Initialize -Description "Skip the Database Mail Enabled agent check" Set-PSFConfig -Module dbachecks -Name skip.agent.servicestartmode -Validation bool -Value $false -Initialize -Description "Skip the Agent Service State check" Set-PSFConfig -Module dbachecks -Name skip.agent.servicestate -Validation bool -Value $false -Initialize -Description "Skip the Agent Service Start Mode check" -Set-PSFConfig -Module dbachecks -Name skip.agent.operatorname -Validation bool -Value $false -Initialize -Description "Skip the Agent Operator Name check" -Set-PSFConfig -Module dbachecks -Name skip.agent.operatoremail -Validation bool -Value $false -Initialize -Description "Skip the Agent Operator Email check" +Set-PSFConfig -Module dbachecks -Name skip.agent.dbaoperatorname -Validation bool -Value $false -Initialize -Description "Skip the Agent Operator Name check" +Set-PSFConfig -Module dbachecks -Name skip.agent.dbaoperatoremail -Validation bool -Value $false -Initialize -Description "Skip the Agent Operator Email check" +Set-PSFConfig -Module dbachecks -Name skip.agent.failsafeoperator -Validation bool -Value $false -Initialize -Description "Skip the Agent Failsafe Operator check" +Set-PSFConfig -Module dbachecks -Name skip.agent.databasemailprofile -Validation bool -Value $false -Initialize -Description "Skip the Database Mail Profile check" +Set-PSFConfig -Module dbachecks -Name skip.agent.mailprofile -Validation bool -Value $false -Initialize -Description "Skip the SQL Server Agent Mail Profile check" Set-PSFConfig -Module dbachecks -Name skip.agent.longrunningjobs -Validation bool -Value $false -Initialize -Description "Skip the long running agent jobs check" Set-PSFConfig -Module dbachecks -Name skip.agent.lastjobruntime -Validation bool -Value $false -Initialize -Description "Skip the last agent job time check" +Set-PSFConfig -Module dbachecks -Name skip.agent.jobowner -Validation bool -Value $false -Initialize -Description "Skip the Agent Job Owner check" +Set-PSFConfig -Module dbachecks -Name skip.agent.invalidjobowner.name -Validation bool -Value $false -Initialize -Description "Skip the Agent Job Invalid Owner check" +Set-PSFConfig -Module dbachecks -Name skip.agent.failedjobs -Validation bool -Value $false -Initialize -Description "Skip the Agent Failed Jobs check" +Set-PSFConfig -Module dbachecks -Name skip.agent.JobHistory -Validation bool -Value $false -Initialize -Description "Skip the Agent Job History check" + Set-PSFConfig -Module dbachecks -Name skip.security.containedbautoclose -Validation bool -Value $true -Initialize -Description "Skips the scan for contained databases should have auto close enabled" @@ -389,8 +397,10 @@ Set-PSFConfig -Module dbachecks -Name skip.security.serverprotocol -Validation b #agent Set-PSFConfig -Module dbachecks -Name agent.dbaoperatorname -Value $null -Initialize -Description "Name of the DBA Operator in SQL Agent" Set-PSFConfig -Module dbachecks -Name agent.dbaoperatoremail -Value $null -Initialize -Description "Email address of the DBA Operator in SQL Agent" -Set-PSFConfig -Module dbachecks -Name agent.failsafeoperator -Value $null -Initialize -Description "Email address of the DBA Operator in SQL Agent" +Set-PSFConfig -Module dbachecks -Name agent.failsafeoperator -Value $null -Initialize -Description "Email address of the Failsafe Operator in SQL Agent" +# TODO: Should this be instance instead of agent? Set-PSFConfig -Module dbachecks -Name agent.databasemailprofile -Value $null -Initialize -Description "Name of the Database Mail Profile in SQL Agent" +Set-PSFConfig -Module dbachecks -Name agent.mailprofile -Value $null -Initialize -Description "Name of the SQL Server Agent Mail Profile in SQL Agent" Set-PSFConfig -Module dbachecks -Name agent.validjobowner.name -Value "sa" -Initialize -Description "Agent job owner account should be this user" Set-PSFConfig -Module dbachecks -Name agent.invalidjobowner.name -Value $null -Initialize -Description "Agent job owner account should not be this user" Set-PSFConfig -Module dbachecks -Name agent.alert.messageid -Value @('823', '824', '825') -Initialize -Description "Agent alert messageid to validate; https://www.brentozar.com/blitz/configure-sql-server-alerts/" diff --git a/source/internal/functions/Get-AllAgentInfo.ps1 b/source/internal/functions/Get-AllAgentInfo.ps1 index 3c82897b..f9221e27 100644 --- a/source/internal/functions/Get-AllAgentInfo.ps1 +++ b/source/internal/functions/Get-AllAgentInfo.ps1 @@ -2,12 +2,14 @@ function Get-AllAgentInfo { # Using the unique tags gather the information required Param($Instance, $Tags) + #ToDo: Clean unused SMO classes #clear out the default initialised fields $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Server], $false) $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Database], $false) $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Login], $false) $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Job], $false) $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Operator], $false) + $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.AlertSystem], $false) $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.StoredProcedure], $false) $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Information], $false) $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Settings], $false) @@ -24,28 +26,29 @@ function Get-AllAgentInfo { # Job Server Initial fields $OperatorInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Operator]) - # Database Initial Fields - $DatabaseInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Database]) + # Job Server Alert System Initial fields + $FailsafeInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.AlertSystem]) - # Stored Procedure Initial Fields - $StoredProcedureInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.StoredProcedure]) + # JobServer Initial fields + $AgentMailProfileInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.JobServer]) - # Information Initial Fields + # Database Mail Profile Initial fields + $DatabaseMailProfileInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Mail.MailProfile]) - # Settings Initial Fields - $SettingsInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Settings]) + # JobOwner Initial fields + $JobOwnerInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Job]) - # Login Initial Fields - $LoginInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Login]) + # Invalid JobOwner Initial fields + $InvalidJobOwnerInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Job]) - # Log File Initial Fields - $LogFileInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.LogFile]) + # Failed Job Initial fields + $FailedJobInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Job]) - # Data File Initial Fields - $DataFileInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.DataFile]) + # Agent Alerts Initial fields + $AgentAlertsInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Alert]) - # Configuration cannot have default init fields :-) - $configurations = $false + # Agent Job History Initial fields + $AgentJobHistory = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.JobServer]) # Set up blank ConfigValues object for any config we need to use in the checks $ConfigValues = [PSCustomObject]@{} @@ -55,17 +58,18 @@ function Get-AllAgentInfo { 'DatabaseMailEnabled' { $configurations = $true - $ConfigValues | Add-Member -MemberType NoteProperty -Name 'DatabaseMailEnabled' -Value (Get-DbcConfigValue policy.security.databasemailenabled) + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'DatabaseMailEnabled' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'policy.security.databasemailenabled' }).Value) } 'AgentServiceAccount' { if (($Instance.VersionMajor -ge 14) -or $IsLinux -or $Instance.HostPlatform -eq 'Linux') { - $Agent = @($Instance.Query("SELECT status_desc, startup_type_desc FROM sys.dm_server_services") | Where-Object servicename -like '*Agent*').ForEach{ + $Agent = @($Instance.Query("SELECT status_desc, startup_type_desc, servicename FROM sys.dm_server_services") | Where-Object servicename -Like '*Agent*').ForEach{ [PSCustomObject]@{ - State = $PSItem.status_desc + State = $PSItem.status_desc StartMode = $PSItem.startup_type_desc } } - } else { # Windows + } else { + # Windows $Agent = @(Get-DbaService -ComputerName $Instance.ComputerName -Type Agent) } } @@ -74,58 +78,303 @@ function Get-AllAgentInfo { $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Operator], $OperatorInitFields) $OperatorInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Operator]) - $ConfigValues | Add-Member -MemberType NoteProperty -Name 'DbaOperatorName' -Value (Get-DbcConfigValue agent.dbaoperatorname) - $ConfigValues | Add-Member -MemberType NoteProperty -Name 'DbaOperatorEmail' -Value (Get-DbcConfigValue agent.dbaoperatoremail) + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'DbaOperatorName' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'agent.dbaoperatorname' }).Value) + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'DbaOperatorEmail' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'agent.dbaoperatoremail' }).Value) $Operator = $ConfigValues.DbaOperatorName.ForEach{ [PSCustomObject]@{ - InstanceName = $Instance.Name - ExpectedOperatorName = $PSItem - ActualOperatorName = $Instance.JobServer.Operators.Name - ExpectedOperatorEmail = 'null' - ActualOperatorEmail = 'null' + InstanceName = $Instance.Name + ExpectedOperatorName = $PSItem + ActualOperatorName = $Instance.JobServer.Operators.Name + ExpectedOperatorEmail = 'null' + ActualOperatorEmail = 'null' } } $Operator += $ConfigValues.DbaOperatorEmail.ForEach{ [PSCustomObject]@{ - InstanceName = $Instance.Name - ExpectedOperatorName = 'null' - ActualOperatorName = 'null' - ExpectedOperatorEmail = $PSItem - ActualOperatorEmail = $Instance.JobServer.Operators.EmailAddress + InstanceName = $Instance.Name + ExpectedOperatorName = 'null' + ActualOperatorName = 'null' + ExpectedOperatorEmail = $PSItem + ActualOperatorEmail = $Instance.JobServer.Operators.EmailAddress } } } 'FailsafeOperator' { + $FailsafeInitFields.Add("FailSafeOperator") | Out-Null # so we can check failsafe operators + $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.AlertSystem], $FailsafeInitFields) + $FailsafeInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.AlertSystem]) + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'FailsafeOperator' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'agent.failsafeoperator' }).Value) + + $failsafeOperator = $ConfigValues.FailsafeOperator.ForEach{ + [PSCustomObject]@{ + InstanceName = $Instance.Name + ExpectedFailSafeOperator = $PSItem + ActualFailSafeOperator = $Instance.JobServer.AlertSystem.FailSafeOperator + } + } } 'DatabaseMailProfile' { + $DatabaseMailProfileInitFields.Add("Name") | Out-Null # so we can check failsafe operators + $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Mail.MailProfile], $DatabaseMailProfileInitFields) + $DatabaseMailProfileInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Mail.MailProfile]) + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'DatabaseMailProfile' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'agent.databasemailprofile' }).Value) + + $databaseMailProfile = $ConfigValues.DatabaseMailProfile.ForEach{ + [PSCustomObject]@{ + InstanceName = $Instance.Name + ExpectedDatabaseMailProfile = $ConfigValues.DatabaseMailProfile + ActualDatabaseMailProfile = $Instance.Mail.Profiles.Name + } + } } 'AgentMailProfile' { + $AgentMailProfileInitFields.Add("DatabaseMailProfile") | Out-Null # so we can check failsafe operators + $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.JobServer], $AgentMailProfileInitFields) + $AgentMailProfileInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.JobServer]) + + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'AgentMailProfile' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'agent.databasemailprofile' }).Value) + + $agentMailProfile = $ConfigValues.AgentMailProfile.ForEach{ + [PSCustomObject]@{ + InstanceName = $Instance.Name + ExpectedAgentMailProfile = $ConfigValues.AgentMailProfile + ActualAgentMailProfile = $Instance.JobServer.DatabaseMailProfile + } + } } 'FailedJob' { + $FailedJobInitFields.Add("Name") | Out-Null # so we can check Job Name + $FailedJobInitFields.Add("IsEnabled") | Out-Null # so we can check Job status + $FailedJobInitFields.Add("LastRunDate") | Out-Null # so we can check Job LastRunDate + $FailedJobInitFields.Add("LastRunOutcome") | Out-Null # so we can check Job LastRunOutcome + $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Job], $FailedJobInitFields) + $FailedJobInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Job]) + + $maxdays = ($__dbcconfig | Where-Object { $_.Name -eq 'agent.failedjob.since' }).Value + $startdate = (Get-Date).AddDays( - $maxdays) + + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'FailedJob' -Value 'Succeeded' + + $JobsFailed = ($Instance.JobServer.Jobs | Where-Object { $_.IsEnabled -and ($_.LastRunDate -gt $startdate) }).ForEach{ + [PSCustomObject]@{ + InstanceName = $Instance.Name + JobName = $PSItem.Name + ExpectedOutcome = $ConfigValues.FailedJob + LastRunOutcome = $PSItem.LastRunOutcome + } + } } 'ValidJobOwner' { + $JobOwnerInitFields.Add("OwnerLoginName") | Out-Null # so we can check Job Owner + $JobOwnerInitFields.Add("Name") | Out-Null # so we can check Job Name + $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Job], $JobOwnerInitFields) + $JobOwnerInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Job]) + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'TargetJobOwner' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'agent.validjobowner.name' }).Value) + + $JobOwner = $Instance.JobServer.Jobs.ForEach{ + [PSCustomObject]@{ + InstanceName = $Instance.Name + JobName = $PSItem.Name + ExpectedJobOwnerName = $ConfigValues.TargetJobOwner #$PSItem + ActualJobOwnerName = $PSItem.OwnerLoginName + } + } } - 'InValidJobOwner' { + 'InvalidJobOwner' { + $InvalidJobOwnerInitFields.Add("OwnerLoginName") | Out-Null # so we can check Job Owner + $InvalidJobOwnerInitFields.Add("Name") | Out-Null # so we can check Job Name + $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Job], $InvalidJobOwnerInitFields) + $InvalidJobOwnerInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Job]) + + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'InvalidJobOwner' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'agent.invalidjobowner.name' }).Value) + + $InvalidJobOwner = $Instance.JobServer.Jobs.ForEach{ + [PSCustomObject]@{ + InstanceName = $Instance.Name + JobName = $PSItem.Name + ExpectedJobOwnerName = $ConfigValues.InvalidJobOwner + ActualJobOwnerName = $PSItem.OwnerLoginName + } + } } 'AgentAlert' { + $AgentAlertsInitFields.Add("Severity") | Out-Null # so we can check Alert Severity + $AgentAlertsInitFields.Add("IsEnabled") | Out-Null # so we can check Alert status + $AgentAlertsInitFields.Add("JobName") | Out-Null # so we can check Alert job + $AgentAlertsInitFields.Add("HasNotification") | Out-Null # so we can check Alert notification + + $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Alert], $AgentAlertsInitFields) + $AgentAlertsInitFields = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.Alert]) + + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'AgentAlertSeverity' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'agent.alert.Severity' }).Value) + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'AgentAlertMessageId' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'agent.alert.messageid' }).Value) + + $Severities = $ConfigValues.AgentAlertSeverity.ForEach{ + $Severity = [int]($PSItem) + $sev = $Instance.JobServer.Alerts.Where{ $_.Severity -eq $Severity } + [PSCustomObject]@{ + InstanceName = $Instance.Name + AlertName = $sev.Name + Severity = $sev.Severity + IsEnabled = $sev.IsEnabled + JobName = $sev.JobName + HasNotification = $sev.HasNotification + AgentAlertSeverity = $Severity + } + } + + $MessageIDs = $ConfigValues.AgentAlertMessageId.ForEach{ + $MessageID = [int]($PSItem) + $msgID = $Instance.JobServer.Alerts.Where{ $_.MessageID -eq $MessageID } + [PSCustomObject]@{ + InstanceName = $Instance.Name + AlertName = $msgID.Name + MessageID = $msgID.MessageID + IsEnabled = $msgID.IsEnabled + JobName = $msgID.JobName + HasNotification = $msgID.HasNotification + AgentMessageID = $MessageID + } + } + $AgentAlerts = [PSCustomObject]@{ + Severities = $Severities + MessageIDs = $MessageIDs + } } 'JobHistory' { + $AgentJobHistory.Add("MaximumHistoryRows") | Out-Null # so we can check Alert Severity + $AgentJobHistory.Add("MaximumJobHistoryRows") | Out-Null # so we can check Alert status + + $Instance.SetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.JobServer], $AgentJobHistory) + $AgentJobHistory = $Instance.GetDefaultInitFields([Microsoft.SqlServer.Management.Smo.Agent.JobServer]) + + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'AgentMaximumHistoryRows' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'agent.history.maximumhistoryrows' }).Value) + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'AgentMaximumJobHistoryRows' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'agent.history.maximumjobhistoryrows' }).Value) + $JobHistory = [PSCustomObject]@{ + InstanceName = $Instance.Name + CurrentMaximumHistoryRows = $Instance.JobServer.MaximumHistoryRows + ExpectedMaximumHistoryRows = $ConfigValues.AgentMaximumHistoryRows + CurrentMaximumJobHistoryRows = $Instance.JobServer.MaximumJobHistoryRows + ExpectedMaximumJobHistoryRows = $ConfigValues.AgentMaximumJobHistoryRows + } } 'LongRunningJob' { + $query = "SELECT + JobName, + AvgSec, + start_execution_date as StartDate, + RunningSeconds, + RunningSeconds - AvgSec AS Diff + FROM + ( + SELECT + j.name AS JobName, + start_execution_date, + AVG(DATEDIFF(SECOND, 0, STUFF(STUFF(RIGHT('000000' + + CONVERT(VARCHAR(6),jh.run_duration),6),5,0,':'),3,0,':'))) AS AvgSec, + ja.start_execution_date as startdate, + DATEDIFF(second, ja.start_execution_date, GetDate()) AS RunningSeconds + FROM msdb.dbo.sysjobactivity ja + JOIN msdb.dbo.sysjobs j + ON ja.job_id = j.job_id + JOIN msdb.dbo.sysjobhistory jh + ON jh.job_id = j.job_id + WHERE start_execution_date is not null + AND stop_execution_date is null + AND run_duration < 235959 + AND run_duration >= 0 + AND ja.start_execution_date > DATEADD(day,-1,GETDATE()) + GROUP BY j.name,j.job_id,start_execution_date,stop_execution_date,ja.job_id + ) AS t + ORDER BY JobName;" + $runningjobs = Invoke-DbaQuery -SqlInstance $Instance -Database msdb -Query $query + + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'LongRunningJob' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'agent.longrunningjob.percentage' }).Value) + $LongRunningJobs = $($runningjobs | Where-Object { $_.AvgSec -ne 0 }).ForEach{ + [PSCustomObject]@{ + InstanceName = $Instance.Name + JobName = $PSItem.JobName + RunningSeconds = $PSItem.RunningSeconds + Average = $PSItem.AvgSec + Diff = $PSItem.Diff + ExpectedLongRunningJobPercentage = $ConfigValues.LongRunningJob + ActualLongRunningJobPercentage = [math]::Round($PSItem.Diff / $PSItem.AvgSec * 100) + } + } } 'LastJobRunTime' { + $maxdays = ($__dbcconfig | Where-Object { $_.Name -eq 'agent.failedjob.since' }).Value + $query = "IF OBJECT_ID('tempdb..#dbachecksLastRunTime') IS NOT NULL DROP Table #dbachecksLastRunTime + SELECT * INTO #dbachecksLastRunTime + FROM + ( + SELECT + j.job_id, + j.name AS JobName, + DATEDIFF(SECOND, 0, STUFF(STUFF(RIGHT('000000' + CONVERT(VARCHAR(6),jh.run_duration),6),5,0,':'),3,0,':')) AS Duration + FROM msdb.dbo.sysjobs j + INNER JOIN + ( + SELECT job_id, instance_id = MAX(instance_id) + FROM msdb.dbo.sysjobhistory + GROUP BY job_id + ) AS h + ON j.job_id = h.job_id + INNER JOIN + msdb.dbo.sysjobhistory AS jh + ON jh.job_id = h.job_id + AND jh.instance_id = h.instance_id + WHERE msdb.dbo.agent_datetime(jh.run_date, jh.run_time) > DATEADD(DAY,- {0},GETDATE()) + AND jh.step_id = 0 + ) AS lrt + IF OBJECT_ID('tempdb..#dbachecksAverageRunTime') IS NOT NULL DROP Table #dbachecksAverageRunTime + SELECT * INTO #dbachecksAverageRunTime + FROM + ( + SELECT + job_id, + AVG(DATEDIFF(SECOND, 0, STUFF(STUFF(RIGHT('000000' + CONVERT(VARCHAR(6),run_duration),6),5,0,':'),3,0,':'))) AS AvgSec + FROM msdb.dbo.sysjobhistory hist + WHERE msdb.dbo.agent_datetime(run_date, run_time) > DATEADD(DAY,- {0},GETDATE()) + AND Step_id = 0 + AND run_duration >= 0 + GROUP BY job_id + ) as art + SELECT + JobName, + Duration, + AvgSec, + Duration - AvgSec AS Diff + FROM #dbachecksLastRunTime lastrun + JOIN #dbachecksAverageRunTime avgrun + ON lastrun.job_id = avgrun.job_id + DROP Table #dbachecksLastRunTime + DROP Table #dbachecksAverageRunTime" -f $maxdays + $lastagentjobruns = Invoke-DbaQuery -SqlInstance $Instance -Database msdb -Query $query + + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'LastJobRuns' -Value (($__dbcconfig | Where-Object { $_.Name -eq 'agent.lastjobruntime.percentage' }).Value) + $LastJobRuns = $($lastagentjobruns | Where-Object { $_.AvgSec -ne 0 }).ForEach{ + [PSCustomObject]@{ + InstanceName = $Instance.Name + JobName = $PSItem.JobName + Duration = $PSItem.Duration + Average = $PSItem.AvgSec + ExpectedRunningJobPercentage = $ConfigValues.LastJobRuns + ActualRunningJobPercentage = [math]::Round($PSItem.Diff / $PSItem.AvgSec * 100) + } + } } Default { } } @@ -141,6 +390,16 @@ function Get-AllAgentInfo { DatabaseMailEnabled = $Instance.Configuration.DatabaseMailEnabled.ConfigValue Agent = @($Agent) Operator = @($Operator) + FailSafeOperator = @($failsafeOperator) + DatabaseMailProfile = @($databaseMailProfile) + AgentMailProfile = @($agentMailProfile) + JobOwner = $JobOwner + InvalidJobOwner = $InvalidJobOwner + JobsFailed = $JobsFailed + LastJobRuns = $LastJobRuns + LongRunningJobs = $LongRunningJobs + AgentAlerts = $AgentAlerts + JobHistory = @($JobHistory) } return $testInstanceObject } \ No newline at end of file