| Server IP : 104.21.37.246 / Your IP : 104.23.243.32 [ Web Server : Apache System : Linux cpanel01wh.bkk1.cloud.z.com 2.6.32-954.3.5.lve1.4.59.el6.x86_64 #1 SMP Thu Dec 6 05:11:00 EST 2018 x86_64 User : cp648411 ( 1354) PHP Version : 7.2.34 Disable Function : NONE Domains : 0 Domains MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : OFF | Pkexec : OFF Directory : /opt/alt/python37/lib/python3.7/site-packages/clwpos/ |
Upload File : |
# -*- coding: utf-8 -*-
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
# wposctl.py - work code for clwposctl utility
from __future__ import absolute_import
import argparse
import json
import os
import subprocess
import sys
from copy import deepcopy
from typing import Dict, Iterator, Set, Tuple, List, Optional
import cpanel
from clcommon import cpapi
from clwpos.optimization_features import (
ALL_OPTIMIZATION_FEATURES,
OBJECT_CACHE_FEATURE,
convert_features_dict_to_interface
)
from clwpos.feature_suites import (
ALL_SUITES,
any_suite_allowed_on_server,
get_suites_allowed_path,
get_admin_suites_config,
write_suites_allowed,
is_suite_allowed_for_user,
extract_features,
is_module_allowed_for_user
)
from clcommon.clpwd import drop_privileges
from clwpos.cl_wpos_exceptions import WposError
from clwpos.user.config import UserConfig
from clwpos.constants import (
PUBLIC_OPTIONS,
ALT_PHP_REDIS_ENABLE_UTILITY,
CLWPOS_UIDS_PATH,
EA_PHP_REDIS_ENABLE_UTILITY,
SUITES_MARKERS,
MIGRATION_NEEDED_MARKER,
SCAN_CACHE
)
from clwpos import gettext as _
from cpanel import enable_without_config_affecting, disable_without_config_affecting, DocRootPath
from clwpos.parse import ArgumentParser, CustomFormatter
from clwpos.logsetup import setup_logging, init_wpos_sentry_safely, ADMIN_LOGFILE_PATH
from clcommon.lib.cledition import is_cl_solo_edition
from clcommon.cpapi.cpapiexceptions import NoPackage
from clwpos.report_generator import ReportGenerator, ReportGeneratorError
from clwpos.utils import (
catch_error,
error_and_exit,
print_data,
check_license_decorator,
set_wpos_icon_visibility,
acquire_lock,
get_default_public_options,
get_pw,
is_redis_configuration_running,
install_monitoring_daemon,
get_admin_options,
clean_clwpos_crons,
is_ui_icon_hidden
)
from clwpos.wpos_hooks import (
install_panel_hooks,
install_yum_universal_hook_alt_php,
_uninstall_hooks
)
from clcommon.clcagefs import setup_mount_dir_cagefs, _remount_cagefs
from clwpos.stats import fill_current_wpos_statistics
DISABLED_OMS_MESSAGE = _("All optimization suites are currently disabled. "
"End-user CL AccelerateWP interface blocked.")
WPOS_SERVICE_ENABLE_ERR_MSG = _("Unable to run CL AccelerateWP daemon. Caching databases won't start and work. "
"You can find detailed information in log file")
REDIS_CONFIGURATION_WARNING_MSG = _("Configuration of PHP redis extension is running in background process. "
"This may take up to several minutes. Until the end of this process "
"functionality of CL AccelerateWP is limited.")
parser = ArgumentParser(
"/usr/bin/clwpos-admin",
"Utility for control CL AccelerateWP admin interface",
formatter_class=CustomFormatter
)
_logger = setup_logging(__name__)
class CloudlinuxWposAdmin(object):
"""
Class for run cloudlinux-wpos-admin commands
"""
def __init__(self):
self._is_json = False
self._opts: argparse.Namespace
self._logger = setup_logging(__name__)
init_wpos_sentry_safely(self._logger)
self.clwpos_path = "/var/clwpos"
self.modules_allowed_name = "modules_allowed.json"
self.is_solo = is_cl_solo_edition(skip_jwt_check=True)
self.wait_child_process = bool(os.environ.get('CL_WPOS_WAIT_CHILD_PROCESS'))
if self.wait_child_process:
self.exec_func = subprocess.run
else:
self.exec_func = subprocess.Popen
@catch_error
def run(self, argv):
"""
Run command action
:param argv: sys.argv[1:]
:return: clwpos-user utility retcode
"""
self._parse_args(argv)
result = getattr(self, self._opts.command.replace("-", "_"))()
print_data(self._is_json, result)
def _parse_args(self, argv):
"""
Parse command line arguments
:param argv: sys.argv[1:]
"""
self._opts = parser.parse_args(argv)
self._is_json = True
@staticmethod
def _create_markers(suites_list):
for suite in suites_list:
if SUITES_MARKERS.get(suite) and not os.path.isfile(SUITES_MARKERS.get(suite)):
open(SUITES_MARKERS.get(suite), 'w').close()
@staticmethod
def _clear_markers(suites_list):
for suite in suites_list:
if SUITES_MARKERS.get(suite) and os.path.isfile(
SUITES_MARKERS.get(suite)):
os.unlink(SUITES_MARKERS.get(suite))
@parser.command(help="Uninstall cache for all domain during downgrade")
def uninstall_cache_for_all_domains(self) -> dict:
"""
This command used during downgrade to lve-utils, which version does not support clwpos
:return:
"""
try:
users = cpapi.cpusers()
except (OSError, IOError, IndexError, NoPackage) as e:
self._logger.warning("Can't get user list from panel: %s", str(e))
return {}
for username in users:
with drop_privileges(username):
for doc_root, wp_path, module in _enabled_modules(username):
disable_without_config_affecting(DocRootPath(doc_root), wp_path, module=module)
return {}
@parser.argument(
"--suites",
help="Argument for suite of list of comma separated suites",
type=str,
required=True
)
@parser.mutual_exclusive_group(
[
(["--allowed"], {"help": "Allow suites for users", "action": "store_true"}),
(["--disallowed"], {"help": "Disallow suites for users", "action": "store_true"}),
(["--allowed-for-all"], {"help": "Allow suites for all users", "action": "store_true"}),
(["--disallowed-for-all"],
{"help": "Disallow suites for all users", "action": "store_true"}),
],
required=True,
)
@parser.argument("--users", help="User or list of comma separated users", type=str,
required=(not is_cl_solo_edition(skip_jwt_check=True) and not ("--allowed-for-all" in sys.argv or "--disallowed-for-all" in sys.argv)))
@parser.command(help="Managing list of allowed suites for users")
@check_license_decorator
def set_suite(self) -> dict:
"""
Write info related to module allowance into user file
"""
suites_list = [suite.strip() for suite in self._opts.suites.split(",")]
for suite in suites_list:
if suite not in ALL_SUITES:
error_and_exit(self._is_json, {'result': _(f'Unsupported suite: {suite}')})
modules_list = [module for suite in suites_list for module in ALL_SUITES[suite].feature_set]
if self.is_solo:
if self._opts.allowed_for_all:
module_allowed = True
elif self._opts.disallowed_for_all:
module_allowed = False
else:
module_allowed = self._opts.allowed
# For Solo we use first user in list
users = cpapi.cpusers()
if not users:
error_and_exit(
self._is_json,
{"result": _("There are no users in the control panel.")},
)
user_list_to_process = [users[0]]
else:
# CL Shared (Pro)
if self._opts.allowed_for_all:
# Process all panel users
user_list_to_process = cpapi.cpusers()
module_allowed = True
self._create_markers(suites_list)
# async call here is fine, also no need to wait
# in most cases it should work fast
# if it works slower - scanning will be in process in UI
self._logger.info('Going to generate users report')
if not os.path.isfile(SCAN_CACHE):
ReportGenerator().scan()
elif self._opts.disallowed_for_all:
# Process all panel users
user_list_to_process = cpapi.cpusers()
module_allowed = False
self._clear_markers(suites_list)
else:
# Process only specified users
module_allowed = self._opts.allowed
user_arg_list = self._opts.users.split(",")
user_list_to_process = [user_arg_list[0].strip()] # in v1 only single user processing is supported
first_user_wpos_enabled = module_allowed and not any_suite_allowed_on_server()
first_user_obj_cache_enabled = module_allowed and OBJECT_CACHE_FEATURE in modules_list \
and not is_module_allowed_for_user(OBJECT_CACHE_FEATURE)
warning_dict = {}
if module_allowed:
retcode, stdout, stderr = install_monitoring_daemon(True)
if retcode:
self._logger.error("Starting service ended with error: %s, %s", stdout, stderr)
warning_dict.update({"warning": WPOS_SERVICE_ENABLE_ERR_MSG})
error_flag = False
is_one_user_processing = len(user_list_to_process) == 1
for username in user_list_to_process:
# update modules only after daemon startup
try:
_error_flag, warning_d = self._process_user_suites(username,
suites_list,
module_allowed,
is_one_user_processing)
except Exception as e:
# ignore all errors for users processing during
# bulk operations in order not to interrupt it
# and raise otherwise (for individual users processing)
if is_one_user_processing:
self._logger.error(
"Error while processing module for '%s': %s",
username, str(e))
raise
self._logger.exception(
"Error while processing module for '%s': %s",
username, str(e))
_error_flag = True
warning_d = {}
# Skip further user processing if error
if _error_flag:
# Set global error flag
error_flag = True
continue
if self.is_solo:
warning_dict.update(warning_d)
if self._opts.allowed:
self.exec_func(
["/usr/sbin/clwpos_collect_information.py", username],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
if module_allowed and is_ui_icon_hidden():
# toggle icon if allow is in progress and icon is hidden in UI
set_wpos_icon_visibility(hide=False)
self.write_public_options(show=True)
if self._opts.allowed_for_all and not self.is_solo:
# /usr/sbin/clwpos_collect_information.py without args processes all users
self.exec_func(["/usr/sbin/clwpos_collect_information.py"], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
_remount_cagefs()
cron_env_dict = {}
if first_user_wpos_enabled:
cron_env_dict.update({
"CLSHARE": "/usr/share/cloudlinux",
"WPOS_REQ_CRON_FILE": "/etc/cron.d/clwpos_req_cron",
"CLWPOS_COLLECT_INFORMATION_CRON": "/etc/cron.d/clwpos_collect_information_cron.py"
})
if not self.is_solo:
# This runs if admin allowed any optimizations group for any user
# and there were no optimization feature allowed on server
setup_mount_dir_cagefs(
CLWPOS_UIDS_PATH, prefix='*', remount_cagefs=True, remount_in_background=not self.wait_child_process
)
install_panel_hooks()
if first_user_obj_cache_enabled:
cron_env_dict.update({
"CLSHARE": "/usr/share/cloudlinux",
"CLWPOS_REDIS_EXTENSION_INSTALLER": "/etc/cron.d/clwpos_redis_extension_installer",
"CLWPOS_CLEANER_CRON": "/etc/cron.d/clwpos_cleaner_cron"
})
# This runs after object_cache module is allowed for any user
# and there were no users on server who are allowed object_cache module before
if not self.is_solo:
warning_dict.update({"warning": REDIS_CONFIGURATION_WARNING_MSG})
self.exec_func([ALT_PHP_REDIS_ENABLE_UTILITY], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
self.exec_func([EA_PHP_REDIS_ENABLE_UTILITY], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
install_yum_universal_hook_alt_php()
elif module_allowed and OBJECT_CACHE_FEATURE in modules_list and is_redis_configuration_running():
warning_dict.update({"warning": REDIS_CONFIGURATION_WARNING_MSG})
if first_user_wpos_enabled or first_user_obj_cache_enabled:
self.exec_func(
["/usr/share/cloudlinux/add_clwpos_crons.sh"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=cron_env_dict
)
if self._opts.disallowed_for_all and not self.is_solo:
_remount_cagefs()
# determine the case of all suites becoming disallowed
# after manipulations with users
if not module_allowed and not any_suite_allowed_on_server():
_uninstall_hooks()
clean_clwpos_crons()
set_wpos_icon_visibility(hide=True)
self.write_public_options(show=False)
# MIGRATION_NEEDED_MARKER is created in .spec only during 1 upgrade
# before_renaming_version -> renaming_version
if self._opts.allowed_for_all and os.path.isfile(MIGRATION_NEEDED_MARKER):
self._logger.info('set-suite for all was called, removing migration marker')
os.remove(MIGRATION_NEEDED_MARKER)
if error_flag:
error_and_exit(
self._is_json,
{
"result": _("User(s) process error. Please check log file %(logfile)s"),
"context": {"logfile": ADMIN_LOGFILE_PATH},
}
)
return warning_dict
@parser.mutual_exclusive_group(
[
(["--hide-icon"], {"help": "Hide AccelerateWP icon", "action": "store_true"}),
(["--show-icon"], {"help": "Show AccelerateWP icon", "action": "store_true"}),
],
required=True,
)
@parser.command(help="Manage global options")
@check_license_decorator
def set_options(self) -> dict:
"""
Set global options that affect all users.
For v1 it is only allowed to control WPOS icon visibility.
"""
retcode, stdout = set_wpos_icon_visibility(hide=self._opts.hide_icon)
if retcode:
error_and_exit(
self._is_json,
{
"result": _("Error during changing of AccelerateWP icon visibility: \n%(error)s"),
"context": {"error": stdout}
},
)
self.write_public_options(self._opts.show_icon)
return {}
@catch_error
@parser.command(help="Return public options")
@check_license_decorator
def get_options(self):
try:
return get_admin_options()
except json.decoder.JSONDecodeError as err:
raise WposError(
message=_(
"File is corrupted: Please, delete file mentioned in details or fix the corrupted line"),
details=str(err))
@catch_error
@parser.mutual_exclusive_group(
[
(["--all"], {"help": "Argument for all users in the panel", "action": "store_true"}),
(["--users"], {"help": "Argument for user or list of comma separated users", "type": str}),
],
required=True,
)
@parser.command(help="Return the report about allowed and restricted user's features")
def get_report(self) -> dict:
"""
Print report in stdout.
[!ATTENTION!] response jsons are different for Solo and Shared!
"""
report = {}
if self.is_solo:
try:
features = extract_features(get_admin_suites_config()['suites'])
except (KeyError, json.JSONDecodeError):
raise WposError(
message=_("Configuration file '%(config_path)s' is corrupted. "
"Check it and make sure it has valid json format.\n"
"Contact CloudLinux support in case you need any assistance."),
context=dict(config_path=get_suites_allowed_path())
)
report = {'features': convert_features_dict_to_interface(features)}
else:
try:
users = self._opts.users.split(',') if self._opts.users else None
report = ReportGenerator().get(target_users=users)
except ReportGeneratorError as e:
error_and_exit(
self._is_json,
{
'result': e.message,
'context': e.context
}
)
except Exception as e:
error_and_exit(
self._is_json,
{
'result': _('Error during getting report: %(error)s'),
'context': {'error': e},
}
)
return report
@catch_error
@parser.mutual_exclusive_group(
[
(["--all"], {"help": "Argument for all users in the panel", "action": "store_true"}),
(["--status"], {"help": "Show scan status", "action": "store_true"}),
],
required=True,
)
@parser.command(help="Create the report about allowed and restricted user's features")
def generate_report(self) -> dict:
if self.is_solo:
error_and_exit(
self._is_json, {"result": _("Solo edition is not supported.")}
)
rg = ReportGenerator()
try:
if self._opts.status:
scan_status = rg.get_status()
else:
# TODO: implement --users support: send List[str] argument
scan_status = rg.scan() # initial status dict, like 0/10
return {
'result': 'success',
**scan_status,
}
except ReportGeneratorError as e:
error_and_exit(
self._is_json,
{
'result': e.message,
'context': e.context
}
)
except Exception as e:
error_and_exit(
self._is_json,
{
'result': _('Error during generating report: %(error)s'),
'context': {'error': str(e)},
}
)
@catch_error
@parser.command(
help="Get current statistics of AccelerateWP enabled sites and allowed user's features")
def get_stat(self) -> dict:
"""AccelerateWP statistics"""
return fill_current_wpos_statistics()
@staticmethod
def write_public_options(show: bool) -> None:
"""Set icon visibility in clwpos public options file"""
with acquire_lock(PUBLIC_OPTIONS):
if not os.path.isfile(PUBLIC_OPTIONS):
public_config_data = get_default_public_options()
else:
try:
with open(PUBLIC_OPTIONS) as f:
public_config_data = json.load(f)
except json.decoder.JSONDecodeError as err:
raise WposError(
message=_("File is corrupted: Please, delete file %(config_file)s"
" or fix the line provided in details"),
details=str(err),
context={'config_file': PUBLIC_OPTIONS})
public_config_data["show_icon"] = show
with open(PUBLIC_OPTIONS, "w") as f:
json.dump(public_config_data, f)
@staticmethod
def all_suites_disabled(suites: Dict[str, bool]) -> bool:
"""
Check if all feature suites are disabled.
"""
return not any(suites.values())
def _process_user_suites(self, user_name: str, suites: List[str],
allowed_state: bool, is_one_user: bool) -> Tuple[bool, Optional[dict]]:
"""
Enable/disable modules for user.
- write admin config for user with new state
- install/uninstall WP plugin
- reload deamon to start/stop redis
:param user_name: username
:param suites: Suites list to process
:param allowed_state: True - allow suite, False - disallow
:param is_one_user: True - utility processes one user, False - some users
For messages backward compatibility
:return: Tuple: (error_flag, warning_flag)
"""
# Get modules_allowed.json for user
try:
pw_info = get_pw(username=user_name)
uid, gid = pw_info.pw_uid, pw_info.pw_gid
except KeyError:
if is_one_user:
error_and_exit(
self._is_json,
{
"result": _("User %(username)s does not exist."),
"context": {"username": user_name},
},
)
self._logger.error("User %s does not exist.", user_name)
return True, None
suites_allowed_path = get_suites_allowed_path(uid)
warning_dict = {}
try:
os.makedirs(os.path.dirname(suites_allowed_path), 0o755, exist_ok=False)
except OSError:
pass
else:
if not self.is_solo and is_one_user:
_remount_cagefs(user_name)
with acquire_lock(suites_allowed_path):
config_contents = get_admin_suites_config(uid)
features_old_state = extract_features(deepcopy(config_contents["suites"]))
config_contents["suites"].update(dict.fromkeys(suites, allowed_state))
features_new_state = extract_features(deepcopy(config_contents["suites"]))
try:
write_suites_allowed(uid, gid, config_contents)
except (IOError, OSError) as err:
if is_one_user:
raise WposError(
message=_("Configuration file '%(path)s' update failed."),
details=str(err),
context=dict(path=suites_allowed_path)
)
self._logger.error("Configuration file %s update failed. Error is %s",
suites_allowed_path, str(err))
return True, None
synchronize_plugins_status_for_user(user_name, uid, features_old_state, features_new_state)
if self.is_solo and self.all_suites_disabled(config_contents["suites"]):
warning_dict.update({"warning": DISABLED_OMS_MESSAGE})
return False, warning_dict
def sync_old_allowed_config(self, uid, suites, allowed):
config_contents = get_admin_suites_config(uid)
def synchronize_plugins_status_for_user(username: str, uid: int, old_state: dict, new_state: dict):
"""
Compare old and new states of modules in admin's wpos config,
determine what modules should be enabled and disabled
and synchronize new state for each panel's user.
"""
old_state = {key for key, value in old_state.items() if value}
new_state = {key for key, value in new_state.items() if value}
enabled_modules = new_state - old_state
disabled_modules = old_state - new_state
synchronize_plugins_for_user(username, uid, enabled_modules, disabled_modules)
def synchronize_plugins_for_user(username: str, uid: int, enabled_modules: Set[str], disabled_modules: Set[str]):
"""
Iterate through user's docroots and wp_paths
and enable/disable modules with wp-cli
not modifying user's wpos config.
"""
with drop_privileges(username):
user_config = UserConfig(username=username)
for doc_root, wp_path, module in user_config.enabled_modules():
if module in enabled_modules:
enable_without_config_affecting(
DocRootPath(doc_root),
wp_path,
module=module,
)
if module in disabled_modules:
disable_without_config_affecting(
DocRootPath(doc_root),
wp_path,
module=module,
)
# we don't hardcode object cache here in case we will
# add some other modules that will require redis running
modules_that_require_redis = [
str(module) for module in ALL_OPTIMIZATION_FEATURES
if module.redis_daemon_required()
]
# reload redis only if we need
if not user_config.is_default_config() \
and set(modules_that_require_redis) & (enabled_modules | disabled_modules):
try:
# Reload redis for user
cpanel.reload_redis(uid)
except WposError as e:
_logger.exception("CL AccelerateWP daemon error: '%s'; details: '%s'; context: '%s'", e.message, e.details, e.context)
except Exception as e:
_logger.exception(e)
def _enabled_modules(username: str) -> Iterator[Tuple[str, str, str]]:
return UserConfig(username=username).enabled_modules()