The merge of add-backend-api into main committed unresolved conflicts
and corrupted uploads; the API could not import on main.
- backend/app/settings.py, deploy/backup-dashboard-api.service,
collectors/veeam/README.md: remove conflict markers, restoring the
add-backend-api versions
- backend/app/__init__.py, backend/tests/__init__.py: were 1024 null
bytes ("source code string cannot contain null bytes"); now empty
- Remove the accidental Backup-Dashboard gitlink at the repo root and a
duplicate collectors/veeam/MockVeeam.ps1 (identical to tests/)
- Restore the 60-minute collector interval from 17ee303, which the
merge reverted to 15, and update the docs to match
Co-Authored-By: Claude Opus 5.5 <[email protected]>
103 lines
5.1 KiB
PowerShell
103 lines
5.1 KiB
PowerShell
<#
|
|
.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 under PowerShell 7 (pwsh.exe),
|
|
which the Veeam module on VBR 13 expects. Install PowerShell 7 machine-wide (MSI or winget)
|
|
first: Microsoft Store installs can't be run by SYSTEM.
|
|
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'),
|
|
# Defaults to the machine-wide PowerShell 7 install
|
|
[string]$PwshPath
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$TaskName = 'Backup Dashboard - Veeam collector'
|
|
$DataDir = Join-Path $env:ProgramData 'BackupDashboard'
|
|
$ConfigPath = Join-Path $DataDir 'veeam-collector.json'
|
|
|
|
# Find PowerShell 7 before changing anything, so a missing install fails cleanly
|
|
if (-not $PwshPath) {
|
|
$PwshPath = Join-Path $env:ProgramFiles 'PowerShell\7\pwsh.exe'
|
|
if (-not (Test-Path $PwshPath)) {
|
|
$onPath = Get-Command pwsh.exe -ErrorAction SilentlyContinue
|
|
if ($onPath) { $PwshPath = $onPath.Source }
|
|
}
|
|
}
|
|
if (-not (Test-Path $PwshPath)) {
|
|
throw "PowerShell 7 not found at '$PwshPath'. Install it machine-wide (e.g. winget install Microsoft.PowerShell) or pass -PwshPath."
|
|
}
|
|
if ($PwshPath -match '\\WindowsApps\\') {
|
|
throw "'$PwshPath' is a Microsoft Store install, which the SYSTEM account can't run. Install the MSI or winget version instead."
|
|
}
|
|
$pwshVersion = & $PwshPath -NoProfile -NonInteractive -Command '$PSVersionTable.PSVersion.ToString()'
|
|
$pwshMajor = & $PwshPath -NoProfile -NonInteractive -Command '$PSVersionTable.PSVersion.Major'
|
|
if ($LASTEXITCODE -or ($pwshMajor -as [int]) -lt 7) {
|
|
throw "'$PwshPath' isn't a working PowerShell 7 (reported '$pwshVersion')."
|
|
}
|
|
|
|
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 <name>)'
|
|
$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 $PwshPath `
|
|
-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, using PowerShell $pwshVersion ($PwshPath)."
|
|
Write-Host ''
|
|
Write-Host 'Next steps:'
|
|
Write-Host " 1. Preview what will be sent: & '$PwshPath' -File '$scriptPath' -DryRun"
|
|
Write-Host " 2. Backfill 30 days of history: & '$PwshPath' -File '$scriptPath' -LookbackHours 720"
|
|
Write-Host " 3. Check the log: Get-Content '$DataDir\veeam-collector.log' -Tail 20"
|