Skip to content

Commit

Permalink
VSSTester: Allow specifying volumes to back up
Browse files Browse the repository at this point in the history
In previous versions of VSSTester, the user had to specify a
database when testing a DiskShadow backup. This would cause
VSSTester to look up the volumes that stored the database
and log files, and then it would snap those volumes.

This approach works for most cases, but it makes it hard to compare
with backup software that only allows specifying volumes, not
databases.

This change allows running a DiskShadow backup of specified
volumes. To get a list of acceptable volume names, the user can
specify one that is invalid. For example:

```
[PS] C:\>.\VSSTester.ps1 -DiskShadow -VolumesToBackup foo
C:\VSSTester.ps1 : Cannot validate argument on parameter 'VolumesToBackup'. Invalid volume specified. Please specify one of the
following values:
C:\
C:\Databases\DB1\
At line:1 char:70
+ ... code\CSS-Exchange\dist\VSSTester.ps1 -DiskShadow -VolumesToBackup foo
```

One or two volumes can be specified. As usual, drive letters must be
provided. For this syntax, the number of provided drive letters must
match the number of specified volumes. Example:

```
VSSTester.ps1 -DiskShadow -VolumesToBackup C:\Databases\DB1\ -ExposeSnapshotsOnDriveLetters X
```

This PR includes several other changes:

* Get-WmiObject has been replaced with Get-CimInstance.
* Drive letters are now specified with -ExposeSnapshotsOnDriveLetters,
  which more clearly communicates the intent and allows specifying
  one or more drive letters with a single parameter.
  • Loading branch information
bill-long committed Dec 6, 2023
1 parent 2f5ef4b commit 6529140
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 194 deletions.
245 changes: 98 additions & 147 deletions Databases/VSSTester/DiskShadow/Invoke-CreateDiskShadowFile.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,30 @@
# Licensed under the MIT License.

