Skip to content

Muppy Task Script-Type GuideΒΆ

IntroductionΒΆ

This guide explains how to create and use script-type tasks (task_type='script') in Muppy. Script-type tasks allow you to execute shell scripts on remote hosts with automatic parameter handling, error management, and integrated logging.

What are Script-Type Tasks?ΒΆ

Script-type tasks are shell scripts stored as Jinja2 templates that execute on remote infrastructure via SSH. They are part of Muppy's task execution framework which also includes:

  • Internal tasks: Python functions using Fabric decorators (@fabric_task)
  • Inline tasks: Shell code written directly in task definitions (future feature)
  • Script tasks: Shell scripts executed via the shell_task.py module

When to Use Script-Type Tasks vs Python TasksΒΆ

Use script-type tasks when: - You need to run shell commands on remote hosts - The logic is primarily shell-based (package installation, configuration, etc.) - The script should be version-controlled as data (XML files) - You want team members to edit scripts through the UI without Python knowledge - The script doesn't require complex Python interactions

Use Python fabric tasks when: - You need Python's full capabilities - Complex data transformation is required - You're orchestrating multiple steps with conditional logic - You need direct Odoo ORM access within the task


Architecture OverviewΒΆ

Execution FlowΒΆ

mpy.task record (XML/GUI)
    ↓
task.run_task() or task.invoke()  [User calls task]
    ↓
render_task_script()  [Jinja2 template evaluation]
    ↓
shell_task.py:run_script()  [Fabric task]
    ↓
Fabric Connection  [SSH via host credentials]
    ↓
Remote Host  [Script execution via shell_program]
    ↓
Result Object  [stdout, stderr, exit_code, etc.]

Key ComponentsΒΆ

  1. mpy.task Model: Stores task definitions, parameters, and script templates
  2. mpy.script Model: References the execution script (shell_task.py)
  3. shell_task.py: The Fabric task that uploads and executes scripts
  4. mpy.task_parameter: Defines parameters available to script templates
  5. Fabric Library: Handles SSH connection and remote execution

Task Model Deep DiveΒΆ

Core Fields for Script TasksΒΆ

File Location: project_addons/muppy_core/models/task.py

Essential Fields:ΒΆ

Field Type Description Example
name Char Task name (also script filename) piqsty_pg_exporter_install_callback_v1.sh
description Text Human-readable description Install Pigsty Prometheus pg_exporter binary
task_type Selection Must be 'script' script
script_id Many2one References mpy.script (must be shell_task_script) muppy_core.shell_task_script__mpy_script
task_category Selection Categorizes task purpose prometheus_exporter_install
is_system Boolean System task (not user-modifiable) True

Script-Specific Fields:ΒΆ

Field Type Description Default
shell_program Char Shell interpreter to use bash
shell_script_username Char/Template User to execute script as Empty (uses host control user)
shell_script_template Text Jinja2 template containing script content Required

Task CategoriesΒΆ

Available categories define the purpose of tasks:

TASK_CATEGORY_LIST = [
    ('host_enrollment_callback', 'Host Enrollment Callback'),
    ('cidr_dynamic_range_parser', 'CIDR Dynamic Range Parser'),
    ('prometheus_exporter_install', 'Prometheus Exporter Install Task'),
    ('devserver_install', 'Dev Server Install'),
]

Task Parameters ModelΒΆ

Parameters define what data the script receives. Each parameter is a record in mpy.task_parameter:

Field Type Purpose
name Char Parameter name (used in template)
type Selection 'p' (positional) or 'n' (named)
value_type Char Type: OdooModelType, JSONType, str, bool, int, float, dict, list
default_value Char String representation of default (for named params)
default_value_is_none Boolean Flag for None defaults
sequence Integer Parameter order

Creating Script-Type Tasks: Two ApproachesΒΆ

Approach 1: GUI Method (Interactive Creation)ΒΆ

This is the easiest way to get started and allows team members without Odoo development experience to create tasks.

