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
|
* 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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 ]
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
45
orchestra/apps/saas/backends/phplist.py
Normal file
45
orchestra/apps/saas/backends/phplist.py
Normal 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)
|
123
orchestra/apps/saas/backends/wordpressmu.py
Normal file
123
orchestra/apps/saas/backends/wordpressmu.py
Normal 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)
|
|
@ -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
|
||||||
|
|
|
@ -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',)
|
||||||
|
|
6
orchestra/apps/saas/services/dokuwiki.py
Normal file
6
orchestra/apps/saas/services/dokuwiki.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from .options import SoftwareService
|
||||||
|
|
||||||
|
|
||||||
|
class DokuWikiService(SoftwareService):
|
||||||
|
verbose_name = "Dowkuwiki"
|
||||||
|
icon = 'orchestra/icons/apps/Dokuwiki.png'
|
6
orchestra/apps/saas/services/drupal.py
Normal file
6
orchestra/apps/saas/services/drupal.py
Normal file
|
@ -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):
|
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 = '<name>.%s' % self.plugin.site_name_base_domain
|
||||||
help_text = _("The final URL would be <site_name>.%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 <site_name>.%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
|
|
||||||
|
|
|
@ -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://<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):
|
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
|
||||||
|
|
23
orchestra/apps/saas/services/wordpress.py
Normal file
23
orchestra/apps/saas/services/wordpress.py
Normal 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',)
|
|
@ -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'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
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():
|
||||||
|
|
|
@ -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'})
|
||||||
|
|
17
orchestra/apps/websites/apps.py
Normal file
17
orchestra/apps/websites/apps.py
Normal 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)
|
|
@ -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)
|
||||||
|
|
|
@ -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'^/[^ ]*$'
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue