diff --git a/Backup-Dashboard b/Backup-Dashboard deleted file mode 160000 index ba79828..0000000 --- a/Backup-Dashboard +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ba798284dfcd1f03e5e5f5c4b27166c340ae95fc diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 06d7405..e69de29 100644 Binary files a/backend/app/__init__.py and b/backend/app/__init__.py differ diff --git a/backend/app/settings.py b/backend/app/settings.py index 1e40664..aa6c20f 100644 --- a/backend/app/settings.py +++ b/backend/app/settings.py @@ -5,12 +5,9 @@ from pathlib import Path # The modal shows this many days of run history; retention can never be shorter. HISTORY_DAYS = 30 -<<<<<<< HEAD -======= # Shared by the systemd service (EnvironmentFile=) and manage.py, so both always use the same DB. DEFAULT_ENV_FILE = "/etc/backup-dashboard/backup-dashboard.env" ->>>>>>> add-backend-api @dataclass(frozen=True) class Settings: @@ -20,13 +17,6 @@ class Settings: serve_static: bool = False -<<<<<<< HEAD -def load_settings() -> Settings: - return Settings( - db_path=Path(os.environ.get("BACKUP_DASH_DB", "backup-dashboard.db")), - retention_days=max(HISTORY_DAYS, int(os.environ.get("BACKUP_DASH_RETENTION_DAYS", "90"))), - serve_static=os.environ.get("BACKUP_DASH_SERVE_STATIC") == "1", -======= def read_env_file(path: Path) -> dict[str, str]: """Parse simple KEY=VALUE lines (the subset systemd's EnvironmentFile accepts).""" values = {} @@ -50,5 +40,4 @@ def load_settings() -> Settings: db_path=Path(env.get("BACKUP_DASH_DB", "backup-dashboard.db")), retention_days=max(HISTORY_DAYS, int(env.get("BACKUP_DASH_RETENTION_DAYS", "90"))), serve_static=env.get("BACKUP_DASH_SERVE_STATIC") == "1", ->>>>>>> add-backend-api ) diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py index 06d7405..e69de29 100644 Binary files a/backend/tests/__init__.py and b/backend/tests/__init__.py differ diff --git a/collectors/veeam/Install-VeeamCollector.ps1 b/collectors/veeam/Install-VeeamCollector.ps1 index 74ab304..e7ddb24 100644 --- a/collectors/veeam/Install-VeeamCollector.ps1 +++ b/collectors/veeam/Install-VeeamCollector.ps1 @@ -22,7 +22,7 @@ param( [Parameter(Mandatory)][string]$ApiUrl, [Parameter(Mandatory)][string]$Tenant, [string]$SourceSystem = 'Veeam B&R', - [ValidateRange(5, 1440)][int]$IntervalMinutes = 15, + [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 diff --git a/collectors/veeam/MockVeeam.ps1 b/collectors/veeam/MockVeeam.ps1 deleted file mode 100644 index 230785c..0000000 --- a/collectors/veeam/MockVeeam.ps1 +++ /dev/null @@ -1,83 +0,0 @@ -# Stand-ins for the Veeam cmdlets, shaped like the real objects' properties the collector reads. -# Dot-source this before running the collector to try it without a VBR server: -# -# . ./tests/MockVeeam.ps1 -# ./Send-VeeamBackupStatus.ps1 -ConfigPath ./tests/config.json -DryRun - -# Fixed to the top of the hour, and IDs derived from names/times, so repeated runs see the same -# jobs and sessions, like a real server -$now = Get-Date -Minute 0 -Second 0 -Millisecond 0 -function New-StableGuid([string]$Seed) { - [guid]::new([Security.Cryptography.MD5]::Create().ComputeHash([Text.Encoding]::UTF8.GetBytes($Seed))) -} -$notSet = Get-Date '1900-01-01' - -function New-MockJob($Name, $Type, $NextRun, [bool]$Enabled = $true) { - [pscustomobject]@{ - Id = New-StableGuid $Name; Name = $Name; JobType = $Type - IsScheduleEnabled = $Enabled - ScheduleOptions = [pscustomobject]@{ NextRun = $NextRun } - } -} - -function New-MockSession($Job, $Start, $End, $State, $Result, $Reason = '', $Tasks = @(), $LogRecords = @(), $Size = 0) { - $s = [pscustomobject]@{ - Id = New-StableGuid "$($Job.Name)|$($Start.Ticks)"; JobId = $Job.Id; JobName = $Job.Name - CreationTime = $Start; EndTime = $End; State = $State; Result = $Result - IsRetryMode = $false - Info = [pscustomobject]@{ Reason = $Reason } - BackupStats = [pscustomobject]@{ BackupSize = $Size; DataSize = $Size * 3 } - Logger = [pscustomobject]@{ Records = $LogRecords } - Tasks = $Tasks - } - $s.Logger | Add-Member ScriptMethod GetLog { [pscustomobject]@{ UpdatedRecords = $this.Records } } - $s | Add-Member ScriptMethod GetTaskSessions { $this.Tasks } - $s -} - -function New-MockTask($Name, $Status, $Reason = '') { - [pscustomobject]@{ Name = $Name; Status = $Status; Info = [pscustomobject]@{ Reason = $Reason } } -} - -$global:MockJobs = @( - (New-MockJob 'File Server – Nightly' 'Backup' $now.Date.AddDays(1).AddHours(22)), - # Older VBR versions return NextRun as a culture-formatted string - (New-MockJob 'SQL01 – Logs' 'Backup' $now.AddMinutes(40).ToString()), - (New-MockJob 'Offsite Copy' 'BackupSync' $null), - (New-MockJob 'Old Exchange' 'Backup' $now.AddDays(1) $false), - # VBR 12+ backup copy job (immediate mode, no schedule): the "policy" job Get-VBRJob returns - (New-MockJob 'Cloud Copy' 'SimpleBackupCopyPolicy' $null) -) -$fs, $sql, $copy, $old, $cloud = $global:MockJobs - -# Its hidden worker job, one per source job. Get-VBRJob doesn't return it, and Get-VBRBackupSession -# returns only the worker's sessions. Shape taken from a real VBR 13 server. -$cloudWorker = [pscustomobject]@{ - Id = New-StableGuid 'Cloud Copy\File Server – Nightly'; Name = 'Cloud Copy\File Server – Nightly' - JobType = 'SimpleBackupCopyWorker'; PolicyJobId = $cloud.Id -} -function New-MockWorkerSession($Start, $End, $Result) { - $s = New-MockSession $cloudWorker $Start $End 'Stopped' $Result -Size 40GB - $s | Add-Member ScriptMethod GetJob { $cloudWorker } - $s -} - -$global:MockSessions = @( - (New-MockSession $fs $now.AddDays(-1).AddHours(-2) $now.AddDays(-1).AddHours(-1) 'Stopped' 'Success' -Size 128GB -Tasks @( - (New-MockTask 'FS01' 'Success'), (New-MockTask 'FS02' 'Success'))), - (New-MockSession $fs $now.AddMinutes(-20) $notSet 'Working' 'None' -Tasks @((New-MockTask 'FS01' 'InProgress'))), - (New-MockSession $sql $now.AddHours(-1) $now.AddMinutes(-55) 'Stopped' 'Warning' -Size 2GB ` - -Tasks @((New-MockTask 'SQL01' 'Warning' 'Failed to truncate transaction logs for database "ERP_PROD"'))), - (New-MockSession $copy $now.AddHours(-5) $now.AddHours(-4) 'Stopped' 'Failed' 'Error: Unable to connect to repository NAS02' ` - -LogRecords @( - [pscustomobject]@{ Title = 'Preparing backup chain'; Status = 'ESucceeded' }, - [pscustomobject]@{ Title = 'Network path not found (0x80070035)'; Status = 'EFailed' })), - (New-MockSession $old $now.AddDays(-10) $now.AddDays(-10).AddHours(1) 'Stopped' 'Success'), # outside 48h lookback - (New-MockWorkerSession $now.AddDays(-1).AddMinutes(-55) $now.AddDays(-1).AddMinutes(-30) 'Success'), - (New-MockWorkerSession $now.AddMinutes(-55) $now.AddMinutes(-35) 'Warning'), - # A session from a job the collector doesn't know (e.g. deleted): must be ignored, not crash - (New-MockSession ([pscustomobject]@{ Id = New-StableGuid 'gone'; Name = 'Deleted job' }) $now.AddHours(-2) $now.AddHours(-1) 'Stopped' 'Success') -) - -function Get-VBRJob { [CmdletBinding()] param() $global:MockJobs } -function Get-VBRBackupSession { [CmdletBinding()] param() $global:MockSessions } diff --git a/collectors/veeam/README.md b/collectors/veeam/README.md index 2346947..65cd8c0 100644 --- a/collectors/veeam/README.md +++ b/collectors/veeam/README.md @@ -13,19 +13,16 @@ account can't run it. The installer checks for this before changing anything. ```bash python manage.py create-key veeam-acme ``` -2. Copy this folder to the VBR server and run: -3. Copy this folder to the VBR server and run, from PowerShell 7 (`pwsh`): +2. Copy this folder to the VBR server and run, from PowerShell 7 (`pwsh`): ```powershell .\Install-VeeamCollector.ps1 -ApiUrl https://backups.example.com -Tenant "Acme Corp" ``` It prompts for the key, then: - writes `C:\ProgramData\BackupDashboard\veeam-collector.json` (readable only by SYSTEM and Administrators) - copies the script to `C:\Program Files\BackupDashboard\` - - registers the scheduled task **Backup Dashboard - Veeam collector** (SYSTEM, every 15 minutes) -4. Preview the payloads, then backfill 30 days so the history view is populated straight away: - - registers the scheduled task **Backup Dashboard - Veeam collector** (SYSTEM, every 15 minutes, + - registers the scheduled task **Backup Dashboard - Veeam collector** (SYSTEM, every 60 minutes, run by `C:\Program Files\PowerShell\7\pwsh.exe`; use `-PwshPath` if it's installed elsewhere) -5. Preview the payloads, then backfill 30 days so the history view is populated straight away +3. Preview the payloads, then backfill 30 days so the history view is populated straight away (from `pwsh`): ```powershell & 'C:\Program Files\BackupDashboard\Send-VeeamBackupStatus.ps1' -DryRun @@ -47,11 +44,10 @@ delete the scheduled task and both `BackupDashboard` folders. | Message | Session reason, each VM or object that didn't succeed, and warning/error lines from the session log | | Size | `BackupStats.BackupSize` | -======= **Backup copy jobs (VBR 12+):** Veeam records their sessions under hidden per-source "worker" jobs. The collector files each worker session under its copy job (via the worker's `PolicyJobId`), so a copy job gets one run per source job it copies, with the source in `copied_from`. -i + Each run covers the last 48 hours (`LookbackHours`), so a few missed runs (server rebooting, API down) fill in automatically. Sessions already delivered are recorded in `veeam-collector.state.json`. Running sessions are re-sent until they finish. @@ -62,7 +58,6 @@ down) fill in automatically. Sessions already delivered are recorded in - Exit codes: `0` OK, `1` some jobs failed to send (they're retried next run), `2` setup failed (config, Veeam module, or Veeam connection). - `API key rejected`: the key was revoked or mistyped. Re-run the installer with a new key. -======= - A job shows **Unknown** on the dashboard: the collector sent the job but matched no sessions to it. Run `.\Get-VeeamCollectorDiagnostics.ps1` (read-only). It lists sessions whose job ID matches no job, plus the type and link properties of the jobs left without sessions. diff --git a/collectors/veeam/Send-VeeamBackupStatus.ps1 b/collectors/veeam/Send-VeeamBackupStatus.ps1 index 79ff658..50a75a3 100644 --- a/collectors/veeam/Send-VeeamBackupStatus.ps1 +++ b/collectors/veeam/Send-VeeamBackupStatus.ps1 @@ -3,7 +3,7 @@ Reports Veeam Backup & Replication job results to the Backup Dashboard. .DESCRIPTION - Runs on the VBR server, normally as a SYSTEM scheduled task every 15 minutes + Runs on the VBR server, normally as a SYSTEM scheduled task every 60 minutes (see Install-VeeamCollector.ps1). For every backup, replica and backup copy job it sends: - the job's next scheduled run (drives the dashboard's Overdue flag), and - every session from the last LookbackHours that the dashboard hasn't already received diff --git a/deploy/backup-dashboard-api.service b/deploy/backup-dashboard-api.service index 805761e..e91b327 100644 --- a/deploy/backup-dashboard-api.service +++ b/deploy/backup-dashboard-api.service @@ -6,13 +6,8 @@ After=network.target User=backupdash Group=backupdash WorkingDirectory=/opt/backup-dashboard/backend -<<<<<<< HEAD -Environment=BACKUP_DASH_DB=/var/lib/backup-dashboard/backup-dashboard.db -Environment=BACKUP_DASH_RETENTION_DAYS=90 -======= # Settings live in this file so manage.py reads exactly the same ones (see deploy/backup-dashboard.env) EnvironmentFile=/etc/backup-dashboard/backup-dashboard.env ->>>>>>> add-backend-api # Bound to localhost: only nginx can reach it, so nginx's auth can't be bypassed from outside. # One worker is plenty at this scale and keeps SQLite writes simple. ExecStart=/opt/backup-dashboard/venv/bin/uvicorn --factory app.main:create_app --host 127.0.0.1 --port 8000 --proxy-headers