From 52b892da077852a0ec941ec1b802e1eedcc85237 Mon Sep 17 00:00:00 2001 From: Sakura Akeno Isayeki Date: Sun, 16 Aug 2026 19:58:15 +0200 Subject: [PATCH] feat(drivers/mongodb): add MongoDB driver, docs, packaging, and tests Add a bundled MongoDB driver that discovers authorized databases and produces filtered per-database mongodump archives or directory dumps. Include the driver executable (prepare/connectivity/health verbs), canonical driver reference, restore runbook, example instance and secret files, and guidance in README and guides. Add Debian packaging for backmaster-driver-mongodb, require mongodb-mongosh and mongodb-database-tools, and update the metapackage and package tests. Add CI and unit tests (tests/mongodb-driver.bash) and run the new tests in the workflow. --- .github/workflows/ci.yml | 1 + .github/workflows/deb.yml | 3 +- README.md | 15 +- config/drivers/mongodb.env.example | 22 +++ config/drivers/mongodb.secrets.env.example | 5 + config/instances/nsys-mongodb.env.example | 17 ++ docs/drivers/index.md | 2 + docs/drivers/mongodb.md | 209 ++++++++++++++++++++ docs/guides/configuration.md | 31 ++- docs/guides/index.md | 1 + docs/guides/installation.md | 24 ++- docs/guides/mongodb-restore.md | 115 +++++++++++ docs/guides/operations.md | 46 ++++- docs/guides/secrets.md | 27 +++ docs/guides/systemd.md | 14 +- docs/guides/troubleshooting.md | 34 +++- drivers/mongodb/driver | 218 +++++++++++++++++++++ packaging/build-deb.sh | 67 +++++-- tests/debian-packages.bash | 27 +++ tests/mongodb-driver.bash | 153 +++++++++++++++ 20 files changed, 995 insertions(+), 36 deletions(-) create mode 100644 config/drivers/mongodb.env.example create mode 100644 config/drivers/mongodb.secrets.env.example create mode 100644 config/instances/nsys-mongodb.env.example create mode 100644 docs/drivers/mongodb.md create mode 100644 docs/guides/mongodb-restore.md create mode 100755 drivers/mongodb/driver mode change 100644 => 100755 packaging/build-deb.sh mode change 100644 => 100755 tests/debian-packages.bash create mode 100755 tests/mongodb-driver.bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c63a0d..eee8f4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: - run: bash tests/staging.bash - run: bash tests/archive-formats.bash - run: bash tests/postgres-driver.bash + - run: bash tests/mongodb-driver.bash - run: bash tests/rclone-exporter.bash - run: bash tests/azcopy-exporter.bash - run: bash tests/debian-packages.bash diff --git a/.github/workflows/deb.yml b/.github/workflows/deb.yml index 8b733cd..f83d1f4 100644 --- a/.github/workflows/deb.yml +++ b/.github/workflows/deb.yml @@ -30,7 +30,7 @@ jobs: run: | set -euo pipefail mapfile -t packages < <(find dist -maxdepth 1 -type f -name '*.deb' -print | sort) - test "${#packages[@]}" -eq 5 + test "${#packages[@]}" -eq 6 for package in "${packages[@]}"; do dpkg-deb --info "$package" dpkg-deb --contents "$package" @@ -42,6 +42,7 @@ jobs: test "$("$package_root/usr/bin/backmaster" --version)" = \ "backmaster ${{ steps.nbgv.outputs.semver2 }}" test -x "$package_root/usr/lib/backmaster/drivers/postgres/driver" + test -x "$package_root/usr/lib/backmaster/drivers/mongodb/driver" test -x "$package_root/usr/lib/backmaster/exporters/rclone/exporter" test -x "$package_root/usr/lib/backmaster/exporters/azcopy/exporter" diff --git a/README.md b/README.md index db44796..63bdf4e 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Backmaster is a modular, fleet-oriented backup orchestrator. A **driver** create a consistent local backup, an **exporter** publishes it, and the core coordinates naming, staging, distributed locking, fallback, retention, and health checks. -PostgreSQL is the first bundled driver. Exporters are available for Microsoft's -AzCopy and for rclone; adding MongoDB, mail, or a new storage service does not -require changing the core. +Bundled drivers cover PostgreSQL and MongoDB. Exporters are available for +Microsoft's AzCopy and for rclone; adding another source or storage service +does not require changing the core. ## Start here @@ -17,10 +17,12 @@ require changing the core. | Protect and rotate credentials | [Secrets and credentials](docs/guides/secrets.md) | | Set up systemd services and schedules | [systemd units and timers](docs/guides/systemd.md) | | Back up PostgreSQL/Patroni | [PostgreSQL driver](docs/drivers/postgresql.md) | +| Back up MongoDB | [MongoDB driver](docs/drivers/mongodb.md) | | Configure Azure Blob or another destination | [rclone exporter](docs/exporters/rclone.md) | | Use Microsoft's Azure-native transfer tool | [AzCopy exporter](docs/exporters/azcopy.md) | | Schedule, monitor, and maintain backups | [Operations guide](docs/guides/operations.md) | | Restore PostgreSQL or perform PITR | [PostgreSQL restore runbook](docs/guides/postgres-restore.md) | +| Restore MongoDB | [MongoDB restore runbook](docs/guides/mongodb-restore.md) | | Diagnose a failure | [Troubleshooting](docs/guides/troubleshooting.md) | | Write a driver | [Driver contract](docs/drivers/index.md) | | Write an exporter | [Exporter contract](docs/exporters/index.md) | @@ -34,14 +36,15 @@ sudo apt update sudo apt install backmaster ``` -The `backmaster` metapackage installs: +Available packages include: | Package | Purpose | | --- | --- | | `backmaster-core` | CLI, lifecycle, staging, and systemd units | | `backmaster-driver-postgres` | Physical/WAL and logical PostgreSQL backups | +| `backmaster-driver-mongodb` | Filtered, database-granular MongoDB dumps | | `backmaster-exporter-rclone` | Azure Blob and other rclone destinations | -| `backmaster` | Convenience metapackage for all three components | +| `backmaster` | Convenience metapackage for core, PostgreSQL, and rclone | Azure-only deployments may install `backmaster-exporter-azcopy` instead of the rclone package. The `backmaster` metapackage retains rclone as its default @@ -148,4 +151,6 @@ staging safety, or retention. both base-backup and WAL cleanup can be set to `unlimited`. - PostgreSQL physical mode supports WAL/PITR; logical mode supports per-database selection. +- MongoDB supports per-database filtering and archive or collection-file dumps; + it does not claim deployment-wide PITR. - Backups are not proven until restore drills are automated and monitored. diff --git a/config/drivers/mongodb.env.example b/config/drivers/mongodb.env.example new file mode 100644 index 0000000..827af31 --- /dev/null +++ b/config/drivers/mongodb.env.example @@ -0,0 +1,22 @@ +# Connection URI must not contain credentials or select a database. +MONGODB_URI=mongodb://127.0.0.1:27017/ +MONGODB_AUTH_DATABASE=admin + +MONGODB_DUMP_FORMAT=archive +MONGODB_FILE_NAMING=plain +MONGODB_GZIP=true +MONGODB_NUM_PARALLEL_COLLECTIONS=4 +MONGODB_INCLUDE_SYSTEM_DATABASES=false +MONGODB_DUMP_DB_USERS_AND_ROLES=false + +# Exact, newline-delimited names. Empty include means every visible database. +# MONGODB_DATABASE_INCLUDE=$'application\nanalytics' +# MONGODB_DATABASE_EXCLUDE=$'scratch\ntest' + +# Optional mongodump read preference document or mode. +# MONGODB_READ_PREFERENCE=secondaryPreferred + +# Optional TLS policy. mongodb+srv:// enables TLS by default. +# MONGODB_TLS_CA_FILE=/etc/ssl/certs/mongodb-ca.pem +# MONGODB_TLS_CERTIFICATE_KEY_FILE=/etc/backmaster/secrets/mongodb-client.pem +# MONGODB_TLS_INSECURE=false diff --git a/config/drivers/mongodb.secrets.env.example b/config/drivers/mongodb.secrets.env.example new file mode 100644 index 0000000..2557e72 --- /dev/null +++ b/config/drivers/mongodb.secrets.env.example @@ -0,0 +1,5 @@ +# Keep this file readable only by root and the Backmaster service identity. +MONGODB_USERNAME=backmaster +MONGODB_PASSWORD=REPLACE_ME +# MONGODB_AUTH_MECHANISM=SCRAM-SHA-256 +# MONGODB_TLS_CERTIFICATE_KEY_FILE_PASSWORD=REPLACE_ME diff --git a/config/instances/nsys-mongodb.env.example b/config/instances/nsys-mongodb.env.example new file mode 100644 index 0000000..b7e667c --- /dev/null +++ b/config/instances/nsys-mongodb.env.example @@ -0,0 +1,17 @@ +INSTANCE_NAME=production-mongodb +DRIVER=mongodb +EXPORTER=rclone +NODE_NAME=axon + +DRIVER_CONFIG=/etc/backmaster/drivers/mongodb/production-mongodb.env +DRIVER_SECRET_FILE=/etc/backmaster/secrets/production-mongodb-driver.env +EXPORTER_CONFIG=/etc/backmaster/exporters/rclone/production-mongodb.env +EXPORTER_SECRET_FILE=/etc/backmaster/secrets/production-mongodb-exporter.env + +STAGING_ROOT=/var/lib/backmaster +BACKUP_ARCHIVE_FORMAT=files +MAX_AGE_SECONDS=82800 +CONSUL_LOCK_KEY=service/backmaster/production-mongodb +CONSUL_LOCK_TIMEOUT=30s +BACKUP_NAME_MODE=daily-time +BACKUP_NAME_SUFFIX_MODE=hostname diff --git a/docs/drivers/index.md b/docs/drivers/index.md index dfbef02..ba0ec66 100644 --- a/docs/drivers/index.md +++ b/docs/drivers/index.md @@ -8,6 +8,8 @@ storage or credentials. - [PostgreSQL](postgresql.md) supports physical base backups with WAL and filtered logical dumps. +- [MongoDB](mongodb.md) supports filtered, database-granular `mongodump` + archives or collection-file directories. Each bundled driver page is the canonical reference for that component's settings, defaults, accepted values, credentials, output, dependencies, and diff --git a/docs/drivers/mongodb.md b/docs/drivers/mongodb.md new file mode 100644 index 0000000..819e52b --- /dev/null +++ b/docs/drivers/mongodb.md @@ -0,0 +1,209 @@ +# MongoDB driver + +The MongoDB driver creates logical, database-granular backups with the official +MongoDB Database Tools. It discovers every database visible to the backup role, +applies exact include and exclude filters, and invokes `mongodump` once per +selected database. + +Two payload layouts are available: + + +| Format | Per-database output | Best for | +| --- | --- | --- | +| `archive` | One `.archive` or `.archive.gz` file | Selective downloads and simple restores | +| `directory` | A directory of BSON and metadata files | Collection-level inspection and selective restore | + + +Archive with gzip and plain naming is the default. It produces one recognizable, +independently downloadable file per database. This driver does not create +storage-engine snapshots or continuous point-in-time recovery. + +## Configuration reference + +The instance selects the policy file with `DRIVER_CONFIG` and may load a +protected `DRIVER_SECRET_FILE`. The secret file is loaded second and wins when +both files set the same variable. + + +| Setting | Required/default | Meaning | +| --- | --- | --- | +| `DRIVER_CONFIG` | required | Readable MongoDB driver policy file | +| `DRIVER_SECRET_FILE` | empty | Optional protected credentials file | +| `MONGODB_URI` | required | `mongodb://` or `mongodb+srv://` deployment URI; do not select a database | +| `MONGODB_USERNAME` | empty | Authentication username; keep in the secret file | +| `MONGODB_PASSWORD` | empty | Authentication password; keep in the secret file | +| `MONGODB_AUTH_DATABASE` | `admin` | Authentication source passed to both tools | +| `MONGODB_AUTH_MECHANISM` | tool default | Optional SCRAM, X.509, AWS, GSSAPI, or PLAIN mechanism supported by the installed tools | +| `MONGODB_DUMP_FORMAT` | `archive` | `archive` for one file per database or `directory` for BSON/metadata trees | +| `MONGODB_FILE_NAMING` | `plain` | `plain` database names or opaque `sha256` top-level names | +| `MONGODB_GZIP` | `true` | Enable `mongodump --gzip` | +| `MONGODB_NUM_PARALLEL_COLLECTIONS` | `4` | Positive `mongodump --numParallelCollections` value for each database | +| `MONGODB_DATABASE_INCLUDE` | empty | Exact newline-delimited database names; empty starts with every visible database | +| `MONGODB_DATABASE_EXCLUDE` | empty | Exact newline-delimited names removed after inclusion | +| `MONGODB_INCLUDE_SYSTEM_DATABASES` | `false` | Include `admin`, `config`, and `local` when selected | +| `MONGODB_DUMP_DB_USERS_AND_ROLES` | `false` | Add `--dumpDbUsersAndRoles` to each per-database dump | +| `MONGODB_READ_PREFERENCE` | tool default | Optional mode or JSON document passed to `mongodump --readPreference` | +| `MONGODB_TLS_CA_FILE` | empty | CA bundle; also enables TLS | +| `MONGODB_TLS_CERTIFICATE_KEY_FILE` | empty | Client certificate/private-key PEM; also enables TLS | +| `MONGODB_TLS_CERTIFICATE_KEY_FILE_PASSWORD` | empty | PEM password; keep in the secret file | +| `MONGODB_TLS_INSECURE` | `false` | Disable certificate and hostname verification; diagnostic use only | + + +Assignments are exported, so AWS IAM credentials and proxy variables supported +by MongoDB tools may be placed in the protected secret file. Prefer workload +identity or short-lived credentials where possible. Do not embed credentials in +`MONGODB_URI`: command-line connection arguments may be visible to other local +processes on systems without process isolation. + +`MONGODB_TLS_INSECURE=true` weakens both certificate-chain and hostname +verification. It should not be used for production backups. A `mongodb+srv://` +URI enables TLS by default according to MongoDB connection-string rules. + +## Discovery and filtering + +Discovery runs `listDatabases` with `authorizedDatabases: true`. The backup role +therefore sees only databases it is permitted to list. An explicitly included +name that is absent or unauthorized fails the backup; it is never silently +ignored. Exclusions win over includes. System databases are removed unless +`MONGODB_INCLUDE_SYSTEM_DATABASES=true`. A final empty selection also fails. + +Use ANSI-C quoting for exact multiline lists: + +```bash +MONGODB_DATABASE_INCLUDE=$'application\nanalytics\nsessions' +MONGODB_DATABASE_EXCLUDE=$'scratch\ntest' +``` + +Plain output names reject path separators, line breaks, `.` and `..`. SHA-256 +naming keeps database names out of top-level archive object names; the index +always preserves the original name. Directory dumps necessarily retain the +MongoDB-created database subdirectory inside their opaque top-level directory. + +## Payload contract + +Default archive output: + +```text +payload/ +├── databases.json +└── databases/ + ├── analytics.archive.gz + └── application.archive.gz +``` + +Directory output: + +```text +payload/ +├── databases.json +└── databases/ + └── application.dump/ + └── application/ + ├── events.bson.gz + └── events.metadata.json.gz +``` + +`databases.json` records the source topology and replica-set name, dump format, +naming and compression policies, collection concurrency, original database +name, and relative path of every dump. The core then adds checksums and the +remote manifest, or wraps the complete payload in a configured whole-backup +archive. + +Every `mongodump --db` invocation has its own read window. Each database dump +can be restored independently, but a backup spanning several databases is not +a single deployment-wide transaction or point-in-time snapshot. `--oplog` +cannot be combined with per-database `--db` dumps and is intentionally not +exposed. Use a MongoDB-supported deployment snapshot or dedicated backup system +when cross-database consistency or PITR is required. + +## Commands + + +| Backmaster command | Driver action | +| --- | --- | +| `backmaster connectivity INSTANCE` | Check tools, authenticate, and run discovery | +| `backmaster health INSTANCE` | Do the connectivity check and report topology plus visible database count | +| `backmaster run INSTANCE` | Discover, filter, dump, checksum, and publish through the configured exporter | + + +The driver executable also implements the standard internal +`prepare PAYLOAD_DIR`, `connectivitycheck`, and `healthcheck` verbs. + +## Dependencies and compatibility + +Install matching, supported releases of `mongodb-mongosh` and +`mongodb-database-tools`. `mongodump` creates BSON data plus collection metadata +and indexes; `mongorestore` is the corresponding restore tool. MongoDB advises +restoring into a compatible MongoDB version or feature compatibility version. +Queryable Encryption collections are not supported by `mongodump`. + +## Setup + +Install the driver with the core and one exporter: + +```bash +sudo apt install \ + backmaster-core \ + backmaster-driver-mongodb \ + backmaster-exporter-rclone +``` + +Copy the packaged examples: + +```bash +docs=/usr/share/doc/backmaster-driver-mongodb/examples + +sudo install -d -m 0755 /etc/backmaster/drivers/mongodb +sudo install -m 0644 \ + "$docs/config/instances/nsys-mongodb.env.example" \ + /etc/backmaster/instances.d/production-mongodb.env +sudo install -m 0644 \ + "$docs/config/drivers/mongodb.env.example" \ + /etc/backmaster/drivers/mongodb/production-mongodb.env +sudo install -m 0640 -o root -g backmaster \ + "$docs/config/drivers/mongodb.secrets.env.example" \ + /etc/backmaster/secrets/production-mongodb-driver.env +``` + +Configure the exporter under the same instance name and use a destination root +not shared with PostgreSQL or another MongoDB instance. Grant the backup role +`listDatabases` and read access to every selected database. Grant the additional +user/role privileges only when `MONGODB_DUMP_DB_USERS_AND_ROLES=true`. + +The packaged services already run as `backmaster`, which is normally sufficient +for TCP or SRV connections. To order the backup after a local server without +changing its user, add matching service drop-ins: + +```ini +[Unit] +After=mongod.service +``` + +Apply the ordering to backup and health services if both need it. `After=` does +not start MongoDB; add `Wants=` only if that coupling is intentional. + +Commission before scheduling: + +```bash +sudo -u backmaster backmaster connectivity production-mongodb +sudo systemctl start backmaster@production-mongodb.service +sudo -u backmaster backmaster health production-mongodb +journalctl -u backmaster@production-mongodb.service -n 200 --no-pager +``` + +Then inspect `databases.json`, download and restore at least one database on an +isolated deployment, and create the timer described in the +[systemd guide](../guides/systemd.md#create-a-backup-timer). See the +[MongoDB restore runbook](../guides/mongodb-restore.md) for exact commands. + +## Sizing and load + +The driver dumps databases sequentially. Within each database, +`MONGODB_NUM_PARALLEL_COLLECTIONS` controls concurrency. Increasing it can +reduce elapsed time but raises source I/O, CPU, memory, connection, and local +file-descriptor pressure. Measure restore speed as well as backup speed. + +With `BACKUP_ARCHIVE_FORMAT=files`, peak staging is approximately the completed +payload plus metadata. Whole-backup archive modes temporarily require both the +payload and its final archive. A failed export intentionally retains the sealed +stage for retry. diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 3884a1d..422c15c 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -2,7 +2,8 @@ This guide covers shared instance configuration. Component-specific behavior is documented with the installed component: see the -[PostgreSQL driver](../drivers/postgresql.md) and +[PostgreSQL](../drivers/postgresql.md) or [MongoDB](../drivers/mongodb.md) +driver and [rclone](../exporters/rclone.md) or [AzCopy](../exporters/azcopy.md) exporter. An instance joins one driver to one exporter. Its name is the filename below @@ -145,6 +146,32 @@ No storage credentials belong in the driver file. The is the canonical list of every driver setting, PostgreSQL/libpq pass-through variable, default, validation rule, and mode-specific dependency. +## MongoDB driver file + +Example: `/etc/backmaster/drivers/mongodb/production-mongodb.env` + +```bash +MONGODB_URI=mongodb://127.0.0.1:27017/ +MONGODB_AUTH_DATABASE=admin +MONGODB_DUMP_FORMAT=archive +MONGODB_FILE_NAMING=plain +MONGODB_GZIP=true +MONGODB_NUM_PARALLEL_COLLECTIONS=4 +MONGODB_INCLUDE_SYSTEM_DATABASES=false +MONGODB_DUMP_DB_USERS_AND_ROLES=false +# MONGODB_DATABASE_INCLUDE=$'application\nanalytics' +# MONGODB_DATABASE_EXCLUDE=$'scratch\ntest' +``` + +Put `MONGODB_USERNAME`, `MONGODB_PASSWORD`, private-key passwords, and cloud +identity secrets in `DRIVER_SECRET_FILE`, not the policy file. Empty inclusion +selects every visible non-system database. Explicit missing or unauthorized +names fail the backup, exclusions win, and an empty final selection fails. + +The [MongoDB driver reference](../drivers/mongodb.md#configuration-reference) +is the canonical setting list and documents TLS, authentication, output +layouts, consistency, permissions, setup, sizing, and health behavior. + ## Rclone exporter file Example: `/etc/backmaster/exporters/rclone/production-postgres.env` @@ -243,3 +270,5 @@ Physical payloads contain `pg_basebackup` tar archives. Logical payloads contain `globals.sql.gz`, `databases.json`, and configurable custom (`.dump`) or plain SQL (`.sql`) dumps under `databases/`. Filenames can be SHA-256 hashes or plain database names; see the [PostgreSQL driver reference](../drivers/postgresql.md). +MongoDB payloads contain `databases.json` plus one archive or BSON directory per +selected database; see the [MongoDB driver reference](../drivers/mongodb.md). diff --git a/docs/guides/index.md b/docs/guides/index.md index 702e9ce..628407f 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -8,6 +8,7 @@ Task-oriented Backmaster documentation: - [systemd units and timers](systemd.md) - [Operations](operations.md) - [PostgreSQL restore and PITR](postgres-restore.md) +- [MongoDB restore](mongodb-restore.md) - [Troubleshooting](troubleshooting.md) Component behavior and extension contracts are documented separately under diff --git a/docs/guides/installation.md b/docs/guides/installation.md index 4ffb8e7..3894339 100644 --- a/docs/guides/installation.md +++ b/docs/guides/installation.md @@ -14,7 +14,8 @@ Consul, jq, systemd, coreutils, and findutils. A usable flow additionally needs: - an exporter package and access to its destination; - synchronized system clocks on all participating nodes. -The PostgreSQL driver depends on `postgresql-client` and `gzip`. The rclone +The PostgreSQL driver depends on `postgresql-client` and `gzip`. The MongoDB +driver depends on `mongodb-mongosh` and `mongodb-database-tools`. The rclone exporter depends on `rclone`; the AzCopy exporter depends on `azcopy`. ## Install Debian packages @@ -44,6 +45,19 @@ sudo apt install \ backmaster-exporter-azcopy ``` +For MongoDB, select its driver explicitly: + +```bash +sudo apt install \ + backmaster-core \ + backmaster-driver-mongodb \ + backmaster-exporter-rclone +``` + +The compatibility `backmaster` metapackage continues to select PostgreSQL and +rclone. Install `backmaster-driver-mongodb` alongside it or use the explicit +component transaction above. + All component packages require the exact same core version. Upgrade them from the same repository transaction rather than mixing files from different builds. @@ -71,9 +85,10 @@ convenience metapackage. | `/var/lib/backmaster/INSTANCE/` | Local staging state | | `/usr/share/doc/backmaster-*/` | Component documentation and examples | -The package creates a generic `backmaster` system user. A source-specific -systemd drop-in may replace it; the PostgreSQL example runs both backup and -health services as `postgres` so local peer authentication and data access work. +The package creates a generic `backmaster` system user. MongoDB connections +normally use that identity. A source-specific systemd drop-in may replace it; +the PostgreSQL example runs both backup and health services as `postgres` so +local peer authentication and data access work. ## Prepare configuration directories @@ -81,6 +96,7 @@ health services as `postgres` so local peer authentication and data access work. sudo install -d -m 0755 \ /etc/backmaster/instances.d \ /etc/backmaster/drivers/postgres \ + /etc/backmaster/drivers/mongodb \ /etc/backmaster/exporters/rclone \ /etc/backmaster/exporters/azcopy sudo install -d -m 0750 -o root -g postgres /etc/backmaster/secrets diff --git a/docs/guides/mongodb-restore.md b/docs/guides/mongodb-restore.md new file mode 100644 index 0000000..2e64757 --- /dev/null +++ b/docs/guides/mongodb-restore.md @@ -0,0 +1,115 @@ +# MongoDB restore runbook + +Restore into an isolated MongoDB deployment first. Never point an unreviewed +restore at production: `mongorestore` can insert into existing collections and +options such as `--drop` are destructive. + +## 1. Select and verify the Backmaster backup + +Choose a backup with a committed `manifest.json`. Download the manifest and +either its individual-file payload or the single archive named in +`manifest.artifact`. Follow the archive extraction and checksum procedure in +the [operations guide](operations.md#restore-and-verification). Do not continue +until the outer archive digest, when present, and `checksums.sha256` both pass. + +The extracted MongoDB payload contains `payload/databases.json`. Inspect it: + +```bash +jq . payload/databases.json +jq -r '.databases[] | [.database, .path] | @tsv' \ + payload/databases.json +``` + +Do not infer database names from hashed paths. The index is authoritative. + +## 2. Prepare the target + +Install a compatible `mongorestore` from MongoDB Database Tools. Create a target +deployment whose version and feature compatibility version are compatible with +the dump. Provision enough capacity for data, indexes, and restore working +space. Keep application clients disconnected until validation finishes. + +Store target credentials in a protected shell variable or tool configuration; +do not reuse the production backup credential. The examples use: + +```bash +target_uri='mongodb://127.0.0.1:27018/' +target_database='application-restore' +``` + +Review the source database's collection validators, collation, indexes, views, +and user-defined roles. Decide whether identities should be restored or +recreated under target-specific policy. + +## 3. Restore an archive dump + +Resolve the indexed path rather than constructing it: + +```bash +source_database='application' +dump_path="$(jq -er --arg database "$source_database" \ + '.databases[] | select(.database == $database) | .path' \ + payload/databases.json)" +gzip_enabled="$(jq -er '.gzip' payload/databases.json)" + +restore_args=(--uri="$target_uri" --archive="payload/$dump_path") +[[ "$gzip_enabled" == false ]] || restore_args+=(--gzip) +restore_args+=(--nsFrom="${source_database}.*" \ + --nsTo="${target_database}.*") + +mongorestore "${restore_args[@]}" +``` + +Omit `--nsFrom` and `--nsTo` to restore the original namespace. Add `--drop` +only after confirming the target is disposable or the replacement is explicitly +approved; it removes target collections before restoring them. + +## 4. Restore a directory dump + +The indexed path points to a top-level dump directory containing a nested +database directory: + +```bash +source_database='application' +dump_path="$(jq -er --arg database "$source_database" \ + '.databases[] | select(.database == $database) | .path' \ + payload/databases.json)" +gzip_enabled="$(jq -er '.gzip' payload/databases.json)" + +restore_args=(--uri="$target_uri") +[[ "$gzip_enabled" == false ]] || restore_args+=(--gzip) +restore_args+=(--nsFrom="${source_database}.*" \ + --nsTo="${target_database}.*" "payload/$dump_path") + +mongorestore "${restore_args[@]}" +``` + +Use `--nsInclude` or `--nsExclude` for a controlled collection subset. Preview +the index and directory contents first; namespace patterns are evaluated by +`mongorestore`, not Backmaster. + +## 5. Validate the restored database + +At minimum: + +1. Compare collection and view inventories. +2. Compare document counts, understanding that live-source writes may make + counts differ between separately timed observations. +3. Confirm validators, collection options, and indexes. +4. Exercise representative application reads against the isolated target. +5. Review `mongorestore` output for skipped or failed documents and indexes. +6. Record the backup name, restore duration, target version, and validation + result in the restore-drill log. + +For a database with critical cross-collection invariants, run application-level +integrity checks. A successful command exit is not proof that the restored +application state is usable. + +## Consistency limitations + +Backmaster invokes `mongodump --db` separately for each selected database. +MongoDB's `--oplog` option cannot be combined with per-database dumps, so the +driver does not claim a deployment-wide snapshot or PITR. Transactions and +writes spanning separate database dump windows may not be mutually consistent. +Use a storage snapshot or MongoDB backup product designed for deployment-wide +consistency when that recovery property is required. diff --git a/docs/guides/operations.md b/docs/guides/operations.md index 4f3dd14..cc625a6 100644 --- a/docs/guides/operations.md +++ b/docs/guides/operations.md @@ -190,7 +190,8 @@ sudo apt install --only-upgrade \ ``` Replace the exporter package with `backmaster-exporter-azcopy` on AzCopy -instances. +instances. Replace `backmaster-driver-postgres` with +`backmaster-driver-mongodb` on MongoDB instances. Then verify: @@ -204,10 +205,43 @@ sudo -u postgres backmaster health production-postgres Configuration below `/etc/backmaster` is administrator-owned. Review release notes and packaged examples before adopting new settings. +## Restore and verification + +Download only backups with a committed `manifest.json`. For the `files` layout, +download `payload/`, `checksums.sha256`, and the manifest into one directory, +then verify from that directory: + +```bash +sha256sum --check checksums.sha256 +``` + +For an archive layout, download the manifest and its `.artifact.file`, verify +the outer digest before extraction, then verify the contained files: + +```bash +archive="$(jq -er '.artifact.file' manifest.json)" +printf '%s %s\n' "$(jq -er '.artifact.sha256' manifest.json)" "$archive" | \ + sha256sum --check - + +case "$(jq -er '.artifact.format' manifest.json)" in + zip) unzip "$archive" ;; + tar.gz) tar -xzf "$archive" ;; + tar.xz) tar -xJf "$archive" ;; + tar.zst) tar --zstd -xf "$archive" ;; +esac + +sha256sum --check checksums.sha256 +``` + +Extract into an empty, isolated restore directory. A checksum proves transport +integrity, not source consistency or application usability. Continue with the +driver-specific restore runbook. + ## Routine restore drills -At least on every material PostgreSQL or storage change—and regularly -thereafter—restore the newest backup to an isolated host, verify checksums, -start PostgreSQL on a non-production port, and run application-level checks. -Periodically test a timestamped PITR and a backup created by each fallback node. -Record restore duration against the recovery-time objective. +At least on every material database or storage change—and regularly +thereafter—restore the newest backup to an isolated host, verify checksums, and +run application-level checks. For PostgreSQL physical mode, periodically test +a timestamped PITR. For MongoDB, test both a complete database and a selective +collection restore. Exercise a backup created by each fallback node and record +restore duration against the recovery-time objective. diff --git a/docs/guides/secrets.md b/docs/guides/secrets.md index 6570b23..ec6b2d3 100644 --- a/docs/guides/secrets.md +++ b/docs/guides/secrets.md @@ -114,6 +114,30 @@ The [PostgreSQL driver reference](../drivers/postgresql.md#postgresql-client-env lists every supported libpq pass-through variable and explains which connection fields Backmaster sets explicitly. +## MongoDB driver credentials + +Keep `MONGODB_URI` free of credentials and put authentication material in the +driver secret file: + +```bash +MONGODB_USERNAME=backmaster +MONGODB_PASSWORD='REPLACE_ME' +# MONGODB_AUTH_MECHANISM=SCRAM-SHA-256 +``` + +X.509 deployments may instead reference a protected client PEM with +`MONGODB_TLS_CERTIFICATE_KEY_FILE` and, when needed, +`MONGODB_TLS_CERTIFICATE_KEY_FILE_PASSWORD`. AWS IAM variables supported by the +MongoDB tools may also be supplied through the secret file. Prefer workload +identity and short-lived credentials over static passwords. + +MongoDB's non-interactive command-line tools receive connection options as +arguments. On hosts where users can inspect one another's process arguments, +apply operating-system process isolation and use a dedicated service identity. +Never log an expanded command or enable shell tracing. See the +[MongoDB driver reference](../drivers/mongodb.md#configuration-reference) for +the complete authentication and TLS surface. + ## Exporter examples An rclone Azure account-key file may contain: @@ -159,6 +183,9 @@ sudo -u postgres backmaster connectivity production-postgres sudo -u postgres backmaster health production-postgres ``` +For a default-user MongoDB instance, run the equivalent commands as +`backmaster`. + Each Backmaster invocation reads the files again, so a oneshot service does not need a daemon restart after rotation. Replace a secret atomically, preserving ownership and mode, then rerun `connectivity`. Keep the old credential valid diff --git a/docs/guides/systemd.md b/docs/guides/systemd.md index d055bd5..b60608e 100644 --- a/docs/guides/systemd.md +++ b/docs/guides/systemd.md @@ -53,7 +53,8 @@ an isolated restore test has passed. ## Configure the service identity -The packaged service templates run as the unprivileged `backmaster` user. A +The packaged service templates run as the unprivileged `backmaster` user. This +is normally the correct identity for MongoDB URI connections. A driver that needs a different operating-system identity must override both the backup and health services. PostgreSQL deployments using local peer authentication normally run as `postgres`: @@ -88,6 +89,17 @@ line when no explicit dependency is required. `After=` controls order only; it does not start the named service. Add `Wants=` or `Requires=` only when that lifecycle coupling is intentional. +For a local MongoDB deployment, keep the default service identity and add only +ordering when needed. Apply it to both backup and health services: + +```ini +[Unit] +After=mongod.service +``` + +Replace `mongod.service` with the installed MongoDB unit name. Connection +credentials still belong in `DRIVER_SECRET_FILE`, not in the systemd unit. + The template's `StateDirectory=backmaster/%i` makes systemd create `/var/lib/backmaster/INSTANCE` for the effective `User=` and `Group=` before each run. This is why backup runs should normally start through systemd instead diff --git a/docs/guides/troubleshooting.md b/docs/guides/troubleshooting.md index 3510ef8..f5710b6 100644 --- a/docs/guides/troubleshooting.md +++ b/docs/guides/troubleshooting.md @@ -110,6 +110,37 @@ Then invoke the archive path on a disposable copy of a WAL file if your operating procedure permits it. Check exporter connectivity, staging space, and the `archive_command` path. The Debian package installs `/usr/bin/backmaster`. +## MongoDB driver + +### `could not query MongoDB deployment` + +Run `mongosh` as the `backmaster` service identity with the configured URI, +authentication source, TLS files, and mechanism. Confirm DNS/SRV resolution, +certificate trust, firewall access, and that the role can run `listDatabases`. +Do not paste a credential-bearing URI into diagnostics. + +### `included database does not exist or is not authorized` + +Every non-empty include line must exactly match a database returned to the +backup role. A missing name may be a typo or a privilege gap. Backmaster fails +instead of publishing an incomplete selection. System databases additionally +require `MONGODB_INCLUDE_SYSTEM_DATABASES=true` to survive final filtering. + +### `mongodump failed for database` + +The role must read every selected collection and its metadata. Enabling +`MONGODB_DUMP_DB_USERS_AND_ROLES` needs additional user/role privileges. +Queryable Encryption collections cannot be dumped with `mongodump`. Check that +MongoDB Database Tools are compatible with the source and reproduce the failing +database dump as the service identity without exposing its password. + +### MongoDB dumps disagree across databases + +Per-database dumps run sequentially and do not form one deployment-wide +snapshot. The driver intentionally does not expose `--oplog`, because MongoDB +does not allow it with `--db`. Use a deployment snapshot or MongoDB backup +system designed for PITR when cross-database consistency is required. + ## Rclone exporter See the [rclone exporter reference](../exporters/rclone.md) for its complete @@ -222,7 +253,8 @@ Do not include secret contents. Useful output is: ```bash backmaster --version -dpkg-query -W 'backmaster*' 'azcopy' 'rclone' 'postgresql-client*' +dpkg-query -W 'backmaster*' 'azcopy' 'rclone' 'postgresql-client*' \ + 'mongodb-database-tools' 'mongodb-mongosh' systemctl cat backmaster@production-postgres.service systemctl cat backmaster@production-postgres.timer journalctl -u backmaster@production-postgres.service -n 200 --no-pager diff --git a/drivers/mongodb/driver b/drivers/mongodb/driver new file mode 100755 index 0000000..06ef752 --- /dev/null +++ b/drivers/mongodb/driver @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +die() { echo "mongodb driver: $*" >&2; exit 1; } + +is_boolean() { [[ "$1" == true || "$1" == false ]]; } + +load_config() { + : "${DRIVER_CONFIG:?DRIVER_CONFIG is required}" + [[ -r "$DRIVER_CONFIG" ]] || die "cannot read $DRIVER_CONFIG" + set -a + # shellcheck disable=SC1090 + source "$DRIVER_CONFIG" + if [[ -n "${DRIVER_SECRET_FILE:-}" ]]; then + [[ -r "$DRIVER_SECRET_FILE" ]] || die "cannot read $DRIVER_SECRET_FILE" + # shellcheck disable=SC1090 + source "$DRIVER_SECRET_FILE" + fi + set +a + + : "${MONGODB_URI:?MONGODB_URI is required}" + MONGODB_AUTH_DATABASE="${MONGODB_AUTH_DATABASE:-admin}" + MONGODB_DUMP_FORMAT="${MONGODB_DUMP_FORMAT:-archive}" + MONGODB_FILE_NAMING="${MONGODB_FILE_NAMING:-plain}" + MONGODB_GZIP="${MONGODB_GZIP:-true}" + MONGODB_NUM_PARALLEL_COLLECTIONS="${MONGODB_NUM_PARALLEL_COLLECTIONS:-4}" + MONGODB_INCLUDE_SYSTEM_DATABASES="${MONGODB_INCLUDE_SYSTEM_DATABASES:-false}" + MONGODB_DATABASE_INCLUDE="${MONGODB_DATABASE_INCLUDE:-}" + MONGODB_DATABASE_EXCLUDE="${MONGODB_DATABASE_EXCLUDE:-}" + MONGODB_DUMP_DB_USERS_AND_ROLES="${MONGODB_DUMP_DB_USERS_AND_ROLES:-false}" + + [[ "$MONGODB_DUMP_FORMAT" == archive || "$MONGODB_DUMP_FORMAT" == directory ]] || \ + die "MONGODB_DUMP_FORMAT must be archive or directory" + [[ "$MONGODB_FILE_NAMING" == sha256 || "$MONGODB_FILE_NAMING" == plain ]] || \ + die "MONGODB_FILE_NAMING must be sha256 or plain" + is_boolean "$MONGODB_GZIP" || die "MONGODB_GZIP must be true or false" + is_boolean "$MONGODB_INCLUDE_SYSTEM_DATABASES" || \ + die "MONGODB_INCLUDE_SYSTEM_DATABASES must be true or false" + is_boolean "$MONGODB_DUMP_DB_USERS_AND_ROLES" || \ + die "MONGODB_DUMP_DB_USERS_AND_ROLES must be true or false" + [[ "$MONGODB_NUM_PARALLEL_COLLECTIONS" =~ ^[1-9][0-9]*$ ]] || \ + die "MONGODB_NUM_PARALLEL_COLLECTIONS must be a positive integer" +} + +connection_arguments() { + CONNECTION_ARGUMENTS=(--uri="$MONGODB_URI") + if [[ -n "${MONGODB_USERNAME:-}" ]]; then + CONNECTION_ARGUMENTS+=(--username="$MONGODB_USERNAME") + fi + if [[ -n "${MONGODB_PASSWORD:-}" ]]; then + CONNECTION_ARGUMENTS+=(--password="$MONGODB_PASSWORD") + fi + if [[ -n "${MONGODB_AUTH_DATABASE:-}" ]]; then + CONNECTION_ARGUMENTS+=(--authenticationDatabase="$MONGODB_AUTH_DATABASE") + fi + if [[ -n "${MONGODB_AUTH_MECHANISM:-}" ]]; then + CONNECTION_ARGUMENTS+=(--authenticationMechanism="$MONGODB_AUTH_MECHANISM") + fi + if [[ -n "${MONGODB_TLS_CA_FILE:-}" ]]; then + CONNECTION_ARGUMENTS+=(--tls --tlsCAFile="$MONGODB_TLS_CA_FILE") + fi + if [[ -n "${MONGODB_TLS_CERTIFICATE_KEY_FILE:-}" ]]; then + CONNECTION_ARGUMENTS+=(--tls --tlsCertificateKeyFile="$MONGODB_TLS_CERTIFICATE_KEY_FILE") + fi + if [[ -n "${MONGODB_TLS_CERTIFICATE_KEY_FILE_PASSWORD:-}" ]]; then + CONNECTION_ARGUMENTS+=(--tlsCertificateKeyFilePassword="$MONGODB_TLS_CERTIFICATE_KEY_FILE_PASSWORD") + fi + if [[ "${MONGODB_TLS_INSECURE:-false}" == true ]]; then + CONNECTION_ARGUMENTS+=(--tls --tlsInsecure) + elif [[ "${MONGODB_TLS_INSECURE:-false}" != false ]]; then + die "MONGODB_TLS_INSECURE must be true or false" + fi +} + +discover() { + local expression output + connection_arguments + expression='const l=db.adminCommand({listDatabases:1,nameOnly:true,authorizedDatabases:true}); if (!l.ok) throw new Error(l.errmsg); const h=db.adminCommand({hello:1}); print(JSON.stringify({databases:l.databases.map(x=>x.name).sort(),topology:h.msg==="isdbgrid"?"sharded":(h.setName?"replica-set":"standalone"),set_name:h.setName||null,writable_primary:h.isWritablePrimary===true}));' + output="$(mongosh "${CONNECTION_ARGUMENTS[@]}" --quiet --norc --eval "$expression")" || \ + die "could not query MongoDB deployment" + jq -e '.databases | type == "array" and all(.[]; type == "string")' \ + >/dev/null <<<"$output" || die "MongoDB discovery returned invalid JSON" + DISCOVERY_JSON="$output" +} + +database_is_listed() { + local database="$1" list="$2" item + while IFS= read -r item || [[ -n "$item" ]]; do + [[ -n "$item" && "$item" == "$database" ]] && return 0 + done <<<"$list" + return 1 +} + +database_is_system() { + [[ "$1" == admin || "$1" == config || "$1" == local ]] +} + +validate_includes() { + local available="$1" requested + [[ -n "$MONGODB_DATABASE_INCLUDE" ]] || return 0 + while IFS= read -r requested || [[ -n "$requested" ]]; do + [[ -n "$requested" ]] || continue + database_is_listed "$requested" "$available" || \ + die "included database does not exist or is not authorized: $requested" + done <<<"$MONGODB_DATABASE_INCLUDE" +} + +dump_database() { + local database="$1" destination="$2" output="$3" + local -a arguments + connection_arguments + arguments=("${CONNECTION_ARGUMENTS[@]}" --db="$database" + --numParallelCollections="$MONGODB_NUM_PARALLEL_COLLECTIONS") + [[ "$MONGODB_GZIP" == false ]] || arguments+=(--gzip) + [[ "$MONGODB_DUMP_DB_USERS_AND_ROLES" == false ]] || \ + arguments+=(--dumpDbUsersAndRoles) + if [[ -n "${MONGODB_READ_PREFERENCE:-}" ]]; then + arguments+=(--readPreference="$MONGODB_READ_PREFERENCE") + fi + case "$MONGODB_DUMP_FORMAT" in + archive) arguments+=(--archive="$destination/$output") ;; + directory) arguments+=(--out="$destination/$output") ;; + esac + mongodump "${arguments[@]}" +} + +prepare() { + local destination="$1" available database digest extension output index temporary count=0 + [[ -d "$destination" ]] || die "staging payload does not exist: $destination" + [[ -z "$(find "$destination" -mindepth 1 -print -quit)" ]] || \ + die "staging payload is not empty" + discover + available="$(jq -r '.databases[]' <<<"$DISCOVERY_JSON")" + validate_includes "$available" + + case "$MONGODB_DUMP_FORMAT:$MONGODB_GZIP" in + archive:true) extension=archive.gz ;; + archive:false) extension=archive ;; + directory:*) extension=dump ;; + esac + mkdir -p "$destination/databases" + index="$destination/databases.json" + jq -n --arg dump_format "$MONGODB_DUMP_FORMAT" \ + --arg file_naming "$MONGODB_FILE_NAMING" \ + --argjson gzip "$MONGODB_GZIP" \ + --argjson parallel "$MONGODB_NUM_PARALLEL_COLLECTIONS" \ + --argjson discovery "$DISCOVERY_JSON" \ + '{format:"backmaster-mongodb-logical-v1",dump_format:$dump_format, + file_naming:$file_naming,gzip:$gzip, + num_parallel_collections:$parallel,source_topology:$discovery.topology, + source_set_name:$discovery.set_name,databases:[]}' >"$index" + + while IFS= read -r database || [[ -n "$database" ]]; do + [[ -n "$database" ]] || continue + if [[ -n "$MONGODB_DATABASE_INCLUDE" ]] && \ + ! database_is_listed "$database" "$MONGODB_DATABASE_INCLUDE"; then + continue + fi + if database_is_listed "$database" "$MONGODB_DATABASE_EXCLUDE"; then + continue + fi + if [[ "$MONGODB_INCLUDE_SYSTEM_DATABASES" == false ]] && \ + database_is_system "$database"; then + continue + fi + case "$MONGODB_FILE_NAMING" in + sha256) + digest="$(printf '%s' "$database" | sha256sum)" + output="${digest%% *}.$extension" + ;; + plain) + [[ "$database" != *'/'* && "$database" != *$'\n'* && \ + "$database" != *$'\r'* && "$database" != . && \ + "$database" != .. ]] || \ + die "database name cannot be used as a plain path: $database" + output="$database.$extension" + ;; + esac + dump_database "$database" "$destination/databases" "$output" || \ + die "mongodump failed for database: $database" + [[ -e "$destination/databases/$output" ]] || \ + die "mongodump did not create expected output for database: $database" + temporary="${index}.tmp" + jq --arg database "$database" --arg path "databases/$output" \ + '.databases += [{database:$database,path:$path}]' \ + "$index" >"$temporary" + mv -- "$temporary" "$index" + count=$((count + 1)) + done <<<"$available" + ((count > 0)) || die "database filters selected no databases" +} + +connectivitycheck() { + command -v mongosh >/dev/null || die "mongosh is unavailable" + command -v mongodump >/dev/null || die "mongodump is unavailable" + command -v jq >/dev/null || die "jq is unavailable" + discover +} + +healthcheck() { + connectivitycheck + jq -r '"OK: MongoDB is reachable; topology=" + .topology + + (if .set_name then " set=" + .set_name else "" end) + + " visible_databases=" + (.databases | length | tostring)' \ + <<<"$DISCOVERY_JSON" +} + +main() { + load_config + case "${1:-}" in + prepare) [[ $# -eq 2 ]] || exit 64; prepare "$2" ;; + connectivitycheck) connectivitycheck ;; + healthcheck) healthcheck ;; + *) die "unsupported command: ${1:-none}" ;; + esac +} + +main "$@" diff --git a/packaging/build-deb.sh b/packaging/build-deb.sh old mode 100644 new mode 100755 index 1584180..0e2d80a --- a/packaging/build-deb.sh +++ b/packaging/build-deb.sh @@ -21,7 +21,8 @@ fi readonly MAINTAINER="Nodsoft Systems " readonly CORE_PACKAGE="backmaster-core" -readonly DRIVER_PACKAGE="backmaster-driver-postgres" +readonly POSTGRES_DRIVER_PACKAGE="backmaster-driver-postgres" +readonly MONGODB_DRIVER_PACKAGE="backmaster-driver-mongodb" readonly RCLONE_EXPORTER_PACKAGE="backmaster-exporter-rclone" readonly AZCOPY_EXPORTER_PACKAGE="backmaster-exporter-azcopy" readonly META_PACKAGE="backmaster" @@ -127,33 +128,64 @@ EOF chmod 0755 "$core_root/DEBIAN/postrm" # PostgreSQL driver: source-specific executable, configuration, and runbook. -driver_root="$(package_root "$DRIVER_PACKAGE")" +driver_root="$(package_root "$POSTGRES_DRIVER_PACKAGE")" install -d -m 0755 \ "$driver_root/DEBIAN" \ "$driver_root/usr/lib/backmaster/drivers/postgres" \ - "$driver_root/usr/share/doc/$DRIVER_PACKAGE/drivers" \ - "$driver_root/usr/share/doc/$DRIVER_PACKAGE/guides" \ - "$driver_root/usr/share/doc/$DRIVER_PACKAGE/examples/config/drivers" \ - "$driver_root/usr/share/doc/$DRIVER_PACKAGE/examples/config/instances" \ - "$driver_root/usr/share/doc/$DRIVER_PACKAGE/examples/deploy" + "$driver_root/usr/share/doc/$POSTGRES_DRIVER_PACKAGE/drivers" \ + "$driver_root/usr/share/doc/$POSTGRES_DRIVER_PACKAGE/guides" \ + "$driver_root/usr/share/doc/$POSTGRES_DRIVER_PACKAGE/examples/config/drivers" \ + "$driver_root/usr/share/doc/$POSTGRES_DRIVER_PACKAGE/examples/config/instances" \ + "$driver_root/usr/share/doc/$POSTGRES_DRIVER_PACKAGE/examples/deploy" install -m 0755 "$ROOT/drivers/postgres/driver" \ "$driver_root/usr/lib/backmaster/drivers/postgres/driver" install -m 0644 "$ROOT/docs/guides/postgres-restore.md" \ - "$driver_root/usr/share/doc/$DRIVER_PACKAGE/guides/postgres-restore.md" + "$driver_root/usr/share/doc/$POSTGRES_DRIVER_PACKAGE/guides/postgres-restore.md" install -m 0644 "$ROOT/docs/drivers/postgresql.md" \ - "$driver_root/usr/share/doc/$DRIVER_PACKAGE/drivers/postgresql.md" -cp -a "$ROOT/config/drivers/." \ - "$driver_root/usr/share/doc/$DRIVER_PACKAGE/examples/config/drivers/" -cp -a "$ROOT/config/instances/." \ - "$driver_root/usr/share/doc/$DRIVER_PACKAGE/examples/config/instances/" -cp -a "$ROOT/deploy/." "$driver_root/usr/share/doc/$DRIVER_PACKAGE/examples/deploy/" -write_control "$DRIVER_PACKAGE" \ + "$driver_root/usr/share/doc/$POSTGRES_DRIVER_PACKAGE/drivers/postgresql.md" +install -m 0644 \ + "$ROOT/config/drivers/postgres.env.example" \ + "$ROOT/config/drivers/patroni-postgres.yaml.example" \ + "$driver_root/usr/share/doc/$POSTGRES_DRIVER_PACKAGE/examples/config/drivers/" +install -m 0644 "$ROOT/config/instances/nsys-postgres.env.example" \ + "$driver_root/usr/share/doc/$POSTGRES_DRIVER_PACKAGE/examples/config/instances/" +cp -a "$ROOT/deploy/." \ + "$driver_root/usr/share/doc/$POSTGRES_DRIVER_PACKAGE/examples/deploy/" +write_control "$POSTGRES_DRIVER_PACKAGE" \ "$CORE_PACKAGE (= $VERSION), bash (>= 4.4), gzip, postgresql-client" \ "PostgreSQL driver for Backmaster" \ "Produces physical PostgreSQL base backups with continuous WAL or filtered logical database dumps through a configured Backmaster exporter." \ "Breaks: $META_PACKAGE (<< $VERSION) Replaces: $META_PACKAGE (<< $VERSION)" +# MongoDB driver: database-granular logical dumps and restore documentation. +mongodb_driver_root="$(package_root "$MONGODB_DRIVER_PACKAGE")" +install -d -m 0755 \ + "$mongodb_driver_root/DEBIAN" \ + "$mongodb_driver_root/usr/lib/backmaster/drivers/mongodb" \ + "$mongodb_driver_root/usr/share/doc/$MONGODB_DRIVER_PACKAGE/drivers" \ + "$mongodb_driver_root/usr/share/doc/$MONGODB_DRIVER_PACKAGE/guides" \ + "$mongodb_driver_root/usr/share/doc/$MONGODB_DRIVER_PACKAGE/examples/config/drivers" \ + "$mongodb_driver_root/usr/share/doc/$MONGODB_DRIVER_PACKAGE/examples/config/instances" +install -m 0755 "$ROOT/drivers/mongodb/driver" \ + "$mongodb_driver_root/usr/lib/backmaster/drivers/mongodb/driver" +install -m 0644 "$ROOT/docs/drivers/mongodb.md" \ + "$mongodb_driver_root/usr/share/doc/$MONGODB_DRIVER_PACKAGE/drivers/mongodb.md" +install -m 0644 "$ROOT/docs/guides/mongodb-restore.md" \ + "$mongodb_driver_root/usr/share/doc/$MONGODB_DRIVER_PACKAGE/guides/mongodb-restore.md" +install -m 0644 \ + "$ROOT/config/drivers/mongodb.env.example" \ + "$ROOT/config/drivers/mongodb.secrets.env.example" \ + "$mongodb_driver_root/usr/share/doc/$MONGODB_DRIVER_PACKAGE/examples/config/drivers/" +install -m 0644 "$ROOT/config/instances/nsys-mongodb.env.example" \ + "$mongodb_driver_root/usr/share/doc/$MONGODB_DRIVER_PACKAGE/examples/config/instances/" +write_control "$MONGODB_DRIVER_PACKAGE" \ + "$CORE_PACKAGE (= $VERSION), bash (>= 4.4), jq, mongodb-database-tools, mongodb-mongosh" \ + "MongoDB driver for Backmaster" \ + "Discovers authorized MongoDB databases and produces filtered, independently restorable mongodump archives or collection-file directories." \ + "Breaks: $META_PACKAGE (<< $VERSION) +Replaces: $META_PACKAGE (<< $VERSION)" + # Rclone exporter: destination-specific executable and configuration examples. exporter_root="$(package_root "$RCLONE_EXPORTER_PACKAGE")" install -d -m 0755 \ @@ -199,12 +231,13 @@ write_control "$AZCOPY_EXPORTER_PACKAGE" \ meta_root="$(package_root "$META_PACKAGE")" install -d -m 0755 "$meta_root/DEBIAN" write_control "$META_PACKAGE" \ - "$CORE_PACKAGE (= $VERSION), $DRIVER_PACKAGE (= $VERSION), $RCLONE_EXPORTER_PACKAGE (= $VERSION)" \ + "$CORE_PACKAGE (= $VERSION), $POSTGRES_DRIVER_PACKAGE (= $VERSION), $RCLONE_EXPORTER_PACKAGE (= $VERSION)" \ "complete Backmaster backup system" \ "Convenience metapackage installing the Backmaster core, PostgreSQL driver, and rclone exporter." build_package "$CORE_PACKAGE" -build_package "$DRIVER_PACKAGE" +build_package "$POSTGRES_DRIVER_PACKAGE" +build_package "$MONGODB_DRIVER_PACKAGE" build_package "$RCLONE_EXPORTER_PACKAGE" build_package "$AZCOPY_EXPORTER_PACKAGE" build_package "$META_PACKAGE" diff --git a/tests/debian-packages.bash b/tests/debian-packages.bash old mode 100644 new mode 100755 index 4eabb24..d429589 --- a/tests/debian-packages.bash +++ b/tests/debian-packages.bash @@ -16,6 +16,7 @@ field() { dpkg-deb --field "$(package_path "$1")" "$2"; } contents() { dpkg-deb --contents "$(package_path "$1")"; } packages=(backmaster backmaster-core backmaster-driver-postgres \ + backmaster-driver-mongodb \ backmaster-exporter-rclone backmaster-exporter-azcopy) for package in "${packages[@]}"; do [[ -f "$(package_path "$package")" ]] || { echo "missing package: $package" >&2; exit 1; } @@ -25,6 +26,9 @@ done [[ "$(field backmaster Depends)" == \ "backmaster-core (= $version), backmaster-driver-postgres (= $version), backmaster-exporter-rclone (= $version)" ]] [[ "$(field backmaster-driver-postgres Depends)" == *"backmaster-core (= $version)"* ]] +[[ "$(field backmaster-driver-mongodb Depends)" == *"backmaster-core (= $version)"* ]] +[[ "$(field backmaster-driver-mongodb Depends)" == *"mongodb-database-tools"* ]] +[[ "$(field backmaster-driver-mongodb Depends)" == *"mongodb-mongosh"* ]] [[ "$(field backmaster-exporter-rclone Depends)" == *"backmaster-core (= $version)"* ]] [[ "$(field backmaster-exporter-azcopy Depends)" == *"backmaster-core (= $version)"* ]] [[ "$(field backmaster-exporter-azcopy Depends)" == *"azcopy"* ]] @@ -36,6 +40,7 @@ for dependency in gzip tar xz-utils zip zstd; do done [[ "$(field backmaster-core Replaces)" == "backmaster (<< $version)" ]] [[ "$(field backmaster-driver-postgres Replaces)" == "backmaster (<< $version)" ]] +[[ "$(field backmaster-driver-mongodb Replaces)" == "backmaster (<< $version)" ]] [[ "$(field backmaster-exporter-rclone Replaces)" == "backmaster (<< $version)" ]] contents backmaster-core | grep '/usr/bin/backmaster$' >/dev/null @@ -51,6 +56,10 @@ if contents backmaster-core | grep '/usr/lib/backmaster/drivers/postgres/driver$ echo "backmaster-core unexpectedly contains the PostgreSQL driver" >&2 exit 1 fi +if contents backmaster-core | grep '/usr/lib/backmaster/drivers/mongodb/driver$' >/dev/null; then + echo "backmaster-core unexpectedly contains the MongoDB driver" >&2 + exit 1 +fi if contents backmaster-core | grep '/usr/lib/backmaster/exporters/rclone/exporter$' >/dev/null; then echo "backmaster-core unexpectedly contains the rclone exporter" >&2 exit 1 @@ -62,10 +71,28 @@ fi contents backmaster-driver-postgres | grep '/usr/lib/backmaster/drivers/postgres/driver$' >/dev/null contents backmaster-driver-postgres | grep '/usr/share/doc/backmaster-driver-postgres/drivers/postgresql.md$' >/dev/null contents backmaster-driver-postgres | grep '/usr/share/doc/backmaster-driver-postgres/guides/postgres-restore.md$' >/dev/null +if contents backmaster-driver-postgres | grep '/mongodb' >/dev/null; then + echo "backmaster-driver-postgres unexpectedly contains MongoDB files" >&2 + exit 1 +fi if contents backmaster-driver-postgres | grep '/usr/bin/backmaster$' >/dev/null; then echo "backmaster-driver-postgres unexpectedly contains the core CLI" >&2 exit 1 fi +contents backmaster-driver-mongodb | grep '/usr/lib/backmaster/drivers/mongodb/driver$' >/dev/null +contents backmaster-driver-mongodb | grep '/usr/share/doc/backmaster-driver-mongodb/drivers/mongodb.md$' >/dev/null +contents backmaster-driver-mongodb | grep '/usr/share/doc/backmaster-driver-mongodb/guides/mongodb-restore.md$' >/dev/null +contents backmaster-driver-mongodb | grep '/mongodb.env.example$' >/dev/null +contents backmaster-driver-mongodb | grep '/mongodb.secrets.env.example$' >/dev/null +contents backmaster-driver-mongodb | grep '/nsys-mongodb.env.example$' >/dev/null +if contents backmaster-driver-mongodb | grep '/postgres' >/dev/null; then + echo "backmaster-driver-mongodb unexpectedly contains PostgreSQL files" >&2 + exit 1 +fi +if contents backmaster-driver-mongodb | grep '/usr/bin/backmaster$' >/dev/null; then + echo "backmaster-driver-mongodb unexpectedly contains the core CLI" >&2 + exit 1 +fi contents backmaster-exporter-rclone | grep '/usr/lib/backmaster/exporters/rclone/exporter$' >/dev/null contents backmaster-exporter-rclone | grep '/usr/share/doc/backmaster-exporter-rclone/exporters/rclone.md$' >/dev/null if contents backmaster-exporter-rclone | grep '/usr/bin/backmaster$' >/dev/null; then diff --git a/tests/mongodb-driver.bash b/tests/mongodb-driver.bash new file mode 100755 index 0000000..6835369 --- /dev/null +++ b/tests/mongodb-driver.bash @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +readonly ROOT +temporary="$(mktemp -d)" +readonly temporary +trap 'rm -rf -- "$temporary"' EXIT +mkdir -p "$temporary/bin" + +cat >"$temporary/bin/mongosh" <<'EOF' +#!/usr/bin/env bash +set -Eeuo pipefail +printf '%s\n' "${TEST_DISCOVERY_JSON:-{\"databases\":[\"admin\",\"analytics\",\"app\",\"config\",\"local\",\"scratch\"],\"topology\":\"replica-set\",\"set_name\":\"rs0\",\"writable_primary\":false}}" +printf '%s\n' "$*" >>"$TEST_MONGOSH_LOG" +EOF + +cat >"$temporary/bin/mongodump" <<'EOF' +#!/usr/bin/env bash +set -Eeuo pipefail +database= archive= out= gzip=false +for argument in "$@"; do + case "$argument" in + --db=*) database="${argument#*=}" ;; + --archive=*) archive="${argument#*=}" ;; + --out=*) out="${argument#*=}" ;; + --gzip) gzip=true ;; + esac +done +[[ -n "$database" ]] +if [[ -n "$archive" ]]; then + mkdir -p "$(dirname -- "$archive")" + printf 'archive of %s\n' "$database" >"$archive" +else + mkdir -p "$out/$database" + printf 'bson of %s\n' "$database" >"$out/$database/widgets.bson" + [[ "$gzip" == false ]] || mv "$out/$database/widgets.bson" \ + "$out/$database/widgets.bson.gz" +fi +printf '%s\n' "$database" >>"$TEST_DUMP_LOG" +printf '%s\n' "$*" >>"$TEST_DUMP_ARGUMENT_LOG" +EOF +chmod +x "$temporary/bin/"* + +cat >"$temporary/archive.env" <<'EOF' +MONGODB_URI=mongodb://127.0.0.1:27017/ +MONGODB_USERNAME=backup +MONGODB_PASSWORD=secret +MONGODB_DATABASE_INCLUDE=$'analytics\napp\nscratch' +MONGODB_DATABASE_EXCLUDE=scratch +MONGODB_DUMP_FORMAT=archive +MONGODB_FILE_NAMING=plain +MONGODB_GZIP=true +MONGODB_NUM_PARALLEL_COLLECTIONS=8 +EOF +archive_payload="$temporary/archive" +mkdir "$archive_payload" +PATH="$temporary/bin:$PATH" DRIVER_CONFIG="$temporary/archive.env" \ + TEST_MONGOSH_LOG="$temporary/mongosh-log" \ + TEST_DUMP_LOG="$temporary/archive-dumps" \ + TEST_DUMP_ARGUMENT_LOG="$temporary/archive-arguments" \ + "$ROOT/drivers/mongodb/driver" prepare "$archive_payload" +[[ "$(<"$temporary/archive-dumps")" == $'analytics\napp' ]] +[[ -s "$archive_payload/databases/analytics.archive.gz" ]] +[[ -s "$archive_payload/databases/app.archive.gz" ]] +[[ "$(jq -r '.format' "$archive_payload/databases.json")" == \ + backmaster-mongodb-logical-v1 ]] +[[ "$(jq -r '.source_topology' "$archive_payload/databases.json")" == replica-set ]] +[[ "$(jq -r '.source_set_name' "$archive_payload/databases.json")" == rs0 ]] +[[ "$(jq -r '.databases[].path' "$archive_payload/databases.json")" == \ + $'databases/analytics.archive.gz\ndatabases/app.archive.gz' ]] +grep -- '--numParallelCollections=8' "$temporary/archive-arguments" >/dev/null +grep -- '--gzip' "$temporary/archive-arguments" >/dev/null +grep -- '--username=backup' "$temporary/archive-arguments" >/dev/null +grep -- '--password=secret' "$temporary/archive-arguments" >/dev/null + +cat >"$temporary/directory.env" <<'EOF' +MONGODB_URI=mongodb://127.0.0.1:27017/ +MONGODB_DUMP_FORMAT=directory +MONGODB_FILE_NAMING=sha256 +MONGODB_GZIP=false +MONGODB_DATABASE_INCLUDE=app +MONGODB_DUMP_DB_USERS_AND_ROLES=true +MONGODB_READ_PREFERENCE=secondaryPreferred +EOF +directory_payload="$temporary/directory" +mkdir "$directory_payload" +PATH="$temporary/bin:$PATH" DRIVER_CONFIG="$temporary/directory.env" \ + TEST_MONGOSH_LOG="$temporary/directory-mongosh" \ + TEST_DUMP_LOG="$temporary/directory-dumps" \ + TEST_DUMP_ARGUMENT_LOG="$temporary/directory-arguments" \ + "$ROOT/drivers/mongodb/driver" prepare "$directory_payload" +directory_path="$(jq -r '.databases[0].path' "$directory_payload/databases.json")" +[[ "$directory_path" =~ ^databases/[0-9a-f]{64}\.dump$ ]] +[[ -s "$directory_payload/$directory_path/app/widgets.bson" ]] +grep -- '--dumpDbUsersAndRoles' "$temporary/directory-arguments" >/dev/null +grep -- '--readPreference=secondaryPreferred' "$temporary/directory-arguments" >/dev/null +if grep -- '--gzip' "$temporary/directory-arguments" >/dev/null; then + echo "uncompressed directory dump unexpectedly used gzip" >&2 + exit 1 +fi + +cat >"$temporary/system.env" <<'EOF' +MONGODB_URI=mongodb://127.0.0.1:27017/ +MONGODB_INCLUDE_SYSTEM_DATABASES=true +MONGODB_DATABASE_INCLUDE=admin +EOF +mkdir "$temporary/system" +PATH="$temporary/bin:$PATH" DRIVER_CONFIG="$temporary/system.env" \ + TEST_MONGOSH_LOG="$temporary/system-mongosh" \ + TEST_DUMP_LOG="$temporary/system-dumps" \ + TEST_DUMP_ARGUMENT_LOG="$temporary/system-arguments" \ + "$ROOT/drivers/mongodb/driver" prepare "$temporary/system" +[[ "$(<"$temporary/system-dumps")" == admin ]] + +cat >"$temporary/missing.env" <<'EOF' +MONGODB_URI=mongodb://127.0.0.1:27017/ +MONGODB_DATABASE_INCLUDE=missing +EOF +mkdir "$temporary/missing" +if PATH="$temporary/bin:$PATH" DRIVER_CONFIG="$temporary/missing.env" \ + TEST_MONGOSH_LOG="$temporary/missing-mongosh" \ + TEST_DUMP_LOG="$temporary/missing-dumps" \ + TEST_DUMP_ARGUMENT_LOG="$temporary/missing-arguments" \ + "$ROOT/drivers/mongodb/driver" prepare "$temporary/missing" \ + 2>"$temporary/missing-error"; then + echo "missing included database unexpectedly succeeded" >&2 + exit 1 +fi +grep -- 'included database does not exist or is not authorized' \ + "$temporary/missing-error" >/dev/null + +cat >"$temporary/invalid.env" <<'EOF' +MONGODB_URI=mongodb://127.0.0.1:27017/ +MONGODB_GZIP=maybe +EOF +if PATH="$temporary/bin:$PATH" DRIVER_CONFIG="$temporary/invalid.env" \ + "$ROOT/drivers/mongodb/driver" healthcheck \ + 2>"$temporary/invalid-error"; then + echo "invalid Boolean unexpectedly succeeded" >&2 + exit 1 +fi +grep -- 'MONGODB_GZIP must be true or false' "$temporary/invalid-error" >/dev/null + +PATH="$temporary/bin:$PATH" DRIVER_CONFIG="$temporary/archive.env" \ + TEST_MONGOSH_LOG="$temporary/health-mongosh" \ + TEST_DUMP_LOG="$temporary/health-dumps" \ + TEST_DUMP_ARGUMENT_LOG="$temporary/health-arguments" \ + "$ROOT/drivers/mongodb/driver" healthcheck >"$temporary/health-output" +grep -- 'topology=replica-set set=rs0 visible_databases=6' \ + "$temporary/health-output" >/dev/null + +echo "MongoDB driver tests passed"