ok

Mini Shell

Direktori : /proc/thread-self/root/opt/cloudlinux/venv/lib/python3.11/site-packages/xray/adviser/
Upload File :
Current File : //proc/thread-self/root/opt/cloudlinux/venv/lib/python3.11/site-packages/xray/adviser/clwpos_get.py

# -*- 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