File manager - Edit - /usr/share/web-monitoring-tool/cron_control.py
Back
#!/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)
| ver. 1.4 |
Github
|
.
| PHP 8.1.34 | Generation time: 0.04 |
proxy
|
phpinfo
|
Settings