Python Tools
This image is a Python 3 task runner with ExifTool and a small web control panel
for running known, mounted Python maintenance tasks. It starts the control panel
with python -m app.
Control Panel
The application discovers direct *.py files in each non-hidden directory
under /opt/tasks. Each directory is a task group. Tasks in one group run
sequentially; different groups may run concurrently. It records run history and
combined output in SQLite under /var/lib/server-maintenance, and supports
cancellation of active task process groups.
The image contains application code only. Mount these paths at runtime:
/opt/tasksread-only: reviewed task scripts and their input manifests./opt/credentialsread-only: named credential files used by those scripts./var/lib/server-maintenance: persistent SQLite data, logs, and task state.
Task processes receive a minimal environment. To forward non-secret container
settings to tasks, list their names in SERVER_MAINTENANCE_TASK_ENV, separated
by commas. This allowlist prevents unrelated container configuration from
being exposed to every task; credentials should remain in /opt/credentials.
Tasks are never imported during discovery. Their optional description is the first line of their module docstring. A task must not require stdin or command line arguments, should write useful output, and must return a non-zero exit code on failure.
Task definitions
Each non-hidden directory directly below /opt/tasks is a task group. Its
direct *.py files are the exposed tasks. Use an optional
task-inputs.json beside those files to define the group's title, task titles
and descriptions, task order, and web form fields. It is parsed as data only
and currently uses version 1:
{
"version": 1,
"title": "Example Tasks",
"tasks": {
"inspect.py": {
"title": "Inspect item",
"description": "Checks one item without changing it.",
"inputs": [
{"name": "identifier", "label": "Identifier", "type": "text", "required": true},
{"name": "attempts", "label": "Attempts", "type": "integer", "minimum": 1, "maximum": 5, "default": 2},
{"name": "mode", "label": "Mode", "type": "choice", "options": ["standard", "extended"]},
{"name": "alerts", "label": "Alerts", "type": "multi_choice", "options": ["email", "webhook"]},
{"name": "review_csv", "label": "Review CSV", "type": "file", "accept": [".csv"], "maximum_bytes": 10485760}
],
"wait_for_result": true,
"execution": {
"field": "mode",
"dry_run_value": "review",
"dry_run_label": "Review",
"execute_value": "apply",
"execute_label": "Execute"
}
}
}
}
title is optional on a group and task. A group otherwise uses its directory
name; a task otherwise uses its filename without .py, with underscores made
readable. description is optional on a task and otherwise comes from the
first non-empty line of its module docstring. The order of keys in tasks
controls the listed tasks' display order. Include a task as {} when only
ordering is needed. Unlisted direct Python files follow alphabetically.
Each task declaration may contain title, description, inputs,
wait_for_result, download_artifacts, and execution. Set
wait_for_result for short tasks where the form should remain blocked until it
reports success or failure. The control panel disables its controls and warns
before page unload while such a task runs. download_artifacts additionally
starts the first non-JSON artifact download after a successful browser run;
reserve it for files such as archives that the browser should download
automatically.
Every input has name, label, and type; required defaults to false.
default is allowed for every type except file.
text: optionalpattern, a Python-compatible regular expression.integer: optionalminimum,maximum, and positivestep.dateanddatetime: ISO-formatted browser date controls.choice:optionsis required, as strings or{ "value", "label" }objects.multi_choice: the same requiredoptions; itsdefaultis an array.file: optional lowercase extension list inacceptand positive byte cap inmaximum_bytes.
Set sensitive to true on text or file inputs that must not appear in run
history. Text is masked, the database records [redacted], and the original
value is available only through a private execution-input file. Uploaded files
are likewise retained privately for the run and then removed.
For review-first tasks, use execution. It produces one split button: click
the main part to use its current mode, or use the arrow to select the other
mode. A successful review run can be repeated with its saved inputs in the
execution mode from the run-detail page. The task must still revalidate the
live filesystem before changing anything.
{
"field": "mode",
"dry_run_value": "review",
"dry_run_label": "Review",
"execute_value": "apply",
"execute_label": "Execute"
}
The server validates every submitted value before a run is queued. It records
the validated object in the run database and writes it to a per-run file.
Sensitive fields are redacted in the database and use a private 0600
execution file instead. Tasks read the applicable file path from
SERVER_MAINTENANCE_INPUT; no user value is appended to the command line. A
task without an entry in the manifest gets an empty JSON object. Uploaded files
are stored in a private per-run directory; their absolute paths are provided in
the input JSON and the files are removed when the task finishes, fails, or is
cancelled.
Artifacts
Tasks can write files to the directory named by
SERVER_MAINTENANCE_ARTIFACTS. Run-detail pages show them when the task
finishes. JSON artifacts offer an Open link that renders the JSON in a new
tab and leaves the artifact available; other artifacts offer a download link.
The server deletes an artifact after its download transfer ends; any artifact
not downloaded is removed after 24 hours. A browser cannot confirm that a
downloaded file was retained on the client device.
Background tasks
An optional background-tasks.json beside a group's task files declares
internal recurring scripts. These scripts must live below the group's
internal/ directory, are not exposed in the UI, and are launched at most once
per configured interval. Their latest output is retained below the persistent
state directory rather than being added to normal run history.
{
"version": 1,
"tasks": [
{"id": "poll", "script": "internal/poll.py", "interval_seconds": 60}
]
}
compose.example.yaml is suitable for adding the service to an existing Compose
stack. Set PYTHON_TOOLS_VOLUME_ROOT to the host directory that will hold
state, tasks, and credentials; set
SERVER_MAINTENANCE_ALLOWED_PROXY_IPS to the reverse proxy's direct peer
address. Keep the application behind an authenticated reverse proxy.