%PDF- %PDF-
Direktori : /proc/227033/root/opt/alt/python37/lib/python3.7/site-packages/ssa/ |
Current File : //proc/227033/root/opt/alt/python37/lib/python3.7/site-packages/ssa/manager.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 classes implementing SSA Manager behaviour """ import json import logging import os import re import subprocess from glob import iglob from clcommon.lib.cledition import is_cl_solo_edition from .configuration import load_validated_parser, load_configuration from .internal.constants import flag_file from .internal.exceptions import SSAManagerError from .internal.utils import ssa_version from .modules.autotracer import AutoTracer from .modules.decision_maker import DecisionMaker class Manager: """ SSA Manager class. """ def __init__(self): self.logger = logging.getLogger('manager') self.ini_file_name = 'clos_ssa.ini' self.substrings_to_exclude_dir_paths = ( 'php44', 'php51', 'php52', 'php53', 'php\d+-imunify', 'php-internal' ) self.wildcard_ini_locations = ( '/opt/alt/php[0-9][0-9]/link/conf', '/var/cagefs/*/*/etc/cl.php.d/alt-php[0-9][0-9]', '/opt/cpanel/ea-php[0-9][0-9]/root/etc/php.d', '/opt/plesk/php/[0-9].[0-9]/etc/php.d', '/usr/local/php[0-9][0-9]/lib/php.conf.d', '/usr/share/cagefs/.cpanel.multiphp/opt/cpanel/ea-php[0-9][0-9]/root/etc/php.d', '/usr/share/cagefs-skeleton/usr/local/php[0-9][0-9]/lib/php.conf.d' ) self.subprocess_errors = ( OSError, ValueError, subprocess.SubprocessError ) @staticmethod def response(*args, **kwargs) -> 'json str': """ Form a success json response with given kwargs """ raw_response = {'result': 'success'} raw_response.update({k: v for k, v in kwargs.items()}) return json.dumps(raw_response) @property def _enabled(self) -> bool: """ Is SSA enabled """ return os.path.isfile(flag_file) @property def _restart_required_settings(self) -> set: """ Configuration settings required Request Processor restart """ return {'requests_duration', 'ignore_list'} @property def solo_filtered_settings(self) -> set: return {'correlation', 'correlation_coefficient', 'request_number', 'time', 'domains_number'} def _restart_required(self, settings: dict) -> set: """ SSA Agent requires restart in case of changing these configuration: - requests_duration - ignore_list """ return self._restart_required_settings.intersection(settings) def run_service_utility(self, command: str, check_retcode=False) -> subprocess.CompletedProcess: """ Run /sbin/service utility to make given operation with SSA Agent service :command: command to invoke :check_retcode: whether to run with check or not :return: subprocess info about completed process """ try: result = subprocess.run(['/sbin/service', 'ssa-agent', command], capture_output=True, text=True, check=check_retcode) self.logger.info(f'ssa-agent {command} succeeded') except subprocess.CalledProcessError as e: self.logger.error( f'SSA Agent {e.cmd} failed with code {e.returncode}: {e.stdout or e.stderr}', extra={'cmd': e.cmd, 'retcode': e.returncode, 'stdout': e.stdout, 'stderr': e.stderr}) raise SSAManagerError( f'SSA Agent {e.cmd} failed with code {e.returncode}: {e.stdout or e.stderr}') except self.subprocess_errors as e: self.logger.error(f'Failed to run {command} command for SSA Agent', extra={'err': str(e)}) raise SSAManagerError( f'Failed to run {command} for SSA Agent: {e}') return result def set_config(self, args: dict) -> 'json str': """ Change SSA config and restart it. :args: dict to override current option values :return: JSON encoded result of the action """ config = load_validated_parser() config.override(args) try: config.write_ssa_conf() except OSError as e: self.logger.error('Failed to update SSA config file', extra={'err': str(e)}) raise SSAManagerError(f'Failed to update SSA config file: {e}') if self._restart_required(args): self.run_service_utility('restart', check_retcode=True) return self.response() def get_config(self) -> 'json str': """ Get current SSA config. :return: JSON encoded current config """ full_config = load_configuration() if is_cl_solo_edition(skip_jwt_check=True): filtered_config = {key: value for key, value in full_config.items() if key not in self.solo_filtered_settings} return self.response(config=filtered_config) return self.response(config=full_config) def get_ssa_status(self) -> 'json str': """ Get current status of SSA. :return: JSON encoded current status """ status = 'enabled' if self._enabled else 'disabled' return self.response(ssa_status=status) def enable_ssa(self) -> 'json str': """ Enable SSA: - add clos_ssa extension for each PHP version on server - add clos_ssa extension into cagefs for each user and each ver - start SSA Agent (if it is not already started) - restart Apache (etc.) and FPM, reset CRIU images - create flag_file indicating that SSA is enabled successfully :return: JSON encoded current status """ if self._enabled: raise SSAManagerError('SSA is already enabled', flag='warning') self.generate_inis() self.start_ssa_agent() self.create_flag() return self.response() def disable_ssa(self) -> 'json str': """ Disable SSA: - remove clos_ssa extension for each PHP version on server - remove clos_ssa extension from cagefs for each user and each ver - stop SSA Agent - restart Apache (etc.) and FPM, reset CRIU images - remove flag_file indicating that SSA is enabled :return: JSON encoded current status """ if not self._enabled: raise SSAManagerError('SSA is already disabled', flag='warning') self.remove_clos_inis() self.stop_ssa_agent() self.remove_flag() return self.response() def get_stats(self) -> 'json str': """ Get SSA statistics. Includes: - config values - version - SSA status (enabled|disabled) - SSA Agent status (active|inactive) :return: JSON encoded current statistics """ _config = {key: str(value).lower() for key, value in load_configuration().items()} return self.response( config=_config, version=ssa_version(), status='enabled' if self._enabled else 'disabled', agent_status=self.status_ssa_agent(), autotracing=AutoTracer().get_stats() ) def unused_dir_path(self, dir_path: str) -> list: """ Checking for substrings in a string. """ res = [substring for substring in self.substrings_to_exclude_dir_paths if re.search(substring, dir_path)] return res def existing_paths(self) -> str: """ Generator of existing paths (matching known wildcard locations) for additional ini files """ for location in self.wildcard_ini_locations: for dir_path in iglob(location): if self.unused_dir_path(dir_path): continue yield dir_path def generate_single_ini(self, ini_path: str) -> None: """ Enable SSA extension for single ini_path (given) """ with open(os.path.join(ini_path, self.ini_file_name), 'w') as ini: ini.write('extension=clos_ssa.so') def generate_inis(self) -> None: """ Place clos_ssa.ini into each existing Additional ini path, including cagefs ones """ self.logger.info('Generating clos_ssa.ini files...') for ini_path in self.existing_paths(): self.generate_single_ini(ini_path) self.logger.info('Finished!') def find_clos_inis(self) -> str: """ Generator function searching for clos_ssa.ini files in all existing Additional ini paths """ for ini_path in self.existing_paths(): for name in os.listdir(ini_path): if self.ini_file_name in name: yield os.path.join(ini_path, name) def remove_clos_inis(self) -> None: """ Remove all gathered clos_ssa.ini files """ self.logger.info('Removing clos_ssa.ini files...') for clos_ini in self.find_clos_inis(): os.unlink(clos_ini) self.logger.info('Finished!') def start_ssa_agent(self) -> None: """ Start SSA Agent service or restart it if it is accidentally already running """ agent_status = self.run_service_utility('status') if agent_status.returncode: self.run_service_utility('start', check_retcode=True) else: self.run_service_utility('restart', check_retcode=True) def stop_ssa_agent(self) -> None: """ Stop SSA Agent service or do nothing if it is accidentally not running """ agent_status = self.run_service_utility('status') if not agent_status.returncode: self.run_service_utility('stop', check_retcode=True) def status_ssa_agent(self) -> str: """ Get SSA Agent status: active or inactive """ try: self.run_service_utility('status', check_retcode=True) except SSAManagerError: return 'inactive' return 'active' def create_flag(self) -> None: """ Create a flag file indicating successful enablement """ with open(flag_file, 'w'): pass self.logger.info(f'Flag file {flag_file} created') def remove_flag(self) -> None: """ Remove a flag file indicating enablement """ try: os.unlink(flag_file) self.logger.info(f'Flag file {flag_file} removed') except OSError as e: self.logger.warning( f'Flag file {flag_file} removal failed: {str(e)}') def get_report(self) -> 'json str': """ Get last report. :return: JSON encoded report """ report = DecisionMaker().get_json_report() return self.response(**report) def regenerate_inis(self) -> None: """ Regenerates clos_ssa inis while SSA is enabled """ if self._enabled: self.generate_inis() def initialize_manager() -> 'Manager instance': """ Factory function for appropriate manager initialization :return: appropriate manager instance """ return Manager()