Mini Shell

Direktori : /opt/cloudlinux/venv/lib/python3.11/site-packages/lvestats/lib/
Upload File :
Current File : //opt/cloudlinux/venv/lib/python3.11/site-packages/lvestats/lib/cloudlinux_statistics.py

# coding=utf-8
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT

import json
import logging
import math
import os
from collections import defaultdict
from datetime import datetime  # NOQA
from typing import Dict, List, Optional, Tuple, Union  # NOQA

import sqlalchemy  # NOQA

import lvestats.lib.commons.decorators
from clcommon import cpapi
from clcommon.cpapi.pluginlib import getuser
from clcommon.lib import GovernorStatus, MySQLGovernor, MySQLGovException
from lvestats.lib import config
from lvestats.lib.commons import dateutil, func
from lvestats.lib.commons.func import deserialize_lve_id, get_users_for_reseller
from lvestats.lib.commons.logsetup import setup_logging
from lvestats.lib.commons.users_manager import g_users_manager
from lvestats.lib.dbengine import make_db_engine
from lvestats.lib.info.lveinfomain import main_
from lvestats.lib.lve_list import LVEList
from lvestats.lib.lveinfolib import HistoryShowUnion
from lvestats.lib.lveinfolib_gov import HistoryShowDBGov
from lvestats.lib.parsers.cloudlinux_statistics_parser import setup_statistics_parser
from lvestats.lib.uidconverter import (
    username_to_uid_local,
    reseller_to_uid,
    uid_to_username,
)

NOT_AVAILABLE = 'N/A'


class CloudLinuxStatistics(object):
    def __init__(
            self,
            engine,  # type: sqlalchemy.engine.base.Engine
            show_columns,  # type: List[str]
            order_by='',  # type: str
            server_id=None,  # type: Optional[str]
            by_usage=None,  # type: Optional[str]
            by_usage_percentage=0.9,  # type: float
            by_fault=None,  # type: Optional[str]
            threshold=1,  # type: int
            limit=0,  # type: int
            uid=None,  # type: Optional[Union[int, List[int], Tuple[int]]]
            time_unit=60  # type: int
    ):
        """
        Initializes statistics object used by SPA UI. Note, that if we set order_by to mysql_*, and set
        by_usage or by_fault, end result will will not be ordered by mysql_ column, using uid for ordering instead
        """
        self.order_by_lve = None
        self.order_by_gov = None
        self.server_id = server_id
        self.engine = engine
        self.limit_gov = 0
        self.limit_lve = 0
        self.uid = uid
        self.time_unit = time_unit
        self.is_many_uids = self.uid is None or isinstance(self.uid, (list, tuple))
        self._lve_results = []
        self._gov_results = []
        self._resellers_results = []

        if 'all' in show_columns:
            show_columns = ['cpu', 'ep', 'vmem', 'pmem', 'nproc', 'io', 'iops', 'nproc', 'mysql']
        self.show_columns = [s.lower() for s in show_columns]
        self.by_fault = self.normalize_by_fault(by_fault)
        self.order_by = order_by
        self.init_order_by(order_by, self.by_fault)
        self._show_columns_lve = []
        self._show_columns_gov = []
        self._gov_key_index = None
        self._lve_key_index = None
        self.init_show_columns()
        if self.order_by_gov:
            self.limit_gov = limit
        else:
            self.limit_lve = limit
        self.limit = limit
        self.by_usage_lve = None
        self.by_usage_gov = None
        self.init_by_usage(by_usage)
        self.by_usage_percentage = by_usage_percentage
        self.threshold = threshold

        self.mysql_governor = MySQLGovernor()
        self.log = logging.getLogger(self.__class__.__name__)
        self.show_mysql = self._show_mysql()
        self._admins_uids = self._get_admin_uids() if getuser() == 'root' else set()

    @staticmethod
    def _get_admin_uids():
        try:
            return set(username_to_uid_local(admin) for admin in cpapi.admins()) - {None}
        except (cpapi.NotSupported, AttributeError):
            return set()

    def _show_mysql(self):
        # type: () -> bool
        return self.mysql_governor.is_governor_present() and 'mysql' in self.show_columns

    def _index_of(self, list_, key):
        # type: (List, Union[str,int]) -> Optional[int]
        if key in list_:
            return list_.index(key)

    def init_show_columns(self):
        show_columns_lve = []
        show_columns_gov = []
        show_columns_gov_singleuser = ['from', 'to']
        show_columns_lve_singleuser = ['id', 'from', 'to']
        columns = []
        for column in self.show_columns:
            if column != 'mysql':
                columns += ['a' + column, 'l' + column, column + 'f']
        if len(columns):
            show_columns_lve = ['id'] + columns
            show_columns_lve_singleuser += columns

        if 'mysql' in self.show_columns:
            show_columns_gov = ['id', 'cpu', 'lcpu', 'read', 'lread', 'write', 'lwrite']
            show_columns_gov_singleuser = show_columns_gov + ['from', 'to']

        self._show_columns_lve = show_columns_lve if self.is_many_uids else show_columns_lve_singleuser
        self._show_columns_gov = show_columns_gov if self.is_many_uids else show_columns_gov_singleuser
        if self.is_many_uids:
            self._gov_key_index = self._index_of(self._show_columns_gov, 'id')
            self._lve_key_index = self._index_of(self._show_columns_lve, 'id')
        else:
            self._gov_key_index = self._index_of(self._show_columns_gov, 'from')
            self._lve_key_index = self._index_of(self._show_columns_lve, 'from')

    def init_order_by(self, order_by, by_fault):
        # type: (str) -> None
        if order_by:
            order_by = order_by.lower()
            if order_by.endswith('faults'):
                self.order_by_lve = order_by.replace('_faults', '') + 'f'
            elif order_by.startswith('mysql'):
                self.order_by_gov = order_by[len('mysql_'):]
            else:
                self.order_by_lve = 'a' + order_by
        elif by_fault and by_fault[0] == 'any':
            self.order_by_lve = 'cpuf'
        else:
            self.order_by_lve = by_fault and by_fault[0]

    def get_username(self, uid):
        # type: (int) -> Tuple[str]
        username = uid_to_username(uid, self.server_id, self.server_id, self.engine)
        return username or NOT_AVAILABLE

    def _get_default_user_usage(self):
        """
        Return default info about user usage (mysql)
        :rtype: dict
        """
        usage_field = defaultdict(dict)
        for column in self.show_columns:
            if column != 'mysql':
                usage_field[column]['lve'] = 0.0
        if self.show_mysql:
            usage_field['cpu']['mysql'] = 0.0
            usage_field['io']['mysql'] = 0.0
        return usage_field

    def _get_default_user_limits(self, username):
        """
        Return default info about user limits (mysql)
        :rtype: dict
        """
        limit_field = defaultdict(dict)
        for column in self.show_columns:
            if column != 'mysql':
                limit_field[column]['lve'] = 0.0
        if self.show_mysql:
            cpu_limit, io_limit = self.get_user_limits_safe(username)
            limit_field['cpu']['mysql'] = cpu_limit
            limit_field['io']['mysql'] = io_limit
        return limit_field

    def _get_default_user_faults(self):
        fault_field = {}
        for column in self.show_columns:
            if column != 'mysql':
                fault_field[column] = {'lve': 0}
        return fault_field

    def get_item(self, ikey):
        if self.is_many_uids:
            _, is_reseller = deserialize_lve_id(ikey)
            username = self.get_username(ikey)
        else:
            _, is_reseller = deserialize_lve_id(self.uid)
            username = self.get_username(self.uid)
        item = {
            'usage': self._get_default_user_usage(),
            'limits': self._get_default_user_limits(username),
        }
        if self.show_columns != ['mysql']:
            item['faults'] = self._get_default_user_faults()
        if self.is_many_uids:
            if is_reseller:
                item.update({'id': ikey, 'name': username})
            else:
                item.update({
                    'id': ikey,
                    'username': username,
                    'domain': g_users_manager.get_domain(username, raise_exc=False) or NOT_AVAILABLE,
                    'reseller': g_users_manager.get_reseller(username, raise_exc=False) or NOT_AVAILABLE,
                })
        else:
            item.update({
                'from': int(ikey),
                'to': int(ikey) + self.time_unit})
        return item

    def fill_lve_item(self, item, row):
        if row is None:
            return item
        count = 1 if self.is_many_uids else 1 + 2
        for column in self.show_columns:
            if column == 'mysql':
                continue
            item['usage'][column]['lve'] = row[count]
            item['limits'][column]['lve'] = row[count + 1]
            item['faults'][column]['lve'] = row[count + 2]
            count += 3
        return item

    @staticmethod
    def __get_io_bytes(read, write):
        """
        Get io value by read and write values.
        :param int|float|None read: read speed in MEGAbytes
        :param int|float|None write: write speed in MEGAbytes
        :return int: io, BYTES
        """
        # convert None to zero
        read = read or 0
        write = write or 0

        # calculate io in bytes
        io = (read + write) * 1024 * 1024
        return int(io)

    def fill_gov_item(self, item, row):
        if row is None:
            return item
        item['usage']['cpu']['mysql'] = round(float(row[1] or 0), 1)
        item['limits']['cpu']['mysql'] = round(float(row[2] or 0), 1)
        item['usage']['io']['mysql'] = self.__get_io_bytes(row[3], row[5])
        item['limits']['io']['mysql'] = self.__get_io_bytes(row[4], row[6])
        return item

    def process_data(self, lve_results, gov_results, period_from, period_to):
        from_gov_to_lve_indexes, from_lve_to_gov_indexes,\
            gov_keys, gov_results,\
            lve_keys, lve_results,\
            not_in_gov_indexes, not_in_lve_indexes = self.process_indexes(gov_results, lve_results)
        if not self.show_mysql:
            lve_results = list(lve_results)
        elif self.order_by_gov:
            lve_results = self.sort_by_another_list(from_gov_to_lve_indexes, lve_results, not_in_gov_indexes)
        elif self.order_by_lve:
            gov_results = self.sort_by_another_list(from_lve_to_gov_indexes, gov_results, not_in_lve_indexes)
        else:
            gov_results, lve_results = self.sort_by_key(gov_keys, gov_results, lve_keys, lve_results)
        lendiff = (len(lve_results) - len(gov_results))
        if lendiff != 0:
            from_formatted = period_from.strftime('%Y-%m-%d %H:%M')
            to_formatted = period_to.strftime('%Y-%m-%d %H:%M')
            self.log.warning(
                'len(lve_results) != len(gov_results), it might cause duplicated timestamps '
                'or mix of lve and governor stats from different periods in aggregated data.\n'
                'Utility was called with the following parameters:\n'
                'cloudlinux-statistics --json --time-unit %ss --id %s '
                '--from %s --to %s '
                '--show %s --server_id %s\n'
                'engine=%s\norder_by=%s\nby_fault=%s\n',
                self.time_unit, self.uid, from_formatted, to_formatted,
                " ".join(self.show_columns), self.server_id,
                self.engine, self.order_by, self.by_fault
            )
        self._lve_results = lve_results
        self._gov_results = gov_results

    @staticmethod
    def sort_by_key(gov_keys, gov_results, lve_keys, lve_results):
        # type: (List[int], List, List[int], List) -> Tuple[List, List]
        lve_pos = 0
        gov_pos = 0
        lve_len = len(lve_results)
        gov_len = len(gov_results)
        _lve_results = []
        _gov_results = []
        # maximum # of iterations will be in case when
        # lve_keys like [1,2,7] and gov_keys like [4,5,6]
        for _ in range(lve_len + gov_len):
            if lve_pos == lve_len:  # became out of range
                _lve_results.extend([None] * (gov_len - gov_pos))
                # if also gov_pos == gov_len slice returns [] without error
                _gov_results.extend(gov_results[gov_pos:])
                break
            if gov_pos == gov_len:
                _gov_results.extend([None] * (lve_len - lve_pos))
                _lve_results.extend(lve_results[lve_pos:])
                break
            if lve_keys[lve_pos] == gov_keys[gov_pos]:
                _lve_results.append(lve_results[lve_pos])
                _gov_results.append(gov_results[gov_pos])
                lve_pos += 1
                gov_pos += 1
            elif lve_keys[lve_pos] > gov_keys[gov_pos]:
                _lve_results.append(None)
                _gov_results.append(gov_results[gov_pos])
                gov_pos += 1
            else:
                _gov_results.append(None)
                _lve_results.append(lve_results[lve_pos])
                lve_pos += 1
        return _gov_results, _lve_results

    @staticmethod
    def sort_by_another_list(indexes, results, not_in_indexes):
        # type: (List[int], List, List[int]) -> List
        _results = []
        for index in indexes:
            if index is None:
                _results.append(None)
            else:
                _results.append(results[index])
        for index in not_in_indexes:
            _results.append(results[index])
        results = _results
        return results

    def process_indexes(self, gov_results, lve_results):
        # type: (List, List) -> Tuple[List[int],List[int],List,List,List,List,List,List]
        if self.by_usage_gov:
            gov_uids = [_[0] for _ in gov_results]
            lve_results = [row for row in lve_results if row[0] in gov_uids]
        if self.by_usage_lve or self.by_fault:
            lve_uids = [_[0] for _ in lve_results]
            gov_results = [row for row in gov_results if row[0] in lve_uids]
        gov_keys = [_[self._gov_key_index] for _ in gov_results]
        lve_keys = [_[self._lve_key_index] for _ in lve_results]
        from_gov_to_lve_indexes = self.get_indexes(gov_keys, lve_keys)
        not_in_lve_indexes = [i for i, _ in enumerate(from_gov_to_lve_indexes) if _ is None]
        from_lve_to_gov_indexes = self.get_indexes(lve_keys, gov_keys)
        not_in_gov_indexes = [i for i, _ in enumerate(from_lve_to_gov_indexes) if _ is None]
        return from_gov_to_lve_indexes, from_lve_to_gov_indexes,\
            gov_keys, gov_results,\
            lve_keys, lve_results,\
            not_in_gov_indexes, not_in_lve_indexes

    def get_indexes(self, first_uids, second_uids):
        # type: (List[int], List[int]) -> List[int]
        return [self._index_of(second_uids, uid) for uid in first_uids]

    def get_value(self, key, resellers):
        # type: (int, bool) -> Dict[str, Union[int,float]]
        """
        :type resellers: bool
        :type key: int
        :rtype: dict
        """
        if resellers:
            item = self.get_item(self._resellers_results[key][self._lve_key_index])
            return self.fill_lve_item(item, self._resellers_results[key])
        elif not self.limit or key < self.limit:
            if not self.show_mysql:
                item = self.get_item(self._lve_results[key][self._lve_key_index])
                return self.fill_lve_item(item, self._lve_results[key])
            elif self.order_by_gov or self.limit_gov:
                row = self._gov_results[key]
                if row is not None:
                    item = self.get_item(row[self._gov_key_index])
                    self.fill_gov_item(item, row)
                else:
                    item = self.get_item(self._lve_results[key][self._lve_key_index])
                return self.fill_lve_item(item, self._lve_results[key])
            else:
                row = self._lve_results[key]
                if row is not None:
                    item = self.get_item(row[self._lve_key_index])
                    self.fill_lve_item(item, row)
                else:
                    item = self.get_item(self._gov_results[key][self._gov_key_index])
                return self.fill_gov_item(item, self._gov_results[key])
        else:
            raise IndexError

    def get_gov_row(self, key, indexes, result):
        if key > len(indexes) + 1 or indexes[key] is None:
            return None
        return result[indexes[key]]

    def get_user_limits_safe(self, username, ignore_watched=False):
        # type: (str, bool) -> Tuple[int,int]
        """
        Get cpu and io limit for user
        :return tuple: cpu limit in %, io limit in bytes/s
        """
        cpu_limit, io_limit = 0, 0
        if self.mysql_governor.is_governor_present():
            try:
                if ignore_watched or self.mysql_governor.get_governor_status_by_username(username) == 'watched':
                    cpu_limit, io_limit = self.mysql_governor.get_limits_by_user(username)
            except MySQLGovException as e:
                self.log.info('Cannot get mysql limits for user %s. Error raised: %s', username,
                              e.message % e.context)
        return cpu_limit, io_limit * 1024

    @staticmethod
    def normalize_by_fault(by_fault):
        # type: (Optional[str]) -> Optional[Tuple[str]]
        if by_fault:
            if by_fault == 'mem':
                by_fault = 'vmem'
            return by_fault + 'f',  # it is a tuple, so we need coma at the end

    def init_by_usage(self, by_usage):
        # type: (Optional[str]) -> None
        if by_usage:
            if by_usage.startswith("mysql"):
                self.by_usage_gov = by_usage[len('mysql_'):]
            else:
                self.by_usage_lve = 'a' + by_usage,  # it is a tuple, so we need coma at the end

    def generate_statistics(self, period_from, period_to):
        # type: (datetime, datetime) -> None
        """Generate statistics."""
        period_from_gm = dateutil.local_to_gm(period_from)
        period_to_gm = dateutil.local_to_gm(period_to)

        if self._show_columns_lve:
            history_show = HistoryShowUnion(
                dbengine=self.engine,
                period_from=period_from_gm, period_to=period_to_gm,
                uid=self.uid,
                time_unit=self.time_unit,
                show_columns=self._show_columns_lve,
                server_id=self.server_id,
                order_by=self.order_by_lve,
                by_usage=self.by_usage_lve,
                by_usage_percentage=self.by_usage_percentage,
                by_fault=self.by_fault,
                threshold=self.threshold,
                limit=self.limit_lve)
            history_show.set_normalised_output()
            lve_results = history_show.proceed()
        else:
            lve_results = []

        if self.show_mysql:
            gov_results = HistoryShowDBGov(
                dbengine=self.engine,
                period_from=period_from_gm,
                period_to=period_to_gm,
                time_unit=self.time_unit,
                server_id=self.server_id,
                show_columns=self._show_columns_gov,
                order_by=self.order_by_gov,
                by_usage=self.by_usage_gov,
                by_usage_percentage=self.by_usage_percentage,
                limit=self.limit_gov,
                reverse=True,
                uid=self.uid,
            ).history_dbgov_show()
        else:
            gov_results = []
        if self.is_many_uids:
            users = []
            for item in lve_results:
                uid, is_reseller = deserialize_lve_id(item[self._lve_key_index])
                if uid in self._admins_uids:
                    continue
                if is_reseller:
                    self._resellers_results.append(item)
                else:
                    users.append(item)
            lve_results = users
        self.process_data(lve_results, gov_results, period_from, period_to)


def _get_uid_for_select(for_reseller, uid):
    # type: (str, int) -> Union[List[int], int]
    """
    Get uid that should be selected from history table;
    """

    if getuser() != 'root':
        return os.getuid()

    g_users_manager.build_users_cache(for_reseller)
    if for_reseller:
        users_list = get_users_for_reseller(for_reseller or getuser())
        id_ = list(filter(bool, list(map(username_to_uid_local, users_list))))
        reseller_uid = reseller_to_uid(for_reseller or getuser())
        # LVESTATS-97
        # If uid not in users_list, we return empty list
        if uid:
            return uid if uid in id_ or uid == reseller_uid else []
        # select also information about reseller's containers (if exists)
        if reseller_uid is not None:
            id_.append(reseller_uid)
    else:
        id_ = uid
    return id_


def execute(engine, options, log=None):
    # type: (sqlalchemy.engine.base.Engine, object, Optional[logging.Logger]) -> Tuple[str, int]
    """
    Get command response and exitcode.
    :type log: None | logging.Logger
    :type engine: sqlalchemy.engine.base.Engine
    :rtype: (str, int)
    """
    if not options.json:
        return "Only JSON mode supported for now", -1

    id_ = _get_uid_for_select(options.for_reseller, options.id)
    statistics = get_statistics(engine, id_, log, options)
    json_data = dump_json(statistics)

    return json_data, 0


