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
from __future__ import absolute_import
from __future__ import division
import itertools
import subprocess
from builtins import range
import hashlib
import json
import logging
import math
import os
import random
import string
import sys
import time
import platform
import datetime
from functools import partial
from collections import Counter
from itertools import groupby
from future.utils import iteritems
from operator import attrgetter
from multiprocessing import cpu_count
from socket import getfqdn
import requests
import cldetectlib as detect
import lvectllib
from clcommon import cpapi
from clcommon.const import Feature
from clcommon.lib import MySQLGovernor
from clcommon.utils import (
get_rhn_systemid_value,
run_command,
ExternalProgramFailed,
is_testing_enabled_repo,
is_litespeed_running,
get_cl_version,
get_virt_type,
grep
)
from clcommon.sysctl import SysCtlConf
from clconfig import cagefs_statistics_config, db_governor_lib, clconfig_utils
from cli_utils import print_dictionary, replace_params
from cllimitslib_v2 import LimitsDataStorage, DEFAULTS
from clveconfig import EMPTY_LIMITS
from lveapi import LvpMap
from .cl_summary_arg_parse import parse_cloudlinux_summary_opts
from typing import Dict, Optional, Callable, Union, List, Any, Tuple, AnyStr # NOQA
from clsummary.cl_summary_utils import (
is_statistic_enabled,
set_statistic_collection_enabled,
write_statistics_send_status_to_file,
SummaryStatus,
is_python_selector_installed,
is_ruby_selector_installed,
is_nodejs_selector_installed,
is_php_selector_installed,
dummy_none_function,
get_packages_with_lve_extensions,
is_sending_process_running,
get_statistics_send_status_from_file,
get_client_data_from_jwt_token,
get_cl_plus_sender_status,
)
from clsummary.rpm_packages_statistics import (
get_rpm_packages_info,
)
from clsummary.hardware_statistics import (
get_cpu_metrics,
get_memory_metrics,
NotSupported
)
from clwizard.modules import ALL_MODULES
from lve_utils import PKG_VERSION as LVE_UTILS_PKG_VERSION
from vendors_api.config import CONFIG_PATH, _read_config_file
from cldiaglib import is_email_notification_enabled
from cl_proc_hidepid import get_hidepid_typing_from_mounts
try:
# Package from lvemanager - can be absent
from clselect.clselectstatistics import get_versions_statistics, \
get_native_version_safe, get_php_selector_usage, iter_server_applications, \
get_default_php_version, get_mode_of_php_selector
from clselector.selectorlib import CloudlinuxSelectorLib
from clselect.clselectctl import get_default_version
from lvemanager import PKG_VERSION as LVEMANAGER_PKG_VERSION
from lvemanager import PKG_RELEASE as LVEMANAGER_PKG_RELEASE
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
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'
class StatisticsDict(dict):
"""
Special class to store all metrics before sending it
"""
# a few values which should be return if we can't collect statistic for something module
DEFAULT_RESULTS = {
'str': '-42',
'int': -42,
'float': -42.0,
'str_list': ['-42'],
'int_list': [-42],
'int_dict': {
'-42': -42,
},
'float_dict': {
'-42': -42.0,
},
'str_dict': {
'-42': '-42',
},
# Special default result.
# Only for collecting of statistics about client's rpm packages
'rpm_stat_list': []
}
def run_safe_or_log_errors(self, func, type_of_result, log_message, log_exception=True):
# type: (Callable, str, str, bool) -> Union[Dict[Any], List[str], List[int], int, str]
"""
Method for catching any exceptions while calling passed function,
logging them and return default result in case if exception is present or
return formatted reuslt in case if exception is absent
:param log_exception: True - exception will been logged, False - exception won't been logged
This flag must be used if param `func` is function, which processes intermediate value, which can equal
to -42 (it means that exception was logged in previous moment) and
doesn't call external functions or doesn't process another values. In other cases
log_exception should be equal True, because in calling of external function or processing of another
value can be raised exception, which should be logged.
Example: functions `lambda: intermediate_value[0].property` or
[len(value) for value intermediate_value.values()] should be used with log_exception equal False,
because they only process intermediate value. But function:
def some_function(intermediate_value):
result = external_function()
return len(intermediate_value), result
should be used with log_exception equal True
"""
default_result = self.DEFAULT_RESULTS[type_of_result]
try:
result = func()
except Exception as err:
if log_exception:
app_logger.exception(
'%s. Exception: "%s"',
log_message,
err,
)
result = default_result
return result
def format_metric(self, func, type_of_result, name_of_metric, log_message, log_exception=True):
# type: (Callable, str, str, str, bool) -> None
"""
Method which call `run_safe_or_log_errors` and save result from it
:param log_exception: True - exception will been logged, False - exception won't been logged
See method `run_safe_or_log_errors`
"""
result = self.run_safe_or_log_errors(
func,
type_of_result,
log_message,
log_exception=log_exception,
)
self[name_of_metric] = result
class CloudlinuxSummary(object):
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://papi.g.geo.mycache.org/api/stat-api/clos-stat'
RPM_PACKAGES_URL = 'https://papi.g.geo.mycache.org/api/rpm-stats'
SETTINGS_URL = 'https://1.mirror.g.cdn.mycache.org/static/cl-settings-v1.json'
def __init__(self):
self._opts = dict()
self._security_token = None
self.statistics = StatisticsDict()
self._lvpmap = None
self._system_id = None
self.is_not_in_lve = not bool(os.environ.get('RUNNING_IN_LVE'))
self.packages_by_len = None
self.sysctl = SysCtlConf()
@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):
# type: () -> str
if self._system_id is None:
self._system_id = get_rhn_systemid_value('system_id')
return self._system_id
@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):
# type: () -> 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 = "Getting statistics from server %s" % 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 = "Server answer is: HTTP code {}; Reason: {}".format(
response.status_code,
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']
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. "
"See %s for details." % LOG_FILE})
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') 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) 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():
# type: () -> 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):
# type: (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('Unknown interpreter: {}'.format(interpreter))
def _get_remote_settings(self, settings_url):
try:
settings = requests.get(settings_url).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
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_in_lve(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['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)
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'):
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') and 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._opts.get('--send') and 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 '
'we are not able to make a security token. Check %s '
'for details or contact support.' % LOG_FILE,
'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
os.system('/usr/sbin/cloudlinux-summary --send --json &> /dev/null &')
self._wait_for_background_process()
data = {'timestamp': time.time(), 'result': 'success'}
print_dictionary(data, True)
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_not_in_lve and is_sending_process_running(acquire_lock=True):
# Checking/acquiring of lock is performed outside of LVE 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)
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=True
in order to check it in child process.
:return:
"""
settings = lvectllib.make_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 _should_run_outside_lve(opts: Dict[Any, Any]) -> bool:
"""
Check that passed command should run outside LVE
"""
if any(opts[option] for option in (
'rpm-packages',
'status',
'get-remote',
'enable',
'disable',
)):
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_not_in_lve:
# 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_in_lve()
# We should run only main statistics collection in LVE
# in other cases we skip run in LVE
if self._should_run_outside_lve(self._opts):
exit(0)
try:
rc = self._run_self_in_lve(argv)
exit(rc)
except lvectllib.PyLveError as e:
error_msg = 'failed to run task in lve, error: %s' % e
print(error_msg)
log = logging.getLogger(__name__)
log.exception(error_msg, exc_info=True)
exit(-1)
else:
if self._should_run_outside_lve(self._opts):
err_msg = 'You shouldn\'t use env var ' \
'"RUNNING_IN_LVE" 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)
exit(1)
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.format_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', '{}m'.format(int(math.ceil(time_minutes)))]
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 = "Sending statictics to server %s" % 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 = "Server answer is: HTTP code {}; Reason: {}".format(
response.status_code,
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_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():
# type: () -> str
"""
Get version of control panel
"""
detect.getCP()
return detect.CP_VERSION
def _fill_mysql_governor_statistics(self):
# type: () -> None
"""
Fill dict with statistics by statistics about MySQL governor
"""
mysql_gov_mode = self.statistics.run_safe_or_log_errors(
db_governor_lib.get_gov_mode_operation,
'str',
'Can\'t get MySQL governor mode',
)
if mysql_gov_mode is not None:
self.statistics['mysql_governor_mode'] = mysql_gov_mode
self.statistics.format_metric(
lambda: MySQLGovernor().get_governor_version(),
'str',
'mysql_governor_version',
'Can\'t get MySQL governor version',
)
self.statistics.format_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):
# type: () -> None
"""
Fill dict with statistics by statistics about control panel
"""
self.statistics.format_metric(
detect.getCPName,
'str',
'control_panel_name',
'Can\'t get control panel name',
)
self.statistics.format_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.format_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.format_metric(
self._get_control_panel_apache,
'str',
'control_panel_apache',
'Can\'t get control panel apache',
)
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):
# type: () -> None
"""
Fill dict with statistics by statistics about CageFS
"""
self.statistics.format_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.format_metric(
cagefs_statistics_config.get_cagefs_user_mode,
'str',
'cagefs_user_mode',
'Can\'t get CageFS user mode',
)
self.statistics.format_metric(
partial(cagefs_statistics_config.get_quantity, True),
'str',
'cagefs_enabled_quantity',
'Can\'t get quantity of users with enabled CageFS',
)
self.statistics.format_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):
# type: () -> 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):
# type: () -> 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():
# type: () -> Optional[int]
"""
Get amount of resellers
"""
try:
return len(cpapi.resellers())
except cpapi.NotSupported:
pass
def _fill_resellers_statistics(self):
# type: () -> None
"""
Fill dict with statistics by varied statistics about resellers
"""
self.statistics.format_metric(
lvectllib.lve.is_lve10,
'int',
'reseller_limits_supported_kernel',
'Can\'t detect status of support reseller limits by kernel',
)
self.statistics.format_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.format_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.format_metric(
lambda: self.get_users_and_resellers_with_faults()[0],
'int',
'users_with_faults',
'Can\'t get amount of users with faults for the past 24h',
)
self.statistics.format_metric(
self._get_amount_of_resellers,
'int',
'resellers_total',
'Can\'t get total amount of resellers',
)
self.statistics.format_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.format_metric(
self._get_total_amount_of_endusers,
'int',
'resellers_endusers_total',
'Can\'t get total amount of end-users',
)
self.statistics.format_metric(
lambda: self.get_users_and_resellers_with_faults()[1],
'int',
'resellers_with_faults',
'Can\'t get amount of resellers with faults for the past 24h',
)
if self.statistics['reseller_limits_enabled']:
self.statistics.format_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):
# type: (LimitsDataStorage) -> None
"""
Fill dict with statistics by statistics about default limits
"""
self.statistics.format_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.format_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.format_metric(
lambda: xml_cfg_provider.defaults[DEFAULTS].ncpu,
'int',
'default_limit_ncpu',
'Can\'t get default ncpu limit',
)
self.statistics.format_metric(
lambda: xml_cfg_provider.defaults[DEFAULTS].io,
'int',
'default_limit_io',
'Can\'t get default io limit',
)
self.statistics.format_metric(
lambda: xml_cfg_provider.defaults[DEFAULTS].nproc,
'int',
'default_limit_nproc',
'Can\'t get default nproc limit',
)
self.statistics.format_metric(
lambda: xml_cfg_provider.defaults[DEFAULTS].ep,
'int',
'default_limit_ep',
'Can\'t get default ep limit',
)
self.statistics.format_metric(
lambda: xml_cfg_provider.defaults[DEFAULTS].iops,
'int',
'default_limit_iops',
'Can\'t get default iops limit',
)
self.statistics.format_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.format_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):
# type: (LimitsDataStorage) -> None
"""
Fill dict with statistics by other statistics about limits:
packages_total, users_total, amount users/packages with custom limits
"""
self.statistics.format_metric(
lambda: len(xml_cfg_provider.packages),
'int',
'packages_total',
'Can\'t get total amount of packages',
)
self.statistics.format_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.format_metric(
lambda: len(xml_cfg_provider.users),
'int',
'users_total',
'Can\'t get total amount of users',
)
self.statistics.format_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',
)
def _fill_top_packages_statistics(self, xml_cfg_provider):
# type: (LimitsDataStorage) -> None
"""
Fill dict with statistics by statistics about top packages on server
"""
for i in range(1, 4):
top_result = self.statistics.run_safe_or_log_errors(
partial(self._get_top_package_by_number_of_users, i, xml_cfg_provider),
'str',
'Can\'t get top %s package by users' % i,
)
# Break cycle if result is None,
# beacuse package with number more than i doesn't exists
if 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.format_metric(
lambda: top_result[1].name, # pylint: disable=cell-var-from-loop
'str',
'top_%s_package_name' % i,
'Can\'t get package name of top %s package by users' % i,
log_exception=False,
)
self.statistics.format_metric(
lambda: int(top_result[0]), # pylint: disable=cell-var-from-loop
'int',
'top_%s_package_users_num' % i,
'Can\'t get amount of users in top %s package by users' % i,
log_exception=False,
)
self.statistics.format_metric(
lambda: self._cpu_limit_to_percents(
(top_result[1].limits or EMPTY_LIMITS).cpu, # pylint: disable=cell-var-from-loop
(top_result[1].limits or EMPTY_LIMITS).ncpu, # pylint: disable=cell-var-from-loop
),
'int',
'top_%s_package_limit_speed' % i,
'Can\'t get speed limit of top %s package by users' % i,
log_exception=False,
)
self.statistics.format_metric(
lambda: self._get_cpu_limit_units(
(top_result[1].limits or EMPTY_LIMITS).cpu, # pylint: disable=cell-var-from-loop
),
'str',
'top_%s_package_limit_cpu_origin_units' % i,
'Can\'t get cpu origin units of limit of top %s package by users' % i,
log_exception=False,
)
self.statistics.format_metric(
lambda: (top_result[1].limits or EMPTY_LIMITS).ncpu, # pylint: disable=cell-var-from-loop
'int',
'top_%s_package_limit_ncpu' % i,
'Can\'t get ncpu limit of top %s package by users' % i,
log_exception=False,
)
self.statistics.format_metric(
lambda: (top_result[1].limits or EMPTY_LIMITS).io, # pylint: disable=cell-var-from-loop
'int',
'top_%s_package_limit_io' % i,
'Can\'t get io limit of top %s package by users' % i,
log_exception=False,
)
self.statistics.format_metric(
lambda: (top_result[1].limits or EMPTY_LIMITS).nproc, # pylint: disable=cell-var-from-loop
'int',
'top_%s_package_limit_nproc' % i,
'Can\'t get nproc limit of top %s package by users' % i,
log_exception=False,
)
self.statistics.format_metric(
lambda: (top_result[1].limits or EMPTY_LIMITS).ep, # pylint: disable=cell-var-from-loop
'int',
'top_%s_package_limit_ep' % i,
'Can\'t get ep limit of top %s package by users' % i,
log_exception=False,
)
self.statistics.format_metric(
lambda: (top_result[1].limits or EMPTY_LIMITS).iops, # pylint: disable=cell-var-from-loop
'int',
'top_%s_package_limit_iops' % i,
'Can\'t get iops limit of top %s package by users' % i,
log_exception=False,
)
self.statistics.format_metric(
lambda: self._mempages_to_mb((top_result[1].limits or EMPTY_LIMITS).vmem), # pylint: disable=cell-var-from-loop
'int',
'top_%s_package_limit_vmem_mb' % i,
'Can\'t get vmem limit of top %s package by users' % i,
log_exception=False,
)
self.statistics.format_metric(
lambda: self._mempages_to_mb((top_result[1].limits or EMPTY_LIMITS).pmem), # pylint: disable=cell-var-from-loop
'int',
'top_%s_package_limit_pmem_mb' % i,
'Can\'t get pmem limit of top %s package by users' % i,
log_exception=False,
)
def _fill_limits_statistics(self):
# type: () -> 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):
# type: () -> None
"""
Fill dict with statistics by statistics about mod_lsapi
"""
raw_lsapi_info = self.statistics.run_safe_or_log_errors(
self.get_raw_lsapi_info,
'str',
'Can\'t get raw mod_lsapi info',
)
if raw_lsapi_info is not None:
self.statistics.format_metric(
lambda: raw_lsapi_info['criu']['status'],
'str',
'lsapi_criu_service_status',
'Can\'t get status of criu service',
log_exception=False,
)
self.statistics.format_metric(
lambda: raw_lsapi_info['criu']['version'],
'str',
'lsapi_criu_service_version',
'Can\'t get version of criu service',
log_exception=False,
)
self.statistics.format_metric(
lambda: raw_lsapi_info['lsapiConf']['lsapi_criu'],
'str',
'lsapi_option_criu',
'Can\'t get state of criu',
log_exception=False,
)
self.statistics.format_metric(
lambda: raw_lsapi_info['lsapiConf']['lsapi_with_connection_pool'],
'str',
'lsapi_option_connection_pool',
'Can\'t get state of mod_lsapi connection pool',
log_exception=False,
)
self.statistics.format_metric(
lambda: raw_lsapi_info['libVersion'],
'str',
'lsapi_lib_version',
'Can\'t get version of mod_lsapi lib',
log_exception=False,
)
self.statistics.format_metric(
lambda: raw_lsapi_info['modStatus'],
'str',
'lsapi_mod_status',
'Can\'t get mod_lsapi status',
log_exception=False,
)
self.statistics.format_metric(
lambda: raw_lsapi_info['modVersion'],
'str',
'lsapi_mod_version',
'Can\'t get mod_lsapi version',
log_exception=False,
)
self.statistics.format_metric(
lambda: raw_lsapi_info['totalDomain'],
'int',
'lsapi_total_domain_count',
'Can\'t get total amount of domains which use mod_lsapi',
log_exception=False,
)
self.statistics.format_metric(
lambda: raw_lsapi_info['domainStat'],
'int_dict',
'lsapi_domain_stat',
'Can\'t get statistics of domains with mod_lsapi',
log_exception=False,
)
self.statistics.format_metric(
lambda: raw_lsapi_info['controlPanel'],
'str',
'lsapi_apache_environment',
'Can\'t get apache environment',
log_exception=False,
)
else:
self.statistics['lsapi_mod_status'] = NOT_INSTALLED
def _fill_php_selector_statistics(self):
# type: () -> None
"""
Fill dict with statistics by varied statistics about PHP selector
"""
php_interpreters = self.statistics.run_safe_or_log_errors(
lambda: get_versions_statistics('php'),
'int',
'Can\'t get statistics about PHP interpreters'
)
if php_interpreters is not None:
self.statistics.format_metric(
partial(self._get_status_of_selector, 'php'),
'str',
'selector_php_status',
'Can\'t get status of PHP selector',
)
self.statistics.format_metric(
partial(self._get_list_versions_of_interperters, php_interpreters, INSTALLED),
'str_list',
'selector_php_versions_installed',
'Can\'t get list of versions of installed PHP interpreters',
log_exception=False,
)
self.statistics.format_metric(
partial(self._get_list_versions_of_interperters, php_interpreters, ENABLED),
'str_list',
'selector_php_versions_enabled',
'Can\'t get list of versions of enabled PHP interpreters',
log_exception=False,
)
self.statistics.format_metric(
get_default_php_version,
'str',
'selector_php_version_default',
'Can\'t get default version of PHP interpreter',
)
self.statistics.format_metric(
get_native_version_safe,
'str',
'selector_php_version_native',
'Can\'t get native version of PHP interpreter',
)
self.statistics.format_metric(
CloudlinuxSelectorLib('php').php_selector_is_enabled,
'int',
'selector_php_enabled_ui',
'Can\'t get state of UI of PHP selector',
)
self.statistics.format_metric(
get_mode_of_php_selector,
'str',
'selector_php_mode',
'Can\'t get mode of PHP selector',
)
php_usage_summary = self.statistics.run_safe_or_log_errors(
get_php_selector_usage,
'int',
'Can\'t get summary usage of PHP selector',
)
if 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.format_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',
'Can\'t get amount of domains which use PHP selector per PHP version',
log_exception=False,
)
self.statistics.format_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',
'Can\'t get amount of users which use PHP selector per PHP version',
log_exception=False,
)
else:
self.statistics['selector_php_status'] = NOT_INSTALLED
@staticmethod
def _get_average_apps_per_domain(total_apps, amount_of_apps_per_domain):
# type: (int, 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, amount_of_apps_per_user):
# type: (int, 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):
# type: (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):
# type: (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):
# type: (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):
# type: (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):
# type: (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):
# type: (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):
# type: (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):
# type: (List) -> Dict
"""
Get amount of applications per each versoin 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, state):
# type: (Dict, 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 iteritems(interpreters_stats) if stat[state]]
@staticmethod
def _get_list_of_applications(interpreter):
# type: (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)
else:
return list()
def _fill_selectors_statistics(self):
# type: () -> None
"""
Fill dict with statistics by varied statistics about ruby/nodejs/python selectors
"""
for selector in self.SELECTORS:
interpreters_stats = self.statistics.run_safe_or_log_errors(
partial(get_versions_statistics, selector),
'int',
'Can\'t get statistics about {} interpreters'.format(selector),
)
if interpreters_stats is None:
self.statistics['selector_' + selector + '_status'] = NOT_INSTALLED
continue
self.statistics.format_metric(
partial(self._get_status_of_selector, selector),
'str',
'selector_' + selector + '_status',
'Can\'t get status of {} selector'.format(selector),
)
self.statistics.format_metric(
partial(self._get_list_versions_of_interperters, interpreters_stats, INSTALLED),
'str_list',
'selector_' + selector + '_versions_installed',
'Can\'t get list of versions of installed {} interpreters'.format(selector),
log_exception=False,
)
self.statistics.format_metric(
partial(self._get_list_versions_of_interperters, interpreters_stats, ENABLED),
'str_list',
'selector_' + selector + '_versions_enabled',
'Can\'t get list of versions of enabled {} interpreters'.format(selector),
log_exception=False,
)
interpreter_apps = self.statistics.run_safe_or_log_errors(
partial(self._get_list_of_applications, selector),
'int',
'Can\'t get list of {} applications'.format(selector),
)
self.statistics.format_metric(
lambda: len(interpreter_apps), # pylint: disable=cell-var-from-loop
'int',
'selector_' + selector + '_applications_amount',
'Can\'t get total amount of {} applications'.format(selector),
log_exception=False,
)
self.statistics['selector_' + selector + '_used'] = \
self.statistics['selector_' + selector + '_applications_amount'] > 0
self.statistics.format_metric(
partial(self._get_amount_of_runned_apps, interpreter_apps),
'int',
'selector_' + selector + '_applications_running',
'Can\'t get amount of runned application for {} selector'.format(selector),
log_exception=False,
)
default_version_of_selector = self.statistics.run_safe_or_log_errors(
partial(get_default_version, selector),
'str',
'Can\'t get default version of {} selector'.format(selector),
)
if default_version_of_selector is not None:
self.statistics['selector_' + selector + '_default_version'] = default_version_of_selector
self.statistics.format_metric(
partial(self._get_max_apps_per_domain, interpreter_apps),
'int',
'selector_' + selector + '_max_applications_per_domain',
'Can\'t get max applications per domain for {} interpreter'.format(selector),
)
self.statistics.format_metric(
partial(self._get_max_apps_per_user, interpreter_apps),
'int',
'selector_' + selector + '_max_applications_per_user',
'Can\'t get max applications per user for {} interpreter'.format(selector),
)
self.statistics.format_metric(
partial(self._get_amount_of_users_with_apps, interpreter_apps),
'int',
'selector_' + selector + '_num_users_with_apps',
'Can\'t get amount of users with applications for {} interpeter'.format(selector),
)
self.statistics.format_metric(
partial(self._get_amount_of_domains_with_apps, interpreter_apps),
'int',
'selector_' + selector + '_num_domains_with_apps',
'Can\'t get amount of domains with applications for {} interpeter'.format(selector),
)
average_apps_per_domain = self.statistics.run_safe_or_log_errors(
partial(
self._get_average_apps_per_domain,
self.statistics['selector_' + selector + '_applications_amount'],
self.statistics['selector_' + selector + '_num_domains_with_apps'],
),
'int',
'Can\'t get average amount of applications per domain for {} interpreter'.format(selector),
)
if average_apps_per_domain is not None:
self.statistics['selector_' + selector + '_average_applications_per_domain'] = \
average_apps_per_domain
average_apps_per_user = self.statistics.run_safe_or_log_errors(
partial(
self._get_average_apps_per_user,
self.statistics['selector_' + selector + '_applications_amount'],
self.statistics['selector_' + selector + '_num_users_with_apps'],
),
'int',
'Can\'t get average amount of applications per user for {} interpreter'.format(selector),
)
if average_apps_per_user is not None:
self.statistics['selector_' + selector + '_average_applications_per_user'] = \
average_apps_per_user
self.statistics.format_metric(
partial(self._get_amount_of_apps_per_each_version_of_interpreters, interpreter_apps),
'int_dict',
'selector_' + selector + '_num_applications_by_interpreter',
'Can\'t get amount of applications per each version of {} interpreters'.format(selector),
)
@staticmethod
def _get_wizard_statistics():
# type: () -> 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 = dict()
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):
# type: () -> None
"""
Fill dict with statistics by varied statistics about cloudlinux-wizard
"""
wizard_statistics = self.statistics.run_safe_or_log_errors(
self._get_wizard_statistics,
'str',
'Can\'t get statistics about cloudlinux-wizard',
)
self.statistics.format_metric(
lambda: wizard_statistics['wizard_status'],
'str',
'wizard_status',
'Can\'t get status of cloudlinux-wizard',
log_exception=False,
)
for module in ALL_MODULES:
self.statistics.format_metric(
lambda: wizard_statistics['wizard_module_' + module], # pylint: disable=cell-var-from-loop
'str',
'wizard_module_' + module,
'Can\'t get statistics about module "{}" of cloudlinux-wizard'.format(module),
log_exception=False,
)
@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
"""
import psutil
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 os.path.exists(kmodlve_version_file):
with open(kmodlve_version_file) 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) 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) 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'
elif symlink_protection_enabled:
return 'symlinks_only'
elif hardlink_protection_enabled:
return 'hardlinks_only'
else:
return 'no'
def _fill_system_statistics(self):
self.statistics.format_metric(
cpu_count,
'str',
'cpu_amount',
'Can\'t get cpu amount',
log_exception=False
)
self.statistics.format_metric(
is_testing_enabled_repo,
'int',
'testing_repository_enabled',
'Can\'t get testing repository status',
log_exception=False
)
kernel_info = self._get_kernel_info()
self.statistics.format_metric(
lambda: kernel_info[0],
'str',
'kernel_release',
'Can\'t get kernel release info',
log_exception=False
)
self.statistics.format_metric(
lambda: kernel_info[1],
'str',
'installed_kmod_lve_version',
'Can\'t get installed kmod-lve version info',
log_exception=False
)
vendor_integration_info = self._get_integration_info()
self.statistics.format_metric(
lambda: vendor_integration_info['integration_scripts_used'],
'int',
'integration_scripts_used',
'Can\'t get integration_scripts_used info',
log_exception=False
)
self.statistics.format_metric(
lambda: vendor_integration_info['integration_scripts_specified'],
'str_list',
'integration_scripts_specified',
'Can\'t get integrations scripts specified info',
log_exception=False
)
memory_usage = self._get_memory_used()
self.statistics.format_metric(
lambda: memory_usage[0],
'float',
'memory_total_mb',
'Can\'t get total memory info',
log_exception=False
)
self.statistics.format_metric(
lambda: memory_usage[1],
'float',
'memory_used_mb',
'Can\'t get used memory info',
log_exception=False
)
self.statistics.format_metric(
self._get_lve_extensions_packages_amount,
'int',
'lve_extension_packages_amount',
'Can\'t get lve extension usage info',
log_exception=False
)
self.statistics.format_metric(
is_email_notification_enabled,
'int',
'cldiag_cron_check_enabled',
'Can\'t get is cldiag cron check enabled',
log_exception=True
)
self.statistics.format_metric(
self._get_total_domains_amount,
'int',
'domains_total',
'Can\'t get domains amount',
log_exception=True
)
self.statistics.format_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',
log_exception=True
)
self.statistics.format_metric(
self._is_link_traversal_protection_enabled,
'str',
'link_traversal_protection_enabled',
'Can\'t get link traversal protection enabled parameter',
log_exception=True
)
self.statistics.format_metric(
get_virt_type,
'str',
'virt_type',
'Can\'t get the virtualization type',
log_exception=True
)
self.statistics.format_metric(
getfqdn,
'str',
'hostname',
'Can\'t get the hostname',
log_exception=True
)
def _fill_dict_with_rpm_packages_statistics(self):
self.statistics.format_metric(
lambda: self.system_id or UNKNOWN_RHN_ID,
'str',
'system_id',
'Can\'t get system ID',
)
self.statistics.format_metric(
get_cl_version,
'str',
'os_version',
'Can\'t get version of OS'
)
self.statistics.format_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
"""
# Get all mounting parameters including hidepid
parameters = [
# 2 mounting params: fs.protected_symlinks_create and
# fs.protected_hardlinks_create are gathered in
# _is_link_traversal_protection_enabled method
'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
'kernel.memcg_oom_disable', # Only for CL7+
'fs.process_symlinks_by_task' # Only for CPanel on CL7+
]
for p in parameters:
self.statistics.format_metric(
partial(self._get_proc_param, p),
'int',
# It is forbidden to use '.' in the field name
p.replace('.', '_'),
'Can\'t get {0}'.format(p),
log_exception=True,
)
self.statistics.format_metric(
get_hidepid_typing_from_mounts,
'int',
'hidepid',
'Can\'t get hidepid value',
log_exception=True,
)
def _fill_centralized_management_statistics(self):
"""
Filling stats centralized management
- jwt token metrics (cl_plus existence and client_id)
- centralized management existence
"""
# Get metrics from jwt token and process them
data = get_client_data_from_jwt_token()
self.statistics.format_metric(
lambda: None if data is None
else data.get('cl_plus', None),
'int',
'cl_plus',
'Can\'t get cl_plus information'
)
self.statistics.format_metric(
lambda: None if data is None
else data.get('client_id', None),
'int',
'client_id',
'Can\'t get client_id value'
)
# Get information about forcible disabling of CM
self.statistics.format_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.format_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 = dict()
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.format_metric(
partial(cpu_metrics.get, metric, None),
metric_type,
metric,
f'Can\'t parse {metric} metric for all cores',
)
# Get memory metrics
self.statistics.format_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, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
return p.returncode, p.stdout
def _prepare_statistics(self):
self.statistics.format_metric(
# security_token is property
lambda: self.security_token,
'str',
'security_token',
'Can\'t get or generate security token',
)
self.statistics.format_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'] = '{}-{}'.format(
LVEMANAGER_PKG_VERSION,
LVEMANAGER_PKG_RELEASE,
)
else:
self.statistics['lvemanager_version'] = None
self.statistics.format_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):
self._fill_mysql_governor_statistics()
self._fill_control_panel_statistics()
self._fill_cagefs_statistics()
self._fill_resellers_statistics()
self._fill_limits_statistics()
self._fill_lsapi_statistics()
self._fill_php_selector_statistics()
self._fill_selectors_statistics()
self._fill_wizard_statistics()
self._fill_system_statistics()
self._fill_proc_params_statistics()
self._fill_centralized_management_statistics()
self._fill_hardware_statistics()
self._fill_act_cagefs_disabled_statistics()
@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)
@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'
elif unit.endswith('mhz'):
return 'mhz'
elif unit.endswith('ghz'):
return 'ghz'
elif unit.isdigit():
return 'old_cpu_format'
else:
return 'unknown: %s' % 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) # pylint: disable=round-builtin
def get_users_amount_per_plan(self, xml_cfg_provider):
# type: (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, xml_cfg_provider):
# type: (int, 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='success', data=None, exit_code=0, is_statistic_enabled=None):
# type: (str, object, int, Optional[bool]) -> None
"""
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, error_code=1):
# type: (Dict, int) -> 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():
# type: () -> 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)
return json.loads(p.stdout)
def _get_lvpmap():
lvpmap = LvpMap()
lvpmap.name_map.link_xml_node()
return lvpmap
Zerion Mini Shell 1.0