diff --git a/TODO.md b/TODO.md index 1d469d27..ce13247c 100644 --- a/TODO.md +++ b/TODO.md @@ -203,11 +203,12 @@ POST INSTALL ssh-keygen ssh-copy-id root@ +Php binaries should have this format: /usr/bin/php5.2-cgi -* 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() + +* logs on panel/logs/ ? mkdir ~webapps, backend post save signal? +* transaction fault tolerant on backend.execute() * and other IfModule on backend SecRule diff --git a/orchestra/apps/lists/backends.py b/orchestra/apps/lists/backends.py index 21db339d..d969c912 100644 --- a/orchestra/apps/lists/backends.py +++ b/orchestra/apps/lists/backends.py @@ -107,7 +107,14 @@ class MailmanBackend(ServiceController): sed -i -e '/^.*\s%(name)s\(%(address_regex)s\)\s*$/d' \\ -e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s""") % context ) - self.append("rmlist -a %(name)s" % context) + self.append(textwrap.dedent("""\ + # Non-existent list archives produce exit code 1 + exit_code=0 + rmlist -a %(name)s || exit_code=$? + if [[ $exit_code != 0 && $exit_code != 1 ]]; then + exit $exit_code + fi""") % context + ) def commit(self): context = self.get_context_files() diff --git a/orchestra/apps/orchestration/helpers.py b/orchestra/apps/orchestration/helpers.py index c3427957..d0f1671a 100644 --- a/orchestra/apps/orchestration/helpers.py +++ b/orchestra/apps/orchestration/helpers.py @@ -38,24 +38,30 @@ def message_user(request, logs): ids = [] for log in logs: total += 1 - ids.append(log.pk) + if log.state != log.EXCEPTION: + # EXCEPTION logs are not stored on the database + ids.append(log.pk) if log.state == log.SUCCESS: successes += 1 errors = total-successes - if total > 1: + if len(ids) == 1: + url = reverse('admin:orchestration_backendlog_change', args=ids) + href = 'backends'.format(url) + elif len(ids) > 1: url = reverse('admin:orchestration_backendlog_changelist') url += '?id__in=%s' % ','.join(map(str, ids)) + href = 'backends'.format(url) else: - url = reverse('admin:orchestration_backendlog_change', args=ids) + href = '' if errors: msg = ungettext( - _('{errors} out of {total} backends has fail to execute.'), - _('{errors} out of {total} backends have fail to execute.'), + _('{errors} out of {total} {href} has fail to execute.'), + _('{errors} out of {total} {href} have fail to execute.'), errors) - messages.error(request, mark_safe(msg.format(errors=errors, total=total, url=url))) + messages.error(request, mark_safe(msg.format(errors=errors, total=total, href=href))) else: msg = ungettext( - _('{total} backend has been executed.'), - _('{total} backends have been executed.'), + _('{total} {href} has been executed.'), + _('{total} {href} have been executed.'), total) - messages.success(request, mark_safe(msg.format(total=total, url=url))) + messages.success(request, mark_safe(msg.format(total=total, href=href))) diff --git a/orchestra/apps/orchestration/manager.py b/orchestra/apps/orchestration/manager.py index 2d1667f5..d0b220dd 100644 --- a/orchestra/apps/orchestration/manager.py +++ b/orchestra/apps/orchestration/manager.py @@ -1,12 +1,15 @@ import logging import threading +import traceback from django import db +from django.core.mail import mail_admins from orchestra.utils.python import import_class from . import settings from .helpers import send_report +from .models import BackendLog logger = logging.getLogger(__name__) @@ -29,11 +32,16 @@ def close_connection(execute): def wrapper(*args, **kwargs): try: log = execute(*args, **kwargs) - except: - logger.error('EXCEPTION executing backend %s %s' % (str(args), str(kwargs))) - raise + except Exception as e: + subject = 'EXCEPTION executing backend(s) %s %s' % (str(args), str(kwargs)) + message = traceback.format_exc() + logger.error(subject) + logger.error(message) + mail_admins(subject, message) + # We don't propagate the exception further to avoid transaction rollback else: # Using the wrapper function as threader messenger for the execute output + # Absense of it will indicate a failure at this stage wrapper.log = log finally: db.connection.close() @@ -78,13 +86,18 @@ def execute(operations, async=False): logs = [] # collect results for execution, operations in executions: - for operation in operations: - logger.info("Executed %s" % str(operation)) - operation.log = execution.log - operation.save() - stdout = execution.log.stdout.strip() - stdout and logger.debug('STDOUT %s', stdout) - stderr = execution.log.stderr.strip() - stderr and logger.debug('STDERR %s', stderr) - logs.append(execution.log) + # There is no log if an exception has been rised at the very end of the execution + if hasattr(execution, 'log'): + for operation in operations: + logger.info("Executed %s" % str(operation)) + operation.log = execution.log + operation.save() + stdout = execution.log.stdout.strip() + stdout and logger.debug('STDOUT %s', stdout) + stderr = execution.log.stderr.strip() + stderr and logger.debug('STDERR %s', stderr) + logs.append(execution.log) + else: + mocked_log = BackendLog(state=BackendLog.EXCEPTION) + logs.append(mocked_log) return logs diff --git a/orchestra/apps/orchestration/models.py b/orchestra/apps/orchestration/models.py index 0c3d969e..65ee36e7 100644 --- a/orchestra/apps/orchestration/models.py +++ b/orchestra/apps/orchestration/models.py @@ -52,6 +52,8 @@ class BackendLog(models.Model): FAILURE = 'FAILURE' ERROR = 'ERROR' REVOKED = 'REVOKED' + # Special state for mocked backendlogs + EXCEPTION = 'EXCEPTION' STATES = ( (RECEIVED, RECEIVED), diff --git a/orchestra/apps/payments/methods/options.py b/orchestra/apps/payments/methods/options.py index 3e69910b..5ac1e0d8 100644 --- a/orchestra/apps/payments/methods/options.py +++ b/orchestra/apps/payments/methods/options.py @@ -26,9 +26,9 @@ class PaymentMethod(plugins.Plugin): return plugins @classmethod - def clean_data(cls, data): + def clean_data(cls): """ model clean, uses cls.serializer by default """ - serializer = cls.serializer(data=data) + serializer = cls.serializer(data=self.instance.data) if not serializer.is_valid(): serializer.errors.pop('non_field_errors', None) raise ValidationError(serializer.errors) @@ -43,11 +43,11 @@ class PaymentMethod(plugins.Plugin): self.serializer.plugin = self return self.serializer - def get_label(self, data): - return data[self.label_field] + def get_label(self): + return self.instance.data[self.label_field] - def get_number(self, data): - return data[self.number_field] + def get_number(self): + return self.instance.data[self.number_field] - def get_bill_message(self, source): + def get_bill_message(self): return '' diff --git a/orchestra/apps/payments/methods/sepadirectdebit.py b/orchestra/apps/payments/methods/sepadirectdebit.py index 7217cbfa..bfd8979e 100644 --- a/orchestra/apps/payments/methods/sepadirectdebit.py +++ b/orchestra/apps/payments/methods/sepadirectdebit.py @@ -45,9 +45,9 @@ class SEPADirectDebit(PaymentMethod): serializer = SEPADirectDebitSerializer due_delta = datetime.timedelta(days=5) - def get_bill_message(self, source): + def get_bill_message(self): return _("This bill will been automatically charged to your bank account " - " with IBAN number
%s.") % source.number + " with IBAN number
%s.") % self.instance.number @classmethod def process(cls, transactions): diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py index 834c34ee..fb91d2cc 100644 --- a/orchestra/apps/payments/models.py +++ b/orchestra/apps/payments/models.py @@ -36,27 +36,27 @@ class PaymentSource(models.Model): @cached_property def service_instance(self): """ Per request lived method_instance """ - return self.method_class() + return self.method_class(self) @cached_property def label(self): - return self.method_instance.get_label(self.data) + return self.method_instance.get_label() @cached_property def number(self): - return self.method_instance.get_number(self.data) + return self.method_instance.get_number() def get_bill_context(self): method = self.method_instance return { - 'message': method.get_bill_message(self), + 'message': method.get_bill_message(), } def get_due_delta(self): return self.method_instance.due_delta def clean(self): - self.data = self.method_instance.clean_data(self.data) + self.data = self.method_instance.clean_data() class TransactionQuerySet(models.QuerySet): diff --git a/orchestra/apps/saas/models.py b/orchestra/apps/saas/models.py index 9f51aeb1..b059f103 100644 --- a/orchestra/apps/saas/models.py +++ b/orchestra/apps/saas/models.py @@ -38,13 +38,13 @@ class SaaS(models.Model): @cached_property def service_instance(self): """ Per request lived service_instance """ - return self.service_class() + return self.service_class(self) def get_site_name(self): - return self.service_instance.get_site_name(self) + return self.service_instance.get_site_name() def clean(self): - self.data = self.service_instance.clean_data(self) + self.data = self.service_instance.clean_data() def set_password(self, password): self.password = password diff --git a/orchestra/apps/saas/services/options.py b/orchestra/apps/saas/services/options.py index bf6380e4..9ba21b89 100644 --- a/orchestra/apps/saas/services/options.py +++ b/orchestra/apps/saas/services/options.py @@ -84,10 +84,9 @@ class SoftwareService(plugins.Plugin): plugins.append(import_class(cls)) return plugins - @classmethod - def clean_data(cls, saas): + def clean_data(cls): """ model clean, uses cls.serizlier by default """ - serializer = cls.serializer(data=saas.data) + serializer = cls.serializer(data=self.instance.data) if not serializer.is_valid(): raise ValidationError(serializer.errors) return serializer.data @@ -96,8 +95,10 @@ class SoftwareService(plugins.Plugin): def get_change_readonly_fileds(cls): return cls.change_readonly_fileds + ('username',) - def get_site_name(self, saas): - return self.site_name or '.'.join((saas.site_name, self.site_name_base_domain)) + def get_site_name(self): + return self.site_name or '.'.join( + (self.instance.site_name, self.site_name_base_domain) + ) def get_form(self): self.form.plugin = self diff --git a/orchestra/apps/webapps/admin.py b/orchestra/apps/webapps/admin.py index 30a40180..a472427d 100644 --- a/orchestra/apps/webapps/admin.py +++ b/orchestra/apps/webapps/admin.py @@ -69,11 +69,12 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) url = change_url(website) name = "%s on %s" % (website.name, content.path) websites.append('%s' % (url, name)) - add_url = reverse('admin:websites_website_add') - # TODO support for preselecting related web app on website - add_url += '?account=%s' % webapp.account_id - plus = '+' - websites.append('%s%s' % (add_url, plus, ugettext("Add website"))) + if not websites: + add_url = reverse('admin:websites_website_add') + # TODO support for preselecting related web app on website + add_url += '?account=%s' % webapp.account_id + plus = '+' + websites.append('%s%s' % (add_url, plus, ugettext("Add website"))) return '
'.join(websites) display_websites.short_description = _("web sites") display_websites.allow_tags = True diff --git a/orchestra/apps/webapps/backends/__init__.py b/orchestra/apps/webapps/backends/__init__.py index 6b701e3c..ea0e2213 100644 --- a/orchestra/apps/webapps/backends/__init__.py +++ b/orchestra/apps/webapps/backends/__init__.py @@ -9,13 +9,22 @@ class WebAppServiceMixin(object): 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) + self.append(textwrap.dedent("""\ + CREATED=0 + [[ ! -e %(app_path)s ]] && CREATED=1 + mkdir -p %(app_path)s + 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) + self.append(textwrap.dedent("""\ + if [[ $CREATED == 1 ]]; then + cp -r %(under_construction_path)s %(app_path)s + chown -R %(user)s:%(group)s %(app_path)s + fi""") % context + ) def delete_webapp_dir(self, context): self.append("rm -fr %(app_path)s" % context) diff --git a/orchestra/apps/webapps/backends/phpfcgid.py b/orchestra/apps/webapps/backends/phpfcgid.py index 9745e127..d8103b84 100644 --- a/orchestra/apps/webapps/backends/phpfcgid.py +++ b/orchestra/apps/webapps/backends/phpfcgid.py @@ -13,7 +13,7 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController): """ Per-webapp fcgid application """ verbose_name = _("PHP-Fcgid") directive = 'fcgid' - default_route_match = "webapp.type.endswith('-fcgid')" + default_route_match = "webapp.type_class.php_execution == 'fcgid'" def save(self, webapp): context = self.get_context(webapp) @@ -37,6 +37,8 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController): echo -e '%(cmd_options)s' > %(cmd_options_path)s; UPDATED_APACHE=1 }""" ) % context ) + else: + self.append("rm -f %(cmd_options_path)s" % context) def delete(self, webapp): context = self.get_context(webapp) @@ -50,14 +52,14 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController): def get_fcgid_wrapper(self, webapp, context): opt = webapp.type_instance # Format PHP init vars - init_vars = opt.get_php_init_vars(webapp) + init_vars = opt.get_php_init_vars() 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_binary': opt.get_php_binary_path(), + 'php_rc': opt.get_php_rc_path(), 'php_init_vars': init_vars, }) return textwrap.dedent("""\ @@ -82,7 +84,7 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController): def get_context(self, webapp): context = super(PHPFcgidBackend, self).get_context(webapp) - wrapper_path = settings.WEBAPPS_FCGID_PATH % context + wrapper_path = settings.WEBAPPS_FCGID_WRAPPER_PATH % context context.update({ 'wrapper': self.get_fcgid_wrapper(webapp, context), 'wrapper_path': wrapper_path, diff --git a/orchestra/apps/webapps/backends/phpfpm.py b/orchestra/apps/webapps/backends/phpfpm.py index d5b6dafb..77384459 100644 --- a/orchestra/apps/webapps/backends/phpfpm.py +++ b/orchestra/apps/webapps/backends/phpfpm.py @@ -13,7 +13,7 @@ from .. import settings class PHPFPMBackend(WebAppServiceMixin, ServiceController): """ Per-webapp php application """ verbose_name = _("PHP-FPM") - default_route_match = "webapp.type.endswith('-fpm')" + default_route_match = "webapp.type_class.php_execution == 'fpm'" def save(self, webapp): context = self.get_context(webapp) @@ -45,7 +45,7 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController): def get_fpm_config(self, webapp, context): context.update({ - 'init_vars': webapp.type_instance.get_php_init_vars(webapp), + 'init_vars': webapp.type_instance.get_php_init_vars(), 'fpm_port': webapp.get_fpm_port(), 'max_children': webapp.get_options().get('processes', False), 'request_terminate_timeout': webapp.get_options().get('timeout', False), @@ -76,4 +76,3 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController): 'fpm_path': settings.WEBAPPS_PHPFPM_POOL_PATH % context, }) return context - diff --git a/orchestra/apps/webapps/models.py b/orchestra/apps/webapps/models.py index 6dc6410f..ccb23005 100644 --- a/orchestra/apps/webapps/models.py +++ b/orchestra/apps/webapps/models.py @@ -44,12 +44,12 @@ class WebApp(models.Model): @cached_property def type_instance(self): """ Per request lived type_instance """ - return self.type_class() + return self.type_class(self) def clean(self): apptype = self.type_instance - apptype.validate(self) - self.data = apptype.clean_data(self) + apptype.validate() + self.data = apptype.clean_data() @cached def get_options(self): @@ -58,7 +58,7 @@ class WebApp(models.Model): } def get_directive(self): - return self.type_instance.get_directive(self) + return self.type_instance.get_directive() def get_path(self): context = { @@ -102,10 +102,10 @@ class WebAppOption(models.Model): @cached_property def option_instance(self): """ Per request lived option instance """ - return self.option_class() + return self.option_class(self) def clean(self): - self.option_instance.validate(self) + self.option_instance.validate() services.register(WebApp) @@ -117,9 +117,9 @@ services.register(WebApp) @receiver(pre_save, sender=WebApp, dispatch_uid='webapps.type.save') def type_save(sender, *args, **kwargs): instance = kwargs['instance'] - instance.type_instance.save(instance) + instance.type_instance.save() @receiver(pre_delete, sender=WebApp, dispatch_uid='webapps.type.delete') def type_delete(sender, *args, **kwargs): instance = kwargs['instance'] - instance.type_instance.delete(instance) + instance.type_instance.delete() diff --git a/orchestra/apps/webapps/options.py b/orchestra/apps/webapps/options.py index 9ee71ca7..ff9cf4dc 100644 --- a/orchestra/apps/webapps/options.py +++ b/orchestra/apps/webapps/options.py @@ -37,12 +37,12 @@ class AppOption(Plugin): groups[opt.group] = [opt] return groups - def validate(self, option): - if self.regex and not re.match(self.regex, option.value): + def validate(self): + if self.regex and not re.match(self.regex, self.instance.value): raise ValidationError({ 'value': ValidationError(_("'%(value)s' does not match %(regex)s."), params={ - 'value': option.value, + 'value': self.instance.value, 'regex': self.regex }), }) diff --git a/orchestra/apps/webapps/settings.py b/orchestra/apps/webapps/settings.py index e52d1e65..022d0753 100644 --- a/orchestra/apps/webapps/settings.py +++ b/orchestra/apps/webapps/settings.py @@ -2,7 +2,9 @@ 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)s/webapps/%(app_name)s/') + WEBAPPS_FPM_LISTEN = getattr(settings, 'WEBAPPS_FPM_LISTEN', # '127.0.0.1:9%(app_id)03d @@ -13,11 +15,12 @@ WEBAPPS_PHPFPM_POOL_PATH = getattr(settings, 'WEBAPPS_PHPFPM_POOL_PATH', '/etc/php5/fpm/pool.d/%(user)s-%(app_name)s.conf') -WEBAPPS_FCGID_PATH = getattr(settings, 'WEBAPPS_FCGID_PATH', +WEBAPPS_FCGID_WRAPPER_PATH = getattr(settings, 'WEBAPPS_FCGID_WRAPPER_PATH', '/home/httpd/fcgi-bin.d/%(user)s/%(app_name)s-wrapper') WEBAPPS_FCGID_CMD_OPTIONS_PATH = getattr(settings, 'WEBAPPS_FCGID_CMD_OPTIONS_PATH', + # Loaded by Apache '/etc/apache2/fcgid-conf/%(user)s-%(app_name)s.conf') @@ -25,19 +28,50 @@ 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', - 'orchestra.apps.webapps.types.WebalizerApp', - 'orchestra.apps.webapps.types.WordPressMuApp', - 'orchestra.apps.webapps.types.DokuWikiMuApp', - 'orchestra.apps.webapps.types.DrupalMuApp', - 'orchestra.apps.webapps.types.SymbolicLinkApp', - 'orchestra.apps.webapps.types.WordPressApp', + 'orchestra.apps.webapps.types.php.PHPFPMApp', + 'orchestra.apps.webapps.types.php.PHPFCGIDApp', + 'orchestra.apps.webapps.types.misc.StaticApp', + 'orchestra.apps.webapps.types.misc.WebalizerApp', + 'orchestra.apps.webapps.types.saas.WordPressMuApp', + 'orchestra.apps.webapps.types.saas.DokuWikiMuApp', + 'orchestra.apps.webapps.types.saas.DrupalMuApp', + 'orchestra.apps.webapps.types.misc.SymbolicLinkApp', + 'orchestra.apps.webapps.types.wordpress.WordPressFPMApp', + 'orchestra.apps.webapps.types.wordpress.WordPressFCGIDApp', )) + + +WEBAPPS_PHP_FCGID_VERSIONS = getattr(settings, 'WEBAPPS_PHP_FCGID_VERSIONS', ( + ('5.4', '5.4'), + ('5.3', '5.3'), + ('5.2', '5.2'), + ('4', '4'), +)) + + +WEBAPPS_PHP_FCGID_DEFAULT_VERSION = getattr(settings, 'WEBAPPS_PHP_FCGID_DEFAULT_VERSION', + '5.4') + + +WEBAPPS_PHP_CGI_BINARY_PATH = getattr(settings, 'WEBAPPS_PHP_CGI_BINARY_PATH', + # Path of the cgi binary used by fcgid + '/usr/bin/php%(php_version)s-cgi') + +WEBAPPS_PHP_CGI_RC_PATH = getattr(settings, 'WEBAPPS_PHP_CGI_RC_PATH', + # Path to php.ini + '/etc/php%(php_version)s/cgi/') + + +WEBAPPS_PHP_FPM_VERSIONS = getattr(settings, 'WEBAPPS_PHP_FPM_VERSIONS', ( + ('5.4', '5.4'), +)) + + +WEBAPPS_PHP_FPM_DEFAULT_VERSION = getattr(settings, 'WEBAPPS_PHP_DEFAULT_VERSION', + '5.4') + + 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', @@ -51,14 +85,6 @@ WEBAPPS_UNDER_CONSTRUCTION_PATH = getattr(settings, 'WEBAPPS_UNDER_CONSTRUCTION_ # WEBAPPS_TYPES[webapp_type] = value -WEBAPPS_DEFAULT_TYPE = getattr(settings, 'WEBAPPS_DEFAULT_TYPE', 'php5.5') - - -WEBAPPS_DEFAULT_HTTPS_CERT = getattr(settings, 'WEBAPPS_DEFAULT_HTTPS_CERT', - ('/etc/apache2/cert', '/etc/apache2/cert.key') -) - - WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTION', [ 'exec', 'passthru', diff --git a/orchestra/apps/webapps/types.py b/orchestra/apps/webapps/types.py deleted file mode 100644 index 3a472177..00000000 --- a/orchestra/apps/webapps/types.py +++ /dev/null @@ -1,408 +0,0 @@ -import os - -from django import forms -from django.core.exceptions import ValidationError -from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ - -from orchestra import plugins -from orchestra.plugins.forms import PluginDataForm -from orchestra.core import validators -from orchestra.forms import widgets -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): - name = None - verbose_name = "" - help_text= "" - form = PluginDataForm - change_form = None - serializer = None - icon = 'orchestra/icons/apps.png' - unique_name = False - option_groups = (AppOption.FILESYSTEM, AppOption.PROCESS, AppOption.PHP) - - @classmethod - @cached - def get_plugins(cls): - plugins = [] - for cls in settings.WEBAPPS_TYPES: - plugins.append(import_class(cls)) - return plugins - - def clean_data(self, webapp): - """ model clean, uses cls.serizlier by default """ - 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' - return self.form - - def get_change_form(self): - form = self.change_form or self.form - form.plugin = self - form.plugin_field = 'type' - return form - - def get_serializer(self): - self.serializer.plugin = self - return self.serializer - - def validate(self, instance): - """ Unique name validation """ - if self.unique_name: - if not instance.pk and Webapp.objects.filter(name=instance.name, type=instance.type).exists(): - raise ValidationError({ - 'name': _("A WordPress blog with this name already exists."), - }) - - @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): - """ Generates grouped choices ready to use in Field.choices """ - # generators can not be @cached - yield (None, '-------') - 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 - - def delete(self, instance): - pass - - 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 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 % 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 = {} - options = webapp.options.all() - if per_account: - options = webapp.account.webapps.filter(webapp_type=webapp.type) - php_options = [option.name for option in type(self).get_php_options()] - for opt in options: - 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(',') - 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['dissabled_functions'] = ','.join(disabled_functions) - if settings.WEBAPPS_PHP_ERROR_LOG_PATH and 'error_log' not in init_vars: - context = self.get_context(webapp) - error_log_path = os.path.normpath(settings.WEBAPPS_PHP_ERROR_LOG_PATH % context) - init_vars['error_log'] = error_log_path - 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.") - icon = 'orchestra/icons/apps/PHPFPM.png' - - -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 = os.path.normpath(settings.WEBAPPS_FCGID_PATH % context) - return ('fcgid', webapp.get_path(), wrapper_path) - - -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.") - icon = 'orchestra/icons/apps/PHPFCGI.png' - - -class StaticApp(AppType): - name = 'static' - verbose_name = "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' - option_groups = (AppOption.FILESYSTEM,) - - -class WebalizerApp(AppType): - name = 'webalizer' - verbose_name = "Webalizer" - directive = ('static', '%(app_path)s%(site_name)s') - help_text = _("This creates a Webalizer application under " - "~/webapps/<app_name>-<site_name>") - icon = 'orchestra/icons/apps/Stats.png' - option_groups = () - - def get_directive(self, webapp): - webalizer_path = os.path.join(webapp.get_path(), '%(site_name)s') - webalizer_path = os.path.normpath(webalizer_path) - return ('static', webalizer_path) - - -class WordPressMuApp(PHPAppType): - name = 'wordpress-mu' - verbose_name = "WordPress (SaaS)" - directive = ('fpm', 'fcgi://127.0.0.1:8990/home/httpd/wordpress-mu/') - help_text = _("This creates a WordPress site on a multi-tenant WordPress server.
" - "By default this blog is accessible via <app_name>.blogs.orchestra.lan") - icon = 'orchestra/icons/apps/WordPressMu.png' - unique_name = True - option_groups = () - fpm_listen = settings.WEBAPPS_WORDPRESSMU_LISTEN - - -class DokuWikiMuApp(PHPAppType): - name = 'dokuwiki-mu' - verbose_name = "DokuWiki (SaaS)" - directive = ('alias', '/home/httpd/wikifarm/farm/') - help_text = _("This create a DokuWiki wiki into a shared DokuWiki server.
" - "By default this wiki is accessible via <app_name>.wikis.orchestra.lan") - icon = 'orchestra/icons/apps/DokuWikiMu.png' - unique_name = True - option_groups = () - fpm_listen = settings.WEBAPPS_DOKUWIKIMU_LISTEN - - -class MoodleMuApp(PHPAppType): - name = 'moodle-mu' - verbose_name = "Moodle (SaaS)" - directive = ('alias', '/home/httpd/wikifarm/farm/') - help_text = _("This create a Moodle site into a shared Moodle server.
" - "By default this wiki is accessible via <app_name>.moodle.orchestra.lan") - icon = 'orchestra/icons/apps/MoodleMu.png' - unique_name = True - option_groups = () - fpm_listen = settings.WEBAPPS_MOODLEMU_LISTEN - - -class DrupalMuApp(PHPAppType): - name = 'drupal-mu' - verbose_name = "Drupdal (SaaS)" - directive = ('fpm', 'fcgi://127.0.0.1:8991/home/httpd/drupal-mu/') - help_text = _("This creates a Drupal site into a multi-tenant Drupal server.
" - "The installation will be completed after visiting " - "http://<app_name>.drupal.orchestra.lan/install.php?profile=standard
" - "By default this site will be accessible via <app_name>.drupal.orchestra.lan") - icon = 'orchestra/icons/apps/DrupalMu.png' - unique_name = True - option_groups = () - fpm_listen = settings.WEBAPPS_DRUPALMU_LISTEN - - -from rest_framework import serializers -from orchestra.forms import widgets -class SymbolicLinkForm(PluginDataForm): - path = forms.CharField(label=_("Path"), widget=forms.TextInput(attrs={'size':'100'}), - help_text=_("Path for the origin of the symbolic link.")) - - -class SymbolicLinkSerializer(serializers.Serializer): - path = serializers.CharField(label=_("Path")) - - -class SymbolicLinkApp(PHPAppType): - name = 'symbolic-link' - verbose_name = "Symbolic link" - form = SymbolicLinkForm - serializer = SymbolicLinkSerializer - icon = 'orchestra/icons/apps/SymbolicLink.png' - change_readonly_fileds = ('path',) - - -class WordPressForm(PluginDataForm): - db_name = forms.CharField(label=_("Database name"), - help_text=_("Database used for this webapp.")) - db_user = forms.CharField(label=_("Database user"),) - db_pass = forms.CharField(label=_("Database user password"), - help_text=_("Initial database password.")) - - -class WordPressSerializer(serializers.Serializer): - db_name = serializers.CharField(label=_("Database name"), required=False) - db_user = serializers.CharField(label=_("Database user"), required=False) - db_pass = serializers.CharField(label=_("Database user password"), required=False) - - -from orchestra.apps.databases.models import Database, DatabaseUser -from orchestra.utils.python import random_ascii - - -class WordPressApp(PHPAppType): - name = 'wordpress' - verbose_name = "WordPress" - icon = 'orchestra/icons/apps/WordPress.png' - change_form = WordPressForm - serializer = WordPressSerializer - change_readonly_fileds = ('db_name', 'db_user', 'db_pass',) - help_text = _("Visit http://<domain.lan>/wp-admin/install.php to finish the installation.") - - def get_db_name(self, webapp): - db_name = 'wp_%s_%s' % (webapp.name, webapp.account) - # Limit for mysql database names - return db_name[:65] - - def get_db_user(self, webapp): - db_name = self.get_db_name(webapp) - # Limit for mysql user names - return db_name[:17] - - def get_db_pass(self): - return random_ascii(10) - - def validate(self, webapp): - create = not webapp.pk - if create: - db = Database(name=self.get_db_name(webapp), account=webapp.account) - user = DatabaseUser(username=self.get_db_user(webapp), password=self.get_db_pass(), - account=webapp.account) - for obj in (db, user): - try: - obj.full_clean() - except ValidationError, e: - raise ValidationError({ - 'name': e.messages, - }) - - def save(self, webapp): - create = not webapp.pk - if create: - db_name = self.get_db_name(webapp) - db_user = self.get_db_user(webapp) - db_pass = self.get_db_pass() - db = Database.objects.create(name=db_name, account=webapp.account) - user = DatabaseUser(username=db_user, account=webapp.account) - user.set_password(db_pass) - user.save() - db.users.add(user) - webapp.data = { - 'db_name': db_name, - 'db_user': db_user, - 'db_pass': db_pass, - } - else: - # Trigger related backends - for related in self.get_related(webapp): - related.save() - - def delete(self, webapp): - for related in self.get_related(webapp): - related.delete() - - def get_related(self, webapp): - related = [] - try: - db_user = DatabaseUser.objects.get(username=webapp.data.get('db_user')) - except DatabaseUser.DoesNotExist: - pass - else: - related.append(db_user) - try: - db = Database.objects.get(name=webapp.data.get('db_name')) - except Database.DoesNotExist: - pass - else: - related.append(db) - return related diff --git a/orchestra/apps/webapps/types/__init__.py b/orchestra/apps/webapps/types/__init__.py new file mode 100644 index 00000000..4fa2a773 --- /dev/null +++ b/orchestra/apps/webapps/types/__init__.py @@ -0,0 +1,119 @@ +from django.core.exceptions import ValidationError + +from orchestra import plugins +from orchestra.plugins.forms import PluginDataForm +from orchestra.utils.functional import cached +from orchestra.utils.python import import_class + +from .. import settings +from ..options import AppOption + + +class AppType(plugins.Plugin): + name = None + verbose_name = "" + help_text= "" + form = PluginDataForm + change_form = None + serializer = None + icon = 'orchestra/icons/apps.png' + unique_name = False + option_groups = (AppOption.FILESYSTEM, AppOption.PROCESS, AppOption.PHP) + # TODO generic name like 'execution' ? + php_execution = None + + @classmethod + @cached + def get_plugins(cls): + plugins = [] + for cls in settings.WEBAPPS_TYPES: + plugins.append(import_class(cls)) + return plugins + + def clean_data(self): + """ model clean, uses cls.serizlier by default """ + if self.serializer: + serializer = self.serializer(data=self.instance.data) + if not serializer.is_valid(): + raise ValidationError(serializer.errors) + return serializer.data + return {} + + def get_directive(self): + raise NotImplementedError + + def get_form(self): + self.form.plugin = self + self.form.plugin_field = 'type' + return self.form + + def get_change_form(self): + form = self.change_form or self.form + form.plugin = self + form.plugin_field = 'type' + return form + + def get_serializer(self): + self.serializer.plugin = self + return self.serializer + + def validate(self): + """ Unique name validation """ + if self.unique_name: + if not self.instance.pk and Webapp.objects.filter(name=self.instance.name, type=self.instance.type).exists(): + raise ValidationError({ + 'name': _("A WordPress blog with this name already exists."), + }) + + @classmethod + @cached + def get_php_options(cls): + # TODO validate php options once a php version has been selected (deprecated directives) + 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): + """ Generates grouped choices ready to use in Field.choices """ + # generators can not be @cached + yield (None, '-------') + 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): + pass + + def delete(self): + pass + + def get_related_objects(self): + pass + + def get_directive_context(self): + return { + 'app_id': self.instance.id, + 'app_name': self.instance.name, + 'user': self.instance.account.username, + } + diff --git a/orchestra/apps/webapps/types/misc.py b/orchestra/apps/webapps/types/misc.py new file mode 100644 index 00000000..7a6d3d1a --- /dev/null +++ b/orchestra/apps/webapps/types/misc.py @@ -0,0 +1,59 @@ +import os + +from django import forms +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from orchestra.plugins.forms import PluginDataForm + +from ..options import AppOption + +from . import AppType +from .php import PHPAppType + + +class StaticApp(AppType): + name = 'static' + verbose_name = "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' + option_groups = (AppOption.FILESYSTEM,) + + def get_directive(self): + return ('static', self.instance.get_path()) + + +class WebalizerApp(AppType): + name = 'webalizer' + verbose_name = "Webalizer" + directive = ('static', '%(app_path)s%(site_name)s') + help_text = _("This creates a Webalizer application under " + "~/webapps/<app_name>-<site_name>") + icon = 'orchestra/icons/apps/Stats.png' + option_groups = () + + def get_directive(self, webapp): + webalizer_path = os.path.join(webapp.get_path(), '%(site_name)s') + webalizer_path = os.path.normpath(webalizer_path) + return ('static', webalizer_path) + + +class SymbolicLinkForm(PluginDataForm): + path = forms.CharField(label=_("Path"), widget=forms.TextInput(attrs={'size':'100'}), + help_text=_("Path for the origin of the symbolic link.")) + + +class SymbolicLinkSerializer(serializers.Serializer): + path = serializers.CharField(label=_("Path")) + + +class SymbolicLinkApp(PHPAppType): + name = 'symbolic-link' + verbose_name = "Symbolic link" + form = SymbolicLinkForm + serializer = SymbolicLinkSerializer + icon = 'orchestra/icons/apps/SymbolicLink.png' + change_readonly_fileds = ('path',) + + diff --git a/orchestra/apps/webapps/types/php.py b/orchestra/apps/webapps/types/php.py new file mode 100644 index 00000000..e324abc4 --- /dev/null +++ b/orchestra/apps/webapps/types/php.py @@ -0,0 +1,131 @@ +import os + +from django import forms +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from orchestra.forms import widgets +from orchestra.plugins.forms import PluginDataForm + +from .. import settings + +from . import AppType + + +class PHPAppType(AppType): + FPM = 'fpm' + FCGID = 'fcgid' + + php_version = 5.4 + fpm_listen = settings.WEBAPPS_FPM_LISTEN + + def get_context(self): + """ context used to format settings """ + return { + 'home': self.instance.account.main_systemuser.get_home(), + 'account': self.instance.account.username, + 'user': self.instance.account.username, + 'app_name': self.instance.name, + } + + def get_php_init_vars(self, per_account=False): + """ + process php options for inclusion on php.ini + per_account=True merges all (account, webapp.type) options + """ + init_vars = {} + options = self.instance.options.all() + if per_account: + options = self.instance.account.webapps.filter(webapp_type=self.instance.type) + php_options = [option.name for option in type(self).get_php_options()] + for opt in options: + 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(',') + 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['dissabled_functions'] = ','.join(disabled_functions) + if settings.WEBAPPS_PHP_ERROR_LOG_PATH and 'error_log' not in init_vars: + context = self.get_context() + error_log_path = os.path.normpath(settings.WEBAPPS_PHP_ERROR_LOG_PATH % context) + init_vars['error_log'] = error_log_path + return init_vars + + +class PHPFPMAppForm(PluginDataForm): + php_version = forms.ChoiceField(label=_("PHP version"), + choices=settings.WEBAPPS_PHP_FPM_VERSIONS, + initial=settings.WEBAPPS_PHP_FPM_DEFAULT_VERSION) + + +class PHPFPMAppSerializer(serializers.Serializer): + php_version = serializers.ChoiceField(label=_("PHP version"), + choices=settings.WEBAPPS_PHP_FPM_VERSIONS, + default=settings.WEBAPPS_PHP_FPM_DEFAULT_VERSION) + + +class PHPFPMApp(PHPAppType): + name = 'php-fpm' + php_execution = PHPAppType.FPM + verbose_name = "PHP FPM" + help_text = _("This creates a PHP application under ~/webapps/<app_name>
" + "PHP-FPM will be used to execute PHP files.") + icon = 'orchestra/icons/apps/PHPFPM.png' + form = PHPFPMAppForm + serializer = PHPFPMAppSerializer + + def get_directive(self): + context = self.get_directive_context() + socket_type = 'unix' + if ':' in self.fpm_listen: + socket_type = 'tcp' + socket = self.fpm_listen % context + return ('fpm', socket_type, socket, self.instance.get_path()) + + +class PHPFCGIDAppForm(PluginDataForm): + php_version = forms.ChoiceField(label=_("PHP version"), + choices=settings.WEBAPPS_PHP_FCGID_VERSIONS, + initial=settings.WEBAPPS_PHP_FCGID_DEFAULT_VERSION) + + +class PHPFCGIDAppSerializer(serializers.Serializer): + php_version = serializers.ChoiceField(label=_("PHP version"), + choices=settings.WEBAPPS_PHP_FCGID_VERSIONS, + default=settings.WEBAPPS_PHP_FCGID_DEFAULT_VERSION) + + +class PHPFCGIDApp(PHPAppType): + name = 'php-fcgid' + php_execution = PHPAppType.FCGID + verbose_name = "PHP FCGID" + help_text = _("This creates a PHP application under ~/webapps/<app_name>
" + "Apache-mod-fcgid will be used to execute PHP files.") + icon = 'orchestra/icons/apps/PHPFCGI.png' + form = PHPFCGIDAppForm + serializer = PHPFCGIDAppSerializer + + def get_directive(self): + context = self.get_directive_context() + wrapper_path = os.path.normpath(settings.WEBAPPS_FCGID_PATH % context) + return ('fcgid', self.instance.get_path(), wrapper_path) + + def get_php_binary_path(self): + default_version = settings.WEBAPPS_PHP_FCGID_DEFAULT_VERSION + context = { + 'php_version': self.instance.data.get('php_version', default_version) + } + return os.path.normpath(settings.WEBAPPS_PHP_CGI_BINARY_PATH % context) + + def get_php_rc_path(self): + default_version = settings.WEBAPPS_PHP_FCGID_DEFAULT_VERSION + context = { + 'php_version': self.instance.data.get('php_version', default_version) + } + return os.path.normpath(settings.WEBAPPS_PHP_CGI_RC_PATH % context) + diff --git a/orchestra/apps/webapps/types/saas.py b/orchestra/apps/webapps/types/saas.py new file mode 100644 index 00000000..894032d3 --- /dev/null +++ b/orchestra/apps/webapps/types/saas.py @@ -0,0 +1,54 @@ +from django.utils.translation import ugettext_lazy as _ + +from . import AppType +from .. import settings + + +class WordPressMuApp(AppType): + name = 'wordpress-mu' + verbose_name = "WordPress (SaaS)" + directive = ('fpm', 'fcgi://127.0.0.1:8990/home/httpd/wordpress-mu/') + help_text = _("This creates a WordPress site on a multi-tenant WordPress server.
" + "By default this blog is accessible via <app_name>.blogs.orchestra.lan") + icon = 'orchestra/icons/apps/WordPressMu.png' + unique_name = True + option_groups = () + fpm_listen = settings.WEBAPPS_WORDPRESSMU_LISTEN + + +class DokuWikiMuApp(AppType): + name = 'dokuwiki-mu' + verbose_name = "DokuWiki (SaaS)" + directive = ('alias', '/home/httpd/wikifarm/farm/') + help_text = _("This create a DokuWiki wiki into a shared DokuWiki server.
" + "By default this wiki is accessible via <app_name>.wikis.orchestra.lan") + icon = 'orchestra/icons/apps/DokuWikiMu.png' + unique_name = True + option_groups = () + fpm_listen = settings.WEBAPPS_DOKUWIKIMU_LISTEN + + +class MoodleMuApp(AppType): + name = 'moodle-mu' + verbose_name = "Moodle (SaaS)" + directive = ('alias', '/home/httpd/wikifarm/farm/') + help_text = _("This create a Moodle site into a shared Moodle server.
" + "By default this wiki is accessible via <app_name>.moodle.orchestra.lan") + icon = 'orchestra/icons/apps/MoodleMu.png' + unique_name = True + option_groups = () + fpm_listen = settings.WEBAPPS_MOODLEMU_LISTEN + + +class DrupalMuApp(AppType): + name = 'drupal-mu' + verbose_name = "Drupdal (SaaS)" + directive = ('fpm', 'fcgi://127.0.0.1:8991/home/httpd/drupal-mu/') + help_text = _("This creates a Drupal site into a multi-tenant Drupal server.
" + "The installation will be completed after visiting " + "http://<app_name>.drupal.orchestra.lan/install.php?profile=standard
" + "By default this site will be accessible via <app_name>.drupal.orchestra.lan") + icon = 'orchestra/icons/apps/DrupalMu.png' + unique_name = True + option_groups = () + fpm_listen = settings.WEBAPPS_DRUPALMU_LISTEN diff --git a/orchestra/apps/webapps/types/wordpress.py b/orchestra/apps/webapps/types/wordpress.py new file mode 100644 index 00000000..b835a3a3 --- /dev/null +++ b/orchestra/apps/webapps/types/wordpress.py @@ -0,0 +1,123 @@ +from django import forms +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from orchestra.apps.databases.models import Database, DatabaseUser +from orchestra.plugins.forms import PluginDataForm +from orchestra.utils.python import random_ascii + +from .. import settings + +from .php import (PHPAppType, PHPFCGIDApp, PHPFPMApp, PHPFCGIDAppForm, PHPFCGIDAppSerializer, + PHPFPMAppForm, PHPFPMAppSerializer) + + +class WordPressAbstractAppForm(PluginDataForm): + db_name = forms.CharField(label=_("Database name"), + help_text=_("Database used for this webapp.")) + db_user = forms.CharField(label=_("Database user"),) + db_pass = forms.CharField(label=_("Database user password"), + help_text=_("Initial database password.")) + + +class WordPressAbstractAppSerializer(serializers.Serializer): + db_name = serializers.CharField(label=_("Database name"), required=False) + db_user = serializers.CharField(label=_("Database user"), required=False) + db_pass = serializers.CharField(label=_("Database user password"), required=False) + + +class WordPressAbstractApp(object): + icon = 'orchestra/icons/apps/WordPress.png' + change_readonly_fileds = ('db_name', 'db_user', 'db_pass',) + help_text = _("Visit http://<domain.lan>/wp-admin/install.php to finish the installation.") + + def get_db_name(self): + db_name = 'wp_%s_%s' % (self.instance.name, self.instance.account) + # Limit for mysql database names + return db_name[:65] + + def get_db_user(self): + db_name = self.get_db_name() + # Limit for mysql user names + return db_name[:16] + + def get_db_pass(self): + return random_ascii(10) + + def validate(self): + super(WordPressAbstractApp, self).validate() + create = not self.instance.pk + if create: + db = Database(name=self.get_db_name(), account=self.instance.account) + user = DatabaseUser(username=self.get_db_user(), password=self.get_db_pass(), + account=self.instance.account) + for obj in (db, user): + try: + obj.full_clean() + except ValidationError as e: + raise ValidationError({ + 'name': e.messages, + }) + + def save(self): + create = not self.instance.pk + if create: + db_name = self.get_db_name() + db_user = self.get_db_user() + db_pass = self.get_db_pass() + db = Database.objects.create(name=db_name, account=self.instance.account) + user = DatabaseUser(username=db_user, account=self.instance.account) + user.set_password(db_pass) + user.save() + db.users.add(user) + self.instance.data = { + 'db_name': db_name, + 'db_user': db_user, + 'db_pass': db_pass, + } + else: + # Trigger related backends + for related in self.get_related(): + related.save() + + def delete(self): + for related in self.get_related(): + related.delete() + + def get_related(self): + related = [] + account = self.instance.account + try: + db_user = account.databaseusers.get(username=self.instance.data.get('db_user')) + except DatabaseUser.DoesNotExist: + pass + else: + related.append(db_user) + try: + db = account.databases.get(name=self.instance.data.get('db_name')) + except Database.DoesNotExist: + pass + else: + related.append(db) + return related + + +class WordPressFPMApp(WordPressAbstractApp, PHPFPMApp): + name = 'wordpress-fpm' + php_execution = PHPAppType.FPM + verbose_name = "WordPress (FPM)" + serializer = type('WordPressFPMSerializer', + (WordPressAbstractAppSerializer, PHPFPMAppSerializer), {}) + change_form = type('WordPressFPMForm', + (WordPressAbstractAppForm, PHPFPMAppForm), {}) + + +class WordPressFCGIDApp(WordPressAbstractApp, PHPFCGIDApp): + name = 'wordpress-fcgid' + php_execution = PHPAppType.FCGID + verbose_name = "WordPress (FCGID)" + serializer = type('WordPressFCGIDSerializer', + (WordPressAbstractAppSerializer, PHPFCGIDAppSerializer), {}) + change_form = type('WordPressFCGIDForm', + (WordPressAbstractAppForm, PHPFCGIDAppForm), {}) diff --git a/orchestra/apps/websites/backends/apache.py b/orchestra/apps/websites/backends/apache.py index 2f8b5428..f117e02f 100644 --- a/orchestra/apps/websites/backends/apache.py +++ b/orchestra/apps/websites/backends/apache.py @@ -1,6 +1,6 @@ -import textwrap import os import re +import textwrap from django.template import Template, Context from django.utils.translation import ugettext_lazy as _ diff --git a/orchestra/plugins/options.py b/orchestra/plugins/options.py index f3bffaba..3c97d951 100644 --- a/orchestra/plugins/options.py +++ b/orchestra/plugins/options.py @@ -8,6 +8,10 @@ class Plugin(object): icon = None change_readonly_fileds = () + def __init__(self, instance=None): + # Related model instance of this plugin + self.instance = instance + @classmethod def get_name(cls): return getattr(cls, 'name', cls.__name__)