Step-by-Step GUI WorkflowΒΆ

  1. Navigate to Tasks Module
  2. Go to: Infrastructure β†’ Tasks β†’ Tasks
  3. Click Create

  4. Fill Basic Information

  5. Name: Enter the script filename (e.g., my_script.sh)
  6. Description: Brief explanation of what the script does
  7. Task Type: Select script
  8. Script: Select shell_task_script (the standard execution script)

  9. Configure Script Settings

  10. Shell Program: Usually bash (default)
  11. Shell Script Username: Template for which user runs the script
    • Example: {{ params.get('server_obj').username }}
    • Leave empty to use host's control user
  12. Task Category: Choose the appropriate category

  13. Write the Shell Script Template

  14. Click in Shell Script Template field
  15. Write your Jinja2 template with bash script
  16. Access parameters via: {{ params.get('param_name') }}
  17. Use standard Jinja2 syntax for logic

  18. Add Parameters (One2Many field)

  19. Click Add a line in the Parameters section
  20. For each parameter:

    • Name: Variable name (e.g., server_obj)
    • Type: Select Positional or Named
    • Value Type: Select the Odoo type
    • Default Value: (for named parameters only)
    • Sequence: Order of execution (for positional)
  21. Save and Test

  22. Click Save
  23. System automatically validates XML structure
  24. Click Run Task button (if available from calling model)

GUI Example: Create a PostgreSQL Client InstallerΒΆ

Steps: 1. Create record with name: install_pg_client.sh 2. Set Shell Script Template to:

#!/bin/bash
set -e

PG_VERSION="{{ params.get('pg_version') }}"
echo "Installing PostgreSQL client version $PG_VERSION..."

sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y postgresql-client-$PG_VERSION
3. Add one parameter: - Name: pg_version - Type: Named - Value Type: str - Default Value: "14"

When to Use GUI: - Prototyping and testing scripts - One-off administrative tasks - When you want quick feedback without code deployment - For teams without Git workflow requirement

Approach 2: XML Data File Method (Version Controlled)ΒΆ

This approach stores tasks as XML data files, enabling version control and deployment automation.

XML File StructureΒΆ

Create a file: project_addons/my_module/data/my_tasks.xml

<?xml version="1.0" ?>
<odoo>
    <data noupdate="0">

        <!-- Task Definition -->
        <record id="my_script_task__mpy_task" model="mpy.task">
            <field name="name">my_script.sh</field>
            <field name="description">My awesome task description</field>
            <field name="script_id" ref="muppy_core.shell_task_script__mpy_script"/>
            <field name="task_type">script</field>
            <field name="task_category">devserver_install</field>
            <field name="shell_program">bash</field>
            <field name="shell_script_username">{{ params.get('host_obj').control_user_name }}</field>
            <field name="is_system" eval="True"/>

            <field name="shell_script_template"><![CDATA[#!/bin/bash
set -e

# Script content here
echo "Hello from my script!"
]]></field>
        </record>

        <!-- Parameter 1: Positional -->
        <record id="my_script_task__mpy_task_param_host" model="mpy.task_parameter">
            <field name="task_id" ref="my_module.my_script_task__mpy_task"/>
            <field name="sequence" eval="1"/>
            <field name="name">host_obj</field>
            <field name="type">p</field>
            <field name="value_type">OdooModelType</field>
        </record>

        <!-- Parameter 2: Named with default -->
        <record id="my_script_task__mpy_task_param_version" model="mpy.task_parameter">
            <field name="task_id" ref="my_module.my_script_task__mpy_task"/>
            <field name="sequence" eval="2"/>
            <field name="name">version</field>
            <field name="type">n</field>
            <field name="value_type">str</field>
            <field name="default_value">1.0</field>
        </record>

    </data>
</odoo>

XML Pattern ConventionsΒΆ

Record ID Pattern: {task_purpose}__{type} followed by __{model_name}

piqsty_pg_exporter_install_callback_v1__mpy_task
install_postgresql_client__mpy_task
my_custom_deployment__mpy_task

Parameter ID Pattern: Parent task ID + _param_ + parameter name

piqsty_pg_exporter_install_callback_v1__mpy_task_param_peo
install_postgresql_client__mpy_task_param_version

When to Use XML:ΒΆ

  • Production environments
  • Tasks needed for module functionality
  • Tasks requiring version control
  • Tasks shared across team/deployments
  • Complex parameter configurations

Shell Script TemplatesΒΆ

Template Context VariablesΒΆ

When Jinja2 evaluates your script template, these variables are available:

context = {
    'params': {
        'param_name': param_value,  # All positional and named parameters
        'another_param': another_value,
        # ...
    },
    'shell_script_name': 'my_script.sh',  # Task name
    'shell_script_username': 'postgres',  # Evaluated username
}

Accessing Parameters in TemplatesΒΆ

Simple parameter access:

#!/bin/bash
VERSION="{{ params.get('version') }}"
echo "Installing version: $VERSION"

Accessing Odoo object fields:

#!/bin/bash
# From prometheus_exporter_obj
DOWNLOAD_URL="{{ params.get('prometheus_exporter_obj').source_url }}"
BINARY_NAME="{{ params.get('prometheus_exporter_obj').exporter_software_release_id.command_name }}"
USERNAME="{{ params.get('prometheus_exporter_obj').exporter_software_release_id.run_as_user }}"

Conditional logic:

#!/bin/bash
ENVIRONMENT="{{ params.get('environment', 'production') }}"

if [ "$ENVIRONMENT" = "development" ]; then
    echo "Running in development mode"
    # dev setup
else
    echo "Running in production mode"
    # prod setup
fi

List parameters:

#!/bin/bash
# params.packages is a list
echo "Installing packages: {{ params.get('packages') | join(' ') }}"

Idempotency: Making Scripts Safe to Run Multiple TimesΒΆ

Always design scripts to be idempotent. This means running them multiple times produces the same result as running once.

Good idempotent patterns:

#!/bin/bash
set -e

BINARY_NAME="pg_exporter"
VERSION="{{ params.get('version') }}"

# βœ“ Check if already installed
if [ -f "/usr/bin/${BINARY_NAME}" ]; then
    INSTALLED=$(/usr/bin/${BINARY_NAME} --version 2>&1 | grep -oP "version \K[0-9.]+" || echo "unknown")
    if [ "$INSTALLED" = "$VERSION" ]; then
        echo "Already installed. Skipping."
        exit 0
    fi
fi

# ... rest of installation ...

Poor (non-idempotent) patterns to avoid:

#!/bin/bash

# βœ— No checks - will fail if run twice
sudo apt-get install my-package
mkdir /opt/my-app
cp config /etc/my-app/

# Better version:
sudo apt-get install -y my-package || true  # -y skips confirmation
mkdir -p /opt/my-app  # -p doesn't fail if exists
[ -f /etc/my-app/config ] || cp config /etc/my-app/

Error HandlingΒΆ

Always use set -e at the start:

#!/bin/bash
set -e  # Exit on any error

# Any command failure will stop execution
wget https://example.com/file
tar -xzf file.tar.gz
mv binary /usr/bin/

Provide helpful error messages:

#!/bin/bash
set -e

echo "Starting installation..."

if ! wget -q "$URL" -O "$FILENAME"; then
    echo "βœ— Failed to download from $URL" >&2
    exit 1
fi

echo "βœ“ Download complete"

Real-World Example: Binary Installation ScriptΒΆ

#!/bin/bash
# Real-world example from piqsty_pg_exporter_install_callback_v1.sh
set -e

# Extract parameters from Odoo objects
DOWNLOAD_URL="{{ params.get('prometheus_exporter_obj').source_url }}"
BINARY_NAME="{{ params.get('prometheus_exporter_obj').exporter_software_release_id.command_name }}"
VERSION="{{ params.get('prometheus_exporter_obj').exporter_software_release_id.version }}"
FILENAME="{{ params.get('prometheus_exporter_obj').exporter_software_release_id.filename }}"

