Update 9/25/20: There’s another version of this script has has been rewritten form scratch. IMO, maybe it’s better… here.
SysAdmins, SysEngineers, DBAs, or Devs are often tasked with moving & restoring databases to/from backups as part of spot-checking, testing, archival, migration, etc. purposes. Here’s a PowerShell method that interfaces with a Microsoft SQL server using SQL commands. This can be adapted to various environments as necessary.
# Set Variables
$orgName='TestOrg'
$databaseName="$orgName`_MSCRM"
$dbData='mscrm'
$dbLog='mscrm_log'
$overwriteFlag=$true
$sourceSqlServer='CRM-SQL01'
$sourceSa='domain\crmSa1'
$sourceSaPassword='somePassword'
$destinationSqlServer='CRM-SQL02'
$destinationSa='domain\crmSa2'
$destinationSaPassword='someOtherPassword'
$logFile='C:\Logs\$orgName_DbShipping.txt'
$serverAdminUsername='domain\adminDude'
$serverAdminPassword='sherverAdminPassword'
$serverAdminCredential=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $serverAdminUsername,$(ConvertTo-securestring $serverAdminPassword -AsPlainText -Force)
function addUserToLocalGroup{
param(
$computername=$env:computername,
$localAdminCred,
[string[]]$accountToAdd,
$localGroup='Administrators'
)
try{
$session=new-pssession $computername -Credential $localAdminCred -ea Stop
}
catch{
write-warning "Unable to connect to WinRM of $computername"
return $false
}
invoke-command -session $session -scriptblock{
param($principleName,$groupName)
$members=get-localgroupmember $groupName
if(!($principleName -in $members.Name)){
try{
write-host "Adding $principleName into $groupName";
Add-LocalGroupMember -Group $groupName -Member $principleName -ea Stop;
$currentMembers=get-localgroupmember $groupName|ft|out-string
write-host "$principleName has been added to $groupName successfully:`r`n$currentMembers";
return $true
}
catch{
write-warning "$error"
return $false
}
}
else{
write-host "$principleName is already a member of $groupName."
return $true}
} -args $accountToAdd,$localGroup
remove-pssession $session
}
function shipDatabase{
param(
$databaseName,$dbData,$dbLog,$overwrite,$serverAdminCredential
$sourceSqlServer,$sourceSa,$sourceSaPassword,
$destinationSqlServer,$destinationSa,$destinationSaPassword
)
# Start the timer for this activity
$stopWatch= [System.Diagnostics.Stopwatch]::StartNew()
# Credentials
$sourceSaCred=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $sourceSa,$(ConvertTo-securestring $sourceSaPassword -AsPlainText -Force)
$destinationSaCred=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $destinationSa,$(ConvertTo-securestring $destinationSaPassword -AsPlainText -Force)
addUserToLocalGroup $sourceSqlServer $serverAdminCredential $sourceSa
addUserToLocalGroup $destinationSqlServer $serverAdminCredential $destinationSa
# Scan for next available drive letter, excluding D "CD Rom" and H "Home"
$unavailableDriveLetters=(Get-Volume).DriveLetter|sort
$availableDriveLetters=.{(65..90|%{[char]$_})|?{$_ -notin $unavailableDriveLetters}}
[char]$firstAvailableDriveLetter=$availableDriveLetters[0]
[char]$secondAvailableDriveLetter=$availableDriveLetters[1]
function mountDriveAsUser($username,$password,$driveLetter,$uncPath){
if(get-psdrive $driveLetter -ea SilentlyContinue){
#Remove-PSDrive $firstAvailableDriveLetter -ea SilentlyContinue #This does not affect drives being mounted by 'net use' command
net use /delete ($driveLetter+':') 2>&1>null
}
try{
# This command cannot persist when out of scope of function; hence, net use is required
# New-PSDrive –Name $mountLetter –PSProvider FileSystem –Root $uncPath –Persist -Credential $mountAsCred|out-null
net use "$driveLetter`:" "$uncPath" /user:$username $password /persistent:Yes 2>&1>null
if(test-path "$driveLetter`:\"){
write-host "$driveLetter`: has successfully mounted.";
#return $true;
}
else{
write-host "Unable to mount drive $driveLetter";
#return $false
}
}
catch{
write-warning "$error"
#return $false
}
}
function triggerSqlBackup($sqlServer,$databaseName){
# Ensure the the Jump Box has SQL PowerShell tools
$moduleName='SqlServer'
if(!(Get-Module -ListAvailable -Name $moduleName -ea SilentlyContinue)){
if(!('NuGet' -in (get-packageprovider).Name)){
try{
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction SilentlyContinue;
}
catch{
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NetFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value '1' -Type DWord
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\.NetFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value '1' -Type DWord
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction SilentlyContinue;
}
}
Install-Module -Name $moduleName -Force -Confirm:$false
}
# Trigger backup of target database into the default backup directory
$database=Get-SqlDatabase -ServerInstance $sqlServer | Where { $_.Name -eq $databaseName }
$backupDuration=(measure-command{Backup-SqlDatabase -DatabaseObject $database -CompressionOption On -CopyOnly -Verbose}).TotalHours
return $backupDuration
}
$backupDuration=triggerSqlBackup $sourceSqlServer $databaseName
# Get default backup directory of a SQL server and mount it as the First Available Drive Letter
function convertLocalToUnc($localPath,$computername){
$uncPath=.{$x=$localPath -replace "^([a-zA-Z])\:","\\$computername\`$1`$";
if($x -match '\\$'){return $x.Substring(0,$x.length-1)}else{return $x}
}
$validLocal=if($localPath){test-path $localPath -ErrorAction SilentlyContinue}else{$false}
$validUnc=if($uncPath){test-path $uncPath -ErrorAction SilentlyContinue}else{$false}
if($validUnc){write-host "$uncPath is reachable, using credentials of $(whoami)"}
else{write-warning "$computername is unreachable, using credentials of $(whoami)"}
return $uncPath
}
function invokeGetDefaultSqlPaths($sqlServer,$saCredential){
$defaultValues=invoke-command -ComputerName $sqlServer -Credential $saCredential -ScriptBlock{
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
$sqlConnection = New-Object ('Microsoft.SqlServer.Management.Smo.Server') $env:computername
$defaultBackupDirectory=$sqlConnection.Settings.BackupDirectory
$defaultDataDirectory=$sqlConnection.Settings.DefaultFile
$defaultLogDirectory=$sqlConnection.Settings.DefaultLog
return @($defaultDataDirectory,$defaultLogDirectory,$defaultBackupDirectory)
}
if($defaultValues){return $defaultValues}
else{return $false}
}
# Mount Source UNC path
$sourceUncPath=convertLocalToUnc $(invokeGetDefaultSqlPaths $sourceSqlServer $sourceSaCred)[2] $sourceSqlServer
mountDriveAsUser $sourceSA $sourceSaPassword $firstAvailableDriveLetter $sourceUncPath
$sourceFolder="$firstAvailableDriveLetter`:\"
# Mount Destination UNC path
$destinationDefaults=invokeGetDefaultSqlPaths $destinationSqlServer $destinationSaCred
$destinationUncPath=convertLocalToUnc $destinationDefaults[2] $destinationSqlServer
mountDriveAsUser $destinationSa $destinationSaPassword $secondAvailableDriveLetter $destinationUncPath
$destinationFolder="$secondAvailableDriveLetter`:\"
#if(!(test-path $destinationFolder)){mkdir $destinationFolder}
#$exportDirectory=$(split-path $backupFile -parent)
#$fileName=$(split-path $backupFile -leaf)
#$fileSize=(Get-Item $backupFile).length/1GB
#$destinationFile="$importDirectory\$fileName"
#if(get-item $destinationFile -ea SilentlyContinue){rm $destinationFile -Force}
# Give permissions to current user toward import directory, in case it's not already present
#[string]$currentAccount=$(whoami)
#$Acl = Get-Acl $destinationFolder
#$addPermission = New-Object System.Security.AccessControl.FileSystemAccessRule("$currentAccount", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")
#$Acl.SetAccessRule($addPermission)
#Set-Acl $destinationFolder $Acl
#write-host (get-acl $destinationFolder|fl|out-string)
# Copy backup file to destination
$sourceBackupFile="$sourceFolder\$databaseName.bak"
$sanityPassed=(test-path $sourceFolder) -and (test-path $destinationFolder) -and (test-path $sourceBackupFile)
if ($sanityPassed){
# rm $destinationFolder\*.* #Purge
$fileSize=(get-item $sourceBackupFile).Length/1GB
$transferTime=(measure-command {robocopy "$sourceFolder" "$destinationFolder" "$databaseName.bak"}).TotalHours
$transferSpeed=[math]::round($fileSize/$transferTime,2)
#write-host "$([math]::round($fileSize,2)) GB was copied in $([math]::round($transferTime,2)) hours => Speed: $transferSpeed GB/Hour"
}
# Rename the file to reflect its creation date stamp
#$pacificTime=[System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId( (Get-Date), 'Pacific Standard Time')
#$dateStamp = $pacificTime.tostring("MM-dd-yyyy-HHmm")
#Rename-Item -Path "$destinationFolder\$databaseName.bak" -NewName "$databaseName`_$dateStamp_PST.bak"
$dataFile = ($destinationDefaults[0]+ "\$databaseName.mdf") -replace "\\{2}",'\'
$logFile = ($destinationDefaults[1]+ "\$databaseName.ldf") -replace "\\{2}",'\'
$destinationBackupFile="$($destinationDefaults[2])\$databaseName.bak"
function triggerSqlImport{
param(
$sqlServer,
$databaseName,
$dataFile,
$logFile,
$dbData='mscrm',
$dbLog='mscrm_log',
$saCred,
$backupFile,
$overWrite
)
$session=new-pssession $sqlServer -Credential $saCred
$dbImportResult=invoke-command -Session $session {
param($databaseName,$backupFile,$dataFile,$logFile,$dbData,$dbLog,$overwrite)
# Ensure the the Jump Box has SQL PowerShell tools
$moduleName='sqlps'
if(!(Get-Module -ListAvailable -Name $moduleName -ea SilentlyContinue)){
if(!('NuGet' -in (get-packageprovider).Name)){
try{
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction SilentlyContinue;
}
catch{
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NetFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value '1' -Type DWord
Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\.NetFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value '1' -Type DWord
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction SilentlyContinue;
}
}
Install-Module -Name $moduleName -Force -Confirm:$false
}
import-module $moduleName
# Trigger backup of target database into the default backup directory
#$relocateData = New-Object Microsoft.SqlServer.Management.Smo.RelocateFile($dbData, "$dataFile")
#$relocateLog = New-Object Microsoft.SqlServer.Management.Smo.RelocateFile($dbLog, "$logFile")
#Restore-SqlDatabase -ServerInstance $sqlServer -Database $databaseName -BackupFile $backupFile -RelocateFile @($relocateData,$relocateLog)
$sqlRestoreOverWrite = @"
USE [master]
-- IF DB_ID('$databaseName') IS NULL
-- BEGIN
RESTORE DATABASE [$databaseName] FROM DISK = N'$backupFile' WITH FILE = 1,
MOVE N'$dbData' TO N'$dataFile',
MOVE N'$dbLog' TO N'$logFile',
NOUNLOAD, REPLACE, STATS = 5
ALTER DATABASE [$databaseName]
SET MULTI_USER
-- END
"@
$dbExists=(invoke-sqlcmd "SELECT CASE WHEN DB_ID('$databaseName') IS NULL THEN CAST(0 AS BIT) ELSE CAST(1 AS BIT) END").Column1
if(!$dbExists){
write-host "$databaseName is now being added to $env:computername:`r`n$sqlRestoreOverWrite"
invoke-sqlcmd $sqlRestoreOverWrite
}
elseif($overwrite){
write-host "$databaseName exists and over-write flag is True. Executing...`r`n$sqlRestoreOverWrite"
invoke-sqlcmd $sqlRestoreOverWrite
}
else{
write-host "$databaseName currently Exists and over-write flag has been set as False"
}
# Validation
CD SQLSERVER:\SQL\$env:computername\DEFAULT
$currentDb=Get-SqlDatabase -Name $databaseName|select Name,RecoveryModel,CompatibilityLevel,CreateDate,DataSpaceUsage,LastBackupDate,Owner,PrimaryFilePath
return ($currentDb|out-string).Trim()
} -Args $databaseName,$backupFile,$dataFile,$logFile,$dbData,$dbLog,$overwrite
Remove-PSSession $session
return $dbImportResult
}
$importClock= [System.Diagnostics.Stopwatch]::StartNew()
$importResult=triggerSqlImport $destinationSqlServer $databaseName $dataFile $logFile $dbData $dbLog $destinationSaCred $destinationBackupFile $overwrite
#write-host $importResult
$importHours=$importClock.Elapsed.TotalHours;
$importClock.Stop();
# Cleanup Routine
write-host "Cleaning up backup files at $sourceSqlServer"
remove-item -path $sourceBackupFile -force
net use /delete $($firstAvailableDriveLetter+':') 2>&1>null
net use /delete $($secondAvailableDriveLetter+':') 2>&1>null
# Total duration
$hoursElapsed=$stopWatch.Elapsed.TotalHours;
$stopWatch.Stop();
$shippingResult="Overall stats`r
-----------------------`r
$importResult`r
Source SQL Server`t: $sourceSqlServer
Destination SQL Server`t: $destinationSqlServer
Database size`t: $([math]::round($fileSize,2))`r
Backup duration (hour)`t: $([math]::round($backupDuration,2))`r
File transfer duration`t: $([math]::round($transferTime,2))`r
DB import duration`t: $([math]::round($importHours,2))`r
Total DB shipping duration`t: $([math]::round($hoursElapsed,2))`r
Aggregate speed`t: $([math]::round($hoursElapsed/$fileSize,2)) GB/Hour`r
-----------------------"
#write-host $shippingResult
return $shippingResult
}
shipDatabase $databaseName $dbData $dbLog $overwriteFlag $serverAdminCredential `
$sourceSqlServer $sourceSa $sourceSaPassword `
$destinationSqlServer $destinationSa $destinationSaPassword `
$logFile