Exemple #1
0
def get_database_config(parsed: ConfigParser, manager: ConfigManager) -> dict:
    """
    Generate a populated
    configuration dictionary.
    TODO: This should be shared
    with dbManager.py.

    :param parsed: ConfigParser
    :param manager: ConfigManager
    :return: Dictionary
    """
    config: dict = {}

    config['engine']: str = parsed.get('database', 'engine') if \
        parsed.has_option('database', 'engine') else \
        'sqlite'

    config['host']: str = parsed.get('database', 'host') if \
        parsed.has_option('database', 'host') else \
        'localhost'

    config['username']: str = parsed.get('database', 'username') if \
        parsed.has_option('database', 'username') else \
        manager.getDbUser()

    config['password']: str = parsed.get('database', 'password') if \
        parsed.has_option('database', 'password') else \
        manager.getDbPassword()

    if config['engine'] == 'sqlite':
        config['port']: Optional[int] = None
        config['path']: str = parsed.get('database', 'path') if \
            parsed.has_option('database', 'path') else \
            os.path.join(
                manager.getEtcDir(),
                manager.getDbSchema() + '.sqlite'
            )
    elif config['engine'] == 'mysql':
        config['port']: Optional[int] = manager.get('database', 'port') if \
            manager.has_option('database', 'port') else \
            3306
        config['path']: str = manager.getDbSchema()
    else:
        raise NotImplementedError('{} is not supported'.format(
            config['engine']))

    return config
Exemple #2
0
class DbManagerBase(TortugaObjectManager):
    """
    Class for db management.

    :param engine: a SQLAlchemy database engine instance
    :param init:   a flag that is set when the database has not yet
                   been initialized. If this flag is set, not attempts
                   will be made to load/map kit tables. This flag is
                   cleared once the database has been initialized.

    """
    def __init__(self, engine=None):
        super(DbManagerBase, self).__init__()

        self.Session = None

        if not engine:
            self._cm = ConfigManager()

            self._dbConfig = self._refreshDbConfig()

            engineURI = self.__getDbEngineURI()

            if self._dbConfig['engine'] == 'mysql':
                # Set default SQLAlchemy engine arguments for MySQL
                kwargs = {
                    'pool_size': 10,
                    'max_overflow': 2,
                    'pool_recycle': 600,
                    'echo': False,
                    'pool_timeout': 60,
                }
            else:
                kwargs = {}

            self._engine = sqlalchemy.create_engine(engineURI, **kwargs)
        else:
            self._engine = engine

        self._metadata = sqlalchemy.MetaData(self._engine)
        self._mapped_tables = {}

    def _map_db_tables(self):
        #
        # Make sure all kit table mappers have been registered
        #
        load_kits()
        for kit_installer_class in get_all_kit_installers():
            kit_installer = kit_installer_class()
            kit_installer.register_database_table_mappers()
        #
        # Map all tables that haven't yet been mapped
        #
        for table_mapper in get_all_table_mappers():
            key = table_mapper.__name__
            if key not in self._mapped_tables.keys():
                logger.debug('Mapping table: {}'.format(key))
                self._mapped_tables[key] = table_mapper()
                self._mapped_tables[key].map(self)

    @property
    def engine(self):
        """
        SQLAlchemy Engine object property
        """
        self._map_db_tables()
        return self._engine

    def session(self):
        """
        Database session context manager
        """
        return SessionContextManager(self)

    def init_database(self):
        #
        # Create tables
        #
        self._map_db_tables()
        try:
            self._metadata.create_all(self.engine)
        except Exception:
            self.getLogger().exception('SQLAlchemy raised exception')
            raise DbError('Check database settings or credentials')

    @property
    def metadata(self):
        return self._metadata

    def __getDbEngineURI(self):
        dbPort = self._dbConfig['port']
        dbHost = self._dbConfig['host']
        engine = self._dbConfig['engine']
        dbUser = self._dbConfig['username']
        dbPassword = self._dbConfig['password']

        if engine == 'sqlite':
            engineURI = 'sqlite:///%s' % (self._dbConfig['path'])
        else:
            if dbUser is not None:
                if dbPassword is not None:
                    userspec = '%s:%s' % (dbUser, dbPassword)
                else:
                    userspec = dbUser
            else:
                userspec = None

            if dbPort is not None:
                hostspec = '%s:%s' % (dbHost, dbPort)
            else:
                hostspec = dbHost

            if userspec is not None:
                engineURI = '%s://%s@%s/%s' % (engine, userspec, hostspec,
                                               self._cm.getDbSchema())
            else:
                engineURI = '%s://%s/%s' % (engine, hostspec,
                                            self._cm.getDbSchema())

        return engineURI

    def _getDefaultDbEngine(self):         \
            # pylint: disable=no-self-use

        return 'sqlite'

    def _getDefaultDbHost(self):         \
            # pylint: disable=no-self-use

        return 'localhost'

    def _getDefaultDbPort(self, engine):         \
            # pylint: disable=no-self-use

        # MySQL default port
        if engine == 'mysql':
            return 3306

        return None

    def _getDefaultDbUserName(self):
        return self._cm.getDbUser()

    def _getDefaultDbPassword(self):
        if os.path.exists(self._cm.getDbPasswordFile()):
            with open(self._cm.getDbPasswordFile()) as fp:
                dbPassword = fp.read()
        else:
            dbPassword = None

        return dbPassword

    def _refreshDbConfig(self, cfg=None):
        dbConfig = {}

        if cfg is None:
            cfg = configparser.ConfigParser()

            cfg.read(os.path.join(self._cm.getKitConfigBase(), 'tortuga.ini'))

        # Database engine
        val = cfg.get('database', 'engine').strip().lower() \
            if cfg.has_option('database', 'engine') else \
            self._getDefaultDbEngine()

        dbConfig['engine'] = val

        if dbConfig['engine'] == 'sqlite':
            # If database is sqlite, read the path
            dbConfig['path'] = cfg.get('database', 'path') \
                if cfg.has_section('database') and \
                cfg.has_option('database', 'path') else \
                os.path.join(self._cm.getEtcDir(),
                             self._cm.getDbSchema() + '.sqlite')

        # Database host
        val = cfg.get('database', 'host') \
            if cfg.has_option('database', 'host') else \
            self._getDefaultDbHost()

        dbConfig['host'] = val

        # Database port
        val = cfg.get('database', 'port') \
            if cfg.has_option('database', 'port') else None

        dbConfig['port'] = val if val else self._getDefaultDbPort(
            engine=dbConfig['engine'])

        # Database username
        val = cfg.get('database', 'username') \
            if cfg.has_option('database', 'username') \
            else self._getDefaultDbUserName()

        dbConfig['username'] = val

        # Database password
        val = cfg.get('database', 'password') \
            if cfg.has_option('database', 'password') \
            else self._getDefaultDbPassword()

        dbConfig['password'] = val

        return dbConfig

    def get_backend_opts(self):         \
            # pylint: disable=no-self-use

        return {
            'mysql_engine': 'InnoDB',
        }

    def getMetadataTable(self, table):
        return self._metadata.tables[table]

    def openSession(self):
        """ Open db session. """
        session_factory = sqlalchemy.orm.sessionmaker(bind=self.engine)
        self.Session = sqlalchemy.orm.scoped_session(session_factory)

        return self.Session()

    def closeSession(self):
        """Close scoped_session."""

        self.Session.remove()
