HEX
Server: LiteSpeed
System: Linux catuipe.dhs10.info 4.18.0-553.111.1.lve.el8.x86_64 #1 SMP Fri Mar 13 13:42:17 UTC 2026 x86_64
User: paradatacom (1125)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: //usr/share/web-monitoring-tool/cron_control.py
#!/opt/cloudlinux/venv/bin/python3 -bb
# coding=utf-8
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2020 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#

import getopt
import logging
import os
import random
import stat
import sys
import tempfile

from sentry import init_wmt_sentry_client, setup_logger

__ALL__ = ["add_cron", "erase_cron", "remove_cron", "add_cron_task"]

WMT_CRONS = ['wmt-clickhouse-reporter', 'wmt-email-reporter']

# Every WMT cron that takes an flock, including wmt-file-rotator which is not
# in WMT_CRONS (never written to /etc/cron.d here) but still uses a lock under
# WMT_LOCK_DIR; pre-create all of them so none is created lazily by flock.
WMT_LOCK_NAMES = WMT_CRONS + ['wmt-file-rotator']

# Root-only directory for the cron flock files. Keeping the locks out of
# world-traversable /run denies an unprivileged user the ability to open()
# (and so advisory-LOCK_EX) them and stall the root reporter crons.
WMT_LOCK_DIR = '/run/web-monitoring-tool'


def check_cron_file_name(file_name):
    """
    Reject a cron file_name that would escape /etc/cron.d/.

    Only a bare basename is accepted: no path separators, no ".."
    segments and no absolute path that os.path.join would honor.

    :param str file_name: Name of cron-file in /etc/cron.d
    :raise ValueError: if file_name is not a safe basename
    """
    if (not file_name or file_name in ('.', '..')
            or os.path.isabs(file_name)
            or os.path.basename(file_name) != file_name):
        raise ValueError('Unsafe cron file name: %s' % file_name)


def usage():
    print('')
    print('Use following syntax to manage WMT cron jobs install utility:')
    print(sys.argv[0] + " [OPTIONS]")
    print('Options:')
    print(" -i | --install     : install wmt cron jobs")
    print(" -d | --delete     : delete  wmt cron jobs")
    print(" -u | --update     : update  wmt cron jobs")


def add_cron(file_name, minute, hour, day, month, day_of_week, user, command,
             check_command=True):
    """
    Add new cron task into crontab schedule if this task or command wasn't already existed in the cron-file.

    :param str file_name: Name of cron-file in /etc/cron.d
    :param minute: Integer or char 'r' if to set random minute
    :param hour: Integer or char 'r' if to set random hour
    :param int, str day: Day number
    :param int, str month: Month number
    :param int, str day_of_week: Day of week number
    :param str user: Under what user do run command
    :param str command: What command do run
    :param bool check_command: If it is False, check that whole cron-task line already exists in crontab,
        check that command string exists instead. Default is True, check a command string.
    """
    if minute == 'r':
        minute = int(round(random.uniform(0, 59)))  # pylint: disable=round-builtin
    if hour == 'r':
        hour = int(round(random.uniform(0, 23)))  # pylint: disable=round-builtin
    try:
        cron_task = format_cron_task(minute, hour, day, month, day_of_week, user, command)
        add_cron_task(file_name, cron_task, check_command)
    except TypeError:
        sys.stderr.write("Can not add task with wrong syntax")


