diff --git a/TODO.md b/TODO.md
index 0dc0b51a..fcdfaba0 100644
--- a/TODO.md
+++ b/TODO.md
@@ -161,23 +161,23 @@
* Rename apache logs ending on .log in order to logrotate easily
-* SaaS wordpress multiple blogs per user? separate users from sites? SaaSUser SaaSSite models
-* Custom domains for SaaS apps (wordpress Vhost) SaaSSite.domain ?
+* multitenant webapps modeled on WepApp -> name unique for all accounts
+
+* webapp compat webapp-options
+* webapps modeled on classes instead of settings?
* Change account and orders
+* Mix webapps type with backends (two for the price of one)
-==== SaaS ====
-Wordpress
----------
-* site_name
-* email
-* site_title
-* site_domain (optional)
+* Webapp options and type compatibility
-BSCW
-----
-* email
-* username
-* quota
-* password (optional)
+Multi-tenant WebApps
+--------------------
+* SaaS - Those apps that can't use custom domain
+* WebApp - Those apps that can use custom domain
+
+* Howto upgrade webapp PHP version? SetHandler php54-cgi ? or create a new app
+
+
+* prevent @pangea.org email addresses on contacts
diff --git a/orchestra/admin/actions.py b/orchestra/admin/actions.py
index 75ac3d9e..9231b128 100644
--- a/orchestra/admin/actions.py
+++ b/orchestra/admin/actions.py
@@ -38,7 +38,7 @@ class SendEmail(object):
raise PermissionDenied
initial={
'email_from': self.default_from,
- 'to': ' '.join(self.queryset.values_list('email', flat=True))
+ 'to': ' '.join(self.get_queryset_emails())
}
form = self.form(initial=initial)
if request.POST.get('post'):
@@ -63,8 +63,10 @@ class SendEmail(object):
# Display confirmation page
return render(request, self.template, self.context)
+ def get_queryset_emails(self):
+ return self.queryset.value_list('email', flat=True)
+
def confirm_email(self, request, **options):
- num = len(self.queryset)
email_from = options['email_from']
extra_to = options['extra_to']
subject = options['subject']
@@ -72,13 +74,15 @@ class SendEmail(object):
# The user has already confirmed
if request.POST.get('post') == 'email_confirmation':
emails = []
- for contact in self.queryset.all():
- emails.append((subject, message, email_from, [contact.email]))
+ num = 0
+ for email in self.get_queryset_emails():
+ emails.append((subject, message, email_from, [email]))
+ num += 1
if extra_to:
emails.append((subject, message, email_from, extra_to))
- send_mass_mail(emails)
+ send_mass_mail(emails, fail_silently=False)
msg = ungettext(
- _("Message has been sent to %s.") % str(contact),
+ _("Message has been sent to one %s.") % self.opts.verbose_name_plural,
_("Message has been sent to %i %s.") % (num, self.opts.verbose_name_plural),
num
)
diff --git a/orchestra/apps/accounts/actions.py b/orchestra/apps/accounts/actions.py
index 7a6bd349..fd583fd2 100644
--- a/orchestra/apps/accounts/actions.py
+++ b/orchestra/apps/accounts/actions.py
@@ -8,6 +8,8 @@ from django.utils.translation import ungettext, ugettext_lazy as _
from orchestra.admin.decorators import action_with_confirmation
from orchestra.core import services
+from . import settings
+
@transaction.atomic
@action_with_confirmation()
@@ -38,6 +40,7 @@ list_contacts.verbose_name = _("List contacts")
def service_report(modeladmin, request, queryset):
+ # TODO resources
accounts = []
fields = []
# First we get related manager names to fire a prefetch related
@@ -59,4 +62,4 @@ def service_report(modeladmin, request, queryset):
'accounts': accounts,
'date': timezone.now().today()
}
- return render(request, 'admin/accounts/account/service_report.html', context)
+ return render(request, settings.ACCOUNTS_SERVICE_REPORT_TEMPLATE, context)
diff --git a/orchestra/apps/accounts/settings.py b/orchestra/apps/accounts/settings.py
index f2dd8847..7d840bb2 100644
--- a/orchestra/apps/accounts/settings.py
+++ b/orchestra/apps/accounts/settings.py
@@ -47,3 +47,7 @@ ACCOUNTS_CREATE_RELATED = getattr(settings, 'ACCOUNTS_CREATE_RELATED', (
_("Designates whether to creates a related subdomain <username>.orchestra.lan or not."),
),
))
+
+
+ACCOUNTS_SERVICE_REPORT_TEMPLATE = getattr(settings, 'ACCOUNTS_SERVICE_REPORT_TEMPLATE',
+ 'admin/accounts/account/service_report.html')
diff --git a/orchestra/apps/accounts/templates/admin/accounts/account/service_report.html b/orchestra/apps/accounts/templates/admin/accounts/account/service_report.html
index 3e6c84d3..1ac7a5fb 100644
--- a/orchestra/apps/accounts/templates/admin/accounts/account/service_report.html
+++ b/orchestra/apps/accounts/templates/admin/accounts/account/service_report.html
@@ -54,7 +54,10 @@
{{ opts.verbose_name_plural|capfirst }}
{% for obj in related %}
- - {{ obj }}{% if not obj|isactive %} ({% trans "disabled" %}){% endif %}
+ - {{ obj }}
+ {% if not obj|isactive %} ({% trans "disabled" %}){% endif %}
+ {{ obj.get_description|capfirst }}
+
{% endfor %}
{% endfor %}
diff --git a/orchestra/apps/bills/models.py b/orchestra/apps/bills/models.py
index 34f507e5..e9d4926e 100644
--- a/orchestra/apps/bills/models.py
+++ b/orchestra/apps/bills/models.py
@@ -159,7 +159,8 @@ class Bill(models.Model):
return now + relativedelta(months=1)
def close(self, payment=False):
- assert self.is_open, "Bill not in Open state"
+ if not self.is_open:
+ raise TypeError("Bill not in Open state.")
if payment is False:
payment = self.account.paymentsources.get_default()
if not self.due_on:
diff --git a/orchestra/apps/domains/models.py b/orchestra/apps/domains/models.py
index 71c24043..da164077 100644
--- a/orchestra/apps/domains/models.py
+++ b/orchestra/apps/domains/models.py
@@ -1,6 +1,6 @@
from django.core.exceptions import ValidationError
from django.db import models
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ungettext, ugettext_lazy as _
from orchestra.core import services
from orchestra.core.validators import validate_ipv4_address, validate_ipv6_address, validate_ascii
@@ -50,6 +50,15 @@ class Domain(models.Model):
def subdomains(self):
return Domain.objects.filter(name__regex='\.%s$' % self.name)
+ def get_description(self):
+ if self.is_top:
+ num = self.subdomains.count()
+ return ungettext(
+ _("top domain with one subdomain"),
+ _("top domain with %d subdomains") % num,
+ num)
+ return _("subdomain")
+
def get_absolute_url(self):
return 'http://%s' % self.name
diff --git a/orchestra/apps/lists/backends.py b/orchestra/apps/lists/backends.py
index 3629dc9c..9e2d9c0f 100644
--- a/orchestra/apps/lists/backends.py
+++ b/orchestra/apps/lists/backends.py
@@ -37,8 +37,8 @@ class MailmanBackend(ServiceController):
[[ $(grep "^\s*%(address_domain)s\s*$" %(virtual_alias_domains)s) ]] || {
echo "%(address_domain)s" >> %(virtual_alias_domains)s
UPDATED_VIRTUAL_ALIAS_DOMAINS=1
- }""" % context
- ))
+ }""") % context
+ )
def exclude_virtual_alias_domain(self, context):
address_domain = context['address_domain']
@@ -58,13 +58,11 @@ class MailmanBackend(ServiceController):
self.append(textwrap.dedent("""\
[[ ! -e %(mailman_root)s/lists/%(name)s ]] && {
newlist --quiet --emailhost='%(domain)s' '%(name)s' '%(admin)s' '%(password)s'
- }""" % context))
+ }""") % context)
# Custom domain
if mail_list.address:
- aliases = self.get_virtual_aliases(context)
+ context['aliases'] = self.get_virtual_aliases(context)
# Preserve indentation
- spaces = ' '*4
- context['aliases'] = spaces + aliases.replace('\n', '\n'+spaces)
self.append(textwrap.dedent("""\
if [[ ! $(grep '\s\s*%(name)s\s*$' %(virtual_alias)s) ]]; then
echo '# %(banner)s\n%(aliases)s
@@ -78,23 +76,28 @@ class MailmanBackend(ServiceController):
' >> %(virtual_alias)s
UPDATED_VIRTUAL_ALIAS=1
fi
- fi""" % context
- ))
- self.append('echo "require_explicit_destination = 0" | '
- '%(mailman_root)s/bin/config_list -i /dev/stdin %(name)s' % context)
+ fi""") % context
+ )
+ self.append(
+ 'echo "require_explicit_destination = 0" | '
+ '%(mailman_root)s/bin/config_list -i /dev/stdin %(name)s' % context
+ )
self.append(textwrap.dedent("""\
echo "host_name = '%(address_domain)s'" | \
- %(mailman_root)s/bin/config_list -i /dev/stdin %(name)s""" % context))
+ %(mailman_root)s/bin/config_list -i /dev/stdin %(name)s""") % context
+ )
else:
# Cleanup shit
self.append(textwrap.dedent("""\
if [[ ! $(grep '\s\s*%(name)s\s*$' %(virtual_alias)s) ]]; then
sed -i "/^.*\s%(name)s\s*$/d" %(virtual_alias)s
- fi""" % context
- ))
+ fi""") % context
+ )
# Update
if context['password'] is not None:
- self.append('%(mailman_root)s/bin/change_pw --listname="%(name)s" --password="%(password)s"' % context)
+ self.append(
+ '%(mailman_root)s/bin/change_pw --listname="%(name)s" --password="%(password)s"' % context
+ )
self.include_virtual_alias_domain(context)
def delete(self, mail_list):
@@ -102,8 +105,8 @@ class MailmanBackend(ServiceController):
self.exclude_virtual_alias_domain(context)
self.append(textwrap.dedent("""\
sed -i -e '/^.*\s%(name)s\(%(address_regex)s\)\s*$/d' \\
- -e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s""" % context
- ))
+ -e 'N; /^\s*\\n\s*$/d; P; D' %(virtual_alias)s""") % context
+ )
self.append("rmlist -a %(name)s" % context)
def commit(self):
@@ -111,8 +114,8 @@ class MailmanBackend(ServiceController):
self.append(textwrap.dedent("""
[[ $UPDATED_VIRTUAL_ALIAS == 1 ]] && { postmap %(virtual_alias)s; }
[[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]] && { /etc/init.d/postfix reload; }
- """ % context
- ))
+ """) % context
+ )
def get_context_files(self):
return {
@@ -163,7 +166,7 @@ class MailmanTraffic(ServiceMonitor):
| tr '\\n' '+' \\
| xargs -i echo {} )
echo ${OBJECT_ID} $(( ${SIZE}*${SUBSCRIBERS} ))
- }""" % current_date))
+ }""") % current_date)
def monitor(self, mail_list):
context = self.get_context(mail_list)
diff --git a/orchestra/apps/mailboxes/actions.py b/orchestra/apps/mailboxes/actions.py
new file mode 100644
index 00000000..81b9150a
--- /dev/null
+++ b/orchestra/apps/mailboxes/actions.py
@@ -0,0 +1,7 @@
+from orchestra.admin.actions import SendEmail
+
+
+class SendMailboxEmail(SendEmail):
+ def get_queryset_emails(self):
+ for mailbox in self.queryset.all():
+ yield mailbox.get_local_address()
diff --git a/orchestra/apps/mailboxes/admin.py b/orchestra/apps/mailboxes/admin.py
index 7571a0e0..2ca53893 100644
--- a/orchestra/apps/mailboxes/admin.py
+++ b/orchestra/apps/mailboxes/admin.py
@@ -11,6 +11,8 @@ from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
from orchestra.admin.utils import admin_link, change_url
from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
+from . import settings
+from .actions import SendMailboxEmail
from .filters import HasMailboxListFilter, HasForwardListFilter, HasAddressListFilter
from .forms import MailboxCreationForm, MailboxChangeForm, AddressForm
from .models import Mailbox, Address, Autoresponse
@@ -71,6 +73,13 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
display_addresses.short_description = _("Addresses")
display_addresses.allow_tags = True
+ def get_actions(self, request):
+ if settings.MAILBOXES_LOCAL_ADDRESS_DOMAIN:
+ self.actions = (SendMailboxEmail(),)
+ else:
+ self.actions = ()
+ return super(MailboxAdmin, self).get_actions(request)
+
def get_fieldsets(self, request, obj=None):
fieldsets = super(MailboxAdmin, self).get_fieldsets(request, obj=obj)
if obj and obj.filtering == obj.CUSTOM:
diff --git a/orchestra/apps/mailboxes/backends.py b/orchestra/apps/mailboxes/backends.py
index c01893bf..acaa541c 100644
--- a/orchestra/apps/mailboxes/backends.py
+++ b/orchestra/apps/mailboxes/backends.py
@@ -89,10 +89,10 @@ class PasswdVirtualUserBackend(ServiceController):
context = {
'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH
}
- self.append(
- "[[ $UPDATED_VIRTUAL_MAILBOX_MAPS == 1 ]] && { postmap %(virtual_mailbox_maps)s; }"
- % context
- )
+ self.append(textwrap.dedent("""\
+ [[ $UPDATED_VIRTUAL_MAILBOX_MAPS == 1 ]] && {
+ postmap %(virtual_mailbox_maps)s
+ }""" % context))
def get_context(self, mailbox):
context = {
@@ -123,8 +123,7 @@ class PostfixAddressBackend(ServiceController):
[[ $(grep "^\s*%(domain)s\s*$" %(virtual_alias_domains)s) ]] || {
echo "%(domain)s" >> %(virtual_alias_domains)s
UPDATED_VIRTUAL_ALIAS_DOMAINS=1
- }""" % context
- ))
+ }""") % context)
def exclude_virtual_alias_domain(self, context):
domain = context['domain']
@@ -151,8 +150,7 @@ class PostfixAddressBackend(ServiceController):
sed -i "s/^%(email)s\s.*$/${LINE}/" %(virtual_alias_maps)s
UPDATED_VIRTUAL_ALIAS_MAPS=1
fi
- fi""" % context
- ))
+ fi""") % context)
else:
logger.warning("Address %i is empty" % address.pk)
self.append('sed -i "/^%(email)s\s/d" %(virtual_alias_maps)s')
@@ -163,8 +161,7 @@ class PostfixAddressBackend(ServiceController):
if [[ $(grep "^%(email)s\s" %(virtual_alias_maps)s) ]]; then
sed -i "/^%(email)s\s.*$/d" %(virtual_alias_maps)s
UPDATED_VIRTUAL_ALIAS_MAPS=1
- fi""" % context
- ))
+ fi""") % context)
def save(self, address):
context = self.get_context(address)
@@ -181,8 +178,8 @@ class PostfixAddressBackend(ServiceController):
self.append(textwrap.dedent("""
[[ $UPDATED_VIRTUAL_ALIAS_MAPS == 1 ]] && { postmap %(virtual_alias_maps)s; }
[[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]] && { /etc/init.d/postfix reload; }
- """ % context
- ))
+ """) % context
+ )
def get_context_files(self):
return {
diff --git a/orchestra/apps/mailboxes/models.py b/orchestra/apps/mailboxes/models.py
index 798c194e..0740def0 100644
--- a/orchestra/apps/mailboxes/models.py
+++ b/orchestra/apps/mailboxes/models.py
@@ -77,6 +77,11 @@ class Mailbox(models.Model):
address.delete()
else:
address.save()
+
+ def get_local_address(self):
+ if not settings.MAILBOXES_LOCAL_ADDRESS_DOMAIN:
+ raise AttributeError("Mailboxes do not have a defined local address domain")
+ return '@'.join((self.name, settings.MAILBOXES_LOCAL_ADDRESS_DOMAIN))
class Address(models.Model):
diff --git a/orchestra/apps/mailboxes/settings.py b/orchestra/apps/mailboxes/settings.py
index 5ca32e30..d00ceb7d 100644
--- a/orchestra/apps/mailboxes/settings.py
+++ b/orchestra/apps/mailboxes/settings.py
@@ -61,7 +61,12 @@ MAILBOXES_MAILBOX_FILTERINGS = getattr(settings, 'MAILBOXES_MAILBOX_FILTERINGS',
})
-MAILBOXES_MAILBOX_DEFAULT_FILTERING = getattr(settings, 'MAILBOXES_MAILBOX_DEFAULT_FILTERING', 'REDIRECT')
+MAILBOXES_MAILBOX_DEFAULT_FILTERING = getattr(settings, 'MAILBOXES_MAILBOX_DEFAULT_FILTERING',
+ 'REDIRECT')
MAILBOXES_MAILDIRSIZE_PATH = getattr(settings, 'MAILBOXES_MAILDIRSIZE_PATH', '%(home)s/Maildir/maildirsize')
+
+
+MAILBOXES_LOCAL_ADDRESS_DOMAIN = getattr(settings, 'MAILBOXES_LOCAL_ADDRESS_DOMAIN',
+ 'orchestra.lan')
diff --git a/orchestra/apps/miscellaneous/models.py b/orchestra/apps/miscellaneous/models.py
index 51bfa759..f3efe473 100644
--- a/orchestra/apps/miscellaneous/models.py
+++ b/orchestra/apps/miscellaneous/models.py
@@ -60,6 +60,9 @@ class Miscellaneous(models.Model):
except type(self).account.field.rel.to.DoesNotExist:
return self.is_active
+ def get_description(self):
+ return ' '.join((str(self.amount), self.service.description or self.service.verbose_name))
+
def clean(self):
if self.identifier:
self.identifier = self.identifier.strip()
diff --git a/orchestra/apps/orchestration/backends.py b/orchestra/apps/orchestration/backends.py
index e8a9c8b8..ba4bd87c 100644
--- a/orchestra/apps/orchestration/backends.py
+++ b/orchestra/apps/orchestration/backends.py
@@ -10,6 +10,14 @@ from orchestra import plugins
from . import methods
+class ServiceMount(plugins.PluginMount):
+ def __init__(cls, name, bases, attrs):
+ # Make sure backends specify a model attribute
+ if not (attrs.get('abstract', False) or name == 'ServiceBackend' or cls.model):
+ raise AttributeError("'%s' does not have a defined model attribute." % cls)
+ super(ServiceMount, cls).__init__(name, bases, attrs)
+
+
class ServiceBackend(plugins.Plugin):
"""
Service management backend base class
@@ -27,7 +35,7 @@ class ServiceBackend(plugins.Plugin):
ignore_fields = []
actions = []
- __metaclass__ = plugins.PluginMount
+ __metaclass__ = ServiceMount
def __unicode__(self):
return type(self).__name__
diff --git a/orchestra/apps/orchestration/methods.py b/orchestra/apps/orchestration/methods.py
index abe69f08..c8b4f682 100644
--- a/orchestra/apps/orchestration/methods.py
+++ b/orchestra/apps/orchestration/methods.py
@@ -50,10 +50,11 @@ def SSH(backend, log, server, cmds, async=False):
key = settings.ORCHESTRATION_SSH_KEY_PATH
try:
ssh.connect(addr, username='root', key_filename=key, timeout=10)
- except socket.error:
- logger.error('%s timed out on %s' % (backend, server))
+ except socket.error, e:
+ logger.error('%s timed out on %s' % (backend, addr))
log.state = log.TIMEOUT
- log.save(update_fields=['state'])
+ log.stderr = str(e)
+ log.save(update_fields=['state', 'stderr'])
return
transport = ssh.get_transport()
diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py
index c343ff85..83f27029 100644
--- a/orchestra/apps/payments/admin.py
+++ b/orchestra/apps/payments/admin.py
@@ -24,6 +24,7 @@ STATE_COLORS = {
class PaymentSourceAdmin(SelectPluginAdminMixin, AccountAdminMixin, admin.ModelAdmin):
list_display = ('label', 'method', 'number', 'account_link', 'is_active')
list_filter = ('method', 'is_active')
+ search_fields = ('account__username', 'account__full_name', 'data')
plugin = PaymentMethod
plugin_field = 'method'
diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py
index 7ffcd236..86111dad 100644
--- a/orchestra/apps/payments/models.py
+++ b/orchestra/apps/payments/models.py
@@ -119,23 +119,27 @@ class Transaction(models.Model):
if amount >= self.bill.total:
raise ValidationError(_("New transactions can not be allocated for this bill."))
+ def check_state(*args):
+ if self.state not in args:
+ raise TypeError("Transaction not in %s" % ' or '.join(args))
+
def mark_as_processed(self):
- assert self.state == self.WAITTING_PROCESSING
+ self.check_state(self.WAITTING_PROCESSING)
self.state = self.WAITTING_EXECUTION
self.save(update_fields=['state'])
def mark_as_executed(self):
- assert self.state == self.WAITTING_EXECUTION
+ self.check_state(self.WAITTING_EXECUTION)
self.state = self.EXECUTED
self.save(update_fields=['state'])
def mark_as_secured(self):
- assert self.state == self.EXECUTED
+ self.check_state(self.EXECUTED)
self.state = self.SECURED
self.save(update_fields=['state'])
def mark_as_rejected(self):
- assert self.state == self.EXECUTED
+ self.check_state(self.EXECUTED)
self.state = self.REJECTED
self.save(update_fields=['state'])
@@ -167,22 +171,26 @@ class TransactionProcess(models.Model):
def __unicode__(self):
return '#%i' % self.id
+ def check_state(*args):
+ if self.state not in args:
+ raise TypeError("Transaction process not in %s" % ' or '.join(args))
+
def mark_as_executed(self):
- assert self.state == self.CREATED
+ self.check_state(self.CREATED)
self.state = self.EXECUTED
for transaction in self.transactions.all():
transaction.mark_as_executed()
self.save(update_fields=['state'])
def abort(self):
- assert self.state in [self.CREATED, self.EXCECUTED]
+ self.check_state(self.CREATED, self.EXCECUTED)
self.state = self.ABORTED
for transaction in self.transaction.all():
transaction.mark_as_aborted()
self.save(update_fields=['state'])
def commit(self):
- assert self.state in [self.CREATED, self.EXECUTED]
+ self.check_state(self.CREATED, self.EXECUTED)
self.state = self.COMMITED
for transaction in self.transactions.processing():
transaction.mark_as_secured()
diff --git a/orchestra/apps/resources/actions.py b/orchestra/apps/resources/actions.py
index 5a1f5865..db2c4aa4 100644
--- a/orchestra/apps/resources/actions.py
+++ b/orchestra/apps/resources/actions.py
@@ -9,33 +9,32 @@ from django.utils.translation import ungettext, ugettext_lazy as _
def run_monitor(modeladmin, request, queryset):
""" Resource and ResourceData run monitors """
referer = request.META.get('HTTP_REFERER')
- if not queryset:
- modeladmin.message_user(request, _("No resource has been selected,"))
- return redirect(referer)
async = modeladmin.model.monitor.func_defaults[0]
- results = []
+ logs = set()
for resource in queryset:
- result = resource.monitor()
+ results = resource.monitor()
if not async:
- results += result
+ for result in results:
+ if hasattr(result, 'log'):
+ logs.add(result.log.pk)
modeladmin.log_change(request, resource, _("Run monitors"))
- num = len(queryset)
if async:
+ num = len(queryset)
link = reverse('admin:djcelery_taskstate_changelist')
msg = ungettext(
_("One selected resource has been scheduled for monitoring.") % link,
_("%s selected resource have been scheduled for monitoring.") % (num, link),
num)
else:
- if len(results) == 1:
- log = results[0].log
- link = reverse('admin:orchestration_backendlog_change', args=(log.pk,))
- msg = _("One selected resource has been monitored.") % link
- elif len(results) >= 1:
- logs = [str(result.log.pk) for result in results]
+ num = len(logs)
+ if num == 1:
+ log = logs.pop()
+ link = reverse('admin:orchestration_backendlog_change', args=(log,))
+ msg = _("One related monitor has been executed.") % link
+ elif num >= 1:
link = reverse('admin:orchestration_backendlog_changelist')
link += '?id__in=%s' % ','.join(logs)
- msg = _("%s selected resources have been monitored.") % (num, link)
+ msg = _("%s related monitors have been executed.") % (num, link)
else:
msg = _("No related monitors have been executed.")
modeladmin.message_user(request, mark_safe(msg))
diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py
index cb4d1af2..525d7f25 100644
--- a/orchestra/apps/resources/models.py
+++ b/orchestra/apps/resources/models.py
@@ -119,8 +119,7 @@ class Resource(models.Model):
def get_model_path(self, monitor):
""" returns a model path between self.content_type and monitor.model """
resource_model = self.content_type.model_class()
- model_path = ServiceMonitor.get_backend(monitor).model
- monitor_model = get_model(model_path)
+ monitor_model = ServiceMonitor.get_backend(monitor).model_class()
return get_model_field_path(monitor_model, resource_model)
def sync_periodic_task(self):
@@ -223,6 +222,7 @@ class ResourceData(models.Model):
)
else:
fields = '__'.join(path)
+ monitor_model = ServiceMonitor.get_backend(monitor).model_class()
objects = monitor_model.objects.filter(**{fields: self.object_id})
pks = objects.values_list('id', flat=True)
ct = ContentType.objects.get_for_model(monitor_model)
diff --git a/orchestra/apps/saas/backends/wordpressmu.py b/orchestra/apps/saas/backends/wordpressmu.py
deleted file mode 100644
index 6b187424..00000000
--- a/orchestra/apps/saas/backends/wordpressmu.py
+++ /dev/null
@@ -1,101 +0,0 @@
-import re
-
-import requests
-from django.utils.translation import ugettext_lazy as _
-
-from orchestra.apps.orchestration import ServiceController
-
-from . import SaaSServiceMixin
-from .. import settings
-
-
-class WordpressMuBackend(SaaSServiceMixin, ServiceController):
- verbose_name = _("Wordpress multisite")
-
- @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
- if base_url.endswith('/'):
- base_url = base_url[:-1]
- return base_url
-
- def create_blog(self, webapp, server):
- emails = webapp.account.contacts.filter(email_usage__contains='')
- email = emails.values_list('email', flat=True).first()
-
- base_url = self.get_base_url()
- session = requests.Session()
- self.login(session)
-
- url = base_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]': email,
- '_wpnonce_add-blog': wpnonce,
- }
- # TODO validate response
- response = session.post(url, data=data)
-
- def delete_blog(self, webapp, server):
- # OH, I've enjoied so much coding this methods that I want to thanks
- # the wordpress team for the excellent software they are producing
- context = self.get_context(webapp)
- session = requests.Session()
- self.login(session)
-
- base_url = self.get_base_url()
- search = base_url + '/wp-admin/network/sites.php?s=%(name)s&action=blogs' % context
- regex = re.compile(
- '%(name)s' % context
- )
- content = session.get(search).content
- ids = regex.search(content).groups()
- if len(ids) > 1:
- raise ValueError("Multiple matches")
-
- delete = re.compile('(.*)')
- content = delete.search(content).groups()[0]
- wpnonce = re.compile('_wpnonce=([^"]*)"')
- wpnonce = wpnonce.search(content).groups()[0]
- delete = '/wp-admin/network/sites.php?action=confirm&action2=deleteblog'
- delete += '&id=%d&_wpnonce=%d' % (ids[0], wpnonce)
-
- content = session.get(delete).content
- wpnonce = re.compile('name="_wpnonce"\s+value="([^"]*)"')
- wpnonce = wpnonce.search(content).groups()[0]
- data = {
- 'action': 'deleteblog',
- 'id': ids[0],
- '_wpnonce': wpnonce,
- '_wp_http_referer': '/wp-admin/network/sites.php',
- }
- delete = base_url + '/wp-admin/network/sites.php?action=deleteblog'
- session.post(delete, data=data)
-
- def save(self, webapp):
- self.append(self.create_blog, webapp)
-
- def delete(self, webapp):
- self.append(self.delete_blog, webapp)
diff --git a/orchestra/apps/saas/services/dokuwiki.py b/orchestra/apps/saas/services/dokuwiki.py
deleted file mode 100644
index 2ff53b91..00000000
--- a/orchestra/apps/saas/services/dokuwiki.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .options import SoftwareService
-
-
-class DokuwikiService(SoftwareService):
- verbose_name = "Dowkuwiki"
- icon = 'saas/icons/Dokuwiki.png'
diff --git a/orchestra/apps/saas/services/drupal.py b/orchestra/apps/saas/services/drupal.py
deleted file mode 100644
index 65ae9e5e..00000000
--- a/orchestra/apps/saas/services/drupal.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .options import SoftwareService
-
-
-class DrupalService(SoftwareService):
- verbose_name = "Drupal"
- icon = 'saas/icons/Drupal.png'
diff --git a/orchestra/apps/saas/services/wordpress.py b/orchestra/apps/saas/services/wordpress.py
deleted file mode 100644
index 978e2de5..00000000
--- a/orchestra/apps/saas/services/wordpress.py
+++ /dev/null
@@ -1,23 +0,0 @@
-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 = 'saas/icons/WordPress.png'
- site_name_base_domain = 'blogs.orchestra.lan'
- change_readonly_fileds = ('email',)
diff --git a/orchestra/apps/saas/settings.py b/orchestra/apps/saas/settings.py
index 6cc78283..ef5257e8 100644
--- a/orchestra/apps/saas/settings.py
+++ b/orchestra/apps/saas/settings.py
@@ -2,31 +2,8 @@ from django.conf import settings
SAAS_ENABLED_SERVICES = getattr(settings, 'SAAS_ENABLED_SERVICES', (
- 'orchestra.apps.saas.services.wordpress.WordPressService',
- 'orchestra.apps.saas.services.drupal.DrupalService',
- 'orchestra.apps.saas.services.dokuwiki.DokuwikiService',
'orchestra.apps.saas.services.moodle.MoodleService',
'orchestra.apps.saas.services.bscw.BSCWService',
'orchestra.apps.saas.services.gitlab.GitLabService',
'orchestra.apps.saas.services.phplist.PHPListService',
))
-
-
-SAAS_WORDPRESSMU_BASE_URL = getattr(settings, 'SAAS_WORDPRESSMU_BASE_URL',
- 'http://%(site_name)s.example.com')
-
-
-SAAS_WORDPRESSMU_ADMIN_PASSWORD = getattr(settings, 'SAAS_WORDPRESSMU_ADMIN_PASSWORD',
- 'secret')
-
-
-SAAS_DOKUWIKIMU_TEMPLATE_PATH = setattr(settings, 'SAAS_DOKUWIKIMU_TEMPLATE_PATH',
- '/home/httpd/htdocs/wikifarm/template.tar.gz')
-
-
-SAAS_DOKUWIKIMU_FARM_PATH = getattr(settings, 'SAAS_DOKUWIKIMU_FARM_PATH',
- '/home/httpd/htdocs/wikifarm/farm')
-
-
-SAAS_DRUPAL_SITES_PATH = getattr(settings, 'SAAS_DRUPAL_SITES_PATH',
- '/home/httpd/htdocs/drupal-mu/sites/%(site_name)s')
diff --git a/orchestra/apps/systemusers/models.py b/orchestra/apps/systemusers/models.py
index cf53230c..0703fe03 100644
--- a/orchestra/apps/systemusers/models.py
+++ b/orchestra/apps/systemusers/models.py
@@ -66,13 +66,17 @@ class SystemUser(models.Model):
def has_shell(self):
return self.shell not in settings.SYSTEMUSERS_DISABLED_SHELLS
+ def get_description(self):
+ return self.get_shell_display()
+
def save(self, *args, **kwargs):
if not self.home:
self.home = self.get_base_home()
super(SystemUser, self).save(*args, **kwargs)
def clean(self):
- self.home = os.path.normpath(self.home)
+ if self.home:
+ self.home = os.path.normpath(self.home)
if self.directory:
directory_error = None
if self.has_shell:
@@ -119,10 +123,7 @@ class SystemUser(models.Model):
return os.path.normpath(settings.SYSTEMUSERS_HOME % context)
def get_home(self):
- return os.path.join(
- self.home or self.get_base_home(),
- self.directory
- )
+ return os.path.join(self.home, self.directory)
services.register(SystemUser)
diff --git a/orchestra/apps/saas/backends/dokuwikimu.py b/orchestra/apps/webapps/backends/dokuwikimu.py
similarity index 91%
rename from orchestra/apps/saas/backends/dokuwikimu.py
rename to orchestra/apps/webapps/backends/dokuwikimu.py
index 78d0d721..76eb1b51 100644
--- a/orchestra/apps/saas/backends/dokuwikimu.py
+++ b/orchestra/apps/webapps/backends/dokuwikimu.py
@@ -4,12 +4,12 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceController
-from . import SaaSServiceMixin
from .. import settings
-class DokuWikiMuBackend(SaaSServiceMixin, ServiceController):
+class DokuWikiMuBackend(ServiceController):
verbose_name = _("DokuWiki multisite")
+ model = 'webapps.WebApp'
def save(self, webapp):
context = self.get_context(webapp)
diff --git a/orchestra/apps/saas/backends/drupalmu.py b/orchestra/apps/webapps/backends/drupalmu.py
similarity index 50%
rename from orchestra/apps/saas/backends/drupalmu.py
rename to orchestra/apps/webapps/backends/drupalmu.py
index 1e0b56fb..7f9caa71 100644
--- a/orchestra/apps/saas/backends/drupalmu.py
+++ b/orchestra/apps/webapps/backends/drupalmu.py
@@ -1,30 +1,32 @@
import os
+import textwrap
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.orchestration import ServiceController
-from . import SaaSServiceMixin
from .. import settings
-class DrupalMuBackend(SaaSServiceMixin, ServiceController):
+class DrupalMuBackend(ServiceController):
verbose_name = _("Drupal multisite")
-
+ model = 'webapps.WebApp'
+
def save(self, webapp):
context = self.get_context(webapp)
- self.append("mkdir %(drupal_path)s" % context)
- self.append("chown -R www-data %(drupal_path)s" % context)
- self.append(
- "# the following assumes settings.php to be previously configured\n"
- "REGEX='^\s*$databases\[.default.\]\[.default.\]\[.prefix.\]'\n"
- "CONFIG='$databases[\'default\'][\'default\'][\'prefix\'] = \'%(app_name)s_\';'\n"
- "if [[ ! $(grep $REGEX %(drupal_settings)s) ]]; then\n"
- " echo $CONFIG >> %(drupal_settings)s\n"
- "fi" % context
+ self.append(textwrap.dedent("""\
+ mkdir %(drupal_path)s
+ chown -R www-data %(drupal_path)s
+
+ # the following assumes settings.php to be previously configured
+ REGEX='^\s*$databases\[.default.\]\[.default.\]\[.prefix.\]'
+ CONFIG='$databases[\'default\'][\'default\'][\'prefix\'] = \'%(app_name)s_\';'
+ if [[ ! $(grep $REGEX %(drupal_settings)s) ]]; then
+ echo $CONFIG >> %(drupal_settings)s
+ fi""") % context
)
- def selete(self, webapp):
+ def delete(self, webapp):
context = self.get_context(webapp)
self.append("rm -fr %(app_path)s" % context)
diff --git a/orchestra/apps/webapps/backends/wordpressmu.py b/orchestra/apps/webapps/backends/wordpressmu.py
new file mode 100644
index 00000000..87a5786c
--- /dev/null
+++ b/orchestra/apps/webapps/backends/wordpressmu.py
@@ -0,0 +1,124 @@
+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'
+
+ @property
+ def script(self):
+ return self.cmds
+
+ def login(self, session):
+ base_url = self.get_base_url()
+ login_url = base_url + '/wp-login.php'
+ login_data = {
+ 'log': 'admin',
+ 'pwd': settings.WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD,
+ 'redirect_to': '/wp-admin/'
+ }
+ response = session.post(login_url, data=login_data)
+ if response.url != base_url + '/wp-admin/':
+ raise IOError("Failure login to remote application")
+
+ def get_base_url(self):
+ base_url = settings.WEBAPPS_WORDPRESSMU_BASE_URL
+ return base_url.rstrip('/')
+
+ def validate_response(self, response):
+ if response.status_code != 200:
+ errors = re.findall(r'\n\t(.*)
', response.content)
+ raise RuntimeError(errors[0] if errors else 'Unknown %i error' % response.status_code)
+
+ def get_id(self, session, webapp):
+ search = self.get_base_url()
+ search += '/wp-admin/network/sites.php?s=%s&action=blogs' % webapp.name
+ regex = re.compile(
+ '%s' % webapp.name
+ )
+ content = session.get(search).content
+ # Get id
+ ids = regex.search(content)
+ if not ids:
+ raise RuntimeError("Blog '%s' not found" % webapp.name)
+ ids = ids.groups()
+ if len(ids) > 1:
+ raise ValueError("Multiple matches")
+ # Get wpnonce
+ wpnonce = re.search(r'(.*)', content).groups()[0]
+ wpnonce = re.search(r'_wpnonce=([^"]*)"', wpnonce).groups()[0]
+ return int(ids[0]), wpnonce
+
+ def create_blog(self, webapp, server):
+ session = requests.Session()
+ self.login(session)
+
+ # Check if blog already exists
+ try:
+ self.get_id(session, webapp)
+ except RuntimeError:
+ url = self.get_base_url()
+ url += '/wp-admin/network/site-new.php'
+ content = session.get(url).content
+
+ wpnonce = re.compile('name="_wpnonce_add-blog"\s+value="([^"]*)"')
+ wpnonce = wpnonce.search(content).groups()[0]
+
+ url += '?action=add-site'
+ data = {
+ 'blog[domain]': webapp.name,
+ 'blog[title]': webapp.name,
+ 'blog[email]': webapp.account.email,
+ '_wpnonce_add-blog': wpnonce,
+ }
+
+ # Validate response
+ response = session.post(url, data=data)
+ self.validate_response(response)
+
+ def delete_blog(self, webapp, server):
+ # OH, I've enjoied so much coding this methods that I want to thanks
+ # the wordpress team for the excellent software they are producing
+ session = requests.Session()
+ self.login(session)
+
+ try:
+ id, wpnonce = self.get_id(session, webapp)
+ except RuntimeError:
+ pass
+ else:
+ delete = self.get_base_url()
+ delete += '/wp-admin/network/sites.php?action=confirm&action2=deleteblog'
+ delete += '&id=%d&_wpnonce=%s' % (id, wpnonce)
+
+ content = session.get(delete).content
+ wpnonce = re.compile('name="_wpnonce"\s+value="([^"]*)"')
+ wpnonce = wpnonce.search(content).groups()[0]
+ data = {
+ 'action': 'deleteblog',
+ 'id': id,
+ '_wpnonce': wpnonce,
+ '_wp_http_referer': '/wp-admin/network/sites.php',
+ }
+ delete = self.get_base_url()
+ delete += '/wp-admin/network/sites.php?action=deleteblog'
+ response = session.post(delete, data=data)
+ self.validate_response(response)
+
+ def save(self, webapp):
+ if webapp.type != 'wordpress-mu':
+ return
+ self.append(self.create_blog, webapp)
+
+ def delete(self, webapp):
+ if webapp.type != 'wordpress-mu':
+ return
+ self.append(self.delete_blog, webapp)
diff --git a/orchestra/apps/webapps/models.py b/orchestra/apps/webapps/models.py
index 3faafe83..350ac20e 100644
--- a/orchestra/apps/webapps/models.py
+++ b/orchestra/apps/webapps/models.py
@@ -29,10 +29,29 @@ class WebApp(models.Model):
def __unicode__(self):
return self.get_name()
+ def get_description(self):
+ return self.get_type_display()
+
+ def clean(self):
+ # Validate unique webapp names
+ if self.app_type.get('unique_name', False):
+ try:
+ webapp = WebApp.objects.exclude(id=self.pk).get(name=self.name, type=self.type)
+ except WebApp.DoesNotExist:
+ pass
+ else:
+ raise ValidationError({
+ 'name': _("A webapp with this name already exists."),
+ })
+
@cached
def get_options(self):
return { opt.name: opt.value for opt in self.options.all() }
+ @property
+ def app_type(self):
+ return settings.WEBAPPS_TYPES[self.type]
+
def get_name(self):
return self.name or settings.WEBAPPS_BLANK_NAME
@@ -40,7 +59,7 @@ class WebApp(models.Model):
return settings.WEBAPPS_FPM_START_PORT + self.account_id
def get_directive(self):
- directive = settings.WEBAPPS_TYPES[self.type]['directive']
+ directive = self.app_type['directive']
args = directive[1:] if len(directive) > 1 else ()
return directive[0], args
diff --git a/orchestra/apps/webapps/settings.py b/orchestra/apps/webapps/settings.py
index ca1a318f..8ab5c8b7 100644
--- a/orchestra/apps/webapps/settings.py
+++ b/orchestra/apps/webapps/settings.py
@@ -59,6 +59,26 @@ WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', {
'help_text': _("This creates a Webalizer application under "
"~/webapps/<app_name>-<site_name>")
},
+ 'wordpress-mu': {
+ 'verbose_name': _("Wordpress (SaaS)"),
+ 'directive': ('fpm', 'fcgi://127.0.0.1:8990/home/httpd/wordpress-mu/'),
+ 'help_text': _("This creates a Wordpress site on a multi-tenant Wordpress server.
"
+ "By default this blog is accessible via <app_name>.blogs.orchestra.lan")
+ },
+ 'dokuwiki-mu': {
+ 'verbose_name': _("DokuWiki (SaaS)"),
+ 'directive': ('alias', '/home/httpd/wikifarm/farm/'),
+ 'help_text': _("This create a Dokuwiki wiki into a shared Dokuwiki server.
"
+ "By default this wiki is accessible via <app_name>.wikis.orchestra.lan")
+ },
+ 'drupal-mu': {
+ 'verbose_name': _("Drupdal (SaaS)"),
+ 'directive': ('fpm', 'fcgi://127.0.0.1:8991/home/httpd/drupal-mu/'),
+ 'help_text': _("This creates a Drupal site into a multi-tenant Drupal server.
"
+ "The installation will be completed after visiting "
+ "http://<app_name>.drupal.orchestra.lan/install.php?profile=standard
"
+ "By default this site will be accessible via <app_name>.drupal.orchestra.lan")
+ }
})
@@ -282,3 +302,22 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
r'^[^ ]+$'
),
})
+
+
+
+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_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_DRUPAL_SITES_PATH = getattr(settings, 'WEBAPPS_DRUPAL_SITES_PATH',
+ '/home/httpd/htdocs/drupal-mu/sites/%(site_name)s')
diff --git a/orchestra/apps/websites/backends/apache.py b/orchestra/apps/websites/backends/apache.py
index 8e7416e7..b2cce739 100644
--- a/orchestra/apps/websites/backends/apache.py
+++ b/orchestra/apps/websites/backends/apache.py
@@ -50,8 +50,8 @@ class Apache2Backend(ServiceController):
} || {
echo -e '%(apache_conf)s' > %(sites_available)s
UPDATED=1
- }""" % context
- ))
+ }""") % context
+ )
self.enable_or_disable(site)
def delete(self, site):
@@ -92,7 +92,7 @@ class Apache2Backend(ServiceController):
Options +ExecCGI
AddHandler fcgid-script .php
FcgidWrapper %(fcgid_path)s\
- """ % context)
+ """) % context
for option in content.webapp.options.filter(name__startswith='Fcgid'):
fcgid += " %s %s\n" % (option.name, option.value)
fcgid += "\n"
@@ -231,7 +231,7 @@ class Apache2Traffic(ServiceMonitor):
END_DATE=$(date '+%%Y%%m%%d%%H%%M%%S' -d '%(current_date)s')
LOG_FILE="$3"
{
- { grep "%(ignore_hosts)s" "${LOG_FILE}" || echo '\\n'; } \\
+ { grep %(ignore_hosts)s "${LOG_FILE}" || echo '\\n'; } \\
| awk -v ini="${INI_DATE}" -v end="${END_DATE}" '
BEGIN {
sum = 0
diff --git a/orchestra/apps/websites/models.py b/orchestra/apps/websites/models.py
index 64377f51..3b91fd4d 100644
--- a/orchestra/apps/websites/models.py
+++ b/orchestra/apps/websites/models.py
@@ -12,6 +12,7 @@ from . import settings
class Website(models.Model):
+ """ Models a web site, also known as virtual host """
name = models.CharField(_("name"), max_length=128, unique=True,
validators=[validators.validate_name])
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py
index 9d607217..4c594510 100644
--- a/orchestra/conf/base_settings.py
+++ b/orchestra/conf/base_settings.py
@@ -122,8 +122,8 @@ AUTHENTICATION_BACKENDS = [
]
-#TODO Email config
-#EMAIL_BACKEND = 'djcelery_email.backends.CeleryEmailBackend'
+# Email config
+EMAIL_BACKEND = 'djcelery_email.backends.CeleryEmailBackend'
#################################