# Copyright (c) Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2018 All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import json import traceback import subprocess import configparser import exec_command from lve_diagnostic import get_cp from dashboard_malfunctions import criu_settings_malfunctions from dashboard_malfunctions import lsapi_settings_malfunctions from dashboard_malfunctions import liblsapi_malfunctions from stat_utils import cpanel_whmapi, plesk_bin_php_handler, dump_loaded_modules, dump_lsapi, query_strings from stat_utils import get_da_domains, get_da_php_options, read_da_config, liblsapi_path, pretty_version_keys, count_domains, StatUtilsException import selector_usage_lib class ModLsapiStatException(Exception): pass def get(as_json=False): """ Return statistics data :param as_json: return json string if True, dict otherwise :return: statistics data dict( `controlPanel`: EA3/EA4/Plesk/DirectAdmin/Unknown CP, `criu`: dict( `status`: running/stopped, `version`: version ), `domainStat`: dict( `version`: `domains_num`, ... ) or dict(`error`: description) if some error occurred, `lsapiConf`: dict( lsapi_with_connection_pool: on/off, lsapi_criu: on/off ) ) or its json-packed version """ error = None stats = {} control_panel = get_cp() try: if control_panel.name == 'cPanel': if os.path.exists('/etc/cpanel/ea4/is_ea4'): stats['domainStat'] = get_cpanel_ea4_stat() else: error = 'mod_lsapi domains stat is currently unsupported for EA3' elif control_panel.name == 'Plesk': stats['domainStat'] = get_plesk_stat() elif control_panel.name == 'DirectAdmin': stats['domainStat'] = get_da_stat() else: error = 'mod_lsapi domains stat is currently unsupported for {0}'.format(control_panel.name) except ModLsapiStatException as e: error = str(e) if error is not None: stats['domainStat'] = dict() stats['domainStatError'] = error stats['controlPanel'] = str(control_panel) stats.update(get_lsapi_conf()) stats.update(get_crui_stat()) stats.update({'totalDomain': sum(stats['domainStat'].values())}) analyze_malfunctions(stats) if as_json: return json.dumps(stats, sort_keys=True) else: return stats def get_cpanel_ea4_stat(lsapi_only=True, with_selector=True): """ Collect mod_lsapi statistics for cPanel EA4 through WHM API :param lsapi_only: return only lsapi domains statistics if True, or full statistics if False :param with_selector: take into account the statistics of php selector :return: if lsapi_only return lsapi domains per version stat dict( `version`: `domains_num`, ... ) else return full statistics per handler stat dict( `handler`: {`version`: `domains_num`, ... } ... ) """ domains_per_version = dict() # to store `version`: `domains_list` handlers_stat = dict() # to store `handler`: {`version`: `domains_num`, ...} domain_users = dict() # to store `domain`: `user` correspondence try: # get all vhosts along with versions vhosts_data = cpanel_whmapi('php_get_vhost_versions').get('versions') for vhost in vhosts_data: domains_per_version.setdefault(vhost.get('version'), set()).add(vhost.get('vhost')) domain_users[vhost.get('vhost')] = vhost.get('account') # get handlers for versions handlers_data = cpanel_whmapi('php_get_handlers').get('version_handlers') version_handlers = dict([(h.get('version'), h.get('current_handler')) for h in handlers_data]) all_versions = list(version_handlers.keys()) # map {version: domains_list} to handlers, domains count in place for ver, handler in version_handlers.items(): handlers_stat.setdefault(handler, dict()).update({ver: domains_per_version.get(ver, set())}) # reinspect handlers stat against selector if with_selector: s_checked_version_handlers = selector_usage_lib.ea4_selector_check(domains_per_version, domain_users, handlers_stat) handlers_stat = s_checked_version_handlers # update structure with versions, which are not used by one handler, e.g. `ver: 0 domains` for h, v in handlers_stat.items(): v.update(dict.fromkeys(set(all_versions).difference(list(v.keys())), set())) except (KeyError, TypeError, StatUtilsException): raise ModLsapiStatException(''.join(traceback.format_exc().split('\n'))) # return only number of domains return count_domains(handlers_stat, all_versions, lsapi_only) def get_plesk_stat(lsapi_only=True, with_selector=True): """ Collect mod_lsapi statistics for Plesk Collects lsapi domains ONLY :param lsapi_only: return only lsapi domains statistics if True, or full statistics if False :param with_selector: take into account the statistics of php selector :return: if lsapi_only return lsapi domains per version dict( `version`: `domains_num` ... ) else return stats with handler dict( `lsapi`: {`version`: `domains_num`, ... } ) """ handler_tmpl = 'alt-php{v}' custom_version = 'alt-php56' domain_version_stat = dict() try: all_handlers = plesk_bin_php_handler('list') # on Plesk mod_lsapi is used only through handlers x-httpd-lsphp-*, which are added by --setup lsphp_handlers = [h for h in all_handlers if 'lsphp' in h.get('id')] for handler in lsphp_handlers: handler_id = handler.get('id') php_version = ''.join(handler.get('fullVersion').split('.')[:-1]) domains = set([d.get('domain') for d in plesk_bin_php_handler('get-usage', id=handler_id)]) # x-httpd-lsphp-custom domains are to be checked withon selector if 'custom' in handler_id: version_id = 'custom' custom_version = handler_tmpl.format(v=php_version) else: version_id = handler_tmpl.format(v=php_version) domain_version_stat[version_id] = domains # PLACE SELECTOR CHECK HERE (should be done for custom handler) if with_selector: domain_version_stat = selector_usage_lib.plesk_selector_check(domain_version_stat, custom_version) except (KeyError, TypeError, AttributeError, StatUtilsException, selector_usage_lib.SelectorStatException): raise ModLsapiStatException(''.join(traceback.format_exc().split('\n'))) # return only number of domains result_stat = { 'lsapi': {k: len(v) for k, v in domain_version_stat.items()} } return result_stat['lsapi'] if lsapi_only else result_stat def get_da_stat(lsapi_only=True, with_selector=True): """ Collect lsapi domains statistics fro DirectAdmin :param lsapi_only: return only lsapi domains statistics if True, or full statistics if False :param with_selector: take into account the statistics of php selector :return: if lsapi_only return lsapi domains per version stat dict( `version`: `domains_num`, ... ) else return full statistics per handler stat dict( `handler`: {`version`: `domains_num`, ... } ... ) """ domain_conf_path = '/usr/local/directadmin/data/users/{user}/domains/{domain}.conf' handler_stat = dict() try: # get php settings from option.conf php_options = get_da_php_options() php1_ver = php_options[1]['version'] php2_ver = php_options[2]['version'] php1_handler = php_options[1]['handler'] php2_handler = php_options[2]['handler'] php1_label = pretty_version_keys(php1_ver) php2_label = pretty_version_keys(php2_ver) # get user: domains correspondence user_domains = get_da_domains() joined = set() [joined.update(v) for v in user_domains.values()] # analyze options.conf settings for versions if php2_ver == 'no': # no secondary php set, assume all domains use primary version_stat = {php1_label: joined} handler_stat[php1_handler] = {php1_label: joined} elif php1_ver == 'no': # no primary php set, assume all domains use secondary version_stat = {php2_label: joined} handler_stat[php2_handler] = {php2_label: joined} else: version_stat = {php1_label: set(), php2_label: set()} # if both php releases in options.conf are set, that means that DA PHP selector is enabled for user, domains in user_domains.items(): # find php release settings for each domain for domain in domains: config_path = domain_conf_path.format(user=user, domain=domain) try: # try to find which version domain uses as primary conf_parser, global_section = read_da_config(config_path) php_setting = conf_parser.getint(global_section, 'php1_select') version = pretty_version_keys(php_options[php_setting]['version']) version_stat.get(version).add(domain) except configparser.NoOptionError: # means that domain do not use DA PHP selector and uses primary php version from options.conf version_stat.get(php1_label).add(domain) # if both php releases in options.conf are set, that means that DA PHP selector is enabled # create per-handler statistics if php1_handler == php2_handler: handler_stat[php1_handler] = {php1_label: version_stat[php1_label], php2_label: version_stat[php2_label]} else: handler_stat[php1_handler] = {php1_label: version_stat[php1_label], php2_label: set()} handler_stat[php2_handler] = {php2_label: version_stat[php2_label], php1_label: set()} if with_selector: selector_updated = selector_usage_lib.da_selector_check(version_stat.get(php1_label), user_domains, php1_label) handler_stat.get(php1_handler).update(selector_updated) except (KeyError, TypeError, AttributeError, StatUtilsException): raise ModLsapiStatException(''.join(traceback.format_exc().split('\n'))) # return only number of domains return count_domains(handler_stat, [php1_label, php2_label], lsapi_only) def get_crui_stat(): """ Get criu service info :return: dict( `criu`: dict( `status`: running/stopped, `version`: version ) ) """ criu_version = ''.join(exec_command.exec_command('/usr/sbin/criu -V')) try: subprocess.check_call(['/sbin/service', 'criu', 'status'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) criu_service_status = 'running' except subprocess.CalledProcessError: criu_service_status = 'stopped' return { 'criu': { 'status': criu_service_status, 'version': criu_version.split(' ')[1] if criu_version else 'not installed' } } def get_lsapi_conf(): """ Retrieve lsapi configuration: - crui - connection pool and versions of module and library :return: dict( lsapiConf: dict( lsapi_with_connection_pool: on/off, lsapi_criu: on/off ), modVersion: version, libVersion: version ) """ # Plesk carries upstream apache 2.4.6, which says # `Passing arguments to httpd using apachectl is no longer supported.` # have to query with httpd instead of apachectl apache_conf = dump_lsapi() apache_modules = dump_loaded_modules() mod_status = apache_modules.get('lsapi_module', None) return { 'lsapiConf': { 'lsapi_criu': apache_conf.get('lsapi_criu', 'off'), 'lsapi_with_connection_pool': apache_conf.get('lsapi_with_connection_pool', 'off') }, 'modVersion': apache_conf.get('version', None), 'libVersion': query_strings(liblsapi_path(), 'liblsapi_version'), 'modStatus': 'disabled' if not mod_status else 'enabled' } def analyze_malfunctions(stats_dict): """ Detect configuration malfunctions and update resulting statistics dict accordingly For now only criu malfunctions are presented See malfunctions in dashboard_malfunctions.py module :param stats_dict: resulting statistics dict """ malfunctions = list() def add_malfunction(malfunc_dict, key): try: # try to detect one malfunction malfunctions.append(malfunc_dict[key]) except KeyError: # no malfunction found pass criu_settings = '{opt}_{serv}'.format(opt=stats_dict['lsapiConf']['lsapi_criu'], serv=stats_dict['criu']['status']) lsapi_settings = stats_dict['modStatus'] # for further extension of similar malfunctions: # malfuncs = tuple of malfunctions # keys = tuple of according keys # for malfunc, k in zip(malfuncs, keys): # add_malfunction(malfunc, k) add_malfunction(liblsapi_malfunctions, stats_dict['libVersion']) add_malfunction(lsapi_settings_malfunctions, lsapi_settings) add_malfunction(criu_settings_malfunctions, criu_settings) # no need in `malfunctions` field if there are no malfunctions if not malfunctions: return else: stats_dict.update({'malfunctions': malfunctions})