def add_cron_task(file_name, task, check_command=False):
    """
    Add new cron task in cron format if this task or command in this task wasn't already existed in the cron-file.

    :param str file_name: Name of cron-file in /etc/cron.d
    :param str task: Cron task in format "min hour day mon d_of_w user command"
    :param bool check_command: If it is False, check that whole cron-task line already exists in crontab,
        check that command string exists instead. Default is False, check a whole cron-task string.
    """
    if any(c in task for c in ('\n', '\r', '\0')):
        raise TypeError("Cron task contains illegal control character")
    check_cron_file_name(file_name)
    cron_file_path = os.path.join('/etc/cron.d/', file_name)
    try:
        content = []
        try:
            fd = os.open(cron_file_path, os.O_RDONLY | os.O_NOFOLLOW)
            with os.fdopen(fd, 'r') as f:
                content = f.readlines()
        except FileNotFoundError:
            pass
        if not is_in_cron(task, content, check_command):
            new_content = ''.join(content) + "%s\n" % task
            tmp_fd, tmp_path = tempfile.mkstemp(
                dir='/etc/cron.d', prefix='.wmt-')
            fdopen_owns = False
            try:
                os.fchmod(tmp_fd, 0o644)
                f = os.fdopen(tmp_fd, 'w')
                # Ownership of tmp_fd has transferred to the file object.
                fdopen_owns = True
                with f:
                    f.write(new_content)
                os.rename(tmp_path, cron_file_path)
            except BaseException:
                # Bugbot a780de2b: close raw fd if fdopen never took
                # ownership (fchmod or fdopen raised).
                if not fdopen_owns:
                    try:
                        os.close(tmp_fd)
                    except OSError:
                        pass
                try:
                    os.unlink(tmp_path)
                except OSError:
                    pass
                raise
    except (IOError, OSError):
        return False
    return True


def remove_cron(file_name):
    """
    Remove cron-file from fs

    :param str file_name: Name of cron-file in /etc/cron.d
    """
    check_cron_file_name(file_name)
    try:
        os.remove(
            os.path.join('/etc/cron.d/', file_name)
        )
    except (OSError, IOError):
        pass


def erase_cron(file_name):
    """
    Make cron-file empty

    :param str file_name: Name of cron-file in /etc/cron.d
    """
    check_cron_file_name(file_name)
    try:
        fd = os.open(
            os.path.join("/etc/cron.d/", file_name),
            os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW,
            0o644
        )
        os.close(fd)
    except (IOError, OSError) as err:
        sys.stderr.write("Can not erase crontab file %s because %s\n" % (
            file_name, str(err)))


def format_cron_task(minute, hour, day, month, day_of_week, user, command):
    """
    Build cron-task string in the cron format

    :param minute: Integer or char 'r' if to set random minute
    :param hour: Integer or char 'r' if to set random hour
    :param int day: Day number
    :param int month: Month number
    :param int day_of_week: Day of week number
    :param str user: Under what user do run command
    :param str command: What command do run
    :return: Cron-task in the cron format
    :rtype: str
    """
    arguments = (minute, hour, day, month, day_of_week, user, command)
    for arg in arguments:
        if arg is None:
            raise TypeError("Wrong schedule for cron task")
        # reject control bytes that would smuggle extra cron lines
        if isinstance(arg, str) and any(c in arg for c in ('\n', '\r', '\0')):
            raise TypeError("Cron field contains illegal control character")
    return "%2s %2s %2s %2s %2s %10s %s" % arguments


def parse_cron_task(task):
    """
    Split cron task string into cron task parts

    :param str task: Cron-task string in the cron format
    :return: List of cron-task parts
    :rtype: list of str
    """
    return task.split(None, 6)


def get_task_in_cron(crontab, get_parsed=False):
    """
    Returns iterator through crontab tasks

    :param iterable crontab: Iterator with crontab tasks' strings
    :param bool get_parsed: If it is True, return crontab task as list of task's parts
        return crontab task as a string instead
    :return: Crontab task
    :rtype: str
    :rtype: list of str
    """
    for cron_t in (s.strip() for s in crontab):
        try:
            if get_parsed:
                t = parse_cron_task(cron_t)
            else:
                t = format_cron_task(*parse_cron_task(cron_t))
        except TypeError:
            sys.stderr.write("Wrong crontab task syntax: %s\n" % cron_t)
        else:
            yield t


def is_task_in_cron(task, cron_content):
    """
    Find first occurence of task in cron-file if it has

    :param str task: Cron-task in cront format to compare with
    :param list cron_content: list of cron contents lines
    :return: True if such a task is already existed in cron-file, False instead
    :rtype: bool
    """
    for t in get_task_in_cron(cron_content):
        if t == task:
            return True
    return False


