Veeam S3 Inventory, Backblaze Repo Automation, Copy Job Config#46
Veeam S3 Inventory, Backblaze Repo Automation, Copy Job Config#46
Conversation
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>
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>
📝 WalkthroughWalkthroughThis PR reorganizes Veeam backup automation scripts from a flat structure into a hierarchical Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (9)
bdr-veeam/README.md (1)
72-85: Field type clarification needed.Line 79 lists
CUSTOM_FIELD_ORPHANS_FOUNDas "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_DAYScontains 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-Transcriptfor 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 = 40and truncates. But if the repo name is shorter (e.g., 45 chars), the"S3 Copy - $REPO_SHORT".Length -gt 50check passes incorrectly because"S3 Copy - ".Lengthis 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_BYTESis 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]::MinValueAnd 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
📒 Files selected for processing (14)
bdr-veeam/Find-VeeamS3OrphanedBackupData.ps1bdr-veeam/README.mdbdr-veeam/veeam-add-backup-copy-job.ps1bdr-veeam/veeam-add-backup-repo.ps1bdr-veeam/veeam-clear-management-server.ps1bdr-veeam/veeam-configure-backblaze-repo.ps1bdr-veeam/veeam-configure-local-backup-jobs.ps1bdr-veeam/veeam-configure-s3-copy-job.ps1bdr-veeam/veeam-inventory.ps1bdr-veeam/veeam-remove-management-server.ps1bdr-veeam/veeam-s3-copy-job-diag.ps1iaas-backblaze/b2-bucket-audit.ps1iaas-backblaze/b2-fix-lifecycle.ps1veeam/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
| **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`). | ||
|
|
There was a problem hiding this comment.
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.
| 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 "" |
There was a problem hiding this comment.
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.
| 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.
| 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 "" |
There was a problem hiding this comment.
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.
| 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).
| 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)." |
There was a problem hiding this comment.
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.
| 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.
| # 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: $_" | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: https://helpcenter.veeam.com/docs/vbr/powershell/new-vbrbackupwindowoptions.html
- 2: https://helpcenter.veeam.com/docs/backup/powershell/new-vbrbackupwindowoptions.html
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.
| # 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 "==========================================" |
There was a problem hiding this comment.
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:
- RMM variable declaration block (with comment header)
- Input handling section (detecting $RMM, capturing $Description, setting $LogPath)
- 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.
| $MY_MODULE_PATH = "$env:ProgramFiles\Veeam\Backup and Replication\Console\" | ||
| $env:PSModulePath = $env:PSModulePath + "$([System.IO.Path]::PathSeparator)$MY_MODULE_PATH" |
There was a problem hiding this comment.
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.
| $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 |
There was a problem hiding this comment.
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.
| 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.
Summary
Key Technical Fixes
Cleanup
veeam/veeam-add-backup-repo.ps1veeam-configure-*patternTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation