ok
Direktori : /opt/cloudlinux/venv/lib/python3.11/site-packages/lvestats/plugins/generic/ |
Current File : //opt/cloudlinux/venv/lib/python3.11/site-packages/lvestats/plugins/generic/snapshot_saver.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 from __future__ import absolute_import from __future__ import division from builtins import range import logging import string from random import choice import sqlalchemy.orm.session # pylint: disable=unused-import from clcommon.clpwd import ClPwd from clcommon.cpapi import NotSupported from clcommon.cpapi.cpapiexceptions import NoDBAccessData, NoPackage from clcommon.utils import ExternalProgramFailed from sqlalchemy.orm import sessionmaker from typing import List, Tuple, Dict, Optional # pylint: disable=unused-import from lvestats.core.plugin import LveStatsPlugin from lvestats.lib.commons import proctitle from lvestats.lib.commons.func import get_chunks, reboot_lock from lvestats.lib.commons.htpasswd import HtpasswdFile from lvestats.lib.commons.litespeed import LiteSpeed, LiteSpeedException, LiteSpeedInvalidCredentials from lvestats.lib.snapshot import Snapshot from lvestats.lib.ustate import MySQLOperationalError, SQLSnapshot, get_lveps from lvestats.orm.incident import incident DEFAULT_PERIOD_BETWEEN_INCIDENTS = 300 # time separating incidents DEFAULT_SNAPSHOTS_PER_MINUTE = 2 # number of snapshots per minute DEFAULT_MAX_SNAPSHOTS_PER_INCIDENT = 10 # the maximum number of snapshots in the incident APACHE = 'apache' LITESPEED = 'litespeed' class LitespeedHelper(object): def __init__(self): self.is_running = False self.state_changed = False # config option; None if server should be # detected automatically, # False if we must use apache # True if we must use litespeed self.force_litespeed = None # create random login-password pair self.login = 'lve-stats-admin' self.password = self.generate_random_password() self.broken_config = False def check_litespeed_state(self): """Check litespeed state""" litespeed_running = LiteSpeed.is_litespeed_running() self.state_changed = litespeed_running != self.is_running self.is_running = litespeed_running def dump_passwd(self): try: passwdfile = HtpasswdFile(LiteSpeed.HTPASSWD_PATH) except ValueError: self.broken_config = True logging.warning("Can't change the password. Please, check the file:\n '%s'", LiteSpeed.HTPASSWD_PATH) return passwdfile.update(self.login, self.password) passwdfile.save() logging.debug("Password successfully changed.") def get_user_data(self, username): # type: (str) -> list """Get user data proxy method""" litespeed = LiteSpeed(self.login, self.password) try: return litespeed.get_user_data(username) except LiteSpeedInvalidCredentials: self.dump_passwd() raise @staticmethod def generate_random_password(): # type: () -> str chars = string.ascii_letters + string.digits + '!@#$%^&*()' return ''.join(choice(chars) for _ in range(16)) def get_use_litespeed(self): # type: () -> bool """Get what we must use: litespeed or apache""" is_litespeed_running = self.is_running if self.broken_config: return False elif self.force_litespeed is not None: return self.force_litespeed return is_litespeed_running class SnapshotHelper(object): username_dbquery_map = None ps = dict() clpwd = None sql_snap = SQLSnapshot() def __init__(self): self.litespeed_died = False self._sql_snapshot_works = None self.log = logging.getLogger('SnapshotSaver') def get_names(self, lve_id): # type: (int) -> List[str] try: return self.clpwd.get_names(lve_id) except ClPwd.NoSuchUserException: return list() def get_snapshot_data(self, lve_id, litespeed_info): # type: (int, LitespeedHelper) -> Tuple[dict, list, list] processes = self.ps.get(lve_id, {}).get('TID', {}) queries = [] urls = [] for username in self.get_names(lve_id): queries += self.username_dbquery_map.get(username, []) use_litespeed = litespeed_info.get_use_litespeed() if use_litespeed: try: urls += litespeed_info.get_user_data(username) except LiteSpeedException as e: # do not save message every time if not self.litespeed_died: logging.warning('Error during getting information from Litespeed: %s' % str(e)) self.litespeed_died = True urls += proctitle.Proctitle().get_user_data(username) else: urls += proctitle.Proctitle().get_user_data(username) return processes, queries, urls def invalidate(self, lve_id_list): # type: (List[int]) -> None self.clpwd = ClPwd() all_usernames = [] for lve_id in lve_id_list: all_usernames += self.get_names(int(lve_id)) self.username_dbquery_map = self.retrieve_queries(all_usernames) try: self.ps = get_lveps() except ExternalProgramFailed as e: self.log.warning('An error occurred while getting processes list; %s', str(e)) def retrieve_queries(self, login_names): # type: (Optional[List[str]]) -> Dict[str, List[str]] try: with self.sql_snap as db_requests: result = db_requests.get(login_names) if not self._sql_snapshot_works: self._sql_snapshot_works = True self.log.info('SQL snapshot is supported and operational') return result except MySQLOperationalError as e: self.log.warning('An error occurred while getting MySQL process list; %s', str(e)) except (NotSupported, NoDBAccessData, NoPackage) as e: # errors which we can write only once # because who needs this message in log each 5 seconds? if self._sql_snapshot_works in [None, True]: self._sql_snapshot_works = False self.log.info('SQL snapshot is not supported by this control panel: %s', e) return {} class SnapshotSaver(LveStatsPlugin): server_id = '' period_between_incidents = DEFAULT_PERIOD_BETWEEN_INCIDENTS max_snapshots_per_incident = DEFAULT_MAX_SNAPSHOTS_PER_INCIDENT _period = 60 // DEFAULT_SNAPSHOTS_PER_MINUTE def __init__(self): self.incidents_last_ts = {} self.log = logging.getLogger('SnapshotSaver') self.session = None # type: sqlalchemy.orm.session.Session self._snapshots_data = {} self.incidents_cache = {} self.first_run = True self.last_run = 0 self.litespeed_info = LitespeedHelper() self.snapshots_enabled = True self.compresslevel = 1 self._helper = SnapshotHelper() def set_config(self, config): # type: (dict) -> None self.server_id = config.get('server_id', self.server_id) self.period_between_incidents = int(config.get('period_between_incidents', self.period_between_incidents)) self.max_snapshots_per_incident = int(config.get('max_snapshots_per_incident', self.max_snapshots_per_incident)) self._period = 60 // int(config.get('snapshots_per_minute', DEFAULT_SNAPSHOTS_PER_MINUTE)) self.litespeed_info.force_litespeed = self._get_webserver_option(config) self.setup_litespeed(force_webserver_message=True) self.snapshots_enabled = config.get('disable_snapshots', "false").lower() != "true" self.compresslevel = max(min(int(config.get('compresslevel', 1)), 9), 1) def _get_webserver_option(self, config): # type: (dict) -> Optional[bool] """ Check which webserver we must force to use: Apache or Litespeed :return: None if webserver autodetect False if apache should be used True if litespeed should be used """ if 'litespeed' in config: option = config['litespeed'].lower() if option in ['on', '1']: return True if option in ['off', '0']: return False return None def _incomplete_incidents_query(self): # type: () -> sqlalchemy.orm.query.Query """Generate sqlalchemy Query instance for select incomplete incidents""" return self.session.query(incident).filter( incident.server_id == self.server_id, incident.incident_end_time.is_(None)) def finalize_incidents(self): # type: () -> None not_finalize_incidents_query = self._incomplete_incidents_query().filter( incident.dump_time < self.now - self.period_between_incidents) finalized_numb = not_finalize_incidents_query.update({incident.incident_end_time: incident.dump_time}) self.log.debug('%i old incidents period was finalized' % finalized_numb) self.session.commit() def save_old_incidents(self): # type: () -> None old_incidents = {uid: ts for uid, ts in list(self.incidents_last_ts.items()) if ts < self.now - self.period_between_incidents} if old_incidents: try: for _inc in self.session.query(incident).filter( incident.server_id == self.server_id, incident.incident_end_time.is_(None), incident.uid.in_(list(old_incidents.keys()))).all(): _inc.incident_end_time = old_incidents[_inc.uid] except Exception as e: self.session.rollback() self.log.error("Unable to save old incidents: (%s)", str(e)) else: self.session.commit() for uid in list(old_incidents.keys()): self.incidents_last_ts.pop(uid) try: self.incidents_cache.pop(uid) except KeyError: pass def get_incident(self, uid): # type: (int) -> incident _inc = self._incomplete_incidents_query().filter( incident.uid == uid).first() if not _inc: _inc = incident(uid=uid, incident_start_time=self.now, server_id=self.server_id, snapshot_count=0, incident_end_time=None) self.session.add(_inc) self.log.debug( 'New incident-period for uid %s started; incident_start_time=%i' % (uid, self.now)) self.incidents_last_ts[uid] = self.now return _inc def init_session(self): if self.first_run: # Running for the first time self.session = sessionmaker(bind=self.engine)() self.finalize_incidents() self.first_run = False def process_lve(self, lve_id, faults): # type: (int, Dict[str, int]) -> None try: lve_id = int(lve_id) self.incidents_last_ts[lve_id] = self.now _inc = self.incidents_cache[lve_id] self.log.debug( 'Faults %s for uid %s detected; timestamp %i' % (faults, lve_id, self.now)) if _inc["snapshot_count"] < self.max_snapshots_per_incident: self.save_snapshot(_inc, faults) self.log.debug( 'Snapshot for uid %s with timestamp %i saved' % (lve_id, self.now)) except Exception as e: self.session.rollback() self.log.warning("Unable to save incident for LVE %s (%s)" % (lve_id, str(e))) def setup_litespeed(self, force_webserver_message=False): # type: (bool) -> None """Check state and configure access to litespeed""" self.litespeed_info.check_litespeed_state() if self.litespeed_info.state_changed and self.litespeed_info.is_running: self.litespeed_info.dump_passwd() if self.litespeed_info.state_changed or force_webserver_message: use_litespeed = self.litespeed_info.get_use_litespeed() litespeed_running = self.litespeed_info.is_running if use_litespeed and not litespeed_running: self.log.info("Litespeed is not running properly. " "Check litespeed license key.") webserver = LITESPEED if use_litespeed else APACHE msg = "{} webserver will be used now to obtain data".format(webserver) self.log.info(msg) def execute(self, lve_data): # type: (dict) -> None if not self.snapshots_enabled: return self.init_session() self.setup_litespeed() lve_ids = list(lve_data.get('lve_usage', {}).keys()) lve_faults = lve_data.get('faults', {}) self._helper.invalidate(lve_ids) self.cache_snapshots(lve_faults) if self.now - self.last_run >= self._period: self.last_run = self.now with reboot_lock(): self.save_old_incidents() self._insert_new_incidents(lve_faults) self._increment_snapshot_count(lve_faults) for lve_id, faults in list(lve_faults.items()): self.process_lve(lve_id, faults) lve_data["faults"] = {} self._snapshots_data = {} def _increment_snapshot_count(self, lve_faults): # type: (Dict[int, Dict[str, int]]) -> None lve_id_list = list(lve_faults.keys()) for chunk in get_chunks(lve_id_list, 250): self._incomplete_incidents_query().filter( incident.uid.in_(chunk), incident.snapshot_count < self.max_snapshots_per_incident ).update({ "snapshot_count": incident.snapshot_count + 1, "dump_time": int(self.now) }, synchronize_session=False) self.session.commit() def _insert_new_incidents(self, lve_faults): # type: (Dict[int, Dict[str, int]]) -> None new_incidents = {lve_id: { 'uid': lve_id, "incident_start_time": self.now, "server_id": self.server_id, "snapshot_count": 0, "incident_end_time": None } for lve_id, _ in list(lve_faults.items()) if lve_id not in self.incidents_cache} if new_incidents: self.incidents_cache.update(new_incidents) insert_list = (incident(**_inc) for _inc in list(new_incidents.values())) try: # Better to use # self.session.bulk_insert_mappings(incident, new_incidents) # but it will be available only in SQLAlchemy version > 1.0 self.session.add_all(insert_list) except Exception as e: self.log.error("Unable to save new incidents: %s", str(e)) else: self.session.commit() def cache_snapshots(self, lve_faults): # type: (Dict[int, Dict[str, int]]) -> None for lve_id in list(lve_faults.keys()): if lve_id not in self._snapshots_data: self._snapshots_data[lve_id] = \ self._helper.get_snapshot_data(lve_id, self.litespeed_info) def save_snapshot(self, _incident, faults): # type: (dict, Dict[str, int]) -> None lve_id = _incident["uid"] processes, queries, urls = self._snapshots_data.get(lve_id, ({}, [], [])) snapshot_ = Snapshot(_incident, self.compresslevel) data = { 'uid': _incident["uid"], 'dump_time': int(self.now), 'server_id': self.server_id, 'incident_start_time': _incident["incident_start_time"], 'snap_proc': processes, 'snap_sql': queries, 'snap_http': urls, 'snap_faults': faults} try: snapshot_.save(data) except IOError as e: self.log.error(str(e)) _incident["snapshot_count"] += 1