class TortugaDeployer: \
        # pylint: disable=too-many-public-methods
    def __init__(self, logger, cmdline_options=None):
        self._cm = ConfigManager()

        self._logger = logger

        self._osObjectFactory = osUtility.getOsObjectFactory()

        self._settings = self.__load_settings(cmdline_options)

        self._settings['installer_software_profile'] = 'Installer'
        self._settings['installer_hardware_profile'] = 'Installer'

        self._settings['eulaAccepted'] = False

        self._settings['fqdn'] = getfqdn()

        self._settings['osInfo'] = getOsInfo()

        self._forceCleaning = False
        self._depotCreated = False

        fsManager = self._osObjectFactory.getOsFileSystemManager()

        self._lockFilePath = os.path.join(
            fsManager.getOsLockFilePath(), 'tortuga-setup')

        langdomain = 'tortuga-config'

        localedir = os.path.join(self._cm.getRoot(), 'share', 'locale')

        if not os.path.exists(localedir):
            # Try the system path
            localedir = '/usr/share/locale'

        gettext.bindtextdomain(langdomain, localedir)
        gettext.textdomain(langdomain)
        self.gettext = gettext.gettext
        self._ = self.gettext

        self._logger.info('Detected OS: [%s]', self._settings['osInfo'])

    def __load_settings(self, cmdline_options):
        settings = dict(list(cmdline_options.items()))

        default_cfgfile = os.path.join(
            self._cm.getKitConfigBase(), 'tortuga.ini')

        if 'inifile' in cmdline_options and \
                cmdline_options['inifile'] != default_cfgfile:
            # Copy configuration specified on command-line to
            # $TORTUGA_ROOT/config/tortuga.ini

            self._logger.info(
                'Using configuration file [%s]' % (settings['inifile']))

            self._logger.info(
                'Copying configuration to [%s]' % (default_cfgfile))

            if os.path.exists(default_cfgfile):
                # Back up existing 'tortuga.ini'
                shutil.move(default_cfgfile, default_cfgfile + '.orig')

            shutil.copyfile(cmdline_options['inifile'], default_cfgfile)

        settings['inifile'] = default_cfgfile

        cfg = configparser.ConfigParser()
        cfg.read(settings['inifile'])

        settings['timezone'] = ''
        settings['utc'] = False
        settings['keyboard'] = 'us'
        settings['language'] = 'en_US.UTF-8'

        # Get database setting
        value = cfg.get('database', 'engine') \
            if cfg.has_section('database') and \
            cfg.has_option('database', 'engine') else None

        if value and value not in ('mysql', 'sqlite'):
            raise InvalidArgument(
                'Unsupported database engine [%s]' % (value))

        settings['database'] = {
            'engine': value if value else 'mysql'
        }

        # Get depot directory
        if cfg.has_section('installer') and \
                cfg.has_option('installer', 'depotpath'):
            settings['depotpath'] = cfg.get('installer', 'depotpath')

            # For consistency's sake...
            self._cm.setDepotDir(settings['depotpath'])
        else:
            settings['depotpath'] = self._cm.getDepotDir()

        # Internal web port
        settings['intWebPort'] = cfg.getint('installer', 'intWebPort') \
            if cfg.has_section('installer') and \
            cfg.has_option('installer', 'intWebPort') else \
            self._cm.getIntWebPort()

        self._cm.setIntWebPort(settings['intWebPort'])

        # Admin port
        settings['adminPort'] = cfg.getint('installer', 'adminPort') \
            if cfg.has_section('installer') and \
            cfg.has_option('installer', 'adminPort') else \
            self._cm.getAdminPort()

        self._cm.setAdminPort(settings['adminPort'])

        # IntWebServicePort
        settings['intWebServicePort'] = cfg.getint(
            'installer', 'intWebServicePort') \
            if cfg.has_section('installer') and \
            cfg.has_option('installer', 'intWebServicePort') else \
            self._cm.getIntWebServicePort()

        self._cm.setIntWebServicePort(settings['intWebServicePort'])

        return settings

    def _get_setting(self, name, section=None):
        if section and section in self._settings:
            return self._settings[section][name] \
                if name in self._settings[section] else None

        return self._settings[name] if name in self._settings else None

    def eout(self, message, *args):
        """
        Output messages to STDERR with Internationalization.
        Additional arguments will be used to substitute variables in the
        message output
        """
        if args:
            mesg = self.gettext(message) % args
        else:
            mesg = self.gettext(message)
        sys.stderr.write(mesg)

    def out(self, message, *args):
        """
        Output messages to STDOUT with Internationalization.
        Additional arguments will be used to substitute variables in the
        message output
        """
        if args:
            mesg = self.gettext(message) % args
        else:
            mesg = self.gettext(message)
        sys.stdout.write(mesg)

    def prompt(self, default_value,
               auto_answer_default_value, text_list, question,
               tag=None, section=None, isPassword=False):
        """Generic user prompting routine"""

        resp_value = None

        bDefaults = self._settings['defaults']

        if tag:
            resp_value = self._get_setting(tag, section=section)
            if not resp_value and bDefaults:
                # Use the default value
                default_value = auto_answer_default_value
        elif bDefaults:
            default_value = auto_answer_default_value

        if text_list:
            self.out('\n')

            for line in text_list:
                self.out(line + '\n')

        if default_value and not isPassword:
            self.out('\n%s [%s]: ' % (question, default_value))
        else:
            self.out('\n%s: ' % (question))

        if bDefaults or resp_value:
            if resp_value:
                value = resp_value
            else:
                value = auto_answer_default_value
            if not isPassword:
                self.out('%s\n' % value)
        else:
            if isPassword:
                import getpass
                value = getpass.getpass('').strip()
            else:
                value = input('').strip()
            if not value:
                value = default_value

        return value

    def checkPreInstallConfiguration(self):     # pylint: disable=no-self-use
        """
        Raises:
            InvalidMachineConfiguration
        """

        # Check for existence of /etc/hosts
        if not os.path.exists('/etc/hosts'):
            raise InvalidMachineConfiguration(
                '/etc/hosts file is missing. Unable to proceed with'
                ' installation')

    def preInstallPrep(self):
        bAcceptEula = self._settings['acceptEula']

        license_file = ' %s/LICENSE' % (self._cm.getEtcDir())

        print()

        if bAcceptEula:
            cmd = 'cat %s\n' % (license_file)
            os.system(cmd)
        else:
            cmd = 'more %s\n' % (license_file)

            print("To install Tortuga you must read and agree to "
                  "the following EULA.")

            print("Press 'Enter' to continue...")

            input('')
            os.system(cmd)
            print()
            while True:
                print('Do you agree? [Yes / No]', end=' ')
                answer = input('').lower()

                if answer not in ['yes', 'no', 'y', 'n']:
                    print('Invalid response. Please respond \'Yes\''
                          ' or \'No\'')

                    continue
                break
            if answer[0] == 'n':
                raise EulaAcceptanceRequired(
                    'You must accept the EULA to install Tortuga')

        self._settings['eulaAccepted'] = \
            'Accepted on: %s local machine time' % (time.ctime())

        # Restore resolv.conf if we have a backup
        if osUtility.haveBackupFile('/etc/resolv.conf'):
            osUtility.restoreFile('/etc/resolv.conf')

    def _runCommandWithSpinner(self, cmd, statusMsg, logFileName):
        self._logger.debug(
            '_runCommandWithSpinner(cmd=[%s], logFileName=[%s])' % (
                cmd, logFileName))

        self.out(statusMsg + '  ')

        # Open the log file in unbuffered mode
        fpOut = open(logFileName, 'ab', 0)

        p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
                             stderr=subprocess.STDOUT, bufsize=1,
                             close_fds=True)

        for i in itertools.cycle(['/', '-', '\\', '|']):
            buf = p.stdout.readline()

            sys.stdout.write('')
            sys.stdout.flush()

            if not buf:
                break

            fpOut.write(buf)

            sys.stdout.write(i)
            sys.stdout.flush()

        sys.stdout.write(' ')
        self.out('done.\n')

        retval = p.wait()

        fpOut.close()

        return retval

    def puppetApply(self):
        '''
        Complete the installer configuration by running against the
        previously installed Puppet master.  Display a spinner while Puppet
        runs.
        '''

        self._logger.info('Running Puppet for post-configuration')

        logFileName = '/tmp/tortuga_setup.log'

        cmd = ('/opt/puppetlabs/bin/puppet agent --color false --onetime'
               ' --no-daemonize --detailed-exitcodes --verbose 2>&1')

        retval = self._runCommandWithSpinner(
            cmd, statusMsg=(
                '\nCompleting installer configuration.'
                ' Please wait...'), logFileName=logFileName)

        if retval not in (0, 2):
            # Puppet can return a non-zero return code, even if it was
            # successful.

            errmsg = 'Puppet post-configuration failed (see log file %s)' % (
                logFileName)

            self._logger.error(errmsg)

            self.out(errmsg + '\n')

            raise Exception(errmsg)

        self._logger.info('Puppet post-configuration completed')

    def startSetup(self):
        # If force was specified clean first and then run...
        bForce = self._settings['force']

        if bForce:
            self._forceCleaning = True

            self.out(
                '--force option specified. Cleaning previous'
                ' installation.\n')

            self.cleanup()

            self._forceCleaning = False

        if os.path.exists(self._lockFilePath):
            raise SoftwareAlreadyDeployed(
                "\ntortuga-setup has already been run.\n\n"
                "Use --force option to force reinstallation.")

        open(self._lockFilePath, 'w').close()

        self.out('Tortuga Setup\n')

    def getClusterConfig(self):
        sysManager = self._osObjectFactory.getOsSysManager()

        self._settings['timezone'], self._settings['utc'] = \
            sysManager.findTimeInfo()

        self._settings['keyboard'] = sysManager.findKeyboard()

        self._settings['language'] = sysManager.findLanguage()

        self.out(_('\nStarting Tortuga setup...\n'))

        # Ports configuration
        if not self._settings['defaults']:
            intWebPort, adminPort, intWebServicePort = self.configurePorts()

            self._cm.setIntWebPort(intWebPort)
            self._cm.setAdminPort(adminPort)
            self._cm.setIntWebServicePort(intWebServicePort)

            self._settings['intWebPort'] = intWebPort
            self._settings['adminPort'] = adminPort
            self._settings['intWebServicePort'] = intWebServicePort

        # Admin username and password
        self._settings['adminUsername'], \
            self._settings['adminPassword'] = self.promptForAdminCredentials()

    def prepDepot(self):
        depotpath = None

        if not self._settings['defaults']:
            self.out(
                _('Tortuga requires a directory for storage of OS'
                  ' distribution media and other files required for'
                  ' node provisioning.\n\n'))

        while not depotpath:
            if self._settings['defaults']:
                response = self._settings['depotpath']
            else:
                try:
                    response = input(
                        'Please enter a depot path (Ctrl-C to interrupt)'
                        ' [%s]: ' % (self._settings['depotpath']))
                except KeyboardInterrupt:
                    raise InvalidArgument(_('Aborted by user.'))

                if not response:
                    response = self._settings['depotpath']

            if not response.startswith('/'):
                errmsg = 'Depot path must be fully-qualified'

                if not self._settings['defaults']:
                    self.out('Error: %s\n' % (errmsg))

                    continue

                raise InvalidArgument(errmsg)

            if response == '/':
                errmsg = 'Depot path cannot be system root directory'

                if not self._settings['defaults']:
                    self.out(_('Error: %s\n' % (errmsg)))

                    continue

                raise InvalidArgument(errmsg)

            if os.path.exists(response):
                if not self._settings['force']:
                    if not self._settings['defaults']:
                        self.out(
                            _('Directory [%s] already exists. Do you wish to'
                              ' remove it [N/y]? ') % (response))

                        remove_response = input('')

                        if not remove_response or \
                                remove_response[0].lower() == 'n':
                            continue_response = input(
                                'Do you wish to continue [N/y]? ')

                            if continue_response and \
                                    continue_response[0].lower() == 'y':
                                continue

                            raise InvalidArgument(_('Aborted by user.'))
                    else:
                        raise InvalidArgument(
                            _('Existing depot directory [%s] will not be'
                              ' removed.') % (response))
                else:
                    self.out(
                        _('\nRemoving existing depot directory [%s]... ') % (
                            response))

                    depotpath = response

                    tortugaSubprocess.executeCommand(
                        'rm -rf %s/*' % (depotpath))

                    self.out(_('done.\n'))
            else:
                depotpath = response

        self._settings['depotpath'] = depotpath

        self._cm.setDepotDir(self._settings['depotpath'])

    def _portPrompt(self, promptStr, defaultValue):
        while True:
            tmpPort = self.prompt(
                defaultValue, defaultValue, None, promptStr)

            try:
                tmpPort = int(tmpPort)

                if tmpPort <= 0 or tmpPort > 65535:
                    raise ValueError('Port must be between 1 and 65535')

                # Success
                break
            except ValueError as ex:
                self.out('Error: ' + str(ex) + '\n')

        return tmpPort

    def configurePorts(self):
        reconfigurePorts = self.prompt(
            'N', 'N', [
                'The following ports will be used by Tortuga:'
                '',
                '    +-----------------------------+-------+',
                '    | Description                 | Port  |',
                '    +-----------------------------+-------+',
                '    | Internal webserver          | %5d |' % (
                    self._settings['intWebPort']),
                '    | SSL webservice daemon       | %5d |' % (
                    self._settings['adminPort']),
                '    | Local webservice daemon     | %5d |' % (
                    self._settings['intWebServicePort']),
                '    +-----------------------------+-------+'
            ], 'Do you wish to change the default configuration [N/y]?')

        if not reconfigurePorts or reconfigurePorts[0].lower() == 'n':
            return self._settings['intWebPort'], \
                self._settings['adminPort'], \
                self._settings['intWebServicePort']

        # Internal web server port
        intWebPort = self._portPrompt(
            'Enter port for internal webserver',
            self._settings['intWebPort'])

        # SSL webservice daemon port
        adminPort = self._portPrompt(
            'Enter port for SSL webservice daemon',
            self._settings['adminPort'])

        # Local webservice daemon port
        intWebServicePort = self._portPrompt(
            'Enter port for local webservice daemon',
            self._settings['intWebServicePort'])

        return intWebPort, adminPort, intWebServicePort

    def _removePackageSources(self):
        pkgManager = self._osObjectFactory.getOsPackageManager()
        for pkgSrcName in pkgManager.getPackageSourceNames():
            self._logger.info(
                'Removing package source [%s]' % (pkgSrcName))
            pkgManager.removePackageSource(pkgSrcName)

    def _disableTortugaws(self):
        self.out('  * Disabling Tortuga webservice\n')

        _tortugaWsManager = self._osObjectFactory.getTortugawsManager()
        serviceName = _tortugaWsManager.getServiceName()
        _osServiceManager = getOsObjectFactory().getOsServiceManager()

        try:
            _osServiceManager.stop(serviceName)
        except CommandFailed:
            pass

    def cleanup(self):
        # If possible, remove any package sources we added
        self._removePackageSources()

        osUtility.removeFile(self._lockFilePath)

        osUtility.removeFile(self._cm.getProfileNiiFile())

        # Turn off the webservice daemon
        self._disableTortugaws()

        # Restore resolv.conf
        if osUtility.haveBackupFile('/etc/resolv.conf'):
            osUtility.restoreFile('/etc/resolv.conf')

        # Drop database
        dbManager = self._osObjectFactory.getOsApplicationManager(
            self._settings['database']['engine'])

        try:
            dbSchema = self._cm.getDbSchema()

            self.out('  * Removing database [%s]\n' % (dbSchema))

            dbManager.destroyDb(dbSchema)
        except Exception as ex:   # pylint: disable=broad-except
            self._logger.exception(
                'Could not destroy existing db: {}'.format(ex))

        # Remove DB password file
        osUtility.removeFile(self._cm.getDbPasswordFile())

        # Remove CFM secret
        cfmSecretFile = self._cm.getCfmSecretFile()
        if os.path.exists(cfmSecretFile):
            osUtility.removeFile(self._cm.getCfmSecretFile())

        # Generic cleanup
        osUtility.removeLink('/etc/tortuga-release')

        # Cleanup or remove depot directory
        errmsg = 'Removing contents of [%s]' % (self._settings['depotpath'])

        self._logger.debug(errmsg)

        if self._depotCreated:
            self.out('  * %s\n' % (errmsg))

            osUtility.removeDir(self._settings['depotpath'])
        else:
            if self._settings['depotpath']:
                self.out('  * %s\n' % (errmsg))

                tortugaSubprocess.executeCommand(
                    'rm -rf %s/*' % (self._settings['depotpath']))

                self.out('\n')

        if not self._forceCleaning:
            self.out('Consult log(s) for further details.\n')

            self._logger.error('Installation failed')

    def runSetup(self):
        """ Installer setup. """
        self.checkPreInstallConfiguration()

        # Do not run cleanup if this fails.
        self.startSetup()

        try:
            self.preInstallPrep()

            self.getClusterConfig()

            self.prepDepot()

            self.preConfig()

            self.pre_init_db()

            self.puppetBootstrap()

            dbm, session = self.initDatabase()

            try:

                self.createAdminUser(
                    session,
                    self._settings['adminUsername'],
                    self._settings['adminPassword'])

                self.installKits(dbm)

                self.enableComponents(session)
            finally:
                dbm.closeSession()

                self.puppetApply()

            self.out('\nTortuga installation completed successfully!\n\n')

            print('Run \"exec -l $SHELL\" to initialize Tortuga environment\n')
        except Exception:  # pylint: disable=broad-except
            self._logger.exception('Fatal error occurred during setup')

            raise TortugaException('Installation failed')

    def _generate_db_password(self):
        """
        Generate a database password.

        """
        #
        # Because Apache httpd server is not installed at the time this
        # runs, we cannot set the ownership of this file to be 'apache'
        # (which is necessary for the Tortuga webservice).
        #
        # Set ownership of file to root:puppet.
        #
        # When the Puppet bootstrap runs, it changes the ownership to
        # 'apache:puppet' and everybody is happy!
        #
        puppet_user = pwd.getpwnam('puppet')
        gid = puppet_user[3]
        self._generate_password_file(self._cm.getDbPasswordFile(), gid=gid)

    def _generate_redis_password(self):
        """
        Generate a password for Redis.

        """
        #
        # Puppet needs read access to this file so that it can use it for
        # writing the redis config file.
        #
        puppet_user = pwd.getpwnam('puppet')
        gid = puppet_user[3]
        self._generate_password_file(self._cm.getRedisPasswordFile(), gid=gid)

    def _generate_password_file(self, file_name: str,
                                password_length: int = 32,
                                uid: int = 0, gid: int = 0,
                                mode: int = 0o440):
        """
        Generate a password in a file.

        :param file_name:       the name of the file in which the password
                                will be stored
        :param password_length: the length of the password, default = 32
        :param uid:             the uid (owner) of the file, default = 0
        :param gid:             the gid (group) of the file, default = 0
        :param mode:            the file perms, default 0440

        """
        password = self._generate_password(password_length)

        with open(file_name, 'w') as fp:
            fp.write(password)

        os.chown(file_name, uid, gid)
        os.chmod(file_name, mode)

    def _generate_password(self, length: int = 8) -> str:
        """
        Generate a password.

        :param length: the length of the password

        :return:       the generated password

        """
        chars = string.ascii_letters + string.digits

        return ''.join([random.choice(chars) for _ in range(length)])

    def preConfig(self):
        # Create default hieradata directory
        hieraDataDir = '/etc/puppetlabs/code/environments/production/data'
        if not os.path.exists(hieraDataDir):
            os.makedirs(hieraDataDir)

        # Derive host name of puppet master from FQDN
        fqdn = self._settings['fqdn']

        configDict = {
            'version': 5,
            'DNSZone': 'private',
            'puppet_server': fqdn,
            'depot': self._settings['depotpath'],
        }

        with open(os.path.join(hieraDataDir, 'tortuga-common.yaml'),
                  'wb') as fp:
            fp.write(
                yaml.safe_dump(
                    configDict, explicit_start=True,
                    default_flow_style=False).encode())

        self._generate_db_password()
        self._generate_redis_password()

    def pre_init_db(self):
        # If using 'mysql' as the database backend, we need to install the
        # puppetlabs-mysql Puppet module prior to bootstrapping. This used
        # to be done in 'install-tortuga.sh'

        if self._settings['database']['engine'] == 'mysql':
            print('\nUsing MySQL as backing database.')

            puppet_module = 'puppetlabs-mysql'

            logmsg = f'Installing \'{puppet_module}\' module'

            self._logger.debug(logmsg)

            print(f'\n{logmsg}...', end='')

            cmd = ('/opt/puppetlabs/bin/puppet module install'
                   f' --color false {puppet_module}')
            tortugaSubprocess.executeCommand(cmd)

            print('done.')

    def puppetBootstrap(self):
        localPuppetRoot = os.path.join(self._cm.getEtcDir(), 'puppet')

        logFileName = '/tmp/bootstrap.log'

        puppet_server = self._settings['fqdn']

        # Bootstrap using Puppet
        cmd = ('/opt/puppetlabs/bin/puppet apply --verbose'
               ' --detailed-exitcodes'
               ' --execute "class { \'tortuga::installer\':'
               ' puppet_server => \'%s\','
               '}"' % (puppet_server)
               )

        retval = self._runCommandWithSpinner(
            cmd, '\nPerforming pre-configuration... Please wait...',
            logFileName=logFileName)

        if retval not in (0, 2):
            # Puppet can return a non-zero return code, even if it was
            # successful.
            self._logger.debug(
                'Puppet pre-configuration returned non-zero'
                ' return code [%d]' % (retval))

            errmsg = 'Puppet bootstrap failed (see log file %s)' % (
                logFileName)

            self._logger.error(errmsg)

            raise Exception(errmsg)

        self._logger.debug('Puppet pre-configuration completed')

    def initDatabase(self) -> Tuple[Any, Session]:
        msg = _('Initializing database')

        self._logger.info(msg)

        print_('\n' + msg + '... ', end='')

        # This cannot be a global import since the database configuration
        # may be set in this script.
        from tortuga.db.dbManager import DbManager

        dbm = DbManager()

        # create database
        dbm.init_database()

        session = dbm.openSession()

        # Prime the database previously created as part of the bootstrap
        try:
            dbUtility.primeDb(session, self._settings)

            dbUtility.init_global_parameters(session, self._settings)

            print_(_('done'))

            session.commit()
        except Exception as exc:  # pylint: disable=broad-except
            session.rollback()

            print_(_('failed.'))

            print_(_('Exception raised initializing database:') +
                   ' {0}'.format(exc), file=sys.stderr)

        self._logger.debug('Done initializing database')

        return dbm, session

    def installKits(self, dbm):
        self._logger.info('Installing kits')

        self.out('\n' + _('Installing kits') + '...\n')

        kitApi = KitApi()

        # Iterate over the glob of 'kits-*.tar.bz2'
        kitFileGlob = '%s/kits/kit-*.tar.bz2' % (self._cm.getRoot())

        # Split comma-separated list of kits to skip installing. Sorry, you
        # cannot skip installing the base kit.
        val = self._settings['skip_kits'] \
            if 'skip_kits' in self._settings else ''

        skip_kits = set([
            item for item in val.split(',') if item != 'base']) \
            if val else set()

        for kitPackage in glob.glob(kitFileGlob):
            try:
                kit = get_metadata_from_archive(kitPackage)
            except KitNotFound:
                msg = 'Kit [%s] is malformed/invalid. Skipping.' % (
                    os.path.basename(kitPackage))

                self._logger.error(msg)

                self.out('   %s\n' % (msg))

                continue

            if kit['name'] in skip_kits:
                msg = 'Kit [%s] installation skipped.' % (kit['name'])

                self.out('   %s\n' % (msg))

                self._logger.info(msg)

                continue

            try:
                kitApi.installKitPackage(dbm, kitPackage)
            except EulaAcceptanceRequired:
                msg = 'Kit [%s] requires EULA acceptance. Skipping.' % (
                    kitPackage)

                self.out('   %s\n' % (msg))

                self._logger.info(msg)

                continue

            self.out('   - %s installed.\n' % (kit['name']))

            self._logger.info('Kit [%s] installed' % (kit['name']))

        self._logger.info('Done installing kits')

        load_kits()

    def enableComponents(self, session: Session):
        """
        Raises:
            ConfigurationError
        """

        self._logger.info('Enabling \'installer\' component')

        base_kit = KitApi().getKit(session, 'base')

        enabledComponents = ['installer']

        # get list of components from 'base' kit
        components = [c for c in base_kit.getComponentList()
                      if c.getName() in enabledComponents]

        installerNode = NodeApi().getInstallerNode(session)

        for component in components:
            SoftwareProfileApi().enableComponent(
                session,
                installerNode.getSoftwareProfile().getName(),
                base_kit.getName(),
                base_kit.getVersion(),
                base_kit.getIteration(),
                component.getName(), compVersion=component.getVersion(),
            )

    def promptForAdminCredentials(self):
        # Get admin username and password for use with web service

        if self._settings['defaults']:
            self.out(_('\nUsing default Tortuga admin user name/password.\n'))

            return 'admin', 'password'

        username = password = None

        # Administrator username
        while True:
            username = self.prompt(
                'admin', 'admin',
                ['Enter name for Tortuga admin user.',
                 'This user is not associated with any system user.'],
                'Admin user name')

            if len(username) > 3:
                break

            self.out('Admin user name must be at least 4 characters.')

        # Administrator password
        while True:
            password = self.prompt(
                '', 'password',
                ['Enter password for Tortuga admin user.'],
                'Admin password', None, None, True)

            if len(password) < 4:
                self.out('Admin password must be at least 4 characters.')
                continue

            confirmPassword = self.prompt(
                '', 'password',
                ['Confirm admin password.'],
                'Confirm password', None, None, True)

            if confirmPassword == password:
                self.out('\n')
                break

            self.out('Passwords did not match.')

        return username, password

    def createAdminUser(self, session: Session, username, password):
        msg = _('Adding administrative user')

        self._logger.info(msg)

        self.out('\n' + msg + '... ')

        AdminApi().addAdmin(
            session,
            username, password, False,
            description='Added by tortuga-setup')

        self.out(_('done.') + '\n')
