Cronomicon · Python Guide
📗 Usage Guide

Cronomicon

The self-hosted Script Orchestrator — schedule and run Bash, Ansible, Terraform, PowerShell, Perl and Python jobs across your fleet from one auditable web console.

Guide Python guide — running Python scripts with Cronomicon Version PY1.0

How to run Python well inside Cronomicon: how a python run actually reaches its target and what interpreter it expects there, how env, run inputs and outputs look from inside a Python script, why input() can never work and what to use instead, the exit-code semantics that decide whether your run reads Success or Failed, and how the Scripts catalog detects the variables your code consumes.

Python is a shell-family run type, so most of the machinery is shared with Bash: read the Bash Guide for the two-executor model, targeting, bastions and secrets in full depth — this guide states the shared rules briefly and goes deep only where Python differs. Companions: the User Manual, the Administrator Manual (§4, SSH executor), and the Runner Install Guide / Runner Config Guide / Runner Security Guide guides.

The one-sentence model. A Python script is a Script (run type python), a Scope names the hosts it may touch, a Job binds the two to schedules and env — and the run executes over SSH: the dialing side (the Cronomicon server or a runner) connects to each target and invokes python3 there.


1 · How a Python run reaches its target

Like Bash, a python run can execute on either executor — the in-app SSH executor (the default via the resolution chain; the pool is off until an operator sets CRONOMICON_SSH_EXECUTOR_ENABLED=true, or shell-family runs queue forever) or a runner inside the target's segment. On both, the mechanics are identical: the dialing side opens an SSH connection to each target and runs your script with python3 on the target. A run with no injected environment invokes python3 -c '<your body>'; the moment the run carries env (job env, schedule env, overrides, bound references, run-input answers), the invocation becomes python3 - and the program — an os.environ prelude plus your body — arrives on stdin, so no value ever touches argv (§4). Two consequences worth internalizing:

Runner capability nuance. A runner claims the python run type when python3 (or python) is on its own PATH — the auto-detect probe — even though execution happens on the targets over SSH. For a runner host without a local interpreter that serves Python-capable targets, declare the capability explicitly (-capabilities bash,python / CRONOMICON_RUNNER_CAPABILITIES); otherwise the run queues “waiting for a python-capable runner.”


2 · Getting scripts in

Identical to the rest of the catalog (Bash Guide §2 in depth): any raw *.py under scripts/ in the definitions repo is auto-discovered as a Script with run type python (the name keeps its extension; sub-folders shape the catalog's folder tree); or author a kind: Script YAML with exactly one of command / script / scriptPath and optional declared prompts; jobs bind by script_ref, or in-app via the Composer. Wire cronomicon validate . into CI — and ruff (or flake8) sits beside it as naturally as shellcheck does for bash.

The catalog reads your code for its variables. The Scripts view's “Variables this script uses” — the same list the Run dialog and Composer hint from — comes from a heuristic scan of environment access in the body:


3 · Targets: what differs from Bash

Targeting follows the shared shell-family rules — a scope fans out to its member hosts, a pinned target_host wins outright on every run path, a per-run host subset is SSH-executor-only (422 scope_membership otherwise), host keys fail closed on both executors. One thing is Python-specific:


4 · Env, run inputs, outputs — and exit codes that tell the truth

Reading the environment

Injected environment (job env ← schedule env ← per-run overrides, plus bound references and the dispatcher's CRONOMICON_RUN_* context set) arrives as an os.environ prelude ahead of your body, delivered on stdin — never argv, never /proc-visible. By the time your first line runs it is ordinary environment:

import os

release = os.environ["TARGET_RELEASE"]              # a run-input answer — required
api_key = os.environ["CRONOMICON_SECRET_API_KEY"]      # a bound secret reference
region  = os.getenv("REGION", "us-east-1")          # optional, with a default
print(f"run {os.environ['CRONOMICON_RUN_ID']} on scope {os.environ['CRONOMICON_RUN_SCOPE']}")

Because the prelude sets os.environ in-process, child processes you spawn (subprocess.run) inherit everything automatically, exactly as they would from an exported shell variable.

Run inputs, not input()

input() raises EOFError on both executors (§1) — there is no TTY and stdin already carried your program. Declare operator questions as run inputs (spec.prompts — name, label, required, default, options); the Run dialog asks, records provenance, and the answers arrive as env vars. prompt_enforcement: block makes a missing required answer reject the run instead of warning.

Exit codes & exceptions

Exit 0 is success; anything else fails that host, and per-host results aggregate — all ok → success, all failed → failure, mixed → warning (partial). Python makes the honest default easy: an uncaught exception exits 1 with a traceback in the run log, so the anti-pattern is not silent success but the opposite — a bare except Exception: pass that swallows the failure and exits 0. Catch what you can handle, let the rest propagate, and use sys.exit(n) (or raise SystemExit(n)) when you classify failures yourself.

Passing data to downstream workflow steps (A12)

Print the marker on stdout, exactly as in Bash:

print(f"::cronomicon-output name=VERSION::{version}")

A downstream step declares an input bound to that output and receives it as an env var. If a captured value carries an injected secret, the executor drops all captured outputs and fails the run (output_secret_leak) rather than let the secret propagate — emit something derived, never the secret itself. If your script prints volumes of output, remember the marker must be its own line on stdout (not stderr, not inside JSON you dump).


5 · Secrets in Python

The full model — reference bindings, fail-closed resolution, redactor seeding, the runner-side Secret injection flag — is in Bash Guide §5 and applies unchanged: bound Secrets and Variables arrive as CRONOMICON_SECRET_<name> / CRONOMICON_VAR_<name> in os.environ, delivered over stdin, never argv. The Python-shaped edges:


6 · Best-practice checklist


7 · Troubleshooting

SymptomLikely cause → fix
Per-host failure “python3: command not found”The target has no python3 on the SSH session's PATH — install it, or provide the name (symlink / python-is-python3).
EOFError at an input() callThere is no TTY and stdin carried the program (§1). Declare a run input and read the answer from os.environ.
KeyError: 'NAME' tracebackA required env var was not supplied — the direct-subscript form has no default. Bind the reference / declare the run input, or switch to os.getenv("NAME", default) if absence is legitimate.
SyntaxError on a construct that works on your machineThe target's interpreter is older than yours. Write for the fleet's floor (§6).
ModuleNotFoundErrorThe import isn't installed on that target. Standard library first; real dependencies are managed on the targets, not by Cronomicon.
Runs sit queued forever (executor ssh)The SSH executor pool is disabled — CRONOMICON_SSH_EXECUTOR_ENABLED=true, or route the job to a runner.
Queued, “waiting for a python-capable runner”No online runner claims python — the probe found no local interpreter on the runner host. Declare it: -capabilities bash,python.
Run failed, reason output_secret_leakA ::cronomicon-output value contained an injected secret; all outputs were dropped deliberately. Emit something derived instead.
A secret appears in a target's process listingIt was passed on a child's argv (§5). Move it to the child's environment or a stdin pipe — redaction cannot reach a remote process table.

Anything not listed here — host keys, bastions, cred_error/conn_error, scope membership, timeouts, executor_lost — behaves exactly as for Bash: see Bash Guide §7.