aboutsummaryrefslogtreecommitdiff
path: root/src/property/cron.lisp
blob: d486ed9429a7761e94a371240099399337c6edf7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
;;; Consfigurator -- Lisp declarative configuration management system

;;; Copyright (C) 2021  Sean Whitton <spwhitton@spwhitton.name>

;;; This file is free software; you can redistribute it and/or modify
;;; it under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3, or (at your option)
;;; any later version.

;;; This file is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.

;;; You should have received a copy of the GNU General Public License
;;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

(in-package :consfigurator.property.cron)
(named-readtables:in-readtable :consfigurator)

;;; A number of techniques here are from Propellor's Cron properties module.

(defpropspec system-job :posix (desc when user shell-command)
  "Installs a cronjob running SHELL-COMMAND as USER to /etc/cron.*.
DESC must be unique, as it will be used as a filename for a script.  WHEN is
either :DAILY, WEEKLY, :MONTHLY or a string formatted according to crontab(5),
e.g. \"0 3 * * *\".

The output of the cronjob will be mailed only if the job exits nonzero."
  (:desc #?"Cronned ${desc}")
  (:hostattrs
   ;; /etc/cron.* is Debian-specific.  Also, we rely on runuser(1), which is
   ;; Linux-specific.  This is done because su(1) for non-interactive usage
   ;; has some pitfalls, and the command line argument '-c' is not portable.
   (os:required 'os:debianlike))
  (let* ((times (not (keywordp when)))
         (dir (ensure-directory-pathname
               (strcat "/etc/cron." (if (keywordp when)
                                        (string-downcase (symbol-name when))
                                        "d"))))
         (job (merge-pathnames (string->filename desc) dir))
         (script (merge-pathnames (strcat (string->filename desc) "_cronjob")
                                  #P"/usr/local/bin/"))
         (script* (escape-sh-token (unix-namestring script))))
    `(eseqprops
      (apt:service-installed-running "cron")
      (apt:installed "moreutils")
      (file:has-content ,job
        ,`(,@(and (not times) '("#!/bin/sh" "" "set -e" ""))
           "SHELL=/bin/sh"
           "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"
           ""
           ,@`(,(if times
                    #?"${when}	${user}	chronic ${script*}"
                    (if (string= user "root")
                        (format nil "chronic ~A" script*)
                        (format nil "chronic runuser -u ~A -c ~A"
                                user script*)))))
        ,@(and (not times) '(:mode #o755)))
      ;; Using a separate script makes for more readable e-mail subject lines,
      ;; and also makes it easy to do a manual run of the job.
      (file:has-content ,script
        ,`("#!/bin/sh"
           ""
           "set -e"
           ""
           ;; Use flock(1) to ensure that only one instance of the job is ever
           ;; running, no matter how long one run of the job takes.
           ,(format nil "flock -n ~A sh -c ~A"
                    (escape-sh-token (unix-namestring job))
                    (escape-sh-token shell-command)))
        :mode #o755))))

(defproplist nice-system-job :posix (desc when user shell-command)
  "Like CRON:SYSTEM-JOB, but run the command niced and ioniced."
  (:desc #?"Cronned ${desc}, niced and ioniced")
  (system-job desc when user (format nil "nice ionice -c 3 sh -c ~A"
                                     (escape-sh-token shell-command))))