""" ConfigurationData module is the base for cfg files management """ from __future__ import print_function from __future__ import absolute_import from __future__ import division import os.path import zlib import zipfile import six from six.moves import _thread as thread import time import DIRAC from diraccfg import CFG from DIRAC.Core.Utilities.File import mkDir from DIRAC.Core.Utilities import List, Time from DIRAC.Core.Utilities.ReturnValues import S_OK, S_ERROR from DIRAC.Core.Utilities.LockRing import LockRing from DIRAC.FrameworkSystem.Client.Logger import gLogger __RCSID__ = "45ddde113 (2021-10-01 11:19:55 +0200) Chris Burr " class ConfigurationData(object): def __init__(self, loadDefaultCFG=True): envVar = os.environ.get("DIRAC_FEWER_CFG_LOCKS", "no").lower() self.__locksEnabled = envVar not in ("y", "yes", "t", "true", "on", "1") if self.__locksEnabled: lr = LockRing() self.threadingEvent = lr.getEvent() self.threadingEvent.set() self.threadingLock = lr.getLock() self.runningThreadsNumber = 0 self.__compressedConfigurationData = None self.configurationPath = "/DIRAC/Configuration" self.backupsDir = os.path.join(DIRAC.rootPath, "etc", "csbackup") self._isService = False self.localCFG = CFG() self.remoteCFG = CFG() self.mergedCFG = CFG() self.remoteServerList = [] if loadDefaultCFG: defaultCFGFile = os.path.join(DIRAC.rootPath, "etc", "dirac.cfg") gLogger.debug("dirac.cfg should be at", "%s" % defaultCFGFile) retVal = self.loadFile(defaultCFGFile) if not retVal["OK"]: gLogger.warn("Can't load %s file" % defaultCFGFile) self.sync() def getBackupDir(self): return self.backupsDir def sync(self): gLogger.debug("Updating configuration internals") self.mergedCFG = self.remoteCFG.mergeWith(self.localCFG) self.remoteServerList = [] localServers = self.extractOptionFromCFG( "%s/Servers" % self.configurationPath, self.localCFG, disableDangerZones=True ) if localServers: self.remoteServerList.extend(List.fromChar(localServers, ",")) remoteServers = self.extractOptionFromCFG( "%s/Servers" % self.configurationPath, self.remoteCFG, disableDangerZones=True ) if remoteServers: self.remoteServerList.extend(List.fromChar(remoteServers, ",")) self.remoteServerList = List.uniqueElements(self.remoteServerList) self.__compressedConfigurationData = None def loadFile(self, fileName): try: fileCFG = CFG() fileCFG.loadFromFile(fileName) except IOError: self.localCFG = self.localCFG.mergeWith(fileCFG) return S_ERROR("Can't load a cfg file '%s'" % fileName) return self.mergeWithLocal(fileCFG) def mergeWithLocal(self, extraCFG): self.lock() try: self.localCFG = self.localCFG.mergeWith(extraCFG) self.unlock() gLogger.debug("CFG merged") except Exception as e: self.unlock() return S_ERROR("Cannot merge with new cfg: %s" % str(e)) self.sync() return S_OK() def loadRemoteCFGFromCompressedMem(self, data): if six.PY3 and isinstance(data, str): data = data.encode(errors="surrogateescape") sUncompressedData = zlib.decompress(data).decode() self.loadRemoteCFGFromMem(sUncompressedData) def loadRemoteCFGFromMem(self, data): self.lock() self.remoteCFG.loadFromBuffer(data) self.unlock() self.sync() def loadConfigurationData(self, fileName=False): name = self.getName() self.lock() try: if not fileName: fileName = "%s.cfg" % name if fileName[0] != "/": fileName = os.path.join(DIRAC.rootPath, "etc", fileName) self.remoteCFG.loadFromFile(fileName) except Exception as e: print(e) self.unlock() self.sync() def getCommentFromCFG(self, path, cfg=False): if not cfg: cfg = self.mergedCFG self.dangerZoneStart() try: levelList = [level.strip() for level in path.split("/") if level.strip() != ""] for section in levelList[:-1]: cfg = cfg[section] return self.dangerZoneEnd(cfg.getComment(levelList[-1])) except Exception: pass return self.dangerZoneEnd(None) def getSectionsFromCFG(self, path, cfg=False, ordered=False): if not cfg: cfg = self.mergedCFG self.dangerZoneStart() try: levelList = [level.strip() for level in path.split("/") if level.strip() != ""] for section in levelList: cfg = cfg[section] return self.dangerZoneEnd(cfg.listSections(ordered)) except Exception: pass return self.dangerZoneEnd(None) def getOptionsFromCFG(self, path, cfg=False, ordered=False): if not cfg: cfg = self.mergedCFG self.dangerZoneStart() try: levelList = [level.strip() for level in path.split("/") if level.strip() != ""] for section in levelList: cfg = cfg[section] return self.dangerZoneEnd(cfg.listOptions(ordered)) except Exception: pass return self.dangerZoneEnd(None) def extractOptionFromCFG(self, path, cfg=False, disableDangerZones=False): if not cfg: cfg = self.mergedCFG if not disableDangerZones: self.dangerZoneStart() try: levelList = [level.strip() for level in path.split("/") if level.strip() != ""] for section in levelList[:-1]: cfg = cfg[section] if levelList[-1] in cfg.listOptions(): return self.dangerZoneEnd(cfg[levelList[-1]]) except Exception: pass if not disableDangerZones: self.dangerZoneEnd() def setOptionInCFG(self, path, value, cfg=False, disableDangerZones=False): if not cfg: cfg = self.localCFG if not disableDangerZones: self.dangerZoneStart() try: levelList = [level.strip() for level in path.split("/") if level.strip() != ""] for section in levelList[:-1]: if section not in cfg.listSections(): cfg.createNewSection(section) cfg = cfg[section] cfg.setOption(levelList[-1], value) finally: if not disableDangerZones: self.dangerZoneEnd() self.sync() def deleteOptionInCFG(self, path, cfg=False): if not cfg: cfg = self.localCFG self.dangerZoneStart() try: levelList = [level.strip() for level in path.split("/") if level.strip() != ""] for section in levelList[:-1]: if section not in cfg.listSections(): return cfg = cfg[section] cfg.deleteKey(levelList[-1]) finally: self.dangerZoneEnd() self.sync() def generateNewVersion(self): self.setVersion(Time.toString()) self.sync() gLogger.info("Generated new version %s" % self.getVersion()) def setVersion(self, version, cfg=False): if not cfg: cfg = self.remoteCFG self.setOptionInCFG("%s/Version" % self.configurationPath, version, cfg) def getVersion(self, cfg=False): if not cfg: cfg = self.remoteCFG value = self.extractOptionFromCFG("%s/Version" % self.configurationPath, cfg) if value: return value return "0" def getName(self): return self.extractOptionFromCFG("%s/Name" % self.configurationPath, self.mergedCFG) def exportName(self): return self.setOptionInCFG("%s/Name" % self.configurationPath, self.getName(), self.remoteCFG) def getRefreshTime(self): try: return int(self.extractOptionFromCFG("%s/RefreshTime" % self.configurationPath, self.mergedCFG)) except Exception: return 300 def getPropagationTime(self): try: return int(self.extractOptionFromCFG("%s/PropagationTime" % self.configurationPath, self.mergedCFG)) except Exception: return 300 def getSlavesGraceTime(self): try: return int(self.extractOptionFromCFG("%s/SlavesGraceTime" % self.configurationPath, self.mergedCFG)) except Exception: return 600 def mergingEnabled(self): try: val = self.extractOptionFromCFG("%s/EnableAutoMerge" % self.configurationPath, self.mergedCFG) return val.lower() in ("yes", "true", "y") except Exception: return False def getAutoPublish(self): value = self.extractOptionFromCFG("%s/AutoPublish" % self.configurationPath, self.localCFG) if value and value.lower() in ("no", "false", "n"): return False else: return True def getAutoSlaveSync(self): value = self.extractOptionFromCFG("%s/AutoSlaveSync" % self.configurationPath, self.localCFG) if value and value.lower() in ("no", "false", "n"): return False else: return True def getServers(self): return list(self.remoteServerList) def getConfigurationGateway(self): return self.extractOptionFromCFG("/DIRAC/Gateway", self.localCFG) def setServers(self, sServers): self.setOptionInCFG("%s/Servers" % self.configurationPath, sServers, self.remoteCFG) self.sync() def deleteLocalOption(self, optionPath): self.deleteOptionInCFG(optionPath, self.localCFG) def getMasterServer(self): return self.extractOptionFromCFG("%s/MasterServer" % self.configurationPath, self.remoteCFG) def setMasterServer(self, sURL): self.setOptionInCFG("%s/MasterServer" % self.configurationPath, sURL, self.remoteCFG) self.sync() def getCompressedData(self): if self.__compressedConfigurationData is None: self.__compressedConfigurationData = zlib.compress(str(self.remoteCFG).encode(), 9) return self.__compressedConfigurationData def isMaster(self): value = self.extractOptionFromCFG("%s/Master" % self.configurationPath, self.localCFG) if value and value.lower() in ("yes", "true", "y"): return True else: return False def getServicesPath(self): return "/Services" def setAsService(self): self._isService = True def isService(self): return self._isService def useServerCertificate(self): value = self.extractOptionFromCFG("/DIRAC/Security/UseServerCertificate") if value and value.lower() in ("y", "yes", "true"): return True return False def skipCACheck(self): value = self.extractOptionFromCFG("/DIRAC/Security/SkipCAChecks") if value and value.lower() in ("y", "yes", "true"): return True return False def dumpLocalCFGToFile(self, fileName): try: with open(fileName, "w") as fd: fd.write(str(self.localCFG)) gLogger.verbose("Configuration file dumped", "'%s'" % fileName) except IOError: gLogger.error("Can't dump cfg file", "'%s'" % fileName) return S_ERROR("Can't dump cfg file '%s'" % fileName) return S_OK() def getRemoteCFG(self): return self.remoteCFG def getMergedCFGAsString(self): return str(self.mergedCFG) def dumpRemoteCFGToFile(self, fileName): with open(fileName, "w") as fd: fd.write(str(self.remoteCFG)) def __backupCurrentConfiguration(self, backupName): configurationFilename = "%s.cfg" % self.getName() configurationFile = os.path.join(DIRAC.rootPath, "etc", configurationFilename) today = Time.date() backupPath = os.path.join(self.getBackupDir(), str(today.year), "%02d" % today.month) mkDir(backupPath) backupFile = os.path.join(backupPath, configurationFilename.replace(".cfg", ".%s.zip" % backupName)) if os.path.isfile(configurationFile): gLogger.info("Making a backup of configuration in %s" % backupFile) try: with zipfile.ZipFile(backupFile, "w", zipfile.ZIP_DEFLATED) as zf: zf.write(configurationFile, "%s.backup.%s" % (os.path.split(configurationFile)[1], backupName)) except Exception: gLogger.exception() gLogger.error("Cannot backup configuration data file", "file %s" % backupFile) else: gLogger.warn("CS data file does not exist", configurationFile) def writeRemoteConfigurationToDisk(self, backupName=False): configurationFile = os.path.join(DIRAC.rootPath, "etc", "%s.cfg" % self.getName()) try: with open(configurationFile, "w") as fd: fd.write(str(self.remoteCFG)) except Exception as e: gLogger.fatal( "Cannot write new configuration to disk!", "file %s exception %s" % (configurationFile, repr(e)) ) return S_ERROR("Can't write cs file %s!: %s" % (configurationFile, repr(e).replace(",)", ")"))) if backupName: self.__backupCurrentConfiguration(backupName) return S_OK() def setRemoteCFG(self, cfg, disableSync=False): self.remoteCFG = cfg.clone() if not disableSync: self.sync() def lock(self): """ Locks Event to prevent further threads from reading. Stops current thread until no other thread is accessing. PRIVATE USE """ if not self.__locksEnabled: return self.threadingEvent.clear() while self.runningThreadsNumber > 0: time.sleep(0.1) def unlock(self): """ Unlocks Event. PRIVATE USE """ if not self.__locksEnabled: return self.threadingEvent.set() def dangerZoneStart(self): """ Start of danger zone. This danger zone may be or may not be a mutual exclusion zone. Counter is maintained to know how many threads are inside and be able to enable and disable mutual exclusion. PRIVATE USE """ if not self.__locksEnabled: return self.threadingEvent.wait() self.threadingLock.acquire() self.runningThreadsNumber += 1 try: self.threadingLock.release() except thread.error: pass def dangerZoneEnd(self, returnValue=None): """ End of danger zone. PRIVATE USE """ if not self.__locksEnabled: return returnValue self.threadingLock.acquire() self.runningThreadsNumber -= 1 try: self.threadingLock.release() except thread.error: pass return returnValue