name : mysql.py
# coding=utf-8
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2020 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#
import time
import warnings
import subprocess
from typing import Optional, Tuple

try:
    import pymysql
except ImportError:
    pymysql = None

from clcommon.cpapi import db_access
from clcommon.cpapi.cpapiexceptions import NoDBAccessData, NotSupported
from clcommon import mysql_lib

from .collector_base import CollectorBase

from cl_plus.daemon.daemon_control import _SYSTEMCTL_BIN

# enough to collect only several times per minute, cause we rely on DB counter
MYSQL_COLLECTION_INTERVAL = 30

def _total_queries_num_request(db, query_str):
    """
    Execs passed query and returns value
    """
    with warnings.catch_warnings():
        warnings.filterwarnings('ignore', category=pymysql.Warning)
        data = db.execute_query(query_str)
    return data[0][1]


class MySQLCollector(CollectorBase):
    class DBAccess:
        def __init__(self, login: Optional[str] = None, password: Optional[str] = None):
            self.login = login
            self.password = password
            self.expire_time = time.time() + 3600

        def __eq__(self, other):
            return self.login == other.login and self.password == other.password

        def __bool__(self):
            return bool(self.login and self.password and not self.is_expired())

        def is_expired(self) -> bool:
            return time.time() > self.expire_time

    def __init__(self, _logger):
        super(MySQLCollector, self).__init__(_logger)
        self._is_mysql_error = False
        self.collection_interval = MYSQL_COLLECTION_INTERVAL
        self.access = self.DBAccess()
        self.previous_num, self.current_num = None, None
        self._aggregation_times = 0

    def init(self):
        """
        Initialize MySQL collector
        :return: None
        """
        self._aggregated_data = 0
        self._logger.info("MySQL collector init")
        self._logger.info('MySQL collector initial values: previous: %s, current: %s',
                          str(self.previous_num),
                          str(self.current_num))

    def _get_total_queries_num(self, dblogin: str, dbpass: str):
        """
        Gets total queries number via Queries variable in DB
        it is a total counter of queries made to DB

        it is used be such monitoring system as Nagios and mysqladmin returns it
        https://github.com/nagios-plugins/nagios-plugins/blob/master/plugins/check_mysql.c
        """
        if not pymysql:
            return "No MySQL client libraries installed", 0

        max_retries = 3
        retry_delay = 5  # seconds

        for attempt in range(max_retries):
            try:
                connector = mysql_lib.MySQLConnector(host='localhost',
                                                     user=dblogin,
                                                     passwd=dbpass,
                                                     db='mysql',
                                                     use_unicode=True,
                                                     charset='utf8')
                with connector.connect() as db:
                    total = _total_queries_num_request(db, 'show global status where Variable_name = \'Questions\';')
                self._aggregation_times += 1
                # Log success if we had previous errors
                if attempt > 0:
                    self._logger.info("MySQL collector: connection succeeded on attempt %d", attempt + 1)
                return 'OK', int(total)
            except Exception as e:
                # Check if it's a connection error that we should retry
                is_connection_error = (hasattr(e, 'args') and len(e.args) > 0 and 
                                     isinstance(e.args[0], int) and e.args[0] in (2002, 2003))

                if attempt < max_retries - 1 and is_connection_error:
                    self._logger.info("MySQL collector: connection attempt %d/%d failed, retrying in %d seconds: %s",
                                     attempt + 1, max_retries, retry_delay, str(e))
                    time.sleep(retry_delay)
                    continue
                else:
                    # Last attempt failed - try to restart cl_plus_sender.service
                    if is_connection_error:
                        self._logger.warning("MySQL collector: all %d connection attempts failed, attempting to restart cl_plus_sender.service", max_retries)
                        if self._restart_cl_plus_sender_service():
                            # Give service time to start
                            time.sleep(10)
                            # Try one more time after service restart
                            try:
                                connector = mysql_lib.MySQLConnector(host='localhost',
                                                                     user=dblogin,
                                                                     passwd=dbpass,
                                                                     db='mysql',
                                                                     use_unicode=True,
                                                                     charset='utf8')
                                with connector.connect() as db:
                                    total = _total_queries_num_request(db, 'show global status where Variable_name = \'Questions\';')
                                self._aggregation_times += 1
                                self._logger.info("MySQL collector: connection succeeded after cl_plus_sender.service restart")
                                return 'OK', int(total)
                            except Exception as retry_e:
                                self._logger.error("MySQL collector: connection failed even after cl_plus_sender.service restart: %s", str(retry_e))
                                return str(retry_e), 0
                        else:
                            self._logger.error("MySQL collector: failed to restart cl_plus_sender.service")
                    return str(e), 0

        return "Max retries exceeded", 0

    def _restart_cl_plus_sender_service(self):
        """
        Restart cl_plus_sender.service using systemctl, but only if it's enabled and not active
        :return: True if successful, False otherwise
        """
        try:
            # Check if service is enabled
            check_enabled = subprocess.run([_SYSTEMCTL_BIN, 'is-enabled', 'cl_plus_sender.service'],
                                        capture_output=True, text=True, timeout=10,
                                        env={'PATH': '/usr/bin:/bin'})  # Restrict PATH

            if check_enabled.returncode != 0 or check_enabled.stdout.strip() == 'disabled':
                self._logger.info("MySQL collector: cl_plus_sender.service is disabled, skipping restart")
                return False

            # Service is enabled, proceed with restart
            self._logger.info("MySQL collector: cl_plus_sender.service is enabled, attempting restart")
            result = subprocess.run([_SYSTEMCTL_BIN, 'restart', 'cl_plus_sender.service'],
                                   capture_output=True, text=True, timeout=30,
                                   env={'PATH': '/usr/bin:/bin'})  # Restrict PATH
            if result.returncode == 0:
                self._logger.info("MySQL collector: successfully restarted cl_plus_sender.service")
                return True
            else:
                self._logger.error("MySQL collector: failed to restart cl_plus_sender.service: %s", result.stderr)
                return False
        except subprocess.TimeoutExpired:
            self._logger.error("MySQL collector: timeout while checking/restarting cl_plus_sender.service")
            return False
        except Exception as e:
            self._logger.error("MySQL collector: error checking/restarting cl_plus_sender.service: %s", str(e))
            return False

    def _get_db_access(self) -> Tuple[str, str]:
        """
        Get DB access data from cpapi function. Logs error
        :return: tuple (db_root_login, db_password)
            None, None if error
        """
        if not self.access:
            try:
                access = db_access()    # {'pass': str, 'login': str}
                self.access = self.DBAccess(access['login'], access['pass'])
            except (NoDBAccessData, NotSupported, KeyError, TypeError) as e:
                # Can't retrieve data
                if not self._is_mysql_error:
                    self._logger.warn("MySQL collector: can't obtain MySQL DB access: %s", str(e))
                    self._is_mysql_error = True
        return self.access.login, self.access.password

    def aggregate_new_data(self):
        """
        Retrieve and aggregate new data
        :return None
        """
        self._get_db_access()
        if not self.access:     # access data still is not set
            return

        # New data present - aggregate
        message, total_queries = self._get_total_queries_num(self.access.login, self.access.password)

        if message == 'OK':
            self.previous_num = self.current_num
            self.current_num = total_queries

            # no previous value to compare during 1st iteration after collector is started
            if self.previous_num is not None and self.current_num is not None:

                # if mysql counter was re-set
                if self.current_num < self.previous_num:
                    self._logger.info('MySQL collector: QUERIES counter was re-set in database, '
                                      're-set collectors previous counter as well')
                    self.previous_num = 0
                self._aggregated_data += self.current_num - self.previous_num

            self._is_mysql_error = False
            return

        # Retrieve data error
        if not self._is_mysql_error:
            self._logger.error("MySQL collector can't obtain MySQL queries number: %s", message)
            self._is_mysql_error = True

    def get_averages(self):
        """
        Get collector's averages data
        :return: dict:
            { "mysql_queries_num": 16}
            or None if can't get data
        """
        mysql_queries_num = max(0, self._aggregated_data - self._aggregation_times)
        self._aggregated_data = 0
        self._aggregation_times = 0
        return {"mysql_queries_num": mysql_queries_num}

© 2025 UnknownSec
afwwrfwafr45458465
Password