diff --git a/step-templates/sql-backup-database.json b/step-templates/sql-backup-database.json index 2948781a..e8f9f010 100755 --- a/step-templates/sql-backup-database.json +++ b/step-templates/sql-backup-database.json @@ -3,9 +3,9 @@ "Name": "SQL - Backup Database", "Description": "Backup a MS SQL Server database to the file system.", "ActionType": "Octopus.Script", - "Version": 12, + "Version": 13, "Properties": { - "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\"\n\nfunction ConnectToDatabase() {\n param($server, $SqlLogin, $SqlPassword, $ConnectionTimeout)\n\n $server.ConnectionContext.StatementTimeout = $ConnectionTimeout\n\n if ($null -ne $SqlLogin) {\n\n if ($null -eq $SqlPassword) {\n throw \"SQL Password must be specified when using SQL authentication.\"\n }\n\n $server.ConnectionContext.LoginSecure = $false\n $server.ConnectionContext.Login = $SqlLogin\n $server.ConnectionContext.Password = $SqlPassword\n\n Write-Host \"Connecting to server using SQL authentication as $SqlLogin.\"\n $server = New-Object Microsoft.SqlServer.Management.Smo.Server $server.ConnectionContext\n }\n else {\n Write-Host \"Connecting to server using Windows authentication.\"\n }\n\n try {\n $server.ConnectionContext.Connect()\n }\n catch {\n Write-Error \"An error occurred connecting to the database server!`r`n$($_.Exception.ToString())\"\n }\n}\n\nfunction AddPercentHandler {\n param($smoBackupRestore, $action)\n\n $percentEventHandler = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { Write-Host $dbName $action $_.Percent \"%\" }\n $completedEventHandler = [Microsoft.SqlServer.Management.Common.ServerMessageEventHandler] { Write-Host $_.Error.Message }\n\n $smoBackupRestore.add_PercentComplete($percentEventHandler)\n $smoBackupRestore.add_Complete($completedEventHandler)\n $smoBackupRestore.PercentCompleteNotification = 10\n}\n\nfunction CreateDevice {\n param($smoBackupRestore, $directory, $name)\n\n $devicePath = [System.IO.Path]::Combine($directory, $name)\n $smoBackupRestore.Devices.AddDevice($devicePath, \"File\")\n return $devicePath\n}\n\nfunction CreateDevices {\n param($smoBackupRestore, $devices, $directory, $dbName, $incremental, $timestamp)\n\n $targetPaths = New-Object System.Collections.Generic.List[System.String]\n\n $extension = \".bak\"\n\n if ($incremental -eq $true) {\n $extension = \".trn\"\n }\n\n if ($devices -eq 1) {\n $deviceName = $dbName + \"_\" + $timestamp + $extension\n $targetPath = CreateDevice $smoBackupRestore $directory $deviceName\n $targetPaths.Add($targetPath)\n }\n else {\n for ($i = 1; $i -le $devices; $i++) {\n $deviceName = $dbName + \"_\" + $timestamp + \"_\" + $i + $extension\n $targetPath = CreateDevice $smoBackupRestore $directory $deviceName\n $targetPaths.Add($targetPath)\n }\n }\n return $targetPaths\n}\n\nfunction BackupDatabase {\n param (\n [Microsoft.SqlServer.Management.Smo.Server]$server,\n [string]$dbName,\n [string]$BackupDirectory,\n [int]$devices,\n [int]$compressionOption,\n [boolean]$incremental,\n [boolean]$copyonly,\n [string]$timestamp,\n [string]$timestampFormat,\n [boolean]$RetentionPolicyEnabled,\n [int]$RetentionPolicyCount\n )\n\n $smoBackup = New-Object Microsoft.SqlServer.Management.Smo.Backup\n $targetPaths = CreateDevices $smoBackup $devices $BackupDirectory $dbName $incremental $timestamp\n\n Write-Host \"Attempting to backup database $server.Name.$dbName to:\"\n $targetPaths | ForEach-Object { Write-Host $_ }\n Write-Host \"\"\n\n if ($incremental -eq $true) {\n $smoBackup.Action = \"Log\"\n $smoBackup.BackupSetDescription = \"Log backup of \" + $dbName\n $smoBackup.LogTruncation = \"Truncate\"\n }\n else {\n $smoBackup.Action = \"Database\"\n $smoBackup.BackupSetDescription = \"Full Backup of \" + $dbName\n }\n\n $smoBackup.BackupSetName = $dbName + \" Backup\"\n $smoBackup.MediaDescription = \"Disk\"\n $smoBackup.CompressionOption = $compressionOption\n $smoBackup.CopyOnly = $copyonly\n $smoBackup.Initialize = $true\n $smoBackup.Database = $dbName\n\n try {\n AddPercentHandler $smoBackup \"backed up\"\n $smoBackup.SqlBackup($server)\n Write-Host \"Backup completed successfully.\"\n\n if ($RetentionPolicyEnabled -eq $true) {\n ApplyRetentionPolicy $BackupDirectory $dbName $RetentionPolicyCount $Incremental $Devices $timestampFormat\n }\n }\n catch {\n Write-Error \"An error occurred backing up the database!`r`n$($_.Exception.ToString())\"\n }\n}\n\nfunction ApplyRetentionPolicy {\n param (\n [string]$BackupDirectory,\n [string]$dbName,\n [int]$RetentionPolicyCount,\n [boolean]$Incremental,\n [int]$Devices,\n [string]$timestampFormat\n )\n\n if ($RetentionPolicyCount -le 0) {\n Write-Host \"RetentionPolicyCount must be greater than 0. Exiting.\"\n return\n }\n\n $extension = if ($Incremental) { '.trn' } else { '.bak' }\n # This pattern helps to isolate the timestamp and possible device part from the filename\n $pattern = '^' + [regex]::Escape($dbName) + '_(\\d{4}-\\d{2}-\\d{2}-\\d{6})(?:_(\\d+))?' + [regex]::Escape($extension) + '$'\n\n $allBackups = Get-ChildItem -Path $BackupDirectory -File | Where-Object { $_.Name -match $pattern }\n\n # Group backups by their base name (assuming base name includes date but not part number)\n $backupGroups = $allBackups | Group-Object { if ($_ -match $pattern) { $Matches[1] } }\n\n # Sort groups by the latest file within each group, assuming the filename includes a sortable date\n $sortedGroups = $backupGroups | Sort-Object { [DateTime]::ParseExact($_.Name, \"yyyy-MM-dd-HHmmss\", $null) } -Descending\n\n # Select the latest groups based on retention policy count\n $groupsToKeep = $sortedGroups | Select-Object -First $RetentionPolicyCount\n\n # Flatten the list of files to keep\n $filesToKeep = $groupsToKeep | ForEach-Object { $_.Group } | ForEach-Object { $_.FullName }\n\n # Identify files to remove\n $filesToRemove = $allBackups | Where-Object { $filesToKeep -notcontains $_.FullName }\n\n foreach ($file in $filesToRemove) {\n Remove-Item $file.FullName -Force\n Write-Host \"Removed old backup file: $($file.Name)\"\n }\n\n Write-Host \"Retention policy applied successfully. Retained the most recent $RetentionPolicyCount backups.\"\n}\n\n\nfunction Invoke-SqlBackupProcess {\n param (\n [hashtable]$OctopusParameters\n )\n\n # Extracting parameters from the hashtable\n $ServerName = $OctopusParameters['Server']\n $DatabaseName = $OctopusParameters['Database']\n $BackupDirectory = $OctopusParameters['BackupDirectory']\n $CompressionOption = [int]$OctopusParameters['Compression']\n $Devices = [int]$OctopusParameters['Devices']\n $Stamp = $OctopusParameters['Stamp']\n $UseSqlServerTimeStamp = $OctopusParameters['UseSqlServerTimeStamp']\n $SqlLogin = $OctopusParameters['SqlLogin']\n $SqlPassword = $OctopusParameters['SqlPassword']\n $ConnectionTimeout = $OctopusParameters['ConnectionTimeout']\n $Incremental = [boolean]::Parse($OctopusParameters['Incremental'])\n $CopyOnly = [boolean]::Parse($OctopusParameters['CopyOnly'])\n $RetentionPolicyEnabled = [boolean]::Parse($OctopusParameters['RetentionPolicyEnabled'])\n $RetentionPolicyCount = [int]$OctopusParameters['RetentionPolicyCount']\n\n [System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SMO\") | Out-Null\n [System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SmoExtended\") | Out-Null\n [System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.ConnectionInfo\") | Out-Null\n [System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SmoEnum\") | Out-Null\n\n $server = New-Object Microsoft.SqlServer.Management.Smo.Server $ServerName\n\n ConnectToDatabase $server $SqlLogin $SqlPassword $ConnectionTimeout\n\n $database = $server.Databases | Where-Object { $_.Name -eq $DatabaseName }\n $timestampFormat = \"yyyy-MM-dd-HHmmss\"\n if ($UseSqlServerTimeStamp -eq $true) {\n $timestampFormat = \"yyyyMMdd_HHmmss\"\n }\n $timestamp = if (-not [string]::IsNullOrEmpty($Stamp)) { $Stamp } else { Get-Date -format $timestampFormat }\n\n if ($null -eq $database) {\n Write-Error \"Database $DatabaseName does not exist on $ServerName\"\n }\n\n if ($Incremental -eq $true) {\n if ($database.RecoveryModel -eq 3) {\n write-error \"$DatabaseName has Recovery Model set to Simple. Log backup cannot be run.\"\n }\n\n if ($database.LastBackupDate -eq \"1/1/0001 12:00 AM\") {\n write-error \"$DatabaseName has no Full backups. Log backup cannot be run.\"\n }\n }\n\n if ($RetentionPolicyEnabled -eq $true -and $RetentionPolicyCount -gt 0) {\n if (-not [int]::TryParse($RetentionPolicyCount, [ref]$null) -or $RetentionPolicyCount -le 0) {\n Write-Error \"RetentionPolicyCount must be an integer greater than zero.\"\n }\n }\n\n BackupDatabase $server $DatabaseName $BackupDirectory $Devices $CompressionOption $Incremental $CopyOnly $timestamp $timestampFormat $RetentionPolicyEnabled $RetentionPolicyCount\n}\n\nif (Test-Path -Path \"Variable:OctopusParameters\") {\n Invoke-SqlBackupProcess -OctopusParameters $OctopusParameters\n}\n", + "Octopus.Action.Script.ScriptBody": "$ErrorActionPreference = \"Stop\"\n$EnableVerboseOutput = $false # pester does not support -Verbose; this is a workaround\n\nfunction ConnectToDatabase() {\n param($server, $SqlLogin, $SqlPassword, $ConnectionTimeout)\n\n $server.ConnectionContext.StatementTimeout = $ConnectionTimeout\n\n if ($null -ne $SqlLogin) {\n\n if ($null -eq $SqlPassword) {\n throw \"SQL Password must be specified when using SQL authentication.\"\n }\n\n $server.ConnectionContext.LoginSecure = $false\n $server.ConnectionContext.Login = $SqlLogin\n $server.ConnectionContext.Password = $SqlPassword\n\n Write-Host \"Connecting to server using SQL authentication as $SqlLogin.\"\n $server = New-Object Microsoft.SqlServer.Management.Smo.Server $server.ConnectionContext\n }\n else {\n Write-Host \"Connecting to server using Windows authentication.\"\n }\n\n try {\n $server.ConnectionContext.Connect()\n }\n catch {\n Write-Error \"An error occurred connecting to the database server!`r`n$($_.Exception.ToString())\"\n }\n}\n\nfunction AddPercentHandler {\n param($smoBackupRestore, $action)\n\n $percentEventHandler = [Microsoft.SqlServer.Management.Smo.PercentCompleteEventHandler] { Write-Host $dbName $action $_.Percent \"%\" }\n $completedEventHandler = [Microsoft.SqlServer.Management.Common.ServerMessageEventHandler] { Write-Host $_.Error.Message }\n\n $smoBackupRestore.add_PercentComplete($percentEventHandler)\n $smoBackupRestore.add_Complete($completedEventHandler)\n $smoBackupRestore.PercentCompleteNotification = 10\n}\n\nfunction CreateDevice {\n param($smoBackupRestore, $directory, $name)\n\n $devicePath = [System.IO.Path]::Combine($directory, $name)\n $smoBackupRestore.Devices.AddDevice($devicePath, \"File\")\n return $devicePath\n}\n\nfunction CreateDevices {\n param($smoBackupRestore, $devices, $directory, $dbName, $incremental, $timestamp)\n\n $targetPaths = New-Object System.Collections.Generic.List[System.String]\n\n $extension = \".bak\"\n\n if ($incremental -eq $true) {\n $extension = \".trn\"\n }\n\n if ($devices -eq 1) {\n $deviceName = $dbName + \"_\" + $timestamp + $extension\n $targetPath = CreateDevice $smoBackupRestore $directory $deviceName\n $targetPaths.Add($targetPath)\n }\n else {\n for ($i = 1; $i -le $devices; $i++) {\n $deviceName = $dbName + \"_\" + $timestamp + \"_\" + $i + $extension\n $targetPath = CreateDevice $smoBackupRestore $directory $deviceName\n $targetPaths.Add($targetPath)\n }\n }\n return $targetPaths\n}\n\nfunction BackupDatabase {\n param (\n [Microsoft.SqlServer.Management.Smo.Server]$server,\n [string]$dbName,\n [string]$BackupDirectory,\n [int]$devices,\n [int]$compressionOption,\n [boolean]$incremental,\n [boolean]$copyonly,\n [string]$timestamp,\n [string]$timestampFormat,\n [boolean]$RetentionPolicyEnabled,\n [int]$RetentionPolicyCount\n )\n\n $smoBackup = New-Object Microsoft.SqlServer.Management.Smo.Backup\n $targetPaths = CreateDevices $smoBackup $devices $BackupDirectory $dbName $incremental $timestamp\n\n Write-Host \"Attempting to backup database $server.Name.$dbName to:\"\n $targetPaths | ForEach-Object { Write-Host $_ }\n Write-Host \"\"\n\n if ($incremental -eq $true) {\n $smoBackup.Action = \"Log\"\n $smoBackup.BackupSetDescription = \"Log backup of \" + $dbName\n $smoBackup.LogTruncation = \"Truncate\"\n }\n else {\n $smoBackup.Action = \"Database\"\n $smoBackup.BackupSetDescription = \"Full Backup of \" + $dbName\n }\n\n $smoBackup.BackupSetName = $dbName + \" Backup\"\n $smoBackup.MediaDescription = \"Disk\"\n $smoBackup.CompressionOption = $compressionOption\n $smoBackup.CopyOnly = $copyonly\n $smoBackup.Initialize = $true\n $smoBackup.Database = $dbName\n\n try {\n AddPercentHandler $smoBackup \"backed up\"\n $smoBackup.SqlBackup($server)\n Write-Host \"Backup completed successfully.\"\n\n if ($RetentionPolicyEnabled -eq $true) {\n ApplyRetentionPolicy $BackupDirectory $dbName $RetentionPolicyCount $Incremental $Devices $timestampFormat\n }\n }\n catch {\n Write-Error \"An error occurred backing up the database!`r`n$($_.Exception.ToString())\"\n }\n}\n\nfunction ApplyRetentionPolicy {\n param (\n [string]$BackupDirectory,\n [string]$dbName,\n [int]$RetentionPolicyCount,\n [bool]$Incremental = $false,\n [int]$Devices = 1,\n [string]$timestampFormat = \"yyyy-MM-dd-HHmmss\"\n )\n\n # Check if RetentionPolicyCount is defined\n if (-not $PSBoundParameters.ContainsKey('RetentionPolicyCount')) {\n Write-Host \"Retention policy not applied as RetentionPolicyCount is undefined.\"\n return\n }\n\n # Set the appropriate file extension\n $extension = if ($Incremental) { '.trn' } else { '.bak' }\n\n # Prepare the regex pattern for matching the files\n $dateRegex = $timestampFormat -replace \"yyyy\", \"\\d{4}\" -replace \"MM\", \"\\d{2}\" -replace \"dd\", \"\\d{2}\" -replace \"HH\", \"\\d{2}\" -replace \"mm\", \"\\d{2}\" -replace \"ss\", \"\\d{2}\"\n $devicePattern = if ($Devices -gt 1) { \"(_\\d+)\" } else { \"\" }\n $regexPattern = \"^${dbName}_${dateRegex}${devicePattern}${extension}$\"\n\n # Get all matching files in the directory\n $allBackups = Get-ChildItem -Path $BackupDirectory -Filter \"*$extension\" | Where-Object { $_.Name -match $regexPattern }\n\n # If there are no matching backups, exit\n if (-not $allBackups) {\n Write-Host \"No matching backups found.\"\n return\n }\n\n # If RetentionPolicyCount is zero, don't delete or keep any backups\n if ($RetentionPolicyCount -le 0) {\n if($EnableVerboseOutput) { # pester does not support -Verbose; this is a workaround\n Write-Host \"Retention policy not applied as RetentionPolicyCount is set to 0.\"\n }\n } elseif ($Devices -gt 1) {\n # Group by the timestamp part (ignore the device number)\n $groupedBackups = $allBackups | Group-Object {\n # Extract the timestamp, ignoring the device part if there are multiple devices\n ($_.Name -replace \"${devicePattern}${extension}$\", \"\") -replace \"^${dbName}_\", \"\"\n }\n\n # Sort the groups by the timestamp\n $sortedGroups = $groupedBackups | Sort-Object Name\n\n # Get the groups to keep\n $groupsToKeep = $sortedGroups | Select-Object -Last $RetentionPolicyCount\n $filesToKeep = $groupsToKeep | ForEach-Object { $_.Group }\n\n # Flatten the collection of files to keep, ensuring that FullName is accessed correctly\n $filesToKeepFlattened = $filesToKeep | ForEach-Object { $_ | Select-Object -ExpandProperty FullName }\n $filesToDelete = $allBackups | Where-Object { $filesToKeepFlattened -notcontains $_.FullName }\n\n # Delete the old backups\n $filesToDelete | ForEach-Object {\n if($EnableVerboseOutput) { # pester does not support -Verbose; this is a workaround\n Write-Host \"Deleting old backup: $($_.FullName)\"\n }\n Remove-Item -Path $_.FullName -Force\n }\n\n # List the files to keep\n $filesToKeepFlattened | ForEach-Object {\n Write-Verbose \"Keeping backup: $($_)\"\n }\n\n Write-Host \"Retention policy applied. Kept $RetentionPolicyCount most recent backups.\"\n } else {\n # Single device: simply sort the backups by timestamp\n $sortedBackups = $allBackups | Sort-Object Name\n\n # Get the backups to keep\n $backupsToKeep = $sortedBackups | Select-Object -Last $RetentionPolicyCount\n $filesToDelete = $allBackups | Where-Object { $backupsToKeep -notcontains $_ }\n\n # Delete the old backups\n $filesToDelete | ForEach-Object {\n if($EnableVerboseOutput) { # pester does not support -Verbose; this is a workaround\n Write-Host \"Deleting old backup: $($_.FullName)\"\n }\n Remove-Item -Path $_.FullName -Force\n }\n\n # List the files to keep\n $backupsToKeep | ForEach-Object {\n if($EnableVerboseOutput) { # pester does not support -Verbose; this is a workaround\n Write-Host \"Keeping backup: $($_.FullName)\"\n }\n }\n\n if($EnableVerboseOutput) { # pester does not support -Verbose; this is a workaround\n Write-Host \"Retention policy applied. Kept $RetentionPolicyCount most recent backups.\"\n }\n }\n}\n\nfunction Invoke-SqlBackupProcess {\n param (\n [hashtable]$OctopusParameters\n )\n\n # Extracting parameters from the hashtable\n $ServerName = $OctopusParameters['Server']\n $DatabaseName = $OctopusParameters['Database']\n $BackupDirectory = $OctopusParameters['BackupDirectory']\n $CompressionOption = [int]$OctopusParameters['Compression']\n $Devices = [int]$OctopusParameters['Devices']\n $Stamp = $OctopusParameters['Stamp']\n $UseSqlServerTimeStamp = $OctopusParameters['UseSqlServerTimeStamp']\n $SqlLogin = $OctopusParameters['SqlLogin']\n $SqlPassword = $OctopusParameters['SqlPassword']\n $ConnectionTimeout = $OctopusParameters['ConnectionTimeout']\n $Incremental = [boolean]::Parse($OctopusParameters['Incremental'])\n $CopyOnly = [boolean]::Parse($OctopusParameters['CopyOnly'])\n $RetentionPolicyEnabled = [boolean]::Parse($OctopusParameters['RetentionPolicyEnabled'])\n $RetentionPolicyCount = [int]$OctopusParameters['RetentionPolicyCount']\n\n [System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SMO\") | Out-Null\n [System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SmoExtended\") | Out-Null\n [System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.ConnectionInfo\") | Out-Null\n [System.Reflection.Assembly]::LoadWithPartialName(\"Microsoft.SqlServer.SmoEnum\") | Out-Null\n\n $server = New-Object Microsoft.SqlServer.Management.Smo.Server $ServerName\n\n ConnectToDatabase $server $SqlLogin $SqlPassword $ConnectionTimeout\n\n $database = $server.Databases | Where-Object { $_.Name -eq $DatabaseName }\n $timestampFormat = \"yyyy-MM-dd-HHmmss\"\n if ($UseSqlServerTimeStamp -eq $true) {\n $timestampFormat = \"yyyyMMdd_HHmmss\"\n }\n $timestamp = if (-not [string]::IsNullOrEmpty($Stamp)) { $Stamp } else { Get-Date -format $timestampFormat }\n\n if ($null -eq $database) {\n Write-Error \"Database $DatabaseName does not exist on $ServerName\"\n }\n\n if ($Incremental -eq $true) {\n if ($database.RecoveryModel -eq 3) {\n write-error \"$DatabaseName has Recovery Model set to Simple. Log backup cannot be run.\"\n }\n\n if ($database.LastBackupDate -eq \"1/1/0001 12:00 AM\") {\n write-error \"$DatabaseName has no Full backups. Log backup cannot be run.\"\n }\n }\n\n if ($RetentionPolicyEnabled -eq $true -and $RetentionPolicyCount -gt 0) {\n if (-not [int]::TryParse($RetentionPolicyCount, [ref]$null) -or $RetentionPolicyCount -le 0) {\n Write-Error \"RetentionPolicyCount must be an integer greater than zero.\"\n }\n }\n\n BackupDatabase $server $DatabaseName $BackupDirectory $Devices $CompressionOption $Incremental $CopyOnly $timestamp $timestampFormat $RetentionPolicyEnabled $RetentionPolicyCount\n}\n\nif (Test-Path -Path \"Variable:OctopusParameters\") {\n Invoke-SqlBackupProcess -OctopusParameters $OctopusParameters\n}\n", "Octopus.Action.Script.Syntax": "PowerShell" }, "SensitiveProperties": {}, @@ -139,10 +139,10 @@ } } ], - "LastModifiedOn": "2024-03-26T09:30:00.0000000-07:00", + "LastModifiedOn": "2024-09-11T09:30:00.0000000-07:00", "LastModifiedBy": "bcullman", "$Meta": { - "ExportedAt": "2024-03-26T09:30:00.0000000-07:00", + "ExportedAt": "2024-09-11T09:30:00.0000000-07:00", "OctopusVersion": "2022.3.10640", "Type": "ActionTemplate" }, diff --git a/step-templates/tests/sql-backup-database.ScriptBody.Tests.ps1 b/step-templates/tests/sql-backup-database.ScriptBody.Tests.ps1 index bbcd4c0c..a1397070 100644 --- a/step-templates/tests/sql-backup-database.ScriptBody.Tests.ps1 +++ b/step-templates/tests/sql-backup-database.ScriptBody.Tests.ps1 @@ -51,6 +51,11 @@ function SetupTestEnvironment { $fileName = "$DatabaseName" + "_$dateSuffix" + $deviceSuffix + $fileExtension $filePath = Join-Path -Path $BackupDirectory -ChildPath $fileName New-Item -Path $filePath -ItemType "file" -Force | Out-Null + + # Validate that the file was created + if (-not (Test-Path -Path $filePath)) { + throw "Failed to create backup file: $filePath" + } } } } @@ -64,6 +69,10 @@ function SetupTestEnvironment { foreach ($filename in $challengingFilenames) { $filePath = Join-Path -Path $BackupDirectory -ChildPath $filename New-Item -Path $filePath -ItemType "file" -Force | Out-Null + # Validate that the challenging file was created + if (-not (Test-Path -Path $filePath)) { + throw "Failed to create challenging file: $filePath" + } } } @@ -75,43 +84,47 @@ Describe "ApplyRetentionPolicy Tests" { $script:StartDate = Get-Date $script:timestampFormat = "yyyy-MM-dd-HHmmss" $script:challengingFilenames = @( - # similar DB name noted during PR review - "ExampleDB_final_2024-03-18-1030.bak", - # Similar DB name, valid timestamp. Might be confused with a backup for a different but similarly named database. - "ExampleDB1_2024-03-18-1030.bak", - # Same DB, different valid timestamp. Tests accuracy of timestamp matching. - "ExampleDB_2024-03-19-1030.bak", - # Similar timestamp format, but different. Might test pattern matching robustness. - "ExampleDB_20240318_1030.bak", - # Different DB, valid timestamp. Should not be matched if script correctly identifies DB name. - "TestDB_2024-03-18-1030.bak", - # Non-backup file type with valid naming. Should be ignored by the cleanup script. - "ExampleDB_2024-03-18-1030.log", - # Completely unrelated file. Should always be ignored by the cleanup script. - "RandomFile.txt", - # Similar DB name with underscore. Might be confused with main database name if script uses loose matching. - "Example_DB_2024-03-18-1030.bak", - # Same DB name, lowercase. Tests case sensitivity of the script. - "exampledb_2024-03-18-1030.bak", - # Similar timestamp, underscore separator. Variation in timestamp format might challenge pattern matching. - "ExampleDB_2024-03-18_1030.bak", - # Different DB, valid timestamp for trn. Tests database name matching accuracy with incremental backups. - "AnotherDB_2024-03-18-1030.trn" - ) + # similar DB name noted during PR review + "ExampleDB_final_2024-03-18-1030.bak", + # Similar DB name, valid timestamp. Might be confused with a backup for a different but similarly named database. + "ExampleDB1_2024-03-18-1030.bak", + # Same DB, different valid timestamp. Tests accuracy of timestamp matching. + "ExampleDB_2024-03-19-1030.bak", + # Similar timestamp format, but different. Might test pattern matching robustness. + "ExampleDB_20240318_1030.bak", + # Different DB, valid timestamp. Should not be matched if script correctly identifies DB name. + "TestDB_2024-03-18-1030.bak", + # Non-backup file type with valid naming. Should be ignored by the cleanup script. + "ExampleDB_2024-03-18-1030.log", + # Completely unrelated file. Should always be ignored by the cleanup script. + "RandomFile.txt", + # Similar DB name with underscore. Might be confused with main database name if script uses loose matching. + "Example_DB_2024-03-18-1030.bak", + # Same DB name, lowercase. Tests case sensitivity of the script. + "exampledb_2024-03-18-1030.bak", + # Similar timestamp, underscore separator. Variation in timestamp format might challenge pattern matching. + "ExampleDB_2024-03-18_1030.bak", + # Different DB, valid timestamp for trn. Tests database name matching accuracy with incremental backups. + "AnotherDB_2024-03-18-1030.trn" + ) } - Context "ApplyRetentionPolicy functionality for single device backups" { - BeforeAll { - $Devices = 1 - $IncrementalFiles = 10 - $FullBackupFiles = 10 - SetupTestEnvironment -BackupDirectory $BackupDirectory -DatabaseName $DatabaseName -IncrementalFiles $IncrementalFiles -FullBackupFiles $FullBackupFiles -StartDate $StartDate -Devices $Devices -timestampFormat $timestampFormat -challengingFilenames $challengingFilenames - } + BeforeEach { + $Devices = 1 + $IncrementalFiles = 10 + $FullBackupFiles = 10 + SetupTestEnvironment -BackupDirectory $BackupDirectory -DatabaseName $DatabaseName -IncrementalFiles $IncrementalFiles -FullBackupFiles $FullBackupFiles -StartDate $StartDate -Devices $Devices -timestampFormat $timestampFormat -challengingFilenames $challengingFilenames + } + + AfterEach { + Remove-Item -Path $BackupDirectory -Recurse -Force + } - It "Retains the specified number of the most recent backups" { + Context "ApplyRetentionPolicy functionality for single device backups" { + It "Retains the specified number of the most recent backups" { $RetentionPolicyCount = 3 - ApplyRetentionPolicy -BackupDirectory $BackupDirectory -dbName $DatabaseName -RetentionPolicyCount $RetentionPolicyCount -Incremental $false -Devices $Devices -timestampFormat $timestampFormat + ApplyRetentionPolicy -BackupDirectory $BackupDirectory -dbName $DatabaseName -RetentionPolicyCount $RetentionPolicyCount -Incremental $false -Devices $Devices -timestampFormat $timestampFormat -Verbose:$VerbosePreference $extension = '.bak' $devicePattern = if ($Devices -gt 1) { "(_\d+)" } else { "" } @@ -141,74 +154,72 @@ Describe "ApplyRetentionPolicy Tests" { It "Retains only the most recent backup when the RetentionPolicyCount is 1" { $RetentionPolicyCount = 1 - - ApplyRetentionPolicy -BackupDirectory $BackupDirectory -dbName $DatabaseName -RetentionPolicyCount $RetentionPolicyCount -Incremental $false -Devices $Devices -timestampFormat $timestampFormat + $InitialFileCount = (Get-ChildItem -Path $BackupDirectory).Count $extension = '.bak' $devicePattern = if ($Devices -gt 1) { "(_\d+)" } else { "" } $dateRegex = $timestampFormat -replace "yyyy", "\d{4}" -replace "MM", "\d{2}" -replace "dd", "\d{2}" -replace "HH", "\d{2}" -replace "mm", "\d{2}" -replace "ss", "\d{2}" $regexPattern = "^${DatabaseName}_${dateRegex}${devicePattern}${extension}$" + $affectedFiles = @(Get-ChildItem -Path $BackupDirectory -Filter "*$extension" | Where-Object { $_.Name -match $regexPattern }) + + ApplyRetentionPolicy -BackupDirectory $BackupDirectory -dbName $DatabaseName -RetentionPolicyCount $RetentionPolicyCount -Incremental $false -Devices $Devices -timestampFormat $timestampFormat + $retainedFiles = @(Get-ChildItem -Path $BackupDirectory -Filter "*$extension" | Where-Object { $_.Name -match $regexPattern }) $retainedFiles.Count | Should Be $RetentionPolicyCount + $deletedFiles = $affectedFiles.Count - $RetentionPolicyCount + $deletedFiles | Should Be ($affectedFiles.Count - $retainedFiles.Count) } It "Does not delete files that do not match the backup file naming pattern" { - # Define the extension based on whether we're dealing with incremental backups or full backups $extension = '.bak' - # Define the regex pattern to match the backup files $regexPattern = "^${DatabaseName}_\d{4}-\d{2}-\d{2}-\d{6}${extension}$" - - # Count files that do not match the backup file naming convention before applying the retention policy $initialUnrelatedFileCount = @(Get-ChildItem -Path $BackupDirectory -File | Where-Object { -not ($_.Name -match $regexPattern) }).Count - # Apply the retention policy ApplyRetentionPolicy -BackupDirectory $BackupDirectory -dbName $DatabaseName -RetentionPolicyCount 3 -Incremental $false -Devices $Devices -timestampFormat $timestampFormat - # Count files that do not match the backup file naming convention after applying the retention policy $finalUnrelatedFileCount = @(Get-ChildItem -Path $BackupDirectory -File | Where-Object { -not ($_.Name -match $regexPattern) }).Count - - # The count of unrelated files should remain the same before and after applying the retention policy $finalUnrelatedFileCount | Should Be $initialUnrelatedFileCount } It "Retains the specified number of the most recent incremental backups" { $RetentionPolicyCount = 5 - - ApplyRetentionPolicy -BackupDirectory $BackupDirectory -dbName $DatabaseName -RetentionPolicyCount $RetentionPolicyCount -Incremental $true -Devices $Devices -timestampFormat $timestampFormat - $extension = '.trn' $devicePattern = if ($Devices -gt 1) { "(_\d+)" } else { "" } $dateRegex = $timestampFormat -replace "yyyy", "\d{4}" -replace "MM", "\d{2}" -replace "dd", "\d{2}" -replace "HH", "\d{2}" -replace "mm", "\d{2}" -replace "ss", "\d{2}" $regexPattern = "^${DatabaseName}_${dateRegex}${devicePattern}${extension}$" + $affectedFiles = @(Get-ChildItem -Path $BackupDirectory -Filter "*$extension" | Where-Object { $_.Name -match $regexPattern }) + + ApplyRetentionPolicy -BackupDirectory $BackupDirectory -dbName $DatabaseName -RetentionPolicyCount $RetentionPolicyCount -Incremental $true -Devices $Devices -timestampFormat $timestampFormat $retainedFiles = @(Get-ChildItem -Path $BackupDirectory -Filter "*$extension" | Where-Object { $_.Name -match $regexPattern }) $retainedFiles.Count | Should Be $RetentionPolicyCount - } - - AfterAll { - Remove-Item -Path $BackupDirectory -Recurse -Force + $deletedFiles = $affectedFiles.Count - $RetentionPolicyCount + $deletedFiles | Should Be ($affectedFiles.Count - $retainedFiles.Count) } } Context "ApplyRetentionPolicy functionality for multi-device backups" { - BeforeAll { + BeforeEach { $Devices = 4 SetupTestEnvironment -BackupDirectory $BackupDirectory -DatabaseName $DatabaseName -IncrementalFiles $IncrementalFiles -FullBackupFiles $FullBackupFiles -StartDate $StartDate -Devices $Devices -timestampFormat $timestampFormat -challengingFilenames $challengingFilenames } It "Retains the specified number of the most recent backups" { $RetentionPolicyCount = 3 - - ApplyRetentionPolicy -BackupDirectory $BackupDirectory -dbName $DatabaseName -RetentionPolicyCount $RetentionPolicyCount -Incremental $false -Devices $Devices -timestampFormat $timestampFormat - $extension = '.bak' $timestampFormat = "yyyy-MM-dd-HHmmss" $devicePattern = if ($Devices -gt 1) { "(_\d+)" } else { "" } $dateRegex = $timestampFormat -replace "yyyy", "\d{4}" -replace "MM", "\d{2}" -replace "dd", "\d{2}" -replace "HH", "\d{2}" -replace "mm", "\d{2}" -replace "ss", "\d{2}" $regexPattern = "^${DatabaseName}_${dateRegex}${devicePattern}${extension}$" + $affectedFiles = @(Get-ChildItem -Path $BackupDirectory -Filter "*$extension" | Where-Object { $_.Name -match $regexPattern }) + + ApplyRetentionPolicy -BackupDirectory $BackupDirectory -dbName $DatabaseName -RetentionPolicyCount $RetentionPolicyCount -Incremental $false -Devices $Devices -timestampFormat $timestampFormat + $retainedFiles = Get-ChildItem -Path $BackupDirectory -Filter "*$extension" | Where-Object { $_.Name -match $regexPattern } $totalExpectedRetainedFiles = $RetentionPolicyCount * $Devices $retainedFiles.Count | Should Be $totalExpectedRetainedFiles + $deletedFiles = $affectedFiles.Count - $RetentionPolicyCount * $Devices + $deletedFiles | Should Be ($affectedFiles.Count - $retainedFiles.Count) } It "Does not delete files that do not match the backup file naming pattern for multiple devices" { @@ -224,21 +235,17 @@ Describe "ApplyRetentionPolicy Tests" { It "Correctly retains the specified number of the most recent incremental backups for multiple devices" { $RetentionPolicyCount = 5 $Incremental = $true - - ApplyRetentionPolicy -BackupDirectory $BackupDirectory -dbName $DatabaseName -RetentionPolicyCount $RetentionPolicyCount -Incremental $Incremental -Devices $Devices -timestampFormat $timestampFormat - $extension = '.trn' $timestampFormat = "yyyy-MM-dd-HHmmss" $devicePattern = if ($Devices -gt 1) { "(_\d+)" } else { "" } $dateRegex = $timestampFormat -replace "yyyy", "\d{4}" -replace "MM", "\d{2}" -replace "dd", "\d{2}" -replace "HH", "\d{2}" -replace "mm", "\d{2}" -replace "ss", "\d{2}" $regexPattern = "^${DatabaseName}_${dateRegex}${devicePattern}${extension}$" + + ApplyRetentionPolicy -BackupDirectory $BackupDirectory -dbName $DatabaseName -RetentionPolicyCount $RetentionPolicyCount -Incremental $Incremental -Devices $Devices -timestampFormat $timestampFormat + $retainedFiles = Get-ChildItem -Path $BackupDirectory -Filter "*$extension" | Where-Object { $_.Name -match $regexPattern } $totalExpectedRetainedFiles = $RetentionPolicyCount * $Devices $retainedFiles.Count | Should Be $totalExpectedRetainedFiles } - - AfterAll { - Remove-Item -Path $BackupDirectory -Recurse -Force - } } }