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.

6
  • 2
    Does it have to be in a user-specific crontab? Most prepackaged cron jobs go into one of the /etc/cron.* directories.
    – user
    Jul 21, 2016 at 13:26
  • Does CentOS have /etc/cron.d? If so, put your script in there using a name unique to your application
    – roaima
    Jul 21, 2016 at 13:26
  • yes it's user specific. I can't add it to /etc/cron.d because it's a root file, thus only root's job are alowed inside (I could sudo but that a bad practice I've been told) Jul 21, 2016 at 13:28
  • 1
    same as /etc/crontab, files in /etc/cron.d/ have an extra field for the username, immediately after the schedule spec. e.g. * * * * * username /path/to/script. See man 5 crontab and search for SYSTEM CRON.
    – cas
    Jul 21, 2016 at 13:30
  • See also stackoverflow.com/questions/610839/…
    – rogerdpack
    Apr 17, 2018 at 19:29

8 Answers 8

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 -"
10
  • that depends: do you need what would have been in there? :) Jul 21, 2016 at 13:18
  • awww ... not running as root? ... I'll rewrite it ... Jul 21, 2016 at 13:30
  • eh, I like that :D Jul 21, 2016 at 15:39
  • 1
    See my answer further down for a working fix to the * (wildcard) expansion issue. Feb 26, 2020 at 7:57
  • 1
    your answer puts all the crontabs in a single line, so it doesnt work
    – CompEng88
    Apr 15, 2021 at 15:05
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.)

6
  • 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. Jul 21, 2016 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.
    – roaima
    Jul 21, 2016 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, 2016 at 0: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, 2021 at 15:28
  • 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
    – roaima
    Feb 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.

1
  • 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) Jul 21, 2016 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
1
  • 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"
    – roaima
    Feb 9, 2021 at 6: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
3
  • (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) Aug 14 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? Aug 14 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
    – roaima
    Aug 14 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 -
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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.