📁
SKYSHELL MANAGER
PHP v8.1.34
Create
Create
Path:
root
/
home
/
terracebizon
/
public_html
/
wp-includes
/
js
/
tinymce
/
themes
/
Name
Size
Perm
Actions
📁
inlite
-
0755
🗑️
🏷️
🔒
📁
modern
-
0755
🗑️
🏷️
🔒
📄
config.php
6.83 KB
0444
🗑️
🏷️
⬇️
✏️
🔒
📄
error_log
12934.91 KB
0644
🗑️
🏷️
⬇️
✏️
🔒
Edit: report_generator.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 from __future__ import absolute_import import os import sys import time import fcntl import json from psutil import pid_exists from typing import Optional, Dict, Union, List, Tuple from secureio import write_file_via_tempfile, disable_quota from clwpos.data_collector_utils import _add_wp_path_info from clcommon.clpwd import ClPwd, drop_privileges from clwpos.feature_suites.configurations import FeatureStatusEnum, extract_suites from clwpos.utils import acquire_lock, get_server_wide_options from clcommon.cpapi import userdomains, cpusers from clwpos.feature_suites import ( get_admin_suites_config, extract_features, AWPSuite, PremiumSuite, CDNSuite, CDNSuitePro, ) from clwpos.user.config import UserConfig from clwpos.logsetup import setup_logging from clwpos.cl_wpos_exceptions import WposError from clwpos import gettext as _ from clwpos.constants import SCANNING_STATUS, LAST_SCANNED_TS, SCAN_CACHE logger = setup_logging(__name__) class ReportGeneratorError(WposError): """ Raised when some bad happened during report generating/getting """ pass class ScanStatus: """ Type for handly scan status manipulations """ def __init__(self, status, pid: Optional[int] = None): self.pid = pid try: if isinstance(status, str): self.current, self.total = map(int, status.split('/')) elif isinstance(status, tuple): self.current, self.total = map(int, status) else: raise ReportGeneratorError( message=_('Unable to parse scan status: %(status)s, type of: %(type)s'), context={'status': str(status), 'type': type(status)}, ) except ValueError: raise ReportGeneratorError( message=_('Unable to parse scan status: %(status)s'), context={'status': str(status)}, ) def __hash__(self): return hash((self.current, self.total)) def __eq__(self, other): if isinstance(other, ScanStatus): return self.current, self.total == other.current, other.total elif isinstance(other, str): return str(self) == other.strip() elif isinstance(other, tuple): return tuple(self) == other return False def __str__(self): return f'{self.current}/{self.total}' def __iter__(self): for i in self.current, self.total: yield i @staticmethod def read() -> Optional['ScanStatus']: """ Read status file, with shared blocking flock """ st = None if os.path.exists(SCANNING_STATUS): with open(SCANNING_STATUS, 'r') as f: fcntl.flock(f.fileno(), fcntl.LOCK_SH) data = f.read() fcntl.flock(f.fileno(), fcntl.LOCK_UN) # status file was created, but not written with data yet if not data: return st try: data = json.loads(data) pid = data.get('pid') st = ScanStatus( (data['current'], data['total']), int(pid) if pid else None, ) except Exception as e: logger.error('Can\'t parse scan status json: %s', e) return st def to_json(self) -> str: """ Status JSON representation to write """ return json.dumps( { 'current': self.current, 'total': self.total, 'pid': self.pid, } ) def check_pid(self) -> bool: """ Return True if scan process is still alive """ return bool(self.pid) and pid_exists(self.pid) class ReportGenerator: """ Class for admin report generation/getting """ def __init__(self): self._clpwd = ClPwd() self.status = ScanStatus((0, 0)) self.result = { 'version': '1', 'users': {}, } self.scan_status_fd = None self.logger = logger @staticmethod def is_scan_running() -> bool: """ Checks: 1) SCANNING_STATUS file exists 2) SCANNING_STATUS has the scan process pid and it is still alive """ status = ScanStatus.read() if status: if status.check_pid(): return True return False @staticmethod def get_scan_status() -> Optional[ScanStatus]: """ Read status file and get status """ return ScanStatus.read() def scan(self, user_list: Optional[List[str]] = None) -> Dict[str, int]: """ Public entry point for the scan procedure Do some checks, prepare users list, fork process to actually do scan, and return initial scan progress `user_list` - users to scan; if None - scan all """ if self.is_scan_running(): raise ReportGeneratorError( message=_('Another scan in progress: %(status)s'), context={'status': str(self.get_scan_status())} ) usernames = self.__get_usernames() self.logger.debug('Found usernames: %s', usernames) if user_list: usernames = self.__filter_usernames(user_list, usernames) self.logger.debug('Filtered usernames: %s', usernames) self.status.total = len(usernames) # fork the scan to another process and return initial status immediately fp = os.fork() if fp: # Hold the caller's cron flock across the fork->claim gap: wait # (bounded) until the child has written SCANNING_STATUS so # is_scan_running() is True before we return. Otherwise a concurrent # generate-report could pass the guard in that window and start an # overlapping scan (the flock is released as soon as we return, now # that the child detaches the inherited stdio). self._await_scan_claim(fp) self.logger.debug('Scan forked to: %d', fp) return {'total_users_scanned': 0, 'total_users_to_scan': self.status.total} else: # Detach the long-running scan child from any stdout/stderr the caller # inherited to us (e.g. clwposcron's Popen pipe). Otherwise the caller's # communicate() would not see EOF until this whole tenant-docroot walk # finishes, wedging the flock-guarded cron. Own log file is unaffected. self._detach_inherited_stdio() try: self._scan(usernames) sys.exit(0) except Exception as e: self.logger.exception('Error during user sites scan: %s', e) sys.exit(1) def _await_scan_claim(self, child_pid: int, timeout: float = 5.0) -> None: """ Block until the forked scan child has claimed the scan (written SCANNING_STATUS with its live pid, so is_scan_running() is True), or until the child is gone or a short timeout elapses. Called by the parent before it returns, so the caller's cron flock is not released until the single-scan guard is armed. Bounded, so a wedged child can never hang the caller; only the claim is awaited, never the scan itself. """ deadline = time.monotonic() + timeout while time.monotonic() < deadline: if self.is_scan_running(): return try: # Reap with WNOHANG rather than poll pid_exists(): a child that # crashed before claiming stays a zombie (we are its parent and # nothing else waits on it), and pid_exists() is True for # zombies -- so the old check would burn the whole timeout # while the caller's cron flock is held. reaped_pid, _ = os.waitpid(child_pid, os.WNOHANG) except ChildProcessError: # Already reaped elsewhere: the child is gone either way. return if reaped_pid == child_pid: # Child exited/crashed before claiming: nothing to guard. return time.sleep(0.02) def _detach_inherited_stdio(self) -> None: """ Executes in the forked scan child. Start a new session and redirect stdout/stderr to /dev/null so inherited pipe write-ends are released and the parent that forked us can be reaped independently of this scan. """ try: os.setsid() devnull_fd = os.open(os.devnull, os.O_WRONLY) os.dup2(devnull_fd, 1) os.dup2(devnull_fd, 2) if devnull_fd > 2: os.close(devnull_fd) except OSError as e: self.logger.exception('Failed to detach forked scan child stdio: %s', e) def _scan(self, usernames: List[str]) -> None: """ Executes in forked process Get domain and docroots list for usernames Count user sites, write to SCAN_CACHE Create and remove SCANNING_STATUS file `userdomains` examples: [{'domain': 'res1.com', 'docroot': '/home/res1/public_html'}], [{'domain': 'cltest1.com', 'docroot': '/home/cltest1/public_html'}, {'domain': 'mk.cltest1.com', 'docroot': '/home/cltest1/public_html/mk'}] """ self.scan_status_fd = open(SCANNING_STATUS, 'w') self.status.pid = os.getpid() self.logger.debug('Set status pid to: %d', self.status.pid) # Claim the scan up-front, before the per-user loop, so a parent waiting # in _await_scan_claim() -- and any concurrent generate-report -- observe # is_scan_running() as True as early as possible (closes the overlap gap). self.__update_scan_status() for username in usernames: user_data = {} self.status.current += 1 self.__update_scan_status() for domain_name, doc_root in userdomains(username): user_data.setdefault(doc_root, []).append(domain_name) with drop_privileges(username): user_data = _add_wp_path_info(user_data) # set-comprehension and rstrip() used to delete duplicated paths in case of subdomains # ex: {'/dr1': {'wp_paths': ['', 'wordpress', 'sub']}, '/dr1/sub': {'wp_paths': ['']}} wp_paths = { os.path.join(_doc_root, _wp_path).rstrip('/') for _doc_root, values in user_data.items() for _wp_path in values.get('wp_paths', []) } self.result['users'].update( { username: {'wp_sites_count': len(wp_paths)}, } ) self.__write_scan_result() self.scan_status_fd.close() os.unlink(SCANNING_STATUS) _ts = self.__update_scan_timestamp() self.logger.debug('New scan timestamp: %d', _ts) def get_status(self) -> dict: """ CLI command to get scan status Cases: 1) no previous scans 2) scan in progress 3) scan finished (success) 4) scan process crashed """ result = { 'scan_status': 'idle', 'total_users_scanned': 0, 'total_users_to_scan': None, 'last_scan_time': None, } if not os.path.exists(SCAN_CACHE): result.update({'total_users_to_scan': len(cpusers())}) return result if self.is_scan_running(): status = self.get_scan_status() result.update( { 'scan_status': 'in_progress', 'total_users_scanned': status.current, 'total_users_to_scan': status.total, } ) else: if not os.path.exists(SCANNING_STATUS): result.update( { 'total_users_to_scan': len(cpusers()), 'last_scan_time': self._get_scan_timestamp(), } ) else: result.update( { 'scan_status': 'error', 'scan_error': _('Failed to generate report'), 'total_users_to_scan': len(cpusers()), 'last_scan_time': self._get_scan_timestamp(), } ) # Only reap a status file that actually holds a record. An empty # one means a scan has just created it (_scan opens with 'w') # and has not written its claim yet: unlinking it there would # strand the child on an unlinked inode, so _await_scan_claim # would never observe the claim, would time out and release the # caller's cron flock - reopening the overlapping-scan window. try: has_record = os.path.getsize(SCANNING_STATUS) > 0 except OSError: has_record = False if has_record: os.unlink(SCANNING_STATUS) return result @staticmethod def _get_scan_timestamp() -> Optional[int]: """ Read timestamp from LAST_SCANNED_TS Returns None in case of any error """ try: with open(LAST_SCANNED_TS, 'rt') as f: t = f.read().strip() return int(t) except Exception as e: logger.error('Can\'t read last scan timestamp: %s', e) return None @staticmethod def __filter_usernames(to_scan, usernames: List[str]) -> List[str]: """ Return usernames from --users cli argument """ return list(filter(lambda x: x in to_scan, usernames)) @staticmethod def __get_usernames() -> List[str]: """ Get usernames from cpapi """ return list(cpusers()) @staticmethod def __update_scan_timestamp() -> int: """ Write current timestamp to LAST_SCANNED_TS Return timestamp for the later use, if needed """ ts = int(time.time()) write_file_via_tempfile(str(ts), LAST_SCANNED_TS, 0o600) return ts def __update_scan_status(self) -> None: """ Write current status to SCANNING_STATUS Data example: "0/10", without any other symbols """ f = self.scan_status_fd # type: TextIO f_no = f.fileno() f.seek(0) # Blocking LOCK_EX: readers (ScanStatus.read holds LOCK_SH, polled every # 20ms by _await_scan_claim and by any get-scan-status caller) only hold # the lock for a microseconds-long read, so waiting is bounded in # practice. LOCK_NB here would raise BlockingIOError on any overlap with # a reader and kill the whole scan child. fcntl.flock(f_no, fcntl.LOCK_EX) f.write(self.status.to_json()) f.flush() # Truncate to what we just wrote: rewriting from offset 0 without this # leaves the tail of a longer previous record behind, so ScanStatus.read() # gets trailing bytes, json.loads raises, read() returns None and the # single-scan guard silently disappears. f.truncate() fcntl.flock(f_no, fcntl.LOCK_UN) def __write_scan_result(self) -> None: """ Write final scan result to SCANNING_STATUS via temp file """ write_file_via_tempfile(json.dumps(self.result), SCAN_CACHE, 0o600) # get-report part def get(self, target_users=None): """ 1. get-report when there is no cache file: - start generating report - return total_users_scanned, total_users_to_scan keys 2. get-report when there is no cache file, but scanning in progress: - return total_users_scanned, total_users_to_scan keys 3. get-report when cache present, no scanning running - return data from cache - no keys total_users_scanned, total_users_to_scan in response 4. get-report when cache present, scanning is running - return data from cache - return total_users_scanned, total_users_to_scan keys """ report = {} scanned_cache = self._get_from_file_with_locking(SCAN_CACHE, is_json=True) scanning_status = self.get_scan_status() # no cache and scanning is not running right now -> # run scanning if not scanned_cache and not scanning_status: scan = self.scan() return scan # no cache, but scanning is running right now -> # returns status from status file if scanning_status: report['total_users_scanned'] = scanning_status.current report['total_users_to_scan'] = scanning_status.total # if scanned cache -> append it to report if scanned_cache: report['last_scan_time'] = self._get_scan_timestamp() # cache already generated, let`s read it users_cache = scanned_cache['users'] ( report['users'], report['total_wordpress_count'], report['total_users_count'], total_users_with_active_awp, total_users_with_active_premium, total_users_with_active_cdn, total_users_with_active_cdn_pro, total_sites_with_active_awp, total_sites_with_active_premium, total_sites_with_active_cdn, total_sites_with_active_cdn_pro, ) = self._get_users_report(users_cache, target_users) report['total_users_count_active'] = { AWPSuite.name: total_users_with_active_awp, PremiumSuite.name: total_users_with_active_premium, CDNSuite.name: total_users_with_active_cdn, CDNSuitePro.name: total_users_with_active_cdn_pro, } report['total_sites_count_active'] = { AWPSuite.name: total_sites_with_active_awp, PremiumSuite.name: total_sites_with_active_premium, CDNSuite.name: total_sites_with_active_cdn, CDNSuitePro.name: total_sites_with_active_cdn_pro, } return report @staticmethod def _is_allowed_suite(user_suites_data, user_features_data, suite): is_suite_allowed = user_suites_data.get(suite.name).status == FeatureStatusEnum.ALLOWED return is_suite_allowed and any( [ user_features_data.get(feature.NAME.lower()) and user_features_data.get(feature.NAME.lower()).status == FeatureStatusEnum.ALLOWED for feature in suite.features ] ) @staticmethod def _sites_with_active_features(username): with drop_privileges(username), disable_quota(): wps_with_enabled_awp_features = UserConfig(username).wp_paths_with_active_suite_features(AWPSuite.features) wps_with_enabled_awp_premium_features = UserConfig(username).wp_paths_with_active_suite_features( PremiumSuite.features ) wps_with_enabled_cdn_features = UserConfig(username).wp_paths_with_active_suite_features(CDNSuite.primary) return wps_with_enabled_awp_features, wps_with_enabled_awp_premium_features, wps_with_enabled_cdn_features def _get_users_report( self, user_scanned_data: Dict, target_users: Union[None, List] ) -> Tuple[List, int, int, int, int, int, int, int, int, int, int]: """ Returns list of final users data: example: [ {"username": "user1", "features": {"object_cache": true}, "wp_sites_count": "1"}, .... ] """ all_users_info = [] total_wp_sites_count, total_users_count = 0, 0 total_users_with_active_awp = 0 total_users_with_active_premium = 0 total_users_with_active_cdn = 0 total_users_with_active_cdn_pro = 0 total_sites_with_active_awp = 0 total_sites_with_active_premium = 0 total_sites_with_active_cdn = 0 total_sites_with_active_cdn_pro = 0 default_config = get_server_wide_options() for username, scanned_data in user_scanned_data.items(): wps_per_user_with_enabled_awp_features = 0 wps_per_user_with_enabled_awp_premium_features = 0 wps_per_user_with_enabled_cdn = 0 wps_per_user_with_enabled_cdn_pro = 0 if target_users and username not in target_users: continue uid = self._uid_by_name(username) if not uid: self.logger.warning('Cannot get user %s uid', username) continue # let`s not break report collection for all users try: user_features_data = extract_features( uid, get_admin_suites_config(uid), server_wide_options=default_config ) user_suites_data = extract_suites(get_admin_suites_config(uid), server_wide_options=default_config) except Exception: self.logger.exception('Unable to get features data from admin config') user_suites_data = {} user_features_data = {} # broken users must not fail whole report try: wps_with_enabled_awp_features, wps_with_enabled_awp_premium_features, wps_with_enabled_cdn_features = ( self._sites_with_active_features(username) ) except Exception as e: self.logger.warning('Unable to get information for report from user config: %s', str(e)) wps_with_enabled_awp_features, wps_with_enabled_awp_premium_features, wps_with_enabled_cdn_features = ( [], [], [], ) total_users_count += 1 total_wp_sites_count += int(scanned_data['wp_sites_count']) # if feature is allowed and activated by end-user if self._is_allowed_suite(user_suites_data, user_features_data, AWPSuite) and wps_with_enabled_awp_features: total_users_with_active_awp += 1 total_sites_with_active_awp += len(wps_with_enabled_awp_features) wps_per_user_with_enabled_awp_features = len(wps_with_enabled_awp_features) if ( self._is_allowed_suite(user_suites_data, user_features_data, PremiumSuite) and wps_with_enabled_awp_premium_features ): total_users_with_active_premium += 1 total_sites_with_active_premium += len(wps_with_enabled_awp_premium_features) wps_per_user_with_enabled_awp_premium_features = len(wps_with_enabled_awp_premium_features) if wps_with_enabled_cdn_features: if self._is_allowed_suite(user_suites_data, user_features_data, CDNSuitePro): total_users_with_active_cdn_pro += 1 total_sites_with_active_cdn_pro += len(wps_with_enabled_cdn_features) wps_per_user_with_enabled_cdn_pro = len(wps_with_enabled_cdn_features) elif self._is_allowed_suite(user_suites_data, user_features_data, CDNSuite): total_users_with_active_cdn += 1 total_sites_with_active_cdn += len(wps_with_enabled_cdn_features) wps_per_user_with_enabled_cdn = len(wps_with_enabled_cdn_features) all_users_info.append( { 'username': username, 'suites': user_suites_data, 'wp_sites_count': scanned_data['wp_sites_count'], 'accelerate_wp_active_sites_count': wps_per_user_with_enabled_awp_features, 'accelerate_wp_premium_sites_count': wps_per_user_with_enabled_awp_premium_features, 'accelerate_wp_cdn_sites_count': wps_per_user_with_enabled_cdn, 'accelerate_wp_cdn_pro_sites_count': wps_per_user_with_enabled_cdn_pro, } ) # stop iterating through users if targets already found if target_users and len(target_users) == len(all_users_info): break return ( all_users_info, total_wp_sites_count, total_users_count, total_users_with_active_awp, total_users_with_active_premium, total_users_with_active_cdn, total_users_with_active_cdn_pro, total_sites_with_active_awp, total_sites_with_active_premium, total_sites_with_active_cdn, total_sites_with_active_cdn_pro, ) def _get_from_file_with_locking(self, path: str, is_json: bool, with_lock: bool = False) -> Union[Dict, str, None]: """ Reads file with locking if needed """ if not os.path.exists(path): return None try: if with_lock: with acquire_lock(path): return self._read_report_file(path, is_json) else: return self._read_report_file(path, is_json) except Exception as e: raise ReportGeneratorError( message=_('Can`t read %(file)s. Reason: %(error)s'), context={'file': path, 'error': str(e)} ) @staticmethod def _read_report_file(path: str, is_json: bool) -> Union[Dict, str, None]: """ Reads file for report """ with open(path, "r") as file: if is_json: return json.load(file) else: return file.read().strip() def _uid_by_name(self, name): try: return self._clpwd.get_uid(name) except ClPwd.NoSuchUserException: return None
Save