18

I have a deployment script.  It must add something to a user crontab (trigger a script that cleans logs every XXX days); however, this must be done only during the first deployment, or when it needs to be updated.

(I can run xxx.py deploy env or xxx.py update env.)

so I need to do this:

Check if my cronJob already exists
Submit my cronJob if it does not already exist
or
update my cronJob if one of the parameter(s) of the command is different

I don't see how to add/check/remove something to the crontab without using crontab -e or "manually" editing the crontab file (download it, rewrite it, re-upload it).

PS: this is a user specific cronjob; "webadmin" is going to do it and he should not use sudo to do it.

8 Answers8

21

My best idea so far

To check first if the content matches what should be in there and only update if it doesn't:

if [[ $(crontab -l | egrep -v "^(#|$)" | grep -q 'some_command'; echo $?) == 1 ]]
then
    set -f
    echo $(crontab -l ; echo '* 1 * * * some_command') | crontab -
    set +f
fi

but this gets complicated enough to build a separate script around that cron task.

Other ideas

You could send the string via stdin to crontab (beware, this clears out any previous crontab entries):

echo "* 1 * * * some_command" | crontab -

This should even work right through ssh:

echo "* 1 * * * some_command" | ssh user@host "crontab -"

if you want to append to the file you could use this:

# on the machine itself
echo "$(echo '* 1 * * * some_command' ; crontab -l 2>&1)" | crontab -
# via ssh
echo "$(echo '* 1 * * * some_command' ; ssh user@host crontab -l 2>&1)" | ssh user@host "crontab -"
7

For the record I'm going to suggest using /etc/cron.d/. Only root can write files here but the entries can be configured to run as any user (without need for sudo at run-time). This example defines the task named my_webadmin that will execute /usr/local/bin/tidy_logfiles as the user webadmin every Sunday at midnight:

echo '0 0 * * 0 webadmin /usr/local/bin/tidy_logfiles' >/etc/cron.d/my_webadmin

An important part is that the my_webadmin should be unique to you (not necessarily unique for the run, though) because any installation package can also write files here and you want to avoid a clash. Having this uniqueness constraint, you can update my_webadmin with a simple overwrite, since you know it's "yours" and won't contain entries for anyone/anything else.

Furthermore, with this approach it becomes trivial to remove the cron entry

rm -f /etc/cron.d/my_webadmin

Possibly outside the scope of your question, but if you have remote access to the root account (or via sudo) you can even provision remotely,

echo '0 0 * * 0 webadmin /usr/local/bin/tidy_logfiles' > ~/webadmin.cron
scp -p ~/webadmin.cron root@remote_host:/etc/cron.d/my_webadmin

or,

echo '0 0 * * 0 webadmin /usr/local/bin/tidy_logfiles' |
    ssh -q root@remote_host 'cat >/etc/cron.d/my_webadmin'

and remove the provisioning,

ssh -nq root@remote_host rm -f /etc/cron.d/my_webadmin

(Note that in many cases you cannot provide root's password for the scp/ssh commands because the root account is constrained to prevent password-based logins. Instead you need to have set up public/private key certificates. Also, by implication the local account (whatever it is) will have full root access to the remote server.)

Chris Davies
  • 116,213
  • 16
  • 160
  • 287
  • It's my client server, I cannot log as root, I can sudo into it BUT, if I do that they'll kill me.

    This is a webadlmin job so it should only be in the webadmin stuff, that's what the sysadmin told me.

    – sliders_alpha Jul 21 '16 at 15:40
  • @sliders_alpha the job only runs as webadmin. It's the provisioning that requires root equivalence. However, I'll look for a non-root solution too. – Chris Davies Jul 21 '16 at 15:44
  • 2
    +1. /etc/cron.d/ exists for exactly this purpose - so that packages / deployments can just drop a crontab file in here. – cas Jul 22 '16 at 00:20
  • cas and roaima are spot on. this is why /etc/cron.d/ exists -- so you can "install" a crontab for a specific app without having to append / detect anything – CompEng88 Apr 15 '21 at 15:28
  • Is there a way to make cronjobs put in /etc/cron.d/somefile show up in a (non-root) user's crontab -l? How do I make it so that those jobs are "for a specific user"? You mention, that it is possible to configure the jobs in /etc/cron.d/ to run without root. How? – Zelphir Kaltstahl Feb 22 '22 at 11:56
  • 1
    @ZelphirKaltstahl (a) no, (b) I've updated the answer (first paragraph) to reference the username field of this system crontab file, (c) the example I gave runs without root – Chris Davies Feb 22 '22 at 12:23
3

I highly recommend using Ansible* for this rather than rolling your own. Or Puppet or Chef — but Ansible is well-suited for zero-infrastructure deploy scripts like this.

That's because there are already modules meant to solve problems like this, and config management tools have have idempotence as a basic design goal — that's the property of only changing when it needs to even if you accidentally (or intentionally) run it again.

In particular, Ansible's cron module can modify user crontabs. As a bonus, if you want to later adjust to use system crontabs, it'll be a very easy tweak rather than a rewrite.


* disclaimer: I work for Red Hat, and Ansible is a Red Hat sponsored project.

mattdm
  • 40,245
  • Yup, thing is, I didn't knew about ansible 2 month ago, and now we have a massive python deployer script (but he is MAGNIFICIENT, readable, maintanable, ;))

    Next time, I'll use ansible but right now going back is impossible (money money money)

    – sliders_alpha Jul 21 '16 at 15:38
2

If you want to add a cron job via the target account, run crontab -e. This command passes the crontab through an editor. Tell it to use an editor command that modifies the crontab as you desire. The editor command is executed as a shell snippet with the name of a temporary file appended.

unset VISUAL
EDITOR='update_crontab () {
  set -e
  new=$(mktemp)
  if <"$1" grep -v "^#" | grep -w do_stuff; then
    # Remove existing entries containing do_stuff
    grep -v -w do_stuff "$1" >"$new"
  else
    cp "$1" "$new"
  fi
  # Add the new entry
  echo "1 2 3 4 5 do_stuff --new-options" >>"$new"
  mv "$new" "$1"
}
update_crontab' crontab -e

