Backup & Restore (Admin)

Export Configuration Backup

GET /api/v1/admin/backup

Export all configuration as a downloadable JSON file. Admin-only.

Response: 200 OK — application/json download with Content-Disposition: attachment; filename="osprey-config-YYYYMMDD-HHMMSS.json"

{
  "version": "1.0",
  "exported_at": "2026-02-24T12:00:00Z",
  "networks": [...],
  "autonomous_systems": [...],
  "routing_domains": [...],
  "protocol_instances": [...],
  "areas": [...],
  "collector_configs": [...],
  "alert_rules": [...],
  "notification_channels": [...],
  "snmp_targets": [...],
  "users": [{"username": "admin", "display_name": "Administrator", "role": "admin", "is_active": true}],
  "system_settings": {"topology.stale_retention_hours": "168", ...},
  "maintenance_windows": [...]
}

Notes:

Error responses:


Export Database Dump

GET /api/v1/admin/backup/database

Full PostgreSQL dump via pg_dump --compress=gzip:6. Returns the compressed dump as a streaming download. Admin-only. The response is streamed directly from pg_dump stdout -- no memory buffering on the server regardless of database size. Rate-limited to one database backup per user per minute (429).

The 200 and the download headers are withheld until pg_dump has actually produced its first byte. pg_dump starts successfully and only then exits non-zero for a server-version mismatch, an authentication failure or an unreachable database, so committing the status line earlier delivered those failures as a successful download of an empty .sql.gz. A failure after the first byte cannot change the status code -- the response is already streaming -- and surfaces as a truncated body plus a server-side error log.

Response: 200 OK — application/gzip download with Content-Disposition: attachment; filename="osprey-database-YYYYMMDD-HHMMSS.sql.gz"

Error responses:


List Deleted-Area Tombstones

GET /api/v1/admin/deleted-areas

Admin-only. Returns { "deleted_areas": [ { id, area_label, area_name, protocol, network_name, deleted_at } ] }, newest first — the tombstones every hierarchy delete transaction writes. Topology history keeps dead area UUIDs by design, and these records are the only surviving way to name them.

Purge History of Deleted Areas

DELETE /api/v1/admin/history?area_ids=<uuid>,<uuid>

Admin-only. Deletes topology events, snapshots and incidents for the given dead area UUIDs (typically taken from the tombstone list) — the after-the-fact purge for history retained when an area was deleted without ?delete_history=true. Returns { "purged_areas": n }. 400 on missing/malformed area_ids.

Hierarchy delete semantics: every hierarchy DELETE (area, PI, RD, AS) is refused with 409 when an enabled recorder homed outside the delete set feeds any affected area — the cascade cannot stop that recorder and it would re-create the entity within the hour. The 409 body names the recorder and its home area; ?force=true proceeds anyway (history, layouts and alert scopes will not survive the round-trip). All nested hierarchy routes also verify the parent chain and return 404 for a child addressed under the wrong parent, and every hierarchy PUT now has true patch semantics: only fields present in the body are applied.

Restore Configuration Backup

POST /api/v1/admin/restore

Import configuration from a previously exported JSON backup file. Admin-only.

Body size limit: 10 MB

Request body: The same JSON structure as the GET /api/v1/admin/backup response. Only backup format version 1.0 is accepted; an unknown version is rejected instead of being interpreted as the current schema.

Response: 200 OK

{
  "created": 5,
  "skipped": 12,
  "errors": ["area \"0.0.0.1\": parent PI not found"]
}

Response fields:

Field Type Description
created integer Number of new entities created
skipped integer Number of entities skipped (already exist, matched by name)
errors string[] List of non-fatal errors encountered during import

Behavior:

Error responses:


Restore Database

POST /api/v1/admin/restore/database

Restore a full PostgreSQL database from a previously exported .sql or .sql.gz dump file. Admin-only. Osprey first fully decompresses, size-checks, and screens the SQL in bounded chunks into a private disk-backed staging file. No database command runs until validation succeeds; large single-row INSERT/COPY payloads have no separate scanner line limit.

The screener tracks SQL tokens across line boundaries, standard and PostgreSQL escape strings, identifiers, nested comments, dollar-quoted bodies, and COPY ... FROM stdin data. Role/user administration, grants/revokes, LOAD, DO, REASSIGN OWNED, FROM PROGRAM, transaction-control statements (BEGIN/COMMIT/ROLLBACK/SAVEPOINT and aliases), and arbitrary psql meta-commands are rejected; harmless words inside quoted values or COPY data are not treated as statements.

Warning: A successful restore replaces all existing data. The ag_catalog/public schema reset, import, and verification of essential Osprey schema tables run in one PostgreSQL transaction, so prohibited input, malformed uploads, non-Osprey SQL files, and SQL import failures preserve the existing database.

Content-Type: multipart/form-data

Capacity policy: api.max_restore_size is an optional hard ceiling applying to both HTTP upload and decompressed SQL; default 0 disables that arbitrary ceiling. The effective limit follows current disk capacity. api.restore_staging_dir selects the staging filesystem (packaged default /var/lib/osprey/restore), while api.restore_min_free_bytes (default 5 GiB) and api.restore_min_free_percent (default 10) define the larger mandatory reserve. Osprey additionally retains one staged-SQL size for database construction and checks api.restore_database_dir (default /var/lib/postgresql) for local PostgreSQL before import. A custom staging path in the Debian package also requires a matching systemd ReadWritePaths= drop-in because the API unit uses ProtectSystem=strict.

Form fields:

Field Type Description
file file A .sql or .sql.gz file from GET /admin/backup/database

Response: 200 OK

{
  "status": "ok",
  "message": "Database restored from osprey-database-20260306-120000.sql. Services should be restarted."
}

Error responses: