Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
7894d4e
Add Veeam S3 bucket inventory script with NinjaOne WYSIWYG output
Mar 23, 2026
0853c8f
Fix function ordering and HTML encoding for S3 bucket inventory
Mar 23, 2026
df1e882
Use $env: variables directly instead of local variable copies
Mar 23, 2026
46aaa9b
Remove explicit Connect-VBRServer, use implicit local connection
Mar 23, 2026
0c40c1b
Rewrite S3 inventory data collection based on Veeam 12 diagnostics
Mar 23, 2026
fb7c706
Use copy job TargetRepository for bucket name, child backups for size
Mar 23, 2026
cf41846
Add multi-field output: bucket name, size, orphan detection, HTML tables
Mar 23, 2026
4d7d4a7
Switch orphan detection to time-based + unlinked job approach
Mar 23, 2026
91aa674
Expand orphan detection to ALL backups, not just S3
Mar 23, 2026
ac1e57b
Rename orphan env vars to drop S3 prefix since they cover all repos
Mar 23, 2026
b886f13
Rename script to veeam-inventory.ps1
Mar 23, 2026
c25407d
Add failed backup detection with checkbox and WYSIWYG fields
Mar 23, 2026
4a1064f
Only flag failed backups if the most recent run failed
Mar 23, 2026
75d8049
Add B2 bucket audit script to compare against active bucket list
Mar 23, 2026
b75970c
Fix B2 API: use v4 endpoint and ASCII encoding
Mar 23, 2026
990e025
Add bucket sizes from B2 daily usage reports, fix auth fallback
Mar 23, 2026
4e1f462
Add veeam-create-s3-repo.ps1 with scoped B2 key generation
Mar 23, 2026
384a440
Require ORG_UUID (fail if empty), rename GUID -> UUID throughout
Mar 23, 2026
493428b
Default immutability to 14 days
Mar 23, 2026
6153e11
ORG_UUID reads from NinjaOne custom field (CUSTOM_FIELD_ORG_UUID)
Mar 23, 2026
046ec06
Fix B2 auth: use -Credential instead of manual Basic header
Mar 23, 2026
3e04637
Read ORG_UUID from org-level NinjaOne field via Ninja-Property-Get
Mar 23, 2026
cac752f
Fix B2 API header validation and simplify org UUID read
Mar 23, 2026
8931a08
Add missing accountId to b2_update_bucket retention call
Mar 23, 2026
f49340e
Validate ORG_UUID is actually a UUID before proceeding
Mar 23, 2026
a0cd47f
Read org UUID via Ninja-Property-Get using field name from env var
Mar 23, 2026
276a081
Add idempotency, disable versioning, key propagation delay
Mar 23, 2026
44e4a6c
Save B2 credentials to NinjaOne immediately after creation
Mar 23, 2026
e219791
Add listAllBucketNames capability to scoped key
Mar 23, 2026
6410780
Add lifecycle rule to purge hidden versions after immutability + 1 day
Mar 23, 2026
eade4dc
Fix Veeam repo creation: remove bucket-level retention, fix DLL path
Mar 23, 2026
001e859
Fix NonInteractive prompt error in Veeam repo creation
Mar 23, 2026
71a5577
Enable SSE-B2 (AES256) server-side encryption on all buckets
Mar 23, 2026
0e5be31
Remove -NonInteractive from PS7 bootstrap
Mar 23, 2026
00c23d9
Redact secrets from log output
Mar 23, 2026
e9d56d5
Pre-trust B2 SSL certificate to prevent Veeam interactive prompt
Mar 23, 2026
de3bed6
Fix hanging: add -Confirm:\$false and force -EnableBucketAutoProvisio…
Mar 23, 2026
d00abf6
Remove -Confirm param, not supported on this Veeam cmdlet
Mar 23, 2026
920c840
Add interactive diagnostic for S3 repo creation
Mar 23, 2026
78c418b
Add script to clear old Veeam management server from endpoints
Mar 23, 2026
11c86df
Add S3 backup copy job configuration script
Mar 23, 2026
3296463
Read retention from org-level Cloud RPO custom field
Mar 23, 2026
2a16e29
Detect missing or incomplete S3 backup copy job
Mar 23, 2026
75e9fe2
Add backup window: Mon-Sat 10 PM - 5 AM, Sunday all day
Mar 23, 2026
79b5987
Rename Cloud RPO to Cloud Retention
Mar 23, 2026
f973ebf
Add Saturday to all-day backup window
Mar 23, 2026
f64ff31
Add local backup schedule configuration script
Mar 23, 2026
be37ce5
Rename to veeam-configure-local-backup-jobs.ps1
Mar 23, 2026
4d60aab
Adjust local backup window to 6 AM - 9 PM
Mar 23, 2026
01d98ad
Fix PS7.4+ SQLite conflict, rename to veeam-configure-backblaze-repo
Mar 23, 2026
ea28af2
Use assembly resolve handler for SQLite conflict instead of preload
Mar 23, 2026
c5b7bc3
Add Veeam native DLL path to fix SQLite type initializer failure
Mar 23, 2026
d0ce94b
Add debug logging to B2 credential check
Mar 23, 2026
118c5f1
Force 64-bit PS7 in bootstrap across all Veeam scripts
Mar 23, 2026
1775f3c
Upgrade SQLite fix to full PATH + assembly resolve on all scripts
Mar 23, 2026
646a828
Fix null assembly name error and truncate copy job name to 50 chars
Mar 23, 2026
c7487a4
Fix RetentionType: use RestoreDays not Days
Mar 23, 2026
2ecc61a
Use New-VBRBackupWindowOptions to create proper window object
Mar 23, 2026
2e9bdcc
Fix backup window: create job first, apply window after
Mar 23, 2026
6f5a602
Add step logging and ConfirmPreference before job creation
Mar 23, 2026
6175573
Add required -Mode Periodic parameter to Add-VBRBackupCopyJob
Mar 23, 2026
3b8b54a
Switch to Immediate mode for backup window support
Mar 23, 2026
9476c46
Switch to Periodic mode with daily 10 PM schedule
Mar 23, 2026
9f12619
Create without schedule for now, add diag to discover schedule params
Mar 23, 2026
a07ef50
Set periodic schedule after job creation using correct cmdlets
Mar 23, 2026
09212a6
Add encryption from source backup key, remove schedule restrictions
Mar 23, 2026
44f768e
Add backup window schedule and encryption to S3 copy job
Mar 23, 2026
88bc753
Fix encryption: use Get-VBRJob options instead of StorageOptions cmdlet
Mar 23, 2026
86174f7
Fix encryption: use New-VBRBackupCopyJobStorageOptions properly
Mar 23, 2026
6cb5ef0
Pass all required params to New-VBRBackupCopyJobStorageOptions
Mar 23, 2026
afe37df
Fix StorageOptions: use correct mandatory enum values
Mar 23, 2026
46b066a
Use StorageOptimizationType Automatic for copy jobs
Mar 23, 2026
b8ed700
Apply all settings (schedule, encryption, enable) on every run
Mar 23, 2026
2a0ba93
Rename existing copy job to match naming pattern if needed
Mar 23, 2026
657fce7
Replace hardcoded Veeam install paths with $env:ProgramFiles
Mar 23, 2026
f46dc74
Replace all hardcoded Program Files paths with $env:ProgramFiles
Mar 23, 2026
c69c990
Add comprehensive README for bdr-veeam scripts
Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
448 changes: 0 additions & 448 deletions bdr-veeam/Find-VeeamS3OrphanedBackupData.ps1

