From 40930a480eb526539272880f3deffc1b4b913a67 Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Mon, 23 Mar 2015 15:36:51 +0000 Subject: [PATCH] Refactores webapps and SaaS --- TODO.md | 37 ++++-- orchestra/admin/options.py | 5 +- orchestra/apps/accounts/admin.py | 7 + orchestra/apps/mailboxes/backends.py | 2 +- orchestra/apps/orchestration/backends.py | 1 + orchestra/apps/orchestration/manager.py | 9 +- orchestra/apps/orchestration/methods.py | 11 +- orchestra/apps/payments/methods/options.py | 21 +-- orchestra/apps/resources/admin.py | 5 +- orchestra/apps/resources/models.py | 3 +- orchestra/apps/saas/admin.py | 6 +- .../{webapps => saas}/backends/dokuwikimu.py | 0 .../{webapps => saas}/backends/drupalmu.py | 0 orchestra/apps/saas/backends/phplist.py | 45 +++++++ orchestra/apps/saas/backends/wordpressmu.py | 123 ++++++++++++++++++ orchestra/apps/saas/models.py | 29 ++++- orchestra/apps/saas/services/bscw.py | 4 +- orchestra/apps/saas/services/dokuwiki.py | 6 + orchestra/apps/saas/services/drupal.py | 6 + orchestra/apps/saas/services/options.py | 46 +++---- orchestra/apps/saas/services/phplist.py | 88 ++++++++++++- orchestra/apps/saas/services/wordpress.py | 23 ++++ orchestra/apps/saas/settings.py | 40 ++++++ orchestra/apps/webapps/admin.py | 12 +- orchestra/apps/webapps/backends/php.py | 6 +- .../apps/webapps/backends/wordpressmu.py | 10 +- orchestra/apps/webapps/models.py | 2 +- orchestra/apps/webapps/settings.py | 38 ------ orchestra/apps/webapps/types/__init__.py | 30 +---- orchestra/apps/webapps/types/php.py | 17 ++- orchestra/apps/webapps/types/saas.py | 54 -------- orchestra/apps/webapps/types/wordpress.py | 13 +- orchestra/apps/websites/admin.py | 5 - orchestra/apps/websites/apps.py | 17 +++ orchestra/apps/websites/backends/apache.py | 47 ++++--- orchestra/apps/websites/directives.py | 53 ++++---- orchestra/apps/websites/models.py | 1 + orchestra/apps/websites/serializers.py | 8 +- orchestra/apps/websites/settings.py | 15 ++- orchestra/plugins/forms.py | 6 +- orchestra/plugins/options.py | 33 ++++- orchestra/utils/python.py | 13 ++ 42 files changed, 618 insertions(+), 279 deletions(-) rename orchestra/apps/{webapps => saas}/backends/dokuwikimu.py (100%) rename orchestra/apps/{webapps => saas}/backends/drupalmu.py (100%) create mode 100644 orchestra/apps/saas/backends/phplist.py create mode 100644 orchestra/apps/saas/backends/wordpressmu.py create mode 100644 orchestra/apps/saas/services/dokuwiki.py create mode 100644 orchestra/apps/saas/services/drupal.py create mode 100644 orchestra/apps/saas/services/wordpress.py delete mode 100644 orchestra/apps/webapps/types/saas.py create mode 100644 orchestra/apps/websites/apps.py diff --git a/TODO.md b/TODO.md index 38030156..f1fc95e5 100644 --- a/TODO.md +++ b/TODO.md @@ -147,7 +147,6 @@ * Resource graph for each related object -* Rename apache logs ending on .log in order to logrotate easily * multitenant webapps modeled on WepApp -> name unique for all accounts @@ -193,9 +192,6 @@ Php binaries should have this format: /usr/bin/php5.2-cgi * and other IfModule on backend SecRule -* monitor in batches doesnt work!!! - - * Orchestra global search box on the header, based https://github.com/django/django/blob/master/django/contrib/admin/options.py#L866 and iterating over all registered services and inspectin its admin.search_fields @@ -214,17 +210,10 @@ Php binaries should have this format: /usr/bin/php5.2-cgi * Display admin.is_active (disabled account/order by) -* show details data on webapp changelist - * lock resource monitoring -* Optimize backends like mail backend (log files single read), single "/var/log/vsftpd.log{,.1}" on ftp traffic - - * -EXecCGI in common CMS upload locations /wp-upload/upload/uploads * cgi user / pervent shell access -* merge php wrapper configuration to optimize process classes - * prevent stderr when users exists on backend i.e. mysql user create @@ -235,3 +224,29 @@ Php binaries should have this format: /usr/bin/php5.2-cgi * php-fpm disable execCGI * SuexecUserGroup needs to be per app othewise wrapper/fpm user can't be correct + +* wprdess-mu saas app that create a Website object???? + +* tags = GenericRelation(TaggedItem, related_query_name='bookmarks') + +* make home for all systemusers (/home/username) and fix monitors + +* user provided crons + +* ```1 {s+=$1} END {print s}' $1 || echo 0 + awk 'BEGIN { size = 0 } NR > 1 { size += $1 } END { print size }' $1 || echo 0 }""")) def monitor(self, mailbox): diff --git a/orchestra/apps/orchestration/backends.py b/orchestra/apps/orchestration/backends.py index cd0d09f8..46a070a6 100644 --- a/orchestra/apps/orchestration/backends.py +++ b/orchestra/apps/orchestration/backends.py @@ -35,6 +35,7 @@ class ServiceBackend(plugins.Plugin): ignore_fields = [] actions = [] default_route_match = 'True' + block = False # Force the backend manager to block in multiple backend executions and execute them synchronously __metaclass__ = ServiceMount diff --git a/orchestra/apps/orchestration/manager.py b/orchestra/apps/orchestration/manager.py index 69ca0f4e..66694792 100644 --- a/orchestra/apps/orchestration/manager.py +++ b/orchestra/apps/orchestration/manager.py @@ -1,6 +1,7 @@ import logging import threading import traceback +from collections import OrderedDict from django import db from django.core.mail import mail_admins @@ -51,8 +52,9 @@ def close_connection(execute): def execute(operations, async=False): """ generates and executes the operations on the servers """ - scripts = {} + scripts = OrderedDict() cache = {} + block = False # Generate scripts per server+backend for operation in operations: logger.debug("Queued %s" % str(operation)) @@ -77,6 +79,8 @@ def execute(operations, async=False): pre_action.send(**kwargs) method(operation.instance) post_action.send(**kwargs) + if backend.block: + block = True # Execute scripts on each server threads = [] executions = [] @@ -88,8 +92,11 @@ def execute(operations, async=False): execute = close_connection(execute) # DEBUG: substitute all thread related stuff for this function #execute(server, async=async) + logger.debug('%s is going to be executed on %s' % (backend, server)) thread = threading.Thread(target=execute, args=(server,), kwargs={'async': async}) thread.start() + if block: + thread.join() threads.append(thread) executions.append((execute, operations)) [ thread.join() for thread in threads ] diff --git a/orchestra/apps/orchestration/methods.py b/orchestra/apps/orchestration/methods.py index 346fe60b..36f6f9e2 100644 --- a/orchestra/apps/orchestration/methods.py +++ b/orchestra/apps/orchestration/methods.py @@ -10,6 +10,8 @@ import paramiko from celery.datastructures import ExceptionInfo from django.conf import settings as djsettings +from orchestra.utils.python import CaptureStdout + from . import settings @@ -37,7 +39,6 @@ def SSH(backend, log, server, cmds, async=False): channel = None ssh = None try: - logger.debug('%s is going to be executed on %s' % (backend, server)) # Avoid "Argument list too long" on large scripts by genereting a file # and scping it to the remote server with os.fdopen(os.open(path, os.O_WRONLY | os.O_CREAT, 0600), 'w') as handle: @@ -129,15 +130,19 @@ def Python(backend, log, server, cmds, async=False): log.save(update_fields=['script']) try: for cmd in cmds: - result = cmd(server) - log.stdout += str(result) + with CaptureStdout() as stdout: + result = cmd(server) + for line in stdout: + log.stdout += unicode(line, errors='replace') + '\n' if async: log.save(update_fields=['stdout']) except: log.exit_code = 1 log.state = log.FAILURE log.traceback = ExceptionInfo(sys.exc_info()).traceback + logger.error('Exception while executing %s on %s' % (backend, server)) else: log.exit_code = 0 log.state = log.SUCCESS + logger.debug('%s execution state on %s is %s' % (backend, server, log.state)) log.save() diff --git a/orchestra/apps/payments/methods/options.py b/orchestra/apps/payments/methods/options.py index 5ac1e0d8..e69634f5 100644 --- a/orchestra/apps/payments/methods/options.py +++ b/orchestra/apps/payments/methods/options.py @@ -13,9 +13,8 @@ class PaymentMethod(plugins.Plugin): label_field = 'label' number_field = 'number' process_credit = False - form = None - serializer = None due_delta = relativedelta.relativedelta(months=1) + plugin_field = 'method' @classmethod @cached @@ -25,24 +24,6 @@ class PaymentMethod(plugins.Plugin): plugins.append(import_class(cls)) return plugins - @classmethod - def clean_data(cls): - """ model clean, uses cls.serializer by default """ - serializer = cls.serializer(data=self.instance.data) - if not serializer.is_valid(): - serializer.errors.pop('non_field_errors', None) - raise ValidationError(serializer.errors) - return serializer.data - - def get_form(self): - self.form.plugin = self - self.form.plugin_field = 'method' - return self.form - - def get_serializer(self): - self.serializer.plugin = self - return self.serializer - def get_label(self): return self.instance.data[self.label_field] diff --git a/orchestra/apps/resources/admin.py b/orchestra/apps/resources/admin.py index a61b449b..47824fd5 100644 --- a/orchestra/apps/resources/admin.py +++ b/orchestra/apps/resources/admin.py @@ -182,15 +182,16 @@ def resource_inline_factory(resources): return len(resources) def get_queryset(self): + """ Filter disabled resources """ queryset = super(ResourceInlineFormSet, self).get_queryset() - return queryset.order_by('-id').filter(resource__is_active=True) + return queryset.filter(resource__is_active=True) @cached_property def forms(self, resources=resources): forms = [] resources_copy = list(resources) # Remove queryset disabled objects - queryset = [data for data in self.queryset if data.resource in resources] + queryset = [data for data in self.get_queryset() if data.resource in resources] if self.instance.pk: # Create missing resource data queryset_resources = [data.resource for data in queryset] diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py index 24e0d3fd..cbc77286 100644 --- a/orchestra/apps/resources/models.py +++ b/orchestra/apps/resources/models.py @@ -170,6 +170,7 @@ class ResourceData(models.Model): updated_at = models.DateTimeField(_("updated"), null=True, editable=False) allocated = models.DecimalField(_("allocated"), max_digits=8, decimal_places=2, null=True, blank=True) + content_object = GenericForeignKey() class Meta: @@ -326,9 +327,9 @@ def create_resource_relation(): field for field in related._meta.virtual_fields if field.rel.to != ResourceData ] - relation = GenericRelation('resources.ResourceData') for ct, resources in Resource.objects.group_by('content_type').iteritems(): model = ct.model_class() + relation = GenericRelation('resources.ResourceData') model.add_to_class('resource_set', relation) model.resources = ResourceHandler() Resource._related.add(model) diff --git a/orchestra/apps/saas/admin.py b/orchestra/apps/saas/admin.py index 4d1f33eb..197fd39e 100644 --- a/orchestra/apps/saas/admin.py +++ b/orchestra/apps/saas/admin.py @@ -1,6 +1,8 @@ from django.contrib import admin -from django.utils.translation import ugettext_lazy as _ +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext, ugettext_lazy as _ +from orchestra.admin import ExtendedModelAdmin from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.plugins.admin import SelectPluginAdminMixin @@ -8,7 +10,7 @@ from .models import SaaS from .services import SoftwareService -class SaaSAdmin(SelectPluginAdminMixin, AccountAdminMixin, admin.ModelAdmin): +class SaaSAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin): list_display = ('username', 'service', 'display_site_name', 'account_link') list_filter = ('service',) plugin = SoftwareService diff --git a/orchestra/apps/webapps/backends/dokuwikimu.py b/orchestra/apps/saas/backends/dokuwikimu.py similarity index 100% rename from orchestra/apps/webapps/backends/dokuwikimu.py rename to orchestra/apps/saas/backends/dokuwikimu.py diff --git a/orchestra/apps/webapps/backends/drupalmu.py b/orchestra/apps/saas/backends/drupalmu.py similarity index 100% rename from orchestra/apps/webapps/backends/drupalmu.py rename to orchestra/apps/saas/backends/drupalmu.py diff --git a/orchestra/apps/saas/backends/phplist.py b/orchestra/apps/saas/backends/phplist.py new file mode 100644 index 00000000..49bc6d49 --- /dev/null +++ b/orchestra/apps/saas/backends/phplist.py @@ -0,0 +1,45 @@ +import json +import re + +import requests +from django.utils.translation import ugettext_lazy as _ + +from orchestra.apps.orchestration import ServiceController + +from .. import settings + + +class PhpListSaaSBackend(ServiceController): + verbose_name = _("phpList SaaS") + model = 'saas.SaaS' + default_route_match = "saas.service == 'phplist'" + block = True + + def initialize_database(self, saas, server): + base_domain = settings.SAAS_PHPLIST_BASE_DOMAIN + admin_link = 'http://%s.%s/admin/' % (saas.get_site_name(), base_domain) + admin_content = requests.get(admin_link).content + if admin_content.startswith('Cannot connect to Database'): + raise RuntimeError("Database is not yet configured") + install = re.search(r'([^"]+firstinstall[^"]+)', admin_content) + if install: + if not saas.password: + raise RuntimeError("Password is missing") + install = install.groups()[0] + install_link = admin_link + install[1:] + post = { + 'adminname': saas.username, + 'orgname': saas.account.username, + 'adminemail': saas.account.username, + 'adminpassword': saas.password, + } + print json.dumps(post, indent=4) + response = requests.post(install_link, data=post) + print response.content + if response.status_code != 200: + raise RuntimeError("Bad status code %i" % response.status_code) + elif saas.password: + raise NotImplementedError + + def save(self, saas): + self.append(self.initialize_database, saas) diff --git a/orchestra/apps/saas/backends/wordpressmu.py b/orchestra/apps/saas/backends/wordpressmu.py new file mode 100644 index 00000000..e0283611 --- /dev/null +++ b/orchestra/apps/saas/backends/wordpressmu.py @@ -0,0 +1,123 @@ +import re + +import requests +from django.utils.translation import ugettext_lazy as _ + +from orchestra.apps.orchestration import ServiceController + +from .. import settings + + +class WordpressMuBackend(ServiceController): + verbose_name = _("Wordpress multisite") + model = 'webapps.WebApp' + default_route_match = "webapp.type == 'wordpress-mu'" + + @property + def script(self): + return self.cmds + + def login(self, session): + base_url = self.get_base_url() + login_url = base_url + '/wp-login.php' + login_data = { + 'log': 'admin', + 'pwd': settings.WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD, + 'redirect_to': '/wp-admin/' + } + response = session.post(login_url, data=login_data) + if response.url != base_url + '/wp-admin/': + raise IOError("Failure login to remote application") + + def get_base_url(self): + base_url = settings.WEBAPPS_WORDPRESSMU_BASE_URL + return base_url.rstrip('/') + + def validate_response(self, response): + if response.status_code != 200: + errors = re.findall(r'\n\t

