Mini Shell
"""
Work with cron
.. note::
Salt does not escape cron metacharacters automatically. You should
backslash-escape percent characters and any other metacharacters that might
be interpreted incorrectly by the shell.
"""
import logging
import os
import random
import salt.utils.data
import salt.utils.files
import salt.utils.functools
import salt.utils.path
import salt.utils.stringutils
TAG = "# Lines below here are managed by Salt, do not edit\n"
SALT_CRON_IDENTIFIER = "SALT_CRON_IDENTIFIER"
SALT_CRON_NO_IDENTIFIER = "NO ID SET"
log = logging.getLogger(__name__)
def __virtual__():
if salt.utils.path.which("crontab"):
return True
else:
return (False, "Cannot load cron module: crontab command not found")
def _ensure_string(val):
# Account for cases where the identifier is not a string
# which would cause to_unicode to fail.
if not isinstance(val, str):
val = str(val)
try:
return salt.utils.stringutils.to_unicode(val)
except TypeError:
return ""
def _cron_id(cron):
"""SAFETYBELT, Only set if we really have an identifier"""
cid = None
if cron["identifier"]:
cid = cron["identifier"]
else:
cid = SALT_CRON_NO_IDENTIFIER
if cid:
return _ensure_string(cid)
def _cron_matched(cron, cmd, identifier=None):
"""Check if:
- we find a cron with same cmd, old state behavior
- but also be smart enough to remove states changed crons where we do
not removed priorly by a cron.absent by matching on the provided
identifier.
We assure retrocompatibility by only checking on identifier if
and only if an identifier was set on the serialized crontab
"""
ret, id_matched = False, None
cid = _cron_id(cron)
if cid:
if not identifier:
identifier = SALT_CRON_NO_IDENTIFIER
eidentifier = _ensure_string(identifier)
# old style second round
# after saving crontab, we must check that if
# we have not the same command, but the default id
# to not set that as a match
if (
cron.get("cmd", None) != cmd
and cid == SALT_CRON_NO_IDENTIFIER
and eidentifier == SALT_CRON_NO_IDENTIFIER
):
id_matched = False
else:
# on saving, be sure not to overwrite a cron
# with specific identifier but also track
# crons where command is the same
# but with the default if that we gonna overwrite
if (
cron.get("cmd", None) == cmd
and cid == SALT_CRON_NO_IDENTIFIER
and identifier
):
cid = eidentifier
id_matched = eidentifier == cid
if ((id_matched is None) and cmd == cron.get("cmd", None)) or id_matched:
ret = True
return ret
def _needs_change(old, new):
if old != new:
if new == "random":
# Allow switch from '*' or not present to 'random'
if old == "*":
return True
elif new is not None:
return True
return False
def _render_tab(lst):
"""
Takes a tab list structure and renders it to a list for applying it to
a file
"""
ret = []
for pre in lst["pre"]:
ret.append(f"{pre}\n")
if ret:
if ret[-1] != TAG:
ret.append(TAG)
else:
ret.append(TAG)
for env in lst["env"]:
if (env["value"] is None) or (env["value"] == ""):
ret.append('{}=""\n'.format(env["name"]))
else:
ret.append("{}={}\n".format(env["name"], env["value"]))
for cron in lst["crons"]:
if cron["comment"] is not None or cron["identifier"] is not None:
comment = "#"
if cron["comment"]:
comment += " {}".format(cron["comment"].replace("\n", "\n# "))
if cron["identifier"]:
comment += " {}:{}".format(SALT_CRON_IDENTIFIER, cron["identifier"])
comment += "\n"
ret.append(comment)
ret.append(
"{}{} {} {} {} {} {}\n".format(
cron["commented"] is True and "#DISABLED#" or "",
cron["minute"],
cron["hour"],
cron["daymonth"],
cron["month"],
cron["dayweek"],
cron["cmd"],
)
)
for cron in lst["special"]:
if cron["comment"] is not None or cron["identifier"] is not None:
comment = "#"
if cron["comment"]:
comment += " {}".format(cron["comment"].rstrip().replace("\n", "\n# "))
if cron["identifier"]:
comment += " {}:{}".format(SALT_CRON_IDENTIFIER, cron["identifier"])
comment += "\n"
ret.append(comment)
ret.append(
"{}{} {}\n".format(
cron["commented"] is True and "#DISABLED#" or "",
cron["spec"],
cron["cmd"],
)
)
return ret
def _get_cron_cmdstr(path, user=None):
"""
Returns a format string, to be used to build a crontab command.
"""
if user:
cmd = f"crontab -u {user}"
else:
cmd = "crontab"
return f"{cmd} {path}"
def _check_instance_uid_match(user):
"""
Returns true if running instance's UID matches the specified user UID
"""
return os.geteuid() == __salt__["file.user_to_uid"](user)
def write_cron_file(user, path):
"""
Writes the contents of a file to a user's crontab
CLI Example:
.. code-block:: bash
salt '*' cron.write_cron_file root /tmp/new_cron
.. versionchanged:: 2015.8.9
.. note::
Some OS' do not support specifying user via the `crontab` command i.e. (Solaris, AIX)
"""
# Some OS' do not support specifying user via the `crontab` command
if __grains__.get("os_family") in ("Solaris", "AIX"):
return (
__salt__["cmd.retcode"](
_get_cron_cmdstr(path), runas=user, python_shell=False
)
== 0
)
# If Salt is running from same user as requested in cron module we don't need any user switch
elif _check_instance_uid_match(user):
return __salt__["cmd.retcode"](_get_cron_cmdstr(path), python_shell=False) == 0
# If Salt is running from root user it could modify any user's crontab
elif _check_instance_uid_match("root"):
return (
__salt__["cmd.retcode"](_get_cron_cmdstr(path, user), python_shell=False)
== 0
)
# Edge cases here, let's try do a runas
else:
return (
__salt__["cmd.retcode"](
_get_cron_cmdstr(path), runas=user, python_shell=False
)
== 0
)
def write_cron_file_verbose(user, path):
"""
Writes the contents of a file to a user's crontab and return error message on error
CLI Example:
.. code-block:: bash
salt '*' cron.write_cron_file_verbose root /tmp/new_cron
.. versionchanged:: 2015.8.9
.. note::
Some OS' do not support specifying user via the `crontab` command i.e. (Solaris, AIX)
"""
# Some OS' do not support specifying user via the `crontab` command
if __grains__.get("os_family") in ("Solaris", "AIX"):
return __salt__["cmd.run_all"](
_get_cron_cmdstr(path), runas=user, python_shell=False
)
# If Salt is running from same user as requested in cron module we don't need any user switch
elif _check_instance_uid_match(user):
return __salt__["cmd.run_all"](_get_cron_cmdstr(path), python_shell=False)
# If Salt is running from root user it could modify any user's crontab
elif _check_instance_uid_match("root"):
return __salt__["cmd.run_all"](_get_cron_cmdstr(path, user), python_shell=False)
# Edge cases here, let's try do a runas
else:
return __salt__["cmd.run_all"](
_get_cron_cmdstr(path), runas=user, python_shell=False
)
def _write_cron_lines(user, lines):
"""
Takes a list of lines to be committed to a user's crontab and writes it
"""
lines = [salt.utils.stringutils.to_str(_l) for _l in lines]
path = salt.utils.files.mkstemp()
# Some OS' do not support specifying user via the `crontab` command
if __grains__.get("os_family") in ("Solaris", "AIX"):
with salt.utils.files.fpopen(
path, "w+", uid=__salt__["file.user_to_uid"](user), mode=0o600
) as fp_:
fp_.writelines(lines)
ret = __salt__["cmd.run_all"](
_get_cron_cmdstr(path), runas=user, python_shell=False
)
# If Salt is running from same user as requested in cron module we don't need any user switch
elif _check_instance_uid_match(user):
with salt.utils.files.fpopen(path, "w+", mode=0o600) as fp_:
fp_.writelines(lines)
ret = __salt__["cmd.run_all"](_get_cron_cmdstr(path), python_shell=False)
# If Salt is running from root user it could modify any user's crontab
elif _check_instance_uid_match("root"):
with salt.utils.files.fpopen(path, "w+", mode=0o600) as fp_:
fp_.writelines(lines)
ret = __salt__["cmd.run_all"](_get_cron_cmdstr(path, user), python_shell=False)
# Edge cases here, let's try do a runas
else:
with salt.utils.files.fpopen(
path, "w+", uid=__salt__["file.user_to_uid"](user), mode=0o600
) as fp_:
fp_.writelines(lines)
ret = __salt__["cmd.run_all"](
_get_cron_cmdstr(path), runas=user, python_shell=False
)
os.remove(path)
return ret
def _date_time_match(cron, **kwargs):
"""
Returns true if the minute, hour, etc. params match their counterparts from
the dict returned from list_tab().
"""
return all(
[
kwargs.get(x) is None
or cron[x] == str(kwargs[x])
or (str(kwargs[x]).lower() == "random" and cron[x] != "*")
for x in ("minute", "hour", "daymonth", "month", "dayweek")
]
)
def raw_cron(user):
"""
Return the contents of the user's crontab
CLI Example:
.. code-block:: bash
salt '*' cron.raw_cron root
"""
# Some OS' do not support specifying user via the `crontab` command
if __grains__.get("os_family") in ("Solaris", "AIX"):
cmd = "crontab -l"
# Preserve line endings
lines = salt.utils.data.decode(
__salt__["cmd.run_stdout"](
cmd, runas=user, ignore_retcode=True, rstrip=False, python_shell=False
)
).splitlines(True)
# If Salt is running from same user as requested in cron module we don't need any user switch
elif _check_instance_uid_match(user):
cmd = "crontab -l"
# Preserve line endings
lines = salt.utils.data.decode(
__salt__["cmd.run_stdout"](
cmd, ignore_retcode=True, rstrip=False, python_shell=False
)
).splitlines(True)
# If Salt is running from root user it could modify any user's crontab
elif _check_instance_uid_match("root"):
cmd = f"crontab -u {user} -l"
# Preserve line endings
lines = salt.utils.data.decode(
__salt__["cmd.run_stdout"](
cmd, ignore_retcode=True, rstrip=False, python_shell=False
)
).splitlines(True)
# Edge cases here, let's try do a runas
else:
cmd = "crontab -l"
# Preserve line endings
lines = salt.utils.data.decode(
__salt__["cmd.run_stdout"](
cmd, runas=user, ignore_retcode=True, rstrip=False, python_shell=False
)
).splitlines(True)
if lines and lines[0].startswith(
"# DO NOT EDIT THIS FILE - edit the master and reinstall."
):
del lines[0:3]
return "".join(lines)
def list_tab(user):
"""
Return the contents of the specified user's crontab
CLI Example:
.. code-block:: bash
salt '*' cron.list_tab root
"""
data = raw_cron(user)
ret = {"pre": [], "crons": [], "special": [], "env": []}
flag = False
comment = None
identifier = None
for line in data.splitlines():
if line == "# Lines below here are managed by Salt, do not edit":
flag = True
continue
if flag:
commented_cron_job = False
if line.startswith("#DISABLED#"):
# It's a commented cron job
line = line[10:]
commented_cron_job = True
if line.startswith("@"):
# Its a "special" line
dat = {}
comps = line.split()
if len(comps) < 2:
# Invalid line
continue
dat["spec"] = comps[0]
dat["cmd"] = " ".join(comps[1:])
dat["identifier"] = identifier
dat["comment"] = comment
dat["commented"] = False
if commented_cron_job:
dat["commented"] = True
ret["special"].append(dat)
identifier = None
comment = None
commented_cron_job = False
elif line.startswith("#"):
# It's a comment! Catch it!
comment_line = line.lstrip("# ")
# load the identifier if any
if SALT_CRON_IDENTIFIER in comment_line:
parts = comment_line.split(SALT_CRON_IDENTIFIER)
comment_line = parts[0].rstrip()
# skip leading :
if len(parts[1]) > 1:
identifier = parts[1][1:]
if comment is None:
comment = comment_line
else:
comment += "\n" + comment_line
elif line.find("=") > 0 and (
" " not in line or line.index("=") < line.index(" ")
):
# Appears to be a ENV setup line
comps = line.split("=", 1)
dat = {}
dat["name"] = comps[0]
dat["value"] = comps[1]
ret["env"].append(dat)
elif len(line.split(" ")) > 5:
# Appears to be a standard cron line
comps = line.split(" ")
dat = {
"minute": comps[0],
"hour": comps[1],
"daymonth": comps[2],
"month": comps[3],
"dayweek": comps[4],
"identifier": identifier,
"cmd": " ".join(comps[5:]),
"comment": comment,
"commented": False,
}
if commented_cron_job:
dat["commented"] = True
ret["crons"].append(dat)
identifier = None
comment = None
commented_cron_job = False
else:
ret["pre"].append(line)
return ret
# For consistency's sake
ls = salt.utils.functools.alias_function(list_tab, "ls")
def get_entry(user, identifier=None, cmd=None):
"""
Return the specified entry from user's crontab.
identifier will be used if specified, otherwise will lookup cmd
Either identifier or cmd should be specified.
user:
User's crontab to query
identifier:
Search for line with identifier
cmd:
Search for cron line with cmd
CLI Example:
.. code-block:: bash
salt '*' cron.get_entry root identifier=task1
"""
if identifier and cmd:
log.warning("Both identifier and cmd are specified. Only using identifier.")
cmd = None
cron_entries = list_tab(user).get("crons", []) + list_tab(user).get("special", [])
for cron_entry in cron_entries:
if identifier and cron_entry.get("identifier") == identifier:
return cron_entry
elif cmd and cron_entry.get("cmd") == cmd:
return cron_entry
return False
def set_special(user, special, cmd, commented=False, comment=None, identifier=None):
"""
Set up a special command in the crontab.
CLI Example:
.. code-block:: bash
salt '*' cron.set_special root @hourly 'echo foobar'
"""
lst = list_tab(user)
for cron in lst["crons"] + lst["special"]:
cid = _cron_id(cron)
if _cron_matched(cron, cmd, identifier):
test_setted_id = (
cron["identifier"] is None
and SALT_CRON_NO_IDENTIFIER
or cron["identifier"]
)
tests = [
(cron["comment"], comment),
(cron["commented"], commented),
(identifier, test_setted_id),
(cron.get("minute"), None),
(cron.get("hour"), None),
(cron.get("daymonth"), None),
(cron.get("month"), None),
(cron.get("dayweek"), None),
(cron.get("spec"), special),
]
if cid or identifier:
tests.append((cron["cmd"], cmd))
if any([_needs_change(x, y) for x, y in tests]):
if "spec" in cron:
rm_special(user, cmd, identifier=cid)
else:
rm_job(user, cmd, identifier=cid)
# Use old values when setting the new job if there was no
# change needed for a given parameter
if not _needs_change(cron.get("spec"), special):
special = cron.get("spec")
if not _needs_change(cron.get("commented"), commented):
commented = cron.get("commented")
if not _needs_change(cron.get("comment"), comment):
comment = cron.get("comment")
if not _needs_change(cron["cmd"], cmd):
cmd = cron["cmd"]
if cid == SALT_CRON_NO_IDENTIFIER:
if identifier:
cid = identifier
if (
cid == SALT_CRON_NO_IDENTIFIER
and cron["identifier"] is None
):
cid = None
cron["identifier"] = cid
if not cid or (cid and not _needs_change(cid, identifier)):
identifier = cid
jret = set_special(
user,
special,
cmd,
commented=commented,
comment=comment,
identifier=identifier,
)
if jret == "new":
return "updated"
else:
return jret
return "present"
cron = {
"spec": special,
"cmd": cmd,
"identifier": identifier,
"comment": comment,
"commented": commented,
}
lst["special"].append(cron)
comdat = _write_cron_lines(user, _render_tab(lst))
if comdat["retcode"]:
# Failed to commit, return the error
return comdat["stderr"]
return "new"
def _get_cron_date_time(**kwargs):
"""
Returns a dict of date/time values to be used in a cron entry
"""
# Define ranges (except daymonth, as it depends on the month)
range_max = {
"minute": list(list(range(60))),
"hour": list(list(range(24))),
"month": list(list(range(1, 13))),
"dayweek": list(list(range(7))),
}
ret = {}
for param in ("minute", "hour", "month", "dayweek"):
value = str(kwargs.get(param, "1")).lower()
if value == "random":
ret[param] = str(random.sample(range_max[param], 1)[0])
elif len(value.split(":")) == 2:
cron_range = sorted(value.split(":"))
start, end = int(cron_range[0]), int(cron_range[1])
ret[param] = str(random.randint(start, end))
else:
ret[param] = value
if ret["month"] in "1 3 5 7 8 10 12".split():
daymonth_max = 31
elif ret["month"] in "4 6 9 11".split():
daymonth_max = 30
else:
# This catches both '2' and '*'
daymonth_max = 28
daymonth = str(kwargs.get("daymonth", "1")).lower()
if daymonth == "random":
ret["daymonth"] = str(
random.sample(list(list(range(1, (daymonth_max + 1)))), 1)[0]
)
else:
ret["daymonth"] = daymonth
return ret
def set_job(
user,
minute,
hour,
daymonth,
month,
dayweek,
cmd,
commented=False,
comment=None,
identifier=None,
):
"""
Sets a cron job up for a specified user.
CLI Example:
.. code-block:: bash
salt '*' cron.set_job root '*' '*' '*' '*' 1 /usr/local/weekly
"""
# Scrub the types
minute = str(minute).lower()
hour = str(hour).lower()
daymonth = str(daymonth).lower()
month = str(month).lower()
dayweek = str(dayweek).lower()
lst = list_tab(user)
for cron in lst["crons"] + lst["special"]:
cid = _cron_id(cron)
if _cron_matched(cron, cmd, identifier):
test_setted_id = (
cron["identifier"] is None
and SALT_CRON_NO_IDENTIFIER
or cron["identifier"]
)
tests = [
(cron["comment"], comment),
(cron["commented"], commented),
(identifier, test_setted_id),
(cron.get("minute"), minute),
(cron.get("hour"), hour),
(cron.get("daymonth"), daymonth),
(cron.get("month"), month),
(cron.get("dayweek"), dayweek),
(cron.get("spec"), None),
]
if cid or identifier:
tests.append((cron["cmd"], cmd))
if any([_needs_change(x, y) for x, y in tests]):
if "spec" in cron:
rm_special(user, cmd, identifier=cid)
else:
rm_job(user, cmd, identifier=cid)
# Use old values when setting the new job if there was no
# change needed for a given parameter
if not _needs_change(cron.get("minute"), minute):
minute = cron.get("minute")
if not _needs_change(cron.get("hour"), hour):
hour = cron.get("hour")
if not _needs_change(cron.get("daymonth"), daymonth):
daymonth = cron.get("daymonth")
if not _needs_change(cron.get("month"), month):
month = cron.get("month")
if not _needs_change(cron.get("dayweek"), dayweek):
dayweek = cron.get("dayweek")
if not _needs_change(cron["commented"], commented):
commented = cron["commented"]
if not _needs_change(cron["comment"], comment):
comment = cron["comment"]
if not _needs_change(cron["cmd"], cmd):
cmd = cron["cmd"]
if cid == SALT_CRON_NO_IDENTIFIER:
if identifier:
cid = identifier
if (
cid == SALT_CRON_NO_IDENTIFIER
and cron["identifier"] is None
):
cid = None
cron["identifier"] = cid
if not cid or (cid and not _needs_change(cid, identifier)):
identifier = cid
jret = set_job(
user,
minute,
hour,
daymonth,
month,
dayweek,
cmd,
commented=commented,
comment=comment,
identifier=identifier,
)
if jret == "new":
return "updated"
else:
return jret
return "present"
cron = {
"cmd": cmd,
"identifier": identifier,
"comment": comment,
"commented": commented,
}
cron.update(
_get_cron_date_time(
minute=minute, hour=hour, daymonth=daymonth, month=month, dayweek=dayweek
)
)
lst["crons"].append(cron)
comdat = _write_cron_lines(user, _render_tab(lst))
if comdat["retcode"]:
# Failed to commit, return the error
return comdat["stderr"]
return "new"
def rm_special(user, cmd, special=None, identifier=None):
"""
Remove a special cron job for a specified user.
CLI Example:
.. code-block:: bash
salt '*' cron.rm_special root /usr/bin/foo
"""
lst = list_tab(user)
ret = "absent"
rm_ = None
for ind, val in enumerate(lst["special"]):
if rm_ is not None:
break
if _cron_matched(val, cmd, identifier=identifier):
if special is None:
# No special param was specified
rm_ = ind
else:
if val["spec"] == special:
rm_ = ind
if rm_ is not None:
lst["special"].pop(rm_)
ret = "removed"
comdat = _write_cron_lines(user, _render_tab(lst))
if comdat["retcode"]:
# Failed to commit, return the error
return comdat["stderr"]
return ret
def rm_job(
user,
cmd,
minute=None,
hour=None,
daymonth=None,
month=None,
dayweek=None,
identifier=None,
):
"""
Remove a cron job for a specified user. If any of the day/time params are
specified, the job will only be removed if the specified params match.
CLI Example:
.. code-block:: bash
salt '*' cron.rm_job root /usr/local/weekly
salt '*' cron.rm_job root /usr/bin/foo dayweek=1
"""
lst = list_tab(user)
ret = "absent"
rm_ = None
for ind, val in enumerate(lst["crons"]):
if rm_ is not None:
break
if _cron_matched(val, cmd, identifier=identifier):
if not any(
[x is not None for x in (minute, hour, daymonth, month, dayweek)]
):
# No date/time params were specified
rm_ = ind
else:
if _date_time_match(
val,
minute=minute,
hour=hour,
daymonth=daymonth,
month=month,
dayweek=dayweek,
):
rm_ = ind
if rm_ is not None:
lst["crons"].pop(rm_)
ret = "removed"
comdat = _write_cron_lines(user, _render_tab(lst))
if comdat["retcode"]:
# Failed to commit, return the error
return comdat["stderr"]
return ret
rm = salt.utils.functools.alias_function(rm_job, "rm")
def set_env(user, name, value=None):
"""
Set up an environment variable in the crontab.
CLI Example:
.. code-block:: bash
salt '*' cron.set_env root MAILTO user@example.com
"""
lst = list_tab(user)
for env in lst["env"]:
if name == env["name"]:
if value != env["value"]:
rm_env(user, name)
jret = set_env(user, name, value)
if jret == "new":
return "updated"
else:
return jret
return "present"
env = {"name": name, "value": value}
lst["env"].append(env)
comdat = _write_cron_lines(user, _render_tab(lst))
if comdat["retcode"]:
# Failed to commit, return the error
return comdat["stderr"]
return "new"
def rm_env(user, name):
"""
Remove cron environment variable for a specified user.
CLI Example:
.. code-block:: bash
salt '*' cron.rm_env root MAILTO
"""
lst = list_tab(user)
ret = "absent"
rm_ = None
for ind, val in enumerate(lst["env"]):
if name == val["name"]:
rm_ = ind
if rm_ is not None:
lst["env"].pop(rm_)
ret = "removed"
comdat = _write_cron_lines(user, _render_tab(lst))
if comdat["retcode"]:
# Failed to commit, return the error
return comdat["stderr"]
return ret
Zerion Mini Shell 1.0