function Invoke-CreateDiskShadowFile {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWMICmdlet', '', Justification = 'Required to get drives on old systems')]
[OutputType([string[]])]
param(
[Parameter(Mandatory = $true)]
[string]
$OutputPath,

[Parameter(Mandatory = $true)]
[string]
$ServerName,

[Parameter(Mandatory = $true)]
[Parameter(Mandatory = $true, ParameterSetName = "BackupByDatabase")]
[object[]]
$Databases,

[Parameter(Mandatory = $true)]
[Parameter(Mandatory = $true, ParameterSetName = "BackupByDatabase")]
[object]
$DatabaseToBackup,

[Parameter(Mandatory = $true)]
[string]
$DatabaseDriveLetter,
[Parameter(Mandatory = $true, ParameterSetName = "BackupByVolume")]
[object[]]
$VolumesToBackup,

[Parameter(Mandatory = $true)]
[string]
$LogDriveLetter
[string[]]
$DriveLetters
)

function Out-DHSFile {
Expand Down Expand Up @@ -74,171 +72,124 @@ function Invoke-CreateDiskShadowFile {
Out-DHSFile "writer exclude {a65faa63-5ea8-4ebc-9dbd-a0c4db26912a}"
Out-DHSFile " "

# add databases to exclude
# ------------------------
foreach ($db in $Databases) {
if ($db.Identity -ne $DatabaseToBackup.Identity) {
if ($db.Server.Name -eq $ServerName) {
Out-DHSFile "writer exclude `"Microsoft Exchange Writer:\Microsoft Exchange Server\Microsoft Information Store\$serverName\$($db.Guid)`""
} else {
#if passive copy, add it with replica in the string
Out-DHSFile "writer exclude `"Microsoft Exchange Replica Writer:\Microsoft Exchange Server\Microsoft Information Store\Replica\$serverName\$($db.Guid)`""
if ($DatabaseToBackup) {
# add databases to exclude
# ------------------------
foreach ($db in $Databases) {
if ($db.Identity -ne $DatabaseToBackup.Identity) {
if ($db.Server.Name -eq $ServerName) {
Out-DHSFile "writer exclude `"Microsoft Exchange Writer:\Microsoft Exchange Server\Microsoft Information Store\$serverName\$($db.Guid)`""
} else {
#if passive copy, add it with replica in the string
Out-DHSFile "writer exclude `"Microsoft Exchange Replica Writer:\Microsoft Exchange Server\Microsoft Information Store\Replica\$serverName\$($db.Guid)`""
}
}
}
}

Out-DHSFile " "
Out-DHSFile "Begin backup"

# add the volumes for the included database
# -----------------------------------------
#gets a list of mount points on local server
$mpVolumes = Get-WmiObject -Query "select name, DeviceId from win32_volume where DriveType=3 AND DriveLetter=NULL"
$deviceIDs = @()

$dbMP = $false
$logMP = $false

#if no MountPoints ($mpVolumes) causes null-valued error, need to handle
if ($null -ne $mpVolumes) {
foreach ($mp in $mpVolumes) {
$mpName = (($mp.name).substring(0, $mp.name.length - 1))
#if following mount point path exists in database path use deviceID in DiskShadow config file
if ($DatabaseToBackup.EdbFilePath.PathName.StartsWith($mpName, [System.StringComparison]::OrdinalIgnoreCase)) {
Write-Host " Mount point: $($mp.name) in use for database path: "
#Write-host "Yes. I am a database in MountPoint"
Write-Host " The selected database path is: $($DatabaseToBackup.EdbFilePath.PathName)"
$dbEdbVol = $mp.DeviceId
Write-Host " adding deviceID to file: $dbEdbVol"

#add device ID to array
$deviceID1 = $mp.DeviceID
$dbMP = $true
if ($DatabaseToBackup) {
# add the volumes for the included database
# -----------------------------------------
#gets a list of mount points on local server
$mpVolumes = Get-CimInstance -Query "select name, DeviceId from win32_volume where DriveType=3 AND DriveLetter=NULL"
$deviceIDs = @()

$dbMP = $false
$logMP = $false

#if no MountPoints ($mpVolumes) causes null-valued error, need to handle
if ($null -ne $mpVolumes) {
foreach ($mp in $mpVolumes) {
$mpName = (($mp.name).substring(0, $mp.name.length - 1))
#if following mount point path exists in database path use deviceID in DiskShadow config file
if ($DatabaseToBackup.EdbFilePath.PathName.StartsWith($mpName, [System.StringComparison]::OrdinalIgnoreCase)) {
Write-Host " Mount point: $($mp.name) in use for database path: "
Write-Host " The selected database path is: $($DatabaseToBackup.EdbFilePath.PathName)"
$dbEdbVol = $mp.DeviceId
Write-Host " adding deviceID to file: $dbEdbVol"

#add device ID to array
$deviceID1 = $mp.DeviceID
$dbMP = $true
}

#if following mount point path exists in log path use deviceID in DiskShadow config file
if ($DatabaseToBackup.LogFolderPath.PathName.ToLower().Contains($mpName.ToLower())) {
Write-Host
Write-Host " Mount point: $($mp.name) in use for log path: "
Write-Host " The log folder path of selected database is: $($DatabaseToBackup.LogFolderPath.PathName)"
$dbLogVol = $mp.DeviceId
Write-Host " adding deviceID to file: $dbLogVol"
$deviceID2 = $mp.DeviceID
$logMP = $true
}
}
}

#if following mount point path exists in log path use deviceID in DiskShadow config file
if ($DatabaseToBackup.LogFolderPath.PathName.ToLower().Contains($mpName.ToLower())) {
Write-Host
Write-Host " Mount point: $($mp.name) in use for log path: "
#Write-host "Yes. My logs are in a MountPoint"
Write-Host " The log folder path of selected database is: $($DatabaseToBackup.LogFolderPath.PathName)"
$dbLogVol = $mp.DeviceId
Write-Host " adding deviceID to file: $dbLogVol"
$deviceID2 = $mp.DeviceID
$logMP = $true
}
if ($dbMP -eq $false) {
$dbEdbVol = ($DatabaseToBackup.EdbFilePath.PathName).substring(0, 2)
Write-Host " The selected database path is '$($DatabaseToBackup.EdbFilePath.PathName)' so adding volume $dbEdbVol to backup scope"
$deviceID1 = $dbEdbVol
}
$deviceIDs = $deviceID1, $deviceID2
}

if ($dbMP -eq $false) {
$dbEdbVol = ($DatabaseToBackup.EdbFilePath.PathName).substring(0, 2)
Write-Host " The selected database path is '$($DatabaseToBackup.EdbFilePath.PathName)' so adding volume $dbEdbVol to backup scope"
$deviceID1 = $dbEdbVol
}
if ($logMP -eq $false) {
$dbLogVol = ($DatabaseToBackup.LogFolderPath.PathName).substring(0, 2)
Write-Host " The selected database log folder path is '$($DatabaseToBackup.LogFolderPath.PathName)' so adding volume $dbLogVol to backup scope"
$deviceID2 = $dbLogVol
}

if ($logMP -eq $false) {
$dbLogVol = ($DatabaseToBackup.LogFolderPath.PathName).substring(0, 2)
Write-Host " The selected database log folder path is '$($DatabaseToBackup.LogFolderPath.PathName)' so adding volume $dbLogVol to backup scope"
$deviceID2 = $dbLogVol
$deviceIDs = @($deviceID1)
if ($deviceID2 -ne $deviceID1) {
$deviceIDs += $deviceID2
}
} else {
$validVolumes = Get-CimInstance -Query "select name, DeviceId from win32_volume where DriveType=3" |
Where-Object { $_.Name -match "^\w:" } | Select-Object Name, DeviceID
$deviceIDs = @()
foreach ($v in $VolumesToBackup) {
$volToBackup = $validVolumes | Where-Object { $_.Name -eq $v }
if ($null -eq $volToBackup) {
Write-Warning "Failed to find volume by name: $v. Available volumes:`n$([string]::Join("`n", $validVolumes))"
exit
}

$deviceIDs += $volToBackup.DeviceID
}
}

# Here is where we start adding the appropriate volumes or MountPoints to the DiskShadow config file
# We make sure that we add only one Logical volume when we detect the EDB and log files
# are on the same volume

Write-Host
$deviceIDs = $deviceID1, $deviceID2
$comp = [string]::Compare($deviceID1, $deviceID2, $True)
if ($comp -eq 0) {
$dID = $deviceIDs[0]
Write-Debug -Message ('$dID = ' + $dID.ToString())
Write-Debug "When the database and log files are on the same volume, we add the volume only once"
if ($dID.length -gt "2") {
$addVol = "add volume $dID alias vss_test_" + ($dID).ToString().substring(11, 8)
Write-Host $addVol
Out-DHSFile $addVol
} else {
$addVol = "add volume $dID alias vss_test_" + ($dID).ToString().substring(0, 1)
Write-Host $addVol
Out-DHSFile $addVol
}
} else {
Write-Host " "
foreach ($device in $deviceIDs) {
if ($device.length -gt "2") {
Write-Host " Adding the Mount Point for DSH file"
$addVol = "add volume $device alias vss_test_" + ($device).ToString().substring(11, 8)
Write-Host " $addVol"
Out-DHSFile $addVol
} else {
Write-Host " Adding the volume for DSH file"
$addVol = "add volume $device alias vss_test_" + ($device).ToString().substring(0, 1)
Write-Host " $addVol"
Out-DHSFile $addVol
}
}
for ($i = 0; $i -lt $deviceIDs.Count; $i++) {
$id = $deviceIDs[$i]
Write-Debug -Message ('$id = ' + $id.ToString())
$addVol = "add volume $id alias vss_test_$i"
Write-Host $addVol
Out-DHSFile $addVol
}

Out-DHSFile "create"
Out-DHSFile " "
Write-Host "$(Get-Date) Getting drive letters for exposing backup snapshot"

# check to see if the drives are the same for both database and logs
# if the same volume is used, only one drive letter is needed for exposure
# if two volumes are used, two drive letters are needed

$matchCondition = "^[a-z]:$"
Write-Debug $matchCondition

$dbSnapVol = $DatabaseDriveLetter
if ($comp -eq 0) {
$logSnapVol = $dbSnapVol
Write-Host " Since the same volume is used for this database's EDB and logs, we only need a single drive"
Write-Host " letter to expose the backup snapshot."
} else {
$logSnapVol = $LogDriveLetter
Write-Host " Since different volumes are used for this database's EDB and logs, we need two drive"
Write-Host " letters to expose the backup snapshot."
}

Write-Debug "dbSnapVol: $dbSnapVol | logSnapVol: $logSnapVol"

# expose the drives
# if volumes are the same only one entry is needed
if ($dbEdbVol -eq $dbLogVol) {
if ($dbEdbVol.length -gt "2") {
$dbVolStr = "expose %vss_test_" + ($dbEdbVol).substring(11, 8) + "% $($dbSnapVol):"
Out-DHSFile $dbVolStr
} else {
$dbVolStr = "expose %vss_test_" + ($dbEdbVol).substring(0, 1) + "% $($dbSnapVol):"
Out-DHSFile $dbVolStr
}
} else {
# volumes are different, getting both
# if MountPoint use first part of string, if not use first letter
if ($dbEdbVol.length -gt "2") {
$dbVolStr = "expose %vss_test_" + ($dbEdbVol).substring(11, 8) + "% $($dbSnapVol)"
Out-DHSFile $dbVolStr
} else {
$dbVolStr = "expose %vss_test_" + ($dbEdbVol).substring(0, 1) + "% $($dbSnapVol)"
Out-DHSFile $dbVolStr
}
if ($deviceIDs.Count -lt $DriveLetters.Count) {
Write-Warning "Determined that we need $($deviceIDs.Count) drive letters to expose the snapshots, but only $($DriveLetters.Count) were provided. Exiting."
exit
}

# if MountPoint use first part of string, if not use first letter
if ($dbLogVol.length -gt "2") {
$logVolStr = "expose %vss_test_" + ($dbLogVol).substring(11, 8) + "% $($logSnapVol):"
Out-DHSFile $logVolStr
} else {
$logVolStr = "expose %vss_test_" + ($dbLogVol).substring(0, 1) + "% $($logSnapVol):"
Out-DHSFile $logVolStr
}
for ($i = 0; $i -lt $deviceIDs.Count; $i++) {
$dbVolStr = "expose %vss_test_$($i)% $($DriveLetters[$i]):"
Out-DHSFile $dbVolStr
}

# ending data of file
Out-DHSFile "end backup"

if ($dbSnapVol -eq $logSnapVol) {
return @($dbSnapVol)
} else {
return @($dbSnapVol, $logSnapVol)
}
# return the drive letters we used
return $DriveLetters | Select-Object -First ($deviceIDs.Count)
}
8 changes: 2 additions & 6 deletions Databases/VSSTester/DiskShadow/Invoke-DiskShadow.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,10 @@ function Invoke-DiskShadow {
param(
[Parameter(Mandatory = $true)]
[string]
$OutputPath,

[Parameter(Mandatory = $true)]
[object]
$DatabaseToBackup
$OutputPath
)

Write-Host "$(Get-Date) Starting DiskShadow copy of Exchange database: $Database"
Write-Host "$(Get-Date) Starting DiskShadow copy."
Write-Host " Running the following command:"
Write-Host " `"C:\Windows\System32\DiskShadow.exe /s $OutputPath\DiskShadow.dsh /l $OutputPath\DiskShadow.log`""

Expand Down
8 changes: 4 additions & 4 deletions Databases/VSSTester/Logging/Invoke-DisableExtraTracing.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function Invoke-DisableExTRATracing {
[string]
$ServerName,

[Parameter(Mandatory = $true)]
[Parameter(Mandatory = $false)]
[object]
$DatabaseToBackup,

Expand All @@ -24,9 +24,8 @@ function Invoke-DisableExTRATracing {
$OutputPath
)
Write-Host "$(Get-Date) Disabling ExTRA Tracing..."
$dbMountedOn = $DatabaseToBackup.Server.Name
if ($dbMountedOn -eq "$ServerName") {
#stop active copy
$traceLocalServerOnly = $null -eq $DatabaseToBackup -or $DatabaseToBackup.Server.Name -eq $ServerName
if ($traceLocalServerOnly) {
Write-Host
Write-Host " Stopping Exchange Trace data collector on $ServerName..."
logman stop vssTester -s $ServerName
Expand All @@ -35,6 +34,7 @@ function Invoke-DisableExTRATracing {
Write-Host
} else {
#stop passive copy
$dbMountedOn = $DatabaseToBackup.Server.Name
Write-Host " Stopping Exchange Trace data collector on $ServerName..."
logman stop vssTester-Passive -s $ServerName
Write-Host " Deleting Exchange Trace data collector on $ServerName..."
Expand Down
8 changes: 4 additions & 4 deletions Databases/VSSTester/Logging/Invoke-EnableExtraTracing.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function Invoke-EnableExTRATracing {
[string]
$ServerName,

[Parameter(Mandatory = $true)]
[Parameter(Mandatory = $false)]
[object]
$DatabaseToBackup,

Expand Down Expand Up @@ -53,10 +53,9 @@ function Invoke-EnableExTRATracing {
}
}

$dbMountedOn = $DatabaseToBackup.Server.Name
$traceLocalServerOnly = $null -eq $DatabaseToBackup -or $DatabaseToBackup.Server.Name -eq $ServerName

#active server, only get tracing from active node
if ($dbMountedOn -eq $ServerName) {
if ($traceLocalServerOnly) {
Write-Host "Creating Exchange Trace data collector set..."
Invoke-ExtraTracingCreate -ComputerName $ServerName -LogmanName "VSSTester" -OutputPath $OutputPath
Write-Host "Starting Exchange Trace data collector..."
Expand All @@ -70,6 +69,7 @@ function Invoke-EnableExTRATracing {
Write-Host
} else {
#passive server, get tracing from both active and passive nodes
$dbMountedOn = $DatabaseToBackup.Server.Name
Write-Host "Copying the ExTRA config file 'EnabledTraces.config' file to $dbMountedOn..."
#copy EnabledTraces.config from current passive copy to active copy server
Copy-Item "c:\EnabledTraces.Config" "\\$dbMountedOn\c$\EnabledTraces.config" -Force
Expand Down
Loading

0 comments on commit 6529140

Please sign in to comment.