This file was deleted.

205 changes: 205 additions & 0 deletions bdr-veeam/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# BDR Veeam Scripts

Scripts for managing Veeam Backup & Replication servers deployed by DTC via NinjaRMM.

## Core Automation Scripts (New)

These scripts form the automated BDR provisioning and monitoring pipeline. They are designed to run from NinjaRMM with org-level and device-level custom fields.

### veeam-configure-backblaze-repo.ps1
Creates a Backblaze B2 bucket and registers it as a Veeam S3-compatible repository.

**What it does:**
1. Reads org UUID from NinjaRMM org-level field
2. Creates a B2 bucket: `{org_uuid_nodashes}-{time_short_id}-veeam`
3. Enables Object Lock (for immutability) and SSE-B2 encryption
4. Sets lifecycle rule to purge hidden file versions after immutability + 1 day
5. Creates a scoped B2 application key restricted to that bucket only
6. Saves bucket name + scoped key to NinjaRMM device fields (before Veeam step)
7. Registers the Veeam S3 repository with immutability enabled

**Idempotency:** If bucket name + keys already exist in NinjaRMM, skips B2 creation and only creates the Veeam repo. Safe to re-run after failures.

**NinjaRMM Fields:**

| Variable | Level | Type | Purpose |
|----------|-------|------|---------|
| `CUSTOM_FIELD_ORG_UUID` | Org | Text | Organization UUID (read via Ninja-Property-Get) |
| `B2_ADMIN_KEY_ID` | Org | Text | Master B2 key ID (never stored on device) |
| `B2_ADMIN_APP_KEY` | Org | Secure | Master B2 app key |
| `B2_ENDPOINT` | Org | Text | e.g. `https://s3.us-west-002.backblazeb2.com` |
| `B2_REGION` | Org | Text | e.g. `us-west-002` |
| `IMMUTABILITY_DAYS` | Script | Text | Default: 14 |
| `CUSTOM_FIELD_S3_BUCKET_NAME` | Device | Text | Output: bucket name |
| `CUSTOM_FIELD_S3_KEY_ID` | Device | Secure | Output: scoped key ID |
| `CUSTOM_FIELD_S3_APP_KEY` | Device | Secure | Output: scoped app key |

