Veeam v13 Fleet Upgrade — Runbook & Milestone Tracker
Book: Backup Operations | Audience: T3 / Code Commander (T2 with supervision) Platform: Veeam Backup & Replication 12.x → 13.0.2.29 on DTC BDR appliances Ticket: HALO 1179664 (project tracking) — engineering history under HALO 1146283 Author: Z. Boogher | Status: Active project
Purpose
End-to-end procedure for taking a DTC BDR appliance from any 12.x/13.0.1.x build to 13.0.2.29, proving the upgrade did no harm, applying DTC's scheduling policy, and confirming the site is clean afterwards.
Any T3 can pick up any device mid-sequence. Find the device on the tracker, see which milestone reads PENDING, and follow that section below.
No field work is required for any milestone. Every step is remote. The only reason to dispatch is hardware failure — a dead disk, a BDR that will not power on, or a box that cannot be brought back online remotely.
The two scripts
| Script | Repo path | NinjaOne | Purpose |
|---|---|---|---|
| Veeam v13 Upgrade | msp-script-library/bdr-veeam/Invoke-VeeamV13Upgrade.ps1 |
Script 203 | M1 + M2 |
| Veeam Schedule Window (Standard) | msp-script-library/bdr-veeam/Set-VeeamScheduleWindow.ps1 |
— | M3 |
Both Run As SYSTEM. Both work on 12.x and 13.x unmodified.
READ THE LOG, NOT THE BADGE
Both scripts use exit code 2 for expected mid-states — hop complete awaiting reboot, offsite copy-watch holding, a preflight gate halt, changes pending in report mode. NinjaOne renders exit 2 as FAILURE.
A red result is very often a device progressing normally. Always open the activity log and read the text.
| Exit | Meaning |
|---|---|
| 0 | Converged / clean |
| 2 | Expected mid-state, or a validation/gate/copy condition needing attention |
| 1 | Genuine script failure |
Milestone model
A device is only COMPLETE at M7. Being on build 13.0.2.29 is M1 — roughly one seventh of the work.
M1 — Upgraded
Installed build is exactly 13.0.2.29.
Run script 203. It is convergent and idempotent: every run resolves to VALIDATE, OFFSITE COPY WATCH, UPGRADE, or NOTHING based on what it finds. Safe to re-run at any time.
Hop logic (Veeam KB4763):
- at or above 13.0.2.29 → converged, no-op
- at or above 12.3.1.1139 → single direct hop to v13
- below 12.3.1.1139 → hop via the 12.3.2 ISO first
Both hops chain in one run; the script reboots itself between them.
Proof: log line Post-upgrade build: 13.0.2.29, or live NinjaOne inventory.
M2 — Validated
The upgrade was proven non-destructive on that specific device. Jobs, repositories, object storage, S3 credentials, copy jobs and restore points are diffed against a baseline captured immediately before the hop, and validation fails closed.
Automatic on the run after the hop reboot. Nothing to do by hand.
Validation cannot be done retroactively. It is a diff against a pre-hop baseline. A device that was already at 13.0.2.29 before this project can never earn M2 — mark it N/A and rely on M4 instead.
Restore-point checks tolerate one missed hourly cycle during the reboot. A newest point moving backwards beyond 26 h, or a count drop beyond 10%, still fails and holds the device.
Proof: log line VALIDATION PASSED.
M3 — Schedule Applied
DTC scheduling policy written to the jobs so a local backup cannot hold a restore-point lock while the offsite copy is reading it.
| Job type | Policy |
|---|---|
| Server / image (periodic) | permitted 06:00–20:59, Mon–Sat |
| Workstation (daily) | start 21:00, Mon–Fri |
| Cloud / S3 copy | left at its 22:00 daily start |
Run intervals are preserved — an hourly job stays hourly at the same offset. The script verifies this by reading Kind / FullPeriod / HourlyOffset back after each write and failing the job if any moved.
Procedure:
mode=report(default) — read-only. Confirm what it intends to change.mode=apply+applyChanges=1+serverDays=Mon,Tue,Wed,Thu,Fri,Sat- Confirm in the console.
Proof: APPLIED ... interval ... (unchanged) for each job, and the console Time Periods grid showing green Mon–Sat 06:00–20:59 with Sunday empty.
Rollback: the script saves original state to C:\ProgramData\DTC\Logs\VeeamSchedule\schedule-original_<timestamp>.json before the first write and logs the exact undo command.
M4 — Jobs Verified
The jobs actually run inside the new window and still succeed. This is the milestone that proves the schedule change did no harm.
Wait one full cycle after M3, then confirm:
- no server session outside 06:00–20:59
- the workstation job fired at 21:00
- the offsite copy fired at 22:00 and succeeded
A copy job that was already failing before the upgrade is a pre-existing fault. Flag it; do not count it against M4.
M5 — Agents Current
Veeam Agents do not update themselves. A site can be fully on v13 with every endpoint still running a 6.x agent. This is the most commonly missed milestone.
Console → Inventory → Protection Group → Agent Version column. Upgrade from the console, or via Install-VBRDiscoveredComputerAgent.
Endpoints reporting RebootRequired=True cannot finish the agent upgrade until that workstation reboots. Reboot it, then retry — otherwise the attempt simply times out.
Script 203 also upgrades any agent the v13 installer names as a blocker (event id=106), so some sites clear M5 as a side effect of M1.
Proof: no agent below 7.x remains, or every remaining one is recorded with a reason.
M6 — Cleanup Done
Stale and orphaned backup data removed: offsite restore points for machines that no longer exist, disabled or abandoned jobs, registrations for decommissioned endpoints.
List offsite restore points per VM and compare against the live job configuration. Anything with no matching job is orphaned.
Confirm with the client or AM before deleting any backup data. A stale restore point can also mean a broken agent or a disabled backup account rather than a dead machine — both have been seen on this fleet.
Removing an agent registration does not remove its backup files; those stay on the repository and remain restorable.
M7 — Complete
M1–M6 all DONE. Roll-up only.
Known failure modes
Every one of these was found on a live device during the fleet waves. The scripts handle them; this section is for reading logs and for anything done by hand.
Installer
The v13 setup engine refuses LocalSystem (event id=103). Start-Process -Credential cannot fix it — that yields a UAC-filtered token and fails identically. The installer must run via a one-shot Scheduled Task as the LAPS local admin at RunLevel Highest, and the task account must be COMPUTERNAME\user (a .\user form fails SID resolution).
A rejected credential fails at launch, silently. The task registers, Windows rejects the logon, the task never enters Running, and LastTaskResult stays 0 from a prior state — which reads as a successful install that never happened. Look for Security event 4625, substatus 0xC000006A (bad password). Cause is almost always a stale lapsPassword custom field. The current script validates the credential before the download and fails with a named reason.
Exit 3010 with an unchanged build is not a failure. It means a prerequisite was installed and needs a reboot (event id=012, Microsoft Visual C++ redistributable). Reboot and re-run; the hop completes on the next pass.
Exit 1603 with the build at target is a partial success. An ancillary component failed while the core product upgraded fine. Check the component logs; the reboot usually clears it.
The patch engine cannot run from read-only media — running setup from a mounted ISO fails with "The media is write protected". Setup must be copied to a writable local folder first.
Service handling
Never force-kill VeeamBackupSvc. taskkill wedged a BDR badly enough that the service would not restart and orphaned workers held port 9392 for hours. Only a reboot cleared it. Graceful stop only.
The installer's 5-minute stop budget is the core problem. Setup restarts VeeamBackupSvc to analyse the config DB, and each service start spawns a batch of maintenance jobs (STARTRESYNC, STARTDBMAINTENANCE, STARTCATCLEANUP and others). The service cannot complete a stop while those run; setup gives up at 300 s with event id=113 and rolls back. Pre-stopping does not help — the blocking jobs are created after setup restarts the service. The script runs a watchdog during the install window that terminates those workers mid-install.
A service stuck in StopPending never resolves on its own and will halt every subsequent run. Reboot clears it.
MSSQL boot race: VeeamBackupSvc has no SQL dependency in SCM, so at boot it can start before MSSQL is ready, fail, exhaust its retries and stay down along with its dependents.
PowerShell
Veeam PowerShell v13 requires PowerShell 7 — v13 is built on .NET Core. NinjaOne invokes scripts under Windows PowerShell 5.1, so on a v13 box every Veeam cmdlet fails at Import-Module.
The v12 module is the mirror image: .NET Framework, and it fails under pwsh with "The type initializer for Veeam.Backup.Common.SSslOptions threw an exception".
Working around it by hand:
# on a v13 box
& pwsh -NoProfile -Command {
Import-Module Veeam.Backup.PowerShell -DisableNameChecking -WarningAction SilentlyContinue
Get-VBRJob -WarningAction SilentlyContinue | Select-Object Name, JobType
}
PowerShell 7 is a DTC build standard and is present on the BDR fleet. The scripts select the runtime per call from the installed build.
Console
The v13 console must be launched AS ADMINISTRATOR or it fails with "Access to the registry key ...\Plugins is denied". The key grants Administrators, and a non-elevated process carries that as a deny-only SID under UAC.
Disk
Setup needs roughly 29.3 GB free on C: regardless of where the ISO is staged. Several fleet BDRs have 109–117 GB system drives beside multi-TB repositories, so the script stages the ISO on the largest volume but still gates C: separately.
Veeam's own log tree is usually the culprit when C: is short. One fleet device held 58 GB across 12,522 files in C:\ProgramData\Veeam\Backup dating back to 2024. The folder is hidden, so a plain directory scan misses it. The script prunes it before the space gate, reading the path from Veeam's LogDirectory value and abandoning the prune entirely if it resolves inside a repository path.
Configuration backup
Not a gate, deliberately. A large share of the fleet has a broken configuration-backup job — six devices share a single dangling repository GUID inherited from a provisioning template. It is not repairable by cmdlet: all four *ConfigurationBackup* cmdlets were enumerated on both 12.2 and 13.0, neither exposes a settings-level setter, and the GUID is stored binary so it appears nowhere as text in the config DB. Only the console dialog writes the configuration fresh.
Halting those upgrades protected a rollback point the devices did not have. Repair is a separate workstream.
Scheduler XML format (reference)
Not documented by Veeam. Reverse-engineered on a live device; recorded here because any future schedule automation depends on it.
The permitted-hours window for a periodic job is an XML string at OptionsPeriodically.Schedule:
<scheduler><Sunday>1,1,1,...</Sunday><Monday>1,1,1,1,1,1,0,0,...,0,1,1,1</Monday>...</scheduler>
Seven day elements, 24 comma-separated values each, hour 0–23.
POLARITY IS INVERTED:
0= PERMITTED,1= DENIED. Proven against live session data — a job whose XML carried1at hours 0–4 and 20–23 had observed start hours of 5 through 21, and its all-1Sunday row had never produced a Sunday run.
Other gotchas:
OptionsBackupWindowis not the control.IsEnabledwas False andBackupWindowa zero-length string on every job tested. Do not use it.FullPeriodis in seconds despiteKindreading"Hours". A job atFullPeriod 3600/HourlyOffset 15runs at :15 every hour.- The daily day-set property is
DaysSrv, notDayOfWeek, and setting it requiresKind = 'SelectedDays'. Read it withGetDays(). - A daily job has one start time and no window. Only periodic jobs have a Time Periods grid.
Copy-job periodic conversion — do not use
Converting a SimpleBackupCopyPolicy from Daily to Periodic works via PowerShell and reads back correctly, but the Veeam console cannot render the result — opening the job throws "Sequence contains no matching element".
The capability is retained in the script behind convertCopyToPeriodic, defaulted off. Reverting the schedule clears the console error completely.
A daily 22:00 copy start already runs uninterrupted into Monday morning, which was the substantive goal.
Reference devices
The process is proven end to end on real hardware, not in theory.
DTCBSURE-4557 — the reference device for the full sequence. Upgraded 12.2.0.334 → 12.3.2.4465 → 13.0.2.29 and passed validation against baseline (4 jobs, 3 repositories, 1 object storage repo, 5 S3 credentials, 3 backup objects, no restore-point regression). Schedule policy applied 2026-09-01 with both jobs verified by independent read-back and the hourly interval preserved. Jobs confirmed running and successful in the new window. M1–M4 complete.
Every installer failure mode in this page was found and fixed on this device.
DTCBSURE-GODWIN — the reference device for the schedule work. The scheduler XML format and its inverted polarity were reverse-engineered here, and the copy-job console limitation was found here. M3 applied and verified.
Tracker
Per-device milestone tracking lives on the workbook attached to HALO 1179664:
- Milestones — definitions and completion criteria (mirrors this page)
- Fleet Status — one row per BDR, M1–M7 per device
- Summary — progress by milestone, build and track
- Blockers — the open worklist with owners
- Change Log — what was done, when, by whom
Related documents
- Veeam Backup and Replication Standards (id 1004)
- Veeam BDR Deployment SOP (id 1096)
- Veeam Backup Daily Operations & Verification SOP (id 1093)
- Veeam Troubleshooting Playbook (id 1115)
- Veeam & BDR Troubleshooting Guide (id 1946)
- BDR Storage Alerts & Capacity Issues (id 1947)
- Agent & Endpoint Offline (id 1948)
- Veeam Console Connection & Permission Errors (id 1953)
- Adding & Replacing Computers in Veeam BDR (id 1951)
Change record
| Date | Change | By |
|---|---|---|
| 2026-09-01 | Page created. Runbook, milestone model M1–M7, scheduler XML format, known failure modes. | Z. Boogher |