Skip to Content

restore_database.sh — Generic Odoo database restore for PMIVerse

The checked-out module boundaries, ownership model, safety constraints, and focused test selection are documented in the restore architecture and maintenance contract.

Location

/srv/pmiverse/_pmiconfig/scripts/restore/restore_database.sh

Purpose

  • Restore an Odoo database backup into a local PMIVerse development suite.
  • Optionally neutralize the restored database by disabling scheduled actions, outgoing and incoming mail, installed payment and delivery integrations, clearing installed IAP tokens, disconnecting supported bank-sync models, and setting website.robots to noindex, nofollow.
  • Resolve paths from the centralized config and PMIVerse filesystem conventions; minimal per-db config is stored centrally.

Prerequisites

  • PMIVerse layout under /srv/pmiverse with suites in _pmisuite<version>.
  • psql, createdb, dropdb, pg_restore, unzip, and zipinfo available.
  • A PostgreSQL role able to create databases, rename the target database, and terminate its active connections. Existing targets must be owned by that role unless it is a PostgreSQL superuser.
  • Odoo virtualenv present for the target suite (e.g., /srv/pmiverse/_pmisuite18/venv/bin/python).
  • Central config: /srv/pmiverse/_pmiregistry/config/pmiverse.conf.
  • The script invokes the Odoo shell with --load=web at runtime to avoid requiring optional server-wide modules (e.g., odoo.addons.openupgrade_framework). This does not modify your suite config and keeps behavior identical otherwise.

Central configuration

Global roots

Defined in pmiverse.conf (with reasonable defaults if undefined): - PMIVERSE_ROOT="/srv/pmiverse" - PMICONFIG_DIR="$PMIVERSE_ROOT/_pmiconfig" - PMISUITE_PREFIX="_pmisuite"

Database registry (preferred)

The active /srv/pmiverse/_pmiregistry/config/pmiverse.conf owns the installation's inventory. The following entries and commands use synthetic names; substitute aliases from your active registry.

pmiverse.conf contains a CSV-like heredoc variable PMIVERSE_DB_REGISTRY with six columns:

# name,db_name,version,source_dir_name,target_dir_name,backups_dirname
demo,demo.example,18,backup-a/demo.example,demo,
  • name: short alias for invocation (e.g., demo).
  • db_name: Postgres database name; also used as the filestore folder name.
  • version: PMIsuite version recorded for that registry entry. Other suite versions may be selected through supported explicit CLI values or configured legacy per-target overrides, but they are not active registry aliases unless listed in PMIVERSE_DB_REGISTRY.
  • source_dir_name: relative subdirectory under suite data/<backups_dirname>/ where ZIPs live, including the host folder on the local rsync/restore server.
  • target_dir_name: working folder name under suite data/dumps/.
  • backups_dirname: optional per-db parent folder name for backups (commonly backups or backups-main). If empty, default backups is used unless overridden on CLI.

Legacy per-target overrides (e.g., DEMO_SOURCE_DIR, DEMO_ODOO_VENV) may still exist in pmiverse.conf and are honored, but prefer the registry.

Local full-history mirrors use data/backups/backup-a/<database>/ and data/backups/backup-b/<database>/. Remote producers retain data/backups/. When restoring directly on a remote producer, select its original database folder explicitly with --source; the registry defaults describe local copies.


Supported backup formats and layouts

Restore expects a .zip containing: - The database dump as one of: - dump.sql (preferred), or any single *.sql file, or - a pg_dump custom archive *.dump or *.backup (restored via pg_restore). - The filestore directory, discovered recursively. Supported shapes inside the zip: - */filestore/<db_name> - */<db_name>/filestore - */<db_name> (canonical filestore root folder) - any */filestore (renamed to <db_name> during restore)

The companion backup_odoo18_dbs.sh script produces a canonical zip layout: - one timestamped custom-format *.dump at the archive root, and - <db_name>/... for filestore content (relative paths, no absolute prefixes).

Archives must contain exactly one supported dump. Absolute names, .. or . path components, backslashes, control characters, and ZIP symlink entries are rejected before extraction. Extracted paths are checked again for symlinks. Dump-only archives are supported: they publish the database while retaining the existing live filestore unchanged, which is safer than deleting it.


Resolution order (how values are determined)

Highest priority first: 1. CLI options (including --backups-dirname) 2. Legacy per-target variables from pmiverse.conf (if set) 3. Database registry entry in pmiverse.conf 4. Derived from PMIVerse layout: PMIVERSE_ROOT/_pmisuite<version>/... 5. Built-in defaults

