Mini Shell
"""
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