224 lines
6 KiB
Python
224 lines
6 KiB
Python
#!/usr/local/bin/python3
|
|
|
|
from crontab import CronTab
|
|
import argparse
|
|
import logging
|
|
import time
|
|
import subprocess
|
|
import sys
|
|
import signal
|
|
import os
|
|
|
|
|
|
def _parse_crontab(crontab_file: str) -> list:
|
|
"""The method includes a functionality to parse the crontab file, and it returns a list of CronTab jobs
|
|
|
|
Keyword arguments:
|
|
crontab_file -> Specify the inserted crontab file
|
|
"""
|
|
|
|
logger = logging.getLogger("parser")
|
|
|
|
logger.info(f"Reading crontab from {crontab_file}")
|
|
|
|
if not os.path.isfile(crontab_file):
|
|
logger.error(f"Crontab {crontab_file} does not exist. Exiting!")
|
|
sys.exit(1)
|
|
|
|
with open(crontab_file, "r") as crontab:
|
|
lines: list = crontab.readlines()
|
|
|
|
logger.info(f"{len(lines)} lines read from crontab {crontab_file}")
|
|
|
|
jobs: list = list()
|
|
|
|
for i, line in enumerate(lines):
|
|
line: str = line.strip()
|
|
|
|
if not line:
|
|
continue
|
|
|
|
if line.startswith("#"):
|
|
continue
|
|
|
|
logger.info(f"Parsing line {line}")
|
|
|
|
expression: list = line.split(" ", 5)
|
|
cron_expression: str = " ".join(expression[0:5])
|
|
|
|
logger.info(f"Cron expression is {cron_expression}")
|
|
|
|
try:
|
|
cron_entry = CronTab(cron_expression)
|
|
except ValueError as e:
|
|
logger.critical(
|
|
f"Unable to parse crontab. Line {i + 1}: Illegal cron expression {cron_expression}. Error message: {e}"
|
|
)
|
|
sys.exit(1)
|
|
|
|
command: str = expression[5]
|
|
|
|
logger.info(f"Command is {command}")
|
|
|
|
jobs.append([cron_entry, command])
|
|
|
|
if len(jobs) == 0:
|
|
logger.error(
|
|
"Specified crontab does not contain any scheduled execution. Exiting!"
|
|
)
|
|
sys.exit(1)
|
|
|
|
return jobs
|
|
|
|
|
|
def _get_next_executions(jobs: list):
|
|
"""The method includes a functionality to extract the execution time and job itself from the submitted job list
|
|
|
|
Keyword arguments:
|
|
jobs -> Specify the inserted list of jobs
|
|
"""
|
|
|
|
logger = logging.getLogger("next-exec")
|
|
|
|
scheduled_executions: tuple = tuple(
|
|
(x[1], int(x[0].next(default_utc=True)) + 1) for x in jobs
|
|
)
|
|
|
|
logger.debug(f"Next executions of scheduled are {scheduled_executions}")
|
|
|
|
next_exec_time: int = int(min(scheduled_executions, key=lambda x: x[1])[1])
|
|
|
|
logger.debug(f"Next execution is in {next_exec_time} second(s)")
|
|
|
|
next_commands: list = [x[0] for x in scheduled_executions if x[1] == next_exec_time]
|
|
|
|
logger.debug(
|
|
f"Next commands to be executed in {next_exec_time} are {next_commands}"
|
|
)
|
|
|
|
return next_exec_time, next_commands
|
|
|
|
|
|
def _loop(jobs: list, test_mode: bool = False):
|
|
"""The method includes a functionality to loop over all jobs inside the crontab file and execute them
|
|
|
|
Keyword arguments:
|
|
jobs -> Specify the inserted jobs as list
|
|
test_mode -> Specify if you want to use the test mode or not (default False)
|
|
"""
|
|
|
|
logger = logging.getLogger("loop")
|
|
|
|
logger.info("Entering main loop")
|
|
|
|
if test_mode is False:
|
|
while True:
|
|
sleep_time, commands = _get_next_executions(jobs)
|
|
|
|
logger.debug(f"Sleeping for {sleep_time} second(s)")
|
|
|
|
if sleep_time <= 1:
|
|
logger.debug("Sleep time <= 1 second, ignoring.")
|
|
time.sleep(1)
|
|
continue
|
|
|
|
time.sleep(sleep_time)
|
|
|
|
for command in commands:
|
|
_execute_command(command)
|
|
else:
|
|
sleep_time, commands = _get_next_executions(jobs)
|
|
|
|
logger.debug(f"Sleeping for {sleep_time} second(s)")
|
|
|
|
if sleep_time <= 1:
|
|
logger.debug("Sleep time <= 1 second, ignoring.")
|
|
time.sleep(1)
|
|
|
|
time.sleep(sleep_time)
|
|
|
|
for command in commands:
|
|
_execute_command(command)
|
|
|
|
|
|
def _execute_command(command: str):
|
|
"""The method includes a functionality to execute a crontab command
|
|
|
|
Keyword arguments:
|
|
command -> Specify the inserted command for the execution
|
|
"""
|
|
|
|
logger = logging.getLogger("exec")
|
|
|
|
logger.info(f"Executing command {command}")
|
|
|
|
result = subprocess.run(
|
|
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
|
|
)
|
|
|
|
logger.info(f"Standard output: {result.stdout}")
|
|
logger.info(f"Standard error: {result.stderr}")
|
|
|
|
|
|
def _signal_handler():
|
|
"""The method includes a functionality for the signal handler to exit a process"""
|
|
|
|
logger = logging.getLogger("signal")
|
|
logger.info("Exiting")
|
|
sys.exit(0)
|
|
|
|
|
|
def main():
|
|
"""The method includes a functionality to control and execute crontab entries
|
|
|
|
Arguments:
|
|
-c -> Specify the inserted crontab file
|
|
-L -> Specify the inserted log file
|
|
-C -> Specify the if the output should be forwarded to the console
|
|
-l -> Specify the log level
|
|
"""
|
|
signal.signal(signal.SIGINT, _signal_handler)
|
|
signal.signal(signal.SIGTERM, _signal_handler)
|
|
|
|
parser = argparse.ArgumentParser(description="cron")
|
|
parser.add_argument("-c", "--crontab", required=True, type=str)
|
|
logging_target = parser.add_mutually_exclusive_group(required=True)
|
|
logging_target.add_argument("-L", "--logfile", type=str)
|
|
logging_target.add_argument("-C", "--console", action="store_true")
|
|
parser.add_argument(
|
|
"-l",
|
|
"--loglevel",
|
|
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
default="INFO",
|
|
type=str,
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
log_level = getattr(logging, args.loglevel.upper(), logging.INFO)
|
|
|
|
if args.console:
|
|
logging.basicConfig(
|
|
filemode="w",
|
|
level=log_level,
|
|
format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
|
|
)
|
|
else:
|
|
logging.basicConfig(
|
|
filename=args.logfile,
|
|
filemode="a+",
|
|
level=log_level,
|
|
format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s",
|
|
)
|
|
|
|
logger = logging.getLogger("main")
|
|
|
|
logger.info("Starting cron")
|
|
|
|
jobs: list = _parse_crontab(args.crontab)
|
|
|
|
_loop(jobs)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|