Derived defaults for a given suite <N>: - SUITE_DIR = /srv/pmiverse/_pmisuite<N> - FILESTORE_LOCATION = $SUITE_DIR/data/filestore - ODOO_VENV = $SUITE_DIR/venv/bin/python - ODOO_BIN = $SUITE_DIR/repos/OCB/odoo-bin - ODOO_CONFIG = $SUITE_DIR/config/pmisuite<N>.conf - SOURCE_DIR = $SUITE_DIR/data/<backups_dirname>/<source_dir_name or db_name> - DESTINATION_DIR = $SUITE_DIR/data/dumps/<target_dir_name or alias or db short> - backups_dirname default is backups unless set by CLI or registry per-db.

Use --dry-run to print the fully resolved configuration including the source of each value and then exit without executing.


Usage

Quick restore by alias (preferred)

/srv/pmiverse/_pmiconfig/scripts/restore/restore_database.sh demo

With neutralization

/srv/pmiverse/_pmiconfig/scripts/restore/restore_database.sh demo --neutralize

Only apply neutralization (no restore)

/srv/pmiverse/_pmiconfig/scripts/restore/restore_database.sh demo --only-neutralize

Dry-run (show resolution and exit)

/srv/pmiverse/_pmiconfig/scripts/restore/restore_database.sh demo --dry-run

Explicit flags (override registry/derivations)

/srv/pmiverse/_pmiconfig/scripts/restore/restore_database.sh \
  --suite 18 \
  --db demo.example \
  --source /srv/pmiverse/_pmisuite18/data/backups/backup-b/demo.example \
  --dest   /srv/pmiverse/_pmisuite18/data/dumps/demo \
  --filestore /srv/pmiverse/_pmisuite18/data/filestore \
  --odoo-venv /srv/pmiverse/_pmisuite18/venv/bin/python \
  --odoo-bin  /srv/pmiverse/_pmisuite18/repos/OCB/odoo-bin \
  --odoo-conf /srv/pmiverse/_pmisuite18/config/pmisuite18.conf \
  --neutralize

Override backups dirname for a run

/srv/pmiverse/_pmiconfig/scripts/restore/restore_database.sh demo --backups-dirname backups

You can also use the long form --target <name> instead of a positional alias.


Options reference

  • NAME or --target NAME — alias from the registry (e.g., demo).
  • --suite N — suite version (used for derivations if needed).
  • --db NAME — database name.
  • --source DIR — directory with backup zips (newest file is used).
  • --source-file FILE — select exactly one readable regular ZIP; mutually exclusive with --source. This interface never follows symlinks, including broken symlinks or symlinked path components.
  • --dest DIR — working directory where the zip is copied and unzipped.
  • --filestore DIR — suite filestore root (destination for <db_name> folder).
  • --odoo-venv PATH — Python executable inside the Odoo virtualenv.
  • --odoo-bin PATH — path to odoo-bin.
  • --odoo-conf PATH — path to pmisuite<N>.conf.
  • --backups-dirname VAL — backups parent folder name (backups or backups-main).
  • --neutralize — perform SQL + ORM neutralization after restore.
  • --only-neutralize — do not restore; just neutralize the existing DB.
  • --dry-run — print resolved config and exit.
  • -h|--help — show built-in help text and exit.

Staged publication and rollback

  1. Resolve configuration; validate the exact source or newest directory ZIP.
  2. Acquire persistent, non-blocking resource locks in the fixed global order PostgreSQL then filestore. The PostgreSQL lock is /srv/pmiverse/.pmis-restore-locks/.pmis-restore-postgresql-<sha256>.lock; its hash covers exactly postgresql-local-default:<effective-database-name>. The filestore lock is <physical-filestore-root>/.pmis-restore-filestore-<sha256>.lock; its hash covers exactly filestore:<canonical-live-filestore-path>. Neither identity includes suite, alias, archive, work directory, or the other resource. Raw database names and paths are never filename components. If either lock is unavailable or fails pathname/device/inode verification, every acquired descriptor closes before mutable target inspection or mutation.
  3. Reject unsafe archive entries, extract into a unique clean work directory, locate exactly one dump, validate custom dumps with pg_restore --list or plain SQL for non-empty readable content, and locate a supported filestore.
  4. While holding the lock, record whether the final database and live filestore originally existed. Copy any incoming filestore to a unique hidden staging directory directly under the final filestore root. The live filestore remains untouched.
  5. Restore the dump into a collision-resistant temporary database. A failure here drops only that temporary database and leaves the live pair unchanged.
  6. From the postgres maintenance database, terminate target connections, rename any existing target to a unique rollback name, and rename the temporary database to the requested original target name.
  7. Rename any live filestore to its unique rollback path, then rename the staged filestore into place. These same-filesystem filesystem renames are atomic individually, but PostgreSQL and filesystem rename are not one cross-system atomic transaction.
  8. Run post-restore operations and requested neutralization against the final target. The old database/filestore pair remains recoverable until both pass.
  9. Remove rollback artifacts only after that success boundary, then release both resource locks. Lock files persist; closing descriptors releases the advisory locks on success, failure, rollback, or signal.

