Developing for Cetmix Tower
This file summarizes best practices that should be followed when developing for Cetmix Tower.
General rules
- Always add complete description in the "Note" field about what does this record (command, flight plan, jet template, file template, variable, etc.) do.
- Add tags to records when possible. E.g. if a flight plan is used to start a jet that uses Docker to run, add
JetsandDockertags. - Tend to use simple reusable constructions.
Variables and Metadata
- Use variables to store configuration values that can be updated manually. E.g. Odoo edition, nginx timeout, database name, etc.
- Use metadata to store system parameters and complex data structures used in automation only. E.g. Docker build details, server monitoring data, etc.
- Variable values resolution order:
Jet -> Jet Template -> Server -> Global - Benefit from using higher level variable values instead of defining them directly in every entity. E.g. if for most servers the home dir is
/home/<username>, add a global value instead of adding it to every server. - Use readable and self-explanatory references and names:
- Bad: "Var1"
var_1 - Good: "Request Timeout"
request_timeout - Use custom values in flight plans in case those variables are not used outside of the flight plan context.
SSH commands
- Use simple commands. One Tower SSH command - one SSH command on server. Avoid running multiple shell commands using
&∨. - Never change directories in SSH command using
chdir. Use thepathfield in command or in a flight plan line to specify the directory. - Never use
sudo()explicitly. If you need to run command usingsudo(), enable theuse_sudooption in the flight plan line or command wizard. - When Tower runs a command that contains
&∨usingsudo()with password it splits such command into single ones. If you want to prevent the command from being split when executed, set theno_split_for_sudofield toTrue.
Python commands
- Always use the
get_by_reference(<reference>)function to get a record using its reference. Use the system variables when available to access servers, jets and other objects. E.g.:
env["cx.tower.server"].search([("reference", "=", "my_server")]) # bad
server.get_by_reference("my_server") # good
- Python commands are executed using Odoo
safe_evalmethod. Some functions likegetattr,setattretc. are not available in the running context. - Never use
.properties to update fields. Use thewritemethod instead:
server.note = "Some note" # wrong, will raise an error
server.write({"note": "Some note"}) # correct
- Use
custom_valuesto modify existing variable values or pass values between commands in the current flight plan. Prefer adding a_prefix for custom values you create in the flight plan context to avoid possible shadowing of the existing variables. E.g.:
custom_values["odoo_version"] = "22.0" # overrides value set in server or jet
custom_values["_such_much_value"] = "wow" # creates a new temporary value that will exist only inside this flight plan run
- Prefer to use the model functions described in the model reference. However you can use any functions using source code as a reference.
Flight plans
- Prefer to re-use existing commands when creating new flight plans.
- Opt for creating generic flight plans using the python conditions in the
conditionfield for the conditional-based flow. - Hint: include a Python command or commands that analyze and modify the running context by adding and adjusting custom values that are later checked in the plan condition. Example:
# Python command
if server.os == "ubuntu_2804":
custom_values["_is_ubuntu_latest"] = True
# And check as a condition in the flight plan line
{{ _is_ubuntu_latest }}
-
Use post run actions to control the flow based on the command result. E.g. if you want a flight plan to continue running despite the command error, add a post run action:
if exit_code != 0 -> Run next command -
Group often used command sequences into smaller flight plans and reuse them later in bigger plans using commands with action
Run flight plan. - When working with Jets, prefer triggering actions using commands with the
Trigger jet actionaction if you need to run the connected flight plan for the jet.
Jets / Jet Templates
- Prefer to use unified action patterns across jet templates. E.g.:
| Action | What is done | Initial State | Transit State | Final State | On Error State |
|---|---|---|---|---|---|
| Prepare | Preparing the initial structure, e.g. folders and files | Preparing | Draft | ||
| Build | Building an image, compiling a binary, etc. | Draft | Building | Stopped | |
| Start | Starts the app/container/pod | Stopped | Starting | Running | |
| Stop | Stops the app/container/pod | Running | Stopping | Stopped | |
| Restart | Optional action if you need to restart or reload without stopping | Running | Restarting | Running | Stopped |
| Remove | Removes the container or app binary but leaves the directory structure so it can be re-used | Stopped | Removing | Removed | |
| Destroy | Final action that removes all the resources (files, databases, images) and performs a clean up | Removed | Destroying |
- You can use an action without a flight plan if you want to keep the actions template while not doing any actual actions during the state transition.
- Jet can be deleted only when the
deletablefield is set toTrue. This is the default value. It makes sense to set it toFalseusing a Python command when the Jet is in the "Prepare" or "Build" actions and set it back toTruein the "Destroy" action. - Build and Start need different plans. Start must not reuse the Build plan — Build creates resources; re-running on Start fails. Use different commands for Start (e.g.
docker startfor Docker). Production templates should define Stop, Remove, Destroy with distinct plans. Actions that do the same thing in different contexts (e.g. different template variants or different initial states) can share a plan.
Jet Template structure: atomized vs monolithic
Choose between atomized (separate reusable Jet Templates) and monolithic (single Jet Template with everything) based on your use case.
Atomized structure (building blocks)
Keep database, web proxy, and application as separate Jet Templates. Use template dependencies (template_requires_ids) so an app Jet requires a DB Jet in running state, etc.
| Pros | Cons |
|---|---|
| Reusability — MariaDB, Traefik, or Nginx templates can be shared across many app templates | More setup — user creates multiple Jets (DB, proxy, app) instead of one |
Explicit dependencies — template_requires_ids documents what this template needs |
Orchestration — user must know deployment order (DB → proxy → app) |
| Independent lifecycle — upgrade MariaDB once, all dependent Jets benefit | Networking — cross-Jet connectivity (Docker networks, hostnames) can be trickier |
| Modular testing — components can be tested in isolation | More complexity — more templates to maintain and document |
Use atomized when:
- Building shared infrastructure (e.g. one Traefik for many apps)
- Components have different lifecycles or owners
- Creating a library of reusable templates for advanced users
- Following the demo data pattern: docker → nginx/postgres/mariadb → odoo/wordpress
Monolithic structure (single stack)
One Jet Template deploys the full stack (e.g. Traefik + MariaDB + WordPress in one Docker Compose).
| Pros | Cons |
|---|---|
| Simplicity — one Jet to create and manage | No reuse — each stack is self-contained |
| Easier onboarding — fewer steps for new users | Tighter coupling — components cannot be shared |
| Simpler networking — services share one Compose network | All-or-nothing — upgrade one component, redeploy entire stack |
| Quick deployment — suitable for demos and POCs |
Use monolithic when:
- Targeting simple single-server deployments
- Ease of use and onboarding are top priority
- Components are always deployed together
- Building demos, POCs, or small projects
Choosing the approach
- Atomized: shared infra, platform teams, template libraries, mix-and-match scenarios.
- Monolithic: single-server stacks, quick demos, straightforward workflows.
Document your choice in the YAML manifest so users understand the intended use case.