This approach is more reliable than the native crontab -l | … | crontab - because this one is vulnerable to a race condition if the crontab is edited concurrently: modifications made between the call to crontab -l and the call to crontab - would be undone.

2

This is an adaptation of what @phillip-zyan-k-lee-stockmann offered, based on his "Best idea so far" code.

My changes from his (excellent and helpful snippet) are basically:

  • Regex for not just the command name, but also the whole entry including time strings. That way it could support adding a command even if there are same-named or overlapping-named commands in other entries. (It still won't add the same command on the same schedule twice.)
  • A bit of logging
  • I switched (and named) mine to hourly for various reasons; easy to adjust it back per crontab syntax

And so here's my code for what I called crontab-add-hourly.sh:

#!/bin/bash

# PURPOSE:
# To allow simple, programmatic addition of commands/entries into the crontab (if not already present)

cmd=$1
entry="0 * * * * $cmd"
printf "we want to add this entry:\n$entry\n\n" 
escapedEntry=$(printf '%s\n' "$entry" | sed 's:[][\/.^$*]:\\&:g') #from: https://unix.stackexchange.com/a/129063/320236
printf "but first we'll see if it's already in there using this regex pattern:\n$escapedEntry\n\n"

if [[ $(crontab -l | egrep -v '^(#|$)' | grep -q "$escapedEntry"; echo $?) == 1 ]] # from: https://unix.stackexchange.com/a/297377/320236
then
    printf "all clear; pattern was not already present; adding command to crontab hourly:\n$cmd\n\n"
    (crontab -l ; printf "$entry\n\n") | crontab -
else
    printf "pattern already present; no action taken\n\n"
fi

Example usage and output:

$ ./crontab-add-hourly.sh my-script.bash

we want to add this entry:
0 * * * * my-script.bash

but first we'll see if it's already in there using this regex pattern:
0 \* \* \* \* my-script\.bash

all clear; pattern was not already present; adding command to crontab hourly:
my-script.bash
  • If the $cmd or $entry contains % it will trigger unexpected formatting in the printf statement. Don't do printf "$var/n" but instead use printf "%s/n" "$var" – Chris Davies Feb 09 '21 at 06:20
2

TL;DR: This actually works, tested in Bash 4.4.

if [[ $(crontab -l | egrep -v "^(#|$)" | grep -q 'some_command'; echo $?) == 1 ]]
then
    printf '%s\n' "$(crontab -l; echo '* * * * * some_command')" | crontab -
fi
Chris Davies
  • 116,213
  • 16
  • 160
  • 287
  • (1) Why not say just (crontab -l; echo '* * * * * some_command') | crontab - and leave out the printf?  (2) You don’t need to echo $?, and capture the output from that to test the exit status from a command.  You could do crontab -l | egrep -v "^(#|$)" | grep -q 'some_command' and then (on the next line, or separated by a ;) if [ "$?" = 1 ].  (Yes, even $? should be quoted.)  But why compare to 1?  It’s standard to compare to 0 (e.g.,  if [ "$?" != 0 ].  But then; you don’t need to use $? at all; … (Cont’d) – G-Man Says 'Reinstate Monica' Aug 14 '22 at 20:57
  • (Cont’d) … just do if ! crontab -l | egrep -v "^(#|$)" | grep -q 'some_command' (… then).  (3) And why bother to grep out blank lines?  grep 'some_command' isn’t going to match a blank line; why not just say grep -v "^#" | grep -q 'some_command'?  But then consider grep -q '^[^#]*some_command'.  (Of course you should be sure that some_command isn’t blank, or a regular expression that matches anything other than itself.)  (4) @roaima: You know you said /n when you meant \n in your comment, right? – G-Man Says 'Reinstate Monica' Aug 14 '22 at 20:57
  • @G-ManSays'ReinstateMonica' thanks, I keep doing that here on SE; not sure why. I've removed the comment as it seems that at some point I just edited the answer anyway – Chris Davies Aug 14 '22 at 21:13
1

To add new job from terminal

(crontab -l 2>/dev/null; echo "25 2 * * * command/script") | crontab -

To modify existing cron job entry from terminal

(crontab -l | sed -e 's|old_part_of_job|new_part_of_job|g') | crontab -
Firoz
  • 11
  • 1
0
echo "$(crontab -l 2>&1 ; echo '58 6 16 3 * some_command )" | crontab -

TO append to Crontab above command works best for me. It will retain old crontab jobs