<# .SYNOPSIS Installs the Veeam collector on a VBR server as a SYSTEM scheduled task. .DESCRIPTION - Copies Send-VeeamBackupStatus.ps1 to Program Files (only admins can modify it, which matters because it runs as SYSTEM). - Writes the config to ProgramData\BackupDashboard and restricts that folder to SYSTEM and Administrators, since it contains the API key. - Registers a scheduled task that runs every IntervalMinutes. Safe to re-run to change settings or update the script. .EXAMPLE .\Install-VeeamCollector.ps1 -ApiUrl https://backups.example.com -Tenant "Acme Corp" (prompts for the API key) #> #Requires -RunAsAdministrator [CmdletBinding()] param( [Parameter(Mandatory)][string]$ApiUrl, [Parameter(Mandatory)][string]$Tenant, [string]$SourceSystem = 'Veeam B&R', [ValidateRange(5, 1440)][int]$IntervalMinutes = 60, [ValidateRange(1, 720)][int]$LookbackHours = 48, [string]$InstallDir = (Join-Path $env:ProgramFiles 'BackupDashboard') ) $ErrorActionPreference = 'Stop' $TaskName = 'Backup Dashboard - Veeam collector' $DataDir = Join-Path $env:ProgramData 'BackupDashboard' $ConfigPath = Join-Path $DataDir 'veeam-collector.json' if ($ApiUrl -notmatch '^https://') { Write-Warning 'ApiUrl is not HTTPS: the API key would be sent in clear text.' } # Read the key as a SecureString so it isn't echoed to the console or kept in shell history $secure = Read-Host -AsSecureString 'API key (from: python manage.py create-key )' $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) try { $apiKey = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } if (-not $apiKey.StartsWith('bdk_')) { throw "That doesn't look like a dashboard API key (expected 'bdk_...')." } # Config folder: SYSTEM + Administrators only (SIDs so this works on non-English Windows) New-Item -ItemType Directory -Path $DataDir -Force | Out-Null & icacls.exe $DataDir /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' | Out-Null if ($LASTEXITCODE) { throw "icacls failed on $DataDir" } [ordered]@{ ApiUrl = $ApiUrl.TrimEnd('/') ApiKey = $apiKey Tenant = $Tenant SourceSystem = $SourceSystem LookbackHours = $LookbackHours } | ConvertTo-Json | Set-Content -Path $ConfigPath -Encoding UTF8 New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null $scriptPath = Join-Path $InstallDir 'Send-VeeamBackupStatus.ps1' Copy-Item -Path (Join-Path $PSScriptRoot 'Send-VeeamBackupStatus.ps1') -Destination $scriptPath -Force Unblock-File -Path $scriptPath $action = New-ScheduledTaskAction -Execute 'pwsh.exe' ` -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$scriptPath`"" $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) ` -RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes) $principal = New-ScheduledTaskPrincipal -UserId 'S-1-5-18' -LogonType ServiceAccount -RunLevel Highest $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 30) ` -MultipleInstances IgnoreNew -StartWhenAvailable Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal ` -Settings $settings -Description 'Sends Veeam job results to the Backup Dashboard' -Force | Out-Null Write-Host "Installed. Config: $ConfigPath" Write-Host "Task '$TaskName' runs every $IntervalMinutes minutes as SYSTEM." Write-Host '' Write-Host 'Next steps:' Write-Host " 1. Preview what will be sent: & '$scriptPath' -DryRun" Write-Host " 2. Backfill 30 days of history: & '$scriptPath' -LookbackHours 720" Write-Host " 3. Check the log: Get-Content '$DataDir\veeam-collector.log' -Tail 20"