Shell Restore Callbacks¶
Related guide
This page covers shell script callbacks specifically. For Python callbacks and the general callback mechanism, see Restore Callbacks.
When to Use a Shell Callback¶
Write your pg_restore callback as a shell script when:
- Your post-restore logic is a few lines of SQL, a
psqlcommand, or acurlcall - You have an existing bash script you want to reuse
- You want to avoid the burden of maintaining deploying a new Muppy addon just for post-restore automation (Enterprise Customer Only)
Write your callback in Python (@fabric_task) when:
- You need complex branching logic
- You need Odoo ORM access (reading/writing Muppy ORM Objects inside the callback)
- The callback coordinates multiple tasks or services
How It Works¶
A shell callback is a mpy.task record with Task Type = Shell Script and Category = PG Restore Callback.
At each restore step, Muppy:
- Renders your
Shell Script Template(Jinja2) with the current restore context - Uploads the rendered script to the target host
- Executes it as
shell_script_usernameviasudo su - <user> -c 'bash <script>' - If the script exits with a non-zero code, the restore is aborted (same semantics as raising in a Python callback)
The callback wrapper reuses the existing SSH connection — no new connection is opened between steps.
For the general shell task mechanism, see Shell Script Tasks.
Available Template Variables¶
Your Shell Script Template is rendered as a Jinja2 template. Variables are accessed via params.get(...):
| Variable | Type | Notes |
|---|---|---|
{{ params.get('step') }} |
str | Current step: s3_download / create_user / drop_db / create_db / pg_restore |
{{ params.get('db_name') }} |
str | Name of the database being restored |
{{ params.get('db_owner') }} |
str | PostgreSQL role owning the database |
{{ params.get('db_comment') }} |
str | Comment entered in the restore wizard (may be empty) |
{{ params.get('pg_cluster_obj') }} |
mpy.pg_cluster |
Access cluster fields: .version, .cluster_name, .name, .owner |
{{ params.get('pg_dump_obj') }} |
mpy.pg_dump or False |
False at s3_download if dump record not yet synced |
{{ params.get('pg_dump_file_path') }} |
str | Absolute path to the dump file on the host (empty string at s3_download if not downloaded yet) |
Shell Script Username Context¶
Two different Jinja2 contexts
The Shell Script Username field and the Shell Script Template field are both Jinja2 templates, but they use different contexts:
| Field | Context | Access syntax |
|---|---|---|
Shell Script Username |
Flat params_dict |
{{ pg_cluster_obj.owner }} |
Shell Script Template |
{'params': params_dict, ...} |
{{ params.get('pg_cluster_obj').owner }} |
Example — to run the script as the PostgreSQL cluster owner:
- In Shell Script Username:
{{ pg_cluster_obj.owner }} - In Shell Script Template:
{{ params.get('pg_cluster_obj').owner }}
Declaring Parameters on Your Task¶
Your task must declare exactly 2 positional parameters matching the pg_restore callback invocation:
| Sequence | Name | Type | Value type |
|---|---|---|---|
| 1 | step |
Positional | str |
| 2 | pg_cluster_obj |
Positional | OdooModelType |
| 3–7 | pg_dump_obj, pg_dump_file_path, db_name, db_comment, db_owner |
Named | (as appropriate) |
The 5 named parameters are passed as kwargs by Muppy's restore engine and are automatically available in params during rendering. Declaring them explicitly is recommended for documentation and UI clarity.
Muppy ships an example record with the correct parameter declarations — see Using the Example Record.
Dispatcher Pattern¶
Handle all 5 steps in a single script with a case statement:
#!/usr/bin/env bash
set -euo pipefail
STEP="{{ params.get('step') }}"
DB_NAME="{{ params.get('db_name') }}"
DB_OWNER="{{ params.get('db_owner') }}"
PG_CLUSTER_NAME="{{ params.get('pg_cluster_obj').cluster_name }}"
PG_CLUSTER_ID="{{ params.get('pg_cluster_obj').ubuntu_cluster_id }}"
echo "[callback] step=${STEP} db=${DB_NAME} cluster=${PG_CLUSTER_ID}"
case "${STEP}" in
s3_download)
# After the dump file is downloaded from S3
;;
create_user)
# After the owner role is created
;;
drop_db)
# After the target database is dropped
;;
create_db)
# After the empty target database is created
# Good place for extensions (pg_trgm, unaccent, ...)
;;
pg_restore)
# After pg_restore completes
# Good place for anonymization, grants, schema migrations
;;
*)
echo "[callback] WARNING: unknown step '${STEP}'" >&2
;;
esac
exit 0
Example 1 — Anonymize Emails at pg_restore¶
#!/usr/bin/env bash
set -euo pipefail
STEP="{{ params.get('step') }}"
DB_NAME="{{ params.get('db_name') }}"
PG_CLUSTER_ID="{{ params.get('pg_cluster_obj').ubuntu_cluster_id }}"
case "${STEP}" in
pg_restore)
echo "[callback] Anonymizing emails in '${DB_NAME}'..."
psql --cluster="${PG_CLUSTER_ID}" -d "${DB_NAME}" -c \
"UPDATE res_partner
SET email = 'anon-' || id || '@example.com'
WHERE email IS NOT NULL AND email != '';"
echo "[callback] Anonymization complete."
;;
*)
;;
esac
Example 2 — Create Extension at create_db¶
#!/usr/bin/env bash
set -euo pipefail
STEP="{{ params.get('step') }}"
DB_NAME="{{ params.get('db_name') }}"
PG_CLUSTER_ID="{{ params.get('pg_cluster_obj').ubuntu_cluster_id }}"
case "${STEP}" in
create_db)
echo "[callback] Installing pg_trgm extension in '${DB_NAME}'..."
psql --cluster="${PG_CLUSTER_ID}" -d "${DB_NAME}" -c \
"CREATE EXTENSION IF NOT EXISTS pg_trgm;"
;;
*)
;;
esac
Example 3 — Slack Notification at pg_restore¶
#!/usr/bin/env bash
set -euo pipefail
STEP="{{ params.get('step') }}"
DB_NAME="{{ params.get('db_name') }}"
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
case "${STEP}" in
pg_restore)
curl -s -X POST "${SLACK_WEBHOOK}" \
-H "Content-Type: application/json" \
-d "{\"text\": \"Restore of \`${DB_NAME}\` completed on $(hostname).\"}"
;;
*)
;;
esac
Error Handling¶
Use set -euo pipefail at the top of every script. If the script exits with a non-zero code, Muppy raises MpyException and the restore is aborted — identical semantics to raise in a Python callback.
#!/usr/bin/env bash
set -euo pipefail # ← Any failing command aborts the restore
# This will abort the restore if psql fails
psql --cluster="${PG_CLUSTER_ID}" -d "${DB_NAME}" -c "..."
Security¶
mpy.taskrecords require themuppy_core.group_usersecurity group (infrastructure users only). Shell callbacks are not exposed via the MGX/Manganese interface.- The script runs on the PostgreSQL host as
shell_script_username. For PostgreSQL operations, set it to the cluster owner (e.g.,postgres).
Configuration & Selection¶
Shell callbacks are selected at the same three levels as Python callbacks:
| Level | Where |
|---|---|
| Default per database | mpy.pg_database → pg_restore_callback_task_id field |
| Stored with the backup | "Backup Databases" wizard → written to mpy.pg_dump |
| Override at restore time | "Restore PostgreSQL Databases" wizard |
Set Task Type = Shell Script and Category = PG Restore Callback to distinguish your task from others in the dropdown.
Using the Example Record¶
Muppy ships a ready-to-use example record at:
Settings → Tasks → pg_restore_callback_example.sh (inactive by default)
To create your callback from it:
- Open the record and click Action → Duplicate
- Rename the copy and edit
Shell Script Template - Set Active = True on your copy
- Assign it to a
mpy.pg_databaseor use it in a restore wizard
The example record has all 7 parameters pre-declared with the correct types.
Versioning Your Callback in an Addon¶
Enterprise customers can version their callback in an Muppy addon's XML data file:
<record id="my_anonymize_callback__mpy_task" model="mpy.task">
<field name="name">anonymize_on_restore.sh</field>
<field name="script_id" ref="muppy_core.shell_task_script__mpy_script" />
<field name="task_type">script</field>
<field name="task_category">pg_restore_callback</field>
<field name="shell_program">bash</field>
<field name="shell_script_username">{{ pg_cluster_obj.owner }}</field>
<field name="active" eval="True" />
<field name="shell_script_template">#!/usr/bin/env bash
set -euo pipefail
STEP="{{ params.get('step') }}"
DB_NAME="{{ params.get('db_name') }}"
case "${STEP}" in
pg_restore)
psql --cluster="{{ params.get('pg_cluster_obj').version }}/{{ params.get('pg_cluster_obj').cluster_name }}" \
-d "${DB_NAME}" \
-c "UPDATE res_partner SET email = 'anon-' || id || '@example.com';"
;;
*) ;;
esac
</field>
</record>
<!-- Positional parameters (order matters) -->
<record id="my_anonymize_callback__param_step" model="mpy.task_parameter">
<field name="task_id" ref="my_anonymize_callback__mpy_task" />
<field name="sequence" eval="1" />
<field name="name">step</field>
<field name="type">p</field>
<field name="value_type">str</field>
</record>
<record id="my_anonymize_callback__param_cluster" model="mpy.task_parameter">
<field name="task_id" ref="my_anonymize_callback__mpy_task" />
<field name="sequence" eval="2" />
<field name="name">pg_cluster_obj</field>
<field name="type">p</field>
<field name="value_type">OdooModelType</field>
</record>
<!-- Named parameters (3–7: pg_dump_obj, pg_dump_file_path, db_name, db_comment, db_owner) -->
<record id="my_anonymize_callback__param_dbname" model="mpy.task_parameter">
<field name="task_id" ref="my_anonymize_callback__mpy_task" />
<field name="sequence" eval="5" />
<field name="name">db_name</field>
<field name="type">n</field>
<field name="value_type">str</field>
</record>
<!-- ... declare remaining named params similarly -->