Mini Shell
# -*- coding: utf-8 -*-
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
import os
import pwd
import requests
import requests.packages.urllib3.exceptions as urllib_exceptions #pylint: disable=E0401
import shutil
from requests.exceptions import ConnectionError
from future.utils import iteritems
from past.builtins import basestring
from . import clpassenger
from . import clselectctl
from . import utils
from clcommon.cpapi import userdomains
from clcommon.utils import mod_makedirs
from .clselectexcept import ClSelectExcept
from .clselectnodejs.apps_manager import ApplicationsManager
from .clselectnodejsuser import environments, interpreters
from .baseclselect import APP_STARTED_CONST, APP_STOPPED_CONST
from .clselectprint import clprint
DEFAULT_STARTUP_FILE = 'app.js'
DEFAULT_APP_STATE = APP_STARTED_CONST
def _create_environment(user, directory, version, env_name=None, destroy_first=False):
prefix = _get_prefix(user, directory)
if not env_name:
env_name = version
environment = environments.Environment(env_name, user, prefix)
if not environment.exists() or destroy_first:
try:
interpreter = interpreters.interpreters(key='version')[version]
except KeyError:
raise ClSelectExcept.NoSuchAlternativeVersion(version)
environment.create(interpreter, destroy_first=destroy_first)
return environment
def _get_environment(user, directory, app_summary=None):
prefix = _get_prefix(user, directory)
if app_summary is None:
user_summary = clpassenger.summary(user)
try:
app_summary = utils.get_using_realpath_keys(user, directory, user_summary)
except KeyError:
raise ClSelectExcept.NoSuchApplication(
'No such application (or application not configured) "%s"' %
directory)
binary = app_summary['binary']
env_name = os.path.basename(os.path.dirname(os.path.dirname(binary)))
environment = environments.Environment(env_name, user, prefix)
return environment
def _get_prefix(user, directory):
_, rel_dir = utils.get_abs_rel(user, directory)
return os.path.join(environments.DEFAULT_PREFIX, rel_dir)
def _ensure_version_enabled(version, user):
"""
Check whether particular interpreter version is enabled and raises
exception if not
:param user: user to include in exception
"""
from .clselectnodejs import node_manager
if not node_manager.NodeManager().is_version_enabled(version):
raise ClSelectExcept.UnableToSetAlternative(
user, version, 'version is not enabled')
def _get_existing_nesting_app_for(new_app_directory, user, apps_manager=None):
"""
Return None if new_app_directory is not nested inside an existing
user application, and the name of conflicting application otherwise
"""
if apps_manager is None:
apps_manager = ApplicationsManager()
abs_app_path, _ = utils.get_abs_rel(user, new_app_directory)
full_config = apps_manager.get_user_config_data(user)
for existing_app in full_config.keys():
# In case home directory was symlinked after config was written
existing_app_abs_path, _ = utils.get_abs_rel(user, existing_app)
if (abs_app_path + os.sep).startswith(existing_app_abs_path + os.sep):
return existing_app
return None
def create(user, directory, alias, version=None, doc_root=None, app_mode=None,
env_vars=None, startup_file=None, domain_name=None, apps_manager=None, passenger_log_file=None):
"""
Create application
:param user: unix user name
:param directory: application path in user's home (app-root)
:param alias: alias (app-uri)
:param version: version of interpreter
:param doc_root: doc_root
:param app_mode: application mode for nodejs
:param env_vars: dict with enviroment variables
:param startup_file: main application file
:param domain_name: domain name
:param apps_manager: Application Manager. Class that responsible
for gathering and writing information about applications
:param passenger_log_file: Passenger log filename to write to app's .htaccess
:return: None
"""
if apps_manager is None:
apps_manager = ApplicationsManager()
if version is None:
raise ClSelectExcept.WrongData('Not passed version as argument')
_ensure_version_enabled(version, user)
if startup_file is None:
startup_file = DEFAULT_STARTUP_FILE
if env_vars is None:
env_vars = {}
if app_mode is None:
app_mode = 'production'
conflicting_app = _get_existing_nesting_app_for(directory, user, apps_manager)
if conflicting_app is not None:
raise ClSelectExcept.BusyApplicationRoot(conflicting_app)
alias = clselectctl.get_alias(alias)
environment = _create_environment(user, directory, version)
binary = environment.interpreter().binary
clpassenger.configure(user, directory, alias, apps_manager.INTERPRETER, binary,
doc_root=doc_root, startup_file=startup_file,
passenger_log_file=passenger_log_file)
# webapp was started
clpassenger.restart(user, directory)
if not domain_name:
# if domain name not defined - try to detemine it
summary_data = clpassenger.summary(user)
app_summary = utils.get_using_realpath_keys(user, directory, summary_data)
domain_name = app_summary['domain']
app_data = {
u'nodejs_version': version,
u'domain': domain_name,
u'app_uri': alias,
u'app_status': DEFAULT_APP_STATE,
u'startup_file': startup_file,
u'app_mode': app_mode,
u'config_files': [],
u'env_vars': env_vars,
}
if passenger_log_file:
app_data[u'passenger_log_file'] = passenger_log_file
apps_manager.add_app_to_config(user, directory, app_data)
try:
apps_manager.add_env_vars_for_htaccess(user, directory, env_vars, doc_root)
except Exception as err:
clprint.print_diag('text', {'status': 'ERROR', 'message': str(err)})
def _create_symlink_to_node_modules(username, environment_path, app_root):
"""
Creates symlink to app's node_modules in app_root
:param username: user name
:param environment_path: Path to app's virtual environment
:param app_root: Application root
:return: None
"""
# This function logic is duplicated in npm wrapper, but we still need it here too. See commit
# message of LVEMAN-1335
message = 'Cloudlinux NodeJS Selector demands to store node modules for application '\
'in separate folder (virtual environment) pointed by symlink '\
'called "node_modules". That`s why application should not contain folder/file '\
'with such name in application root'
link_name = os.path.join(pwd.getpwnam(username).pw_dir, app_root, 'node_modules')
if os.path.islink(link_name):
try:
os.remove(link_name)
except OSError:
raise ClSelectExcept.RemoveSymlinkError(
'Can`t remove symlink %(link_name)s' %
{'link_name': link_name}
)
elif os.path.exists(link_name): # link_name not a old symlink, but exists
raise ClSelectExcept.SymlinkError(message)
link_to = os.path.join(environment_path, 'lib/node_modules')
if not os.path.exists(link_to):
mod_makedirs(link_to, 0o775)
try:
os.symlink(link_to, link_name)
except OSError:
raise ClSelectExcept.CreateSymlinkError('Error creating symlink. ' + message)
def destroy(user, app_directory, doc_root, apps_manager=None):
"""
Destroy web app with specified directory for specified user
:param user: username
:param app_directory: application directory
:param doc_root: Document root for selected domain
:param apps_manager: Application Manager. Class that responsible
for gathering and writing information about applications
:return: None
"""
if apps_manager is None:
apps_manager = ApplicationsManager()
# get app state
app_config = apps_manager.get_app_config(user, app_directory)
# remove env from htaccess
try:
# if domain is already removed we shouldn't do anything
if doc_root is not None:
apps_manager.add_env_vars_for_htaccess(user, app_directory, None, doc_root)
except Exception as err:
clprint.print_diag('text', {'status': 'ERROR', 'message': str(err)})
if app_config.get('app_status') == APP_STOPPED_CONST:
# stopped app
user_home = pwd.getpwnam(user).pw_dir
# Remove app's nodenv, e.g. /home/cltest1/nodevenv/test_app
nodevenv_path = '/'.join([user_home, 'nodevenv', app_directory])
try:
# Remove app's nodenv, e.g. /home/cltest1/nodevenv/test_app
shutil.rmtree(nodevenv_path)
except OSError:
pass
# if domain is already removed we shouldn't do anything
if doc_root is not None:
# Remove NodeJS lines from .htaccess
htaccess_filename = apps_manager.get_htaccess_by_appdir(user, app_directory, doc_root)
clpassenger.remove_passenger_lines_from_htaccess(htaccess_filename)
# remove app from node-selector.json file
apps_manager.remove_app_from_config(user, app_directory)
# Update application status in passenger
try:
clpassenger.restart(user, app_directory)
except ClSelectExcept.MissingApprootDirectory:
pass
return
# Moved before removing an app from node-selector.json because in case if
# app_root is absent we still want to remove venv dir of an application.
# The method didn't anyhing if app_root is absent and skip other commands
# including removal of a venv dir
prefix = _get_prefix(user, app_directory)
abs_dir, _ = utils.get_abs_rel(user, prefix)
try:
# Remove app's nodenv, e.g. /home/cltest1/nodevenv/test_app
shutil.rmtree(abs_dir)
except OSError:
pass
user_summary = clpassenger.summary(user)
# remove app from node-selector.json file
app_in_config = apps_manager.remove_app_from_config(user, app_directory)
try:
utils.get_using_realpath_keys(user, app_directory, user_summary)
except KeyError:
# if app was existed and app's dir is not exists, we skip all further actions
if app_in_config:
return None
else:
raise ClSelectExcept.WrongData("No such application (or application not configured) \"%s\""
% app_directory)
# remove app from passenger
clpassenger.unconfigure(user, app_directory) # Clear .htaccess
try:
clpassenger.restart(user, app_directory)
except ClSelectExcept.MissingApprootDirectory:
pass
def start(user, app_directory, doc_root, apps_manager=None):
"""
Starts web app with specified directory for specified user
:param user: username
:param app_directory: application directory
:param doc_root: Document root for selected domain
:param apps_manager: Application Manager. Class that responsible
for gathering and writing information about applications
:return: None
"""
if apps_manager is None:
apps_manager = ApplicationsManager()
app_config = apps_manager.get_app_config(user, app_directory)
if app_config is None:
raise ClSelectExcept.WrongData("No such application (or application not configured) \"%s\""
% app_directory)
# Do nothing if application already started
if app_config.get('app_status') == APP_STARTED_CONST:
return
# Create .htaccess file for the application
apps_manager.update_htaccess_file(user, app_directory, doc_root)
# Save new application status in user's config
apps_manager.set_app_status(user, app_directory, APP_STARTED_CONST)
# Update application status in passenger
clpassenger.restart(user, app_directory)
def restart(user, app_directory, doc_root, apps_manager=None):
"""
Restarts web app with specified directory for specified user
:param user: username
:param app_directory: application directory
:param doc_root: Document root for selected domain
:param apps_manager: Application Manager. Class that responsible
for gathering and writing information about applications
:return: None
"""
if apps_manager is None:
apps_manager = ApplicationsManager()
app_config = apps_manager.get_app_config(user, app_directory)
if app_config is None:
raise ClSelectExcept.WrongData("No such application (or application not configured) \"%s\""
% app_directory)
# If application was stopped - start it
if app_config.get('app_status') == APP_STOPPED_CONST:
start(user, app_directory, doc_root)
else:
clpassenger.restart(user, app_directory)
def stop(user, app_directory, doc_root, apps_manager=None):
"""
Stops web app with specified directory for specified user
:param user: username
:param app_directory: application directory
:param doc_root: Document root for selected domain
:param apps_manager: Application Manager. Class that responsible
for gathering and writing information about applications
:return: None
"""
if apps_manager is None:
apps_manager = ApplicationsManager()
app_config = apps_manager.get_app_config(user, app_directory)
if app_config is None:
raise ClSelectExcept.WrongData("No such application (or application not configured) \"%s\""
% app_directory)
# Do nothing if application is not started
if app_config.get('app_status') == APP_STOPPED_CONST:
return
htaccess_filename = apps_manager.get_htaccess_by_appdir(user, app_directory, doc_root, app_config)
# Remove NodeJS lines from .htaccess
clpassenger.remove_passenger_lines_from_htaccess(htaccess_filename)
# Save new application status in user's config
apps_manager.set_app_status(user, app_directory, APP_STOPPED_CONST)
# Update application status in passenger
clpassenger.restart(user, app_directory)
def check_response_from_webapp(domain, alias, action=None):
"""
Check response from user's webapp before and after calling action.
Also compare both responses
:param domain: domain associated with webapp
:param alias: URI associated with webapp
:param action: called action, that make something with webapp: install modules, transit it, etc
:return: None
"""
app_is_inaccessible_before = 'Web application is inaccessible by its address "%s". The operation wasn\'t performed.'
app_is_inaccessible_after = 'The operation was performed, but check availability of application has failed. ' \
'Web application is inaccessible by its address "%s" after the operation.'
app_is_broken = 'The operation was performed, but check availability of application has failed. ' \
'Web application responds, but its return code "%s" or ' \
'content type before operation "%s" doesn\'t equal to contet type after operation "%s".'
requests.packages.urllib3.disable_warnings(urllib_exceptions.InsecureRequestWarning) #pylint: disable=E1101
if not callable(action):
raise ClSelectExcept.WrongData('Wrong action for calling in checking webapp')
webapp_url = 'https://{domain}/{alias}'.format(
domain=domain,
alias=alias,
)
# for hiding from the module of Apache `mod_security`
headers = {
'User-Agent': 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.13) '
'Gecko/20101209 CentOS/3.6-2.el5.centos Firefox/3.6.13'
}
try:
request = requests.get(webapp_url, verify=False, headers=headers)
except ConnectionError:
# site is not available by https protocol
webapp_url = webapp_url.replace('https://', 'http://')
try:
request = requests.get(webapp_url, verify=False, headers=headers)
except ConnectionError:
raise ClSelectExcept.WebAppError(app_is_inaccessible_before % webapp_url)
before_mime_type = request.headers.get('Content-Type')
before_status = request.headers.get('Status')
action()
try:
request = requests.get(webapp_url, verify=False, headers=headers)
except ConnectionError:
raise ClSelectExcept.WebAppError(app_is_inaccessible_after % webapp_url)
after_mime_type = request.headers.get('Content-Type')
after_status = request.headers.get('Status')
# assume that app is broken if:
# it's response Content-Type or Status code (if first code wasn't 500) changed
# if last Status code was 500 (internal error)
if before_mime_type.lower() != after_mime_type.lower() or \
(before_status != after_status and before_status is not None and before_status[:3] != '500') \
or (after_status is not None and after_status[:3] == '500'):
raise ClSelectExcept.WebAppError(app_is_broken % (after_status, before_mime_type, after_mime_type))
def _get_info_about_webapp(app_summary=None, user=None):
"""
Get info (alias and domain) about user's web application
:param app_summary: dict -> summary info about user's web application
:param user: str -> name of unix user
:return: tuple -> (alias, domain)
"""
if app_summary is None:
raise ClSelectExcept.WrongData('Was passed incorrect summary info about application')
if user is None:
raise ClSelectExcept.WrongData('Was passed incorrect name of user')
alias = app_summary['alias']
user_domains = userdomains(user)
found_domains = [
domain for domain, doc_root in user_domains
if utils.realpaths_are_equal(user, doc_root, app_summary['docroot'])]
if len(found_domains) == 0:
raise ClSelectExcept.WrongData('Can not found suitable domain for application')
app_domain = found_domains[0]
return alias, app_domain
def install(user, directory, extension='-', skip_web_check=False, apps_manager=None):
"""
Install nodejs extension to user's webapp
:param user: name os unix user
:param directory: directory with webapp (app-root)
:param extension: name and version of extension
:param skip_web_check: skip check web application after change it's properties
:param apps_manager: Application Manager. Class that responsible
for gathering and writing information about applications
:return: None
"""
if apps_manager is None:
apps_manager = ApplicationsManager()
user_config = apps_manager.get_user_config_data(user)
try:
app_data = utils.get_using_realpath_keys(user, directory, user_config)
except KeyError:
raise ClSelectExcept.WrongData('Record about application {} is absent'.format(directory))
if app_data['app_status'] != APP_STARTED_CONST:
skip_web_check = True
else:
alias = app_data['app_uri']
app_domain = app_data['domain']
nodejs_version = app_data['nodejs_version']
cwd, _ = utils.get_abs_rel(user, directory)
environment = _create_environment(user, directory, nodejs_version)
def action():
# npm install
environment.extension_install(extension=extension, cwd=cwd)
clpassenger.restart(user, directory)
if not skip_web_check:
try:
check_response_from_webapp(
domain=app_domain,
alias=alias,
action=action,
)
except ClSelectExcept.WebAppError as err:
raise ClSelectExcept.WebAppError('An error occured during installation of modules. %s' % err)
else:
action()
def summary(user):
summ = {}
for directory, data in iteritems(clpassenger.summary(user)):
if data['interpreter'] != ApplicationsManager.INTERPRETER:
continue
environment = _get_environment(user, directory, data).as_deepdict()
summ[directory] = {
'domain': data['domain'],
'alias': data['alias'],
'environment': environment['name'],
'interpreter': environment['interpreter'],
}
# add only list with additions domains
if "domains" in data and len(data["domains"]) > 1:
summ[directory]["domains"] = data["domains"]
return summ
def validate_env_vars(env_vars):
if type(env_vars) is not dict:
raise TypeError
for item in env_vars:
if not isinstance(env_vars[item], basestring):
raise TypeError
return env_vars
Zerion Mini Shell 1.0