# -*- 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
"""
This module contains X Ray Smart Advice local utility main class
"""
import json
import logging
import os
import hashlib
import pwd
import subprocess
from contextlib import contextmanager
from dataclasses import asdict, dataclass
from typing import Any, Tuple, Optional, List
from enum import Enum
from clwpos.user.config import LicenseApproveStatus
from schema import Schema, SchemaError
from clwpos.papi import (
is_feature_visible,
is_feature_allowed,
is_subscription_pending,
get_subscription_upgrade_url,
get_license_approve_status,
approve_license_agreement,
get_license_agreement_text,
get_subscriptions_info,
calculate_cdn_usage,
get_user_auth_key,
is_feature_hidden_server_wide,
is_smart_advice_notifications_disabled_server_wide,
is_smart_advice_reminders_disabled_server_wide
)
from clcommon.cpapi import (
docroot,
is_panel_feature_supported,
get_user_emails_list,
panel_awp_link,
getCPName,
userdomains
)
from clcommon.clwpos_lib import is_wp_path
from clcommon.clpwd import drop_privileges
from clcommon.const import Feature
from .advice_types import advice_mapping
from .advice_types.wpos_base import WPOSModuleApply
from .progress import SmartAdviceProgress
from .schemas import (
advice_list_schema,
detailed_advice_schema,
user_sites_info_schema
)
from ..apiclient import get_client
from ..internal.constants import advice_pending_storage, advice_processed_storage, advice_list_cache, advice_reason_max_len
from ..internal.exceptions import XRayAPIError, XRayError, SmartAdvicePluginError
from ..internal.user_plugin_utils import user_mode_advice_verification, username_verification, get_xray_exec_user
from ..internal.utils import timestamp, safe_move, get_user_php_version, filelock
from ..analytics.utils import report_usage_action_or_error
advice_cache_separator = ';'
class AdviceActions(Enum):
APPLY = 'apply'
ROLLBACK = 'rollback'
@dataclass
class SmartAdviceOptions:
panel_type: str
panel_url: str
panel_emails: str
upgrade_url: str
upgrade_url_cdn: str
subscription: dict
notifications: dict
class SmartAdviceUtil:
"""Main Smart Advice local utility class"""
def __init__(self):
self.logger = logging.getLogger('smart_advice')
# check existence of pending and apply storage and create it if missing
for stor in (advice_pending_storage, advice_processed_storage):
self.create_dir(stor)
# initialize Adviser API client
adviser_client_object = get_client('adviser')
self.adviser_client = adviser_client_object()
@staticmethod
def create_dir(dpath: str) -> None:
"""Create dir if missing"""
if not os.path.isdir(dpath):
os.mkdir(dpath)
@staticmethod
def response(**kwargs) -> str:
"""
Create JSON response message with result field == success
and given keyword arguments in other fields
:return: json packed string
"""
initial = {'result': 'success', 'timestamp': timestamp()}
if kwargs:
initial.update(kwargs)
return json.dumps(dict(sorted(initial.items())))
@staticmethod
def _apply_datafile(a_id: int) -> str:
"""Per-advice file with results of apply"""
return f'{advice_processed_storage}/{a_id}'
def _apply_results(self, a_id: int) -> Optional[str]:
"""Retrieve data stored in per-advice file with results of apply"""
datafile = self._apply_datafile(a_id)
if os.path.isfile(datafile):
try:
with open(datafile) as _f:
data = json.load(_f)
except OSError:
data = None
except json.JSONDecodeError:
with open(datafile) as _f:
data = _f.read()
finally:
os.unlink(datafile)
return data
@staticmethod
def _pending_flag(a_id: int) -> str:
"""Per-advice pending flag"""
return f'{advice_pending_storage}/{a_id}'
def is_advice_pending(self, advice_id: int) -> bool:
"""Is advice in pending state"""
return os.path.exists(self._pending_flag(advice_id))
def _progress_file(self, a_id: int) -> str:
"""Per-advice progress storage"""
return self._pending_flag(a_id) + '.progress'
def _get_user_awp_link(self, username):
try:
return panel_awp_link(username)
except Exception:
self.logger.exception('Error while getting user login link')
return ''
@staticmethod
def get_advice_instance(advice_type: str) -> WPOSModuleApply:
"""Get appropriate instance of Apply actions class by advice type"""
return advice_mapping[advice_type]
def _validate(self, data: Any, schema: Schema) -> Any:
"""Validate given data using given schema"""
try:
return schema.validate(data)
except SchemaError as e:
self.logger.error('Failed to validate API response: %s', data)
msg = e.errors[-1] or e.autos[-1]
raise XRayAPIError(f'Malformed API response: {msg}')
@user_mode_advice_verification
def get_detailed_advice(self, advice_id: int) -> Tuple[dict, object]:
"""
Get advice details from API along with an
appropriate Advice object by obtained advice type
"""
_response = self._validate(
data=self.adviser_client.advice_details(advice_id),
schema=detailed_advice_schema)
return _response, self.get_advice_instance(_response['advice']['type'])
@staticmethod
def dump_to_file(dst: str, data: Any, as_json: bool = False) -> None:
"""Dump data inside given dst using .bkp file and then move"""
_tmp_dst = dst + '.bkp'
with open(_tmp_dst, 'w') as _f:
if as_json:
try:
json.dump(data, _f)
except TypeError:
_f.write(data)
else:
_f.write(data)
safe_move(_tmp_dst, dst)
def progress(self, advice_id: int, current: SmartAdviceProgress) -> SmartAdviceProgress:
"""
Smart Advice apply own progress,
based on current and previous results of 'clwpos get-progress'.
Returns the maximum progress values among current and previous.
Previous (latest) is stored inside a pending file-flag.
"""
@contextmanager
def resolve_progress() -> SmartAdviceProgress:
"""
Get progress stored in file and return the maximum one among
current and stored results
"""
self.logger.debug('Current progress value: %s', current)
progress_dst = self._progress_file(advice_id)
try:
# read stored result
with open(progress_dst) as prev:
prev_stages = SmartAdviceProgress(**json.load(prev))
except (OSError, json.JSONDecodeError) as e:
self.logger.debug('Error during reading stored value: %s',
str(e))
# or set a dummy one
prev_stages = SmartAdviceProgress()
self.logger.debug('Stored progress value: %s', prev_stages)
yield current if current > prev_stages else prev_stages
if self.is_advice_pending(advice_id) and current > prev_stages:
# update stored result only if pending file exists
self.logger.debug('Updating stored progress: %s', current)
self.dump_to_file(progress_dst, asdict(current),
as_json=True)
return resolve_progress()
def get_current_status(self, _id: int, advice_obj: object,
advice_data: dict) -> Tuple[str, dict]:
"""
Resolve if advice is in pending status.
Progress is retrieved for pending advice only.
Dummy result (0, 0) is returned for other advice states.
"""
if self.is_advice_pending(_id):
status = 'pending'
with self.progress(_id, advice_obj.get_progress(
advice_data['metadata']['username'])) as p:
stages = p
else:
status = advice_data['advice']['status']
stages = SmartAdviceProgress()
return status, asdict(stages)
def advice_list(self, extended=False) -> str:
"""Load validated advice list and update it before returning"""
api_response = self._validate(data=self.adviser_client.advice_list(),
schema=advice_list_schema)
response_advice_list = self.prepare_advices_response(api_response, extended=extended)
# cl-smart-advice plugin syncing should not break whole command
try:
self.sync_advices_wordpress_plugin(current_advices=api_response)
except Exception:
self.logger.exception('Unable to sync cl-smart-advice plugin during getting list of advices')
return self.response(data=response_advice_list)
@username_verification
def get_site_statuses(self, username) -> str:
api_response = self._validate(data=self.adviser_client.site_info(username),
schema=user_sites_info_schema)
site_statuses_data = {}
for website in api_response:
domain = website['domain']
if domain not in site_statuses_data:
site_statuses_data[domain] = []
site_statuses_data[domain].append({
'website': website['website'],
'urls_scanned': len(website['urls']),
'advices': self.prepare_advices_response(website['advices'])
})
result = [{'domain': domain, 'websites': websites} for domain, websites in site_statuses_data.items()]
return self.response(data=result)
def _does_user_exist_on_server(self, username):
try:
pwd.getpwnam(username)
return True
except KeyError:
logging.info('User %s does not exist anymore on this server, skipping advices for him')
return False
def prepare_advices_response(self, from_api, status_from_microservice=False, extended=False):
advices = from_api.copy()
visible_advices = []
for item in advices:
# skip advices which appear to be linked with non existing users
if not self._does_user_exist_on_server(item['metadata']['username']):
continue
advice_itself = item['advice']
advice_type = advice_itself['type']
advice_instance = self.get_advice_instance(advice_type)
advice_itself['description'] = advice_instance.short_description
# when we need real response from microservice - only before sending json to wp plugin
if status_from_microservice:
status = advice_itself['status']
stages = {'total_stages': 0, 'completed_stages': 0}
else:
status, stages = self.get_current_status(advice_itself['id'],
advice_instance, item)
if extended:
advice_itself['detailed_description'] = advice_instance.detailed_description
advice_itself['status'] = status
advice_itself['is_premium'] = advice_instance.is_premium_feature
advice_itself['module_name'] = advice_instance.module_name
advice_itself['license_status'] = get_license_approve_status(
advice_instance.module_name, item['metadata']['username']).name
if advice_itself['is_premium']:
subscription_status = self._get_subscription_status(
advice_instance.module_name, item['metadata']['username'])
advice_itself['subscription'] = dict(
status=subscription_status,
upgrade_url=get_subscription_upgrade_url(
advice_instance.module_name, item['metadata']['username'])
)
advice_itself.update(stages)
if is_feature_visible(advice_instance.module_name,
item['metadata']['username']) and \
not is_feature_hidden_server_wide(advice_instance.module_name):
visible_advices.append(item)
return visible_advices
def _get_subscription_status(self, module_name, username):
"""
Determines current subscription status based on feature status.
"""
subscription_status = 'no'
if is_feature_allowed(module_name, username):
subscription_status = 'active'
if is_subscription_pending(module_name, username):
subscription_status = 'pending'
return subscription_status
def advice_details(self, advice_id: int) -> str:
"""Load validated advice details and update it before returning"""
api_response, advice_instance = self.get_detailed_advice(advice_id)
advice_itself = api_response['advice']
"""
The microservice responds with field names: title and description.
We must assign:
[local advice] detailed_description = [microservice] description
[local advice] description = [microservice] title
"""
# Until we won't rename detailed_description to description
if advice_itself['description']:
advice_itself['detailed_description'] = advice_itself['description']
else:
advice_itself['detailed_description'] = advice_instance.detailed_description
# Until we won't rename description to title
if advice_itself['title']:
advice_itself['description'] = advice_itself['title']
else:
advice_itself['description'] = advice_instance.short_description
del advice_itself['title']
return self.response(data=api_response)
def manage_advice(self, action, advice_id: int, ignore_errors: bool = False,
async_mode: bool = False, source: str = 'ACCELERATE_WP',
reason: str = None, accept_terms: bool = False,
analytics_data: str = None) -> Optional[str]:
open(self._pending_flag(advice_id), 'w').close()
if async_mode:
# put itself to background in async mode
# and return current status
child = os.fork()
if child:
return self.manage_advice_status(advice_id)
output = self._exec_advice_managing(action, advice_id, ignore_errors, async_mode,
source, reason, accept_terms)
if analytics_data is not None:
report_usage_action_or_error(analytics_data, advice_id, source, output, action)
if async_mode:
# retrun no output in async mode
output = None
return output
def _exec_advice_managing(self, action: str, advice_id: int, ignore_errors: bool = False,
async_mode: bool = False, source: str = 'ACCELERATE_WP',
reason: str = None, accept_terms: bool = False) -> Optional[str]:
"""Execute managing advice with passed action: apply/rollback"""
try:
api_response, advice_instance = self.get_detailed_advice(advice_id)
if action == AdviceActions.APPLY.value:
if accept_terms:
approve_license_agreement(
advice_instance.module_name, api_response['metadata']['username'])
if get_license_approve_status(
advice_instance.module_name,
api_response['metadata']['username']) == LicenseApproveStatus.NOT_APPROVED:
return json.dumps({
'result': 'LICENCE_TERMS_APPROVE_REQUIRED',
'text': 'License approve required to use this feature. '
'Open AccelerateWP plugin in your control panel, apply advice and '
'accept terms and conditions to proceed with installation.'
})
action_result, output = advice_instance.apply(**api_response['metadata'],
ignore_errors=ignore_errors)
if action_result:
self.logger.debug('Applied successfully')
self.adviser_client.update_advice(advice_id=advice_id,
status='applied',
source=source)
try:
self._sync_advice(api_response['metadata']['username'],
api_response['metadata']['domain'],
api_response['metadata']['website'])
except Exception:
self.logger.exception('Error while syncing cl-smart-advice plugin on advice apply')
elif action == AdviceActions.ROLLBACK.value:
action_result, output = advice_instance.rollback(**api_response['metadata'])
if action_result:
self.logger.debug('Rollback successfully')
if reason:
self.adviser_client.update_advice(advice_id=advice_id,
status='review',
source=source,
reason=reason[:advice_reason_max_len])
else:
self.adviser_client.update_advice(advice_id=advice_id,
status='review',
source=source)
try:
self._sync_advice(api_response['metadata']['username'],
api_response['metadata']['domain'],
api_response['metadata']['website'])
except Exception:
self.logger.exception('Error while syncing cl-smart-advice plugin on advice rollback')
else:
raise ValueError(f'Unsupported action with advice, passed action: {action}')
finally:
if os.path.isfile(self._pending_flag(advice_id)):
os.unlink(self._pending_flag(advice_id))
if async_mode:
# in async mode write result to a special file
self.dump_to_file(self._apply_datafile(advice_id),
output)
# return output for analytics will be cleared after reporting
return output
else:
# in sync mode return output
return output
def advice_counters(self) -> str:
"""Return advice counters for a server"""
try:
api_response = self._validate(data=self.adviser_client.advice_list(),
schema=advice_list_schema)
except XRayError:
# bad API responses (500 for example)
# or JWT token failed check (CL Shared for example)
result = dict.fromkeys(['total', 'applied'], None)
else:
result = dict(
total=len(api_response),
applied=len([advice for advice in api_response if
advice['advice']['status'] == 'applied'])
)
return self.response(data=result)
def manage_advice_status(self, advice_id: int) -> str:
"""
Return current status and progress of managing particular advice.
"""
api_response, advice_instance = self.get_detailed_advice(advice_id)
status, stages = self.get_current_status(advice_id, advice_instance,
api_response)
subscription_status = self._get_subscription_status(
advice_instance.module_name, api_response['metadata']['username'])
if status != 'pending':
# include result of apply/rollback for non-pending advice only
result = self._apply_results(advice_id)
# drop stored progress
_progress = self._progress_file(advice_id)
if os.path.isfile(_progress):
os.unlink(_progress)
if result:
return self.response(
status=status,
subscription=dict(
object_cache=subscription_status
),
upgrade_url=get_subscription_upgrade_url(
advice_instance.module_name,
api_response['metadata']['username']),
**stages,
data=result)
return self.response(
status=status,
subscription=dict(
object_cache=subscription_status
),
upgrade_url=get_subscription_upgrade_url(
advice_instance.module_name,
api_response['metadata']['username']),
**stages
)
def _sync_advice(self, username, domain, website):
current_advices = self._validate(data=self.adviser_client.advice_list(),
schema=advice_list_schema)
current_advices_by_site = self.group_alive_advice_ids_by_website(current_advices)
try:
self._run_smart_advice_script(username, website, domain,
self.prepare_advices_response(current_advices,
status_from_microservice=True, extended=True))
except Exception:
self.logger.exception('Smart Advice plugin sync failed for website: %s', str(website))
return
cached_advices_by_site = self._read_advices_cache()
key = f'{domain}{advice_cache_separator}{username}{advice_cache_separator}{website}'
if key not in cached_advices_by_site:
cached_advices_by_site[key] = {}
cached_advices_by_site[key] = current_advices_by_site[key]
self._update_advices_cache(cached_advices_by_site)
def _get_cdn_usage_statistics(self, username):
from xray.adviser.awp_provision_api import AWPProvisionAPI
acc_id = get_user_auth_key(username)
cdn_usage = AWPProvisionAPI().awp_client_api.get_usage(acc_id)
return cdn_usage
@username_verification
def get_options(self, username):
"""
gets options:
"panel_type": "cpanel",
"panel_url": "https:/cpanel-foo.com/", # or https://plesk-bar.com/
"panel_emails": "root@hosting.com,manager@hostring.com,foo@bar.com",
"upgrade_url": "https://...",
"upgrade_url_cdn": "https://...",
"subscription": dict,
"notifications": dict,
"""
try:
# [("domain.com"),("/path/to/docroot")]
domain = userdomains(username)[0][0]
email = get_user_emails_list(username, domain)
except Exception:
self.logger.exception('Unable to get user emails')
email = ''
try:
subscription = get_subscriptions_info(username)
except Exception:
self.logger.exception('Cannot obtain subscription info')
subscription = {}
notifications = dict(
email_status="disabled_server" if is_smart_advice_notifications_disabled_server_wide() is True else "enabled",
reminders_status="disabled_server" if is_smart_advice_reminders_disabled_server_wide() is True else "enabled",
)
return self.response(**asdict(
SmartAdviceOptions(panel_type=getCPName(),
panel_url=self._get_user_awp_link(username),
panel_emails=email,
upgrade_url=get_subscription_upgrade_url('object_cache', username),
upgrade_url_cdn=get_subscription_upgrade_url('cdn', username),
subscription=subscription,
notifications=notifications)
))
def sync_advices_wordpress_plugin(self, current_advices=None):
if not current_advices:
current_advices = self._validate(data=self.adviser_client.advice_list(),
schema=advice_list_schema)
current_advices_by_site = self.group_alive_advice_ids_by_website(current_advices)
if not current_advices_by_site:
return self.response(data='No advices found on the server')
cached_advices_by_site = self._read_advices_cache()
updated_cache = self._sync_sa_plugin(current_advices_by_site, cached_advices_by_site,
current_advices_json=self.prepare_advices_response(current_advices,
extended=True))
self._update_advices_cache(updated_cache)
return self.response(data='Plugin installed')
def _sync_sa_plugin(self, current_advices, cached_advices, current_advices_json=None):
filtered_advices_by_user = None
updated_advices = {}
for key, id_status_hash in current_advices.items():
(domain, username, website) = key.split(advice_cache_separator)
if not self._should_sync_website(id_status_hash, cached_advices.get(key)):
updated_advices[key] = id_status_hash
continue
# keep only current user advices
if current_advices_json:
filtered_advices_by_user = [advice for advice in current_advices_json
if advice.get('metadata', {}).get('username') == username]
try:
self._run_smart_advice_script(username, website, domain, filtered_advices_by_user)
except Exception:
self.logger.exception('Smart advice plugin sync failed for website: %s', str(website))
continue
updated_advices[key] = id_status_hash
return updated_advices
@staticmethod
def _update_advices_cache(new_cached_advices):
with open(f'{advice_list_cache}.lock', 'a+') as lock_fd:
with filelock(lock_fd):
with open(advice_list_cache, 'w') as f:
json.dump(new_cached_advices, f, indent=2)
def _read_advices_cache(self):
cached_advices_by_site = {}
if os.path.exists(advice_list_cache):
try:
with open(advice_list_cache) as f:
cached_advices_by_site = json.load(f)
except Exception:
self.logger.exception('Unable to read advices cache json')
cached_advices_by_site = {}
return cached_advices_by_site
@staticmethod
def _should_sync_website(current_hash, cached_hash):
return current_hash != cached_hash
@staticmethod
def make_hash(id_status_pairs):
h = hashlib.md5()
for advice_id, status in id_status_pairs:
h.update(str(advice_id).encode())
h.update(status.encode())
return h.hexdigest()
def group_alive_advice_ids_by_website(self, advices):
websites = {}
for advice in advices:
metadata = advice['metadata']
advice_data = advice['advice']
advice_instance = self.get_advice_instance(advice_data['type'])
module_name = advice_instance.module_name
if is_feature_hidden_server_wide(module_name):
continue
key = f'{metadata["domain"]}{advice_cache_separator}{metadata["username"]}' \
f'{advice_cache_separator}{metadata["website"]}'
if key not in websites:
websites[key] = []
websites[key].append((advice_data['id'], advice_data['status']))
return {
key: self.make_hash(tuple(sorted(id_status_pairs, key=lambda item: item[0])))
for key, id_status_pairs in websites.items()
}
@staticmethod
def _save_custom_php(path, content):
"""
For easy mocking
"""
with open(path, 'w') as f:
f.write(content)
def _prepare_php_for_plesk(self, username, domain):
clwpos_dir = f'{pwd.getpwnam(username).pw_dir}/.clwpos'
custom_php = '.php-ini'
custom_php_full = os.path.join(clwpos_dir, custom_php)
extra_paths = [
'/opt/alt/php-xray/',
'/opt/cloudlinux-site-optimization-module/',
'/opt/cloudlinux/'
]
updated_setting = None
open_basedir_setting = subprocess.run(f'/usr/sbin/plesk bin site --show-php-settings {domain} | /usr/bin/grep open_basedir',
text=True, capture_output=True, shell=True)
open_basedir_setting_sanitized = open_basedir_setting.stdout.replace('open_basedir =', '').replace('open_basedir=', '').strip()
self.logger.info('Current open_basedir settings: %s', str(open_basedir_setting.stdout))
# empty string means no restrictions; no need to add the extra paths
if len(open_basedir_setting_sanitized) == 0:
updated_setting = 'open_basedir ='
# non-empty string and not "none"
elif len(open_basedir_setting_sanitized) > 0 and open_basedir_setting_sanitized != 'none':
# if 'none' is in the list of values obtained by splitting the string
if 'none' in open_basedir_setting_sanitized.split(':'):
updated_setting = 'open_basedir = none'
elif open_basedir_setting_sanitized.startswith(':'):
updated_setting = 'open_basedir = {WEBSPACEROOT}{/}{:}{TMP}{/}:' + ":".join(extra_paths)
else:
# collect missing extra paths
missing_paths = []
for extra_path in extra_paths:
# extra path not present
if extra_path not in open_basedir_setting.stdout:
missing_paths.append(extra_path)
# some extra paths are missing, settings update is required
if missing_paths:
updated_setting = f'open_basedir = {open_basedir_setting_sanitized}:{":".join(missing_paths)}'
if updated_setting:
self.logger.info('Updating open_basedir setting: %s for domain: %s', updated_setting, domain)
# Both folder for custom PHP ini file and the file itself should be owned by appropriate user. We don't want
# to leave files owned by root in user's home directory.
with drop_privileges(username):
if not os.path.exists(custom_php_full):
if not os.path.isdir(clwpos_dir):
os.mkdir(clwpos_dir, mode=0o700)
self._save_custom_php(custom_php_full, updated_setting)
result = subprocess.run(['/usr/sbin/plesk', 'bin', 'site', '--update-php-settings', domain, '-settings',
custom_php_full],
text=True, capture_output=True)
self.logger.info('Plesk settings update result: %s', str(result.stdout))
self.logger.error('open_basedir setting updated for domain "%s" from "%s" to "%s"',
str(domain),
str(open_basedir_setting.stdout),
str(updated_setting))
def _run_smart_advice_script(self, username, website, domain, advices_json=None):
if advices_json is None:
json_string = self.response(data=[])
else:
json_string = self.response(data=advices_json)
try:
attrs = self._prepare_smart_advice_script_args(username, website, domain, json_string)
except ValueError:
return
command = self._get_smart_advice_script_cmd(username, attrs)
result = subprocess.run(command, text=True, capture_output=True)
if result.returncode != 0:
self.logger.error('Smart advice plugin failed with exit code: %s, stdout: %s, stderr: %s',
str(result.returncode),
str(result.stdout),
str(result.stderr),
extra={
'fingerprint': f'smart-advice-plugin-sh-returncode-{result.returncode}',
})
raise SmartAdvicePluginError('Smart Advice plugin installation failed')
def _prepare_smart_advice_script_args(self, username, website, domain: List[str], advices_json: str) -> List[str]:
panel = getCPName()
script_name = 'run.sh'
script_path = f'/opt/alt/php-xray/php/smart-advice-plugin/{script_name}'
panel_user_awp_link = self._get_user_awp_link(username)
self.logger.info('PANEL AWP LINK %s', panel_user_awp_link)
try:
email = get_user_emails_list(username, domain)
except Exception:
self.logger.exception('Unable to get user emails')
email = ''
full_website_path = docroot(domain)[0] + website
if not is_wp_path(full_website_path):
self.logger.info('[Smart Advice Plugin]: wordpress: %s does not exist, skipped',
full_website_path)
raise ValueError('Non-existing wordpress site')
php_info = get_user_php_version(username)
for php_data in php_info:
if php_data.get('vhost') == domain:
php_binary_path = php_data.get('php_binary')
break
else:
self.logger.error('Php data for domain %s was not found in %s', domain, str(php_info))
raise SmartAdvicePluginError(f'Unable to get php version for domain {domain}')
if not php_binary_path:
self.logger.error('Php version for user: %s was not obtained',
username)
raise SmartAdvicePluginError(f'Php binary was not identified for domain {domain}')
if panel == 'Plesk':
try:
self._prepare_php_for_plesk(username, domain)
except Exception:
self.logger.exception('Setting up php for Plesk for SA plugin failed')
res = [
script_path,
php_binary_path,
full_website_path,
advices_json,
email,
panel_user_awp_link,
]
return res
@staticmethod
def _get_smart_advice_script_cmd(username: str, attrs: List[str]) -> List[str]:
if is_panel_feature_supported(Feature.CAGEFS):
res = ['/sbin/cagefs_enter_user', username, '/bin/bash']
res.extend(attrs)
else:
attrs[3] = "'" + attrs[3] + "'" # escape json string
command_str = '/bin/bash ' + ' '.join(attrs)
res = ['sudo', '-u', username, '-s', '/bin/bash', '-c', command_str]
return res
def get_agreement_text(self, feature):
"""
Method with handles work with agreements.
"""
user_context = get_xray_exec_user()
if not user_context:
raise SmartAdvicePluginError('This command can only be executed as user')
return json.dumps({
'result': 'success',
'timestamp': timestamp(),
'text': get_license_agreement_text(feature, user_context)
})
def create_subscription(self, advice_id):
"""
Proxy method to AWP which creates subscription
listening for the status of module and automatic advice appy.
"""
user_context = get_xray_exec_user()
if not user_context:
raise SmartAdvicePluginError('This command can only be executed as user')
_, advice_instance = self.get_detailed_advice(advice_id)
result = subprocess.run([
'cloudlinux-awp-user',
'--user', str(user_context),
'subscription',
'--listen',
'--advice-id', str(advice_id),
'--feature', advice_instance.module_name
], text=True, capture_output=True)
if result.returncode != 0:
self.logger.error('Create subscription smart advice plugin failed with exit code: '
'%s, stdout: %s, stderr: %s',
str(result.returncode),
str(result.stdout),
str(result.stderr),
extra={
'fingerprint': f'smart-advice-plugin-subscription-returncode-{result.returncode}',
})
raise SmartAdvicePluginError('cloudlinux-awp-user call failed %s' % result.stderr)
|