echo "Installing ${BINARY_NAME} version ${VERSION}"

# Idempotency check
if [ -f "/usr/bin/${BINARY_NAME}" ]; then
    INSTALLED_VERSION=$(/usr/bin/${BINARY_NAME} --version 2>&1 | grep -oP 'version \K[0-9.]+' || echo "unknown")
    if [ "${INSTALLED_VERSION}" = "${VERSION}" ]; then
        echo "βœ“ Already installed. Skipping."
        exit 0
    fi
fi

# Create isolated temp directory
TMP_DIR=$(mktemp -d)
trap "rm -rf ${TMP_DIR}" EXIT
cd "${TMP_DIR}"

# Download
echo "⬇ Downloading from ${DOWNLOAD_URL}..."
wget -q "${DOWNLOAD_URL}" -O "${FILENAME}"

# Extract (flat structure)
echo "πŸ“¦ Extracting..."
tar -xzf "${FILENAME}"

# Install
echo "πŸ“ Installing to /usr/bin..."
chmod +x "${BINARY_NAME}"
mv "${BINARY_NAME}" /usr/bin/

# Verify
/usr/bin/${BINARY_NAME} --version

echo "βœ“ Installation complete"

Task ParametersΒΆ

Positional ParametersΒΆ

Definition: Must be provided in order; no defaults allowed.

<record id="my_task__mpy_task_param_host" model="mpy.task_parameter">
    <field name="task_id" ref="my_module.my_task__mpy_task"/>
    <field name="sequence" eval="1"/>  <!-- Order matters -->
    <field name="name">host_obj</field>
    <field name="type">p</field>  <!-- 'p' = positional -->
    <field name="value_type">OdooModelType</field>
</record>

Usage in script:

HOST_NAME="{{ params.get('host_obj').name }}"
echo "Installing on: $HOST_NAME"

Invocation:

task_obj.run_task(host_obj)  # First positional param
# or
task_obj.invoke(host_obj)    # Async version

Named ParametersΒΆ

Definition: Optional; can have defaults; provided by name.

<record id="my_task__mpy_task_param_version" model="mpy.task_parameter">
    <field name="task_id" ref="my_module.my_task__mpy_task"/>
    <field name="sequence" eval="2"/>
    <field name="name">pg_version</field>
    <field name="type">n</field>  <!-- 'n' = named -->
    <field name="value_type">str</field>
    <field name="default_value">14</field>  <!-- Default if not provided -->
</record>

Usage:

task_obj.run_task(host_obj, pg_version="13")  # Override default
task_obj.run_task(host_obj)                    # Uses default "14"

Value Types ReferenceΒΆ

Type Python Equivalent Example
str string "production"
int integer 8080
float float 1.5
bool boolean True / False
dict dictionary {"key": "value"}
list list ["item1", "item2"]
OdooModelType Odoo recordset env['mpy.host'].browse(5)
JSONType any JSON Complex nested structures

Special Parameter: _imq_loggerΒΆ

For asynchronous tasks, you can receive a task logger:

<record id="my_task__mpy_task_param_logger" model="mpy.task_parameter">
    <field name="task_id" ref="my_module.my_task__mpy_task"/>
    <field name="sequence" eval="3"/>
    <field name="name">_imq_logger</field>
    <field name="type">n</field>
    <field name="value_type"></field>
    <field name="default_value_is_none" eval="True"/>
</record>

This allows logging from your Python code when invoking async:

task_obj.invoke(host_obj, _imq_logger=my_logger)


Execution FlowΒΆ

How shell_task.py WorksΒΆ

Location: project_addons/muppy_core/scripts/shell_task.py

The run_script() Fabric task performs these steps:

  1. Generate unique filename:

    /tmp/{uuid}_{task_name}
    

  2. Upload script: Write template-rendered content to file

  3. Set permissions: chmod 744 (owner RWX, group/other RX)

  4. Set ownership: Change owner if shell_script_username specified

  5. Execute script:

    # If username specified:
    sudo su - {username} -c '{shell_program} {script_path}'
    
    # Otherwise:
    {shell_program} {script_path}
    

  6. Cleanup: Delete temporary script file

  7. Return result: Fabric Result object with exit code, stdout, stderr