def dump_json(statistics):
    # type: (Dict) -> str
    json_data = json.dumps(statistics)
    return json_data


def get_statistics(
        engine,  # type: sqlalchemy.engine.base.Engine
        id_,  # type: Optional[Union[int, List[int], Tuple[int]]]
        log,  # type: Optional[logging.Logger]
        options,
        timestamp=dateutil.gm_datetime_to_unixtimestamp()  # type: int
):
    # type: (...) -> Dict[str, Union[LVEList, str, int]]
    result = {
        'result': 'success',
        'timestamp': timestamp
    }
    # LVESTATS-97
    # If both params (for_reseller and id) are used
    # and id_ list is empty, we return `Permission denied` message
    if options.for_reseller is not None and options.id is not None and id_ == []:
        error_msg = (
            'Permission denied. User with id '
            f'{options.id} does not belong reseller `{options.for_reseller}`'
        )
        result['result'] = error_msg
        return result
    try:
        stat = CloudLinuxStatistics(
            engine,
            show_columns=options.show,
            order_by=options.order_by,
            server_id=options.server_id,
            by_usage=options.by_usage,
            by_usage_percentage=options.percentage / 100.,
            by_fault=options.by_fault,
            threshold=options.threshold,
            limit=options.limit,
            uid=id_,
            time_unit=options.time_unit)
        stat.generate_statistics(getattr(options, 'from'), options.to)

        lvelist = LVEList(stat)
        lvelist_resellers = LVEList(stat, resellers=True)
        if stat.is_many_uids:
            result['users'] = lvelist
            result['resellers'] = lvelist_resellers
        else:
            result['user'] = lvelist
    except Exception as e:
        result["result"] = str(e)
        if log:
            log.error(str(e))

    # add mysql status info
    status, error = func.get_governor_status()
    result['mySqlGov'] = status
    if status == GovernorStatus.ERROR:
        result['warning'] = error.message
        result['context'] = error.context
    return result


