diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bc8dfd3..5400221d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - fixed typo in Invoke-PerfAndValidateCheck so we know which version we're looking at +- New database level check `LastFullBackup` - Updated HADR checks to use dbatools new outputs ## [3.0.2] - 2025-03-10 diff --git a/containers/JessAndBeard.psm1 b/containers/JessAndBeard.psm1 index 20a7a393..f77ded8c 100644 --- a/containers/JessAndBeard.psm1 +++ b/containers/JessAndBeard.psm1 @@ -2379,6 +2379,13 @@ The Tags are the same" PassedChange = 0 # + or - the number of tests passed for v5 FailedChange = 0 # + or - the number of tests failed for v5 SkippedChange = -3 # + or - the number of tests skipped for v5 + }, + @{ + Name = 'LastFullBackup' + RunChange = +1 # + or - the number of tests run for v5 + PassedChange = 0 # + or - the number of tests passed for v5 + FailedChange = 0 # + or - the number of tests failed for v5 + SkippedChange = +1 # + or - the number of tests skipped for v5 } ) $runchange = 0 diff --git a/source/checks/Databasev5.Tests.ps1 b/source/checks/Databasev5.Tests.ps1 index bea0c23d..ec9f84e1 100644 --- a/source/checks/Databasev5.Tests.ps1 +++ b/source/checks/Databasev5.Tests.ps1 @@ -307,7 +307,7 @@ Describe "Page Verify" -Tag PageVerify, Medium, Database -ForEach $InstancesToTe Describe "Foreign keys and check constraints not trusted" -Tag FKCKTrusted, Low, Database -ForEach $InstancesToTest { $Skip = ($__dbcconfig | Where-Object Name -EQ 'skip.database.fkcktrusted').Value - Context "Testing Foreign Keys and Check Constraints are not trusted <_.Name>" { + Context "Testing Foreign Keys and Check Constraints are not trusted <_.Name>" { It "Database <_.Database> Foreign Key <_.Name> on table <_.Parent> should be trusted on <_.SqlInstance>" -Skip:$skip -ForEach $psitem.Databases.Where{ if ($Database) { $_.Name -in $Database } else { $psitem.ConfigValues.fkcktrustedexclude -notcontains $psitem.Name } }.ForeignKeys { $psitem.IsChecked | Should -Be $true -Because "This can have a huge performance impact on queries. SQL Server won't use untrusted constraints to build better execution plans. It will also avoid data violation" @@ -319,3 +319,38 @@ Describe "Foreign keys and check constraints not trusted" -Tag FKCKTrusted, Low } } +#TODO: test if database is offline or not `IsAccessible` +Describe "Last Full Backup Times" -Tag LastFullBackup, LastBackup, Backup, DISA, Varied -ForEach $InstancesToTest { + $Skip = ($__dbcconfig | Where-Object Name -EQ 'skip.database.lastfullbackup').Value + #TODO: also skip secondaries? and read only? + Context "Testing last full backups on <_.Name>" { + It "Database <_.Name> should have full backups less than <_.ConfigValues.fullmaxdays> on <_.SqlInstance>" -Skip:$skip -ForEach $psitem.Databases.Where{if ($Database) { $_.Name -in $Database } else { $psitem.ConfigValues.fullbackupexclude -notcontains $psitem.Name } }.Where{if ($psitem.ConfigValues.skipreadonly) { -not $psitem.readonly } else {$psitem} }.Where{if ($psitem.ConfigValues.skipsecondaries) { $psitem.AGReplicaRole -ne 'Secondary' } else {$psitem} } { #TODO: secondary? is that right + $psitem.LastFullBackup.ToUniversalTime() | Should -BeGreaterThan (Get-Date).ToUniversalTime().AddDays( - ($psitem.ConfigValues.fullmaxdays)) -Because "Taking regular backups is extraordinarily important" + } + } +} + + +#TODO: convert checks +# TestLastBackup +# TestLastBackupVerifyOnly +# LastGoodCheckDb +# IdentityUsage +# DuplicateIndex +# UnusedIndex +# DisabledIndex +# DatabaseGrowthEvent +# LastFullBackup +# LastDiffBackup +# LastLogBackup +# LogfilePercentUsed +# LogfileSize +# FutureFileGrowth +# FileGroupBalanced +# CertificateExpiration +# DatafileAutoGrowthType +# OrphanedUser +# MaxDopDatabase +# DatabaseExists +# CLRAssembliesSafe +# SymmetricKeyEncryptionLevel diff --git a/source/internal/configurations/configuration.ps1 b/source/internal/configurations/configuration.ps1 index 427385a3..81088f44 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', 'DatafileAutoGrowthType', 'DisabledIndex', 'DuplicateIndex', 'FileGroupBalanced', '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' +$__dbachecksNotv5 = 'ADUser', 'BuiltInAdmin', 'EngineServiceAdmin', 'FullTextServiceAdmin', 'LocalWindowsGroup', 'PublicPermission', 'SqlBrowserServiceAccount', 'TempDbConfiguration','CertificateExpiration', 'DatabaseExists', 'DatabaseGrowthEvent', 'DatafileAutoGrowthType', 'DisabledIndex', 'DuplicateIndex', 'FileGroupBalanced', 'FutureFileGrowth', 'IdentityUsage', 'LastDiffBackup', '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" @@ -178,7 +178,9 @@ Set-PSFConfig -Module dbachecks -Name policy.database.clrassembliessafeexcludedb Set-PSFConfig -Module dbachecks -Name policy.database.pseudosimpleexcludedb -Value @('tempdb', 'model') -Initialize -Description "A List of databases that we do not want to check for pseudosimple recovery modelasd a" Set-PSFConfig -Module dbachecks -Name policy.database.contdbautocloseexclude -Value @('msdb') -Initialize -Description "A List of contained database that we we do not want to check for autoclose" Set-PSFConfig -Module dbachecks -Name policy.database.contdbsqlauthexclude -Value @() -Initialize -Description "A list of databases that we do not want to check for contained databases with SQL authenticated users" -Set-PSFConfig -Module dbachecks -Name policy.database.logfilepercentused -Value 75 -Initialize -Description " The % log used we should stay below" +Set-PSFConfig -Module dbachecks -Name policy.database.logfilepercentused -Value 75 -Initialize -Description "The % log used we should stay below" +Set-PSFConfig -Module dbachecks -Name policy.database.fullbackupexclude -Value @('tempdb') -Initialize -Description "The list of databases that we do not want to check the date of the last full backup of." + # Policy for Ola Hallengren Maintenance Solution Set-PSFConfig -Module dbachecks -Name policy.ola.installed -Validation bool -Value $true -Initialize -Description "Checks to see if Ola Hallengren solution is installed" @@ -344,6 +346,8 @@ Set-PSFConfig -Module dbachecks -Name skip.database.recoverymodel -Validation bo Set-PSFConfig -Module dbachecks -Name skip.database.pseudosimple -Validation bool -Value $false -Initialize -Description "Skip the database PseudoSimple recovery model test" Set-PSFConfig -Module dbachecks -Name skip.database.pageverify -Validation bool -Value $false -Initialize -Description "Skip the database page verify test" Set-PSFConfig -Module dbachecks -Name skip.database.fkcktrusted -Validation bool -Value $false -Initialize -Description "Skip the check for foreign keys and constraints being trusted" +Set-PSFConfig -Module dbachecks -Name skip.database.lastfullbackup -Validation bool -Value $false -Initialize -Description "Skip the check for last full backup" + Set-PSFConfig -Module dbachecks -Name skip.logshiptesting -Validation bool -Value $false -Initialize -Description "Skip the logshipping test" diff --git a/source/internal/functions/Get-AllDatabaseInfo.ps1 b/source/internal/functions/Get-AllDatabaseInfo.ps1 index 04748e78..3cd33235 100644 --- a/source/internal/functions/Get-AllDatabaseInfo.ps1 +++ b/source/internal/functions/Get-AllDatabaseInfo.ps1 @@ -158,13 +158,21 @@ function Get-AllDatabaseInfo { } 'PageVerify' { $pageverify = $true - $ConfigValues | Add-Member -MemberType NoteProperty -Name 'pageverifyexclude' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.database.contdbsqlauthexclude').Value + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'pageverifyexclude' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.database.contdbsqlauthexclude').Value #TODO: fix that config name $ConfigValues | Add-Member -MemberType NoteProperty -Name 'pageverify' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.pageverify').Value } 'FKCKTrusted' { $trusted = $true $ConfigValues | Add-Member -MemberType NoteProperty -Name 'fkcktrustedexclude' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.database.fkcktrustedexclude').Value } + 'LastFullBackup' { + $lastFullBackup = $true + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'fullbackupexclude' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.database.fullbackupexclude').Value + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'fullmaxdays' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.backup.fullmaxdays').Value + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'graceperiod' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.backup.newdbgraceperiod').Value + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'skipreadonly' -Value ($__dbcconfig | Where-Object Name -EQ 'skip.backup.readonly').Value + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'skipsecondaries' -Value ($__dbcconfig | Where-Object Name -EQ 'skip.backup.secondaries').Value + } Default { } } @@ -195,7 +203,7 @@ function Get-AllDatabaseInfo { Trustworthy = @(if ($trustworthy) { $psitem.Trustworthy }) Status = @(if ($status) { $psitem.Status }) IsDatabaseSnapshot = @(if ($status) { $psitem.IsDatabaseSnapshot }) # needed for status test - Readonly = @(if ($status) { $psitem.Readonly }) # needed for status test + Readonly = @(if ($status -or $lastFullBackup) { $psitem.Readonly }) # needed for status test & lastfullbackup QueryStore = @(if ($qs) { $psitem.QueryStoreOptions.ActualState }) CompatibilityLevel = @(if ($compatibilitylevel) { $psitem.CompatibilityLevel }) ServerLevel = @(if ($compatibilitylevel) { [Enum]::GetNames('Microsoft.SqlServer.Management.Smo.CompatibilityLevel').Where{ $psitem -match $Instance.VersionMajor } }) @@ -208,6 +216,8 @@ function Get-AllDatabaseInfo { PageVerify = @(if ($pageverify) { $psitem.PageVerify }) ForeignKeys = @(if ($trusted) {$psitem.Tables.ForeignKeys | Where-Object {-not $_.NotForReplication} | Select-Object Name, Parent, @{l='Database';e={$_.Parent.Parent.Name}}, IsChecked } ) Constraints = @(if ($trusted) {$psitem.Tables.Checks | Where-Object {(-not $_.NotForReplication) -and $_.IsEnabled} | Select-Object Name, Parent, @{l='Database';e={$_.Parent.Parent.Name}}, IsChecked } ) + LastFullBackup = @(if ($lastFullBackup) {$psitem.LastBackupDate}) + AGReplicaRole = @(if ($lastFullBackup) { if ($psitem.AvailabilityGroupName) {$psitem.parent.AvailabilityGroups[$psitem.AvailabilityGroupName].LocalReplicaRole} else { 'N\A' }}) # TODO: this needs testing! } } }