SSH Connection & Gateway SupportΒΆ

The Fabric library automatically: - Creates SSH connection using host credentials - Handles SSH key authentication - Supports SSH gateways/proxies if configured - Manages connection lifecycle


Invoking TasksΒΆ

Method 1: Synchronous Execution (run_task)ΒΆ

Blocks until task completes.

From Python code:

def my_action(self):
    task_obj = self.env['mpy.task'].search_by_code('my_module:my_task.sh')
    host_obj = self.host_id

    # Positional and named parameters
    result = task_obj.run_task(host_obj, version="1.0", debug=True)

    if result.failed:
        raise ValueError(f"Task failed: {result.stderr}")

    self.message_post(body=f"Output: {result.stdout}")

Method 2: Asynchronous Execution (invoke)ΒΆ

Returns immediately; task runs in background via message queue.

From Python code:

def my_action(self):
    task_obj = self.task_id

    result = task_obj.invoke(
        self.host_id,
        version="1.0",
        _imq_message_name="my_task_run",
        _imq_message_group="my_tasks",
    )

    self.message_post(body="Task started in background")

Method 3: Search by Code StringΒΆ

Instead of finding the task record first:

task_code = "odoo.addons.muppy_core.scripts.shell_task:piqsty_pg_exporter_install_callback_v1.sh"
task_obj = self.env['mpy.task'].search_by_code(task_code)
result = task_obj.run_task(host_obj, prometheus_exporter_obj=exporter)

Code Format: module_name:task_name

Method 4: Button Action on Task FormΒΆ

From the Task form view in the UI:

<record id="mpy_task_view_form" model="ir.ui.view">
    <field name="model">mpy.task</field>
    <field name="arch" type="xml">
        <form>
            <!-- fields... -->
            <button name="run_task" type="object" string="Run Task"
                    class="btn-primary"/>
        </form>
    </field>
</record>

Callback Pattern: Auto-Running on Software ReleaseΒΆ

Tasks can be automatically invoked during software release installation:

Software Release Model:

callback_task_id = fields.Many2one('mpy.task', ...)  # Links to task
callback_task_code = fields.Char(...)  # Code string: "module:task.sh"

Invocation location (software_release.py line ~159):

if self.callback_task_id:
    callback_task_id.invoke(host_obj, prometheus_exporter_obj=exporter)


Real-World ExamplesΒΆ

Example 1: PostgreSQL Client InstallationΒΆ

File: project_addons/muppy_dev_server/data/dev_server_script_install_task_pg_client.xml

<record id="dev_server_install_pg_client__mpy_task" model="mpy.task">
    <field name="name">install_postgresql_client.sh</field>
    <field name="description">Install PostgreSQL client</field>
    <field name="script_id" ref="muppy_core.shell_task_script__mpy_script"/>
    <field name="task_type">script</field>
    <field name="task_category">devserver_install</field>
    <field name="shell_program">bash</field>

    <field name="shell_script_template"><![CDATA[#!/bin/bash
set -e

PG_VERSION="{{ params.get('postgresql_version', '14') }}"

echo "Installing PostgreSQL client $PG_VERSION..."
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
    | tee /etc/apt/sources.list.d/pgdg.list

sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y postgresql-client-$PG_VERSION
]]></field>
</record>

<record id="dev_server_install_pg_client__mpy_task_param_version" model="mpy.task_parameter">
    <field name="task_id" ref="muppy_dev_server.dev_server_install_pg_client__mpy_task"/>
    <field name="sequence" eval="1"/>
    <field name="name">postgresql_version</field>
    <field name="type">n</field>
    <field name="value_type">str</field>
    <field name="default_value">14</field>
</record>

Usage:

task = env['mpy.task'].search_by_code('muppy_dev_server:install_postgresql_client.sh')
task.run_task(host_obj, postgresql_version="15")

Example 2: System Packages InstallationΒΆ

Location: project_addons/muppy_dev_server/data/dev_server_script_install_task_system_prerequisites.xml

This task installs base packages with OS version detection:

#!/bin/bash
set -e

OS_VERSION=$(lsb_release -rs)

if [ "$OS_VERSION" = "24.04" ]; then
    # Ubuntu 24.04 specific packages
    sudo apt-get update
    sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
        build-essential python3-dev git
elif [ "$OS_VERSION" = "22.04" ]; then
    # Ubuntu 22.04 specific packages
    sudo apt-get update
    sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
        build-essential python3-dev git
else
    echo "Unsupported OS version: $OS_VERSION" >&2
    exit 1
fi

Example 3: Pigsty pg_exporter Binary InstallationΒΆ

Location: project_addons/muppy_prometheus_exporters/data/piqsty_pg_exporter_install_callback_mpy_task.xml

This is the fixed task we created, showing: - Idempotent installation (version checking) - Parameter extraction from Odoo objects - Error handling with helpful messages - Cleanup using shell traps

See the "Shell Script Templates" section for full example.

Example 4: Odoo/ikb InstallationΒΆ

Location: project_addons/muppy_dev_server/data/dev_server_script_install_tasks.xml

Complex task with multiple named parameters:

task.run_task(
    dev_server_obj.host_id,           # Positional
    dev_server_obj,                   # Positional
    python_version="cpython@3.12.8",  # Named
    odoo_version="18",                # Named
    dev_mode=True,                    # Named
)


Testing and DebuggingΒΆ

Running a Task Manually from CodeΒΆ

# In Python interpreter or action method
task_obj = self.env['mpy.task'].search([
    ('name', '=', 'install_postgresql_client.sh')
])

host_obj = self.env['mpy.host'].search([], limit=1)

result = task_obj.run_task(host_obj, postgresql_version="15")

print(f"Exit code: {result.exited}")
print(f"Success: {result.ok}")
print(f"Output:\n{result.stdout}")
print(f"Errors:\n{result.stderr}")

Viewing Task LogsΒΆ

Odoo Server Logs:

# Watch server logs while task runs
tail -f /var/log/odoo/odoo-server.log | grep "mpy.task"

Fabric Debug Output:

# Run with debug logging
bin/start_odoo --log-level=debug

Common Testing IssuesΒΆ

Issue Solution
Template variables undefined Check parameter names match exactly
SSH connection fails Verify host credentials and SSH keys
Script fails with "Permission denied" Ensure script is executable (happens automatically)
Idempotency check doesn't work Binary version output format may differ
Parameter not found in script Use {{ params.get('name', 'default') }}

Creating a Test TaskΒΆ

For prototyping:

  1. Create task via GUI with simple script:

    #!/bin/bash
    echo "Test message: {{ params.get('message', 'hello') }}"
    hostname
    date
    

  2. Run from action method:

    task = self.task_id
    result = task.run_task(self.host_id, message="my test")
    print(result.stdout)  # See output
    


Advanced TopicsΒΆ

Message Queue IntegrationΒΆ

For asynchronous task execution with job tracking:

from odoo.addons.inouk_message_queue.models.message_queue import current_logger

@api.multi
def start_background_task(self):
    logger = current_logger()

    # Task runs asynchronously
    self.task_id.invoke(
        self.host_id,
        _imq_message_name="deploy_task",
        _imq_message_group="deployments",
        _imq_logger=logger,  # Receives task logger
    )

    self.message_post(body="Deployment started")

SSH Gateway/Proxy SupportΒΆ

Automatically handled by Fabric via host configuration:

# Host model can specify gateway
host_obj.ssh_gateway_id  # Many2one to another host

Fabric automatically routes connections through gateway.

Task SynchronizationΒΆ

System automatically scans Python files for Fabric tasks and creates task records:

# In script.py
@fabric_task()
def my_task(cnx, host_obj, param1):
    """Task docstring"""
    # Implementation
    pass

Manually trigger synchronization:

self.env['mpy.script'].sync_all_fabric_tasks()


Workflow RecommendationsΒΆ

Development WorkflowΒΆ

Rapid Prototyping: 1. Create task via GUI with shell_script_template 2. Run immediately to test 3. Iterate on script content 4. Once working, export to XML for version control

Converting GUI Task to XML:

1. Create in GUI
2. Copy shell_script_template content
3. Create XML data file with copied content
4. Delete GUI record
5. Commit XML to Git

Production WorkflowΒΆ

  1. Write task as XML in module's data/ directory
  2. Add to __manifest__.py data file list:
    'data': [
        'data/my_tasks.xml',
    ],
    
  3. Commit to version control
  4. Deploy module to production
  5. Invoke via callbacks or from model methods

Sharing Tasks Between EnvironmentsΒΆ

From Development to Staging/Production:

# Export from development
git checkout staging
git merge develop  # includes new task XML

# Install on staging
cd /opt/muppy/appserver-mpy13c
/usr/local/python/current/bin/ikb install
bin/start_odoo -u muppy_prometheus_exporters --stop-after-init

# Task is now available

Best PracticesΒΆ

IdempotencyΒΆ

Always assume your script might run twice on same host:

#!/bin/bash
set -e

# βœ“ Check before action
if [ ! -d "/opt/myapp" ]; then
    mkdir -p "/opt/myapp"
fi

# βœ“ Use || true for non-critical commands
apt-get update || true

# βœ“ Check existing version
if command -v myapp &> /dev/null; then
    VERSION=$(myapp --version)
    if [ "$VERSION" = "1.0" ]; then
        exit 0  # Already at desired version
    fi
fi

Error HandlingΒΆ

#!/bin/bash
set -e  # Critical: exit on error

# Helpful error messages
if ! command -v wget &> /dev/null; then
    echo "ERROR: wget not found. Install with: apt-get install wget" >&2
    exit 1
fi

# Provide context in messages
echo "⬇ Downloading from: $URL"
echo "πŸ“¦ Installing to: $INSTALL_DIR"
echo "βœ“ Installation complete"

Logging and FeedbackΒΆ

#!/bin/bash

# Use clear prefixes
echo "[INFO] Starting installation..."
echo "[WARN] Backup directory not found"
echo "[ERR] Download failed" >&2

# Show progress
echo "Step 1: Downloading..."
# step 1

echo "Step 2: Extracting..."
# step 2

echo "Step 3: Installing..."
# step 3

Security ConsiderationsΒΆ

Be careful with secrets: - Never hardcode passwords or tokens - Use Odoo Vault fields when available - Don't echo sensitive parameters - Use set +x around sensitive operations

#!/bin/bash
set -e

# βœ“ Safely handle credentials
API_KEY="{{ params.get('api_key') }}"

# βœ“ Disable echo for password operations
set +x
curl -H "Authorization: Bearer $API_KEY" https://api.example.com/
set -x

Code ConventionsΒΆ

Parameter naming:

# βœ“ Objects end with _obj
prometheus_exporter_obj
host_obj
server_obj

# βœ“ IDs end with _id
host_id
server_id

# βœ“ Collections end with _ids or _objs
host_ids
server_objs

Task naming:

# βœ“ Descriptive action + target + version
install_postgresql_client_v1.sh
build_odoo_ikb_v2.sh
configure_traefik_proxy_v1.sh

# βœ“ Include callback purpose in callback tasks
piqsty_pg_exporter_install_callback_v1.sh
traefik_config_update_callback_v1.sh

Jinja2 template style:

#!/bin/bash
# βœ“ Always quote template variables
VERSION="{{ params.get('version') }}"
echo "Installing: $VERSION"

# βœ“ Use params.get() with defaults
LEVEL="{{ params.get('log_level', 'info') }}"

# βœ— Avoid
{{ params['version'] }}  # Crashes if undefined
{{ params.version }}     # Crashes if undefined


AppendixΒΆ

Task Category ReferenceΒΆ