@lvestats.lib.commons.decorators.no_sigpipe
def main(engine, argv_, server_id='localhost', log=None):
    # type: (sqlalchemy.engine.base.Engine, List[str], str, Optional[logging.Logger]) -> int
    """
    Execute command and return exitcode
    """
    if log is None:
        log = setup_logging(
            {},
            caller_name="CloudLinuxStatistics",
            file_level=logging.WARNING,
            console_level=logging.FATAL,
        )
    options = setup_statistics_parser(argv_, server_id)

    json_str, exit_code = execute(engine, options, log=log)
    print(json_str)
    return exit_code


def get_users_and_resellers_with_faults(period='1d'):
    """
    Return amount of users and resellers with faults
    """
    cnf = config.read_config()
    dbengine = make_db_engine(cnf)
    server_id = cnf.get('server_id', 'localhost')
    opt_list = ['--by-fault', 'any', '--json', '--period=' + period]
    options = setup_statistics_parser(opt_list, server_id)
    json_str, _ = execute(dbengine, options, log=None)
    json_data = json.loads(json_str)
    resellers = json_data['resellers']
    users = json_data['users']
    return len(users), len(resellers)


def get_max_memory_for_lve(lve_id, period_seconds):
    data = main_(
        config.read_config(),
        argv_=['--json', '--id', str(lve_id), '--show-columns',
               'mPMem', '--period', str(int(math.ceil(period_seconds / 60))) + 'm'],
    )
    if data:
        parsed_data = json.loads(data)
    try:
        max_pmem = max(x['mPMem'] for x in parsed_data['data'])
    except (ValueError, KeyError):
        max_pmem = None
    return max_pmem

Zerion Mini Shell 1.0