Skip to content

Veeam S3 Inventory, Backblaze Repo Automation, Copy Job Config#46

Open
Gumbees wants to merge 77 commits intomainfrom
enhancement/veeam-s3-bucket-inventory
Open

Veeam S3 Inventory, Backblaze Repo Automation, Copy Job Config#46
Gumbees wants to merge 77 commits intomainfrom
enhancement/veeam-s3-bucket-inventory

Conversation

@Gumbees
Copy link
Contributor

@Gumbees Gumbees commented Mar 23, 2026

Summary

  • veeam-configure-backblaze-repo.ps1 - Automated B2 bucket creation with scoped keys, Object Lock, SSE-B2 encryption, lifecycle rules, and Veeam S3 repo registration. Saves credentials to NinjaRMM device fields.
  • veeam-configure-s3-copy-job.ps1 - Creates/updates a single S3 backup copy job with all backup sources linked. Applies schedule (Mon-Fri 10 PM-5 AM, Sat-Sun all day), encryption from existing key, and enables the job. Idempotent... re-running enforces all settings.
  • veeam-configure-local-backup-jobs.ps1 - Sets backup windows on local jobs (Mon-Fri 6 AM-9 PM, Sat-Sun disabled) to complement the S3 copy schedule.
  • veeam-inventory.ps1 - Comprehensive health check: S3 bucket name/size, orphaned backups (all repos, time-based), failed job detection (last-run-only), missing S3 copy job detection. Writes to NinjaRMM checkbox + WYSIWYG fields.
  • veeam-clear-management-server.ps1 - Removes old Veeam management server registration from endpoints for server migrations.
  • b2-bucket-audit.ps1 - Lists all B2 buckets, compares against active list, flags unused in red. Pulls sizes from daily usage reports.
  • b2-fix-lifecycle.ps1 - Applies lifecycle rules to existing B2 buckets to fix storage bloat from hidden version accumulation.
  • README.md - Full technical reference covering PS7 bootstrap, SQLite conflict fix, Veeam cmdlet gotchas, B2 API patterns, NinjaRMM field integration.

