Refactores webapps and SaaS

This commit is contained in:
Marc Aymerich 2015-03-23 15:36:51 +00:00
parent 2d3e925c36
commit 40930a480e
42 changed files with 618 additions and 279 deletions

37
TODO.md
View File

@ -147,7 +147,6 @@
* Resource graph for each related object * 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 * 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
* <IfModule security2_module> and other IfModule on backend SecRule * <IfModule security2_module> 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 * 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) * Display admin.is_active (disabled account/order by)
* show details data on webapp changelist
* lock resource monitoring * 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 * -EXecCGI in common CMS upload locations /wp-upload/upload/uploads
* cgi user / pervent shell access * 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 * 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 * php-fpm disable execCGI
* SuexecUserGroup needs to be per app othewise wrapper/fpm user can't be correct * 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
* ```<?php
$moodle_host = $SERVER[HTTP_HOST];
require_once(/etc/moodles/.$moodle_host.config.php);``` moodle/drupla/php-list multi-tenancy
* make account available on all admin forms
* WPMU blog traffic
* normurlpath '' returns '/'
* rename webapps.type to something more generic
* initial configuration of multisite sas apps with password stored in DATA
* websites links on webpaps ans saas
* /var/lib/fcgid/wrappers/ rm write permissions

View File

@ -118,10 +118,13 @@ class ChangeAddFieldsMixin(object):
return super(ChangeAddFieldsMixin, self).get_prepopulated_fields(request, obj) return super(ChangeAddFieldsMixin, self).get_prepopulated_fields(request, obj)
return {} return {}
def get_change_readonly_fields(self, request, obj=None):
return self.change_readonly_fields
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
fields = super(ChangeAddFieldsMixin, self).get_readonly_fields(request, obj) fields = super(ChangeAddFieldsMixin, self).get_readonly_fields(request, obj)
if obj: if obj:
return fields + self.change_readonly_fields return fields + self.get_change_readonly_fields(request, obj=obj)
return fields return fields
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):

View File

@ -205,6 +205,13 @@ class AccountAdminMixin(object):
formfield.initial = 1 formfield.initial = 1
return formfield return formfield
def get_formset(self, request, obj=None, **kwargs):
""" provides form.account for convinience """
formset = super(AccountAdminMixin, self).get_formset(request, obj=obj, **kwargs)
formset.form.account = self.account
formset.account = self.account
return formset
def get_account_from_preserve_filters(self, request): def get_account_from_preserve_filters(self, request):
preserved_filters = self.get_preserved_filters(request) preserved_filters = self.get_preserved_filters(request)
preserved_filters = dict(parse_qsl(preserved_filters)) preserved_filters = dict(parse_qsl(preserved_filters))

View File

@ -281,7 +281,7 @@ class MaildirDisk(ServiceMonitor):
current_date = self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z") current_date = self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z")
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
function monitor () { function monitor () {
awk 'NR>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): def monitor(self, mailbox):

View File

@ -35,6 +35,7 @@ class ServiceBackend(plugins.Plugin):
ignore_fields = [] ignore_fields = []
actions = [] actions = []
default_route_match = 'True' default_route_match = 'True'
block = False # Force the backend manager to block in multiple backend executions and execute them synchronously
__metaclass__ = ServiceMount __metaclass__ = ServiceMount

View File

@ -1,6 +1,7 @@
import logging import logging
import threading import threading
import traceback import traceback
from collections import OrderedDict
from django import db from django import db
from django.core.mail import mail_admins from django.core.mail import mail_admins
@ -51,8 +52,9 @@ def close_connection(execute):
def execute(operations, async=False): def execute(operations, async=False):
""" generates and executes the operations on the servers """ """ generates and executes the operations on the servers """
scripts = {} scripts = OrderedDict()
cache = {} cache = {}
block = False
# Generate scripts per server+backend # Generate scripts per server+backend
for operation in operations: for operation in operations:
logger.debug("Queued %s" % str(operation)) logger.debug("Queued %s" % str(operation))
@ -77,6 +79,8 @@ def execute(operations, async=False):
pre_action.send(**kwargs) pre_action.send(**kwargs)
method(operation.instance) method(operation.instance)
post_action.send(**kwargs) post_action.send(**kwargs)
if backend.block:
block = True
# Execute scripts on each server # Execute scripts on each server
threads = [] threads = []
executions = [] executions = []
@ -88,8 +92,11 @@ def execute(operations, async=False):
execute = close_connection(execute) execute = close_connection(execute)
# DEBUG: substitute all thread related stuff for this function # DEBUG: substitute all thread related stuff for this function
#execute(server, async=async) #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 = threading.Thread(target=execute, args=(server,), kwargs={'async': async})
thread.start() thread.start()
if block:
thread.join()
threads.append(thread) threads.append(thread)
executions.append((execute, operations)) executions.append((execute, operations))
[ thread.join() for thread in threads ] [ thread.join() for thread in threads ]

View File

@ -10,6 +10,8 @@ import paramiko
from celery.datastructures import ExceptionInfo from celery.datastructures import ExceptionInfo
from django.conf import settings as djsettings from django.conf import settings as djsettings
from orchestra.utils.python import CaptureStdout
from . import settings from . import settings
@ -37,7 +39,6 @@ def SSH(backend, log, server, cmds, async=False):
channel = None channel = None
ssh = None ssh = None
try: 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 # Avoid "Argument list too long" on large scripts by genereting a file
# and scping it to the remote server # and scping it to the remote server
with os.fdopen(os.open(path, os.O_WRONLY | os.O_CREAT, 0600), 'w') as handle: 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']) log.save(update_fields=['script'])
try: try:
for cmd in cmds: for cmd in cmds:
result = cmd(server) with CaptureStdout() as stdout:
log.stdout += str(result) result = cmd(server)
for line in stdout:
log.stdout += unicode(line, errors='replace') + '\n'
if async: if async:
log.save(update_fields=['stdout']) log.save(update_fields=['stdout'])
except: except:
log.exit_code = 1 log.exit_code = 1
log.state = log.FAILURE log.state = log.FAILURE
log.traceback = ExceptionInfo(sys.exc_info()).traceback log.traceback = ExceptionInfo(sys.exc_info()).traceback
logger.error('Exception while executing %s on %s' % (backend, server))
else: else:
log.exit_code = 0 log.exit_code = 0
log.state = log.SUCCESS log.state = log.SUCCESS
logger.debug('%s execution state on %s is %s' % (backend, server, log.state))
log.save() log.save()

View File

@ -13,9 +13,8 @@ class PaymentMethod(plugins.Plugin):
label_field = 'label' label_field = 'label'
number_field = 'number' number_field = 'number'
process_credit = False process_credit = False
form = None
serializer = None
due_delta = relativedelta.relativedelta(months=1) due_delta = relativedelta.relativedelta(months=1)
plugin_field = 'method'
@classmethod @classmethod
@cached @cached
@ -25,24 +24,6 @@ class PaymentMethod(plugins.Plugin):
plugins.append(import_class(cls)) plugins.append(import_class(cls))
return plugins 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): def get_label(self):
return self.instance.data[self.label_field] return self.instance.data[self.label_field]

View File

@ -182,15 +182,16 @@ def resource_inline_factory(resources):
return len(resources) return len(resources)
def get_queryset(self): def get_queryset(self):
""" Filter disabled resources """
queryset = super(ResourceInlineFormSet, self).get_queryset() 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 @cached_property
def forms(self, resources=resources): def forms(self, resources=resources):
forms = [] forms = []
resources_copy = list(resources) resources_copy = list(resources)
# Remove queryset disabled objects # 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: if self.instance.pk:
# Create missing resource data # Create missing resource data
queryset_resources = [data.resource for data in queryset] queryset_resources = [data.resource for data in queryset]

View File

@ -170,6 +170,7 @@ class ResourceData(models.Model):
updated_at = models.DateTimeField(_("updated"), null=True, editable=False) updated_at = models.DateTimeField(_("updated"), null=True, editable=False)
allocated = models.DecimalField(_("allocated"), max_digits=8, decimal_places=2, allocated = models.DecimalField(_("allocated"), max_digits=8, decimal_places=2,
null=True, blank=True) null=True, blank=True)
content_object = GenericForeignKey() content_object = GenericForeignKey()
class Meta: class Meta:
@ -326,9 +327,9 @@ def create_resource_relation():
field for field in related._meta.virtual_fields if field.rel.to != ResourceData 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(): for ct, resources in Resource.objects.group_by('content_type').iteritems():
model = ct.model_class() model = ct.model_class()
relation = GenericRelation('resources.ResourceData')
model.add_to_class('resource_set', relation) model.add_to_class('resource_set', relation)
model.resources = ResourceHandler() model.resources = ResourceHandler()
Resource._related.add(model) Resource._related.add(model)

View File

@ -1,6 +1,8 @@
from django.contrib import admin 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.apps.accounts.admin import AccountAdminMixin
from orchestra.plugins.admin import SelectPluginAdminMixin from orchestra.plugins.admin import SelectPluginAdminMixin
@ -8,7 +10,7 @@ from .models import SaaS
from .services import SoftwareService 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_display = ('username', 'service', 'display_site_name', 'account_link')
list_filter = ('service',) list_filter = ('service',)
plugin = SoftwareService plugin = SoftwareService

View File

@ -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)

View File

@ -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'<body id="error-page">\n\t<p>(.*)</p></body>', 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(
'<a href="http://[\.\-\w]+/wp-admin/network/site-info\.php\?id=([0-9]+)"\s+'
'class="edit">%s</a>' % 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'<span class="delete">(.*)</span>', 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)

View File

@ -1,4 +1,6 @@
from django.db import models 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.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from jsonfield import JSONField from jsonfield import JSONField
@ -12,20 +14,21 @@ from .services import SoftwareService
class SaaS(models.Model): class SaaS(models.Model):
service = models.CharField(_("service"), max_length=32, service = models.CharField(_("service"), max_length=32,
choices=SoftwareService.get_plugin_choices()) 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."), help_text=_("Required. 64 characters or fewer. Letters, digits and ./-/_ only."),
validators=[validators.validate_username]) 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"), account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='saas') 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: class Meta:
verbose_name = "SaaS" verbose_name = "SaaS"
verbose_name_plural = "SaaS" verbose_name_plural = "SaaS"
unique_together = ( unique_together = (
('username', 'service'), ('username', 'service'),
('site_name', 'service'), # ('site_name', 'service'),
) )
def __unicode__(self): def __unicode__(self):
@ -49,4 +52,22 @@ class SaaS(models.Model):
def set_password(self, password): def set_password(self, password):
self.password = password self.password = password
services.register(SaaS) 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

View File

@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from .. import settings
from .options import SoftwareService, SoftwareServiceForm from .options import SoftwareService, SoftwareServiceForm
@ -19,10 +20,11 @@ class BSCWDataSerializer(serializers.Serializer):
class BSCWService(SoftwareService): class BSCWService(SoftwareService):
name = 'bscw'
verbose_name = "BSCW" verbose_name = "BSCW"
form = BSCWForm form = BSCWForm
serializer = BSCWDataSerializer serializer = BSCWDataSerializer
icon = 'orchestra/icons/apps/BSCW.png' icon = 'orchestra/icons/apps/BSCW.png'
# TODO override from settings # TODO override from settings
site_name = 'bascw.orchestra.lan' site_name = settings.SAAS_BSCW_DOMAIN
change_readonly_fileds = ('email',) change_readonly_fileds = ('email',)

View File

@ -0,0 +1,6 @@
from .options import SoftwareService
class DokuWikiService(SoftwareService):
verbose_name = "Dowkuwiki"
icon = 'orchestra/icons/apps/Dokuwiki.png'

View File

@ -0,0 +1,6 @@
from .options import SoftwareService
class DrupalService(SoftwareService):
verbose_name = "Drupal"
icon = 'orchestra/icons/apps/Drupal.png'

View File

@ -14,9 +14,10 @@ from .. import settings
class SoftwareServiceForm(PluginDataForm): class SoftwareServiceForm(PluginDataForm):
site_name = forms.CharField(widget=widgets.ShowTextWidget, required=False)
password = forms.CharField(label=_("Password"), required=False, password = forms.CharField(label=_("Password"), required=False,
widget=widgets.ReadOnlyWidget('<strong>Unknown password</strong>'), widget=widgets.ReadOnlyWidget('<strong>Unknown password</strong>'),
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 " "service's password, but you can change the password using "
"<a href=\"password/\">this form</a>.")) "<a href=\"password/\">this form</a>."))
password1 = forms.CharField(label=_("Password"), validators=[validators.validate_password], password1 = forms.CharField(label=_("Password"), validators=[validators.validate_password],
@ -38,13 +39,16 @@ class SoftwareServiceForm(PluginDataForm):
self.fields['password'].widget = forms.HiddenInput() self.fields['password'].widget = forms.HiddenInput()
site_name = self.plugin.site_name site_name = self.plugin.site_name
if site_name: if site_name:
link = '<a href="http://%s">%s</a>' % (site_name, site_name) site_name_link = '<a href="http://%s">%s</a>' % (site_name, site_name)
self.fields['site_name'].widget = widgets.ReadOnlyWidget(site_name, mark_safe(link))
self.fields['site_name'].required = False
else: else:
base_name = self.plugin.site_name_base_domain site_name_link = '&lt;name&gt;.%s' % self.plugin.site_name_base_domain
help_text = _("The final URL would be &lt;site_name&gt;.%s") % base_name self.fields['site_name'].initial = site_name_link
self.fields['site_name'].help_text = help_text ## 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 &lt;site_name&gt;.%s") % base_name
# self.fields['site_name'].help_text = help_text
def clean_password2(self): def clean_password2(self):
if not self.is_change: if not self.is_change:
@ -69,12 +73,13 @@ class SoftwareServiceForm(PluginDataForm):
class SoftwareService(plugins.Plugin): class SoftwareService(plugins.Plugin):
form = SoftwareServiceForm form = SoftwareServiceForm
serializer = None
site_name = None site_name = None
site_name_base_domain = 'orchestra.lan' site_name_base_domain = 'orchestra.lan'
has_custom_domain = False
icon = 'orchestra/icons/apps.png' icon = 'orchestra/icons/apps.png'
change_readonly_fileds = ('username',) change_readonly_fileds = ('site_name',)
class_verbose_name = _("Software as a Service") class_verbose_name = _("Software as a Service")
plugin_field = 'service'
@classmethod @classmethod
@cached @cached
@ -84,27 +89,18 @@ class SoftwareService(plugins.Plugin):
plugins.append(import_class(cls)) plugins.append(import_class(cls))
return plugins 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 @classmethod
def get_change_readonly_fileds(cls): 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): def get_site_name(self):
return self.site_name or '.'.join( 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): def save(self):
self.form.plugin = self pass
self.form.plugin_field = 'service'
return self.form
def get_serializer(self): def delete(self):
self.serializer.plugin = self pass
return self.serializer

View File

@ -1,14 +1,100 @@
from django import forms from django import forms
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ 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 from .options import SoftwareService, SoftwareServiceForm
class PHPListForm(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://&lt;name&gt;.{}/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 <a href={0}>{0}</a>").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): class PHPListService(SoftwareService):
name = 'phplist'
verbose_name = "phpList" verbose_name = "phpList"
form = PHPListForm form = PHPListForm
change_form = PHPListChangeForm
change_readonly_fileds = ('db_name',)
serializer = PHPListSerializer
icon = 'orchestra/icons/apps/Phplist.png' 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

View File

@ -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',)

View File

@ -6,4 +6,44 @@ SAAS_ENABLED_SERVICES = getattr(settings, 'SAAS_ENABLED_SERVICES', (
'orchestra.apps.saas.services.bscw.BSCWService', 'orchestra.apps.saas.services.bscw.BSCWService',
'orchestra.apps.saas.services.gitlab.GitLabService', 'orchestra.apps.saas.services.gitlab.GitLabService',
'orchestra.apps.saas.services.phplist.PHPListService', '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'
)

View File

@ -48,21 +48,15 @@ class WebAppOptionInline(admin.TabularInline):
class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin): class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin):
list_display = ('name', 'type', 'display_detail', 'display_websites', 'account_link') list_display = ('name', 'type', 'display_detail', 'display_websites', 'account_link')
list_filter = ('type',) list_filter = ('type',)
# add_fields = ('account', 'name', 'type')
# fields = ('account_link', 'name', 'type')
inlines = [WebAppOptionInline] inlines = [WebAppOptionInline]
readonly_fields = ('account_link',) readonly_fields = ('account_link', )
change_readonly_fields = ('name', 'type') change_readonly_fields = ('name', 'type', 'display_websites')
search_fuelds = ('name', 'account__username') search_fields = ('name', 'account__username', 'data', 'website__domains__name')
list_prefetch_related = ('content_set__website',) list_prefetch_related = ('content_set__website',)
plugin = AppType plugin = AppType
plugin_field = 'type' plugin_field = 'type'
plugin_title = _("Web application 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): def display_websites(self, webapp):
websites = [] websites = []
for content in webapp.content_set.all(): for content in webapp.content_set.all():

View File

@ -13,6 +13,7 @@ from .. import settings
class PHPBackend(WebAppServiceMixin, ServiceController): class PHPBackend(WebAppServiceMixin, ServiceController):
verbose_name = _("PHP FPM/FCGID") verbose_name = _("PHP FPM/FCGID")
default_route_match = "webapp.type == 'php'" default_route_match = "webapp.type == 'php'"
MERGE = settings.WEBAPPS_MERGE_PHP_WEBAPPS
def save(self, webapp): def save(self, webapp):
context = self.get_context(webapp) context = self.get_context(webapp)
@ -89,8 +90,9 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
) )
def get_fpm_config(self, webapp, context): def get_fpm_config(self, webapp, context):
merge = settings.WEBAPPS_MERGE_PHP_WEBAPPS
context.update({ 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), 'max_children': webapp.get_options().get('processes', False),
'request_terminate_timeout': webapp.get_options().get('timeout', 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): def get_fcgid_wrapper(self, webapp, context):
opt = webapp.type_instance opt = webapp.type_instance
# Format PHP init vars # Format PHP init vars
init_vars = opt.get_php_init_vars() init_vars = opt.get_php_init_vars(merge=self.MERGE)
if init_vars: if init_vars:
init_vars = [ '-d %s="%s"' % (k,v) for k,v in init_vars.iteritems() ] init_vars = [ '-d %s="%s"' % (k,v) for k,v in init_vars.iteritems() ]
init_vars = ', '.join(init_vars) init_vars = ', '.join(init_vars)

View File

@ -10,8 +10,8 @@ from .. import settings
class WordpressMuBackend(ServiceController): class WordpressMuBackend(ServiceController):
verbose_name = _("Wordpress multisite") verbose_name = _("Wordpress multisite")
model = 'webapps.WebApp' model = 'saas.SaaS'
default_route_match = "webapp.type == 'wordpress-mu'" default_route_match = "saas.service == 'wordpress-mu'"
@property @property
def script(self): def script(self):
@ -22,7 +22,7 @@ class WordpressMuBackend(ServiceController):
login_url = base_url + '/wp-login.php' login_url = base_url + '/wp-login.php'
login_data = { login_data = {
'log': 'admin', 'log': 'admin',
'pwd': settings.WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD, 'pwd': settings.WEBSITES_WORDPRESSMU_ADMIN_PASSWORD,
'redirect_to': '/wp-admin/' 'redirect_to': '/wp-admin/'
} }
response = session.post(login_url, data=login_data) response = session.post(login_url, data=login_data)
@ -30,7 +30,7 @@ class WordpressMuBackend(ServiceController):
raise IOError("Failure login to remote application") raise IOError("Failure login to remote application")
def get_base_url(self): def get_base_url(self):
base_url = settings.WEBAPPS_WORDPRESSMU_BASE_URL base_url = settings.WEBSITES_WORDPRESSMU_BASE_URL
return base_url.rstrip('/') return base_url.rstrip('/')
def validate_response(self, response): def validate_response(self, response):
@ -86,8 +86,6 @@ class WordpressMuBackend(ServiceController):
self.validate_response(response) self.validate_response(response)
def delete_blog(self, webapp, server): 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() session = requests.Session()
self.login(session) self.login(session)

View File

@ -24,7 +24,7 @@ class WebApp(models.Model):
choices=AppType.get_plugin_choices()) choices=AppType.get_plugin_choices())
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='webapps') related_name='webapps')
data = JSONField(_("data"), blank=True, data = JSONField(_("data"), blank=True, default={},
help_text=_("Extra information dependent of each service.")) help_text=_("Extra information dependent of each service."))
class Meta: class Meta:

View File

@ -38,9 +38,6 @@ WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', (
'orchestra.apps.webapps.types.php.PHPApp', 'orchestra.apps.webapps.types.php.PHPApp',
'orchestra.apps.webapps.types.misc.StaticApp', 'orchestra.apps.webapps.types.misc.StaticApp',
'orchestra.apps.webapps.types.misc.WebalizerApp', '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.misc.SymbolicLinkApp',
'orchestra.apps.webapps.types.wordpress.WordPressApp', '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', WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST = getattr(settings, 'WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST',
'mysql.orchestra.lan') 'mysql.orchestra.lan')

View File

@ -14,11 +14,10 @@ class AppType(plugins.Plugin):
verbose_name = "" verbose_name = ""
help_text= "" help_text= ""
form = PluginDataForm form = PluginDataForm
change_form = None
serializer = None
icon = 'orchestra/icons/apps.png' icon = 'orchestra/icons/apps.png'
unique_name = False unique_name = False
option_groups = (AppOption.FILESYSTEM, AppOption.PROCESS, AppOption.PHP) option_groups = (AppOption.FILESYSTEM, AppOption.PROCESS, AppOption.PHP)
plugin_field = 'type'
# TODO generic name like 'execution' ? # TODO generic name like 'execution' ?
@classmethod @classmethod
@ -29,33 +28,6 @@ class AppType(plugins.Plugin):
plugins.append(import_class(cls)) plugins.append(import_class(cls))
return plugins 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): def validate(self):
""" Unique name validation """ """ Unique name validation """
if self.unique_name: if self.unique_name:

View File

@ -66,15 +66,21 @@ class PHPApp(AppType):
'app_name': self.instance.name, '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 process php options for inclusion on php.ini
per_account=True merges all (account, webapp.type) options per_account=True merges all (account, webapp.type) options
""" """
init_vars = {} init_vars = {}
options = self.instance.options.all() options = self.instance.options.all()
if per_account: if merge:
options = self.instance.account.webapps.filter(webapp_type=self.instance.type) # 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()] php_options = [option.name for option in type(self).get_php_options()]
for opt in options: for opt in options:
if opt.name in php_options: if opt.name in php_options:
@ -97,11 +103,8 @@ class PHPApp(AppType):
def get_directive(self): def get_directive(self):
context = self.get_directive_context() context = self.get_directive_context()
if self.is_fpm: if self.is_fpm:
socket_type = 'unix'
if ':' in self.FPM_LISTEN:
socket_type = 'tcp'
socket = self.FPM_LISTEN % context 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: elif self.is_fcgid:
wrapper_path = os.path.normpath(self.FCGID_WRAPPER_PATH % context) wrapper_path = os.path.normpath(self.FCGID_WRAPPER_PATH % context)
return ('fcgid', self.instance.get_path(), wrapper_path) return ('fcgid', self.instance.get_path(), wrapper_path)

View File

@ -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.<br>"
"By default this blog is accessible via &lt;app_name&gt;.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.<br>"
"By default this wiki is accessible via &lt;app_name&gt;.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.<br>"
"By default this wiki is accessible via &lt;app_name&gt;.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.<br>"
"The installation will be completed after visiting "
"http://&lt;app_name&gt;.drupal.orchestra.lan/install.php?profile=standard<br>"
"By default this site will be accessible via &lt;app_name&gt;.drupal.orchestra.lan")
icon = 'orchestra/icons/apps/DrupalMu.png'
unique_name = True
option_groups = ()
fpm_listen = settings.WEBAPPS_DRUPALMU_LISTEN

View File

@ -64,12 +64,11 @@ class WordPressApp(PHPApp):
}) })
def save(self): def save(self):
create = not self.instance.pk db_name = self.get_db_name()
if create: db_user = self.get_db_user()
db_name = self.get_db_name() db_pass = self.get_db_pass()
db_user = self.get_db_user() db, db_created = Database.objects.get_or_create(name=db_name, account=self.instance.account)
db_pass = self.get_db_pass() if db_created:
db = Database.objects.create(name=db_name, account=self.instance.account)
user = DatabaseUser(username=db_user, account=self.instance.account) user = DatabaseUser(username=db_user, account=self.instance.account)
user.set_password(db_pass) user.set_password(db_pass)
user.save() user.save()
@ -82,7 +81,7 @@ class WordPressApp(PHPApp):
else: else:
# Trigger related backends # Trigger related backends
for related in self.get_related(): for related in self.get_related():
related.save(updated_fields=[]) related.save(update_fields=[])
def delete(self): def delete(self):
for related in self.get_related(): for related in self.get_related():

View File

@ -24,11 +24,6 @@ class WebsiteDirectiveInline(admin.TabularInline):
op.name: str(unicode(op.help_text)) for op in SiteDirective.get_plugins() 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): def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'value': if db_field.name == 'value':
kwargs['widget'] = forms.TextInput(attrs={'size':'100'}) kwargs['widget'] = forms.TextInput(attrs={'size':'100'})

View File

@ -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)

View File

@ -98,32 +98,38 @@ class Apache2Backend(ServiceController):
""" reload Apache2 if necessary """ """ reload Apache2 if necessary """
self.append('if [[ $UPDATED == 1 ]]; then service apache2 reload; fi') 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): def get_content_directives(self, site):
directives = [] directives = []
for content in site.content_set.all(): for content in site.content_set.all():
directive = content.webapp.get_directive() directive = content.webapp.get_directive()
method, args = directive[0], directive[1:] context = self.get_content_context(content)
method = getattr(self, 'get_%s_directives' % method) directives += self.get_directives(directive, context)
directives += method(content, *args)
return directives return directives
def get_static_directives(self, content, app_path): def get_static_directives(self, context, app_path):
context = self.get_content_context(content)
context['app_path'] = app_path % context context['app_path'] = app_path % context
location = "%(location)s/" % context location = "%(location)s/" % context
directive = "Alias %(location)s/ %(app_path)s/" % context directive = "Alias %(location)s/ %(app_path)s/" % context
return [(location, directive)] return [(location, directive)]
def get_fpm_directives(self, content, socket_type, socket, app_path): def get_fpm_directives(self, context, socket, app_path):
if socket_type == 'unix': if ':' in socket:
target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/' # TCP socket
if content.path != '/':
target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/$1'
elif socket_type == 'tcp':
target = 'fcgi://%(socket)s%(app_path)s/$1' target = 'fcgi://%(socket)s%(app_path)s/$1'
else: else:
raise TypeError("%s socket not supported." % socket_type) # UNIX socket
context = self.get_content_context(content) 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({ context.update({
'app_path': app_path, 'app_path': app_path,
'socket': socket, 'socket': socket,
@ -135,8 +141,7 @@ class Apache2Backend(ServiceController):
) )
return [(location, directives)] return [(location, directives)]
def get_fcgid_directives(self, content, app_path, wrapper_path): def get_fcgid_directives(self, context, app_path, wrapper_path):
context = self.get_content_context(content)
context.update({ context.update({
'app_path': app_path, 'app_path': app_path,
'wrapper_path': wrapper_path, 'wrapper_path': wrapper_path,
@ -202,7 +207,17 @@ class Apache2Backend(ServiceController):
) )
proxies.append((location, proxy)) proxies.append((location, proxy))
return proxies 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): # def get_protections(self, site):
# protections = '' # protections = ''
# context = self.get_context(site) # context = self.get_context(site)

View File

@ -15,6 +15,7 @@ class SiteDirective(Plugin):
HTTPD = 'HTTPD' HTTPD = 'HTTPD'
SEC = 'ModSecurity' SEC = 'ModSecurity'
SSL = 'SSL' SSL = 'SSL'
SAAS = 'SaaS'
help_text = "" help_text = ""
unique = True unique = True
@ -76,31 +77,6 @@ class Proxy(SiteDirective):
group = SiteDirective.HTTPD group = SiteDirective.HTTPD
class UserGroup(SiteDirective):
name = 'user_group'
verbose_name = _("SuexecUserGroup")
help_text = _("<tt>user [group]</tt>, 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): class ErrorDocument(SiteDirective):
name = 'error_document' name = 'error_document'
verbose_name = _("ErrorDocumentRoot") verbose_name = _("ErrorDocumentRoot")
@ -151,3 +127,30 @@ class SecEngine(SiteDirective):
help_text = _("URL location for disabling modsecurity engine.") help_text = _("URL location for disabling modsecurity engine.")
regex = r'^/[^ ]*$' regex = r'^/[^ ]*$'
group = SiteDirective.SEC 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'^/[^ ]*$'

View File

@ -141,6 +141,7 @@ class Content(models.Model):
return self.path return self.path
def clean(self): def clean(self):
# TODO do it on the field?
self.path = normurlpath(self.path) self.path = normurlpath(self.path)
def get_absolute_url(self): def get_absolute_url(self):

View File

@ -23,7 +23,7 @@ class RelatedDomainSerializer(AccountSerializerMixin, serializers.HyperlinkedMod
class RelatedWebAppSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer): class RelatedWebAppSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Content.webapp.field.rel.to # model = Content.webapp.field.rel.to
fields = ('url', 'name', 'type') fields = ('url', 'name', 'type')
def from_native(self, data, files=None): 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) domains = RelatedDomainSerializer(many=True, allow_add_remove=True, required=False)
contents = ContentSerializer(required=False, many=True, allow_add_remove=True, contents = ContentSerializer(required=False, many=True, allow_add_remove=True,
source='content_set') source='content_set')
options = OptionField(required=False) directives = OptionField(required=False)
class Meta: class Meta:
model = Website model = Website
fields = ('url', 'name', 'port', 'domains', 'is_active', 'contents', 'options') fields = ('url', 'name', 'protocol', 'domains', 'is_active', 'contents', 'directives')
postonly_fileds = ('name',) postonly_fileds = ('name',)
def full_clean(self, instance): 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']: for domain in instance._m2m_data['domains']:
try: try:
validate_domain_protocol(instance, domain, instance.protocol) validate_domain_protocol(instance, domain, instance.protocol)

View File

@ -34,13 +34,15 @@ WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', 'domains.Doma
WEBSITES_ENABLED_DIRECTIVES = getattr(settings, 'WEBSITES_ENABLED_DIRECTIVES', ( WEBSITES_ENABLED_DIRECTIVES = getattr(settings, 'WEBSITES_ENABLED_DIRECTIVES', (
'orchestra.apps.websites.directives.Redirect', 'orchestra.apps.websites.directives.Redirect',
'orchestra.apps.websites.directives.Proxy', 'orchestra.apps.websites.directives.Proxy',
'orchestra.apps.websites.directives.UserGroup',
'orchestra.apps.websites.directives.ErrorDocument', 'orchestra.apps.websites.directives.ErrorDocument',
'orchestra.apps.websites.directives.SSLCA', 'orchestra.apps.websites.directives.SSLCA',
'orchestra.apps.websites.directives.SSLCert', 'orchestra.apps.websites.directives.SSLCert',
'orchestra.apps.websites.directives.SSLKey', 'orchestra.apps.websites.directives.SSLKey',
'orchestra.apps.websites.directives.SecRuleRemove', 'orchestra.apps.websites.directives.SecRuleRemove',
'orchestra.apps.websites.directives.SecEngine', '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', #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'),
})

View File

@ -21,8 +21,12 @@ class PluginDataForm(forms.ModelForm):
if self.instance.pk: if self.instance.pk:
for field in self.plugin.get_change_readonly_fileds(): for field in self.plugin.get_change_readonly_fileds():
value = getattr(self.instance, field, None) or self.instance.data[field] 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].required = False
self.fields[field].widget = ReadOnlyWidget(value) self.fields[field].widget = ReadOnlyWidget(value, display)
# self.fields[field].help_text = None # self.fields[field].help_text = None
def clean(self): def clean(self):

View File

@ -6,7 +6,11 @@ class Plugin(object):
# Used on select plugin view # Used on select plugin view
class_verbose_name = None class_verbose_name = None
icon = None icon = None
change_form = None
form = None
serializer = None
change_readonly_fileds = () change_readonly_fileds = ()
plugin_field = None
def __init__(self, instance=None): def __init__(self, instance=None):
# Related model instance of this plugin # Related model instance of this plugin
@ -49,7 +53,34 @@ class Plugin(object):
@classmethod @classmethod
def get_change_readonly_fileds(cls): 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): class PluginModelAdapter(Plugin):

View File

@ -1,6 +1,8 @@
import sys
import collections import collections
import random import random
import string import string
from cStringIO import StringIO
def import_class(cls): def import_class(cls):
@ -76,3 +78,14 @@ class AttrDict(dict):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(AttrDict, self).__init__(*args, **kwargs) super(AttrDict, self).__init__(*args, **kwargs)
self.__dict__ = self 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