Category Use Case Example
prometheus_exporter_install Install monitoring exporters pg_exporter, node_exporter
devserver_install Development server setup Odoo, ikb, dependencies
host_enrollment_callback Post-enrollment host setup Initial security config
cidr_dynamic_range_parser CIDR parsing utility IP range calculations

Parameter Value-Type ReferenceΒΆ

# Primitive types
'str'      # String: "hello", "1.0"
'int'      # Integer: 42, 8080
'float'    # Float: 3.14, 1.5
'bool'     # Boolean: True, False

# Complex types
'dict'     # Dictionary: {"key": "value"}
'list'     # List: ["item1", "item2"]

# Odoo types
'OdooModelType'  # Recordset: env['model'].search()
'JSONType'       # Any JSON-serializable structure

# Special
''         # For _imq_logger (no type checking)

Fabric Result Object FieldsΒΆ

The result returned from task execution includes:

result.command       # Command that was run
result.ok           # Boolean: did it succeed?
result.failed       # Boolean: did it fail?
result.exited       # Integer: exit code
result.stdout       # String: standard output
result.stderr       # String: standard error
result.return_code  # Alias for exited

Example usage:

result = task_obj.run_task(host_obj)

if result.ok:
    print("Success!")
    print(result.stdout)
else:
    print(f"Failed with code {result.exited}")
    print(result.stderr)

Troubleshooting GuideΒΆ

Script not found / Template variables undefinedΒΆ

Symptoms:

KeyError: 'undefined_param'

Solution: 1. Check parameter names match exactly 2. Verify parameter is defined in mpy.task_parameter 3. Use params.get('name', 'default') instead of params['name']

SSH permission deniedΒΆ

Symptoms:

Authentication failed

Solution: 1. Verify host SSH keys are configured 2. Check control user has SSH access 3. Verify firewall allows SSH

Script runs but produces wrong outputΒΆ

Symptoms:

$VARIABLE shows as "$VARIABLE" instead of value

Solution: Use double quotes, not single quotes:

# βœ— Wrong
MESSAGE='{{ params.get("msg") }}'  # Single quotes prevent Jinja2 eval

# βœ“ Correct
MESSAGE="{{ params.get('msg') }}"  # Double quotes allow Jinja2 eval

Task marked as failed but script succeededΒΆ

Symptoms:

result.failed = True
result.stdout = "Success"
result.exited = 0

Solution: This can happen due to connection timeout or host disconnection. Check: 1. Host is reachable via SSH 2. Script completes in reasonable time 3. Network connectivity during execution


Quick ReferenceΒΆ

Create Task in GUIΒΆ

  1. Go to Infrastructure β†’ Tasks β†’ Tasks
  2. Click Create
  3. Fill name, description, task_type='script', script_id=shell_task_script
  4. Write template in Shell Script Template
  5. Add parameters in Parameters section
  6. Save

Create Task in XMLΒΆ

<record id="my_task__mpy_task" model="mpy.task">
    <field name="name">my_task.sh</field>
    <field name="script_id" ref="muppy_core.shell_task_script__mpy_script"/>
    <field name="task_type">script</field>
    <field name="task_category">devserver_install</field>
    <field name="shell_script_template"><![CDATA[
#!/bin/bash
# Your script here
]]></field>
</record>

Invoke Task from CodeΒΆ

# Synchronous
result = task_obj.run_task(host_obj, param1="value1")

# Asynchronous
task_obj.invoke(host_obj, param1="value1")

Access Parameters in ScriptΒΆ

#!/bin/bash
PARAM="{{ params.get('param_name') }}"
FIELD="{{ params.get('obj_param').field_name }}"

Additional ResourcesΒΆ

  • Task Model: project_addons/muppy_core/models/task.py
  • Shell Task Executor: project_addons/muppy_core/scripts/shell_task.py
  • API Execution: project_addons/muppy_core/api/__init__.py
  • Real Examples: See all dev_server_script_*.xml and piqsty_pg_exporter_install_callback_mpy_task.xml files