diff --git a/TODO.md b/TODO.md index 54d7a587..563b1bfb 100644 --- a/TODO.md +++ b/TODO.md @@ -206,3 +206,10 @@ ssh-copy-id root@ * symbolicLink webapp (link stuff from other places) +* logs on panle/logs/ ? mkdir ~webapps, backend post save signal? +* transaction abortion on backend.generation, transaction fault tolerant on backend.execute() +* and other IfModule on backend SecRule + + +* webalizer backend on webapps and check webapps.websites.all() +* monitor in batches doesnt work!!! diff --git a/orchestra/apps/accounts/settings.py b/orchestra/apps/accounts/settings.py index 7d840bb2..68de9753 100644 --- a/orchestra/apps/accounts/settings.py +++ b/orchestra/apps/accounts/settings.py @@ -42,7 +42,7 @@ ACCOUNTS_CREATE_RELATED = getattr(settings, 'ACCOUNTS_CREATE_RELATED', ( ('domains.Domain', 'name', { - 'name': '"%s.orchestra.lan" % account.username' + 'name': '"%s.orchestra.lan" % account.username.replace("_", "-")', }, _("Designates whether to creates a related subdomain <username>.orchestra.lan or not."), ), diff --git a/orchestra/apps/contacts/admin.py b/orchestra/apps/contacts/admin.py index 7bd1d9d2..b7dc5d5c 100644 --- a/orchestra/apps/contacts/admin.py +++ b/orchestra/apps/contacts/admin.py @@ -106,8 +106,7 @@ class ContactInline(admin.StackedInline): insertattr(AccountAdmin, 'inlines', ContactInline) search_fields = ( - 'contacts__short_name', 'contacts__full_name', 'contacts__phone', - 'contacts__phone2', 'contacts__email' + 'contacts__short_name', 'contacts__full_name', ) for field in search_fields: insertattr(AccountAdmin, 'search_fields', field) diff --git a/orchestra/apps/domains/backends.py b/orchestra/apps/domains/backends.py index 396a167f..48a9b044 100644 --- a/orchestra/apps/domains/backends.py +++ b/orchestra/apps/domains/backends.py @@ -33,7 +33,10 @@ class Bind9MasterDomainBackend(ServiceController): self.append(textwrap.dedent("""\ echo -e '%(zone)s' > %(zone_path)s.tmp diff -N -I'^\s*;;' %(zone_path)s %(zone_path)s.tmp || UPDATED=1 - mv %(zone_path)s.tmp %(zone_path)s""" % context + 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 )) self.update_conf(context) @@ -78,7 +81,7 @@ class Bind9MasterDomainBackend(ServiceController): 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_cls=backend, action=Operation.SAVE, instance=domain) + operation = Operation.create(backend, peration.SAVE, domain) servers = [] for server in router.get_servers(operation): servers.append(server.get_ip()) diff --git a/orchestra/apps/domains/models.py b/orchestra/apps/domains/models.py index 58e41f2e..8cb70acc 100644 --- a/orchestra/apps/domains/models.py +++ b/orchestra/apps/domains/models.py @@ -160,14 +160,14 @@ class Domain(models.Model): type=Record.SOA, value=' '.join(soa) )) - is_a = not types or Record.A in types or Record.AAAA in types - if Record.MX not in types and is_a: + is_host = self.is_top or not types or Record.A in types or Record.AAAA in types + if Record.MX not in types and is_host: for mx in settings.DOMAINS_DEFAULT_MX: records.append(AttrDict( type=Record.MX, value=mx )) - if (Record.A not in types and Record.AAAA not in types) and is_a: + if (Record.A not in types and Record.AAAA not in types) and is_host: records.append(AttrDict( type=Record.A, value=settings.DOMAINS_DEFAULT_A @@ -250,4 +250,5 @@ class Record(models.Model): def get_ttl(self): return self.ttl or settings.DOMAINS_DEFAULT_TTL + services.register(Domain) diff --git a/orchestra/apps/domains/settings.py b/orchestra/apps/domains/settings.py index b4dcf916..6293e6a4 100644 --- a/orchestra/apps/domains/settings.py +++ b/orchestra/apps/domains/settings.py @@ -34,7 +34,7 @@ DOMAINS_SLAVES_PATH = getattr(settings, 'DOMAINS_SLAVES_PATH', '/etc/bind/named. DOMAINS_CHECKZONE_BIN_PATH = getattr(settings, 'DOMAINS_CHECKZONE_BIN_PATH', - '/usr/sbin/named-checkzone -i local') + '/usr/sbin/named-checkzone -i local -k fail -n fail') DOMAINS_CHECKZONE_PATH = getattr(settings, 'DOMAINS_CHECKZONE_PATH', '/dev/shm') diff --git a/orchestra/apps/orchestration/manager.py b/orchestra/apps/orchestration/manager.py index c1605c83..2d1667f5 100644 --- a/orchestra/apps/orchestration/manager.py +++ b/orchestra/apps/orchestration/manager.py @@ -52,7 +52,7 @@ def execute(operations, async=False): for server in operation.servers: key = (server, operation.backend) if key not in scripts: - scripts[key] = (operation.backend, [operation]) + scripts[key] = (operation.backend(), [operation]) scripts[key][0].prepare() else: scripts[key][1].append(operation) diff --git a/orchestra/apps/orchestration/models.py b/orchestra/apps/orchestration/models.py index 788cb3cc..7c0d8b60 100644 --- a/orchestra/apps/orchestration/models.py +++ b/orchestra/apps/orchestration/models.py @@ -119,17 +119,17 @@ class BackendOperation(models.Model): def __hash__(self): """ set() """ - backend_cls = type(self.backend) - return hash(backend_cls) + hash(self.instance) + hash(self.action) + backend = getattr(self, 'backend', self.backend) + return hash(backend) + hash(self.instance) + hash(self.action) def __eq__(self, operation): """ set() """ return hash(self) == hash(operation) @classmethod - def create(cls, backend_cls, instance, action, servers=None): - op = cls(backend=backend_cls.get_name(), instance=instance, action=action) - op.backend = backend_cls() + def create(cls, backend, instance, action, servers=None): + op = cls(backend=backend.get_name(), instance=instance, action=action) + op.backend = backend # instance should maintain any dynamic attribute until backend execution # deep copy is prefered over copy otherwise objects will share same atributes (queryset cache) op.instance = copy.deepcopy(instance) @@ -154,7 +154,7 @@ class BackendOperation(models.Model): """ if self.action == self.DELETE: if hasattr(self.backend, 'get_context'): - self.backend.get_context(op.instance) + self.backend.get_context(self.instance) def backend_class(self): return ServiceBackend.get_backend(self.backend) @@ -194,7 +194,7 @@ class Route(models.Model): def get_servers(cls, operation, **kwargs): cache = kwargs.get('cache', {}) servers = [] - backend_cls = type(operation.backend) + backend_cls = operation.backend key = (backend_cls.get_name(), operation.action) try: routes = cache[key] diff --git a/orchestra/apps/systemusers/admin.py b/orchestra/apps/systemusers/admin.py index b71b98ea..61117c5b 100644 --- a/orchestra/apps/systemusers/admin.py +++ b/orchestra/apps/systemusers/admin.py @@ -42,7 +42,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende 'fields': ('shell', ('home', 'directory'), 'groups'), }), ) - search_fields = ['username'] + search_fields = ('username', 'account__username') readonly_fields = ('account_link',) change_readonly_fields = ('username',) filter_horizontal = ('groups',) diff --git a/orchestra/apps/systemusers/models.py b/orchestra/apps/systemusers/models.py index d7cace57..687651a2 100644 --- a/orchestra/apps/systemusers/models.py +++ b/orchestra/apps/systemusers/models.py @@ -26,7 +26,6 @@ class SystemUser(models.Model): Username max_length determined by LINUX system user lentgh: 32 """ - # TODO max_length username = models.CharField(_("username"), max_length=32, unique=True, help_text=_("Required. 64 characters or fewer. Letters, digits and ./-/_ only."), validators=[validators.validate_username]) diff --git a/orchestra/apps/webapps/admin.py b/orchestra/apps/webapps/admin.py index 78ef8806..3da35a84 100644 --- a/orchestra/apps/webapps/admin.py +++ b/orchestra/apps/webapps/admin.py @@ -9,8 +9,9 @@ from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.forms.widgets import DynamicHelpTextSelect from orchestra.plugins.admin import SelectPluginAdminMixin -from . import settings, options -from .applications import App +from . import settings +from .options import AppOption +from .types import AppType from .models import WebApp, WebAppOption @@ -19,7 +20,7 @@ class WebAppOptionInline(admin.TabularInline): extra = 1 OPTIONS_HELP_TEXT = { - op.name: str(unicode(op.help_text)) for op in options.get_enabled().values() + op.name: str(unicode(op.help_text)) for op in AppOption.get_plugins() } class Media: @@ -35,7 +36,7 @@ class WebAppOptionInline(admin.TabularInline): plugin = self.parent_object.type_class else: request = kwargs['request'] - plugin = App.get_plugin(request.GET['type']) + plugin = AppType.get_plugin(request.GET['type']) kwargs['choices'] = plugin.get_options_choices() # Help text based on select widget kwargs['widget'] = DynamicHelpTextSelect( @@ -52,8 +53,8 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) inlines = [WebAppOptionInline] readonly_fields = ('account_link',) change_readonly_fields = ('name', 'type') - list_prefetch_related = ('content_set__website',) - plugin = App + list_prefetch_related = ('contents__website',) + plugin = AppType plugin_field = 'type' plugin_title = _("Web application type") @@ -63,7 +64,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) def display_websites(self, webapp): websites = [] - for content in webapp.content_set.all(): + for content in webapp.contents.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 fb6e432b..81155bc9 100644 --- a/orchestra/apps/webapps/backends/__init__.py +++ b/orchestra/apps/webapps/backends/__init__.py @@ -1,44 +1,15 @@ import pkgutil import textwrap -from .. import settings - class WebAppServiceMixin(object): model = 'webapps.WebApp' directive = None - def valid_directive(self, webapp): - return settings.WEBAPPS_TYPES[webapp.type]['directive'][0] == self.directive - def create_webapp_dir(self, context): self.append("mkdir -p %(app_path)s" % context) self.append("chown %(user)s:%(group)s %(app_path)s" % context) - 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 = [] - options = webapp.options.all() - if per_account: - options = webapp.account.webapps.filter(webapp_type=webapp.type) - for opt in options: - name = opt.name.replace('PHP-', '') - value = "%s" % opt.value - init_vars.append((name, value)) - enabled_functions = [] - for value in options.filter(name='php-enabled_functions').values_list('value', flat=True): - enabled_functions += enabled_functions.get().value.split(',') - if enabled_functions: - disabled_functions = [] - 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))) - return init_vars - def delete_webapp_dir(self, context): self.append("rm -fr %(app_path)s" % context) diff --git a/orchestra/apps/webapps/backends/phpfpm.py b/orchestra/apps/webapps/backends/phpfpm.py index 6841140d..07f065c5 100644 --- a/orchestra/apps/webapps/backends/phpfpm.py +++ b/orchestra/apps/webapps/backends/phpfpm.py @@ -13,12 +13,9 @@ from .. import settings class PHPFPMBackend(WebAppServiceMixin, ServiceController): """ Per-webapp php application """ verbose_name = _("PHP-FPM") - directive = 'fpm' default_route_match = "webapp.type.endswith('-fpm')" def save(self, webapp): - if not self.valid_directive(webapp): - return context = self.get_context(webapp) self.create_webapp_dir(context) self.append(textwrap.dedent("""\ @@ -31,8 +28,6 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController): )) def delete(self, webapp): - if not self.valid_directive(webapp): - return context = self.get_context(webapp) self.append("rm '%(fpm_path)s'" % context) self.delete_webapp_dir(context) @@ -43,8 +38,8 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController): super(PHPFPMBackend, self).commit() self.append(textwrap.dedent(""" [[ $UPDATEDFPM == 1 ]] && { - service php5-fpm start service php5-fpm reload + service php5-fpm start }""")) def get_context(self, webapp): diff --git a/orchestra/apps/webapps/models.py b/orchestra/apps/webapps/models.py index 6c0dd08b..b503a5c7 100644 --- a/orchestra/apps/webapps/models.py +++ b/orchestra/apps/webapps/models.py @@ -2,6 +2,8 @@ import re from django.core.exceptions import ValidationError from django.db import models +from django.db.models.signals import pre_save, pre_delete +from django.dispatch import receiver from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from jsonfield import JSONField @@ -9,7 +11,7 @@ from jsonfield import JSONField from orchestra.core import validators, services from orchestra.utils.functional import cached -from . import settings, options +from . import settings from .types import AppType @@ -53,17 +55,11 @@ class WebApp(models.Model): opt.name: opt.value for opt in self.options.all() } - @property - def app_type(self): - return settings.WEBAPPS_TYPES[self.type] - def get_fpm_port(self): return settings.WEBAPPS_FPM_START_PORT + self.account_id def get_directive(self): - directive = self.app_type['directive'] - args = directive[1:] if len(directive) > 1 else () - return directive[0], args + return self.type_instance.get_directive(self) def get_path(self): context = { @@ -86,8 +82,7 @@ class WebApp(models.Model): class WebAppOption(models.Model): webapp = models.ForeignKey(WebApp, verbose_name=_("Web application"), related_name='options') - name = models.CharField(_("name"), max_length=128, - choices=((op.name, op.verbose_name) for op in options.get_enabled().values())) + name = models.CharField(_("name"), max_length=128, choices=AppType.get_options_choices()) value = models.CharField(_("value"), max_length=256) class Meta: @@ -98,18 +93,24 @@ class WebAppOption(models.Model): def __unicode__(self): return self.name + @cached_property + def option_class(self): + return SiteDirective.get_plugin(self.name) + + @cached_property + def option_instance(self): + """ Per request lived option instance """ + return self.option_class() + def clean(self): - option = options.get_enabled()[self.name] - option.validate(self) + self.option_instance.validate(self) services.register(WebApp) -# Admin bulk deletion doesn't call model.delete(), we use signals instead of model method overriding - -from django.db.models.signals import pre_save, pre_delete -from django.dispatch import receiver +# Admin bulk deletion doesn't call model.delete() +# So, signals are used instead of model method overriding @receiver(pre_save, sender=WebApp, dispatch_uid='webapps.type.save') def type_save(sender, *args, **kwargs): diff --git a/orchestra/apps/webapps/options.py b/orchestra/apps/webapps/options.py index b7d89d91..fa1854e6 100644 --- a/orchestra/apps/webapps/options.py +++ b/orchestra/apps/webapps/options.py @@ -1,300 +1,350 @@ from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ +from orchestra.plugins import Plugin +from orchestra.utils.functional import cached from orchestra.utils.python import import_class from . import settings -class AppOption(object): - def __init__(self, name, *args, **kwargs): - self.name = name - self.verbose_name = kwargs.pop('verbose_name', name) - self.help_text = kwargs.pop('help_text', '') - for k,v in kwargs.iteritems(): - setattr(self, k, v) +class AppOption(Plugin): + PHP = 'PHP' + PROCESS = 'Process' + FILESYSTEM = 'FileSystem' - def validate(self, webapp): - if self.regex and not re.match(self.regex, webapp.value): + help_text = "" + group = None + + @classmethod + @cached + def get_plugins(cls): + plugins = [] + for cls in settings.WEBAPPS_ENABLED_OPTIONS: + plugins.append(import_class(cls)) + return plugins + + @classmethod + @cached + def get_option_groups(cls): + groups = {} + for opt in cls.get_plugins(): + try: + groups[opt.group].append(opt) + except KeyError: + groups[opt.group] = [opt] + return groups + + def validate(self, option): + if self.regex and not re.match(self.regex, option.value): raise ValidationError({ 'value': ValidationError(_("'%(value)s' does not match %(regex)s."), params={ - 'value': webapp.value, + 'value': option.value, 'regex': self.regex }), }) -public_root = AppOption('public-root', - verbose_name=_("Public root"), - help_text=_("Document root relative to webapps/<webapp>/"), - regex=r'[^ ]+' -) +class PublicRoot(AppOption): + name = 'public-root' + verbose_name = _("Public root") + help_text = _("Document root relative to webapps/<webapp>/") + regex = r'[^ ]+' + group = AppOption.FILESYSTEM -timeout = AppOption('timeout', + +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 # 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)."), - regex=r'^[0-9]{1,3}$', -) + verbose_name = _("Process timeout") + help_text = _("Maximum time in seconds allowed for a request to complete (a number between 0 and 999).") + regex = r'^[0-9]{1,3}$' + group = AppOption.PROCESS -processes = AppOption('processes', + +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 -php_enabled_functions = AppOption('php-enabled_functions', - verbose_name=_("Enabled functions"), - help_text = ' '.join(settings.WEBAPPS_PHP_DISABLED_FUNCTIONS), + +class PHPEnabledFunctions(AppOption): + name = 'enabled_functions' + verbose_name=_("Enabled functions") + help_text = ' '.join(settings.WEBAPPS_PHP_DISABLED_FUNCTIONS) regex=r'^[\w\.,-]+$' -) + group = AppOption.PHP -php_allow_url_include = AppOption('PHP-allow_url_include', - verbose_name=_("Allow URL include"), + +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, " - "require_once (On or Off)."), + "require_once (On or Off).") regex=r'^(On|Off|on|off)$' -) + group = AppOption.PHP -php_allow_url_fopen = AppOption('PHP-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)."), + +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)$' -) + group = AppOption.PHP -php_auto_append_file = AppOption('PHP-auto_append_file', - verbose_name=_("Auto append file"), - help_text=_("Specifies the name of a file that is automatically parsed after the main file."), + +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\.,-/]+$' -) + group = AppOption.PHP -php_auto_prepend_file = AppOption('PHP-auto_prepend_file', - verbose_name=_("Auto prepend file"), - help_text=_("Specifies the name of a file that is automatically parsed before the main file."), + +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\.,-/]+$' -) + group = AppOption.PHP -php_date_timezone = AppOption('PHP-date.timezone', - verbose_name=_("date.timezone"), - help_text=_("Sets the default timezone used by all date/time functions (Timezone string 'Europe/London')."), + +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+$' -) + group = AppOption.PHP -php_default_socket_timeout = AppOption('PHP-default_socket_timeout', - verbose_name=_("Default socket timeout"), - help_text=_("Number between 0 and 999."), + +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}$' -) + group = AppOption.PHP -php_display_errors = AppOption('PHP-display_errors', - verbose_name=_("Display errors"), + +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 " - "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)$' -) + group = AppOption.PHP -php_extension = AppOption('PHP-extension', - verbose_name=_("Extension"), + +class PHPExtension(AppOption): + name = 'extension' + verbose_name=_("Extension") regex=r'^[^ ]+$' -) + group = AppOption.PHP -php_magic_quotes_gpc = AppOption('PHP-magic_quotes_gpc', - verbose_name=_("Magic quotes GPC"), + +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) " - "DEPRECATED as of PHP 5.3.0."), - regex=r'^(On|Off|on|off)$', + "DEPRECATED as of PHP 5.3.0.") + regex=r'^(On|Off|on|off)$' deprecated=5.3 -) + group = AppOption.PHP -php_magic_quotes_runtime = AppOption('PHP-magic_quotes_runtime', - verbose_name=_("Magic quotes runtime"), + +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 " - "with a backslash (On or Off) DEPRECATED as of PHP 5.3.0."), - regex=r'^(On|Off|on|off)$', - deprecated=5.3 -) - -php_magic_quotes_sybase = AppOption('PHP-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)."), + "with a backslash (On or Off) DEPRECATED as of PHP 5.3.0.") regex=r'^(On|Off|on|off)$' -) + deprecated=5.3 + group = AppOption.PHP -php_max_execution_time = AppOption('PHP-max_execution_time', - verbose_name=_("Max execution time"), + +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)$' + 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 " - "the parser (Integer between 0 and 999)."), + "the parser (Integer between 0 and 999).") regex=r'^[0-9]{1,3}$' -) + group = AppOption.PHP -php_max_input_time = AppOption('PHP-max_input_time', - verbose_name=_("Max input time"), + +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 " - "(Integer between 0 and 999)."), + "(Integer between 0 and 999).") regex=r'^[0-9]{1,3}$' -) + group = AppOption.PHP -php_max_input_vars = AppOption('PHP-max_input_vars', - verbose_name=_("Max input vars"), + +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 " - "and $_COOKIE superglobal separately) (Integer between 0 and 9999)."), + "and $_COOKIE superglobal separately) (Integer between 0 and 9999).") regex=r'^[0-9]{1,4}$' -) + group = AppOption.PHP -php_memory_limit = AppOption('PHP-memory_limit', - verbose_name=_("Memory limit"), + +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 " - "(Value between 0M and 999M)."), + "(Value between 0M and 999M).") regex=r'^[0-9]{1,3}M$' -) + group = AppOption.PHP -php_mysql_connect_timeout = AppOption('PHP-mysql.connect_timeout', - verbose_name=_("Mysql connect timeout"), - help_text=_("Number between 0 and 999."), + +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}$' -) + group = AppOption.PHP -php_output_buffering = AppOption('PHP-output_buffering', - verbose_name=_("Output buffering"), - help_text=_("Turn on output buffering (On or Off)."), + +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)$' -) + group = AppOption.PHP -php_register_globals = AppOption('PHP-register_globals', - verbose_name=_("Register globals"), + +class PHPRegisterGlobals(AppOption): + 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)$' -) + group = AppOption.PHP -php_post_max_size = AppOption('PHP-post_max_size', - verbose_name=_("Post max size"), - help_text=_("Sets max size of post data allowed (Value between 0M and 999M)."), + +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$' -) + group = AppOption.PHP -php_sendmail_path = AppOption('PHP-sendmail_path', - verbose_name=_("sendmail_path"), - help_text=_("Where the sendmail program can be found."), + +class PHPSendmailPath(AppOption): + name = 'sendmail_path' + verbose_name=_("sendmail_path") + help_text=_("Where the sendmail program can be found.") regex=r'^[^ ]+$' -) + group = AppOption.PHP -php_session_bug_compat_warn = AppOption('PHP-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)."), + +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)$' -) + group = AppOption.PHP -php_session_auto_start = AppOption('PHP-session.auto_start', - verbose_name=_("session.auto_start"), + +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 " - "startup (On or Off)."), + "startup (On or 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)$' -) -php_safe_mode = AppOption('PHP-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)$', deprecated=5.3 -) -php_suhosin_post_max_vars = AppOption('PHP-suhosin.post.max_vars', - verbose_name=_("Suhosin POST max vars"), - help_text=_("Number between 0 and 9999."), + 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}$' -) -php_suhosin_get_max_vars = AppOption('PHP-suhosin.get.max_vars', - verbose_name=_("Suhosin GET max vars"), - help_text=_("Number between 0 and 9999."), + 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}$' -) -php_suhosin_request_max_vars = AppOption('PHP-suhosin.request.max_vars', - verbose_name=_("Suhosin request max vars"), - help_text=_("Number between 0 and 9999."), + 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}$' -) -php_suhosin_session_encrypt = AppOption('PHP-suhosin.session.encrypt', - verbose_name=_("suhosin.session.encrypt"), - help_text=_("On or Off"), + 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)$' -) -php_suhosin_simulation = AppOption('PHP-suhosin.simulation', - verbose_name=_("Suhosin simulation"), - help_text=_("On or 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)$' -) -php_suhosin_executor_include_whitelist = AppOption('PHP-suhosin.executor.include.whitelist', - verbose_name=_("suhosin.executor.include.whitelist"), + group = AppOption.PHP + + +class PHPSuhosinExecutorIncludeWhitelist(AppOption): + name = 'suhosin.executor.include.whitelist' + verbose_name=_("suhosin.executor.include.whitelist") regex=r'.*$' -) -php_upload_max_filesize = AppOption('PHP-upload_max_filesize', - verbose_name=_("upload_max_filesize"), - help_text=_("Value between 0M and 999M."), + 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$' -) -php_zend_extension = AppOption('PHP-post_max_size', - verbose_name=_("zend_extension"), + group = AppOption.PHP + + +class PHPPostMaxSize(AppOption): + name = 'post_max_size' + verbose_name=_("zend_extension") regex=r'^[^ ]+$' -) - - -filesystem = [ - public_root, -] - -process = [ - timeout, - processes, -] - -php = [ - php_enabled_functions, - php_allow_url_include, - php_allow_url_fopen, - php_auto_append_file, - php_auto_prepend_file, - php_date_timezone, - php_default_socket_timeout, - php_display_errors, - php_extension, - php_magic_quotes_gpc, - php_magic_quotes_runtime, - php_magic_quotes_sybase, - php_max_execution_time, - php_max_input_time, - php_max_input_vars, - php_memory_limit, - php_mysql_connect_timeout, - php_output_buffering, - php_register_globals, - php_post_max_size, - php_sendmail_path, - php_session_bug_compat_warn, - php_session_auto_start, - php_safe_mode, - php_suhosin_post_max_vars, - php_suhosin_get_max_vars, - php_suhosin_request_max_vars, - php_suhosin_session_encrypt, - php_suhosin_simulation, - php_suhosin_executor_include_whitelist, - php_upload_max_filesize, - php_zend_extension, -] - - -_enabled = None - -def get_enabled(): - global _enabled - if _enabled is None: - from . import settings - _enabled = {} - for op in settings.WEBAPPS_ENABLED_OPTIONS: - op = import_class(op) - _enabled[op.name] = op - return _enabled + group = AppOption.PHP diff --git a/orchestra/apps/webapps/settings.py b/orchestra/apps/webapps/settings.py index bfb109aa..3bb0d541 100644 --- a/orchestra/apps/webapps/settings.py +++ b/orchestra/apps/webapps/settings.py @@ -2,29 +2,30 @@ from django.conf import settings from django.utils.translation import ugettext_lazy as _ -WEBAPPS_BASE_ROOT = getattr(settings, 'WEBAPPS_BASE_ROOT', '%(home)s/webapps/%(app_name)s/') +WEBAPPS_BASE_ROOT = getattr(settings, 'WEBAPPS_BASE_ROOT', '{home}/webapps/{app_name}/') + WEBAPPS_FPM_LISTEN = getattr(settings, 'WEBAPPS_FPM_LISTEN', -# '/var/run/%(user)s-%(app_name)s.sock') - '127.0.0.1:%(fpm_port)s') - + # '127.0.0.1:9{app_id:03d} + '/opt/php/5.4/socks/{user}-{app_name}.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)s-%(app_name)s.conf') + '/etc/php5/fpm/pool.d/{user}-{app_name}.conf') WEBAPPS_FCGID_PATH = getattr(settings, 'WEBAPPS_FCGID_PATH', - '/home/httpd/fcgid/%(user)s/%(app_name)s-wrapper') + '/home/httpd/fcgid/{user}/{app_name}-wrapper') WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', ( - 'orchestra.apps.webapps.types.Php55App', - 'orchestra.apps.webapps.types.Php52App', - 'orchestra.apps.webapps.types.Php4App', + 'orchestra.apps.webapps.types.PHP54App', + 'orchestra.apps.webapps.types.PHP52App', + 'orchestra.apps.webapps.types.PHP4App', 'orchestra.apps.webapps.types.StaticApp', 'orchestra.apps.webapps.types.WebalizerApp', 'orchestra.apps.webapps.types.WordPressMuApp', @@ -35,6 +36,7 @@ WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', ( )) + #WEBAPPS_TYPES_OVERRIDE = getattr(settings, 'WEBAPPS_TYPES_OVERRIDE', {}) #for webapp_type, value in WEBAPPS_TYPES_OVERRIDE.iteritems(): # if value is None: @@ -76,50 +78,55 @@ WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTIO WEBAPPS_ENABLED_OPTIONS = getattr(settings, 'WEBAPPS_ENABLED_OPTIONS', ( - 'orchestra.apps.webapps.options.public_root', - 'orchestra.apps.webapps.options.timeout', - 'orchestra.apps.webapps.options.processes', - 'orchestra.apps.webapps.options.php_enabled_functions', - 'orchestra.apps.webapps.options.php_allow_url_include', - 'orchestra.apps.webapps.options.php_allow_url_fopen', - 'orchestra.apps.webapps.options.php_auto_append_file', - 'orchestra.apps.webapps.options.php_auto_prepend_file', - 'orchestra.apps.webapps.options.php_date_timezone', - 'orchestra.apps.webapps.options.php_default_socket_timeout', - 'orchestra.apps.webapps.options.php_display_errors', - 'orchestra.apps.webapps.options.php_extension', - 'orchestra.apps.webapps.options.php_magic_quotes_gpc', - 'orchestra.apps.webapps.options.php_magic_quotes_runtime', - 'orchestra.apps.webapps.options.php_magic_quotes_sybase', - 'orchestra.apps.webapps.options.php_max_execution_time', - 'orchestra.apps.webapps.options.php_max_input_time', - 'orchestra.apps.webapps.options.php_max_input_vars', - 'orchestra.apps.webapps.options.php_memory_limit', - 'orchestra.apps.webapps.options.php_mysql_connect_timeout', - 'orchestra.apps.webapps.options.php_output_buffering', - 'orchestra.apps.webapps.options.php_register_globals', - 'orchestra.apps.webapps.options.php_post_max_size', - 'orchestra.apps.webapps.options.php_sendmail_path', - 'orchestra.apps.webapps.options.php_session_bug_compat_warn', - 'orchestra.apps.webapps.options.php_session_auto_start', - 'orchestra.apps.webapps.options.php_safe_mode', - 'orchestra.apps.webapps.options.php_suhosin_post_max_vars', - 'orchestra.apps.webapps.options.php_suhosin_get_max_vars', - 'orchestra.apps.webapps.options.php_suhosin_request_max_vars', - 'orchestra.apps.webapps.options.php_suhosin_session_encrypt', - 'orchestra.apps.webapps.options.php_suhosin_simulation', - 'orchestra.apps.webapps.options.php_suhosin_executor_include_whitelist', - 'orchestra.apps.webapps.options.php_upload_max_filesize', - 'orchestra.apps.webapps.options.php_zend_extension', + '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', + 'orchestra.apps.webapps.options.PHPAllowURLInclude', + 'orchestra.apps.webapps.options.PHPAllowURLFopen', + 'orchestra.apps.webapps.options.PHPAutoAppendFile', + 'orchestra.apps.webapps.options.PHPAutoPrependFile', + 'orchestra.apps.webapps.options.PHPDateTimeZone', + 'orchestra.apps.webapps.options.PHPDefaultSocketTimeout', + 'orchestra.apps.webapps.options.PHPDisplayErrors', + 'orchestra.apps.webapps.options.PHPExtension', + 'orchestra.apps.webapps.options.PHPMagicQuotesGPC', + 'orchestra.apps.webapps.options.PHPMagicQuotesRuntime', + 'orchestra.apps.webapps.options.PHPMaginQuotesSybase', + 'orchestra.apps.webapps.options.PHPMaxExecutonTime', + 'orchestra.apps.webapps.options.PHPMaxInputTime', + 'orchestra.apps.webapps.options.PHPMaxInputVars', + 'orchestra.apps.webapps.options.PHPMemoryLimit', + 'orchestra.apps.webapps.options.PHPMySQLConnectTimeout', + 'orchestra.apps.webapps.options.PHPOutputBuffering', + 'orchestra.apps.webapps.options.PHPRegisterGlobals', + 'orchestra.apps.webapps.options.PHPPostMaxSize', + 'orchestra.apps.webapps.options.PHPSendmailPath', + 'orchestra.apps.webapps.options.PHPSessionBugCompatWarn', + 'orchestra.apps.webapps.options.PHPSessionAutoStart', + 'orchestra.apps.webapps.options.PHPSafeMode', + 'orchestra.apps.webapps.options.PHPSuhosinPostMaxVars', + 'orchestra.apps.webapps.options.PHPSuhosinGetMaxVars', + 'orchestra.apps.webapps.options.PHPSuhosinRequestMaxVars', + 'orchestra.apps.webapps.options.PHPSuhosinSessionEncrypt', + 'orchestra.apps.webapps.options.PHPSuhosinSimulation', + 'orchestra.apps.webapps.options.PHPSuhosinExecutorIncludeWhitelist', + 'orchestra.apps.webapps.options.PHPUploadMaxFileSize', + 'orchestra.apps.webapps.options.PHPPostMaxSize', )) WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD = getattr(settings, 'WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD', 'secret') -WEBAPPS_WORDPRESSMU_BASE_URL = getattr(settings, 'WEBAPPS_WORDPRESSMU_BASE_URL', +WEBAPPS_WORDPRESSMU_BASE_URL = getattr(settings, 'WEBAPPS_WORDPRESSMU_BASE_URL', 'http://blogs.orchestra.lan/') +WEBAPPS_WORDPRESSMU_LISTEN = getattr(settings, 'WEBAPPS_WORDPRESSMU_LISTEN', + '/opt/php/5.4/socks/wordpress-mu.sock' +) + WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH = getattr(settings, 'WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH', '/home/httpd/htdocs/wikifarm/template.tar.gz') @@ -127,9 +134,22 @@ WEBAPPS_DOKUWIKIMU_TEMPLATE_PATH = getattr(settings, 'WEBAPPS_DOKUWIKIMU_TEMPLAT WEBAPPS_DOKUWIKIMU_FARM_PATH = getattr(settings, 'WEBAPPS_DOKUWIKIMU_FARM_PATH', '/home/httpd/htdocs/wikifarm/farm') +WEBAPPS_DOKUWIKIMU_LISTEN = getattr(settings, 'WEBAPPS_DOKUWIKIMU_LISTEN', + '/opt/php/5.4/socks/dokuwiki-mu.sock' +) -WEBAPPS_DRUPAL_SITES_PATH = getattr(settings, 'WEBAPPS_DRUPAL_SITES_PATH', - '/home/httpd/htdocs/drupal-mu/sites/%(site_name)s') + +WEBAPPS_DRUPALMU_SITES_PATH = getattr(settings, 'WEBAPPS_DRUPALMU_SITES_PATH', + '/home/httpd/htdocs/drupal-mu/sites/{site_name}') + +WEBAPPS_DRUPALMU_LISTEN = getattr(settings, 'WEBAPPS_DRUPALMU_LISTEN', + '/opt/php/5.4/socks/drupal-mu.sock' +) + + +WEBAPPS_MOODLEMU_LISTEN = getattr(settings, 'WEBAPPS_MOODLEMU_LISTEN', + '/opt/php/5.4/socks/moodle-mu.sock' +) WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST = getattr(settings, 'WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST', diff --git a/orchestra/apps/webapps/types.py b/orchestra/apps/webapps/types.py index 87815836..f02ffd70 100644 --- a/orchestra/apps/webapps/types.py +++ b/orchestra/apps/webapps/types.py @@ -11,6 +11,7 @@ from orchestra.utils.functional import cached from orchestra.utils.python import import_class from . import options, settings +from .options import AppOption class AppType(plugins.Plugin): @@ -22,11 +23,7 @@ class AppType(plugins.Plugin): serializer = None icon = 'orchestra/icons/apps.png' unique_name = False - options = ( - ('Process', options.process), - ('PHP', options.php), - ('File system', options.filesystem), - ) + option_groups = (AppOption.FILESYSTEM, AppOption.PROCESS, AppOption.PHP) @classmethod @cached @@ -36,16 +33,18 @@ class AppType(plugins.Plugin): plugins.append(import_class(cls)) return plugins - @classmethod - def clean_data(cls, webapp): + def clean_data(self, webapp): """ model clean, uses cls.serizlier by default """ - if cls.serializer: - serializer = cls.serializer(data=webapp.data) + if self.serializer: + serializer = self.serializer(data=webapp.data) if not serializer.is_valid(): raise ValidationError(serializer.errors) return serializer.data return {} + def get_directive(self, webapp): + return ('static', webapp.get_path()) + def get_form(self): self.form.plugin = self self.form.plugin_field = 'type' @@ -69,19 +68,40 @@ class AppType(plugins.Plugin): 'name': _("A WordPress blog with this name already exists."), }) - def get_options(self): - pass + @classmethod + @cached + def get_php_options(cls): + php_version = getattr(cls, 'php_version', 1) + php_options = AppOption.get_option_groups()[AppOption.PHP] + return [op for op in php_options if getattr(cls, 'deprecated', 99) > php_version] + + @classmethod + @cached + def get_options(cls): + """ Get enabled options based on cls.option_groups """ + groups = AppOption.get_option_groups() + options = [] + for group in cls.option_groups: + group_options = groups[group] + if group == AppOption.PHP: + group_options = cls.get_php_options() + if group is None: + options.insert(0, (group, group_options)) + else: + options.append((group, group_options)) + return options @classmethod def get_options_choices(cls): - enabled = options.get_enabled().values() + """ Generates grouped choices ready to use in Field.choices """ + # generators can not be @cached yield (None, '-------') - for option in cls.options: - if hasattr(option, '__iter__'): - yield (option[0], [(op.name, op.verbose_name) for op in option[1] if op in enabled]) - elif option in enabled: - yield (option.name, option.verbose_name) - + for group, options in cls.get_options(): + if group is None: + for option in options: + yield (option.name, option.verbose_name) + else: + yield (group, [(op.name, op.verbose_name) for op in options]) def save(self, instance): pass @@ -91,36 +111,84 @@ class AppType(plugins.Plugin): def get_related_objects(self, instance): pass + + def get_directive_context(self, webapp): + return { + 'app_id': webapp.id, + 'app_name': webapp.name, + 'user': webapp.account.username, + } -class Php55App(AppType): - name = 'php5.5-fpm' - verbose_name = "PHP 5.5 FPM" -# 'fpm', ('unix:/var/run/%(user)s-%(app_name)s.sock|fcgi://127.0.0.1%(app_path)s',), - directive = ('fpm', 'fcgi://{}%(app_path)s'.format(settings.WEBAPPS_FPM_LISTEN)) +class PHPAppType(AppType): + php_version = 5.4 + fpm_listen = settings.WEBAPPS_FPM_LISTEN + + def get_directive(self, webapp): + context = self.get_directive_context(webapp) + socket_type = 'unix' + if ':' in self.fpm_listen: + socket_type = 'tcp' + socket = self.fpm_listen.format(context) + return ('fpm', socket_type, socket, webapp.get_path()) + + 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() + 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] + for opt in options: + if opt.option_class in php_options: + init_vars.append( + (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(',') + if enabled_functions: + disabled_functions = [] + 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)) + ) + return init_vars + + +class PHP54App(PHPAppType): + name = 'php5.4-fpm' + php_version = 5.4 + verbose_name = "PHP 5.4 FPM" help_text = _("This creates a PHP5.5 application under ~/webapps/<app_name>
" "PHP-FPM will be used to execute PHP files.") - options = ( - ('Process', options.process), - ('PHP', [op for op in options.php if getattr(op, 'deprecated', 99) > 5.5]), - ('File system', options.filesystem), - ) icon = 'orchestra/icons/apps/PHPFPM.png' -class Php52App(AppType): - name = 'php5.2-fcgi' - verbose_name = "PHP 5.2 FCGI" - directive = ('fcgi', settings.WEBAPPS_FCGID_PATH) +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>
" "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) -class Php4App(AppType): - name = 'php4-fcgi' - verbose_name = "PHP 4 FCGI" - directive = ('fcgi', settings.WEBAPPS_FCGID_PATH) +class PHP4App(PHP52App): + name = 'php4-fcgid' + php_version = 4 + 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.") icon = 'orchestra/icons/apps/PHPFCGI.png' @@ -129,13 +197,11 @@ class Php4App(AppType): class StaticApp(AppType): name = 'static' verbose_name = "Static" - directive = ('static',) help_text = _("This creates a Static application under ~/webapps/<app_name>
" "Apache2 will be used to serve static content and execute CGI files.") icon = 'orchestra/icons/apps/Static.png' - options = ( - ('File system', options.filesystem), - ) + option_groups = (AppOption.FILESYSTEM,) + class WebalizerApp(AppType): name = 'webalizer' @@ -144,10 +210,13 @@ class WebalizerApp(AppType): help_text = _("This creates a Webalizer application under " "~/webapps/<app_name>-<site_name>") icon = 'orchestra/icons/apps/Stats.png' - options = () + option_groups = () + + def get_directive(self, webapp): + return ('static', webapp.get_path()) -class WordPressMuApp(AppType): +class WordPressMuApp(PHPAppType): name = 'wordpress-mu' verbose_name = "WordPress (SaaS)" directive = ('fpm', 'fcgi://127.0.0.1:8990/home/httpd/wordpress-mu/') @@ -155,10 +224,11 @@ class WordPressMuApp(AppType): "By default this blog is accessible via <app_name>.blogs.orchestra.lan") icon = 'orchestra/icons/apps/WordPressMu.png' unique_name = True - options = () + option_groups = () + fpm_listen = settings.WEBAPPS_WORDPRESSMU_LISTEN -class DokuWikiMuApp(AppType): +class DokuWikiMuApp(PHPAppType): name = 'dokuwiki-mu' verbose_name = "DokuWiki (SaaS)" directive = ('alias', '/home/httpd/wikifarm/farm/') @@ -166,10 +236,11 @@ class DokuWikiMuApp(AppType): "By default this wiki is accessible via <app_name>.wikis.orchestra.lan") icon = 'orchestra/icons/apps/DokuWikiMu.png' unique_name = True - options = () + option_groups = () + fpm_listen = settings.WEBAPPS_DOKUWIKIMU_LISTEN -class MoodleMuApp(AppType): +class MoodleMuApp(PHPAppType): name = 'moodle-mu' verbose_name = "Moodle (SaaS)" directive = ('alias', '/home/httpd/wikifarm/farm/') @@ -177,10 +248,11 @@ class MoodleMuApp(AppType): "By default this wiki is accessible via <app_name>.moodle.orchestra.lan") icon = 'orchestra/icons/apps/MoodleMu.png' unique_name = True - options = () + option_groups = () + fpm_listen = settings.WEBAPPS_MOODLEMU_LISTEN -class DrupalMuApp(AppType): +class DrupalMuApp(PHPAppType): name = 'drupal-mu' verbose_name = "Drupdal (SaaS)" directive = ('fpm', 'fcgi://127.0.0.1:8991/home/httpd/drupal-mu/') @@ -190,7 +262,8 @@ class DrupalMuApp(AppType): "By default this site will be accessible via <app_name>.drupal.orchestra.lan") icon = 'orchestra/icons/apps/DrupalMu.png' unique_name = True - options = () + option_groups = () + fpm_listen = settings.WEBAPPS_DRUPALMU_LISTEN from rest_framework import serializers @@ -204,7 +277,7 @@ class SymbolicLinkSerializer(serializers.Serializer): path = serializers.CharField(label=_("Path")) -class SymbolicLinkApp(AppType): +class SymbolicLinkApp(PHPAppType): name = 'symbolic-link' verbose_name = "Symbolic link" form = SymbolicLinkForm @@ -231,7 +304,7 @@ from orchestra.apps.databases.models import Database, DatabaseUser from orchestra.utils.python import random_ascii -class WordPressApp(AppType): +class WordPressApp(PHPAppType): name = 'wordpress' verbose_name = "WordPress" icon = 'orchestra/icons/apps/WordPress.png' diff --git a/orchestra/apps/websites/admin.py b/orchestra/apps/websites/admin.py index 71c4242c..6efec8d6 100644 --- a/orchestra/apps/websites/admin.py +++ b/orchestra/apps/websites/admin.py @@ -10,17 +10,18 @@ from orchestra.admin.utils import admin_link, change_url from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin from orchestra.forms.widgets import DynamicHelpTextSelect -from . import settings, options +from . import settings +from .directives import SiteDirective from .forms import WebsiteAdminForm -from .models import Content, Website, WebsiteOption +from .models import Content, Website, Directive -class WebsiteOptionInline(admin.TabularInline): - model = WebsiteOption +class DirectiveInline(admin.TabularInline): + model = Directive extra = 1 - OPTIONS_HELP_TEXT = { - op.name: str(unicode(op.help_text)) for op in options.get_enabled().values() + DIRECTIVES_HELP_TEXT = { + op.name: str(unicode(op.help_text)) for op in SiteDirective.get_plugins() } # class Media: @@ -34,9 +35,9 @@ class WebsiteOptionInline(admin.TabularInline): if db_field.name == 'name': # Help text based on select widget kwargs['widget'] = DynamicHelpTextSelect( - 'this.id.replace("name", "value")', self.OPTIONS_HELP_TEXT + 'this.id.replace("name", "value")', self.DIECTIVES_HELP_TEXT ) - return super(WebsiteOptionInline, self).formfield_for_dbfield(db_field, **kwargs) + return super(DirectiveInline, self).formfield_for_dbfield(db_field, **kwargs) class ContentInline(AccountAdminMixin, admin.TabularInline): @@ -60,7 +61,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): list_display = ('name', 'display_domains', 'display_webapps', 'account_link') list_filter = ('port', 'is_active') change_readonly_fields = ('name',) - inlines = [ContentInline, WebsiteOptionInline] + inlines = [ContentInline, DirectiveInline] filter_horizontal = ['domains'] fieldsets = ( (None, { @@ -70,7 +71,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): ) form = WebsiteAdminForm filter_by_account_fields = ['domains'] - list_prefetch_related = ('domains', 'content_set__webapp') + list_prefetch_related = ('domains', 'contents__webapp') search_fields = ('name', 'account__username', 'domains__name') def display_domains(self, website): @@ -85,7 +86,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): def display_webapps(self, website): webapps = [] - for content in website.content_set.all(): + for content in website.contents.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 380f3299..b3d487c8 100644 --- a/orchestra/apps/websites/backends/apache.py +++ b/orchestra/apps/websites/backends/apache.py @@ -68,36 +68,54 @@ class Apache2Backend(ServiceController): def get_content_directives(self, site): directives = '' - for content in site.content_set.all().order_by('-path'): - method, args = content.webapp.get_directive() + for content in site.contents.all().order_by('-path'): + directive = content.webapp.get_directive() + method, agrs = directive[0], directive[1:] method = getattr(self, 'get_%s_directives' % method) directives += method(content, *args) return directives - def get_static_directives(self, content, *args): + def get_static_directives(self, content, app_path): context = self.get_content_context(content) - context['path'] = args[0] % context if args else content.webapp.get_path() + context['app_path'] = app_path return "Alias %(location)s %(path)s\n" % context - def get_fpm_directives(self, content, *args): + def get_fpm_directives(self, content, socket_type, socket, app_path): + if socket_type == 'unix': + target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/' + if content.path != '/': + target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/$1' + elif socket_type == 'tcp': + target = 'fcgi://%(socket)s%(app_path)s/$1' + else: + raise TypeError("%s socket not supported." % socket_type) context = self.get_content_context(content) - context['fcgi_path'] = args[0] % context - directive = "ProxyPassMatch ^%(location)s(.*\.php(/.*)?)$ %(fcgi_path)s$1\n" - return directive % context + context.update({ + 'app_path': app_path, + 'socket': socket, + }) + return textwrap.dedent("""\ + ProxyPassMatch ^%(location)s/(.*\.php(/.*)?)$ {target} + Alias %(location)s/ %(app_path)s/ + """.format(target=target) % context + ) - def get_fcgi_directives(self, content, fcgid_path): + def get_fcgi_directives(self, content, app_path, wrapper_path): context = self.get_content_context(content) - context['fcgid_path'] = fcgid_path % context - fcgid = self.get_static_directives(content) - fcgid += textwrap.dedent("""\ + context.update({ + 'app_path': app_path, + 'wrapper_path': wrapper_path, + }) + fcgid = textwrap.dedent("""\ + Alias %(location)s %(app_path)s ProxyPass %(location)s ! Options +ExecCGI AddHandler fcgid-script .php - FcgidWrapper %(fcgid_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 += " %s %s\n" % (option.name, option.value) fcgid += "\n" return fcgid diff --git a/orchestra/apps/websites/backends/webalizer.py b/orchestra/apps/websites/backends/webalizer.py index 4b1c9258..ef973082 100644 --- a/orchestra/apps/websites/backends/webalizer.py +++ b/orchestra/apps/websites/backends/webalizer.py @@ -24,17 +24,21 @@ class WebalizerBackend(ServiceController): )) def delete(self, content): - pass - # TODO delete has to be done on webapp deleteion, not content deletion -# context = self.get_context(content) -# self.append("rm -fr %(webalizer_path)s" % context) -# self.append("rm %(webalizer_conf_path)s" % context) + 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 + 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) def get_context(self, content): conf_file = "%s.conf" % content.website.unique_name context = { 'site_logs': content.website.get_www_access_log_path(), 'site_name': content.website.name, + 'webapp_path': content.webapp.get_path(), 'webalizer_path': os.path.join(content.webapp.get_path(), content.website.name), 'webalizer_conf_path': os.path.join(settings.WEBSITES_WEBALIZER_PATH, conf_file), 'user': content.webapp.account.username, diff --git a/orchestra/apps/websites/directives.py b/orchestra/apps/websites/directives.py new file mode 100644 index 00000000..93169784 --- /dev/null +++ b/orchestra/apps/websites/directives.py @@ -0,0 +1,151 @@ +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ + +from orchestra.plugins import Plugin +from orchestra.utils.functional import cached +from orchestra.utils.python import import_class + +from . import settings + + +# TODO multiple and unique validation support in the formset +class SiteDirective(Plugin): + HTTPD = 'httpd' + SEC = 'sec' + SSL = 'ssl' + + help_text = "" + unique = True + + @classmethod + @cached + def get_plugins(cls): + plugins = [] + for cls in settings.WEBSITES_ENABLED_DIRECTIVES: + plugins.append(import_class(cls)) + return plugins + + @classmethod + @cached + def get_option_groups(cls): + groups = {} + for opt in cls.get_plugins(): + try: + groups[opt.group].append(opt) + except KeyError: + groups[opt.group] = [opt] + return groups + + @classmethod + def get_plugin_choices(cls): + """ Generates grouped choices ready to use in Field.choices """ + # generators can not be @cached + yield (None, '-------') + options = cls.get_option_groups() + for option in options.pop(None, ()): + yield (option.name, option.verbose_name) + for group, options in options.iteritems(): + yield (group, [(op.name, op.verbose_name) for op in options]) + + def validate(self, website): + if self.regex and not re.match(self.regex, website.value): + raise ValidationError({ + 'value': ValidationError(_("'%(value)s' does not match %(regex)s."), + params={ + 'value': website.value, + 'regex': self.regex + }), + }) + + +class Redirect(SiteDirective): + name = 'redirect' + verbose_name=_("Redirection") + help_text = _("<website path> <destination URL>") + regex = r'^[^ ]+\s[^ ]+$' + group = SiteDirective.HTTPD + + +class Proxy(SiteDirective): + name = 'proxy' + verbose_name=_("Proxy") + help_text = _("<website path> <target URL>") + regex = r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$' + group = SiteDirective.HTTPD + + +class UserGroup(SiteDirective): + name = 'user_group' + verbose_name=_("SuexecUserGroup") + help_text = _("user [group], username and optional groupname.") + regex = r'^[\w/_]+(\s[\w/_]+)*$' + group = SiteDirective.HTTPD + + def validate(self, directive): + super(UserGroupDirective, self).validate(directive) + options = directive.split() + syetmusers = [options[0]] + if len(options) > 1: + systemusers.append(options[1]) + # TODO not sure about this dependency maybe make it part of pangea only + from orchestra.apps.users.models import SystemUser + errors = [] + for user in systemusers: + if not SystemUser.objects.filter(username=user).exists(): + erros.append("") + if errors: + raise ValidationError({ + 'value': errors + }) + + +class ErrorDocument(SiteDirective): + name = 'error_document' + verbose_name=_("ErrorDocumentRoot") + help_text = _("<error code> <URL/path/message>
" + " 500 http://foo.example.com/cgi-bin/tester
" + " 404 /cgi-bin/bad_urls.pl
" + " 401 /subscription_info.html
" + " 403 \"Sorry can't allow you access today\"") + regex = r'[45]0[0-9]\s.*' + group = SiteDirective.HTTPD + + +class SSLCA(SiteDirective): + name = 'ssl_ca' + verbose_name=_("SSL CA") + help_text = _("Filesystem path of the CA certificate file.") + regex = r'^[^ ]+$' + group = SiteDirective.SSL + + +class SSLCert(SiteDirective): + name = 'ssl_cert' + verbose_name=_("SSL cert") + help_text = _("Filesystem path of the certificate file.") + regex = r'^[^ ]+$' + group = SiteDirective.SSL + + +class SSLKey(SiteDirective): + name = 'ssl_key' + verbose_name=_("SSL key") + help_text = _("Filesystem path of the key file.") + regex = r'^[^ ]+$' + group = SiteDirective.SSL + + +class SecRuleRemove(SiteDirective): + name = 'sec_rule_remove' + verbose_name=_("SecRuleRemoveById") + help_text = _("Space separated ModSecurity rule IDs.") + regex = r'^[0-9\s]+$' + group = SiteDirective.SEC + + +class SecEngine(SiteDirective): + name = 'sec_engine' + verbose_name=_("Modsecurity engine") + help_text = _("On or Off, defaults to On") + regex = r'^(On|Off)$' + group = SiteDirective.SEC diff --git a/orchestra/apps/websites/models.py b/orchestra/apps/websites/models.py index 9bc2a59f..d05fc848 100644 --- a/orchestra/apps/websites/models.py +++ b/orchestra/apps/websites/models.py @@ -2,12 +2,14 @@ import re from django.core.exceptions import ValidationError from django.db import models +from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from orchestra.core import validators, services from orchestra.utils.functional import cached -from . import settings, options +from . import settings +from .directives import SiteDirective class Website(models.Model): @@ -16,6 +18,7 @@ class Website(models.Model): validators=[validators.validate_name]) account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), related_name='websites') + # TODO protocol port = models.PositiveIntegerField(_("port"), choices=settings.WEBSITES_PORT_CHOICES, default=settings.WEBSITES_DEFAULT_PORT) @@ -49,9 +52,9 @@ class Website(models.Model): raise TypeError('No protocol for port "%s"' % self.port) @cached - def get_options(self): + def get_directives(self): return { - opt.name: opt.value for opt in self.options.all() + opt.name: opt.value for opt in self.directives.all() } def get_absolute_url(self): @@ -78,29 +81,34 @@ class Website(models.Model): return path.replace('//', '/') -class WebsiteOption(models.Model): +class Directive(models.Model): website = models.ForeignKey(Website, verbose_name=_("web site"), - related_name='options') + related_name='directives') name = models.CharField(_("name"), max_length=128, - choices=((op.name, op.verbose_name) for op in options.get_enabled().values())) + choices=SiteDirective.get_plugin_choices()) value = models.CharField(_("value"), max_length=256) - class Meta: -# unique_together = ('website', 'name') - verbose_name = _("option") - verbose_name_plural = _("options") - def __unicode__(self): return self.name + @cached_property + def directive_class(self): + return SiteDirective.get_plugin(self.name) + + @cached_property + def directive_instance(self): + """ Per request lived directive instance """ + return self.directive_class() + def clean(self): - option = options.get_enabled()[self.name] - option.validate(self) + self.directive_instance.validate(self) class Content(models.Model): - webapp = models.ForeignKey('webapps.WebApp', verbose_name=_("web application")) - website = models.ForeignKey('websites.Website', verbose_name=_("web site")) + webapp = models.ForeignKey('webapps.WebApp', verbose_name=_("web application"), + related_name='contents') + website = models.ForeignKey('websites.Website', verbose_name=_("web site"), + related_name='contents') path = models.CharField(_("path"), max_length=256, blank=True, validators=[validators.validate_url_path]) diff --git a/orchestra/apps/websites/options.py b/orchestra/apps/websites/options.py deleted file mode 100644 index c768f054..00000000 --- a/orchestra/apps/websites/options.py +++ /dev/null @@ -1,127 +0,0 @@ -from django.core.exceptions import ValidationError -from django.utils.translation import ugettext_lazy as _ - -from orchestra.utils.python import import_class - -from . import settings - - -# TODO multiple and unique validation support in the formset -class SiteOption(object): - unique = True - - def __init__(self, name, *args, **kwargs): - self.name = name - self.verbose_name = kwargs.pop('verbose_name', name) - self.help_text = kwargs.pop('help_text', '') - for k,v in kwargs.iteritems(): - setattr(self, k, v) - - def validate(self, website): - if self.regex and not re.match(self.regex, website.value): - raise ValidationError({ - 'value': ValidationError(_("'%(value)s' does not match %(regex)s."), - params={ - 'value': website.value, - 'regex': self.regex - }), - }) - - -directory_protection = SiteOption('directory_protection', - verbose_name=_("Directory protection"), - help_text=_("Space separated ..."), - regex=r'^([\w/_]+)\s+(\".*\")\s+([\w/_\.]+)$', -) - -redirect = SiteOption('redirect', - verbose_name=_("Redirection"), - help_text=_("<website path> <destination URL>"), - regex=r'^[^ ]+\s[^ ]+$', -) - -proxy = SiteOption('proxy', - verbose_name=_("Proxy"), - help_text=_("<website path> <target URL>"), - regex=r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$', -) - -ssl_ca = SiteOption('ssl_ca', - verbose_name=_("SSL CA"), - help_text=_("Filesystem path of the CA certificate file."), - regex=r'^[^ ]+$' -) - -ssl_cert = SiteOption('ssl_cert', - verbose_name=_("SSL cert"), - help_text=_("Filesystem path of the certificate file."), - regex=r'^[^ ]+$', -) - -ssl_key = SiteOption('ssl_key', - verbose_name=_("SSL key"), - help_text=_("Filesystem path of the key file."), - regex=r'^[^ ]+$', -) - -sec_rule_remove = SiteOption('sec_rule_remove', - verbose_name=_("SecRuleRemoveById"), - help_text=_("Space separated ModSecurity rule IDs."), - regex=r'^[0-9\s]+$', -) - -sec_engine = SiteOption('sec_engine', - verbose_name=_("Modsecurity engine"), - help_text=_("On or Off, defaults to On"), - regex=r'^(On|Off)$', -) - -user_group = SiteOption('user_group', - verbose_name=_("SuexecUserGroup"), - help_text=_("user [group], username and optional groupname."), - # TODO validate existing user/group - regex=r'^[\w/_]+(\s[\w/_]+)*$', -) - -error_document = SiteOption('error_document', - verbose_name=_("ErrorDocumentRoot"), - help_text=_("<error code> <URL/path/message>
" - " 500 http://foo.example.com/cgi-bin/tester
" - " 404 /cgi-bin/bad_urls.pl
" - " 401 /subscription_info.html
" - " 403 \"Sorry can't allow you access today\""), - regex=r'[45]0[0-9]\s.*', -) - - -ssl = [ - ssl_ca, - ssl_cert, - ssl_key, -] - -sec = [ - sec_rule_remove, - sec_engine, -] - -httpd = [ - directory_protection, - redirect, - proxy, - user_group, - error_document, -] - - -_enabled = None - -def get_enabled(): - global _enabled - if _enabled is None: - from . import settings - _enabled = {} - for op in settings.WEBSITES_ENABLED_OPTIONS: - op = import_class(op) - _enabled[op.name] = op - return _enabled diff --git a/orchestra/apps/websites/serializers.py b/orchestra/apps/websites/serializers.py index 87287133..728d9484 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='content_set') + source='contents') options = OptionField(required=False) class Meta: diff --git a/orchestra/apps/websites/settings.py b/orchestra/apps/websites/settings.py index 1f167224..38689ba2 100644 --- a/orchestra/apps/websites/settings.py +++ b/orchestra/apps/websites/settings.py @@ -14,10 +14,10 @@ WEBSITES_PORT_CHOICES = getattr(settings, 'WEBSITES_PORT_CHOICES', ( WEBSITES_PROTOCOL_CHOICES = getattr(settings, 'WEBSITES_PROTOCOL_CHOICES', ( - ('http', 'HTTP'), - ('https', 'HTTPS'), - ('http-https', 'HTTP and HTTPS), - ('https-only', 'HTTPS only'), + ('http', "HTTP"), + ('https', "HTTPS"), + ('http-https', _("HTTP and HTTPS")), + ('https-only', _("HTTPS only")), )) WEBSITES_DEFAULT_PORT = getattr(settings, 'WEBSITES_DEFAULT_PORT', 80) @@ -29,17 +29,16 @@ WEBSITES_DEFAULT_IP = getattr(settings, 'WEBSITES_DEFAULT_IP', '*') WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', 'domains.Domain') -WEBSITES_ENABLED_OPTIONS = getattr(settings, 'WEBSITES_ENABLED_OPTIONS', ( - 'orchestra.apps.websites.options.directory_protection', - 'orchestra.apps.websites.options.redirect', - 'orchestra.apps.websites.options.proxy', - 'orchestra.apps.websites.options.ssl_ca', - 'orchestra.apps.websites.options.ssl_cert', - 'orchestra.apps.websites.options.ssl_key', - 'orchestra.apps.websites.options.sec_rule_remove', - 'orchestra.apps.websites.options.sec_engine', - 'orchestra.apps.websites.options.user_group', - 'orchestra.apps.websites.options.error_document', +WEBSITES_ENABLED_DIRECTIVES = getattr(settings, 'WEBSITES_ENABLED_DIRECTIVES', ( + 'orchestra.apps.websites.directives.Redirect', + 'orchestra.apps.websites.directives.Proxy', + 'orchestra.apps.websites.directives.UserGroup', + 'orchestra.apps.websites.directives.ErrorDocument', + 'orchestra.apps.websites.directives.SSLCA', + 'orchestra.apps.websites.directives.SSLCert', + 'orchestra.apps.websites.directives.SSLKey', + 'orchestra.apps.websites.directives.SecRuleRemove', + 'orchestra.apps.websites.directives.SecEngine', )) diff --git a/orchestra/plugins/options.py b/orchestra/plugins/options.py index 3f2387b1..f3bffaba 100644 --- a/orchestra/plugins/options.py +++ b/orchestra/plugins/options.py @@ -17,12 +17,12 @@ class Plugin(object): return cls.plugins @classmethod - @cached def get_plugin(cls, name): - for plugin in cls.get_plugins(): - if plugin.get_name() == name: - return plugin - raise KeyError('This plugin is not registered') + if not hasattr(cls, '_registry'): + cls._registry = { + plugin.get_name(): plugin for plugin in cls.get_plugins() + } + return cls._registry[name] @classmethod def get_verbose_name(cls): @@ -38,7 +38,9 @@ class Plugin(object): choices = [] for plugin in cls.get_plugins(): verbose = plugin.get_verbose_name() - choices.append((plugin.get_name(), verbose)) + choices.append( + (plugin.get_name(), verbose) + ) return sorted(choices, key=lambda e: e[1]) @classmethod diff --git a/orchestra/utils/functional.py b/orchestra/utils/functional.py index 01800b3d..2cb5f42f 100644 --- a/orchestra/utils/functional.py +++ b/orchestra/utils/functional.py @@ -1,7 +1,8 @@ def cached(func): """ caches func return value """ def cached_func(self, *args, **kwargs): - attr = '_cached_' + func.__name__ + # id(self) prevents sharing within subclasses + attr = '_cached_%s_%i' % (func.__name__, id(self)) key = (args, tuple(kwargs.items())) try: return getattr(self, attr)[key] @@ -13,4 +14,3 @@ def cached(func): setattr(self, attr, {key: value}) return value return cached_func -