Created
May 20, 2026 02:29
-
-
Save MacsInSpace/c48725b3d093419ef510de0b9cc2ec03 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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