Key Technical Fixes

  • PS7 64-bit enforcement (32-bit PS7 crashes Veeam's x64 SQLite)
  • PS7.4+ SQLite conflict resolution (PATH + AssemblyResolve handler)
  • Non-interactive execution (removed -NonInteractive, ConfirmPreference=None)
  • B2 auth token header validation bypass (-SkipHeaderValidation)
  • Scoped B2 keys with listAllBucketNames for Veeam compatibility
  • B2 lifecycle rules to prevent version storage bloat

Cleanup

  • Removed stale duplicate: veeam/veeam-add-backup-repo.ps1
  • Removed old orphan finder, diag scripts, and legacy copy job script
  • Renamed scripts to consistent veeam-configure-* pattern

Test plan

  • Tested veeam-configure-backblaze-repo on live BDR (bucket creation, scoped key, Veeam repo)
  • Tested veeam-configure-s3-copy-job on live BDR (job creation, schedule, source linking)
  • Tested veeam-inventory on live BDR (bucket name, size, orphan detection, failed job detection)
  • Tested b2-fix-lifecycle on existing buckets
  • Test veeam-configure-local-backup-jobs
  • Test b2-bucket-audit with active bucket list
  • Test encryption on S3 copy job (StorageOptimizationType: Automatic)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Veeam/Backblaze B2 integration scripts for S3 repository provisioning and configuration.
    • Automated backup job scheduling and S3 copy job management.
    • Health monitoring and backup inventory tracking capabilities.
    • Backblaze B2 bucket auditing and lifecycle management tools.
    • Diagnostics and troubleshooting utilities.
  • Documentation

    • Comprehensive automation suite reference guide.

Gumbees and others added 30 commits March 23, 2026 09:56
Queries all S3-compatible object storage repositories from Veeam B&R,
collects bucket names and storage usage, and writes an HTML table to a
NinjaOne WYSIWYG custom field. Includes PS7 bootstrap for Veeam 12.x
module compatibility and dual interactive/RMM execution modes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move helper functions (Format-SizeBytes, Build-HtmlInventoryTable,
ConvertTo-SafeHtml) above script logic so they're defined before use.
Replace System.Web.HttpUtility dependency with lightweight
ConvertTo-SafeHtml function for PS7 compatibility. Use SCREAMING_SNAKE_CASE
for VALID_INPUT. Clean up comment header to match repo convention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
RMM injects variables as environment variables. Read them directly
via $env:DESCRIPTION and $env:CUSTOM_FIELD_S3_INVENTORY throughout
the script instead of copying to local variables. Interactive mode
now writes to $env: too for consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Veeam module auto-connects when running on the local BDR server.
Connect-VBRServer hits the Identity service which fails in PS7
under SYSTEM context. Other Veeam scripts in the repo use the
same implicit connection pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bucket name: drill into ArchiveRepository, AmazonCompatibleOptions,
Options, Info sub-objects, and copy job TargetRepository. Direct
top-level properties are null on Veeam 12 skeleton objects.

Size: sum GetAllStorages() on backup objects per RepositoryId as
fallback when repo-level UsedSpace is unavailable.

Drop Total Capacity column (S3 buckets have no fixed capacity).

Update diag script to test all 5 extraction paths so we can
validate which properties actually return data on live BDR servers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bucket name: Get-VBRObjectStorageRepository returns skeleton objects
with null sub-objects on Veeam 12. But Get-VBRBackupCopyJob's
TargetRepository has populated AmazonCompatibleOptions.BucketName.
Use copy job path as primary source.

Size: Parent backups with IsTruePerVmContainer=true have 0 storages.
Enumerate FindChildBackups() per-VM children and sum their
GetAllStorages() entries for actual size data.

Simplified the inventory script by removing dead paths that returned
null on real Veeam 12 servers. Updated diag to test child backup
size extraction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NinjaOne custom fields:
- S3 bucket name (text): last used bucket name for device group columns
- S3 bucket size (text): last used bucket size for device group columns
- S3 orphans found (integer): 1/0 flag for alerting
- S3 inventory (WYSIWYG): full HTML table of all S3 repos
- S3 orphans (WYSIWYG): HTML table of orphaned backup data

Orphan detection: compares child backup machine names in S3 repos
against active machines in source backup jobs linked to copy jobs.
Child backups for machines no longer in any active source job are
flagged as orphaned.

Refactored HTML generation to a generic Build-HtmlTable function
and NinjaOne writes to a Set-NinjaField helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GetObjects() returns 0 on Windows Agent backup types, so the
active machine name matching approach failed (everything looked
orphaned). Two detection methods now:

1. Unlinked: S3 backup data whose parent job ID doesn't match
   any active copy job targeting an S3 repo
2. Stale: child backups whose last storage date exceeds the
   ORPHAN_DAYS_THRESHOLD (default 30 days)

Added $env:ORPHAN_DAYS_THRESHOLD for per-client tuning.
Orphan table now shows Reason column (No active job / Stale X days).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Orphan detection now scans every backup across all repositories
(local, S3, etc.) for stale or unlinked entries. S3-specific
fields (bucket name, size, inventory table) remain S3-only.

Active job detection uses Get-VBRJob + Get-VBRBackupCopyJob +
Get-VBRComputerBackupJob to build a complete set of active job
IDs. Backups whose JobId doesn't match any active job = unlinked.

Orphan table column changed from "Bucket" to "Repository" since
entries can now come from any repo type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CUSTOM_FIELD_S3_ORPHANS_FOUND -> CUSTOM_FIELD_ORPHANS_FOUND
CUSTOM_FIELD_S3_ORPHANS -> CUSTOM_FIELD_ORPHANED_BACKUPS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers S3 bucket details, storage sizing, and orphan detection
across all repository types - not just S3 bucket inventory anymore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Queries Get-VBRBackupSession for completed sessions with Failed
or Warning result within FAILED_BACKUP_HOURS (default 24).

New NinjaOne fields:
- CUSTOM_FIELD_FAILED_BACKUP: checkbox (1/0) for device group alerts
- CUSTOM_FIELD_FAILED_BACKUPS: WYSIWYG HTML table with job name,
  result, end time, and duration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Groups all sessions by JobId, keeps only the latest per job.
If a job failed but then re-ran successfully, it's no longer
flagged. The checkbox clears automatically on the next script
run after the job succeeds.

Removed FAILED_BACKUP_HOURS config since we check last run
status regardless of time window.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Standalone script in iaas-backblaze/ that queries the Backblaze B2
API for all buckets in the account, compares against a known-good
list of active buckets (from ACTIVE_BUCKETS_CSV env var - accepts
comma-separated names or a file path), and generates an HTML table
with unused buckets highlighted in red and sorted to the top.

Outputs to CUSTOM_FIELD_B2_AUDIT WYSIWYG field in NinjaOne.
Runs centrally, not per-device.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
v3 endpoint returns 401. B2 API v4 is current as of April 2025.
Also switch base64 encoding from UTF8 to ASCII per B2 docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
B2 has no real-time bucket size API. Instead, pulls the daily
audit CSV from the b2-reports-{accountId} bucket (auto-generated
by Backblaze) and extracts storageByteCount per bucket.

Auth now tries v2, v4, v3 in order since v2 is most reliable.
Also handles both v2 (apiUrl) and v4 (apiInfo.storageApi.apiUrl)
response shapes.

Table now shows: Bucket Name, Type, Size, Status.
Reports bucket is excluded from the output table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full automated flow:
1. Auth to B2 with admin/master key (org-level in NinjaRMM)
2. Create bucket: {org_guid_nodashes}-{time_based_id}-veeam
3. Enable object lock with governance retention
4. Create a scoped B2 application key restricted to that bucket
   only (read/write/delete + immutability capabilities)
5. Register Veeam S3 repository using the SCOPED key (not admin)
6. Store bucket name + scoped key ID + scoped app key in device-
   level NinjaRMM fields

Each BDR server only has credentials for its own bucket. Admin
key never stored on endpoints. Keys are rotatable by re-running
or via a future rotation script.

Replaces the old veeam-add-backup-repo.ps1 for S3 use cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Invoke-RestMethod rejects manually-constructed Authorization
headers when the base64 value contains characters like = or +.
Using -Authentication Basic with -Credential (PSCredential)
lets PowerShell handle the encoding correctly.

Fixed in both veeam-create-s3-repo.ps1 and b2-bucket-audit.ps1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Org-level custom fields aren't injected as env vars by the RMM.
Use Ninja-Property-Get -Organization dtcOrgGuid to read the value
at runtime. Falls back to CUSTOM_FIELD_ORG_UUID env var if set.

Also fixed B2 auth to use -Credential instead of manual header.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
B2 auth tokens contain = characters that Invoke-RestMethod rejects
in manual headers. Added Invoke-B2Api wrapper using
-SkipHeaderValidation for all post-auth B2 API calls.

Simplified org UUID: reads directly from env var since org-level
NinjaRMM fields inherit to device level as script variables.
Removed the broken Ninja-Property-Get -Organization approach.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Regex check for standard UUID format. Catches cases where the
field name gets passed instead of the value (e.g. "dtcOrgGuid"
instead of "a1b2c3d4-5e6f-...").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CUSTOM_FIELD_ORG_UUID holds the field name (e.g. "dtcOrgGuid"),
not the value. Use Ninja-Property-Get to fetch the actual UUID.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If bucket name + scoped keys already exist in NinjaRMM fields,
skip B2 creation entirely and just create the Veeam repository.
This allows re-running after a Veeam failure without creating
duplicate buckets/keys in B2.

Added 5-second delay after scoped key creation to allow B2 to
propagate the key before Veeam tries to authenticate with it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously the NinjaField writes were at the end of the script,
after the Veeam repo creation. If Veeam failed, the script exited
and the bucket name + scoped keys were never saved. Now saves
right after B2 key creation, before attempting Veeam setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Required for bucket-restricted keys to work with Veeam's S3
ListBuckets call during repository setup. Without it, Veeam
gets "Invalid credentials" because it can't enumerate buckets
to find the target bucket.

Per Backblaze docs: bucket-scoped keys must explicitly include
listAllBucketNames for S3-compatible ListBuckets to work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Without a lifecycle rule, B2 keeps ALL file versions forever. This
causes massive storage bloat (e.g. 11 TB active data but 50 TB in
B2 due to accumulated hidden versions from Veeam overwrites).

New buckets: lifecycle rule added during creation, deletes hidden
file versions after IMMUTABILITY_DAYS + 1 (default: 15 days).
This ensures immutability-locked files survive their full retention
window before being eligible for cleanup.

Existing buckets: b2-fix-lifecycle.ps1 applies the same lifecycle
rule retroactively. Supports single bucket or ALL *-veeam buckets.
B2 processes lifecycle rules daily so existing hidden versions
will be cleaned up over the following days.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Gumbees and others added 28 commits March 23, 2026 16:10
PS7.4+ ships Microsoft.Data.Sqlite that conflicts with Veeam's
bundled version, causing "type initializer" errors on repo creation.
Preload Veeam's DLL before the module imports to win the assembly
binding race. Added to all 4 Veeam scripts.

Renamed veeam-create-s3-repo.ps1 -> veeam-configure-backblaze-repo.ps1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Simple preload doesn't work because PS7 may have already loaded
its own version. Assembly resolve handler intercepts ALL assembly
load requests and redirects them to Veeam's Backup directory,
ensuring Veeam's Microsoft.Data.Sqlite.dll wins the binding race
regardless of load order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SqliteConnection type initializer fails because it can't find
the native e_sqlite3.dll library. Veeam stores it under
Backup\runtimes\win-x64\native\. Adding both that path and the
Backup directory to PATH before the module loads ensures the
native dependency is found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Logs the env var values and Ninja-Property-Get results to
identify why existing credentials aren't being detected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SQLite type initializer failure was caused by launching
32-bit PS7 (Program Files (x86)). Veeam's native e_sqlite3.dll
is x64 only. Get-Command pwsh.exe found the x86 version first
because it was earlier in PATH.

Now always checks Program Files (64-bit) first, falls back to
Get-Command only if 64-bit path doesn't exist.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The simple preload wasn't enough. All 4 Veeam scripts now have
the same fix that works on veeam-configure-backblaze-repo:
- Add Veeam runtimes/win-x64/native to PATH for e_sqlite3.dll
- Register AssemblyResolve handler for managed DLL redirects

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Assembly resolve handler was called with empty Name string causing
"value cannot be an empty string" errors. Added null check.

Copy job name "S3 Copy - {bucket}" exceeded Veeam's 50 char limit
with UUID-based bucket names. Now truncates the repo name to fit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Veeam enum is RestoreDays/RestorePoints, not Days/Cycles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BackupWindowOptions param needs a VBRBackupWindowOptions object,
not a raw string. Use New-VBRBackupWindowOptions to convert.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add-VBRBackupCopyJob doesn't accept window params directly.
Must create job first, then:
1. Set-VBRBackupCopyJob -Anytime:\$false
2. Set-VBRBackupCopyJob -BackupWindowOptions (from New-VBRBackupWindowOptions)

Uses New-VBRBackupWindowOptions with -FromDay/-FromHour/-ToDay/-ToHour
instead of raw binary string.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added Write-Host before each step so we can see exactly where
it hangs. Set ConfirmPreference=None right before the create
call in case module import resets it.

Simplified window to single range (Mon 22:00 - Fri 05:00).
Weekend window can be configured manually if the Set call fails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The cmdlet was hanging because -Mode is a required parameter
and wasn't being supplied. Diag script now dumps the Mode enum
values and tries Periodic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Periodic mode doesn't support Anytime or BackupWindowOptions.
Immediate mode does. Confirmed from diag:
- Periodic: "Cannot specify AnyTime/BackupWindowOptions in Periodic mode"
- Immediate: supports both

Flow: Create with -Mode Immediate, then Set -AnyTime:\$false,
then Set -BackupWindowOptions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Periodic mode uses ScheduleOptions instead of BackupWindowOptions.
Copies latest restore points (pruning) on a daily schedule at 10 PM.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create job without schedule first, then apply via
Set-VBRBackupCopyJob -ScheduleOptions using
New-VBRServerScheduleOptions + New-VBRPeriodicallyOptions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Encryption: finds the encryption key from the first source backup
job that has encryption enabled, applies it to the copy job via
StorageOptions. Falls back to first available key if source jobs
don't have one.

Schedule: removed all window/schedule restrictions. Job runs 24/7
in Periodic mode, copying whenever new data is available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Schedule: Uses New-VBRPeriodicallyOptions with -PeriodicallySchedule
(VBRBackupWindowOptions) to restrict when the periodic job runs.
Mon-Fri 10 PM - 5 AM, Sat-Sun all day. Checks hourly for new data.

Encryption: Finds the encryption key from source backup jobs and
applies it via StorageOptions. Falls back to first available key.

Steps: Create job -> Set schedule -> Set encryption -> Enable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New-VBRBackupCopyJobStorageOptions was hanging (prompting for input).
Now sets encryption directly on the job's BackupStorageOptions via
Get-VBRJob -> GetOptions() -> Set-VBRJobOptions, bypassing the
copy-job-specific cmdlet that prompts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous attempt used Get-VBRJob internal options which don't have
StorageEncryptionKey property on copy jobs. Now uses the correct
cmdlet: New-VBRBackupCopyJobStorageOptions -EnableEncryption
-EncryptionKey with the VBREncryptionKey object from
Get-VBREncryptionKey, then Set-VBRBackupCopyJob -StorageOptions.

Simplified to just use the first available encryption key since
source job options don't expose the key object directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Was hanging because CompressionLevel and StorageOptimizationType
are likely required. Now passing all params explicitly:
-CompressionLevel Auto
-StorageOptimizationType Local
-EnableDataDeduplication
-EnableEncryption
-EncryptionKey

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CompressionLevel and StorageOptimizationType are both MANDATORY.
- CompressionLevel: Auto
- StorageOptimizationType: LocalTarget (not "Local" which is ambiguous)

Removed -EnableDataDeduplication (not needed for S3 copy).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LocalTarget not supported by image-level backup copy jobs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refactored so schedule, encryption, and enable steps run on BOTH
existing and new copy jobs. Whether the job was just created or
already existed, all settings are enforced every time the script
runs. This ensures re-running the script fixes any missing config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If the copy job targeting the S3 repo has a different name than
"S3 Copy - {repo_name}", it gets renamed to match. This ensures
consistency even for manually-created or legacy copy jobs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Substitutes all "C:\Program Files\Veeam\..." literals with
$env:ProgramFiles-based paths. Inside AssemblyResolve scriptblocks, uses
the $VEEAM_BACKUP_DIR variable captured from the enclosing scope instead
of a second hardcoded literal, since scriptblocks capture their outer
scope in PowerShell.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 5 scripts (4 main + diag) now use $env:ProgramFiles instead
of "C:\Program Files" for Veeam paths. Works correctly on systems
where Program Files is on a different drive or path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents all core automation scripts, NinjaRMM field mappings,
and a technical reference covering every gotcha encountered:
- PS7 bootstrap (64-bit requirement)
- SQLite conflict fix (PATH + AssemblyResolve)
- Non-interactive execution pitfalls
- Veeam cmdlet parameter quirks and mandatory enums
- B2 API auth, scoped keys, lifecycle rules
- NinjaRMM org/device field reading patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

This PR reorganizes Veeam backup automation scripts from a flat structure into a hierarchical bdr-veeam/ directory with enhanced functionality. It removes five legacy scripts and introduces nine new scripts for configuring Backblaze B2 integration, managing S3 backup copy jobs, inventorying repositories, and handling management server registration. Documentation and auxiliary B2 lifecycle tools are also included.

Changes

Cohort / File(s) Summary
Removed Legacy Scripts
bdr-veeam/Find-VeeamS3OrphanedBackupData.ps1, bdr-veeam/veeam-add-backup-copy-job.ps1, bdr-veeam/veeam-add-backup-repo.ps1, bdr-veeam/veeam-remove-management-server.ps1, veeam/veeam-add-backup-repo.ps1
Deletion of five legacy Veeam automation scripts previously handling backup repo creation, backup copy job setup, orphaned backup detection, and management server cleanup. Total of 831 lines removed.
Backblaze B2 Repository Setup
bdr-veeam/veeam-configure-backblaze-repo.ps1
New script implementing B2 bucket provisioning with scoped credentials, B2 lifecycle rule setup, SSL certificate handling, and integration with NinjaOne RMM device fields for credential persistence (588 lines). Includes helper functions for B2 API calls and short ID generation.
S3 Backup Copy Job Configuration
bdr-veeam/veeam-configure-s3-copy-job.ps1
New script ensuring a single S3-targeting backup copy job exists with all eligible source jobs linked, supporting creation/update workflows and storage option/encryption configuration (396 lines). Locates S3 repositories via NinjaOne field or auto-discovery.
Local Backup Job Scheduling
bdr-veeam/veeam-configure-local-backup-jobs.ps1
New script applying Mon–Fri backup windows to all local (non-copy) Veeam backup jobs and agent/computer backup jobs, with fallback scheduling strategies (242 lines). Includes PowerShell 7 bootstrap and SQLite conflict mitigation.
Inventory & Health Check
bdr-veeam/veeam-inventory.ps1
New comprehensive inventory script populating NinjaOne fields with S3 repository usage, orphaned/stale backup tracking, failed job reporting, and missing copy job detection (684 lines). Generates HTML tables for console/RMM display.
Management Server Cleanup
bdr-veeam/veeam-clear-management-server.ps1
New script removing prior Veeam management server association from endpoints by deleting Veeam certificates, clearing registry values, and restarting Veeam services (130 lines). Replaces the deleted veeam-remove-management-server.ps1 with enhanced registry/service handling.
Diagnostic & Testing
bdr-veeam/veeam-s3-copy-job-diag.ps1
New diagnostic script for troubleshooting S3 backup copy job configuration, testing storage options with various compression/encryption combinations, and verifying first copy job setup (143 lines).
Backblaze B2 Utilities
iaas-backblaze/b2-bucket-audit.ps1, iaas-backblaze/b2-fix-lifecycle.ps1
New B2 scripts: audit retrieves latest account audit CSV, parses bucket usage, marks buckets as in-use/unused, and writes HTML to NinjaOne (364 lines); fix-lifecycle applies SSE-B2 encryption and version purge rules to one or multiple buckets (106 lines).
Documentation
bdr-veeam/README.md
New comprehensive documentation covering all scripts, technical requirements (PowerShell 7, SQLite conflicts), NinjaOne field integration, and Veeam 12.x gotchas (205 lines).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • scottlei4dtctoday

Poem

🐰 Hop, hop! The scripts now dance in concert fine,
With B2 buckets, copy jobs, and SQLite to align,
Backups orphaned, inventories bright,
This refactored warren shines with all its might! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main components added: Veeam S3 inventory functionality, Backblaze repository automation, and S3 copy job configuration—all core themes throughout the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch enhancement/veeam-s3-bucket-inventory

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (9)
bdr-veeam/README.md (1)

72-85: Field type clarification needed.

Line 79 lists CUSTOM_FIELD_ORPHANS_FOUND as "Checkbox" type, but the inventory script writes "1" or "0" as strings (line 660: "$([int]($ORPHAN_COUNT -gt 0))"). NinjaRMM checkbox fields typically expect boolean or specific values. Verify the field type is correct, or document it as "Integer" for consistency with the implementation.

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

In `@bdr-veeam/README.md` around lines 72 - 85, The README incorrectly documents
CUSTOM_FIELD_ORPHANS_FOUND as a "Checkbox" while the inventory script writes
string "1"/"0" via the expression "$([int]($ORPHAN_COUNT -gt 0))"; update the
README to reflect the actual stored type (e.g., "Integer" or "String '1'/'0'")
or change the script to write a true boolean compatible with NinjaRMM
checkboxes; reference the field name CUSTOM_FIELD_ORPHANS_FOUND and the
inventory script expression "$([int]($ORPHAN_COUNT -gt 0))" when making the
change so the docs and implementation agree.
iaas-backblaze/b2-bucket-audit.ps1 (2)

221-229: Silent byte parsing failure could mask data issues.

Line 226 silently catches parsing failures for storage bytes, defaulting to 0. This could mask corrupted CSV data. Consider logging a warning when parsing fails so data quality issues are visible.

💡 Suggested improvement
                         $BID = $FIELDS[$IDX_BUCKET_ID]
                         $BYTES = [long]0
-                        try { $BYTES = [long]$FIELDS[$IDX_STORED] } catch {}
+                        try { $BYTES = [long]$FIELDS[$IDX_STORED] }
+                        catch { Write-Warning "  Could not parse bytes for bucket $BID : $($FIELDS[$IDX_STORED])" }
                         if ($BID) { $BUCKET_SIZES[$BID] = $BYTES }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@iaas-backblaze/b2-bucket-audit.ps1` around lines 221 - 229, The CSV byte
parsing silently swallows errors in the try/catch that sets $BYTES (inside the
for loop over $CSV_LINES), which can hide corrupted data; update the parsing
around the try { $BYTES = [long]$FIELDS[$IDX_STORED] } catch {} to log a warning
(including the offending $I row index, $BID/$FIELDS[$IDX_BUCKET_ID], and the raw
$FIELDS[$IDX_STORED] value) when conversion fails and before defaulting $BYTES
to 0, so $BUCKET_SIZES entries and operators can spot data quality issues.

1-364: Missing transcript logging for audit trail.

This script makes API calls and writes to NinjaOne fields but lacks Start-Transcript/Stop-Transcript. As per coding guidelines, logging should be enabled for troubleshooting and audit purposes.

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

In `@iaas-backblaze/b2-bucket-audit.ps1` around lines 1 - 364, The script lacks
transcript logging; add Start-Transcript at script startup (e.g., after initial
env/input handling or right after the "=== B2 Bucket Audit ===" banner) to
capture the full run and write to a timestamped temp file, and ensure
Stop-Transcript is always called before any early exits or at the end (wrap main
work in try/finally or trap to call Stop-Transcript); update places that call
exit (e.g., after auth failure and failed list_buckets) to Stop-Transcript
first, and leave Set-NinjaField usage unchanged — just ensure the transcript is
stopped prior to writing NinjaOne fields or exiting so the audit trail is
complete.
iaas-backblaze/b2-fix-lifecycle.ps1 (2)

23-24: Silent failure on PURGE_DAYS parsing.

If $env:PURGE_DAYS contains an invalid value, the empty catch block silently falls back to the default without logging. Consider logging a warning so operators know their input was ignored.

💡 Suggested fix
 $PURGE_DAYS = 15
-if ($env:PURGE_DAYS) { try { $PURGE_DAYS = [int]$env:PURGE_DAYS } catch {} }
+if ($env:PURGE_DAYS) {
+    try { $PURGE_DAYS = [int]$env:PURGE_DAYS }
+    catch { Write-Warning "Invalid PURGE_DAYS value '$($env:PURGE_DAYS)', using default: 15" }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@iaas-backblaze/b2-fix-lifecycle.ps1` around lines 23 - 24, The try/catch
around parsing $env:PURGE_DAYS currently swallows errors; update the catch to
log a warning that includes the invalid input and the fact the script is falling
back to the default $PURGE_DAYS value so operators are notified. Locate the
parsing block around $PURGE_DAYS and $env:PURGE_DAYS (the try { $PURGE_DAYS =
[int]$env:PURGE_DAYS } catch {}) and replace the empty catch with a logging call
(e.g., Write-Warning or Write-Host) that prints the invalid $env:PURGE_DAYS
value and the default being used.

1-106: Missing transcript logging for audit trail.

This script modifies B2 bucket configurations but doesn't use Start-Transcript/Stop-Transcript for logging. As per coding guidelines, scripts should wrap logic in transcript logging for troubleshooting. Consider adding:

Start-Transcript -Path "$env:WINDIR\logs\b2-fix-lifecycle.log"
# ... existing logic ...
Stop-Transcript
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@iaas-backblaze/b2-fix-lifecycle.ps1` around lines 1 - 106, The script lacks
transcript audit logging; wrap the main execution in
Start-Transcript/Stop-Transcript to record changes: call Start-Transcript near
the start (before authentication and the foreach over $TARGET_BUCKETS) and
ensure Stop-Transcript always runs by putting the core logic (auth, Invoke-B2Api
calls, the foreach loop that updates buckets) inside a try/finally where
Stop-Transcript is invoked in the finally block; reference
Start-Transcript/Stop-Transcript and the existing Invoke-B2Api and the bucket
update block (the code that builds $UPDATE_BODY and calls b2_update_bucket) so
the transcript captures all actions and errors.
bdr-veeam/veeam-configure-backblaze-repo.ps1 (1)

203-207: Silent failure when reading org UUID.

The empty catch block at line 206 silently swallows errors from Ninja-Property-Get. If the field name is misconfigured, this could result in a confusing "CUSTOM_FIELD_ORG_UUID is empty" error instead of the actual cause.

💡 Suggested improvement
 $ORG_UUID = $null
 if ($env:CUSTOM_FIELD_ORG_UUID) {
     try {
         $ORG_UUID = Ninja-Property-Get $env:CUSTOM_FIELD_ORG_UUID 2>$null
-    } catch { }
+    } catch { Write-Warning "Failed to read org UUID from field '$($env:CUSTOM_FIELD_ORG_UUID)': $_" }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bdr-veeam/veeam-configure-backblaze-repo.ps1` around lines 203 - 207, The
empty catch in the block that calls Ninja-Property-Get (when
$env:CUSTOM_FIELD_ORG_UUID is set) hides failures and leads to misleading
"CUSTOM_FIELD_ORG_UUID is empty" symptoms; update the catch to surface the error
(e.g., log the exception with context via Write-Error/Write-Host or rethrow) and
include the attempted property name and the original exception message so
$ORG_UUID assignment issues are visible when Ninja-Property-Get fails.
bdr-veeam/veeam-configure-s3-copy-job.ps1 (1)

287-301: Job name truncation could cause issues.

If the repo name is exactly 50 characters, line 290 calculates 50 - 10 = 40 and truncates. But if the repo name is shorter (e.g., 45 chars), the "S3 Copy - $REPO_SHORT".Length -gt 50 check passes incorrectly because "S3 Copy - ".Length is 10, making the total 55. The substring then fails or produces unexpected results.

The condition should check the combined length, which it does, but the truncation math assumes a fixed overhead. Consider validating this edge case.

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

In `@bdr-veeam/veeam-configure-s3-copy-job.ps1` around lines 287 - 301, The
truncation logic for $REPO_SHORT is incorrect because it assumes a fixed 10-char
prefix; compute the prefix/header length and use it to derive a safe maxRepoLen
before truncating: calculate headerLen = ("S3 Copy - ").Length (or similar),
then if ("S3 Copy - $REPO_SHORT".Length -gt 50) set $REPO_SHORT =
$REPO_SHORT.Substring(0, [Math]::Max(0, 50 - headerLen)) to avoid negative
lengths or incorrect truncation; keep the rest of the rename flow
($EXPECTED_NAME, Set-VBRBackupCopyJob, try/catch) unchanged.
bdr-veeam/veeam-inventory.ps1 (1)

407-410: Unused variable $LAST_USED_SIZE_BYTES.

$LAST_USED_SIZE_BYTES is assigned at lines 409 and 469 but never used. Consider removing it or using it if there's a future need.

🧹 Remove unused variable
 $LAST_USED_BUCKET = "N/A"
 $LAST_USED_SIZE = "N/A"
-$LAST_USED_SIZE_BYTES = [long]0
 $LAST_USED_TIME = [datetime]::MinValue

And at line 469:

         $LAST_USED_BUCKET = $BUCKET_NAME
         $LAST_USED_SIZE = $USED_SPACE_DISPLAY
-        $LAST_USED_SIZE_BYTES = $USED_SPACE_BYTES
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bdr-veeam/veeam-inventory.ps1` around lines 407 - 410, The variable
$LAST_USED_SIZE_BYTES is declared and assigned but never used; remove its
declaration and any subsequent assignments to it (search for the symbol
$LAST_USED_SIZE_BYTES and delete those lines) so the script doesn't contain dead
state, or if the intended behavior is to track size in bytes, replace its unused
assignments by actually using it where sizes are computed/compared (e.g.,
integrate $LAST_USED_SIZE_BYTES into the size calculation/formatting logic that
currently sets $LAST_USED_SIZE or $LAST_USED_TIME); ensure references to
$LAST_USED_BUCKET, $LAST_USED_SIZE, and $LAST_USED_TIME remain unchanged.
bdr-veeam/veeam-s3-copy-job-diag.ps1 (1)

26-29: Consider adding Veeam version detection.

Per learnings, Veeam scripts should detect the installed version and branch logic for new vs legacy command syntax using [version] comparisons. While this diagnostic script currently works without it, adding version detection would make it more robust across different Veeam installations.

💡 Example version detection pattern
# Detect Veeam version
$veeamPath = "$env:ProgramFiles\Veeam\Backup and Replication\Backup\Veeam.Backup.Core.dll"
if (Test-Path $veeamPath) {
    $veeamVersion = [version](Get-Item $veeamPath).VersionInfo.FileVersion
    Write-Host "Veeam version: $veeamVersion"
    
    # Branch logic for different versions
    if ($veeamVersion -ge [version]"12.0") {
        # New syntax
    } else {
        # Legacy syntax
    }
}

Based on learnings: "Veeam scripts should detect installed Veeam version and branch logic for new vs legacy command syntax using [version] comparisons"

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

In `@bdr-veeam/veeam-s3-copy-job-diag.ps1` around lines 26 - 29, Add Veeam version
detection and branching so the script handles new vs legacy command syntax;
check for the Veeam.Backup.Core.dll (e.g. using $env:ProgramFiles path used for
$MY_MODULE_PATH), read its FileVersion into a [version] object (store in a
$veeamVersion variable) and use an if/else on $veeamVersion (e.g. -ge
[version]"12.0") to choose appropriate command sets before calling Get-Module
-Name Veeam.Backup.PowerShell or any subsequent Veeam cmdlets; ensure the
detection logs the detected version (e.g. Write-Host) and falls back safely if
the DLL is missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bdr-veeam/README.md`:
- Around line 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.

In `@bdr-veeam/veeam-configure-backblaze-repo.ps1`:
- Around line 576-582: The Write-Host line that prints the scoped key uses
$SCOPED_KEY_ID_OUT.Substring(...), which will throw if $SCOPED_KEY_ID_OUT is
null; update the output to guard against null/empty by checking
$SCOPED_KEY_ID_OUT (or using a conditional expression) and printing a safe
placeholder (e.g., "<none>" or "N/A") when null, otherwise printing the
truncated substring using the current [Math]::Min logic; locate the string
rendering around the Write-Host that references $SCOPED_KEY_ID_OUT and replace
it with this null-safe conditional rendering.

In `@bdr-veeam/veeam-configure-local-backup-jobs.ps1`:
- Around line 76-80: The banner printout in the Write-Host block currently says
"Mon-Fri 5 AM - 10 PM" but the scheduling logic uses hours 6-21 (6 AM - 9 PM);
update the displayed schedule text in the Write-Host lines (the header block
that prints "=== Veeam Local Backup Schedule Configuration ===" and the
following schedule description) to read "Mon-Fri 6 AM - 9 PM" so the
documentation matches the implementation (or alternatively adjust the scheduling
logic that uses the 6-21 window to match the printed 5-10 window if you prefer
changing behavior).
- Around line 194-198: The fallback schedule currently sets the job start time
to "05:00" which is outside the allowed backup window; update the
Set-VBRJobSchedule call (function Set-VBRJobSchedule, parameter -At) to use
"06:00" and adjust the success message in the subsequent Write-Host (the string
"Schedule set (daily Mon-Fri at 5 AM).") to reflect "6 AM" so the fallback
aligns with the 6 AM backup window.

In `@bdr-veeam/veeam-configure-s3-copy-job.ps1`:
- Around line 348-359: The schedule only creates $WEEKNIGHT_WINDOW (Mon-Fri
22:00–05:00) but omits an all-day weekend window; add a second window (e.g.,
$WEEKEND_WINDOW = New-VBRBackupWindowOptions -FromDay Saturday -FromHour 0
-ToDay Sunday -ToHour 23) and include both windows in the periodic schedule
passed to New-VBRPeriodicallyOptions (assign the combined windows to
-PeriodicallySchedule), then recreate $SCHEDULE and call Set-VBRBackupCopyJob
-Job $COPY_JOB -ScheduleOptions $SCHEDULE so the comment "Mon-Fri 10 PM - 5 AM,
Sat-Sun all day" is actually implemented.

In `@bdr-veeam/veeam-s3-copy-job-diag.ps1`:
- Around line 27-28: The script uses ALL_CAPS/Pascal variables instead of
camelCase; rename each listed variable (e.g., $MY_MODULE_PATH -> $myModulePath,
$PWSH_PATH -> $pwshPath, $VBD -> $vbd, $VBR -> $vbr, $D -> $d (or better
descriptive name), $N -> $n, $F -> $f, $KEY -> $key, $OPTS -> $opts, $CMD ->
$cmd, $P -> $p, $LINE -> $line, $PN -> $pn, $CJ -> $cj, $T -> $t, $CL -> $cl) to
camelCase and update every usage throughout the file (including concatenations,
environment assignments like $env:PSModulePath, function calls, and
comparisons). Ensure you also update any comments or string interpolations that
reference the old names and run a quick local lint/test to confirm no remaining
references to the old variable names.
- Line 130: The Set-VBRBackupCopyJob call lacks local error handling and reports
success before confirming completion; update the code that calls
Set-VBRBackupCopyJob with ErrorAction Stop (or wrap it in a try block) and add a
dedicated catch that logs the failure (including $CJ and $OPTS context and the
exception) and sets an appropriate error flag or returns/throws so the outer
flow doesn't report success; only emit the success message after
Set-VBRBackupCopyJob completes without error.
- Around line 1-143: Add the required three-part RMM script structure: insert an
RMM variable declaration block at the top that defines/initializes $RMM and
includes the comment header, add an input handling section that detects $RMM,
captures $Description and sets $LogPath, and wrap the entire script logic (the
existing diagnostic steps that use New-VBRBackupCopyJobStorageOptions,
Get-VBREncryptionKey, Get-VBRBackupCopyJob, Set-VBRBackupCopyJob, etc.) inside
Start-Transcript/Stop-Transcript so all output is logged; ensure the script
still supports dual execution modes (interactive vs RMM) and that $LogPath is
used for the transcript destination.

---

Nitpick comments:
In `@bdr-veeam/README.md`:
- Around line 72-85: The README incorrectly documents CUSTOM_FIELD_ORPHANS_FOUND
as a "Checkbox" while the inventory script writes string "1"/"0" via the
expression "$([int]($ORPHAN_COUNT -gt 0))"; update the README to reflect the
actual stored type (e.g., "Integer" or "String '1'/'0'") or change the script to
write a true boolean compatible with NinjaRMM checkboxes; reference the field
name CUSTOM_FIELD_ORPHANS_FOUND and the inventory script expression
"$([int]($ORPHAN_COUNT -gt 0))" when making the change so the docs and
implementation agree.

In `@bdr-veeam/veeam-configure-backblaze-repo.ps1`:
- Around line 203-207: The empty catch in the block that calls
Ninja-Property-Get (when $env:CUSTOM_FIELD_ORG_UUID is set) hides failures and
leads to misleading "CUSTOM_FIELD_ORG_UUID is empty" symptoms; update the catch
to surface the error (e.g., log the exception with context via
Write-Error/Write-Host or rethrow) and include the attempted property name and
the original exception message so $ORG_UUID assignment issues are visible when
Ninja-Property-Get fails.

In `@bdr-veeam/veeam-configure-s3-copy-job.ps1`:
- Around line 287-301: The truncation logic for $REPO_SHORT is incorrect because
it assumes a fixed 10-char prefix; compute the prefix/header length and use it
to derive a safe maxRepoLen before truncating: calculate headerLen = ("S3 Copy -
").Length (or similar), then if ("S3 Copy - $REPO_SHORT".Length -gt 50) set
$REPO_SHORT = $REPO_SHORT.Substring(0, [Math]::Max(0, 50 - headerLen)) to avoid
negative lengths or incorrect truncation; keep the rest of the rename flow
($EXPECTED_NAME, Set-VBRBackupCopyJob, try/catch) unchanged.

In `@bdr-veeam/veeam-inventory.ps1`:
- Around line 407-410: The variable $LAST_USED_SIZE_BYTES is declared and
assigned but never used; remove its declaration and any subsequent assignments
to it (search for the symbol $LAST_USED_SIZE_BYTES and delete those lines) so
the script doesn't contain dead state, or if the intended behavior is to track
size in bytes, replace its unused assignments by actually using it where sizes
are computed/compared (e.g., integrate $LAST_USED_SIZE_BYTES into the size
calculation/formatting logic that currently sets $LAST_USED_SIZE or
$LAST_USED_TIME); ensure references to $LAST_USED_BUCKET, $LAST_USED_SIZE, and
$LAST_USED_TIME remain unchanged.

In `@bdr-veeam/veeam-s3-copy-job-diag.ps1`:
- Around line 26-29: Add Veeam version detection and branching so the script
handles new vs legacy command syntax; check for the Veeam.Backup.Core.dll (e.g.
using $env:ProgramFiles path used for $MY_MODULE_PATH), read its FileVersion
into a [version] object (store in a $veeamVersion variable) and use an if/else
on $veeamVersion (e.g. -ge [version]"12.0") to choose appropriate command sets
before calling Get-Module -Name Veeam.Backup.PowerShell or any subsequent Veeam
cmdlets; ensure the detection logs the detected version (e.g. Write-Host) and
falls back safely if the DLL is missing.

In `@iaas-backblaze/b2-bucket-audit.ps1`:
- Around line 221-229: The CSV byte parsing silently swallows errors in the
try/catch that sets $BYTES (inside the for loop over $CSV_LINES), which can hide
corrupted data; update the parsing around the try { $BYTES =
[long]$FIELDS[$IDX_STORED] } catch {} to log a warning (including the offending
$I row index, $BID/$FIELDS[$IDX_BUCKET_ID], and the raw $FIELDS[$IDX_STORED]
value) when conversion fails and before defaulting $BYTES to 0, so $BUCKET_SIZES
entries and operators can spot data quality issues.
- Around line 1-364: The script lacks transcript logging; add Start-Transcript
at script startup (e.g., after initial env/input handling or right after the
"=== B2 Bucket Audit ===" banner) to capture the full run and write to a
timestamped temp file, and ensure Stop-Transcript is always called before any
early exits or at the end (wrap main work in try/finally or trap to call
Stop-Transcript); update places that call exit (e.g., after auth failure and
failed list_buckets) to Stop-Transcript first, and leave Set-NinjaField usage
unchanged — just ensure the transcript is stopped prior to writing NinjaOne
fields or exiting so the audit trail is complete.

In `@iaas-backblaze/b2-fix-lifecycle.ps1`:
- Around line 23-24: The try/catch around parsing $env:PURGE_DAYS currently
swallows errors; update the catch to log a warning that includes the invalid
input and the fact the script is falling back to the default $PURGE_DAYS value
so operators are notified. Locate the parsing block around $PURGE_DAYS and
$env:PURGE_DAYS (the try { $PURGE_DAYS = [int]$env:PURGE_DAYS } catch {}) and
replace the empty catch with a logging call (e.g., Write-Warning or Write-Host)
that prints the invalid $env:PURGE_DAYS value and the default being used.
- Around line 1-106: The script lacks transcript audit logging; wrap the main
execution in Start-Transcript/Stop-Transcript to record changes: call
Start-Transcript near the start (before authentication and the foreach over
$TARGET_BUCKETS) and ensure Stop-Transcript always runs by putting the core
logic (auth, Invoke-B2Api calls, the foreach loop that updates buckets) inside a
try/finally where Stop-Transcript is invoked in the finally block; reference
Start-Transcript/Stop-Transcript and the existing Invoke-B2Api and the bucket
update block (the code that builds $UPDATE_BODY and calls b2_update_bucket) so
the transcript captures all actions and errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 506a7d68-81c4-49fc-a7c8-7b6a7dbb0148

📥 Commits

Reviewing files that changed from the base of the PR and between 1ac5146 and c69c990.

📒 Files selected for processing (14)
  • bdr-veeam/Find-VeeamS3OrphanedBackupData.ps1
  • bdr-veeam/README.md
  • bdr-veeam/veeam-add-backup-copy-job.ps1
  • bdr-veeam/veeam-add-backup-repo.ps1
  • bdr-veeam/veeam-clear-management-server.ps1
  • bdr-veeam/veeam-configure-backblaze-repo.ps1
  • bdr-veeam/veeam-configure-local-backup-jobs.ps1
  • bdr-veeam/veeam-configure-s3-copy-job.ps1
  • bdr-veeam/veeam-inventory.ps1
  • bdr-veeam/veeam-remove-management-server.ps1
  • bdr-veeam/veeam-s3-copy-job-diag.ps1
  • iaas-backblaze/b2-bucket-audit.ps1
  • iaas-backblaze/b2-fix-lifecycle.ps1
  • veeam/veeam-add-backup-repo.ps1
💤 Files with no reviewable changes (5)
  • veeam/veeam-add-backup-repo.ps1
  • bdr-veeam/veeam-add-backup-repo.ps1
  • bdr-veeam/veeam-remove-management-server.ps1
  • bdr-veeam/Find-VeeamS3OrphanedBackupData.ps1
  • bdr-veeam/veeam-add-backup-copy-job.ps1

Comment on lines +142 to +146
**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`).

Copy link
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.

Comment on lines +576 to +582
Write-Host "=== Complete ==="
Write-Host " Bucket: $BUCKET_NAME"
Write-Host " Repository: $BUCKET_NAME"
Write-Host " Folder: $BUCKET_SHORT_ID"
Write-Host " Scoped key: $($SCOPED_KEY_ID_OUT.Substring(0, [Math]::Min(8, $SCOPED_KEY_ID_OUT.Length)))..."
Write-Host " Immutability: $IMMUTABILITY_DAYS days"
Write-Host ""
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential null reference on SCOPED_KEY_ID_OUT.

If $SCOPED_KEY_ID_OUT is null or shorter than 8 characters, line 580 could throw an exception on .Substring(). The [Math]::Min handles short strings but not null.

🛡️ Defensive fix
 Write-Host "=== Complete ==="
 Write-Host "  Bucket:       $BUCKET_NAME"
 Write-Host "  Repository:   $BUCKET_NAME"
 Write-Host "  Folder:       $BUCKET_SHORT_ID"
-Write-Host "  Scoped key:   $($SCOPED_KEY_ID_OUT.Substring(0, [Math]::Min(8, $SCOPED_KEY_ID_OUT.Length)))..."
+Write-Host "  Scoped key:   $(if ($SCOPED_KEY_ID_OUT) { $SCOPED_KEY_ID_OUT.Substring(0, [Math]::Min(8, $SCOPED_KEY_ID_OUT.Length)) + '...' } else { 'N/A' })"
 Write-Host "  Immutability: $IMMUTABILITY_DAYS days"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Write-Host "=== Complete ==="
Write-Host " Bucket: $BUCKET_NAME"
Write-Host " Repository: $BUCKET_NAME"
Write-Host " Folder: $BUCKET_SHORT_ID"
Write-Host " Scoped key: $($SCOPED_KEY_ID_OUT.Substring(0, [Math]::Min(8, $SCOPED_KEY_ID_OUT.Length)))..."
Write-Host " Immutability: $IMMUTABILITY_DAYS days"
Write-Host ""
Write-Host "=== Complete ==="
Write-Host " Bucket: $BUCKET_NAME"
Write-Host " Repository: $BUCKET_NAME"
Write-Host " Folder: $BUCKET_SHORT_ID"
Write-Host " Scoped key: $(if ($SCOPED_KEY_ID_OUT) { $SCOPED_KEY_ID_OUT.Substring(0, [Math]::Min(8, $SCOPED_KEY_ID_OUT.Length)) + '...' } else { 'N/A' })"
Write-Host " Immutability: $IMMUTABILITY_DAYS days"
Write-Host ""
🧰 Tools
🪛 PSScriptAnalyzer (1.24.0)

[warning] 576-576: File 'veeam-configure-backblaze-repo.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 577-577: File 'veeam-configure-backblaze-repo.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 578-578: File 'veeam-configure-backblaze-repo.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 579-579: File 'veeam-configure-backblaze-repo.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 580-580: File 'veeam-configure-backblaze-repo.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 581-581: File 'veeam-configure-backblaze-repo.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 582-582: File 'veeam-configure-backblaze-repo.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)

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

In `@bdr-veeam/veeam-configure-backblaze-repo.ps1` around lines 576 - 582, The
Write-Host line that prints the scoped key uses
$SCOPED_KEY_ID_OUT.Substring(...), which will throw if $SCOPED_KEY_ID_OUT is
null; update the output to guard against null/empty by checking
$SCOPED_KEY_ID_OUT (or using a conditional expression) and printing a safe
placeholder (e.g., "<none>" or "N/A") when null, otherwise printing the
truncated substring using the current [Math]::Min logic; locate the string
rendering around the Write-Host that references $SCOPED_KEY_ID_OUT and replace
it with this null-safe conditional rendering.

Comment on lines +76 to +80
Write-Host "=== Veeam Local Backup Schedule Configuration ==="
Write-Host "Description: $env:DESCRIPTION"
Write-Host "Schedule: Mon-Fri 5 AM - 10 PM"
Write-Host " Sat-Sun disabled"
Write-Host ""
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Documentation mismatch with implementation.

Line 78 states "Mon-Fri 5 AM - 10 PM" but the actual window logic at lines 159-164 implements 6 AM - 9 PM (hours 6-21). The header comment at line 2 correctly says "6 AM - 9 PM".

📝 Suggested fix
 Write-Host "=== Veeam Local Backup Schedule Configuration ==="
 Write-Host "Description: $env:DESCRIPTION"
-Write-Host "Schedule:    Mon-Fri 5 AM - 10 PM"
+Write-Host "Schedule:    Mon-Fri 6 AM - 9 PM"
 Write-Host "             Sat-Sun disabled"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Write-Host "=== Veeam Local Backup Schedule Configuration ==="
Write-Host "Description: $env:DESCRIPTION"
Write-Host "Schedule: Mon-Fri 5 AM - 10 PM"
Write-Host " Sat-Sun disabled"
Write-Host ""
Write-Host "=== Veeam Local Backup Schedule Configuration ==="
Write-Host "Description: $env:DESCRIPTION"
Write-Host "Schedule: Mon-Fri 6 AM - 9 PM"
Write-Host " Sat-Sun disabled"
Write-Host ""
🧰 Tools
🪛 PSScriptAnalyzer (1.24.0)

[warning] 76-76: File 'veeam-configure-local-backup-jobs.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 77-77: File 'veeam-configure-local-backup-jobs.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 78-78: File 'veeam-configure-local-backup-jobs.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 79-79: File 'veeam-configure-local-backup-jobs.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 80-80: File 'veeam-configure-local-backup-jobs.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)

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

In `@bdr-veeam/veeam-configure-local-backup-jobs.ps1` around lines 76 - 80, The
banner printout in the Write-Host block currently says "Mon-Fri 5 AM - 10 PM"
but the scheduling logic uses hours 6-21 (6 AM - 9 PM); update the displayed
schedule text in the Write-Host lines (the header block that prints "=== Veeam
Local Backup Schedule Configuration ===" and the following schedule description)
to read "Mon-Fri 6 AM - 9 PM" so the documentation matches the implementation
(or alternatively adjust the scheduling logic that uses the 6-21 window to match
the printed 5-10 window if you prefer changing behavior).

Comment on lines +194 to +198
try {
Set-VBRJobSchedule -Job $JOB `
-DailyOptions (New-VBRDailyOptions -DayOfWeek Monday,Tuesday,Wednesday,Thursday,Friday -Period 1) `
-At "05:00"
Write-Host " [OK] Schedule set (daily Mon-Fri at 5 AM)."
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fallback schedule start time inconsistent with backup window.

The fallback schedule starts at "05:00" (5 AM), but the backup window allows jobs from 6 AM. This could allow a job to start at 5 AM outside the intended window.

📝 Suggested fix
             try {
                 Set-VBRJobSchedule -Job $JOB `
                     -DailyOptions (New-VBRDailyOptions -DayOfWeek Monday,Tuesday,Wednesday,Thursday,Friday -Period 1) `
-                    -At "05:00"
-                Write-Host "    [OK] Schedule set (daily Mon-Fri at 5 AM)."
+                    -At "06:00"
+                Write-Host "    [OK] Schedule set (daily Mon-Fri at 6 AM)."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
Set-VBRJobSchedule -Job $JOB `
-DailyOptions (New-VBRDailyOptions -DayOfWeek Monday,Tuesday,Wednesday,Thursday,Friday -Period 1) `
-At "05:00"
Write-Host " [OK] Schedule set (daily Mon-Fri at 5 AM)."
try {
Set-VBRJobSchedule -Job $JOB `
-DailyOptions (New-VBRDailyOptions -DayOfWeek Monday,Tuesday,Wednesday,Thursday,Friday -Period 1) `
-At "06:00"
Write-Host " [OK] Schedule set (daily Mon-Fri at 6 AM)."
🧰 Tools
🪛 PSScriptAnalyzer (1.24.0)

[warning] 198-198: File 'veeam-configure-local-backup-jobs.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)

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

In `@bdr-veeam/veeam-configure-local-backup-jobs.ps1` around lines 194 - 198, The
fallback schedule currently sets the job start time to "05:00" which is outside
the allowed backup window; update the Set-VBRJobSchedule call (function
Set-VBRJobSchedule, parameter -At) to use "06:00" and adjust the success message
in the subsequent Write-Host (the string "Schedule set (daily Mon-Fri at 5
AM).") to reflect "6 AM" so the fallback aligns with the 6 AM backup window.

Comment on lines +348 to +359
# Schedule: Mon-Fri 10 PM - 5 AM, Sat-Sun all day
try {
Write-Host ""
Write-Host "Applying schedule..."
$WEEKNIGHT_WINDOW = New-VBRBackupWindowOptions -FromDay Monday -FromHour 22 -ToDay Friday -ToHour 5
$PERIOD_OPTS = New-VBRPeriodicallyOptions -PeriodicallyKind Hours -FullPeriod 1 -PeriodicallySchedule $WEEKNIGHT_WINDOW
$SCHEDULE = New-VBRServerScheduleOptions -Type Periodically -PeriodicallyOptions $PERIOD_OPTS
Set-VBRBackupCopyJob -Job $COPY_JOB -ScheduleOptions $SCHEDULE
Write-Host " [OK] Schedule: Mon-Fri 10 PM - 5 AM, Sat-Sun all day."
} catch {
Write-Warning " Failed to set schedule: $_"
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Veeam PowerShell New-VBRBackupWindowOptions parameters

💡 Result:

The New-VBRBackupWindowOptions cmdlet in Veeam Backup & Replication PowerShell creates a VBRBackupWindowOptions object defining a time interval for job completion. Syntax: New-VBRBackupWindowOptions [-Enabled] [-FromDay] [-FromHour] [-ToDay] [-ToHour] [] Parameters: - FromDay: Day of week when the backup window opens. Type: DayOfWeek. Default: Sunday. Optional. - FromHour: Hour (0-23) when the backup window opens. Type: Int32. Default: 0. Optional. - ToDay: Day of week when the backup window closes. Type: DayOfWeek. Default: Saturday. Optional. - ToHour: Hour when the backup window closes (closes at end of this hour). Type: Int32. Optional. - Enabled: Switch to enable the backup window. Type: SwitchParameter. Optional. Example: $windowoptions = New-VBRBackupWindowOptions -FromDay Monday -ToDay Friday -FromHour 20 -ToHour 22 -Enabled This object is used with cmdlets like Set-VBRJobSchedule to apply to jobs.

Citations:


Schedule implementation is incomplete and does not match the documented intent.

The New-VBRBackupWindowOptions parameters are valid, but the schedule configured at line 352 only implements the Mon-Fri 10 PM - 5 AM window. The comment documents "Mon-Fri 10 PM - 5 AM, Sat-Sun all day", but the Saturday-Sunday all-day scheduling is missing. You need an additional backup window for weekends (e.g., New-VBRBackupWindowOptions -FromDay Saturday -ToDay Sunday -Enabled) to match the documented intent.

🧰 Tools
🪛 PSScriptAnalyzer (1.24.0)

[warning] 350-350: File 'veeam-configure-s3-copy-job.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 351-351: File 'veeam-configure-s3-copy-job.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 356-356: File 'veeam-configure-s3-copy-job.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)

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

In `@bdr-veeam/veeam-configure-s3-copy-job.ps1` around lines 348 - 359, The
schedule only creates $WEEKNIGHT_WINDOW (Mon-Fri 22:00–05:00) but omits an
all-day weekend window; add a second window (e.g., $WEEKEND_WINDOW =
New-VBRBackupWindowOptions -FromDay Saturday -FromHour 0 -ToDay Sunday -ToHour
23) and include both windows in the periodic schedule passed to
New-VBRPeriodicallyOptions (assign the combined windows to
-PeriodicallySchedule), then recreate $SCHEDULE and call Set-VBRBackupCopyJob
-Job $COPY_JOB -ScheduleOptions $SCHEDULE so the comment "Mon-Fri 10 PM - 5 AM,
Sat-Sun all day" is actually implemented.

Comment on lines +1 to +143
# Diagnostic: S3 copy job - encryption setup
# Run interactively on the BDR server in PS7

if ($PSVersionTable.PSVersion.Major -lt 7) {
$PWSH_PATH = "$env:ProgramFiles\PowerShell\7\pwsh.exe"
if (Test-Path $PWSH_PATH) {
& $PWSH_PATH -NoProfile -ExecutionPolicy Bypass -File $MyInvocation.MyCommand.Path
exit $LASTEXITCODE
}
}

if ($PSVersionTable.PSVersion.Major -ge 7) {
$VBD = "$env:ProgramFiles\Veeam\Backup and Replication\Backup"
$VBR = "$VBD\runtimes\win-x64\native"
foreach ($D in @($VBR, $VBD)) { if ((Test-Path $D) -and $env:PATH -notlike "*$D*") { $env:PATH = "$D;$env:PATH" } }
if (Test-Path $VBD) {
$null = [System.AppDomain]::CurrentDomain.add_AssemblyResolve({
param($s, $a); if (-not $a.Name) { return $null }
$N = [System.Reflection.AssemblyName]::new($a.Name)
$F = Join-Path $VBD "$($N.Name).dll"
if (Test-Path $F) { return [System.Reflection.Assembly]::LoadFrom($F) }; return $null
})
}
}

$ConfirmPreference = 'None'
$MY_MODULE_PATH = "$env:ProgramFiles\Veeam\Backup and Replication\Console\"
$env:PSModulePath = $env:PSModulePath + "$([System.IO.Path]::PathSeparator)$MY_MODULE_PATH"
Get-Module -ListAvailable -Name Veeam.Backup.PowerShell | Import-Module -WarningAction SilentlyContinue

Write-Host "=========================================="
Write-Host "1: New-VBRBackupCopyJobStorageOptions - all params + enum values"
Write-Host "=========================================="
try {
$CMD = Get-Command New-VBRBackupCopyJobStorageOptions -ErrorAction Stop
foreach ($KEY in ($CMD.Parameters.Keys | Sort-Object)) {
$P = $CMD.Parameters[$KEY]
$LINE = " $KEY ($($P.ParameterType.Name))"
if ($P.ParameterType.IsEnum) {
$LINE += " = $([Enum]::GetNames($P.ParameterType) -join ', ')"
}
if ($P.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.Mandatory }) {
$LINE += " [MANDATORY]"
}
Write-Host $LINE
}
} catch { Write-Host " FAILED: $_" }

Write-Host ""
Write-Host "=========================================="
Write-Host "2: Get encryption key"
Write-Host "=========================================="
$KEY = $null
try {
$KEY = Get-VBREncryptionKey | Select-Object -First 1
Write-Host " Key: $($KEY.Id) - $($KEY.Description)"
} catch { Write-Host " FAILED: $_" }

Write-Host ""
Write-Host "=========================================="
Write-Host "3: Try New-VBRBackupCopyJobStorageOptions (minimal)"
Write-Host "=========================================="
try {
Write-Host " Trying with just -EnableEncryption -EncryptionKey..."
$OPTS = New-VBRBackupCopyJobStorageOptions -EnableEncryption -EncryptionKey $KEY
Write-Host " [OK] $($OPTS.GetType().FullName)"
$OPTS | Get-Member -MemberType Property | ForEach-Object {
$PN = $_.Name; try { Write-Host " $PN = $($OPTS.$PN)" } catch {}
}
} catch { Write-Host " FAILED: $_" }

Write-Host ""
Write-Host "=========================================="
Write-Host "4: Try with all params"
Write-Host "=========================================="

# Get CompressionLevel enum values
Write-Host " CompressionLevel values:"
try {
$T = [Veeam.Backup.PowerShell.Infos.VBRBackupCopyJobCompressionLevel]
[Enum]::GetNames($T) | ForEach-Object { Write-Host " $_" }
} catch { Write-Host " Could not get enum" }

Write-Host " StorageOptimizationType values:"
try {
$T = [Veeam.Backup.PowerShell.Infos.VBRBackupCopyJobStorageOptimizationType]
[Enum]::GetNames($T) | ForEach-Object { Write-Host " $_" }
} catch { Write-Host " Could not get enum" }

Write-Host ""
try {
Write-Host " Trying with all params..."
$OPTS = New-VBRBackupCopyJobStorageOptions `
-CompressionLevel Auto `
-StorageOptimizationType Local `
-EnableDataDeduplication `
-EnableEncryption `
-EncryptionKey $KEY
Write-Host " [OK]"
} catch {
Write-Host " FAILED: $_"
Write-Host ""
Write-Host " Trying different CompressionLevel values..."
foreach ($CL in @("Auto", "None", "Optimal", "High", "Extreme", "0", "1", "4", "5", "6", "9")) {
try {
$OPTS = New-VBRBackupCopyJobStorageOptions -CompressionLevel $CL -EnableEncryption -EncryptionKey $KEY
Write-Host " [OK] CompressionLevel=$CL"
break
} catch { Write-Host " [$CL] FAILED" }
}
}

Write-Host ""
Write-Host "=========================================="
Write-Host "5: Get existing copy job and try Set-VBRBackupCopyJob"
Write-Host "=========================================="
try {
$CJ = Get-VBRBackupCopyJob | Select-Object -First 1
if ($CJ) {
Write-Host " Copy job: $($CJ.Name)"
Write-Host " Current StorageOptions:"
if ($CJ.StorageOptions) {
$CJ.StorageOptions | Get-Member -MemberType Property | ForEach-Object {
$PN = $_.Name; try { Write-Host " $PN = $($CJ.StorageOptions.$PN)" } catch {}
}
}
Write-Host ""
Write-Host " Trying Set-VBRBackupCopyJob -StorageOptions..."
if ($OPTS) {
Set-VBRBackupCopyJob -Job $CJ -StorageOptions $OPTS
Write-Host " [OK] Encryption set!"
} else {
Write-Host " No StorageOptions object available from previous steps."
}
} else {
Write-Host " No copy job found."
}
} catch { Write-Host " FAILED: $_" }

Write-Host ""
Write-Host "=========================================="
Write-Host "Done"
Write-Host "=========================================="
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Script must follow the three-part structure.

This script violates the mandatory three-part structure required for all PowerShell scripts. Even diagnostic scripts must include:

  1. RMM variable declaration block (with comment header)
  2. Input handling section (detecting $RMM, capturing $Description, setting $LogPath)
  3. Script logic wrapped in Start-Transcript/Stop-Transcript

The missing transcript logging is particularly critical, as it prevents troubleshooting of diagnostic runs. As per coding guidelines, all scripts must support dual execution modes and implement full logging.

🔧 Proposed fix to add required structure

Add after line 3:

 # Run interactively on the BDR server in PS7
+
+## PLEASE COMMENT YOUR VARIABLES DIRECTLY BELOW HERE IF YOU'RE RUNNING FROM A RMM
+## (none required for diagnostic script)
+
+# Input handling
+if ($RMM -eq 1) {
+    $description = "Veeam S3 Copy Job Diagnostic"
+    $logPath = if ($RMMScriptPath) { Join-Path $RMMScriptPath "logs" } else { "$env:WINDIR\logs" }
+} else {
+    $description = "Veeam S3 Copy Job Diagnostic"
+    $logPath = "$env:WINDIR\logs"
+}
+
+if (-not (Test-Path $logPath)) { New-Item -ItemType Directory -Path $logPath -Force | Out-Null }
+$scriptLogName = "veeam-s3-copy-job-diag"
+$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
+$logFile = Join-Path $logPath "$scriptLogName-$timestamp.log"
+
+Start-Transcript -Path $logFile -Append
+Write-Host "Script: $description"
+Write-Host "Log Path: $logPath"
+Write-Host "RMM Mode: $(if ($RMM -eq 1) { 'Yes' } else { 'No' })"
+Write-Host ""

Add before line 143:

+
+Stop-Transcript
🧰 Tools
🪛 PSScriptAnalyzer (1.24.0)

[warning] 68-68: Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

(PSAvoidUsingEmptyCatchBlock)


[warning] 124-124: Empty catch block is used. Please use Write-Error or throw statements in catch blocks.

(PSAvoidUsingEmptyCatchBlock)


[warning] 31-31: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 32-32: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 33-33: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 45-45: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 47-47: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 49-49: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 50-50: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 51-51: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 52-52: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 56-56: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 57-57: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 59-59: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 60-60: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 61-61: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 62-62: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 64-64: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 66-66: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 68-68: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 70-70: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 72-72: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 73-73: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 74-74: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 75-75: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 78-78: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 81-81: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 82-82: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 84-84: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 87-87: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 88-88: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 90-90: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 92-92: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 99-99: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 101-101: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 102-102: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 103-103: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 107-107: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 109-109: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 113-113: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 114-114: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 115-115: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 116-116: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 120-120: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 121-121: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 124-124: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 127-127: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 128-128: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 131-131: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 133-133: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 136-136: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 138-138: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 140-140: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 141-141: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 142-142: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 143-143: File 'veeam-s3-copy-job-diag.ps1' uses Write-Host. Avoid using Write-Host because it might not work in all hosts, does not work when there is no host, and (prior to PS 5.0) cannot be suppressed, captured, or redirected. Instead, use Write-Output, Write-Verbose, or Write-Information.

(PSAvoidUsingWriteHost)


[warning] 18-18: The parameter 's' has been declared but not used.

(PSReviewUnusedParameter)

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

In `@bdr-veeam/veeam-s3-copy-job-diag.ps1` around lines 1 - 143, Add the required
three-part RMM script structure: insert an RMM variable declaration block at the
top that defines/initializes $RMM and includes the comment header, add an input
handling section that detects $RMM, captures $Description and sets $LogPath, and
wrap the entire script logic (the existing diagnostic steps that use
New-VBRBackupCopyJobStorageOptions, Get-VBREncryptionKey, Get-VBRBackupCopyJob,
Set-VBRBackupCopyJob, etc.) inside Start-Transcript/Stop-Transcript so all
output is logged; ensure the script still supports dual execution modes
(interactive vs RMM) and that $LogPath is used for the transcript destination.

Comment on lines +27 to +28
$MY_MODULE_PATH = "$env:ProgramFiles\Veeam\Backup and Replication\Console\"
$env:PSModulePath = $env:PSModulePath + "$([System.IO.Path]::PathSeparator)$MY_MODULE_PATH"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use camelCase for variable names.

Variable $MY_MODULE_PATH should follow camelCase convention. Similar issues exist throughout the script with $PWSH_PATH (line 5), $VBD (line 13), $VBR (line 14), $D (line 15), $N (line 19), $F (line 20), $KEY (line 53), $OPTS (lines 65+), $CMD (line 35), $P (line 37), $LINE (line 38), $PN (line 68), $CJ (line 118), $T (line 80), and $CL (line 104).

As per coding guidelines, PowerShell variables should use camelCase naming.

📝 Example fix for this section
-$MY_MODULE_PATH = "$env:ProgramFiles\Veeam\Backup and Replication\Console\"
-$env:PSModulePath = $env:PSModulePath + "$([System.IO.Path]::PathSeparator)$MY_MODULE_PATH"
+$myModulePath = "$env:ProgramFiles\Veeam\Backup and Replication\Console\"
+$env:PSModulePath = $env:PSModulePath + "$([System.IO.Path]::PathSeparator)$myModulePath"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$MY_MODULE_PATH = "$env:ProgramFiles\Veeam\Backup and Replication\Console\"
$env:PSModulePath = $env:PSModulePath + "$([System.IO.Path]::PathSeparator)$MY_MODULE_PATH"
$myModulePath = "$env:ProgramFiles\Veeam\Backup and Replication\Console\"
$env:PSModulePath = $env:PSModulePath + "$([System.IO.Path]::PathSeparator)$myModulePath"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bdr-veeam/veeam-s3-copy-job-diag.ps1` around lines 27 - 28, The script uses
ALL_CAPS/Pascal variables instead of camelCase; rename each listed variable
(e.g., $MY_MODULE_PATH -> $myModulePath, $PWSH_PATH -> $pwshPath, $VBD -> $vbd,
$VBR -> $vbr, $D -> $d (or better descriptive name), $N -> $n, $F -> $f, $KEY ->
$key, $OPTS -> $opts, $CMD -> $cmd, $P -> $p, $LINE -> $line, $PN -> $pn, $CJ ->
$cj, $T -> $t, $CL -> $cl) to camelCase and update every usage throughout the
file (including concatenations, environment assignments like $env:PSModulePath,
function calls, and comparisons). Ensure you also update any comments or string
interpolations that reference the old names and run a quick local lint/test to
confirm no remaining references to the old variable names.

Write-Host ""
Write-Host " Trying Set-VBRBackupCopyJob -StorageOptions..."
if ($OPTS) {
Set-VBRBackupCopyJob -Job $CJ -StorageOptions $OPTS
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling to Set-VBRBackupCopyJob.

The call to Set-VBRBackupCopyJob lacks error handling. If this operation fails, the error will propagate to the outer catch block on line 138, but success is reported on line 131 before the command completes. This could report false success if the cmdlet throws an error.

🛡️ Proposed fix
         Write-Host "  Trying Set-VBRBackupCopyJob -StorageOptions..."
         if ($OPTS) {
-            Set-VBRBackupCopyJob -Job $CJ -StorageOptions $OPTS
-            Write-Host "  [OK] Encryption set!"
+            try {
+                Set-VBRBackupCopyJob -Job $CJ -StorageOptions $OPTS
+                Write-Host "  [OK] Encryption set!"
+            } catch {
+                Write-Host "  FAILED to set encryption: $_"
+            }
         } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Set-VBRBackupCopyJob -Job $CJ -StorageOptions $OPTS
Write-Host " Trying Set-VBRBackupCopyJob -StorageOptions..."
if ($OPTS) {
try {
Set-VBRBackupCopyJob -Job $CJ -StorageOptions $OPTS
Write-Host " [OK] Encryption set!"
} catch {
Write-Host " FAILED to set encryption: $_"
}
} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bdr-veeam/veeam-s3-copy-job-diag.ps1` at line 130, The Set-VBRBackupCopyJob
call lacks local error handling and reports success before confirming
completion; update the code that calls Set-VBRBackupCopyJob with ErrorAction
Stop (or wrap it in a try block) and add a dedicated catch that logs the failure
(including $CJ and $OPTS context and the exception) and sets an appropriate
error flag or returns/throws so the outer flow doesn't report success; only emit
the success message after Set-VBRBackupCopyJob completes without error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant