AlkantarClanX12

Your IP : 18.191.165.149


Current Path : /opt/cloudlinux/venv/lib64/python3.11/site-packages/clselector/
Upload File :
Current File : //opt/cloudlinux/venv/lib64/python3.11/site-packages/clselector/cl_selector.py

# coding:utf-8

# license.py - work code for cloudlinux-license utility
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT


from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
import fcntl
import sys
import time
import errno

import clcommon.cpapi as cpapi
import contextlib
import json
import os
import subprocess
import traceback

from typing import AnyStr  # NOQA
from future.utils import iteritems

from clcommon import ClPwd
from clcommon.clexception import FormattedException
from clcommon.mail_helper import MailHelper
from clcommon.clfunc import is_ascii_string
from cllicense import CloudlinuxLicenseLib

from clselect import clselectctl

from clselect.utils import get_abs_rel, mkdir_p, run_process_in_cagefs

from clselect.baseclselect import BaseSelectorError, AcquireApplicationLockError
from cli_utils import print_dictionary, replace_params
from clselect.clselectnodejs import CONFIG_DIR
from clselect.clselectnodejs.pkgmanager import PkgManager
from clselector.clpassenger_detectlib import is_clpassenger_active
from collections import defaultdict
from email.mime.text import MIMEText
from tempfile import mkstemp

from .cl_selector_arg_parse import NODEJS, PYTHON, PHP
from .cl_selector_arg_parse import parse_cloudlinux_selector_opts
from .selectorlib import CloudlinuxSelectorLib, OK_RES_DICT, ClSelectExcept

from clselect.clselectexcept import ClSelectExcept as ClSelectExcept_old


LOCK = '.lock'


# For unit tests
def _open(file_name, mode):
    return open(file_name, mode)


class CloudlinuxSelector(object):

    def __init__(self):
        self._is_json = False
        self._opts = {}
        self._selector_lib = None
        # For convenient checking during arg parsing and other operations.
        self._is_root_user = os.geteuid() == 0
        self._lock = None
        self._is_bkg_option_present = False
        self._bkg_option = '--background'
        self._nj_ver_move_from = ''
        self._pid_file_name = os.path.join(CONFIG_DIR, 'cloudlinux-selector_bkg.pid')

    def is_app_lock_needed(self):
        """
        Check if cloudlinux-selector called with application operations
        :return:  True if lock is need
        """
        # locking is implemented only for python and nodejs
        if self._opts['--interpreter'] not in [PYTHON, NODEJS]:
            return False

        if any([self._opts['change-version-multiple'], self._opts['create']]):
            return False
        if any([
            self._opts['start'],
            self._opts['restart'],
            self._opts['destroy'],
            self._opts['migrate'],
            self._opts['stop'],
            self._opts['install-modules'],
            self._opts['uninstall-modules'],
            self._opts['run-script'],
            self._opts['--app-mode'],
            self._opts['--env-vars'],
            self._opts['--new-app-root'],
            self._opts['--new-domain'],
            self._opts['--new-app-uri'],
            self._opts['--new-version'],
            self._opts['--startup-file']]):
            return True
        return False

    def acquire_app_lock_if_needed(
            self,
            ignore_missing_app_root=False,
            ignore_missing_doc_root=False,
    ):
        """
        Acquire lock for application if this lock is needed
        :return: None
        """
        if not self.is_app_lock_needed():
            return
        username, app_root = self._opts['--user'], self._opts['--app-root']
        _, app_venv = self._selector_lib.apps_manager.get_app_folders(
            username, app_root, chk_app_root=not ignore_missing_app_root,
            chk_env=not ignore_missing_doc_root)
        if not os.path.exists(app_venv):
            return

        lock_file = os.path.join(app_venv, LOCK)
        try:
            self._lock = open(lock_file, 'a+')
            fcntl.flock(self._lock.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        except IOError as e:
            if e.errno == errno.EDQUOT:
                reason = 'Disk quota exceeded. Please, free space and try again.'
                raise AcquireApplicationLockError(app_root, reason=reason)
            raise AcquireApplicationLockError(app_root)

    def send_notification_if_needed(self):
        if self._is_root_user and self._opts['--new-version']:
            self.send_notification()

    def send_notification(self):
        # NOTE(vlebedev): As of now, email notifications about selector changes don't contain enough info to be useful.
        #                 Moreover, as of the moment of writing, these messages are plain wrong as they always mention
        #                 only NodeJS, not the actual Selector being changed.
        #                 An investigation is required to clarify whether this functionality is needed at all
        #                 and - if yes - what pieces of information should be supplied in such notifications.
        #                 For more info, have a look at Jira:
        #                  * https://cloudlinux.atlassian.net/browse/LVEMAN-1904
        #                  * https://cloudlinux.atlassian.net/browse/LVEMAN-1903
        return

        MSG_TEMP = "NodeJS version for your application %s  was changed by admin. " \
                   "Please verify that application functions correctly."
        msg = MIMEText(MSG_TEMP % self._opts['--app-root'])

        me = 'CloudlinuxNodejsNotify@noresponse.com'
        msg['Subject'] = 'NodeJS version for your application %s  was changed by admin' % self._opts['--app-root']
        msg['From'] = me
        try:
            cp_userinfo = cpapi.cpinfo(
                self._opts['--user'],
                keyls=('mail', 'dns', 'locale', 'reseller'))[0]
            user_data_email = cp_userinfo[0]  # user's email

            msg['To'] = user_data_email

            mailhelper = MailHelper()
            mailhelper.sendmail(me, [user_data_email], msg)
        except (IndexError, KeyError, cpapi.cpapiexceptions.NotSupported):
            # can't get user mail or mail corrupted
            pass

    @staticmethod
    def parse_modules(modules_options):
        if not modules_options:
            return ()
        return [module for module in modules_options.strip().split(',') if module]

    def run(self, argv):
        """
        Run command action
        """
        self._is_json = "--json" in argv
        # Check background option
        self._is_bkg_option_present = self._bkg_option in argv
        if self._is_bkg_option_present:
            argv.remove(self._bkg_option)
        try:
            licence = CloudlinuxLicenseLib()
            if not licence.get_license_status():
                self._is_json = True
                return self._error_and_exit({"result": "Cloudlinux license isn't valid"})

            # get arguments, fill the value of --user argument if only --domain was given
            self._opts = self._parse_args(argv)
            self._selector_lib = CloudlinuxSelectorLib(self._opts['--interpreter'])
            self._selector_lib.check_selector_is_available()

            if self._selector_lib.should_be_runned_as_user(self._opts):
                with self._lock_interpreter_if_needed():
                    result = run_process_in_cagefs(
                        self._opts['--user'],
                        self._selector_lib.CLOUDLINUX_SELECTOR_UTILITY,
                        argv,
                    )
                    returncode = result['returncode']
                    self._print_raw_data(result['output'])
                    self.send_notification_if_needed()
                return returncode
            elif self._selector_lib.should_run_user_without_cagefs(self._opts):
                user_run_cmd = ['/usr/bin/sudo', '-u', self._opts['--user'],
                                self._selector_lib.CLOUDLINUX_SELECTOR_UTILITY] + argv
                with self._lock_interpreter_if_needed():
                    process = subprocess.Popen(user_run_cmd, env={})
                    process.communicate()
                    self.send_notification_if_needed()
                return process.returncode

            self.acquire_app_lock_if_needed(
                ignore_missing_app_root=self._opts['destroy'],
                ignore_missing_doc_root=self._opts['destroy'],
            )  # ignore app root and doc root for destroy option

            if self._opts['--passenger-log-file']:
                # Passenger log filename passed, check it
                message, log_filename = self._passenger_log_filename_validator(self._opts['--user'],
                                                                               self._opts['--passenger-log-file'])
                if message == "OK":
                    self._opts['--passenger-log-file'] = log_filename
                else:
                    self._error_and_exit(dict(result=message))
            if self._opts['set']:
                self.run_set()
            elif self._opts['migrate']:
                self.run_migrate_application()
            elif self._opts['import-applications']:
                self.run_import_applications()
            elif self._opts['create']:
                self.run_create()
            elif self._opts['destroy']:
                self.run_destroy()
            elif self._opts['start']:
                self.run_start()
            elif self._opts['restart']:
                self.run_restart()
            elif self._opts['stop']:
                self.run_stop()
            elif self._opts['read-config']:
                self.run_read_config()
            elif self._opts['save-config']:
                self.run_save_config()
            elif self._opts['install-modules']:
                self.run_install_modules()
            elif self._opts['uninstall-modules']:
                self.run_uninstall_modules()
            elif self._opts['install-version'] or self._opts['uninstall-version']:
                self.run_manage_version()
            elif self._opts['enable-version'] or self._opts['disable-version']:
                self.run_disable_or_enable_version()
            elif self._opts['run-script']:
                self._print_data(
                    self._selector_lib.run_script(
                        self._opts['--user'], self._opts['--app-root'],
                        self._opts['--script-name'], self._opts['<script_args>']
                    )
                )
            elif self._opts['change-version-multiple']:
                self._start_change_all_apps_versions()
            elif self._opts['make-defaults-config']:
                self._selector_lib.replace_mysqli()
            elif self._opts['setup']:
                self.run_setup()
            else:
                self.run_get()
        except (ClSelectExcept_old.ConfigNotFound,
                ClSelectExcept_old.WrongData,
                ClSelectExcept_old.NoSuchAlternativeVersion) as e:
            self._error_and_exit(dict(result=str(e)))
        except (ClSelectExcept_old.NativeNotInstalled,
                ClSelectExcept_old.MissingCagefsPackage) as e:
            if not self._opts['make-defaults-config']:
                # pylint: disable=exception-message-attribute
                self._error_and_exit(dict(result=e.message, context=e.context))
            # hack for alt-php spec that calls this method
            # just do not print error because it is not needed in rpm log
            exit(0)
        except ClSelectExcept_old.FileProcessError as e:
            self._error_and_exit(dict(result=e))
        except FormattedException as e:
            if e.details:
                self._error_and_exit(dict(result=e.message, context=e.context, details=e.details))
            else:
                self._error_and_exit(dict(result=e.message, context=e.context))
        except Exception as err:
            msg = traceback.format_exc()
            list_err_msg = traceback.format_exception_only(type(err), err)
            if isinstance(list_err_msg, list):
              err_msg = '\n'.join(list_err_msg)
            else:
              err_msg = list_err_msg
            self._error_and_exit(dict(
                result=err_msg,
                details=msg
            ))
        finally:
            if self._is_bkg_option_present:
                # If we worked in background remove pid file
                try:
                    os.remove(self._pid_file_name)
                except:
                    pass
        return 0

    def run_set(self):
        if self._opts['--default-version'] is not None:
            self._print_data(self._selector_lib.set_default_version(self._opts['--default-version']))
        elif self._opts['--current-version'] is not None:
            self._print_data(self._selector_lib.set_current_version(self._opts['--current-version']))
        elif self._opts['--reset-extensions']:
            self._print_data(self._selector_lib.reset_extensions(self._opts['--version']))
        elif self._opts['--selector-status'] is not None:
            self._print_data(self._selector_lib.set_selector_status(self._opts['--selector-status']))
        elif self._opts['--supported-versions'] is not None:
            self._print_data(self._selector_lib.set_supported_versions(self._opts['--supported-versions']))
        elif self._opts['--extensions'] is not None and self._opts['--version'] is not None:
            self._print_data(self._selector_lib.set_extensions(self._opts['--extensions'], self._opts['--version']))
        elif self._opts['--options'] is not None and self._opts['--version'] is not None:
            self._print_data(self._selector_lib.set_options(self._opts['--options'], self._opts['--version']))
        elif self._is_nodejs or self._is_python:
            self.run_change(self._opts['--user'], self._opts['--app-root'],
                            self._opts['--app-mode'],
                            self._opts['--env-vars'], self._opts['--new-app-root'], self._opts['--new-domain'],
                            self._opts['--new-app-uri'], self._opts['--new-version'], self._opts['--startup-file'],
                            self._opts['--skip-web-check'], self._opts['--entry-point'], self._opts['--config-files'],
                            self._opts['--passenger-log-file'])
        # XXX: should we return some error if no option was selected?

    def run_setup(self):
        self._selector_lib.setup_selector()

    def run_change(self, user, app_root, app_mode, env_vars, new_app_root, new_domain,
                   new_app_uri, new_version, startup_file, skip_web_check, entry_point, config_files,
                   passenger_log_file):
        """
        Call selectorctl to change application parameter
        :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 main directory (application name)
        :param app_mode: application mode
        :param env_vars: dict with environment variables
        :param new_app_root: new application main directory (new application name)
        :param new_domain:  new application domain
        :param new_app_uri: new application uri
        :param new_version: new version for nodejs interpreter
        :param startup_file: new startup file for application
        :param skip_web_check: skip check web application after change it's properties
        :param passenger_log_file: Passenger log filename
        :return: None
        """
        if user is None:
            self._error_and_exit({
                'result': 'ERROR: User is not specified'})
        if new_app_root is not None:
            # Change app-root
            r = self._selector_lib.relocate(user, app_root, new_app_root)
            # after relocate we need to change current app_root to new one
            app_root = new_app_root
            if r['status'].upper() != 'OK':
                self._print_data(r)
                sys.exit(1)
        if new_app_uri is not None or new_domain is not None:
            # Change app-uri
            r = self._selector_lib.transit(user, app_root, new_app_uri, new_domain)
            if r['status'].upper() != 'OK':
                self._print_data(r)
                sys.exit(1)
        if any((app_mode, env_vars, startup_file, entry_point, config_files is not None,
                passenger_log_file is not None)):
            # create list of config files
            if config_files is not None:
                config_files = [item for item in config_files.split(',') if item != '']
            # Change app-mode, environment variables or startup file
            r = self._selector_lib.set_variables(user, app_root, app_mode, env_vars,
                                                 startup_file, entry_point, config_files, passenger_log_file)
            if r['status'].upper() != 'OK':
                self._print_data(r)
                sys.exit(1)
        if new_version is not None:
            # Change interpreter version for application
            r = self._selector_lib.change_version(user, app_root, new_version, skip_web_check)
            if r['status'].upper() != 'OK':
                self._print_data(r)
                sys.exit(1)
        # print_data create {status:ok, timestamp:} and print it
        self._print_data({})

    def run_import_applications(self):
        self._print_data(self._selector_lib.run_import_applications())

    def run_migrate_application(self):
        self._print_data(self._selector_lib.run_migrate_application(
            self._opts['--user'], self._opts['--app-root']))

    def run_get(self):
        if self._opts['--get-default-version']:
            self._print_data(self._selector_lib.get_default_version())
        elif self._opts['--get-selector-status']:
            self._print_data(self._selector_lib.get_selector_status())
        elif self._opts['--get-supported-versions']:
            self._print_data(self._selector_lib.get_supported_versions())
        elif self._opts['--get-current-version']:
            self._print_data(self._selector_lib.get_current_version(self._opts['--user']))
        elif self._opts['--interpreter'] == PHP:
            self._print_data(self._selector_lib.get_full())
        else:
            res = {'passenger_active': is_clpassenger_active()}
            if self._opts['--interpreter'] == NODEJS:
                res.update(self._selector_lib.get_apps_users_info(self._opts['--user']))
                # Applications count from background process
                remaining_apps_count, total_apps_count = self._get_apps_count_from_pid_file()
                if remaining_apps_count is not None and total_apps_count is not None:
                    res['remaining_apps_count'] = remaining_apps_count
                    res['total_apps_count'] = total_apps_count
            elif self._opts['--interpreter'] == PYTHON:
                res.update(self._selector_lib.get_apps_users_info(self._opts['--user']))
            if 'result' in res:
                self._print_data(res, result=res['result'])
            else:
                self._print_data(res)

    def run_create(self):
        # Not allow to create application on locked version
        if self._is_version_locked_by_background_process(self._opts['--version']):
            self._error_and_exit({
                'result': 'Can\'t create application: Nodejs version %(version)s is locked by background process',
                'context': {'version': self._opts['--version']},
            })
        if not is_clpassenger_active():
            # passenger not active, application creation not allowed
            if self._opts['--interpreter'] == PYTHON:
                url = 'https://docs.cloudlinux.com/python_selector/#installation'
            else:
                url = 'https://docs.cloudlinux.com/index.html?installation.html'
            self._error_and_exit({
                'result': 'Application creation not allowed, '
                          'Phusion Passenger seems absent, please see %(url)s for details',
                'context': {
                    'url': url
                },
            })

        self._print_data(
            self._selector_lib.create_app(
                self._opts['--app-root'],
                self._opts['--app-uri'],
                self._opts['--version'],
                self._opts['--user'],
                self._opts['--domain'],
                self._opts['--app-mode'],
                self._opts['--startup-file'],
                self._opts['--env-vars'],
                self._opts['--entry-point'],
                self._opts['--passenger-log-file']
            ))

    def run_destroy(self):
        self._print_data(self._selector_lib.destroy_app(self._opts['--app-root'],
                                                        self._opts['--user']))

    def run_start(self):
        self._print_data(self._selector_lib.start_app(self._opts['--app-root'],
                                                      self._opts['--user']))

    def run_restart(self):
        self._print_data(self._selector_lib.restart_app(self._opts['--app-root'],
                                                        self._opts['--user']))

    def run_stop(self):
        self._print_data(self._selector_lib.stop_app(self._opts['--app-root'],
                                                     self._opts['--user']))

    def run_read_config(self):
        self._print_data(
            self._selector_lib.read_app_config(
                self._opts['--app-root'],
                self._opts['--config-file'],
                self._opts['--user']))

    def run_save_config(self):
        self._print_data(
            self._selector_lib.save_app_config(
                self._opts['--app-root'],
                self._opts['--config-file'],
                self._opts['--content'],
                self._opts['--user']))

    def run_install_modules(self):
        self._print_data(
            self._selector_lib.install_modules(
                self._opts['--app-root'],
                user=self._opts['--user'],
                domain=self._opts['--domain'],
                skip_web_check=self._opts['--skip-web-check'],
                spec_file=self._opts['--requirements-file'],
                modules=self.parse_modules(self._opts['--modules']),
            )
        )

    def run_uninstall_modules(self):
        self._print_data(
            self._selector_lib.uninstall_modules(
                self._opts['--app-root'],
                modules=self.parse_modules(self._opts['--modules']),
                user=self._opts['--user'],
                domain=self._opts['--domain'],
                skip_web_check=self._opts['--skip-web-check'],
            )
        )

    def run_disable_or_enable_version(self):
        """
        Disable or enable interpreter version
        :return: None
        """

        version = self._opts['--version']
        target_version_status = self._opts['enable-version']
        try:
            self._print_data(self._selector_lib.set_version_status(target_version_status, version))
        except BaseSelectorError as e:
            self._error_and_exit({
                'result': str(e),
            })

    def run_manage_version(self):
        ver = str(self._opts['--version'])
        try:
            if self._opts['install-version']:
                res = self._selector_lib.selector_manager.install_version(ver)
            else:
                res = self._selector_lib.selector_manager.uninstall_version(ver)
        except Exception as e:
            res = str(e)
        if res is None:
            self._print_data(OK_RES_DICT)
        elif isinstance(res, dict):
            self._error_and_exit(res)
        else:
            self._error_and_exit({'result': res})

    def _parse_args(self, argv):
        """
        Parse CLI arguments
        """
        status, data = parse_cloudlinux_selector_opts(
            argv, self._is_json, as_from_root=self._is_root_user)
        if not status:
            # exit with error if can`t parse CLI arguments
            self._error_and_exit(replace_params(data))

        # For php we check only user exists
        if data['--interpreter'] == 'php':
            if data['--user']:
                try:
                    pwd = ClPwd()
                    pwd.get_pw_by_name(data['--user'])
                except ClPwd.NoSuchUserException:
                    raise ClSelectExcept(
                        {
                            'message': 'No such user (%s)',
                            'context': {
                                'user': data['--user']
                            },
                        }
                    )
            return data

        # We can't detect CPanel under user in CageFS, so we check CPanel specific directory /usr/local/cpanel
        # In cageFS this directory present, but we can't read it content
        # May be this is temporary solution, possibly change after PTCCLIB-170
        if not os.path.isdir('/usr/local/cpanel') and (data['import-applications'] or data['migrate']):
            self._error_and_exit({'result': 'success',
                                  'warning': 'Import/migrate of Python Selector applications is not supported'})

        # try to resolve username (e.g. if only domain was specified in cli)
        # DO NOT RESOLVE DOMAIN HERE!
        # it leads to confusion between the "user's main domain"
        # and the "domain where application works"
        data['--user'], _ = CloudlinuxSelectorLib.safely_resolve_username_and_doc_root(
            data['--user'], data['--domain'])

        # validate app_root before passing it to create & transit methods
        # to make them 'safe' and avoid code duplicates
        for app_root_arg in ['--app-root', '--new-app-root']:
            if not data.get(app_root_arg):
                continue

            _, directory = get_abs_rel(data['--user'], data[app_root_arg])
            try:
                # directory name must not be one of the reserved names and
                # should not contain invalid symbols.
                clselectctl.check_directory(directory)
            except ValueError as e:
                self._error_and_exit(dict(
                    result=str(e)
                ))
            data[app_root_arg] = directory
        return data

    def _error_and_exit(self, message, error_code=1):
        """
        Print error and exit
        :param dict message: Dictionary with keys "result" as string and optional "context" as dict
        """
        if "status" in message:
            message["result"] = message["status"]
            del(message["status"])
        if self._is_json:
            message.update({"timestamp": time.time()})
            print_dictionary(message, True)
        else:
            try:
                print(str(message["result"]) % message.get("context", {}))
            except KeyError:
                print("Error: %s" % message)
        sys.exit(error_code)

    @staticmethod
    def _print_raw_data(data):
        # type: (AnyStr) -> None
        """
        Print raw data.
        Function should be used in case if you want
        to print a json string as an output from other utilities
        """

        print(data)

    def _print_data(self, data, force_json=False, result="success"):
        """
        Output data wrapper
        :param: `dict` data - data for output to stdout
        :param: `bool` force_json - always output json format
        """
        if isinstance(data, dict):
            data = data.copy()
            # data may be Exception object with data and context inside
            if "data" in data and isinstance(data["data"], dict):
                data = data["data"]
            # data may already contain "status", so we wont rewrite it
            data.setdefault("status", result)

            # rename "status": "ok" to "result": "success"
            if data["status"].lower() == "ok":
                data["result"] = "success"
                if self._opts['--interpreter'] == PHP and self._selector_lib.check_multiphp_system_default():
                    data['warning'] = 'MultiPHP system default PHP version is alt-php. ' \
                                      'PHP Selector does not work and should be disabled!'

            # do not set result to status, if result was passed
            elif 'result' not in data and 'status' in data:
                data["result"] = data["status"]
            del(data["status"])
            # and do update timestamp with current time
            data.update({"timestamp": time.time()})
        print_dictionary(data, self._is_json or force_json)

    @property
    def _is_nodejs(self):
        return self._opts['--interpreter'].lower() == NODEJS

    @property
    def _is_python(self):
        return self._opts['--interpreter'].lower() == PYTHON

    def _is_interpreter_lock_needed(self):
        # Only NodeJs & Python has interpreter locking
        if self._opts['--interpreter'] in [NODEJS, PYTHON]:
            # We will lock only new version because old is unknown before
            # we SU to user and read it's app configs. We can implement ugly
            # workaround later if someone ask it
            new_version = self._opts['--new-version']
            return bool(new_version)
        return False

    @contextlib.contextmanager
    def _lock_interpreter_if_needed(self):
        """
        Wrapper over contextmanager of PkgManager in order not
        to try acquire lock when it is not needed.
        """
        if self._is_interpreter_lock_needed():
            # TODO: we need to simplify access and usage
            # of apps_manager / pkg_manager  methods
            mgr = self._selector_lib.apps_manager
            with mgr.acquire_interpreter_lock(self._opts['--new-version']):
                yield
        else:
            yield

    def _get_nj_versions(self):
        """
        Retrives NodeJS versions from arguments and converts them to major versions
        :return: Cortege (from_version, to_version)
        """
        from_version = self._opts['--from-version']
        to_version = self._opts['--new-version']
        from_version = self._selector_lib.get_major_version_from_short(from_version)
        to_version = self._selector_lib.get_major_version_from_short(to_version)
        if from_version == to_version:
            self._error_and_exit({'result': '--from-version and --new-version should be different'})
        return from_version, to_version

    def _check_environment_for_move_apps(self):
        """
        Checks arguments and environment before start group applications move
        :return: Cortege (from_version, to_version)
        """
        from_version, to_version = self._get_nj_versions()
        pkg_manager = PkgManager()
        installed_nj_versions = pkg_manager.installed_versions
        if to_version not in installed_nj_versions:
            self._error_and_exit({
                'result': 'Can\'t move NodeJS applications to Nodejs version %(version)s. No such version installed.',
                'context': {'version': to_version},
            })
        # For running process: print error if we trying to start background process and another one already running
        if not self._is_bkg_option_present and self._is_background_process_already_running():
            self._error_and_exit({'result': 'Another background process already started.'})
        return from_version, to_version

    def _start_change_all_apps_versions(self):
        """
        Change all applications all users versions
        :return:
        """
        from_version, to_version = self._check_environment_for_move_apps()
        # No background process running
        if not self._is_bkg_option_present:
            # Option --background not specified, start background process
            # For example:
            # cloudlinux-selector change-version-multiple --json --interpreter=nodejs --from-version=6 --new-version=9 --background
            command = "%s change-version-multiple --json --interpreter=nodejs --from-version=%s --new-version=%s %s >/dev/null &" %\
                      (self._selector_lib.CLOUDLINUX_SELECTOR_UTILITY, from_version, to_version, self._bkg_option)
            subprocess.run(command, shell=True, executable='/bin/bash')
            # Exit without process end waiting
            self._print_data(OK_RES_DICT)
            return
        # Option --background specified, start move application
        # Scan all users/apps, build appliction list to move
        users_apps_list, total_apps_count = self._get_all_apps_by_version(from_version)
        # Do nothing if application list is empty
        if not users_apps_list or total_apps_count == 0:
            return
        # Create pid file for background process
        self._write_pid_file(from_version, to_version, total_apps_count)
        # Move applications
        self._move_apps_by_list(users_apps_list, to_version, total_apps_count)

    def _move_apps_by_list(self, apps_dict, to_version, total_apps_count):
        """
        Move applications from list from one NodeJS version to another
        :type dict
        :param apps_dict: Application list. List example:
            {'cltest1': [u'modjsapp_root'], 'cltest2': [u'app2', u'main_app']}
        :param to_version: Move applications to this version
        :param total_apps_count: Total applications count for move
        :return: None
        """
        for user_name, user_app_list in iteritems(apps_dict):
            for app_root in user_app_list:
                # cloudlinux-selector set --json --interpreter nodejs  --user <str> --app-root <str> --new-version <str>
                cmd = [ self._selector_lib.CLOUDLINUX_SELECTOR_UTILITY, 'set', '--json', '--interpreter', NODEJS,
                        '--user', user_name, '--app-root', app_root, '--new-version', to_version ]
                process = subprocess.Popen(cmd)
                process.communicate()
                total_apps_count -= 1
                # update pid file
                self._change_pid_file(total_apps_count)
                time.sleep(30)

    def _get_all_apps_by_version(self, from_version):
        """
        Retrives list of all NodeJS applications for all users, which uses supplied version of NodeJS
        :param from_version: Required NodeJS version
        :return: Cortege: (application_list, application_count). Example:
            ({'cltest1': [u'modjsapp_root'], 'cltest2': [u'app2', u'main_app']}, 3)
        """
        users_apps_dict = defaultdict(list)
        # 0 -- we always root here
        user_info = self._selector_lib.apps_manager.get_users_dict()
        total_apps_count = 0
        for user_name, user_pw_entry in iteritems(user_info):
            try:
                user_app_data = self._selector_lib.apps_manager.read_user_selector_config_json(
                    user_pw_entry.pw_dir,
                    user_pw_entry.pw_uid,
                    user_pw_entry.pw_gid,
                )
                # user_app_data example:
                # {u'modjsapp_root': {u'domain': u'cltest1.com', u'app_uri': u'modjsappuri', u'nodejs_version': u'8',
                #                     u'app_status': u'started', u'env_vars': {}, u'app_mode': u'production',
                #                     u'config_files': [], u'startup_file': u'app.js'}}
                for app_root, app_info in iteritems(user_app_data):
                    # if application on from_version - add it to list for move
                    if app_info['nodejs_version'] == from_version:
                        users_apps_dict[user_name].append(app_root)
                        total_apps_count += 1
            except (BaseSelectorError, TypeError, KeyError, AttributeError):
                # Skip user if config is unreadable
                continue
        return users_apps_dict, total_apps_count

    def _is_background_process_already_running(self):
        """
        Determine is background process already working
        :return: True|False
        """
        try:
            data = json.load(_open(self._pid_file_name, 'r'))
            self._nj_ver_move_from = data['from_version']
            return True
        except:
            pass
        # No background process found
        return False

    def _is_version_locked_by_background_process(self, nj_version):
        """
        Checks if NodeJS version blocked by background operation
        :param nj_version: NodeJS version to check
        :return: True - version is locked, False - not locked
        """
        if self._opts['--interpreter'] == PYTHON:
            return False
        # Check version and use default version if need
        nj_version = self._selector_lib.resolve_version(nj_version)
        nj_version = self._selector_lib.get_major_version_from_short(nj_version)
        is_bkg_process_present = self._is_background_process_already_running()
        if is_bkg_process_present and nj_version == self._nj_ver_move_from:
            return True
        return False

    def _write_pid_file(self, from_version, to_version, total_apps_count):
        """
        Creates pid file for background process move version from version to version
        :param from_version: Move from NJ version
        :param to_version: Move to NJ version
        :param total_apps_count: Total application count to move
        :return: None
        """
        json.dump({
            'pid': os.getpid(),
            'from_version': str(from_version),
            'to_version': str(to_version),
            'total_apps_count': total_apps_count,
            'remaining_apps_count': total_apps_count,
            'time': float(time.time()),
        }, _open(self._pid_file_name, 'w'))
        # Make file readable by anyone
        os.chmod(self._pid_file_name, 0o644)

    def _read_pid_file(self):
        """
        Reads pid file and returns it's content as dictionary
        :return: Dictionary
        """
        f = _open(self._pid_file_name, 'r')
        pid_data = json.load(f)
        f.close()
        return pid_data

    def _change_pid_file(self, remaining_apps_count):
        """
        Creates pid file for background process move version from version to version
        :param remaining_apps_count: Remaining application count to move
        :return: None
        """
        try:
            pid_data = self._read_pid_file()
            pid_data['remaining_apps_count'] = remaining_apps_count

            _, temp_file_name = mkstemp(dir=CONFIG_DIR)
            json.dump(pid_data, _open(temp_file_name, 'w'))
            os.rename(temp_file_name, self._pid_file_name)
            # Make file readable by anyone
            os.chmod(self._pid_file_name, 0o644)
        except (OSError, IOError, KeyError):
           return

    def _get_apps_count_from_pid_file(self):
        """
        Retrieves application counts from pid file
        :return: Cortege (remaining_apps_count, total_apps_count)
            If no background process started, returns None, None
        """
        try:
            f = _open(self._pid_file_name, 'r')
            pid_data = json.load(f)
            f.close()
            return pid_data['remaining_apps_count'], pid_data['total_apps_count']
        except (OSError, IOError, KeyError):
            return None, None

    @staticmethod
    def _passenger_log_filename_validator(username, log_filename):
        """
        Validates passenger log file name
        :param username: User's name
        :param log_filename: passenger log file name to validate
        :return: tuple: (message, log_filename).
            message: "OK" - filename is valid, any other string - invalid, error text
            log_filename: corrected log filename - simlink dereferencing, appends user's homedir for relative paths, etc
        """
        pwd = ClPwd()
        user_homedir = pwd.get_homedir(username)
        try:
            if not is_ascii_string(log_filename):
                return "ERROR: Passenger log filename should contain only english letters", None
            if os.path.isdir(log_filename):
                return "ERROR: Passenger log file should be a filename, not a directory name", None
            if not log_filename.startswith(os.path.sep):
                log_filename = os.path.join(user_homedir, log_filename)
            log_realpath = os.path.realpath(log_filename)
            if log_realpath.startswith(user_homedir+os.sep):
                dirname = os.path.dirname(log_realpath)
                if not os.path.exists(dirname):
                    mkdir_p(dirname)
                return "OK", log_realpath
        except (OSError, IOError) as exc:
            return "%s" % str(exc), None
        return "ERROR: Passenger log file should be placed in user's home", None