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.robotstonoindex, nofollow. - Resolve paths from the centralized config and PMIVerse filesystem conventions; minimal per-db config is stored centrally.
Prerequisites
- PMIVerse layout under
/srv/pmiversewith suites in_pmisuite<version>. psql,createdb,dropdb,pg_restore,unzip, andzipinfoavailable.- 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=webat 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 inPMIVERSE_DB_REGISTRY.source_dir_name: relative subdirectory under suitedata/<backups_dirname>/where ZIPs live, including the host folder on the local rsync/restore server.target_dir_name: working folder name under suitedata/dumps/.backups_dirname: optional per-db parent folder name for backups (commonlybackupsorbackups-main). If empty, defaultbackupsis 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
NAMEor--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 toodoo-bin.--odoo-conf PATH— path topmisuite<N>.conf.--backups-dirname VAL— backups parent folder name (backupsorbackups-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
- Resolve configuration; validate the exact source or newest directory ZIP.
- 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 exactlypostgresql-local-default:<effective-database-name>. The filestore lock is<physical-filestore-root>/.pmis-restore-filestore-<sha256>.lock; its hash covers exactlyfilestore:<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. - Reject unsafe archive entries, extract into a unique clean work directory,
locate exactly one dump, validate custom dumps with
pg_restore --listor plain SQL for non-empty readable content, and locate a supported filestore. - 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.
- Restore the dump into a collision-resistant temporary database. A failure here drops only that temporary database and leaves the live pair unchanged.
- From the
postgresmaintenance database, terminate target connections, rename any existing target to a unique rollback name, and rename the temporary database to the requested original target name. - 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.
- Run post-restore operations and requested neutralization against the final target. The old database/filestore pair remains recoverable until both pass.
- 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_DIRresolution via--dry-runand 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/*.backupfile. - 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.examplemirror 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 pipefailand to work even if parts ofpmiverse.confare 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.