param( [Parameter(Mandatory=$true)] [string]$SqlInstance, [Parameter(Mandatory=$false)] [string]$Directory, [Parameter(Mandatory=$false)] [switch]$Force ) # backup.ps1 # # TODO: Parallelize backups for multiple DBs in the instance # Import SQL Server PowerShell module try { # Try to import the newer SqlServer module first if (Get-Module -ListAvailable -Name SqlServer) { Import-Module SqlServer -ErrorAction Stop Write-Host "INFO: SqlServer PowerShell module loaded successfully." } # Fall back to older SQLPS module if available elseif (Get-Module -ListAvailable -Name SQLPS) { Import-Module SQLPS -ErrorAction Stop Write-Host "INFO: SQLPS PowerShell module loaded successfully." } else { throw "No SQL Server PowerShell module found" } # Verify Invoke-Sqlcmd is available if (-not (Get-Command Invoke-Sqlcmd -ErrorAction SilentlyContinue)) { throw "Invoke-Sqlcmd command not available" } } catch { Write-Host "ERROR: Failed to import SQL Server PowerShell module. Please install it using: Install-Module -Name SqlServer -AllowClobber" Write-Host "ERROR: $($_.Exception.Message)" exit 1 } $instanceName = $SqlInstance.Split('\')[1] # Use provided directory parameter or default to instance-based path if ($Directory) { $directory = $Directory Write-Host "INFO: Using provided directory: $directory" } else { $directory = "C:\Rubrik\$instanceName" Write-Host "INFO: Using default directory: $directory" } $fullBackupDay = 'Thursday' $fullBackupOverdueDays = 7 # Force full backup if last full backup is older than this many days $checkCluster = $false #$logFile = "C:\Rubrik\backup-$instanceName.log" $logFile = "H:\Backup\backup-$instanceName.log" $fullFlag = $directory + "\last_full.flag" $diffFlag = $directory + "\last_diff.flag" $today = (Get-Date).Date function FlagTakenToday($flagPath) { if (Test-Path $flagPath) { $flagDate = (Get-Content $flagPath | Out-String).Trim() return ($flagDate -eq $today.ToString("yyyy-MM-dd")) } return $false } function GetLastFullBackupDate($flagPath) { if (Test-Path $flagPath) { $flagDate = (Get-Content $flagPath | Out-String).Trim() try { return [DateTime]::ParseExact($flagDate, "yyyy-MM-dd", $null) } catch { Write-Log "WARNING: Could not parse last full backup date from flag file: $flagDate" return $null } } return $null } function IsFullBackupOverdue($flagPath, $overdueDays) { $lastFullDate = GetLastFullBackupDate $flagPath if ($null -eq $lastFullDate) { Write-Log "WARNING: No last full backup date found. Full backup is considered overdue." return $true } $daysSinceLastFull = ($today - $lastFullDate).Days $isOverdue = $daysSinceLastFull -gt $overdueDays Write-Log "INFO: Last full backup was $daysSinceLastFull days ago on $($lastFullDate.ToString('yyyy-MM-dd')). Overdue threshold: $overdueDays days." return $isOverdue } function Write-Log($message) { $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logEntry = "$timestamp $message" Add-Content -Path $logFile -Value $logEntry Write-Host $logEntry } # Check if directory exists and is a symbolic link (unless -Force is specified) if (-not (Test-Path $directory)) { Write-Log "ERROR: Directory '$directory' does not exist. Exiting script." exit 1 } if (-not $Force) { $directoryInfo = Get-Item $directory if (-not ($directoryInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) { Write-Log "ERROR: Directory '$directory' is not a symbolic link. Exiting script." exit 1 } Write-Log "INFO: Directory '$directory' exists and is a symbolic link. Target: $($directoryInfo.Target). Proceeding." } else { Write-Log "INFO: Force parameter specified. Skipping symbolic link check for directory '$directory'." } if ($checkCluster) { # Check if SQL instance is running locally $localNode = $env:COMPUTERNAME $clusterInstance = Get-ClusterResource | Where-Object { $_.ResourceType -eq "SQL Server" -and $_.Name -eq "SQL Server ($instanceName)" } if ($clusterInstance) { $ownerNode = $clusterInstance.OwnerNode.Name if ($ownerNode -ne $localNode) { Write-Log "SQL instance '$SqlInstance' is not running on local node '$localNode'. Exiting script." exit 1 } else { Write-Log "SQL instance '$SqlInstance' is running on local node '$localNode'. Proceeding." } } else { Write-Log "ERROR: SQL instance '$SqlInstance' not found in cluster resources." exit 1 } } else { Write-Log "INFO: Cluster check is disabled. Proceeding without verification." } # Check if full backup is overdue regardless of the day $isFullBackupOverdue = IsFullBackupOverdue $fullFlag $fullBackupOverdueDays if ((Get-Date).DayOfWeek -eq $fullBackupDay) { if (-not (FlagTakenToday $fullFlag)) { $backupType = "FULL" $cleanupTime = 168 Set-Content $fullFlag $today.ToString("yyyy-MM-dd") Write-Log "Selected FULL backup (scheduled day). Flag updated." } else { $backupType = "LOG" $cleanupTime = 24 Write-Log "FULL backup already taken today. Selected LOG backup." } } elseif ($isFullBackupOverdue) { if (-not (FlagTakenToday $fullFlag)) { $backupType = "FULL" $cleanupTime = 168 Set-Content $fullFlag $today.ToString("yyyy-MM-dd") Write-Log "Selected FULL backup (overdue - forcing full backup). Flag updated." } else { $backupType = "LOG" $cleanupTime = 24 Write-Log "FULL backup already taken today (was overdue). Selected LOG backup." } } else { if (-not (FlagTakenToday $diffFlag)) { $backupType = "DIFF" $cleanupTime = 168 Set-Content $diffFlag $today.ToString("yyyy-MM-dd") Write-Log "Selected DIFF backup. Flag updated." } else { $backupType = "LOG" $cleanupTime = 24 Write-Log "DIFF backup already taken today. Selected LOG backup." } } $query = "EXECUTE [dbo].[DatabaseBackup] @Databases = 'ALL_DATABASES', @Directory = '$directory', @BackupType = '$backupType', @Verify = 'N', @CleanupTime = $cleanupTime, @CheckSum = 'Y', @LogToTable = 'Y'" Write-Log "Executing backup type: $backupType" Write-Log "SQL Query: $query" try { # Execute the backup using PowerShell SQL module with better error handling # Capture verbose output from Ola H scripts $infoMessages = @() # Create event handlers to capture SQL Server messages $connection = New-Object System.Data.SqlClient.SqlConnection $connection.ConnectionString = "Server=$SqlInstance;Integrated Security=true;Connection Timeout=30" # Event handler for informational messages (PRINT statements) $connection.add_InfoMessage({ param($sqlSender, $e) $message = $e.Message if ($message -and $message.Trim() -ne "") { $script:infoMessages += $message Write-Log "SQL INFO: $message" } }) try { $connection.Open() $command = New-Object System.Data.SqlClient.SqlCommand $command.Connection = $connection $command.CommandText = $query $command.CommandTimeout = 0 # No timeout for backup operations Write-Log "Executing SQL command with message capture..." # Execute and capture any result sets $reader = $command.ExecuteReader() # Process any result sets while ($reader.Read()) { $rowData = @() for ($i = 0; $i -lt $reader.FieldCount; $i++) { $rowData += "$($reader.GetName($i)): $($reader.GetValue($i))" } if ($rowData.Count -gt 0) { Write-Log "SQL RESULT: $($rowData -join ', ')" } } $reader.Close() } finally { if ($connection.State -eq [System.Data.ConnectionState]::Open) { $connection.Close() } $connection.Dispose() } Write-Log "$backupType Backup execution completed successfully." Write-Log "Total informational messages captured: $($infoMessages.Count)" } catch { Write-Log "ERROR: Backup execution failed with exception: $($_.Exception.Message)" # Log additional SQL Server error details if available if ($_.Exception.InnerException) { Write-Log "ERROR: Inner Exception: $($_.Exception.InnerException.Message)" } # Check for SQL Server specific errors if ($_.Exception -is [System.Data.SqlClient.SqlException]) { Write-Log "ERROR: SQL Server Error Details:" foreach ($sqlError in $_.Exception.Errors) { Write-Log "ERROR: Severity: $($sqlError.Class), State: $($sqlError.State), Number: $($sqlError.Number)" Write-Log "ERROR: Message: $($sqlError.Message)" if ($sqlError.Procedure) { Write-Log "ERROR: Procedure: $($sqlError.Procedure), Line: $($sqlError.LineNumber)" } } } # Clean up connection if it exists if ($connection -and $connection.State -eq [System.Data.ConnectionState]::Open) { $connection.Close() $connection.Dispose() } exit 1 }