Mini Shell

Direktori : /opt/cloudlinux/venv/lib64/python3.11/site-packages/clselector/
Upload File :
Current File : //opt/cloudlinux/venv/lib64/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:
            from clcagefslib.selector.configure 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


Zerion Mini Shell 1.0