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.pymodule
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ΒΆ
- mpy.task Model: Stores task definitions, parameters, and script templates
- mpy.script Model: References the execution script (
shell_task.py) - shell_task.py: The Fabric task that uploads and executes scripts
- mpy.task_parameter: Defines parameters available to script templates
- 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ΒΆ
- Navigate to Tasks Module
- Go to: Infrastructure β Tasks β Tasks
-
Click Create
-
Fill Basic Information
- Name: Enter the script filename (e.g.,
my_script.sh) - Description: Brief explanation of what the script does
- Task Type: Select
script -
Script: Select
shell_task_script(the standard execution script) -
Configure Script Settings
- Shell Program: Usually
bash(default) - Shell Script Username: Template for which user runs the script
- Example:
{{ params.get('server_obj').username }} - Leave empty to use host's control user
- Example:
-
Task Category: Choose the appropriate category
-
Write the Shell Script Template
- Click in Shell Script Template field
- Write your Jinja2 template with bash script
- Access parameters via:
{{ params.get('param_name') }} -
Use standard Jinja2 syntax for logic
-
Add Parameters (One2Many field)
- Click Add a line in the Parameters section
-
For each parameter:
- Name: Variable name (e.g.,
server_obj) - Type: Select
PositionalorNamed - Value Type: Select the Odoo type
- Default Value: (for named parameters only)
- Sequence: Order of execution (for positional)
- Name: Variable name (e.g.,
-
Save and Test
- Click Save
- System automatically validates XML structure
- 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
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:
-
Generate unique filename:
/tmp/{uuid}_{task_name} -
Upload script: Write template-rendered content to file
-
Set permissions:
chmod 744(owner RWX, group/other RX) -
Set ownership: Change owner if
shell_script_usernamespecified -
Execute script:
# If username specified: sudo su - {username} -c '{shell_program} {script_path}' # Otherwise: {shell_program} {script_path} -
Cleanup: Delete temporary script file
-
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:
-
Create task via GUI with simple script:
#!/bin/bash echo "Test message: {{ params.get('message', 'hello') }}" hostname date -
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ΒΆ
- Write task as XML in module's
data/directory - Add to
__manifest__.pydata file list:'data': [ 'data/my_tasks.xml', ], - Commit to version control
- Deploy module to production
- 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ΒΆ
- Go to Infrastructure β Tasks β Tasks
- Click Create
- Fill name, description, task_type='script', script_id=shell_task_script
- Write template in Shell Script Template
- Add parameters in Parameters section
- 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_*.xmlandpiqsty_pg_exporter_install_callback_mpy_task.xmlfiles