Mini Shell
# -*- 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 a wrapper around `clwpos-user get` local utility
"""
import json
import logging
import os
import subprocess
import multiprocessing
from typing import Optional
from clcommon.const import Feature
from clcommon.cpapi import is_panel_feature_supported
from clsummary.summary import CloudlinuxSummary
try:
from clwpos.papi import is_wpos_visible
except ImportError:
# case when wpos is not installed yet
is_wpos_visible = lambda username: None
from ..apiclient import get_client
from ..internal.nginx_utils import NginxUserCache
from xray.smart_advice_plugin_helpers import get_plugin_status
logger = logging.getLogger('clwpos_util')
class ClWposGetter:
util = "/usr/bin/clwpos-user"
def post_metadata(self, username: str, domain: str) -> None:
"""Construct and POST metadata to Smart Advice microservice"""
if self.nginx_cache_for_user(username):
logger.info('ea-nginx detected, skipping metadata send')
return
json_data = self.construct_metadata(username, domain)
logger.debug('Got WPOS: %s', str(json_data))
if json_data:
self.send(json_data)
else:
logger.error('Metadata for user %s with domain %s will not be sent', username, domain)
@staticmethod
def nginx_cache_for_user(username: str) -> bool:
"""
Check nginx cache status for given user
"""
return NginxUserCache(username).is_enabled
@property
def wrapper(self) -> Optional[str]:
"""Special hack for executing user commands on Solo"""
if not is_panel_feature_supported(Feature.CAGEFS):
return f'sudo -u %(username)s -s /bin/bash -c'
@property
def cmd(self) -> str:
"""Resolve command to execute"""
if not is_panel_feature_supported(Feature.CAGEFS):
return f'{self.util} scan --website %(website)s'
else:
return f'/sbin/cagefs_enter_user %(username)s {self.util} scan --website %(website)s'
def utility(self, username: str, domainname: str) -> Optional[dict]:
"""
External call of `clwpos-user get` utility
"""
if not os.path.isfile(self.util):
return
# resolve concrete command to execute
if self.wrapper is not None:
_exec = (self.wrapper % {'username': username, 'website': domainname}).split()
_exec.append(self.cmd % {'username': username, 'website': domainname})
else:
_exec = (self.cmd % {'username': username, 'website': domainname}).split()
try:
# check return code instead of check=True, because CalledProcessError may not store
# stout/stderr
result = subprocess.run(_exec, capture_output=True, text=True)
# in case something really bad happened to process (killed/..etc)
except (OSError, ValueError, subprocess.SubprocessError) as e:
logger.error('Error running %s: %s', self.util, e)
return None
if result.returncode != 0:
logger.error('Metadata collection via %s failed. stdout: %s, stderr: %s',
self.util,
str(getattr(result, 'stdout', None)),
str(getattr(result, 'stderr', None)))
return None
try:
return json.loads(result.stdout.strip())
except json.JSONDecodeError:
logger.error('Invalid JSON from %s for metadata collection. stdout: %s',
self.util,
str(getattr(result, 'stdout', None)))
return None
@staticmethod
def get_advices_for_website(advices, user, domain, website):
"""
Iterate through advices and return only those which relate to current
user-domain-site
"""
return [
advice
for advice in advices
if advice['metadata']['username'] == user and
advice['metadata']['domain'] == domain and
advice['metadata']['website'] == website
]
def get_updated_extended_metadata(self, username, domain, current_advices):
"""
For getting extended metadata which will be sent daily by cron
"""
final_metadata = {}
websites_metadata = self.utility(username, domain)
if not websites_metadata:
return final_metadata
websites = []
for site, issues in websites_metadata['issues'].items():
path = f'/{site}'
website_issues = websites_metadata['issues'].get(site, [])
advice_for_website = self.get_advices_for_website(current_advices, username, domain, path)
try:
smart_advice_plugin_status = get_plugin_status(username,
domain,
path,
website_issues,
advice_for_website)
except Exception:
logger.exception('Getting Smart Advice plugin status failed')
websites.append(dict(path=path,
issues=website_issues))
else:
websites.append(dict(path=path,
issues=website_issues,
wp_plugin_status=smart_advice_plugin_status))
final_metadata = {
'username': username,
'domain': domain,
'websites': websites,
'server_load_rate': self.server_load_rate(),
}
return final_metadata
def construct_metadata(self, username: str, domainname: str) -> dict:
"""
Ensure format accepted by Smart Advice POST requests/metadata endpoint
"""
dummy_result = None
data = self.utility(username, domainname)
if data is not None:
dummy_result = {'username': username,
'domain': domainname,
'websites':
[dict(path=f"/{site}",
issues=data['issues'].get(site, []))
for site, issues in data['issues'].items()
],
'server_load_rate': self.server_load_rate()}
return dummy_result
@staticmethod
def server_load_rate() -> float:
""""""
try:
domains_total = CloudlinuxSummary._get_total_domains_amount()
except Exception as e:
# something went wrong while querying Summary
logger.error('Unable to get domains_total stats: %s', str(e))
return -1.0
# returns None is cpu_count is undetermined, assume 1 CPU thus
cpu_count = multiprocessing.cpu_count() or 1.0
return domains_total / cpu_count
@staticmethod
def send(stat: dict) -> None:
"""
Send gathered metadata to adviser miscroservice.
Ignore sending if websites are empty
"""
if stat['websites']:
client = get_client('adviser')
client().send_stat(data=stat)
Zerion Mini Shell 1.0