Metadata-Version: 2.5
Name: zabbix-crontroller
Version: 0.0.1
Summary: Monitor cron jobs in Zabbix with crontab as the single source of truth.
Project-URL: Homepage, https://github.com/theriverman/zabbix-crontroller
Project-URL: Repository, https://github.com/theriverman/zabbix-crontroller
Author-email: Kristof Daja <22156894+theriverman@users.noreply.github.com>
License: MIT
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: No Input/Output (Daemon)
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: English
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Monitoring
Classifier: Topic :: System :: Systems Administration
Classifier: Topic :: Text Processing
Classifier: Topic :: Utilities
Requires-Python: >=3.14
Description-Content-Type: text/markdown

# Zabbix-cRontroller
Monitor cron jobs in Zabbix with crontab as the single source of truth

# Introduction
Keep job schedules and metadata in the user's crontab. The `zc` execution wrapper
records each run's exit status, duration, stdout and stderr in private per-user
storage. It forwards output to cron or the entry's existing redirections.

This implementation provides local run evidence. Zabbix integration and direct
file, syslog and journal collection are not implemented yet.

# Installation & Integration

Use Python 3.14 and uv on Linux with cron installed. From a checkout, install for
the current user and create the short-name symlink:

```sh
UV_TOOL_BIN_DIR="$HOME/.local/bin" uv tool install --python 3.14 .
ln -s zabbix-crontroller "$HOME/.local/bin/zc"
```

For a system installation with sudo:

```sh
sudo env UV_TOOL_DIR=/opt/zabbix-crontroller/tools \
  UV_PYTHON_INSTALL_DIR=/opt/zabbix-crontroller/python \
  UV_TOOL_BIN_DIR=/usr/local/bin uv tool install --python 3.14 .
sudo ln -s zabbix-crontroller /usr/local/bin/zc
```

These are local installations, not release builds. Release packages are produced
only by tagged GitHub workflows. The executable is `zabbix-crontroller`; `zc`
must be a symlink to it. Use its absolute path in cron, or explicitly set cron's
`PATH` to include its directory. A non-root example is
`PATH=/home/batch/.local/bin:/usr/bin:/bin`; cron does not expand `$HOME` in PATH.
For system installations, the uv tool environment and interpreter must be readable
and executable by the job owners; the shared `/opt` locations avoid placing them
inside root's private home. Validate from each job owner's account.

Validate a candidate file, or the current user's installed crontab:

```sh
zc validate example.crontab
zc validate
```

Validation checks the complete syntax and then wrapper and shell availability as
the invoking user. It never executes a job. Success returns `0`; invalid input,
acquisition failures and unavailable executables return `1`. Missing crontabs are
failures, not successful empty configurations.

# Syntax

Keep schedules and commands in your normal user crontab. The
[Syntax Specification](docs/SYNTAX_SPECIFICATION.md) defines the frozen `1.0`
notation supported by the parser.

1. Declare `# zc:version = "1.0"` exactly once, before `# zc:start`.
2. Enclose all monitored jobs in one section, from `# zc:start` to `# zc:end`.
3. Start each job declaration with `job`: a stable, unique identifier using
   lowercase ASCII letters, digits, underscores or hyphens, beginning with a letter.
4. Add a required `name` and, if useful, an optional `description`. These can
   contain Unicode; the description may appear before or after the name.
5. Put exactly one cron entry immediately after its metadata. It completes the
   job declaration. `# zc:end` closes the entire section, not an individual job.
6. Begin the command with `zc run JOB -- 'COMMAND'`, repeating the exact `job`
   identifier. Use one literally quoted shell command. Both executable names and
   absolute paths ending in `zc` or `zabbix-crontroller` are accepted.

For example, a description is not required:

```cron
# zc:version = "1.0"
PATH=/usr/local/bin:/usr/bin:/bin
# zc:start
# zc:job = "backup-postgres"
# zc:name = "Orders database backup"
30 1 * * * zc run backup-postgres -- '/opt/company/maintenance/bin/backup-postgres --database orders'
# zc:end
```

Write one property per line using a quoted TOML string. Keep each metadata header
and its cron entry together. Blank lines, ordinary comments and environment
assignments may appear between completed jobs. Delimiters may have trailing
`#` comments, but take no value. Changing a name or command does not require
changing the job identifier.

The wrapper executes the quoted command using cron's `SHELL`, or `/bin/sh` when
unset. Put pipelines, command chains and substitutions inside single quotes so
they execute within the monitored shell. Keep existing output redirections
outside the quotes if they should receive forwarded output while capture stays
separate. Use `'"'"'` to include an apostrophe in a single-quoted command.
Cron still requires escaping literal percentages as `\%`, even inside quotes.
See [runtime behaviour and evidence](docs/RUNTIME.md) for limits and failure
handling.

Every cron entry inside the section needs its own metadata. An additional entry
without metadata is an error, including after the final job. Unannotated entries
are allowed before and after the section. Job metadata outside the section is
an error. An empty section is valid, but both delimiters remain required.