(.*)

', response.content) + raise RuntimeError(errors[0] if errors else 'Unknown %i error' % response.status_code) + + def get_id(self, session, webapp): + search = self.get_base_url() + search += '/wp-admin/network/sites.php?s=%s&action=blogs' % webapp.name + regex = re.compile( + '%s' % webapp.name + ) + content = session.get(search).content + # Get id + ids = regex.search(content) + if not ids: + raise RuntimeError("Blog '%s' not found" % webapp.name) + ids = ids.groups() + if len(ids) > 1: + raise ValueError("Multiple matches") + # Get wpnonce + wpnonce = re.search(r'(.*)', content).groups()[0] + wpnonce = re.search(r'_wpnonce=([^"]*)"', wpnonce).groups()[0] + return int(ids[0]), wpnonce + + def create_blog(self, webapp, server): + session = requests.Session() + self.login(session) + + # Check if blog already exists + try: + self.get_id(session, webapp) + except RuntimeError: + url = self.get_base_url() + url += '/wp-admin/network/site-new.php' + content = session.get(url).content + + wpnonce = re.compile('name="_wpnonce_add-blog"\s+value="([^"]*)"') + wpnonce = wpnonce.search(content).groups()[0] + + url += '?action=add-site' + data = { + 'blog[domain]': webapp.name, + 'blog[title]': webapp.name, + 'blog[email]': webapp.account.email, + '_wpnonce_add-blog': wpnonce, + } + + # Validate response + response = session.post(url, data=data) + self.validate_response(response) + + def delete_blog(self, webapp, server): + session = requests.Session() + self.login(session) + + try: + id, wpnonce = self.get_id(session, webapp) + except RuntimeError: + pass + else: + delete = self.get_base_url() + delete += '/wp-admin/network/sites.php?action=confirm&action2=deleteblog' + delete += '&id=%d&_wpnonce=%s' % (id, wpnonce) + + content = session.get(delete).content + wpnonce = re.compile('name="_wpnonce"\s+value="([^"]*)"') + wpnonce = wpnonce.search(content).groups()[0] + data = { + 'action': 'deleteblog', + 'id': id, + '_wpnonce': wpnonce, + '_wp_http_referer': '/wp-admin/network/sites.php', + } + delete = self.get_base_url() + delete += '/wp-admin/network/sites.php?action=deleteblog' + response = session.post(delete, data=data) + self.validate_response(response) + + def save(self, webapp): + if webapp.type != 'wordpress-mu': + return + self.append(self.create_blog, webapp) + + def delete(self, webapp): + if webapp.type != 'wordpress-mu': + return + self.append(self.delete_blog, webapp) diff --git a/orchestra/apps/saas/models.py b/orchestra/apps/saas/models.py index b059f103..d764bdef 100644 --- a/orchestra/apps/saas/models.py +++ b/orchestra/apps/saas/models.py @@ -1,4 +1,6 @@ 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 @@ -12,20 +14,21 @@ from .services import SoftwareService class SaaS(models.Model): service = models.CharField(_("service"), max_length=32, choices=SoftwareService.get_plugin_choices()) - username = models.CharField(_("username"), max_length=64, + username = models.CharField(_("name"), max_length=64, help_text=_("Required. 64 characters or fewer. Letters, digits and ./-/_ only."), validators=[validators.validate_username]) - site_name = NullableCharField(_("site name"), max_length=32, null=True) +# site_name = NullableCharField(_("site name"), max_length=32, null=True) account = models.ForeignKey('accounts.Account', verbose_name=_("account"), related_name='saas') - data = JSONField(_("data"), help_text=_("Extra information dependent of each service.")) + data = JSONField(_("data"), default={}, + help_text=_("Extra information dependent of each service.")) class Meta: verbose_name = "SaaS" verbose_name_plural = "SaaS" unique_together = ( ('username', 'service'), - ('site_name', 'service'), +# ('site_name', 'service'), ) def __unicode__(self): @@ -49,4 +52,22 @@ class SaaS(models.Model): def set_password(self, password): self.password = password + services.register(SaaS) + + +# Admin bulk deletion doesn't call model.delete() +# So, signals are used instead of model method overriding + +@receiver(pre_save, sender=SaaS, dispatch_uid='saas.service.save') +def type_save(sender, *args, **kwargs): + instance = kwargs['instance'] + instance.service_instance.save() + +@receiver(pre_delete, sender=SaaS, dispatch_uid='saas.service.delete') +def type_delete(sender, *args, **kwargs): + instance = kwargs['instance'] + try: + instance.service_instance.delete() + except KeyError: + pass diff --git a/orchestra/apps/saas/services/bscw.py b/orchestra/apps/saas/services/bscw.py index 877c61c4..cc462aae 100644 --- a/orchestra/apps/saas/services/bscw.py +++ b/orchestra/apps/saas/services/bscw.py @@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from .. import settings from .options import SoftwareService, SoftwareServiceForm @@ -19,10 +20,11 @@ class BSCWDataSerializer(serializers.Serializer): class BSCWService(SoftwareService): + name = 'bscw' verbose_name = "BSCW" form = BSCWForm serializer = BSCWDataSerializer icon = 'orchestra/icons/apps/BSCW.png' # TODO override from settings - site_name = 'bascw.orchestra.lan' + site_name = settings.SAAS_BSCW_DOMAIN change_readonly_fileds = ('email',) diff --git a/orchestra/apps/saas/services/dokuwiki.py b/orchestra/apps/saas/services/dokuwiki.py new file mode 100644 index 00000000..a89eeb46 --- /dev/null +++ b/orchestra/apps/saas/services/dokuwiki.py @@ -0,0 +1,6 @@ +from .options import SoftwareService + + +class DokuWikiService(SoftwareService): + verbose_name = "Dowkuwiki" + icon = 'orchestra/icons/apps/Dokuwiki.png' diff --git a/orchestra/apps/saas/services/drupal.py b/orchestra/apps/saas/services/drupal.py new file mode 100644 index 00000000..285a4e6b --- /dev/null +++ b/orchestra/apps/saas/services/drupal.py @@ -0,0 +1,6 @@ +from .options import SoftwareService + + +class DrupalService(SoftwareService): + verbose_name = "Drupal" + icon = 'orchestra/icons/apps/Drupal.png' diff --git a/orchestra/apps/saas/services/options.py b/orchestra/apps/saas/services/options.py index 9ba21b89..a192b83c 100644 --- a/orchestra/apps/saas/services/options.py +++ b/orchestra/apps/saas/services/options.py @@ -14,9 +14,10 @@ from .. import settings class SoftwareServiceForm(PluginDataForm): + site_name = forms.CharField(widget=widgets.ShowTextWidget, required=False) password = forms.CharField(label=_("Password"), required=False, widget=widgets.ReadOnlyWidget('Unknown password'), - help_text=_("Servide passwords are not stored, so there is no way to see this " + help_text=_("Passwords are not stored, so there is no way to see this " "service's password, but you can change the password using " "this form.")) password1 = forms.CharField(label=_("Password"), validators=[validators.validate_password], @@ -38,13 +39,16 @@ class SoftwareServiceForm(PluginDataForm): self.fields['password'].widget = forms.HiddenInput() site_name = self.plugin.site_name if site_name: - link = '%s' % (site_name, site_name) - self.fields['site_name'].widget = widgets.ReadOnlyWidget(site_name, mark_safe(link)) - self.fields['site_name'].required = False + site_name_link = '%s' % (site_name, site_name) else: - base_name = self.plugin.site_name_base_domain - help_text = _("The final URL would be <site_name>.%s") % base_name - self.fields['site_name'].help_text = help_text + site_name_link = '<name>.%s' % self.plugin.site_name_base_domain + self.fields['site_name'].initial = site_name_link +## self.fields['site_name'].widget = widgets.ReadOnlyWidget(site_name, mark_safe(link)) +## self.fields['site_name'].required = False +# else: +# base_name = self.plugin.site_name_base_domain +# help_text = _("The final URL would be <site_name>.%s") % base_name +# self.fields['site_name'].help_text = help_text def clean_password2(self): if not self.is_change: @@ -69,12 +73,13 @@ class SoftwareServiceForm(PluginDataForm): class SoftwareService(plugins.Plugin): form = SoftwareServiceForm - serializer = None site_name = None site_name_base_domain = 'orchestra.lan' + has_custom_domain = False icon = 'orchestra/icons/apps.png' - change_readonly_fileds = ('username',) + change_readonly_fileds = ('site_name',) class_verbose_name = _("Software as a Service") + plugin_field = 'service' @classmethod @cached @@ -84,27 +89,18 @@ class SoftwareService(plugins.Plugin): plugins.append(import_class(cls)) return plugins - def clean_data(cls): - """ model clean, uses cls.serizlier by default """ - serializer = cls.serializer(data=self.instance.data) - if not serializer.is_valid(): - raise ValidationError(serializer.errors) - return serializer.data - @classmethod def get_change_readonly_fileds(cls): - return cls.change_readonly_fileds + ('username',) + fields = super(SoftwareService, cls).get_change_readonly_fileds() + return fields + ('username',) def get_site_name(self): return self.site_name or '.'.join( - (self.instance.site_name, self.site_name_base_domain) + (self.instance.username, self.site_name_base_domain) ) - def get_form(self): - self.form.plugin = self - self.form.plugin_field = 'service' - return self.form + def save(self): + pass - def get_serializer(self): - self.serializer.plugin = self - return self.serializer + def delete(self): + pass diff --git a/orchestra/apps/saas/services/phplist.py b/orchestra/apps/saas/services/phplist.py index 346da96a..9ed80994 100644 --- a/orchestra/apps/saas/services/phplist.py +++ b/orchestra/apps/saas/services/phplist.py @@ -1,14 +1,100 @@ from django import forms +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from orchestra.apps.databases.models import Database, DatabaseUser +from orchestra.forms import widgets +from orchestra.plugins.forms import PluginDataForm + +from .. import settings from .options import SoftwareService, SoftwareServiceForm class PHPListForm(SoftwareServiceForm): - email = forms.EmailField(label=_("Email"), widget=forms.TextInput(attrs={'size':'40'})) + admin_username = forms.CharField(label=_("Admin username"), required=False, + widget=widgets.ReadOnlyWidget('admin')) + + def __init__(self, *args, **kwargs): + super(PHPListForm, self).__init__(*args, **kwargs) + self.fields['username'].label = _("Name") + base_domain = self.plugin.site_name_base_domain + help_text = _("Admin URL http://<name>.{}/admin/").format(base_domain) + self.fields['site_name'].help_text = help_text + + +class PHPListChangeForm(PHPListForm): +# site_name = forms.CharField(widget=widgets.ShowTextWidget, required=False) + db_name = forms.CharField(label=_("Database name"), + help_text=_("Database used for this webapp.")) + + def __init__(self, *args, **kwargs): + super(PHPListChangeForm, self).__init__(*args, **kwargs) + site_name = self.instance.get_site_name() + admin_url = "http://%s/admin/" % site_name + help_text = _("Admin URL {0}").format(admin_url) + self.fields['site_name'].help_text = help_text + + +class PHPListSerializer(serializers.Serializer): + db_name = serializers.CharField(label=_("Database name"), required=False) class PHPListService(SoftwareService): + name = 'phplist' verbose_name = "phpList" form = PHPListForm + change_form = PHPListChangeForm + change_readonly_fileds = ('db_name',) + serializer = PHPListSerializer icon = 'orchestra/icons/apps/Phplist.png' + site_name_base_domain = settings.SAAS_PHPLIST_BASE_DOMAIN + + def get_db_name(self): + db_name = 'phplist_mu_%s' % self.instance.username + # Limit for mysql database names + return db_name[:65] + + def get_db_user(self): + return settings.SAAS_PHPLIST_DB_NAME + + def validate(self): + super(PHPListService, self).validate() + create = not self.instance.pk + if create: + db = Database(name=self.get_db_name(), account=self.instance.account) + try: + db.full_clean() + except ValidationError as e: + raise ValidationError({ + 'name': e.messages, + }) + + def save(self): + db_name = self.get_db_name() + db_user = self.get_db_user() + db, db_created = Database.objects.get_or_create(name=db_name, account=self.instance.account) + user = DatabaseUser.objects.get(username=db_user) + db.users.add(user) + self.instance.data = { + 'db_name': db_name, + } + if not db_created: + # Trigger related backends + for related in self.get_related(): + related.save(update_fields=[]) + + def delete(self): + for related in self.get_related(): + related.delete() + + def get_related(self): + related = [] + account = self.instance.account + try: + db = account.databases.get(name=self.instance.data.get('db_name')) + except Database.DoesNotExist: + pass + else: + related.append(db) + return related diff --git a/orchestra/apps/saas/services/wordpress.py b/orchestra/apps/saas/services/wordpress.py new file mode 100644 index 00000000..ac61ec5a --- /dev/null +++ b/orchestra/apps/saas/services/wordpress.py @@ -0,0 +1,23 @@ +from django import forms +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from .options import SoftwareService, SoftwareServiceForm + + +class WordPressForm(SoftwareServiceForm): + email = forms.EmailField(label=_("Email"), widget=forms.TextInput(attrs={'size':'40'})) + + +class WordPressDataSerializer(serializers.Serializer): + email = serializers.EmailField(label=_("Email")) + + +class WordPressService(SoftwareService): + verbose_name = "WordPress" + form = WordPressForm + serializer = WordPressDataSerializer + icon = 'orchestra/icons/apps/WordPress.png' + site_name_base_domain = 'blogs.orchestra.lan' + change_readonly_fileds = ('email',) diff --git a/orchestra/apps/saas/settings.py b/orchestra/apps/saas/settings.py index ef5257e8..971ff0f0 100644 --- a/orchestra/apps/saas/settings.py +++ b/orchestra/apps/saas/settings.py @@ -6,4 +6,44 @@ SAAS_ENABLED_SERVICES = getattr(settings, 'SAAS_ENABLED_SERVICES', ( 'orchestra.apps.saas.services.bscw.BSCWService', 'orchestra.apps.saas.services.gitlab.GitLabService', 'orchestra.apps.saas.services.phplist.PHPListService', + 'orchestra.apps.saas.services.wordpress.WordPressService', + 'orchestra.apps.saas.services.dokuwiki.DokuWikiService', + 'orchestra.apps.saas.services.drupal.DrupalService', )) + + +SAAS_WORDPRESS_ADMIN_PASSWORD = getattr(settings, 'SAAS_WORDPRESSMU_ADMIN_PASSWORD', + 'secret' +) + +SAAS_WORDPRESS_BASE_URL = getattr(settings, 'SAAS_WORDPRESS_BASE_URL', + 'http://blogs.orchestra.lan/' +) + + +SAAS_DOKUWIKI_TEMPLATE_PATH = getattr(settings, 'SAAS_DOKUWIKI_TEMPLATE_PATH', + '/home/httpd/htdocs/wikifarm/template.tar.gz') + +SAAS_DOKUWIKI_FARM_PATH = getattr(settings, 'WEBSITES_DOKUWIKI_FARM_PATH', + '/home/httpd/htdocs/wikifarm/farm' +) + +SAAS_DRUPAL_SITES_PATH = getattr(settings, 'WEBSITES_DRUPAL_SITES_PATH', + '/home/httpd/htdocs/drupal-mu/sites/%(site_name)s' +) + + +SAAS_PHPLIST_DB_NAME = getattr(settings, 'SAAS_PHPLIST_DB_NAME', + 'phplist_mu' +) + +SAAS_PHPLIST_BASE_DOMAIN = getattr(settings, 'SAAS_PHPLIST_BASE_DOMAIN', + 'lists.orchestra.lan' +) + + +SAAS_BSCW_DOMAIN = getattr(settings, 'SAAS_BSCW_DOMAIN', + 'bscw.orchestra.lan' +) + + diff --git a/orchestra/apps/webapps/admin.py b/orchestra/apps/webapps/admin.py index b151a0ff..95ad696d 100644 --- a/orchestra/apps/webapps/admin.py +++ b/orchestra/apps/webapps/admin.py @@ -48,21 +48,15 @@ class WebAppOptionInline(admin.TabularInline): class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin): list_display = ('name', 'type', 'display_detail', 'display_websites', 'account_link') list_filter = ('type',) -# add_fields = ('account', 'name', 'type') -# fields = ('account_link', 'name', 'type') inlines = [WebAppOptionInline] - readonly_fields = ('account_link',) - change_readonly_fields = ('name', 'type') - search_fuelds = ('name', 'account__username') + readonly_fields = ('account_link', ) + change_readonly_fields = ('name', 'type', 'display_websites') + search_fields = ('name', 'account__username', 'data', 'website__domains__name') list_prefetch_related = ('content_set__website',) plugin = AppType plugin_field = 'type' plugin_title = _("Web application type") -# TYPE_HELP_TEXT = { -# app.get_name(): str(unicode(app.help_text)) for app in App.get_plugins() -# } - def display_websites(self, webapp): websites = [] for content in webapp.content_set.all(): diff --git a/orchestra/apps/webapps/backends/php.py b/orchestra/apps/webapps/backends/php.py index 6388cba1..33ede752 100644 --- a/orchestra/apps/webapps/backends/php.py +++ b/orchestra/apps/webapps/backends/php.py @@ -13,6 +13,7 @@ from .. import settings class PHPBackend(WebAppServiceMixin, ServiceController): verbose_name = _("PHP FPM/FCGID") default_route_match = "webapp.type == 'php'" + MERGE = settings.WEBAPPS_MERGE_PHP_WEBAPPS def save(self, webapp): context = self.get_context(webapp) @@ -89,8 +90,9 @@ class PHPBackend(WebAppServiceMixin, ServiceController): ) def get_fpm_config(self, webapp, context): + merge = settings.WEBAPPS_MERGE_PHP_WEBAPPS context.update({ - 'init_vars': webapp.type_instance.get_php_init_vars(), + 'init_vars': webapp.type_instance.get_php_init_vars(merge=self.MERGE), 'max_children': webapp.get_options().get('processes', False), 'request_terminate_timeout': webapp.get_options().get('timeout', False), }) @@ -116,7 +118,7 @@ class PHPBackend(WebAppServiceMixin, ServiceController): def get_fcgid_wrapper(self, webapp, context): opt = webapp.type_instance # Format PHP init vars - init_vars = opt.get_php_init_vars() + init_vars = opt.get_php_init_vars(merge=self.MERGE) if init_vars: init_vars = [ '-d %s="%s"' % (k,v) for k,v in init_vars.iteritems() ] init_vars = ', '.join(init_vars) diff --git a/orchestra/apps/webapps/backends/wordpressmu.py b/orchestra/apps/webapps/backends/wordpressmu.py index 690d1b38..1aa75203 100644 --- a/orchestra/apps/webapps/backends/wordpressmu.py +++ b/orchestra/apps/webapps/backends/wordpressmu.py @@ -10,8 +10,8 @@ from .. import settings class WordpressMuBackend(ServiceController): verbose_name = _("Wordpress multisite") - model = 'webapps.WebApp' - default_route_match = "webapp.type == 'wordpress-mu'" + model = 'saas.SaaS' + default_route_match = "saas.service == 'wordpress-mu'" @property def script(self): @@ -22,7 +22,7 @@ class WordpressMuBackend(ServiceController): login_url = base_url + '/wp-login.php' login_data = { 'log': 'admin', - 'pwd': settings.WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD, + 'pwd': settings.WEBSITES_WORDPRESSMU_ADMIN_PASSWORD, 'redirect_to': '/wp-admin/' } response = session.post(login_url, data=login_data) @@ -30,7 +30,7 @@ class WordpressMuBackend(ServiceController): raise IOError("Failure login to remote application") def get_base_url(self): - base_url = settings.WEBAPPS_WORDPRESSMU_BASE_URL + base_url = settings.WEBSITES_WORDPRESSMU_BASE_URL return base_url.rstrip('/') def validate_response(self, response): @@ -86,8 +86,6 @@ class WordpressMuBackend(ServiceController): self.validate_response(response) def delete_blog(self, webapp, server): - # OH, I've enjoied so much coding this methods that I want to thank - # the wordpress team for the excellent software they are producing session = requests.Session() self.login(session) diff --git a/orchestra/apps/webapps/models.py b/orchestra/apps/webapps/models.py index d35e9017..0372f3dc 100644 --- a/orchestra/apps/webapps/models.py +++ b/orchestra/apps/webapps/models.py @@ -24,7 +24,7 @@ class WebApp(models.Model): choices=AppType.get_plugin_choices()) account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), related_name='webapps') - data = JSONField(_("data"), blank=True, + data = JSONField(_("data"), blank=True, default={}, help_text=_("Extra information dependent of each service.")) class Meta: diff --git a/orchestra/apps/webapps/settings.py b/orchestra/apps/webapps/settings.py index c951ae90..f7dc8d72 100644 --- a/orchestra/apps/webapps/settings.py +++ b/orchestra/apps/webapps/settings.py @@ -38,9 +38,6 @@ WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', ( 'orchestra.apps.webapps.types.php.PHPApp', '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.WordPressApp', )) @@ -152,40 +149,5 @@ WEBAPPS_ENABLED_OPTIONS = getattr(settings, 'WEBAPPS_ENABLED_OPTIONS', ( )) -WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD = getattr(settings, 'WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD', - 'secret') - -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') - -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_DRUPALMU_SITES_PATH = getattr(settings, 'WEBAPPS_DRUPALMU_SITES_PATH', - '/home/httpd/htdocs/drupal-mu/sites/%(site_name)s') - -WEBAPPS_DRUPALMU_LISTEN = getattr(settings, 'WEBAPPS_DRUPALMU_LISTEN', - '/opt/php/5.4/socks/drupal-mu.sock' -) - - -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', 'mysql.orchestra.lan') diff --git a/orchestra/apps/webapps/types/__init__.py b/orchestra/apps/webapps/types/__init__.py index 933e23bc..cd78848b 100644 --- a/orchestra/apps/webapps/types/__init__.py +++ b/orchestra/apps/webapps/types/__init__.py @@ -14,11 +14,10 @@ class AppType(plugins.Plugin): 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) + plugin_field = 'type' # TODO generic name like 'execution' ? @classmethod @@ -29,33 +28,6 @@ class AppType(plugins.Plugin): 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: diff --git a/orchestra/apps/webapps/types/php.py b/orchestra/apps/webapps/types/php.py index bba1ad38..9edd647b 100644 --- a/orchestra/apps/webapps/types/php.py +++ b/orchestra/apps/webapps/types/php.py @@ -66,15 +66,21 @@ class PHPApp(AppType): 'app_name': self.instance.name, } - def get_php_init_vars(self, per_account=False): + def get_php_init_vars(self, merge=False): """ process php options for inclusion on php.ini per_account=True merges all (account, webapp.type) options """ init_vars = {} options = self.instance.options.all() - if per_account: - options = self.instance.account.webapps.filter(webapp_type=self.instance.type) + if merge: + # Get options from the same account and php_version webapps + options = [] + php_version = self.get_php_version() + webapps = self.instance.account.webapps.filter(webapp_type=self.instance.type) + for webapp in webapps: + if webapp.type_instance.get_php_version == php_version: + options += list(webapp.options.all()) php_options = [option.name for option in type(self).get_php_options()] for opt in options: if opt.name in php_options: @@ -97,11 +103,8 @@ class PHPApp(AppType): def get_directive(self): context = self.get_directive_context() if self.is_fpm: - 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()) + return ('fpm', socket, self.instance.get_path()) elif self.is_fcgid: wrapper_path = os.path.normpath(self.FCGID_WRAPPER_PATH % context) return ('fcgid', self.instance.get_path(), wrapper_path) diff --git a/orchestra/apps/webapps/types/saas.py b/orchestra/apps/webapps/types/saas.py deleted file mode 100644 index 894032d3..00000000 --- a/orchestra/apps/webapps/types/saas.py +++ /dev/null @@ -1,54 +0,0 @@ -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 index 5278a5bf..b0cfbe7b 100644 --- a/orchestra/apps/webapps/types/wordpress.py +++ b/orchestra/apps/webapps/types/wordpress.py @@ -64,12 +64,11 @@ class WordPressApp(PHPApp): }) 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) + db_name = self.get_db_name() + db_user = self.get_db_user() + db_pass = self.get_db_pass() + db, db_created = Database.objects.get_or_create(name=db_name, account=self.instance.account) + if db_created: user = DatabaseUser(username=db_user, account=self.instance.account) user.set_password(db_pass) user.save() @@ -82,7 +81,7 @@ class WordPressApp(PHPApp): else: # Trigger related backends for related in self.get_related(): - related.save(updated_fields=[]) + related.save(update_fields=[]) def delete(self): for related in self.get_related(): diff --git a/orchestra/apps/websites/admin.py b/orchestra/apps/websites/admin.py index eacb8740..3a2874e8 100644 --- a/orchestra/apps/websites/admin.py +++ b/orchestra/apps/websites/admin.py @@ -24,11 +24,6 @@ class WebsiteDirectiveInline(admin.TabularInline): op.name: str(unicode(op.help_text)) for op in SiteDirective.get_plugins() } -# class Media: -# css = { -# 'all': ('orchestra/css/hide-inline-id.css',) -# } - def formfield_for_dbfield(self, db_field, **kwargs): if db_field.name == 'value': kwargs['widget'] = forms.TextInput(attrs={'size':'100'}) diff --git a/orchestra/apps/websites/apps.py b/orchestra/apps/websites/apps.py new file mode 100644 index 00000000..a803a7f4 --- /dev/null +++ b/orchestra/apps/websites/apps.py @@ -0,0 +1,17 @@ +from django.apps import AppConfig +from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.contenttypes.models import ContentType + +from orchestra.utils import database_ready + + +class WebsiteConfig(AppConfig): + name = 'orchestra.apps.websites' + + def ready(self): + if database_ready(): + from .models import Content + qset = Content.content_type.field.get_limit_choices_to() + for ct in ContentType.objects.filter(qset): + relation = GenericRelation('websites.Content') + ct.model_class().add_to_class('content_set', relation) diff --git a/orchestra/apps/websites/backends/apache.py b/orchestra/apps/websites/backends/apache.py index 46470c87..af4a48d4 100644 --- a/orchestra/apps/websites/backends/apache.py +++ b/orchestra/apps/websites/backends/apache.py @@ -98,32 +98,38 @@ class Apache2Backend(ServiceController): """ reload Apache2 if necessary """ self.append('if [[ $UPDATED == 1 ]]; then service apache2 reload; fi') + def get_directives(self, directive, context): + method, args = directive[0], directive[1:] + try: + method = getattr(self, 'get_%s_directives' % method) + except AttributeError: + raise AttributeError("%s does not has suport for '%s' directive." % + (self.__class__.__name__, method)) + return method(context, *args) + def get_content_directives(self, site): directives = [] for content in site.content_set.all(): directive = content.webapp.get_directive() - method, args = directive[0], directive[1:] - method = getattr(self, 'get_%s_directives' % method) - directives += method(content, *args) + context = self.get_content_context(content) + directives += self.get_directives(directive, context) return directives - def get_static_directives(self, content, app_path): - context = self.get_content_context(content) + def get_static_directives(self, context, app_path): context['app_path'] = app_path % context location = "%(location)s/" % context directive = "Alias %(location)s/ %(app_path)s/" % context return [(location, directive)] - 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': + def get_fpm_directives(self, context, socket, app_path): + if ':' in socket: + # TCP socket target = 'fcgi://%(socket)s%(app_path)s/$1' else: - raise TypeError("%s socket not supported." % socket_type) - context = self.get_content_context(content) + # UNIX socket + target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/' + if context['location'] != '/': + target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/$1' context.update({ 'app_path': app_path, 'socket': socket, @@ -135,8 +141,7 @@ class Apache2Backend(ServiceController): ) return [(location, directives)] - def get_fcgid_directives(self, content, app_path, wrapper_path): - context = self.get_content_context(content) + def get_fcgid_directives(self, context, app_path, wrapper_path): context.update({ 'app_path': app_path, 'wrapper_path': wrapper_path, @@ -202,7 +207,17 @@ class Apache2Backend(ServiceController): ) proxies.append((location, proxy)) return proxies - + + def get_saas(self, directives): + saas = [] + for name, value in directives.iteritems(): + if name.endswith('-saas'): + context = { + 'location': normurlpath(value), + } + directive = settings.WEBSITES_SAAS_DIRECTIVES[name] + saas += self.get_directive(context, directive) + return saas # def get_protections(self, site): # protections = '' # context = self.get_context(site) diff --git a/orchestra/apps/websites/directives.py b/orchestra/apps/websites/directives.py index 8f1da48c..2c9cf9b4 100644 --- a/orchestra/apps/websites/directives.py +++ b/orchestra/apps/websites/directives.py @@ -15,6 +15,7 @@ class SiteDirective(Plugin): HTTPD = 'HTTPD' SEC = 'ModSecurity' SSL = 'SSL' + SAAS = 'SaaS' help_text = "" unique = True @@ -76,31 +77,6 @@ class Proxy(SiteDirective): 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(UserGroup, self).validate(directive) - options = directive.value.split() - systemusers = [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.systemusers.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") @@ -151,3 +127,30 @@ class SecEngine(SiteDirective): help_text = _("URL location for disabling modsecurity engine.") regex = r'^/[^ ]*$' group = SiteDirective.SEC + + +class WordPressSaaS(SiteDirective): + name = 'wordpress-saas' + verbose_name = "WordPress" + help_text = _("URL location for mounting wordpress multisite.") +# fpm_listen = settings.WEBAPPS_WORDPRESSMU_LISTEN + group = SiteDirective.SAAS + regex = r'^/[^ ]*$' + + +class DokuWikiSaaS(SiteDirective): + name = 'dokuwiki-saas' + verbose_name = "DokuWiki" + help_text = _("URL location for mounting wordpress multisite.") +# fpm_listen = settings.WEBAPPS_DOKUWIKIMU_LISTEN + group = SiteDirective.SAAS + regex = r'^/[^ ]*$' + + +class DrupalSaaS(SiteDirective): + name = 'drupal-saas' + verbose_name = "Drupdal" + help_text = _("URL location for mounting wordpress multisite.") +# fpm_listen = settings.WEBAPPS_DRUPALMU_LISTEN + group = SiteDirective.SAAS + regex = r'^/[^ ]*$' diff --git a/orchestra/apps/websites/models.py b/orchestra/apps/websites/models.py index 441f6b7f..84557dc6 100644 --- a/orchestra/apps/websites/models.py +++ b/orchestra/apps/websites/models.py @@ -141,6 +141,7 @@ class Content(models.Model): return self.path def clean(self): + # TODO do it on the field? self.path = normurlpath(self.path) def get_absolute_url(self): diff --git a/orchestra/apps/websites/serializers.py b/orchestra/apps/websites/serializers.py index 837f6d6b..64e337d7 100644 --- a/orchestra/apps/websites/serializers.py +++ b/orchestra/apps/websites/serializers.py @@ -23,7 +23,7 @@ class RelatedDomainSerializer(AccountSerializerMixin, serializers.HyperlinkedMod class RelatedWebAppSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): class Meta: - model = Content.webapp.field.rel.to +# model = Content.webapp.field.rel.to fields = ('url', 'name', 'type') def from_native(self, data, files=None): @@ -46,15 +46,15 @@ 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') - options = OptionField(required=False) + directives = OptionField(required=False) class Meta: model = Website - fields = ('url', 'name', 'port', 'domains', 'is_active', 'contents', 'options') + fields = ('url', 'name', 'protocol', 'domains', 'is_active', 'contents', 'directives') postonly_fileds = ('name',) def full_clean(self, instance): - """ Prevent multiples domains on the same port """ + """ Prevent multiples domains on the same protocol """ for domain in instance._m2m_data['domains']: try: validate_domain_protocol(instance, domain, instance.protocol) diff --git a/orchestra/apps/websites/settings.py b/orchestra/apps/websites/settings.py index 52f360b0..6af8cf79 100644 --- a/orchestra/apps/websites/settings.py +++ b/orchestra/apps/websites/settings.py @@ -34,13 +34,15 @@ WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', 'domains.Doma 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', + 'orchestra.apps.websites.directives.WordPressSaaS', + 'orchestra.apps.websites.directives.DokuWikiSaaS', + 'orchestra.apps.websites.directives.DrupalSaaS', )) @@ -73,3 +75,14 @@ WEBSITES_TRAFFIC_IGNORE_HOSTS = getattr(settings, 'WEBSITES_TRAFFIC_IGNORE_HOSTS #WEBSITES_DEFAULT_SSl_KEY = getattr(settings, 'WEBSITES_DEFAULT_SSl_KEY', # '') + +WEBAPPS_SAAS_DIRECTIVES = getattr(settings, 'WEBAPPS_SAAS_DIRECTIVES', { + 'wordpress-saas': ('fpm', '/home/httpd/wordpress-mu/', '/opt/php/5.4/socks/wordpress-mu.sock'), + 'drupal-saas': ('fpm', '/home/httpd/drupal-mu/', '/opt/php/5.4/socks/drupal-mu.sock'), + 'dokuwiki-saas': ('fpm', '/home/httpd/moodle-mu/', '/opt/php/5.4/socks/moodle-mu.sock'), +# 'moodle-saas': ('fpm', '/home/httpd/moodle-mu/', '/opt/php/5.4/socks/moodle-mu.sock'), +}) + + + + diff --git a/orchestra/plugins/forms.py b/orchestra/plugins/forms.py index 4bd1937b..529f2564 100644 --- a/orchestra/plugins/forms.py +++ b/orchestra/plugins/forms.py @@ -21,8 +21,12 @@ class PluginDataForm(forms.ModelForm): if self.instance.pk: for field in self.plugin.get_change_readonly_fileds(): value = getattr(self.instance, field, None) or self.instance.data[field] + display = value + foo_display = getattr(self.instance, 'get_%s_display' % field, None) + if foo_display: + display = foo_display() self.fields[field].required = False - self.fields[field].widget = ReadOnlyWidget(value) + self.fields[field].widget = ReadOnlyWidget(value, display) # self.fields[field].help_text = None def clean(self): diff --git a/orchestra/plugins/options.py b/orchestra/plugins/options.py index 3c97d951..ef0b0cd2 100644 --- a/orchestra/plugins/options.py +++ b/orchestra/plugins/options.py @@ -6,7 +6,11 @@ class Plugin(object): # Used on select plugin view class_verbose_name = None icon = None + change_form = None + form = None + serializer = None change_readonly_fileds = () + plugin_field = None def __init__(self, instance=None): # Related model instance of this plugin @@ -49,7 +53,34 @@ class Plugin(object): @classmethod def get_change_readonly_fileds(cls): - return cls.change_readonly_fileds + return (cls.plugin_field,) + cls.change_readonly_fileds + + 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 = self.plugin_field + return self.form + + def get_change_form(self): + form = self.change_form or self.form + form.plugin = self + form.plugin_field = self.plugin_field + return form + + def get_serializer(self): + self.serializer.plugin = self + return self.serializer class PluginModelAdapter(Plugin): diff --git a/orchestra/utils/python.py b/orchestra/utils/python.py index 0bfb997c..c5dc153b 100644 --- a/orchestra/utils/python.py +++ b/orchestra/utils/python.py @@ -1,6 +1,8 @@ +import sys import collections import random import string +from cStringIO import StringIO def import_class(cls): @@ -76,3 +78,14 @@ class AttrDict(dict): def __init__(self, *args, **kwargs): super(AttrDict, self).__init__(*args, **kwargs) self.__dict__ = self + + +class CaptureStdout(list): + def __enter__(self): + self._stdout = sys.stdout + sys.stdout = self._stringio = StringIO() + return self + + def __exit__(self, *args): + self.extend(self._stringio.getvalue().splitlines()) + sys.stdout = self._stdout