"""
Module for managing the installation of DIRAC components:
MySQL, DB's, NoSQL DBs, Services, Agents, Executors

It only makes use of defaults in LocalInstallation Section in dirac.cfg

The Following Options are used::

  /DIRAC/Setup:             Setup to be used for any operation
  /LocalInstallation/InstanceName:    Name of the Instance for the current Setup (default /DIRAC/Setup)
  /LocalInstallation/LogLevel:        LogLevel set in "run" script for all components installed
  /LocalInstallation/RootPath:        Python 2 only! Used instead of rootPath in "run" script
                                      if defined (if links are used to named versions)
  /LocalInstallation/InstancePath:    Python 2 only! Location where runit and
                                      startup directories are created (default rootPath)
  /LocalInstallation/UseVersionsDir:  Python 2 only! DIRAC is installed under
                                      versions/<Versioned Directory> with a link from pro
                                      (This option overwrites RootPath and InstancePath)
  /LocalInstallation/Host:            Used when build the URL to be published for the installed
                                      service (default: socket.getfqdn())
  /LocalInstallation/RunitDir:        Location where runit directory is created (default InstancePath/runit)
  /LocalInstallation/StartupDir:      Location where startup directory is created (default InstancePath/startup)
  /LocalInstallation/Database/User:                 (default Dirac)
  /LocalInstallation/Database/Password:             (must be set for SystemAdministrator Service to work)
  /LocalInstallation/Database/RootUser:             (default root)
  /LocalInstallation/Database/RootPwd:              (must be set for SystemAdministrator Service to work)
  /LocalInstallation/Database/Host:                 (must be set for SystemAdministrator Service to work)
  /LocalInstallation/Database/Port:                 (default 3306)
  /LocalInstallation/NoSQLDatabase/Host:            (must be set for SystemAdministrator Service to work)
  /LocalInstallation/NoSQLDatabase/Port:            (default 9200)
  /LocalInstallation/NoSQLDatabase/User:            (default '')
  /LocalInstallation/NoSQLDatabase/Password:        (not strictly necessary)
  /LocalInstallation/NoSQLDatabase/SSL:             (default True)

The setupSite method (used by the dirac-setup-site command) will use the following info::

  /LocalInstallation/Systems:       List of Systems to be defined for this instance
                                    in the CS (default: Configuration, Framework)
  /LocalInstallation/Databases:     List of MySQL Databases to be installed and configured
  /LocalInstallation/Services:      List of System/ServiceName to be setup
  /LocalInstallation/Agents:        List of System/AgentName to be setup
  /LocalInstallation/WebPortal:     Boolean to setup the Web Portal (default no)
  /LocalInstallation/ConfigurationMaster: Boolean, requires Configuration/Server to be given
                                          in the list of Services (default: no)
  /LocalInstallation/PrivateConfiguration: Boolean, requires Configuration/Server to be given
                                           in the list of Services (default: no)

If a Master Configuration Server is being installed the following Options can be used::

  /LocalInstallation/ConfigurationName: Name of the Configuration (default: Setup )
  /LocalInstallation/AdminUserName:  Name of the Admin user (default: None )
  /LocalInstallation/AdminUserDN:    DN of the Admin user certificate (default: None )
  /LocalInstallation/AdminUserEmail: Email of the Admin user (default: None )
  /LocalInstallation/AdminGroupName: Name of the Admin group (default: dirac_admin )
  /LocalInstallation/HostDN: DN of the host certificate (default: None )
  /LocalInstallation/VirtualOrganization: Name of the main Virtual Organization (default: None)

"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

__RCSID__ = "$Id$"

import glob
import importlib
import inspect
import io
import MySQLdb
import os
import pkgutil
import re
import shutil
import stat
import time
from collections import defaultdict

import importlib_resources
import six
import subprocess32 as subprocess
from diraccfg import CFG
from prompt_toolkit import prompt

import DIRAC
from DIRAC import rootPath
from DIRAC import gConfig
from DIRAC import gLogger
from DIRAC.Core.Utilities.Subprocess import systemCall
from DIRAC.Core.Utilities.ReturnValues import S_OK, S_ERROR

from DIRAC.Core.Utilities.Version import getVersion
from DIRAC.Core.Utilities.File import mkDir, mkLink
from DIRAC.ConfigurationSystem.Client.CSAPI import CSAPI
from DIRAC.ConfigurationSystem.Client.Helpers import (
    cfgPath,
    cfgPathToList,
    cfgInstallPath,
    cfgInstallSection,
    CSGlobals,
)
from DIRAC.Core.Security.Properties import (
    ALARMS_MANAGEMENT,
    SERVICE_ADMINISTRATOR,
    CS_ADMINISTRATOR,
    JOB_ADMINISTRATOR,
    FULL_DELEGATION,
    PROXY_MANAGEMENT,
    OPERATOR,
    NORMAL_USER,
    TRUSTED_HOST,
)

from DIRAC.ConfigurationSystem.Client import PathFinder
from DIRAC.Core.Utilities.MySQL import MySQL
from DIRAC.Core.Base.private.ModuleLoader import ModuleLoader
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,
    findAgents,
    findServices,
    findExecutors,
    findSystems,
)
from DIRAC.FrameworkSystem.Client.ComponentMonitoringClient import ComponentMonitoringClient

__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.get(compType, {}).get(system, {}),
        "Installed": component in installedDict.get(compType, {}).get(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


def _getSectionName(compType):
    """
    Returns the section name for a component in the CS
    For self.instance, the section for service is Services,
    whereas the section for agent is Agents
    """
    return "%ss" % compType.title()


class ComponentInstaller(object):
    def __init__(self):
        self.gDefaultPerms = (
            stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH
        )

        # On command line tools this can be set to True to abort after the first error.
        self.exitOnError = False

        # First some global defaults
        gLogger.debug("DIRAC Root Path =", rootPath)

        self.mysqlMode = ""
        self.localCfg = None
        self.cfgFile = ""
        self.setup = ""
        self.instance = ""
        self.logLevel = ""
        self.linkedRootPath = ""
        self.host = ""
        self.basePath = ""
        self.instancePath = ""
        self.runitDir = ""
        self.startDir = ""
        self.db = {}
        self.mysqlUser = ""
        self.mysqlPassword = ""
        self.mysqlRootUser = ""
        self.mysqlRootPwd = ""
        self.mysqlHost = ""
        self.mysqlPort = ""
        self.noSQLHost = ""
        self.noSQLPort = ""
        self.noSQLUser = ""
        self.noSQLPassword = ""
        self.noSQLSSL = ""
        self.controlDir = ""
        self.componentTypes = ["service", "agent", "executor"]
        self.monitoringClient = None

        self.loadDiracCfg()

    def resultIndexes(self, componentTypes):
        resultIndexes = {}
        for cType in componentTypes:
            resultIndexes[cType] = _getSectionName(cType)
        return S_OK(resultIndexes)

    def loadDiracCfg(self):
        """Read again defaults from dirac.cfg"""
        from DIRAC.Core.Utilities.Network import getFQDN

        self.localCfg = CFG()
        self.cfgFile = os.path.join(rootPath, "etc", "dirac.cfg")
        try:
            self.localCfg.loadFromFile(self.cfgFile)
        except Exception:
            gLogger.always("Can't load ", self.cfgFile)
            gLogger.always("Might be OK if setting up the site")

        self.setup = self.localCfg.getOption(cfgPath("DIRAC", "Setup"), "")
        self.instance = self.localCfg.getOption(cfgInstallPath("InstanceName"), self.setup)
        self.logLevel = self.localCfg.getOption(cfgInstallPath("LogLevel"), "INFO")
        if six.PY2:
            self.linkedRootPath = self.localCfg.getOption(cfgInstallPath("RootPath"), rootPath)
        else:
            self.linkedRootPath = rootPath

        self.host = self.localCfg.getOption(cfgInstallPath("Host"), getFQDN())

        self.basePath = os.path.dirname(rootPath)
        if six.PY2:
            self.instancePath = self.localCfg.getOption(cfgInstallPath("InstancePath"), rootPath)
            useVersionsDir = self.localCfg.getOption(cfgInstallPath("UseVersionsDir"), False)
            if useVersionsDir:
                # This option takes precedence and has no effect for Python 3 installations
                self.instancePath = os.path.dirname(os.path.dirname(rootPath))
                self.linkedRootPath = os.path.join(self.instancePath, "pro")
            gLogger.verbose("Using Instance Base Dir at", self.instancePath)
        else:
            self.instancePath = rootPath

        self.runitDir = os.path.join(self.instancePath, "runit")
        self.runitDir = self.localCfg.getOption(cfgInstallPath("RunitDir"), self.runitDir)
        gLogger.verbose("Using Runit Dir at", self.runitDir)

        self.startDir = os.path.join(self.instancePath, "startup")
        self.startDir = self.localCfg.getOption(cfgInstallPath("StartupDir"), self.startDir)
        gLogger.verbose("Using Startup Dir at", self.startDir)

        self.controlDir = os.path.join(self.instancePath, "control")
        self.controlDir = self.localCfg.getOption(cfgInstallPath("ControlDir"), self.controlDir)
        gLogger.verbose("Using Control Dir at", self.controlDir)

        # Now some MySQL default values
        self.mysqlRootPwd = self.localCfg.getOption(cfgInstallPath("Database", "RootPwd"), self.mysqlRootPwd)
        if self.mysqlRootPwd:
            gLogger.verbose("Reading Root MySQL Password from local configuration")
        else:
            gLogger.warn("MySQL root password not found")

        self.mysqlUser = self.localCfg.getOption(cfgInstallPath("Database", "User"), self.mysqlUser)
        if self.mysqlUser:
            gLogger.verbose("Reading MySQL User from local configuration")
        else:
            gLogger.warn("Using 'Dirac' as MySQL user name")
            self.mysqlUser = "Dirac"

        self.mysqlPassword = self.localCfg.getOption(cfgInstallPath("Database", "Password"), self.mysqlPassword)
        if self.mysqlPassword:
            gLogger.verbose("Reading %s MySQL Password from local configuration " % self.mysqlUser)
        else:
            gLogger.warn("MySQL password not found")

        self.mysqlHost = self.localCfg.getOption(cfgInstallPath("Database", "Host"), "")
        if self.mysqlHost:
            gLogger.verbose("Using MySQL Host from local configuration", self.mysqlHost)
        else:
            gLogger.warn("Using the same host for MySQL as dirac services")
            self.mysqlHost = self.host

        self.mysqlPort = self.localCfg.getOption(cfgInstallPath("Database", "Port"), 0)
        if self.mysqlPort:
            gLogger.verbose("Using MySQL Port from local configuration ", self.mysqlPort)
        else:
            gLogger.warn("Using port '3306' as MySQL port")
            self.mysqlPort = 3306

        self.mysqlRootUser = self.localCfg.getOption(cfgInstallPath("Database", "RootUser"), "")
        if self.mysqlRootUser:
            gLogger.verbose("Using MySQL root user from local configuration ", self.mysqlRootUser)
        else:
            gLogger.warn("Using 'root' as root MySQL user")
            self.mysqlRootUser = "root"

        self.mysqlMode = self.localCfg.getOption(cfgInstallPath("Database", "MySQLMode"), "")
        if self.mysqlMode:
            gLogger.verbose("Configuring MySQL server as %s" % self.mysqlMode)

        # Now some noSQL defaults
        self.noSQLHost = self.localCfg.getOption(cfgInstallPath("NoSQLDatabase", "Host"), "")
        if self.noSQLHost:
            gLogger.verbose("Using NoSQL Host from local configuration", self.noSQLHost)
        else:
            gLogger.warn("Using the same host for NoSQL as dirac services")
            self.noSQLHost = self.host

        self.noSQLPort = self.localCfg.getOption(cfgInstallPath("NoSQLDatabase", "Port"), 0)
        if self.noSQLPort:
            gLogger.verbose("Using NoSQL Port from local configuration ", self.noSQLPort)
        else:
            gLogger.warn("Using the default port 9200")
            self.noSQLPort = 9200

        self.noSQLUser = self.localCfg.getOption(cfgInstallPath("NoSQLDatabase", "User"), self.noSQLUser)
        if self.noSQLUser:
            gLogger.verbose("Reading NoSQL User from local configuration")
        else:
            gLogger.warn("NoSQL user not found")

        self.noSQLPassword = self.localCfg.getOption(cfgInstallPath("NoSQLDatabase", "Password"), self.noSQLPassword)
        if self.noSQLPassword:
            gLogger.verbose("Reading %s NoSQL Password from local configuration " % self.noSQLUser)
        else:
            gLogger.warn("NoSQL password not found")

        self.noSQLSSL = self.localCfg.getOption(cfgInstallPath("NoSQLDatabase", "SSL"), self.noSQLSSL)
        if self.noSQLSSL:
            gLogger.verbose("Reading NoSQL SSL choice from local configuration")
        else:
            gLogger.warn("NoSQL SSL choice not found")

        # Now ready to insert components in the Component Monitoring DB
        self.monitoringClient = ComponentMonitoringClient()
        gLogger.verbose("Client configured for Component Monitoring")

    def getInfo(self):
        result = getVersion()
        if not result["OK"]:
            return result
        rDict = result["Value"]
        rDict["Setup"] = self.setup or "Unknown"
        return S_OK(rDict)

    @deprecated("Use DIRAC.Core.Utilities.Extensions.extensionsByPriority instead")
    def getExtensions(self):
        """Get the list of installed extensions"""
        extensions = extensionsByPriority()
        try:
            extensions.remove("DIRAC")
        except Exception:
            error = "DIRAC is not properly installed"
            gLogger.exception(error)
            if self.exitOnError:
                DIRAC.exit(-1)
            return S_ERROR(error)
        return S_OK(extensions)

    def _addCfgToDiracCfg(self, cfg):
        """
        Merge cfg into existing dirac.cfg file
        """
        newCfg = self.localCfg.mergeWith(cfg) if str(self.localCfg) else cfg
        result = newCfg.writeToFile(self.cfgFile)
        if not result:
            return result
        self.loadDiracCfg()
        return result

    def _addCfgToCS(self, cfg):
        """
        Merge cfg into central CS
        """
        gLogger.debug("Adding CFG to CS:")
        gLogger.debug(cfg)

        cfgClient = CSAPI()
        result = cfgClient.downloadCSData()
        if not result["OK"]:
            return result
        result = cfgClient.mergeFromCFG(cfg)
        if not result["OK"]:
            return result
        result = cfgClient.commit()
        return result

    def _addCfgToLocalCS(self, cfg):
        """
        Merge cfg into local CS
        """
        csName = self.localCfg.getOption(cfgPath("DIRAC", "Configuration", "Name"), "")
        if not csName:
            error = "Missing %s" % cfgPath("DIRAC", "Configuration", "Name")
            if self.exitOnError:
                gLogger.error(error)
                DIRAC.exit(-1)
            return S_ERROR(error)

        csCfg = CFG()
        csFile = os.path.join(rootPath, "etc", "%s.cfg" % csName)
        if os.path.exists(csFile):
            csCfg.loadFromFile(csFile)
        newCfg = csCfg.mergeWith(cfg) if str(csCfg) else cfg
        return newCfg.writeToFile(csFile)

    def _removeOptionFromCS(self, path):
        """
        Delete options from central CS
        """
        cfgClient = CSAPI()
        result = cfgClient.downloadCSData()
        if not result["OK"]:
            return result
        result = cfgClient.delOption(path)
        if not result["OK"]:
            return result
        result = cfgClient.commit()
        return result

    def _removeSectionFromCS(self, path):
        """
        Delete setions from central CS
        """
        cfgClient = CSAPI()
        result = cfgClient.downloadCSData()
        if not result["OK"]:
            return result
        result = cfgClient.delSection(path)
        if not result["OK"]:
            return result
        result = cfgClient.commit()
        return result

    def _getCentralCfg(self, installCfg):
        """
        Create the skeleton of central Cfg for an initial Master CS
        """
        # First copy over from installation cfg
        centralCfg = CFG()

        # DIRAC/Extensions
        extensions = self.localCfg.getOption(cfgInstallPath("Extensions"), [])
        centralCfg.createNewSection("DIRAC", "")
        if extensions:
            centralCfg["DIRAC"].addKey("Extensions", ",".join(extensions), "")  # pylint: disable=no-member

        vo = self.localCfg.getOption(cfgInstallPath("VirtualOrganization"), "")
        if vo:
            centralCfg["DIRAC"].addKey("VirtualOrganization", vo, "")  # pylint: disable=no-member

        for section in ["Systems", "Resources", "Resources/Sites", "Operations", "Registry"]:
            if installCfg.isSection(section):
                centralCfg.createNewSection(section, contents=installCfg[section])

        # Now try to add things from the Installation section
        # Registry
        adminUserName = self.localCfg.getOption(cfgInstallPath("AdminUserName"), "")
        adminUserDN = self.localCfg.getOption(cfgInstallPath("AdminUserDN"), "")
        adminUserEmail = self.localCfg.getOption(cfgInstallPath("AdminUserEmail"), "")
        adminGroupName = self.localCfg.getOption(cfgInstallPath("AdminGroupName"), "dirac_admin")
        hostDN = self.localCfg.getOption(cfgInstallPath("HostDN"), "")
        defaultGroupName = self.localCfg.getOption(cfgInstallPath("DefaultGroupName"), "dirac_user")
        adminGroupProperties = [
            ALARMS_MANAGEMENT,
            SERVICE_ADMINISTRATOR,
            CS_ADMINISTRATOR,
            JOB_ADMINISTRATOR,
            FULL_DELEGATION,
            PROXY_MANAGEMENT,
            OPERATOR,
        ]
        defaultGroupProperties = [NORMAL_USER]
        defaultHostProperties = [
            TRUSTED_HOST,
            CS_ADMINISTRATOR,
            JOB_ADMINISTRATOR,
            FULL_DELEGATION,
            PROXY_MANAGEMENT,
            OPERATOR,
        ]

        for section in (
            cfgPath("Registry"),
            cfgPath("Registry", "Users"),
            cfgPath("Registry", "Groups"),
            cfgPath("Registry", "Hosts"),
        ):
            if not centralCfg.isSection(section):
                centralCfg.createNewSection(section)

        # pylint: disable=unsubscriptable-object
        if adminUserName:
            if not (adminUserDN and adminUserEmail):
                gLogger.error("AdminUserName is given but DN or Mail is missing it will not be configured")
            else:
                for section in [
                    cfgPath("Registry", "Users", adminUserName),
                    cfgPath("Registry", "Groups", defaultGroupName),
                    cfgPath("Registry", "Groups", adminGroupName),
                ]:
                    if not centralCfg.isSection(section):
                        centralCfg.createNewSection(section)

                if centralCfg["Registry"].existsKey("DefaultGroup"):  # pylint: disable=no-member
                    centralCfg["Registry"].deleteKey("DefaultGroup")  # pylint: disable=no-member
                centralCfg["Registry"].addKey("DefaultGroup", defaultGroupName, "")  # pylint: disable=no-member

                if centralCfg["Registry"]["Users"][adminUserName].existsKey("DN"):
                    centralCfg["Registry"]["Users"][adminUserName].deleteKey("DN")
                centralCfg["Registry"]["Users"][adminUserName].addKey("DN", adminUserDN, "")

                if centralCfg["Registry"]["Users"][adminUserName].existsKey("Email"):
                    centralCfg["Registry"]["Users"][adminUserName].deleteKey("Email")
                centralCfg["Registry"]["Users"][adminUserName].addKey("Email", adminUserEmail, "")

                # Add Admin User to Admin Group and default group
                for group in [adminGroupName, defaultGroupName]:
                    if not centralCfg["Registry"]["Groups"][group].isOption("Users"):
                        centralCfg["Registry"]["Groups"][group].addKey("Users", "", "")
                    users = centralCfg["Registry"]["Groups"][group].getOption("Users", [])
                    if adminUserName not in users:
                        centralCfg["Registry"]["Groups"][group].appendToOption("Users", ", %s" % adminUserName)
                    if not centralCfg["Registry"]["Groups"][group].isOption("Properties"):
                        centralCfg["Registry"]["Groups"][group].addKey("Properties", "", "")

                properties = centralCfg["Registry"]["Groups"][adminGroupName].getOption("Properties", [])
                for prop in adminGroupProperties:
                    if prop not in properties:
                        properties.append(prop)
                        centralCfg["Registry"]["Groups"][adminGroupName].appendToOption("Properties", ", %s" % prop)

                properties = centralCfg["Registry"]["Groups"][defaultGroupName].getOption("Properties", [])
                for prop in defaultGroupProperties:
                    if prop not in properties:
                        properties.append(prop)
                        centralCfg["Registry"]["Groups"][defaultGroupName].appendToOption("Properties", ", %s" % prop)

        # Add the master Host description
        if hostDN:
            hostSection = cfgPath("Registry", "Hosts", self.host)
            if not centralCfg.isSection(hostSection):
                centralCfg.createNewSection(hostSection)
            if centralCfg["Registry"]["Hosts"][self.host].existsKey("DN"):
                centralCfg["Registry"]["Hosts"][self.host].deleteKey("DN")
            centralCfg["Registry"]["Hosts"][self.host].addKey("DN", hostDN, "")
            if not centralCfg["Registry"]["Hosts"][self.host].isOption("Properties"):
                centralCfg["Registry"]["Hosts"][self.host].addKey("Properties", "", "")
            properties = centralCfg["Registry"]["Hosts"][self.host].getOption("Properties", [])
            for prop in defaultHostProperties:
                if prop not in properties:
                    properties.append(prop)
                    centralCfg["Registry"]["Hosts"][self.host].appendToOption("Properties", ", %s" % prop)

        # Operations
        if adminUserEmail:
            operationsCfg = self.__getCfg(cfgPath("Operations", "Defaults", "EMail"), "Production", adminUserEmail)
            centralCfg = centralCfg.mergeWith(operationsCfg)
            operationsCfg = self.__getCfg(cfgPath("Operations", "Defaults", "EMail"), "Logging", adminUserEmail)
            centralCfg = centralCfg.mergeWith(operationsCfg)

        # Website
        websiteCfg = self.__getCfg(cfgPath("WebApp", "Access"), "upload", "TrustedHost")
        centralCfg = centralCfg.mergeWith(websiteCfg)

        return centralCfg

    def __getCfg(self, section, option="", value=""):
        """
        Create a new Cfg with given info
        """
        if not section:
            return None
        cfg = CFG()
        sectionList = []
        for sect in cfgPathToList(section):
            if not sect:
                continue
            sectionList.append(sect)
            cfg.createNewSection(cfgPath(*sectionList))
        if not sectionList:
            return None

        if option and value:
            sectionList.append(option)
            cfg.setOption(cfgPath(*sectionList), value)

        return cfg

    def addOptionToDiracCfg(self, option, value):
        """
        Add Option to dirac.cfg
        """
        optionList = cfgPathToList(option)
        optionName = optionList[-1]
        section = cfgPath(*optionList[:-1])
        cfg = self.__getCfg(section, optionName, value)

        if not cfg:
            return S_ERROR("Wrong option: %s = %s" % (option, value))

        if self._addCfgToDiracCfg(cfg):
            return S_OK()

        return S_ERROR("Could not merge %s=%s with local configuration" % (option, value))

    def removeComponentOptionsFromCS(self, system, component, mySetup=None):
        """
        Remove the section with Component options from the CS, if possible
        """
        if mySetup is None:
            mySetup = self.setup

        result = self.monitoringClient.getInstallations(
            {"UnInstallationTime": None, "Instance": component}, {"DIRACSystem": system}, {}, True
        )
        if not result["OK"]:
            return result
        installations = result["Value"]

        instanceOption = cfgPath("DIRAC", "Setups", mySetup, system)
        if gConfig:
            compInstance = gConfig.getValue(instanceOption, "")
        else:
            compInstance = self.localCfg.getOption(instanceOption, "")

        if len(installations) == 1:
            remove = True
            removeMain = False
            installation = installations[0]
            cType = installation["Component"]["Type"]

            # Is the component a rename of another module?
            isRenamed = installation["Instance"] != installation["Component"]["DIRACModule"]

            result = self.monitoringClient.getInstallations(
                {"UnInstallationTime": None},
                {"DIRACSystem": system, "DIRACModule": installation["Component"]["DIRACModule"]},
                {},
                True,
            )
            if not result["OK"]:
                return result
            installations = result["Value"]

            # If the component is not renamed we keep it in the CS if there are any renamed ones
            if not isRenamed:
                if len(installations) > 1:
                    remove = False
            # If the component is renamed and is the last one, we remove the entry for the main module as well
            else:
                if len(installations) == 1:
                    removeMain = True

            if remove:
                result = self._removeSectionFromCS(
                    cfgPath("Systems", system, compInstance, installation["Component"]["Type"].title() + "s", component)
                )
                if not result["OK"]:
                    return result

                if not isRenamed and cType == "service":
                    result = self._removeOptionFromCS(cfgPath("Systems", system, compInstance, "URLs", component))
                    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 removeMain:
                result = self._removeSectionFromCS(
                    cfgPath(
                        "Systems",
                        system,
                        compInstance,
                        installation["Component"]["Type"].title() + "s",
                        installation["Component"]["Module"],
                    )
                )
                if not result["OK"]:
                    return result

                if cType == "service":
                    result = self._removeOptionFromCS(
                        cfgPath("Systems", system, compInstance, "URLs", installation["Component"]["Module"])
                    )
                    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

            return S_OK("Successfully removed entries from CS")
        return S_OK("Instances of this component still exist. It won't be completely removed")

    def addDefaultOptionsToCS(
        self,
        gConfig_o,
        componentType,
        systemName,
        component,
        extensions,
        mySetup=None,
        specialOptions={},
        overwrite=False,
        addDefaultOptions=True,
    ):
        """
        Add the section with the component options to the CS
        """
        if mySetup is None:
            mySetup = self.setup

        if gConfig_o:
            gConfig_o.forceRefresh()

        system = systemName.replace("System", "")
        instanceOption = cfgPath("DIRAC", "Setups", mySetup, system)
        if gConfig_o:
            compInstance = gConfig_o.getValue(instanceOption, "")
        else:
            compInstance = self.localCfg.getOption(instanceOption, "")
        if not compInstance:
            return S_ERROR("%s not defined in %s" % (instanceOption, self.cfgFile))

        sectionName = _getSectionName(componentType)

        # Check if the component CS options exist
        addOptions = True
        componentSection = cfgPath("Systems", system, compInstance, sectionName, component)
        if not overwrite:
            if gConfig_o:
                result = gConfig_o.getOptions(componentSection)
                if result["OK"]:
                    addOptions = False

        if not addOptions:
            return S_OK("Component options already exist")

        # Add the component options now
        result = self.getComponentCfg(
            componentType, system, component, compInstance, extensions, specialOptions, addDefaultOptions
        )
        if not result["OK"]:
            return result
        compCfg = result["Value"]

        gLogger.notice("Adding to CS", "%s %s/%s" % (componentType, system, component))
        resultAddToCFG = self._addCfgToCS(compCfg)
        if componentType == "executor":
            # Is it a container ?
            execList = compCfg.getOption("%s/Load" % componentSection, [])
            for element in execList:
                result = self.addDefaultOptionsToCS(
                    gConfig_o, componentType, systemName, element, extensions, self.setup, {}, overwrite
                )
                if not result["OK"]:
                    gLogger.warn("Can't add to default CS", result["Message"])
                resultAddToCFG.setdefault("Modules", {})
                resultAddToCFG["Modules"][element] = result["OK"]
        return resultAddToCFG

    def addDefaultOptionsToComponentCfg(self, componentType, systemName, component, extensions):
        """
        Add default component options local component cfg
        """
        system = systemName.replace("System", "")
        instanceOption = cfgPath("DIRAC", "Setups", self.setup, system)
        compInstance = self.localCfg.getOption(instanceOption, "")
        if not compInstance:
            return S_ERROR("%s not defined in %s" % (instanceOption, self.cfgFile))

        # Add the component options now
        result = self.getComponentCfg(componentType, system, component, compInstance, extensions)
        if not result["OK"]:
            return result
        compCfg = result["Value"]

        compCfgFile = os.path.join(rootPath, "etc", "%s_%s.cfg" % (system, component))
        if compCfg.writeToFile(compCfgFile):  # this returns a True/False
            return S_OK()
        return S_ERROR()

    def addCfgToComponentCfg(self, componentType, systemName, component, cfg):
        """
        Add some extra configuration to the local component cfg
        """
        sectionName = _getSectionName(componentType)

        if not cfg:
            return S_OK()
        system = systemName.replace("System", "")
        instanceOption = cfgPath("DIRAC", "Setups", self.setup, system)
        compInstance = self.localCfg.getOption(instanceOption, "")

        if not compInstance:
            return S_ERROR("%s not defined in %s" % (instanceOption, self.cfgFile))
        compCfgFile = os.path.join(rootPath, "etc", "%s_%s.cfg" % (system, component))
        compCfg = CFG()
        if os.path.exists(compCfgFile):
            compCfg.loadFromFile(compCfgFile)
        sectionPath = cfgPath("Systems", system, compInstance, sectionName)

        newCfg = self.__getCfg(sectionPath)
        newCfg.createNewSection(cfgPath(sectionPath, component), "Added by ComponentInstaller", cfg)
        if newCfg.writeToFile(compCfgFile):
            return S_OK(compCfgFile)
        error = "Can not write %s" % compCfgFile
        gLogger.error(error)
        return S_ERROR(error)

    def getComponentCfg(
        self, componentType, system, component, compInstance, extensions, specialOptions={}, addDefaultOptions=True
    ):
        """
        Get the CFG object of the component configuration
        """
        sectionName = _getSectionName(componentType)

        componentModule = specialOptions.get("Module", component)
        compCfg = CFG()
        if addDefaultOptions:
            for ext in extensions:
                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.loadFromBuffer(cfgTemplate)
                compCfg = loadCfg.mergeWith(compCfg)

            compPath = cfgPath(sectionName, componentModule)
            if not compCfg.isSection(compPath):
                error = "Can not find %s in template" % compPath
                gLogger.error(error)
                if self.exitOnError:
                    DIRAC.exit(-1)
                return S_ERROR(error)

            compCfg = compCfg[sectionName][componentModule]  # pylint: disable=unsubscriptable-object

            # Delete Dependencies section if any
            compCfg.deleteKey("Dependencies")

        sectionPath = cfgPath("Systems", system, compInstance, sectionName)
        cfg = self.__getCfg(sectionPath)
        cfg.createNewSection(cfgPath(sectionPath, component), "", compCfg)

        for option, value in specialOptions.items():
            cfg.setOption(cfgPath(sectionPath, component, option), value)

        # Add the service URL
        if componentType == "service":
            port = compCfg.getOption("Port", 0)
            if port and self.host:
                urlsPath = cfgPath("Systems", system, compInstance, "URLs")
                cfg.createNewSection(urlsPath)
                failoverUrlsPath = cfgPath("Systems", system, compInstance, "FailoverURLs")
                cfg.createNewSection(failoverUrlsPath)
                cfg.setOption(cfgPath(urlsPath, component), "dips://%s:%d/%s/%s" % (self.host, port, system, component))

        return S_OK(cfg)

    def addDatabaseOptionsToCS(self, gConfig_o, systemName, dbName, mySetup=None, overwrite=False):
        """
        Add the section with the database options to the CS
        """
        if mySetup is None:
            mySetup = self.setup

        if gConfig_o:
            gConfig_o.forceRefresh()

        system = systemName.replace("System", "")
        instanceOption = cfgPath("DIRAC", "Setups", mySetup, system)
        if gConfig_o:
            compInstance = gConfig_o.getValue(instanceOption, "")
        else:
            compInstance = self.localCfg.getOption(instanceOption, "")
        if not compInstance:
            return S_ERROR("%s not defined in %s" % (instanceOption, self.cfgFile))

        # Check if the component CS options exist
        addOptions = True
        if not overwrite:
            databasePath = cfgPath("Systems", system, compInstance, "Databases", dbName)
            result = gConfig_o.getOptions(databasePath)
            if result["OK"]:
                addOptions = False
        if not addOptions:
            return S_OK("Database options already exist")

        # Add the component options now
        result = self.getDatabaseCfg(system, dbName, compInstance)
        if not result["OK"]:
            return result
        databaseCfg = result["Value"]
        gLogger.notice("Adding to CS", "%s/%s" % (system, dbName))
        return self._addCfgToCS(databaseCfg)

    def removeDatabaseOptionsFromCS(self, gConfig_o, system, dbName, mySetup=None):
        """
        Remove the section with database options from the CS, if possible
        """
        if mySetup is None:
            mySetup = self.setup

        result = self.monitoringClient.installationExists(
            {"UnInstallationTime": None}, {"DIRACSystem": system, "Type": "DB", "DIRACModule": dbName}, {}
        )
        if not result["OK"]:
            return result
        exists = result["Value"]

        instanceOption = cfgPath("DIRAC", "Setups", mySetup, system)
        if gConfig_o:
            compInstance = gConfig_o.getValue(instanceOption, "")
        else:
            compInstance = self.localCfg.getOption(instanceOption, "")

        if not exists:
            result = self._removeSectionFromCS(cfgPath("Systems", system, compInstance, "Databases", dbName))
            if not result["OK"]:
                return result

        return S_OK("Successfully removed entries from CS")

    def getDatabaseCfg(self, system, dbName, compInstance):
        """
        Get the CFG object of the database configuration
        """
        databasePath = cfgPath("Systems", system, compInstance, "Databases", dbName)
        cfg = self.__getCfg(databasePath, "DBName", dbName)
        cfg.setOption(cfgPath(databasePath, "Host"), self.mysqlHost)
        cfg.setOption(cfgPath(databasePath, "Port"), self.mysqlPort)

        return S_OK(cfg)

    def addSystemInstance(self, systemName, compInstance, mySetup=None, myCfg=False):
        """
        Add a new system self.instance to dirac.cfg and CS
        """
        if mySetup is None:
            mySetup = self.setup

        system = systemName.replace("System", "")
        gLogger.notice(
            "Adding %s system as %s self.instance for %s self.setup to dirac.cfg and CS"
            % (system, compInstance, mySetup)
        )

        cfg = self.__getCfg(cfgPath("DIRAC", "Setups", mySetup), system, compInstance)
        if myCfg:
            if not self._addCfgToDiracCfg(cfg):
                return S_ERROR("Failed to add system self.instance to dirac.cfg")

        return self._addCfgToCS(cfg)

    def printStartupStatus(self, rDict):
        """
        Print in nice format the return dictionary from self.getStartupComponentStatus
        (also returned by self.runsvctrlComponent)
        """
        fields = ["Name", "Runit", "Uptime", "PID"]
        records = []
        try:
            for comp in rDict:
                records.append([comp, rDict[comp]["RunitStatus"], rDict[comp]["Timeup"], str(rDict[comp]["PID"])])
            printTable(fields, records)
        except Exception as x:
            print("Exception while gathering data for printing: %s" % str(x))
        return S_OK()

    def printOverallStatus(self, rDict):
        """
        Print in nice format the return dictionary from self.getOverallStatus
        """
        fields = ["System", "Name", "Type", "Setup", "Installed", "Runit", "Uptime", "PID"]
        records = []
        try:
            for compType in rDict:
                for system in rDict[compType]:
                    for component in rDict[compType][system]:
                        record = [system, component, compType.lower()[:-1]]
                        if rDict[compType][system][component]["Setup"]:
                            record.append("SetUp")
                        else:
                            record.append("NotSetUp")
                        if rDict[compType][system][component]["Installed"]:
                            record.append("Installed")
                        else:
                            record.append("NotInstalled")
                        record.append(str(rDict[compType][system][component]["RunitStatus"]))
                        record.append(str(rDict[compType][system][component]["Timeup"]))
                        record.append(str(rDict[compType][system][component]["PID"]))
                        records.append(record)
            printTable(fields, records)
        except Exception as x:
            print("Exception while gathering data for printing: %s" % str(x))

        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"""
        return list(findSystems(extensions))

    def getSoftwareComponents(self, extensions):
        """
        Get the list of all the components (services, agents, executors) for which the software
        is installed on the system
        """
        # The Gateway does not need a handler
        services = defaultdict(list, {"Framework": ["Gateway"]})
        agents = defaultdict(list)
        executors = defaultdict(list)
        remainders = defaultdict(lambda: defaultdict(list))

        # Components other than services, agents and executors
        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):
        """
        Get the list of all the components (services, agents, executors)
        installed on the system in the runit directory
        """
        result = self.resultIndexes(self.componentTypes)
        if not result["OK"]:
            return result
        resultIndexes = result["Value"]

        resultDict = defaultdict(lambda: defaultdict(list))
        for system in os.listdir(self.runitDir):
            systemDir = os.path.join(self.runitDir, system)
            for component in os.listdir(systemDir):
                runFile = os.path.join(systemDir, component, "run")
                try:
                    with io.open(runFile, "rt") as rFile:
                        body = rFile.read()
                except IOError:
                    pass
                else:
                    for cType in self.componentTypes:
                        if "dirac-%s" % cType in body or (cType.lower() == "service" and "tornado-start-all" in body):
                            resultDict[cType][system].append(component)

        return S_OK({resultIndexes[cType]: dict(resultDict[cType]) for cType in self.componentTypes})

    def getSetupComponents(self):
        """
        Get the list of all the components (services, agents, executors)
        set up for running with runsvdir in startup directory
        """
        if not os.path.isdir(self.startDir):
            return S_ERROR("Startup Directory does not exit: %s" % self.startDir)

        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:
                with io.open(runFile, "rt") as rfile:
                    body = rfile.read()
            except IOError:
                pass
            else:
                for cType in self.componentTypes:
                    if "dirac-%s" % cType in body or (cType.lower() == "service" and "tornado-start-all" in body):
                        system, compT = component.split("_", 1)
                        resultDict[cType][system].append(compT)

        return S_OK({resultIndexes[cType]: dict(resultDict[cType]) for cType in self.componentTypes})

    def getStartupComponentStatus(self, componentTupleList):
        """
        Get the list of all the components (services, agents, executors)
        set up for running with runsvdir in startup directory
        """
        try:
            if componentTupleList:
                cList = []
                for componentTuple in componentTupleList:
                    cList.extend(glob.glob(os.path.join(self.startDir, "_".join(componentTuple))))
            else:
                cList = glob.glob(os.path.join(self.startDir, "*"))
        except Exception:
            error = "Failed to parse List of Components"
            gLogger.exception(error)
            if self.exitOnError:
                DIRAC.exit(-1)
            return S_ERROR(error)

        result = self.execCommand(0, ["runsvstat"] + cList)
        if not result["OK"]:
            return result
        output = result["Value"][1].strip().split("\n")

        componentDict = {}
        for line in output:
            if not line:
                continue
            cname, routput = line.split(":")
            cname = cname.replace("%s/" % self.startDir, "")
            run = False
            reResult = re.search("^ run", routput)
            if reResult:
                run = True
            down = False
            reResult = re.search("^ down", routput)
            if reResult:
                down = True
            reResult = re.search("([0-9]+) seconds", routput)
            timeup = 0
            if reResult:
                timeup = reResult.group(1)
            reResult = re.search("pid ([0-9]+)", routput)
            pid = 0
            if reResult:
                pid = reResult.group(1)
            runsv = "Not running"
            if run or down:
                runsv = "Running"
            reResult = re.search("runsv not running", routput)
            if reResult:
                runsv = "Not running"

            runDict = {}
            runDict["CPU"] = -1
            runDict["MEM"] = -1
            runDict["VSZ"] = -1
            runDict["RSS"] = -1
            if pid:  # check the process CPU usage and memory
                # PID %CPU %MEM VSZ
                result = self.execCommand(0, ["ps", "-p", pid, "u"])
                if result["OK"] and result["Value"]:
                    stats = result["Value"][1]
                    values = re.findall(r"\d*\.\d+|\d+", stats)
                    if values:
                        runDict["CPU"] = values[1]
                        runDict["MEM"] = values[2]
                        runDict["VSZ"] = values[3]
                        runDict["RSS"] = values[4]

            runDict["Timeup"] = timeup
            runDict["PID"] = pid
            runDict["RunitStatus"] = "Unknown"
            if run:
                runDict["RunitStatus"] = "Run"
            if down:
                runDict["RunitStatus"] = "Down"
            if runsv == "Not running":
                runDict["RunitStatus"] = "NoRunitControl"
            componentDict[cname] = runDict

        return S_OK(componentDict)

    def getComponentModule(self, system, component, compType):
        """
        Get the component software module
        """
        self.setup = CSGlobals.getSetup()
        self.instance = gConfig.getValue(cfgPath("DIRAC", "Setups", self.setup, system), "")
        if not self.instance:
            return S_OK(component)
        module = gConfig.getValue(cfgPath("Systems", system, self.instance, compType, component, "Module"), "")
        if not module:
            module = component
        return S_OK(module)

    def getOverallStatus(self, extensions):
        """
        Get the list of all the components (services, agents, executors)
        set up for running with runsvdir in startup directory
        """
        result = self.getSoftwareComponents(extensions)
        if not result["OK"]:
            return result
        softDict = result["Value"]

        result = self.getSetupComponents()
        if not result["OK"]:
            return result
        setupDict = result["Value"]

        result = self.getInstalledComponents()
        if not result["OK"]:
            return result
        installedDict = result["Value"]

        result = self.getStartupComponentStatus([])
        if not result["OK"]:
            return result
        runitDict = result["Value"]

        # Collect the info now
        result = self.resultIndexes(self.componentTypes)
        if not result["OK"]:
            return result
        resultIndexes = result["Value"]

        resultDict = defaultdict(lambda: defaultdict(dict))
        for cType in resultIndexes.values():
            if "Services" in softDict:
                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[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[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({k: dict(v) for k, v in resultDict.items()})

    def checkComponentModule(self, componentType, system, module):
        """
        Check existence of the given module
        and if it inherits from the proper class
        """
        if componentType == "agent":
            loader = ModuleLoader("Agent", PathFinder.getAgentSection, AgentModule)
        elif componentType == "service":
            loader = ModuleLoader("Service", PathFinder.getServiceSection, RequestHandler, moduleSuffix="Handler")
        elif componentType == "executor":
            loader = ModuleLoader("Executor", PathFinder.getExecutorSection, ExecutorModule)
        else:
            return S_ERROR("Unknown component type %s" % componentType)

        return loader.loadModule("%s/%s" % (system, module))

    def checkComponentSoftware(self, componentType, system, component, extensions):
        """
        Check the component software
        """
        result = self.getSoftwareComponents(extensions)
        if not result["OK"]:
            return result
        softComp = result["Value"]

        try:
            softDict = softComp[_getSectionName(componentType)]
        except KeyError:
            return S_ERROR("Unknown component type %s" % componentType)

        if system in softDict and component in softDict[system]:
            return S_OK()

        return S_ERROR("Unknown Component %s/%s" % (system, component))

    def runsvctrlComponent(self, system, component, mode):
        """
        Execute runsvctrl and check status of the specified component
        """
        if mode not in ["u", "d", "o", "p", "c", "h", "a", "i", "q", "1", "2", "t", "k", "x", "e"]:
            return S_ERROR('Unknown runsvctrl mode "%s"' % mode)

        startCompDirs = glob.glob(os.path.join(self.startDir, "%s_%s" % (system, component)))
        # Make sure that the Configuration server restarts first and the SystemAdmin restarts last
        tmpList = list(startCompDirs)
        for comp in tmpList:
            if "Framework_SystemAdministrator" in comp:
                startCompDirs.append(startCompDirs.pop(startCompDirs.index(comp)))
            if "Configuration_Server" in comp:
                startCompDirs.insert(0, startCompDirs.pop(startCompDirs.index(comp)))
        startCompList = [[k] for k in startCompDirs]
        for startComp in startCompList:
            result = self.execCommand(0, ["runsvctrl", mode] + startComp)
            if not result["OK"]:
                return result
            time.sleep(2)

        # Check the runsv status
        if system == "*" or component == "*":
            time.sleep(10)

        # Final check
        result = self.getStartupComponentStatus([(system, component)])
        if not result["OK"]:
            gLogger.error("Failed to start the component %s %s" % (system, component))
            return S_ERROR("Failed to start the component")

        return result

    def getLogTail(self, system, component, length=100):
        """
        Get the tail of the component log file
        """
        retDict = {}
        for startCompDir in glob.glob(os.path.join(self.startDir, "%s_%s" % (system, component))):
            compName = os.path.basename(startCompDir)
            logFileName = os.path.join(startCompDir, "log", "current")
            if not os.path.exists(logFileName):
                retDict[compName] = "No log file found"
            else:
                with io.open(logFileName, "rt") as logFile:
                    lines = [line.strip() for line in logFile.readlines()]
                retDict[compName] = "\n".join(lines[-length:])

        return S_OK(retDict)

    def setupSite(self, scriptCfg, cfg=None):
        """
        Setup a new site using the options defined
        """
        # First we need to find out what needs to be installed
        # by default use dirac.cfg, but if a cfg is given use it and
        # merge it into the dirac.cfg
        diracCfg = CFG()
        installCfg = None
        if cfg:
            try:
                installCfg = CFG()
                installCfg.loadFromFile(cfg)

                for section in ["DIRAC", "LocalSite", cfgInstallSection]:
                    if installCfg.isSection(section):
                        diracCfg.createNewSection(section, contents=installCfg[section])

                if self.instancePath != self.basePath:
                    if not diracCfg.isSection("LocalSite"):
                        diracCfg.createNewSection("LocalSite")
                    diracCfg.setOption(cfgPath("LocalSite", "InstancePath"), self.instancePath)

                self._addCfgToDiracCfg(diracCfg)
            except Exception:  # pylint: disable=broad-except
                error = "Failed to load %s" % cfg
                gLogger.exception(error)
                if self.exitOnError:
                    DIRAC.exit(-1)
                return S_ERROR(error)

        # Now get the necessary info from self.localCfg
        setupSystems = self.localCfg.getOption(cfgInstallPath("Systems"), ["Configuration", "Framework"])
        setupDatabases = self.localCfg.getOption(cfgInstallPath("Databases"), [])
        setupServices = [k.split("/") for k in self.localCfg.getOption(cfgInstallPath("Services"), [])]
        setupAgents = [k.split("/") for k in self.localCfg.getOption(cfgInstallPath("Agents"), [])]
        setupExecutors = [k.split("/") for k in self.localCfg.getOption(cfgInstallPath("Executors"), [])]
        setupWeb = self.localCfg.getOption(cfgInstallPath("WebPortal"), False)
        setupConfigurationMaster = self.localCfg.getOption(cfgInstallPath("ConfigurationMaster"), False)
        setupPrivateConfiguration = self.localCfg.getOption(cfgInstallPath("PrivateConfiguration"), False)
        setupConfigurationName = self.localCfg.getOption(cfgInstallPath("ConfigurationName"), self.setup)
        setupAddConfiguration = self.localCfg.getOption(cfgInstallPath("AddConfiguration"), True)

        for serviceTuple in setupServices:
            if len(serviceTuple) != 2:
                error = "Wrong service specification: system/service"
                if self.exitOnError:
                    gLogger.error(error)
                    DIRAC.exit(-1)
                return S_ERROR(error)
            serviceSysInstance = serviceTuple[0]
            if serviceSysInstance not in setupSystems:
                setupSystems.append(serviceSysInstance)

        for agentTuple in setupAgents:
            if len(agentTuple) != 2:
                error = "Wrong agent specification: system/agent"
                if self.exitOnError:
                    gLogger.error(error)
                    DIRAC.exit(-1)
                return S_ERROR(error)
            agentSysInstance = agentTuple[0]
            if agentSysInstance not in setupSystems:
                setupSystems.append(agentSysInstance)

        for executorTuple in setupExecutors:
            if len(executorTuple) != 2:
                error = "Wrong executor specification: system/executor"
                if self.exitOnError:
                    gLogger.error(error)
                    DIRAC.exit(-1)
                return S_ERROR(error)
            executorSysInstance = executorTuple[0]
            if executorSysInstance not in setupSystems:
                setupSystems.append(executorSysInstance)

        # And to find out the available extensions
        extensions = extensionsByPriority()

        # Make sure the necessary directories are there
        if self.basePath != self.instancePath:
            mkDir(self.instancePath)

            instanceEtcDir = os.path.join(self.instancePath, "etc")
            etcDir = os.path.dirname(self.cfgFile)
            if not os.path.exists(instanceEtcDir):
                mkLink(etcDir, instanceEtcDir)

            if os.path.realpath(instanceEtcDir) != os.path.realpath(etcDir):
                error = "Instance etc (%s) is not the same as DIRAC etc (%s)" % (instanceEtcDir, etcDir)
                if self.exitOnError:
                    gLogger.error(error)
                    DIRAC.exit(-1)
                return S_ERROR(error)

        # if any server or agent needs to be install we need the startup directory and runsvdir running
        if setupServices or setupAgents or setupExecutors or setupWeb:
            if not os.path.exists(self.startDir):
                mkDir(self.startDir)
            # And need to make sure runsvdir is running
            result = self.execCommand(0, ["ps", "-ef"])
            if not result["OK"]:
                if self.exitOnError:
                    gLogger.error("Failed to verify runsvdir running", result["Message"])
                    DIRAC.exit(-1)
                return S_ERROR(result["Message"])
            processList = result["Value"][1].split("\n")

            # it is pointless to look for more detailed command.
            # Nobody uses runsvdir.... so if it is there, it is us.
            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"],
                        stdout=devnull,
                        stderr=devnull,
                        universal_newlines=True,
                    )

        if ["Configuration", "Server"] in setupServices and setupConfigurationMaster:
            # 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")
            cfg.setOption(cfgPath("DIRAC", "Configuration", "Name"), setupConfigurationName)

            serversCfgPath = cfgPath("DIRAC", "Configuration", "Servers")
            if not self.localCfg.getOption(serversCfgPath, []):
                serverUrl = "dips://%s:9135/Configuration/Server" % self.host
                cfg.setOption(serversCfgPath, serverUrl)
                gConfigurationData.setOptionInCFG(serversCfgPath, serverUrl)
            instanceOptionPath = cfgPath("DIRAC", "Setups", self.setup)
            instanceCfg = self.__getCfg(instanceOptionPath, "Configuration", self.instance)
            cfg = cfg.mergeWith(instanceCfg)
            self._addCfgToDiracCfg(cfg)

            result = self.getComponentCfg(
                "service", "Configuration", "Server", self.instance, extensions, addDefaultOptions=True
            )
            if not result["OK"]:
                if self.exitOnError:
                    DIRAC.exit(-1)
                return result
            compCfg = result["Value"]
            cfg = cfg.mergeWith(compCfg)
            gConfigurationData.mergeWithLocal(cfg)

            self.addDefaultOptionsToComponentCfg("service", "Configuration", "Server", ["DIRAC"])
            centralCfg = self._getCentralCfg(installCfg or self.localCfg)
            self._addCfgToLocalCS(centralCfg)
            self.setupComponent("service", "Configuration", "Server", ["DIRAC"], checkModule=False)
            self.runsvctrlComponent("Configuration", "Server", "t")

            while ["Configuration", "Server"] in setupServices:
                setupServices.remove(["Configuration", "Server"])

        time.sleep(5)

        # Now need to check if there is valid CS to register the info
        result = scriptCfg.enableCS()
        if not result["OK"]:
            if self.exitOnError:
                DIRAC.exit(-1)
            return result

        cfgClient = CSAPI()
        if not cfgClient.initialize():
            error = "Configuration Server not defined"
            if self.exitOnError:
                gLogger.error(error)
                DIRAC.exit(-1)
            return S_ERROR(error)

        # We need to make sure components are connecting to the Master CS, that is the only one being update
        localServers = self.localCfg.getOption(cfgPath("DIRAC", "Configuration", "Servers"))
        masterServer = gConfig.getValue(cfgPath("DIRAC", "Configuration", "MasterServer"), "")
        initialCfg = self.__getCfg(cfgPath("DIRAC", "Configuration"), "Servers", localServers)
        masterCfg = self.__getCfg(cfgPath("DIRAC", "Configuration"), "Servers", masterServer)
        self._addCfgToDiracCfg(masterCfg)

        # 1.- Setup the instances in the CS
        # If the Configuration Server used is not the Master, it can take some time for this
        # info to be propagated, this may cause the later self.setup to fail
        if setupAddConfiguration:
            gLogger.notice("Registering System instances")
            for system in setupSystems:
                self.addSystemInstance(system, self.instance, self.setup, True)
            for system, service in setupServices:
                if not self.addDefaultOptionsToCS(None, "service", system, service, extensions, overwrite=True)["OK"]:
                    # If we are not allowed to write to the central CS, add the configuration to the local file
                    gLogger.warn(
                        "Can't write to central CS, so adding to the specific component CFG",
                        "for %s : %s" % (system, service),
                    )
                    res = self.addDefaultOptionsToComponentCfg("service", system, service, extensions)
                    if not res["OK"]:
                        gLogger.warn("Can't write to the specific component CFG")
            for system, agent in setupAgents:
                if not self.addDefaultOptionsToCS(None, "agent", system, agent, extensions, overwrite=True)["OK"]:
                    # If we are not allowed to write to the central CS, add the configuration to the local file
                    gLogger.warn("Can't write to central CS, so adding to the specific component CFG")
                    res = self.addDefaultOptionsToComponentCfg("agent", system, agent, extensions)
                    if not res["OK"]:
                        gLogger.warn("Can't write to the specific component CFG", "for %s : %s" % (system, agent))
            for system, executor in setupExecutors:
                if not self.addDefaultOptionsToCS(None, "executor", system, executor, extensions, overwrite=True)["OK"]:
                    # If we are not allowed to write to the central CS, add the configuration to the local file
                    gLogger.warn("Can't write to central CS, so adding to the specific component CFG")
                    res = self.addDefaultOptionsToComponentCfg("executor", system, executor, extensions)
                    if not res["OK"]:
                        gLogger.warn("Can't write to the specific component CFG", "for %s : %s" % (system, executor))
        else:
            gLogger.warn("Configuration parameters definition is not requested")

        if ["Configuration", "Server"] in setupServices and setupPrivateConfiguration:
            cfg = self.__getCfg(cfgPath("DIRAC", "Configuration"), "AutoPublish", "no")
            self._addCfgToDiracCfg(cfg)

        # 2.- Install requested Databases
        # if MySQL is not installed locally, we assume a host is given
        if setupDatabases:
            result = self.getDatabases()
            if not result["OK"]:
                if self.exitOnError:
                    gLogger.error("Failed to get databases", result["Message"])
                    DIRAC.exit(-1)
                return result
            installedDatabases = result["Value"]
            result = self.getAvailableDatabases()
            gLogger.debug("Available databases", result)
            if not result["OK"]:
                return result
            dbDict = result["Value"]

            for dbName in setupDatabases:
                gLogger.verbose("Setting up database", dbName)
                if dbName not in installedDatabases:
                    result = self.installDatabase(dbName)
                    if not result["OK"]:
                        gLogger.error(result["Message"])
                        DIRAC.exit(-1)
                    extension, system = result["Value"]
                    gLogger.notice("Database %s from %s/%s installed" % (dbName, extension, system))
                else:
                    gLogger.notice("Database %s already installed" % dbName)

                dbSystem = dbDict[dbName]["System"]
                result = self.addDatabaseOptionsToCS(None, dbSystem, dbName, overwrite=True)
                if not result["OK"]:
                    gLogger.error("Database %s CS registration failed: %s" % (dbName, result["Message"]))

        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 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:
            result = self.setupComponent("service", system, service, extensions)
            if not result["OK"]:
                gLogger.error(result["Message"])
                continue

        # 4.- Now the agents
        for system, agent in setupAgents:
            result = self.setupComponent("agent", system, agent, extensions)
            if not result["OK"]:
                gLogger.error(result["Message"])
                continue

        # 5.- Now the executors
        for system, executor in setupExecutors:
            result = self.setupComponent("executor", system, executor, extensions)
            if not result["OK"]:
                gLogger.error(result["Message"])
                continue

        # 6.- And finally the Portal
        if setupWeb:
            self.setupPortal()

        if localServers != masterServer:
            self._addCfgToDiracCfg(initialCfg)
            for system, service in setupServices:
                self.runsvctrlComponent(system, service, "t")
            for system, agent in setupAgents:
                self.runsvctrlComponent(system, agent, "t")
            for system, executor in setupExecutors:
                self.runsvctrlComponent(system, executor, "t")

        return S_OK()

    def _createRunitLog(self, runitCompDir):
        self.controlDir = os.path.join(runitCompDir, "control")
        mkDir(self.controlDir)

        logDir = os.path.join(runitCompDir, "log")
        mkDir(logDir)

        logConfigFile = os.path.join(logDir, "config")
        with io.open(logConfigFile, "w") as fd:
            fd.write(
                u"""s10000000
  n20
  """
            )

        logRunFile = os.path.join(logDir, "run")
        with io.open(logRunFile, "w") as fd:
            fd.write(
                u"""#!/bin/bash

rcfile=%(bashrc)s
[[ -e $rcfile ]] && source ${rcfile}
#
exec svlogd .
  """
                % {"bashrc": os.path.join(self.instancePath, "bashrc")}
            )

        os.chmod(logRunFile, self.gDefaultPerms)

    def installComponent(self, componentType, system, component, extensions, componentModule="", checkModule=True):
        """
        Install runit directory for the specified component
        """
        # Check if the component is already installed
        runitCompDir = os.path.join(self.runitDir, system, component)
        if os.path.exists(runitCompDir):
            msg = "%s %s_%s already installed" % (componentType, system, component)
            gLogger.notice(msg)
            return S_OK(runitCompDir)

        # Check that the software for the component is installed
        # 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 or component
            result = self.checkComponentModule(componentType, system, cModule)
            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))

        # Retrieve bash variables to be set
        result = gConfig.getOption("DIRAC/Setups/%s/%s" % (CSGlobals.getSetup(), system))
        if not result["OK"]:
            return result
        self.instance = result["Value"]

        specialOptions = {}
        if componentModule:
            specialOptions["Module"] = componentModule
        result = self.getComponentCfg(
            componentType, system, component, self.instance, extensions, specialOptions=specialOptions
        )
        if not result["OK"]:
            return result
        compCfg = result["Value"]

        section = _getSectionName(componentType)

        bashVars = ""
        if compCfg.isSection("Systems/%s/%s/%s/%s/Environment" % (system, self.instance, section, component)):
            dictionary = compCfg.getAsDict()
            bashSection = dictionary["Systems"][system][self.instance][section][component]["BashVariables"]
            for var in bashSection:
                bashVars = "%s\nexport %s=%s" % (bashVars, var, bashSection[var])

        # Now do the actual installation
        try:
            componentCfg = os.path.join(self.linkedRootPath, "etc", "%s_%s.cfg" % (system, component))
            if not os.path.exists(componentCfg):
                io.open(componentCfg, "w").close()

            self._createRunitLog(runitCompDir)

            runFile = os.path.join(runitCompDir, "run")
            with io.open(runFile, "w") as fd:
                fd.write(
                    u"""#!/bin/bash

rcfile=%(bashrc)s
[[ -e $rcfile ]] && source ${rcfile}
#
exec 2>&1
#
[[ "%(componentType)s" = "agent" ]] && renice 20 -p $$
#%(bashVariables)s
#
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("-", "_"),
                        "system": system,
                        "component": component,
                        "componentCfg": componentCfg,
                    }
                )

            os.chmod(runFile, self.gDefaultPerms)

            cTypeLower = componentType.lower()
            if cTypeLower == "agent":
                # This is, e.g., /opt/dirac/runit/WorkfloadManagementSystem/Matcher/control/t
                stopFile = os.path.join(runitCompDir, "control", "t")
                # This is, e.g., /opt/dirac/control/WorkfloadManagementSystem/Matcher/
                controlDir = self.runitDir.replace("runit", "control")
                with io.open(stopFile, "w") as fd:
                    fd.write(
                        u"""#!/bin/bash

echo %(controlDir)s/%(system)s/%(component)s/stop_%(type)s
touch %(controlDir)s/%(system)s/%(component)s/stop_%(type)s
"""
                        % {"controlDir": controlDir, "system": system, "component": component, "type": cTypeLower}
                    )

                os.chmod(stopFile, self.gDefaultPerms)

        except Exception:
            error = "Failed to prepare self.setup for %s %s/%s" % (componentType, system, component)
            gLogger.exception(error)
            if self.exitOnError:
                DIRAC.exit(-1)
            return S_ERROR(error)

        result = self.execCommand(5, [runFile])

        gLogger.notice(result["Value"][1])

        return S_OK(runitCompDir)

    def setupComponent(self, componentType, system, component, extensions, componentModule="", checkModule=True):
        """
        Install and create link in startup
        """
        result = self.installComponent(componentType, system, component, extensions, componentModule, checkModule)
        if not result["OK"]:
            return result

        # Create the startup entry now
        runitCompDir = result["Value"]
        startCompDir = os.path.join(self.startDir, "%s_%s" % (system, component))
        mkDir(self.startDir)
        if not os.path.lexists(startCompDir):
            gLogger.notice("Creating startup link at", startCompDir)
            mkLink(runitCompDir, startCompDir)

            # Wait for the service to be recognised (can't use isfile as supervise/ok is a device)
            start = time.time()
            while (time.time() - 10) < start:
                time.sleep(1)
                if os.path.exists(os.path.join(startCompDir, "supervise", "ok")):
                    break
            else:
                return S_ERROR("Failed to find supervise/ok for component %s_%s" % (system, component))

        # Check the runsv status
        start = time.time()
        while (time.time() - 20) < start:
            result = self.getStartupComponentStatus([(system, component)])
            if not result["OK"]:
                continue
            if result["Value"] and result["Value"]["%s_%s" % (system, component)]["RunitStatus"] == "Run":
                break
            time.sleep(1)
        else:
            return S_ERROR("Failed to start the component %s_%s" % (system, component))

        resDict = {}
        resDict["ComponentType"] = componentType
        resDict["RunitStatus"] = result["Value"]["%s_%s" % (system, component)]["RunitStatus"]

        return S_OK(resDict)

    def unsetupComponent(self, system, component):
        """
        Remove link from startup
        """
        for startCompDir in glob.glob(os.path.join(self.startDir, "%s_%s" % (system, component))):
            try:
                os.unlink(startCompDir)
            except Exception:
                gLogger.exception()

        return S_OK()

    def uninstallComponent(self, system, component, removeLogs):
        """
        Remove startup and runit directories
        """
        self.runsvctrlComponent(system, component, "d")

        self.unsetupComponent(system, component)

        if removeLogs:
            for runitCompDir in glob.glob(os.path.join(self.runitDir, system, component)):
                try:
                    shutil.rmtree(runitCompDir)
                except Exception:
                    gLogger.exception()

        result = self.removeComponentOptionsFromCS(system, component)
        if not result["OK"]:
            return result

        return S_OK()

    def setupPortal(self):
        """
        Install and create link in startup
        """
        result = self.installPortal()
        if not result["OK"]:
            return result

        # Create the startup entries now
        runitCompDir = result["Value"]
        startCompDir = os.path.join(self.startDir, "Web_WebApp")

        mkDir(self.startDir)

        mkLink(runitCompDir, startCompDir)

        time.sleep(5)

        # Check the runsv status
        start = time.time()
        while (time.time() - 10) < start:
            result = self.getStartupComponentStatus([("Web", "WebApp")])
            if not result["OK"]:
                return S_ERROR("Failed to start the Portal")
            if result["Value"] and result["Value"]["%s_%s" % ("Web", "WebApp")]["RunitStatus"] == "Run":
                break
            time.sleep(1)

        # Final check
        return self.getStartupComponentStatus([("Web", "WebApp")])

    def installPortal(self):
        """
        Install runit directories for the Web Portal
        """
        # Check that the software for the Web Portal is installed
        if six.PY2:
            webDir = os.path.join(self.linkedRootPath, "WebAppDIRAC")
            webappInstalled = os.path.exists(webDir)
        else:
            from importlib import metadata  # pylint: disable=no-name-in-module

            try:
                metadata.version("WebAppDIRAC")
            except metadata.PackageNotFoundError:
                webappInstalled = False
            else:
                webappInstalled = True

        if not webappInstalled:
            error = "WebApp extension not installed"
            gLogger.error(error)
            if self.exitOnError:
                gLogger.error(error)
                DIRAC.exit(-1)
            return S_ERROR(error)

        # Check if the component is already installed
        runitWebAppDir = os.path.join(self.runitDir, "Web", "WebApp")

        # Check if the component is already installed
        if os.path.exists(runitWebAppDir):
            msg = "Web Portal already installed"
            gLogger.notice(msg)
        else:
            gLogger.notice("Installing Web Portal")
            # Now do the actual installation
            try:
                self._createRunitLog(runitWebAppDir)
                runFile = os.path.join(runitWebAppDir, "run")
                with io.open(runFile, "w") as fd:
                    fd.write(
                        u"""#!/bin/bash

rcfile=%(bashrc)s
[[ -e $rcfile ]] && source $rcfile
#
exec 2>&1
#
exec dirac-webapp-run -p < /dev/null
  """
                        % {"bashrc": os.path.join(self.instancePath, "bashrc"), "DIRAC": self.linkedRootPath}
                    )

                os.chmod(runFile, self.gDefaultPerms)
            except Exception:
                error = "Failed to prepare self.setup for Web Portal"
                gLogger.exception(error)
                if self.exitOnError:
                    DIRAC.exit(-1)
                return S_ERROR(error)

            result = self.execCommand(5, [runFile])
            gLogger.notice(result["Value"][1])

        return S_OK(runitWebAppDir)

    def getMySQLPasswords(self):
        """
        Get MySQL passwords from local configuration or prompt
        """
        if not self.mysqlRootPwd:
            self.mysqlRootPwd = prompt(u"MySQL root password: ", is_password=True)

        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 = prompt(u"MySQL Dirac password: ", is_password=True)

        return S_OK()

    def setMySQLPasswords(self, root="", dirac=""):
        """
        Set MySQL passwords
        """
        if root:
            self.mysqlRootPwd = root
        if dirac:
            self.mysqlPassword = dirac

        return S_OK()

    def getMySQLStatus(self):
        """
        Get the status of the MySQL database installation
        """
        result = self.execCommand(0, ["mysqladmin", "status"])
        if not result["OK"]:
            return result

        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 = extensionsByPriority()

        res = self.getAvailableSQLDatabases(extensions)
        gLogger.debug("Available SQL databases", res)
        if not res["OK"]:
            return res
        sqlDBs = res["Value"]

        res = self.getAvailableESDatabases(extensions)
        gLogger.debug("Available ES databases", res)
        if not res["OK"]:
            return res
        esDBs = res["Value"]

        allDBs = sqlDBs.copy()
        allDBs.update(esDBs)

        return S_OK(allDBs)

    def getAvailableSQLDatabases(self, extensions):
        """
        Find the sql files

        :param list extensions: list of DIRAC extensions
        :return: dict of MySQL DBs
        """
        dbDict = {}
        for extension in extensions:
            databases = findDatabases(extensions)
            for systemName, dbSql in databases:
                dbName = dbSql.replace(".sql", "")
                dbDict[dbName] = {}
                dbDict[dbName]["Type"] = "MySQL"
                # TODO: Does this need to be replaced
                dbDict[dbName]["Extension"] = extension.replace("DIRAC", "")
                dbDict[dbName]["System"] = systemName.replace("System", "")
        return S_OK(dbDict)

    def getAvailableESDatabases(self, extensions):
        """
        Find the ES DBs definitions, by introspection.

        This method makes a few assumptions:
        - the files defining modules interacting with ES DBs are found in the xyzSystem/DB/ directories
        - the files defining modules interacting with ES DBs are named xyzDB.py (e.g. MonitoringDB.py)
        - the modules define ES DBs classes with the same name of the module (e.g. class MonitoringDB())
        - the classes are inheriting from the ElasticDB module (e.g. class MonitoringDB(ElasticDB))

        Result should be something like::

           {'MonitoringDB': {'Type': 'ES', 'System': 'Monitoring', 'Extension': ''},
            'ElasticJobParametersDB': {'Type': 'ES', 'System': 'WorkloadManagement', 'Extension': ''}}

        :param list extensions: list of DIRAC extensions
        :return: dict of ES DBs
        """
        dbDict = {}
        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([extension, systemName, "DB", dbName]))
                    dbClass = getattr(module, dbName)
                except (AttributeError, ImportError):
                    continue
                if "ElasticDB" not in str(inspect.getmro(dbClass)):
                    continue

                dbDict[dbName] = {}
                dbDict[dbName]["Type"] = "ES"
                # TODO: Does this need to be replaced
                dbDict[dbName]["Extension"] = extension.replace("DIRAC", "")
                dbDict[dbName]["System"] = systemName

        return S_OK(dbDict)

    def getDatabases(self):
        """
        Get the list of installed databases
        """
        result = self.execMySQL("SHOW DATABASES")
        if not result["OK"]:
            return result
        dbList = []
        for dbName in result["Value"]:
            if not dbName[0] in ["Database", "information_schema", "mysql", "test"]:
                dbList.append(dbName[0])

        return S_OK(dbList)

    def installDatabase(self, dbName):
        """
        Install requested DB in MySQL server
        """
        if six.PY3:
            dbName = MySQLdb.escape_string(dbName.encode()).decode()
        else:
            dbName = MySQLdb.escape_string(dbName)
        if not self.mysqlRootPwd:
            rootPwdPath = cfgInstallPath("Database", "RootPwd")
            return S_ERROR("Missing %s in %s" % (rootPwdPath, self.cfgFile))

        if not self.mysqlPassword:
            self.mysqlPassword = self.localCfg.getOption(
                cfgPath("Systems", "Databases", "Password"), self.mysqlPassword
            )
            if not self.mysqlPassword:
                mysqlPwdPath = cfgPath("Systems", "Databases", "Password")
                return S_ERROR("Missing %s in %s" % (mysqlPwdPath, self.cfgFile))

        gLogger.notice("Installing", dbName)

        # is there by chance an extension of it?
        for extension in extensionsByPriority():
            databases = {k: v for v, k in findDatabases(extension)}
            filename = dbName + ".sql"
            if filename in databases:
                break
        else:
            error = "Database %s not found" % dbName
            gLogger.error(error)
            if self.exitOnError:
                DIRAC.exit(-1)
            return S_ERROR(error)
        systemName = databases[filename]
        moduleName = ".".join([extension, systemName, "DB"])
        gLogger.debug("Installing %s from %s" % (filename, moduleName))
        dbSql = importlib_resources.read_text(moduleName, filename)

        # just check
        result = self.execMySQL("SHOW STATUS")
        if not result["OK"]:
            error = "Could not connect to MySQL server"
            gLogger.error(error)
            if self.exitOnError:
                DIRAC.exit(-1)
            return S_ERROR(error)
        gLogger.debug("SHOW STATUS : OK")

        # now creating the Database
        result = self.execMySQL("CREATE DATABASE `%s`" % dbName)
        if not result["OK"] and "database exists" not in result["Message"]:
            gLogger.error("Failed to create databases", result["Message"])
            if self.exitOnError:
                DIRAC.exit(-1)
            return result
        gLogger.debug("CREATE DATABASES : OK")

        perms = (
            "SELECT,INSERT,LOCK TABLES,UPDATE,DELETE,CREATE,DROP,ALTER,REFERENCES,"
            "CREATE VIEW,SHOW VIEW,INDEX,TRIGGER,ALTER ROUTINE,CREATE ROUTINE"
        )
        cmd = "GRANT %s ON `%s`.* TO '%s'@'%%'" % (perms, dbName, self.mysqlUser)
        result = self.execMySQL(cmd)
        if not result["OK"]:
            error = "Error executing '%s'" % cmd
            gLogger.error(error, result["Message"])
            if self.exitOnError:
                DIRAC.exit(-1)
            return S_ERROR(error)
        gLogger.debug("%s : OK" % cmd, result["Value"])
        result = self.execMySQL("FLUSH PRIVILEGES")
        if not result["OK"]:
            gLogger.error("Failed to flush privileges", result["Message"])
            if self.exitOnError:
                exit(-1)
            return result
        gLogger.debug("FLUSH PRIVILEGES : OK")

        # first getting the lines to be executed, and then execute them
        try:
            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
            sqlString = "\n".join(x for x in cmdLines if not x.startswith("--"))

            # Now run each command (They are seperated by ;)
            # Ignore any empty ones
            cmds = [x.strip() for x in sqlString.split(";") if x.strip()]
            for cmd in cmds:
                result = self.execMySQL(cmd, dbName)
                if not result["OK"]:
                    error = "Failed to initialize Database"
                    gLogger.notice(cmd)
                    gLogger.error(error, result["Message"])
                    if self.exitOnError:
                        DIRAC.exit(-1)
                    return S_ERROR(error)

        except Exception as e:
            gLogger.error(str(e))
            if self.exitOnError:
                DIRAC.exit(-1)
            return S_ERROR(error)

        return S_OK([extension, systemName])

    def uninstallDatabase(self, gConfig_o, dbName):
        """
        Remove a database from DIRAC
        """
        result = self.getAvailableDatabases()
        if not result["OK"]:
            return result

        dbSystem = result["Value"][dbName]["System"]

        result = self.removeDatabaseOptionsFromCS(gConfig_o, dbSystem, dbName)
        if not result["OK"]:
            return result

        return S_OK("DB successfully uninstalled")

    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 = []
        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", "")
                gLogger.info("Found file to source: %s" % sourcedDBbFileName)
                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())

            # Creating/adding cmdLines
            else:
                if line.strip():
                    cmdLines.append(line.strip())

        return cmdLines

    def execMySQL(self, cmd, dbName="mysql", localhost=False):
        """
        Execute MySQL Command
        """
        if not self.mysqlRootPwd:
            return S_ERROR("MySQL root password is not defined")
        if dbName not in self.db:
            dbHost = self.mysqlHost
            if localhost:
                dbHost = "localhost"
            self.db[dbName] = MySQL(dbHost, self.mysqlRootUser, self.mysqlRootPwd, dbName, self.mysqlPort)
        if not self.db[dbName]._connected:
            error = "Could not connect to MySQL server"
            gLogger.error(error)
            if self.exitOnError:
                DIRAC.exit(-1)
            return S_ERROR(error)
        return self.db[dbName]._query(cmd)

    def _addMySQLToDiracCfg(self):
        """
        Add the database access info to the local configuration
        """
        if not self.mysqlPassword:
            return S_ERROR("Missing %s in %s" % (cfgInstallPath("Database", "Password"), self.cfgFile))

        sectionPath = cfgPath("Systems", "Databases")
        cfg = self.__getCfg(sectionPath, "User", self.mysqlUser)
        cfg.setOption(cfgPath(sectionPath, "Password"), self.mysqlPassword)
        cfg.setOption(cfgPath(sectionPath, "Host"), self.mysqlHost)
        cfg.setOption(cfgPath(sectionPath, "Port"), self.mysqlPort)

        return self._addCfgToDiracCfg(cfg)

    def _addNoSQLToDiracCfg(self):
        """
        Add the NoSQL database access info to the local configuration
        """
        sectionPath = cfgPath("Systems", "NoSQLDatabases")
        cfg = self.__getCfg(sectionPath, "Host", self.noSQLHost)
        cfg.setOption(cfgPath(sectionPath, "Port"), self.noSQLPort)
        if self.noSQLUser:
            cfg.setOption(cfgPath(sectionPath, "User"), self.noSQLUser)
        if self.noSQLPassword:
            cfg.setOption(cfgPath(sectionPath, "Password"), self.noSQLPassword)
        if self.noSQLSSL:
            cfg.setOption(cfgPath(sectionPath, "SSL"), self.noSQLSSL)

        return self._addCfgToDiracCfg(cfg)

    def execCommand(self, timeout, cmd):
        """
        Execute command tuple and handle Error cases
        """
        gLogger.debug("Executing command %s with timeout %d" % (cmd, timeout))
        result = systemCall(timeout, cmd)
        if not result["OK"]:
            if timeout and result["Message"].startswith("Timeout"):
                return result
            gLogger.error("Failed to execute", "%s: %s" % (cmd[0], result["Message"]))
            if self.exitOnError:
                DIRAC.exit(-1)
            return result

        if result["Value"][0]:
            error = "Failed to execute"
            gLogger.error(error, cmd[0])
            gLogger.error("Exit code:", ("%s\n" % result["Value"][0]) + "\n".join(result["Value"][1:]))
            if self.exitOnError:
                DIRAC.exit(-1)
            error = S_ERROR(error)
            error["Value"] = result["Value"]
            return error

        gLogger.verbose(result["Value"][1])

        return result

    def installTornado(self):
        """
        Install runit directory for the tornado, and add the configuration of the required service
        """
        # Check if the Tornado itself is already installed
        runitCompDir = os.path.join(self.runitDir, "Tornado", "Tornado")
        if os.path.exists(runitCompDir):
            msg = "Tornado_Tornado already installed"
            gLogger.notice(msg)
            return S_OK(runitCompDir)

        # Check the setup for the given system
        result = gConfig.getOption("DIRAC/Setups/%s/Tornado" % (CSGlobals.getSetup()))
        if not result["OK"]:
            return result
        self.instance = result["Value"]

        # Now do the actual installation
        try:

            self._createRunitLog(runitCompDir)

            runFile = os.path.join(runitCompDir, "run")
            with io.open(runFile, "wt") as fd:
                fd.write(
                    u"""#!/bin/bash
rcfile=%(bashrc)s
[ -e $rcfile ] && source $rcfile
#
export DIRAC_USE_TORNADO_IOLOOP=Yes
exec 2>&1
#
#
exec tornado-start-all
"""
                    % {"bashrc": os.path.join(self.instancePath, "bashrc")}
                )

            os.chmod(runFile, self.gDefaultPerms)

        except Exception:
            error = "Failed to prepare self.setup forTornado"
            gLogger.exception(error)
            if self.exitOnError:
                DIRAC.exit(-1)
            return S_ERROR(error)

        result = self.execCommand(5, [runFile])

        gLogger.notice(result["Value"][1])

        return S_OK(runitCompDir)

    def addTornadoOptionsToCS(self, gConfig_o):
        """
        Add the section with the component options to the CS
        """
        if gConfig_o:
            gConfig_o.forceRefresh()

        instanceOption = cfgPath("DIRAC", "Setups", self.setup, "Tornado")

        if gConfig_o:
            compInstance = gConfig_o.getValue(instanceOption, "")
        else:
            compInstance = self.localCfg.getOption(instanceOption, "")
        if not compInstance:
            return S_ERROR("%s not defined in %s" % (instanceOption, self.cfgFile))
        tornadoSection = cfgPath("Systems", "Tornado", compInstance)

        cfg = self.__getCfg(tornadoSection, "Port", 8443)
        # cfg.setOption(cfgPath(tornadoSection, 'Password'), self.mysqlPassword)
        return self._addCfgToCS(cfg)

    def setupTornadoService(self, system, component, extensions, componentModule="", checkModule=True):
        """
        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
        system = "Tornado"
        component = "Tornado"
        componentType = "Tornado"
        runitCompDir = os.path.join(self.runitDir, "Tornado", "Tornado")
        startCompDir = os.path.join(self.startDir, "%s_%s" % (system, component))
        mkDir(self.startDir)
        if not os.path.lexists(startCompDir):
            gLogger.notice("Creating startup link at", startCompDir)
            mkLink(runitCompDir, startCompDir)

            # Wait for the service to be recognised (can't use isfile as supervise/ok is a device)
            start = time.time()
            while (time.time() - 10) < start:
                time.sleep(1)
                if os.path.exists(os.path.join(startCompDir, "supervise", "ok")):
                    break
            else:
                return S_ERROR("Failed to find supervise/ok for component %s_%s" % (system, component))

        # Check the runsv status
        start = time.time()
        while (time.time() - 20) < start:
            result = self.getStartupComponentStatus([(system, component)])
            if not result["OK"]:
                continue
            if result["Value"] and result["Value"]["%s_%s" % (system, component)]["RunitStatus"] == "Run":
                break
            time.sleep(1)
        else:
            return S_ERROR("Failed to start the component %s_%s" % (system, component))

        resDict = {}
        resDict["ComponentType"] = componentType
        resDict["RunitStatus"] = result["Value"]["%s_%s" % (system, component)]["RunitStatus"]

        return S_OK(resDict)

        # port = compCfg.getOption('Port', 0)
        # if port and self.host:
        #   urlsPath = cfgPath('Systems', system, compInstance, 'URLs')
        #   cfg.createNewSection(urlsPath)
        #   failoverUrlsPath = cfgPath('Systems', system, compInstance, 'FailoverURLs')
        #   cfg.createNewSection(failoverUrlsPath)
        #   cfg.setOption(cfgPath(urlsPath, component),
        #                 'dips://%s:%d/%s/%s' % (self.host, port, system, component))


gComponentInstaller = ComponentInstaller()
