Compare commits

..

4 Commits

Author SHA1 Message Date
jamesp de47f41fc3 Increase CleanupTime to 48 2026-06-24 14:52:29 +01:00
jamesp bbbc824916 Improve LSN gap check 2026-06-24 14:49:18 +01:00
jamesp ab12c29244 Merge branch 'main' of https://git.pattinson.org/jamesp/zf-sql 2026-06-24 12:28:59 +01:00
jamesp ae9f794377 Latest update 2026-06-24 12:28:01 +01:00
7 changed files with 266 additions and 532 deletions
+215 -1
View File
@@ -233,5 +233,219 @@ Default: `C:\Rubrik\backup-multi-{InstanceName}.log`
## License ## License
This script is provided as-is for database backup automation. Ensure compliance with your organization's backup and retention policies.</content> This script is provided as-is for database backup automation. Ensure compliance with your organization's backup and retention policies.
---
# RestoreScript.ps1 - SQL Server Database Restore Script
A PowerShell script for restoring SQL Server databases from backup files, with support for full, differential, and log backups. It automatically catalogs backups from a specified directory and handles file relocation during restore operations.
## Features
- **Automatic Backup Cataloging**: Scans a directory for backup files and organizes them by database, type (FULL, DIFF, LOG), and timestamp
- **Flexible Restore Options**: Supports restoring from full backups only, or full + diffs + logs in the correct sequence
- **File Relocation**: Moves database and log files to specified paths during restore (e.g., for different drive layouts)
- **Backup Verification**: Validates backup integrity and provides LSN range information
- **LSN-Aware Sequencing**: Applies backups in the correct order to avoid LSN conflicts (full → all diffs → logs after latest diff)
- **Error Handling**: Robust error detection with detailed SQL error messages
## Requirements
### System Requirements
- **PowerShell**: Version 5.1 or higher
- **Operating System**: Windows Server 2016 or later (or Windows 10/11 for development/testing)
- **SQL Server**: SQL Server 2016 or later (Express, Standard, Enterprise, or Developer editions)
- **Permissions**: SQL Server sysadmin privileges or appropriate database restore permissions
### Software Dependencies
- **SQL Server PowerShell Module**: Either `SqlServer` or `SQLPS` module must be available
- Install with: `Install-Module -Name SqlServer -AllowClobber`
### Network Requirements
- **SQL Server Connectivity**: The script must be able to connect to the target SQL Server instance
- **Backup Source Access**: Read access to the backup files directory
- **Restore Destination Access**: Read/write access to the data and log file directories
## Installation
1. **Download the Script**:
```powershell
# Place RestoreScript.ps1 in your preferred scripts directory
# Example: C:\Rubrik\Scripts\
```
2. **Install SQL Server PowerShell Module** (if not already installed):
```powershell
Install-Module -Name SqlServer -AllowClobber -Force
```
3. **Verify Permissions**:
- Ensure the account running the script has SQL Server sysadmin privileges
- Verify read access to backup directories and write access to restore destinations
## Usage
### Basic Syntax
```powershell
.\RestoreScript.ps1 -SqlInstance "ServerName\InstanceName" -LiveMountRoot "C:\BackupPath" -Action <action> [parameters]
```
### Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `SqlInstance` | String | Yes | sqlfcsql\TESTINST | SQL Server instance name (e.g., "SERVER01\SQL2019") |
| `LiveMountRoot` | String | Yes | - | Root directory containing backup files |
| `Action` | String | Yes | - | Action to perform: `catalog`, `restore`, or `verify` |
| `DatabaseName` | String | No | - | Specific database name (required for restore, optional for others) |
| `DataPath` | String | No | - | Directory for data files (required for restore) |
| `LogPath` | String | No | - | Directory for log files (required for restore) |
### Actions
#### Catalog Action
Lists all backup files found in the LiveMountRoot directory, organized by database and backup type.
**Example**:
```powershell
.\RestoreScript.ps1 -SqlInstance "SQLFC1\SQLFC1P" -LiveMountRoot "C:\Rubrik\lm\" -Action catalog
```
**Output**:
```
Database Backups Catalog:
=========================
Database: DataAW2
Type: FULL
Backup: 20251127093305
C:\Rubrik\lm\DataAW2_FULL_20251127093305_0001.bak
Type: DIFF
Backup: 20251128005350
C:\Rubrik\lm\DataAW2_DIFF_20251128005350_0001.bak
...
Type: LOG
Backup: 20251130130213
C:\Rubrik\lm\DataAW2_LOG_20251130130213_0001.trn
...
Summary of Databases:
- DataAW2
```
#### Restore Action
Restores a specific database using the latest full backup, all subsequent diffs, and logs taken after the latest diff. Files are relocated to the specified DataPath and LogPath.
**Example**:
```powershell
.\RestoreScript.ps1 -SqlInstance "SQLFC1\SQLFC1P" -LiveMountRoot "C:\Rubrik\lm\" -Action restore -DatabaseName "DataAW2" -DataPath "F:\Data" -LogPath "G:\Logs"
```
**Output**:
```
Restoring FULL backup for DataAW2...
Applying DIFF backup 20251128005350 for DataAW2...
Applying DIFF backup 20251129010111 for DataAW2...
Applying DIFF backup 20251130000053 for DataAW2...
Applying DIFF backup 20251201113159 for DataAW2...
Applying LOG backup 20251201124800 for DataAW2...
Finalizing restore for DataAW2...
Restore completed for DataAW2
```
#### Verify Action
Verifies backup file integrity and displays LSN ranges and backup details.
**Example**:
```powershell
.\RestoreScript.ps1 -SqlInstance "SQLFC1\SQLFC1P" -LiveMountRoot "C:\Rubrik\lm\" -Action verify -DatabaseName "DataAW2"
```
**Output**:
```
Verifying backups for database: DataAW2
Verifying FULL backup 20251127093305 for DataAW2...
Verification successful for FULL 20251127093305
Backup Details:
Start Date: 11/27/2025 09:33:05
Finish Date: 11/27/2025 09:33:07
First LSN: 56000000289000001
Last LSN: 56000000289300001
Database Backup LSN: 56000000182200001
Differential Base LSN:
...
Backup Summary for database: DataAW2
===================================
FULL Backups:
Date: 11/27/2025 09:33:07 | LSN Range: 56000000289000001 - 56000000289300001
DIFFERENTIAL Backups:
Date: 11/25/2025 00:21:45 | Base LSN: 56000000182200001 | LSN Range: 56000000270100001 - 56000000270400001
...
LOG Backups:
Point-in-Time Range: 11/30/2025 13:02:13 to 12/01/2025 12:48:00
LSN Range: 56000000345300001 - 56000000353400001
No gaps detected in LOG sequence
```
## Restore Logic
The script follows a specific restore sequence to ensure data consistency:
1. **Full Backup**: Always starts with the latest full backup
2. **Differential Backups**: Applies all differential backups taken after the full backup
3. **Log Backups**: Applies only log backups taken after the latest differential backup
This approach avoids LSN conflicts that can occur when mixing diffs and logs from different time ranges.
## Error Handling
### Common Issues
#### "Directory lookup for the file failed"
**Cause**: Original file paths don't exist on the restore server
**Solution**: Use `-DataPath` and `-LogPath` to relocate files to valid directories
#### "The log in this backup set terminates at LSN X, which is too early to apply"
**Cause**: Log backup is from before the applied differential backup
**Solution**: The script automatically handles this by only applying logs after the latest diff
#### "Database not found in catalog"
**Cause**: No backups found for the specified database
**Solution**: Run catalog action first to verify available databases
#### "Access denied" to directories
**Cause**: Insufficient permissions on data/log paths
**Solution**: Ensure the SQL Server service account has write access to the specified directories
## Troubleshooting
### Debug Steps
1. Run the `catalog` action to verify backup files are detected correctly
2. Run the `verify` action to check backup integrity and LSN ranges
3. Ensure DataPath and LogPath directories exist and are writable
4. Check SQL Server error logs for additional details
### Performance Considerations
- Restore operations can be time-intensive for large databases
- Ensure sufficient disk space on target directories
- Monitor SQL Server performance during restore operations
## Security Considerations
- **SQL Permissions**: Requires sysadmin or database restore permissions
- **File System Access**: Read access to backup sources, write access to restore destinations
- **Data Protection**: Ensure backup files are from trusted sources
## Support and Maintenance
### Monitoring
- Verify restore completion and database accessibility
- Check SQL Server logs for any restore-related errors
- Validate file locations after restore
### Best Practices
- Test restores in development environments first
- Keep backup files organized and accessible
- Document custom DataPath/LogPath configurations
- Regularly verify backup integrity with the verify action</content>
<parameter name="filePath">README.md <parameter name="filePath">README.md
+18 -1
View File
@@ -577,7 +577,24 @@ function Print-BackupSummary {
Write-Host " Point-in-Time Range: $(([datetime]$firstLog.BackupStartDate).ToString([System.Globalization.CultureInfo]::CurrentCulture)) to $(([datetime]$lastLog.BackupFinishDate).ToString([System.Globalization.CultureInfo]::CurrentCulture))" Write-Host " Point-in-Time Range: $(([datetime]$firstLog.BackupStartDate).ToString([System.Globalization.CultureInfo]::CurrentCulture)) to $(([datetime]$lastLog.BackupFinishDate).ToString([System.Globalization.CultureInfo]::CurrentCulture))"
Write-Host " LSN Range: $($firstLog.FirstLSN) - $($lastLog.LastLSN)" Write-Host " LSN Range: $($firstLog.FirstLSN) - $($lastLog.LastLSN)"
# Check for gaps # Check that the first LOG connects to the most recent FULL (or DIFF if one exists).
$baseLastLsn = $null
$baseLabel = $null
if ($Headers.ContainsKey('DIFF') -and $Headers['DIFF'].Count -gt 0) {
$latestDiff = @($Headers['DIFF'] | Sort-Object { $_.Key })[-1]
$dh = @($latestDiff.Header)[0]
if ($dh) { $baseLastLsn = $dh.LastLSN; $baseLabel = "DIFF $($latestDiff.Key)" }
}
if (-not $baseLastLsn -and $Headers.ContainsKey('FULL') -and $Headers['FULL'].Count -gt 0) {
$latestFull = @($Headers['FULL'] | Sort-Object { $_.Key })[-1]
$fh = @($latestFull.Header)[0]
if ($fh) { $baseLastLsn = $fh.LastLSN; $baseLabel = "FULL $($latestFull.Key)" }
}
if ($baseLastLsn -and [decimal]$firstLog.FirstLSN -gt [decimal]$baseLastLsn) {
Write-Host " *** LSN GAP: first LOG (LSN $($firstLog.FirstLSN)) does not connect to $baseLabel (ends LSN $baseLastLsn) — point-in-time restore will fail ***"
}
# Check for gaps within the LOG sequence
$gaps = @() $gaps = @()
for ($i = 1; $i -lt $logItems.Count; $i++) { for ($i = 1; $i -lt $logItems.Count; $i++) {
$prevHeader = @($logItems[$i-1].Header)[0] $prevHeader = @($logItems[$i-1].Header)[0]
+33 -6
View File
@@ -93,7 +93,8 @@ function Get-ExceptionMessageList {
function Write-ExceptionDiagnostics { function Write-ExceptionDiagnostics {
param( param(
[string]$Prefix, [string]$Prefix,
[System.Exception]$Exception [System.Exception]$Exception,
[System.Management.Automation.ErrorRecord]$ErrorRecord
) )
if (-not $Exception) { if (-not $Exception) {
@@ -101,7 +102,8 @@ function Write-ExceptionDiagnostics {
return return
} }
$detailLines = Get-ExceptionMessageList -Exception $Exception # Force array semantics so single detail lines are not treated as scalar strings.
$detailLines = @(Get-ExceptionMessageList -Exception $Exception)
if ($detailLines.Count -eq 0) { if ($detailLines.Count -eq 0) {
Write-Log "ERROR: $Prefix $($Exception.Message)" Write-Log "ERROR: $Prefix $($Exception.Message)"
return return
@@ -111,6 +113,31 @@ function Write-ExceptionDiagnostics {
for ($i = 1; $i -lt $detailLines.Count; $i++) { for ($i = 1; $i -lt $detailLines.Count; $i++) {
Write-Log "ERROR: detail[$i]: $($detailLines[$i])" Write-Log "ERROR: detail[$i]: $($detailLines[$i])"
} }
if ($ErrorRecord) {
if ($ErrorRecord.FullyQualifiedErrorId) {
Write-Log "ERROR: FullyQualifiedErrorId: $($ErrorRecord.FullyQualifiedErrorId)"
}
if ($ErrorRecord.CategoryInfo) {
Write-Log "ERROR: CategoryInfo: $($ErrorRecord.CategoryInfo)"
}
$recordText = $ErrorRecord.ToString()
if (-not [string]::IsNullOrWhiteSpace($recordText)) {
$recordLines = @($recordText -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
foreach ($line in $recordLines) {
Write-Log "ERROR: Record: $line"
}
}
}
$exceptionText = $Exception.ToString()
if (-not [string]::IsNullOrWhiteSpace($exceptionText)) {
$exceptionLines = @($exceptionText -split "`r?`n" | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
foreach ($line in $exceptionLines) {
Write-Log "ERROR: Exception: $line"
}
}
} }
function Test-ServiceAccountFile { function Test-ServiceAccountFile {
@@ -153,7 +180,7 @@ function Connect-RscWithDiagnostics {
Write-Log "INFO: $Context Connected to Rubrik Security Cloud." Write-Log "INFO: $Context Connected to Rubrik Security Cloud."
} catch { } catch {
$prefix = "($Context Connect-Rsc -ServiceAccountFile $ServiceAccountFile) failed." $prefix = "($Context Connect-Rsc -ServiceAccountFile $ServiceAccountFile) failed."
Write-ExceptionDiagnostics -Prefix $prefix -Exception $_.Exception Write-ExceptionDiagnostics -Prefix $prefix -Exception $_.Exception -ErrorRecord $_
Write-Log "ERROR: Troubleshooting hints: verify file permissions/content, outbound HTTPS connectivity, proxy configuration, and local system time sync." Write-Log "ERROR: Troubleshooting hints: verify file permissions/content, outbound HTTPS connectivity, proxy configuration, and local system time sync."
throw throw
} }
@@ -379,7 +406,7 @@ try {
} }
Write-Log "INFO: Retrieved paths: $($paths -join ', ')" Write-Log "INFO: Retrieved paths: $($paths -join ', ')"
} catch { } catch {
Write-ExceptionDiagnostics -Prefix "Failed to retrieve paths from Rubrik." -Exception $_.Exception Write-ExceptionDiagnostics -Prefix "Failed to retrieve paths from Rubrik." -Exception $_.Exception -ErrorRecord $_
exit 1 exit 1
} }
@@ -505,7 +532,7 @@ function Get-BackupType($directoryParam) {
$reason = if ($isFullBackupOverdue) { "overdue" } else { "scheduled" } $reason = if ($isFullBackupOverdue) { "overdue" } else { "scheduled" }
return @{ Type = "FULL"; CleanupTime = 168; Reason = $reason } return @{ Type = "FULL"; CleanupTime = 168; Reason = $reason }
} else { } else {
return @{ Type = "LOG"; CleanupTime = 24; Reason = "full already taken today" } return @{ Type = "LOG"; CleanupTime = 48; Reason = "full already taken today" }
} }
} }
@@ -536,7 +563,7 @@ function Get-BackupType($directoryParam) {
} }
return @{ Type = "DIFF"; CleanupTime = 168; Reason = "differential scheduled" } return @{ Type = "DIFF"; CleanupTime = 168; Reason = "differential scheduled" }
} else { } else {
return @{ Type = "LOG"; CleanupTime = 24; Reason = "diff already taken today" } return @{ Type = "LOG"; CleanupTime = 48; Reason = "diff already taken today" }
} }
} }
-271
View File
@@ -1,271 +0,0 @@
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
}
-123
View File
@@ -1,123 +0,0 @@
##########################################################################
#
# Update an SLA MV to point to the local host
# Created by Rubrik PS for ZF, September 2025
#
# Must be run with a Global service account.
#
# Requires RubrikSecurityCloud module to be installed and working with
# a Global Service Account with the following rights (TBC)
#
# Create the service account file with:
# Set-RscServiceAccountFile sa.json -OutputFilePath Global.xml
#
# Example invocation
# .\claimInstance.ps1 -sqlInstance "sqlfcsql\TESTINST" -mvName "JP-ZF-SQL"
#
# v0.1 Initial Release
#
##########################################################################
param (
[Parameter(Mandatory=$False,
HelpMessage="Instance to claim")]
[string]$sqlInstance,
[Parameter(Mandatory=$False,
HelpMessage="Do not change the MV")]
[switch]$dryrun
)
# SA File must be an absolute path
$GlobalSAFile = "C:\Rubrik\scripts\sa.xml"
$logFile = "C:\Rubrik\scripts\claimInstance.log"
$mvName = "JP-ZF-SQL"
$sqlInstance = "sqlfcsql\TESTINST"
$checkCluster = $true
###########################
# Script begins
###########################
$ErrorActionPreference = 'Stop'
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
}
Import-Module RubrikSecurityCloud
if ($checkCluster) {
# Check if SQL instance is running locally
$localNode = $env:COMPUTERNAME
$instanceName = $sqlInstance.Split('\')[1]
$clusterInstance = Get-ClusterResource | Where-Object { $_.ResourceType -eq "SQL Server" -and $_.Name -eq "SQL Server ($instanceName)" }
if ($clusterInstance) {
$ownerNode = $clusterInstance.OwnerNode
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."
}
Connect-Rsc -ServiceAccountFile $GlobalSAFile
Write-Log "Connected to Rubrik Security Cloud."
$myHost = Get-RscHost -Name $env:COMPUTERNAME -OsType WINDOWS
$query = New-RscQuery -GqlQuery slaManagedVolumes -AddField Nodes.HostDetail, Nodes.SmbShare, Nodes.ClientConfig, Nodes.ClientConfig.BackupScript
$query.var.filter = @(Get-RscType -Name Filter)
$query.var.filter[0].field = "NAME_EXACT_MATCH"
$query.var.filter[0].Texts = $mvName
#$query.Field.Nodes = @(Get-RscType -Name ManagedVolume -InitialProperties name, Id, hostDetail.Id, hostDetail.Status, hostDetail.Name)
$mvDetail = $query.Invoke().nodes[0]
Write-Log "Found Managed Volume: $($mvDetail.Name) (ID: $($mvDetail.Id), Status: $($mvDetail.hostDetail.Status), HostDetail Name: $($mvDetail.hostDetail.Name))"
if ($myHost.Id -ne $mvDetail.hostDetail.Id) {
Write-Log "WARNING: Host ID ($($myHost.Id)) does not match Managed Volume HostDetail ID ($($mvDetail.hostDetail.Id))."
$query = New-RscMutation -GqlMutation updateManagedVolume
$query.Var.input = Get-RscType -Name UpdateManagedVolumeInput
$query.Var.input.update = Get-RscType -Name ManagedVolumeUpdateInput
$query.Var.input.update.config = Get-RscType -Name ManagedVolumePatchConfigInput
$query.Var.input.update.slaClientConfig = Get-RscType -Name ManagedVolumePatchSlaClientConfigInput
$query.Var.input.Id = $mvDetail.Id
$query.Var.input.update.Name = $mvName
$query.Var.input.update.config.SmbDomainName = $mvDetail.SmbShare.DomainName
$query.Var.input.update.config.SmbValidIps = $myHost.Name
$query.Var.input.update.config.SmbValidUsers = $mvDetail.SmbShare.ValidUsers + $mvDetail.SmbShare.ActiveDirectoryGroups
$query.Var.input.update.slaClientConfig.clientHostId = $myHost.Id
$query.Var.input.update.slaClientConfig.channelHostMountPaths = $mvDetail.ClientConfig.ChannelHostMountPaths
$query.Var.input.update.slaClientConfig.backupScriptCommand = $mvDetail.ClientConfig.BackupScript.ScriptCommand
$query.Var.input.update.slaClientConfig.shouldDisablePostBackupScriptOnBackupFailure = $true
$query.Var.input.update.slaClientConfig.shouldDisablePostBackupScriptOnBackupSuccess = $true
$query.Var.input.update.slaClientConfig.shouldDisablePreBackupScript = $true
$query.gqlRequest().Variables
if (-not $dryrun) {
$result = $query.Invoke()
} else {
Write-Log "Dry run mode: Managed Volume update not invoked."
}
} else {
Write-Log "Host ID ($($myHost.Id)) matches Managed Volume HostDetail ID ($($mvDetail.hostDetail.Id)). No action needed."
}
Disconnect-Rsc
-2
View File
@@ -1,2 +0,0 @@
@echo off
powershell -ExecutionPolicy Bypass -File "%~dp0setActiveNode.ps1" %*
-128
View File
@@ -1,128 +0,0 @@
##########################################################################
#
# Update an SLA MV to point to the correct host
# Created by Rubrik PS for ZF, September 2025
#
# Must be run with a Global service account.
#
# Requires RubrikSecurityCloud module to be installed and working with
# a Global Service Account with the following rights (TBC)
#
# Create the service account file with:
# Set-RscServiceAccountFile sa.json -OutputFilePath sa-rbksql.xml
#
# Example invocation
# .\setActiveNode.ps1 -SqlInstance "sqlfcsql\TESTINST" -mvName "JP-ZF-SQL"
#
# v0.1 Initial Release
#
##########################################################################
param (
[Parameter(Mandatory=$True,
HelpMessage="Instance to claim")]
[string]$SqlInstance,
[Parameter(Mandatory=$True,
HelpMessage="Managed Volume name")]
[string]$mvName,
[Parameter(Mandatory=$False,
HelpMessage="Do not change the MV")]
[switch]$dryrun
)
# SA File must be an absolute path
$SAFile = "C:\Rubrik\scripts\rbksql.xml"
$logFile = "C:\Rubrik\scripts\setActiveNode.log"
$checkCluster = $true
###########################
# Script begins
###########################
$ErrorActionPreference = 'Stop'
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
}
Import-Module RubrikSecurityCloud
if ($checkCluster) {
# Check if SQL instance is running locally
$localNode = $env:COMPUTERNAME
$instanceName = $SqlInstance.Split('\')[1]
$clusterInstance = Get-ClusterResource | Where-Object { $_.ResourceType -eq "SQL Server" -and $_.Name -eq "SQL Server ($instanceName)" }
if ($clusterInstance) {
$ownerNode = $clusterInstance.OwnerNode
if ($ownerNode -ne $localNode) {
Write-Log "SQL instance '$SqlInstance' is not running on local node '$localNode'. Updating the MV."
Connect-Rsc -ServiceAccountFile $SAFile
Write-Log "Connected to Rubrik Security Cloud."
$newHost = Get-RscHost -Name $ownerNode -OsType WINDOWS
$query = New-RscQuery -GqlQuery slaManagedVolumes -AddField Nodes.HostDetail, Nodes.SmbShare, Nodes.ClientConfig, Nodes.ClientConfig.BackupScript, Nodes.ClientConfig.PreBackupScript
$query.var.filter = @(Get-RscType -Name Filter)
$query.var.filter[0].field = "NAME_EXACT_MATCH"
$query.var.filter[0].Texts = $mvName
$mvDetail = $query.Invoke().nodes[0]
Write-Log "Found Managed Volume: $($mvDetail.Name) (ID: $($mvDetail.Id), Status: $($mvDetail.hostDetail.Status), HostDetail Name: $($mvDetail.hostDetail.Name))"
$query = New-RscMutation -GqlMutation updateManagedVolume
$query.Var.input = Get-RscType -Name UpdateManagedVolumeInput
$query.Var.input.update = Get-RscType -Name ManagedVolumeUpdateInput
$query.Var.input.update.config = Get-RscType -Name ManagedVolumePatchConfigInput
$query.Var.input.update.slaClientConfig = Get-RscType -Name ManagedVolumePatchSlaClientConfigInput
$query.Var.input.Id = $mvDetail.Id
$query.Var.input.update.Name = $mvName
$query.Var.input.update.config.SmbDomainName = $mvDetail.SmbShare.DomainName
$query.Var.input.update.config.SmbValidIps = $newHost.Name
$query.Var.input.update.config.SmbValidUsers = $mvDetail.SmbShare.ValidUsers + $mvDetail.SmbShare.ActiveDirectoryGroups
$query.Var.input.update.slaClientConfig.clientHostId = $newHost.Id
$query.Var.input.update.slaClientConfig.channelHostMountPaths = $mvDetail.ClientConfig.ChannelHostMountPaths
$query.Var.input.update.slaClientConfig.backupScriptCommand = $mvDetail.ClientConfig.BackupScript.ScriptCommand
# Only set pre-backup script fields if a pre-backup script was configured
if ($mvDetail.ClientConfig.PreBackupScript.ScriptCommand) {
$query.Var.input.update.slaClientConfig.preBackupScriptCommand = $mvDetail.ClientConfig.PreBackupScript.ScriptCommand
$query.Var.input.update.slaClientConfig.preBackupScriptTimeout = $mvDetail.ClientConfig.PreBackupScript.Timeout
$query.Var.input.update.slaClientConfig.shouldCancelBackupOnPreBackupScriptFailure = $mvDetail.ClientConfig.ShouldCancelBackupOnPreBackupScriptFailure
$query.Var.input.update.slaClientConfig.shouldDisablePreBackupScript = $false
} else {
$query.Var.input.update.slaClientConfig.shouldDisablePreBackupScript = $true
}
$query.Var.input.update.slaClientConfig.shouldDisablePostBackupScriptOnBackupFailure = $true
$query.Var.input.update.slaClientConfig.shouldDisablePostBackupScriptOnBackupSuccess = $true
$query.gqlRequest().Variables
if (-not $dryrun) {
$result = $query.Invoke()
} else {
Write-Log "Dry run mode: Managed Volume update not invoked."
}
# Now must exit 1 to stop the backup continuing on the wrong node
Disconnect-Rsc
exit 1
} else {
Write-Log "SQL instance '$SqlInstance' is running on local node '$localNode'. No action needed."
}
} 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."
}