Exemple #4
0
class DbManager(TortugaObjectManager):
    """
    Class for db management.

    :param engine: a SQLAlchemy database engine instance
    :param init:   a flag that is set when the database has not yet
                   been initialized. If this flag is set, not attempts
                   will be made to load/map kit tables. This flag is
                   cleared once the database has been initialized.

    """
    def __init__(self, engine=None):
        super().__init__()

        if not engine:
            self._cm = ConfigManager()

            self._dbConfig = self._refreshDbConfig()

            engineURI = self.__getDbEngineURI()

            if self._dbConfig['engine'] == 'sqlite' and \
                    not os.path.exists(self._dbConfig['path']):
                # Ensure SQLite database file is created with proper permissions
                fd = os.open(
                    self._dbConfig['path'], os.O_CREAT, mode=0o600)

                os.close(fd)

            self._engine = sqlalchemy.create_engine(engineURI)
        else:
            self._engine = engine

        self.Session = sqlalchemy.orm.scoped_session(
            sqlalchemy.orm.sessionmaker(bind=self.engine))

    def _register_database_tables(self):
        for kit_installer_class in get_all_kit_installers():
            kit_installer = kit_installer_class()
            kit_installer.register_database_tables()

    @property
    def engine(self):
        """
        SQLAlchemy Engine object property
        """
        self._register_database_tables()
        return self._engine

    def session(self):
        """
        Database session context manager
        """
        return SessionContextManager(self)

    def init_database(self):
        #
        # Create tables
        #
        self._register_database_tables()
        try:
            ModelBase.metadata.create_all(self.engine)
        except Exception:
            self._logger.exception('SQLAlchemy raised exception')
            raise DbError('Check database settings or credentials')

    @property
    def metadata(self):
        return self._metadata

    def __getDbEngineURI(self):
        dbPort = self._dbConfig['port']
        dbHost = self._dbConfig['host']
        engine = self._dbConfig['engine']
        dbUser = self._dbConfig['username']
        dbPassword = self._dbConfig['password']

        if engine == 'sqlite':
            engineURI = 'sqlite:///%s' % (self._dbConfig['path'])
        else:
            if dbUser is not None:
                if dbPassword is not None:
                    userspec = '%s:%s' % (dbUser, dbPassword)
                else:
                    userspec = dbUser
            else:
                userspec = None

            if dbPort is not None:
                hostspec = '%s:%s' % (dbHost, dbPort)
            else:
                hostspec = dbHost

            engineURI = f'{engine}+pymysql' if engine == 'mysql' else engine
            engineURI += '://'

            if userspec is not None:
                engineURI += f'{userspec}@'

            engineURI += f'{hostspec}' + '/{}'.format(self._cm.getDbSchema())

        return engineURI

    def _getDefaultDbEngine(self): \
            # pylint: disable=no-self-use
        return 'sqlite'

    def _getDefaultDbHost(self): \
            # pylint: disable=no-self-use
        return 'localhost'

    def _getDefaultDbPort(self, engine): \
            # pylint: disable=no-self-use
        # MySQL default port
        if engine == 'mysql':
            return 3306

        return None

    def _getDefaultDbUserName(self):
        return self._cm.getDbUser()

    def _getDefaultDbPassword(self):
        if os.path.exists(self._cm.getDbPasswordFile()):
            with open(self._cm.getDbPasswordFile()) as fp:
                dbPassword = fp.read()
        else:
            dbPassword = None

        return dbPassword

    def _refreshDbConfig(self, cfg=None):
        dbConfig = {}

        if cfg is None:
            cfg = configparser.ConfigParser()

            cfg.read(os.path.join(self._cm.getKitConfigBase(), 'tortuga.ini'))

        # Database engine
        val = cfg.get('database', 'engine').strip().lower() \
            if cfg.has_option('database', 'engine') else \
            self._getDefaultDbEngine()

        dbConfig['engine'] = val

        if dbConfig['engine'] == 'sqlite':
            # If database is sqlite, read the path
            dbConfig['path'] = cfg.get('database', 'path') \
                if cfg.has_section('database') and \
                cfg.has_option('database', 'path') else \
                os.path.join(self._cm.getEtcDir(),
                             self._cm.getDbSchema() + '.sqlite')

        # Database host
        val = cfg.get('database', 'host') \
            if cfg.has_option('database', 'host') else \
            self._getDefaultDbHost()

        dbConfig['host'] = val

        # Database port
        val = cfg.get('database', 'port') \
            if cfg.has_option('database', 'port') else None

        dbConfig['port'] = val if val else self._getDefaultDbPort(
            engine=dbConfig['engine'])

        # Database username
        val = cfg.get('database', 'username') \
            if cfg.has_option('database', 'username') \
            else self._getDefaultDbUserName()

        dbConfig['username'] = val

        # Database password
        val = cfg.get('database', 'password') \
            if cfg.has_option('database', 'password') \
            else self._getDefaultDbPassword()

        dbConfig['password'] = val

        return dbConfig

    def get_backend_opts(self): \
            # pylint: disable=no-self-use
        return {
            'mysql_engine': 'InnoDB',
        }

    def getMetadataTable(self, table):
        return self._metadata.tables[table]

    def openSession(self):
        """ Open db session. """

        return self.Session()

    def closeSession(self):
        """Close scoped_session."""

        self.Session.remove()
