Skip to content

Instantly share code, notes, and snippets.

@MacsInSpace
Created May 20, 2026 02:29
Show Gist options
  • Select an option

  • Save MacsInSpace/c48725b3d093419ef510de0b9cc2ec03 to your computer and use it in GitHub Desktop.

Select an option

Save MacsInSpace/c48725b3d093419ef510de0b9cc2ec03 to your computer and use it in GitHub Desktop.
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Silently installs Veeam Backup & Replication Community Edition (free).
.DESCRIPTION
Resolves the latest VBR ISO URL by:
1. Scraping the Veeam R&D forum sticky (static HTML, no JS, Veeam-maintained):
https://forums.veeam.com/veeam-backup-replication-f2/current-version-t9456.html
This gives us the current version string e.g. "13.0.1.2067"
2. Probing the Veeam CDN with HEAD requests to find the date-stamped filename:
https://download2.veeam.com/VBR/v{major}/VeeamBackup&Replication_{ver}_{date}.iso
Scans backwards from today. Veeam releases a few times a year so this
typically resolves in under 10 probes.
Falls back to -IsoUrl or -ExistingIsoPath if auto-resolution isn't needed.
.PARAMETER IsoUrl
Skip auto-resolution and use this URL directly.
.PARAMETER DownloadDir
Where to save the ISO. Defaults to $env:TEMP. Deleted after install unless -KeepIso.
.PARAMETER ExistingIsoPath
Path to an already-downloaded ISO. Skips download entirely.
Useful when deploying multiple servers from a file share.
.PARAMETER LicenceFile
Path to a .lic file. Omit for Community (free) edition (10-workload limit).
.PARAMETER KeepIso
Keep the downloaded ISO after installation (default: delete it).
.EXAMPLE
.\Install-VeeamVBR.ps1
.EXAMPLE
.\Install-VeeamVBR.ps1 -ExistingIsoPath '\\fileserver\iso\veeam.iso'
.EXAMPLE
.\Install-VeeamVBR.ps1 -IsoUrl 'https://download2.veeam.com/VBR/v13/VeeamBackup&Replication_13.0.1.2067_20260312.iso'
#>
[CmdletBinding()]
param (
[string] $IsoUrl,
[string] $DownloadDir = $env:TEMP,
[string] $ExistingIsoPath,
[string] $LicenceFile = '',
[switch] $KeepIso
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
function Write-Step ([string]$Msg) { Write-Host "[*] $Msg" -ForegroundColor Cyan }
function Write-OK ([string]$Msg) { Write-Host "[+] $Msg" -ForegroundColor Green }
function Write-Warn ([string]$Msg) { Write-Host "[!] $Msg" -ForegroundColor Yellow }
# ── Step 1: Scrape the Veeam forum sticky for the current version ─────────────
# URL: https://forums.veeam.com/veeam-backup-replication-f2/current-version-t9456.html
# This is a locked, Veeam-maintained sticky — static HTML, no login required.
# The page title and first post contain the build number in the format: "13.0.1.2067"
function Get-CurrentVeeamVersion {
$stickyUrl = 'https://forums.veeam.com/veeam-backup-replication-f2/current-version-t9456.html'
Write-Step "Fetching current version from Veeam forum sticky..."
try {
$html = (Invoke-WebRequest -Uri $stickyUrl -UseBasicParsing -TimeoutSec 20).Content
}
catch {
throw "Could not reach Veeam forums: $_"
}
# Match version pattern: major.minor.patch.build e.g. 13.0.1.2067
# The page title is: "[ALL VERSIONS] Current build is 13.0.1.2067 (March 13, 2026)"
$match = [regex]::Match($html, 'Current build is (\d+\.\d+\.\d+\.\d+)')
if (-not $match.Success) {
# Fallback: grab any semver-style version from the page title area
$match = [regex]::Match($html, '<title>[^<]*?(\d{2,}\.\d+\.\d+\.\d+)')
}
if (-not $match.Success) {
throw ("Could not parse version from forum sticky. " +
"Check manually: $stickyUrl")
}
$version = $match.Groups[1].Value
Write-OK "Current VBR version: $version"
return $version
}
# ── Step 2: Probe CDN for the date-stamped ISO filename ──────────────────────
# URL pattern: https://download2.veeam.com/VBR/v{major}/VeeamBackup&Replication_{ver}_{yyyyMMdd}.iso
# The major version is the first segment of the version string.
# We scan backwards from today since we don't know the release date.
function Get-VeeamIsoUrl ([string]$Version) {
$major = ($Version -split '\.')[0]
$baseUrl = "https://download2.veeam.com/VBR/v$major/VeeamBackup%26Replication_${Version}_"
Write-Step "Probing CDN for ISO date stamp (version $Version)..."
$today = [DateTime]::Today
for ($i = 0; $i -le 400; $i++) {
$date = $today.AddDays(-$i).ToString('yyyyMMdd')
$candidate = "${baseUrl}${date}.iso"
try {
$req = [System.Net.HttpWebRequest]::Create($candidate)
$req.Method = 'HEAD'
$req.Timeout = 2000
$resp = $req.GetResponse()
$sizeGb = [math]::Round($resp.ContentLength / 1GB, 1)
$resp.Close()
Write-OK "Found ISO: $candidate (${sizeGb} GB)"
# Return URL with literal & for the downloader (not %26)
return $candidate -replace '%26', '&'
}
catch [System.Net.WebException] {
$code = [int]$_.Exception.Response.StatusCode
if ($code -in @(403, 404)) { continue }
Write-Warn "Unexpected HTTP $code on date $date — continuing"
}
catch { continue }
}
throw ("Could not locate ISO for VBR $Version on the Veeam CDN after scanning 400 days. " +
"Try passing -IsoUrl manually.")
}
# ── Main ──────────────────────────────────────────────────────────────────────
$mountResult = $null
$isoPath = $null
$deleteIso = $false
try {
# 1. Resolve ISO source ───────────────────────────────────────────────────
if ($ExistingIsoPath -and (Test-Path $ExistingIsoPath)) {
Write-OK "Using existing ISO: $ExistingIsoPath"
$isoPath = $ExistingIsoPath
} else {
$url = if ($IsoUrl) {
Write-OK "Using supplied URL: $IsoUrl"
$IsoUrl
} else {
$version = Get-CurrentVeeamVersion
Get-VeeamIsoUrl $version
}
$isoFilename = ($url -split '/')[-1]
$isoPath = Join-Path $DownloadDir $isoFilename
if (Test-Path $isoPath) {
Write-OK "ISO already present at $isoPath — skipping download."
} else {
Write-Step "Downloading ISO to: $isoPath"
Write-Step "This will take a while (~10 GB)..."
Start-BitsTransfer -Source $url -Destination $isoPath -DisplayName "Veeam VBR ISO" -Description $isoFilename
$deleteIso = $true
Write-OK "Download complete."
}
}
# 2. Mount ISO ────────────────────────────────────────────────────────────
Write-Step "Mounting ISO..."
$mountResult = Mount-DiskImage -ImagePath $isoPath -PassThru
$drive = ($mountResult | Get-Volume).DriveLetter + ':'
Write-OK "Mounted at $drive"
# 3. Find setup.exe ───────────────────────────────────────────────────────
$setupExe = Join-Path $drive 'Setup.exe'
if (-not (Test-Path $setupExe)) {
throw "Setup.exe not found at $setupExe — ISO may not have mounted correctly."
}
# 4. Build install args ───────────────────────────────────────────────────
#
# /silent Unattended — no GUI
# /accepteula Accept Veeam EULA
# /acceptthirdpartylicenses Accept bundled third-party EULAs
# /noreboot Suppress automatic reboot (handle yourself)
# /licfile:"path" Paid licence — omit for Community/free edition
$installArgs = '/silent /accepteula /acceptthirdpartylicenses /noreboot'
if ($LicenceFile -and (Test-Path $LicenceFile)) {
$installArgs += " /licfile:`"$LicenceFile`""
Write-Step "Licence file: $LicenceFile"
} else {
Write-Step "No licence file — installing Community (free) edition."
}
# 5. Run installer ────────────────────────────────────────────────────────
Write-Step "Starting installer — typically takes 10-20 minutes..."
Write-Host " $setupExe $installArgs" -ForegroundColor DarkGray
$proc = Start-Process -FilePath $setupExe -ArgumentList $installArgs -Wait -PassThru
switch ($proc.ExitCode) {
0 { Write-OK "Installation completed successfully." }
3010 { Write-OK "Installation completed. ** REBOOT REQUIRED before Veeam will function. **" }
default {
throw "Installer exited with code $($proc.ExitCode). Check logs at: C:\ProgramData\Veeam\Setup\Temp\"
}
}
} finally {
# 6. Cleanup ──────────────────────────────────────────────────────────────
if ($mountResult -and $isoPath) {
Write-Step "Dismounting ISO..."
Dismount-DiskImage -ImagePath $isoPath -ErrorAction SilentlyContinue | Out-Null
Write-OK "ISO dismounted."
}
if ($deleteIso -and -not $KeepIso -and $isoPath -and (Test-Path $isoPath)) {
Write-Step "Removing downloaded ISO..."
Remove-Item $isoPath -Force -ErrorAction SilentlyContinue
Write-OK "ISO removed."
}
}
Write-OK "Done."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment