D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
proc
/
self
/
root
/
opt
/
cloudlinux
/
venv
/
lib
/
python3.11
/
site-packages
/
clselect
/
Filename :
cluserselect.py
back
Copy
# -*- 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 print_function from __future__ import absolute_import from __future__ import division import json import os import sys import uuid import signal import secureio from future.moves import configparser as ConfigParser from stat import S_IRUSR, S_IWUSR, S_IRGRP, S_IROTH from future.utils import iteritems from pathlib import Path import psutil from .clselect import ClSelect from .clselectexcept import ClSelectExcept from clcommon import ClPwd, clcaptain from .clselectprint import clprint from . import utils from clcommon.utils import ExternalProgramFailed try: from clcagefslib.const import BASEDIR from clcagefslib.fs import get_user_prefix from clcagefslib.selector.configure import is_ea4_enabled, read_cpanel_ea4_php_conf, configure_alt_php from clcagefslib.selector.panel.da import da_change_user_php_ini from clcagefslib.selector.panel.isp import ispmanager_create_user_wrapper except ImportError: pass class ClUserSelect(ClSelect): CAGEFS_PATH = '/var/cagefs' SELECTOR_PATH = '/usr/selector' NATIVE_PATH = SELECTOR_PATH if utils.in_cagefs() else '/usr/share/cagefs-skeleton/usr/selector' CAGEFS_EXCLUDE = '/etc/cagefs/exclude' SELECTOR2_DIR = '.cl.selector/selector.path' def clean_crui_images(self, users=None): """ Creates flags mod_lsapi_reset_me in users' home directories in order to recreate CRIU images when php version/extensions/options have changed For details see LVEMAN-1210 :param users: list of usernames (strings) """ # There is not reliable way to check if CRIU is enabled inside CageFS # So let's always create the "mod_lsapi_reset_me" flag if not utils.in_cagefs() and not os.path.isfile('/var/run/mod_lsapi/criu.enabled'): return for user in users: pw = self._clpwd.get_pw_by_name(user) path = os.path.join(pw.pw_dir, 'mod_lsapi_reset_me') if not os.path.isfile(path): previous_user_data = self._change_uid(user) try: clcaptain.write(path) except (OSError, ExternalProgramFailed) as e: raise ClSelectExcept.UnableToSaveData(path, e) finally: ClUserSelect._restore_uid(previous_user_data) @staticmethod def switch_symlink_for_alt_php(version, pw, exit_on_error=True, configure_multiphp = True): """ Switch symlink for alt php. Create .cagefs directory if not created Rerurn True if error has occured """ if not os.path.isdir(BASEDIR) and not utils.in_cagefs(): print('ERROR: CageFS not installed.') if exit_on_error: sys.exit(1) else: return True if configure_multiphp and is_ea4_enabled(): conf = read_cpanel_ea4_php_conf() if conf: try: # get default system php version selected via MultiPHP Manager in cPanel WHM default_php = conf['default'] # LVEMAN-1170: do not configure PHP Selector when system default version is alt-php if not default_php.startswith('ea-php'): print('ERROR: system default PHP version is alt-php. ' 'PHP Selector is disabled. Use cPanel MultiPHP manager instead.') if exit_on_error: sys.exit(1) else: return True except KeyError: pass # configure alt php - create .cagefs dir and create symlink error = configure_alt_php(pw, version, write_log=False, drop_perm=(os.geteuid() == 0), configure_multiphp = configure_multiphp) if error and exit_on_error: sys.exit(1) return error def apply_symlinks_rules(self): if self.without_cagefs: print('ERROR: this option does not work in "single user" mode (when CageFS is disabled)') sys.exit(1) if os.geteuid() != 0: print('ERROR: root privileges required') sys.exit(1) users_vers_dict = self.get_user_version_map() for user, version in iteritems(users_vers_dict): print('Processing user', user) pw = self._clpwd.get_pw_by_name(user) ClUserSelect.switch_symlink_for_alt_php(version, pw, exit_on_error=False, configure_multiphp=False) def __init__(self, item='php', exclude_pid_list=None): ClSelect.__init__(self, item) self._clpwd = ClPwd() self._user_excludes = set() if exclude_pid_list: self.exclude_pid_list = exclude_pid_list else: self.exclude_pid_list = [] def get_version(self, user, show_native_version=False): """ Returns alternative version for a user @param user: string @return: string """ self._check_user_in_cagefs(user) alt_path = self._compose_user_alt_path(user) native = self._compose_native_info(show_native_version) if not os.path.isdir(alt_path): return native alternatives = self.get_all_alternatives_data() full_path = os.path.join(alt_path, self._item) if not os.path.islink(full_path): return native link_dst = os.readlink(full_path) if self.without_cagefs: if not self._native_contents: self._load_native_contents(self._item) if link_dst == self._native_contents[self._item]: return native if os.path.dirname(link_dst) == self.SELECTOR_PATH: return native try: version = list(filter( (lambda i: alternatives[i]['data'][self._item] == link_dst), list(alternatives.keys())))[0] return (version, alternatives[version]['version'], alternatives[version]['data'][self._item]) except (IndexError, KeyError): return native def create_dir(self, path, user): if not os.path.isdir(path): previous_user_data = self._change_uid(user) try: clcaptain.mkdir(path) except (OSError, ExternalProgramFailed) as e: raise ClSelectExcept.UnableToSaveData(path, e) finally: ClUserSelect._restore_uid(previous_user_data) def create_selector_symlinks(self, user): """ Creates additional directory and symlinks for use in "without CageFS" mode """ homedir = self._clpwd.get_homedir(user) path_in_home = os.path.join(homedir, self.SELECTOR2_DIR) cur_user = self._change_uid(user) self.create_dir(path_in_home, user) self._create_symlink('../php-cli', path_in_home+'/php', check_existence=True) self._create_symlink('../php', path_in_home+'/php-cgi', check_existence=True) self._restore_uid(cur_user) def get_default_version(self): if os.path.isfile(ClSelect.DEFAULTS_PATH): try: return self._dh.get('versions', self._item) except (ConfigParser.Error, IOError, KeyError): return 'native' return 'native' def set_version_from_backup(self, user): user_backup_path = os.path.join(self._clpwd.get_homedir(user), '.cl.selector', 'defaults.cfg') if not os.path.isfile(user_backup_path): self.set_version(user, self.get_default_version()) else: try: dh = self._get_default_config_handler(user_backup_path) self.set_version(user, dh.get('versions', self._item)) except (ConfigParser.Error, IOError, KeyError) as e: print('Error while restoring settings from backup', str(e)) sys.exit(1) def set_version(self, user, version, return_summary=False, show_native_version=False, exit_on_error=True): """ Sets alternative version for a users with the same uid @param user: string @return: None """ if os.geteuid() != 0 and (message := self.get_version_selection_disabled_msg(user)): raise ClSelectExcept.VersionModificationBlocked(message) data = utils.apply_for_at_least_one_user( self._set_version, self._clpwd.get_names(self._clpwd.get_uid(user)), ClSelectExcept.NoUserSelector, version, return_summary, show_native_version, exit_on_error ) if return_summary: return data def _set_version(self, user, version, return_summary=False, show_native_version=False, exit_on_error=True): """ Sets alternative version for a user @param user: string @return: None """ if self.without_cagefs: previous_user_data = self._change_uid(user) self._check_user_in_cagefs(user) alt_path = self._compose_user_alt_path(user) if not os.path.isdir(alt_path): if self.without_cagefs: self.create_dir(alt_path, user) else: raise ClSelectExcept.NoUserSelector(user) alternatives = self.get_all_alternatives_data() if version not in alternatives and version != 'native': raise ClSelectExcept.NoSuchAlternativeVersion(version) self._remove_alternatives_links(alt_path) pw = self._clpwd.get_pw_by_name(user) if version == 'native': if self.without_cagefs: if not self._native_contents: self._load_native_contents(self._item) for item, native_path in iteritems(self._native_contents): self._create_symlink(native_path, alt_path+'/'+item, user, version) else: ini = 'php.ini' new_ini_created = False new_ini_path = os.path.join("%s.etc" % (self.NATIVE_PATH,), ini) if os.path.exists(new_ini_path): src = os.path.join("%s.etc" % self.SELECTOR_PATH, ini) dst = os.path.join(alt_path, ini) self._create_symlink(src, dst, user, version) new_ini_created = True for filename in os.listdir(self.NATIVE_PATH): if self._item not in filename: continue if filename.endswith('.ini') and new_ini_created: continue dst = os.path.join(alt_path, filename) src = os.path.join(self.SELECTOR_PATH, filename) self._create_symlink(src, dst, user, version) else: for item, path in iteritems(alternatives[version]['data']): self._create_symlink(path, os.path.join(alt_path, item), user, version) if self.without_cagefs: ClUserSelect._restore_uid(previous_user_data) else: ClUserSelect.switch_symlink_for_alt_php(version, pw, exit_on_error = exit_on_error) self._switch_php_da_isp(user, version) self._reload_processes(user) self._backup_settings(user) if return_summary: return self.get_summary(user, show_native_version) def get_version_selection_disabled_msg(self, username: str) -> str: """ Returns a message indicating that the selection of the PHP version is disabled for the user, based on the configuration file. Args: username (str): The username for which to check the configuration. Returns: str: The message indicating that version selection is disabled, or an empty string if the configuration file does not exist or does not contain the message. """ uid = self._clpwd.get_uid(username) config_file = Path(f'/var/cloudlinux/cl.selector/uids/{uid}/version_selection_conf.json') if not config_file.exists(): return '' try: with config_file.open(encoding='utf-8') as f: config_data = json.load(f) return config_data.get('version_selection_disabled_msg', '') except (OSError, ValueError): return '' def get_summary(self, user, show_native_version=False): """ Returns state of alternatives @param user: string @return: tuple """ self._check_user_in_cagefs(user) alternatives = self.get_all_alternatives_data() native_info = self._compose_native_info(show_native_version) summary = {'native': { 'enabled': True, 'default': False, 'selected': False}} alt_versions = sorted(alternatives.keys()) alt_versions.append('native') selected_version = self.get_version(user)[0] for version in alt_versions: if version not in summary: summary[version] = {} summary[version]['enabled'] = not self._dh.has_option( "%s%s" % (self._item, version), 'state') summary[version]['default'] = False summary[version]['selected'] = False try: default_version = self._dh.get('versions', self._item) except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): default_version = 'native' try: summary[default_version]['default'] = True summary[selected_version]['selected'] = True except KeyError: raise ClSelectExcept.NoSuchAlternativeVersion(default_version) summary[native_info[0]] = summary.pop('native') alt_versions.remove('native') alt_versions.append(native_info[0]) for idx in range(len(alt_versions)): v = alt_versions[idx] alt_versions[idx] = ( v, (summary[v]['enabled'], summary[v]['default'], summary[v]['selected'])) return tuple(alt_versions) def change_to_version(self, new_version, current_version): """ Changes users of a supplied version to specified_version @param version: string @param current_version: string """ users = self.list_users(current_version) for user in users: try: self.set_version(user, new_version, exit_on_error = False) except Exception as e: #catch every errors, print it and go to the next user clprint.print_diag('text', {'status': 'ERROR', 'message': str(e)}) pass self.clean_crui_images(users) def list_users(self, version): """ Returns users of a certain alternative """ data = self.get_version_user_map() if version in data: return data[version] return [] def list_all_users(self): """ Returns all valid system users @return: list """ if self.without_cagefs: from .clselectctlphp import get_cpanel_user return [get_cpanel_user()] return list(self._get_system_users().difference( self._get_user_excludes())) def cagefs_copy_etc(self, user): config = dict() config['init'] = 0 config['reinit'] = 0 config['verbose'] = 0 LIBDIR = '/usr/share/cagefs' sys.path.append(LIBDIR) try: import cagefsctl except ImportError: print('ERROR: CageFS not installed.') sys.exit(1) cagefs_etc_path = os.path.join(BASEDIR, get_user_prefix(user), user, 'etc') if not os.path.exists(cagefs_etc_path + '/cl.selector') or \ not os.path.exists(cagefs_etc_path + '/cl.php.d'): cagefsctl.cpetc_for_user(user, config) def get_user_version_map(self): """ Returns user version map as dict @return: dict """ actual_users = self.list_all_users() data = {} for user in actual_users: try: data[user] = self.get_version(user, False)[0] except ClSelectExcept.NotCageFSUser: continue return data def get_version_user_map(self, user_names = None): """ Returns users grouped by version @return: dict """ actual_users = user_names or self.list_all_users() data = {} for user in actual_users: try: version = self.get_version(user, False)[0] if not version in data: data[version] = [] data[version].append(user) except ClSelectExcept.NotCageFSUser: continue return data def _create_symlink(src, dst, user=None, version=None, check_existence=False): """ Creates symlink from src to dst @param src: string @param dst: string @param user: string @param version: string @param check_existence: bool @return: None """ try: if check_existence: if os.path.islink(dst): if os.readlink(dst) != src: os.unlink(dst) else: return else: utils.remove_file_or_dir(dst) clcaptain.symlink(src, dst) except Exception as e: if user is not None and version is not None: raise ClSelectExcept.UnableToSetAlternative(user, version, e) raise ClSelectExcept.SelectorException( "Cannot create symlink from %s to %s (%s)" % (src, dst, e)) _create_symlink = staticmethod(_create_symlink) def _get_user_excludes(self): """ Returns list of user excludes @return: list """ if self._user_excludes: return self._user_excludes if not os.path.isdir(self.CAGEFS_EXCLUDE): return set() for item in os.listdir(self.CAGEFS_EXCLUDE): full_item_path = os.path.join(self.CAGEFS_EXCLUDE, item) self._user_excludes.update( set( map((lambda x: x.strip()), utils.read_file_as_string(full_item_path).splitlines()))) return self._user_excludes def _check_user_in_cagefs(self, user): """ Check that cagefs enabled for user """ if self.without_cagefs: return if utils.in_cagefs(): return LIBDIR = '/usr/share/cagefs' sys.path.append(LIBDIR) try: import cagefsctl except ImportError: print('ERROR: CageFS not installed.') sys.exit(1) try: if not cagefsctl.is_user_enabled(user): raise ClSelectExcept.NotCageFSUser(user) except AttributeError: print('ERROR: CageFS version is unsupported. Please update CageFS.') sys.exit(1) def _remove_alternatives_links(self, path): """ Removes all symlinks from directory @param path: string @return: None """ for filename in os.listdir(path): if self._item not in filename: continue full_path = os.path.join(path, filename) if not os.path.islink(full_path): continue os.unlink(full_path) def _compose_user_alt_path(self, user): """ Composes and returns user alternative directory path @param user: string @return: string """ if self.without_cagefs: homedir = self._clpwd.get_homedir(user) return homedir + '/.cl.selector' uid = str(self._clpwd.get_uid(user)) return ( '/etc/cl.selector' if utils.in_cagefs() else os.path.join(self.CAGEFS_PATH, uid[-2:], user, 'etc', 'cl.selector') ) def _get_system_users(self): """ Returns set of system users @return: set """ users_dict = self._clpwd.get_user_dict() return set(users_dict.keys()) def _delete_if_symlink(file_path): """ Deletes file to be written if it is a symlink """ if os.path.islink(file_path): try: os.unlink(file_path) except OSError: raise ClSelectExcept.UnableToSaveData( file_path, "Cannot delete symlink while saving data") _delete_if_symlink = staticmethod(_delete_if_symlink) def _change_uid(self, user): """ Changes to another uid and returns tuple of previous euid and egid @param user: string @return: tuple """ entry = self._clpwd.get_pw_by_name(user) new_uid = entry.pw_uid new_gid = entry.pw_gid cur_euid = os.geteuid() cur_egid = os.getegid() if cur_euid == new_uid: return cur_euid, cur_egid try: os.setegid(new_gid) os.seteuid(new_uid) secureio.set_capability() return cur_euid, cur_egid except OSError as e: raise ClSelectExcept.UnableToChangeToAnotherUser(user, e) def _restore_uid(uid_and_gid): """ Restores changed uid and gid to original ones @param uid_and_gid: tuple @return: None """ if uid_and_gid[0] != os.geteuid(): secureio.set_capability(clear=True) try: os.setegid(uid_and_gid[1]) os.seteuid(uid_and_gid[0]) except OSError as e: raise ClSelectExcept.UnableToChangeToAnotherUser(str(uid_and_gid[0]), e) _restore_uid = staticmethod(_restore_uid) def _write_to_file(self, user, file_contents, file_path, create=True): """ Saves data to file @param user: string @param file_contents: string @param file_path: string @return: None """ if not create and not os.path.exists(file_path): return self._delete_if_symlink(file_path) previous_user_data = self._change_uid(user) file_directory = os.path.dirname(file_path) try: # Replace tempfile.mkstemp with str(uuid.uuid4()) dirname = 'clseltmp_%s' % str(uuid.uuid4()) temp_path = os.path.join(file_directory, dirname) clcaptain.write(temp_path, "%s\n" % (file_contents,)) except (IOError, OSError, ExternalProgramFailed) as e: try: if os.path.exists(temp_path): os.unlink(temp_path) except: pass ClUserSelect._restore_uid(previous_user_data) raise ClSelectExcept.UnableToSaveData(file_path, e) else: try: mask = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH os.rename(temp_path, file_path) os.chmod(file_path, mask) except OSError: pass ClUserSelect._restore_uid(previous_user_data) def _reload_processes(self, user): """ Reloads user process """ try: next_parent = psutil.Process() for i in range(2): next_parent = next_parent.parent() if next_parent is not None: self.exclude_pid_list.append(next_parent.pid) else: break except psutil.NoSuchProcess: pass try: uid = ClPwd().get_uid(user) except (ClPwd.NoSuchUserException, ): # no such user return try: for proc in psutil.process_iter(): try: if uid not in [proc.uids().real, proc.uids().effective] or proc.name().find(self._item) == -1: continue pid = proc.pid except psutil.NoSuchProcess: continue try: if pid not in self.exclude_pid_list: os.kill(pid, signal.SIGHUP) except (OSError, ): continue except (OSError, IOError): # psutil reads /proc FS as usual FS, skip read errors pass def _skim_over_extensions(path): """ Get extension names from user extensions file comments """ extensions = [] try: ini = open(path) for line in ini: if line.startswith(';---'): ext = line[4:line.rfind('---')] extensions.append(ext) ini.close() except (OSError, IOError): pass return extensions _skim_over_extensions = staticmethod(_skim_over_extensions) def _backup_settings(self, user): """ Scans all user settings and backups'em in homedir as INI file @param user: string """ self._check_user_in_cagefs(user) backup_contents = [] user_alt_path = self._compose_user_alt_path(user) user_ext_path = os.path.join(os.path.dirname(user_alt_path), 'cl.php.d') alternatives = self.get_all_alternatives_data() user_backup_path = os.path.join( self._clpwd.get_homedir(user), '.cl.selector') if not os.path.isdir(user_backup_path): previous_user_data = self._change_uid(user) try: clcaptain.mkdir(user_backup_path) except (OSError, ExternalProgramFailed) as e: ClUserSelect._restore_uid(previous_user_data) raise ClSelectExcept.UnableToSaveData(user_backup_path, e) ClUserSelect._restore_uid(previous_user_data) user_backup_file = os.path.join( user_backup_path, 'defaults.cfg') if os.path.isdir(user_alt_path): version = '[versions]\n%s = %s\n' % ( self._item, self.get_version(user)[0]) else: version = '[versions]\n%s = native\n' % (self._item,) backup_contents.append(version) for alt in sorted(alternatives.keys()): if self.without_cagefs: curr_ext_path = user_alt_path + "/alt_php" + alt.replace('.', '') + '.ini' else: curr_ext_path = os.path.join( user_ext_path, "alt-php%s" % ( (alt.replace('.', ''),)), 'alt_php.ini') extensions = self._skim_over_extensions(curr_ext_path) backup_contents.append( "[%s%s]\nmodules = %s\n" % ( self._item, alt, ','.join(sorted(extensions)))) self._write_to_file( user, '\n'.join(backup_contents), user_backup_file) def _switch_php_da_isp(self, user, version): if self.without_cagefs: return da_change_user_php_ini(user, version) ispmanager_create_user_wrapper(user, version)