shell bypass 403
# 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}