HOME


Mini Shell 1.0
DIR: /opt/cloudlinux/venv/lib64/python3.11/site-packages/xray/adviser/
Upload File :
Current File : //opt/cloudlinux/venv/lib64/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

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.warning('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

        # when daemon is not running user has no way to understand which
        # php versions are used on server
        if not is_wpos_visible(username):
            return None

        # 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

    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)