Mini Shell

Direktori : /proc/self/root/proc/thread-self/root/opt/alt/python37/lib/python3.7/site-packages/ssa/
Upload File :
Current File : //proc/self/root/proc/thread-self/root/opt/alt/python37/lib/python3.7/site-packages/ssa/manager.py

"""
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 .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 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 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 initialize_manager() -> 'Manager instance':
    """
    Factory function for appropriate manager initialization
    :return: appropriate manager instance
    """
    return Manager()

Zerion Mini Shell 1.0