def is_command_in_cron(task, cron_content):
    """
    :param str task: Task with command to looking for
    :param list cron_content: list of cron content lines
    :return: True if such a command is already existed in cron-file, False instead
    :rtype: bool

    Find first occurence of command in cron-file if it has
    """
    command = parse_cron_task(task)[-1]
    for t in get_task_in_cron(cron_content, get_parsed=True):
        if t[-1] == command:
            return True
    return False


def is_in_cron(task, cron_content, check_command=False):
    """
    Find first occurence of command or task in cron-file if it has

    :param str task: Task or command to looking for
    :param list cron_content: content of cron file
    :param bool check_command: If it is True, check command occurence, check task occurence instead
    :return: True if such a command or task is already existed in cron-file, False instead
    :rtype: bool
    """
    if check_command:
        return is_command_in_cron(task, cron_content)
    return is_task_in_cron(task, cron_content)


def is_valid_cron_task(t):
    """
    Straight-forward approach to cron minute/hour validation
    (our crons always have digits on minute/hour positions)

    Better to clone croniter package for advanced validation
    """
    if not t[0].isdigit() or int(t[0]) >= 60:
        return False
    if not t[1].isdigit() or int(t[1]) >= 24:
        return False
    return True


def get_cron_list():
    return WMT_CRONS


def get_cron_lock_path(cron_name):
    """
    Build the flock path for a WMT cron under the root-only lock dir.

    :param str cron_name: WMT cron name
    :return: Absolute path to the cron's flock file
    :rtype: str
    """
    return os.path.join(WMT_LOCK_DIR, '%s.cronlock' % cron_name)


def get_lock_dir_prefix():
    """
    Self-healing shell prefix that recreates the root-only lock dir before
    flock runs.

    WMT_LOCK_DIR lives on tmpfs /run, wiped on every reboot; tmpfiles.d
    recreates it at boot but a cron may fire in the gap before that (or on a
    host without systemd-tmpfiles). The cron runs as root, so this mkdir/chmod
    yields a root:root 0o700 dir and keeps the advisory locks unreachable by
    unprivileged users. flock cannot mkdir a missing parent itself (it would
    exit 66), so without this the reporter crons would silently stop.

    :return: Shell snippet ending in ' && '
    :rtype: str
    """
    return 'mkdir -p %s && chmod 0700 %s && ' % (WMT_LOCK_DIR, WMT_LOCK_DIR)


def get_cron_params(cron_name):
    """
    Get crontab entries for WMT cron jobs
    """
    wmt_bin = '/usr/share/web-monitoring-tool/wmtbin'
    wmt_api = os.path.join(wmt_bin, 'wmt-api')
    logfile = '/var/log/cl_wmt.log'

    lock_dir_prefix = get_lock_dir_prefix()

    if cron_name == 'wmt-clickhouse-reporter':
        send_to_clickhouse = f'{wmt_api} --send-clickhouse'
        lock_file = get_cron_lock_path(cron_name)

        return [
            'r', 'r', '*', '*', '*', 'root',
            f'{lock_dir_prefix}/usr/bin/flock -n {lock_file} {send_to_clickhouse} &>> {logfile}'
        ]
    elif cron_name == 'wmt-email-reporter':
        report_to_mail_cmd = f'{wmt_api} --send-email'
        lock_file = get_cron_lock_path(cron_name)

        return [
            '0', '0', '*', '*', '*', 'root',
            f'{lock_dir_prefix}/usr/bin/flock -n {lock_file} {report_to_mail_cmd} &>> {logfile}'
        ]
    elif cron_name == 'wmt-file-rotator':
        rotate_wmt_files = '-name "wmt-db-*.sqlite" -delete -or -name "wmt_report*.json" -delete'
        linux_find = f'/usr/bin/find /var/lve/wmt/ -maxdepth 1 -mtime +7 {rotate_wmt_files}'
        lock_file = get_cron_lock_path(cron_name)

        return [
            '0', '0', '*', '*', '*', 'root',
            f'{lock_dir_prefix}/usr/bin/flock -n {lock_file} ' + linux_find
        ]
    else:
        raise ValueError('Invalid cron name: %s', cron_name)


