Skip to content

Latest commit

 

History

199 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NOTE: this README is a brief overview.
Best source of truth is =dron --help=.

#+begin_src python :results drawer replace :exports results
from dron.cli import cli
return cli.help.replace('\b', '')
#+end_src

#+RESULTS:
:results:
dron -- simple frontend for Systemd, inspired by cron.


- *d* stands for 'Systemd'
- *ron* stands for 'cron'

dron is my attempt to overcome things that make working with Systemd tedious
:end:

* What does it do?

=dron= is a small Python CLI for defining scheduled jobs in Python while letting
the operating system do the actual running and scheduling:

- on Linux it manages user-level =systemd= =.service= and =.timer= units
- on macOS it manages user-level =launchd= LaunchAgents

Jobs live in a Python module, defaulting to =drontab.<hostname>=. The module must
define a =jobs()= function that yields =dron.api.job(...)= objects.

#+begin_src python :results drawer replace :exports results
from dron.cli import _drontab_example
return _drontab_example()
#+end_src

#+RESULTS:
:results:
from dron.api import job

# at the moment you're expected to define jobs() function that yields jobs
# in the future I might add more mechanisms
def jobs():
    # simple job that doesn't do much
    yield job(
        'daily',
        '/home/user/scripts/run-borg /home/user',
        unit_name='borg-backup-home',
    )

    yield job(
        'daily',
        'linkchecker https://beepb00p.xyz',
        unit_name='linkchecker-beepb00p',
    )

    # drontab is simply python code!
    # so if you're annoyed by having to remember Systemd syntax, you can use a helper function
    def every(*, mins: int) -> str:
        return f'*:0/{mins}'

    # make sure my website is alive, it will send local email on failure
    yield job(
        every(mins=10),
        'ping https://beepb00p.xyz',
        unit_name='ping-beepb00p',
    )
:end:

Run =dron apply= after editing the module. =dron= imports the jobs, builds the
desired backend units, compares them with currently managed units, and applies
the add/update/delete plan.

* Why?

In short, because I want to benefit from the heavy lifting that Systemd does: timeouts, resource management, restart policies, powerful scheduling specs and logging,
while not having to manually manipulate numerous unit files and restart the daemon all over.

I elaborate on what led me to implement it and motivation [[https://beepb00p.xyz/scheduler.html#what_do_i_want][here]]. Also:

- why not just use [[https://beepb00p.xyz/scheduler.html#cron][cron]]?
- why not just use [[https://beepb00p.xyz/scheduler.html#systemd][systemd]]?

* Setting up

1. install system dependencies if needed; on Linux, =dbus-python= may require
   DBus development libraries
2. install =dron=: =pip3 install --user git+https://github.com/karlicoss/dron=
3. install =sendmail= from your package manager if you want local job failure
   emails

* Using

#+begin_src python :results output replace :exports results
from click.testing import CliRunner
from dron.cli import cli

print(CliRunner().invoke(cli, ['--help'], prog_name='dron').output)
#+end_src

#+RESULTS:
#+begin_example
Usage: dron [OPTIONS] COMMAND [ARGS]...

  dron -- simple frontend for Systemd, inspired by cron.

  - *d* stands for 'Systemd'
  - *ron* stands for 'cron'

  dron is my attempt to overcome things that make working with Systemd tedious

Options:
  --marker TEXT  Use custom marker instead of default `(MANAGED BY DRON)`.
                 Useful for developing/testing.
  --help         Show this message and exit.

Commands:
  apply      Apply drontab (like 'crontab' with no args)
  debug      Print some debug info
  job        Actions on individual jobs
  monitor    Monitor services/timers managed by dron
  print      Parse and print drontab
  uninstall  Remove all managed jobs (will ask for confirmation)

  ,* Why?

  In short, because I want to benefit from the heavy lifting that Systemd does:
  timeouts, resource management, restart policies, powerful scheduling specs and
  logging, while not having to manually manipulate numerous unit files and
  restart the daemon all over.

  I elaborate on what led me to implement it and motivation
  [[https://beepb00p.xyz/scheduler.html#what_do_i_want][here]]. Also:

  - why not just use [[https://beepb00p.xyz/scheduler.html#cron][cron]]?
  - why not just use [[https://beepb00p.xyz/scheduler.html#systemd][systemd]]?

#+end_example

Common commands:

- =dron apply --dry-run= imports the drontab module, verifies and generates
  backend units, and shows the add/update/delete plan without applying it
- =dron apply= applies that plan
- =dron print= shows parsed jobs, with =--pretty= for a table
- =dron monitor= opens the Textual monitor; =dron monitor --once= prints a
  one-shot table
- =dron job past <unit>= shows previous runs for one unit
- =dron job run <unit>= runs one job immediately, ignoring the timer
- =dron uninstall= removes all managed jobs after confirmation

Use =--module= with =apply= and =print= to point at a different drontab module.

* Job syntax

The idea is that it's a simple Python DSL that lets you define jobs with minimal
friction.

#+begin_src python :eval no
job(when, command, *, unit_name=None, on_failure=(notify.email_local,), **kwargs)
#+end_src

=when= is a backend schedule string such as =daily= or =*:0/10=. Passing =None=
creates a service without a timer, so it can still be run manually with
=dron job run=. =command= can be a shell string or a sequence of command parts.

Any extra keyword arguments are passed to the backend unit generator, which is
useful when you need systemd-specific service properties.

=load_profile=True= passes =--load-profile= to the macOS launchd wrapper, which sources =~/.profile= in noninteractive =/bin/bash= when launching jobs and failure notifications.
The job and its failure notification commands inherit the exported variables, including Python's startup settings.
This runs on every invocation and does not depend on the launchd session environment being initialized first.
This is opt-in and only supported by the launchd backend.

To enable it by default in a shared macOS drontab module:

#+begin_src python :eval no
from functools import partial
from dron.api import job as dron_job

job = partial(dron_job, load_profile=True)
#+end_src

* Caveats

- =launchd= support intentionally has a smaller schedule surface than =systemd=
- older =systemd= versions only accepted absolute paths for =ExecStart=; generated
  units are verified before =apply= installs them

* Potential improvements

- make applying changes more atomic, for example rolling back all changes until
  daemon reload succeeds
- more failure report mechanisms

About

What if cron and systemd had a baby?

Topics

Resources

Stars

60 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages