Skip to content

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 psql command, or a curl call
  • 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:

  1. Renders your Shell Script Template (Jinja2) with the current restore context
  2. Uploads the rendered script to the target host
  3. Executes it as shell_script_username via sudo su - <user> -c 'bash <script>'
  4. 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.task records require the muppy_core.group_user security 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_databasepg_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:

  1. Open the record and click Action → Duplicate
  2. Rename the copy and edit Shell Script Template
  3. Set Active = True on your copy
  4. Assign it to a mpy.pg_database or 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 -->