Latest update
This commit is contained in:
@@ -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
|
||||||
+62
-20
@@ -9,7 +9,7 @@ param(
|
|||||||
[ValidateSet("catalog", "restore", "verify")]
|
[ValidateSet("catalog", "restore", "verify")]
|
||||||
[string]$Action,
|
[string]$Action,
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$false)]
|
||||||
[string]$SqlInstance = "sqlfcsql\TESTINST",
|
[string]$SqlInstance = "sqlfcsql\TESTINST",
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
[Parameter(Mandatory=$false)]
|
||||||
@@ -66,7 +66,11 @@ function Catalog-Backups {
|
|||||||
$timeStr = $parts[$typeIndex + 2]
|
$timeStr = $parts[$typeIndex + 2]
|
||||||
$stripe = 0
|
$stripe = 0
|
||||||
if ($parts.Length -gt $typeIndex + 3) {
|
if ($parts.Length -gt $typeIndex + 3) {
|
||||||
$stripe = [int]$parts[$typeIndex + 3]
|
$stripeVal = 0
|
||||||
|
if (-not [int]::TryParse($parts[$typeIndex + 3], [ref]$stripeVal)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$stripe = $stripeVal
|
||||||
}
|
}
|
||||||
$key = "$dateStr$timeStr"
|
$key = "$dateStr$timeStr"
|
||||||
|
|
||||||
@@ -140,9 +144,10 @@ function Restore-Database {
|
|||||||
|
|
||||||
if ($StopAtTime) {
|
if ($StopAtTime) {
|
||||||
try {
|
try {
|
||||||
$targetStopAtTime = [datetime]::Parse($StopAtTime)
|
# Parse using the current OS locale so users can enter dates in their own format.
|
||||||
|
$targetStopAtTime = [datetime]::Parse($StopAtTime, [System.Globalization.CultureInfo]::CurrentCulture)
|
||||||
} catch {
|
} catch {
|
||||||
Write-Error "StopAtTime '$StopAtTime' is not a valid datetime value"
|
Write-Error "StopAtTime '$StopAtTime' is not a valid datetime value (expected format for your locale: $([System.Globalization.CultureInfo]::CurrentCulture.DateTimeFormat.ShortDatePattern) HH:mm:ss)"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,10 +212,7 @@ function Restore-Database {
|
|||||||
$fullFiles = $selectedFull.Files
|
$fullFiles = $selectedFull.Files
|
||||||
$fullHeader = $selectedFull.Header
|
$fullHeader = $selectedFull.Header
|
||||||
|
|
||||||
$fullBaseLsn = $fullHeader.CheckpointLSN
|
$fullBaseLsn = if ($fullHeader.CheckpointLSN) { [decimal]$fullHeader.CheckpointLSN } else { [decimal]$fullHeader.FirstLSN }
|
||||||
if (-not $fullBaseLsn) {
|
|
||||||
$fullBaseLsn = $fullHeader.FirstLSN
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create directories if specified
|
# Create directories if specified
|
||||||
if ($DataPath -and -not (Test-Path $DataPath)) {
|
if ($DataPath -and -not (Test-Path $DataPath)) {
|
||||||
@@ -277,7 +279,7 @@ function Restore-Database {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($diffHeader.DifferentialBaseLSN -ne $fullBaseLsn) {
|
if ([decimal]$diffHeader.DifferentialBaseLSN -ne $fullBaseLsn) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,15 +311,52 @@ function Restore-Database {
|
|||||||
$logStartKey = if ($selectedDiffKey) { $selectedDiffKey } else { $latestFullKey }
|
$logStartKey = if ($selectedDiffKey) { $selectedDiffKey } else { $latestFullKey }
|
||||||
$logKeys = @()
|
$logKeys = @()
|
||||||
|
|
||||||
|
# Determine the LastLSN of the last backup that will be restored (FULL or DIFF).
|
||||||
|
$lastRestoredHeader = if ($selectedDiff) {
|
||||||
|
Get-BackupInfo -Files $selectedDiff.Files -Instance $Instance
|
||||||
|
} else {
|
||||||
|
$fullHeader
|
||||||
|
}
|
||||||
|
$lastRestoredLsn = if ($lastRestoredHeader) { [decimal]$lastRestoredHeader.LastLSN } else { $null }
|
||||||
|
|
||||||
# Select LOG backups after the latest restored FULL/DIFF point.
|
# Select LOG backups after the latest restored FULL/DIFF point.
|
||||||
if ($dbCatalog.ContainsKey('LOG')) {
|
if ($dbCatalog.ContainsKey('LOG')) {
|
||||||
$candidateLogKeys = @($dbCatalog['LOG'].Keys | Where-Object { $_ -gt $logStartKey } | Sort-Object)
|
$candidateLogKeys = @($dbCatalog['LOG'].Keys | Where-Object { $_ -gt $logStartKey } | Sort-Object)
|
||||||
|
|
||||||
|
# Validate LSN continuity: the first candidate LOG must connect to the end of the FULL/DIFF chain.
|
||||||
|
# If there is a gap (first LOG's FirstLSN > FULL/DIFF LastLSN), no logs can be applied.
|
||||||
|
if ($candidateLogKeys.Count -gt 0 -and $lastRestoredLsn) {
|
||||||
|
$firstLogFiles = $dbCatalog['LOG'][$candidateLogKeys[0]] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
|
||||||
|
$firstLogHeader = Get-BackupInfo -Files $firstLogFiles -Instance $Instance
|
||||||
|
if ($firstLogHeader -and [decimal]$firstLogHeader.FirstLSN -gt $lastRestoredLsn) {
|
||||||
|
Write-Warning "LSN gap detected: first available LOG backup ($($candidateLogKeys[0])) starts at LSN $($firstLogHeader.FirstLSN), but the restored FULL/DIFF ends at LSN $lastRestoredLsn. No log backups can be applied."
|
||||||
|
$candidateLogKeys = @()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$skipLogSelection = $false
|
||||||
if ($isPointInTimeRestore -and $candidateLogKeys.Count -eq 0) {
|
if ($isPointInTimeRestore -and $candidateLogKeys.Count -eq 0) {
|
||||||
|
# Only error if the target point is beyond the last restored backup's finish time.
|
||||||
|
# If the target is at or before that time, no logs are required and restore can proceed.
|
||||||
|
$lastRestoredFinish = if ($lastRestoredHeader) {
|
||||||
|
try { [datetime]$lastRestoredHeader.BackupFinishDate } catch { $null }
|
||||||
|
} else { $null }
|
||||||
|
$targetIsBeyondRestored = $true
|
||||||
|
if ($targetStopAtTime -and $lastRestoredFinish -and $targetStopAtTime -le $lastRestoredFinish) {
|
||||||
|
$targetIsBeyondRestored = $false
|
||||||
|
}
|
||||||
|
if ($targetStopAtLsn -and $lastRestoredLsn -and $targetStopAtLsn -le $lastRestoredLsn) {
|
||||||
|
$targetIsBeyondRestored = $false
|
||||||
|
}
|
||||||
|
if ($targetIsBeyondRestored) {
|
||||||
Write-Error "Point-in-time restore requested but no LOG backups are available after the selected FULL/DIFF restore point"
|
Write-Error "Point-in-time restore requested but no LOG backups are available after the selected FULL/DIFF restore point"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
# Target is within the FULL/DIFF range; no logs needed.
|
||||||
|
$skipLogSelection = $true
|
||||||
|
}
|
||||||
|
|
||||||
if ($StopAtTime) {
|
if (-not $skipLogSelection -and $StopAtTime) {
|
||||||
$cutoffFound = $false
|
$cutoffFound = $false
|
||||||
foreach ($key in $candidateLogKeys) {
|
foreach ($key in $candidateLogKeys) {
|
||||||
$logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
|
$logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
|
||||||
@@ -353,7 +392,7 @@ function Restore-Database {
|
|||||||
Write-Error "StopAtTime '$StopAtTime' is not covered by available LOG backups after the selected FULL/DIFF restore point"
|
Write-Error "StopAtTime '$StopAtTime' is not covered by available LOG backups after the selected FULL/DIFF restore point"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} elseif ($StopAtLSN) {
|
} elseif (-not $skipLogSelection -and $StopAtLSN) {
|
||||||
$cutoffFound = $false
|
$cutoffFound = $false
|
||||||
foreach ($key in $candidateLogKeys) {
|
foreach ($key in $candidateLogKeys) {
|
||||||
$logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
|
$logFiles = $dbCatalog['LOG'][$key] | Sort-Object { $_.Stripe } | ForEach-Object { $_.File }
|
||||||
@@ -395,7 +434,7 @@ function Restore-Database {
|
|||||||
$logKeys = $candidateLogKeys
|
$logKeys = $candidateLogKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($isPointInTimeRestore -and $logKeys.Count -eq 0) {
|
if (-not $skipLogSelection -and $isPointInTimeRestore -and $logKeys.Count -eq 0) {
|
||||||
Write-Error "Point-in-time restore requested but no usable LOG backups were selected"
|
Write-Error "Point-in-time restore requested but no usable LOG backups were selected"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -431,7 +470,7 @@ function Restore-Database {
|
|||||||
}
|
}
|
||||||
if ($isPointInTimeRestore) {
|
if ($isPointInTimeRestore) {
|
||||||
if ($StopAtTime) {
|
if ($StopAtTime) {
|
||||||
Write-Host "PITR Target: STOPAT '$StopAtTime'"
|
Write-Host "PITR Target: STOPAT '$($targetStopAtTime.ToString([System.Globalization.CultureInfo]::CurrentCulture))'"
|
||||||
} else {
|
} else {
|
||||||
$previewLsn = if ($StopAtLSN -like 'lsn:*') { $StopAtLSN } else { "lsn:$StopAtLSN" }
|
$previewLsn = if ($StopAtLSN -like 'lsn:*') { $StopAtLSN } else { "lsn:$StopAtLSN" }
|
||||||
Write-Host "PITR Target: STOPATMARK '$previewLsn'"
|
Write-Host "PITR Target: STOPATMARK '$previewLsn'"
|
||||||
@@ -473,8 +512,11 @@ function Restore-Database {
|
|||||||
# Apply STOPAT/STOPATMARK only on the final LOG restore statement.
|
# Apply STOPAT/STOPATMARK only on the final LOG restore statement.
|
||||||
if ($isPointInTimeRestore -and $i -eq ($logKeys.Count - 1)) {
|
if ($isPointInTimeRestore -and $i -eq ($logKeys.Count - 1)) {
|
||||||
if ($StopAtTime) {
|
if ($StopAtTime) {
|
||||||
$escapedStopAtTime = $StopAtTime.Replace("'", "''")
|
# Use the parsed datetime formatted as ISO 8601, not the raw input string.
|
||||||
$logWithClause = "$logWithClause, STOPAT = '$escapedStopAtTime'"
|
# SQL Server requires a consistent datetime format; the user's input may be
|
||||||
|
# locale-specific (e.g. dd/MM/yyyy) which SQL Server does not accept.
|
||||||
|
$sqlStopAt = $targetStopAtTime.ToString("yyyy-MM-dd HH:mm:ss")
|
||||||
|
$logWithClause = "$logWithClause, STOPAT = '$sqlStopAt'"
|
||||||
} elseif ($StopAtLSN) {
|
} elseif ($StopAtLSN) {
|
||||||
$normalizedLsn = if ($StopAtLSN -like "lsn:*") { $StopAtLSN } else { "lsn:$StopAtLSN" }
|
$normalizedLsn = if ($StopAtLSN -like "lsn:*") { $StopAtLSN } else { "lsn:$StopAtLSN" }
|
||||||
$escapedLsn = $normalizedLsn.Replace("'", "''")
|
$escapedLsn = $normalizedLsn.Replace("'", "''")
|
||||||
@@ -510,7 +552,7 @@ function Print-BackupSummary {
|
|||||||
foreach ($item in $fullItems) {
|
foreach ($item in $fullItems) {
|
||||||
$header = @($item.Header)[0]
|
$header = @($item.Header)[0]
|
||||||
if (-not $header) { continue }
|
if (-not $header) { continue }
|
||||||
Write-Host " Date: $($header.BackupFinishDate) | LSN Range: $($header.FirstLSN) - $($header.LastLSN)"
|
Write-Host " Date: $(([datetime]$header.BackupFinishDate).ToString([System.Globalization.CultureInfo]::CurrentCulture)) | LSN Range: $($header.FirstLSN) - $($header.LastLSN)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,7 +563,7 @@ function Print-BackupSummary {
|
|||||||
foreach ($item in $diffItems) {
|
foreach ($item in $diffItems) {
|
||||||
$header = @($item.Header)[0]
|
$header = @($item.Header)[0]
|
||||||
if (-not $header) { continue }
|
if (-not $header) { continue }
|
||||||
Write-Host " Date: $($header.BackupFinishDate) | Base LSN: $($header.DifferentialBaseLSN) | LSN Range: $($header.FirstLSN) - $($header.LastLSN)"
|
Write-Host " Date: $(([datetime]$header.BackupFinishDate).ToString([System.Globalization.CultureInfo]::CurrentCulture)) | Base LSN: $($header.DifferentialBaseLSN) | LSN Range: $($header.FirstLSN) - $($header.LastLSN)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,7 +574,7 @@ function Print-BackupSummary {
|
|||||||
$firstLog = @($logItems[0].Header)[0]
|
$firstLog = @($logItems[0].Header)[0]
|
||||||
$lastLog = @($logItems[-1].Header)[0]
|
$lastLog = @($logItems[-1].Header)[0]
|
||||||
Write-Host "LOG Backups:"
|
Write-Host "LOG Backups:"
|
||||||
Write-Host " Point-in-Time Range: $($firstLog.BackupStartDate) to $($lastLog.BackupFinishDate)"
|
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 for gaps
|
||||||
@@ -590,8 +632,8 @@ function Verify-Backups {
|
|||||||
$header = Get-BackupInfo -Files $files -Instance $Instance
|
$header = Get-BackupInfo -Files $files -Instance $Instance
|
||||||
if ($header) {
|
if ($header) {
|
||||||
Write-Host " Backup Details:"
|
Write-Host " Backup Details:"
|
||||||
Write-Host " Start Date: $($header.BackupStartDate)"
|
Write-Host " Start Date: $(([datetime]$header.BackupStartDate).ToString([System.Globalization.CultureInfo]::CurrentCulture))"
|
||||||
Write-Host " Finish Date: $($header.BackupFinishDate)"
|
Write-Host " Finish Date: $(([datetime]$header.BackupFinishDate).ToString([System.Globalization.CultureInfo]::CurrentCulture))"
|
||||||
Write-Host " First LSN: $($header.FirstLSN)"
|
Write-Host " First LSN: $($header.FirstLSN)"
|
||||||
Write-Host " Last LSN: $($header.LastLSN)"
|
Write-Host " Last LSN: $($header.LastLSN)"
|
||||||
if ($header.DatabaseBackupLSN) {
|
if ($header.DatabaseBackupLSN) {
|
||||||
|
|||||||
+31
-4
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
@echo off
|
|
||||||
powershell -ExecutionPolicy Bypass -File "%~dp0setActiveNode.ps1" %*
|
|
||||||
@@ -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."
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user