ok
Direktori : /opt/cloudlinux/venv/lib/python3.11/site-packages/clselector/ |
Current File : //opt/cloudlinux/venv/lib/python3.11/site-packages/clselector/selectorlib.py |
#!/opt/cloudlinux/venv/bin/python3 -bb # 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 division from __future__ import absolute_import import base64 import collections import glob import json import os import pathlib import pwd import re import shutil import subprocess import sys import traceback from typing import Set # NOQA from future.utils import iteritems import cldetectlib as detect import clselect.clselectctlnodejsuser import clselect.clselectexcept import clselect.clselectctlpython import clselect.clselectctlruby import clselect.clselectpythonuser import clselect.clselectnodejsuser from clcommon.clexception import FormattedException from clcommon.clpwd import resolve_username_and_doc_root from clcommon.cpapi import CP_NAME, docroot from clcommon.cpapi.cpapiexceptions import NoDomain, NotSupported, IncorrectData from cllimits.lib import exec_utility from clselect import clselectctl from clselect import clpassenger from clselect import ClUserSelect, ClSelect, ClExtSelect from clselect.baseclselect import APP_STARTED_CONST, ENABLED_STATUS, DISABLED_STATUS, BaseSelectorError from clselect.clselectctlnodejsuser import validate_env_vars from clselect.clselectctlphp import format_summary, API_1 from clselect.clselectexcept import ClSelectExcept as ClSelectExceptions from clselect.clselectnodejs import NodeJSConfigError from clselect.clselectnodejs.node_manager import NodeManager from clselect.clselectpython.apps_manager import ( PythonAppFormatVersion, get_venv_rel_path ) from clselect.clselectpython.python_manager import PythonManager from clselect.clselectpythonuser.environments import Environment as PythonEnvironment # NOQA from clselect.clselectnodejsuser.environments import Environment as NodeJsEnvironment # NOQA from clselect.utils import ( mkdir_p, file_read, file_write, get_using_realpath_keys, get_abs_rel, delete_using_realpath_keys, ) from secureio import get_perm from clconfig.ui_config_lib import _set_ui_config, UIConfigException class ClSelectExcept(FormattedException): pass class ClSelectDomainNotFound(ClSelectExcept): """ Custom exception in case if user doesn't have the specific domain """ pass OK_RES_DICT = {"status": "ok"} class CloudlinuxSelectorLib(object): def __init__(self, interpreter): self.interpreter = interpreter self._SELECTORCTL_UTILITY = "/usr/bin/selectorctl" self.DYNAMIC_UI_CTL_CMD = '/usr/share/l.v.e-manager/utils/dynamicui.py' self.CLOUDLINUX_SELECTOR_UTILITY = '/usr/sbin/cloudlinux-selector' self.NODEJS_INTERPRETER = "nodejs" self.PYTHON_INTERPRETER = "python" self.RUBY_INTERPRETER = "ruby" self.PHP_INTERPRETER = "php" # self.selector_manager - responsible for actual selector high-level API # self.apps_manager - responsible for gathering and set information about applications # self.selector_user_lib - set of modules which responsible user's part of work with virtual envs and etc # self.selector_old_lib - old libraries for work with applications self.selector_manager = None self.apps_manager = None self.selector_user_lib = None self.selector_old_lib = None if self.interpreter == self.NODEJS_INTERPRETER: from clselect.clselectnodejs.apps_manager import ApplicationsManager self.apps_manager = ApplicationsManager() self.selector_manager = NodeManager() self.selector_user_lib = clselect.clselectnodejsuser self.selector_old_lib = clselect.clselectctlnodejsuser elif self.interpreter == self.PYTHON_INTERPRETER: from clselect.clselectpython.apps_manager import ApplicationsManager self.apps_manager = ApplicationsManager() self.selector_manager = PythonManager() self.selector_user_lib = clselect.clselectpythonuser self.selector_old_lib = clselect.clselectctlpython elif self.interpreter == self.PHP_INTERPRETER: from clselect.clselectphp.php_manager import PhpManager self.selector_manager = PhpManager() def check_selector_is_available(self): """ Checks that selector is able to work on current os environment :return: """ if self.interpreter != self.PHP_INTERPRETER: return # check that native version is installed for php self.selector_manager.cl_select_lib.check_requirements() def safely_resolve_doc_root_for_app(self, username, app_root): """Get doc_root from application config or raise exception""" domain = self.apps_manager.get_app_domain(username, app_root) _, doc_root = self.safely_resolve_username_and_doc_root(username, domain) return doc_root @staticmethod def safely_resolve_username_and_doc_root(user=None, domain=None): """ Safely resolve username and doc_root by domain, or resolve document root by username, or resolve document root and username by effective uid :param user: str -> name of unix user :param domain: str -> domain of panel user :return: tuple -> user, doc_root """ try: result_user, result_doc_root = resolve_username_and_doc_root( user=user, domain=domain, ) except NoDomain: raise ClSelectDomainNotFound( { 'message': 'No such domain: %(domain)s' if domain is not None else 'No such user: %(user)s', 'context': { 'domain': domain, 'user': user, }, } ) except NotSupported: raise ClSelectExcept( { 'message': 'Nodejs selector not supported for %(panel)s', 'context': { 'panel': CP_NAME }, } ) except IncorrectData: raise ClSelectExcept( { 'message': 'Domain %(domain)s is not owned by the user %(user)s', 'context': { 'domain': domain, 'user': user }, } ) return result_user, result_doc_root @staticmethod def should_be_runned_as_user(opts): """ Check whether selector should be run through "su - user" :param opts: dict of parsed cli params :return: True if should be run through su or False if not """ result = True euid, egid = get_perm() if opts is None: result = False elif not isinstance(opts, dict): result = False elif opts['--user'] is None and opts['--domain'] is None: # if --user and --domain are absent - root result = False elif euid != 0 and egid != 0: result = False elif opts['--interpreter'] == 'php': result = False return result @staticmethod def should_run_user_without_cagefs(opts): return opts['--interpreter'] == 'php' and\ opts['--user'] is not None and\ os.geteuid() == 0 @staticmethod def _get_user_pwd_data(user=None): """ Resolves user eigher with passed username or with getting current user ID :param user: str or None -> username to be resolved :return: obj -> pwd user object """ userdata = pwd.getpwnam(user) return userdata @staticmethod def user_and_domain_checker(user=None, domain=None): """ Check if user and domain are None :param user: name of unix user :param domain: domain of panel user :return: None """ if user is None and domain is None: raise ClSelectExcept( { 'message': 'User or domain parameter must be specified if current user is root', 'context': {} } ) @staticmethod def _return_error(result, **kwargs): err = {"status": "ERROR: %s" % result} if len(kwargs) > 0: err.update(kwargs) return err @staticmethod def _return_with_status_error(result, details=None): """ Construct error dict in one place :param result: error string :return: dict with 'status':'error' and error message """ err = { 'status': 'error', 'result': result } if details: err.update({'details': details}) return err def _change_selector_status(self, status): """ Set CL selector status in it's config :param status: set status of selector :return: error or ok message """ if status not in (ENABLED_STATUS, DISABLED_STATUS,): return self._return_error( 'Unknown selector status provided: "{}". ' "Can be only 'enabled' or 'disabled'".format(status)) try: self.selector_manager.selector_enabled = status == ENABLED_STATUS except BaseSelectorError as e: return self._return_error(e) # Backward compatibility with cloudlinux-config on python if self.interpreter == self.PYTHON_INTERPRETER: # Create hidePythonApp dictionary config_dict = {'uiSettings': {'hidePythonApp': status != ENABLED_STATUS}} try: # _set_ui_config writes changes and updates UI _set_ui_config(config_dict) # We dont need to call self.update_ui after return OK_RES_DICT except UIConfigException: pass # Backward compatibility with cloudlinux-config on nodejs if self.interpreter == self.NODEJS_INTERPRETER: config_dict = {'uiSettings': {'hideNodeJsApp': status != ENABLED_STATUS}} try: _set_ui_config(config_dict) return OK_RES_DICT except UIConfigException: pass self.update_ui() return OK_RES_DICT @staticmethod def get_nodejs_selector_status(): res = {'selector_enabled': False} if NodeManager().selector_enabled: res['selector_enabled'] = True return res def get_php_selector_status(self): return {'PHPSelector': ENABLED_STATUS if self.selector_manager.selector_enabled else DISABLED_STATUS} def get_summary(self): try: res = self.selector_manager.get_summary() res.update(self.get_selector_status()) return res except BaseSelectorError as e: return self._return_error(e) @staticmethod def get_python_selector_status(): res = {'selector_enabled': False} if PythonManager().selector_enabled: res['selector_enabled'] = True return res @staticmethod def get_user_home(user): try: return pwd.getpwnam(user).pw_dir except KeyError: raise ClSelectExcept({ 'message': 'No such user: `%(user)s`' }) def get_nodejs_summary(self): res = NodeManager().get_summary() res.update(self.get_selector_status()) return res def run_script(self, user, app_root, script_name, script_args=None): """ Runs script to execute other script inside user app environment :param user: str -> owner of application :param app_root: str -> application directory :param script_name: str -> name of script :param script_args: list of str -> arguments for the script :return: dict """ user_home = self.get_user_home(user) if self.interpreter == self.NODEJS_INTERPRETER: interpreter_path = self.apps_manager.get_binary_path( user, app_root, user_home, 'npm') elif self.interpreter == self.PYTHON_INTERPRETER: interpreter_path = self.apps_manager.get_binary_path( user, app_root, user_home, 'python') else: raise NotImplementedError() cmd = [interpreter_path] if self.interpreter == self.NODEJS_INTERPRETER: cmd.append('run-script') cmd.append(script_name) if script_args is not None: if self.interpreter == self.NODEJS_INTERPRETER: cmd.append('--') cmd.extend(script_args) result = self._run_interpreter(cmd, user_home, app_root) return result @staticmethod def _run_interpreter(cmd, user_home, app_root): """ Run interpreter in users environment :param cmd: list -> command to execute :param user_home: -> user home directory :param app_root: -> app path :return: dict """ p = subprocess.Popen( args=cmd, cwd=os.path.join(user_home, app_root), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) try: stdout, stderr = p.communicate() except OSError as e: raise ClSelectExcept({ 'message': ('run-script call: `%(args)s` failed ' 'with error: %(err)s'), 'context': {'args': cmd, 'err': e} }) output_string = ( 'returncode: {}\n' 'stdout:\n{}\n' 'stderr:\n{}\n' ).format( p.returncode, stdout.strip(), stderr.strip()) if any('out of memory' in output.lower() for output in (stdout, stderr)): pid = os.getpid() proc_limits_path = f'/proc/{pid}/limits' process_limits = pathlib.Path(proc_limits_path).read_text() output_string += ( '\nOut of memory error may be caused by hitting LVE limits\n' 'or "Max data size", "Max address space" or "Max resident set" process limits\n' 'Please check LVE limits and process limits. Readjust them if necessary\n' 'More info: https://docs.cloudlinux.com/shared/cloudlinux_os_components/#known-restrictions-and-issues' f'\n\nprocess limits "{proc_limits_path}":\n{process_limits}\n' ) result = {'data': base64.b64encode(output_string.encode()).decode()} result.update({'status': 'success'}) if p.returncode != 0: result['warning'] = f'Script exit code: {p.returncode}' return result def get_full(self): if self.interpreter in (self.NODEJS_INTERPRETER, self.PHP_INTERPRETER): return self.selector_manager.get_summary() @staticmethod def _add_statistics_field(versions_list): """ Add selector usage statistics (amount of domains that use some version, etc) Fist parameter is an array with such format: [{'version': '5.6'}, {'version': '7.6'}] Output is an array with such format: [{'version': '5.6', 'users': 10}, ...] :type versions_list: list :rtype: list """ user_version_map = ClUserSelect().get_user_version_map() # count users per php version version_user_map = collections.Counter() for user, version in iteritems(user_version_map): version_user_map[version] += 1 # and add additional field to the output for item in versions_list: item['total_users'] = version_user_map[item['version']] return versions_list def get_supported_versions(self): """ Retrieves supported versions list and default version """ if self.interpreter == self.PHP_INTERPRETER: # ToDo: make get_supported_versions do not request all information (like it return PHP) data = ClSelect(self.interpreter).get_summary(False) json_data = format_summary(data, format='json', api_version=API_1) selectorctl_result = json.loads(json_data) # Our Interpreter-specific selectorctl commands should support API >= 1 elif self.selector_manager is not None: selectorctl_result = self.selector_manager.get_summary() else: selectorctl_result = {} if selectorctl_result.get('status') == 'ERROR': # e.g. {"status": "ERROR", "message": "alt-php packages not found"} result = {"status": selectorctl_result['message']} else: result = selectorctl_result return result def get_current_version(self, user=None): """ Retrives current version for user """ user = [user] if user else None if self.interpreter == self.PHP_INTERPRETER: return self.selector_manager.get_current_version(user) else: return self._return_error('Supported only by php selector') def get_default_version(self): json_dict = self.get_supported_versions() try: default_version = json_dict['default_version'] except KeyError: if 'message' in json_dict: return self._return_error(json_dict['message']) else: return {"status": "ERROR", "data": json_dict} return {'default_version': default_version} def get_selector_status(self): if self.interpreter == self.NODEJS_INTERPRETER: return self.get_nodejs_selector_status() elif self.interpreter == self.PYTHON_INTERPRETER: return self.get_python_selector_status() elif self.interpreter == self.PHP_INTERPRETER: return self.get_php_selector_status() def php_selector_is_disabled(self): return not self.selector_manager.selector_enabled def php_selector_is_enabled(self): return self.selector_manager.selector_enabled def check_multiphp_system_default(self): """ Returns True when MultiPHP system default PHP version is alt-php and PHP Selector is NOT disabled For details please see LVEMAN-1170 """ if self.interpreter != self.PHP_INTERPRETER: return False try: if '/usr/share/cagefs' not in sys.path: sys.path.append('/usr/share/cagefs') from cagefsctl import is_ea4_enabled, read_cpanel_ea4_php_conf except ImportError: return False if is_ea4_enabled() and not self.php_selector_is_disabled(): 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'] if not default_php.startswith('ea-php'): return True except KeyError: pass return False def set_supported_versions(self, versions): if self.interpreter in (self.NODEJS_INTERPRETER, self.PYTHON_INTERPRETER,): for version, enable_disable in iteritems(versions): try: result = self.set_version_status(enable_disable, version) except BaseSelectorError as e: return self._return_error(e) if result != OK_RES_DICT: return result return OK_RES_DICT json_dict = self.get_supported_versions() try: alternatives_list = json_dict["available_versions"] except KeyError: return self._return_error("corrupted answer from %s --json --summary --interpreter %s" % ( self._SELECTORCTL_UTILITY, self.interpreter)) alternatives_versions = {x["version"] for x in alternatives_list} if set(versions.keys()) - alternatives_versions: return self._return_error( "invalid alternative versions (%s), see %s --summary --interpreter %s for valid versions" % ( ', '.join(set(versions.keys()) - alternatives_versions), self._SELECTORCTL_UTILITY, self.interpreter)) # TODO: change error message success = 0 errors_list = [] for version, to_enable in iteritems(versions): if to_enable: ClSelect(self.interpreter).enable_version(str(version)) else: ClSelect(self.interpreter).disable_version(str(version)) success += 1 if success == len(versions): return OK_RES_DICT elif success > 0: return { "status": "WARNING: only {} of {} commands was successful. " "Errors was: {}".format(success, len(versions), errors_list) } else: return self._return_error("All commands were failed" "Errors was: {}".format(errors_list)) def set_default_version(self, version): try: if self.selector_manager is not None: self.selector_manager.switch_default_version(version) except clselect.clselectexcept.ClSelectExcept.NoSuchAlternativeVersion: # No such alt-php version raise ClSelectExcept({'message': "No such php version: %(ver)s", 'context': {'ver': version}}) except (clselect.clselectexcept.BaseClSelectException, NodeJSConfigError) as e: return self._return_with_status_error(str(e)) return OK_RES_DICT def set_version_status(self, target_version_status, version): """ Disable or enable version of selector :param target_version_status: disable or enable version of interpreter :param version: version of interpreter :return: OK_RES_DICT """ if target_version_status: self.selector_manager.set_version_status(version, ENABLED_STATUS) else: self.selector_manager.set_version_status(version, DISABLED_STATUS) return OK_RES_DICT def set_current_version(self, version): try: if self.interpreter == self.PHP_INTERPRETER: self.selector_manager.switch_current_version(version) except clselect.clselectexcept.ClSelectExcept.NoSuchAlternativeVersion: # No such alt-php version raise ClSelectExcept({'message': "No such php version: %(ver)s", 'context': {'ver': version}}) except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) return OK_RES_DICT def reset_extensions(self, version): extensions = {} try: if self.interpreter == self.PHP_INTERPRETER: extensions = self.selector_manager.reset_extensions(version) except clselect.clselectexcept.ClSelectExcept.NoSuchAlternativeVersion: # No such alt-php version raise ClSelectExcept({'message': "No such php version: %(ver)s", 'context': {'ver': version}}) except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) return extensions def set_selector_status(self, status): if self.interpreter in (self.NODEJS_INTERPRETER, self.PYTHON_INTERPRETER, self.PHP_INTERPRETER): return self._change_selector_status(status) def update_ui(self): if detect.is_cpanel() or detect.is_plesk() or detect.is_da(): retcode, out, err = exec_utility( self.DYNAMIC_UI_CTL_CMD, ['--sync-conf=all'], stderr=True) if retcode != 0: return self._return_error( "Can not sync UI with reason: {} {}".format(out, err)) return None def set_extensions(self, extensions, version): result = OK_RES_DICT.copy() if self.interpreter == self.PHP_INTERPRETER: try: data = self.selector_manager.set_extensions(version, extensions) if data: result.update(data) except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) return result def set_options(self, options, version): if self.interpreter == self.PHP_INTERPRETER: try: self.selector_manager.set_options(version, options) except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) return OK_RES_DICT def resolve_version(self, version): """ Attempts to get or verify version to be passed to external program Currently supported version is one digit (6 or 8). If version is None, return a default version :param version: str or None -> version to be verified or found :return: str -> digit as string """ if version is None: default_version = self.get_default_version().get('default_version', '') if default_version is None or default_version == '': # Interpreter default version not defined raise ClSelectExcept("{} default version not defined".format(self.interpreter)) if self.interpreter == self.NODEJS_INTERPRETER: m = re.match(r'(?P<version>\d+)', default_version) if not m: raise ClSelectExcept({'message': "Incorrect selector version: %(ver)s", 'context': {'ver': default_version}}) return m.group('version') else: return default_version if isinstance(version, (int, float)): if self.interpreter == self.NODEJS_INTERPRETER: # For NodeJS use only major version return str(int(version)) # TODO: check among supported versions else: # For Python use full version return str(version) if version == 'native': raise ClSelectExcept({'message': "Unsupported version: %(ver)s", 'context': {'ver': version}}) return version # TODO: do check among supported versions def create_app(self, app_root, app_uri, version, user=None, domain=None, app_mode=None, startup_file=None, env_vars=None, entry_point=None, passenger_log_file=None): """ Creates application for specified user, interpreter and version If user is None we hope that the external application resolves a user Currently NodeJS supported only :param domain: str -> domain of the application :param app_root: str -> app path relative to user home :param app_uri: str -> URI path of the application :param version: str or None -> version of the interpreter :param user: str or None -> username of user who owns the app :param app_mode: str or None -> application mode (development or production) :param startup_file: str or None -> main application file :param env_vars: json_string or None -> enviroment variables for application :param entry_point: Application entry point (used only for python interpreter). :param passenger_log_file: Passenger log filename :return: dict """ self.user_and_domain_checker(user, domain) version = self.resolve_version(version) if env_vars is not None: env_vars = validate_env_vars(json.loads(env_vars)) user, doc_root = self.safely_resolve_username_and_doc_root(user, domain) try: if self.interpreter == self.PYTHON_INTERPRETER: self.selector_old_lib.create(user, app_root, app_uri, version, doc_root=doc_root, env_vars=env_vars, startup_file=startup_file, domain_name=domain, entry_point=entry_point, apps_manager=self.apps_manager, passenger_log_file=passenger_log_file) elif self.interpreter == self.RUBY_INTERPRETER: clselect.clselectctlruby.create(user, app_root, app_uri, version, doc_root=doc_root) # ruby elif self.interpreter == self.NODEJS_INTERPRETER: # args[0] - directory, args[1] - alias (app-uri) # nodejs self.selector_old_lib.create( user, app_root, app_uri, version=version, doc_root=doc_root, app_mode=app_mode, env_vars=env_vars, startup_file=startup_file, domain_name=domain, apps_manager=self.apps_manager, passenger_log_file=passenger_log_file ) except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(result=str(e)) # TODO: catch ClSelectExcept.BusyApplicationRoot return OK_RES_DICT @staticmethod def _get_application_path(app_root, userdata): """ Resolve app root to absolute path and checks if it exists :param app_root: str -> relative a user homedir app path :param userdata: obj -> pwd user object :return: str -> absolute path to app """ app_path = os.path.join(userdata.pw_dir, app_root) if not os.path.isdir(app_path): raise ClSelectExcept({'message': "No such application: %(app)s", 'context': {'app': app_root}}) return app_path def read_app_config(self, app_root, config_path, user=None): """ Reads file and returns its content as Base64 string :param app_root: str -> path to app relative to user home :param user: str -> username to resolve app path :param config_path: str -> file to be read (relative to app path) :return: dict """ result = OK_RES_DICT.copy() self.user_and_domain_checker(user, None) userdata = self._get_user_pwd_data(user) app_path = self._get_application_path(app_root, userdata) full_config_path = os.path.join(app_path, config_path) if not os.path.exists(full_config_path): raise ClSelectExcept( { 'message': "Configuration file not found: %(path)s", 'context': {'path': full_config_path} } ) try: with open(full_config_path, 'rb') as f: data = f.read() result.update({'data': base64.b64encode(data).decode()}) return result except Exception as e: return self._return_with_status_error(getattr(e, 'strerror', '')) def save_app_config(self, app_root, config_path, content, user=None): """ Saves data passed as Base64 string to specified file :param content: data for saving in app's config :param app_root: str -> path to app relative to user home :param user: str -> username to resolve app path :param config_path: str -> file to be read (relative to app path) :param content: str -> Base64-encoded string :return: dict """ self.user_and_domain_checker(user, None) userdata = self._get_user_pwd_data(user) app_path = self._get_application_path(app_root, userdata) full_config_path = os.path.join(app_path, config_path) try: with open(full_config_path, 'wb') as f: f.write(base64.b64decode(content)) return OK_RES_DICT except Exception as e: return self._return_with_status_error(getattr(e, 'strerror', '')) @staticmethod def _add_user_or_domain(user, domain, args): result_args = list(args) if domain is not None: result_args.extend(['--domain', domain]) elif user is not None: result_args.extend(['--user', user]) else: raise ClSelectExcept('User or domain parameter must be specified if current user is root') return result_args def uninstall_modules(self, app_root, modules, user=None, domain=None, skip_web_check=False): """ Uninstall described modules for user's webapp :param app_root: directory with webapp :param modules: comma-separated list of modules to uninstall :param user: name of unix user :param domain: domain of user :param skip_web_check: skip check web application after changing its properties :return: None """ self.user_and_domain_checker(user, domain) if self.interpreter != self.PYTHON_INTERPRETER: raise ClSelectExcept({ 'message': 'Uninstall command is available only for python interpreter, not %(interp)s', 'context': { 'interp': self.interpreter, }, }) try: for module in modules: self.selector_old_lib.uninstall(user, app_root, module) except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) else: return OK_RES_DICT def install_modules(self, app_root, user=None, domain=None, skip_web_check=False, spec_file=None, modules=()): """ Install described modules for user's webapp :type domain: domain of user :param user: name of unix user :param app_root: directory with webapp :param skip_web_check: skip check web application after change it's properties :param spec_file: file containing modules and their versions to install :param modules: list of installed modules :return: None """ self.user_and_domain_checker(user, domain) if self.interpreter == self.NODEJS_INTERPRETER: try: self.selector_old_lib.install(user, app_root, skip_web_check=skip_web_check, apps_manager=self.apps_manager) except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) else: return OK_RES_DICT elif self.interpreter == self.PYTHON_INTERPRETER: try: if modules: for module in modules: self.selector_old_lib.install(user, app_root, module, None, skip_web_check=skip_web_check, apps_manager=self.apps_manager) elif spec_file: self.selector_old_lib.install(user, app_root, None, spec_file, skip_web_check=skip_web_check, apps_manager=self.apps_manager) else: err = "Please, specify modules or requirements file with modules" return self._return_with_status_error(err) except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) else: return OK_RES_DICT else: raise ClSelectExcept({ 'message': 'Unknown interpreter: %(interp)s', 'context': { 'interp': self.interpreter, }, }) def destroy_app(self, app_root, user): """ Destroy specified application root directory and user name :param app_root: Application directory :param user: name of unix user :return: dict """ self.user_and_domain_checker(user, None) try: if self.interpreter == self.RUBY_INTERPRETER: clselect.clselectctlruby.destroy(user, app_root) else: try: doc_root = self.safely_resolve_doc_root_for_app(user, app_root) except TypeError: raise ClSelectExceptions.WrongData( message='No such application or it\'s broken. ' 'Unable to find app-root folder by this path %(app_root)s', context={ 'app_root': app_root } ) except ClSelectDomainNotFound: # We still want to clean up an application doc_root = None if self.interpreter in (self.NODEJS_INTERPRETER, self.PYTHON_INTERPRETER): self.selector_old_lib.destroy(user, app_root, doc_root=doc_root, apps_manager=self.apps_manager) else: raise ClSelectExceptions.InterpreterError( message='Unknown interpreter: %(interp)s', context={'interp': self.interpreter}) except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) return OK_RES_DICT def start_app(self, app_root, username): """ Start specified application root directory and user name :param app_root: Application directory :param username: name of unix user :return: dict """ self.user_and_domain_checker(username, None) try: doc_root = self.safely_resolve_doc_root_for_app(username, app_root) if self.interpreter in (self.NODEJS_INTERPRETER, self.PYTHON_INTERPRETER): self.selector_old_lib.start(username, app_root, doc_root, self.apps_manager) else: raise ClSelectExceptions.InterpreterError( message='Unknown interpreter: %(interp)s', context={'interp': self.interpreter}) return OK_RES_DICT except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) def restart_app(self, app_root, username): """ Destroy specified application root directory and user name :param app_root: Application directory :param username: name of unix user :return: dict """ self.user_and_domain_checker(username, None) try: doc_root = self.safely_resolve_doc_root_for_app(username, app_root) if self.interpreter == self.NODEJS_INTERPRETER: self.selector_old_lib.restart(username, app_root, doc_root, self.apps_manager) elif self.interpreter == self.PYTHON_INTERPRETER: self.selector_old_lib.restart(username, app_root, doc_root, self.apps_manager) else: raise ClSelectExceptions.InterpreterError( message='Unknown interpreter: %(interp)s', context={'interp': self.interpreter}) return OK_RES_DICT except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) def stop_app(self, app_root, user): """ Start specified application root directory and user name :param app_root: Application directory :param user: name of unix user :return: dict """ self.user_and_domain_checker(user, None) try: doc_root = self.safely_resolve_doc_root_for_app(user, app_root) if self.interpreter == self.NODEJS_INTERPRETER: self.selector_old_lib.stop(user, app_root, doc_root, self.apps_manager) elif self.interpreter == self.PYTHON_INTERPRETER: self.selector_old_lib.stop(user, app_root, doc_root, self.apps_manager) else: raise ClSelectExceptions.InterpreterError( message='Unknown interpreter: %(interp)s', context={'interp': self.interpreter}) return OK_RES_DICT except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) @staticmethod def _replace_old_env_and_prompt_in_binaries_in_environment(new_env, old_env, new_rel, old_rel): # type: (str, str, str, str) -> None """ Replace old prompt and env_var in binaries in new environment Working with bytes here, because of python binary """ old_prompt = ('(' + old_rel + ':').encode() new_prompt = ('(' + new_rel + ':').encode() for venv_bin_file in glob.glob(os.path.join(new_env, '*', 'bin', '*')): if not os.path.isdir(venv_bin_file): try: old_activate = file_read(venv_bin_file, mode='rb') if old_env.encode() in old_activate or old_prompt in old_activate: _new_activate = old_activate.replace(old_env.encode(), new_env.encode()) new_activate = _new_activate.replace(old_prompt, new_prompt) file_write(venv_bin_file, new_activate, 'wb') except IOError: _, _, traceback_ = sys.exc_info() sys.stderr.write(str(traceback.print_tb(traceback_))) def get_app_summary(self, username, app_config_dict_full, app_root): """ Retrieve application info from user's applications config. Analog of function clpassenger.summary :param username: User name :param app_config_dict_full: Full user's application config. :param app_root: Application root :return: Dictionary with application info Example: { 'binary': '/home/cltest1/virtualenv/new_app_root/2.7/bin/python', # + 'domain': 'cltest1.com', # + 'alias': 'app1', # + 'htaccess': '/home/cltest1/public_html/app1/.htaccess', # + 'interpreter': 'python', # + 'directory': '/home/cltest1/new_app_root', # + 'docroot': '/home/cltest1/public_html', # + 'domains': ['cltest1.com'] } """ try: app_config_dict = get_using_realpath_keys(username, app_root, app_config_dict_full) user_home = pwd.getpwnam(username).pw_dir doc_root = docroot(app_config_dict['domain'])[0] app_info_dict = { app_root: { 'interpreter': self.apps_manager.INTERPRETER, 'binary': self.apps_manager.get_binary_path(username, app_root, user_home), 'alias': app_config_dict['app_uri'], 'domain': app_config_dict['domain'], 'docroot': doc_root, 'htaccess': '/'.join([doc_root, app_config_dict['app_uri'], '.htaccess']), 'directory': '/'.join([user_home, app_root]), 'domains': [app_config_dict['domain']] } } return app_info_dict except KeyError: # we return empty dict because app doesn't exist return {} @staticmethod def _move_app_from_old_dir_to_new(old_directory, new_directory): # type: (str, str) -> None """ Move all items from old directory of application to new directory :param old_directory: full real path to old directory of applicaton :param new_directory: full real path to new directory of applicaton """ if not os.path.exists(new_directory): mkdir_p(new_directory) os.rename(old_directory, new_directory) else: # move all items from old directory to new for item in os.listdir(old_directory): shutil.move(os.path.join(old_directory, item), new_directory) os.rmdir(old_directory) def _relocate(self, user, old_directory, new_directory): """ Move user's application from directory to new_directory :param user: application owner. unix like user name :param old_directory: current directory with application :param new_directory: new directory for application :return: None """ full_config = self.apps_manager.get_user_config_data(user) try: app_config = get_using_realpath_keys(user, old_directory, full_config) except KeyError: raise ClSelectExceptions.NoSuchApplication( 'No such application (or application not configured) "%s"' % old_directory) old_abs, old_rel = get_abs_rel(user, old_directory) new_abs, new_rel = get_abs_rel(user, new_directory) try: # Directory name must not be one of the reserved names and # should not contain invalid symbols. clselectctl.check_directory(new_rel) except ValueError as e: raise ClSelectExceptions.WrongData(str(e)) # Get application summary for the application # Application summary example # {'new_app_root': # {'binary': '/home/cltest1/virtualenv/new_app_root/2.7/bin/python', # 'domain': 'cltest1.com', # 'alias': 'app1', # 'htaccess': '/home/cltest1/public_html/app1/.htaccess', # 'interpreter': 'python', # 'directory': '/home/cltest1/new_app_root', # 'docroot': '/home/cltest1/public_html', # 'domains': ['cltest1.com']} # } # TODO: why do we check only for applications of same type and not other (node/ruby/python)? new_user_summary = self.get_app_summary(user, full_config, new_rel) try: get_using_realpath_keys(user, new_rel, new_user_summary) except KeyError: pass else: raise ClSelectExceptions.AppRootBusy(new_abs) old_user_summary = self.get_app_summary(user, full_config, old_rel) try: old_user_app_summary = get_using_realpath_keys(user, old_rel, old_user_summary) except KeyError: raise ClSelectExceptions.WrongData("No such application (or application not configured) \"%s\"" % old_directory) doc_root = old_user_app_summary['docroot'] alias = old_user_app_summary['alias'] env_name = self.selector_old_lib._get_environment( user, old_directory, app_summary=old_user_summary[old_directory]).name if self.interpreter == self.PYTHON_INTERPRETER: ver, rel_venv = get_venv_rel_path(user, old_directory) _old_env, _ = get_abs_rel( user, rel_venv) _new_env, _ = get_abs_rel( user, get_venv_rel_path(user, new_directory, version=ver)[1]) elif self.interpreter == self.NODEJS_INTERPRETER: _old_env, _ = get_abs_rel(user, os.path.join( self.selector_user_lib.environments.DEFAULT_PREFIX, old_rel)) _new_env, _ = get_abs_rel(user, os.path.join( self.selector_user_lib.environments.DEFAULT_PREFIX, new_rel)) else: raise NotImplementedError() old_env = os.path.join(_old_env, '') new_env = os.path.join(_new_env, '') new_env_dir = os.path.dirname(_new_env) # needed to avoid copy in shutil.move if not os.path.exists(new_env_dir): os.makedirs(new_env_dir) shutil.move(old_env, new_env) self._move_app_from_old_dir_to_new(old_abs, new_abs) if self.interpreter == self.PYTHON_INTERPRETER: self._replace_old_env_and_prompt_in_binaries_in_environment(new_env, old_env, new_rel, old_rel) if self.interpreter == self.PYTHON_INTERPRETER: _, prefix = get_venv_rel_path(user, new_directory) elif self.interpreter == self.NODEJS_INTERPRETER: prefix = self.selector_old_lib._get_prefix(user, new_directory) else: raise NotImplementedError() environment = self.selector_user_lib.environments.Environment(env_name, user, prefix) binary = environment.interpreter().binary app_status = get_using_realpath_keys(user, old_directory, full_config)['app_status'] if app_status == APP_STARTED_CONST: # Clear .htaccess from CL's directives clpassenger.unconfigure(user, old_directory) passenger_log_file_to_set = full_config[old_directory].get('passenger_log_file', None) clpassenger.configure(user, new_directory, alias, self.interpreter, binary, doc_root=doc_root, startup_file=app_config['startup_file'], passenger_log_file=passenger_log_file_to_set) clpassenger.restart(user, new_directory) # update config delete_using_realpath_keys(user, old_directory, full_config) full_config[new_directory] = app_config self.apps_manager.write_full_user_config_data(user, full_config) def relocate(self, user, old_app_root, new_app_root): """ Call selectorctl to relocate application from old_app_root to new_app_root :param user: application owner :param old_app_root: current application directory (current application name) :param new_app_root: new application directory (new application name) :return: json """ try: if self.interpreter in (self.PYTHON_INTERPRETER, self.NODEJS_INTERPRETER): self._relocate(user, old_app_root, new_app_root) elif self.interpreter == self.RUBY_INTERPRETER: # last param in relocate not used clselect.clselectctlruby.relocate(user, old_app_root, new_app_root, None) except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) return OK_RES_DICT # TODO: we have something similar in clpassenger.move # one day we should remove one of that methods @staticmethod def _transit_htaccess_file(old_doc_root, old_alias, new_doc_root, new_alias): # type: (str, str, str, str) -> None """ :param old_doc_root: path to old doc root of application :param old_alias: old alias (uri) of application :param new_doc_root: path to new doc root of application :param new_alias: new alias (uri) of application :return: None """ # Copy existing .htaccess to new location htaccess = '.htaccess' # Get path to old .htaccess old_htaccess_file = os.path.join(old_doc_root, old_alias, htaccess) # Create path for new .htaccess new_htaccess_file = os.path.join(new_doc_root, new_alias, htaccess) if os.path.realpath(old_htaccess_file) \ == os.path.realpath(new_htaccess_file): return new_htaccess_path = os.path.dirname(new_htaccess_file) if not os.path.isdir(new_htaccess_path): os.makedirs(new_htaccess_path) shutil.copy(old_htaccess_file, new_htaccess_file) def _transit(self, user, directory, new_doc_root, new_domain, alias=None): """ Change application URI :param user: application owner. unix like user name :param directory: directory with application. (app-root) :param alias: new alias (app-uri) for application or None if change only the domain :param new_doc_root: NEW doc_root to transit application to :param new_domain: NEW domain to transit application to :return: None """ full_config = self.apps_manager.get_user_config_data(user) try: app_config = get_using_realpath_keys(user, directory, full_config) except KeyError: raise ClSelectExceptions.NoSuchApplication( 'No such application (or application not configured) "{}"'.format( directory)) apps_summary = self.get_app_summary(user, full_config, directory) try: old_app_summary = get_using_realpath_keys(user, directory, apps_summary) except KeyError: raise ClSelectExceptions.WrongData( 'No such application ' '(or application not configured) "{}"'.format(directory)) old_alias = old_app_summary['alias'] old_doc_root = old_app_summary['docroot'] new_alias = old_alias if alias is None else clselectctl.get_alias(alias) environment = self.selector_old_lib._get_environment( user, directory, app_summary=apps_summary[directory]) binary = environment.interpreter().binary if full_config[directory]['app_status'] == APP_STARTED_CONST: passenger_log_file_to_set = full_config[directory].get('passenger_log_file', None) clpassenger.configure(user, directory, new_alias, self.interpreter, binary, True, 'transit', doc_root=new_doc_root, startup_file=app_config['startup_file'], passenger_log_file=passenger_log_file_to_set) clpassenger.move(user, directory, old_alias, new_alias, old_doc_root=old_doc_root, new_doc_root=new_doc_root) clpassenger.restart(user, directory) else: # New doc root should be equal to old doc root # if we don't want to change domain for application. if new_doc_root is None: new_doc_root = old_doc_root self._transit_htaccess_file(old_doc_root, old_alias, new_doc_root, new_alias) app_config['app_uri'] = new_alias if new_domain is not None: app_config['domain'] = new_domain self.apps_manager.write_full_user_config_data(user, full_config) def transit(self, user, app_root, new_app_uri=None, new_domain=None): """ Call selectorctl to transit application to new_app_uri :param user: application owner :param app_root: application directory (application name) :param new_app_uri: new uri or None if change only the domain :param new_domain: new domain or None if change only the app_uri :return: json """ try: if new_domain is None: new_doc_root = None else: _, new_doc_root = self.safely_resolve_username_and_doc_root(user, new_domain) if self.interpreter in (self.PYTHON_INTERPRETER, self.NODEJS_INTERPRETER): self._transit(user, app_root, new_doc_root, new_domain, new_app_uri) elif self.interpreter == self.RUBY_INTERPRETER: clselect.clselectctlruby.transit(user, app_root, new_app_uri, doc_root=new_doc_root) else: raise NotImplementedError() except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) return OK_RES_DICT def _relocate_nodejs_extensions(self, user, app_root, new_env, old_extensions): # type: (str, str, NodeJsEnvironment, Set) -> None """ Install nodejs extensions to new nodejs environment and change symlink <user_homedir>/<app-root>/node_modules to new environment. Raise exception `WebAppError` if npm will return non-zero code """ npm_ret_code = 0 if old_extensions: try: npm_ret_code = new_env.extension_install_single_call(old_extensions) except ClSelectExceptions.FileProcessError: pass # Change symlink <user_homedir>/<app-root>/node_modules to new environment self.selector_old_lib._create_symlink_to_node_modules(user, new_env.path, app_root) if npm_ret_code != 0: raise ClSelectExceptions.WebAppError("Module installation has been failed. Please, check npm logs.") @staticmethod def _relocate_python_extensions(new_env, old_extensions): # type: (PythonEnvironment, Set) -> None """ Install python extensions to new python environment. They are equivalent to extensions from old environment. Remove python extensions which not existing in old environment from new environment """ new_extensions = set(new_env.extensions()) for extension in new_extensions - old_extensions: try: new_env.extension_uninstall(extension) except ClSelectExceptions.ExternalProgramFailed: # TODO: logging # https://cloudlinux.atlassian.net/browse/LVEMAN-1465 pass for extension in old_extensions - new_extensions: try: new_env.extension_install(extension) except ClSelectExceptions.ExternalProgramFailed: # TODO: logging # https://cloudlinux.atlassian.net/browse/LVEMAN-1465 pass def _change_version(self, user, directory, version=None, skip_web_check=False): """ Set current interpreter version for the application :param user: application owner. unix like user name :param directory: app_root - main directory with user application :param version: new version of python interpreter or None if we get current :param skip_web_check: skip check web application after change it's properties :return: None """ full_config = self.apps_manager.get_user_config_data(user) try: app_config = get_using_realpath_keys(user, directory, full_config) except KeyError: raise ClSelectExceptions.NoSuchApplication( 'No such application (or application not configured) "%s"' % directory) old_environment = self.selector_old_lib._get_environment(user, directory) # reads .htaccess if not version: return {old_environment.name: old_environment.interpreter().as_dict()} # SET new interpreter: new_environment = self.selector_old_lib._create_environment(user, directory, version) self._ensure_version_enabled(version, user) # Get extensions list for old environment installed_extensions = set(old_environment.extensions()) if self.interpreter == self.PYTHON_INTERPRETER: self._relocate_python_extensions(new_environment, installed_extensions) elif self.interpreter == self.NODEJS_INTERPRETER: self._relocate_nodejs_extensions(user, directory, new_environment, installed_extensions) # Reconfigure clpassenger user_summary_data = clpassenger.summary(user) app_summary = get_using_realpath_keys(user, directory, user_summary_data) doc_root = app_summary['docroot'] binary = new_environment.interpreter().binary alias, app_domain = self.selector_old_lib._get_info_about_webapp(app_summary, user) def action(): # Clear .htaccess clpassenger.unconfigure(user, directory) passenger_log_file_to_set = full_config[directory].get('passenger_log_file', None) clpassenger.configure(user, directory, alias, self.interpreter, binary, doc_root=doc_root, startup_file=app_config['startup_file'], passenger_log_file=passenger_log_file_to_set) # Create restart.txt file in tmp directory clpassenger.restart(user, directory) full_config[directory]['%s_version' % self.interpreter] = version self.apps_manager.write_full_user_config_data(user, full_config) if not skip_web_check: try: self.selector_old_lib.check_response_from_webapp( domain=app_domain, alias=alias, action=action, ) except ClSelectExceptions.WebAppError as err: raise ClSelectExceptions.WebAppError('An error occured during changing version. %s' % err) else: action() def change_version(self, user, app_root, new_version, skip_web_check): """ Call selectorctl to change current interpreter version to new_version for application :param user: application owner :param app_root: application directory (application name) :param new_version: new nodejs interpreter version :param skip_web_check: skip check web application after change it's properties :return: json """ try: if self.interpreter not in (self.NODEJS_INTERPRETER, self.PYTHON_INTERPRETER): raise NotImplementedError # forbid user to change python version unless he migrates application # to new python selector version if self.interpreter == self.PYTHON_INTERPRETER: app_version = self.apps_manager.get_app_config(user, app_root).get( 'app_version', PythonAppFormatVersion.LEGACY) if app_version == PythonAppFormatVersion.LEGACY: return { 'status': 'This application was created by too old version ' 'of python selector and we cannot change version ' 'without migration to the new application format. ' 'To do that you can use `cloudlinux-selector migrate` ' 'command or just click button in web UI.', } if self.apps_manager.get_app_status(user, app_root) == APP_STARTED_CONST: self._change_version(user, app_root, new_version, skip_web_check=skip_web_check) else: # Supplied application is stopper - run special function for change interpreter version for it self._change_version_for_stopped_app(user, app_root, new_version) return OK_RES_DICT except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) def set_variables_for_litespeed(self, user, app_root, env_vars): if self.interpreter == self.PYTHON_INTERPRETER or self.interpreter == self.NODEJS_INTERPRETER: doc_root = self.safely_resolve_doc_root_for_app(user, app_root) # need for add_env_vars_for_htaccess try: self.apps_manager.add_env_vars_for_htaccess(user, app_root, env_vars, doc_root) except Exception as err: raise ClSelectExceptions.WebAppError( "Unable to set environment variables in htaccess file for the application." "Error: {}".format(err) ) def _set_variables(self, user, directory, app_mode, env_vars, startup_file, entry_point, config_files, passenger_log_file): """ Set application mode, environment variables and startup_file for application :param config_files: names of config files (such as requirements.txt or etc) :param entry_point: the specified entrypoint for application :param user: application owner. unix like user name :param directory: directory with application :param app_mode: expected application mode :param env_vars: dict with environment variables for application :param startup_file: main file for application :param passenger_log_file: Passenger log filename :return: None """ full_config = self.apps_manager.get_user_config_data(user) try: app_config_data = get_using_realpath_keys(user, directory, full_config) except KeyError: raise ClSelectExceptions.NoSuchApplication( 'No such application (or application not configured) "%s"' % directory) if self.interpreter == self.PYTHON_INTERPRETER \ and env_vars is not None \ and app_config_data['env_vars'] != env_vars \ and app_config_data['app_version'] == PythonAppFormatVersion.LEGACY: raise ClSelectExceptions.WebAppError( "Unable to set environment variables. " "Application was created too long time ago. " "Please, migrate your application to newer version " "before changing interpreter version" ) # Update user's config if app_mode is not None: app_config_data['app_mode'] = app_mode if env_vars is not None: # Python env vars task # TODO: LVEMAN-1466 app_config_data['env_vars'] = env_vars if entry_point is not None: app_config_data['entry_point'] = entry_point if config_files is not None: app_config_data['config_files'] = config_files if startup_file is not None and startup_file != app_config_data.get('startup_file') or \ passenger_log_file is not None: if startup_file is not None and startup_file != app_config_data.get('startup_file'): # Startup file changing app_config_data['startup_file'] = startup_file if self.interpreter == self.PYTHON_INTERPRETER: startup_file_full_path = self.selector_old_lib._get_full_path_to_startup_file( user, directory, startup_file ) # We are using the main startup file with name `passenger_wsgi.py` in case custom name of # startup file, because passenger doesn't support directive for set entry_point. # In tje file `passenger_wsgi.py` we can set entry_point and custom name of startup file. self.selector_old_lib.setup_wsgi(user, directory, startup_file_full_path, entry_point) if passenger_log_file is not None: # Set/remove PassengerAppLogFile # Remove passenger log from app config if passenger_log_file path is empty app_config_data['passenger_log_file'] = None if passenger_log_file == '' else passenger_log_file env = self.selector_old_lib._get_environment(user, directory) user_summary = clpassenger.summary(user) user_app_summary = get_using_realpath_keys(user, directory, user_summary) alias = user_app_summary['alias'] binary = env.interpreter().binary doc_root = user_app_summary['docroot'] htaccess_path = user_app_summary['htaccess'] clpassenger._unconfigure(htaccess=htaccess_path) # If main file is not configured, use default value if startup_file is None and app_config_data.get('startup_file') is None: clpassenger.configure(user, directory, alias, self.interpreter, binary, doc_root=doc_root, passenger_log_file=passenger_log_file) else: clpassenger.configure(user, directory, alias, self.interpreter, binary, doc_root=doc_root, startup_file=app_config_data.get('startup_file'), passenger_log_file=passenger_log_file) clpassenger.restart(user, directory) self.apps_manager.write_full_user_config_data(user, full_config) self.set_variables_for_litespeed(user, directory, env_vars) def set_variables(self, user, app_root, app_mode, env_vars, startup_file, entry_point, config_files, passenger_log_file): """ Call selectorctl to set variables for application :param config_files: names of config files (such as requirements.txt or etc) (only for python) :param entry_point: the specified entrypoint for application (only for python) :param user: application owner :param app_root: application directory (application name) :param app_mode: application mode :param env_vars: json_string with environment variables for application :param startup_file: main file for application :param passenger_log_file: Passenger log filename :return: json """ if env_vars is not None: try: env_dict = validate_env_vars(json.loads(env_vars)) except (TypeError, ValueError): return self._return_with_status_error('wrong json format for environment variable list') else: env_dict = None try: if self.interpreter in (self.NODEJS_INTERPRETER, self.PYTHON_INTERPRETER) and \ self.apps_manager.get_app_status(user, app_root) == APP_STARTED_CONST: self._set_variables(user, app_root, app_mode, env_dict, startup_file, entry_point, config_files, passenger_log_file) else: # Supplied application is stopped - run special function for change a few variables of application self._set_variables_for_stopped_app(user, app_root, app_mode, env_dict, startup_file, entry_point, config_files, passenger_log_file) return OK_RES_DICT except clselect.clselectexcept.BaseClSelectException as e: return self._return_with_status_error(str(e)) def get_apps_users_info(self, user=None): """ Retrieves info about all installed interpreters and user(s) applictions :param user: User name for read applictions. If None all users will be processed :return: Dict with info """ try: result_dict = self.apps_manager.get_applications_users_info(user) return result_dict except BaseSelectorError as e: return {'result': e.message, 'context': e.context} # pylint: disable=exception-message-attribute def _ensure_version_enabled(self, new_version, username): """ Check whether particular interpreter version is enabled and raises exception if not :param username: user to include in exception :param new_version: new interpreter version """ if not self.selector_manager.is_version_enabled(new_version): raise clselect.ClSelectExcept.UnableToSetAlternative(username, new_version, 'version is not enabled') def _change_version_for_stopped_app(self, username, app_root, new_version): """ Changes version for stopped application :param username: application owner :param app_root: application directory (application name) :param new_version: new nodejs interpreter version :return: None """ self._ensure_version_enabled(new_version, username) # Get extensions list fom old environment old_version = self.apps_manager.get_interpreter_version_for_app(username, app_root) old_environment = self.selector_old_lib._create_environment(username, app_root, old_version, None) old_extensions = set(old_environment.extensions()) # Create new environment new_environment = self.selector_old_lib._create_environment(username, app_root, new_version, None) # install extension to new app for extension in old_extensions: try: new_environment.extension_install(extension) except clselect.clselectexcept.ClSelectExcept.ExternalProgramFailed: pass # Update user's app config app_config = self.apps_manager.get_user_config_data(username) app_config[app_root]['%s_version' % self.interpreter] = new_version self.apps_manager.write_full_user_config_data(username, app_config) def _set_variables_for_stopped_app(self, username, app_root, app_mode, env_vars_dict, startup_file, entry_point, config_files, passenger_log_file): """ Sets new app_mode, environment variables and startup file for stopped NodeJS application :param config_files: names of config files (such as requirements.txt or etc) (only for python) :param entry_point: the specified entrypoint for application (only for python) :param str username: application owner :param str app_root: application directory (application name) :param str app_mode: New application mode, can be None :param dict env_vars_dict: New environment variables, can be None :param startup_file: New startup file, can be None :param passenger_log_file: Passenger log filename :return: None """ # Update user's app config app_config = self.apps_manager.get_user_config_data(username) if app_mode: app_config[app_root]['app_mode'] = app_mode if env_vars_dict: app_config[app_root]['env_vars'] = env_vars_dict if startup_file and startup_file != app_config[app_root].get('startup_file'): app_config[app_root]['startup_file'] = startup_file if entry_point: app_config[app_root]['entry_point'] = entry_point if config_files: app_config[app_root]['config_files'] = config_files if passenger_log_file is not None and passenger_log_file != '': app_config[app_root]['passenger_log_file'] = passenger_log_file else: app_config[app_root]['passenger_log_file'] = None self.apps_manager.write_full_user_config_data(username, app_config) self.set_variables_for_litespeed(username, app_root, env_vars_dict) @staticmethod def get_major_version_from_short(version): """ Retrieves major version from full. If already short, return it with no difference :param version: Full/short :return: Short version as string """ return str(int(version.split('.')[0])) @staticmethod def replace_mysqli(): """ Replace mysqli extension to nd_mysqli for defaults. Warning: only for PHP. See LVEMAN-1399 for details :return: """ # Get available alt-php versions list. # For example: ['5.1', '5.2', '5.3', '5.4', '5.5', '5.6', '4.4', '7.2', '7.0', '7.1'] alt_php_versions_list = list(ClSelect('php').get_all_alternatives_data().keys()) cl_ext_select = ClExtSelect() # php by default for alt_php_ver in alt_php_versions_list: # Replace mysqli -> nd_mysqli for the version for new installations according to LVEMAN-1399 cl_ext_select.list_extensions(alt_php_ver) @classmethod def setup_selector(cls): """ Setup php selector for work (suggested to use after native php is installed) """ subprocess.check_output(['cagefsctl', '--force-update']) subprocess.check_output(['cagefsctl', '--remount-all']) subprocess.check_output(['/usr/sbin/cloudlinux-selector', 'make-defaults-config', '--json', '--interpreter', 'php']) subprocess.check_output(['/usr/share/l.v.e-manager/utils/cache_phpdata.py']) cls.replace_mysqli() def run_import_applications(self): """ Scan users home dirs for .htaccess files and import applications to new config file. """ if self.interpreter != self.PYTHON_INTERPRETER: raise NotImplementedError # We don't need to import apps for DA elif detect.is_da(): return OK_RES_DICT try: self.apps_manager.import_legacy_applications_to_config() return OK_RES_DICT except Exception as e: return self._return_with_status_error(str(e)) def run_migrate_application(self, user, app_root): """ Convert applications created in older selector versions to new format """ if self.interpreter != self.PYTHON_INTERPRETER: raise NotImplementedError("Migration is only available " "for python selector") self.apps_manager.migrate_application(user, app_root) return OK_RES_DICT