Mini Shell
# 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
Zerion Mini Shell 1.0