### veeam-configure-s3-copy-job.ps1
Creates or updates the S3 backup copy job. Ensures all backup jobs are linked as sources.

**What it does:**
1. Finds the S3 repository (from NinjaRMM field or auto-detect)
2. Collects all source backup jobs (Get-VBRJob + Get-VBRComputerBackupJob, excludes copy jobs)
3. If a copy job targeting the S3 repo exists: adds missing sources, renames to match pattern
4. If none exists: creates one in Periodic mode
5. Applies schedule, encryption, and enables the job (every run, not just creation)

**Schedule:** Mon-Fri 10 PM - 5 AM, Sat-Sun all day

**NinjaRMM Fields:**

| Variable | Level | Type | Purpose |
|----------|-------|------|---------|
| `CUSTOM_FIELD_S3_BUCKET_NAME` | Device | Text | Used to find the S3 repo |
| `CUSTOM_FIELD_CLOUD_RETENTION` | Org | Text | Retention days (default: 30) |

### veeam-configure-local-backup-jobs.ps1
Configures backup windows on all local backup jobs.

**Schedule:** Mon-Fri 6 AM - 9 PM, Sat-Sun disabled

This complements the S3 copy job schedule. 1-hour buffer (9 PM - 10 PM) between local backups stopping and S3 copy starting.

### veeam-inventory.ps1
Comprehensive inventory and health check. Populates multiple NinjaRMM fields.

**What it detects:**
- S3 bucket name and storage size (via Veeam backup copy job + child backup storages)
- Orphaned/stale backups across all repos (configurable threshold, default 30 days)
- Failed backup jobs (checks most recent session per job, clears when job succeeds)
- Missing S3 copy job (no S3 repo, no copy job, or missing source jobs)

**NinjaRMM Fields:**

| Variable | Type | Purpose |
|----------|------|---------|
| `CUSTOM_FIELD_S3_BUCKET_NAME` | Text | Last used S3 bucket name |
| `CUSTOM_FIELD_S3_BUCKET_SIZE` | Text | Last used S3 bucket size |
| `CUSTOM_FIELD_S3_INVENTORY` | WYSIWYG | HTML table of all S3 repos |
| `CUSTOM_FIELD_ORPHANS_FOUND` | Checkbox | 1 if orphaned backups exist |
| `CUSTOM_FIELD_ORPHANED_BACKUPS` | WYSIWYG | HTML table of orphaned backups |
| `CUSTOM_FIELD_FAILED_BACKUP` | Checkbox | 1 if any job's last run failed |
| `CUSTOM_FIELD_FAILED_BACKUPS` | WYSIWYG | HTML table of failed jobs |
| `CUSTOM_FIELD_S3_COPY_MISSING` | Checkbox | 1 if S3 copy job missing/incomplete |
| `ORPHAN_DAYS_THRESHOLD` | Config | Days before stale (default: 30) |

### veeam-clear-management-server.ps1
Removes old Veeam management server registration from endpoints. Run on the endpoint (not the server).

**Use when:** Migrating endpoints to a new Veeam B&R server and getting "host is managed by another backup server" error.

## Backblaze B2 Scripts (iaas-backblaze/)

