@@ -71,7 +71,7 @@ def __init__(
7171 self .connectors = connectors
7272 self .custom_scripts = custom_scripts
7373 self .notify = notify
74-
74+
7575
7676 def parse (self ):
7777 cmds = []
@@ -267,6 +267,75 @@ def configure_credential(self):
267267 print (f'{ bcolors .FAIL } Credential: Failed to program { cred_type } , name { name } { bcolors .ENDC } ' )
268268 continue
269269
270+ def validate_cron_format (self , cron_expression ):
271+ """Validate if a cron expression is in proper format
272+ Returns tuple: (is_valid: bool, error_message: str)
273+ """
274+ if not cron_expression or not isinstance (cron_expression , str ):
275+ return False , "Cron expression cannot be empty"
276+
277+ # Remove extra whitespace and split
278+ parts = cron_expression .strip ().split ()
279+
280+ # Standard cron should have 5 parts: minute hour day month day_of_week
281+ if len (parts ) != 5 :
282+ return False , f"Cron expression must have exactly 5 parts (minute hour day month day_of_week), got { len (parts )} : { cron_expression } "
283+
284+ # Define valid ranges for each field
285+ field_ranges = [
286+ (0 , 59 , "minute" ), # minute: 0-59
287+ (0 , 23 , "hour" ), # hour: 0-23
288+ (1 , 31 , "day" ), # day: 1-31
289+ (1 , 12 , "month" ), # month: 1-12
290+ (0 , 7 , "day_of_week" ) # day_of_week: 0-7 (0 and 7 are Sunday)
291+ ]
292+
293+ for i , (part , (min_val , max_val , field_name )) in enumerate (zip (parts , field_ranges )):
294+ if not self ._validate_cron_field (part , min_val , max_val , field_name ):
295+ return False , f"Invalid { field_name } field: '{ part } ' (should be { min_val } -{ max_val } or valid cron syntax)"
296+
297+ return True , "Valid cron expression"
298+
299+ def _validate_cron_field (self , field , min_val , max_val , field_name ):
300+ """Validate individual cron field"""
301+ # Allow wildcards
302+ if field == "*" :
303+ return True
304+
305+ # Allow step values (*/5, */10, etc.)
306+ if field .startswith ("*/" ):
307+ try :
308+ step = int (field [2 :])
309+ return step > 0 and step <= max_val
310+ except ValueError :
311+ return False
312+
313+ # Allow ranges (1-5, 10-15, etc.)
314+ if "-" in field :
315+ try :
316+ start , end = field .split ("-" , 1 )
317+ start_num = int (start )
318+ end_num = int (end )
319+ return (min_val <= start_num <= max_val and
320+ min_val <= end_num <= max_val and
321+ start_num <= end_num )
322+ except ValueError :
323+ return False
324+
325+ # Allow comma-separated lists (1,3,5 or 10,20,30, etc.)
326+ if "," in field :
327+ try :
328+ values = [int (x .strip ()) for x in field .split ("," )]
329+ return all (min_val <= val <= max_val for val in values )
330+ except ValueError :
331+ return False
332+
333+ # Allow single numbers
334+ try :
335+ num = int (field )
336+ return min_val <= num <= max_val
337+ except ValueError :
338+ return False
270339
271340
272341 def configure_schedule (self ):
@@ -280,14 +349,40 @@ def configure_schedule(self):
280349 print (f"{ bcolors .WARNING } Scheduler: No scheduler configuration found{ bcolors .ENDC } " )
281350 return
282351
352+ # Check for LB_JOB_SCHEDULE environment variable
353+ lb_job_schedule = os .environ .get ('LB_JOB_SCHEDULE' )
354+ if lb_job_schedule :
355+ print (f'{ bcolors .OKGREEN } Found LB_JOB_SCHEDULE environment variable: { lb_job_schedule } { bcolors .ENDC } ' )
356+
357+ # Validate the cron format
358+ is_valid , validation_message = self .validate_cron_format (lb_job_schedule )
359+ if not is_valid :
360+ print (f'{ bcolors .FAIL } ERROR: LB_JOB_SCHEDULE has invalid cron format: { validation_message } { bcolors .ENDC } ' )
361+ print (f'{ bcolors .FAIL } Examples of valid cron formats:{ bcolors .ENDC } ' )
362+ print (f'{ bcolors .FAIL } */15 * * * * (every 15 minutes){ bcolors .ENDC } ' )
363+ print (f'{ bcolors .FAIL } 0 */2 * * * (every 2 hours){ bcolors .ENDC } ' )
364+ print (f'{ bcolors .FAIL } 30 9 * * 1-5 (9:30 AM, Monday to Friday){ bcolors .ENDC } ' )
365+ print (f'{ bcolors .FAIL } 0 0 1 * * (first day of every month){ bcolors .ENDC } ' )
366+ print (f'{ bcolors .FAIL } Falling back to YAML configuration cadence values{ bcolors .ENDC } ' )
367+ lb_job_schedule = None # Disable override
368+ else :
369+ print (f'{ bcolors .OKGREEN } LB_JOB_SCHEDULE validation passed: { validation_message } { bcolors .ENDC } ' )
370+ print (f'{ bcolors .OKGREEN } This will override all cadence values in the scheduler configuration{ bcolors .ENDC } ' )
371+
372+
283373 unskript_crontab_file = "/etc/unskript/unskript_crontab.tab"
284374 crons = []
285375 try :
286376 for schedule in config :
287377 if schedule .get ('enable' ) is False :
288378 print (f'Skipping' )
289379 continue
290- cadence = schedule .get ('cadence' )
380+ if lb_job_schedule :
381+ cadence = lb_job_schedule
382+ print (f"{ bcolors .OKGREEN } Using LB_JOB_SCHEDULE override: { cadence } { bcolors .ENDC } " )
383+ else :
384+ cadence = schedule .get ('cadence' )
385+
291386 job_name = schedule .get ('job_name' )
292387 # look up the job name and get the commands
293388 job = self .jobs .get (job_name )
0 commit comments