cronscript 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. #!/bin/bash
  2. # No way I try to deal with a crippled sh just for POSIX foo.
  3. # Copyright (C) 2009-2016, 2018 Joerg Jaspert <joerg@debian.org>
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License as
  7. # published by the Free Software Foundation; version 2.
  8. #
  9. # This program is distributed in the hope that it will be useful, but
  10. # WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. # General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  17. # Homer: Are you saying you're never going to eat any animal again? What
  18. # about bacon?
  19. # Lisa: No.
  20. # Homer: Ham?
  21. # Lisa: No.
  22. # Homer: Pork chops?
  23. # Lisa: Dad, those all come from the same animal.
  24. # Homer: Heh heh heh. Ooh, yeah, right, Lisa. A wonderful, magical animal.
  25. # exit on errors
  26. set -e
  27. # A pipeline's return status is the value of the last (rightmost)
  28. # command to exit with a non-zero status, or zero if all commands exit
  29. # successfully.
  30. set -o pipefail
  31. # make sure to only use defined variables
  32. set -u
  33. # ERR traps should be inherited from functions too. (And command
  34. # substitutions and subshells and whatnot, but for us the functions is
  35. # the important part here)
  36. set -E
  37. # If the extglob shell option is enabled using the shopt builtin,
  38. # several extended pattern matching operators are recognized. We use
  39. # it for the POSSIBLEARGS and the first case ${ARGS} matching.
  40. shopt -s extglob
  41. # And use one locale, no matter what the caller has set
  42. export LANG=C.UTF-8
  43. export LC_ALL=C.UTF-8
  44. # One arg please
  45. declare -lr ARG=${1:-"meh"}
  46. # program name is the (lower cased) first argument.
  47. PROGRAM="${ARG}"
  48. # import the general variable set. (This will overwrite configdir, but
  49. # it is expected to have the same value)
  50. export SCRIPTVARS=${configdir:?Please define configdir to run this script}/vars
  51. . ${SCRIPTVARS}
  52. # set DEBUG if you want to see a little more logs (needs to be used more)
  53. DEBUG=${DEBUG:-0}
  54. # Check if the argument is a known one. If so, lock us so that only
  55. # one copy of the type of cronscript runs. The $type.tasks file is
  56. # mandantory, so use that for locking.
  57. case ${ARG} in
  58. ${POSSIBLEARGS})
  59. # Only one of me should ever run.
  60. FLOCKER=${FLOCKER:-""}
  61. [[ ${FLOCKER} != ${configdir}/${PROGRAM}.tasks ]] && exec env FLOCKER="${configdir}/${PROGRAM}.tasks" flock -E 0 -en "${configdir}/${PROGRAM}.tasks" "$0" "$@" || :
  62. ;;
  63. *)
  64. cat - <<EOF
  65. This is the cronscript. It needs an argument or it won't do anything
  66. for you.
  67. Currently accepted Arguments: ${POSSIBLEARGS}
  68. To see what they do, you want to look at the files
  69. \$ARGUMENT.{tasks,functions,variables} in ${configdir}.
  70. EOF
  71. exit 0
  72. ;;
  73. esac
  74. function includetasks() {
  75. local NAME=${1:?}
  76. _preparetasks ${NAME}
  77. _runtasks ${NAME}
  78. }
  79. function _preparetasks() {
  80. local NAME=${1:?}
  81. # Each "cronscript" may have a variables and a functions file
  82. # that we source
  83. for what in variables functions; do
  84. if [[ -f ${configdir}/${NAME}.${what} ]]; then
  85. . ${configdir}/${NAME}.${what}
  86. fi
  87. done
  88. }
  89. function _runtasks() {
  90. local NAME=${1:?}
  91. # Which list of tasks should we run?
  92. local TASKLIST="${configdir}/${NAME}.tasks"
  93. # This loop simply wants to be fed by a list of values (see below)
  94. # made out of 5 columns.
  95. # The first four are the array values for the stage function, the
  96. # fifth tells us if we should background the stage call.
  97. #
  98. # - FUNC - the function name to call
  99. # - ARGS - Possible arguments to hand to the function. Can be the empty string
  100. # - TIME - The timestamp name. Can be the empty string
  101. # - ERR - if this is the string false, then the call will be surrounded by
  102. # set +e ... set -e calls, so errors in the function do not exit
  103. # the script. Can be the empty string, meaning true.
  104. # - BG - Background the function stage?
  105. #
  106. # ATTENTION: Spaces in arguments or timestamp names need to be escaped by \
  107. #
  108. # NOTE 1: There are special values for the first column (FUNC).
  109. # NOSTAGE - do not call stage function, call the command directly.
  110. # RMSTAGE - clean out the stages directory, and as such
  111. # the recording what already ran in an earlier cronscript.
  112. # Note: Only really makes sense at beginning of a tasks file,
  113. # the stages directory gets cleared at successful exit anyways.
  114. # RMSTAGE simply ensures that ALL of the crons tasks ALWAYS run.
  115. # INCLUDE - Runs another task list after including corresponding functions
  116. # Note 2: If you want to hand an empty value to the stage function,
  117. # use the word "none" in the list below.
  118. while read FUNC ARGS TIME ERR BACKGROUND; do
  119. debug "FUNC: $FUNC ARGS: $ARGS TIME: $TIME ERR: $ERR BG: $BACKGROUND"
  120. # Empty values in the value list are the string "none" (or the
  121. # while read loop won't work). Here we ensure that variables that
  122. # can be empty, are empty if the string none is set for them.
  123. for var in ARGS TIME; do
  124. if [[ ${!var} == none ]]; then
  125. typeset ${var}=''
  126. fi
  127. done
  128. # ERR/BACKGROUND are boolean for all but LOCK/UNLOCK, check that they are.
  129. for var in ERR BACKGROUND; do
  130. if [[ ${!var} != false ]] && [[ ${!var} != true ]]; then
  131. if [[ ${FUNC} != LOCK ]] && [[ ${FUNC} != UNLOCK ]]; then
  132. error "Illegal value ${!var} for ${var} (should be true or false), line for function ${FUNC}"
  133. fi
  134. fi
  135. done
  136. case ${FUNC} in
  137. NOSTAGE)
  138. ${ARGS}
  139. ;;
  140. RMSTAGE)
  141. # Make sure we remove our stage files, so all the
  142. # actions will be done again.
  143. rm -f ${stagedir}/*
  144. ;;
  145. LOCK)
  146. # We are asked to set a lock, so try to get it.
  147. # For this we redefine what the columns mean.
  148. # ARGS: Name of the lockfile
  149. # TIME: How long to wait for getting the (exclusive) lock
  150. # ERR: shared == shared lock, may be hold more than once, exclusive == exclusive, only one.
  151. lock ${ARGS} ${TIME} ${ERR}
  152. ;;
  153. UNLOCK)
  154. unlock ${ARGS}
  155. ;;
  156. INCLUDE)
  157. includetasks ${ARGS}
  158. ;;
  159. *)
  160. GO=(
  161. FUNC=${FUNC}
  162. TIME=${TIME}
  163. ARGS=${ARGS}
  164. ERR=${ERR}
  165. )
  166. if [[ ${BACKGROUND} == true ]]; then
  167. stage $GO &
  168. else
  169. stage $GO
  170. fi
  171. ;;
  172. esac < /dev/null
  173. done < <(grep -v '^#' ${TASKLIST} )
  174. }
  175. function lock() {
  176. local LOCK=${1:-}
  177. local TIME=${2:-600}
  178. local TYPE=${3:-exclusive}
  179. if [[ -z ${LOCK} ]]; then
  180. log_error "No lockfile name given"
  181. exit 21
  182. fi
  183. local LOCKFILE=
  184. if [[ $LOCK == /* ]]; then
  185. LOCKFILE=${LOCK}
  186. else
  187. # Prepend LOCK_ to lock name to get to variable name,
  188. # kind of namespace
  189. local lvar="LOCK_${LOCK}"
  190. LOCKFILE=${!lvar}
  191. fi
  192. # bash can't open a file read-only, while creating it,
  193. # so we need to create it ourselves.
  194. if ! [[ -e $LOCKFILE ]]; then
  195. install -m 444 /dev/null $LOCKFILE || {
  196. log_error "Could not create lock ${LOCKFILE}"
  197. laststeps 2
  198. }
  199. fi
  200. # Get filehandle
  201. local randomstring
  202. exec {randomstring}<${LOCKFILE}
  203. # Store filehandle for later
  204. LOCKFD[${LOCK}]=${randomstring}
  205. # "Abusing" the err column, expecting the shared/exclusive value there.
  206. # Any wrong value means exclusive.
  207. case ${ERR} in
  208. shared|exclusive)
  209. flockparm="--${ERR}"
  210. ;;
  211. *)
  212. flockparm="--exclusive"
  213. ;;
  214. esac
  215. # Deal with time being special, usually it means false or true,
  216. # but for locks we want a timeout. So if its set to one of the usuals,
  217. # assume 300
  218. if [[ ${TIME} == none ]]; then
  219. TIME=300
  220. fi
  221. # Now try to get the lock
  222. set +e
  223. flock ${flockparm} --timeout ${TIME} --conflict-exit-code 3 ${LOCKFD[${LOCK}]}
  224. ret=$?
  225. set -e
  226. case ${ret} in
  227. 0)
  228. return
  229. ;;
  230. 3)
  231. log_error "Could not get lock ${LOCKFILE}, timeout"
  232. laststeps 2
  233. ;;
  234. *)
  235. log_error "Could not get lock ${LOCKFILE}"
  236. laststeps 2
  237. esac
  238. }
  239. function unlock() {
  240. local LOCK=${1:-}
  241. if [[ -z ${LOCK} ]]; then
  242. # Warn, but continue, unlock will happen at script end time
  243. log "No lockfile name given"
  244. fi
  245. local randomstring=${LOCKFD[${LOCK}]}
  246. exec {randomstring}>&-
  247. }
  248. function laststeps() {
  249. local successval=${1:-0}
  250. # Redirect output to another file, as we want to compress our logfile
  251. # and ensure its no longer used
  252. exec > "$logdir/after${PROGRAM}.log" 2>&1
  253. # Now, at the very (successful) end of this run, make sure we remove
  254. # our stage files, so the next script run will do it all again.
  255. if [[ ${successval} -eq 0 ]]; then
  256. rm -f ${stagedir}/*
  257. fi
  258. bzip2 -9 ${LOGFILE}
  259. # Logfile should be gone, remove the symlink
  260. [[ -L ${logdir}/${PROGRAM} ]] && [[ ! -f ${logdir}/${PROGRAM} ]] && rm -f ${logdir}/${PROGRAM} || log "Logfile still exists or symlink gone already? Something fishy going on"
  261. # FIXME: Mail the log when its non-empty
  262. [[ -s "${logdir}/after${PROGRAM}.log" ]] || rm "${logdir}/after${PROGRAM}.log"
  263. }
  264. (
  265. # Where we store lockfile filehandles
  266. declare -A LOCKFD
  267. # common functions are "outsourced"
  268. . "${configdir}/common"
  269. # Timestamp when we started
  270. NOW=$(date "+%Y.%m.%d-%H:%M:%S")
  271. # A logfile for every cron script
  272. LOGFILE="${logdir}/${PROGRAM}_${NOW}.log"
  273. # Each "cronscript" may have a variables and a functions file
  274. # that we source
  275. _preparetasks ${PROGRAM}
  276. # Get rid of tempfiles at the end
  277. trap cleanup EXIT TERM HUP INT QUIT
  278. # An easy access by name for the current log
  279. ln -sf ${LOGFILE} ${logdir}/${PROGRAM}
  280. # And from here, all output to the log please
  281. exec >> "$LOGFILE" 2>&1
  282. # The stage function uses this directory
  283. # This amends the stagedir variable from "vars"
  284. stagedir="${stagedir}/${PROGRAM}"
  285. # Ensure the dir exists
  286. mkdir -p ${stagedir}
  287. # Run all tasks
  288. _runtasks ${PROGRAM}
  289. # we need to wait for the background processes before the end of the cron script
  290. wait
  291. # Common to all cron scripts
  292. log "Cron script successful, all done"
  293. laststeps 0
  294. )