### b2-bucket-audit.ps1
Lists all B2 buckets in the account, compares against a known-good list, flags unused buckets in red.

### b2-fix-lifecycle.ps1
Applies lifecycle rules to existing B2 buckets to purge hidden file versions. Fixes storage bloat from B2 keeping all versions forever.

## Technical Reference

### PS7 Bootstrap
All scripts that use the Veeam PowerShell module include a PS7 bootstrap. Veeam 12.x requires PowerShell 7+.

```powershell
if ($PSVersionTable.PSVersion.Major -lt 7) {
$PWSH_PATH = "$env:ProgramFiles\PowerShell\7\pwsh.exe"
if (-not (Test-Path $PWSH_PATH)) {
$PWSH_CANDIDATE = Get-Command pwsh.exe -ErrorAction SilentlyContinue
$PWSH_PATH = if ($PWSH_CANDIDATE) { $PWSH_CANDIDATE.Source } else { $null }
}
# ...
}
```

**Critical:** Always prefer 64-bit PS7 (`$env:ProgramFiles`, not `$env:ProgramFiles(x86)`). Veeam's native SQLite DLL is x64 only. Using 32-bit PS7 causes `SqliteConnection` type initializer failures.

### PS7.4+ SQLite Conflict
PowerShell 7.4+ ships `Microsoft.Data.Sqlite` that conflicts with Veeam's bundled version. The fix has two parts:

1. **Add Veeam's native DLL path to PATH** so `e_sqlite3.dll` is found:
```powershell
$VEEAM_RUNTIMES = "$env:ProgramFiles\Veeam\Backup and Replication\Backup\runtimes\win-x64\native"
$env:PATH = "$VEEAM_RUNTIMES;$env:PATH"
```

2. **Register an AssemblyResolve handler** to redirect managed DLL loads to Veeam's versions:
```powershell
$null = [System.AppDomain]::CurrentDomain.add_AssemblyResolve({
param($sender, $args)
if (-not $args.Name) { return $null } # Null check required
$ASSEMBLY_NAME = [System.Reflection.AssemblyName]::new($args.Name)
$VEEAM_DLL = Join-Path $VEEAM_BACKUP_DIR "$($ASSEMBLY_NAME.Name).dll"
if (Test-Path $VEEAM_DLL) { return [System.Reflection.Assembly]::LoadFrom($VEEAM_DLL) }
return $null
})
```

**Important:** The null check on `$args.Name` is required. .NET passes empty assembly names which crash the handler without it.

### Veeam PowerShell Gotchas (Veeam 12.x)

**Non-interactive execution:**
- Do NOT use `-NonInteractive` in the PS7 bootstrap. Veeam cmdlets use `Read-Host` internally for certificate trust prompts, which hard-fails in NonInteractive mode (hangs instead of erroring).
- Set `$ConfirmPreference = 'None'` before Veeam cmdlet calls.
- `-Confirm:$false` is NOT a valid parameter on most Veeam cmdlets (they don't implement `ShouldProcess`).

Comment on lines +142 to +146

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistency: -NonInteractive used in veeam-inventory.ps1 despite warning.

Line 143 warns "Do NOT use -NonInteractive in the PS7 bootstrap" but veeam-inventory.ps1 line 29 includes -NonInteractive in the PS7 bootstrap args. Either the warning should be updated to clarify when it's safe to use, or the inventory script should be corrected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bdr-veeam/README.md` around lines 142 - 146, The README warns against using
-NonInteractive in the PS7 bootstrap but veeam-inventory.ps1 still passes
-NonInteractive in its PS7 bootstrap args; fix the inconsistency by removing
-NonInteractive from the PS7 bootstrap invocation in veeam-inventory.ps1 (search
for the PS7 bootstrap args array/Invoke-Command or Start-Process call that
includes "-NonInteractive") and ensure the script instead sets
$ConfirmPreference = 'None' before calling Veeam cmdlets (and does not append
-Confirm:$false to those cmdlet calls); alternatively, if there's a valid reason
to keep -NonInteractive, update the README text to document the specific
safe-to-use case and reference the exact bootstrap invocation in
veeam-inventory.ps1.

**Add-VBRAmazonS3CompatibleRepository:**
- `-EnableBucketAutoProvision:$false` is required. Default changed to `$true` in Veeam 12.3.1 and hangs on non-AWS S3 endpoints.
- `-EnableBackupImmutability` is required when the B2 bucket has Object Lock enabled. Without it: "Enable backup immutability because the S3 Object Lock feature is enabled."
- Do NOT set bucket-level `defaultRetention` via B2 API. Veeam manages immutability per-object. Bucket default causes "default retention is not supported" errors.

**Add-VBRBackupCopyJob:**
- `-Mode` is a required parameter (values: `Periodic`, `Immediate`). Script hangs if not provided.
- Periodic mode: uses `ScheduleOptions`, does NOT support `Anytime` or `BackupWindowOptions`.
- Immediate mode: supports `Anytime` and `BackupWindowOptions`.
- Job name max length: 50 characters.

**New-VBRBackupCopyJobStorageOptions:**
- `CompressionLevel` and `StorageOptimizationType` are MANDATORY. Hangs if not provided.
- CompressionLevel values: `Auto`, `None`, `DedupeFriendly`, `Optimal`, `High`, `Extreme`
- StorageOptimizationType values: `Automatic`, `LocalTarget`, `LocalTargetHugeBackup`, `LANTarget`, `WANTarget`
- Use `Automatic` for copy jobs (not `LocalTarget` which errors on "image-level backup copy job").

**Get-VBRObjectStorageRepository:**
- Returns skeleton objects with NULL sub-properties (ArchiveRepository, AmazonCompatibleOptions, Options, Info all null).
- To get bucket name: use `Get-VBRBackupCopyJob` -> `.TargetRepository.AmazonCompatibleOptions.BucketName` instead.

**GetObjects() on agent backup jobs:**
- Returns 0 for Windows Agent Backup/Policy types. Only works for VM backups.
- For agent backups, use child backup names or time-based detection instead.

**Backup size calculation:**
- Parent backups with `IsTruePerVmContainer = true` have 0 storages.
- Must call `FindChildBackups()` and sum `GetAllStorages()` -> `Stats.BackupSize` across children.

**RetentionType enum:**
- Values are `RestoreDays` and `RestorePoints` (not `Days` or `Cycles`).

### Backblaze B2 API Reference

**Authentication:**
- Use `-Authentication Basic -Credential $PSCredential` (not manual header construction). B2 auth tokens contain `=` characters that `Invoke-RestMethod` header validation rejects.
- For post-auth API calls, use `-SkipHeaderValidation $true` in the headers.
- API v2 is the most reliable. v4 works but v3 does not exist.

**Bucket creation:**
- `fileLockEnabled: true` enables Object Lock (required for Veeam immutability). Cannot be added after creation.
- Object Lock requires versioning (automatic). Cannot disable versioning on Object Lock buckets.
- Set lifecycle rule `daysFromHidingToDeleting` to purge old hidden versions. Without this, B2 keeps ALL versions forever (causes massive storage bloat, e.g. 11 TB active = 50 TB in B2).

**Scoped application keys:**
- Must include `listAllBucketNames` capability for bucket-restricted keys. Without it, Veeam gets "Invalid credentials" because S3 ListBuckets fails.
- Full capability list for Veeam: `listBuckets`, `listAllBucketNames`, `readBuckets`, `listFiles`, `readFiles`, `writeFiles`, `deleteFiles`, `readBucketEncryption`, `writeBucketEncryption`, `readBucketRetentions`, `writeBucketRetentions`, `readFileRetentions`, `writeFileRetentions`, `readFileLegalHolds`, `writeFileLegalHolds`, `bypassGovernance`

**Daily usage reports:**
- No real-time bucket size API exists.
- B2 generates daily audit CSVs in `b2-reports-{accountId}` bucket with `storageByteCount` per bucket.

### NinjaRMM Integration

**Org-level fields:** Read with `Ninja-Property-Get $env:FIELD_NAME`. The env var contains the field API name (e.g. `dtcOrgGuid`), not the value.

**Device-level fields:** Write with `Ninja-Property-Set $fieldName $value`. Read with `Ninja-Property-Get $fieldName`.

**Important:** `Ninja-Property-Get -Organization` does NOT work from scripts. Org-level fields must be configured to inherit to device level in NinjaRMM, then read normally.
112 changes: 0 additions & 112 deletions bdr-veeam/veeam-add-backup-copy-job.ps1

This file was deleted.

Loading