Source code for glideinwms.factory.glideFactoryDowntimeLib

# SPDX-FileCopyrightText: 2009 Fermi Research Alliance, LLC
# SPDX-License-Identifier: Apache-2.0

# Description:
#   This module implements the functions needed to
#   handle the downtimes

import fcntl
import os.path
import time

from glideinwms.lib import timeConversion


[docs] class DowntimeFile: """Handle a downtime file space separated file with downtime information Each line has space-separated values The first line is a comment (starts with #) and header line : "#%-29s %-30s %-20s %-30s %-20s # %s\n" % ("Start", "End", "Entry", "Frontend", "Sec_Class", "Comment") Each non-comment line in the file has at least two entries start_time end_time expressed in utime if end_time is None, the downtime does not have a set expiration (i.e. it runs forever) Additional entries are used to limit the scope (Entry, Frontend, Sec_Class) and to add a comment """ def __init__(self, fname): self.fname = fname
[docs] def read(self, raise_on_error=False): """Return a list of downtime periods (utimes) a value of None idicates "forever" for example: `[(1215339200,1215439170),(1215439271,None)]` Args: raise_on_error (bool): if not True mask all the exceptions Returns: list: list of downtime periods [(start, end), ...] a value of None idicates "forever", no start time, or no end time timestamps are in seconds from epoch (utime) `[]` returned when `raise_on_error` is False (default) and there is no downtime file """ return read(self.fname, raise_on_error)
[docs] def printDowntime(self, entry="Any", check_time=None): return printDowntime(self.fname, entry, check_time)
# if check_time==None, use current time
[docs] def checkDowntime(self, entry="Any", frontend="Any", security_class="Any", check_time=None): (msg, rtn) = checkDowntime(self.fname, entry, frontend, security_class, check_time) self.downtime_comment = msg return rtn
[docs] def addPeriod( self, start_time, end_time, entry="All", frontend="All", security_class="All", comment="", create_if_empty=True ): """Add a scheduled downtime Maintin a lock (fcntl.LOCK_EX) on the downtime file while writing entry, frontend, and security_class default to "All" Args: start_time (int): start time in seconds from Epoch end_time (int): end time in seconds from Epoch entry (str): entry name or "All" frontend (str): frontend name os "All" security_class (str): security class name or "All" comment (str): comment to add create_if_empty (bool): if False, raise FileNotFoundError if there is not already a downtime file Returns: int: 0 """ return addPeriod(self.fname, start_time, end_time, entry, frontend, security_class, comment, create_if_empty)
[docs] def startDowntime( self, start_time=None, end_time=None, entry="All", frontend="All", security_class="All", comment="", create_if_empty=True, ): """start a downtime that we don't know when it will end if start_time==None, use current time entry, frontend, and security_class default to "All" Args: start_time (int|None): start time in seconds from Epoch end_time (int|None): end time in seconds from Epoch entry (str): entry name or "All" frontend (str): frontend name os "All" security_class (str): security class name or "All" comment (str): comment to add create_if_empty (bool): if False, raise FileNotFoundError if there is not already a downtime file Returns: """ if start_time is None: start_time = int(time.time()) return self.addPeriod(start_time, end_time, entry, frontend, security_class, comment, create_if_empty)
[docs] def endDowntime(self, end_time=None, entry="All", frontend="All", security_class="All", comment=""): """End a downtime (not a scheduled one) if end_time==None, use current time entry, frontend, and security_class default to "All" Args: end_time (int|None): end time in seconds from Epoch. If end_time==None, default, use current time entry (str): entry name or "All" frontend (str): frontend name os "All" security_class (str): security class name or "All" comment (str): comment to add Returns: int: number of records closed """ return endDowntime(self.fname, end_time, entry, frontend, security_class, comment)
[docs] def purgeOldPeriods(self, cut_time=None, raise_on_error=False): """Purge old downtime periods if cut time<0, use current_time-abs(cut_time) Args: cut_time (int): cut time in seconds from epoch, if cut_time==None or 0, use current time, if cut time<0, use current_time-abs(cut_time) raise_on_error (bool): if not True, mask all exceptions Returns: int: number of records purged """ return purgeOldPeriods(self.fname, cut_time, raise_on_error)
############################# # INTERNAL - Do not use #############################
[docs] def read(fname, raise_on_error=False): """Return a list of downtime periods (utimes) a value of None idicates "forever" for example: `[(1215339200,1215439170),(1215439271,None)]` Args: fname (str|Path): downtimes file raise_on_error (bool): if not True mask all the exceptions Returns: list: list of downtime periods [(start, end), ...] a value of None idicates "forever", no start time, or no end time timestamps are in seconds from epoch (utime) `[]` returned when `raise_on_error` is False (default) and there is no file """ try: with open(fname) as fd: fcntl.flock(fd, fcntl.LOCK_SH) lines = fd.readlines() except OSError: if raise_on_error: raise # re-rise the exact same exception like no except else: return [] # no file -> no downtimes out = [] lnr = 0 for long_line in lines: lnr += 1 line = long_line.strip() if len(line) == 0: continue # ignore empty lines if line[0:1] == "#": continue # ignore comments arr = line.split() # Read in lines of the downtime file # Start End Entry Security_Class Comment if len(arr) < 2: if raise_on_error: raise ValueError("%s:%i: Expected pair, got '%s'" % (fname, lnr, line)) else: continue # ignore malformed lines try: start_time = timeConversion.extractISO8601_Local(arr[0]) except ValueError as e: if raise_on_error: raise ValueError("%s:%i: 1st element: %s" % (fname, lnr, e)) from e else: continue # ignore errors try: if arr[1] == "None": end_time = None else: end_time = timeConversion.extractISO8601_Local(arr[1]) except ValueError as e: if raise_on_error: raise ValueError("%s:%i: 2nd element: %s" % (fname, lnr, e)) from e else: continue # ignore errors # Addition. If more arguments exists, parse # Entry, Frontend, Security_Class, Comment if len(arr) >= 3: entry = arr[2] else: entry = "factory" if len(arr) >= 3: frontend = arr[3] else: frontend = "All" if len(arr) >= 4: security_class = arr[4] else: security_class = "All" if len(arr) >= 5: comment = arr[5:] else: comment = "" out.append((start_time, end_time, entry, frontend, security_class, comment)) # end for long_line in lines: return out
[docs] def printDowntime(fname, entry="Any", check_time=None): if check_time is None: check_time = int(time.time()) time_list = read(fname) downtime_keys = {} for time_tuple in time_list: if check_time < time_tuple[0]: continue # check_time before start if (time_tuple[1] is not None) and (check_time > time_tuple[1]): continue if time_tuple[2] in downtime_keys: downtime_keys[time_tuple[2]] += "," + time_tuple[3] + ":" + time_tuple[4] else: downtime_keys[time_tuple[2]] = time_tuple[3] + ":" + time_tuple[4] if "All" in downtime_keys: for e in downtime_keys: if (e != "All") and (e != "factory"): downtime_keys[e] += "," + downtime_keys["All"] if entry == "Any": for e in downtime_keys: print("%-30s Down\t%s" % (e, downtime_keys[e])) else: if entry in downtime_keys: print("%-30s Down\t%s" % (entry, downtime_keys[entry])) else: if ("All" in downtime_keys) and (entry != "factory"): print("%-30s Down\t%s" % (entry, downtime_keys["All"])) else: print("%-30s Up \tAll:All" % (entry))
[docs] def checkDowntime(fname, entry="Any", frontend="Any", security_class="Any", check_time=None): """Check if there is a downtime at `check_time` if check_time==None, use current time "All" (default) is a wildcard for entry, frontend and security_class Args: fname (str|Path): Downtime file entry (str): entry name or "All" frontend (str): frontend name os "All" security_class (str): security class name or "All" check_time: time to check in seconds from epoch, if check_time==None, use current time Returns: (str, bool): tuple with the comment string and True is in downtime or ("", False) is not in downtime """ if check_time is None: check_time = int(time.time()) time_list = read(fname) for time_tuple in time_list: # make sure this is for the right entry if (time_tuple[2] != "All") and (entry != time_tuple[2]): continue if (time_tuple[2] == "All") and (entry == "factory"): continue # make sure that this time tuple applies to this security_class # If the security class does not match the downtime entry, # this is not a relevant downtime # UNLESS the downtime says All if (time_tuple[3] != "All") and (frontend != time_tuple[3]): continue if (time_tuple[4] != "All") and (security_class != time_tuple[4]): continue if check_time < time_tuple[0]: continue # check_time before start comment = " ".join(time_tuple[5][1:]) if time_tuple[1] is None: return (comment, True) # downtime valid until the end of times, so here we go if check_time <= time_tuple[1]: return (comment, True) # within limit return ("", False) # not found a downtime window
[docs] def addPeriod( fname, start_time, end_time, entry="All", frontend="All", security_class="All", comment="", create_if_empty=True ): """Add a downtime period Maintin a lock (fcntl.LOCK_EX) on the downtime file while writing Args: fname (str|Path): downtime file start_time (int): start time in seconds from Epoch end_time (int): end time in seconds from Epoch entry (str): entry name or "All" frontend (str): frontend name os "All" security_class (str): security class name or "All" comment (str): comment to add create_if_empty (bool): if False, raise FileNotFoundError if there is not already a downtime file Returns: int: 0 """ exists = os.path.isfile(fname) if (not exists) and (not create_if_empty): raise FileNotFoundError("[Errno 2] No such file or directory: '%s'" % fname) comment = comment.replace("\n", " ") comment = comment.replace("\r", " ") with open(fname, "a+") as fd: fcntl.flock(fd, fcntl.LOCK_EX) if not exists: # new file, create header fd.write( "#%-29s %-30s %-20s %-30s %-20s # %s\n" % ("Start", "End", "Entry", "Frontend", "Sec_Class", "Comment") ) if end_time is not None: fd.write( "%-30s %-20s %-20s %-30s %-20s # %-20s\n" % ( timeConversion.getISO8601_Local(start_time), timeConversion.getISO8601_Local(end_time), entry, frontend, security_class, comment, ) ) else: fd.write( "%-30s %-30s %-20s %-30s %-20s # %s\n" % (timeConversion.getISO8601_Local(start_time), "None", entry, frontend, security_class, comment) ) return 0
[docs] def purgeOldPeriods(fname, cut_time=None, raise_on_error=False): """Purge old rules using cut_time if cut_time==None or 0, use current time if cut time<0, use current_time-abs(cut_time) Args: fname (str|Path): downtime file cut_time (int): cut time in seconds from epoch, if cut_time==None or 0, use current time, if cut time<0, use current_time-abs(cut_time) raise_on_error (bool): if not True, mask all exceptions Returns: int: number of records purged """ if cut_time is None: cut_time = int(time.time()) elif cut_time <= 0: cut_time = int(time.time()) + cut_time try: fd = open(fname, "r+") except OSError: if raise_on_error: raise # re-rise the exact same exception like no except else: return 0 # no file -> nothing to purge with fd: fcntl.flock(fd, fcntl.LOCK_EX) # read the old info inlines = fd.readlines() outlines = [] lnr = 0 cut_nr = 0 for long_line in inlines: lnr += 1 line = long_line.strip() if len(line) == 0: outlines.append(long_line) continue # pass on empty lines if line[0:1] == "#": outlines.append(long_line) continue # pass on comments arr = line.split() if len(arr) < 2: if raise_on_error: raise ValueError("%s:%i: Expected pair, got '%s'" % (fname, lnr, line)) else: outlines.append(long_line) continue # pass on malformed lines try: if arr[1] == "None": end_time = None else: end_time = timeConversion.extractISO8601_Local(arr[1]) except ValueError as e: if raise_on_error: raise ValueError("%s:%i: 2nd element: %s" % (fname, lnr, e)) from e else: outlines.append(long_line) continue # unknown, pass on if end_time is None: outlines.append(long_line) continue # valid forever, pass on if end_time >= cut_time: outlines.append(long_line) continue # end_time after cut_time, have to keep it # if we got here, the period ended before the cut date... cut it cut_nr += 1 pass # end for # go back to start to rewrite fd.seek(0) fd.writelines(outlines) fd.truncate() return cut_nr
[docs] def endDowntime(fname, end_time=None, entry="All", frontend="All", security_class="All", comment=""): """End a downtime (not a scheduled one) if end_time==None, use current time "All" (default) is a wildcard for entry, frontend and security_class Args: fname (str|Path): Downtime file end_time (int): end time in seconds from epoch, if end_time==None, use current time entry (str): entry name or "All" frontend (str): frontend name os "All" security_class (str): security class name or "All" comment (str): comment to add Returns: int: Number of downtime records closed """ comment = comment.replace("\r", " ") comment = comment.replace("\n", " ") if end_time is None: end_time = int(time.time()) try: fd = open(fname, "r+") except OSError: return 0 # no file -> nothing to end with fd: fcntl.flock(fd, fcntl.LOCK_EX) # read the old info inlines = fd.readlines() outlines = [] lnr = 0 closed_nr = 0 for long_line in inlines: lnr += 1 line = long_line.strip() if len(line) == 0: outlines.append(long_line) continue # pass on empty lines if line[0:1] == "#": outlines.append(long_line) continue # pass on comments arr = line.split() if len(arr) < 2: outlines.append(long_line) continue # pass on malformed lines # make sure this is for the right entry if (entry != "All") and (len(arr) > 2) and (entry != arr[2]): outlines.append(long_line) continue if (entry == "All") and (len(arr) > 2) and ("factory" == arr[2]): outlines.append(long_line) continue if (frontend != "All") and (len(arr) > 3) and (frontend != arr[3]): outlines.append(long_line) continue # make sure that this time tuple applies to this security_class if (security_class != "All") and (len(arr) > 4) and (security_class != arr[4]): outlines.append(long_line) continue cur_start_time = 0 if arr[0] != "None": cur_start_time = timeConversion.extractISO8601_Local(arr[0]) if arr[1] != "None": cur_end_time = timeConversion.extractISO8601_Local(arr[1]) # logic short circuit guarantees that cur_end_time is defined (arr[1] != 'None') if arr[1] == "None" or ( (cur_start_time < int(time.time())) and (cur_end_time > end_time) # pylint: disable=E0606 ): # open period -> close outlines.append("%-30s %-30s" % (arr[0], timeConversion.getISO8601_Local(end_time))) if len(arr) > 2: sep = " " t = 2 for param in arr[2:]: if t < 5: outlines.append("%s%-20s" % (sep, param)) else: outlines.append(f"{sep}{param}") t = t + 1 if comment != "": outlines.append(f"; {comment}") outlines.append("\n") closed_nr += 1 else: # closed just pass on outlines.append(long_line) # Keep parsing file, since there may be multiple downtimes # pass # end for # go back to start to rewrite fd.seek(0) fd.writelines(outlines) fd.truncate() return closed_nr