Sindbad~EG File Manager
# 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 datetime
import platform
import re
from functools import lru_cache, partial
from collections import Counter, defaultdict
from itertools import groupby
from future.utils import iteritems
from operator import attrgetter
from multiprocessing import cpu_count
from socket import getfqdn
from pathlib import Path
import requests
import cldetectlib as detect
import lvectllib
from clcommon import cpapi
from clcommon.const import Feature
from clcommon.lib import MySQLGovernor
from clcommon.lib.cledition import (
is_cl_solo_edition,
is_cl_shared_edition,
CLEditions,
is_ubuntu,
is_secureboot_enabled
)
from clcommon.cpapi import is_panel_feature_supported
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,
is_nginx_running,
)
from clcommon.clwpos_lib import find_wp_paths, get_wp_cache_plugin, get_wp_paths_with_enabled_module
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,
is_statistic_rpm_enabled,
set_statistic_collection_enabled,
set_statistic_rpm_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,
is_active_cloudlinux_license,
)
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 result 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
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(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://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 = dict()
self._security_token = None
self.statistics = 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):
# type: () -> 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):
# 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']
# 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. "
"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
# 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)
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 '
'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
subprocess.run('/usr/sbin/cloudlinux-summary --send --json &> /dev/null &',
shell=True, executable='/bin/bash')
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_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)
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(
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):
exit(0)
elif not is_panel_feature_supported(Feature.LVE):
rc = self._run_self_by_nice(argv)
exit(rc)
else:
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_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)
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.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_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():
# 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) -> 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',
)
self.statistics.format_metric(
is_nginx_running,
'int',
'nginx_running',
'Can\'t get control panel nginx'
)
self.statistics.format_metric(
lambda: len(cpapi.cpusers()),
'int',
'users_total',
'Can\'t get amount of users'
)
if detect.getCPName() == cpapi.PLESK_NAME:
self.statistics.format_metric(
self._is_installed_via_plesk_ext,
'int',
'installed_via_plesk_ext',
'Can\'t determine if CL was installed from Plesk Extension'
)
@staticmethod
def _is_installed_via_plesk_ext():
"""
cldeploy-plesk-ext.log is always created by cldeploy-precheck when it downloads cldeploy script,
if it exists it means there was an attempt to launch a cldeploy from Plesk Extension.
Since this function is in package lve-utils, which depends from lve, it means that current os is CloudLinux,
therefore 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):
# 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.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_wmt_settings_statistics(self):
self.statistics.format_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.format_metric(
lambda: len(all_wp_paths),
'int',
'wordpress_installed_total',
'Can\'t get total count of installed wordpress'
)
self.statistics.format_metric(
lambda: object_cache_plugins,
'int_dict',
'wordpress_object_cache_plugins',
'Can\'t get WP object cache plugins info'
)
self.statistics.format_metric(
lambda: advanced_cache_plugins,
'int_dict',
'wordpress_advanced_cache_plugins',
'Can\'t get WP advanced cache plugins info'
)
self.statistics.format_metric(
lambda: sum(object_cache_plugins.values()),
'int',
'wordpress_object_cache_plugins_total',
'Can\'t get WP object cache total plugins info'
)
self.statistics.format_metric(
lambda: sum(advanced_cache_plugins.values()),
'int',
'wordpress_advanced_cache_plugins_total',
'Can\'t get WP advanced cache total plugins info'
)
self.statistics.format_metric(
lambda: wp_php_versions,
'int_dict',
'wordpress_php_versions',
'Can\'t get WP php version info'
)
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 _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': [],
'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 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):
# type: () -> None
"""
Fill dict with SSA statistics
"""
ssa_stats = self.statistics.run_safe_or_log_errors(
self.get_ssa_stats,
'str',
'Can\'t get SSA statistics',
)
if ssa_stats is not None:
self.statistics.format_metric(
lambda: ssa_stats['config'],
'str_dict',
'ssa_config',
'Can\'t get SSA config'
)
self.statistics.format_metric(
lambda: ssa_stats['version'],
'str',
'ssa_version',
'Can\'t get version of SSA'
)
self.statistics.format_metric(
lambda: ssa_stats['status'],
'str',
'ssa_status',
'Can\'t get state of SSA'
)
self.statistics.format_metric(
lambda: ssa_stats['agent_status'],
'str',
'ssa_agent_status',
'Can\'t get state of SSA agent'
)
self.statistics.format_metric(
lambda: ssa_stats['autotracing']['status'],
'str',
'autotracing_status',
'Can\'t get state of SSA Autotracing'
)
self.statistics.format_metric(
lambda: ssa_stats['autotracing']['disabled_users_quantity'],
'int',
'autotracing_disabled_users_quantity',
'Can\'t get state of SSA Autotracing disabled users'
)
self.statistics.format_metric(
lambda: ssa_stats['autotracing']['rules_version'],
'str',
'autotracing_rules_version',
'Can\'t get SSA Autotracing rules version'
)
self.statistics.format_metric(
lambda: ssa_stats['autotracing']['urls_processed'],
'int',
'autotracing_urls_processed',
'Can\'t get SSA Autotracing processed URLs count'
)
self.statistics.format_metric(
lambda: ssa_stats['autotracing']['urls_selected'],
'int',
'autotracing_urls_selected',
'Can\'t get SSA Autotracing selected URLs count'
)
self.statistics.format_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):
# 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 is_panel_feature_supported(Feature.LVE) and 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'
@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
elif os.path.exists('/etc/cloudlinux-qcow2-install'):
return qcow2
else:
return iso
def _fill_system_statistics(self):
kernel_info = self._get_kernel_info()
if is_panel_feature_supported(Feature.LVE):
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(
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(
lambda: kernel_info[1],
'str',
'installed_kmod_lve_version',
'Can\'t get installed kmod-lve version info',
log_exception=False
)
if not is_cl_solo_edition(skip_jwt_check=True):
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(
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
)
self.statistics.format_metric(
self._get_platform,
'str',
'base_distro',
'Can\'t detect platform for CLOS'
)
self.statistics.format_metric(
self._detect_secureboot,
'str',
'secureboot_status',
'Can\'t detect whether secure boot is enabled'
)
self.statistics.format_metric(
lambda: kernel_info[0],
'str',
'kernel_release',
'Can\'t get kernel release 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_total_domains_amount,
'int',
'domains_total',
'Can\'t get domains amount',
log_exception=True
)
if is_panel_feature_supported(Feature.LVE):
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(
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
)
self.statistics.format_metric(
self._get_cl_installation_source,
'str',
'cloudlinux_installation_source',
'Can\'t get installation source',
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
"""
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.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_kmodlve_params_statistics(self):
"""
Fill dict with statistics about kmodlve parameters usage
"""
parameters = (
'lve_setuid_enter',
)
for p in parameters:
self.statistics.format_metric(
partial(self._get_kmodlve_param_value, p),
'int',
f'kmodlve_{p}',
f'Can\'t get "{p}" kmodlve parameter value',
log_exception=True,
)
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.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'
)
self.statistics.format_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.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, executable='/bin/bash',
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
return p.returncode, p.stdout
def _prepare_cl_normal_statistics(self):
if is_panel_feature_supported(Feature.LVE):
self.statistics.format_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.format_metric(
partial(CLEditions.get_cl_edition, verify_exp=False),
'str',
'cl_edition',
'Can\'t get CloudLinux edition'
)
self.statistics.format_metric(
self._is_solo_marker_exists,
'int',
'is_solo_meta_file_present',
'Can\'t detect solo meta file presence'
)
self.statistics.format_metric(
self._is_admin_marker_exists,
'int',
'is_admin_meta_file_present',
'Can\'t detect admin meta file presence'
)
self.statistics.format_metric(
self._is_container_marker_exists,
'int',
'is_container_meta_file_present',
'Can\'t detect container meta file presence'
)
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',
)
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._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'
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()
f = open(LOG_FOLDER + file, "r")
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]
def _fill_leapp_statistics(self):
# type: () -> None
"""
Fill dict with Leapp upgrade statistics
"""
LEAPP_STATUS = 'leapp_upgrade_status'
leapp_status = self.statistics.run_safe_or_log_errors(
self.get_leapp_stats,
'int',
'Can\'t get Leapp statistics',
)
self.statistics[LEAPP_STATUS] = leapp_status
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):
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_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_leapp_statistics()
def _fill_wpos_statistics(self):
wpos_stats = self.statistics.run_safe_or_log_errors(
self.get_wpos_stats,
'str',
'Can\'t get AccelerateWP statistics',
)
if isinstance(wpos_stats, dict) and wpos_stats.get('result') == 'success':
if wpos_stats.get('features_visible_by_default') is not None:
self.statistics.format_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.format_metric(
lambda: wpos_stats['features_allowed_by_default'],
"str_list",
"wpos_features_allowed_by_default",
"Can't get statistic of using AccelerateWP."
)
self.statistics.format_metric(
lambda: wpos_stats['enabled_sites']['total'],
"int",
"sites_count_with_enabled_wpos",
"Can't get statistic of using AccelerateWP."
)
self.statistics.format_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.format_metric(
lambda: wpos_stats['visible_users']['total'],
"int",
"users_count_with_visible_wpos",
"Can't get statistic of using AccelerateWP."
)
for module in wpos_stats['enabled_sites']:
if module == 'total':
continue
self.statistics.format_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['allowed_users']:
if module == 'total':
continue
self.statistics.format_metric(
lambda: wpos_stats['allowed_users'][module],
"int",
"users_count_with_allowed_" + module,
"Can't get statistic of using AccelerateWP."
)
for module in wpos_stats.get('visible_users', []):
if module == 'total':
continue
self.statistics.format_metric(
lambda: wpos_stats['visible_users'][module],
"int",
"users_count_with_visible_" + module,
"Can't get statistic of using AccelerateWP."
)
self.statistics.format_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.format_metric(
lambda: wpos_stats.get('is_accelerate_wp_icon_enabled'),
"int",
"is_accelerate_wp_icon_enabled",
"Can't get statistic of AccelerateWP icon status."
)
@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)
@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)
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():
# type: () -> 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)
return json.loads(p.stdout)
@staticmethod
def get_leapp_stats():
# type: () -> int
"""
Return statistics from Leapp upgrade logs
"""
leapp_logfile = '/var/log/leapp/leapp-upgrade.log'
# This means upgrade wasn't attempted.
leapp_status = 0
# If the log file exists, `leapp upgrade` was run on the machine.
if os.path.isfile(leapp_logfile):
grep_str = r'An upgrade inhibitor detected\|Workflow interrupted due to the FailPhase'
grep_res = next(grep(grep_str, leapp_logfile, fixed_string=True, multiple_search=False), None)
# No halting messages found, uprgade is (provisionally) successful.
if not grep_res:
leapp_status = 2
else:
leapp_status = 1
return leapp_status
@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().strip()
if param_value == 'Y':
return 1
elif param_value == 'N':
return 0
raise Exception(f'Unable to interpret "{param_name}" kmodlve '
f'parameter value: "{param_value}"')
@staticmethod
def get_smart_advice_stats():
# type: () -> 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)
return json.loads(p.stdout).get('data')
def _get_smart_advice_statistics(self):
# type: () -> Dict
"""
Construct dict with Smart Advice statistics
"""
result = dict(
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):
sa_stats = self.statistics.run_safe_or_log_errors(
self.get_smart_advice_stats,
'str_dict',
'Can\'t get Smart Advice statistics',
)
if sa_stats is not None:
try:
return dict(
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_wpos_stats():
# type: () -> 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)
return json.loads(p.stdout)
def _get_lvpmap():
lvpmap = LvpMap()
lvpmap.name_map.link_xml_node()
return lvpmap
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists