AlkantarClanX12
Current Path : /opt/cloudlinux/venv/lib/python3.11/site-packages/clsummary/ |
Current File : //opt/cloudlinux/venv/lib/python3.11/site-packages/clsummary/summary.py |
# coding=utf-8 # # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENSE.TXT import datetime import hashlib import itertools import json import logging import math import os import platform import random import re import string import subprocess import sys import time import typing from collections import Counter, defaultdict from functools import lru_cache, partial from itertools import groupby from multiprocessing import cpu_count from operator import attrgetter from pathlib import Path from socket import getfqdn from typing import Any, AnyStr, Callable, Dict, List, Optional, Tuple, Union # NOQA import cldetectlib as detect import lvectllib import psutil import requests from cl_proc_hidepid import get_hidepid_typing_from_mounts from clcommon import cpapi from clcommon.clwpos_lib import ( find_wp_paths, get_wp_cache_plugin, get_wp_paths_with_enabled_module, ) from clcommon.const import Feature from clcommon.cpapi import is_panel_feature_supported, is_wp2_environment from clcommon.lib import MySQLGovernor from clcommon.lib.cledition import ( CLEditions, is_cl_shared_edition, is_cl_solo_edition, ) from clcommon.lib.cmt_utils import client_activation_data from clcommon.lib.consts import ACCELERATE_WP_INSTALLED_FROM_CM from clcommon.sysctl import SysCtlConf from clcommon.utils import ( ExternalProgramFailed, get_cl_version, get_rhn_systemid_value, get_virt_type, grep, is_litespeed_running, is_nginx_running, is_secureboot_enabled, is_testing_enabled_repo, is_ubuntu, run_command, ) from clconfig import cagefs_statistics_config, clconfig_utils, db_governor_lib from cldiaglib import is_email_notification_enabled from cli_utils import print_dictionary, replace_params from cllimitslib_v2 import DEFAULTS, LimitsDataStorage from clveconfig import EMPTY_LIMITS from clwizard.modules import ALL_MODULES from lve_utils import PKG_VERSION as LVE_UTILS_PKG_VERSION from lveapi import LvpMap from vendors_api.config import CONFIG_PATH, _read_config_file from clsummary.utils import ( SummaryStatus, dummy_none_function, get_cl_plus_sender_status, get_client_data_from_jwt_token, get_packages_with_lve_extensions, get_statistics_send_status_from_file, is_active_cloudlinux_license, is_nodejs_selector_installed, is_php_selector_installed, is_python_selector_installed, is_ruby_selector_installed, is_sending_process_running, is_statistic_enabled, is_statistic_rpm_enabled, set_statistic_collection_enabled, set_statistic_rpm_collection_enabled, write_statistics_send_status_to_file, ) from clsummary.hardware_statistics import ( NotSupported, get_cpu_metrics, get_memory_metrics, ) from clsummary.rpm_packages_statistics import get_rpm_packages_info from .arg_parse import parse_cloudlinux_summary_opts from .storage import StatisticsDict try: # Package from lvemanager - can be absent from clselect.clselectctl import get_default_version from clselect.clselectstatistics import ( get_default_php_version, get_mode_of_php_selector, get_native_version_safe, get_php_selector_usage, get_versions_statistics, iter_server_applications, ) from clselector.selectorlib import CloudlinuxSelectorLib from lvemanager import PKG_RELEASE as LVEMANAGER_PKG_RELEASE from lvemanager import PKG_VERSION as LVEMANAGER_PKG_VERSION except ImportError: iter_server_applications = dummy_none_function get_mode_of_php_selector = dummy_none_function get_default_php_version = dummy_none_function get_default_version = dummy_none_function get_versions_statistics = dummy_none_function get_native_version_safe = dummy_none_function get_php_selector_usage = dummy_none_function CloudlinuxSelectorLib = None LVEMANAGER_PKG_VERSION = None LVEMANAGER_PKG_RELEASE = None try: from clflags import list_flags_info except ImportError: def list_flags_info(): return [] LOG_FILE = "/var/log/cloudlinux-summary.log" app_logger = logging.getLogger("cloudlinux-summary") UNKNOWN_RHN_ID = "unknown" INSTALLED = "installed" NOT_INSTALLED = "not_installed" NOT_INITIALIZED = "not_initialized" NOT_SELECTED = "not_selected" ENABLED = "enabled" DISABLED = "disabled" ERROR = "-42" def site_has_enabled_modules(site_dict: dict) -> bool: """ Checks if wordpress site has at least one enabled module """ return any(map(lambda module: module["enabled"], site_dict["modules"].values())) class CloudlinuxSummary: DASHBOARD_CERTIFICATE = "/var/lve/dashboard_certificate" CL_PLUS_CM_DISABLED_PATH = "/etc/cl_plus/.disabled" SELECTORS = itertools.compress( ["python", "ruby", "nodejs"], [ cpapi.is_panel_feature_supported(Feature.PYTHON_SELECTOR), cpapi.is_panel_feature_supported(Feature.RUBY_SELECTOR), cpapi.is_panel_feature_supported(Feature.NODEJS_SELECTOR), ], ) # Utility will send statistics to this server SUMMARY_URL = "https://stat-api.cloudlinux.com/api/clos-stat" RPM_PACKAGES_URL = "https://stat-api.cloudlinux.com/api/rpm-stats" SETTINGS_URL = "https://repo.cloudlinux.com/static/cl-settings-v1.json" def __init__(self): self._opts = {} self._security_token = None self.statistics: StatisticsDict = StatisticsDict() self._lvpmap = None self._system_id = None self.is_process_not_limited = self._is_process_not_limited() self.packages_by_len = None self.sysctl = SysCtlConf() @staticmethod def _is_process_not_limited(): """ Return true if process is running outside LVE or it's not running by utility `nice` """ is_running_by_nice = bool(os.environ.get("RUNNING_BY_NICE")) is_running_in_lve = bool(os.environ.get("RUNNING_IN_LVE")) return not is_running_by_nice and not is_running_in_lve @property def lvpmap(self): """ Load lvpmap only when needed """ if self._lvpmap is None: self._lvpmap = _get_lvpmap() return self._lvpmap @property def system_id(self) -> str: if self._system_id is None: self._system_id = get_rhn_systemid_value("system_id") return self._system_id @staticmethod def _get_platform(): return "ubuntu" if is_ubuntu() else "rhel_like" @staticmethod def _detect_secureboot(): return ENABLED if is_secureboot_enabled() else DISABLED @staticmethod def _generate_security_token(): range_for_random_choice = string.ascii_letters + string.digits security_token = "".join(random.choice(range_for_random_choice) for _ in range(64)) return security_token def _get_remote_data(self) -> dict: stat_data = {} if self.security_token is None: message = "Security token is empty" app_logger.error(message) self._error_and_exit({"result": message}) message = f"Getting statistics from server {self.SUMMARY_URL}" app_logger.info(message) params = { "system_id": self.system_id, "security_token": self.security_token, } response = None try: response = requests.get(self.SUMMARY_URL, params=params, timeout=60) except requests.RequestException as e: message = str(e) app_logger.error(message) self._error_and_exit({"result": message}) if not response.ok: message = f"Server answer is: HTTP code {response.status_code}; Reason: {response.reason}" app_logger.info(message) self._error_and_exit({"result": message}) app_logger.info("Received response from the server") try: stat_data = response.json()["result"] # extend remote statistics with Smart Advice statistics, # resolved LOCALLY (for details, please see XRAY-427) if isinstance(stat_data["result"], dict): stat_data["result"].update(self._get_smart_advice_statistics()) except (TypeError, ValueError): message = "Can't parse api response to json" app_logger.error(message) self._error_and_exit({"result": message}) except KeyError as e: app_logger.error('Invalid json response from server, field %s not found in "%s"', str(e), response.text) self._error_and_exit({"result": "Invalid response from server. " f"See {LOG_FILE} for details."}) else: app_logger.info("SUCCESS: received statistics from the server") return stat_data @property def security_token(self): if self._security_token is not None: return self._security_token if os.path.isfile(self.DASHBOARD_CERTIFICATE): self._security_token = self._read_token_from_file() else: # generate token if we do not have certificate file token = self._generate_security_token() self._security_token = token if self._write_token_to_file(token) else None return self._security_token def _write_token_to_file(self, token): """ Write security token to file and return success/fail status :param token: generated security token :return: T/F status """ try: with open(self.DASHBOARD_CERTIFICATE, "w", encoding="utf-8") as f: f.write(token) os.chmod(self.DASHBOARD_CERTIFICATE, 0o600) return True except (IOError, OSError) as e: app_logger.error("Error while writing secure token to file: %s", str(e)) return False def _read_token_from_file(self): try: with open(self.DASHBOARD_CERTIFICATE, "r", encoding="utf-8") as f: return f.read().strip() or None except (IOError, OSError) as e: app_logger.error("Error while reading file with secure token: %s", str(e)) return None @staticmethod def _detect_old_lve_integration() -> bool: """ Detect old LVE limits integration presence according to https://docs.cloudlinux.com/index.html?lve_limits_with_packages.html :return: True/False - present/absent """ # Try to get script name from config return detect.get_boolean_param( file_name=detect.CL_CONFIG_FILE, param_name="CUSTOM_GETPACKAGE_SCRIPT", separator="=", default_val=False, ) @staticmethod def _is_lsapi_present(): """ Detects presence/absence of lsapi :return: True/False """ return os.path.exists("/usr/bin/switch_mod_lsapi") @staticmethod def _get_status_of_selector(interpreter: str) -> str: """ Get selector status for nodejs, python, ruby and php selectors """ # Ruby cannot be disabled, so check on installation is enough if interpreter == "python": if not is_python_selector_installed(): return NOT_INSTALLED elif interpreter == "ruby": return ENABLED if is_ruby_selector_installed() else NOT_INSTALLED elif interpreter == "nodejs": if not is_nodejs_selector_installed(): return NOT_INSTALLED elif interpreter == "php" and not is_php_selector_installed(): return NOT_INSTALLED lib = CloudlinuxSelectorLib(interpreter) if lib is None: return NOT_INSTALLED if interpreter in ["nodejs", "python"]: try: return ENABLED if lib.get_selector_status()["selector_enabled"] else DISABLED except KeyError: return NOT_INSTALLED elif interpreter == "php": return DISABLED if lib.php_selector_is_disabled() else ENABLED raise ValueError(f"Unknown interpreter: {interpreter}") def _get_remote_settings(self, settings_url): try: settings = requests.get(settings_url, timeout=10).json() return settings except requests.RequestException as e: app_logger.error("Request exception while getting remote settings: %s", str(e)) self._error_and_exit({"result": str(e)}) except (ValueError, TypeError) as e: app_logger.error("Error while parsing remote settings: %s", str(e)) return None def _is_statistics_enabled(self): """ Return cl-statistics status """ if self._opts.get("--force-collect"): return True # In Cloudlinux tests environment statistics always enabled is_test_environment = bool(os.environ.get("CL_TEST_SYSTEM")) if is_test_environment: return True settings = self._get_remote_settings(self.SETTINGS_URL) if settings is None: return False try: rollout_group = settings["cl-statistics"]["rollout-group"] return settings["cl-statistics"]["enabled"] and self._match_server(rollout_group) except KeyError as e: app_logger.error("Error occurred while trying to get rollout group: %s", str(e)) self._error_and_exit({"result": str(e)}) @staticmethod def _to_number(hash_server): return int(hash_server, 16) def _match_server(self, url_num): if self.system_id is None: # system_id is None if server is not registered, but we still need to collect statistics return True hash_server = hashlib.sha256(self.system_id.encode()).hexdigest()[:20] return (self._to_number(hash_server) % 2**url_num) == 0 @staticmethod def _wait_for_background_process() -> None: """ Wait for running background process of cl-summary """ retries = 50 while retries and not is_sending_process_running(): retries -= 1 time.sleep(0.1) def _actions_before_run_process_as_limited(self): if self._opts["enable"] or self._opts["disable"]: # Enable/Disable collect statistics set_statistic_collection_enabled(self._opts["enable"]) # Print result data = {"timestamp": time.time(), "result": "success"} print_dictionary(data, True) return if self._opts["enable-rpm"] or self._opts["disable-rpm"]: # Enable/Disable collect statistics set_statistic_rpm_collection_enabled(self._opts["enable-rpm"]) # Print result data = {"timestamp": time.time(), "result": "success"} print_dictionary(data, True) return if self._opts["status"]: # show collecting status here and exit status = "collecting" if is_sending_process_running() else "ready" data = {"timestamp": time.time(), "status": status, "result": "success"} # Add last send statistics status data.update({"sending_status": get_statistics_send_status_from_file()}) print_dictionary(data, True) sys.exit(0) if self._opts.get("get-remote"): result = self._get_remote_data() # Append statistics collection status self._print_result_and_exit(data=result, is_statistic_enabled=is_statistic_enabled()) if self._opts.get("rpm-packages"): if not is_statistic_rpm_enabled() and not self._opts.get("--force-collect"): self._error_and_exit( { "result": "Sending RPM statistics is disabled by admin. " "Use --force-collect to ignore admin`s settings." }, error_code=0, ) self._get_rpm_packages_summary() if self._opts.get("--send"): self._send_statistics_and_save_status( summary=self.statistics, url=self.RPM_PACKAGES_URL, save_status=False, ) app_logger.info("RPM statistics sent") else: print_dictionary(self.statistics, True) return if not self._is_statistics_enabled(): status_dict = { "result": SummaryStatus.FAILED, "reason": "Statistics collection is disabled globally. " "Please, try again later or contact support if it happens again.", "timestamp": time.time(), } write_statistics_send_status_to_file(status_dict) self._error_and_exit( { "result": "Collecting statistics is disabled globally. " "Use --force-collect to ignore global settings" }, error_code=0, ) # check admin`s statistics settings before sending if self._opts.get("--send"): if not self._opts.get("--force-collect") and not is_statistic_enabled(): status_dict = { "result": SummaryStatus.FAILED, "reason": "Statistics collection is disabled by admin. " "Run `cloudlinux-summary enable` and then try again.", "timestamp": time.time(), } write_statistics_send_status_to_file(status_dict) self._error_and_exit( { "result": "Sending statistics is disabled by admin. " "Use --force-collect to ignore admin`s settings." }, error_code=0, ) if self.security_token is None: message = "Statistics was not sent, because security token is empty" app_logger.error(message) status_dict = { "result": SummaryStatus.FAILED, "reason": "We are not able to collect statistics because " f"we are not able to make a security token. Check {LOG_FILE} " "for details or contact support.", "timestamp": time.time(), } write_statistics_send_status_to_file(status_dict) self._error_and_exit({"result": message}) if self._opts.get("--async"): # Async start of collecting statistics if is_sending_process_running(): # Statistics already collecting # status field below may be absent due to race, and # we cannot fix this race because lock cannot be # acquired here due to async running of child process. # Lock will be released in parent process when it dies and # we cannot transfer the lock to child correctly data = {"timestamp": time.time(), "status": "collecting", "result": "success"} else: # temporary marker in order to know # when something crashed in collection process # this write needed to handle case when exec call # fails before reaching write in case of '--send' below write_statistics_send_status_to_file( {"result": SummaryStatus.IN_PROGRESS, "timestamp": time.time(), "reason": None}, ) # No background process found, start new collecting subprocess.run( "/usr/sbin/cloudlinux-summary --send --json &> /dev/null &", shell=True, executable="/bin/bash", check=False, ) self._wait_for_background_process() data = {"timestamp": time.time(), "result": "success"} print_dictionary(data, True) sys.exit(0) # Input logic description: # If --json and --send options are present and collection process found - print error # else - work as usual. # No need to check --json option because it is mandatory and arg parser will fail without it if self._opts["--send"]: if self.is_process_not_limited and is_sending_process_running(acquire_lock=True): # Checking/acquiring of lock is performed as unlimited process only. # Otherwise child process will not do the job due to busy lock. # Lock should be acquired in parent process in order to # avoid race and produce correct status below. # if collection process found (lock is busy) - print 'collecting' status data = {"timestamp": time.time(), "status": "collecting", "result": "success"} print_dictionary(data, True) sys.exit(0) else: # this write needed to handle case when we run --send without --async write_statistics_send_status_to_file( {"result": SummaryStatus.IN_PROGRESS, "timestamp": time.time(), "reason": None}, ) # TODO: we need this bicycle because method pylve.lve_enter_pid does not work properly (surprise!) # when we call lve_enter_pid, lve limits process only by cpu usage, other parameters are unlimited @staticmethod def _run_self_in_lve(args): """ Run same command in lve and set environ RUNNING_IN_LVE=1 in order to check it in child process. :return: """ settings = lvectllib.make_liblve_settings( lvectllib.create_liblve_settings( ls_cpu=15, # 15 percents of CPU (NOT core) ls_cpus=0, ls_memory_phy=1024 * 1024**2, # 1gb ) ) with lvectllib.temporary_lve(settings) as lve_id: args.extend(["--lve-id", str(lve_id)]) return subprocess.call( ["/bin/lve_suwrapper", "-n", str(lve_id), "/usr/sbin/cloudlinux-summary"] + args, env=dict( os.environ, RUNNING_IN_LVE="1", # we use /proc/cpuinfo to get cpu information, but unfortunately # it returns CURRENT cpu speed, which is different in lve environment # and we cannot get right speed value there CPU_DATA=json.dumps(lvectllib.CPUINFO_DATA), ), ) @staticmethod def _run_self_by_nice(args): """ Run same command using utility `nice` and set environ RUNNING_BY_NICE=1 in order to check it in child process. :return: """ return subprocess.call( ["/usr/bin/nice", "-n", "19", "/usr/sbin/cloudlinux-summary"] + args, env=dict( os.environ, RUNNING_BY_NICE="1", ), ) @staticmethod def _should_run_as_unlimited_process(opts: Dict[Any, Any]) -> bool: """ Check that passed command should run as unlimited process """ if any( opts[option] for option in ( "rpm-packages", "status", "get-remote", "enable", "disable", "enable-rpm", "disable-rpm", ) ): return True if any( opts[option] for option in ( "--send", "--async", "--json", "--force-collect", ) ): return False return False def run(self, argv): # get arguments self._opts = self._parse_args(argv) if self.is_process_not_limited: # The call does actions which don't require run in LVE: # - reading/writing status of statistics collection # - getting remote data # - enabling/disabling statistics collection # - processing async run of statistics collection # - running rpm statistics collection self._actions_before_run_process_as_limited() # We should run only main statistics collection in LVE # in other cases we skip run in LVE if self._should_run_as_unlimited_process(self._opts): sys.exit(0) elif not is_panel_feature_supported(Feature.LVE): rc = self._run_self_by_nice(argv) sys.exit(rc) else: try: rc = self._run_self_in_lve(argv) sys.exit(rc) except lvectllib.PyLveError as e: error_msg = f"failed to run task in lve, error: {e}" print(error_msg) log = logging.getLogger(__name__) log.exception(error_msg, exc_info=True) sys.exit(-1) else: if self._should_run_as_unlimited_process(self._opts): err_msg = ( "You shouldn't use env var " '"RUNNING_IN_LVE" or "RUNNING_BY_NICE" for run ' "of any command except collection " "of main statistics." ) data = { "timestamp": time.time(), "result": err_msg, } app_logger.error(err_msg, extra=self._opts) print_dictionary(data, True) sys.exit(1) self.run_get_summary() def run_get_summary(self): start_time = time.time() self._get_summary() running_time = time.time() - start_time self.statistics["cl_summary_execution_time"] = running_time if self._opts["--lve-id"]: self.statistics.add_metric( partial(self._get_max_memory, running_time), "str", "cl_summary_max_mem_used", "Can't get memory usage by cloudlinux-summary", ) if self._opts.get("--send"): self._send_statistics_and_save_status( summary=self.statistics, url=self.SUMMARY_URL, save_status=True, ) app_logger.info("Main statistics sent") else: print_dictionary(self.statistics, True) @staticmethod def _save_status(timestamp: int, summary_result: AnyStr) -> None: """ Save status of sending statistics to json file """ # Also write summary result to file status_dict = {"result": SummaryStatus.SUCCESS, "timestamp": timestamp} if summary_result != "success": # Send error was happened, rewrite status according to LU-1013 status_dict["reason"] = summary_result status_dict["result"] = SummaryStatus.FAILED write_statistics_send_status_to_file(status_dict) def _send_statistics_and_save_status(self, summary: Dict[AnyStr, int], url: AnyStr, save_status: bool) -> None: """ Send statistics data to server and save status to file """ timestamp = int(time.time()) summary["timestamp"] = timestamp s_result = self._send_statistics(summary, url=url) result = {"result": s_result, "timestamp": timestamp} print_dictionary(result, True) if save_status: self._save_status( timestamp, s_result, ) def _get_max_memory(self, running_time): time_minutes = running_time / 60 if time_minutes < 1: return None cmd = [ "/usr/sbin/lveinfo", "--json", "--id", str(self._opts["--lve-id"]), "--show-columns", "mPMem", "--period", f"{int(math.ceil(time_minutes))}m", ] try: rc, json_str, _ = run_command(cmd, return_full_output=True) except ExternalProgramFailed as e: app_logger.warning("Unable to run lveinfo, error: %s", e) return None if rc == 0: parsed_data = json.loads(json_str) try: return max(x["mPMem"] for x in parsed_data["data"]) except (ValueError, KeyError): return None app_logger.error("lveinfo failed with exit code: %i, output: %s", rc, json_str) return None @staticmethod def _send_statistics(data, url): """ Sends statistics to server :param data: Statistics data dict :return: string - message for JSON 'result' key """ out_message = "success" try: message = f"Sending statictics to server {url}" app_logger.info(message) expected_err = requests.RequestException("Unknown exception while sending statistics") for i in range(5): try: response = requests.post(url, json=data, timeout=60) except requests.ConnectionError as err: expected_err = err time.sleep(4**i) else: break else: raise expected_err if response.status_code == 200: app_logger.info("Sending statictics OK") else: out_message = f"Server answer is: HTTP code {response.status_code}; Reason: {response.reason}" app_logger.info(out_message) except requests.RequestException as err: out_message = str(err) app_logger.error(out_message) return out_message def _get_summary(self): result = {"version": 1, "timestamp": time.time()} self._prepare_cl_normal_statistics() self.statistics.update(result) return result def _get_rpm_packages_summary(self): result = {"version": 1, "timestamp": time.time()} self._fill_dict_with_rpm_packages_statistics() self.statistics.update(result) return result @staticmethod def _get_panel_version() -> str: """ Get version of control panel """ detect.getCP() return detect.CP_VERSION def _fill_mysql_governor_statistics(self) -> None: """ Fill dict with statistics by statistics about MySQL governor """ success, mysql_gov_mode = self.statistics.evaluate_safe( db_governor_lib.get_gov_mode_operation, "Can't get MySQL governor mode", ) if not success: self.statistics["mysql_governor_status"] = ERROR elif mysql_gov_mode is not None: self.statistics["mysql_governor_mode"] = mysql_gov_mode self.statistics.add_metric( lambda: MySQLGovernor().get_governor_version(), "str", "mysql_governor_version", "Can't get MySQL governor version", ) self.statistics.add_metric( lambda: MySQLGovernor().get_governor_status()[0], "str", "mysql_governor_status", "Can't get MySQL governor status", ) else: self.statistics["mysql_governor_status"] = NOT_INSTALLED def _fill_control_panel_statistics(self) -> None: """ Fill dict with statistics by statistics about control panel """ self.statistics.add_metric( detect.getCPName, "str", "control_panel_name", "Can't get control panel name", ) self.statistics.add_metric( lambda: [name for name, is_supported in cpapi.get_supported_cl_features().items() if is_supported], "str_list", "supported_cl_features", "Can't get list of supported cl features by control panel", ) self.statistics.add_metric( self._get_panel_version, "str", "control_panel_version", "Can't get control panel version", ) # control_panel_apache metric depends on control_panel_name metric self.statistics.add_metric( self._get_control_panel_apache, "str", "control_panel_apache", "Can't get control panel apache", ) self.statistics.add_metric(is_nginx_running, "int", "nginx_running", "Can't get control panel nginx") self.statistics.add_metric(lambda: len(cpapi.cpusers()), "int", "users_total", "Can't get amount of users") if detect.getCPName() == cpapi.PLESK_NAME: self.statistics.add_metric( self._is_installed_via_plesk_ext, "int", "installed_via_plesk_ext", "Can't determine if CL was installed from Plesk Extension", ) self.statistics.add_metric( self._is_login_via_whmcs_in_use, "int", "login_via_whmcs_in_use", "Can't determine if Login via WHMCS in use", ) self.statistics.add_metric( is_wp2_environment, "int", "is_wp2_environment", "Can't if that is WP2 environment" ) @staticmethod def _is_login_via_whmcs_in_use(): """ Determine whether the login method is via WHMCS """ status = -1 # Not used if detect.getCPName() != cpapi.CPANEL_NAME: return status command = ["/usr/local/cpanel/bin/whmapi1", "get_available_authentication_providers", "--output=jsonpretty"] result = subprocess.run(command, capture_output=True, text=True, check=True) data = json.loads(result.stdout) if "data" in data and "providers" in data["data"]: for provider in data["data"]["providers"]: if provider["provider_name"] == "whmcs": status = 0 # Disabled # If at least one checkbox is enabled in the settings of the login module via WHMCS, # then WHMCS is used if any( [ provider.get("whostmgrd_enabled", False), provider.get("cpaneld_enabled", False), provider.get("webmaild_enabled", False), ] ): status = 1 # Enabled break return status @staticmethod def _is_installed_via_plesk_ext(): """ cldeploy-plesk-ext.log is always created by cldeploy-precheck when it downloads the cldeploy script, if it exists it means there was an attempt to launch cldeploy from the Plesk Extension. Since this function is in package lve-utils, which depends on lve, it means that the current OS is CloudLinux, therefore the conversion was successful. Possible false positives: - if there was failed attempt to convert os from Plesk Ext, and then it was successfully converted after manual run of cldeploy, it will be considered as CLOS installed via Plesk extension - if user has CL already installed and goes to CLOS Plesk Ext and tries to run conversion from there Possible false negatives: - if /var/log/cldeploy-plesk-ext.log was deleted or renamed/archived (e.g. via logrotate) """ result = False ext_log = "/var/log/cldeploy-plesk-ext.log" if os.path.exists(ext_log): result = True return result def _get_control_panel_apache(self): """ Wrapper to retrieve control panel Apache version: EA3 or EA4 for cPanel, native otherwise :return: EA3|EA4|native """ if is_litespeed_running(): if detect.detect_enterprise_litespeed(): result = "litespeed" elif detect.detect_open_litespeed(): result = "openlitespeed" else: # There is no LS config found result = "unknown_litespeed" elif self.statistics["control_panel_name"] == "cPanel": result = "EA4" if detect.is_ea4() else "EA3" else: result = "native" return result @staticmethod def _cagefs_status_wrapper(): """ Wrapper to convert internal values from cagefs_statistics_config.get_cagefs_status function to values for statistics :return: """ cagefs_status = cagefs_statistics_config.get_cagefs_status() if cagefs_status is None: return cagefs_status cagefs_status_map = { cagefs_statistics_config.CAGEFS_STATUS_NOT_INSTALLED: NOT_INSTALLED, cagefs_statistics_config.CAGEFS_STATUS_NOT_INITIALIZED: NOT_INITIALIZED, "Enabled": ENABLED, "Disabled": DISABLED, } return cagefs_status_map.get(cagefs_status, "Unknown") def _fill_cagefs_statistics(self) -> None: """ Fill dict with statistics by statistics about CageFS """ self.statistics.add_metric( self._cagefs_status_wrapper, "str", "cagefs_status", "Can't get CageFS status", ) if self.statistics["cagefs_status"] in [NOT_INSTALLED, NOT_INITIALIZED]: self.statistics["cagefs_user_mode"] = None else: self.statistics.add_metric( cagefs_statistics_config.get_cagefs_user_mode, "str", "cagefs_user_mode", "Can't get CageFS user mode", ) self.statistics.add_metric( partial(cagefs_statistics_config.get_quantity, True), "str", "cagefs_enabled_quantity", "Can't get quantity of users with enabled CageFS", ) self.statistics.add_metric( partial(cagefs_statistics_config.get_quantity, False), "str", "cagefs_disabled_quantity", "Can't get quantity of users with disabled CageFS", ) def _get_amount_of_endusers_under_resellers(self) -> Optional[int]: """ Get amount of end-users which belong to active resellers """ try: lvp_count = Counter(lvp for _, lvp in self.lvpmap.lve_lvp_pairs() if lvp > 0) except cpapi.NotSupported: return None enabled_lvp_id = set(self.lvpmap.name_map.id_list()) return sum(lvp_id in enabled_lvp_id for lvp_id in lvp_count.elements()) def _get_total_amount_of_endusers(self) -> Optional[int]: """ Get total amount of end-users """ try: lvp_count = Counter(lvp for _, lvp in self.lvpmap.lve_lvp_pairs() if lvp > 0) except cpapi.NotSupported: return None return sum(lvp_count.values()) @staticmethod def _get_amount_of_resellers() -> Optional[int]: """ Get amount of resellers """ try: return len(cpapi.resellers()) except cpapi.NotSupported: pass def _fill_resellers_statistics(self) -> None: """ Fill dict with statistics by varied statistics about resellers """ self.statistics.add_metric( lvectllib.lve.is_lve10, "int", "reseller_limits_supported_kernel", "Can't detect status of support reseller limits by kernel", ) self.statistics.add_metric( lvectllib.lve.is_panel_supported, "int", "reseller_limits_supported_control_panel", "Can't detect status of support reseller limits by control panel", ) self.statistics.add_metric( lvectllib.lve.reseller_limit_supported, "int", # name of metric means that reseller limits is supported by all sides: # kmod-lve, liblve, /proc/lve, control panel "reseller_limits_enabled", "Can't detect status of support of reseller limits", ) self.statistics.add_metric( self._get_amount_of_resellers, "int", "resellers_total", "Can't get total amount of resellers", ) self.statistics.add_metric( self._get_amount_of_endusers_under_resellers, "int", "resellers_endusers_under_reseller_limits", "Can't get amount of end-users which belong to active resellers", ) self.statistics.add_metric( self._get_total_amount_of_endusers, "int", "resellers_endusers_total", "Can't get total amount of end-users", ) self.statistics.add_metric( lambda: self._get_resellers_with_faults(), "int", "resellers_with_faults", "Can't get amount of resellers with faults for the past 24h", ) if self.statistics["reseller_limits_enabled"]: self.statistics.add_metric( lambda: len(list(lvectllib.lvp_list())), "int", "resellers_active", "Can't get amount of active resellers", ) else: self.statistics["resellers_active"] = None self.statistics["resellers_endusers_under_reseller_limits"] = None self.statistics["resellers_with_faults"] = None def _fill_default_limits_statistics(self, xml_cfg_provider: LimitsDataStorage) -> None: """ Fill dict with statistics by statistics about default limits """ self.statistics.add_metric( partial( self._cpu_limit_to_percents, xml_cfg_provider.defaults[DEFAULTS].cpu, xml_cfg_provider.defaults[DEFAULTS].ncpu, ), "int", "default_limit_speed", "Can't get default speed limit", ) self.statistics.add_metric( partial(self._get_cpu_limit_units, xml_cfg_provider.defaults[DEFAULTS].cpu), "str", "default_limit_cpu_origin_units", "Can't get cpu origin units of default limit", ) self.statistics.add_metric( lambda: xml_cfg_provider.defaults[DEFAULTS].ncpu, "int", "default_limit_ncpu", "Can't get default ncpu limit", ) self.statistics.add_metric( lambda: xml_cfg_provider.defaults[DEFAULTS].io, "int", "default_limit_io", "Can't get default io limit", ) self.statistics.add_metric( lambda: xml_cfg_provider.defaults[DEFAULTS].nproc, "int", "default_limit_nproc", "Can't get default nproc limit", ) self.statistics.add_metric( lambda: xml_cfg_provider.defaults[DEFAULTS].ep, "int", "default_limit_ep", "Can't get default ep limit", ) self.statistics.add_metric( lambda: xml_cfg_provider.defaults[DEFAULTS].iops, "int", "default_limit_iops", "Can't get default iops limit", ) self.statistics.add_metric( partial(self._mempages_to_mb, xml_cfg_provider.defaults[DEFAULTS].vmem), "int", "default_limit_vmem_mb", "Can't get default vmem limit", ) self.statistics.add_metric( partial(self._mempages_to_mb, xml_cfg_provider.defaults[DEFAULTS].pmem), "int", "default_limit_pmem_mb", "Can't get default pmem limit", ) def _fill_other_limits_statistics(self, xml_cfg_provider: LimitsDataStorage) -> None: """ Fill dict with statistics by other statistics about limits: packages_total, users_total, amount users/packages with custom limits """ self.statistics.add_metric( lambda: len(xml_cfg_provider.packages), "int", "packages_total", "Can't get total amount of packages", ) self.statistics.add_metric( lambda: len(xml_cfg_provider.get_packages_with_custom_limits()), "int", "packages_with_custom_limits", "Can't get amount of packages with custom limits", ) self.statistics.add_metric( lambda: len(xml_cfg_provider.get_users_with_custom_limits()), "int", "users_with_custom_limits", "Can't get amount of users with custom limits", ) self.statistics.add_metric( self._get_users_with_faults, "int", "users_with_faults", "Can't get amount of users with faults for the past 24h", ) def _fill_top_packages_statistics(self, xml_cfg_provider: LimitsDataStorage) -> None: """ Fill dict with statistics by statistics about top packages on server """ # pylint: disable=cell-var-from-loop for i in range(1, 4): success, top_result = self.statistics.evaluate_safe( partial(self._get_top_package_by_number_of_users, i, xml_cfg_provider), f"Can't get top {i} package by users", ) # Break cycle if result is None, # beacuse package with number more than i doesn't exists if not success or top_result is None: break # getting of that metric (and a few metrics below) is wrapped by `format_metric`, # because value of `top_result` can be -42 in case exception while calling # method `_get_top_package_by_number_of_users` self.statistics.add_metric( lambda: top_result[1].name, "str", f"top_{i}_package_name", log_message=None ) self.statistics.add_metric( lambda: int(top_result[0]), "int", f"top_{i}_package_users_num", log_message=None ) self.statistics.add_metric( lambda: self._cpu_limit_to_percents( (top_result[1].limits or EMPTY_LIMITS).cpu, (top_result[1].limits or EMPTY_LIMITS).ncpu, ), "int", f"top_{i}_package_limit_speed", log_message=None ) self.statistics.add_metric( lambda: self._get_cpu_limit_units( (top_result[1].limits or EMPTY_LIMITS).cpu, ), "str", f"top_{i}_package_limit_cpu_origin_units", log_message=None ) self.statistics.add_metric( lambda: (top_result[1].limits or EMPTY_LIMITS).ncpu, "int", f"top_{i}_package_limit_ncpu", log_message=None ) self.statistics.add_metric( lambda: (top_result[1].limits or EMPTY_LIMITS).io, "int", f"top_{i}_package_limit_io", log_message=None ) self.statistics.add_metric( lambda: (top_result[1].limits or EMPTY_LIMITS).nproc, "int", f"top_{i}_package_limit_nproc", log_message=None ) self.statistics.add_metric( lambda: (top_result[1].limits or EMPTY_LIMITS).ep, "int", f"top_{i}_package_limit_ep", log_message=None ) self.statistics.add_metric( lambda: (top_result[1].limits or EMPTY_LIMITS).iops, "int", f"top_{i}_package_limit_iops", log_message=None ) self.statistics.add_metric( lambda: self._mempages_to_mb((top_result[1].limits or EMPTY_LIMITS).vmem), "int", f"top_{i}_package_limit_vmem_mb", log_message=None ) self.statistics.add_metric( lambda: self._mempages_to_mb((top_result[1].limits or EMPTY_LIMITS).pmem), "int", f"top_{i}_package_limit_pmem_mb", log_message=None ) # pylint: enable=cell-var-from-loop def _fill_limits_statistics(self) -> None: """ Fill dict with statistiscs by varied statistics about limits """ xml_cfg_provider = LimitsDataStorage() self._fill_default_limits_statistics(xml_cfg_provider) self._fill_other_limits_statistics(xml_cfg_provider) self._fill_top_packages_statistics(xml_cfg_provider) def _fill_lsapi_statistics(self) -> None: """ Fill dict with statistics by statistics about mod_lsapi """ # ignore success flag here because we mute error messages below # using log_message=None success, raw_lsapi_info = self.statistics.evaluate_safe( self.get_raw_lsapi_info, "Can't get raw mod_lsapi info", ) if not success: self.statistics["lsapi_mod_status"] = ERROR elif raw_lsapi_info is not None: self.statistics.add_metric( lambda: raw_lsapi_info["criu"]["status"], "str", "lsapi_criu_service_status", log_message=None ) self.statistics.add_metric( lambda: raw_lsapi_info["criu"]["version"], "str", "lsapi_criu_service_version", log_message=None ) self.statistics.add_metric( lambda: raw_lsapi_info["lsapiConf"]["lsapi_criu"], "str", "lsapi_option_criu", log_message=None ) self.statistics.add_metric( lambda: raw_lsapi_info["lsapiConf"]["lsapi_with_connection_pool"], "str", "lsapi_option_connection_pool", log_message=None ) self.statistics.add_metric( lambda: raw_lsapi_info["libVersion"], "str", "lsapi_lib_version", log_message=None ) self.statistics.add_metric( lambda: raw_lsapi_info["modStatus"], "str", "lsapi_mod_status", log_message=None ) self.statistics.add_metric( lambda: raw_lsapi_info["modVersion"], "str", "lsapi_mod_version", log_message=None ) self.statistics.add_metric( lambda: raw_lsapi_info["totalDomain"], "int", "lsapi_total_domain_count", log_message=None ) self.statistics.add_metric( lambda: raw_lsapi_info["domainStat"], "int_dict", "lsapi_domain_stat", log_message=None ) self.statistics.add_metric( lambda: raw_lsapi_info["controlPanel"], "str", "lsapi_apache_environment", log_message=None ) else: self.statistics["lsapi_mod_status"] = NOT_INSTALLED def _fill_wmt_settings_statistics(self): self.statistics.add_metric( self._get_wmt_api_config, "str_dict", "wmt_config", "Cant get status of wmt_api_config" ) def _fill_wp_statistics(self): try: all_wp_paths, paths_with_wpos_object_cache, wp_php_versions = self._prepare_wp_statistics() except Exception as e: app_logger.error("Error while getting WordPress statistics %s", str(e)) all_wp_paths, paths_with_wpos_object_cache, wp_php_versions = [], [], {} without_wpos_object_cache = list(set(all_wp_paths) - set(paths_with_wpos_object_cache)) try: object_cache_plugins, advanced_cache_plugins = self._prepare_wp_plugins_statistics( without_wpos_object_cache ) except Exception as e: app_logger.error("Error while getting WordPress plugins statistics %s", str(e)) object_cache_plugins, advanced_cache_plugins = {}, {} if paths_with_wpos_object_cache: object_cache_plugins.update({"cloudlinux_wpos_object_cache": len(paths_with_wpos_object_cache)}) self.statistics.add_metric( lambda: len(all_wp_paths), "int", "wordpress_installed_total", "Can't get total count of installed wordpress", ) self.statistics.add_metric( lambda: object_cache_plugins, "int_dict", "wordpress_object_cache_plugins", "Can't get WP object cache plugins info", ) self.statistics.add_metric( lambda: advanced_cache_plugins, "int_dict", "wordpress_advanced_cache_plugins", "Can't get WP advanced cache plugins info", ) self.statistics.add_metric( lambda: sum(object_cache_plugins.values()), "int", "wordpress_object_cache_plugins_total", "Can't get WP object cache total plugins info", ) self.statistics.add_metric( lambda: sum(advanced_cache_plugins.values()), "int", "wordpress_advanced_cache_plugins_total", "Can't get WP advanced cache total plugins info", ) self.statistics.add_metric( lambda: wp_php_versions, "int_dict", "wordpress_php_versions", "Can't get WP php version info" ) def _fill_per_user_wp_statistics(self): try: users_with_wp, users_with_wp_and_litespeed = self._prepare_per_user_wp_statistics() except Exception as e: app_logger.error("Error while getting per-user WordPress statistics %s", str(e)) users_with_wp = [] users_with_wp_and_litespeed = [] self.statistics.add_metric( lambda: len(users_with_wp), "int", "users_count_with_wordpress_installed", "Can't get total count of users with wordpress installed", ) self.statistics.add_metric( lambda: len(users_with_wp_and_litespeed), "int", "users_count_with_wordpress_and_lscache_installed", "Can't get total count of users with wordpress and LiteSpeed installed", ) def _prepare_wp_plugins_statistics(self, wp_paths): """ Prepares dict with plugin-counter pairs e.g: {'w3-total-cache': 4, 'redis': 1, 'ls': 2} """ object_cache_plugins = self._get_plugin_info("object-cache", wp_paths) advanced_cache_plugins = self._get_plugin_info("advanced-cache", wp_paths) return object_cache_plugins, advanced_cache_plugins @staticmethod def _get_plugin_info(plugin_type: str, wp_paths: List): object_cache_plugins = list(filter(None, [get_wp_cache_plugin(path, plugin_type) for path in wp_paths])) object_plugin_counter_pairs = defaultdict(int) for plugin in object_cache_plugins: object_plugin_counter_pairs[plugin] += 1 return object_plugin_counter_pairs def _prepare_wp_statistics(self) -> Tuple[List[Path], List[Path], Dict[str, int]]: """ Return Wordpress absolute paths, paths with install WPOS object cache module and counter of php versions that are used by these Wordpresses. """ all_wp_paths, paths_with_enabled_wpos_object_cache = [], [] php_versions = defaultdict(int) panel_users = cpapi.cpusers() for user in panel_users: try: domains_info = cpapi.userdomains(user) except Exception: continue docroot_domains_map = defaultdict(list) for domain, docroot in domains_info: if os.path.exists(docroot): docroot_domains_map[docroot].append(domain) for docroot in docroot_domains_map: docroot_info = self._get_docroot_wp_info(user, docroot, docroot_domains_map) docroot_wp_paths = docroot_info["wp_paths"] docroot_php_version = docroot_info["php_version"] all_wp_paths.extend(docroot_wp_paths) paths_with_enabled_wpos_object_cache.extend(docroot_info["wp_paths_with_enabled_wpos_object_cache"]) if docroot_wp_paths and docroot_php_version != "undefined": php_versions[docroot_php_version] += len(docroot_wp_paths) return all_wp_paths, paths_with_enabled_wpos_object_cache, php_versions def _prepare_per_user_wp_statistics(self) -> Tuple[set[str], set[str]]: """ Return Wordpress absolute paths, paths with install LiteSpeed cache plugin. """ users_with_wp, users_with_wp_and_litespeed = set(), set() panel_users = cpapi.cpusers() for user in panel_users: try: domains_info = cpapi.userdomains(user) except Exception as e: app_logger.error("Can't get user's domains info, error: %s", str(e)) continue docroot_domains_map = defaultdict(list) for domain, docroot in domains_info: if os.path.exists(docroot): docroot_domains_map[docroot].append(domain) for docroot in docroot_domains_map: docroot_info = self._get_docroot_wp_info(user, docroot, docroot_domains_map) if len(docroot_info["wp_paths"]) > 0: users_with_wp.add(user) if len(docroot_info["wp_paths_with_enabled_litespeed_cache"]) > 0: users_with_wp_and_litespeed.add(user) return users_with_wp, users_with_wp_and_litespeed def _get_docroot_wp_info( self, user: str, docroot: str, docroot_domains_map: Dict[str, str] ) -> Dict[str, Union[List, str]]: """ Return data about docroot's WP paths and used php versions. """ docroot_info = { "wp_paths": [], "wp_paths_with_enabled_wpos_object_cache": [], "wp_paths_with_enabled_litespeed_cache": set(), "php_version": "undefined", } # filter found WP paths that are the same as other docroots (not current checking) # because all docroots will be checked later anyway wp_paths = [ wp_path for wp_path in find_wp_paths(docroot) if (os.path.join(docroot, wp_path) == docroot or os.path.join(docroot, wp_path) not in docroot_domains_map) ] # if WP paths were not found we are not detecting php version # and path with cloudlinux object_cache module if not wp_paths: return docroot_info docroot_info["wp_paths"] = [Path(docroot).joinpath(path) for path in wp_paths] docroot_info["wp_paths_with_enabled_wpos_object_cache"] = [ Path(docroot).joinpath(path) for path in get_wp_paths_with_enabled_module(user, wp_paths) if path is not None ] for path in docroot_info["wp_paths"]: object_cache_plugin = get_wp_cache_plugin(path, "object-cache") if object_cache_plugin is not None and "litespeed" in object_cache_plugin: docroot_info["wp_paths_with_enabled_litespeed_cache"].add(path) advanced_cache_plugin = get_wp_cache_plugin(path, "advanced-cache") if advanced_cache_plugin is not None and "litespeed" in advanced_cache_plugin: docroot_info["wp_paths_with_enabled_litespeed_cache"].add(path) for domain in docroot_domains_map[docroot]: php_version = self._get_php_version_for_domain(domain) if php_version != "undefined": docroot_info["php_version"] = php_version break return docroot_info def _get_php_version_for_domain(self, domain: str) -> str: """ Return php version that is used for specified domain. Determine whether of MultiPHP Manager or PHP Selector is used. """ if detect.getCPName() not in (cpapi.CPANEL_NAME, cpapi.DIRECTADMIN_NAME, cpapi.PLESK_NAME): return "undefined" system_default_php_version, domains_php_info, selector_php_info = self._get_system_php_info() # get version that is shown in MultiPHP Manager php_info = domains_php_info.get(domain, {}) multi_php_version = php_info.get("php_version_id", "undefined") # in cPanel when PHP Selector is used for domain, domain's php version # in MultiPHP Manager becomes equal to system's default php version # if not equal => PHP Selector is not used if detect.getCPName() == cpapi.CPANEL_NAME and system_default_php_version != multi_php_version: return multi_php_version # trying to get version from PHP Selector info # if not succeeded => user uses MultiPHP for version, domains in selector_php_info.items(): if domain in domains and version != "native": alt_php_version = f'alt-php{version.replace(".", "")}' return alt_php_version if detect.getCPName() == cpapi.DIRECTADMIN_NAME: da_php_version = f'da-php{multi_php_version.replace(".", "")}' return da_php_version if detect.getCPName() == cpapi.PLESK_NAME: handler = php_info.get("handler_type", "") php_version = multi_php_version[: -len(handler)].strip("-") return php_version or "undefined" return multi_php_version @staticmethod @lru_cache(maxsize=None) def _get_system_php_info(): """ Return info about: - system_php_version (implemented only on cPanel) - domains_php_info (php versions and handlers turned on in MultiPHP) - selector_php_info (php version used by panel users via PHP Selector) """ try: system_php_version = cpapi.get_system_php_info()["default_version_id"] except Exception: system_php_version = None domains_php_info = cpapi.get_domains_php_info() php_selector_usage = get_php_selector_usage() if php_selector_usage is not None: selector_php_info = php_selector_usage["domains_by_php_version"] else: selector_php_info = {} return system_php_version, domains_php_info, selector_php_info def _fill_ssa_statistics(self) -> None: """ Fill dict with SSA statistics """ success, ssa_stats = self.statistics.evaluate_safe( self.get_ssa_stats, "Can't get SSA statistics", ) if not success: self.statistics["ssa_status"] = ERROR return if ssa_stats is not None: self.statistics.add_metric(lambda: ssa_stats["config"], "str_dict", "ssa_config", "Can't get SSA config") self.statistics.add_metric( lambda: ssa_stats["version"], "str", "ssa_version", "Can't get version of SSA" ) self.statistics.add_metric(lambda: ssa_stats["status"], "str", "ssa_status", "Can't get state of SSA") self.statistics.add_metric( lambda: ssa_stats["agent_status"], "str", "ssa_agent_status", "Can't get state of SSA agent" ) self.statistics.add_metric( lambda: ssa_stats["autotracing"]["status"], "str", "autotracing_status", "Can't get state of SSA Autotracing", ) self.statistics.add_metric( lambda: ssa_stats["autotracing"]["disabled_users_quantity"], "int", "autotracing_disabled_users_quantity", "Can't get state of SSA Autotracing disabled users", ) self.statistics.add_metric( lambda: ssa_stats["autotracing"]["rules_version"], "str", "autotracing_rules_version", "Can't get SSA Autotracing rules version", ) self.statistics.add_metric( lambda: ssa_stats["autotracing"]["urls_processed"], "int", "autotracing_urls_processed", "Can't get SSA Autotracing processed URLs count", ) self.statistics.add_metric( lambda: ssa_stats["autotracing"]["urls_selected"], "int", "autotracing_urls_selected", "Can't get SSA Autotracing selected URLs count", ) self.statistics.add_metric( lambda: ssa_stats["autotracing"]["rejects"], "int_dict", "autotracing_rejects", "Can't get SSA Autotracing rejects", ) else: # no SSA external utility to get statistics -- SSA is not installed self.statistics["ssa_status"] = NOT_INSTALLED def _fill_php_selector_statistics(self) -> None: """ Fill dict with statistics by varied statistics about PHP selector """ success, php_interpreters = self.statistics.evaluate_safe( lambda: get_versions_statistics("php"), "Can't get statistics about PHP interpreters" ) if not success: self.statistics["selector_php_status"] = ERROR elif php_interpreters is not None: self.statistics.add_metric( partial(self._get_status_of_selector, "php"), "str", "selector_php_status", "Can't get status of PHP selector", ) self.statistics.add_metric( partial(self._get_list_versions_of_interperters, php_interpreters, INSTALLED), "str_list", "selector_php_versions_installed", log_message=None ) self.statistics.add_metric( partial(self._get_list_versions_of_interperters, php_interpreters, ENABLED), "str_list", "selector_php_versions_enabled", log_message=None ) self.statistics.add_metric( get_default_php_version, "str", "selector_php_version_default", "Can't get default version of PHP interpreter", ) self.statistics.add_metric( get_native_version_safe, "str", "selector_php_version_native", "Can't get native version of PHP interpreter", ) self.statistics.add_metric( CloudlinuxSelectorLib("php").php_selector_is_enabled, "int", "selector_php_enabled_ui", "Can't get state of UI of PHP selector", ) self.statistics.add_metric( get_mode_of_php_selector, "str", "selector_php_mode", "Can't get mode of PHP selector", ) success, php_usage_summary = self.statistics.evaluate_safe( get_php_selector_usage, "Can't get summary usage of PHP selector", ) if not success or php_usage_summary is None: self.statistics["selector_php_num_domains_by_interpreter"] = None self.statistics["selector_php_num_users_by_interpreter"] = None else: self.statistics.add_metric( lambda: {v: len(domains) for v, domains in php_usage_summary["domains_by_php_version"].items()}, "int_dict", "selector_php_num_domains_by_interpreter", log_message=None ) self.statistics.add_metric( lambda: {v: len(domains) for v, domains in php_usage_summary["users_by_php_version"].items()}, "int_dict", "selector_php_num_users_by_interpreter", log_message=None ) else: self.statistics["selector_php_status"] = NOT_INSTALLED @staticmethod def _get_average_apps_per_domain(total_apps: int, amount_of_apps_per_domain: int) -> Optional[int]: """ Get average amount of applications per domain :param total_apps: total amount of applications :param amount_of_apps_per_domain: amount of applications per domain """ if total_apps < 1 or amount_of_apps_per_domain < 1: return None return total_apps // amount_of_apps_per_domain @staticmethod def _get_average_apps_per_user(total_apps: int, amount_of_apps_per_user: int) -> Optional[int]: """ Get average amount of applications per user :param total_apps: total amount of applications :param amount_of_apps_per_user: amount of applications per user """ if total_apps < 1 or amount_of_apps_per_user < 1: return None return total_apps // amount_of_apps_per_user @staticmethod def _get_amount_of_runned_apps(apps: list) -> int: """ Get amount of running applications on server :param apps: list of applications for something selector """ return len([app for app in apps if app.app_status and app.app_status == "started"]) @staticmethod def _get_max_apps_per_domain(apps: list) -> int: """ Get maximum amount of applications per domain :param apps: list of applications for something selector """ apps_per_domain = Counter() for app in apps: apps_per_domain[app.doc_root] += 1 # [(doc_root, amount_of_apps_per_domain,),] # We should return 0 if counter is empty most_commons = apps_per_domain.most_common(1) or [(0, 0)] return most_commons[0][1] @staticmethod def _get_max_apps_per_user(apps: list) -> int: """ Get maximum amount of applications per user :param apps: list of applications for something selector """ apps_per_user = Counter() for app in apps: apps_per_user[app.user] += 1 # [(user, amount_of_apps_per_user,),] # We should return 0 if counter is empty most_commons = apps_per_user.most_common(1) or [(0, 0)] return most_commons[0][1] @staticmethod def _get_counter_apps_per_version(apps: list) -> Counter: """ Get Counter object which contains amount applications per version of interpreter :param apps: list of applications for something selector """ apps_per_version = Counter() for app in apps: apps_per_version[app.version] += 1 return apps_per_version def _get_max_apps_per_version(self, apps: list) -> int: """ Get maximum amount of applications per version of interpreter :param apps: list of applications for something selector """ apps_per_version = self._get_counter_apps_per_version(apps) # [(version, amount_of_apps_per_version,),] # We should return 0 if counter is empty most_commons = apps_per_version.most_common(1) or [(0, 0)] return most_commons[0][1] @staticmethod def _get_amount_of_domains_with_apps(apps: list) -> int: """ Get amount of domains with applications :param apps: list of applications for something selector """ domains = set() for app in apps: domains.add(app.doc_root) return len(domains) @staticmethod def _get_amount_of_users_with_apps(apps: list) -> int: """ Get amount of users with applications :param apps: list of applications for something selector """ users = set() for app in apps: users.add(app.user) return len(users) def _get_amount_of_apps_per_each_version_of_interpreters(self, apps: list) -> dict: """ Get amount of applications per each version of interpeters :param apps: list of applications for something selector """ apps_per_version = self._get_counter_apps_per_version(apps) return dict(apps_per_version) @staticmethod def _get_list_versions_of_interperters(interpreters_stats: dict, state: str) -> list[str]: """ Get list of versions of interpreters on server :param interpreters_stats: dict with varied statistics about each version of interpeters :param state: state of interpeters (installed, enabled) """ return [interpreter_stats for interpreter_stats, stat in interpreters_stats.items() if stat[state]] @staticmethod def _get_list_of_applications(interpreter: str) -> list: """ Get list of apllications on server for defined selector """ iter_apps = iter_server_applications(interpreter) if iter_apps is not None: return list(iter_apps) return [] def _fill_selectors_statistics(self) -> None: """ Fill dict with statistics by varied statistics about ruby/nodejs/python selectors """ for selector in self.SELECTORS: success, interpreters_stats = self.statistics.evaluate_safe( partial(get_versions_statistics, selector), f"Can't get statistics about {selector} interpreters", ) if interpreters_stats is None: self.statistics["selector_" + selector + "_status"] = NOT_INSTALLED continue if not success: self.statistics["selector_" + selector + "_status"] = ERROR continue self.statistics.add_metric( partial(self._get_status_of_selector, selector), "str", "selector_" + selector + "_status", f"Can't get status of {selector} selector" if success else None, ) self.statistics.add_metric( partial(self._get_list_versions_of_interperters, interpreters_stats, INSTALLED), "str_list", "selector_" + selector + "_versions_installed", log_message=None ) self.statistics.add_metric( partial(self._get_list_versions_of_interperters, interpreters_stats, ENABLED), "str_list", "selector_" + selector + "_versions_enabled", log_message=None ) success, interpreter_apps = self.statistics.evaluate_safe( partial(self._get_list_of_applications, selector), f"Can't get list of {selector} applications", ) # Following statistics evaluation does not make sense because # data that we are processing is wrong. Report to sentry and wait for fix. if not success: continue self.statistics.add_metric( lambda: len(interpreter_apps), # pylint: disable=cell-var-from-loop "int", "selector_" + selector + "_applications_amount", log_message=None ) self.statistics["selector_" + selector + "_used"] = ( self.statistics["selector_" + selector + "_applications_amount"] > 0 ) self.statistics.add_metric( partial(self._get_amount_of_runned_apps, interpreter_apps), "int", "selector_" + selector + "_applications_running", log_message=None ) success, default_version_of_selector = self.statistics.evaluate_safe( partial(get_default_version, selector), f"Can't get default version of {selector} selector", ) if success and default_version_of_selector is not None: self.statistics["selector_" + selector + "_default_version"] = default_version_of_selector self.statistics.add_metric( partial(self._get_max_apps_per_domain, interpreter_apps), "int", "selector_" + selector + "_max_applications_per_domain", f"Can't get max applications per domain for {selector} interpreter", ) self.statistics.add_metric( partial(self._get_max_apps_per_user, interpreter_apps), "int", "selector_" + selector + "_max_applications_per_user", f"Can't get max applications per user for {selector} interpreter", ) self.statistics.add_metric( partial(self._get_amount_of_users_with_apps, interpreter_apps), "int", "selector_" + selector + "_num_users_with_apps", f"Can't get amount of users with applications for {selector} interpeter", ) self.statistics.add_metric( partial(self._get_amount_of_domains_with_apps, interpreter_apps), "int", "selector_" + selector + "_num_domains_with_apps", f"Can't get amount of domains with applications for {selector} interpeter", ) success, average_apps_per_domain = self.statistics.evaluate_safe( partial( self._get_average_apps_per_domain, self.statistics["selector_" + selector + "_applications_amount"], self.statistics["selector_" + selector + "_num_domains_with_apps"], ), f"Can't get average amount of applications per domain for {selector} interpreter", ) if success and average_apps_per_domain is not None: self.statistics["selector_" + selector + "_average_applications_per_domain"] = average_apps_per_domain success, average_apps_per_user = self.statistics.evaluate_safe( partial( self._get_average_apps_per_user, self.statistics["selector_" + selector + "_applications_amount"], self.statistics["selector_" + selector + "_num_users_with_apps"], ), f"Can't get average amount of applications per user for {selector} interpreter", ) if success and average_apps_per_user is not None: self.statistics["selector_" + selector + "_average_applications_per_user"] = average_apps_per_user self.statistics.add_metric( partial(self._get_amount_of_apps_per_each_version_of_interpreters, interpreter_apps), "int_dict", "selector_" + selector + "_num_applications_by_interpreter", f"Can't get amount of applications per each version of {selector} interpreters", ) @staticmethod def _get_wizard_statistics() -> dict: """ Get wizard status and list of installed modules """ cmd = ["/usr/sbin/cloudlinux-wizard", "status"] ret_code, std_out, std_err = run_command(cmd, return_full_output=True) if ret_code != 0: raise ExternalProgramFailed(std_err) parsed_json = json.loads(std_out) wizard_statistics = {} wizard_statistics["wizard_status"] = parsed_json["wizard_status"] parsed_modules = {module["name"]: module["status"] for module in parsed_json["modules"]} for module in ALL_MODULES: wizard_statistics["wizard_module_" + module] = parsed_modules.get(module, NOT_SELECTED) return wizard_statistics def _fill_wizard_statistics(self) -> None: """ Fill dict with statistics by varied statistics about cloudlinux-wizard """ # ignore non-success here because error messages below are suppressed _, wizard_statistics = self.statistics.evaluate_safe( self._get_wizard_statistics, "Can't get statistics about cloudlinux-wizard", ) self.statistics.add_metric( lambda: wizard_statistics["wizard_status"], "str", "wizard_status", log_message=None ) for module in ALL_MODULES: self.statistics.add_metric( lambda: wizard_statistics["wizard_module_" + module], # pylint: disable=cell-var-from-loop "str", "wizard_module_" + module, log_message=None ) @staticmethod def _get_implemented_integration_scripts(): """ Returns list of implemented scripts in integration.ini """ config = _read_config_file() scripts = [] for section in config: scripts += list(config[section].keys()) return scripts def _get_integration_info(self): """ Checks integration script exists and if exists get list of implemented scripts """ result = {"integration_scripts_used": False, "integration_scripts_specified": []} if not os.path.isfile(CONFIG_PATH): return result result["integration_scripts_used"] = True result["integration_scripts_specified"] = self._get_implemented_integration_scripts() return result @staticmethod def _get_memory_used(): """ Gets memory usage: total and used memory in megabytes """ bytes_in_mb = 1024**2 mem = psutil.virtual_memory() mem_total = float(mem.total) / bytes_in_mb mem_used = float(mem.used) / bytes_in_mb return mem_total, mem_used @staticmethod def _get_kernel_info(): """ Gets kernel info release and module version (starting from 7h) :return: """ kernel_release = platform.release() # modinfo has same version output as rpm -q kmod-lve kmodlve_version_file = "/sys/module/kmodlve/version" kmodlve_version = None if is_panel_feature_supported(Feature.LVE) and os.path.exists(kmodlve_version_file): with open(kmodlve_version_file, "r", encoding="utf-8") as f: kmodlve_version = f.read().strip() return kernel_release, kmodlve_version @staticmethod def _get_lve_extensions_packages_amount(): """ Gets info about lve extensions usage Calculates amount of packages with lve extensions """ return len(get_packages_with_lve_extensions()) @staticmethod def _is_kernel_datacycle_enabled_in_file() -> bool: """ Reads /proc/sys/fs/datacycle/enable in order to check datacycle enabled parameter """ datacycle_file = "/proc/sys/fs/datacycle/enable" if not os.path.exists(datacycle_file): return False with open(datacycle_file, "r", encoding="utf-8") as f: data = f.read().strip() return bool(int(data)) @staticmethod def _is_datacycle_param_was_passed() -> bool: """ Checks if datacycle parameter was given for current boot """ cmdline_file, param_name = "/proc/cmdline", "datacycle" # just in case if not os.path.exists(cmdline_file): return False with open(cmdline_file, "r", encoding="utf-8") as f: data = f.read().strip().split(" ") return param_name in data @staticmethod def _get_total_domains_amount() -> int: """ Returns general amount of domains on server """ cpusers_list = cpapi.cpusers() domains_count = 0 for user in cpusers_list: domains_count += len(cpapi.userdomains(user)) return domains_count @staticmethod def _is_link_traversal_protection_enabled() -> str: """ Returns is links traversal protection enabled on server (symlinks or hardlinks) """ sysctl = SysCtlConf() symlink_protection_enabled = bool(int(sysctl.get("fs.protected_symlinks_create"))) hardlink_protection_enabled = bool(int(sysctl.get("fs.protected_hardlinks_create"))) if symlink_protection_enabled and hardlink_protection_enabled: return "all" if symlink_protection_enabled: return "symlinks_only" if hardlink_protection_enabled: return "hardlinks_only" return "no" @staticmethod def _get_cl_installation_source(): iso = "iso" cldeploy = "cldeploy" qcow2 = "qcow2" if os.path.exists("/etc/cl-convert-saved") or os.path.exists("/var/log/cldeploy"): return cldeploy if os.path.exists("/etc/cloudlinux-qcow2-install"): return qcow2 return iso def _fill_system_statistics(self): kernel_info = self._get_kernel_info() if is_panel_feature_supported(Feature.LVE): self.statistics.add_metric( self._get_lve_extensions_packages_amount, "int", "lve_extension_packages_amount", log_message=None ) self.statistics.add_metric( self._is_link_traversal_protection_enabled, "str", "link_traversal_protection_enabled", log_message=None ) self.statistics.add_metric( lambda: kernel_info[1], "str", "installed_kmod_lve_version", log_message=None ) if not is_cl_solo_edition(skip_jwt_check=True): self.statistics.add_metric( is_email_notification_enabled, "int", "cldiag_cron_check_enabled", log_message=None ) self.statistics.add_metric(cpu_count, "str", "cpu_amount", log_message=None) self.statistics.add_metric( is_testing_enabled_repo, "int", "testing_repository_enabled", log_message=None ) self.statistics.add_metric(self._get_platform, "str", "base_distro", "Can't detect platform for CLOS") self.statistics.add_metric( self._detect_secureboot, "str", "secureboot_status", "Can't detect whether secure boot is enabled" ) self.statistics.add_metric( lambda: kernel_info[0], "str", "kernel_release", log_message=None ) vendor_integration_info = self._get_integration_info() self.statistics.add_metric( lambda: vendor_integration_info["integration_scripts_used"], "int", "integration_scripts_used", log_message=None ) self.statistics.add_metric( lambda: vendor_integration_info["integration_scripts_specified"], "str_list", "integration_scripts_specified", log_message=None ) memory_usage = self._get_memory_used() self.statistics.add_metric( lambda: memory_usage[0], "float", "memory_total_mb", log_message=None ) self.statistics.add_metric( lambda: memory_usage[1], "float", "memory_used_mb", log_message=None ) self.statistics.add_metric( self._get_total_domains_amount, "int", "domains_total", log_message=None ) if is_panel_feature_supported(Feature.LVE): self.statistics.add_metric( lambda: (self._is_kernel_datacycle_enabled_in_file() or self._is_datacycle_param_was_passed()), "int", "kernel_datacycle_usage_enabled", "Can't get kernel datacycle enabled parameter" ) self.statistics.add_metric( get_virt_type, "str", "virt_type", "Can't get the virtualization type" ) self.statistics.add_metric(getfqdn, "str", "hostname", "Can't get the hostname") self.statistics.add_metric( self._get_cl_installation_source, "str", "cloudlinux_installation_source", "Can't get installation source" ) def _fill_dict_with_rpm_packages_statistics(self): self.statistics.add_metric( lambda: self.system_id or UNKNOWN_RHN_ID, "str", "system_id", "Can't get system ID", ) self.statistics.add_metric(get_cl_version, "str", "os_version", "Can't get version of OS") self.statistics.add_metric( get_rpm_packages_info, "rpm_stat_list", "packages", "Can't get info about client's rpm packages" ) def _get_proc_param(self, param: AnyStr) -> Optional[int]: """ Retrieve data from proc/mounts for param :return: param_value - Optional[int], if there is no value - None """ return clconfig_utils.str_to_int(self.sysctl.get(param)) def _fill_proc_params_statistics(self): """ Filling stats about mounting - mount params from parameters list - separate hidepid getting, since it is more complicated than other mounting params """ parameters = [ "kernel.memcg_oom_disable", # Only for CL7+ ] if is_panel_feature_supported(Feature.LVE): # Get all mounting parameters including hidepid # two mounting params: fs.protected_symlinks_create and # fs.protected_hardlinks_create are gathered in # _is_link_traversal_protection_enabled method parameters.extend( [ "fs.enforce_symlinksifowner", "fs.symlinkown_gid", "fs.protected_symlinks_allow_gid", "fs.protected_hardlinks_allow_gid", "fs.global_root_enable", "fs.proc_can_see_other_uid", "fs.proc_super_gid", "fs.xfs.cap_res_quota_disable", # Only for ext4 fs "ubc.ubc_oom_disable", # Only for CL6 "fs.process_symlinks_by_task", # Only for CPanel on CL7+ ] ) for p in parameters: self.statistics.add_metric( partial(self._get_proc_param, p), "int", # It is forbidden to use '.' in the field name p.replace(".", "_"), f"Can't get {p}" ) self.statistics.add_metric( get_hidepid_typing_from_mounts, "int", "hidepid", "Can't get hidepid value" ) def _fill_kmodlve_params_statistics(self): """ Fill dict with statistics about kmodlve parameters usage """ parameters = ("lve_setuid_enter",) for p in parameters: self.statistics.add_metric( partial(self._get_kmodlve_param_value, p), "int", f"kmodlve_{p}", f'Can\'t get "{p}" kmodlve parameter value' ) def _fill_cln_jwt_information(self): """ Fill statistics fields from jwt token. - jwt token metrics (cl_plus existence and client_id) """ # Get metrics from jwt token and process them data = get_client_data_from_jwt_token(check_expiration=False) self.statistics.add_metric( lambda: None if data is None else data.get("cl_plus", None), "int", "cl_plus", "Can't get cl_plus information", ) self.statistics.add_metric( lambda: None if data is None else data.get("client_id", None), "int", "client_id", "Can't get client_id value", ) self.statistics.add_metric( lambda: None if data is None else data.get("is_awp_premium_allowed", None), "int", "jwt_is_awp_premium_allowed", "Can't get is_awp_premium_allowed value", ) self.statistics.add_metric( lambda: None if data is None else data.get("is_awp_cdn_allowed", None), "int", "jwt_is_awp_cdn_allowed", "Can't get is_awp_cdn_allowed value", ) self.statistics.add_metric( lambda: None if data is None else data.get("is_trial", None), "int", "jwt_is_trial_license", "Can't get jwt_is_trial_license value", ) self.statistics.add_metric( partial(is_active_cloudlinux_license, data), "int", "license_active", "Can't get license status value" ) def _fill_centralized_management_statistics(self): """ Filling stats centralized management - centralized management existence """ # Get information about forcible disabling of CM self.statistics.add_metric( # If such file exists, CM disabled and vice versa lambda: os.path.isfile(self.CL_PLUS_CM_DISABLED_PATH), "int", "centralized_management_disabled", "Can't check CM disabling status", ) # Get status of cl_plus_sender service self.statistics.add_metric( get_cl_plus_sender_status, "str", "cl_plus_sender_service_status", "Can't check cl plus sender service status", ) @staticmethod def make_flat_cpu_metrics() -> Dict: """ Prepare list of dicts with CPU metrics Method get_cpu_metrics returns data in following format: [ { "id": 0, "model": "QEMU Virtual CPU version 2.5+" }, { "id": 0, "model": "QEMU Virtual CPU version 2.5+" } ] This helper produces a dict, where each key - metric_name, value - list of values for all CPUs "cpu_model": [ "QEMU Virtual CPU version 2.5+", "QEMU Virtual CPU version 2.5+" ], "cpu_id": [ 0, 0 ] """ result = {} try: cpu_cores = get_cpu_metrics() for cpu_core in cpu_cores: for metric, value in cpu_core.items(): result.setdefault(f"cpu_{metric}", []).append(value) except (OSError, NotSupported) as ex: app_logger.error("CPU metrics getting error: %s", ex) return result def _fill_hardware_statistics(self): """ Filling stats about hardware metrics, specifically: CPU: - cache - frequency - model - id RAM: - ram - swap """ # Get CPUs metrics exp_metrics = ["cpu_id", "cpu_cache_mb", "cpu_model", "cpu_frequency_mhz"] cpu_metrics = self.make_flat_cpu_metrics() for metric in exp_metrics: # All metrics accept model are numeric metric_type = "int_list" if metric != "cpu_model" else "str_list" self.statistics.add_metric( partial(cpu_metrics.get, metric, None), metric_type, metric, f"Can't parse {metric} metric for all cores", ) # Get memory metrics self.statistics.add_metric( get_memory_metrics, "int_dict", "memory", "Can't parse memory metrics", ) def _fill_act_cagefs_disabled_statistics(self): """ Collect CageFS enter errors number: 1. "Act like CageFS is disabled (unable to create LVE).. %d" 2. "Act like CageFS is disabled (unable to enter into NameSpace).. %d" 3. "Act like CageFS is disabled (unable to acquire lock for user %s uid %d)" 4. File-marker /etc/cagefs/fail.on.error presense :return None """ ( self.statistics["act_cagefs_disabled_unable_to_create_lve"], self.statistics["act_cagefs_disabled_unable_to_enter_ns"], self.statistics["act_cagefs_disabled_unable_to_acqure_lock"], ) = self._scan_log_for_act_cagefs_disabled_messages() self.statistics["act_cagefs_disabled_marker_present"] = os.path.exists("/etc/cagefs/fail.on.error") def _scan_log_for_act_cagefs_disabled_messages(self) -> Tuple[int, int, int]: """ Scan /var/log/messages for all needed "Act like CageFS is disabled ..." messages for yesterday :return tuple of ints: Number of "Act like CageFS is disabled (unable to create LVE).. " messages, Number of "Act like CageFS is disabled (unable to enter into NameSpace).. " messages, Number of "Act like CageFS is disabled (unable to acquire lock for user %s uid %d)" messages """ try: returncode, stdout = self._get_data_from_log() if returncode == 0: # Something was found lines_list = stdout.split("\n") # grep for separate messages found_lines_list = list( grep( "Act like CageFS is disabled (unable to create LVE)", fixed_string=True, multiple_search=True, data_from_file=lines_list, ) ) num_unable_to_create_lve = len(found_lines_list) found_lines_list = list( grep( "Act like CageFS is disabled (unable to enter into NameSpace)", fixed_string=True, multiple_search=True, data_from_file=lines_list, ) ) num_unable_to_enter_ns = len(found_lines_list) found_lines_list = list( grep( "Act like CageFS is disabled (unable to acquire lock for user", fixed_string=True, multiple_search=True, data_from_file=lines_list, ) ) num_unable_to_acqure_lock = len(found_lines_list) else: # nothing found num_unable_to_create_lve = 0 num_unable_to_enter_ns = 0 num_unable_to_acqure_lock = 0 return num_unable_to_create_lve, num_unable_to_enter_ns, num_unable_to_acqure_lock except (OSError, IOError): return -42, -42, -42 @staticmethod def _get_data_from_log() -> Tuple[int, str]: """ Scan /var/log/messages for all needed "Act like CageFS is disabled ..." messages for yesterday :return: Tuple (ret code, std_out string) """ os_type = get_cl_version() if os_type in ["cl7", "cl7h", "cl8"]: # CL7, CL8 - use journalctl utility s_cmd = "/usr/bin/journalctl --since yesterday --until today | /usr/bin/grep 'Act like CageFS is disabled'" else: # CL6 # Note: # CL6 systems does not have utility like journalctl, so we are using logs-at script # which can work with different date formats yesterday_date = datetime.date.today() - datetime.timedelta(days=1) date_to_scan = yesterday_date.strftime("%Y-%m-%d") # /usr/share/cloudlinux/logs-at 2021-04-07 /var/log/messages s_cmd = ( f"/usr/share/cloudlinux/logs-at {date_to_scan} " "/var/log/messages | /bin/grep 'Act like CageFS is disabled'" ) p = subprocess.run( s_cmd, text=True, shell=True, executable="/bin/bash", stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False, ) return p.returncode, p.stdout def _prepare_cl_normal_statistics(self): if is_panel_feature_supported(Feature.LVE): self.statistics.add_metric( self._detect_old_lve_integration, "int", "old_way_of_integration_used", "Can't detect old LVE integration mechanism", ) self.statistics["lve_utils_version"] = LVE_UTILS_PKG_VERSION self.statistics.add_metric( partial(CLEditions.get_cl_edition, verify_exp=False), "str", "cl_edition", "Can't get CloudLinux edition" ) self.statistics.add_metric( self._is_solo_marker_exists, "int", "is_solo_meta_file_present", "Can't detect solo meta file presence" ) self.statistics.add_metric( self._is_admin_marker_exists, "int", "is_admin_meta_file_present", "Can't detect admin meta file presence" ) self.statistics.add_metric( self._is_container_marker_exists, "int", "is_container_meta_file_present", "Can't detect container meta file presence", ) self.statistics.add_metric( # security_token is property lambda: self.security_token, "str", "security_token", "Can't get or generate security token", ) self.statistics.add_metric( lambda: self.system_id or UNKNOWN_RHN_ID, "str", "system_id", "Can't get system ID", ) if LVEMANAGER_PKG_VERSION is not None and LVEMANAGER_PKG_RELEASE is not None: self.statistics["lvemanager_version"] = f"{LVEMANAGER_PKG_VERSION}-{LVEMANAGER_PKG_RELEASE}" else: self.statistics["lvemanager_version"] = None self._fill_dict_with_statistics() def _fill_lvemanager_statistics(self): """ Filling stats lvemanager - number of visits on X-Ray tab - number of X-Ray installation - number of LVEManager opens """ LOG_FOLDER = "/var/log/cloudlinux/" COLLECTING_PERIOD = 3600 * 24 LVE_NUMBER_OF_OPENS = "lve_number_of_opens" XRAY_NUMBER_OF_INSTALLS = "xray_number_of_installation" XRAY_NUMBER_OF_VISITS = "xray_number_of_visits" XRAY_ADVANCED_METRICS_STATUS = "xray_advanced_metrics_status" if not os.path.isdir(LOG_FOLDER): return # Custom Exception for breaking out outer loop class BreakOuterLoop(Exception): pass # Nested function to fill statistics def fill_statistics(stats, file): current_date_timestamp = datetime.datetime.now().timestamp() file_path = LOG_FOLDER + file with open(file_path, "r", encoding="utf-8") as f: break_outer_loop = False for line in f: try: log_time = " ".join(line.split(" ")[:2]) log_date_timestamp = datetime.datetime.strptime(log_time, "%Y-%m-%d %H:%M:%S,%f").timestamp() if current_date_timestamp - COLLECTING_PERIOD < log_date_timestamp < current_date_timestamp: message = " ".join(line.strip().split(" ")[2:]) if message == "lvemanager-opened": stats[LVE_NUMBER_OF_OPENS] += 1 elif message == "xray-install-started": stats[XRAY_NUMBER_OF_INSTALLS] += 1 elif message == "xray-tab-clicked": stats[XRAY_NUMBER_OF_VISITS] += 1 else: break_outer_loop = True except (ValueError, AttributeError): continue if break_outer_loop: raise BreakOuterLoop statistics = {LVE_NUMBER_OF_OPENS: 0, XRAY_NUMBER_OF_VISITS: 0, XRAY_NUMBER_OF_INSTALLS: 0} # include files which are contain lvemanager.log in name # except which are ends with .gz # include: lvemanager.log, lvemanager.log.1 lvemanager.log.2 # exclude: lvemanager.log*.gz, lvemanager.log*.bz2 log_files = [name for name in os.listdir(LOG_FOLDER) if re.match(r"^lvemanager\.log(\.\d{1,2})?$", name)] log_files.sort() for log_file in log_files: try: fill_statistics(statistics, log_file) except BreakOuterLoop: pass except OSError: for stat in statistics: statistics[stat] = -1 self.statistics[XRAY_NUMBER_OF_VISITS] = statistics[XRAY_NUMBER_OF_VISITS] self.statistics[XRAY_NUMBER_OF_INSTALLS] = statistics[XRAY_NUMBER_OF_INSTALLS] self.statistics[LVE_NUMBER_OF_OPENS] = statistics[LVE_NUMBER_OF_OPENS] if os.path.exists("/opt/alt/php-xray/php/advanced_metrics.enabled"): self.statistics[XRAY_ADVANCED_METRICS_STATUS] = "enabled" elif os.path.exists("/opt/alt/php-xray/php/advanced_metrics.disabled"): self.statistics[XRAY_ADVANCED_METRICS_STATUS] = "disabled" else: self.statistics[XRAY_ADVANCED_METRICS_STATUS] = "none" def _fill_leapp_statistics(self) -> None: """ Fill dict with Leapp upgrade statistics """ # Upgrade process status: 0 - not started, 1 - error, 2 - success LEAPP_STATUS = "leapp_upgrade_status" # See CLOS-2417 - we want to track users that converted from CentOS 7 to CloudLinux 8 # and offer them a discount on CLOS Shared DISCOUNT_ELIGIBILITY = "centos7_cl7_cl8_transformation" success, elevate_status = self.statistics.evaluate_safe( self.get_leapp_stats, "Can't get Leapp statistics", ) if not success: self.statistics[LEAPP_STATUS] = ERROR return self.statistics[LEAPP_STATUS] = elevate_status["leapp_status"] self.statistics[DISCOUNT_ELIGIBILITY] = elevate_status["discount_eligible"] def _fill_feature_flags_statistics(self) -> None: """ Fill dict with feature flags information """ self.statistics.add_metric( lambda: [f.name for f in list_flags_info()], "str_list", "server_flags_available", "Can't get information about available feature flags", ) self.statistics.add_metric( lambda: [f.name for f in list_flags_info() if f.enabled], "str_list", "server_flags_enabled", "Can't get information about enabled feature flags", ) def _fill_accelerate_wp_promotion_statistics(self) -> None: """ Fill out AccelerateWP promotion status """ # Metric field FIELD = "accelerate_wp_promotion_status" # Status ENABLED_FOR_CURRENT_SERVER = "enabled_for_current_server" ENABLED_FOR_ALL_SERVERS = "enabled_for_all_servers" SKIPPED = "skipped" # flags enable_awp_all_servers_flag = "/var/lve/clflags/enable_awp_all_servers.flag" enable_awp_this_server_flag = "/var/lve/clflags/enable_awp_this_server.flag" skip_awp_setup = "/var/lve/clflags/skip_awp_setup.flag" # Default status status = None if os.path.isfile(enable_awp_all_servers_flag): status = ENABLED_FOR_ALL_SERVERS if os.path.isfile(enable_awp_this_server_flag): status = ENABLED_FOR_CURRENT_SERVER if os.path.isfile(skip_awp_setup): status = SKIPPED self.statistics[FIELD] = status def _prepare_statistics(self): self.statistics.add_metric( # security_token is property lambda: self.security_token, "str", "security_token", "Can't get or generate security token", ) self.statistics.add_metric( lambda: self.system_id or UNKNOWN_RHN_ID, "str", "system_id", "Can't get system ID", ) self.statistics["lve_utils_version"] = LVE_UTILS_PKG_VERSION if LVEMANAGER_PKG_VERSION is not None and LVEMANAGER_PKG_RELEASE is not None: self.statistics["lvemanager_version"] = f"{LVEMANAGER_PKG_VERSION}-{LVEMANAGER_PKG_RELEASE}" else: self.statistics["lvemanager_version"] = None self.statistics.add_metric( self._detect_old_lve_integration, "int", "old_way_of_integration_used", "Can't detect old LVE integration mechanism", ) self._fill_dict_with_statistics() def _fill_dict_with_statistics(self): if is_panel_feature_supported(Feature.GOVERNOR): self._fill_mysql_governor_statistics() if is_panel_feature_supported(Feature.CAGEFS): self._fill_cagefs_statistics() self._fill_act_cagefs_disabled_statistics() if is_panel_feature_supported(Feature.RESELLER_LIMITS): self._fill_resellers_statistics() if is_panel_feature_supported(Feature.LVE): self._fill_limits_statistics() self._fill_kmodlve_params_statistics() if is_panel_feature_supported(Feature.PHP_SELECTOR): self._fill_php_selector_statistics() if ( is_panel_feature_supported(Feature.RUBY_SELECTOR) or is_panel_feature_supported(Feature.PYTHON_SELECTOR) or is_panel_feature_supported(Feature.NODEJS_SELECTOR) ): self._fill_selectors_statistics() if not is_cl_solo_edition(skip_jwt_check=True): self._fill_centralized_management_statistics() if is_panel_feature_supported(Feature.LSAPI): self._fill_lsapi_statistics() if is_panel_feature_supported(Feature.WIZARD): self._fill_wizard_statistics() self._fill_wpos_activation_statistics() self._fill_wpos_statistics() self._fill_cln_jwt_information() self._fill_control_panel_statistics() self._fill_system_statistics() self._fill_proc_params_statistics() self._fill_hardware_statistics() self._fill_wmt_settings_statistics() self._fill_ssa_statistics() self._fill_lvemanager_statistics() self._fill_wp_statistics() self._fill_per_user_wp_statistics() self._fill_leapp_statistics() self._fill_feature_flags_statistics() self._fill_accelerate_wp_promotion_statistics() self._fill_apache2nginx_statistics() def _fill_apache2nginx_statistics(self): self.statistics.add_metric( self.get_apache2nginx_stats, "int_dict", "apache2nginx", "Can't parse Apache2Nginx metrics", ) def _fill_wpos_activation_statistics(self): # AccelerateWP installation was in fact successful on end-server self.statistics.add_metric( lambda: os.path.exists(ACCELERATE_WP_INSTALLED_FROM_CM), "int", "cmt_is_accelerate_wp_free_activation_completed", "Can't get statistic of AccelerateWP CMT activation", ) success, client_activation = self.statistics.evaluate_safe( client_activation_data, "Can't get AccelerateWP activation statistics", ) # client clicked button in centralized monitoring UI self.statistics.add_metric( lambda: client_activation.get("accelerate_wp_free_activate"), "int", "cmt_accelerate_wp_free_activated_in_ui", "Can't get statistic of AccelerateWP CMT activation selected" if success else None, ) def _fill_wpos_statistics(self): success, wpos_stats = self.statistics.evaluate_safe( self.get_wpos_stats, "Can't get AccelerateWP statistics", ) if not success: return if isinstance(wpos_stats, dict) and wpos_stats.get("result") == "success": if wpos_stats.get("features_visible_by_default") is not None: self.statistics.add_metric( lambda: wpos_stats["features_visible_by_default"], "str_list", "wpos_features_visible_by_default", "Can't get statistic of using AccelerateWP.", ) if wpos_stats.get("features_allowed_by_default") is not None: self.statistics.add_metric( lambda: wpos_stats["features_allowed_by_default"], "str_list", "wpos_features_allowed_by_default", "Can't get statistic of using AccelerateWP.", ) self.statistics.add_metric( lambda: wpos_stats["enabled_sites"]["total"], "int", "sites_count_with_enabled_wpos", "Can't get statistic of using AccelerateWP.", ) self.statistics.add_metric( lambda: wpos_stats["allowed_users"]["total"], "int", "users_count_with_allowed_wpos", "Can't get statistic of using AccelerateWP.", ) if wpos_stats.get("visible_users"): self.statistics.add_metric( lambda: wpos_stats["visible_users"]["total"], "int", "users_count_with_visible_wpos", "Can't get statistic of using AccelerateWP.", ) # pylint: disable=cell-var-from-loop for module in wpos_stats["enabled_sites"]: if module == "total": continue self.statistics.add_metric( lambda: wpos_stats["enabled_sites"][module], "int", "sites_count_with_enabled_" + module, "Can't get statistic of using AccelerateWP.", ) for module in wpos_stats.get("enabled_users", []): self.statistics.add_metric( lambda: wpos_stats["enabled_users"][module], "int", "users_count_with_enabled_" + module, "Can't get statistic of using AccelerateWP.", ) for module in wpos_stats["allowed_users"]: if module == "total": continue self.statistics.add_metric( lambda: wpos_stats["allowed_users"][module], "int", "users_count_with_allowed_" + module, "Can't get statistic of using AccelerateWP.", ) for suite in wpos_stats.get("allowed_suites", []): self.statistics.add_metric( lambda: wpos_stats["allowed_suites"][suite], "int", "users_count_with_allowed_suite_" + suite, "Can't get statistic of using AccelerateWP.", ) for suite in wpos_stats.get("enabled_suites", []): self.statistics.add_metric( lambda: wpos_stats["enabled_suites"][suite], "int", "users_count_with_enabled_suite_" + suite, "Can't get statistic of using AccelerateWP.", ) billable_suite_metric = f'{suite}_billable' non_billable_suite_metric = f'{suite}_non_billable' if enabled_billable := wpos_stats.get(billable_suite_metric): self.statistics.format_metric( lambda: enabled_billable, "int", "users_count_with_enabled_suite_" + billable_suite_metric, "Can't get statistic of using AccelerateWP.", ) if enabled_non_billable := wpos_stats.get(non_billable_suite_metric): self.statistics.format_metric( lambda: enabled_non_billable, "int", "users_count_with_enabled_suite_" + non_billable_suite_metric, "Can't get statistic of using AccelerateWP.", ) for module in wpos_stats.get("visible_users", []): if module == "total": continue self.statistics.add_metric( lambda: wpos_stats["visible_users"][module], "int", "users_count_with_visible_" + module, "Can't get statistic of using AccelerateWP.", ) # pylint: enable=cell-var-from-loop for module, upgrade_url in wpos_stats.get("upgrade_urls", {}).items(): self.statistics["upgrade_url_" + module] = upgrade_url self.statistics.add_metric( lambda: wpos_stats["accelerate_wp_suite_enabled_premium_suite_disallowed"], "int", "accelerate_wp_suite_enabled_premium_suite_disallowed", "Can't get statistic of using AccelerateWP.", ) self.statistics.add_metric( lambda: wpos_stats["accelerate_wp_suite_enabled_premium_suite_visible"], "int", "accelerate_wp_suite_enabled_premium_suite_visible", "Can't get statistic of using AccelerateWP.", ) self.statistics.add_metric( lambda: wpos_stats.get("is_accelerate_wp_flag_enabled"), "int", "is_accelerate_wp_flag_enabled", "Can't get statistic of AccelerateWP feature flag.", ) self.statistics.add_metric( lambda: wpos_stats.get("is_accelerate_wp_icon_enabled"), "int", "is_accelerate_wp_icon_enabled", "Can't get statistic of AccelerateWP icon status.", ) self.statistics.add_metric( lambda: wpos_stats.get("is_smart_advice_notifications_enabled"), "str", "is_smart_advice_notifications_enabled", "Can't get statistic of AccelerateWP is_smart_advice_notifications_enabled.", ) self.statistics.add_metric( lambda: wpos_stats.get("is_smart_advice_reminders_enabled"), "str", "is_smart_advice_reminders_enabled", "Can't get statistic of AccelerateWP is_smart_advice_reminders_enabled.", ) self.statistics.add_metric( lambda: wpos_stats.get("is_smart_advice_plugin_installation_enabled"), "str", "is_smart_advice_plugin_installation_enabled", "Can't get statistic of AccelerateWP is_smart_advice_plugin_installation_enabled.", ) self.statistics.add_metric( lambda: wpos_stats.get("is_object_cache_banners_enabled"), "str", "is_object_cache_banners_enabled", "Can't get statistic of AccelerateWP is_object_cache_banners_enabled.", ) if whmcs_stats := wpos_stats.get("whmcs"): for key, value in whmcs_stats.items(): self.statistics[f"awp_whmcs_{key}"] = value @staticmethod def _run_cloudlinux_statistics(args): """ Run cloudlinux-statistics using subprocess and handle errors. :type args: list[str] :rtype: str or None """ cmd = ["/usr/sbin/cloudlinux-statistics"] + args try: rc, json_str, _ = run_command(cmd, return_full_output=True) except ExternalProgramFailed as e: app_logger.warning("Unable to run cloudlinux-statistics, error: %s", e) return None if rc == 0: return json_str app_logger.error("cloudlinux-statistics failed with exit code: %i, output: %s", rc, json_str) return None def get_users_and_resellers_with_faults(self): """ Get number of users and resellers with faults for the past 24h :rtype: tuple[int, int] """ json_str = self._run_cloudlinux_statistics(["--by-fault", "any", "--json", "--period=1d"]) # lve-stats is not installed or util is broken if json_str is None: return None, None try: json_data = json.loads(json_str) resellers = json_data["resellers"] users = json_data["users"] except (KeyError, ValueError, TypeError) as e: app_logger.warning("Something really bad happened to cloudlinux-statistics, The reason is: %s", str(e)) return None, None return len(users), len(resellers) def _get_users_with_faults(self) -> Optional[int]: """ a wrapper method for the get_users_and_resellers_with_faults method. return the number of users with faults in the past 24 hours. """ return self.get_users_and_resellers_with_faults()[0] def _get_resellers_with_faults(self) -> Optional[int]: """ a wrapper method for the get_users_and_resellers_with_faults method. return the number of resellers with faults in the past 24 hours. """ return self.get_users_and_resellers_with_faults()[1] @classmethod def _get_cpu_limit_units(cls, cpu): """Get config cpu limit format""" if cpu is None: return None unit = cpu.lower() if unit.endswith("%"): return "speed" if unit.endswith("mhz"): return "mhz" if unit.endswith("ghz"): return "ghz" if unit.isdigit(): return "old_cpu_format" return f"unknown: {cpu}" @staticmethod def _mempages_to_mb(value): """Convert memory limit from mempages to megabytes""" if value is None: return None return 4 * value // 1024 @staticmethod def _cpu_limit_to_percents(cpu, ncpu): """Convert cpu and ncpu to percents of one core""" if cpu is None: return None speed = lvectllib.convert_to_kernel_format(cpu, lncpu=ncpu or 0) if speed is None: return None return round(speed / 100.0, 1) def get_users_amount_per_plan(self, xml_cfg_provider: LimitsDataStorage) -> list[tuple[int, str], ...]: """ Return list of tuples [users_in_package, package] """ # sort users by package name (needed for right grouping) if self.packages_by_len is None: users_sorted_by_package = sorted( list(user for user in xml_cfg_provider.users.values() if user.package.name is not None), key=attrgetter("package"), ) # group sorted list of users by package name and get amount of them per package packages_by_len = [] for package, group in groupby(users_sorted_by_package, key=attrgetter("package")): num_users = len(list(group)) packages_by_len.append((num_users, package)) packages_by_len.sort(reverse=True) self.packages_by_len = packages_by_len return self.packages_by_len def _get_top_package_by_number_of_users( self, number_of_top: int, xml_cfg_provider: LimitsDataStorage) -> Optional[Tuple[int, str]]: try: return self.get_users_amount_per_plan(xml_cfg_provider)[number_of_top - 1] except IndexError: return None def _parse_args(self, argv): """ Parse CLI arguments """ status, data = parse_cloudlinux_summary_opts(argv) if not status: # exit with error if can`t parse CLI arguments self._error_and_exit(replace_params(data)) return data @staticmethod def _print_result_and_exit( result: str = "success", data: object | None = None, exit_code: int = 0, is_statistic_enabled: bool | None = None) -> typing.NoReturn: """ Print data in default format for web and exit """ message = {"result": result, "timestamp": time.time(), "data": data} if is_statistic_enabled is not None: message["statistic_enabled"] = is_statistic_enabled print_dictionary(message, True) sys.exit(exit_code) @staticmethod def _error_and_exit(message: dict, error_code: int = 1) -> Optional[None]: """ Print error and exit :param dict message: Dictionary with keys "result" as string and optional "context" as dict """ message.update({"timestamp": time.time()}) print_dictionary(message, True) sys.exit(error_code) @staticmethod def get_raw_lsapi_info() -> Optional[Dict]: """ Return mod_lsapi info from switch_mod_lsapi script """ if os.path.isfile("/usr/bin/switch_mod_lsapi"): p = subprocess.run( ["/usr/bin/switch_mod_lsapi", "--stat"], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, ) return json.loads(p.stdout) @staticmethod def _get_wmt_api_config(): """ Return wmt-api config if is_solo_edition True, run wpt-api-solo is command """ command = "/usr/share/web-monitoring-tool/wmtbin/wmt-api" if is_cl_solo_edition(): command += "-solo" if os.path.isfile(command): p = subprocess.run( [command, "--config-get"], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False ) wmt_api_config_summary = json.loads(p.stdout).get("config") wmt_api_config_summary = {k: str(v) for k, v in wmt_api_config_summary.items()} return wmt_api_config_summary @staticmethod def _is_solo_marker_exists(): return os.path.isfile("/etc/cloudlinux-edition-solo") @staticmethod def _is_admin_marker_exists(): return os.path.isfile("/etc/cloudlinux-edition-admin") @staticmethod def _is_container_marker_exists(): return os.path.isfile("/etc/cloudlinux-container") @staticmethod def get_ssa_stats() -> Optional[Dict]: """ Return statistics from cloudlinux-ssa-manager get-stat API """ if os.path.isfile("/usr/sbin/cloudlinux-ssa-manager"): p = subprocess.run( ["/usr/sbin/cloudlinux-ssa-manager", "get-stat"], text=True, capture_output=True, check=False ) return json.loads(p.stdout) @staticmethod def get_leapp_stats() -> dict: """ Return statistics from Leapp upgrade logs """ discount_eligible = 0 leapp_logfile = "/var/log/leapp/leapp-upgrade.log" elevate_cpanel_logfile = "/var/log/elevate-cpanel.log" cldeploy_logfile = "/var/log/cldeploy.log" if os.path.isfile(leapp_logfile) and os.path.isfile(cldeploy_logfile): leapp_mtime = os.path.getmtime(leapp_logfile) cldeploy_mtime = os.path.getmtime(cldeploy_logfile) # The user is eligible for discount when cldeploy and leapp were ran # within 1 month of each other. if leapp_mtime - cldeploy_mtime < 30 * 24 * 60 * 60: discount_eligible = 1 # This means upgrade wasn't attempted. upgrade_status = 0 if detect.getCPName() != cpapi.CPANEL_NAME: # Most likely nopanel, read leapp log file # If the log file exists, `leapp upgrade` was run on the machine. if os.path.isfile(leapp_logfile): # This shows that the upgrade process was not successful. fail_grep_str = r"An upgrade inhibitor detected\|Workflow interrupted due to the FailPhase" # This shows that the upgrade process got to the very last stage - i.e. success. first_boot_grep_str = r"Starting stage After of phase FirstBoot" fail_grep_res = next(grep(fail_grep_str, leapp_logfile, fixed_string=True), None) first_boot_grep_res = next(grep(first_boot_grep_str, leapp_logfile, fixed_string=True), None) # No halting messages found, uprgade is (provisionally) successful. if first_boot_grep_res and not fail_grep_res: upgrade_status = 2 else: upgrade_status = 1 else: # cPanel, read elevate-cpanel log file if os.path.isfile(elevate_cpanel_logfile): success_grep_str = r"Great SUCCESS" grep_res = next(grep(success_grep_str, elevate_cpanel_logfile, fixed_string=True), None) # The process got to the end and reported success, upgrade is successful. if grep_res: upgrade_status = 2 else: upgrade_status = 1 # Discount applies only to users who have successfully upgraded. if upgrade_status != 2: discount_eligible = 0 stat_result = {"leapp_status": upgrade_status, "discount_eligible": discount_eligible} return stat_result @staticmethod def _get_kmodlve_param_value(param_name: str) -> int: """ Get kmodlve parameter value: -1 - not supported (parameter file doesn't exist); 0 - disabled; 1 - enabled """ params_dir = "/sys/module/kmodlve/parameters" param_file = Path(params_dir, param_name) # absense of the file means parameter is not supported if not param_file.exists(): return -1 param_value = param_file.read_text(encoding="utf-8").strip() if param_value == "Y": return 1 if param_value == "N": return 0 raise RuntimeError(f'Unable to interpret "{param_name}" kmodlve ' f'parameter value: "{param_value}"') @staticmethod def get_smart_advice_stats() -> Optional[Dict]: """ Return statistics from cl-smart-advice counters API """ util = "/usr/sbin/cl-smart-advice" if os.path.isfile(util): p = subprocess.run([util, "counters"], text=True, capture_output=True, check=False) return json.loads(p.stdout).get("data") def _get_smart_advice_statistics(self) -> Dict: """ Construct dict with Smart Advice statistics """ result = {"smart_advice_total": None, "smart_advice_applied": None} # checking edition by JWT token only, skipping marker check, # since cl-smart-advice requires JWT # and due to bug with detection on Shared PRO by marker PTCLLIB-238 if not is_cl_shared_edition(skip_marker_check=True): success, sa_stats = self.statistics.evaluate_safe( self.get_smart_advice_stats, "Can't get Smart Advice statistics", ) if sa_stats is not None: try: return {"smart_advice_total": sa_stats["total"], "smart_advice_applied": sa_stats["applied"]} except KeyError as e: app_logger.exception( 'Unexpected json response from server, field %s is missing in "%s"', str(e), sa_stats ) return result return result @staticmethod def get_apache2nginx_stats() -> Optional[Dict]: """ Return statistics by parsing /var/lib/apache2nginx/server.json """ stats = {} if os.path.isfile("/var/lib/apache2nginx/server.json"): with open("/var/lib/apache2nginx/server.json", "r", encoding="utf-8") as f: data = json.load(f) # apache2nginx_total_websites is total number of entries # in server.json stats["total_websites"] = len(data) websites = list(data.values()) # apache2nginx_total_proxy_pass is all website with proxy_reason field proxied = [x for x in websites if x.get("proxy_reason")] stats["total_proxy_pass"] = len(proxied) # proxy pass due to failed directive pasing stats["proxy_pass_due_directive"] = len([x for x in proxied if x["proxy_reason"] == "directive"]) stats["failing_directives"] = [] stats["failing_handlers"] = [] for x in proxied: if x["proxy_reason"] == "directive": failing_directive = x.get("proxy_details") if failing_directive: stats["failing_directives"].append(x["proxy_details"]) elif x["proxy_reason"] == "handler": failing_handler = x.get("proxy_details") if failing_handler: stats["failing_handlers"].append(x["proxy_details"]) # Make entries unique stats["failing_directives"] = list(set(stats["failing_directives"])) stats["failing_handlers"] = list(set(stats["failing_handlers"])) stats["proxy_pass_due_test"] = len([x for x in proxied if x["proxy_reason"] == "test"]) stats["proxy_pass_due_forced"] = len([x for x in proxied if x["proxy_reason"] == "forced"]) stats["proxy_pass_due_handler"] = len([x for x in proxied if x["proxy_reason"] == "handler"]) stats["proxy_pass_due_includes"] = len([x for x in proxied if x["proxy_reason"] == "includes"]) # state is based on string in the /var/lib/apache2nginx/state file # if file is missing, state is unknown if os.path.isfile("/var/lib/apache2nginx/state"): with open("/var/lib/apache2nginx/state", "r", encoding="utf-8") as f: stats["state"] = f.read().strip() return stats @staticmethod def get_wpos_stats() -> Optional[Dict]: """ Return statistics from cl-smart-advice counters API """ util = "/usr/bin/clwpos-admin" if os.path.isfile(util): p = subprocess.run( [util, "get-stat"], text=True, capture_output=True, check=False, ) return json.loads(p.stdout) def _get_lvpmap(): lvpmap = LvpMap() lvpmap.name_map.link_xml_node() return lvpmap