From a02e6289325ab0d81c309ce3ff086fd4b171d317 Mon Sep 17 00:00:00 2001 From: Chris Burr Date: Fri, 9 Apr 2021 08:58:38 +0200 Subject: [PATCH 1/9] Fix bad merge from rel-v7r2 --- src/DIRAC/Core/Utilities/DIRACScript.py | 44 ------------------------- 1 file changed, 44 deletions(-) diff --git a/src/DIRAC/Core/Utilities/DIRACScript.py b/src/DIRAC/Core/Utilities/DIRACScript.py index 3bd4244f3d5..35db01aa4c2 100644 --- a/src/DIRAC/Core/Utilities/DIRACScript.py +++ b/src/DIRAC/Core/Utilities/DIRACScript.py @@ -75,47 +75,3 @@ def __call__(self, func=None): ) return entrypoint.load()._func() - - -def _entrypointToExtension(entrypoint): - """"Get the extension name from an EntryPoint object""" - # In Python 3.9 this can be "entrypoint.module" - module = entrypoint.pattern.match(entrypoint.value).groupdict()["module"] - extensionName = module.split(".")[0] - return extensionName - - -def _extensionsByPriority(): - """Discover extensions using the setuptools metadata - - TODO: This should move into a function which can also be called to fill the CS - """ - # This is only available in Python 3.8+ so it has to be here for now - from importlib import metadata # pylint: disable=no-name-in-module - - priorities = defaultdict(list) - for entrypoint in metadata.entry_points()['dirac']: - extensionName = _entrypointToExtension(entrypoint) - extension_metadata = entrypoint.load()() - priorities[extension_metadata["priority"]].append(extensionName) - - extensions = [] - for priority, extensionNames in sorted(priorities.items()): - if len(extensionNames) != 1: - print( - "WARNING: Found multiple extensions with priority", - "{} ({})".format(priority, extensionNames), - ) - # If multiple are passed, sort the extensions so things are deterministic at least - extensions.extend(sorted(extensionNames)) - return extensions - - -def _getExtensionMetadata(extensionName): - """Get the metadata for a given extension name""" - # This is only available in Python 3.8+ so it has to be here for now - from importlib import metadata # pylint: disable=no-name-in-module - - for entrypoint in metadata.entry_points()['dirac']: - if extensionName == _entrypointToExtension(entrypoint): - return entrypoint.load()() From 634ae93eda047499b2b68870f4e4d3a1ecf7346d Mon Sep 17 00:00:00 2001 From: Chris Burr Date: Thu, 1 Apr 2021 18:25:59 +0200 Subject: [PATCH 2/9] Add ConfigTemplate.cfg and *.sql files to Python 3 sdist/bdist --- setup.cfg | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.cfg b/setup.cfg index 85b8407aa0e..7273919156d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,6 +51,11 @@ install_requires = six sqlalchemy subprocess32 +zip_safe = False +include_package_data = True + +[options.package_data] +* = ConfigTemplate.cfg, *.sql [options.packages.find] where=src From 79cc4a4e3478ec3d1128a2f4191e7b349e198751 Mon Sep 17 00:00:00 2001 From: Chris Burr Date: Mon, 12 Apr 2021 11:11:31 +0200 Subject: [PATCH 3/9] Enable integration tests for Python 3 servers --- .github/workflows/integration.yml | 4 ++ tests/Jenkins/dirac_ci.sh | 83 ++++++++++++++++++++----------- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 301ce32f7d4..dd820c54c55 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -31,6 +31,10 @@ jobs: ###### Python 3 - TEST_NAME: "Python 3 client" ARGS: CLIENT_USE_PYTHON3=Yes + - TEST_NAME: "Python 3 server" + ARGS: SERVER_USE_PYTHON3=Yes + - TEST_NAME: "Python 3 server and client" + ARGS: CLIENT_USE_PYTHON3=Yes SERVER_USE_PYTHON3=Yes MYSQL_VER=8.0 steps: - uses: actions/checkout@v2 diff --git a/tests/Jenkins/dirac_ci.sh b/tests/Jenkins/dirac_ci.sh index 11356615fb4..bedd9ed4f9f 100644 --- a/tests/Jenkins/dirac_ci.sh +++ b/tests/Jenkins/dirac_ci.sh @@ -118,37 +118,62 @@ installSite() { echo "==> Started installing" - if [[ -n "${DEBUG+x}" ]]; then - INSTALLOPTIONS+=("${DEBUG}") - fi - - if [[ "${DIRACOSVER}" ]]; then - INSTALLOPTIONS+=("--dirac-os") - INSTALLOPTIONS+=("--dirac-os-version=${DIRACOSVER}") - fi - - if [[ "$DIRACOS_TARBALL_PATH" ]]; then - { - echo "DIRACOS = $DIRACOS_TARBALL_PATH" - } >> "${SERVERINSTALLDIR}/dirac-ci-install.cfg" - fi - - if [[ -n "${ALTERNATIVE_MODULES+x}" ]]; then - echo "Installing from non-release code" - option="--module=" + if [[ "${USE_PYTHON3:-}" == "Yes" ]]; then + if [[ -n "${DIRACOSVER:-}" ]] && [[ "${DIRACOSVER}" != "master" ]]; then + DIRACOS2_URL="https://github.com/DIRACGrid/DIRACOS2/releases/download/${DIRACOSVER}/DIRACOS-Linux-x86_64.sh" + else + DIRACOS2_URL="https://github.com/DIRACGrid/DIRACOS2/releases/latest/download/DIRACOS-Linux-x86_64.sh" + fi + cd "$SERVERINSTALLDIR" + curl -L "${DIRACOS2_URL}" > "installer.sh" + bash "installer.sh" + rm "installer.sh" + # TODO: Remove these two lines + echo "source \"$PWD/diracos/diracosrc\"" > "$PWD/bashrc" + echo "export X509_CERT_DIR=\"$PWD/diracos/etc/grid-security/certificates\"" >> "$PWD/bashrc" + mv "${SERVERINSTALLDIR}/etc/grid-security/"* "${SERVERINSTALLDIR}/diracos/etc/grid-security/" + rm -rf "${SERVERINSTALLDIR}/etc" + ln -s "${SERVERINSTALLDIR}/diracos/etc" "${SERVERINSTALLDIR}/etc" + source diracos/diracosrc + pip install git+https://gitlab.cern.ch/chaen/fts-rest-flask.git@packaging + pip install 'sqlalchemy<1.4' for module_path in "${ALTERNATIVE_MODULES[@]}"; do - if [[ -d "${module_path}" ]]; then - option+="${module_path}:::$(basename "${module_path}"):::local," - else - option+="${module_path}," - fi + pip install ${PIP_INSTALL_EXTRA_ARGS:-} "${module_path}[server]" done - INSTALLOPTIONS+=("${option: :$((${#option} - 1))}") - fi - - if ! "${SERVERINSTALLDIR}/dirac-install.py" "${INSTALLOPTIONS[@]}" "${SERVERINSTALLDIR}/install.cfg" "${SERVERINSTALLDIR}/dirac-ci-install.cfg"; then - echo "ERROR: dirac-install.py failed" >&2 - exit 1 + cd - + else + if [[ -n "${DEBUG+x}" ]]; then + INSTALLOPTIONS+=("${DEBUG}") + fi + + if [[ "${DIRACOSVER}" ]]; then + INSTALLOPTIONS+=("--dirac-os") + INSTALLOPTIONS+=("--dirac-os-version=${DIRACOSVER}") + fi + + if [[ "$DIRACOS_TARBALL_PATH" ]]; then + { + echo "DIRACOS = $DIRACOS_TARBALL_PATH" + } >> "${SERVERINSTALLDIR}/dirac-ci-install.cfg" + fi + + if [[ -n "${ALTERNATIVE_MODULES+x}" ]]; then + echo "Installing from non-release code" + option="--module=" + for module_path in "${ALTERNATIVE_MODULES[@]}"; do + if [[ -d "${module_path}" ]]; then + option+="${module_path}:::$(basename "${module_path}"):::local," + else + option+="${module_path}," + fi + done + INSTALLOPTIONS+=("${option: :$((${#option} - 1))}") + fi + + if ! "${SERVERINSTALLDIR}/dirac-install.py" "${INSTALLOPTIONS[@]}" "${SERVERINSTALLDIR}/install.cfg" "${SERVERINSTALLDIR}/dirac-ci-install.cfg"; then + echo "ERROR: dirac-install.py failed" >&2 + exit 1 + fi fi echo "==> Done installing, now configuring" From 99b988d8cfafc79c236a27ceabca6c69187dd60a Mon Sep 17 00:00:00 2001 From: Chris Burr Date: Wed, 31 Mar 2021 14:27:01 +0200 Subject: [PATCH 4/9] Various minor Python 3 compatibility fixes --- .../Client/Helpers/CSGlobals.py | 23 ++- .../Core/Utilities/Plotting/ObjectLoader.py | 6 +- .../FileManager/FileManagerBase.py | 16 +- .../DataManagementSystem/DB/FileCatalogDB.py | 2 +- .../Client/ComponentInstaller.py | 138 +++++++++++------- .../private/monitoring/RRDManager.py | 2 +- .../scripts/dirac_rss_query_db.py | 4 +- .../scripts/dirac_rss_set_status.py | 4 +- .../Computing/SSHComputingElement.py | 3 +- .../Computing/SingularityComputingElement.py | 2 +- .../test/Test_SSHComputingElement.py | 4 +- .../test/Test_DIRACCAProxyProvider.py | 5 +- .../test/Test_ProxyProviderFactory.py | 5 +- .../Agent/SiteDirector.py | 2 +- src/DIRAC/__init__.py | 8 +- .../DataManagementSystem/Test_Client_DFC.py | 11 +- tests/Integration/Framework/Test_ProxyDB.py | 3 +- .../Test_DIRACCAProxyProvider.py | 3 +- .../all_integration_client_tests.sh | 3 +- 19 files changed, 148 insertions(+), 96 deletions(-) diff --git a/src/DIRAC/ConfigurationSystem/Client/Helpers/CSGlobals.py b/src/DIRAC/ConfigurationSystem/Client/Helpers/CSGlobals.py index bff05030703..912686f3d74 100644 --- a/src/DIRAC/ConfigurationSystem/Client/Helpers/CSGlobals.py +++ b/src/DIRAC/ConfigurationSystem/Client/Helpers/CSGlobals.py @@ -28,25 +28,32 @@ def __load(self): for extName in self.getCSExtensions() + ['']: try: if not extName.endswith("DIRAC"): - extension = '%sDIRAC' % extName - res = imp.find_module(extension) + extName = '%sDIRAC' % extName + res = imp.find_module(extName) if res[0]: res[0].close() - self.__orderedExtNames.append(extension) - self.__modules[extension] = res + self.__orderedExtNames.append(extName) + self.__modules[extName] = res except ImportError: pass def getCSExtensions(self): if not self.__csExt: - from DIRAC.ConfigurationSystem.Client.Config import gConfig - exts = gConfig.getValue('/DIRAC/Extensions', []) + if six.PY3: + from DIRAC.Core.Utilities.DIRACScript import _extensionsByPriority + exts = _extensionsByPriority() + else: + from DIRAC.ConfigurationSystem.Client.Config import gConfig + exts = gConfig.getValue('/DIRAC/Extensions', []) + + self.__csExt = [] for iP in range(len(exts)): ext = exts[iP] if ext.endswith("DIRAC"): ext = ext[:-5] - exts[iP] = ext - self.__csExt = exts + # If the extension is now "" (i.e. vanilla DIRAC), don't include it + if ext: + self.__csExt.append(ext) return self.__csExt def getInstalledExtensions(self): diff --git a/src/DIRAC/Core/Utilities/Plotting/ObjectLoader.py b/src/DIRAC/Core/Utilities/Plotting/ObjectLoader.py index 24c64e990e0..96993820a88 100644 --- a/src/DIRAC/Core/Utilities/Plotting/ObjectLoader.py +++ b/src/DIRAC/Core/Utilities/Plotting/ObjectLoader.py @@ -6,6 +6,7 @@ from __future__ import print_function import re import os +import six import DIRAC from DIRAC import gLogger from DIRAC.Core.Utilities import List @@ -27,7 +28,10 @@ def loadObjects(path, reFilter=None, parentClass=None): objectsToLoad = {} # Find which object files match for parentModule in parentModuleList: - objDir = os.path.join(DIRAC.rootPath, parentModule, *pathList) + if six.PY3: + objDir = os.path.join(os.path.dirname(os.path.dirname(DIRAC.__file__)), parentModule, *pathList) + else: + objDir = os.path.join(DIRAC.rootPath, parentModule, *pathList) if not os.path.isdir(objDir): continue for objFile in os.listdir(objDir): diff --git a/src/DIRAC/DataManagementSystem/DB/FileCatalogComponents/FileManager/FileManagerBase.py b/src/DIRAC/DataManagementSystem/DB/FileCatalogComponents/FileManager/FileManagerBase.py index a4bde2680f1..f534530e879 100755 --- a/src/DIRAC/DataManagementSystem/DB/FileCatalogComponents/FileManager/FileManagerBase.py +++ b/src/DIRAC/DataManagementSystem/DB/FileCatalogComponents/FileManager/FileManagerBase.py @@ -586,7 +586,7 @@ def _getExistingMetadata(self, lfns, connection=False): return res successful = res['Value']['Successful'] failed = res['Value']['Failed'] - for lfn, error in res['Value']['Failed'].items(): + for lfn, error in list(failed.items()): if error == 'No such file or directory': failed.pop(lfn) return S_OK((successful, failed)) @@ -713,7 +713,7 @@ def setFileStatus(self, lfns, connection=False): return res failed = res['Value']['Failed'] successful = {} - for lfn in res['Value']['Successful'].keys(): + for lfn in res['Value']['Successful']: status = lfns[lfn] if isinstance(status, six.string_types): if status not in self.db.validFileStatus: @@ -870,7 +870,7 @@ def exists(self, lfns, connection=False): # either {lfn : guid} # or P lfn : {PFN : .., GUID : ..} } if isinstance(lfns, dict): - val = lfns.values() + val = list(lfns.values()) # We have values, take the first to identify the type if val: @@ -881,7 +881,7 @@ def exists(self, lfns, connection=False): guidList = [lfns[lfn]['GUID'] for lfn in lfns] elif isinstance(val, six.string_types): # We hope that it is the GUID which is given - guidList = lfns.values() + guidList = list(lfns.values()) if guidList: # A dict { guid: lfn to which it is supposed to be associated } @@ -918,7 +918,7 @@ def getFileSize(self, lfns, connection=False): return res totalSize = 0 - for lfn in res['Value']['Successful'].keys(): + for lfn in res['Value']['Successful']: size = res['Value']['Successful'][lfn]['Size'] res['Value']['Successful'][lfn] = size totalSize += size @@ -1202,7 +1202,7 @@ def changeFileGroup(self, lfns): return res failed = res['Value']['Failed'] successful = {} - for lfn in res['Value']['Successful'].keys(): + for lfn in res['Value']['Successful']: group = lfns[lfn] if isinstance(group, six.string_types): groupRes = self.db.ugManager.findGroup(group) @@ -1232,7 +1232,7 @@ def changeFileOwner(self, lfns): return res failed = res['Value']['Failed'] successful = {} - for lfn in res['Value']['Successful'].keys(): + for lfn in res['Value']['Successful']: owner = lfns[lfn] if isinstance(owner, six.string_types): userRes = self.db.ugManager.findUser(owner) @@ -1262,7 +1262,7 @@ def changeFileMode(self, lfns): return res failed = res['Value']['Failed'] successful = {} - for lfn in res['Value']['Successful'].keys(): + for lfn in res['Value']['Successful']: mode = lfns[lfn] currentMode = res['Value']['Successful'][lfn]['Mode'] if int(currentMode) == int(mode): diff --git a/src/DIRAC/DataManagementSystem/DB/FileCatalogDB.py b/src/DIRAC/DataManagementSystem/DB/FileCatalogDB.py index cbe5343a264..9423679038f 100755 --- a/src/DIRAC/DataManagementSystem/DB/FileCatalogDB.py +++ b/src/DIRAC/DataManagementSystem/DB/FileCatalogDB.py @@ -245,7 +245,7 @@ def exists(self, lfns, credDict): successful = res['Value']['Successful'] notExist = [] - for lfn in res['Value']['Successful'].keys(): + for lfn in list(res['Value']['Successful']): if not successful[lfn]: notExist.append(lfn) successful.pop(lfn) diff --git a/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py b/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py index 7d59f0df1db..c0a33b9e4c5 100644 --- a/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py +++ b/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py @@ -72,6 +72,7 @@ import importlib from diraccfg import CFG +import six import DIRAC from DIRAC import rootPath @@ -282,8 +283,13 @@ def getExtensions(self): """ Get the list of installed extensions """ - initList = glob.glob(os.path.join(rootPath, '*DIRAC', '__init__.py')) - extensions = [os.path.basename(os.path.dirname(k)) for k in initList] + if six.PY3: + from DIRAC.Core.Utilities.DIRACScript import _extensionsByPriority + extensions = _extensionsByPriority() + else: + initList = glob.glob(os.path.join(rootPath, '*DIRAC', '__init__.py')) + extensions = [os.path.basename(os.path.dirname(k)) for k in initList] + try: extensions.remove('DIRAC') except Exception: @@ -788,13 +794,21 @@ def getComponentCfg(self, componentType, system, component, compInstance, extens if addDefaultOptions: extensionsDIRAC = [x + 'DIRAC' for x in extensions] + extensions for ext in extensionsDIRAC + ['DIRAC']: - cfgTemplatePath = os.path.join(rootPath, ext, '%sSystem' % system, 'ConfigTemplate.cfg') - if os.path.exists(cfgTemplatePath): - gLogger.notice('Loading configuration template', cfgTemplatePath) - # Look up the component in this template - loadCfg = CFG() - loadCfg.loadFromFile(cfgTemplatePath) - compCfg = loadCfg.mergeWith(compCfg) + if six.PY3: + import pkg_resources + try: + cfgTemplatePath = pkg_resources.resource_filename(ext, "%sSystem/ConfigTemplate.cfg" % system) + except ModuleNotFoundError: + continue + else: + cfgTemplatePath = os.path.join(rootPath, ext, '%sSystem' % system, 'ConfigTemplate.cfg') + if not os.path.exists(cfgTemplatePath): + continue + gLogger.notice('Loading configuration template', cfgTemplatePath) + # Look up the component in this template + loadCfg = CFG() + loadCfg.loadFromFile(cfgTemplatePath) + compCfg = loadCfg.mergeWith(compCfg) compPath = cfgPath(sectionName, componentModule) if not compCfg.isSection(compPath): @@ -976,14 +990,17 @@ def getAvailableSystems(self, extensions): Get the list of all systems (in all given extensions) locally available """ systems = [] - for extension in extensions: - extensionPath = os.path.join(DIRAC.rootPath, extension, '*System') - for system in [os.path.basename(k).split('System')[0] for k in glob.glob(extensionPath)]: - if system not in systems: - systems.append(system) - - return systems + if six.PY3: + from fnmatch import filter + import importlib.resources # pylint: disable=import-error,no-name-in-module + systems += filter(importlib.resources.contents(extension), "*System") # pylint: disable=no-member + else: + extensionPath = os.path.join(DIRAC.rootPath, extension, '*System') + for system in [os.path.basename(k).split('System')[0] for k in glob.glob(extensionPath)]: + if system not in systems: + systems.append(system) + return list(set(systems)) def getSoftwareComponents(self, extensions): """ @@ -1010,14 +1027,18 @@ def getSoftwareComponents(self, extensions): remainders[cType] = {} for extension in ['DIRAC'] + [x + 'DIRAC' for x in extensions]: - if not os.path.exists(os.path.join(rootPath, extension)): + import importlib + try: + extensionModule = importlib.import_module(extension) + except ImportError: # Not all the extensions are necessarily installed in this self.instance continue - systemList = os.listdir(os.path.join(rootPath, extension)) + extensionDir = os.path.dirname(extensionModule.__file__) + systemList = os.listdir(extensionDir) for sys in systemList: system = sys.replace('System', '') try: - agentDir = os.path.join(rootPath, extension, sys, 'Agent') + agentDir = os.path.join(extensionDir, sys, 'Agent') agentList = os.listdir(agentDir) for agent in agentList: if os.path.splitext(agent)[1] == ".py": @@ -1031,7 +1052,7 @@ def getSoftwareComponents(self, extensions): except OSError: pass try: - serviceDir = os.path.join(rootPath, extension, sys, 'Service') + serviceDir = os.path.join(extensionDir, sys, 'Service') serviceList = os.listdir(serviceDir) for service in serviceList: if service.find('Handler') != -1 and os.path.splitext(service)[1] == '.py': @@ -1043,7 +1064,7 @@ def getSoftwareComponents(self, extensions): except OSError: pass try: - executorDir = os.path.join(rootPath, extension, sys, 'Executor') + executorDir = os.path.join(extensionDir, sys, 'Executor') executorList = os.listdir(executorDir) for executor in executorList: if os.path.splitext(executor)[1] == ".py": @@ -1060,7 +1081,7 @@ def getSoftwareComponents(self, extensions): # Rest of component types for cType in remainingTypes: try: - remainDir = os.path.join(rootPath, extension, sys, cType.title()) + remainDir = os.path.join(extensionDir, sys, cType.title()) remainList = os.listdir(remainDir) for remainder in remainList: if os.path.splitext(remainder)[1] == ".py": @@ -1917,8 +1938,7 @@ def installComponent(self, componentType, system, component, extensions, compone [[ "%(componentType)s" = "agent" ]] && renice 20 -p $$ #%(bashVariables)s # -exec dirac-%(componentType)s \ - %(system)s/%(component)s --cfg %(componentCfg)s < /dev/null +exec dirac-%(componentType)s %(system)s/%(component)s --cfg %(componentCfg)s < /dev/null """ % {'bashrc': os.path.join(self.instancePath, 'bashrc'), 'bashVariables': bashVars, 'componentType': componentType.replace("-", "_"), @@ -2203,15 +2223,24 @@ def getAvailableSQLDatabases(self, extensions): """ dbDict = {} for extension in extensions + ['']: - databases = glob.glob(os.path.join(rootPath, - ('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC'), - '*', 'DB', '*.sql')) + if six.PY3: + import importlib_resources # pylint: disable=import-error + databases = list( + importlib_resources.files(('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC')) + .glob('*/DB/*.sql') + ) + else: + databases = glob.glob(os.path.join( + rootPath, + ('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC'), + '*', 'DB', '*.sql' + )) for dbPath in databases: dbName = os.path.basename(dbPath).replace('.sql', '') dbDict[dbName] = {} dbDict[dbName]['Type'] = 'MySQL' dbDict[dbName]['Extension'] = extension - dbDict[dbName]['System'] = dbPath.split('/')[-3].replace('System', '') + dbDict[dbName]['System'] = str(dbPath).split('/')[-3].replace('System', '') return S_OK(dbDict) @@ -2237,16 +2266,28 @@ def getAvailableESDatabases(self, extensions): for extension in extensions + ['']: # Find *DB.py definitions - pyDBs = glob.glob(os.path.join(rootPath, - ('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC'), - '*', 'DB', '*DB.py')) - pyDBs = [x.replace('.py', '') for x in pyDBs if '__init__' not in x] + if six.PY3: + import importlib_resources # pylint: disable=import-error + pyDBs = list(importlib_resources.files('%sDIRAC' % extension).glob('*/DB/*DB.py')) + else: + pyDBs = glob.glob(os.path.join( + rootPath, + ('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC'), + '*', 'DB', '*DB.py' + )) + pyDBs = [str(x).replace('.py', '') for x in pyDBs if '__init__' not in str(x)] # Find sql files - sqlDBs = glob.glob(os.path.join(rootPath, - ('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC'), - '*', 'DB', '*.sql')) - sqlDBs = [x.replace('.sql', '') for x in sqlDBs] + if six.PY3: + import importlib_resources # pylint: disable=import-error + sqlDBs = list(importlib_resources.files('%sDIRAC' % extension).glob('*/DB/*.sql')) + else: + sqlDBs = glob.glob(os.path.join( + rootPath, + ('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC'), + '*', 'DB', '*.sql' + )) + sqlDBs = [str(x).replace('.sql', '') for x in str(sqlDBs)] # Find *DB.py files that do not have a sql part possible = set(pyDBs) - set(sqlDBs) @@ -2267,7 +2308,7 @@ def getAvailableESDatabases(self, extensions): dbDict[dbName] = {} dbDict[dbName]['Type'] = 'ES' dbDict[dbName]['Extension'] = extension - dbDict[dbName]['System'] = dbPath.split('/')[-3].replace('System', '') + dbDict[dbName]['System'] = str(dbPath).split('/')[-3].replace('System', '') return S_OK(dbDict) @@ -2302,26 +2343,25 @@ def installDatabase(self, dbName): gLogger.notice('Installing', dbName) - dbFile = glob.glob(os.path.join(rootPath, 'DIRAC', '*', 'DB', '%s.sql' % dbName)) # is there by chance an extension of it? - for extension in CSGlobals.getCSExtensions(): - dbFileInExtension = glob.glob(os.path.join(rootPath, - '%sDIRAC' % extension, - '*', - 'DB', - '%s.sql' % dbName)) - if dbFileInExtension: - dbFile = dbFileInExtension + for extension in CSGlobals.getCSExtensions() + [""]: + if six.PY3: + import importlib_resources # pylint: disable=import-error + dbFile = list(importlib_resources.files('%sDIRAC' % extension).glob('*/DB/%s.sql' % dbName)) + else: + dbFile = glob.glob(os.path.join( + rootPath, '%sDIRAC' % extension, '*', 'DB', '%s.sql' % dbName + )) + if dbFile: break - - if not dbFile: + else: error = 'Database %s not found' % dbName gLogger.error(error) if self.exitOnError: DIRAC.exit(-1) return S_ERROR(error) - dbFile = dbFile[0] + dbFile = str(dbFile[0]) gLogger.debug("Installing %s" % dbFile) # just check diff --git a/src/DIRAC/FrameworkSystem/private/monitoring/RRDManager.py b/src/DIRAC/FrameworkSystem/private/monitoring/RRDManager.py index 80495851858..27dc89a6f59 100755 --- a/src/DIRAC/FrameworkSystem/private/monitoring/RRDManager.py +++ b/src/DIRAC/FrameworkSystem/private/monitoring/RRDManager.py @@ -126,7 +126,7 @@ def create(self, type, rrdFile, bucketLength): # 1m res for 1 month # cmd += " RRA:%s:0.9:1:43200" % cf # 1m red for 1 year - cmd += " RRA:%s:0.999:1:%s" % (cf, 31536000 / bucketLength) + cmd += " RRA:%s:0.999:1:%s" % (cf, int(31536000 / bucketLength)) return self.__exec(cmd, rrdFilePath) def __getLastUpdateTime(self, rrdFile): diff --git a/src/DIRAC/ResourceStatusSystem/scripts/dirac_rss_query_db.py b/src/DIRAC/ResourceStatusSystem/scripts/dirac_rss_query_db.py index de13f39f9ec..8dc8d1664b8 100755 --- a/src/DIRAC/ResourceStatusSystem/scripts/dirac_rss_query_db.py +++ b/src/DIRAC/ResourceStatusSystem/scripts/dirac_rss_query_db.py @@ -178,10 +178,10 @@ def unpack(switchDict): statusTypes = [] if switchDict['name'] is not None: - names = filter(None, switchDict['name'].split(',')) + names = list(filter(None, switchDict['name'].split(','))) if switchDict['statusType'] is not None: - statusTypes = filter(None, switchDict['statusType'].split(',')) + statusTypes = list(filter(None, switchDict['statusType'].split(','))) statusTypes = checkStatusTypes(statusTypes) if names and statusTypes: diff --git a/src/DIRAC/ResourceStatusSystem/scripts/dirac_rss_set_status.py b/src/DIRAC/ResourceStatusSystem/scripts/dirac_rss_set_status.py index 45125e5205d..29aa166c090 100755 --- a/src/DIRAC/ResourceStatusSystem/scripts/dirac_rss_set_status.py +++ b/src/DIRAC/ResourceStatusSystem/scripts/dirac_rss_set_status.py @@ -121,10 +121,10 @@ def unpack(switchDict): statusTypes = [] if switchDict['name'] is not None: - names = filter(None, switchDict['name'].split(',')) + names = list(filter(None, switchDict['name'].split(','))) if switchDict['statusType'] is not None: - statusTypes = filter(None, switchDict['statusType'].split(',')) + statusTypes = list(filter(None, switchDict['statusType'].split(','))) statusTypes = checkStatusTypes(statusTypes) if len(names) > 0 and len(statusTypes) > 0: diff --git a/src/DIRAC/Resources/Computing/SSHComputingElement.py b/src/DIRAC/Resources/Computing/SSHComputingElement.py index 9d5cd116e94..bd4f4177f1d 100644 --- a/src/DIRAC/Resources/Computing/SSHComputingElement.py +++ b/src/DIRAC/Resources/Computing/SSHComputingElement.py @@ -21,6 +21,7 @@ from six.moves.urllib.parse import quote as urlquote from six.moves.urllib.parse import unquote as urlunquote +import DIRAC from DIRAC import S_OK, S_ERROR from DIRAC import rootPath from DIRAC import gLogger @@ -424,7 +425,7 @@ def _generateControlScript(self): :return: a path containing the script generated """ # Get the batch system module to use - batchSystemDir = os.path.join(rootPath, "DIRAC", "Resources", "Computing", "BatchSystems") + batchSystemDir = os.path.join(os.path.dirname(DIRAC.__file__), "Resources", "Computing", "BatchSystems") batchSystemScript = os.path.join(batchSystemDir, '%s.py' % self.batchSystem) # Get the executeBatch.py content: an str variable composed of code content that has to be extracted diff --git a/src/DIRAC/Resources/Computing/SingularityComputingElement.py b/src/DIRAC/Resources/Computing/SingularityComputingElement.py index ab57a629991..ccbd5bb2bbf 100644 --- a/src/DIRAC/Resources/Computing/SingularityComputingElement.py +++ b/src/DIRAC/Resources/Computing/SingularityComputingElement.py @@ -36,7 +36,7 @@ __RCSID__ = "$Id$" -DIRAC_INSTALL = os.path.join(DIRAC.rootPath, 'DIRAC', 'Core', 'scripts', 'dirac-install.py') +DIRAC_INSTALL = os.path.join(os.path.dirname(DIRAC.__file__), 'Core', 'scripts', 'dirac-install.py') # Default container to use if it isn't specified in the CE options CONTAINER_DEFROOT = "/cvmfs/cernvm-prod.cern.ch/cvm3" CONTAINER_WORKDIR = "DIRAC_containers" diff --git a/src/DIRAC/Resources/Computing/test/Test_SSHComputingElement.py b/src/DIRAC/Resources/Computing/test/Test_SSHComputingElement.py index 97849e498c9..a51a559e9fa 100644 --- a/src/DIRAC/Resources/Computing/test/Test_SSHComputingElement.py +++ b/src/DIRAC/Resources/Computing/test/Test_SSHComputingElement.py @@ -12,7 +12,7 @@ import shlex import pytest -from DIRAC import rootPath +import DIRAC from DIRAC.Resources.Computing.SSHComputingElement import SSHComputingElement from DIRAC.Resources.Computing.BatchSystems.executeBatch import executeBatchContent @@ -53,7 +53,7 @@ def test_generateControlScript(batchSystem): with open(dest, 'r') as dst: dataDest = dst.read() - batchSystemDir = os.path.join(rootPath, "DIRAC", "Resources", "Computing", "BatchSystems") + batchSystemDir = os.path.join(os.path.dirname(DIRAC.__file__), "Resources", "Computing", "BatchSystems") batchSystemScript = os.path.join(batchSystemDir, '%s.py' % batchSystem) with open(batchSystemScript, 'r') as bsc: dataBatchSystemScript = bsc.read() diff --git a/src/DIRAC/Resources/ProxyProvider/test/Test_DIRACCAProxyProvider.py b/src/DIRAC/Resources/ProxyProvider/test/Test_DIRACCAProxyProvider.py index 466b18415e6..b5ff1c24ebf 100644 --- a/src/DIRAC/Resources/ProxyProvider/test/Test_DIRACCAProxyProvider.py +++ b/src/DIRAC/Resources/ProxyProvider/test/Test_DIRACCAProxyProvider.py @@ -20,12 +20,13 @@ import pytest -from DIRAC import gLogger, rootPath +import DIRAC +from DIRAC import gLogger from DIRAC.Core.Security.X509Chain import X509Chain # pylint: disable=import-error from DIRAC.Resources.ProxyProvider.DIRACCAProxyProvider import DIRACCAProxyProvider -certsPath = os.path.join(rootPath, 'DIRAC/Core/Security/test/certs') +certsPath = os.path.join(os.path.dirname(DIRAC.__file__), 'Core/Security/test/certs') testCAPath = os.path.join(tempfile.mkdtemp(dir='/tmp'), 'ca') testCAConfigFile = os.path.join(testCAPath, 'openssl_config_ca.cnf') diff --git a/src/DIRAC/Resources/ProxyProvider/test/Test_ProxyProviderFactory.py b/src/DIRAC/Resources/ProxyProvider/test/Test_ProxyProviderFactory.py index 453db2fcd1d..8166df1c0e2 100644 --- a/src/DIRAC/Resources/ProxyProvider/test/Test_ProxyProviderFactory.py +++ b/src/DIRAC/Resources/ProxyProvider/test/Test_ProxyProviderFactory.py @@ -7,11 +7,12 @@ import mock import unittest -from DIRAC import S_OK, S_ERROR, gLogger, rootPath +import DIRAC +from DIRAC import S_OK, S_ERROR, gLogger from DIRAC.Resources.ProxyProvider.ProxyProviderFactory import ProxyProviderFactory -certsPath = os.path.join(rootPath, 'DIRAC/Core/Security/test/certs') +certsPath = os.path.join(os.path.dirname(DIRAC.__file__), 'Core/Security/test/certs') def sf_getInfoAboutProviders(of, providerName, option, section): diff --git a/src/DIRAC/WorkloadManagementSystem/Agent/SiteDirector.py b/src/DIRAC/WorkloadManagementSystem/Agent/SiteDirector.py index 6f40099dcbe..9e332c66156 100644 --- a/src/DIRAC/WorkloadManagementSystem/Agent/SiteDirector.py +++ b/src/DIRAC/WorkloadManagementSystem/Agent/SiteDirector.py @@ -53,7 +53,7 @@ from DIRAC.ResourceStatusSystem.Client.SiteStatus import SiteStatus # dirac install file -DIRAC_INSTALL = os.path.join(DIRAC.rootPath, 'DIRAC', 'Core', 'scripts', 'dirac-install.py') +DIRAC_INSTALL = os.path.join(os.path.dirname(DIRAC.__file__), 'Core', 'scripts', 'dirac-install.py') # status TRANSIENT_PILOT_STATUS = ['Submitted', 'Waiting', 'Running', 'Scheduled', 'Ready', 'Unknown'] diff --git a/src/DIRAC/__init__.py b/src/DIRAC/__init__.py index 3d73fd8c876..cdbca15b9d2 100755 --- a/src/DIRAC/__init__.py +++ b/src/DIRAC/__init__.py @@ -122,9 +122,11 @@ alarmMail = "dirac.alarms@gmail.com" # Set rootPath of DIRAC installation - -pythonPath = os.path.realpath(__path__[0]) -rootPath = os.path.dirname(pythonPath) +if six.PY3: + rootPath = sys.base_prefix # pylint: disable=no-member +else: + pythonPath = os.path.realpath(__path__[0]) + rootPath = os.path.dirname(pythonPath) # Import DIRAC.Core.Utils modules diff --git a/tests/Integration/DataManagementSystem/Test_Client_DFC.py b/tests/Integration/DataManagementSystem/Test_Client_DFC.py index 60d083988db..c90151d58f8 100644 --- a/tests/Integration/DataManagementSystem/Test_Client_DFC.py +++ b/tests/Integration/DataManagementSystem/Test_Client_DFC.py @@ -837,15 +837,10 @@ def test_directoryOperations(self): (parentDir, result)) self.assertEqual( - result2['Value'].get( - 'Successful', - {}).get( - parentDir, - {}).get('Owner'), + result2['Value'].get('Successful', {}).get(parentDir, {}).get('Owner'), proxyUser, - "parentDir should not have changed Owner from %s ==> %s)" % - (proxyUser, - result2)) + "parentDir should not have changed Owner from %s ==> %s)" % (proxyUser, result2) + ) self.assertEqual( result2['Value'].get( 'Successful', diff --git a/tests/Integration/Framework/Test_ProxyDB.py b/tests/Integration/Framework/Test_ProxyDB.py index db93264223a..8ed10903a4c 100644 --- a/tests/Integration/Framework/Test_ProxyDB.py +++ b/tests/Integration/Framework/Test_ProxyDB.py @@ -25,12 +25,13 @@ from DIRAC.Core.Base.Script import parseCommandLine parseCommandLine() +import DIRAC from DIRAC import gLogger, gConfig, S_OK, S_ERROR from DIRAC.Core.Security.X509Chain import X509Chain # pylint: disable=import-error from DIRAC.FrameworkSystem.DB.ProxyDB import ProxyDB from DIRAC.Resources.ProxyProvider.DIRACCAProxyProvider import DIRACCAProxyProvider -certsPath = os.path.join(os.environ['DIRAC'], 'DIRAC/Core/Security/test/certs') +certsPath = os.path.join(os.path.dirname(DIRAC.__file__), 'Core/Security/test/certs') ca = DIRACCAProxyProvider() ca.setParameters({'CertFile': os.path.join(certsPath, 'ca/ca.cert.pem'), 'KeyFile': os.path.join(certsPath, 'ca/ca.key.pem')}) diff --git a/tests/Integration/Resources/ProxyProvider/Test_DIRACCAProxyProvider.py b/tests/Integration/Resources/ProxyProvider/Test_DIRACCAProxyProvider.py index 9245972ce55..6fcfdb63956 100644 --- a/tests/Integration/Resources/ProxyProvider/Test_DIRACCAProxyProvider.py +++ b/tests/Integration/Resources/ProxyProvider/Test_DIRACCAProxyProvider.py @@ -12,11 +12,12 @@ from diraccfg import CFG +import DIRAC from DIRAC import gConfig from DIRAC.Core.Security.X509Chain import X509Chain # pylint: disable=import-error from DIRAC.Resources.ProxyProvider.ProxyProviderFactory import ProxyProviderFactory -certsPath = os.path.join(os.environ['DIRAC'], 'DIRAC/Core/Security/test/certs') +certsPath = os.path.join(os.path.dirname(DIRAC.__file__), 'Core/Security/test/certs') diracTestCACFG = """ Resources diff --git a/tests/Integration/all_integration_client_tests.sh b/tests/Integration/all_integration_client_tests.sh index 368e26f4e3a..b443bff0e56 100644 --- a/tests/Integration/all_integration_client_tests.sh +++ b/tests/Integration/all_integration_client_tests.sh @@ -2,8 +2,7 @@ #------------------------------------------------------------------------------- # A convenient way to run all the integration tests for client -> server interaction # -# It supposes that DIRAC client is installed in "${CLIENTINSTALLDIR}" -# and that there's a DIRAC server running with all the services running. +# It supposes that there's a DIRAC server running with all the services running. #------------------------------------------------------------------------------- echo -e '****************************************' From e764f2039ce1236071faf356db746605efb3a657 Mon Sep 17 00:00:00 2001 From: Chris Burr Date: Wed, 7 Apr 2021 16:42:06 +0200 Subject: [PATCH 5/9] Factorise recurseImport to DIRAC.Core.Utilities.Extensions --- src/DIRAC/Core/Base/private/ModuleLoader.py | 46 ++++------------ src/DIRAC/Core/Utilities/DErrno.py | 60 ++++++--------------- src/DIRAC/Core/Utilities/ObjectLoader.py | 51 ++++-------------- src/DIRAC/__init__.py | 26 --------- 4 files changed, 34 insertions(+), 149 deletions(-) diff --git a/src/DIRAC/Core/Base/private/ModuleLoader.py b/src/DIRAC/Core/Base/private/ModuleLoader.py index 34e87e43007..2a520313757 100644 --- a/src/DIRAC/Core/Base/private/ModuleLoader.py +++ b/src/DIRAC/Core/Base/private/ModuleLoader.py @@ -4,13 +4,12 @@ from __future__ import division from __future__ import print_function -import six import os -import imp from DIRAC.Core.Utilities import List from DIRAC import gConfig, S_ERROR, S_OK, gLogger from DIRAC.ConfigurationSystem.Client.Helpers import getInstalledExtensions from DIRAC.ConfigurationSystem.Client import PathFinder +from DIRAC.Core.Utilities.Extensions import recurseImport class ModuleLoader(object): @@ -65,12 +64,11 @@ def loadModules(self, modulesList, hideExceptions=False): # Look what is installed parentModule = None for rootModule in getInstalledExtensions(): - if system.find("System") != len(system) - 6: - parentImport = "%s.%sSystem.%s" % (rootModule, system, self.__csSuffix) - else: - parentImport = "%s.%s.%s" % (rootModule, system, self.__csSuffix) + if not system.endswith("System"): + system += "System" + parentImport = "%s.%s.%s" % (rootModule, system, self.__csSuffix) # HERE! - result = self.__recurseImport(parentImport) + result = recurseImport(parentImport) if not result['OK']: return result parentModule = result['Value'] @@ -113,7 +111,7 @@ def loadModule(self, modName, hideExceptions=False, parentModule=False): for loadModName in loadGroup: if loadModName.find("/") == -1: loadModName = "%s/%s" % (modList[0], loadModName) - result = self.loadModule(loadModName, hideExceptions=hideExceptions, parentModule=False) + result = self.loadModule(loadModName, hideExceptions=hideExceptions) if not result['OK']: return result return S_OK() @@ -146,7 +144,7 @@ def loadModule(self, modName, hideExceptions=False, parentModule=False): if handlerPath.find(".py", len(handlerPath) - 3) > -1: handlerPath = handlerPath[:-3] className = List.fromChar(handlerPath, ".")[-1] - result = self.__recurseImport(handlerPath) + result = recurseImport(handlerPath) if not result['OK']: return S_ERROR("Cannot load user defined handler %s: %s" % (handlerPath, result['Message'])) gLogger.verbose("Loaded %s" % handlerPath) @@ -156,7 +154,7 @@ def loadModule(self, modName, hideExceptions=False, parentModule=False): modImport = module if self.__modSuffix: modImport = "%s%s" % (modImport, self.__modSuffix) - result = self.__recurseImport(modImport, parentModule, hideExceptions=hideExceptions) + result = recurseImport(modImport, parentModule, hideExceptions=hideExceptions) else: # Check to see if the module exists in any of the root modules gLogger.info("Trying to autodiscover %s" % loadName) @@ -166,7 +164,7 @@ def loadModule(self, modName, hideExceptions=False, parentModule=False): if self.__modSuffix: importString = "%s%s" % (importString, self.__modSuffix) gLogger.verbose("Trying to load %s" % importString) - result = self.__recurseImport(importString, hideExceptions=hideExceptions) + result = recurseImport(importString, hideExceptions=hideExceptions) # Error while loading if not result['OK']: return result @@ -203,29 +201,3 @@ def loadModule(self, modName, hideExceptions=False, parentModule=False): gLogger.notice("Loaded module %s" % modName) return S_OK() - - def __recurseImport(self, modName, parentModule=None, hideExceptions=False): - gLogger.debug("importing recursively %s, parentModule=%s, hideExceptions=%s" % (modName, - parentModule, - hideExceptions)) - if isinstance(modName, six.string_types): - modName = List.fromChar(modName, ".") - try: - if parentModule: - impData = imp.find_module(modName[0], parentModule.__path__) - else: - impData = imp.find_module(modName[0]) - impModule = imp.load_module(modName[0], *impData) - if impData[0]: - impData[0].close() - except ImportError as excp: - strExcp = str(excp) - if strExcp.find("No module named") == 0 and strExcp.find(modName[0]) == len(strExcp) - len(modName[0]): - return S_OK() - errMsg = "Can't load %s" % ".".join(modName) - if not hideExceptions: - gLogger.exception(errMsg) - return S_ERROR(errMsg) - if len(modName) == 1: - return S_OK(impModule) - return self.__recurseImport(modName[1:], impModule) diff --git a/src/DIRAC/Core/Utilities/DErrno.py b/src/DIRAC/Core/Utilities/DErrno.py index 1dcfc1353f3..4bbc098237e 100644 --- a/src/DIRAC/Core/Utilities/DErrno.py +++ b/src/DIRAC/Core/Utilities/DErrno.py @@ -43,7 +43,7 @@ import six import os -import imp +import importlib import sys # To avoid conflict, the error numbers should be greater than 1000 @@ -344,50 +344,22 @@ def includeExtensionErrors(): Should be called only at the initialization of DIRAC, so by the parseCommandLine, dirac-agent.py, dirac-service.py, dirac-executor.py """ - - def __recurseImport(modName, parentModule=None, fullName=False): - """ Internal function to load modules - """ - if isinstance(modName, six.string_types): - modName = modName.split(".") - if not fullName: - fullName = ".".join(modName) - try: - if parentModule: - impData = imp.find_module(modName[0], parentModule.__path__) - else: - impData = imp.find_module(modName[0]) - impModule = imp.load_module(modName[0], *impData) - if impData[0]: - impData[0].close() - except ImportError: - return None - if len(modName) == 1: - return impModule - return __recurseImport(modName[1:], impModule, fullName=fullName) - from DIRAC.ConfigurationSystem.Client.Helpers import CSGlobals - allExtensions = CSGlobals.getCSExtensions() - for extension in allExtensions: - ext_derrno = None + for extension in CSGlobals.getCSExtensions(): try: - - ext_derrno = __recurseImport('%sDIRAC.Core.Utilities.DErrno' % extension) - - if ext_derrno: - # The next 3 dictionary MUST be present for consistency - - # Global name of errors - sys.modules[__name__].__dict__.update(ext_derrno.extra_dErrName) - # Dictionary with the error codes - sys.modules[__name__].dErrorCode.update(ext_derrno.extra_dErrorCode) - # Error description string - sys.modules[__name__].dStrError.update(ext_derrno.extra_dStrError) - - # extra_compatErrorString is optional - for err in getattr(ext_derrno, 'extra_compatErrorString', []): - sys.modules[__name__].compatErrorString.setdefault(err, []).extend(ext_derrno.extra_compatErrorString[err]) - - except Exception: + ext_derrno = importlib.import_module('%sDIRAC.Core.Utilities.DErrno' % extension) + except ImportError: pass + else: + # The next 3 dictionary MUST be present for consistency + # Global name of errors + sys.modules[__name__].__dict__.update(ext_derrno.extra_dErrName) + # Dictionary with the error codes + sys.modules[__name__].dErrorCode.update(ext_derrno.extra_dErrorCode) + # Error description string + sys.modules[__name__].dStrError.update(ext_derrno.extra_dStrError) + + # extra_compatErrorString is optional + for err in getattr(ext_derrno, 'extra_compatErrorString', []): + sys.modules[__name__].compatErrorString.setdefault(err, []).extend(ext_derrno.extra_compatErrorString[err]) diff --git a/src/DIRAC/Core/Utilities/ObjectLoader.py b/src/DIRAC/Core/Utilities/ObjectLoader.py index e1e4621c964..4397927a5fb 100644 --- a/src/DIRAC/Core/Utilities/ObjectLoader.py +++ b/src/DIRAC/Core/Utilities/ObjectLoader.py @@ -8,13 +8,13 @@ import six import re -import imp import pkgutil import collections from DIRAC import gLogger, S_OK, S_ERROR from DIRAC.Core.Utilities import DErrno from DIRAC.Core.Utilities import List, DIRACSingleton +from DIRAC.Core.Utilities.Extensions import recurseImport from DIRAC.ConfigurationSystem.Client.Helpers import CSGlobals @@ -41,7 +41,6 @@ def __init__(self, baseModules=False): def _init(self, baseModules): """ Actually performs the initialization """ - if not baseModules: baseModules = ['DIRAC'] self.__rootModules = baseModules @@ -70,7 +69,7 @@ def __rootImport(self, modName, hideExceptions=False): if rootModule: impName = "%s.%s" % (rootModule, impName) gLogger.debug("Trying to load %s" % impName) - result = self.__recurseImport(impName, hideExceptions=hideExceptions) + result = recurseImport(impName, hideExceptions=hideExceptions) # Error. Something cannot be imported. Return error if not result['OK']: return result @@ -81,42 +80,12 @@ def __rootImport(self, modName, hideExceptions=False): # Return nothing found return S_OK() - def __recurseImport(self, modName, parentModule=None, hideExceptions=False, fullName=False): - """ Internal function to load modules - """ - if isinstance(modName, six.string_types): - modName = List.fromChar(modName, ".") - if not fullName: - fullName = ".".join(modName) - if fullName in self.__objs: - return S_OK(self.__objs[fullName]) - try: - if parentModule: - impData = imp.find_module(modName[0], parentModule.__path__) - else: - impData = imp.find_module(modName[0]) - impModule = imp.load_module(modName[0], *impData) - if impData[0]: - impData[0].close() - except Exception as excp: - if "No module named" in str(excp) and modName[0] in str(excp): - return S_OK(None) - errMsg = "Can't load %s in %s" % (".".join(modName), parentModule.__path__[0]) - if not hideExceptions: - gLogger.exception(errMsg) - return S_ERROR(DErrno.EIMPERR, errMsg) - if len(modName) == 1: - self.__objs[fullName] = impModule - return S_OK(impModule) - return self.__recurseImport(modName[1:], impModule, - hideExceptions=hideExceptions, fullName=fullName) - def __generateRootModules(self, baseModules): """ Iterate over all the possible root modules """ self.__rootModules = baseModules for rootModule in reversed(CSGlobals.getCSExtensions()): - if rootModule[-5:] != "DIRAC" and rootModule not in self.__rootModules: + if not rootModule.endswith("DIRAC") and rootModule not in self.__rootModules: self.__rootModules.append("%sDIRAC" % rootModule) self.__rootModules.append("") @@ -136,18 +105,16 @@ def loadModule(self, importString, hideExceptions=False): def loadObject(self, importString, objName=False, hideExceptions=False): """ Load an object from inside a module """ + if not objName: + objName = importString.split(".")[-1] + result = self.loadModule(importString, hideExceptions=hideExceptions) if not result['OK']: return result modObj = result['Value'] - modFile = modObj.__file__ - - if not objName: - objName = List.fromChar(importString, ".")[-1] - try: result = S_OK(getattr(modObj, objName)) - result['ModuleFile'] = modFile + result['ModuleFile'] = modObj.__file__ return result except AttributeError: return S_ERROR(DErrno.EIMPERR, "%s does not contain a %s object" % (importString, objName)) @@ -179,7 +146,7 @@ def getObjects(self, modulePath, reFilter=None, parentClass=None, recurse=False, impPath = modulePath gLogger.debug("Trying to load %s" % impPath) - result = self.__recurseImport(impPath) + result = recurseImport(impPath) if not result['OK']: return result if not result['Value']: @@ -204,7 +171,7 @@ def getObjects(self, modulePath, reFilter=None, parentClass=None, recurse=False, if modKeyName in modules: continue fullName = "%s.%s" % (impPath, modName) - result = self.__recurseImport(modName, parentModule=parentModule, fullName=fullName) + result = recurseImport(modName, parentModule=parentModule, fullName=fullName) if not result['OK']: if continueOnError: gLogger.error("Error loading module but continueOnError is true", "module %s error %s" % (fullName, result)) diff --git a/src/DIRAC/__init__.py b/src/DIRAC/__init__.py index cdbca15b9d2..9ebca2f5097 100755 --- a/src/DIRAC/__init__.py +++ b/src/DIRAC/__init__.py @@ -154,32 +154,6 @@ __siteName = False -# # Update DErrno with the extensions errors -# from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader -# from DIRAC.ConfigurationSystem.Client.Helpers import CSGlobals -# allExtensions = CSGlobals.getCSExtensions() -# -# # Update for each extension. Careful to conflict :-) -# for extension in allExtensions: -# ol = ObjectLoader( baseModules = ["%sDIRAC" % extension] ) -# extraErrorModule = ol.loadModule( 'Core.Utilities.DErrno' ) -# if extraErrorModule['OK']: -# extraErrorModule = extraErrorModule['Value'] -# -# # The next 3 dictionary MUST be present for consistency -# -# # Global name of errors -# DErrno.__dict__.update( extraErrorModule.extra_dErrName ) -# # Dictionary with the error codes -# DErrno.dErrorCode.update( extraErrorModule.extra_dErrorCode ) -# # Error description string -# DErrno.dStrError.update( extraErrorModule.extra_dStrError ) -# -# # extra_compatErrorString is optional -# for err in getattr( extraErrorModule, 'extra_compatErrorString', [] ) : -# DErrno.compatErrorString.setdefault( err, [] ).extend( extraErrorModule.extra_compatErrorString[err] ) - - def siteName(): """ Determine and return DIRAC name for current site From 114464af191f3700b28fe313176987a3f6d1748e Mon Sep 17 00:00:00 2001 From: Chris Burr Date: Thu, 8 Apr 2021 10:40:48 +0200 Subject: [PATCH 6/9] Use DIRAC.Core.Utilities.Extensions in ComponentInstaller --- .../Client/ComponentInstaller.py | 176 +++++------------- .../Service/SystemAdministratorHandler.py | 18 +- 2 files changed, 55 insertions(+), 139 deletions(-) diff --git a/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py b/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py index c0a33b9e4c5..945854be5f5 100644 --- a/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py +++ b/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py @@ -97,7 +97,9 @@ from DIRAC.Core.Base.AgentModule import AgentModule from DIRAC.Core.Base.ExecutorModule import ExecutorModule from DIRAC.Core.DISET.RequestHandler import RequestHandler +from DIRAC.Core.Utilities.Decorators import deprecated from DIRAC.Core.Utilities.PrettyPrint import printTable +from DIRAC.Core.Utilities.Extensions import extensionsByPriority, findDatabases, findModules, findSystems __RCSID__ = "$Id$" @@ -147,7 +149,6 @@ def __init__(self): def loadDiracCfg(self): """ Read again defaults from dirac.cfg """ - from DIRAC.Core.Utilities.Network import getFQDN self.localCfg = CFG() @@ -279,17 +280,10 @@ def getInfo(self): rDict['Setup'] = 'Unknown' return S_OK(rDict) + @deprecated("Use DIRAC.Core.Utilities.Extensions.extensionsByPriority instead") def getExtensions(self): - """ - Get the list of installed extensions - """ - if six.PY3: - from DIRAC.Core.Utilities.DIRACScript import _extensionsByPriority - extensions = _extensionsByPriority() - else: - initList = glob.glob(os.path.join(rootPath, '*DIRAC', '__init__.py')) - extensions = [os.path.basename(os.path.dirname(k)) for k in initList] - + """Get the list of installed extensions""" + extensions = extensionsByPriority() try: extensions.remove('DIRAC') except Exception: @@ -298,7 +292,6 @@ def getExtensions(self): if self.exitOnError: DIRAC.exit(-1) return S_ERROR(error) - return S_OK(extensions) def _addCfgToDiracCfg(self, cfg): @@ -319,7 +312,6 @@ def _addCfgToCS(self, cfg): """ Merge cfg into central CS """ - gLogger.debug("Adding CFG to CS:") gLogger.debug(cfg) @@ -794,20 +786,14 @@ def getComponentCfg(self, componentType, system, component, compInstance, extens if addDefaultOptions: extensionsDIRAC = [x + 'DIRAC' for x in extensions] + extensions for ext in extensionsDIRAC + ['DIRAC']: - if six.PY3: - import pkg_resources - try: - cfgTemplatePath = pkg_resources.resource_filename(ext, "%sSystem/ConfigTemplate.cfg" % system) - except ModuleNotFoundError: - continue - else: - cfgTemplatePath = os.path.join(rootPath, ext, '%sSystem' % system, 'ConfigTemplate.cfg') - if not os.path.exists(cfgTemplatePath): - continue - gLogger.notice('Loading configuration template', cfgTemplatePath) - # Look up the component in this template + cfgTemplateModule = "%s.%sSystem" % (ext, system) + try: + cfgTemplate = importlib_resources.read_text(cfgTemplateModule, "ConfigTemplate.cfg") + except (ImportError, OSError): + continue + gLogger.notice('Loading configuration template from', cfgTemplateModule) loadCfg = CFG() - loadCfg.loadFromFile(cfgTemplatePath) + loadCfg.loadFromBuffer(cfgTemplate) compCfg = loadCfg.mergeWith(compCfg) compPath = cfgPath(sectionName, componentModule) @@ -985,22 +971,10 @@ def printOverallStatus(self, rDict): return S_OK() + @deprecated("Use DIRAC.Core.Utilities.Extensions.findSystems instead") def getAvailableSystems(self, extensions): - """ - Get the list of all systems (in all given extensions) locally available - """ - systems = [] - for extension in extensions: - if six.PY3: - from fnmatch import filter - import importlib.resources # pylint: disable=import-error,no-name-in-module - systems += filter(importlib.resources.contents(extension), "*System") # pylint: disable=no-member - else: - extensionPath = os.path.join(DIRAC.rootPath, extension, '*System') - for system in [os.path.basename(k).split('System')[0] for k in glob.glob(extensionPath)]: - if system not in systems: - systems.append(system) - return list(set(systems)) + """Get the list of all systems (in all given extensions) locally available""" + return list(findSystems(extensions)) def getSoftwareComponents(self, extensions): """ @@ -1103,7 +1077,6 @@ def getInstalledComponents(self): Get the list of all the components ( services and agents ) installed on the system in the runit directory """ - resultDict = {} resultIndexes = {} for cType in self.componentTypes: @@ -1138,7 +1111,6 @@ def getSetupComponents(self): Get the list of all the components ( services and agents ) set up for running with runsvdir in startup directory """ - resultDict = {} resultIndexes = {} for cType in self.componentTypes: @@ -1269,7 +1241,6 @@ def getOverallStatus(self, extensions): Get the list of all the components ( services and agents ) set up for running with runsvdir in startup directory """ - result = self.getSoftwareComponents(extensions) if not result['OK']: return result @@ -2094,7 +2065,6 @@ def installPortal(self): """ Install runit directories for the Web Portal """ - # Check that the software for the Web Portal is installed error = '' webDir = os.path.join(self.linkedRootPath, 'WebAppDIRAC') @@ -2222,26 +2192,14 @@ def getAvailableSQLDatabases(self, extensions): :return: dict of MySQL DBs """ dbDict = {} - for extension in extensions + ['']: - if six.PY3: - import importlib_resources # pylint: disable=import-error - databases = list( - importlib_resources.files(('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC')) - .glob('*/DB/*.sql') - ) - else: - databases = glob.glob(os.path.join( - rootPath, - ('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC'), - '*', 'DB', '*.sql' - )) - for dbPath in databases: - dbName = os.path.basename(dbPath).replace('.sql', '') + for extension in reversed(extensions + ['']): + databases = findDatabases(('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC')) + for systemName, dbSql in databases: + dbName = dbSql.replace('.sql', '') dbDict[dbName] = {} dbDict[dbName]['Type'] = 'MySQL' dbDict[dbName]['Extension'] = extension - dbDict[dbName]['System'] = str(dbPath).split('/')[-3].replace('System', '') - + dbDict[dbName]['System'] = systemName.replace('System', '') return S_OK(dbDict) def getAvailableESDatabases(self, extensions): @@ -2263,52 +2221,26 @@ def getAvailableESDatabases(self, extensions): :return: dict of ES DBs """ dbDict = {} - for extension in extensions + ['']: - - # Find *DB.py definitions - if six.PY3: - import importlib_resources # pylint: disable=import-error - pyDBs = list(importlib_resources.files('%sDIRAC' % extension).glob('*/DB/*DB.py')) - else: - pyDBs = glob.glob(os.path.join( - rootPath, - ('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC'), - '*', 'DB', '*DB.py' - )) - pyDBs = [str(x).replace('.py', '') for x in pyDBs if '__init__' not in str(x)] - - # Find sql files - if six.PY3: - import importlib_resources # pylint: disable=import-error - sqlDBs = list(importlib_resources.files('%sDIRAC' % extension).glob('*/DB/*.sql')) - else: - sqlDBs = glob.glob(os.path.join( - rootPath, - ('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC'), - '*', 'DB', '*.sql' - )) - sqlDBs = [str(x).replace('.sql', '') for x in str(sqlDBs)] - - # Find *DB.py files that do not have a sql part - possible = set(pyDBs) - set(sqlDBs) - databases = [] - for p in possible: - # Introspect all possible ones + for extension in reversed(extensions + ['']): + ext = ('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC') + sqlDatabases = findDatabases(ext) + for systemName, dbName in findModules(ext, "DB", "*DB"): + if (systemName, dbName + ".sql") in sqlDatabases: + continue + + # Introspect all possible ones for a ElasticDB attribute try: - p_mod = p.replace(rootPath, '').lstrip('/').replace('/', '.') - mdb_mod = importlib.import_module(p_mod, p_mod.split('.')[-1]) - cl = getattr(mdb_mod, p_mod.split('.')[-1]) - if 'ElasticDB' in str(inspect.getmro(cl)): - databases.append(p) + module = importlib.import_module(".".join([ext, systemName, "DB", dbName])) + dbClass = getattr(module, dbName) except (AttributeError, ImportError): - pass + continue + if 'ElasticDB' not in str(inspect.getmro(dbClass)): + continue - for dbPath in databases: - dbName = os.path.basename(dbPath) dbDict[dbName] = {} dbDict[dbName]['Type'] = 'ES' dbDict[dbName]['Extension'] = extension - dbDict[dbName]['System'] = str(dbPath).split('/')[-3].replace('System', '') + dbDict[dbName]['System'] = systemName return S_OK(dbDict) @@ -2330,7 +2262,6 @@ def installDatabase(self, dbName): """ Install requested DB in MySQL server """ - if not self.mysqlRootPwd: rootPwdPath = cfgInstallPath('Database', 'RootPwd') return S_ERROR('Missing %s in %s' % (rootPwdPath, self.cfgFile)) @@ -2345,14 +2276,10 @@ def installDatabase(self, dbName): # is there by chance an extension of it? for extension in CSGlobals.getCSExtensions() + [""]: - if six.PY3: - import importlib_resources # pylint: disable=import-error - dbFile = list(importlib_resources.files('%sDIRAC' % extension).glob('*/DB/%s.sql' % dbName)) - else: - dbFile = glob.glob(os.path.join( - rootPath, '%sDIRAC' % extension, '*', 'DB', '%s.sql' % dbName - )) - if dbFile: + ext = ('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC') + databases = {k: v for v, k in findDatabases(ext)} + filename = dbName + ".sql" + if filename in databases: break else: error = 'Database %s not found' % dbName @@ -2360,9 +2287,10 @@ def installDatabase(self, dbName): if self.exitOnError: DIRAC.exit(-1) return S_ERROR(error) - - dbFile = str(dbFile[0]) - gLogger.debug("Installing %s" % dbFile) + systemName = databases[filename] + moduleName = ".".join([ext, systemName, "DB"]) + gLogger.debug("Installing %s from %s" % (filename, moduleName)) + dbSql = importlib_resources.read_text(moduleName, filename) # just check result = self.execMySQL('SHOW STATUS') @@ -2402,7 +2330,7 @@ def installDatabase(self, dbName): # first getting the lines to be executed, and then execute them try: - cmdLines = self._createMySQLCMDLines(dbFile) + cmdLines = self._createMySQLCMDLines(dbSql) # We need to run one SQL cmd at once, mysql is much happier that way. # Create a string of commands, ignoring comment lines @@ -2427,7 +2355,7 @@ def installDatabase(self, dbName): DIRAC.exit(-1) return S_ERROR(error) - return S_OK(dbFile.split('/')[-4:-2]) + return S_OK(extension, systemName) def uninstallDatabase(self, gConfig_o, dbName): """ @@ -2445,16 +2373,14 @@ def uninstallDatabase(self, gConfig_o, dbName): return S_OK('DB successfully uninstalled') - def _createMySQLCMDLines(self, dbFile): - """ Creates a list of MYSQL commands to be executed, inspecting the dbFile(s) - """ + def _createMySQLCMDLines(self, dbSql): + """Creates a list of MYSQL commands to be executed, inspecting the SQL + :param str dbSql: The SQL to parse + :returns: list of str corresponding to executable SQL statements + """ cmdLines = [] - - with io.open(dbFile, 'rt') as fd: - dbLines = fd.readlines() - - for line in dbLines: + for line in dbSql.split("\n"): # Should we first source an SQL file (is this sql file an extension)? if line.lower().startswith('source'): sourcedDBbFileName = line.split(' ')[1].replace('\n', '') @@ -2606,7 +2532,6 @@ def addTornadoOptionsToCS(self, gConfig_o): """ Add the section with the component options to the CS """ - if gConfig_o: gConfig_o.forceRefresh() @@ -2629,7 +2554,6 @@ def setupTornadoService(self, system, component, extensions, """ Install and create link in startup """ - # Create the startup entry now # Force the system and component to be 'Tornado' but preserve the interface and the code # just to allow for easier refactoring maybe later diff --git a/src/DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py b/src/DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py index 74857a9fd8e..94807d64b80 100644 --- a/src/DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py +++ b/src/DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py @@ -32,6 +32,7 @@ from DIRAC import S_OK, S_ERROR, gConfig, rootPath, gLogger from DIRAC.Core.DISET.RequestHandler import RequestHandler from DIRAC.Core.Utilities import Os +from DIRAC.Core.Utilities.Extensions import extensionsByPriority from DIRAC.Core.Utilities.File import mkLink from DIRAC.Core.Utilities.Time import dateTime, fromString, hour, day from DIRAC.Core.Utilities.Subprocess import shellCall, systemCall @@ -623,24 +624,15 @@ def export_getUsedPorts(self): def export_getComponentDocumentation(self, cType, system, module): if cType == 'service': module = '%sHandler' % module - - result = gComponentInstaller.getExtensions() - extensions = result['Value'] # Look for the component in extensions - for extension in extensions: + for extension in extensionsByPriority(): + moduleName = ([extension, system + "System", cType.capitalize(), module]) try: - importedModule = importlib.import_module('%s.%sSystem.%s.%s' % (extension, system, - cType.capitalize(), module)) + importedModule = importlib.import_module(moduleName) return S_OK(importedModule.__doc__) except Exception: pass - - # If not in an extension, try in base DIRAC - try: - importedModule = importlib.import_module('DIRAC.%sSystem.%s.%s' % (system, cType.capitalize(), module)) - return S_OK(importedModule.__doc__) - except Exception: - return S_ERROR('No documentation was found') + return S_ERROR('No documentation was found') @staticmethod def __storeHostInfo(): From cd15833a2cac0ffcfa7b95b6ac6ea3d7791a1535 Mon Sep 17 00:00:00 2001 From: Chris Burr Date: Thu, 8 Apr 2021 13:55:16 +0200 Subject: [PATCH 7/9] Mostly replace getCSExtensions and various object loading clean-ups --- .../Client/Helpers/CSGlobals.py | 19 +- .../Client/LocalConfiguration.py | 2 +- src/DIRAC/Core/Base/private/ModuleLoader.py | 18 +- .../Core/DISET/private/MessageFactory.py | 5 +- src/DIRAC/Core/Utilities/DErrno.py | 10 +- src/DIRAC/Core/Utilities/ObjectLoader.py | 40 +- .../Core/Utilities/Plotting/ObjectLoader.py | 5 +- src/DIRAC/Core/Utilities/Version.py | 38 +- .../Core/Utilities/test/Test_ObjectLoader.py | 36 +- src/DIRAC/Core/scripts/dirac_configure.py | 6 +- src/DIRAC/Core/scripts/dirac_install_db.py | 16 +- .../DataManagementSystem/DB/FileCatalogDB.py | 12 +- .../Client/ComponentInstaller.py | 607 +++++++----------- .../Client/SystemAdministratorClientCLI.py | 8 +- .../Service/ProxyManagerHandler.py | 2 +- .../Service/SystemAdministratorHandler.py | 14 +- .../scripts/dirac_install_component.py | 55 +- .../scripts/dirac_install_tornado_service.py | 6 +- .../Agent/CacheFeederAgent.py | 6 +- .../Agent/ElementInspectorAgent.py | 6 +- .../Agent/SiteInspectorAgent.py | 6 +- .../ResourceStatusSystem/PolicySystem/PEP.py | 9 +- .../Resources/Catalog/FCConditionParser.py | 2 +- .../Resources/Catalog/FileCatalogFactory.py | 7 +- .../Resources/Computing/ComputingElement.py | 3 +- .../Computing/ComputingElementFactory.py | 5 +- .../Resources/IdProvider/IdProviderFactory.py | 5 +- .../Resources/MessageQueue/MQConnector.py | 5 +- .../ProxyProvider/ProxyProviderFactory.py | 5 +- src/DIRAC/Resources/Storage/StorageElement.py | 2 +- src/DIRAC/Resources/Storage/StorageFactory.py | 2 +- src/DIRAC/Workflow/Utilities/Utils.py | 47 +- .../private/SharesCorrector.py | 7 +- tests/Jenkins/dirac-cfg-add-option.py | 4 +- 34 files changed, 388 insertions(+), 632 deletions(-) diff --git a/src/DIRAC/ConfigurationSystem/Client/Helpers/CSGlobals.py b/src/DIRAC/ConfigurationSystem/Client/Helpers/CSGlobals.py index 912686f3d74..1f54aa1f413 100644 --- a/src/DIRAC/ConfigurationSystem/Client/Helpers/CSGlobals.py +++ b/src/DIRAC/ConfigurationSystem/Client/Helpers/CSGlobals.py @@ -12,7 +12,9 @@ import imp import six +from DIRAC.Core.Utilities.Decorators import deprecated from DIRAC.Core.Utilities.DIRACSingleton import DIRACSingleton +from DIRAC.Core.Utilities.Extensions import extensionsByPriority @six.add_metaclass(DIRACSingleton) @@ -25,10 +27,8 @@ def __init__(self): def __load(self): if self.__orderedExtNames: return - for extName in self.getCSExtensions() + ['']: + for extName in extensionsByPriority(): try: - if not extName.endswith("DIRAC"): - extName = '%sDIRAC' % extName res = imp.find_module(extName) if res[0]: res[0].close() @@ -40,15 +40,13 @@ def __load(self): def getCSExtensions(self): if not self.__csExt: if six.PY3: - from DIRAC.Core.Utilities.DIRACScript import _extensionsByPriority - exts = _extensionsByPriority() + exts = extensionsByPriority() else: from DIRAC.ConfigurationSystem.Client.Config import gConfig exts = gConfig.getValue('/DIRAC/Extensions', []) self.__csExt = [] - for iP in range(len(exts)): - ext = exts[iP] + for ext in exts: if ext.endswith("DIRAC"): ext = ext[:-5] # If the extension is now "" (i.e. vanilla DIRAC), don't include it @@ -56,9 +54,9 @@ def getCSExtensions(self): self.__csExt.append(ext) return self.__csExt + @deprecated("Use DIRAC.Core.Utilities.Extensions.extensionsByPriority instead") def getInstalledExtensions(self): - self.__load() - return list(self.__orderedExtNames) + return extensionsByPriority() def getExtensionPath(self, extName): self.__load() @@ -90,11 +88,12 @@ def getCSExtensions(): return Extensions().getCSExtensions() +@deprecated("Use DIRAC.Core.Utilities.Extensions.extensionsByPriority instead") def getInstalledExtensions(): """ Return list of extensions registered in the CS and available in local installation """ - return Extensions().getInstalledExtensions() + return extensionsByPriority() def skipCACheck(): diff --git a/src/DIRAC/ConfigurationSystem/Client/LocalConfiguration.py b/src/DIRAC/ConfigurationSystem/Client/LocalConfiguration.py index 267e18acb86..34e1f98b10b 100755 --- a/src/DIRAC/ConfigurationSystem/Client/LocalConfiguration.py +++ b/src/DIRAC/ConfigurationSystem/Client/LocalConfiguration.py @@ -404,7 +404,7 @@ def enableCS(self): """ Force the connection the Configuration Server - (And incidentaly reinitialize the ObjectLoader and logger) + (And incidentally reinitialize the ObjectLoader and logger) """ res = gRefresher.enable() diff --git a/src/DIRAC/Core/Base/private/ModuleLoader.py b/src/DIRAC/Core/Base/private/ModuleLoader.py index 2a520313757..3f89d8b6b5a 100644 --- a/src/DIRAC/Core/Base/private/ModuleLoader.py +++ b/src/DIRAC/Core/Base/private/ModuleLoader.py @@ -7,13 +7,11 @@ import os from DIRAC.Core.Utilities import List from DIRAC import gConfig, S_ERROR, S_OK, gLogger -from DIRAC.ConfigurationSystem.Client.Helpers import getInstalledExtensions from DIRAC.ConfigurationSystem.Client import PathFinder -from DIRAC.Core.Utilities.Extensions import recurseImport +from DIRAC.Core.Utilities.Extensions import extensionsByPriority, recurseImport class ModuleLoader(object): - def __init__(self, importLocation, sectionFinder, superClass, csSuffix=False, moduleSuffix=False): self.__modules = {} self.__loadedModules = {} @@ -42,7 +40,7 @@ def loadModules(self, modulesList, hideExceptions=False): for modName in modulesList: gLogger.verbose("Checking %s" % modName) # if it's a executor modName name just load it and be done with it - if modName.find("/") > -1: + if "/" in modName: gLogger.verbose("Module %s seems to be a valid name. Try to load it!" % modName) result = self.loadModule(modName, hideExceptions=hideExceptions) if not result['OK']: @@ -63,7 +61,7 @@ def loadModules(self, modulesList, hideExceptions=False): return result # Look what is installed parentModule = None - for rootModule in getInstalledExtensions(): + for rootModule in extensionsByPriority(): if not system.endswith("System"): system += "System" parentImport = "%s.%s.%s" % (rootModule, system, self.__csSuffix) @@ -79,7 +77,7 @@ def loadModules(self, modulesList, hideExceptions=False): parentPath = parentModule.__path__[0] gLogger.notice("Found modules path at %s" % parentImport) for entry in os.listdir(parentPath): - if entry[-3:] != ".py" or entry == "__init__.py": + if entry == "__init__.py" or not entry.endswith(".py"): continue if not os.path.isfile(os.path.join(parentPath, entry)): continue @@ -109,7 +107,7 @@ def loadModule(self, modName, hideExceptions=False, parentModule=False): if loadGroup: gLogger.info("Found load group %s. Will load %s" % (modName, ", ".join(loadGroup))) for loadModName in loadGroup: - if loadModName.find("/") == -1: + if "/" not in loadModName: loadModName = "%s/%s" % (modList[0], loadModName) result = self.loadModule(loadModName, hideExceptions=hideExceptions) if not result['OK']: @@ -121,7 +119,7 @@ def loadModule(self, modName, hideExceptions=False, parentModule=False): loadName = modName gLogger.info("Loading %s" % (modName)) else: - if loadName.find("/") == -1: + if "/" not in loadName: loadName = "%s/%s" % (modList[0], loadName) gLogger.info("Loading %s (%s)" % (modName, loadName)) # If already loaded, skip @@ -141,7 +139,7 @@ def loadModule(self, modName, hideExceptions=False, parentModule=False): gLogger.info("Trying to %s from CS defined path %s" % (loadName, handlerPath)) gLogger.verbose("Found handler for %s: %s" % (loadName, handlerPath)) handlerPath = handlerPath.replace("/", ".") - if handlerPath.find(".py", len(handlerPath) - 3) > -1: + if handlerPath.endswith(".py"): handlerPath = handlerPath[:-3] className = List.fromChar(handlerPath, ".")[-1] result = recurseImport(handlerPath) @@ -158,7 +156,7 @@ def loadModule(self, modName, hideExceptions=False, parentModule=False): else: # Check to see if the module exists in any of the root modules gLogger.info("Trying to autodiscover %s" % loadName) - rootModulesToLook = getInstalledExtensions() + rootModulesToLook = extensionsByPriority() for rootModule in rootModulesToLook: importString = '%s.%sSystem.%s.%s' % (rootModule, system, self.__importLocation, module) if self.__modSuffix: diff --git a/src/DIRAC/Core/DISET/private/MessageFactory.py b/src/DIRAC/Core/DISET/private/MessageFactory.py index 807b94f4db4..5c211f601d9 100644 --- a/src/DIRAC/Core/DISET/private/MessageFactory.py +++ b/src/DIRAC/Core/DISET/private/MessageFactory.py @@ -7,7 +7,7 @@ from DIRAC import S_OK, S_ERROR from DIRAC.FrameworkSystem.Client.Logger import gLogger from DIRAC.Core.Utilities import List -from DIRAC.ConfigurationSystem.Client.Helpers import CSGlobals +from DIRAC.Core.Utilities.Extensions import extensionsByPriority class MessageFactory(object): @@ -256,10 +256,9 @@ def loadObjects(path, reFilter=None, parentClass=None): reFilter = re.compile(r".*[a-z1-9]\.py$") pathList = List.fromChar(path, "/") - parentModuleList = ["%sDIRAC" % ext for ext in CSGlobals.getCSExtensions()] + ['DIRAC'] objectsToLoad = {} # Find which object files match - for parentModule in parentModuleList: + for parentModule in extensionsByPriority(): objDir = os.path.join(DIRAC.rootPath, parentModule, *pathList) if not os.path.isdir(objDir): continue diff --git a/src/DIRAC/Core/Utilities/DErrno.py b/src/DIRAC/Core/Utilities/DErrno.py index 4bbc098237e..ea0cdf23cc4 100644 --- a/src/DIRAC/Core/Utilities/DErrno.py +++ b/src/DIRAC/Core/Utilities/DErrno.py @@ -46,6 +46,8 @@ import importlib import sys +from DIRAC.Core.Utilities.Extensions import extensionsByPriority + # To avoid conflict, the error numbers should be greater than 1000 # We decided to group the by range of 100 per system @@ -344,11 +346,11 @@ def includeExtensionErrors(): Should be called only at the initialization of DIRAC, so by the parseCommandLine, dirac-agent.py, dirac-service.py, dirac-executor.py """ - from DIRAC.ConfigurationSystem.Client.Helpers import CSGlobals - - for extension in CSGlobals.getCSExtensions(): + for extension in reversed(extensionsByPriority()): + if extension == "DIRAC": + continue try: - ext_derrno = importlib.import_module('%sDIRAC.Core.Utilities.DErrno' % extension) + ext_derrno = importlib.import_module('%s.Core.Utilities.DErrno' % extension) except ImportError: pass else: diff --git a/src/DIRAC/Core/Utilities/ObjectLoader.py b/src/DIRAC/Core/Utilities/ObjectLoader.py index 4397927a5fb..0ca54b2a852 100644 --- a/src/DIRAC/Core/Utilities/ObjectLoader.py +++ b/src/DIRAC/Core/Utilities/ObjectLoader.py @@ -13,9 +13,8 @@ from DIRAC import gLogger, S_OK, S_ERROR from DIRAC.Core.Utilities import DErrno -from DIRAC.Core.Utilities import List, DIRACSingleton -from DIRAC.Core.Utilities.Extensions import recurseImport -from DIRAC.ConfigurationSystem.Client.Helpers import CSGlobals +from DIRAC.Core.Utilities import DIRACSingleton +from DIRAC.Core.Utilities.Extensions import extensionsByPriority, recurseImport @six.add_metaclass(DIRACSingleton.DIRACSingleton) @@ -70,23 +69,19 @@ def __rootImport(self, modName, hideExceptions=False): impName = "%s.%s" % (rootModule, impName) gLogger.debug("Trying to load %s" % impName) result = recurseImport(impName, hideExceptions=hideExceptions) - # Error. Something cannot be imported. Return error if not result['OK']: return result - # Huge success! if result['Value']: return S_OK((impName, result['Value'])) - # Nothing found, continue - # Return nothing found return S_OK() def __generateRootModules(self, baseModules): """ Iterate over all the possible root modules """ self.__rootModules = baseModules - for rootModule in reversed(CSGlobals.getCSExtensions()): - if not rootModule.endswith("DIRAC") and rootModule not in self.__rootModules: - self.__rootModules.append("%sDIRAC" % rootModule) + for rootModule in reversed(extensionsByPriority()): + if rootModule not in self.__rootModules: + self.__rootModules.append(rootModule) self.__rootModules.append("") # Reversing the order because we want first to look in the extension(s) @@ -130,20 +125,14 @@ def getObjects(self, modulePath, reFilter=None, parentClass=None, recurse=False, :param continueOnError: if True, continue loading further module even if one fails """ - - if 'OrderedDict' in dir(collections): - modules = collections.OrderedDict() - else: - modules = {} - + modules = collections.OrderedDict() if isinstance(reFilter, six.string_types): reFilter = re.compile(reFilter) for rootModule in self.__rootModules: + impPath = modulePath if rootModule: - impPath = "%s.%s" % (rootModule, modulePath) - else: - impPath = modulePath + impPath = "%s.%s" % (rootModule, impPath) gLogger.debug("Trying to load %s" % impPath) result = recurseImport(impPath) @@ -151,10 +140,8 @@ def getObjects(self, modulePath, reFilter=None, parentClass=None, recurse=False, return result if not result['Value']: continue - parentModule = result['Value'] - fsPath = parentModule.__path__[0] - gLogger.verbose("Loaded module %s at %s" % (impPath, fsPath)) + gLogger.verbose("Loaded module %s at %s" % (impPath, parentModule.__path__)) for _modLoader, modName, isPkg in pkgutil.walk_packages(parentModule.__path__): if reFilter and not reFilter.match(modName): @@ -171,7 +158,7 @@ def getObjects(self, modulePath, reFilter=None, parentClass=None, recurse=False, if modKeyName in modules: continue fullName = "%s.%s" % (impPath, modName) - result = recurseImport(modName, parentModule=parentModule, fullName=fullName) + result = recurseImport(fullName) if not result['OK']: if continueOnError: gLogger.error("Error loading module but continueOnError is true", "module %s error %s" % (fullName, result)) @@ -179,18 +166,15 @@ def getObjects(self, modulePath, reFilter=None, parentClass=None, recurse=False, return result if not result['Value']: continue - modObj = result['Value'] - try: - modClass = getattr(modObj, modName) - except AttributeError: + modClass = getattr(result['Value'], modName, None) + if not modClass: gLogger.warn("%s does not contain a %s object" % (fullName, modName)) continue if parentClass and not issubclass(modClass, parentClass): continue - # Huge success! modules[modKeyName] = modClass return S_OK(modules) diff --git a/src/DIRAC/Core/Utilities/Plotting/ObjectLoader.py b/src/DIRAC/Core/Utilities/Plotting/ObjectLoader.py index 96993820a88..d190d88cd03 100644 --- a/src/DIRAC/Core/Utilities/Plotting/ObjectLoader.py +++ b/src/DIRAC/Core/Utilities/Plotting/ObjectLoader.py @@ -10,7 +10,7 @@ import DIRAC from DIRAC import gLogger from DIRAC.Core.Utilities import List -from DIRAC.ConfigurationSystem.Client.Helpers import CSGlobals +from DIRAC.Core.Utilities.Extensions import extensionsByPriority def loadObjects(path, reFilter=None, parentClass=None): @@ -24,10 +24,9 @@ def loadObjects(path, reFilter=None, parentClass=None): reFilter = re.compile(r".*[a-z1-9]\.py$") pathList = List.fromChar(path, "/") - parentModuleList = ["%sDIRAC" % ext for ext in CSGlobals.getCSExtensions()] + ['DIRAC'] objectsToLoad = {} # Find which object files match - for parentModule in parentModuleList: + for parentModule in extensionsByPriority(): if six.PY3: objDir = os.path.join(os.path.dirname(os.path.dirname(DIRAC.__file__)), parentModule, *pathList) else: diff --git a/src/DIRAC/Core/Utilities/Version.py b/src/DIRAC/Core/Utilities/Version.py index 12fc51ffb7a..1ea925e9403 100644 --- a/src/DIRAC/Core/Utilities/Version.py +++ b/src/DIRAC/Core/Utilities/Version.py @@ -3,50 +3,34 @@ from __future__ import print_function __RCSID__ = "$Id$" -import DIRAC +import importlib from DIRAC import S_OK -from DIRAC.ConfigurationSystem.Client.Helpers import getCSExtensions +from DIRAC.Core.Utilities.Extensions import extensionsByPriority def getCurrentVersion(): """ Get a string corresponding to the current version of the DIRAC package and all the installed extension packages """ - - version = 'DIRAC ' + DIRAC.version - - for ext in getCSExtensions(): + for ext in extensionsByPriority(): try: - import imp - module = imp.find_module("%sDIRAC" % ext) - extModule = imp.load_module("%sDIRAC" % ext, *module) - version = extModule.version - except ImportError: + return S_OK(importlib.import_module(ext).version) + except (ImportError, AttributeError): pass - except AttributeError: - pass - - return S_OK(version) def getVersion(): """ Get a dictionary corresponding to the current version of the DIRAC package and all the installed extension packages """ - vDict = {'Extensions': {}} - vDict['DIRAC'] = DIRAC.version - - for ext in getCSExtensions(): + for ext in extensionsByPriority(): try: - import imp - module = imp.find_module("%sDIRAC" % ext) - extModule = imp.load_module("%sDIRAC" % ext, *module) - vDict['Extensions'][ext] = extModule.version - except ImportError: + version = importlib.import_module(ext).version + except (ImportError, AttributeError): pass - except AttributeError: - pass - + if ext.endswith("DIRAC") and ext != "DIRAC": + ext = ext[:len("DIRAC")] + vDict['Extensions'][ext] = version return S_OK(vDict) diff --git a/src/DIRAC/Core/Utilities/test/Test_ObjectLoader.py b/src/DIRAC/Core/Utilities/test/Test_ObjectLoader.py index 292f76d63a4..abd40502b3b 100644 --- a/src/DIRAC/Core/Utilities/test/Test_ObjectLoader.py +++ b/src/DIRAC/Core/Utilities/test/Test_ObjectLoader.py @@ -1,36 +1,20 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function -import unittest from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader from DIRAC.Core.DISET.RequestHandler import RequestHandler -class ObjectLoaderMainSuccessScenario(unittest.TestCase): +def _check(result): + assert result["OK"], result['Message'] + return result['Value'] - def setUp(self): - self.ol = ObjectLoader() - def __check(self, result): - if not result['OK']: - self.fail(result['Message']) - return result['Value'] - - def test_load(self): - self.__check(self.ol.loadObject("Core.Utilities.List", 'fromChar')) - self.__check(self.ol.loadObject("Core.Utilities.ObjectLoader", "ObjectLoader")) - dataFilter = self.__check(self.ol.getObjects("WorkloadManagementSystem.Service", ".*Handler")) - dataClass = self.__check(self.ol.getObjects("WorkloadManagementSystem.Service", parentClass=RequestHandler)) - self.assertEqual(sorted(dataFilter), sorted(dataClass)) - -############################################################################# -# Test Suite run -############################################################################# - - -if __name__ == '__main__': - suite = unittest.defaultTestLoader.loadTestsFromTestCase(ObjectLoaderMainSuccessScenario) - testResult = unittest.TextTestRunner(verbosity=2).run(suite) - -# EOF#EOF#EOF#EOF#EOF#EOF#EOF#EOF#EOF#EOF#EOF#EOF#EOF#EOF#EOF#EOF#EOF#EOF#EOF# +def test_load(): + _check(ObjectLoader().loadObject("Core.Utilities.List", 'fromChar')) + _check(ObjectLoader().loadObject("Core.Utilities.ObjectLoader", "ObjectLoader")) + assert _check(ObjectLoader().loadObject("Core.Utilities.ObjectLoader")) is ObjectLoader + dataFilter = _check(ObjectLoader().getObjects("WorkloadManagementSystem.Service", ".*Handler")) + dataClass = _check(ObjectLoader().getObjects("WorkloadManagementSystem.Service", parentClass=RequestHandler)) + assert sorted(dataFilter) == sorted(dataClass) diff --git a/src/DIRAC/Core/scripts/dirac_configure.py b/src/DIRAC/Core/scripts/dirac_configure.py index 6aa5194c8fd..7a3dd96ee74 100755 --- a/src/DIRAC/Core/scripts/dirac_configure.py +++ b/src/DIRAC/Core/scripts/dirac_configure.py @@ -245,11 +245,11 @@ def runConfigurationWizard(params): """Interactively configure DIRAC using metadata from installed extensions""" import subprocess from prompt_toolkit import prompt, print_formatted_text, HTML - from DIRAC.Core.Utilities.DIRACScript import _extensionsByPriority, _getExtensionMetadata + from DIRAC.Core.Utilities.Extensions import extensionsByPriority, getExtensionMetadata - extensions = _extensionsByPriority() + extensions = extensionsByPriority() - extensionMetadata = _getExtensionMetadata(extensions[-1]) + extensionMetadata = getExtensionMetadata(extensions[-1]) defaultSetup = extensionMetadata.get("default_setup", "") setups = extensionMetadata.get("setups", {}) diff --git a/src/DIRAC/Core/scripts/dirac_install_db.py b/src/DIRAC/Core/scripts/dirac_install_db.py index 519a2763b10..8c08f6227c8 100755 --- a/src/DIRAC/Core/scripts/dirac_install_db.py +++ b/src/DIRAC/Core/scripts/dirac_install_db.py @@ -37,14 +37,14 @@ def main(): result = gComponentInstaller.installDatabase(db) if not result['OK']: print("ERROR: failed to correctly install %s" % db, result['Message']) - else: - extension, system = result['Value'] - gComponentInstaller.addDatabaseOptionsToCS(gConfig, system, db, overwrite=True) - - if db != 'InstalledComponentsDB': - result = MonitoringUtilities.monitorInstallation('DB', system, db) - if not result['OK']: - print("ERROR: failed to register installation in database: %s" % result['Message']) + continue + extension, system = result['Value'] + gComponentInstaller.addDatabaseOptionsToCS(gConfig, system, db, overwrite=True) + + if db != 'InstalledComponentsDB': + result = MonitoringUtilities.monitorInstallation('DB', system, db) + if not result['OK']: + print("ERROR: failed to register installation in database: %s" % result['Message']) if __name__ == "__main__": diff --git a/src/DIRAC/DataManagementSystem/DB/FileCatalogDB.py b/src/DIRAC/DataManagementSystem/DB/FileCatalogDB.py index 9423679038f..37c47ea1452 100755 --- a/src/DIRAC/DataManagementSystem/DB/FileCatalogDB.py +++ b/src/DIRAC/DataManagementSystem/DB/FileCatalogDB.py @@ -18,13 +18,10 @@ class FileCatalogDB(DB): def __init__(self, databaseLocation='DataManagement/FileCatalogDB'): - """ Standard Constructor - """ - # The database location can be specified in System/Database form or in just the Database name # in the DataManagement system db = databaseLocation - if db.find('/') == -1: + if "/" not in db: db = 'DataManagement/' + db super(FileCatalogDB, self).__init__('FileCatalogDB', db) @@ -37,10 +34,8 @@ def __init__(self, databaseLocation='DataManagement/FileCatalogDB'): self.dmeta = None self.fmeta = None self.datasetManager = None - self.objectLoader = None def setConfig(self, databaseConfig): - self.directories = {} # In memory storage of the various parameters self.users = {} @@ -63,9 +58,6 @@ def setConfig(self, databaseConfig): self.visibleFileStatus = databaseConfig['VisibleFileStatus'] self.visibleReplicaStatus = databaseConfig['VisibleReplicaStatus'] - # Obtain the plugins to be used for DB interaction - self.objectLoader = ObjectLoader() - # Load the configured components for compAttribute, componentType in [("ugManager", "UserGroupManager"), ("seManager", "SEManager"), @@ -87,7 +79,7 @@ def __loadCatalogComponent(self, componentType, componentName): """ Create an object of a given catalog component """ componentModule = 'DataManagementSystem.DB.FileCatalogComponents.%s.%s' % (componentType, componentName) - result = self.objectLoader.loadObject(componentModule, componentName) + result = ObjectLoader().loadObject(componentModule) if not result['OK']: gLogger.error('Failed to load catalog component', '%s: %s' % (componentName, result['Message'])) return result diff --git a/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py b/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py index 945854be5f5..cc7be0b5e4c 100644 --- a/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py +++ b/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py @@ -60,19 +60,22 @@ __RCSID__ = "$Id$" -import os +import getpass +import glob +import importlib +import inspect import io +import os +import pkgutil import re -import glob +import shutil import stat import time -import subprocess32 as subprocess -import shutil -import inspect -import importlib +from collections import defaultdict +import importlib_resources +import subprocess32 as subprocess from diraccfg import CFG -import six import DIRAC from DIRAC import rootPath @@ -99,11 +102,48 @@ from DIRAC.Core.DISET.RequestHandler import RequestHandler from DIRAC.Core.Utilities.Decorators import deprecated from DIRAC.Core.Utilities.PrettyPrint import printTable -from DIRAC.Core.Utilities.Extensions import extensionsByPriority, findDatabases, findModules, findSystems +from DIRAC.Core.Utilities.Extensions import ( + extensionsByPriority, findDatabases, findModules, findAgents, findServices, + findExecutors, findSystems, +) __RCSID__ = "$Id$" +def _safeFloat(value): + try: + return float(value) + except ValueError: + return -1 + + +def _safeInt(value): + try: + return int(value) + except ValueError: + return -1 + + +def _makeComponentDict(component, setupDict, installedDict, compType, system, runitDict): + componentDict = { + 'Setup': component in setupDict[compType][system], + 'Installed': component in installedDict[compType][system], + 'RunitStatus': 'Unknown', + 'Timeup': 0, + 'PID': 0, + } + compDir = system + '_' + component + if compDir in runitDict: + componentDict['RunitStatus'] = runitDict[compDir]['RunitStatus'] + componentDict['Timeup'] = runitDict[compDir]['Timeup'] + componentDict['PID'] = _safeInt(runitDict[compDir].get('PID', -1)) + componentDict['CPU'] = _safeFloat(runitDict[compDir].get('CPU', -1)) + componentDict['MEM'] = _safeFloat(runitDict[compDir].get('MEM', -1)) + componentDict['RSS'] = _safeFloat(runitDict[compDir].get('RSS', -1)) + componentDict['VSZ'] = _safeFloat(runitDict[compDir].get('VSZ', -1)) + return componentDict + + class ComponentInstaller(object): def __init__(self): @@ -146,6 +186,15 @@ def __init__(self): self.loadDiracCfg() + def resultIndexes(self, componentTypes): + resultIndexes = {} + for cType in componentTypes: + result = self._getSectionName(cType) + if not result['OK']: + return result + resultIndexes[cType] = result['Value'] + return S_OK(resultIndexes) + def loadDiracCfg(self): """ Read again defaults from dirac.cfg """ @@ -274,10 +323,7 @@ def getInfo(self): if not result['OK']: return result rDict = result['Value'] - if self.setup: - rDict['Setup'] = self.setup - else: - rDict['Setup'] = 'Unknown' + rDict['Setup'] = self.setup or 'Unknown' return S_OK(rDict) @deprecated("Use DIRAC.Core.Utilities.Extensions.extensionsByPriority instead") @@ -298,10 +344,7 @@ def _addCfgToDiracCfg(self, cfg): """ Merge cfg into existing dirac.cfg file """ - if str(self.localCfg): - newCfg = self.localCfg.mergeWith(cfg) - else: - newCfg = cfg + newCfg = self.localCfg.mergeWith(cfg) if str(self.localCfg) else cfg result = newCfg.writeToFile(self.cfgFile) if not result: return result @@ -341,10 +384,7 @@ def _addCfgToLocalCS(self, cfg): csFile = os.path.join(rootPath, 'etc', '%s.cfg' % csName) if os.path.exists(csFile): csCfg.loadFromFile(csFile) - if str(csCfg): - newCfg = csCfg.mergeWith(cfg) - else: - newCfg = cfg + newCfg = csCfg.mergeWith(cfg) if str(csCfg) else cfg return newCfg.writeToFile(csFile) def _removeOptionFromCS(self, path): @@ -392,11 +432,7 @@ def _getCentralCfg(self, installCfg): if vo: centralCfg['DIRAC'].addKey('VirtualOrganization', vo, '') # pylint: disable=no-member - for section in ['Systems', - 'Resources', - 'Resources/Sites', - 'Operations', - 'Registry']: + for section in ['Systems', 'Resources', 'Resources/Sites', 'Operations', 'Registry']: if installCfg.isSection(section): centralCfg.createNewSection(section, contents=installCfg[section]) @@ -580,10 +616,7 @@ def removeComponentOptionsFromCS(self, system, component, mySetup=None): cType = installation['Component']['Type'] # Is the component a rename of another module? - if installation['Instance'] == installation['Component']['DIRACModule']: - isRenamed = False - else: - isRenamed = True + isRenamed = installation['Instance'] != installation['Component']['DIRACModule'] result = self.monitoringClient.getInstallations( {'UnInstallationTime': None}, @@ -615,8 +648,8 @@ def removeComponentOptionsFromCS(self, system, component, mySetup=None): if not result['OK']: # It is maybe in the FailoverURLs ? result = self._removeOptionFromCS(cfgPath('Systems', system, compInstance, 'FailoverURLs', component)) - if not result['OK']: - return result + if not result['OK']: + return result if removeMain: result = self._removeSectionFromCS(cfgPath('Systems', system, @@ -637,14 +670,10 @@ def removeComponentOptionsFromCS(self, system, component, mySetup=None): if not result['OK']: # it is maybe in the FailoverURLs ? result = self._removeOptionFromCS( - cfgPath( - 'Systems', - system, - compInstance, - 'FailoverURLs', - installation['Component']['Module'])) - if not result['OK']: - return result + cfgPath('Systems', system, compInstance, 'FailoverURLs', installation['Component']['Module']) + ) + if not result['OK']: + return result return S_OK('Successfully removed entries from CS') return S_OK('Instances of this component still exist. It won\'t be completely removed') @@ -777,15 +806,10 @@ def getComponentCfg(self, componentType, system, component, compInstance, extens return result sectionName = result['Value'] - componentModule = component - if "Module" in specialOptions and specialOptions['Module']: - componentModule = specialOptions['Module'] - + componentModule = specialOptions.get('Module', component) compCfg = CFG() - if addDefaultOptions: - extensionsDIRAC = [x + 'DIRAC' for x in extensions] + extensions - for ext in extensionsDIRAC + ['DIRAC']: + for ext in extensions: cfgTemplateModule = "%s.%sSystem" % (ext, system) try: cfgTemplate = importlib_resources.read_text(cfgTemplateModule, "ConfigTemplate.cfg") @@ -982,94 +1006,47 @@ def getSoftwareComponents(self, extensions): is installed on the system """ # The Gateway does not need a handler - services = {'Framework': ['Gateway']} - agents = {} - executors = {} - remainders = {} + services = defaultdict(list, {"Framework": ["Gateway"]}) + agents = defaultdict(list) + executors = defaultdict(list) + remainders = defaultdict(lambda: defaultdict(list)) - resultDict = {} - - remainingTypes = [cType for cType in self.componentTypes if cType not in ['service', 'agent', 'executor']] - resultIndexes = {} # Components other than services, agents and executors - for cType in remainingTypes: - result = self._getSectionName(cType) - if not result['OK']: - return result - resultIndexes[cType] = result['Value'] - resultDict[resultIndexes[cType]] = {} - remainders[cType] = {} - - for extension in ['DIRAC'] + [x + 'DIRAC' for x in extensions]: - import importlib - try: - extensionModule = importlib.import_module(extension) - except ImportError: - # Not all the extensions are necessarily installed in this self.instance - continue - extensionDir = os.path.dirname(extensionModule.__file__) - systemList = os.listdir(extensionDir) - for sys in systemList: - system = sys.replace('System', '') - try: - agentDir = os.path.join(extensionDir, sys, 'Agent') - agentList = os.listdir(agentDir) - for agent in agentList: - if os.path.splitext(agent)[1] == ".py": - agentFile = os.path.join(agentDir, agent) - with io.open(agentFile, 'rt') as afile: - body = afile.read() - if body.find('AgentModule') != -1 or body.find('OptimizerModule') != -1: - if system not in agents: - agents[system] = [] - agents[system].append(agent.replace('.py', '')) - except OSError: - pass - try: - serviceDir = os.path.join(extensionDir, sys, 'Service') - serviceList = os.listdir(serviceDir) - for service in serviceList: - if service.find('Handler') != -1 and os.path.splitext(service)[1] == '.py': - if system not in services: - services[system] = [] - if system == 'Configuration' and service == 'ConfigurationHandler.py': - service = 'ServerHandler.py' - services[system].append(service.replace('.py', '').replace('Handler', '')) - except OSError: - pass - try: - executorDir = os.path.join(extensionDir, sys, 'Executor') - executorList = os.listdir(executorDir) - for executor in executorList: - if os.path.splitext(executor)[1] == ".py": - executorFile = os.path.join(executorDir, executor) - with io.open(executorFile, 'rt') as afile: - body = afile.read() - if body.find('OptimizerExecutor') != -1: - if system not in executors: - executors[system] = [] - executors[system].append(executor.replace('.py', '')) - except OSError: - pass - - # Rest of component types - for cType in remainingTypes: - try: - remainDir = os.path.join(extensionDir, sys, cType.title()) - remainList = os.listdir(remainDir) - for remainder in remainList: - if os.path.splitext(remainder)[1] == ".py": - if system not in remainders[cType]: - remainders[cType][system] = [] - remainders[cType][system].append(remainder.replace('.py', '')) - except OSError: - pass - - resultDict['Services'] = services - resultDict['Agents'] = agents - resultDict['Executors'] = executors - for cType in remainingTypes: - resultDict[resultIndexes[cType]] = remainders[cType] + remainingTypes = set(self.componentTypes) - {'service', 'agent', 'executor'} + result = self.resultIndexes(remainingTypes) + if not result["OK"]: + return result + resultIndexes = result["Value"] + + for extension in extensions: + for system, agent in findAgents(extension): + loader = pkgutil.get_loader(".".join([extension, system, "Agent", agent])) + with io.open(loader.get_filename(), "rt") as fp: + body = fp.read() + if "AgentModule" in body or "OptimizerModule" in body: + agents[system.replace("System", "")].append(agent) + + for system, service in findServices(extension): + if system == "Configuration" and service == "ConfigurationHandler": + service = "ServerHandler" + services[system.replace("System", "")].append(service.replace("Handler", "")) + + for system, executor in findExecutors(extension): + loader = pkgutil.get_loader(".".join([extension, system, "Executor", executor])) + with io.open(loader.get_filename(), "rt") as fp: + body = fp.read() + if "OptimizerExecutor" in body: + executors[system.replace("System", "")].append(executor) + + # Rest of component types + for cType in remainingTypes: + for system, remainder in findModules(extension, cType.title()): + remainders[cType][system.replace("System", "")].append(remainder) + + resultDict = {resultIndexes[cType]: dict(remainders[cType]) for cType in remainingTypes} + resultDict["Services"] = dict(services) + resultDict["Agents"] = dict(agents) + resultDict["Executors"] = dict(executors) return S_OK(resultDict) def getInstalledComponents(self): @@ -1077,68 +1054,62 @@ def getInstalledComponents(self): Get the list of all the components ( services and agents ) installed on the system in the runit directory """ - resultDict = {} - resultIndexes = {} - for cType in self.componentTypes: - result = self._getSectionName(cType) - if not result['OK']: - return result - resultIndexes[cType] = result['Value'] - resultDict[resultIndexes[cType]] = {} + result = self.resultIndexes(self.componentTypes) + if not result["OK"]: + return result + resultIndexes = result["Value"] - systemList = os.listdir(self.runitDir) - for system in systemList: + resultDict = defaultdict(lambda: defaultdict(list)) + for system in os.listdir(self.runitDir): systemDir = os.path.join(self.runitDir, system) - components = os.listdir(systemDir) - for component in components: + for component in os.listdir(systemDir): + runFile = os.path.join(systemDir, component, 'run') try: - runFile = os.path.join(systemDir, component, 'run') with io.open(runFile, 'rt') as rFile: body = rFile.read() - - for cType in self.componentTypes: - if body.find('dirac-%s' % (cType)) != -1: - if system not in resultDict[resultIndexes[cType]]: - resultDict[resultIndexes[cType]][system] = [] - resultDict[resultIndexes[cType]][system].append(component) except IOError: pass + else: + for cType in self.componentTypes: + if 'dirac-%s' % (cType) in body: + resultDict[cType][system].append(component) - return S_OK(resultDict) + return S_OK({ + resultIndexes[cType]: dict(resultDict[cType]) + for cType in self.componentTypes + }) def getSetupComponents(self): """ Get the list of all the components ( services and agents ) set up for running with runsvdir in startup directory """ - resultDict = {} - resultIndexes = {} - for cType in self.componentTypes: - result = self._getSectionName(cType) - if not result['OK']: - return result - resultIndexes[cType] = result['Value'] - resultDict[resultIndexes[cType]] = {} - if not os.path.isdir(self.startDir): return S_ERROR('Startup Directory does not exit: %s' % self.startDir) - componentList = os.listdir(self.startDir) - for component in componentList: + + result = self.resultIndexes(self.componentTypes) + if not result["OK"]: + return result + resultIndexes = result["Value"] + + resultDict = defaultdict(lambda: defaultdict(list)) + for component in os.listdir(self.startDir): + runFile = os.path.join(self.startDir, component, 'run') try: - runFile = os.path.join(self.startDir, component, 'run') with io.open(runFile, 'rt') as rfile: body = rfile.read() - - for cType in self.componentTypes: - if body.find('dirac-%s' % (cType)) != -1: - system, compT = component.split('_', 1) - if system not in resultDict[resultIndexes[cType]]: - resultDict[resultIndexes[cType]][system] = [] - resultDict[resultIndexes[cType]][system].append(compT) except IOError: pass + else: + for cType in self.componentTypes: + if 'dirac-%s' % (cType) in body: + system, compT = component.split('_', 1) + resultDict[cType][system].append(compT) - return S_OK(resultDict) + return S_OK({ + resultIndexes[cType]: dict(resultDict[cType]) + for cType in self.componentTypes + }) def getStartupComponentStatus(self, componentTupleList): """ @@ -1262,119 +1233,33 @@ def getOverallStatus(self, extensions): runitDict = result['Value'] # Collect the info now - resultDict = {} - resultIndexes = {} - for cType in self.componentTypes: - result = self._getSectionName(cType) - if not result['OK']: - return result - resultIndexes[cType] = result['Value'] - resultDict[resultIndexes[cType]] = {} + result = self.resultIndexes(self.componentTypes) + if not result["OK"]: + return result + resultIndexes = result["Value"] - for compType in resultIndexes.values(): + resultDict = defaultdict(lambda: defaultdict(list)) + for cType in resultIndexes.values(): if 'Services' in softDict: - for system in softDict[compType]: - resultDict[compType][system] = {} - for component in softDict[compType][system]: - if system == 'Configuration' and component == 'Configuration': + for system in softDict[cType]: + for component in softDict[cType][system]: + if system == component == 'Configuration': # Fix to avoid missing CS due to different between Service name and Handler name component = 'Server' - resultDict[compType][system][component] = {} - resultDict[compType][system][component]['Setup'] = False - resultDict[compType][system][component]['Installed'] = False - resultDict[compType][system][component]['RunitStatus'] = 'Unknown' - resultDict[compType][system][component]['Timeup'] = 0 - resultDict[compType][system][component]['PID'] = 0 - # TODO: why do we need a try here? - try: - if component in setupDict[compType][system]: - resultDict[compType][system][component]['Setup'] = True - except Exception: - pass - try: - if component in installedDict[compType][system]: - resultDict[compType][system][component]['Installed'] = True - except Exception: - pass - try: - compDir = system + '_' + component - if compDir in runitDict: - resultDict[compType][system][component]['RunitStatus'] = runitDict[compDir]['RunitStatus'] - resultDict[compType][system][component]['Timeup'] = runitDict[compDir]['Timeup'] - try: - resultDict[compType][system][component]['PID'] = int(runitDict[compDir]['PID']) - except ValueError: - resultDict[compType][system][component]['PID'] = -1 - try: - resultDict[compType][system][component]['CPU'] = float(runitDict[compDir]['CPU']) - except ValueError: - resultDict[compType][system][component]['CPU'] = -1 - try: - resultDict[compType][system][component]['MEM'] = float(runitDict[compDir]['MEM']) - except ValueError: - resultDict[compType][system][component]['MEM'] = -1 - try: - resultDict[compType][system][component]['RSS'] = float(runitDict[compDir]['RSS']) - except ValueError: - resultDict[compType][system][component]['RSS'] = -1 - try: - resultDict[compType][system][component]['VSZ'] = float(runitDict[compDir]['VSZ']) - except ValueError: - resultDict[compType][system][component]['VSZ'] = -1 - except Exception: - # print str(x) - pass - + resultDict[cType][system][component] = _makeComponentDict( + component, setupDict, installedDict, cType, system, runitDict + ) # Installed components can be not the same as in the software list if 'Services' in installedDict: - for system in installedDict[compType]: - for component in installedDict[compType][system]: - if compType in resultDict: - if system in resultDict[compType]: - if component in resultDict[compType][system]: - continue - resultDict[compType][system][component] = {} - resultDict[compType][system][component]['Setup'] = False - resultDict[compType][system][component]['Installed'] = True - resultDict[compType][system][component]['RunitStatus'] = 'Unknown' - resultDict[compType][system][component]['Timeup'] = 0 - resultDict[compType][system][component]['PID'] = 0 - # TODO: why do we need a try here? - try: - if component in setupDict[compType][system]: - resultDict[compType][system][component]['Setup'] = True - except Exception: - pass - try: - compDir = system + '_' + component - if compDir in runitDict: - resultDict[compType][system][component]['RunitStatus'] = runitDict[compDir]['RunitStatus'] - resultDict[compType][system][component]['Timeup'] = runitDict[compDir]['Timeup'] - try: - resultDict[compType][system][component]['PID'] = int(runitDict[compDir]['PID']) - except ValueError: - resultDict[compType][system][component]['PID'] = -1 - try: - resultDict[compType][system][component]['CPU'] = float(runitDict[compDir]['CPU']) - except ValueError: - resultDict[compType][system][component]['CPU'] = -1 - try: - resultDict[compType][system][component]['MEM'] = float(runitDict[compDir]['MEM']) - except ValueError: - resultDict[compType][system][component]['MEM'] = -1 - try: - resultDict[compType][system][component]['RSS'] = float(runitDict[compDir]['RSS']) - except ValueError: - resultDict[compType][system][component]['RSS'] = -1 - try: - resultDict[compType][system][component]['VSZ'] = float(runitDict[compDir]['VSZ']) - except ValueError: - resultDict[compType][system][component]['VSZ'] = -1 - except Exception: - # print str(x) - pass + for system in installedDict[cType]: + for component in installedDict[cType][system]: + if component in resultDict.get(cType, {}).get(system, {}): + continue + resultDict[cType][system][component] = _makeComponentDict( + component, setupDict, installedDict, cType, system, runitDict + ) - return S_OK(resultDict) + return S_OK({k: dict(v) for k, v in resultDict.items()}) def checkComponentModule(self, componentType, system, module): """ @@ -1463,11 +1348,7 @@ def getLogTail(self, system, component, length=100): else: with io.open(logFileName, 'rt') as logFile: lines = [line.strip() for line in logFile.readlines()] - - if len(lines) < length: - retDict[compName] = '\n'.join(lines) - else: - retDict[compName] = '\n'.join(lines[-length:]) + retDict[compName] = '\n'.join(lines[-length:]) return S_OK(retDict) @@ -1515,12 +1396,8 @@ def setupSite(self, scriptCfg, cfg=None): setupAddConfiguration = self.localCfg.getOption(cfgInstallPath('AddConfiguration'), True) for serviceTuple in setupServices: - error = '' if len(serviceTuple) != 2: error = 'Wrong service specification: system/service' - # elif serviceTuple[0] not in setupSystems: - # error = 'System %s not available' % serviceTuple[0] - if error: if self.exitOnError: gLogger.error(error) DIRAC.exit(-1) @@ -1530,12 +1407,8 @@ def setupSite(self, scriptCfg, cfg=None): setupSystems.append(serviceSysInstance) for agentTuple in setupAgents: - error = '' if len(agentTuple) != 2: error = 'Wrong agent specification: system/agent' - # elif agentTuple[0] not in setupSystems: - # error = 'System %s not available' % agentTuple[0] - if error: if self.exitOnError: gLogger.error(error) DIRAC.exit(-1) @@ -1545,10 +1418,8 @@ def setupSite(self, scriptCfg, cfg=None): setupSystems.append(agentSysInstance) for executorTuple in setupExecutors: - error = '' if len(executorTuple) != 2: error = 'Wrong executor specification: system/executor' - if error: if self.exitOnError: gLogger.error(error) DIRAC.exit(-1) @@ -1558,10 +1429,7 @@ def setupSite(self, scriptCfg, cfg=None): setupSystems.append(executorSysInstance) # And to find out the available extensions - result = self.getExtensions() - if not result['OK']: - return result - extensions = [k.replace('DIRAC', '') for k in result['Value']] + extensions = extensionsByPriority() # Make sure the necessary directories are there if self.basePath != self.instancePath: @@ -1594,9 +1462,7 @@ def setupSite(self, scriptCfg, cfg=None): # it is pointless to look for more detailed command. # Nobody uses runsvdir.... so if it is there, it is us. - cmdFound = any(['runsvdir' in process for process in processList]) - - if not cmdFound: + if all('runsvdir' not in process for process in processList): gLogger.notice('Starting runsvdir ...') with io.open(os.devnull, 'w') as devnull: subprocess.Popen(['nohup', 'runsvdir', self.startDir, 'log: DIRAC runsv'], @@ -1606,6 +1472,7 @@ def setupSite(self, scriptCfg, cfg=None): # This server hosts the Master of the CS from DIRAC.ConfigurationSystem.Client.ConfigurationData import gConfigurationData gLogger.notice('Installing Master Configuration Server') + cfg = self.__getCfg(cfgPath('DIRAC', 'Setups', self.setup), 'Configuration', self.instance) self._addCfgToDiracCfg(cfg) cfg = self.__getCfg(cfgPath('DIRAC', 'Configuration'), 'Master', 'yes') @@ -1631,19 +1498,15 @@ def setupSite(self, scriptCfg, cfg=None): if not result['OK']: if self.exitOnError: DIRAC.exit(-1) - else: - return result + return result compCfg = result['Value'] cfg = cfg.mergeWith(compCfg) gConfigurationData.mergeWithLocal(cfg) - self.addDefaultOptionsToComponentCfg('service', 'Configuration', 'Server', []) - if installCfg: - centralCfg = self._getCentralCfg(installCfg) - else: - centralCfg = self._getCentralCfg(self.localCfg) + self.addDefaultOptionsToComponentCfg('service', 'Configuration', 'Server', ["DIRAC"]) + centralCfg = self._getCentralCfg(installCfg or self.localCfg) self._addCfgToLocalCS(centralCfg) - self.setupComponent('service', 'Configuration', 'Server', [], checkModule=False) + self.setupComponent('service', 'Configuration', 'Server', ["DIRAC"], checkModule=False) self.runsvctrlComponent('Configuration', 'Server', 't') while ['Configuration', 'Server'] in setupServices: @@ -1721,7 +1584,7 @@ def setupSite(self, scriptCfg, cfg=None): DIRAC.exit(-1) return result installedDatabases = result['Value'] - result = self.getAvailableDatabases(CSGlobals.getCSExtensions()) + result = self.getAvailableDatabases() gLogger.debug("Available databases", result) if not result['OK']: return result @@ -1744,21 +1607,19 @@ def setupSite(self, scriptCfg, cfg=None): if not result['OK']: gLogger.error('Database %s CS registration failed: %s' % (dbName, result['Message'])) - if self.mysqlPassword: - if not self._addMySQLToDiracCfg(): - error = 'Failed to add MySQL user/password to local configuration' - if self.exitOnError: - gLogger.error(error) - DIRAC.exit(-1) - return S_ERROR(error) + if self.mysqlPassword and not self._addMySQLToDiracCfg(): + error = 'Failed to add MySQL user/password to local configuration' + if self.exitOnError: + gLogger.error(error) + DIRAC.exit(-1) + return S_ERROR(error) - if self.noSQLHost: - if not self._addNoSQLToDiracCfg(): - error = 'Failed to add NoSQL connection details to local configuration' - if self.exitOnError: - gLogger.error(error) - DIRAC.exit(-1) - return S_ERROR(error) + if self.noSQLHost and not self._addNoSQLToDiracCfg(): + error = 'Failed to add NoSQL connection details to local configuration' + if self.exitOnError: + gLogger.error(error) + DIRAC.exit(-1) + return S_ERROR(error) # 3.- Then installed requested services for system, service in setupServices: @@ -1846,18 +1707,16 @@ def installComponent(self, componentType, system, component, extensions, compone # Any "Load" or "Module" option in the configuration defining what modules the given "component" # needs to load will be taken care of by self.checkComponentModule. if checkModule: - cModule = componentModule - if not cModule: - cModule = component + cModule = componentModule or component result = self.checkComponentModule(componentType, system, cModule) - if not result['OK']: - if not self.checkComponentSoftware(componentType, system, cModule, extensions)[ - 'OK'] and componentType != 'executor': - error = 'Software for %s %s/%s is not installed' % (componentType, system, component) - if self.exitOnError: - gLogger.error(error) - DIRAC.exit(-1) - return S_ERROR(error) + if (not result['OK'] + and not self.checkComponentSoftware(componentType, system, cModule, extensions)['OK'] + and componentType != 'executor'): + error = 'Software for %s %s/%s is not installed' % (componentType, system, component) + if self.exitOnError: + gLogger.error(error) + DIRAC.exit(-1) + return S_ERROR(error) gLogger.notice('Installing %s %s/%s' % (componentType, system, component)) @@ -2010,11 +1869,9 @@ def uninstallComponent(self, system, component, removeLogs): """ Remove startup and runit directories """ - result = self.runsvctrlComponent(system, component, 'd') - if not result['OK']: - pass + self.runsvctrlComponent(system, component, 'd') - result = self.unsetupComponent(system, component) + self.unsetupComponent(system, component) if removeLogs: for runitCompDir in glob.glob(os.path.join(self.runitDir, system, component)): @@ -2066,7 +1923,6 @@ def installPortal(self): Install runit directories for the Web Portal """ # Check that the software for the Web Portal is installed - error = '' webDir = os.path.join(self.linkedRootPath, 'WebAppDIRAC') if not os.path.exists(webDir): error = 'WebApp extension not installed at %s' % webDir @@ -2118,15 +1974,14 @@ def getMySQLPasswords(self): """ Get MySQL passwords from local configuration or prompt """ - import getpass if not self.mysqlRootPwd: self.mysqlRootPwd = getpass.getpass('MySQL root password: ') if not self.mysqlPassword: # Take it if it is already defined self.mysqlPassword = self.localCfg.getOption('/Systems/Databases/Password', '') - if not self.mysqlPassword: - self.mysqlPassword = getpass.getpass('MySQL Dirac password: ') + if not self.mysqlPassword: + self.mysqlPassword = getpass.getpass('MySQL Dirac password: ') return S_OK() @@ -2148,24 +2003,21 @@ def getMySQLStatus(self): result = self.execCommand(0, ['mysqladmin', 'status']) if not result['OK']: return result - output = result['Value'][1] - _d1, uptime, nthreads, nquestions, nslow, nopens, nflash, nopen, nqpersec = output.split(':') - resDict = {} - resDict['UpTime'] = uptime.strip().split()[0] - resDict['NumberOfThreads'] = nthreads.strip().split()[0] - resDict['NumberOfQuestions'] = nquestions.strip().split()[0] - resDict['NumberOfSlowQueries'] = nslow.strip().split()[0] - resDict['NumberOfOpens'] = nopens.strip().split()[0] - resDict['OpenTables'] = nopen.strip().split()[0] - resDict['FlushTables'] = nflash.strip().split()[0] - resDict['QueriesPerSecond'] = nqpersec.strip().split()[0] - return S_OK(resDict) - def getAvailableDatabases(self, extensions=[]): - """ Find all databases defined - """ + keys = [ + None, "UpTime", "NumberOfThreads", "NumberOfQuestions", + "NumberOfSlowQueries", "NumberOfOpens", "FlushTables", "OpenTables", + "QueriesPerSecond" + ] + return S_OK({ + key: output.strip().split()[0] + for key, output in zip(keys, result['Value'][1].split(":")) if key + }) + + def getAvailableDatabases(self, extensions=None): + """Find all databases defined""" if not extensions: - extensions = CSGlobals.getCSExtensions() + extensions = extensionsByPriority() res = self.getAvailableSQLDatabases(extensions) gLogger.debug("Available SQL databases", res) @@ -2192,13 +2044,14 @@ def getAvailableSQLDatabases(self, extensions): :return: dict of MySQL DBs """ dbDict = {} - for extension in reversed(extensions + ['']): - databases = findDatabases(('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC')) + for extension in extensions: + databases = findDatabases(extensions) for systemName, dbSql in databases: dbName = dbSql.replace('.sql', '') dbDict[dbName] = {} dbDict[dbName]['Type'] = 'MySQL' - dbDict[dbName]['Extension'] = extension + # TODO: Does this need to be replaced + dbDict[dbName]['Extension'] = extension.replace("DIRAC", "") dbDict[dbName]['System'] = systemName.replace('System', '') return S_OK(dbDict) @@ -2221,16 +2074,15 @@ def getAvailableESDatabases(self, extensions): :return: dict of ES DBs """ dbDict = {} - for extension in reversed(extensions + ['']): - ext = ('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC') - sqlDatabases = findDatabases(ext) - for systemName, dbName in findModules(ext, "DB", "*DB"): + for extension in extensions: + sqlDatabases = findDatabases(extension) + for systemName, dbName in findModules(extension, "DB", "*DB"): if (systemName, dbName + ".sql") in sqlDatabases: continue # Introspect all possible ones for a ElasticDB attribute try: - module = importlib.import_module(".".join([ext, systemName, "DB", dbName])) + module = importlib.import_module(".".join([extension, systemName, "DB", dbName])) dbClass = getattr(module, dbName) except (AttributeError, ImportError): continue @@ -2239,7 +2091,8 @@ def getAvailableESDatabases(self, extensions): dbDict[dbName] = {} dbDict[dbName]['Type'] = 'ES' - dbDict[dbName]['Extension'] = extension + # TODO: Does this need to be replaced + dbDict[dbName]['Extension'] = extension.replace("DIRAC", "") dbDict[dbName]['System'] = systemName return S_OK(dbDict) @@ -2275,9 +2128,8 @@ def installDatabase(self, dbName): gLogger.notice('Installing', dbName) # is there by chance an extension of it? - for extension in CSGlobals.getCSExtensions() + [""]: - ext = ('%sDIRAC' % extension).replace('DIRACDIRAC', 'DIRAC') - databases = {k: v for v, k in findDatabases(ext)} + for extension in extensionsByPriority(): + databases = {k: v for v, k in findDatabases(extension)} filename = dbName + ".sql" if filename in databases: break @@ -2288,7 +2140,7 @@ def installDatabase(self, dbName): DIRAC.exit(-1) return S_ERROR(error) systemName = databases[filename] - moduleName = ".".join([ext, systemName, "DB"]) + moduleName = ".".join([extension, systemName, "DB"]) gLogger.debug("Installing %s from %s" % (filename, moduleName)) dbSql = importlib_resources.read_text(moduleName, filename) @@ -2355,13 +2207,13 @@ def installDatabase(self, dbName): DIRAC.exit(-1) return S_ERROR(error) - return S_OK(extension, systemName) + return S_OK([extension, systemName]) def uninstallDatabase(self, gConfig_o, dbName): """ Remove a database from DIRAC """ - result = self.getAvailableDatabases(CSGlobals.getCSExtensions()) + result = self.getAvailableDatabases() if not result['OK']: return result @@ -2385,10 +2237,9 @@ def _createMySQLCMDLines(self, dbSql): if line.lower().startswith('source'): sourcedDBbFileName = line.split(' ')[1].replace('\n', '') gLogger.info("Found file to source: %s" % sourcedDBbFileName) - sourcedDBbFile = os.path.join(rootPath, sourcedDBbFileName) - with io.open(sourcedDBbFile, 'rt') as fdSourced: - dbLinesSourced = fdSourced.readlines() - for lineSourced in dbLinesSourced: + module, filename = sourcedDBbFileName.rsplit("/", 1) + dbSourced = importlib_resources.read_text(module.replace("/", "."), filename) + for lineSourced in dbSourced.split("\n"): if lineSourced.strip(): cmdLines.append(lineSourced.strip()) @@ -2454,7 +2305,7 @@ def execCommand(self, timeout, cmd): """ Execute command tuple and handle Error cases """ - gLogger.debug("executing command %s with timeout %d" % (cmd, timeout)) + gLogger.debug("Executing command %s with timeout %d" % (cmd, timeout)) result = systemCall(timeout, cmd) if not result['OK']: if timeout and result['Message'].find('Timeout') == 0: diff --git a/src/DIRAC/FrameworkSystem/Client/SystemAdministratorClientCLI.py b/src/DIRAC/FrameworkSystem/Client/SystemAdministratorClientCLI.py index 4f73f1fdb9a..2e545e4bb8d 100644 --- a/src/DIRAC/FrameworkSystem/Client/SystemAdministratorClientCLI.py +++ b/src/DIRAC/FrameworkSystem/Client/SystemAdministratorClientCLI.py @@ -21,7 +21,7 @@ from DIRAC.FrameworkSystem.Utilities import MonitoringUtilities from DIRAC.MonitoringSystem.Client.MonitoringClient import MonitoringClient from DIRAC.FrameworkSystem.Client.ComponentInstaller import gComponentInstaller -from DIRAC.ConfigurationSystem.Client.Helpers import getCSExtensions +from DIRAC.Core.Utilities.Extensions import extensionsByPriority from DIRAC.Core.Utilities import List from DIRAC.Core.Utilities.PromptUser import promptUser from DIRAC.Core.Utilities.PrettyPrint import printTable @@ -713,19 +713,19 @@ def do_install(self, args): # Install Module section if not yet there if module: result = gComponentInstaller.addDefaultOptionsToCS(gConfig, option, system, module, - getCSExtensions(), hostSetup) + extensionsByPriority(), hostSetup) # in case of Error we must stop, this can happen when the module name is wrong... if not result['OK']: self._errMsg(result['Message']) return # Add component section with specific parameters only result = gComponentInstaller.addDefaultOptionsToCS(gConfig, option, system, component, - getCSExtensions(), hostSetup, specialOptions, + extensionsByPriority(), hostSetup, specialOptions, addDefaultOptions=True) else: # Install component section result = gComponentInstaller.addDefaultOptionsToCS(gConfig, option, system, component, - getCSExtensions(), hostSetup, specialOptions) + extensionsByPriority(), hostSetup, specialOptions) if not result['OK']: self._errMsg(result['Message']) diff --git a/src/DIRAC/FrameworkSystem/Service/ProxyManagerHandler.py b/src/DIRAC/FrameworkSystem/Service/ProxyManagerHandler.py index 2ceec745eef..3ec724fe31f 100644 --- a/src/DIRAC/FrameworkSystem/Service/ProxyManagerHandler.py +++ b/src/DIRAC/FrameworkSystem/Service/ProxyManagerHandler.py @@ -30,7 +30,7 @@ class ProxyManagerHandler(RequestHandler): def initializeHandler(cls, serviceInfoDict): useMyProxy = cls.srv_getCSOption("UseMyProxy", False) try: - result = ObjectLoader().loadObject('FrameworkSystem.DB.ProxyDB', 'ProxyDB') + result = ObjectLoader().loadObject('FrameworkSystem.DB.ProxyDB') if not result['OK']: gLogger.error('Failed to load ProxyDB class: %s' % result['Message']) return result diff --git a/src/DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py b/src/DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py index 94807d64b80..39ee0c64fb0 100644 --- a/src/DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py +++ b/src/DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py @@ -102,7 +102,7 @@ def export_getSoftwareComponents(self): """ Get the list of all the components ( services and agents ) for which the software is installed on the system """ - return gComponentInstaller.getSoftwareComponents(getCSExtensions()) + return gComponentInstaller.getSoftwareComponents(extensionsByPriority()) types_getInstalledComponents = [] @@ -126,7 +126,7 @@ def export_getOverallStatus(self): """ Get the complete status information for the components in the given list """ - result = gComponentInstaller.getOverallStatus(getCSExtensions()) + result = gComponentInstaller.getOverallStatus(extensionsByPriority()) if not result['OK']: return result statusDict = result['Value'] @@ -153,7 +153,7 @@ def export_getStartupComponentStatus(self, componentTupleList): def export_installComponent(self, componentType, system, component, componentModule=''): """ Install runit directory for the specified component """ - return gComponentInstaller.installComponent(componentType, system, component, getCSExtensions(), componentModule) + return gComponentInstaller.installComponent(componentType, system, component, extensionsByPriority(), componentModule) types_setupComponent = [six.string_types, six.string_types, six.string_types] @@ -161,7 +161,7 @@ def export_setupComponent(self, componentType, system, component, componentModul """ Setup the specified component for running with the runsvdir daemon It implies installComponent """ - result = gComponentInstaller.setupComponent(componentType, system, component, getCSExtensions(), componentModule) + result = gComponentInstaller.setupComponent(componentType, system, component, extensionsByPriority(), componentModule) gConfig.forceRefresh() return result @@ -170,7 +170,7 @@ def export_setupComponent(self, componentType, system, component, componentModul def export_addDefaultOptionsToComponentCfg(self, componentType, system, component): """ Add default component options local component cfg """ - return gComponentInstaller.addDefaultOptionsToComponentCfg(componentType, system, component, getCSExtensions()) + return gComponentInstaller.addDefaultOptionsToComponentCfg(componentType, system, component, extensionsByPriority()) types_unsetupComponent = [six.string_types, six.string_types] @@ -239,7 +239,7 @@ def export_getDatabases(self, mysqlPassword=None): def export_getAvailableDatabases(self): """ Get the list of databases which software is installed in the system """ - return gComponentInstaller.getAvailableDatabases(getCSExtensions()) + return gComponentInstaller.getAvailableDatabases() types_installDatabase = [six.string_types] @@ -272,7 +272,7 @@ def export_addDefaultOptionsToCS(self, componentType, system, component, overwri """ Add default component options to the global CS or to the local options """ return gComponentInstaller.addDefaultOptionsToCS(gConfig, componentType, system, component, - getCSExtensions(), + extensionsByPriority(), overwrite=overwrite) ####################################################################################### diff --git a/src/DIRAC/FrameworkSystem/scripts/dirac_install_component.py b/src/DIRAC/FrameworkSystem/scripts/dirac_install_component.py index 6d9e88d584d..deda9bcfc54 100755 --- a/src/DIRAC/FrameworkSystem/scripts/dirac_install_component.py +++ b/src/DIRAC/FrameworkSystem/scripts/dirac_install_component.py @@ -18,8 +18,8 @@ from DIRAC import gConfig, gLogger, S_OK from DIRAC.Core.Base import Script from DIRAC.Core.Utilities.DIRACScript import DIRACScript +from DIRAC.Core.Utilities.Extensions import extensionsByPriority from DIRAC.FrameworkSystem.Utilities import MonitoringUtilities -from DIRAC.ConfigurationSystem.Client.Helpers import getCSExtensions __RCSID__ = "$Id$" @@ -70,67 +70,62 @@ def main(): if len(args) != 2: Script.showHelp(exitCode=1) - cType = None system = args[0] component = args[1] - compOrMod = module if module else component + compOrMod = module or component - result = gComponentInstaller.getSoftwareComponents(getCSExtensions()) + result = gComponentInstaller.getSoftwareComponents(extensionsByPriority()) if not result['OK']: gLogger.error(result['Message']) DIRACexit(1) - else: - availableComponents = result['Value'] + availableComponents = result['Value'] for compType in availableComponents: if system in availableComponents[compType] and compOrMod in availableComponents[compType][system]: cType = compType[:-1].lower() break - - if not cType: + else: gLogger.error('Component %s/%s is not available for installation' % (system, component)) DIRACexit(1) if module: result = gComponentInstaller.addDefaultOptionsToCS(gConfig, cType, system, module, - getCSExtensions(), + extensionsByPriority(), overwrite=overwrite) result = gComponentInstaller.addDefaultOptionsToCS(gConfig, cType, system, component, - getCSExtensions(), + extensionsByPriority(), specialOptions=specialOptions, overwrite=overwrite, addDefaultOptions=False) else: result = gComponentInstaller.addDefaultOptionsToCS(gConfig, cType, system, component, - getCSExtensions(), + extensionsByPriority(), specialOptions=specialOptions, overwrite=overwrite) if not result['OK']: gLogger.error(result['Message']) DIRACexit(1) - else: - result = gComponentInstaller.installComponent(cType, system, component, getCSExtensions(), module) + result = gComponentInstaller.installComponent(cType, system, component, extensionsByPriority(), module) + if not result['OK']: + gLogger.error(result['Message']) + DIRACexit(1) + gLogger.notice('Successfully installed component %s in %s system, now setting it up' % (component, system)) + result = gComponentInstaller.setupComponent(cType, system, component, extensionsByPriority(), module) + if not result['OK']: + gLogger.error(result['Message']) + DIRACexit(1) + if component == 'ComponentMonitoring': + result = MonitoringUtilities.monitorInstallation('DB', system, 'InstalledComponentsDB') if not result['OK']: gLogger.error(result['Message']) DIRACexit(1) - else: - gLogger.notice('Successfully installed component %s in %s system, now setting it up' % (component, system)) - result = gComponentInstaller.setupComponent(cType, system, component, getCSExtensions(), module) - if not result['OK']: - gLogger.error(result['Message']) - DIRACexit(1) - if component == 'ComponentMonitoring': - result = MonitoringUtilities.monitorInstallation('DB', system, 'InstalledComponentsDB') - if not result['OK']: - gLogger.error(result['Message']) - DIRACexit(1) - result = MonitoringUtilities.monitorInstallation(cType, system, component, module) - if not result['OK']: - gLogger.error(result['Message']) - DIRACexit(1) - gLogger.notice('Successfully completed the installation of %s/%s' % (system, component)) - DIRACexit() + result = MonitoringUtilities.monitorInstallation(cType, system, component, module) + if not result['OK']: + gLogger.error(result['Message']) + DIRACexit(1) + gLogger.notice('Successfully completed the installation of %s/%s' % (system, component)) + DIRACexit() if __name__ == "__main__": diff --git a/src/DIRAC/FrameworkSystem/scripts/dirac_install_tornado_service.py b/src/DIRAC/FrameworkSystem/scripts/dirac_install_tornado_service.py index 30cb0d94458..5ca24aa9110 100755 --- a/src/DIRAC/FrameworkSystem/scripts/dirac_install_tornado_service.py +++ b/src/DIRAC/FrameworkSystem/scripts/dirac_install_tornado_service.py @@ -20,8 +20,8 @@ from DIRAC import gConfig, gLogger, S_OK from DIRAC.Core.Base import Script from DIRAC.Core.Utilities.DIRACScript import DIRACScript +from DIRAC.Core.Utilities.Extensions import extensionsByPriority from DIRAC.FrameworkSystem.Utilities import MonitoringUtilities -from DIRAC.ConfigurationSystem.Client.Helpers import getCSExtensions __RCSID__ = "$Id$" @@ -81,7 +81,7 @@ def main(): compOrMod = module if module else component result = gComponentInstaller.addDefaultOptionsToCS(gConfig, 'service', system, component, - getCSExtensions(), + extensionsByPriority(), specialOptions=specialOptions, overwrite=overwrite) @@ -100,7 +100,7 @@ def main(): DIRACexit(1) gLogger.notice('Successfully installed component %s in %s system, now setting it up' % (component, system)) - result = gComponentInstaller.setupTornadoService(system, component, getCSExtensions(), module) + result = gComponentInstaller.setupTornadoService(system, component, extensionsByPriority(), module) if not result['OK']: gLogger.error(result['Message']) DIRACexit(1) diff --git a/src/DIRAC/ResourceStatusSystem/Agent/CacheFeederAgent.py b/src/DIRAC/ResourceStatusSystem/Agent/CacheFeederAgent.py index 3eeea12d150..4140bb08ee2 100644 --- a/src/DIRAC/ResourceStatusSystem/Agent/CacheFeederAgent.py +++ b/src/DIRAC/ResourceStatusSystem/Agent/CacheFeederAgent.py @@ -47,15 +47,13 @@ def initialize(self): """ Define the commands to be executed, and instantiate the clients that will be used. """ - res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceStatusClient', - 'ResourceStatusClient') + res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceStatusClient') if not res['OK']: self.log.error('Failed to load ResourceStatusClient class: %s' % res['Message']) return res rsClass = res['Value'] - res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceManagementClient', - 'ResourceManagementClient') + res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceManagementClient') if not res['OK']: self.log.error('Failed to load ResourceManagementClient class: %s' % res['Message']) return res diff --git a/src/DIRAC/ResourceStatusSystem/Agent/ElementInspectorAgent.py b/src/DIRAC/ResourceStatusSystem/Agent/ElementInspectorAgent.py index d62227ba4f0..d6f3d4c803d 100644 --- a/src/DIRAC/ResourceStatusSystem/Agent/ElementInspectorAgent.py +++ b/src/DIRAC/ResourceStatusSystem/Agent/ElementInspectorAgent.py @@ -76,15 +76,13 @@ def initialize(self): self.elementType = self.am_getOption('elementType', self.elementType) - res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceStatusClient', - 'ResourceStatusClient') + res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceStatusClient') if not res['OK']: self.log.error('Failed to load ResourceStatusClient class: %s' % res['Message']) return res rsClass = res['Value'] - res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceManagementClient', - 'ResourceManagementClient') + res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceManagementClient') if not res['OK']: self.log.error('Failed to load ResourceManagementClient class: %s' % res['Message']) return res diff --git a/src/DIRAC/ResourceStatusSystem/Agent/SiteInspectorAgent.py b/src/DIRAC/ResourceStatusSystem/Agent/SiteInspectorAgent.py index 443c41cfe8a..4e8faaa58be 100644 --- a/src/DIRAC/ResourceStatusSystem/Agent/SiteInspectorAgent.py +++ b/src/DIRAC/ResourceStatusSystem/Agent/SiteInspectorAgent.py @@ -66,15 +66,13 @@ def initialize(self): maxNumberOfThreads = self.am_getOption('maxNumberOfThreads', self.__maxNumberOfThreads) self.threadPool = ThreadPool(maxNumberOfThreads, maxNumberOfThreads) - res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.SiteStatus', - 'SiteStatus') + res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.SiteStatus') if not res['OK']: self.log.error('Failed to load SiteStatus class: %s' % res['Message']) return res siteStatusClass = res['Value'] - res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceManagementClient', - 'ResourceManagementClient') + res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceManagementClient') if not res['OK']: self.log.error('Failed to load ResourceManagementClient class: %s' % res['Message']) return res diff --git a/src/DIRAC/ResourceStatusSystem/PolicySystem/PEP.py b/src/DIRAC/ResourceStatusSystem/PolicySystem/PEP.py index e2b559bf370..8e6100585b0 100644 --- a/src/DIRAC/ResourceStatusSystem/PolicySystem/PEP.py +++ b/src/DIRAC/ResourceStatusSystem/PolicySystem/PEP.py @@ -46,22 +46,19 @@ def __init__(self, clients=dict()): # Creating the client in the PEP is a convenience for the PDP, that uses internally the RSS clients - res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceStatusClient', - 'ResourceStatusClient') + res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceStatusClient') if not res['OK']: self.log.error('Failed to load ResourceStatusClient class: %s' % res['Message']) raise ImportError(res['Message']) rsClass = res['Value'] - res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceManagementClient', - 'ResourceManagementClient') + res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.ResourceManagementClient') if not res['OK']: self.log.error('Failed to load ResourceManagementClient class: %s' % res['Message']) raise ImportError(res['Message']) rmClass = res['Value'] - res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.SiteStatus', - 'SiteStatus') + res = ObjectLoader().loadObject('DIRAC.ResourceStatusSystem.Client.SiteStatus') if not res['OK']: self.log.error('Failed to load SiteStatus class: %s' % res['Message']) raise ImportError(res['Message']) diff --git a/src/DIRAC/Resources/Catalog/FCConditionParser.py b/src/DIRAC/Resources/Catalog/FCConditionParser.py index 92adf86d964..57e915256c5 100644 --- a/src/DIRAC/Resources/Catalog/FCConditionParser.py +++ b/src/DIRAC/Resources/Catalog/FCConditionParser.py @@ -171,7 +171,7 @@ def __init__(self, tokens): # Load the plugin, and give it the condition objLoader = ObjectLoader() - _class = objLoader.loadObject('Resources.Catalog.ConditionPlugins.%s' % self.pluginName, self.pluginName) + _class = objLoader.loadObject('Resources.Catalog.ConditionPlugins.%s' % self.pluginName) if not _class['OK']: raise Exception(_class['Message']) diff --git a/src/DIRAC/Resources/Catalog/FileCatalogFactory.py b/src/DIRAC/Resources/Catalog/FileCatalogFactory.py index 8bc3ddfe8e4..988f17a3b9e 100644 --- a/src/DIRAC/Resources/Catalog/FileCatalogFactory.py +++ b/src/DIRAC/Resources/Catalog/FileCatalogFactory.py @@ -8,7 +8,7 @@ from DIRAC import gLogger, gConfig, S_OK, S_ERROR from DIRAC.ConfigurationSystem.Client.Helpers.Resources import getCatalogPath from DIRAC.Resources.Catalog.FileCatalogProxyClient import FileCatalogProxyClient -from DIRAC.Core.Utilities import ObjectLoader +from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader __RCSID__ = "$Id$" @@ -45,12 +45,9 @@ def createCatalog(self, catalogName, useProxy=False): return self.__createCatalog(catalogName, catalogType, catalogURL, optionsDict) def __getCatalogClass(self, catalogType): - - objectLoader = ObjectLoader.ObjectLoader() - result = objectLoader.loadObject('Resources.Catalog.%sClient' % catalogType, catalogType + 'Client') + result = ObjectLoader().loadObject('Resources.Catalog.%sClient' % catalogType) if not result['OK']: gLogger.error('Failed to load catalog object', '%s' % result['Message']) - return result def __createCatalog(self, catalogName, catalogType, catalogURL, optionsDict): diff --git a/src/DIRAC/Resources/Computing/ComputingElement.py b/src/DIRAC/Resources/Computing/ComputingElement.py index bab96d81995..a08c56ceafa 100755 --- a/src/DIRAC/Resources/Computing/ComputingElement.py +++ b/src/DIRAC/Resources/Computing/ComputingElement.py @@ -203,8 +203,7 @@ def loadBatchSystem(self): """ if self.batchSystem is None: self.batchSystem = self.ceParameters['BatchSystem'] - objectLoader = ObjectLoader() - result = objectLoader.loadObject('Resources.Computing.BatchSystems.%s' % self.batchSystem, self.batchSystem) + result = ObjectLoader().loadObject('Resources.Computing.BatchSystems.%s' % self.batchSystem) if not result['OK']: self.log.error('Failed to load batch object: %s' % result['Message']) return result diff --git a/src/DIRAC/Resources/Computing/ComputingElementFactory.py b/src/DIRAC/Resources/Computing/ComputingElementFactory.py index f42782fc745..d696e24c4d2 100755 --- a/src/DIRAC/Resources/Computing/ComputingElementFactory.py +++ b/src/DIRAC/Resources/Computing/ComputingElementFactory.py @@ -11,7 +11,7 @@ from __future__ import print_function from DIRAC import S_OK, S_ERROR, gLogger from DIRAC.Resources.Computing.ComputingElement import getCEConfigDict -from DIRAC.Core.Utilities import ObjectLoader +from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader __RCSID__ = "$Id$" @@ -46,8 +46,7 @@ def getCE(self, ceType='', ceName='', ceParametersDict={}): return S_ERROR(error) subClassName = "%sComputingElement" % (ceTypeLocal) - objectLoader = ObjectLoader.ObjectLoader() - result = objectLoader.loadObject('Resources.Computing.%s' % subClassName, subClassName) + result = ObjectLoader().loadObject('Resources.Computing.%s' % subClassName) if not result['OK']: self.log.error('Failed to load object', '%s: %s' % (subClassName, result['Message'])) return result diff --git a/src/DIRAC/Resources/IdProvider/IdProviderFactory.py b/src/DIRAC/Resources/IdProvider/IdProviderFactory.py index 38bdb15612f..1134654f218 100644 --- a/src/DIRAC/Resources/IdProvider/IdProviderFactory.py +++ b/src/DIRAC/Resources/IdProvider/IdProviderFactory.py @@ -11,7 +11,7 @@ from __future__ import print_function from DIRAC import S_OK, S_ERROR, gLogger -from DIRAC.Core.Utilities import ObjectLoader +from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader from DIRAC.ConfigurationSystem.Client.Helpers.Resources import getInfoAboutProviders __RCSID__ = "$Id$" @@ -43,8 +43,7 @@ def getIdProvider(self, idProvider): self.log.verbose('Creating IdProvider', 'of %s type with the name %s' % (pType, idProvider)) subClassName = "%sIdProvider" % (pType) - objectLoader = ObjectLoader.ObjectLoader() - result = objectLoader.loadObject('Resources.IdProvider.%s' % subClassName, subClassName) + result = ObjectLoader().loadObject('Resources.IdProvider.%s' % subClassName) if not result['OK']: self.log.error('Failed to load object', '%s: %s' % (subClassName, result['Message'])) return result diff --git a/src/DIRAC/Resources/MessageQueue/MQConnector.py b/src/DIRAC/Resources/MessageQueue/MQConnector.py index a802078882f..f3bd0da69d9 100644 --- a/src/DIRAC/Resources/MessageQueue/MQConnector.py +++ b/src/DIRAC/Resources/MessageQueue/MQConnector.py @@ -6,7 +6,7 @@ from __future__ import print_function from DIRAC import gLogger, S_OK, S_ERROR -from DIRAC.Core.Utilities import ObjectLoader +from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader from DIRAC.Core.Utilities.DErrno import EMQUKN __RCSID__ = "$Id$" @@ -50,8 +50,7 @@ def getMQConnectorClass(mqType): S_OK or S_ERROR: with loaded specialized class of MQConnector. """ subClassName = mqType + 'MQConnector' - objectLoader = ObjectLoader.ObjectLoader() - result = objectLoader.loadObject('Resources.MessageQueue.%s' % subClassName, subClassName) + result = ObjectLoader().loadObject('Resources.MessageQueue.%s' % subClassName) if not result['OK']: gLogger.error('Failed to load object', '%s: %s' % (subClassName, result['Message'])) return result diff --git a/src/DIRAC/Resources/ProxyProvider/ProxyProviderFactory.py b/src/DIRAC/Resources/ProxyProvider/ProxyProviderFactory.py index 15276455bf4..7f831ea1c32 100644 --- a/src/DIRAC/Resources/ProxyProvider/ProxyProviderFactory.py +++ b/src/DIRAC/Resources/ProxyProvider/ProxyProviderFactory.py @@ -10,7 +10,7 @@ from __future__ import division from __future__ import print_function from DIRAC import S_OK, S_ERROR, gLogger -from DIRAC.Core.Utilities import ObjectLoader +from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader from DIRAC.ConfigurationSystem.Client.Helpers.Resources import getInfoAboutProviders __RCSID__ = "$Id$" @@ -46,8 +46,7 @@ def getProxyProvider(self, proxyProvider): self.log.verbose('Creating ProxyProvider of %s type with the name %s' % (ppType, proxyProvider)) subClassName = "%sProxyProvider" % (ppType) - objectLoader = ObjectLoader.ObjectLoader() - result = objectLoader.loadObject('Resources.ProxyProvider.%s' % subClassName, subClassName) + result = ObjectLoader().loadObject('Resources.ProxyProvider.%s' % subClassName) if not result['OK']: self.log.error('Failed to load object', '%s: %s' % (subClassName, result['Message'])) return result diff --git a/src/DIRAC/Resources/Storage/StorageElement.py b/src/DIRAC/Resources/Storage/StorageElement.py index c1ee256e70d..f454ffe746d 100755 --- a/src/DIRAC/Resources/Storage/StorageElement.py +++ b/src/DIRAC/Resources/Storage/StorageElement.py @@ -419,7 +419,7 @@ def getOccupancy(self, unit='MB', **kwargs): # Call occupancy plugin if requested occupancyPlugin = self.options.get('OccupancyPlugin') if occupancyPlugin: - res = ObjectLoader().loadObject('Resources.Storage.OccupancyPlugins.%s' % occupancyPlugin, occupancyPlugin) + res = ObjectLoader().loadObject('Resources.Storage.OccupancyPlugins.%s' % occupancyPlugin) if not res['OK']: return S_ERROR(errno.EPROTONOSUPPORT, 'Failed to load occupancy plugin %s' % occupancyPlugin) log.verbose('Use occupancy plugin %s' % occupancyPlugin) diff --git a/src/DIRAC/Resources/Storage/StorageFactory.py b/src/DIRAC/Resources/Storage/StorageFactory.py index 8bae9cb467b..2d524b59124 100755 --- a/src/DIRAC/Resources/Storage/StorageFactory.py +++ b/src/DIRAC/Resources/Storage/StorageFactory.py @@ -402,7 +402,7 @@ def __generateStorageObject(self, storageName, pluginName, parameters, hideExcep storageType = 'Proxy' objectLoader = ObjectLoader() - result = objectLoader.loadObject('Resources.Storage.%sStorage' % storageType, storageType + 'Storage', + result = objectLoader.loadObject('Resources.Storage.%sStorage' % storageType, hideExceptions=hideExceptions) if not result['OK']: gLogger.error('Failed to load storage object: %s' % result['Message']) diff --git a/src/DIRAC/Workflow/Utilities/Utils.py b/src/DIRAC/Workflow/Utilities/Utils.py index 72ceb1127ea..23f016dc397 100644 --- a/src/DIRAC/Workflow/Utilities/Utils.py +++ b/src/DIRAC/Workflow/Utilities/Utils.py @@ -4,56 +4,45 @@ from __future__ import division from __future__ import print_function +import importlib import os import time -from DIRAC.ConfigurationSystem.Client.Helpers.CSGlobals import getVO, getCSExtensions - from DIRAC.Core.Workflow.Module import ModuleDefinition +from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader from DIRAC.Core.Workflow.Parameter import Parameter from DIRAC.Core.Workflow.Step import StepDefinition ############################################################################# -def getStepDefinition(stepName, modulesNameList=None, importLine="""""", parametersList=None): +def getStepDefinition(stepName, modulesNameList=None, importLine="", parametersList=None): """ Given a name, a list of modules name, and a list of parameters, returns a step definition. Remember that Step definition = Parameters + Module Instances """ - if modulesNameList is None: modulesNameList = [] if parametersList is None: parametersList = [] - # In case the importLine is not set, this is looking for a DIRAC extension, if any. - # The extension is supposed to be called ExtDIRAC. - if not importLine: - importLine = "DIRAC.Workflow.Modules" - for ext in getCSExtensions(): - if ext.lower() == getVO(): - importLine = ext + "DIRAC.Workflow.Modules" - break - stepDef = StepDefinition(stepName) for moduleName in modulesNameList: + module = None + if importLine: + try: + module = importlib.import_module(importLine + "." + moduleName) + except ImportError: + pass + # In case the importLine is not set, this is looking for a DIRAC extension, if any + if module is None: + module = ObjectLoader().loadModule("Workflow.Modules." + moduleName)["Value"] # create the module definition moduleDef = ModuleDefinition(moduleName) - try: - # Look in the importLine given, or the DIRAC if the given location can't be imported - moduleDef.setDescription(getattr(__import__("%s.%s" % (importLine, moduleName), - globals(), locals(), ['__doc__']), - "__doc__")) - moduleDef.setBody("""\nfrom %s.%s import %s\n""" % (importLine, moduleName, moduleName)) - except ImportError: - alternativeImportLine = "DIRAC.Workflow.Modules" - moduleDef.setDescription(getattr(__import__("%s.%s" % (alternativeImportLine, moduleName), - globals(), locals(), ['__doc__']), - "__doc__")) - moduleDef.setBody("""\nfrom %s.%s import %s\n""" % (alternativeImportLine, moduleName, moduleName)) + moduleDef.setDescription(module.__doc__) + moduleDef.setBody("\nfrom %s import %s\n" % (module.__name__, moduleName)) # add the module to the step, and instance it stepDef.addModule(moduleDef) @@ -88,11 +77,9 @@ def getStepCPUTimes(step_commons): cputime = 0 if 'StartStats' in step_commons: - # 5-tuple: utime, stime, cutime, cstime, elapsed_time - stats = os.times() - cputimeNow = stats[0] + stats[1] + stats[2] + stats[3] - cputimeBefore = step_commons['StartStats'][0] + step_commons['StartStats'][1] \ - + step_commons['StartStats'][2] + step_commons['StartStats'][3] + # os.times() returns a 5-tuple (utime, stime, cutime, cstime, elapsed_time) + cputimeNow = sum(os.times()[:4]) + cputimeBefore = sum(step_commons['StartStats'][:4]) cputime = cputimeNow - cputimeBefore return exectime, cputime diff --git a/src/DIRAC/WorkloadManagementSystem/private/SharesCorrector.py b/src/DIRAC/WorkloadManagementSystem/private/SharesCorrector.py index 7ca32a17e01..dbf84643fbd 100644 --- a/src/DIRAC/WorkloadManagementSystem/private/SharesCorrector.py +++ b/src/DIRAC/WorkloadManagementSystem/private/SharesCorrector.py @@ -7,7 +7,7 @@ __RCSID__ = "$Id$" from DIRAC import gLogger, S_OK, S_ERROR -from DIRAC.Core.Utilities import ObjectLoader +from DIRAC.Core.Utilities.ObjectLoader import ObjectLoader from DIRAC.ConfigurationSystem.Client.Helpers.Operations import Operations from DIRAC.WorkloadManagementSystem.private.correctors.BaseCorrector import BaseCorrector @@ -22,7 +22,6 @@ def __init__(self, opsHelper): self.__shareCorrectors = {} self.__correctorsOrder = [] self.__baseCS = "JobScheduling/ShareCorrections" - self.__objLoader = ObjectLoader.ObjectLoader() def __getCSValue(self, path, defaultValue=''): return self.__opsHelper.getValue("%s/%s" % (self.__baseCS, path), defaultValue) @@ -30,7 +29,7 @@ def __getCSValue(self, path, defaultValue=''): def __getCorrectorClass(self, correctorName): baseImport = "WorkloadManagementSystem.private.correctors" fullCN = "%s.%sCorrector" % (baseImport, correctorName) - result = self.__objLoader.getObjects(baseImport, ".*Corrector", parentClass=BaseCorrector) + result = ObjectLoader().getObjects(baseImport, ".*Corrector", parentClass=BaseCorrector) if not result['OK']: return result data = result['Value'] @@ -45,7 +44,7 @@ def instantiateRequiredCorrectors(self): for corrector in self.__shareCorrectors: if corrector not in correctorsToStart: self.__log.info("Stopping corrector %s" % corrector) - del(self.__shareCorrectors[corrector]) + del self.__shareCorrectors[corrector] for corrector in correctorsToStart: if corrector not in self.__shareCorrectors: self.__log.info("Starting corrector %s" % corrector) diff --git a/tests/Jenkins/dirac-cfg-add-option.py b/tests/Jenkins/dirac-cfg-add-option.py index 2af12c094e7..8119ff9368c 100644 --- a/tests/Jenkins/dirac-cfg-add-option.py +++ b/tests/Jenkins/dirac-cfg-add-option.py @@ -34,14 +34,14 @@ from DIRAC import gConfig from DIRAC import exit as DIRACexit -from DIRAC.ConfigurationSystem.Client.Helpers import getCSExtensions +from DIRAC.Core.Utilities.Extensions import extensionsByPriority from DIRAC.FrameworkSystem.Client.ComponentInstaller import gComponentInstaller # gComponentInstaller.exitOnError = True result = gComponentInstaller.addDefaultOptionsToCS(gConfig, componentType, system, component, - getCSExtensions(), + extensionsByPriority(), specialOptions={}, overwrite=False) if not result['OK']: From b971ea1e23bcb55e751074d4946f042d054ad193 Mon Sep 17 00:00:00 2001 From: Chris Burr Date: Mon, 12 Apr 2021 12:06:02 +0200 Subject: [PATCH 8/9] Fix pycodestyle errors --- .../FrameworkSystem/Client/ComponentInstaller.py | 14 ++++++++------ .../Service/SystemAdministratorHandler.py | 8 ++++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py b/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py index cc7be0b5e4c..e1436379f51 100644 --- a/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py +++ b/src/DIRAC/FrameworkSystem/Client/ComponentInstaller.py @@ -1075,8 +1075,8 @@ def getInstalledComponents(self): resultDict[cType][system].append(component) return S_OK({ - resultIndexes[cType]: dict(resultDict[cType]) - for cType in self.componentTypes + resultIndexes[cType]: dict(resultDict[cType]) + for cType in self.componentTypes }) def getSetupComponents(self): @@ -1107,8 +1107,8 @@ def getSetupComponents(self): resultDict[cType][system].append(compT) return S_OK({ - resultIndexes[cType]: dict(resultDict[cType]) - for cType in self.componentTypes + resultIndexes[cType]: dict(resultDict[cType]) + for cType in self.componentTypes }) def getStartupComponentStatus(self, componentTupleList): @@ -1709,9 +1709,11 @@ def installComponent(self, componentType, system, component, extensions, compone if checkModule: cModule = componentModule or component result = self.checkComponentModule(componentType, system, cModule) - if (not result['OK'] + if ( + not result['OK'] and not self.checkComponentSoftware(componentType, system, cModule, extensions)['OK'] - and componentType != 'executor'): + and componentType != 'executor' + ): error = 'Software for %s %s/%s is not installed' % (componentType, system, component) if self.exitOnError: gLogger.error(error) diff --git a/src/DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py b/src/DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py index 39ee0c64fb0..815b513bf8b 100644 --- a/src/DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py +++ b/src/DIRAC/FrameworkSystem/Service/SystemAdministratorHandler.py @@ -153,7 +153,9 @@ def export_getStartupComponentStatus(self, componentTupleList): def export_installComponent(self, componentType, system, component, componentModule=''): """ Install runit directory for the specified component """ - return gComponentInstaller.installComponent(componentType, system, component, extensionsByPriority(), componentModule) + return gComponentInstaller.installComponent( + componentType, system, component, extensionsByPriority(), componentModule + ) types_setupComponent = [six.string_types, six.string_types, six.string_types] @@ -161,7 +163,9 @@ def export_setupComponent(self, componentType, system, component, componentModul """ Setup the specified component for running with the runsvdir daemon It implies installComponent """ - result = gComponentInstaller.setupComponent(componentType, system, component, extensionsByPriority(), componentModule) + result = gComponentInstaller.setupComponent( + componentType, system, component, extensionsByPriority(), componentModule + ) gConfig.forceRefresh() return result From ed6afa65356210f3c3ad67b5596f89d79235bbcb Mon Sep 17 00:00:00 2001 From: Chris Burr Date: Thu, 22 Apr 2021 10:09:33 +0200 Subject: [PATCH 9/9] Fix tests --- .../plots/qualityplot1.png.py3k | Bin 29370 -> 29230 bytes .../plots/qualityplot2.png.py3k | Bin 37360 -> 37384 bytes tests/Jenkins/dirac_ci.sh | 4 +--- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Integration/AccountingSystem/plots/qualityplot1.png.py3k b/tests/Integration/AccountingSystem/plots/qualityplot1.png.py3k index c50125c7c3eb88df4a903a713372f177516b2161..b7487b57bee920dbbd8e040d60dc3df4b35185bf 100644 GIT binary patch literal 29230 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 D$$s;p literal 29370 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 =y-j&-Sp2_C=rLEi-FSQOF~$S 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 LS)IIUhyVWohZ0u* diff --git a/tests/Integration/AccountingSystem/plots/qualityplot2.png.py3k b/tests/Integration/AccountingSystem/plots/qualityplot2.png.py3k index c3ba03fc141eb4c28935de6700c2ed5f7d4a3606..d5578f293d02b6445c96ffa0f8ffd4652b293e97 100644 GIT binary patch literal 37384 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%0D%nrkwxPfg<$+Q$4MU( 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%0A1(lUo%mFPQxN>4XQu~RAR_I6O0y|B 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 diff --git a/tests/Jenkins/dirac_ci.sh b/tests/Jenkins/dirac_ci.sh index bedd9ed4f9f..8beec165e0d 100644 --- a/tests/Jenkins/dirac_ci.sh +++ b/tests/Jenkins/dirac_ci.sh @@ -128,15 +128,13 @@ installSite() { curl -L "${DIRACOS2_URL}" > "installer.sh" bash "installer.sh" rm "installer.sh" - # TODO: Remove these two lines echo "source \"$PWD/diracos/diracosrc\"" > "$PWD/bashrc" - echo "export X509_CERT_DIR=\"$PWD/diracos/etc/grid-security/certificates\"" >> "$PWD/bashrc" + # TODO: This will be fixed properly as part of https://github.com/DIRACGrid/DIRAC/issues/5082 mv "${SERVERINSTALLDIR}/etc/grid-security/"* "${SERVERINSTALLDIR}/diracos/etc/grid-security/" rm -rf "${SERVERINSTALLDIR}/etc" ln -s "${SERVERINSTALLDIR}/diracos/etc" "${SERVERINSTALLDIR}/etc" source diracos/diracosrc pip install git+https://gitlab.cern.ch/chaen/fts-rest-flask.git@packaging - pip install 'sqlalchemy<1.4' for module_path in "${ALTERNATIVE_MODULES[@]}"; do pip install ${PIP_INSTALL_EXTRA_ARGS:-} "${module_path}[server]" done