Partial overlap always contends: the same effective database with a different filestore, or a different database with the same canonical live filestore, cannot run concurrently. Suite changes and alias versus explicit forms do not alter a resolved resource identity. Restores with both different database names and different canonical live filestore paths may proceed concurrently. --only-neutralize mutates no filestore and therefore acquires only the PostgreSQL resource lock; it still contends with every normal restore of that database.

The default shared PostgreSQL lock root is created only when the invoking ordinary user can do so safely without privilege. An unavailable, unsafe, or unwritable root fails closed. PMIS_RESTORE_LOCK_ROOT exists for controlled fixtures and deployments, but every process capable of targeting the same database must use the same root. The database identity denotes the script's local/default PostgreSQL target namespace: it deliberately serializes equal database names conservatively and does not distinguish clusters through a live connection. It cannot coordinate different hosts or deliberately different lock roots. Cross-system database/filestore publication remains coordinated rollback, not an atomic transaction.

If database publication, filestore publication, post-restore work, or requested neutralization fails, the script attempts to rename the new resources back to their temporary/staged names and restore the old pair. It returns nonzero and prints the exact artifact names plus safe database-rename guidance. If an interruption lands in a rename boundary where automatic state recognition is uncertain, it fails closed and preserves both copies for manual recovery. Reconciliation uses observed presence of the exact target, temporary, and rollback databases plus live, staged, and rollback filestore paths. It handles publication when no old database or filestore existed and dump-only restores without relying on a possibly stale flag. Database rollback is established before filesystem rollback begins. Before publication, cleanup likewise checks the exact generated temporary database and drops it only when the target and rollback state prove ownership unambiguous. Cleanup errors are reported without replacing the restore's original exit status.

Always-run outgoing and incoming mail protections are required. Requested neutralization also requires cron disabling and writes to installed payment providers, delivery carriers, IAP accounts, supported writable bank-sync disconnect fields, and website.robots. The ORM program attempts every relevant operation, reports all required failures, then raises one aggregate exception. Missing optional integrations and installed bank-sync models with no supported writable disconnect field are informational. Any required ORM failure exits the Odoo shell nonzero and keeps the outer restore from deleting rollback artifacts.


Troubleshooting

  • “Error: No files found in the source directory”: verify SOURCE_DIR resolution via --dry-run and confirm backups exist.
  • A missing filestore is handled as the documented dump-only policy; the existing live filestore is retained.
  • “Error: No database dump file found…”: ensure the zip contains either dump.sql, a single *.sql, or a *.dump/*.backup file.
  • Neutralization errors are fatal. The script attempts to roll back the database/filestore pair and preserves recovery artifacts if rollback cannot complete; it never reports restore success after failed neutralization.
  • If exact observed state is ambiguous or a rollback step fails, no unique rollback artifact is deleted. Follow the printed observed-state and recovery commands only after independently confirming all three database names and all three filestore paths.

Examples

  • Restore and neutralize demo:
/srv/pmiverse/_pmiconfig/scripts/restore/restore_database.sh demo --neutralize
  • Resolve the local backups/backup-a/demo.example mirror from the registry:
/srv/pmiverse/_pmiconfig/scripts/restore/restore_database.sh demo --dry-run
  • Explicit paths (bypass registry completely):
/srv/pmiverse/_pmiconfig/scripts/restore/restore_database.sh \
  --suite 18 --db demo.example \
  --source /srv/pmiverse/_pmisuite18/data/backups/backup-b/demo.example \
  --dest   /srv/pmiverse/_pmisuite18/data/dumps/demo \
  --neutralize

Notes

  • The script is designed to be safe under set -euo pipefail and to work even if parts of pmiverse.conf are missing; it falls back to PMIVerse directory conventions.
  • Per-target restore scripts in _pmiregistry/scripts/restore/ share registry-based source selection while retaining their separate restore implementations; this script provides the generic alternative.