# Muppy Documentation (Full) > Complete documentation concatenated for LLM ingestion. ================================================================================ # Home Source: index.md ================================================================================ # Muppy Documentation Muppy manages infrastructure the way you would manage it by hand, only written down and repeatable. It connects over SSH to the **Hosts** you enrol, runs **Tasks** on them, and collects **Facts** back. Everything else on this site is built on those three objects. New here? Read [Muppy Concepts](concepts.md) first — five minutes, and the rest of the site stops being a glossary problem. ## Choose your path
- :material-server: **Run servers** Enrol a Host, run Tasks on it, manage its services, its firewall and its SSH keys. [Muppy Core](muppy_core/index.md) - :material-database: **Run PostgreSQL** Clusters, backups and restores, replication sets, High Availability, point-in-time recovery. [PostgreSQL Guides](guides/postgresql/index.md) - :material-application-braces: **Run Odoo applications** App Definitions, CI/CD from your git pushes, dev / test / staging / production servers, and a browser IDE on each. [Odizy](odizy/index.md) - :material-kubernetes: **Run Kubernetes** Clusters, packages and releases, metapackages, and multi-cloud High Availability with Pack8s. [Muppy K8s](muppy_k8s/index.md) - :material-rocket-launch: **Ship an app on Manganese** Pick an App Definition, describe your build in `mpy_setup.sh` and your processes in `services.yml`, push. [App Servers](app_servers/index.md) - :material-cog: **Configure the instance** Users, S3 buckets, credentials, and the notification channels the queues use. [Configuration](configuration/index.md)
## Also here | | | |---|---| | [Muppy Enterprise](muppy_enterprise/index.md) | Muppy deployed inside your own infrastructure. | | [OVH Public Cloud](ovh/index.md) | Commissioning and importing OVH instances. | | [User Guides](user_guides/restic-backup-restore.md) | Task-sized how-tos: Restic backups, lock monitoring, script-type Tasks. | | [Muppy Manganese](muppy_manganese/designing-your-mgx.md) | Designing an mgx, environment variables, plans, sharing a database. | !!! tip "Reading this with an agent" Every page is also served as raw markdown, and the whole site as a single file. See [Using This Doc with an AI Agent](using-with-an-agent.md) for the URLs and what to hand your agent. ================================================================================ # Muppy Concepts Source: concepts.md ================================================================================ # Muppy Concepts Muppy connects over SSH to **Hosts** in order to run **Tasks** (installation or configuration procedures) and to collect **Facts**. Muppy is modular: Tasks are published in **Addons** that are installed as needed. ## Hosts A **Host** is any machine Muppy manages: a bare-metal server, a VM, or an LXC container. Enrolling a Host is what gives Muppy the SSH access it needs to run Tasks on it. Hosts are reached from **Muppy / Hosts**. See [Hosts](muppy_core/hosts/index.md) for enrollment and day-to-day management. ## Tasks and Facts **Tasks** are: - installation or configuration procedures written in Python, - grouped into **Scripts** (one Script = one Python module), - *exposed* in the Muppy user interface, so any kind of user can launch or schedule them, - executed through a job system that traces every execution and keeps a running log of everything the Tasks did, - **debuggable** and concise — they need far less code than the equivalent shell commands. A Task is declared in Python with the `@fabric_task` decorator; the corresponding `mpy.task` and `mpy.script` records are generated by the synchronisation sweep rather than written by hand. See [Task and Script Synchronisation](muppy_core/tasks-fact-collectors/task-script-sync.md). **Facts** are configuration data that Tasks need in order to run — for example `POSTGRESQL_INSTALLED_VERSIONS`. Facts are collected by running a particular kind of Task, the **Fact Collectors**. See [Tasks & Fact Collectors](muppy_core/tasks-fact-collectors/index.md). ## Addons and Apps Muppy is modular. Tasks are published in **Addons** that can be installed independently. Beyond Tasks, an Addon can also implement user interfaces and monitoring automation — such an Addon is called a **Muppy App**. Muppy's PostgreSQL features, for instance, ship as Muppy Apps. **Muppy Enterprise** customers and **Muppy Partners** can implement their own Addons and Muppy Apps. ================================================================================ # Using This Doc with an AI Agent Source: using-with-an-agent.md ================================================================================ # Using this documentation with an AI agent Every page of this site is published twice: as the HTML you are reading, and as the raw markdown it was written from. An agent reads the markdown — no scraping, no HTML stripping. ## Three ways in | You want | Fetch | |---|---| | one page | that page's `.md` URL | | the list of every page | [`/llms.txt`](https://docs.muppy.io/llms.txt) | | the whole site in one file | [`/llms-full.txt`](https://docs.muppy.io/llms-full.txt) | This follows the [llms.txt convention](https://llmstxt.org/). ## One page Take the page's URL, drop the trailing slash, add `.md`: ``` https://docs.muppy.io/muppy_manganese/services-yml-guide/ ← the page https://docs.muppy.io/muppy_manganese/services-yml-guide.md ← its markdown ``` ```bash curl -s https://docs.muppy.io/muppy_manganese/services-yml-guide.md ``` ### Pages that open a section A page that opens a section — `/app_servers/`, `/muppy_k8s/`, `/guides/postgresql/` — is written as an `index.md` inside its own directory, and its markdown stays there. The rule above would send you one level too high: ``` https://docs.muppy.io/app_servers/ ← the page https://docs.muppy.io/app_servers/index.md ← its markdown https://docs.muppy.io/app_servers.md ← 404, this file does not exist ``` 25 of the 142 pages are of that kind. If a `.md` URL returns 404, append `index.md` to the page URL instead of replacing the trailing slash. Better still, don't derive URLs at all: **every link in `llms.txt` is exact**, section pages included. Your browser may download the `.md` instead of displaying it. That is the browser, not the site — `curl` shows it, and an agent reads it either way. ## The index — `llms.txt` A 15 KB markdown index: the site's name, one line on what Muppy is, then a link to the markdown of all 141 pages. Give an agent this URL and it can pick what it needs. ```bash curl -s https://docs.muppy.io/llms.txt ``` ## Everything — `llms-full.txt` Every page concatenated, each behind a banner naming its title and source path. About 580 KB — roughly 145,000 tokens. That fits a large-context model in one shot. It does **not** fit a small one: use `llms.txt` there and fetch the handful of pages that matter. ## What to say to an agent Paste a URL and the question: > Read https://docs.muppy.io/llms.txt, then tell me how to schedule a > PostgreSQL backup in Muppy. > Read https://docs.muppy.io/llms-full.txt. My App Server's build fails in > `mpy_setup.sh` — what stage runs when, and what environment variables can I read? Agents that browse the web fetch these directly. For one without web access, `curl` the file and paste it in. !!! tip "Claude Code on an App Server" An App Server's browser IDE ships with Claude Code, which fetches URLs itself. Point it at `llms.txt` and let it pull the pages it needs — cheaper than pasting the whole site into the conversation. ================================================================================ # PostgreSQL Activity & Locks Monitoring Source: user_guides/postgresql-lock-monitoring.md ================================================================================ # PostgreSQL Activity and Locks Monitoring ## Overview PostgreSQL uses locks to manage concurrent access to database resources. When multiple transactions try to access the same data, locks ensure data integrity by controlling access order. However, locks can also cause performance issues when queries wait too long for resources held by other transactions. Muppy provides three monitoring tools to help diagnose and resolve PostgreSQL locking and activity issues: | Tool | Purpose | When to Use | |------|---------|-------------| | **Lock Monitor** | View ALL locks in the cluster | Investigating overall lock activity, finding what's holding locks | | **Blocking Lock Monitor** | View ONLY blocking situations | Diagnosing deadlocks or blocked queries | | **pg_stat_activity Monitor** | View all backend connections | Analyzing connection states, finding idle transactions | ### Decision Guide: Which Tool to Use? - **Application seems slow?** Start with **pg_stat_activity Monitor** to see active queries - **Query is stuck waiting?** Use **Blocking Lock Monitor** to find what's blocking it - **Need to understand lock patterns?** Use **Lock Monitor** to see all locks - **Too many connections?** Use **pg_stat_activity Monitor** to identify and terminate idle connections --- ## Key PostgreSQL Concepts ### Lock Types PostgreSQL has several lock types depending on what resource is being locked: | Lock Type | Description | |-----------|-------------| | `relation` | Locks on tables | | `transactionid` | Locks on transaction IDs | | `virtualxid` | Locks on virtual transaction IDs | | `tuple` | Locks on specific rows | | `object` | Locks on database objects | | `advisory` | Application-controlled locks | ### Lock Modes (from weakest to strongest) | Mode | Conflicts With | Typical Operations | |------|----------------|-------------------| | `AccessShareLock` | AccessExclusiveLock | SELECT | | `RowShareLock` | Exclusive, AccessExclusive | SELECT FOR UPDATE/SHARE | | `RowExclusiveLock` | Share, ShareRowExclusive, Exclusive, AccessExclusive | INSERT, UPDATE, DELETE | | `ShareUpdateExclusiveLock` | ShareUpdateExclusive, Share, ShareRowExclusive, Exclusive, AccessExclusive | VACUUM, ANALYZE | | `ShareLock` | RowExclusive, ShareUpdateExclusive, ShareRowExclusive, Exclusive, AccessExclusive | CREATE INDEX | | `ShareRowExclusiveLock` | RowExclusive, ShareUpdateExclusive, Share, ShareRowExclusive, Exclusive, AccessExclusive | - | | `ExclusiveLock` | RowShare, RowExclusive, ShareUpdateExclusive, Share, ShareRowExclusive, Exclusive, AccessExclusive | - | | `AccessExclusiveLock` | ALL modes | DROP TABLE, ALTER TABLE, TRUNCATE | ### Granted vs Awaited Locks - **Granted = True**: The process holds the lock - **Granted = False**: The process is waiting for the lock ### Blocked vs Blocking - **Blocked process**: A query that is waiting because another transaction holds a conflicting lock - **Blocking process**: A query that holds a lock that another transaction is waiting for --- ## Accessing the Monitoring Tools 1. Navigate to **Databases > PostgreSQL > Database Clusters** 2. Select a cluster to open its form view 3. Click the **Action** dropdown menu 4. Choose one of: - **Monitor Database Locks** - Opens Lock Monitor - **Monitor Database Blocking Locks** - Opens Blocking Lock Monitor - **Monitor Cluster Activity** - Opens pg_stat_activity Monitor --- ## Lock Monitor ### Purpose The Lock Monitor displays ALL locks currently held or awaited in the PostgreSQL cluster. Use this tool when you need a comprehensive view of lock activity. ### How It Works The monitor queries the `pg_locks` system view joined with `pg_stat_activity` to show lock information alongside the queries holding them. ### Key Fields | Field | Description | |-------|-------------| | **PID** | Process ID of the backend holding/awaiting the lock | | **Lock Type** | Type of lock (relation, transactionid, etc.) | | **Relation** | Name of the table being locked (if applicable) | | **Mode** | Lock mode (AccessShareLock, RowExclusiveLock, etc.) | | **Granted** | True if lock is held, False if waiting | | **Query Duration** | How long the current query has been running | | **Query** | The SQL query text | | **State** | Backend state (active, idle, idle in transaction) | | **Application Name** | Name of the connected application | | **Client Address** | IP address of the client | ### Using the Lock Monitor 1. **Refresh**: Click **Refresh** to update the lock list with current data 2. **Filter by Database**: Select a database to show only locks for that database 3. **Group By**: Use the search bar groupings to organize locks by: - PID - User - Database - Lock Type - Mode - Relation 4. **Pause Auto-Refresh**: Apply the "Don't Refresh" filter to prevent automatic updates 5. **Custom Query**: Click the **Query** tab to view or modify the SQL query used ### SQL Query Used ```sql SELECT pgl.pid, pgl.locktype, pgl.database AS database_oid, pgsa.datname AS database_name, pgl.relation::regclass AS relation_name, pgl.mode, pgsa.usename AS user_name, pgsa.application_name, pgsa.client_addr, now() - pgsa.query_start AS query_duration, pgsa.query, pgsa.state, pgl.granted, -- ... additional fields FROM pg_locks AS pgl LEFT JOIN pg_stat_activity AS pgsa ON pgl.pid = pgsa.pid ORDER BY (now() - pgsa.query_start) DESC; ``` --- ## Blocking Lock Monitor ### Purpose The Blocking Lock Monitor shows ONLY situations where one query is blocking another. This is the most useful tool when diagnosing performance issues caused by lock contention. ### How It Works The monitor identifies lock conflicts by finding locks that are NOT granted (`NOT blockedl.granted`) and joining with the locks table again to find which process holds the conflicting lock. ### Key Fields | Field | Description | |-------|-------------| | **Locked Item** | The table or resource being contested | | **Waiting Duration** | How long the blocked query has been waiting | | **Blocked PID** | Process ID of the waiting query | | **Blocked Query** | SQL text of the waiting query | | **Blocked Mode** | Lock mode requested by the blocked query | | **Blocking PID** | Process ID of the query holding the lock | | **Blocking Query** | SQL text of the query holding the lock | | **Blocking Mode** | Lock mode held by the blocking query | ### Using the Blocking Lock Monitor 1. **Refresh**: Click **Refresh** to get current blocking situations 2. **Analyze**: Look at both the blocked and blocking queries to understand the conflict 3. **Decide**: Determine which query should be terminated (usually the blocking one) 4. **Act**: Use one of the terminate buttons ### Terminate Buttons Each blocking situation has two terminate options: | Button | Action | When to Use | |--------|--------|-------------| | **Terminate backend of BLOCKING pid** | Kills the process holding the lock | When the blocking query is stuck or less important | | **Terminate backend of BLOCKED pid** | Kills the process waiting for the lock | When the waiting query should be cancelled | ### SQL Query Used ```sql SELECT COALESCE(blockingl.relation::regclass::text, blockingl.locktype) AS locked_item, now() - blockeda.query_start AS waiting_duration, blockeda.pid AS blocked_pid, blockeda.query AS blocked_query, blockedl.mode AS blocked_mode, blockinga.pid AS blocking_pid, blockinga.query AS blocking_query, blockingl.mode AS blocking_mode, -- ... client info fields FROM pg_catalog.pg_locks AS blockedl JOIN pg_stat_activity AS blockeda ON blockedl.pid = blockeda.pid JOIN pg_catalog.pg_locks AS blockingl ON ( (blockingl.transactionid = blockedl.transactionid) OR (blockingl.relation = blockedl.relation AND blockingl.locktype = blockedl.locktype) ) AND blockedl.pid != blockingl.pid JOIN pg_stat_activity AS blockinga ON blockingl.pid = blockinga.pid WHERE NOT blockedl.granted ORDER BY blockeda.query_start; ``` --- ## pg_stat_activity Monitor ### Purpose The pg_stat_activity Monitor shows all PostgreSQL backend connections and their current state. Use this tool to: - Find long-running queries - Identify "idle in transaction" connections - Monitor overall database activity - Terminate problematic connections in bulk ### Key Fields | Field | Description | |-------|-------------| | **PID** | Process ID of the backend | | **Backend Type** | Type of backend (client backend, autovacuum, etc.) | | **Database** | Database name | | **Username** | Connected user | | **Application Name** | Application identifier | | **Client Address** | IP address of the client | | **State** | Current state (active, idle, idle in transaction, etc.) | | **State Age** | Time elapsed since last state change | | **Wait Event Type** | What the backend is waiting for (if any) | | **Query** | Current or last executed query | ### Backend States | State | Description | Action | |-------|-------------|--------| | `active` | Executing a query | Monitor if running too long | | `idle` | Waiting for new command | Generally safe | | `idle in transaction` | In transaction but not executing | **Dangerous** - holds locks! | | `idle in transaction (aborted)` | In failed transaction | Should be terminated | ### Using the pg_stat_activity Monitor 1. **Ignore Idle Connections**: Toggle the checkbox to hide idle connections (enabled by default) 2. **Filter by Database**: Select a specific database to narrow results 3. **Group By**: Organize by state, database, or client 4. **Bulk Terminate**: Select multiple records and use "Terminate Backends" action ### SQL Query Used ```sql SELECT pid, backend_type, datname, usename, application_name, client_addr, state, now() - state_change AS state_age, wait_event_type, wait_event, query FROM pg_stat_activity ORDER BY state_age DESC; ``` --- ## Terminating Processes ### How pg_terminate_backend() Works When you click a terminate button, Muppy executes: ```sql SELECT pg_terminate_backend() ``` This gracefully terminates the specified backend: - The connection is closed - Any open transaction is **rolled back** - The client receives an error about the terminated connection ### Choosing Which Process to Terminate | Scenario | Recommended Action | |----------|-------------------| | Blocking query is stuck/hung | Terminate the **blocking** PID | | Blocking query will finish soon | Wait, or terminate the **blocked** PID | | "idle in transaction" holding locks | Terminate that PID | | Long-running query not needed | Terminate that PID | | Multiple idle connections | Use bulk terminate in pg_stat_activity | ### Warnings - **Transaction Rollback**: Terminating a backend rolls back its transaction. Any uncommitted changes are lost. - **Application Impact**: The connected application will receive an error. Ensure it can handle reconnection. - **Cascading Effects**: If a blocked query is part of a larger operation, terminating the blocker may allow many queries to proceed at once. --- ## Use Cases ### Use Case 1: Diagnosing a Slow Application **Symptoms**: Application responses are slow, database seems unresponsive. **Steps**: 1. Open **pg_stat_activity Monitor** 2. Uncheck "Ignore Idle Connections" to see all connections 3. Look for queries with long **State Age** in "active" state 4. Check **Wait Event Type** - if it shows "Lock", switch to Blocking Lock Monitor 5. If many connections are "idle in transaction", those may be holding locks ### Use Case 2: Resolving a Table Lock Deadlock **Symptoms**: Queries on a specific table are stuck. **Steps**: 1. Open **Blocking Lock Monitor** 2. Click **Refresh** 3. Identify the blocking chain: - Note the **locked_item** (table being contested) - Compare **blocked_query** vs **blocking_query** 4. Decide which transaction can be safely terminated 5. Click **Terminate backend of BLOCKING pid** to release the lock ### Use Case 3: Identifying "idle in transaction" Connections **Symptoms**: Locks held for long periods, performance degradation. **Steps**: 1. Open **pg_stat_activity Monitor** 2. Uncheck "Ignore Idle Connections" 3. Group by **State** 4. Look for "idle in transaction" entries with long **State Age** 5. These connections hold locks without doing work - terminate them ### Use Case 4: Investigating Lock Contention on a Specific Table **Symptoms**: Operations on a particular table are slow. **Steps**: 1. Open **Lock Monitor** 2. Group by **Relation** 3. Find your table and expand to see all locks 4. Look at lock **Mode** - AccessExclusiveLock blocks everything 5. Check **Granted** - False means waiting for lock 6. Find the PID holding the blocking lock and investigate --- ## Best Practices ### Keep Transactions Short Long transactions hold locks longer, increasing blocking potential: ```sql -- Bad: Long transaction holding locks BEGIN; UPDATE large_table SET status = 'processed'; -- ... application does other work for minutes ... COMMIT; -- Better: Quick transactions BEGIN; UPDATE large_table SET status = 'processed'; COMMIT; -- Application does other work separately ``` ### Monitor Proactively - Check for blocking locks regularly, not just when problems occur - Set up alerts for long-running "idle in transaction" connections - Review lock patterns during deployments and migrations ### Coordinate DDL Operations DDL commands (ALTER TABLE, CREATE INDEX) often require AccessExclusiveLock: - Schedule during maintenance windows - Use `CONCURRENTLY` when possible (e.g., `CREATE INDEX CONCURRENTLY`) - Warn users before running DDL on busy tables ### Avoid Long-Running Idle Transactions Applications should: - Commit or rollback transactions promptly - Use connection pooling with transaction-level pooling - Set `idle_in_transaction_session_timeout` in PostgreSQL configuration --- ## Troubleshooting | Symptom | Probable Cause | Solution | |---------|---------------|----------| | No locks displayed | Cluster offline or SSH unreachable | Check cluster state and connectivity | | Query shows "idle" but holds lock | Transaction not committed | Find and terminate idle in transaction | | Blocking Monitor empty but queries slow | No lock conflicts - other bottleneck | Use pg_stat_activity to analyze | | Terminate button doesn't respond | PID already terminated | Refresh the monitor | | Monitor shows stale data | Auto-refresh disabled | Click Refresh or remove "Don't Refresh" filter | | Connection refused error | Cluster not running | Start the PostgreSQL cluster | --- ## Technical Reference ### Model References | Model | Description | |-------|-------------| | `mpy.pg_lock_monitor_wizard` | Lock Monitor wizard | | `mpy.pg_lock` | Individual lock record (transient) | | `mpy.pg_blocking_lock_monitor_wizard` | Blocking Lock Monitor wizard | | `mpy.pg_blocking_lock` | Blocking lock record (transient) | | `mpy.pg_stat_activity_monitor_wizard` | Activity Monitor wizard | | `mpy.pg_stat_activity` | Activity record (transient) | ### Source Files - `project_addons/muppy_postgresql_base/wizards/pg_lock_monitor_wizard.py` - `project_addons/muppy_postgresql_base/wizards/pg_blocking_lock_monitor_wizard.py` - `project_addons/muppy_postgresql_base/wizards/pg_stat_activity_monitor_wizard.py` ### External Documentation - [PostgreSQL Explicit Locking](https://www.postgresql.org/docs/current/explicit-locking.html) - [pg_locks View](https://www.postgresql.org/docs/current/view-pg-locks.html) - [pg_stat_activity View](https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW) - [Exploring Query Locks in Postgres](https://big-elephants.com/2013-09/exploring-query-locks-in-postgres/) ================================================================================ # Task Script-Type Guide Source: user_guides/mpy-task-script-type-guide.md ================================================================================ # 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: ```python 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** - Go to: **Infrastructure → Tasks → Tasks** - Click **Create** 2. **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) 3. **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 - **Task Category**: Choose the appropriate category 4. **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 5. **Add Parameters** (One2Many field) - Click **Add a line** in the Parameters section - 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) 6. **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: ```bash #!/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 my_script.sh My awesome task description script devserver_install bash {{ params.get('host_obj').control_user_name }} host_obj p OdooModelType version n str 1.0 ``` #### 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: ```python 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:** ```bash #!/bin/bash VERSION="{{ params.get('version') }}" echo "Installing version: $VERSION" ``` **Accessing Odoo object fields:** ```bash #!/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:** ```bash #!/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:** ```bash #!/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:** ```bash #!/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:** ```bash #!/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:** ```bash #!/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:** ```bash #!/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 ```bash #!/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. ```xml host_obj p OdooModelType ``` **Usage in script:** ```bash HOST_NAME="{{ params.get('host_obj').name }}" echo "Installing on: $HOST_NAME" ``` **Invocation:** ```python 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. ```xml pg_version n str 14 ``` **Usage:** ```python 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: ```xml _imq_logger n ``` This allows logging from your Python code when invoking async: ```python 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**: ```bash # 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:** ```python 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:** ```python 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: ```python 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: ```xml mpy.task