# Copyright 1999-2017. Plesk International GmbH. All rights reserved. # vim: ts=4 sts=4 sw=4 et : import os import glob import re from time import sleep import php_merge import php_layout import plesk_log import plesk_service log = plesk_log.getLogger(__name__) class PhpFpmSanityCheck: __options = None __accepted_options = ["type", "virtual_host", "override", "remove", "sysuser", "no_reload", "service_action", "service_name", "pool_d", "global_config", "cgi_bin"] def __init__(self, options): self.__options = options def check(self): if not self.__options.type or self.__options.type != "fpm": raise Exception("Internal Error: Wrong php service type(%s) for php fpm" % self.__options.type) """ Check the options are valid """ for key in vars(self.__options): attr = getattr(self.__options, key) if attr and key not in self.__accepted_options: raise Exception("The option '%s' is not acceptable for php fpm" % key) if self.__options.service_action: if self.__options.remove or self.__options.virtual_host: raise Exception("Using service and configuration management options at the same time is not supported.") if not self.__options.service_name: raise Exception("Service name is not specified for fpm service action '%s'" % self.__options.service_action) return True if not self.__options.virtual_host: raise Exception("fpm handler type requires --virtual-host to be specified") if not self.__options.sysuser and not self.__options.remove: raise Exception("fpm handler type requires --sysuser to be specified unless with --remove") class PhpFpmService: pool_dir = None service = None """ php-fpm service manager. """ def __init__(self, options): self.service = options.service_name if options.service_name else php_layout.PHP_FPM_SERVICE_NAME self.pool_dir = options.pool_d if options.pool_d else php_layout.PHP_FPM_INCLUDE_DIR def action(self, act): if act not in ('start', 'stop', 'restart', 'reload'): raise ValueError("Unsupported php-fpm service action '%s'" % act) if act == 'reload': act = php_layout.PHP_FPM_RELOAD_CMD if not plesk_service.action(self.service, act): raise RuntimeError("Failed to %s %s service" % (act, self.service)) if act == php_layout.PHP_FPM_RELOAD_CMD: # In order to minimize occasional implications (race conditions) of reloading # the service by sending a signal (an asynchronous operation) if not self.is_running(): # resurrect fpm service if race condition killed one if not plesk_service.action(self.service, 'restart') or not self.is_running(): raise RuntimeError("Failed to %s %s service" % (act, self.service)) # Some init scripts don't report failure properly if act in ('start', 'restart', php_layout.PHP_FPM_RELOAD_CMD) and not self.status(): raise RuntimeError("Service %s is down after attempt to %s it" % (self.service, act)) log.debug("%s service %s succeeded", self.service, act) def is_running(self): for delay in [1, 1, 2]: if self.status(): sleep(1) if self.status(): return True sleep(delay) return False def status(self): st = plesk_service.action(self.service, 'status') log.debug("%s service status = %s", self.service, st) return st def enable(self): plesk_service.register(self.service, with_resource_controller=True) log.debug("%s service registered successfully", self.service) def disable(self): plesk_service.deregister(self.service) log.debug("%s service deregistered successfully", self.service) def smart_reload(self): """ 'Smart' reload. Suits perfectly if you want to update fpm pools configuration: reloads fpm-service in normal cases, stops service if last pool was removed, starts service if first pool is created.""" running = self.status() have_pools = self._has_pools() act = None register = None if running: if have_pools: act = "reload" else: (act, register) = ("stop", "disable") elif have_pools: (act, register) = ("start", "enable") log.debug("perform smart_reload action for service %s : %s", self.service, act) if act: self.action(act) if register == "enable": self.enable() elif register == "disable": self.disable() def _has_pools(self): for path in glob.glob(os.path.join(self.pool_dir, "*.conf")): with open(path) as f: for line in f: if re.match(r"^\s*\[.+\]", line): log.debug("Found active pool %s for service %s", path, self.service) return True log.debug("No pools found in %s for service %s", self.pool_dir, self.service) return False class PhpFpmPoolConfig: """ php-fpm pools configuration class. For a given virtual host a separate pool is configured with the same name. It will also contain all custom php.ini settings changed from the server-wide php.ini file. """ pool_settings_section = 'php-fpm-pool-settings' allowed_overrides = ( 'access.format', 'access.log', 'catch_workers_output', 'chdir', 'ping.path', 'ping.response', 'pm', 'pm.max_children', 'pm.max_requests', 'pm.max_spare_servers', 'pm.min_spare_servers', 'pm.process_idle_timeout', 'pm.start_servers', 'pm.status_path', 'request_slowlog_timeout', 'request_terminate_timeout', 'rlimit_core', 'rlimit_files', 'security.limit_extensions', # This one is tricky, don't override blindly! 'slowlog', ) allowed_override_prefixes = ( 'env', 'php_value', 'php_flag', 'php_admin_value', 'php_admin_flag', ) def __init__(self, options): self.vhost = options.virtual_host self.user = options.sysuser self.cgi_bin = options.cgi_bin if options.cgi_bin else None self.pool_d = options.pool_d if options.pool_d is not None else php_layout.PHP_FPM_INCLUDE_DIR self.server_wide_php_ini = options.global_config if options.global_config else php_layout.SERVER_WIDE_PHP_INI def merge(self, override_file=None, has_input_stream=True): default_data = """ [php-fpm-pool-settings] pm = ondemand pm.max_children = 5 pm.max_spare_servers = 1 pm.min_spare_servers = 1 pm.process_idle_timeout = 10s pm.start_servers = 1 """ self.config = php_merge.merge_input_configs(override_filename=override_file, allow_pool_settings=True, has_input_stream=has_input_stream, input_string=default_data) def infer_config_path(self): return os.path.join(self.pool_d, self.vhost + '.conf') def open(self, config_path): return open(config_path, 'w') def write(self, fileobject): """ Writes php-fpm pool configuration. All custom php.ini directives are included via php_value[] which is slightly incorrect, since this doesn't respect boolean settings (but doesn't break them either) and effectively allows modification of any customized php.ini settings by php scripts on this vhost. This implies a need for php.ini directives classifier based on their name and possibly php version. On-demand process spawning is used. It was introduced in php-fpm 5.3.9 and allows 0 processes on startup (useful for shared hosting). Looks like all available php-fpm packages in popular repositories are >= 5.3.10, so we don't check php-fpm version here. Also note that php-fpm will ignore any zend_extension directives. """ fileobject.write(php_layout.AUTOGENERATED_CONFIGS) # default pool configuration fileobject.write(""" ; If you need to customize this file, use either custom PHP settings tab in ; Panel or override settings in %(vhosts_d)s/%(vhost)s/conf/php.ini. ; To override pool configuration options, specify them in [%(pool_section)s] ; section of %(vhosts_d)s/%(vhost)s/conf/php.ini file. [%(vhost)s] ; Don't override following options, they are relied upon by Plesk internally prefix = %(vhosts_d)s/$pool user = %(user)s group = psacln listen = php-fpm.sock listen.owner = root listen.group = psaserv listen.mode = 0660 ; Following options can be overridden chdir = / ; Uses for log facility ; If php_value[error_log] is not defined error output will be send for nginx catch_workers_output = yes """ % { 'vhost': self.vhost, 'vhosts_d': php_layout.get_vhosts_system_d(), 'user': self.user, 'pool_section': self.pool_settings_section, }) # php.ini settings overrides try: # Note that we don't process 'HOST=' and 'PATH=' sections here # Also zend_extension directives are not filtered out as php-fpm ignores them anyway fileobject.write("; php.ini custom configuration directives\n") for name, value in sorted(self.config.items('PHP'), key=lambda x: x[0]): fileobject.write("php_value[%s] = %s\n" % (name, value)) fileobject.write("\n") except Exception, ex: log.warning("Processing of additional PHP directives for php-fpm failed: %s", ex) # pool configuration overrides if self.config.has_section(self.pool_settings_section): fileobject.write("; Following directives define pool configuration\n") for name, value in sorted(self.config.items(self.pool_settings_section), key=lambda x: x[0]): if ((name not in self.allowed_overrides and name.split('[', 1)[0] not in self.allowed_override_prefixes)): log.warning("Following pool configuration override is not permitted and was ignored: %s = %s", name, value) else: fileobject.write("%s = %s\n" % (name, value)) fileobject.flush() os.fsync(fileobject.fileno()) def check(self): if not self.cgi_bin: return True p = os.system(self.cgi_bin + " -t >/dev/null 2>&1") if p != 0: return False return True DECLARES = { "init": PhpFpmSanityCheck, "config": PhpFpmPoolConfig, "service": PhpFpmService }