Refactores webapps and SaaS
This commit is contained in:
parent
2d3e925c36
commit
40930a480e
37
TODO.md
37
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
|
|||
* <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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
* ```<?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
|
||||
|
|
|
@ -118,10 +118,13 @@ class ChangeAddFieldsMixin(object):
|
|||
return super(ChangeAddFieldsMixin, self).get_prepopulated_fields(request, obj)
|
||||
return {}
|
||||
|
||||
def get_change_readonly_fields(self, request, obj=None):
|
||||
return self.change_readonly_fields
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
fields = super(ChangeAddFieldsMixin, self).get_readonly_fields(request, obj)
|
||||
if obj:
|
||||
return fields + self.change_readonly_fields
|
||||
return fields + self.get_change_readonly_fields(request, obj=obj)
|
||||
return fields
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
|
|
|
@ -205,6 +205,13 @@ class AccountAdminMixin(object):
|
|||
formfield.initial = 1
|
||||
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):
|
||||
preserved_filters = self.get_preserved_filters(request)
|
||||
preserved_filters = dict(parse_qsl(preserved_filters))
|
||||
|
|
|
@ -281,7 +281,7 @@ class MaildirDisk(ServiceMonitor):
|
|||
current_date = self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z")
|
||||
self.append(textwrap.dedent("""\
|
||||
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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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',)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
from .options import SoftwareService
|
||||
|
||||
|
||||
class DokuWikiService(SoftwareService):
|
||||
verbose_name = "Dowkuwiki"
|
||||
icon = 'orchestra/icons/apps/Dokuwiki.png'
|
|
@ -0,0 +1,6 @@
|
|||
from .options import SoftwareService
|
||||
|
||||
|
||||
class DrupalService(SoftwareService):
|
||||
verbose_name = "Drupal"
|
||||
icon = 'orchestra/icons/apps/Drupal.png'
|
|
@ -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('<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 "
|
||||
"<a href=\"password/\">this form</a>."))
|
||||
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 = '<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
|
||||
site_name_link = '<a href="http://%s">%s</a>' % (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
|
||||
|
|
|
@ -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 <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):
|
||||
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
|
||||
|
|
|
@ -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',)
|
|
@ -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'
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 <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.<br>"
|
||||
"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.<br>"
|
||||
"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.<br>"
|
||||
"The installation will be completed after visiting "
|
||||
"http://<app_name>.drupal.orchestra.lan/install.php?profile=standard<br>"
|
||||
"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
|
|
@ -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():
|
||||
|
|
|
@ -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'})
|
||||
|
|
|
@ -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)
|
|
@ -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,
|
||||
|
@ -203,6 +208,16 @@ 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)
|
||||
|
|
|
@ -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 = _("<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):
|
||||
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'^/[^ ]*$'
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'),
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue