From 44e8b29b4380000487acfa3f435e79183275b08e Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Tue, 10 Mar 2015 16:57:23 +0000 Subject: [PATCH] Fixes on webapps and websites backend execution --- TODO.md | 4 + orchestra/apps/databases/backends.py | 24 +- orchestra/apps/domains/backends.py | 26 +-- orchestra/apps/mailboxes/backends.py | 10 +- orchestra/apps/orchestration/models.py | 2 +- orchestra/apps/systemusers/backends.py | 10 +- orchestra/apps/webapps/admin.py | 4 +- orchestra/apps/webapps/backends/__init__.py | 8 + orchestra/apps/webapps/backends/phpfcgid.py | 78 ++++--- orchestra/apps/webapps/backends/phpfpm.py | 32 +-- orchestra/apps/webapps/backends/static.py | 6 +- orchestra/apps/webapps/backends/wordpress.py | 4 +- orchestra/apps/webapps/models.py | 9 +- orchestra/apps/webapps/options.py | 207 +++++++++--------- orchestra/apps/webapps/settings.py | 31 +-- orchestra/apps/webapps/types.py | 63 ++++-- orchestra/apps/websites/admin.py | 6 +- orchestra/apps/websites/backends/apache.py | 99 ++++----- orchestra/apps/websites/backends/webalizer.py | 16 +- orchestra/apps/websites/directives.py | 6 +- orchestra/apps/websites/models.py | 12 +- orchestra/apps/websites/serializers.py | 2 +- orchestra/conf/base_settings.py | 7 +- orchestra/core/middlewares.py | 47 ++++ 24 files changed, 412 insertions(+), 301 deletions(-) create mode 100644 orchestra/core/middlewares.py diff --git a/TODO.md b/TODO.md index 563b1bfb..1d469d27 100644 --- a/TODO.md +++ b/TODO.md @@ -213,3 +213,7 @@ ssh-copy-id root@ * webalizer backend on webapps and check webapps.websites.all() * monitor in batches doesnt work!!! + + +* mv: cannot move `/home/marcay/webapps/webalizer/' to a subdirectory of itself, `/home/marcay/webapps/webalizer/.deleted' +* Create utility for dealing with web paths '//', leading and ending '/' diff --git a/orchestra/apps/databases/backends.py b/orchestra/apps/databases/backends.py index df1454c8..eca453ae 100644 --- a/orchestra/apps/databases/backends.py +++ b/orchestra/apps/databases/backends.py @@ -31,8 +31,8 @@ class MySQLBackend(ServiceController): }) self.append(textwrap.dedent("""\ mysql -e 'GRANT ALL PRIVILEGES ON `%(database)s`.* TO "%(username)s"@"%(host)s" %(grant)s;' \ - """ % context - )) + """) % context + ) def delete(self, database): if database.type != database.MYSQL: @@ -62,12 +62,12 @@ class MySQLUserBackend(ServiceController): context = self.get_context(user) self.append(textwrap.dedent("""\ mysql -e 'CREATE USER "%(username)s"@"%(host)s";' || true \ - """ % context - )) + """) % context + ) self.append(textwrap.dedent("""\ mysql -e 'UPDATE mysql.user SET Password="%(password)s" WHERE User="%(username)s";' \ - """ % context - )) + """) % context + ) def delete(self, user): if user.type != user.MYSQL: @@ -75,8 +75,8 @@ class MySQLUserBackend(ServiceController): context = self.get_context(user) self.append(textwrap.dedent("""\ mysql -e 'DROP USER "%(username)s"@"%(host)s";' \ - """ % context - )) + """) % context + ) def commit(self): self.append("mysql -e 'FLUSH PRIVILEGES;'") @@ -99,8 +99,8 @@ class MysqlDisk(ServiceMonitor): context = self.get_context(db) self.append(textwrap.dedent("""\ mysql -e 'UPDATE db SET Insert_priv="N", Create_priv="N" WHERE Db="%(db_name)s";'\ - """ % context - )) + """) % context + ) def recovery(self, db): if db.type != db.MYSQL: @@ -108,8 +108,8 @@ class MysqlDisk(ServiceMonitor): context = self.get_context(db) self.append(textwrap.dedent("""\ mysql -e 'UPDATE db SET Insert_priv="Y", Create_priv="Y" WHERE Db="%(db_name)s";'\ - """ % context - )) + """) % context + ) def prepare(self): super(MysqlDisk, self).prepare() diff --git a/orchestra/apps/domains/backends.py b/orchestra/apps/domains/backends.py index 48a9b044..2e653ed3 100644 --- a/orchestra/apps/domains/backends.py +++ b/orchestra/apps/domains/backends.py @@ -36,8 +36,8 @@ class Bind9MasterDomainBackend(ServiceController): mv %(zone_path)s.tmp %(zone_path)s # Because bind realod will not display any fucking error named-checkzone -k fail -n fail %(name)s %(zone_path)s - """ % context - )) + """) % context + ) self.update_conf(context) def update_conf(self, context): @@ -47,13 +47,13 @@ class Bind9MasterDomainBackend(ServiceController): -e 'N; /^\s*\\n\s*$/d; P; D' %(conf_path)s echo '%(conf)s' >> %(conf_path)s UPDATED=1 - }""" % context - )) + }""") % context + ) # Delete ex-top-domains that are now subdomains self.append(textwrap.dedent("""\ sed -i -e '/zone\s\s*".*\.%(name)s".*/,/^\s*};\s*$/d' \\ - -e 'N; /^\s*\\n\s*$/d; P; D' %(conf_path)s""" % context - )) + -e 'N; /^\s*\\n\s*$/d; P; D' %(conf_path)s""") % context + ) if 'zone_path' in context: context['zone_subdomains_path'] = re.sub(r'^(.*/)', r'\1*.', context['zone_path']) self.append('rm -f %(zone_subdomains_path)s' % context) @@ -69,19 +69,19 @@ class Bind9MasterDomainBackend(ServiceController): return self.append(textwrap.dedent("""\ sed -e '/zone\s\s*"%(name)s".*/,/^\s*};\s*$/d' \\ - -e 'N; /^\s*\\n\s*$/d; P; D' %(conf_path)s > %(conf_path)s.tmp""" % context - )) + -e 'N; /^\s*\\n\s*$/d; P; D' %(conf_path)s > %(conf_path)s.tmp""") % context + ) self.append('diff -B -I"^\s*//" %(conf_path)s.tmp %(conf_path)s || UPDATED=1' % context) self.append('mv %(conf_path)s.tmp %(conf_path)s' % context) def commit(self): """ reload bind if needed """ - self.append('[[ $UPDATED == 1 ]] && service bind9 reload') + self.append('if [[ $UPDATED == 1 ]]; then service bind9 reload; fi') def get_servers(self, domain, backend): """ Get related server IPs from registered backend routes """ from orchestra.apps.orchestration.manager import router - operation = Operation.create(backend, peration.SAVE, domain) + operation = Operation.create(backend, domain, Operation.SAVE) servers = [] for server in router.get_servers(operation): servers.append(server.get_ip()) @@ -110,7 +110,7 @@ class Bind9MasterDomainBackend(ServiceController): allow-transfer { %(slaves)s; }; also-notify { %(also_notify)s }; notify yes; - };""" % context) + };""") % context }) return context @@ -131,7 +131,7 @@ class Bind9SlaveDomainBackend(Bind9MasterDomainBackend): def commit(self): """ ideally slave should be restarted after master """ - self.append('[[ $UPDATED == 1 ]] && { sleep 1 && service bind9 reload; } &') + self.append('if [[ $UPDATED == 1 ]]; then { sleep 1 && service bind9 reload; } & fi') def get_masters(self, domain): return self.get_servers(domain, Bind9MasterDomainBackend) @@ -152,6 +152,6 @@ class Bind9SlaveDomainBackend(Bind9MasterDomainBackend): file "%(name)s"; masters { %(masters)s; }; allow-notify { %(masters)s; }; - };""" % context) + };""") % context }) return context diff --git a/orchestra/apps/mailboxes/backends.py b/orchestra/apps/mailboxes/backends.py index 0adcdf90..e466da16 100644 --- a/orchestra/apps/mailboxes/backends.py +++ b/orchestra/apps/mailboxes/backends.py @@ -33,8 +33,8 @@ class PasswdVirtualUserBackend(ServiceController): sed -i 's#^%(username)s:.*#%(passwd)s#' %(passwd_path)s else echo '%(passwd)s' >> %(passwd_path)s - fi""" % context - )) + fi""") % context + ) self.append("mkdir -p %(home)s" % context) self.append("chown %(uid)s:%(gid)s %(home)s" % context) @@ -43,7 +43,8 @@ class PasswdVirtualUserBackend(ServiceController): if [[ ! $(grep "^%(username)s@%(mailbox_domain)s\s" %(virtual_mailbox_maps)s) ]]; then echo "%(username)s@%(mailbox_domain)s\tOK" >> %(virtual_mailbox_maps)s UPDATED_VIRTUAL_MAILBOX_MAPS=1 - fi""" % context)) + fi""") % context + ) def generate_filter(self, mailbox, context): self.append("doveadm mailbox create -u %(username)s Spam" % context) @@ -92,7 +93,8 @@ class PasswdVirtualUserBackend(ServiceController): self.append(textwrap.dedent("""\ [[ $UPDATED_VIRTUAL_MAILBOX_MAPS == 1 ]] && { postmap %(virtual_mailbox_maps)s - }""" % context)) + }""") % context + ) def get_context(self, mailbox): context = { diff --git a/orchestra/apps/orchestration/models.py b/orchestra/apps/orchestration/models.py index 7c0d8b60..0c3d969e 100644 --- a/orchestra/apps/orchestration/models.py +++ b/orchestra/apps/orchestration/models.py @@ -154,7 +154,7 @@ class BackendOperation(models.Model): """ if self.action == self.DELETE: if hasattr(self.backend, 'get_context'): - self.backend.get_context(self.instance) + self.backend().get_context(self.instance) def backend_class(self): return ServiceBackend.get_backend(self.backend) diff --git a/orchestra/apps/systemusers/backends.py b/orchestra/apps/systemusers/backends.py index 4aaee319..eb7d5349 100644 --- a/orchestra/apps/systemusers/backends.py +++ b/orchestra/apps/systemusers/backends.py @@ -26,8 +26,8 @@ class SystemUserBackend(ServiceController): fi mkdir -p %(home)s chmod 750 %(home)s - chown %(username)s:%(username)s %(home)s""" % context - )) + chown %(username)s:%(username)s %(home)s""") % context + ) for member in settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS: context['member'] = member self.append('usermod -a -G %(username)s %(member)s' % context) @@ -40,8 +40,8 @@ class SystemUserBackend(ServiceController): { sleep 2 && killall -u %(username)s -s KILL; } & killall -u %(username)s || true userdel %(username)s || true - groupdel %(username)s || true""" % context - )) + groupdel %(username)s || true""") % context + ) self.delete_home(context, user) def grant_permission(self, user): @@ -145,7 +145,7 @@ class FTPTraffic(ServiceMonitor): print sum }' || [[ $? == 1 ]] && true } | xargs echo ${OBJECT_ID} - }""" % current_date)) + }""") % current_date) def monitor(self, user): context = self.get_context(user) diff --git a/orchestra/apps/webapps/admin.py b/orchestra/apps/webapps/admin.py index 3da35a84..30a40180 100644 --- a/orchestra/apps/webapps/admin.py +++ b/orchestra/apps/webapps/admin.py @@ -53,7 +53,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) inlines = [WebAppOptionInline] readonly_fields = ('account_link',) change_readonly_fields = ('name', 'type') - list_prefetch_related = ('contents__website',) + list_prefetch_related = ('content_set__website',) plugin = AppType plugin_field = 'type' plugin_title = _("Web application type") @@ -64,7 +64,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) def display_websites(self, webapp): websites = [] - for content in webapp.contents.all(): + for content in webapp.content_set.all(): website = content.website url = change_url(website) name = "%s on %s" % (website.name, content.path) diff --git a/orchestra/apps/webapps/backends/__init__.py b/orchestra/apps/webapps/backends/__init__.py index 81155bc9..6b701e3c 100644 --- a/orchestra/apps/webapps/backends/__init__.py +++ b/orchestra/apps/webapps/backends/__init__.py @@ -1,15 +1,22 @@ import pkgutil import textwrap +from .. import settings + class WebAppServiceMixin(object): model = 'webapps.WebApp' directive = None def create_webapp_dir(self, context): + self.append("[[ ! -e %(app_path)s ]] && CREATED=true" % context) self.append("mkdir -p %(app_path)s" % context) self.append("chown %(user)s:%(group)s %(app_path)s" % context) + def set_under_construction(self, context): + if context['under_construction_path']: + self.append("[[ $CREATED ]] && cp -r %(under_construction_path)s %(app_path)s" % context) + def delete_webapp_dir(self, context): self.append("rm -fr %(app_path)s" % context) @@ -21,6 +28,7 @@ class WebAppServiceMixin(object): 'type': webapp.type, 'app_path': webapp.get_path().rstrip('/'), 'banner': self.get_banner(), + 'under_construction_path': settings.settings.WEBAPPS_UNDER_CONSTRUCTION_PATH } diff --git a/orchestra/apps/webapps/backends/phpfcgid.py b/orchestra/apps/webapps/backends/phpfcgid.py index 65e15427..604d0e4d 100644 --- a/orchestra/apps/webapps/backends/phpfcgid.py +++ b/orchestra/apps/webapps/backends/phpfcgid.py @@ -12,54 +12,82 @@ from .. import settings class PHPFcgidBackend(WebAppServiceMixin, ServiceController): """ Per-webapp fcgid application """ verbose_name = _("PHP-Fcgid") - directive = 'fcgi' - default_route_match = "webapp.type.endswith('-fcgi')" + directive = 'fcgid' + default_route_match = "webapp.type.endswith('-fcgid')" def save(self, webapp): - if not self.valid_directive(webapp): - return context = self.get_context(webapp) self.create_webapp_dir(context) + self.set_under_construction(context) self.append("mkdir -p %(wrapper_dir)s" % context) self.append(textwrap.dedent("""\ { - echo -e '%(wrapper_content)s' | diff -N -I'^\s*#' %(wrapper_path)s - + echo -e '%(wrapper)s' | diff -N -I'^\s*#' %(wrapper_path)s - } || { - echo -e '%(wrapper_content)s' > %(wrapper_path)s; UPDATED_APACHE=1 - }""" % context)) + echo -e '%(wrapper)s' > %(wrapper_path)s; UPDATED_APACHE=1 + }""") % context + ) self.append("chmod +x %(wrapper_path)s" % context) self.append("chown -R %(user)s:%(group)s %(wrapper_dir)s" % context) + if context['cmd_options']: + self.append(textwrap.dedent(""" + { + echo -e '%(cmd_options)s' | diff -N -I'^\s*#' %(cmd_options_path)s - + } || { + echo -e '%(cmd_options)s' > %(cmd_options_path)s; UPDATED_APACHE=1 + }""" ) % context + ) def delete(self, webapp): - if not self.valid_directive(webapp): - return context = self.get_context(webapp) self.append("rm '%(wrapper_path)s'" % context) self.delete_webapp_dir(context) def commit(self): - if not self.cmds: - return - super(PHPFcgidBackend, self).commit() - self.append("[[ $UPDATED_APACHE == 1 ]] && { service apache2 reload; }") + self.append('if [[ $UPDATED_APACHE == 1 ]]; then service apache2 reload; fi') + + def get_fcgid_wrapper(self, webapp, context): + opt = webapp.type_instance + # Format PHP init vars + init_vars = opt.get_php_init_vars(webapp) + if init_vars: + init_vars = [ '-d %s="%s"' % (k,v) for k,v in init_vars.iteritems() ] + init_vars = ', '.join(init_vars) + + context.update({ + 'php_binary': opt.php_binary, + 'php_rc': opt.php_rc, + 'php_init_vars': init_vars, + }) + return textwrap.dedent("""\ + #!/bin/sh + # %(banner)s + export PHPRC=%(php_rc)s + exec %(php_binary)s %(php_init_vars)s""") % context + + def get_fcgid_cmd_options(self, webapp, context): + maps = { + 'MaxProcesses': webapp.get_options().get('processes', None), + 'IOTimeout': webapp.get_options().get('timeout', None), + } + cmd_options = [] + for directive, value in maps.iteritems(): + if value: + cmd_options.append("%s %s" % (directive, value)) + if cmd_options: + cmd_options.insert(0, 'FcgidCmdOptions %(wrapper_path)s' % context) + return ' \\\n '.join(cmd_options) def get_context(self, webapp): context = super(PHPFcgidBackend, self).get_context(webapp) - init_vars = self.get_php_init_vars(webapp) - if init_vars: - init_vars = [ '%s="%s"' % (k,v) for k,v in init_vars ] - init_vars = ', -d '.join(init_vars) - context['init_vars'] = '-d %s' % init_vars - else: - context['init_vars'] = '' wrapper_path = settings.WEBAPPS_FCGID_PATH % context context.update({ - 'wrapper_content': textwrap.dedent("""\ - #!/bin/sh - # %(banner)s - export PHPRC=/etc/%(type)s/cgi/ - exec /usr/bin/%(type)s-cgi %(init_vars)s""" % context), + 'wrapper': self.get_fcgid_wrapper(webapp, context), 'wrapper_path': wrapper_path, 'wrapper_dir': os.path.dirname(wrapper_path), }) + context.update({ + 'cmd_options': self.get_fcgid_cmd_options(webapp, context), + 'cmd_options_path': settings.WEBAPPS_FCGID_CMD_OPTIONS_PATH % context, + }) return context diff --git a/orchestra/apps/webapps/backends/phpfpm.py b/orchestra/apps/webapps/backends/phpfpm.py index 07f065c5..d5b6dafb 100644 --- a/orchestra/apps/webapps/backends/phpfpm.py +++ b/orchestra/apps/webapps/backends/phpfpm.py @@ -18,14 +18,15 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController): def save(self, webapp): context = self.get_context(webapp) self.create_webapp_dir(context) + self.set_under_construction(context) self.append(textwrap.dedent("""\ { echo -e '%(fpm_config)s' | diff -N -I'^\s*;;' %(fpm_path)s - } || { echo -e '%(fpm_config)s' > %(fpm_path)s UPDATEDFPM=1 - }""" % context - )) + }""") % context + ) def delete(self, webapp): context = self.get_context(webapp) @@ -37,18 +38,17 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController): return super(PHPFPMBackend, self).commit() self.append(textwrap.dedent(""" - [[ $UPDATEDFPM == 1 ]] && { + if [[ $UPDATEDFPM == 1 ]]; then service php5-fpm reload service php5-fpm start - }""")) + fi""")) - def get_context(self, webapp): - if not self.valid_directive(webapp): - return - context = super(PHPFPMBackend, self).get_context(webapp) + def get_fpm_config(self, webapp, context): context.update({ - 'init_vars': self.get_php_init_vars(webapp), + 'init_vars': webapp.type_instance.get_php_init_vars(webapp), 'fpm_port': webapp.get_fpm_port(), + 'max_children': webapp.get_options().get('processes', False), + 'request_terminate_timeout': webapp.get_options().get('timeout', False), }) context['fpm_listen'] = settings.WEBAPPS_FPM_LISTEN % context fpm_config = Template(textwrap.dedent("""\ @@ -61,12 +61,18 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController): listen.owner = {{ user }} listen.group = {{ group }} pm = ondemand - pm.max_children = 4 - {% for name, value in init_vars %} - php_admin_value[{{ name | safe }}] = {{ value | safe }}{% endfor %}""" + {% if max_children %}pm.max_children = {{ max_children }}{% endif %} + {% if request_terminate_timeout %}request_terminate_timeout = {{ request_terminate_timeout }}{% endif %} + {% for name, value in init_vars.iteritems %} + php_admin_value[{{ name | safe }}] = {{ value | safe }}{% endfor %} + """ )) + return fpm_config.render(Context(context)) + + def get_context(self, webapp): + context = super(PHPFPMBackend, self).get_context(webapp) context.update({ - 'fpm_config': fpm_config.render(Context(context)), + 'fpm_config': self.get_fpm_config(webapp, context), 'fpm_path': settings.WEBAPPS_PHPFPM_POOL_PATH % context, }) return context diff --git a/orchestra/apps/webapps/backends/static.py b/orchestra/apps/webapps/backends/static.py index 67d9a526..6bd1b731 100644 --- a/orchestra/apps/webapps/backends/static.py +++ b/orchestra/apps/webapps/backends/static.py @@ -7,17 +7,13 @@ from . import WebAppServiceMixin class StaticBackend(WebAppServiceMixin, ServiceController): verbose_name = _("Static") - directive = 'static' default_route_match = "webapp.type == 'static'" def save(self, webapp): - if not self.valid_directive(webapp): - return context = self.get_context(webapp) self.create_webapp_dir(context) + self.set_under_construction(context) def delete(self, webapp): - if not self.valid_directive(webapp): - return context = self.get_context(webapp) self.delete_webapp_dir(context) diff --git a/orchestra/apps/webapps/backends/wordpress.py b/orchestra/apps/webapps/backends/wordpress.py index e9c4ab24..b1cdb9a0 100644 --- a/orchestra/apps/webapps/backends/wordpress.py +++ b/orchestra/apps/webapps/backends/wordpress.py @@ -30,8 +30,8 @@ class WordPressBackend(WebAppServiceMixin, ServiceController): mkdir %(app_path)s/wp-content/uploads chmod 750 %(app_path)s/wp-content/uploads chown -R %(user)s:%(group)s %(app_path)s - fi""" % context - )) + fi""") % context + ) def delete(self, webapp): context = self.get_context(webapp) diff --git a/orchestra/apps/webapps/models.py b/orchestra/apps/webapps/models.py index b503a5c7..2eb7bec1 100644 --- a/orchestra/apps/webapps/models.py +++ b/orchestra/apps/webapps/models.py @@ -12,6 +12,7 @@ from orchestra.core import validators, services from orchestra.utils.functional import cached from . import settings +from .options import AppOption from .types import AppType @@ -55,9 +56,6 @@ class WebApp(models.Model): opt.name: opt.value for opt in self.options.all() } - def get_fpm_port(self): - return settings.WEBAPPS_FPM_START_PORT + self.account_id - def get_directive(self): return self.type_instance.get_directive(self) @@ -67,6 +65,9 @@ class WebApp(models.Model): 'app_name': self.name, } path = settings.WEBAPPS_BASE_ROOT % context + public_root = self.options.filter(name='public-root').first() + if public_root: + path = os.path.join(path, public_root.value) return path.replace('//', '/') def get_user(self): @@ -95,7 +96,7 @@ class WebAppOption(models.Model): @cached_property def option_class(self): - return SiteDirective.get_plugin(self.name) + return AppOption.get_plugin(self.name) @cached_property def option_instance(self): diff --git a/orchestra/apps/webapps/options.py b/orchestra/apps/webapps/options.py index fa1854e6..9ee71ca7 100644 --- a/orchestra/apps/webapps/options.py +++ b/orchestra/apps/webapps/options.py @@ -1,3 +1,5 @@ +import re + from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ @@ -54,15 +56,6 @@ class PublicRoot(AppOption): group = AppOption.FILESYSTEM -class DirectoryProtection(AppOption): - name = 'directory-protection' - verbose_name = _("Directory protection") - help_text = _("Space separated ...") - regex = r'^([\w/_]+)\s+(\".*\")\s+([\w/_\.]+)$' - group = AppOption.FILESYSTEM - - - class Timeout(AppOption): name = 'timeout' # FCGID FcgidIOTimeout @@ -78,273 +71,273 @@ class Processes(AppOption): name = 'processes' # FCGID MaxProcesses # FPM pm.max_children - verbose_name=_("Number of processes") - help_text=_("Maximum number of children that can be alive at the same time (a number between 0 and 9).") - regex=r'^[0-9]$' + verbose_name = _("Number of processes") + help_text = _("Maximum number of children that can be alive at the same time (a number between 0 and 9).") + regex = r'^[0-9]$' group = AppOption.PROCESS class PHPEnabledFunctions(AppOption): name = 'enabled_functions' - verbose_name=_("Enabled functions") + verbose_name = _("Enabled functions") help_text = ' '.join(settings.WEBAPPS_PHP_DISABLED_FUNCTIONS) - regex=r'^[\w\.,-]+$' + regex = r'^[\w\.,-]+$' group = AppOption.PHP class PHPAllowURLInclude(AppOption): name = 'allow_url_include' - verbose_name=_("Allow URL include") - help_text=_("Allows the use of URL-aware fopen wrappers with include, include_once, require, " + verbose_name = _("Allow URL include") + help_text = _("Allows the use of URL-aware fopen wrappers with include, include_once, require, " "require_once (On or Off).") - regex=r'^(On|Off|on|off)$' + regex = r'^(On|Off|on|off)$' group = AppOption.PHP class PHPAllowURLFopen(AppOption): name = 'allow_url_fopen' - verbose_name=_("Allow URL fopen") - help_text=_("Enables the URL-aware fopen wrappers that enable accessing URL object like files (On or Off).") - regex=r'^(On|Off|on|off)$' + verbose_name = _("Allow URL fopen") + help_text = _("Enables the URL-aware fopen wrappers that enable accessing URL object like files (On or Off).") + regex = r'^(On|Off|on|off)$' group = AppOption.PHP class PHPAutoAppendFile(AppOption): name = 'auto_append_file' - verbose_name=_("Auto append file") - help_text=_("Specifies the name of a file that is automatically parsed after the main file.") - regex=r'^[\w\.,-/]+$' + verbose_name = _("Auto append file") + help_text = _("Specifies the name of a file that is automatically parsed after the main file.") + regex = r'^[\w\.,-/]+$' group = AppOption.PHP class PHPAutoPrependFile(AppOption): name = 'auto_prepend_file' - verbose_name=_("Auto prepend file") - help_text=_("Specifies the name of a file that is automatically parsed before the main file.") - regex=r'^[\w\.,-/]+$' + verbose_name = _("Auto prepend file") + help_text = _("Specifies the name of a file that is automatically parsed before the main file.") + regex = r'^[\w\.,-/]+$' group = AppOption.PHP class PHPDateTimeZone(AppOption): name = 'date.timezone' - verbose_name=_("date.timezone") - help_text=_("Sets the default timezone used by all date/time functions (Timezone string 'Europe/London').") - regex=r'^\w+/\w+$' + verbose_name = _("date.timezone") + help_text = _("Sets the default timezone used by all date/time functions (Timezone string 'Europe/London').") + regex = r'^\w+/\w+$' group = AppOption.PHP class PHPDefaultSocketTimeout(AppOption): name = 'default_socket_timeout' - verbose_name=_("Default socket timeout") - help_text=_("Number between 0 and 999.") - regex=r'^[0-9]{1,3}$' + verbose_name = _("Default socket timeout") + help_text = _("Number between 0 and 999.") + regex = r'^[0-9]{1,3}$' group = AppOption.PHP class PHPDisplayErrors(AppOption): name = 'display_errors' - verbose_name=_("Display errors") - help_text=_("Determines whether errors should be printed to the screen as part of the output or " + 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).") - regex=r'^(On|Off|on|off)$' + regex = r'^(On|Off|on|off)$' group = AppOption.PHP class PHPExtension(AppOption): name = 'extension' - verbose_name=_("Extension") - regex=r'^[^ ]+$' + verbose_name = _("Extension") + regex = r'^[^ ]+$' group = AppOption.PHP class PHPMagicQuotesGPC(AppOption): 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) " + 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.") - regex=r'^(On|Off|on|off)$' + regex = r'^(On|Off|on|off)$' deprecated=5.3 group = AppOption.PHP class PHPMagicQuotesRuntime(AppOption): 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 " + 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.") - regex=r'^(On|Off|on|off)$' + regex = r'^(On|Off|on|off)$' deprecated=5.3 group = AppOption.PHP class PHPMaginQuotesSybase(AppOption): name = 'magic_quotes_sybase' - verbose_name=_("Magic quotes sybase") - help_text=_("Single-quote is escaped with a single-quote instead of a backslash (On or Off).") - regex=r'^(On|Off|on|off)$' + verbose_name = _("Magic quotes sybase") + help_text = _("Single-quote is escaped with a single-quote instead of a backslash (On or Off).") + regex = r'^(On|Off|on|off)$' group = AppOption.PHP class PHPMaxExecutonTime(AppOption): name = 'max_execution_time' - verbose_name=_("Max execution time") - help_text=_("Maximum time in seconds a script is allowed to run before it is terminated by " + verbose_name = _("Max execution time") + help_text = _("Maximum time in seconds a script is allowed to run before it is terminated by " "the parser (Integer between 0 and 999).") - regex=r'^[0-9]{1,3}$' + regex = r'^[0-9]{1,3}$' group = AppOption.PHP class PHPMaxInputTime(AppOption): name = 'max_input_time' - verbose_name=_("Max input time") - help_text=_("Maximum time in seconds a script is allowed to parse input data, like POST and GET " + verbose_name = _("Max input time") + help_text = _("Maximum time in seconds a script is allowed to parse input data, like POST and GET " "(Integer between 0 and 999).") - regex=r'^[0-9]{1,3}$' + regex = r'^[0-9]{1,3}$' group = AppOption.PHP class PHPMaxInputVars(AppOption): name = 'max_input_vars' - verbose_name=_("Max input vars") - help_text=_("How many input variables may be accepted (limit is applied to $_GET, $_POST " + verbose_name = _("Max input vars") + help_text = _("How many input variables may be accepted (limit is applied to $_GET, $_POST " "and $_COOKIE superglobal separately) (Integer between 0 and 9999).") - regex=r'^[0-9]{1,4}$' + regex = r'^[0-9]{1,4}$' group = AppOption.PHP class PHPMemoryLimit(AppOption): 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 " + 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).") - regex=r'^[0-9]{1,3}M$' + regex = r'^[0-9]{1,3}M$' group = AppOption.PHP class PHPMySQLConnectTimeout(AppOption): name = 'mysql.connect_timeout' - verbose_name=_("Mysql connect timeout") - help_text=_("Number between 0 and 999.") - regex=r'^([0-9]){1,3}$' + verbose_name = _("Mysql connect timeout") + help_text = _("Number between 0 and 999.") + regex = r'^([0-9]){1,3}$' group = AppOption.PHP class PHPOutputBuffering(AppOption): name = 'output_buffering' - verbose_name=_("Output buffering") - help_text=_("Turn on output buffering (On or Off).") - regex=r'^(On|Off|on|off)$' + verbose_name = _("Output buffering") + help_text = _("Turn on output buffering (On or Off).") + regex = r'^(On|Off|on|off)$' group = AppOption.PHP class PHPRegisterGlobals(AppOption): name = 'register_globals' - verbose_name=_("Register globals") - help_text=_("Whether or not to register the EGPCS (Environment, GET, POST, Cookie, Server) " + 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).") - regex=r'^(On|Off|on|off)$' + regex = r'^(On|Off|on|off)$' group = AppOption.PHP class PHPPostMaxSize(AppOption): name = 'post_max_size' - verbose_name=_("Post max size") - help_text=_("Sets max size of post data allowed (Value between 0M and 999M).") - regex=r'^[0-9]{1,3}M$' + verbose_name = _("Post max size") + help_text = _("Sets max size of post data allowed (Value between 0M and 999M).") + regex = r'^[0-9]{1,3}M$' group = AppOption.PHP class PHPSendmailPath(AppOption): name = 'sendmail_path' - verbose_name=_("sendmail_path") - help_text=_("Where the sendmail program can be found.") - regex=r'^[^ ]+$' + verbose_name = _("sendmail_path") + help_text = _("Where the sendmail program can be found.") + regex = r'^[^ ]+$' group = AppOption.PHP class PHPSessionBugCompatWarn(AppOption): name = 'session.bug_compat_warn' - verbose_name=_("session.bug_compat_warn") - help_text=_("Enables an PHP bug on session initialization for legacy behaviour (On or Off).") - regex=r'^(On|Off|on|off)$' + verbose_name = _("session.bug_compat_warn") + help_text = _("Enables an PHP bug on session initialization for legacy behaviour (On or Off).") + regex = r'^(On|Off|on|off)$' group = AppOption.PHP class PHPSessionAutoStart(AppOption): - name = 'session.auto_start', - verbose_name=_("session.auto_start") - help_text=_("Specifies whether the session module starts a session automatically on request " + 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)$' + regex = r'^(On|Off|on|off)$' group = AppOption.PHP class PHPSafeMode(AppOption): name = 'safe_mode' - verbose_name=_("Safe mode") - help_text=_("Whether to enable PHP's safe mode (On or Off) DEPRECATED as of PHP 5.3.0") - regex=r'^(On|Off|on|off)$' + verbose_name = _("Safe mode") + help_text = _("Whether to enable PHP's safe mode (On or Off) DEPRECATED as of PHP 5.3.0") + regex = r'^(On|Off|on|off)$' deprecated=5.3 group = AppOption.PHP class PHPSuhosinPostMaxVars(AppOption): - name = 'suhosin.post.max_vars', - verbose_name=_("Suhosin POST max vars") - help_text=_("Number between 0 and 9999.") - regex=r'^[0-9]{1,4}$' + name = 'suhosin.post.max_vars' + verbose_name = _("Suhosin POST max vars") + help_text = _("Number between 0 and 9999.") + regex = r'^[0-9]{1,4}$' group = AppOption.PHP class PHPSuhosinGetMaxVars(AppOption): name = 'suhosin.get.max_vars' - verbose_name=_("Suhosin GET max vars") - help_text=_("Number between 0 and 9999.") - regex=r'^[0-9]{1,4}$' + verbose_name = _("Suhosin GET max vars") + help_text = _("Number between 0 and 9999.") + regex = r'^[0-9]{1,4}$' group = AppOption.PHP class PHPSuhosinRequestMaxVars(AppOption): name = 'suhosin.request.max_vars' - verbose_name=_("Suhosin request max vars") - help_text=_("Number between 0 and 9999.") - regex=r'^[0-9]{1,4}$' + verbose_name = _("Suhosin request max vars") + help_text = _("Number between 0 and 9999.") + regex = r'^[0-9]{1,4}$' group = AppOption.PHP class PHPSuhosinSessionEncrypt(AppOption): name = 'suhosin.session.encrypt' - verbose_name=_("suhosin.session.encrypt") - help_text=_("On or Off") - regex=r'^(On|Off|on|off)$' + verbose_name = _("suhosin.session.encrypt") + help_text = _("On or Off") + regex = r'^(On|Off|on|off)$' group = AppOption.PHP class PHPSuhosinSimulation(AppOption): name = 'suhosin.simulation' - verbose_name=_("Suhosin simulation") - help_text=_("On or Off") - regex=r'^(On|Off|on|off)$' + verbose_name = _("Suhosin simulation") + help_text = _("On or Off") + regex = r'^(On|Off|on|off)$' group = AppOption.PHP class PHPSuhosinExecutorIncludeWhitelist(AppOption): name = 'suhosin.executor.include.whitelist' - verbose_name=_("suhosin.executor.include.whitelist") - regex=r'.*$' + verbose_name = _("suhosin.executor.include.whitelist") + regex = r'.*$' group = AppOption.PHP class PHPUploadMaxFileSize(AppOption): - name = 'upload_max_filesize', - verbose_name=_("upload_max_filesize") - help_text=_("Value between 0M and 999M.") - regex=r'^[0-9]{1,3}M$' + name = 'upload_max_filesize' + verbose_name = _("upload_max_filesize") + help_text = _("Value between 0M and 999M.") + regex = r'^[0-9]{1,3}M$' group = AppOption.PHP class PHPPostMaxSize(AppOption): name = 'post_max_size' - verbose_name=_("zend_extension") - regex=r'^[^ ]+$' + verbose_name = _("zend_extension") + regex = r'^[^ ]+$' group = AppOption.PHP diff --git a/orchestra/apps/webapps/settings.py b/orchestra/apps/webapps/settings.py index 3bb0d541..e52d1e65 100644 --- a/orchestra/apps/webapps/settings.py +++ b/orchestra/apps/webapps/settings.py @@ -2,28 +2,31 @@ from django.conf import settings from django.utils.translation import ugettext_lazy as _ -WEBAPPS_BASE_ROOT = getattr(settings, 'WEBAPPS_BASE_ROOT', '{home}/webapps/{app_name}/') - - +WEBAPPS_BASE_ROOT = getattr(settings, 'WEBAPPS_BASE_ROOT', '%(home)s/webapps/%(app_name)s/') WEBAPPS_FPM_LISTEN = getattr(settings, 'WEBAPPS_FPM_LISTEN', - # '127.0.0.1:9{app_id:03d} - '/opt/php/5.4/socks/{user}-{app_name}.sock' + # '127.0.0.1:9%(app_id)03d + '/opt/php/5.4/socks/%(user)s-%(app_name)s.sock' ) -WEBAPPS_FPM_START_PORT = getattr(settings, 'WEBAPPS_FPM_START_PORT', 10000) - - WEBAPPS_PHPFPM_POOL_PATH = getattr(settings, 'WEBAPPS_PHPFPM_POOL_PATH', - '/etc/php5/fpm/pool.d/{user}-{app_name}.conf') + '/etc/php5/fpm/pool.d/%(user)s-%(app_name)s.conf') WEBAPPS_FCGID_PATH = getattr(settings, 'WEBAPPS_FCGID_PATH', - '/home/httpd/fcgid/{user}/{app_name}-wrapper') + '/home/httpd/fcgi-bin.d/%(user)s/%(app_name)s-wrapper') +WEBAPPS_FCGID_CMD_OPTIONS_PATH = getattr(settings, 'WEBAPPS_FCGID_CMD_OPTIONS_PATH', + '/etc/apache2/fcgid-conf/%(user)s-%(app_name)s.conf') + + +WEBAPPS_PHP_ERROR_LOG_PATH = getattr(settings, 'WEBAPPS_PHP_ERROR_LOG_PATH', + '') + WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', ( 'orchestra.apps.webapps.types.PHP54App', + 'orchestra.apps.webapps.types.PHP53App', 'orchestra.apps.webapps.types.PHP52App', 'orchestra.apps.webapps.types.PHP4App', 'orchestra.apps.webapps.types.StaticApp', @@ -35,7 +38,10 @@ WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', ( 'orchestra.apps.webapps.types.WordPressApp', )) - +WEBAPPS_UNDER_CONSTRUCTION_PATH = getattr(settings, 'WEBAPPS_UNDER_CONSTRUCTION_PATH', + # Server-side path where a under construction stock page is + # '/var/www/undercontruction/index.html', + '') #WEBAPPS_TYPES_OVERRIDE = getattr(settings, 'WEBAPPS_TYPES_OVERRIDE', {}) #for webapp_type, value in WEBAPPS_TYPES_OVERRIDE.iteritems(): @@ -79,7 +85,6 @@ WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTIO WEBAPPS_ENABLED_OPTIONS = getattr(settings, 'WEBAPPS_ENABLED_OPTIONS', ( 'orchestra.apps.webapps.options.PublicRoot', - 'orchestra.apps.webapps.options.DirectoryProtection', 'orchestra.apps.webapps.options.Timeout', 'orchestra.apps.webapps.options.Processes', 'orchestra.apps.webapps.options.PHPEnabledFunctions', @@ -140,7 +145,7 @@ WEBAPPS_DOKUWIKIMU_LISTEN = getattr(settings, 'WEBAPPS_DOKUWIKIMU_LISTEN', WEBAPPS_DRUPALMU_SITES_PATH = getattr(settings, 'WEBAPPS_DRUPALMU_SITES_PATH', - '/home/httpd/htdocs/drupal-mu/sites/{site_name}') + '/home/httpd/htdocs/drupal-mu/sites/%(site_name)s') WEBAPPS_DRUPALMU_LISTEN = getattr(settings, 'WEBAPPS_DRUPALMU_LISTEN', '/opt/php/5.4/socks/drupal-mu.sock' diff --git a/orchestra/apps/webapps/types.py b/orchestra/apps/webapps/types.py index f02ffd70..d36c1443 100644 --- a/orchestra/apps/webapps/types.py +++ b/orchestra/apps/webapps/types.py @@ -1,3 +1,5 @@ +import os + from django import forms from django.core.exceptions import ValidationError from django.utils.safestring import mark_safe @@ -129,25 +131,31 @@ class PHPAppType(AppType): socket_type = 'unix' if ':' in self.fpm_listen: socket_type = 'tcp' - socket = self.fpm_listen.format(context) + socket = self.fpm_listen % context return ('fpm', socket_type, socket, webapp.get_path()) + def get_context(self, webapp): + """ context used to format settings """ + return { + 'home': webapp.account.main_systemuser.get_home(), + 'account': webapp.account.username, + 'user': webapp.account.username, + 'app_name': webapp.name, + } + def get_php_init_vars(self, webapp, per_account=False): """ process php options for inclusion on php.ini per_account=True merges all (account, webapp.type) options """ - init_vars = [] - php_options = type(self).get_php_options() + init_vars = {} options = webapp.options.all() if per_account: options = webapp.account.webapps.filter(webapp_type=webapp.type) - php_options = [option.name for option in php_options] + php_options = [option.name for option in type(self).get_php_options()] for opt in options: - if opt.option_class in php_options: - init_vars.append( - (opt.name, opt.value) - ) + if opt.name in php_options: + init_vars[opt.name] = opt.value enabled_functions = [] for value in options.filter(name='enabled_functions').values_list('value', flat=True): enabled_functions += enabled_functions.get().value.split(',') @@ -156,9 +164,10 @@ class PHPAppType(AppType): for function in settings.WEBAPPS_PHP_DISABLED_FUNCTIONS: if function not in enabled_functions: disabled_functions.append(function) - init_vars.append( - ('dissabled_functions', ','.join(disabled_functions)) - ) + init_vars['dissabled_functions'] = ','.join(disabled_functions) + if settings.WEBAPPS_PHP_ERROR_LOG_PATH and 'error_log' not in init_vars: + context = self.get_context(webapp) + init_vars['error_log'] = settings.WEBAPPS_PHP_ERROR_LOG_PATH % context return init_vars @@ -171,23 +180,37 @@ class PHP54App(PHPAppType): icon = 'orchestra/icons/apps/PHPFPM.png' -class PHP52App(PHPAppType): - name = 'php5.2-fcgid' - php_version = 5.2 - verbose_name = "PHP 5.2 FCGID" - help_text = _("This creates a PHP5.2 application under ~/webapps/<app_name>
" +class PHP53App(PHPAppType): + name = 'php5.3-fcgid' + php_version = 5.3 + php_binary = '/usr/bin/php5-cgi' + php_rc = '/etc/php5/cgi/' + verbose_name = "PHP 5.3 FCGID" + help_text = _("This creates a PHP5.3 application under ~/webapps/<app_name>
" "Apache-mod-fcgid will be used to execute PHP files.") icon = 'orchestra/icons/apps/PHPFCGI.png' def get_directive(self, webapp): context = self.get_directive_context(webapp) - wrapper_path = settings.WEBAPPS_FCGID_PATH.format(context) - return ('fcgi', webapp.get_path(), wrapper_path) + wrapper_path = settings.WEBAPPS_FCGID_PATH % context + return ('fcgid', webapp.get_path(), wrapper_path) -class PHP4App(PHP52App): +class PHP52App(PHP53App): + name = 'php5.2-fcgid' + php_version = 5.2 + php_binary = '/usr/bin/php5.2-cgi' + php_rc = '/etc/php5.2/cgi/' + verbose_name = "PHP 5.2 FCGID" + help_text = _("This creates a PHP5.2 application under ~/webapps/<app_name>
" + "Apache-mod-fcgid will be used to execute PHP files.") + icon = 'orchestra/icons/apps/PHPFCGI.png' + + +class PHP4App(PHP53App): name = 'php4-fcgid' php_version = 4 + php_binary = '/usr/bin/php4-cgi' verbose_name = "PHP 4 FCGID" help_text = _("This creates a PHP4 application under ~/webapps/<app_name>
" "Apache-mod-fcgid will be used to execute PHP files.") @@ -213,7 +236,7 @@ class WebalizerApp(AppType): option_groups = () def get_directive(self, webapp): - return ('static', webapp.get_path()) + return ('static', os.path.join(webapp.get_path(), '%(site_name)s/')) class WordPressMuApp(PHPAppType): diff --git a/orchestra/apps/websites/admin.py b/orchestra/apps/websites/admin.py index 6efec8d6..7529da91 100644 --- a/orchestra/apps/websites/admin.py +++ b/orchestra/apps/websites/admin.py @@ -35,7 +35,7 @@ class DirectiveInline(admin.TabularInline): if db_field.name == 'name': # Help text based on select widget kwargs['widget'] = DynamicHelpTextSelect( - 'this.id.replace("name", "value")', self.DIECTIVES_HELP_TEXT + 'this.id.replace("name", "value")', self.DIRECTIVES_HELP_TEXT ) return super(DirectiveInline, self).formfield_for_dbfield(db_field, **kwargs) @@ -71,7 +71,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): ) form = WebsiteAdminForm filter_by_account_fields = ['domains'] - list_prefetch_related = ('domains', 'contents__webapp') + list_prefetch_related = ('domains', 'content_set__webapp') search_fields = ('name', 'account__username', 'domains__name') def display_domains(self, website): @@ -86,7 +86,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): def display_webapps(self, website): webapps = [] - for content in website.contents.all(): + for content in website.content_set.all(): webapp = content.webapp url = change_url(webapp) name = "%s on %s" % (webapp.get_type_display(), content.path) diff --git a/orchestra/apps/websites/backends/apache.py b/orchestra/apps/websites/backends/apache.py index b3d487c8..2d66fe05 100644 --- a/orchestra/apps/websites/backends/apache.py +++ b/orchestra/apps/websites/backends/apache.py @@ -44,7 +44,7 @@ class Apache2Backend(ServiceController): """ )) apache_conf = apache_conf.render(Context(context)) - apache_conf += self.get_protections(site) +# apache_conf += self.get_protections(site) context['apache_conf'] = apache_conf self.append(textwrap.dedent("""\ @@ -64,21 +64,21 @@ class Apache2Backend(ServiceController): def commit(self): """ reload Apache2 if necessary """ - self.append('[[ $UPDATED == 1 ]] && service apache2 reload || true') + self.append('if [[ $UPDATED == 1 ]]; then service apache2 reload; fi') def get_content_directives(self, site): directives = '' - for content in site.contents.all().order_by('-path'): + for content in site.content_set.all().order_by('-path'): directive = content.webapp.get_directive() - method, agrs = directive[0], directive[1:] + method, args = directive[0], directive[1:] method = getattr(self, 'get_%s_directives' % method) directives += method(content, *args) return directives def get_static_directives(self, content, app_path): context = self.get_content_context(content) - context['app_path'] = app_path - return "Alias %(location)s %(path)s\n" % context + context['app_path'] = app_path % context + return "Alias %(location)s %(app_path)s\n" % context def get_fpm_directives(self, content, socket_type, socket, app_path): if socket_type == 'unix': @@ -95,29 +95,26 @@ class Apache2Backend(ServiceController): 'socket': socket, }) return textwrap.dedent("""\ - ProxyPassMatch ^%(location)s/(.*\.php(/.*)?)$ {target} - Alias %(location)s/ %(app_path)s/ + ProxyPassMatch ^%(location)s(.*\.php(/.*)?)$ {target} + Alias %(location)s %(app_path)s/ """.format(target=target) % context ) - def get_fcgi_directives(self, content, app_path, wrapper_path): + def get_fcgid_directives(self, content, app_path, wrapper_path): context = self.get_content_context(content) context.update({ 'app_path': app_path, 'wrapper_path': wrapper_path, }) - fcgid = textwrap.dedent("""\ + return textwrap.dedent("""\ Alias %(location)s %(app_path)s ProxyPass %(location)s ! Options +ExecCGI AddHandler fcgid-script .php - FcgidWrapper %(wrapper_path)s\ + FcgidWrapper %(wrapper_path)s + """) % context - for option in content.webapp.options.filter(name__startswith='Fcgid'): - fcgid += " %s %s\n" % (option.name, option.value) - fcgid += "\n" - return fcgid def get_ssl(self, site): cert = settings.WEBSITES_DEFAULT_HTTPS_CERT @@ -129,54 +126,53 @@ class Apache2Backend(ServiceController): SSLEngine on SSLCertificateFile %s SSLCertificateKeyFile %s\ - """ % cert - ) + """) % cert return directives def get_security(self, site): directives = '' - for rules in site.options.filter(name='sec_rule_remove'): + for rules in site.directives.filter(name='sec_rule_remove'): for rule in rules.value.split(): directives += "SecRuleRemoveById %i\n" % int(rule) - for modsecurity in site.options.filter(name='sec_rule_off'): + for modsecurity in site.directives.filter(name='sec_rule_off'): directives += textwrap.dedent("""\ SecRuleEngine Off \ - """ % modsecurity.value) + """) % modsecurity.value if directives: directives = '\n%s\n' % directives return directives def get_redirect(self, site): directives = '' - for redirect in site.options.filter(name='redirect'): + for redirect in site.directives.filter(name='redirect'): if re.match(r'^.*[\^\*\$\?\)]+.*$', redirect.value): directives += "RedirectMatch %s" % redirect.value else: directives += "Redirect %s" % redirect.value return directives - def get_protections(self, site): - protections = '' - context = self.get_context(site) - for protection in site.options.filter(name='directory_protection'): - path, name, passwd = protection.value.split() - path = os.path.join(context['root'], path) - passwd = os.path.join(self.USER_HOME % context, passwd) - protections += textwrap.dedent(""" - - AllowOverride All - #AuthPAM_Enabled off - AuthType Basic - AuthName %s - AuthUserFile %s - - require valid-user - - """ % (path, name, passwd) - ) - return protections +# def get_protections(self, site): +# protections = '' +# context = self.get_context(site) +# for protection in site.directives.filter(name='directory_protection'): +# path, name, passwd = protection.value.split() +# path = os.path.join(context['root'], path) +# passwd = os.path.join(self.USER_HOME % context, passwd) +# protections += textwrap.dedent(""" +# +# AllowOverride All +# #AuthPAM_Enabled off +# AuthType Basic +# AuthName %s +# AuthUserFile %s +# +# require valid-user +# +# """ % (path, name, passwd) +# ) +# return protections def enable_or_disable(self, site): context = self.get_context(site) @@ -184,27 +180,25 @@ class Apache2Backend(ServiceController): self.append(textwrap.dedent("""\ if [[ ! -f %(sites_enabled)s ]]; then a2ensite %(site_unique_name)s.conf - else - UPDATED=0 - fi""" % context - )) + UPDATED=1 + fi""") % context + ) else: self.append(textwrap.dedent("""\ if [[ -f %(sites_enabled)s ]]; then a2dissite %(site_unique_name)s.conf; - else - UPDATED=0 - fi""" % context - )) + UPDATED=1 + fi""") % context + ) def get_username(self, site): - option = site.options.filter(name='user_group').first() + option = site.directives.filter(name='user_group').first() if option: return option.value.split()[0] return site.account.username def get_groupname(self, site): - option = site.options.filter(name='user_group').first() + option = site.directives.filter(name='user_group').first() if option and ' ' in option.value: user, group = option.value.split() return group @@ -236,7 +230,6 @@ class Apache2Backend(ServiceController): 'location': content.path, 'app_name': content.webapp.name, 'app_path': content.webapp.get_path(), - 'fpm_port': content.webapp.get_fpm_port(), }) return context @@ -292,7 +285,7 @@ class Apache2Traffic(ServiceMonitor): print sum }' || [[ $? == 1 ]] && true } | xargs echo ${OBJECT_ID} - }""" % context)) + }""") % context) def monitor(self, site): context = self.get_context(site) diff --git a/orchestra/apps/websites/backends/webalizer.py b/orchestra/apps/websites/backends/webalizer.py index ef973082..db9ca4d2 100644 --- a/orchestra/apps/websites/backends/webalizer.py +++ b/orchestra/apps/websites/backends/webalizer.py @@ -20,18 +20,17 @@ class WebalizerBackend(ServiceController): echo 'Webstats are coming soon' > %(webalizer_path)s/index.html fi echo '%(webalizer_conf)s' > %(webalizer_conf_path)s - chown %(user)s:www-data %(webalizer_path)s""" % context - )) + chown %(user)s:www-data %(webalizer_path)s""") % context + ) def delete(self, content): context = self.get_context(content) - delete_webapp = not content.webapp.pk - # TODO remove when confirmed that it works, otherwise create a second WebalizerBackend for WebApps + delete_webapp = type(content.webapp).objects.filter(pk=content.webapp.pk).exists() if delete_webapp: self.append("mv %(webapp_path)s %(webapp_path)s.deleted" % context) - if delete_webapp or not content.webapp.contents.filter(website=content.website).exists(): - self.append("mv %(webalizer_path)s %(webalizer_path)s.deleted" % context) - self.append("rm %(webalizer_conf_path)s" % context) + if delete_webapp or not content.webapp.content_set.filter(website=content.website).exists(): + self.append("rm -fr %(webalizer_path)s" % context) + self.append("rm -f %(webalizer_conf_path)s" % context) def get_context(self, content): conf_file = "%s.conf" % content.website.unique_name @@ -88,6 +87,5 @@ class WebalizerBackend(ServiceController): SearchEngine mamma.com query= SearchEngine alltheweb.com query= - DumpSites yes""" % context - ) + DumpSites yes""") % context return context diff --git a/orchestra/apps/websites/directives.py b/orchestra/apps/websites/directives.py index 93169784..a665c26c 100644 --- a/orchestra/apps/websites/directives.py +++ b/orchestra/apps/websites/directives.py @@ -10,9 +10,9 @@ from . import settings # TODO multiple and unique validation support in the formset class SiteDirective(Plugin): - HTTPD = 'httpd' - SEC = 'sec' - SSL = 'ssl' + HTTPD = 'HTTPD' + SEC = 'ModSecurity' + SSL = 'SSL' help_text = "" unique = True diff --git a/orchestra/apps/websites/models.py b/orchestra/apps/websites/models.py index d05fc848..bd9c025f 100644 --- a/orchestra/apps/websites/models.py +++ b/orchestra/apps/websites/models.py @@ -66,7 +66,8 @@ class Website(models.Model): return { 'home': self.account.main_systemuser.get_home(), 'account': self.account.username, - 'name': self.name, + 'user': self.account.username, + 'site_name': self.name, 'unique_name': self.unique_name } @@ -105,10 +106,9 @@ class Directive(models.Model): class Content(models.Model): - webapp = models.ForeignKey('webapps.WebApp', verbose_name=_("web application"), - related_name='contents') - website = models.ForeignKey('websites.Website', verbose_name=_("web site"), - related_name='contents') + # related_name is content_set to differentiate between website.content -> webapp + webapp = models.ForeignKey('webapps.WebApp', verbose_name=_("web application")) + website = models.ForeignKey('websites.Website', verbose_name=_("web site")) path = models.CharField(_("path"), max_length=256, blank=True, validators=[validators.validate_url_path]) @@ -124,6 +124,8 @@ class Content(models.Model): def clean(self): if not self.path.startswith('/'): self.path = '/' + self.path + if not self.path.endswith('/'): + self.path = self.path + '/' def get_absolute_url(self): domain = self.website.domains.first() diff --git a/orchestra/apps/websites/serializers.py b/orchestra/apps/websites/serializers.py index 728d9484..87287133 100644 --- a/orchestra/apps/websites/serializers.py +++ b/orchestra/apps/websites/serializers.py @@ -43,7 +43,7 @@ class ContentSerializer(serializers.HyperlinkedModelSerializer): class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): domains = RelatedDomainSerializer(many=True, allow_add_remove=True, required=False) contents = ContentSerializer(required=False, many=True, allow_add_remove=True, - source='contents') + source='content_set') options = OptionField(required=False) class Meta: diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index 4c594510..5bf1cb87 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -36,7 +36,9 @@ MEDIA_URL = '/media/' ALLOWED_HOSTS = '*' # Set this to True to wrap each HTTP request in a transaction on this database. -ATOMIC_REQUESTS = True +# ATOMIC REQUESTS do not wrap middlewares (orchestra.apps.orchestration.middlewares.OperationsMiddleware) +ATOMIC_REQUESTS = False + MIDDLEWARE_CLASSES = ( 'django.middleware.gzip.GZipMiddleware', @@ -46,9 +48,12 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'orchestra.core.caches.RequestCacheMiddleware', + # ATOMIC REQUESTS do not wrap middlewares + 'orchestra.core.middlewares.TransactionMiddleware', 'orchestra.apps.orchestration.middlewares.OperationsMiddleware', # Uncomment the next line for simple clickjacking protection: # 'django.middleware.clickjacking.XFrameOptionsMiddleware', + ) diff --git a/orchestra/core/middlewares.py b/orchestra/core/middlewares.py new file mode 100644 index 00000000..fb6823ff --- /dev/null +++ b/orchestra/core/middlewares.py @@ -0,0 +1,47 @@ +from django.db import connection, transaction + + +class TransactionMiddleware(object): + """ + Transaction middleware. If this is enabled, each view function will be run + with commit_on_response activated - that way a save() doesn't do a direct + commit, the commit is done when a successful response is created. If an + exception happens, the database is rolled back. + """ + + def process_request(self, request): + """Enters transaction management""" + transaction.enter_transaction_management() + + def process_exception(self, request, exception): + """Rolls back the database and leaves transaction management""" + if transaction.is_dirty(): + # This rollback might fail because of network failure for example. + # If rollback isn't possible it is impossible to clean the + # connection's state. So leave the connection in dirty state and + # let request_finished signal deal with cleaning the connection. + transaction.rollback() + transaction.leave_transaction_management() + + def process_response(self, request, response): + """Commits and leaves transaction management.""" + if not transaction.get_autocommit(): + if transaction.is_dirty(): + # Note: it is possible that the commit fails. If the reason is + # closed connection or some similar reason, then there is + # little hope to proceed nicely. However, in some cases ( + # deferred foreign key checks for exampl) it is still possible + # to rollback(). + try: + transaction.commit() + except Exception: + # If the rollback fails, the transaction state will be + # messed up. It doesn't matter, the connection will be set + # to clean state after the request finishes. And, we can't + # clean the state here properly even if we wanted to, the + # connection is in transaction but we can't rollback... + transaction.rollback() + transaction.leave_transaction_management() + raise + transaction.leave_transaction_management() + return response