class RuleEngine(RuleEngineInterface):
    def __init__(self, minTriggerInterval=60):
        self._cm = ConfigManager()
        self._lock = threading.RLock()
        self._processingLock = threading.RLock()
        self._minTriggerInterval = minTriggerInterval
        self._ruleDict = {}
        self._disabledRuleDict = {}  # Used for rules in the disabled state
        self._eventRuleDict = {}  # used for "event" type monitoring
        self._pollTimerDict = {}  # used for "poll" monitoring
        self._receiveRuleDict = {}  # used for "receive" type monitoring
        self._receiveQ = queue.Queue(0)  # infinite size FIFO queue
        self._rulesDir = self._cm.getRulesDir()
        self._logger = logging.getLogger('tortuga.rule.%s' %
                                         self.__class__.__name__)
        self._logger.addHandler(logging.NullHandler())
        self.__initRules()

        # the following are used for "receive" type monitoring
        self._processingTimer = None
        self._processingTimerRunning = False

    def __getRuleDirName(self, applicationName):
        return '%s/%s' % (self._rulesDir, applicationName)

    def __getRuleFileName(self, applicationName, ruleName):
        return '%s/%s.xml' % (self.__getRuleDirName(applicationName), ruleName)

    def __readRuleFile(self, applicationName, ruleName):
        with open(self.__getRuleFileName(applicationName,
                                         ruleName)) as ruleFile:
            content = ruleFile.read()

        return content

    def __writeRuleFile(self, rule):
        ruleDir = self.__getRuleDirName(rule.getApplicationName())

        if not os.path.exists(ruleDir):
            os.makedirs(ruleDir)

        with open(
                self.__getRuleFileName(rule.getApplicationName(),
                                       rule.getName()), 'w') as ruleFile:
            ruleFile.write('%s\n' % (rule.getXmlRep()))

    def __getRuleId(self, applicationName, ruleName):
        # pylint: disable=no-self-use
        return '%s/%s' % (applicationName, ruleName)

    def __checkRuleExists(self, ruleId):
        if ruleId not in self._ruleDict:
            raise RuleNotFound('Rule [%s] not found.' % ruleId)

    def __checkRuleDoesNotExist(self, ruleId):
        """
        Raises:
            RuleAlreadyExists
        """

        if ruleId in self._ruleDict:
            raise RuleAlreadyExists('Rule [%s] already exists.' % ruleId)

    def __checkRuleEnabled(self, ruleId):
        """
        Raises:
            RuleNotFound
            RuleDisabled
        """

        self.__checkRuleExists(ruleId)

        if ruleId in self._disabledRuleDict:
            raise RuleDisabled('Rule [%s] is disabled.' % ruleId)

    def __initRules(self):
        """ Initialize all known rules. """

        self._logger.debug('[%s] Initializing known rules' %
                           (self.__class__.__name__))

        fileList = osUtility.findFiles(self._rulesDir)

        parser = RuleXmlParser()

        for f in fileList:
            try:
                rule = parser.parse(f)
                ruleId = self.__getRuleId(rule.getApplicationName(),
                                          rule.getName())

                self._logger.debug('[%s] Found rule [%s]' %
                                   (self.__class__.__name__, ruleId))

                self.addRule(rule)
            except Exception as ex:
                self._logger.error('[%s] Invalid rule file [%s] (Error: %s)' %
                                   (self.__class__.__name__, f, ex))

    def __evaluateNumbers(self, metric, operator, triggerValue):         \
            # pylint: disable=exec-used

        trigger = None

        try:
            triggerString = '%s %s %s' % (metric, operator, triggerValue)

            self._logger.debug('[%s] Evaluating as numbers: %s' %
                               (self.__class__.__name__, triggerString))

            exec('trigger = %s' % triggerString)

            return trigger
        except Exception as ex:
            self._logger.debug('[%s] Could not evaluate as numbers: %s' %
                               (self.__class__.__name__, ex))

        return None

    def __evaluateStrings(self, metric, operator, triggerValue):         \
            # pylint: disable=exec-used

        trigger = None

        try:
            triggerString = '"%s" %s "%s"' % (metric, operator, triggerValue)

            self._logger.debug('[%s] Evaluating as strings: %s' %
                               (self.__class__.__name__, triggerString))

            exec('trigger = %s' % triggerString)

            return trigger
        except Exception as ex:
            self._logger.debug('[%s] Could not evaluate as strings: %s' %
                               (self.__class__.__name__, ex))

        return None

    def __parseMonitorData(self, monitorData=''):
        if not monitorData:
            return None

        self._logger.debug('[%s] Parsing data: %s' %
                           (self.__class__.__name__, monitorData))

        try:
            return libxml2.parseDoc(monitorData)
        except Exception as ex:
            self._logger.error('[%s] Could not parse data: %s' %
                               (self.__class__.__name__, ex))

        return None

    def __evaluateConditions(self,
                             rule,
                             monitorXmlDoc=None,
                             xPathReplacementDict=None):
        # Return True if all rule conditions were satisfied.
        triggerAction = False

        try:
            if monitorXmlDoc is not None:
                triggerAction = True
                for condition in rule.getConditionList():
                    self._logger.debug('[%s] Evaluating: [%s]' %
                                       (self.__class__.__name__, condition))

                    metricXPath = condition.getMetricXPath()

                    metric = self.__replaceXPathVariables(
                        metricXPath, xPathReplacementDict or {})

                    if metric == metricXPath:
                        # No replacement was done, try to evaluate xpath.
                        metric = monitorXmlDoc.xpathEval('%s' % metricXPath)

                    self._logger.debug('[%s] Got metric: [%s]' %
                                       (self.__class__.__name__, metric))

                    if metric == "" or metric == "nan":
                        self._logger.debug(
                            '[%s] Metric is not defined, will not trigger'
                            ' action' % (self.__class__.__name__))

                        triggerAction = False

                        break

                    operator = condition.getEvaluationOperator()

                    triggerValue = self.__replaceXPathVariables(
                        condition.getTriggerValue(), xPathReplacementDict
                        or {})

                    trigger = self.__evaluateNumbers(metric, operator,
                                                     triggerValue)

                    if trigger is None:
                        trigger = self.__evaluateStrings(
                            metric, operator, triggerValue)

                    self._logger.debug('[%s] Evaluation result: [%s]' %
                                       (self.__class__.__name__, trigger))

                    if not trigger:
                        triggerAction = False
                        break
            else:
                self._logger.debug(
                    '[%s] No monitor xml doc, will not trigger action' %
                    (self.__class__.__name__))
        except Exception as ex:
            self._logger.error('[%s] Could not evaluate data: %s' %
                               (self.__class__.__name__, ex))

            self._logger.debug('[%s] Will not trigger action' %
                               (self.__class__.__name__))

        self._logger.debug('[%s] Returning trigger action flag: [%s]' %
                           (self.__class__.__name__, triggerAction))

        return triggerAction

    def __evaluateXPathVariables(self, xmlDoc, xPathVariableList):
        resultDict = {}

        if not xmlDoc:
            return resultDict

        self._logger.debug('[%s] xPath variable list: %s' %
                           (self.__class__.__name__, xPathVariableList))

        for v in xPathVariableList:
            name = v.getName()

            value = ''

            try:
                self._logger.debug(
                    '[%s] Evaluating xPath variable %s: %s' %
                    (self.__class__.__name__, name, v.getXPath()))

                value = xmlDoc.xpathEval('%s' % v.getXPath())
            except Exception as ex:
                self._logger.error(
                    '[%s] Could not evaluate xPath variable [%s]: %s' %
                    (self.__class__.__name__, name, ex))

                self._logger.debug('[%s] Will replace it with empty string' %
                                   (self.__class__.__name__))

            resultDict[name] = value

        self._logger.debug('[%s] XPath variable replacement dictionary: %s' %
                           (self.__class__.__name__, resultDict))

        return resultDict

    def __replaceXPathVariables(self, inputString, xPathReplacementDict):         \
            # pylint: disable=no-self-use

        outputString = inputString

        #self._logger.debug('Original string: %s' % inputString)

        for key in xPathReplacementDict:
            #self._logger.debug('Replacing: %s' % key)

            outputString = outputString.replace(
                key, '%s' % xPathReplacementDict[key])

        #self._logger.debug('New string: %s' % outputString)

        return outputString

    def __poll(self, rule):
        ruleId = self.__getRuleId(rule.getApplicationName(), rule.getName())

        self._logger.debug('[%s] Begin poll timer for [%s]' %
                           (self.__class__.__name__, ruleId))

        if not self.hasRule(ruleId):
            self._logger.debug('[%s] Timer execution cancelled for [%s]' %
                               (self.__class__.__name__, ruleId))

            return

        rule.ruleInvoked()

        self._logger.debug('[%s] Timer execution started for [%s]' %
                           (self.__class__.__name__, ruleId))

        appMonitor = rule.getApplicationMonitor()

        queryCmd = appMonitor.getQueryCommand()

        self._logger.debug('[%s] Query command: %s' %
                           (self.__class__.__name__, queryCmd))

        actionCmd = appMonitor.getActionCommand()

        self._logger.debug('[%s] Action command: %s' %
                           (self.__class__.__name__, actionCmd))

        xPathReplacementDict = {}

        try:
            invokeAction = True
            queryStdOut = None

            if queryCmd:
                self._logger.debug('[%s] About to invoke: [%s]' %
                                   (self.__class__.__name__, queryCmd))

                try:
                    p = tortugaSubprocess.executeCommand(
                        'source %s/tortuga.sh && ' % (self._cm.getEtcDir()) +
                        queryCmd)

                    queryStdOut = p.getStdOut()

                    appMonitor.queryInvocationSucceeded()
                except Exception as ex:
                    appMonitor.queryInvocationFailed()
                    raise

                monitorXmlDoc = self.__parseMonitorData(queryStdOut)

                xPathReplacementDict = self.__evaluateXPathVariables(
                    monitorXmlDoc, rule.getXPathVariableList())

                invokeAction = self.__evaluateConditions(
                    rule, monitorXmlDoc, xPathReplacementDict)

            if invokeAction:
                try:
                    actionCmd = self.__replaceXPathVariables(
                        actionCmd, xPathReplacementDict)

                    self._logger.debug('[%s] About to invoke: [%s]' %
                                       (self.__class__.__name__, actionCmd))

                    p = tortugaSubprocess.executeCommand(
                        'source %s/tortuga.sh && ' % (self._cm.getEtcDir()) +
                        actionCmd)

                    appMonitor.actionInvocationSucceeded()

                    self._logger.debug('[%s] Done with command: [%s]' %
                                       (self.__class__.__name__, actionCmd))
                except Exception as ex:
                    appMonitor.actionInvocationFailed()
                    raise
            else:
                self._logger.debug('[%s] Will skip action: [%s]' %
                                   (self.__class__.__name__, actionCmd))
        except TortugaException as ex:
            self._logger.error('[%s] %s' % (self.__class__.__name__, ex))

        scheduleTimer = True

        if self.hasRule(ruleId):
            # Check if we need to stop invoking this rule.
            maxActionInvocations = appMonitor.getMaxActionInvocations()

            successfulActionInvocations = \
                appMonitor.getSuccessfulActionInvocations()

            if maxActionInvocations:
                if int(maxActionInvocations) <= successfulActionInvocations:
                    # Rule must be disabled.
                    self._logger.debug(
                        '[%s] Max. number of successful invocations (%s)'
                        ' reached for rule [%s]' %
                        (self.__class__.__name__, maxActionInvocations,
                         ruleId))

                    scheduleTimer = False
                    self.disableRule(rule.getApplicationName(), rule.getName())
        else:
            # Rule is already deleted.
            scheduleTimer = False

        if scheduleTimer:
            pollPeriod = float(appMonitor.getPollPeriod())

            # Make sure we do not fire too often.
            lastSuccessfulActionTime = \
                appMonitor.getLastSuccessfulActionInvocationTime()

            if lastSuccessfulActionTime:
                now = time.time()

                possibleNewSuccessfulActionTime = \
                    now + pollPeriod - lastSuccessfulActionTime

                if possibleNewSuccessfulActionTime < self._minTriggerInterval:
                    pollPeriod = self._minTriggerInterval

                    self._logger.debug(
                        '[%s] Increasing poll period to [%s] for'
                        ' rule [%s]' %
                        (self.__class__.__name__, pollPeriod, ruleId))

            self._logger.debug('[%s] Scheduling new timer for rule [%s] in'
                               ' [%s] seconds' %
                               (self.__class__.__name__, ruleId, pollPeriod))

            t = threading.Timer(pollPeriod, self.__poll, args=[rule])

            t.daemon = True

            self.__runPollTimer(ruleId, t)
        else:
            self._logger.debug(
                '[%s] Will not schedule new timer for rule [%s]' %
                (self.__class__.__name__, rule))

    def __runPollTimer(self, ruleId, pollTimer):
        self._pollTimerDict[ruleId] = pollTimer

        self._logger.debug('[%s] Starting poll timer for [%s]' %
                           (self.__class__.__name__, ruleId))

        pollTimer.start()

    def __cancelPollTimer(self, ruleId):
        if ruleId not in self._pollTimerDict:
            self._logger.debug('[%s] No poll timer for [%s]' %
                               (self.__class__.__name__, ruleId))

            return

        pollTimer = self._pollTimerDict[ruleId]

        self._logger.debug('[%s] Stopping poll timer for [%s]' %
                           (self.__class__.__name__, ruleId))

        pollTimer.cancel()

        del self._pollTimerDict[ruleId]

    def __process(self):
        self._logger.debug('[%s] Begin processing timer' %
                           (self.__class__.__name__))

        while True:
            qSize = self._receiveQ.qsize()

            self._logger.debug('[%s] Current receive Q size: %s' %
                               (self.__class__.__name__, qSize))

            if qSize == 0:
                break

            applicationName, applicationData = self._receiveQ.get()

            self._logger.debug('[%s] Processing data for [%s]' %
                               (self.__class__.__name__, applicationName))

            monitorXmlDoc = self.__parseMonitorData(applicationData)

            for ruleId in self._receiveRuleDict.keys():
                rule = self._receiveRuleDict.get(ruleId)

                # Rule might have been cancelled before we use it.
                if not rule:
                    continue

                # Check if this is appropriate for the data.
                if rule.getApplicationName() != applicationName:
                    continue

                self._logger.debug('[%s] Processing data using rule [%s]' %
                                   (self.__class__.__name__, ruleId))

                rule.ruleInvoked()

                appMonitor = rule.getApplicationMonitor()

                actionCmd = appMonitor.getActionCommand()

                self._logger.debug('[%s] Action command: [%s]' %
                                   (self.__class__.__name__, actionCmd))

                try:
                    xPathReplacementDict = self.__evaluateXPathVariables(
                        monitorXmlDoc, rule.getXPathVariableList())

                    invokeAction = self.__evaluateConditions(
                        rule, monitorXmlDoc, xPathReplacementDict)

                    if invokeAction:
                        try:
                            actionCmd = self.__replaceXPathVariables(
                                actionCmd, xPathReplacementDict)

                            self._logger.debug(
                                '[%s] About to invoke: [%s]' %
                                (self.__class__.__name__, actionCmd))

                            tortugaSubprocess.executeCommand(
                                'source %s/tortuga.sh && ' %
                                (self._cm.getEtcDir()) + actionCmd)

                            appMonitor.actionInvocationSucceeded()

                            self._logger.debug(
                                '[%s] Done with command: [%s]' %
                                (self.__class__.__name__, actionCmd))

                            maxActionInvocations = \
                                appMonitor.getMaxActionInvocations()

                            successfulActionInvocations = \
                                appMonitor.getSuccessfulActionInvocations()

                            if maxActionInvocations:
                                if int(maxActionInvocations) <= \
                                        successfulActionInvocations:
                                    # Rule must be disabled.
                                    self._logger.debug(
                                        '[%s] Max. number of successful'
                                        ' invocations (%s) reached for'
                                        ' rule [%s]' %
                                        (self.__class__.__name__,
                                         maxActionInvocations, ruleId))

                                    self.disableRule(rule.getApplicationName(),
                                                     rule.getName())
                        except Exception as ex:
                            appMonitor.actionInvocationFailed()
                    else:
                        self._logger.debug(
                            '[%s] Will skip action: [%s]' %
                            (self.__class__.__name__, actionCmd))
                except TortugaException as ex:
                    self._logger.error('[%s] %s' %
                                       (self.__class__.__name__, ex))

            self._logger.debug('[%s] No more rules appropriate for [%s]' %
                               (self.__class__.__name__, applicationName))

        # No more data to process, exit timer.
        self._logger.debug('[%s] No more data to process' %
                           (self.__class__.__name__))

        self.__cancelProcessingTimer()

    def __runProcessingTimer(self):
        self._processingLock.acquire()

        try:
            if not self._processingTimerRunning:
                self._logger.debug('[%s] Starting processing timer' %
                                   (self.__class__.__name__))

                self._processingTimer = threading.Timer(5, self.__process)
                self._processingTimer.daemon = True
                self._processingTimer.start()
                self._processingTimerRunning = True
            else:
                self._logger.debug('[%s] Processing timer already running' %
                                   (self.__class__.__name__))
        finally:
            self._processingLock.release()

    def __cancelProcessingTimer(self):
        self._processingLock.acquire()

        try:
            self._processingTimerRunning = False

            self._logger.debug('[%s] Processing timer stopped' %
                               (self.__class__.__name__))
        finally:
            self._processingLock.release()

    def hasRule(self, ruleId):
        return ruleId in self._ruleDict

    def addRule(self, rule):
        self._lock.acquire()

        try:
            return self.__addRule(rule)
        finally:
            self._lock.release()

    def __addRule(self, rule):
        ruleId = self.__getRuleId(rule.getApplicationName(), rule.getName())

        self._logger.debug('[%s] Adding rule: [%s]' %
                           (self.__class__.__name__, ruleId))

        self.__checkRuleDoesNotExist(ruleId)

        # Write rule file.
        self.__writeRuleFile(rule)

        rule.decode()

        self._ruleDict[ruleId] = rule
        if rule.isStatusEnabled():
            self.__enableRule(rule)
        else:
            # Rule is disabled, just put it in the 'disabled' dict
            self._disabledRuleDict[ruleId] = rule

        return ruleId

    def __enableRule(self, rule):
        ruleId = self.__getRuleId(rule.getApplicationName(), rule.getName())

        self._logger.debug('[%s] Enabling rule: [%s]' %
                           (self.__class__.__name__, ruleId))

        appMonitor = rule.getApplicationMonitor()

        monitorType = appMonitor.getType()

        rule.setStatusEnabled()

        if monitorType == 'poll':
            self._logger.debug('[%s] [%s] is poll rule' %
                               (self.__class__.__name__, ruleId))

            pollPeriod = appMonitor.getPollPeriod()

            if not pollPeriod:
                pollPeriod = self._minTriggerInterval

            self._logger.debug(
                '[%s] Preparing poll timer with period %s second(s)' %
                (self.__class__.__name__, pollPeriod))

            t = threading.Timer(float(pollPeriod), self.__poll, args=[rule])
            t.daemon = True
            self.__runPollTimer(ruleId, t)
        elif monitorType == 'receive':
            self._logger.debug('[%s] [%s] is receive rule' %
                               (self.__class__.__name__, ruleId))

            self._receiveRuleDict[ruleId] = rule
        else:
            # assume this is 'event' rule
            self._logger.debug('[%s] [%s] is event rule' %
                               (self.__class__.__name__, ruleId))

            self._eventRuleDict[ruleId] = rule

        if ruleId in self._disabledRuleDict:
            del self._disabledRuleDict[ruleId]

    def enableRule(self, applicationName, ruleName):
        """
        Raises:
            RuleAlreadyEnabled
        """

        self._lock.acquire()

        try:
            ruleId = self.__getRuleId(applicationName, ruleName)

            self.__checkRuleExists(ruleId)

            if ruleId not in self._disabledRuleDict:
                raise RuleAlreadyEnabled('Rule [%s] is already enabled.' %
                                         (ruleId))

            rule = self._ruleDict[ruleId]

            self.__enableRule(rule)

            rule.encode()

            self.__writeRuleFile(rule)

            rule.decode()
        finally:
            self._lock.release()

    def deleteRule(self, applicationName, ruleName):
        self._lock.acquire()
        try:
            return self.__deleteRule(applicationName, ruleName)
        finally:
            self._lock.release()

    def __deleteRule(self, applicationName, ruleName):
        ruleId = self.__getRuleId(applicationName, ruleName)

        self._logger.debug('[%s] Deleting rule %s' %
                           (self.__class__.__name__, ruleId))

        self.__checkRuleExists(ruleId)

        rule = self._ruleDict[ruleId]

        if rule.isStatusEnabled():
            self.__disableRule(rule)

        del self._disabledRuleDict[ruleId]

        del self._ruleDict[ruleId]

        osUtility.removeFile(self.__getRuleFileName(applicationName, ruleName))

    # Put rule in the 'disabled' state.
    def disableRule(self, applicationName, ruleName):
        """
        Raises:
            RuleAlreadyDisabled
        """

        self._lock.acquire()

        try:
            ruleId = self.__getRuleId(applicationName, ruleName)

            self.__checkRuleExists(ruleId)

            if ruleId in self._disabledRuleDict:
                raise RuleAlreadyDisabled('Rule [%s] is already disabled.' %
                                          ruleId)

            rule = self._ruleDict[ruleId]

            self.__disableRule(rule)

            rule.encode()

            self.__writeRuleFile(rule)

            rule.decode()
        finally:
            self._lock.release()

    def __disableRule(self, rule, status='disabled by administrator'):
        ruleId = self.__getRuleId(rule.getApplicationName(), rule.getName())

        self._logger.debug('[%s] Disabling rule [%s]' %
                           (self.__class__.__name__, ruleId))

        appMonitor = rule.getApplicationMonitor()

        monitorType = appMonitor.getType()

        rule.setStatus(status)

        if monitorType == 'poll':
            self.__cancelPollTimer(ruleId)
        elif monitorType == 'receive':
            del self._receiveRuleDict[ruleId]
        else:
            del self._eventRuleDict[ruleId]

        self._disabledRuleDict[ruleId] = rule

    def getRule(self, applicationName, ruleName):
        self._lock.acquire()

        try:
            return self.__getRule(applicationName, ruleName)
        finally:
            self._lock.release()

    def __getRule(self, applicationName, ruleName):
        ruleId = self.__getRuleId(applicationName, ruleName)

        self.__checkRuleExists(ruleId)

        return copy.deepcopy(self._ruleDict[ruleId])

    def getRuleList(self):
        self._lock.acquire()
        try:
            return self.__getRuleList()
        finally:
            self._lock.release()

    def __getRuleList(self):
        ruleList = TortugaObjectList()

        for ruleId in self._ruleDict.keys():
            ruleList.append(copy.deepcopy(self._ruleDict[ruleId]))

        return ruleList

    def receiveApplicationData(self, applicationName, applicationData):
        self._lock.acquire()
        try:
            return self.__receiveApplicationData(applicationName,
                                                 applicationData)
        finally:
            self._lock.release()

    def __receiveApplicationData(self, applicationName, applicationData):
        self._logger.debug('[%s] Received data for [%s]' %
                           (self.__class__.__name__, applicationName))

        self._receiveQ.put((applicationName, applicationData))

        self.__runProcessingTimer()

    def executeRule(self, applicationName, ruleName, applicationData):
        self._lock.acquire()
        try:
            return self.__executeRule(applicationName, ruleName,
                                      applicationData)
        finally:
            self._lock.release()

    def __executeRule(self, applicationName, ruleName, applicationData):
        """
        Raises:
            RuleDisabled
        """

        ruleId = self.__getRuleId(applicationName, ruleName)

        self._logger.debug('[%s] Received request to execute rule [%s]' %
                           (self.__class__.__name__, ruleId))

        self.__checkRuleExists(ruleId)

        if ruleId in self._disabledRuleDict:
            raise RuleDisabled('Rule [%s] is disabled.' % (ruleId))

        rule = self._ruleDict[ruleId]

        appMonitor = rule.getApplicationMonitor()

        monitorType = appMonitor.getType()

        if monitorType == 'poll':
            self._logger.debug('[%s] [%s] is poll rule' %
                               (self.__class__.__name__, ruleId))

            self.__cancelPollTimer(ruleId)

            self.__poll(rule)
        elif monitorType == 'receive':
            self._logger.debug('[%s] [%s] is receive rule' %
                               (self.__class__.__name__, ruleId))

            self._receiveQ.put((applicationName, applicationData))

            self.__runProcessingTimer()
        else:
            # assume this is 'event' rule
            self._logger.debug('[%s] [%s] is event rule' %
                               (self.__class__.__name__, ruleId))

            self.__execute(rule)

    def __execute(self, rule):
        ruleId = self.__getRuleId(rule.getApplicationName(), rule.getName())

        self._logger.debug('[%s] Begin execution for [%s]' %
                           (self.__class__.__name__, ruleId))

        rule.ruleInvoked()

        appMonitor = rule.getApplicationMonitor()

        queryCmd = appMonitor.getQueryCommand()

        self._logger.debug('[%s] Query command: [%s]' %
                           (self.__class__.__name__, queryCmd))

        actionCmd = appMonitor.getActionCommand()

        self._logger.debug('[%s] Action command: [%s]' %
                           (self.__class__.__name__, actionCmd))

        xPathReplacementDict = {}

        try:
            invokeAction = True
            queryStdOut = None

            if queryCmd:
                self._logger.debug('[%s] About to invoke: [%s]' %
                                   (self.__class__.__name__, queryCmd))

                try:
                    p = tortugaSubprocess.executeCommand(
                        'source %s/tortuga.sh && ' % (self._cm.getEtcDir()) +
                        queryCmd)

                    queryStdOut = p.getStdOut()

                    appMonitor.queryInvocationSucceeded()
                except Exception as ex:
                    appMonitor.queryInvocationFailed()
                    raise

                monitorXmlDoc = self.__parseMonitorData(queryStdOut)

                xPathReplacementDict = self.__evaluateXPathVariables(
                    monitorXmlDoc, rule.getXPathVariableList())

                invokeAction = self.__evaluateConditions(
                    rule, monitorXmlDoc, xPathReplacementDict)

            if invokeAction:
                try:
                    actionCmd = self.__replaceXPathVariables(
                        actionCmd, xPathReplacementDict)

                    self._logger.debug('[%s] About to invoke: [%s]' %
                                       (self.__class__.__name__, actionCmd))

                    p = tortugaSubprocess.executeCommand(
                        'source %s/tortuga.sh && ' % (self._cm.getEtcDir()) +
                        actionCmd)

                    appMonitor.actionInvocationSucceeded()

                    self._logger.debug('[%s] Done with command: [%s]' %
                                       (self.__class__.__name__, actionCmd))
                except Exception as ex:
                    appMonitor.actionInvocationFailed()
                    raise
            else:
                self._logger.debug('[%s] Will skip action: [%s]' %
                                   (self.__class__.__name__, actionCmd))
        except TortugaException as ex:
            self._logger.error('[%s] %s' % (self.__class__.__name__, ex))

        if self.hasRule(ruleId):
            # Check if we need to stop invoking this rule.
            maxActionInvocations = appMonitor.getMaxActionInvocations()

            successfulActionInvocations = \
                appMonitor.getSuccessfulActionInvocations()

            if maxActionInvocations:
                if int(maxActionInvocations) <= successfulActionInvocations:
                    # Rule must be disabled.
                    self._logger.debug(
                        '[%s] Max. number of successful invocations (%s)'
                        ' reached for rule [%s]' %
                        (self.__class__.__name__, maxActionInvocations,
                         ruleId))

                    self.disableRule(rule.getApplicationName(), rule.getName())