관리-도구
편집 파일: 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