From 79ae1a79b08849512ffa655c1bef83394956e57a Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Fri, 10 Apr 2015 15:03:38 +0000 Subject: [PATCH] Fixes on webapps --- TODO.md | 5 +- orchestra/contrib/webapps/backends/php.py | 47 +++-- orchestra/contrib/webapps/models.py | 5 +- orchestra/contrib/webapps/options.py | 29 +-- orchestra/contrib/webapps/settings.py | 1 + orchestra/contrib/webapps/types/php.py | 10 +- orchestra/contrib/websites/backends/apache.py | 168 +++++++++++------- 7 files changed, 167 insertions(+), 98 deletions(-) diff --git a/TODO.md b/TODO.md index 662822ab..0833b8f1 100644 --- a/TODO.md +++ b/TODO.md @@ -283,6 +283,9 @@ https://code.djangoproject.com/ticket/24576 * MultiCHoiceField proper serialization # Apache restart fails: detect if appache running, and execute start -# PHP backend is retarded does not detect well the version # Change crons, create cron for deleted webapps and users * UNIFY PHP FPM settings name +# virtualhost name name-account? +# php version update should trigger webiste upgrade (wrapper name/fpm config for apache), public root and other config also needs apache to execute +* add a delay to changes on the webserver apache to no overwelm it with backend executions? +# Delete webapps deletes wrapper that may be used for other sites, maybe merging webapps is a bad idea after all? diff --git a/orchestra/contrib/webapps/backends/php.py b/orchestra/contrib/webapps/backends/php.py index bf9eb3ab..3898b2fe 100644 --- a/orchestra/contrib/webapps/backends/php.py +++ b/orchestra/contrib/webapps/backends/php.py @@ -76,27 +76,44 @@ class PHPBackend(WebAppServiceMixin, ServiceController): self.delete_webapp_dir(context) def delete_fpm(self, webapp, context): - self.append("rm -f %(fpm_path)s" % context) + # Better not delete a pool used by other apps + if not self.MERGE: + self.append("rm -f %(fpm_path)s" % context) def delete_fcgid(self, webapp, context): - self.append("rm -f %(wrapper_path)s" % context) - self.append("rm -f %(cmd_options_path)s" % context) + # Better not delete a wrapper used by other apps + if not self.MERGE: + self.append("rm -f %(wrapper_path)s" % context) + self.append("rm -f %(cmd_options_path)s" % context) + + def prepare(self): + super(PHPBackend, self).prepare() + # Coordinate apache restart with php backend in order not to overdo it + self.append('echo "PHPBackend" >> /dev/shm/restart.apache2') def commit(self): - if self.content: - self.append(textwrap.dedent(""" - if [[ $UPDATEDFPM == 1 ]]; then - service php5-fpm reload - service php5-fpm start - fi - """) - ) - self.append(textwrap.dedent("""\ - if [[ $UPDATED_APACHE == 1 ]]; then + self.append(textwrap.dedent(""" + if [[ $UPDATEDFPM == 1 ]]; then + service php5-fpm reload + service php5-fpm start + fi + # Coordinate apache restart with apache backend + locked=1 + state="$(grep -v 'PHPBackend' /dev/shm/restart.apache2)" || locked=0 + echo -n "$state" > /dev/shm/restart.apache2 + if [[ $UPDATED_APACHE == 1 ]]; then + if [[ $locked == 0 ]]; then service apache2 reload + else + echo "PHPBackend RESTART" >> /dev/shm/restart.apache2 fi - """) - ) + elif [[ "$state" =~ .*RESTART$ ]]; then + rm /dev/shm/restart.apache2 + service apache2 reload + fi + """) + ) + super(PHPBackend, self).commit() def get_fpm_config(self, webapp, context): merge = settings.WEBAPPS_MERGE_PHP_WEBAPPS diff --git a/orchestra/contrib/webapps/models.py b/orchestra/contrib/webapps/models.py index a72da587..e9b8b7b8 100644 --- a/orchestra/contrib/webapps/models.py +++ b/orchestra/contrib/webapps/models.py @@ -1,4 +1,5 @@ import os +from collections import OrderedDict from django.db import models from django.db.models.signals import pre_save, pre_delete @@ -56,9 +57,7 @@ class WebApp(models.Model): @cached def get_options(self): - return { - opt.name: opt.value for opt in self.options.all() - } + return OrderedDict((opt.name, opt.value) for opt in self.options.all()) def get_directive(self): return self.type_instance.get_directive() diff --git a/orchestra/contrib/webapps/options.py b/orchestra/contrib/webapps/options.py index c3732dab..d90cf0bf 100644 --- a/orchestra/contrib/webapps/options.py +++ b/orchestra/contrib/webapps/options.py @@ -56,7 +56,7 @@ class PHPAppOption(AppOption): super(PHPAppOption, self).validate() if self.deprecated: php_version = self.instance.webapp.type_instance.get_php_version_number() - if php_version and php_version > self.deprecated: + if php_version and self.deprecated and float(php_version) > self.deprecated: raise ValidationError( _("This option is deprecated since PHP version %s.") % str(self.deprecated) ) @@ -76,7 +76,8 @@ class Timeout(AppOption): # FPM pm.request_terminate_timeout # PHP max_execution_time ini verbose_name = _("Process timeout") - help_text = _("Maximum time in seconds allowed for a request to complete (a number between 0 and 999).") + help_text = _("Maximum time in seconds allowed for a request to complete (a number between 0 and 999).
" + "Also sets max_request_time when php-cgi is used.") regex = r'^[0-9]{1,3}$' group = AppOption.PROCESS @@ -94,7 +95,7 @@ class Processes(AppOption): class PHPEnabledFunctions(PHPAppOption): name = 'enabled_functions' verbose_name = _("Enabled functions") - help_text = ' '.join(settings.WEBAPPS_PHP_DISABLED_FUNCTIONS) + help_text = ','.join(settings.WEBAPPS_PHP_DISABLED_FUNCTIONS) regex = r'^[\w\.,-]+$' @@ -145,7 +146,7 @@ class PHPDisplayErrors(PHPAppOption): name = 'display_errors' verbose_name = _("Display errors") help_text = _("Determines whether errors should be printed to the screen as part of the output or " - "if they should be hidden from the user (On or Off).") + "if they should be hidden from the user (On or Off).") regex = r'^(On|Off|on|off)$' @@ -159,7 +160,7 @@ class PHPMagicQuotesGPC(PHPAppOption): name = 'magic_quotes_gpc' verbose_name = _("Magic quotes GPC") help_text = _("Sets the magic_quotes state for GPC (Get/Post/Cookie) operations (On or Off) " - "DEPRECATED as of PHP 5.3.0.") + "DEPRECATED as of PHP 5.3.0.") regex = r'^(On|Off|on|off)$' deprecated = 5.3 @@ -168,7 +169,7 @@ class PHPMagicQuotesRuntime(PHPAppOption): name = 'magic_quotes_runtime' verbose_name = _("Magic quotes runtime") help_text = _("Functions that return data from any sort of external source will have quotes escaped " - "with a backslash (On or Off) DEPRECATED as of PHP 5.3.0.") + "with a backslash (On or Off) DEPRECATED as of PHP 5.3.0.") regex = r'^(On|Off|on|off)$' deprecated = 5.3 @@ -200,7 +201,7 @@ class PHPMemoryLimit(PHPAppOption): name = 'memory_limit' verbose_name = _("Memory limit") help_text = _("This sets the maximum amount of memory in bytes that a script is allowed to allocate " - "(Value between 0M and 999M).") + "(Value between 0M and 999M).") regex = r'^[0-9]{1,3}M$' @@ -222,7 +223,7 @@ class PHPRegisterGlobals(PHPAppOption): name = 'register_globals' verbose_name = _("Register globals") help_text = _("Whether or not to register the EGPCS (Environment, GET, POST, Cookie, Server) " - "variables as global variables (On or Off).") + "variables as global variables (On or Off).") regex = r'^(On|Off|on|off)$' @@ -235,21 +236,21 @@ class PHPPostMaxSize(PHPAppOption): class PHPSendmailPath(PHPAppOption): name = 'sendmail_path' - verbose_name = _("sendmail_path") + verbose_name = _("Sendmail path") help_text = _("Where the sendmail program can be found.") regex = r'^[^ ]+$' class PHPSessionBugCompatWarn(PHPAppOption): name = 'session.bug_compat_warn' - verbose_name = _("session.bug_compat_warn") + verbose_name = _("Session bug compat warning") help_text = _("Enables an PHP bug on session initialization for legacy behaviour (On or Off).") regex = r'^(On|Off|on|off)$' class PHPSessionAutoStart(PHPAppOption): name = 'session.auto_start' - verbose_name = _("session.auto_start") + verbose_name = _("Session auto start") help_text = _("Specifies whether the session module starts a session automatically on request " "startup (On or Off).") regex = r'^(On|Off|on|off)$' @@ -287,7 +288,7 @@ class PHPSuhosinRequestMaxVars(PHPAppOption): class PHPSuhosinSessionEncrypt(PHPAppOption): name = 'suhosin.session.encrypt' - verbose_name = _("suhosin.session.encrypt") + verbose_name = _("Suhosin session encrypt") help_text = _("On or Off") regex = r'^(On|Off|on|off)$' @@ -301,13 +302,13 @@ class PHPSuhosinSimulation(PHPAppOption): class PHPSuhosinExecutorIncludeWhitelist(PHPAppOption): name = 'suhosin.executor.include.whitelist' - verbose_name = _("suhosin.executor.include.whitelist") + verbose_name = _("Suhosin executor include whitelist") regex = r'.*$' class PHPUploadMaxFileSize(PHPAppOption): name = 'upload_max_filesize' - verbose_name = _("upload_max_filesize") + verbose_name = _("Upload max filezise") help_text = _("Value between 0M and 999M.") regex = r'^[0-9]{1,3}M$' diff --git a/orchestra/contrib/webapps/settings.py b/orchestra/contrib/webapps/settings.py index 58ded0e4..399a1439 100644 --- a/orchestra/contrib/webapps/settings.py +++ b/orchestra/contrib/webapps/settings.py @@ -110,6 +110,7 @@ WEBAPPS_UNDER_CONSTRUCTION_PATH = getattr(settings, 'WEBAPPS_UNDER_CONSTRUCTION_ # WEBAPPS_TYPES[webapp_type] = value + WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTION', [ 'exec', 'passthru', diff --git a/orchestra/contrib/webapps/types/php.py b/orchestra/contrib/webapps/types/php.py index e210fcaf..5d05c7bc 100644 --- a/orchestra/contrib/webapps/types/php.py +++ b/orchestra/contrib/webapps/types/php.py @@ -1,5 +1,6 @@ import os import re +from collections import OrderedDict from django import forms from django.utils.translation import ugettext_lazy as _ @@ -60,16 +61,16 @@ class PHPApp(AppType): @cached def get_php_options(self): - php_version = self.get_php_version_number() + php_version = float(self.get_php_version_number()) php_options = AppOption.get_option_groups()[AppOption.PHP] - return [op for op in php_options if getattr(self, 'deprecated', 999) > php_version] + return [op for op in php_options if (op.deprecated or 999) > php_version] def get_php_init_vars(self, merge=False): """ process php options for inclusion on php.ini per_account=True merges all (account, webapp.type) options """ - init_vars = {} + init_vars = OrderedDict() options = self.instance.options.all() if merge: # Get options from the same account and php_version webapps @@ -108,6 +109,7 @@ class PHPApp(AppType): context = super(PHPApp, self).get_directive_context() context.update({ 'php_version': self.get_php_version(), + 'php_version_number': self.get_php_version_number(), }) return context @@ -134,4 +136,4 @@ class PHPApp(AppType): raise ValueError("No version number matches for '%s'" % php_version) if len(number) > 1: raise ValueError("Multiple version number matches for '%s'" % php_version) - return float(number[0]) + return number[0] diff --git a/orchestra/contrib/websites/backends/apache.py b/orchestra/contrib/websites/backends/apache.py index 8174ec71..e5a8545e 100644 --- a/orchestra/contrib/websites/backends/apache.py +++ b/orchestra/contrib/websites/backends/apache.py @@ -19,6 +19,7 @@ class Apache2Backend(ServiceController): model = 'websites.Website' related_models = ( ('websites.Content', 'website'), + ('webapps.WebApp', 'website_set'), ) verbose_name = _("Apache 2") @@ -41,9 +42,9 @@ class Apache2Backend(ServiceController): return Template(textwrap.dedent("""\ IncludeOptional /etc/apache2/site[s]-override/{{ site_unique_name }}.con[f] - ServerName {{ site.domains.all|first }}\ - {% if site.domains.all|slice:"1:" %} - ServerAlias {{ site.domains.all|slice:"1:"|join:' ' }}{% endif %}\ + ServerName {{ server_name }}\ + {% if server_alias %} + ServerAlias {{ server_alias|join:' ' }}{% endif %}\ {% if access_log %} CustomLog {{ access_log }} common{% endif %}\ {% if error_log %} @@ -59,9 +60,9 @@ class Apache2Backend(ServiceController): context['port'] = self.HTTP_PORT return Template(textwrap.dedent(""" - ServerName {{ site.domains.all|first }}\ - {% if site.domains.all|slice:"1:" %} - ServerAlias {{ site.domains.all|slice:"1:"|join:' ' }}{% endif %}\ + ServerName {{ server_name }}\ + {% if server_alias %} + ServerAlias {{ server_alias|join:' ' }}{% endif %}\ {% if access_log %} CustomLog {{ access_log }} common{% endif %}\ {% if error_log %} @@ -75,33 +76,67 @@ class Apache2Backend(ServiceController): def save(self, site): context = self.get_context(site) - apache_conf = '# %(banner)s\n' % context - if site.protocol in (site.HTTP, site.HTTP_AND_HTTPS): - apache_conf += self.render_virtual_host(site, context, ssl=False) - if site.protocol in (site.HTTP_AND_HTTPS, site.HTTPS_ONLY, site.HTTPS): - apache_conf += self.render_virtual_host(site, context, ssl=True) - if site.protocol == site.HTTPS_ONLY: - apache_conf += self.render_redirect_https(context) - context['apache_conf'] = apache_conf.replace("'", '"') - self.append(textwrap.dedent("""\ - apache_conf='%(apache_conf)s' - { - echo -e "${apache_conf}" | diff -N -I'^\s*#' %(sites_available)s - - } || { - echo -e "${apache_conf}" > %(sites_available)s - UPDATED=1 - }""") % context - ) - self.enable_or_disable(site) + if context['server_name']: + apache_conf = '# %(banner)s\n' % context + if site.protocol in (site.HTTP, site.HTTP_AND_HTTPS): + apache_conf += self.render_virtual_host(site, context, ssl=False) + if site.protocol in (site.HTTP_AND_HTTPS, site.HTTPS_ONLY, site.HTTPS): + apache_conf += self.render_virtual_host(site, context, ssl=True) + if site.protocol == site.HTTPS_ONLY: + apache_conf += self.render_redirect_https(context) + context['apache_conf'] = apache_conf.replace("'", '"') + self.append(textwrap.dedent("""\ + apache_conf='%(apache_conf)s' + { + echo -e "${apache_conf}" | diff -N -I'^\s*#' %(sites_available)s - + } || { + echo -e "${apache_conf}" > %(sites_available)s + UPDATED=1 + }""") % context + ) + if context['server_name'] and site.active: + self.append(textwrap.dedent("""\ + if [[ ! -f %(sites_enabled)s ]]; then + a2ensite %(site_unique_name)s.conf + UPDATED=1 + fi""") % context + ) + else: + self.append(textwrap.dedent("""\ + if [[ -f %(sites_enabled)s ]]; then + a2dissite %(site_unique_name)s.conf; + UPDATED=1 + fi""") % context + ) def delete(self, site): context = self.get_context(site) self.append("a2dissite %(site_unique_name)s.conf && UPDATED=1" % context) self.append("rm -f %(sites_available)s" % context) + def prepare(self): + super(Apache2Backend, self).prepare() + # Coordinate apache restart with php backend in order not to overdo it + self.append('echo "Apache2Backend" >> /dev/shm/restart.apache2') + def commit(self): """ reload Apache2 if necessary """ - self.append('if [[ $UPDATED == 1 ]]; then service apache2 reload; fi') + self.append(textwrap.dedent("""\ + locked=1 + state="$(grep -v 'Apache2Backend' /dev/shm/restart.apache2)" || locked=0 + echo -n "$state" > /dev/shm/restart.apache2 + if [[ $UPDATED == 1 ]]; then + if [[ $locked == 0 ]]; then + service apache2 reload + else + echo "Apache2Backend RESTART" >> /dev/shm/restart.apache2 + fi + elif [[ "$state" =~ .*RESTART$ ]]; then + rm /dev/shm/restart.apache2 + service apache2 reload + fi""") + ) + super(Apache2Backend, self).commit() def get_directives(self, directive, context): method, args = directive[0], directive[1:] @@ -122,9 +157,15 @@ class Apache2Backend(ServiceController): def get_static_directives(self, context, app_path): context['app_path'] = os.path.normpath(app_path % context) - location = "%(location)s/" % context - directive = "Alias %(location)s/ %(app_path)s/" % context - return [(location, directive)] + directive = self.get_location_filesystem_map(context) + return [ + (context['location'], directive), + ] + + def get_location_filesystem_map(self, context): + if not context['location']: + return 'DocumentRoot %(app_path)s' % context + return 'Alias %(location)s %(app_path)s' % context def get_fpm_directives(self, context, socket, app_path): if ':' in socket: @@ -139,28 +180,28 @@ class Apache2Backend(ServiceController): 'app_path': os.path.normpath(app_path), 'socket': socket, }) - location = "%(location)s/" % context - directives = textwrap.dedent("""\ - ProxyPassMatch ^%(location)s/(.*\.php(/.*)?)$ {target} - Alias %(location)s/ %(app_path)s/""".format(target=target) % context - ) - return [(location, directives)] + directives = "ProxyPassMatch ^%(location)s/(.*\.php(/.*)?)$ {target}\n".format(target=target) % context + directives += self.get_location_filesystem_map(context) + return [ + (context['location'], directives), + ] def get_fcgid_directives(self, context, app_path, wrapper_path): context.update({ 'app_path': os.path.normpath(app_path), 'wrapper_path': wrapper_path, }) - location = "%(location)s/" % context - directives = textwrap.dedent("""\ - Alias %(location)s/ %(app_path)s/ + directives = self.get_location_filesystem_map(context) + directives += textwrap.dedent(""" ProxyPass %(location)s/ ! Options +ExecCGI AddHandler fcgid-script .php FcgidWrapper %(wrapper_path)s """) % context - return [(location, directives)] + return [ + (context['location'], directives), + ] def get_ssl(self, directives): cert = directives.get('ssl-cert') @@ -177,7 +218,9 @@ class Apache2Backend(ServiceController): config += "SSLCertificateKeyFile %s\n" % key[0] if ca: config += "SSLCACertificateFile %s\n" % ca[0] - return [('', config)] + return [ + ('', config), + ] def get_security(self, directives): security = [] @@ -201,20 +244,27 @@ class Apache2Backend(ServiceController): redirect = "RedirectMatch %s %s" % (location, target) else: redirect = "Redirect %s %s" % (location, target) - redirects.append((location, redirect)) + redirects.append( + (location, redirect) + ) return redirects def get_proxies(self, directives): proxies = [] for proxy in directives.get('proxy', []): - location, target = proxy.split() + proxy = proxy.split() + location = proxy[0] + target = proxy[1] + options = ' '.join(proxy[2:]) location = normurlpath(location) proxy = textwrap.dedent("""\ - ProxyPass {location}/ {target} + ProxyPass {location}/ {target} {options} ProxyPassReverse {location}/ {target}""".format( - location=location, target=target) + location=location, target=target, options=options) + ) + proxies.append( + (location, proxy) ) - proxies.append((location, proxy)) return proxies def get_saas(self, directives): @@ -229,23 +279,6 @@ class Apache2Backend(ServiceController): saas += self.get_directives(directive, context) return saas - def enable_or_disable(self, site): - context = self.get_context(site) - if site.is_active: - self.append(textwrap.dedent("""\ - if [[ ! -f %(sites_enabled)s ]]; then - a2ensite %(site_unique_name)s.conf - UPDATED=1 - fi""") % context - ) - else: - self.append(textwrap.dedent("""\ - if [[ -f %(sites_enabled)s ]]; then - a2dissite %(site_unique_name)s.conf; - UPDATED=1 - fi""") % context - ) - def get_username(self, site): option = site.get_directives().get('user_group') if option: @@ -259,10 +292,21 @@ class Apache2Backend(ServiceController): return group return site.get_groupname() + def get_server_names(self, site): + server_name = None + server_alias = [] + for domain in site.domains.all(): + if not server_name and not domain.name.startswith('*'): + server_name = domain.name + else: + server_alias.append(domain.name) + return server_name, server_alias + def get_context(self, site): base_apache_conf = settings.WEBSITES_BASE_APACHE_CONF sites_available = os.path.join(base_apache_conf, 'sites-available') sites_enabled = os.path.join(base_apache_conf, 'sites-enabled') + server_name, server_alias = self.get_server_names(site) context = { 'site': site, 'site_name': site.name, @@ -270,6 +314,8 @@ class Apache2Backend(ServiceController): 'site_unique_name': '0-'+site.unique_name, 'user': self.get_username(site), 'group': self.get_groupname(site), + 'server_name': server_name, + 'server_alias': server_alias, # TODO remove '0-' 'sites_enabled': "%s.conf" % os.path.join(sites_enabled, '0-'+site.unique_name), 'sites_available': "%s.conf" % os.path.join(sites_available, '0-'+site.unique_name),