def create_cron_locks():
    """
    Pre-create the cron flock files under a root-only directory.

    flock(1) opens the lock path O_RDONLY|O_CREAT under the cron umask, so a
    missing lock is otherwise created world-readable (0644) in /run, letting any
    local user open() it and hold an advisory LOCK_EX to stall the root crons.
    Creating the dir 0o700 and each lock 0o600 from privileged install code
    closes that: flock then opens an already-restricted file.
    """
    os.makedirs(WMT_LOCK_DIR, mode=0o700, exist_ok=True)
    # Enforce 0o700 even when the dir pre-exists with a looser mode.
    os.chmod(WMT_LOCK_DIR, 0o700)
    for cron_name in WMT_LOCK_NAMES:
        lock_path = get_cron_lock_path(cron_name)
        fd = os.open(lock_path, os.O_CREAT | os.O_NOFOLLOW, 0o600)
        os.close(fd)


def _ensure_cron_locks_best_effort():
    """Pre-create the lock dir+files, but never let that abort the cron
    (re)write.

    Lock pre-creation is defence-in-depth: each cron command already
    self-heals the 0700 lock dir before flock (see get_lock_dir_prefix), so
    the directory — the load-bearing protection — is restored at runtime even
    if pre-creation here fails. If create_cron_locks() raised and we let it
    propagate, install_crons()/update_crons() would skip rewriting the cron
    files to the new lock paths, the %post/postinst CLI would still exit 0, and
    the system would silently keep the legacy /var/run cron paths. Log and
    continue so the cron files are always migrated.
    """
    try:
        create_cron_locks()
    except OSError:
        logging.getLogger('cron_control').warning(
            'Failed to pre-create cron lock files under %s; continuing with '
            'cron refresh (cron commands self-heal the lock dir at runtime).',
            WMT_LOCK_DIR, exc_info=True)


def install_crons():
    _ensure_cron_locks_best_effort()
    for cron_name in get_cron_list():
        add_cron(cron_name, *get_cron_params(cron_name))


def delete_crons():
    for cron_name in get_cron_list():
        remove_cron(cron_name)


def update_crons():
    # %post / postinst run `-u`; create the dir+locks here too (install_crons
    # / `-i` is only reached later via `wmt-api --start`) so the install and
    # upgrade paths land an already-restricted lock dir, not one created lazily
    # by flock under the cron umask. Best-effort: a failure must not skip the
    # cron rewrite below, which migrates the jobs to the new self-healing paths.
    _ensure_cron_locks_best_effort()
    for cron_name in get_cron_list():
        cron_file_path = os.path.join('/etc/cron.d/', cron_name)
        try:
            st = os.lstat(cron_file_path)
        except OSError:
            continue
        if not stat.S_ISREG(st.st_mode):
            continue
        # always update cron file with actual tasks
        remove_cron(cron_name)
        add_cron(cron_name, *get_cron_params(cron_name))


if __name__ == "__main__":
    logger = setup_logger('cron_control')
    init_wmt_sentry_client()
    try:
        opts, args = getopt.getopt(
            sys.argv[1:],
            "hidu",
            ["help", "postupcp", "install", "delete", "update"]
        )
    except getopt.GetoptError as err:
        # print help information and exit:
        print(str(err))  # will print something like "option -a not recognized"
        usage()
        sys.exit(2)

    try:
        for o, a in opts:
            if o in ("-h", "--help"):
                usage()
                sys.exit()
            elif o in ("-i", "--install"):
                install_crons()
            elif o in ("-d", "--delete"):
                delete_crons()
            elif o in ("-u", "--update"):
                update_crons()
            else:
                usage()
                sys.exit(2)
    except Exception as e:
        logger.exception(e)