Commenting out only a job's cron entry is an error: its metadata will not attach
to a later entry. To stop discovering a job while keeping it running, remove its
metadata header **and move its cron entry outside the monitored section**.
Preserve the relevant environment settings when moving it; section boundaries
do not reset the cron environment. Removing only the header inside the section
is an error. Remove the entire declaration to remove both monitoring and execution.
Malformed configurations prevent publication of the discovery snapshot.

## Syntax example

The [example crontab](example.crontab) contains five monitored jobs and a separate
unmonitored entry:

```cron
# Production batch jobs — svc_batch@lon-batch-01
# Owner: Platform Operations
# Host timezone: Europe/London. Change requests must reference an approved ticket.

# zc:version = "1.0"

SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=platform-operations@example.com
HOME=/var/lib/batch

# zc:start

# zc:job = "supplier-sync"
# zc:name = "Supplier stock synchronisation"
# zc:description = "Collect supplier stock feeds during business hours, Monday–Friday."
12 7-19 * * 1-5 zc run supplier-sync -- '/usr/bin/flock -n /var/lib/batch/locks/supplier-sync.lock /opt/company/integrations/bin/supplier-sync --config /etc/company/supplier-sync.toml' >> /var/log/batch/supplier-sync.log 2>&1

# zc:job = "backup-postgres"
# zc:name = "Orders database backup"
# zc:description = "Nightly database backup; credentials supplied via .pgpass."
30 1 * * * zc run backup-postgres -- '/opt/company/maintenance/bin/backup-postgres --database orders --retention-days 14' >> /var/log/batch/backup-postgres.log 2>&1

# zc:job = "reconcile-payments"
# zc:name = "Reconcile payments"
# zc:description = "Generate the previous business day's reconciliation report before Finance arrives."
15 6 * * 1-5 zc run reconcile-payments -- '/opt/company/finance/bin/reconcile-payments --period previous-business-day' >> /var/log/batch/reconcile-payments.log 2>&1

# zc:job = "staging-cleanup"
# zc:name = "Staging cleanup"
# zc:description = "Remove expired integration staging files during the Sunday maintenance window."
45 3 * * 0 zc run staging-cleanup -- '/usr/bin/find /var/lib/batch/staging -xdev -type f -mtime +30 -delete' >> /var/log/batch/staging-cleanup.log 2>&1

# zc:job = "sap-export"
# zc:name = "SAP export"
# zc:description = "Forward pending warehouse transactions to SAP; prevent overlapping runs."
*/5 * * * * zc run sap-export -- '/usr/bin/flock -n /var/lib/batch/locks/sap-export.lock /opt/company/integrations/bin/sap-export --env production' >> /var/log/batch/sap-export.log 2>&1

# zc:end

# This separate cron entry runs outside monitoring.
45 3 * * 0 /usr/bin/find /var/lib/batch/staging -xdev -type f -mtime +30 -delete >> /var/log/batch/staging-cleanup.log 2>&1
```

# Library usage

Import the parser independently of the application. The caller supplies the
crontab text; the parser does not run `crontab -l`, execute jobs, read files or
contact Zabbix.

```python
from pathlib import Path

from zabbix_crontroller.parser import parse_crontab

text = Path("example.crontab").read_text(encoding="utf-8")
crontab = parse_crontab(text)

for job in crontab.jobs:
    print(job.job_id, job.name, job.schedule, job.command)
```

`parse_crontab(text: str) -> Crontab` returns the declared `version` and a tuple of
`CronJob` records in source order. Jobs contain the identifier, name, optional
description, schedule, complete command, `wrapper_executable`, environment
snapshot and source line numbers. The wrapper field contains its decoded name
or absolute path; parsing does not check whether that executable is installed.
Records and their environment mappings are read-only. Missing or blank
descriptions become `None`. An explicitly empty monitored section returns an
empty `jobs` tuple.

The parser extracts five-field schedules and `@` nicknames. Schedule field
values, nickname availability, calendar ranges, time zones and daemon-specific
extensions are **not validated**. This is a metadata parser, not a replacement
for the daemon's crontab validator. Input is interpreted as a user crontab, with
no seconds or username column. Commands retain their internal and trailing
whitespace, quoting and cron percent sequences without evaluation. Only the
literal wrapper invocation is validated; the inner shell programme remains
opaque. Unmonitored commands retain their previous parsing behaviour.

Each environment snapshot contains only preceding explicit assignments, in file
order, with outer quotes removed. It does not add process environment variables
or cron defaults, perform substitutions, or model daemon-specific overrides.

Invalid input raises `ParseError`, with `ConfigurationError` for version errors
and `CrontabSyntaxError` for other parsing errors. Exceptions expose `message`,
`line_number`, `job_id`, `declaration_line_number` and `previous_line_number`;
inapplicable locations are `None`. Source lines are one-based. The exception text
includes the relevant locations. Parsing returns a complete result or raises an
error; it never returns partial discovery data. Non-string input raises
`TypeError`.

# Contributing

See [CONTRIBUTE.md](CONTRIBUTE.md) for development setup, tests and contribution
guidelines.
