Edit file File name : dynamicuictl.py Content :#!/opt/cloudlinux/venv/bin/python3 -bb # -*- 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 print_function from __future__ import division from __future__ import absolute_import import getopt import glob import os import re import subprocess import sys from typing import Dict # NOQA import simplejson as json from packaging.version import Version from clselect.utils import file_writelines, file_readlines, file_write, \ file_read, grep from clcommon.ui_config import UIConfig NAME = os.path.basename(sys.argv[0]) THEMES_LOCATION = '/usr/local/cpanel/base/frontend' sync_association = ( # None is given to hide old resource usage # cPane config dynamicui 0 - not inverse cPane config value # key name key name 1 - inverse cPane config value # 1 - show # 0 - hide ('hideRubyApp', 'lverubysel', 1), ('hidePythonApp', 'lvepythonsel', 1), ('hideNodeJsApp', 'lvenodejssel', 1), ('hideLVEUserStat', 'lveresusage', 1), ('hidePhpApp', 'lvephpsel', 1), ('hideXrayApp', 'lvexray', 1), ('hideAccelerateWPApp', 'lvewpos', 1), (None, 'enduserlve', 1), # for x3 theme (None, 'cpu_and_concurrent_connection_usage', 1) # for paper lantern theme ) def usage(): print('Usage: %s [-h] [--show dynamicui_key1[,dynamicui_key2..]] [--hide dynamicui_key1[,dynamicui_key2..]]' % NAME) print('[--sync-conf conf_key1[,conf_key2..]] [--no-rebuild-sprites]') print('Options:') print(' -h | --help :print this message') print(' -v | --verbose :detail output actions') print(' --fix-dynamicui :fix new json-style dynamicui.conf') print(' --sync-conf :sync with dynamicui configs') print(' supported keys: all, %s' % ' '.join([k_[0] for k_ in sync_association if isinstance(k_[0], str)])) print(' --show dynamicui_key1[,dynamicui_key2..] \n' ' :show app[s] in cPanel web-interface for user') print(' --hide dynamicui_key1[,dynamicui_key2..] \n' ' :hide app[s] in cPanel web-interface for user') print(' --path PATH :path to specific dynamicui config file') print(' --no-rebuild-sprites :don\'t rebuild sprites after patching dynamicui configs') print(' Association:') print(' conf_key\t\tdynamicui_key') for c1, c2, _ in sync_association: print(' %s\t%s' % (c1, c2)) print('') print(' Examples:') print(' %s --hide=lverubysel --hide=lvepythonsel --show=enduserlve' % NAME) print(' %s --hide=lverubysel,lvepythonsel --show=enduserlve' % NAME) print(' %s --sync-conf all --no-rebuild-sprites' % NAME) print(' %s --sync-conf hideRubyApp, hidePythonApp' % NAME) # add support function "any" for python 2.4 if 'any' not in globals(): def any(iterable): """ backported buildin function "any" for python 2.4 >>> any([False, False, False]) False >>> any([True, False, False]) True :param iterable: :return boll: """ for element in iterable: if element: return True return False def parse_json_with_comments(text, **kwargs): """ Parse json file with comments """ regex_str = r'\s*(#|\/{2}).*$' REGEX_COMMENT = re.compile(regex_str) REGEX_LINE_COMMENT = re.compile(r"^" + regex_str, re.IGNORECASE) REGEX_INLINE = re.compile(r'(:?(?:\s)*([A-Za-z\d\.{}]*)|((?<=\").*\"),?)(?:\s)*(((#|(\/{2})).*)|)$') lines = text.split('\n') for index, line in enumerate(lines): if REGEX_COMMENT.search(line): if REGEX_LINE_COMMENT.search(line): lines[index] = "" elif REGEX_INLINE.search(line): lines[index] = REGEX_INLINE.sub(r'\1', line) # remove trailing comma output = "\n".join(lines) output = re.sub(r",[ \t\r\n]*}", "}", output) output = re.sub(r",[ \t\r\n]*\]", "]", output) try: return json.loads(output, **kwargs) except ValueError: return None def fix_json_format(path): """ Fix json format conf file. Remove remove old-style lines :param: `str` path: path to config file """ if not os.path.isfile(path): return None # skip files with old-style config values if grep('^description=>', path): return False # clean old format lines from file try: f = open(path) output_lines = [] for line in f: line = line.rstrip("\n") directive = DynUiLine(line) if not directive.get_string(): # don`t add old-style line to file output_lines.append(line) except IOError: return None else: f.close() # update conf file try: f = open(path, "w") f.write("\n".join(output_lines)) except IOError: return None else: f.close() return 0 class DynUiLine(object): """ """ key_types = {'skipobj': int} def __init__(self, line): """ Constructor """ self.line = line.strip() self.line_parsed_ = self.pars_directives() def pars_directives(self): """ Parse line "self.line" and return parsed data :return list: return parsed structure; for future manipulation """ line_parsed_ = list() line = self.line for l_ in line.split(','): if l_.count('=>') != 1: continue key_, val_ = list(map(str.strip, l_.split('=>', 1))) # parse and clear line like "key=>val" type_converter = self.key_types.get(key_) if type_converter: try: val_ = type_converter(val_) except ValueError: pass line_parsed_.append([key_, val_]) return line_parsed_ def has_dir(self, key, val=None): """ Check if directive present :param str key: directive name :param str|None val: directive value; if value is None value not check :return bool: return True if directive present [and value] present """ for key_, val_ in self.line_parsed_: if key_ == key: if (val is None) or (val == val_): return True return False def del_dir(self, key): """ Delete directive :param str key: directive name need delete :return bool: return True if directive was deleted """ was_deleted = False for index_, (key_, _) in enumerate(self.line_parsed_): if key_ == key: del self.line_parsed_[index_] was_deleted = True return was_deleted def set_dir(self, key, val): """ Change or create directive :param str key: directive name :param str|int val: directive value :return bool: return True if change or create directive is success; False if not need change """ for index_, (key_, val_) in enumerate(self.line_parsed_): if key == key_: if val != val_: self.line_parsed_[index_][1] = val return True else: return False # add directive if not present self.line_parsed_.append([key, val]) return True def get_string(self): """ Generate and return raw string after manipulations :return str: return line after manipulations """ return ','.join(['=>'.join(map(str, key_val_)) for key_val_ in self.line_parsed_]) def hide_object(self, file_=None): """ Modify directives to hide object :param str|None file_: file name that need hide :return bool: return true if state was changed """ if self.has_dir('file', file_): # append directive "skipobj=>1" to line if len(self.line_parsed_) >= 2: return self.set_dir('skipobj', 1) return False def show_object(self, file_=None): """ Modify directives to show object :param str|None file_: file name that need hide :return bool: return true if state was changed """ if self.has_dir('file', file_): if len(self.line_parsed_) > 2: # detect directive "skipobj=>1" or "skipobj=>0" and delete them return self.del_dir('skipobj') elif len(self.line_parsed_) == 2: if self.has_dir('skipobj'): # detect line "file=>some_name,skipobj=>1" and delete all line self.line_parsed_ = list() return True return False def patch_dynamicui(path, app_show=None, app_hide=None, verbose=False): """ patching dynamicui.conf for add or delete direcitve 'skipobj=>1' in line :param str path: path to dynamicui config file :param list app_show: list apps to show in web-interface; delete directive 'skipobj=>1' in line :param list app_hide: list apps to hide in web-interface; add directive 'skipobj=>1' in line :return: `dict` {app: True,...} | None """ app_show = app_show or list() app_hide = app_hide or list() file_was_patched = False patched_apps = set() dynamicui_lines = file_readlines(path) for line_index, line in enumerate(dynamicui_lines): if line.lstrip('\n').startswith('#'): # ignoring line with comments continue line = line.rstrip('\n') directives = DynUiLine(line) if not directives.line: continue line_was_patched = False for app_name in app_show: if directives.has_dir('file', app_name): patched_apps.add(app_name) if directives.show_object(app_name): line_was_patched = True break for app_name in app_hide: if directives.has_dir('file', app_name): patched_apps.add(app_name) if directives.hide_object(app_name): line_was_patched = True break # line_was_patched = any(map(directives.show_object, app_show) + map(directives.hide_object, app_hide)) if line_was_patched: dynamicui_lines[line_index] = directives.get_string() + '\n' file_was_patched = True if file_was_patched: file_writelines(path, dynamicui_lines, 'w') if verbose: print('%s: file %s was patched' % (NAME, path)) else: if verbose: print('%s: file %s was skiped, not need to patch' % (NAME, path)) return patched_apps def patch_json_dynamicui(path, app_show, app_hide, verbose=False): """ Patch dynamicui.conf file in json format :param: `str` path: path to dynamicui config file :param: `list` app_show: list apps to show in web-interface; delete key 'skipobj' :param: `list` app_hide: list apps to hide in web-interface; add key 'skipobj: 1' :return: `dict` {app: True,...} | None """ # clean json format file for valid parse json conf = parse_json_with_comments(file_read(path)) if conf is None: if verbose: print('%s: file %s was skipped because contains invalid JSON format' % (NAME, path)) return None file_was_patched = False patched_apps = set() file_items = [] for item in conf: app_name = item.get("file") if app_name in app_show: item.pop("skipobj", None) file_was_patched = True patched_apps.add(app_name) elif app_name in app_hide: item["skipobj"] = 1 file_was_patched = True patched_apps.add(app_name) file_items.append(item) # add unexists items with skipobj key # non_patched_hide_app = set(app_hide).difference(patched_apps) # if non_patched_hide_app: # file_was_patched = True # for app_name in non_patched_hide_app: # file_items.append({"name": app_name, "file": app_name, "skipobj": 1}) # patched_apps.add(app_name) if file_was_patched: # https://stackoverflow.com/questions/18337407/saving-utf-8-texts-in-json-dumps-as-utf8-not-as-u-escape-sequence file_write(path, json.dumps(file_items, indent=4, ensure_ascii=False).encode('utf8'), mode='wb') if verbose: print('%s: file %s was patched' % (NAME, path)) else: if verbose: print('%s: file %s was skiped, not need to patch' % (NAME, path)) return patched_apps def patch_dynamicui_wrapper(path, app_show=None, app_hide=None, verbose=False, is_json_format=False): """ Wrapper for patch dynamicui config file. Catch base exceptions and print base messages """ path = os.path.abspath(path) if os.path.isfile(path) and not os.path.islink(path): try: # detect file format json_format = is_json_format and not grep('^description=>', path) if json_format: return patch_json_dynamicui(path, app_show, app_hide, verbose) else: return patch_dynamicui(path, app_show, app_hide, verbose) except IOError as e: if verbose: print('%s: %s' % (NAME, str(e))) else: if verbose: print('%s: file %s not present, skip' % (NAME, path)) return None def patch_themes(themes_dir_list, app_show, app_hide, verbose=False, is_json_format=False): """ Patch dynamicui configuration files for all themes """ for theme_path in themes_dir_list: # set of patched apps of current theme patched_set = set() # first change additional configs for app_name in (app_hide + app_show): dynamicui_conf_path = os.path.join(theme_path, 'dynamicui', 'dynamicui_%s.conf' % app_name) if app_name in app_hide: res = patch_dynamicui_wrapper(dynamicui_conf_path, app_hide=[app_name], verbose=verbose) patched_set |= (res or set()) elif app_name in app_show: res = patch_dynamicui_wrapper(dynamicui_conf_path, app_show=[app_name], verbose=verbose) patched_set |= (res or set()) # now filter all patched apps from start lists not_patched_app_show = list(sorted(set(app_show).difference(patched_set))) not_patched_app_hide = list(sorted(set(app_hide).difference(patched_set))) # check is all application was patched or no if not len(not_patched_app_show) and not len(not_patched_app_hide): continue # next step change main config if need patch anything dynamicui_conf_path = os.path.join(theme_path, 'dynamicui.conf') patch_dynamicui_wrapper(dynamicui_conf_path, not_patched_app_show, not_patched_app_hide, verbose, is_json_format) def load_json_conf(verbose=False, key_filter=None): """ Loading config from UI config file value must be only False or True :param verbose: detailed output enabled :param key_filter: keys to be filtered output dict :return dict: """ key_filter = key_filter or list() output_dict = dict() parsed_config = UIConfig().get_param('uiSettings') if parsed_config: for key, value in parsed_config.items(): if key not in key_filter: continue try: output_dict.update({key: value}) except ValueError: if verbose: print('WARNING: incorrect key "%s" in UI config file, skipped;' ' value must be True or False' % key) # add selector`s status config to result dict sync_selector_setting(output_dict, 'hideNodeJsApp') sync_selector_setting(output_dict, 'hidePythonApp') sync_selector_setting(output_dict, 'hidePhpApp') return output_dict def sync_selector_setting(external_conf, plugin_flag): # type: (Dict, str) -> () """ Adds value of selector status to cpanel config :param external_conf: dict with cpanel config values :param plugin_flag: plugin's flag name """ try: if plugin_flag == 'hideNodeJsApp': from clselect.clselectnodejs.node_manager import NodeManager m = NodeManager() elif plugin_flag == 'hidePythonApp': from clselect.clselectpython.python_manager import PythonManager m = PythonManager() elif plugin_flag == 'hidePhpApp': from clselect.clselectphp.php_manager import PhpManager m = PhpManager() else: raise NotImplementedError() external_conf[plugin_flag] = not m.selector_enabled except ImportError: external_conf[plugin_flag] = False except Exception as e: external_conf[plugin_flag] = False print(e) def sync_conf(themes_dir_list, sync_association=sync_association, key_filter=None, verbose=False, is_json_format=False): app_hide = list() app_show = list() conf = load_json_conf(verbose=verbose, key_filter=key_filter) # generate 'app_show' and 'app_hide' app list for key_, app_name, reverse_boolean in sync_association: val_ = conf.get(key_) if key_ is None: app_hide.append(app_name) # if key_ is None then hide the app if val_ is None: continue if reverse_boolean: val_ = not val_ if val_ is True: app_show.append(app_name) elif val_ is False: app_hide.append(app_name) patch_themes(themes_dir_list, app_show, app_hide, verbose, is_json_format) def main(args=None): try: opts, _ = getopt.getopt(args or sys.argv[1:], 'h:v', ['show=', 'hide=', 'path=', 'sync-conf=', 'no-rebuild-sprites', "fix-dynamicui", 'verbose', 'help']) except getopt.GetoptError as err: print(str(err)) usage() sys.exit(2) fix_dynamicui = False verbose = False app_show = list() app_hide = list() dynamicui_conf_path = None rebuild_sprites = True sync = list() for o, _ in opts: if o in ('-h', '--help'): usage() sys.exit() elif o == '--show': app_show.extend(_.split(',')) elif o == '--hide': app_hide.extend(_.split(',')) elif o == '--path': dynamicui_conf_path = _ elif o == '--fix-dynamicui': fix_dynamicui = True elif o == '--sync-conf': if _ == 'all': sync = [k_[0] for k_ in sync_association] else: sync.extend(_.split(',')) elif o == '--no-rebuild-sprites': rebuild_sprites = False elif o in ('--verbose', '-v'): verbose = True else: usage() sys.exit(2) if fix_dynamicui: for path in glob.glob(os.path.join(THEMES_LOCATION, '*')): if os.path.isdir(path) and not os.path.islink(path): fix_json_format(os.path.join(path, "dynamicui.conf")) return 0 if len(app_show + app_hide) == 0 and not sync: usage() sys.exit(2) # checking the uniqueness of values in the 'app_show' and 'app_hide' if len(set(app_show + app_hide)) != len(app_show + app_hide): print("ERROR: Application names in options '--show' and '--hide' should not be repeated") sys.exit(2) # get CP version import cldetectlib as detect detect.getCP() if detect.CP_NAME != 'cPanel': print('WARNING: Unsupported control panel, this script works only on cPanel. Exiting.') sys.exit(2) is_json_format = Version(detect.CP_VERSION) >= Version("54.0") if dynamicui_conf_path: patch_dynamicui_wrapper(dynamicui_conf_path, app_show, app_hide, verbose, is_json_format) return # list cPanel themes directories, without directories links themes_dir_list = [path for path in glob.glob(os.path.join(THEMES_LOCATION, '*')) if os.path.isdir(path) and not os.path.islink(path)] if sync: sync_conf(themes_dir_list, verbose=verbose, key_filter=sync, is_json_format=is_json_format) patch_themes(themes_dir_list, app_show, app_hide, verbose, is_json_format) if rebuild_sprites: if verbose: print('%s: rebuilding sprites in background' % NAME) if os.path.exists('/usr/local/cpanel/bin/sprite_generator'): subprocess.run("/usr/local/cpanel/bin/sprite_generator --all > /dev/null 2>&1 &", shell=True, executable='/bin/bash') else: subprocess.run("/usr/local/cpanel/bin/rebuild_sprites -cponly -quiet > /dev/null 2>&1 &", shell=True, executable='/bin/bash') if __name__ == '__main__': main() Save