<# .SYNOPSIS Upgrades Veeam Environment to v12.1 .DESCRIPTION This script will upgrade Veeam Backup Enterprise Manager and/or Veeam Backup & Replication Server depending on what's installed. The script is designed to be executed on the server to be upgraded. It's also possible to execute the script from a remote PowerShell session. .PARAMETER ISO Location of Veeam ISO containing upgrade files. If not specified, script will attempt to download the ISO from Veeam's public servers. .PARAMETER License Veeam License key file .PARAMETER ServicePassword Password for the account under which the Veeam Backup Service will run (only required if LocalSystem account is not used) .PARAMETER SqlAuthentication Authentication mode to connect to the database server. Possible options: Windows, Native .PARAMETER SqlPassword Password to connect to the SQL server (only required when using 'Native' authentication mode) .PARAMETER LicenseAutoupdate Boolean to determine if you want to update license automatically .PARAMETER AutoUpgrade Boolean to automatically upgrade existing components in the backup infrastructure .OUTPUTS Update-Veeam.ps1 returns exit code of 0 upon success .EXAMPLE Update-Veeam.ps1 -License "C:\license.lic" Description ----------- Upgrades Veeam environment using the specified license file and downloads the Veeam 12 ISO from Veeam's public servers .EXAMPLE Update-Veeam.ps1 -ISO "C:\VeeamBackup&Replication_12.1.1.56_20240127.iso" -License "C:\license.lic" Description ----------- Upgrades Veeam environment using the specified license file and uses the specified local ISO .NOTES NAME: Update-Veeam.ps1 VERSION: 1.0 AUTHOR: Chris Arceneaux TWITTER: @chris_arceneaux GITHUB: https://github.com/carceneaux .LINK https://helpcenter.veeam.com/docs/backup/vsphere/upgrade_vbr_answer_file.html?ver=120 .LINK https://helpcenter.veeam.com/docs/backup/em/em_silent_upgrade.html?ver=120 .LINK https://github.com/VeeamHub/powershell/blob/master/BR-UpgradeV12.1/Update-Veeam.ps1 Original script by Chris Arceneaux #> #Requires -RunAsAdministrator [CmdletBinding(DefaultParametersetName = "None")] param( [Parameter(Mandatory = $false)] [String] $ISO = "download", [Parameter(Mandatory = $false)] [String] $License, [Parameter(Mandatory = $false)] [String] $ServicePassword, [Parameter(Mandatory = $true, ParameterSetName = "SqlAuth")] [ValidateSet("Windows", "Native")] [String] $SqlAuthentication = "Windows", [Parameter(Mandatory = $true, ParameterSetName = "SqlAuth")] [String] $SqlPassword, [Parameter(Mandatory = $false)] [bool] $LicenseAutoupdate = $true, [Parameter(Mandatory = $false)] [bool] $AutoUpgrade = $true ) # Settting download URL $downloadURL = "https://download2.veeam.com/VDPP/v12/VeeamDataPlatform_23H2_20240928.iso" $isoLabel = "VEEAM DATA PLATFORM PREMIUM" # Setting Log Location $logFolder = $env:SYSTEMDRIVE + "\temp\veeam-upgrade" New-Item -ItemType Directory -Force -Path $logFolder | Out-Null #makes sure folder exists $logFile = "$logFolder\upgrade.log" Clear-Content $logFile -ErrorAction SilentlyContinue Function Get-Software { # Sourced from https://mcpmag.com/articles/2017/07/27/gathering-installed-software-using-powershell.aspx [OutputType('System.Software.Inventory')] [Cmdletbinding()] Param( [Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] [String[]]$Computername = $env:COMPUTERNAME ) Begin { } Process { ForEach ($Computer in $Computername) { If (Test-Connection -ComputerName $Computer -Count 1 -Quiet) { $Paths = @("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "SOFTWARE\\Wow6432node\\Microsoft\\Windows\\CurrentVersion\\Uninstall") ForEach ($Path in $Paths) { Write-Verbose "Checking Path: $Path" # Create an instance of the Registry Object and open the HKLM base key Try { $reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine', $Computer, 'Registry64') } Catch { Write-Error $_ Continue } # Drill down into the Uninstall key using the OpenSubKey Method Try { $regkey = $reg.OpenSubKey($Path) # Retrieve an array of string that contain all the subkey names $subkeys = $regkey.GetSubKeyNames() # Open each Subkey and use GetValue Method to return the required values for each ForEach ($key in $subkeys) { Write-Verbose "Key: $Key" $thisKey = $Path + "\\" + $key Try { $thisSubKey = $reg.OpenSubKey($thisKey) # Prevent Objects with empty DisplayName $DisplayName = $thisSubKey.getValue("DisplayName") If ($DisplayName -AND $DisplayName -notmatch '^Update for|rollup|^Security Update|^Service Pack|^HotFix') { $Date = $thisSubKey.GetValue('InstallDate') If ($Date) { Try { $Date = [datetime]::ParseExact($Date, 'yyyyMMdd', $Null) } Catch { Write-Warning "$($Computer): $_ <$($Date)>" $Date = $Null } } # Create New Object with empty Properties $Publisher = Try { $thisSubKey.GetValue('Publisher').Trim() } Catch { $thisSubKey.GetValue('Publisher') } $Version = Try { #Some weirdness with trailing [char]0 on some strings $thisSubKey.GetValue('DisplayVersion').TrimEnd(([char[]](32, 0))) } Catch { $thisSubKey.GetValue('DisplayVersion') } $UninstallString = Try { $thisSubKey.GetValue('UninstallString').Trim() } Catch { $thisSubKey.GetValue('UninstallString') } $InstallLocation = Try { $thisSubKey.GetValue('InstallLocation').Trim() } Catch { $thisSubKey.GetValue('InstallLocation') } $InstallSource = Try { $thisSubKey.GetValue('InstallSource').Trim() } Catch { $thisSubKey.GetValue('InstallSource') } $HelpLink = Try { $thisSubKey.GetValue('HelpLink').Trim() } Catch { $thisSubKey.GetValue('HelpLink') } $Object = [pscustomobject]@{ Computername = $Computer DisplayName = $DisplayName Version = $Version InstallDate = $Date Publisher = $Publisher UninstallString = $UninstallString InstallLocation = $InstallLocation InstallSource = $InstallSource HelpLink = $HelpLink EstimatedSizeMB = [decimal]([math]::Round(($thisSubKey.GetValue('EstimatedSize') * 1024) / 1MB, 2)) } $Object.pstypenames.insert(0, 'System.Software.Inventory') Write-Output $Object } } Catch { Write-Warning "$Key : $_" } } } Catch { } $reg.Close() } } Else { Write-Error "$($Computer): unable to reach remote system!" } } } } Function Test-PendingReboot { # Sourced from https://www.powershellgallery.com/packages/PendingReboot/0.9.0.6 [CmdletBinding()] param( [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias("CN", "Computer")] [String] $ComputerName = $env:COMPUTERNAME ) process { try { $invokeWmiMethodParameters = @{ Namespace = 'root/default' Class = 'StdRegProv' Name = 'EnumKey' ComputerName = $ComputerName ErrorAction = 'Stop' } $hklm = [UInt32] "0x80000002" ## Query the Component Based Servicing Reg Key $invokeWmiMethodParameters.ArgumentList = @($hklm, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\') $registryComponentBasedServicing = (Invoke-WmiMethod @invokeWmiMethodParameters).sNames -contains 'RebootPending' ## Query WUAU from the registry $invokeWmiMethodParameters.ArgumentList = @($hklm, 'SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\') $registryWindowsUpdateAutoUpdate = (Invoke-WmiMethod @invokeWmiMethodParameters).sNames -contains 'RebootRequired' ## Query JoinDomain key from the registry - These keys are present if pending a reboot from a domain join operation $invokeWmiMethodParameters.ArgumentList = @($hklm, 'SYSTEM\CurrentControlSet\Services\Netlogon') $registryNetlogon = (Invoke-WmiMethod @invokeWmiMethodParameters).sNames $pendingDomainJoin = ($registryNetlogon -contains 'JoinDomain') -or ($registryNetlogon -contains 'AvoidSpnSet') ## Query ComputerName and ActiveComputerName from the registry and setting the MethodName to GetMultiStringValue $invokeWmiMethodParameters.Name = 'GetMultiStringValue' $invokeWmiMethodParameters.ArgumentList = @($hklm, 'SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName\', 'ComputerName') $registryActiveComputerName = Invoke-WmiMethod @invokeWmiMethodParameters $invokeWmiMethodParameters.ArgumentList = @($hklm, 'SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName\', 'ComputerName') $registryComputerName = Invoke-WmiMethod @invokeWmiMethodParameters $pendingComputerRename = $registryActiveComputerName -ne $registryComputerName -or $pendingDomainJoin ## Query PendingFileRenameOperations from the registry $invokeWmiMethodParameters.ArgumentList = @($hklm, 'SYSTEM\CurrentControlSet\Control\Session Manager\', 'PendingFileRenameOperations') $registryPendingFileRenameOperations = (Invoke-WmiMethod @invokeWmiMethodParameters).sValue $registryPendingFileRenameOperationsBool = [bool]$registryPendingFileRenameOperations $isRebootPending = $registryComponentBasedServicing -or ` $pendingComputerRename -or ` $pendingDomainJoin -or ` $registryPendingFileRenameOperationsBool -or ` $systemCenterConfigManager -or ` $registryWindowsUpdateAutoUpdate return $isRebootPending } catch { Write-Warning "$Computer`: $_" } } } Function Write-Log { Param ([string]$logString) $logEntry = "$('[{0:MM/dd/yyyy} {0:HH:mm:ss}]' -f (Get-Date)) $logString" Write-Output $logEntry Write-Output $logEntry | Out-file $logFile -Append } Function Update-Veeam { Param( [String]$SilentInstallExe, [String]$Answer, [String]$Logs ) $params = @( "/AnswerFile" '"{0}"' -f $Answer "/SkipNetworkLogonErrors" "/LogFolder" '"{0}"' -f $Logs ) return (Start-Process "$SilentInstallExe" -Wait -ArgumentList $params -Passthru).ExitCode } Write-Log "INFO: Upgrade logs for this script can be found here: $logFile" # Pending reboot check if (Test-PendingReboot) { throw "This Windows server requires a reboot prior to beginning the Veeam Backup & Replication upgrade. After rebooting this server, you can proceed with the upgrade." } # Enforcing absolute paths if ($iso -ne "download") { $iso = Resolve-Path $iso } if ($license) { $license = Resolve-Path $license } # Determining installed software $vbem = Get-Software | Where-Object { $_.DisplayName -eq "Veeam Backup Enterprise Manager" } | Select-Object DisplayName, Version $vbr = Get-Software | Where-Object { $_.DisplayName -eq "Veeam Backup & Replication Server" } | Select-Object DisplayName, Version if ((-not $vbem) -and (-not $vbr)) { throw "At least 1 Veeam product must be installed on this server: Veeam Backup Enterprise Manager or Veeam Backup & Replication Server" } if ($vbem) { Write-Log "Veeam Backup Enterprise Manager found: $($vbem.Version)" } if ($vbr) { Write-Log "Veeam Backup & Replication Server found: $($vbr.Version)" } # Checking for license if Enteprise Manager is installed if ($vbem -and ($license.Length -eq 0)) { throw "The License parameter MUST be used when upgrading Veeam Backup Enterprise Manager. Please correct and re-run this script." } # If ISO wasn't specified, download it from Veeam's public servers if ($iso -eq "download") { try { $iso = "$logFolder\$(($downloadURL -split "/")[-1])" Write-Log "ISO not specified. Checking if previously downloaded..." if (Test-Path $iso) { Write-Log "ISO found: $iso" } else { Write-Log "ISO not found. Downloading ISO now..." Start-BitsTransfer -Source $downloadURL -Destination $iso Write-Log "ISO downloaded to: $iso" } } catch { Write-Log $_ throw "ISO download failed. Please check upgrade log for more information: $logFile" } } # Mounting ISO try { Write-Log "Mounting ISO in Operating System" Mount-DiskImage -ImagePath $iso | Out-Null $mountDrive = (Get-Volume | Where-Object { $_.FileSystemLabel -like $isoLabel })[0].DriveLetter + ":" if ($mountDrive -eq ":") { throw "Unable to mount ISO. Please check if the ISO is mounted and has valid lable: $isoLabel" } } catch { Write-Log $_ throw "ISO mount failed. Please check upgrade log for more information: $logFile" } # Validating ISO try { # Identifying Silent Install EXE $file = Get-ChildItem -Recurse -Filter "Veeam.Silent.Install.exe" -File -Path $mountDrive $exe = $file.FullName Write-Log "ISO validated, Silent Install EXE found: $exe" } catch { Write-Log $_ Write-Log "Unable to validate ISO. Please investigate and resolve. Logs can be found here: $logFile" Write-Log "Unmounting Veeam ISO" Dismount-DiskImage -ImagePath $iso throw "Incorrect ISO detected! This script was designed to work with a Veeam Backup & Replication ISO. Please correct and re-run this script." } ### VBR PRE-UPGRADE ACTIONS if ($vbr) { try { # Registering VeeamPSSnapin if necessary Write-Log "Registering VeeamPSSnapin if necessary" if (-Not (Get-Module -ListAvailable -Name Veeam.Backup.PowerShell)) { Add-PSSnapin -PassThru VeeamPSSnapIn -ErrorAction Stop | Out-Null } try { # Checking for Cloud Connect environment $state = Get-VBRCloudInfrastructureState # returns error not Cloud Connect Write-Log "Cloud Connect instance found. Determining infrastructure state..." # Pre-upgrade actions for Cloud Connect environment $vcc = $true if ($state -eq "Active") { Write-Log "ACTIVE: Enabling maintenance mode and waiting for all active sessions to complete" Enable-VBRCloudMaintenanceMode } else { Write-Log "MAINTENANCE: Maintenance mode already enabled. Currently waiting for all active sessions to complete" } $sw = [System.Diagnostics.Stopwatch]::StartNew() #stopwatch # Forever loop until Cloud Connect active sessions complete or manual user interrupt while ($true) { $sessions = ([Veeam.Backup.Core.CCloudSession]::GetAll() | Where-Object { $_.JobName -ne "Console" } | Where-Object { $_.State -eq "Working" }).Count if ($sessions -eq 0) { Write-Log "All active sessions have gracefully ended. Total time waiting: $([int]$sw.Elapsed.TotalMinutes) minutes" $sw.Stop() break } Clear-Host Write-Host "Still waiting for $sessions active sessions to complete after $([int]$sw.Elapsed.TotalMinutes) minutes..." Write-Host "To interrupt the wait and forcefully close active sessions, press (I)" # Allows manual escape from loop if ($host.UI.RawUI.KeyAvailable) { $key = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyUp,IncludeKeyDown") if ("i" -like $key.Character) { Write-Log "Manual interrupt received, proceeding with upgrade. $sessions active sessions will be forcefully closed." $sw.Stop() break } } Start-Sleep -Seconds 5 } # Performing Configuration Backup prior to upgrade Write-Log "Performing Configuration Backup prior to upgrade" Start-VBRConfigurationBackupJob } catch { # Pre-upgrade actions for Veeam Backup & Replication environment (no Cloud Connect) $vcc = $false # Performing Configuration Backup prior to upgrade Write-Log "Performing Configuration Backup prior to upgrade" Start-VBRConfigurationBackupJob } } catch { Write-Log "One of the pre-upgrade actions failed. Please investigate and resolve. Logs can be found here: $logFile" Write-Log "Unmounting Veeam ISO" Dismount-DiskImage -ImagePath $iso throw "ERROR: Upgrade halted. Please check logs for more information." } } ### END VBR PRE-UPGRADE ACTIONS ### VBEM UPGRADE if ($vbem) { # Closing open Console sessions Write-Log "Closing open Console sessions" Stop-Process -Name "Veeam.Backup.Shell" -Force -ErrorAction SilentlyContinue # Stopping all Veeam services prior to upgrade Write-Log "Stopping all Veeam services" Get-Service veeam* | Stop-Service try { # Generate upgrade answer file $answerFile = "$logFolder\EmAnswerFile_upgrade.xml" Write-Log "Checking if Enterprise Manager answer file already exists..." if (Test-Path $answerFile) { Write-Log "Answer file: $answerFile" } else { Write-Log "Answer file not found. Generating file now..." Add-Content $answerFile @" $(if ($LicenseAutoupdate){ '' } else { '' }) $(if ($ServicePassword){ ' "@ } } catch { Write-Log $_ Write-Log "Answer file generation failed. Please investigate and resolve. Logs can be found here: $logFile" Write-Log "Unmounting Veeam ISO" Dismount-DiskImage -ImagePath $iso throw "ERROR: Answer file generation failed. Please check logs for more information." } try { # Upgrading Veeam Backup Enterprise Manager Write-Log "Upgrading Veeam Backup Enterprise Manager using answer file: $answerFile" $result = Update-Veeam -SilentInstallExe $exe -Answer $answerFile -Logs $logFolder if ($result -eq 0 -or $result -eq 3010 -or $result -eq 3011) { Write-Log "SUCCESS: ${result}" } else { throw "ERROR: ${result}" } } catch { Write-Log $_ Write-Log "Unmounting Veeam ISO" Dismount-DiskImage -ImagePath $iso throw "Upgrade failed. Please check debug log for more information: $logFolder\EnterpriseManager.log" } Write-Log "Veeam Backup Enterprise Manager has been successfully upgraded" } ### END VBEM UPGRADE ### VBR UPGRADE if ($vbr) { # Closing open Console sessions Write-Log "Closing open Console sessions" Stop-Process -Name "Veeam.Backup.Shell" -Force -ErrorAction SilentlyContinue # Stopping all Veeam services prior to upgrade Write-Log "Stopping all Veeam services" Get-Service veeam* | Stop-Service try { # Generate upgrade answer file $answerFile = "$logFolder\VbrAnswerFile_upgrade.xml" Write-Log "Checking if Veeam Backup & Replication answer file already exists..." if (Test-Path $answerFile) { Write-Log "Answer file: $answerFile" } else { Write-Log "Answer file not found. Generating file now..." Add-Content $answerFile @" $(if ($License){ '' }) $(if ($LicenseAutoupdate){ '' } else { '' }) $(if ($ServicePassword){ ' "@ } } catch { Write-Log $_ Write-Log "Answer file generation failed. Please investigate and resolve. Logs can be found here: $logFile" Write-Log "Unmounting Veeam ISO" Dismount-DiskImage -ImagePath $iso throw "ERROR: Answer file generation failed. Please check logs for more information." } try { # Upgrading Veeam Backup & Replication Server Write-Log "Upgrading Veeam Backup & Replication Server using answer file: $answerFile" $result = Update-Veeam -SilentInstallExe $exe -Answer $answerFile -Logs $logFolder if ($result -eq 0 -or $result -eq 3010 -or $result -eq 3011) { Write-Log "SUCCESS: ${result}" } else { throw "ERROR: ${result}" } } catch { Write-Log $_ Write-Log "Unmounting Veeam ISO" Dismount-DiskImage -ImagePath $iso throw "Upgrade failed. Please check debug log for more information: $logFolder\BackupServer.log" } Write-Log "Veeam Backup & Replication has been successfully upgraded" } ### END VBR UPGRADE ### VBR POST-UPGRADE ACTIONS if ($vbr) { try { if ($vcc) { Write-Log "Disabling Cloud Connect Maintenance Mode" powershell.exe -NoLogo -ExecutionPolicy bypass -NoProfile -Command "Import-Module 'C:\Program Files\Veeam\Backup and Replication\Console\Veeam.Backup.PowerShell\Veeam.Backup.PowerShell.psd1'; Disable-VBRCloudMaintenanceMode" } Write-Log "Shutting down Veeam prior to reboot. This may take a while as all Veeam Proxies & Repositories are currently being upgraded." Get-Service veeam* | Where-Object {$_.Name -ne "VeeamBackupSvc"} | Stop-Service Get-Service veeam* | Stop-Service } catch { Write-Log $_ Write-Log "One of the post-upgrade actions failed. Please investigate and resolve. Logs can be found here: $logFile" Write-Log "Unmounting Veeam ISO" Dismount-DiskImage -ImagePath $iso throw "ERROR: Post-upgrade actions failed. Please check logs for more information." } } ### END VBR POST-UPGRADE ACTIONS Write-Log "Unmounting Veeam ISO" Dismount-DiskImage -ImagePath $iso Write-Log "Script has completed successfully. Please reboot this server prior to using Veeam." Write-Host "This can be done easily in PowerShell as well: Restart-Computer -Force" return 0