Refactored saas and webapps
This commit is contained in:
parent
02b8e24f45
commit
f3551bb13e
30
TODO.md
30
TODO.md
|
@ -161,23 +161,23 @@
|
||||||
|
|
||||||
* Rename apache logs ending on .log in order to logrotate easily
|
* Rename apache logs ending on .log in order to logrotate easily
|
||||||
|
|
||||||
* SaaS wordpress multiple blogs per user? separate users from sites? SaaSUser SaaSSite models
|
* multitenant webapps modeled on WepApp -> name unique for all accounts
|
||||||
* Custom domains for SaaS apps (wordpress Vhost) SaaSSite.domain ?
|
|
||||||
|
* webapp compat webapp-options
|
||||||
|
* webapps modeled on classes instead of settings?
|
||||||
|
|
||||||
* Change account and orders
|
* Change account and orders
|
||||||
|
|
||||||
|
* Mix webapps type with backends (two for the price of one)
|
||||||
|
|
||||||
==== SaaS ====
|
* Webapp options and type compatibility
|
||||||
Wordpress
|
|
||||||
---------
|
|
||||||
* site_name
|
|
||||||
* email
|
|
||||||
* site_title
|
|
||||||
* site_domain (optional)
|
|
||||||
|
|
||||||
BSCW
|
Multi-tenant WebApps
|
||||||
----
|
--------------------
|
||||||
* email
|
* SaaS - Those apps that can't use custom domain
|
||||||
* username
|
* WebApp - Those apps that can use custom domain
|
||||||
* quota
|
|
||||||
* password (optional)
|
* Howto upgrade webapp PHP version? <FilesMatch \.php$> SetHandler php54-cgi</FilesMatch> ? or create a new app
|
||||||
|
|
||||||
|
|
||||||
|
* prevent @pangea.org email addresses on contacts
|
||||||
|
|
|
@ -38,7 +38,7 @@ class SendEmail(object):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
initial={
|
initial={
|
||||||
'email_from': self.default_from,
|
'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)
|
form = self.form(initial=initial)
|
||||||
if request.POST.get('post'):
|
if request.POST.get('post'):
|
||||||
|
@ -63,8 +63,10 @@ class SendEmail(object):
|
||||||
# Display confirmation page
|
# Display confirmation page
|
||||||
return render(request, self.template, self.context)
|
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):
|
def confirm_email(self, request, **options):
|
||||||
num = len(self.queryset)
|
|
||||||
email_from = options['email_from']
|
email_from = options['email_from']
|
||||||
extra_to = options['extra_to']
|
extra_to = options['extra_to']
|
||||||
subject = options['subject']
|
subject = options['subject']
|
||||||
|
@ -72,13 +74,15 @@ class SendEmail(object):
|
||||||
# The user has already confirmed
|
# The user has already confirmed
|
||||||
if request.POST.get('post') == 'email_confirmation':
|
if request.POST.get('post') == 'email_confirmation':
|
||||||
emails = []
|
emails = []
|
||||||
for contact in self.queryset.all():
|
num = 0
|
||||||
emails.append((subject, message, email_from, [contact.email]))
|
for email in self.get_queryset_emails():
|
||||||
|
emails.append((subject, message, email_from, [email]))
|
||||||
|
num += 1
|
||||||
if extra_to:
|
if extra_to:
|
||||||
emails.append((subject, message, email_from, extra_to))
|
emails.append((subject, message, email_from, extra_to))
|
||||||
send_mass_mail(emails)
|
send_mass_mail(emails, fail_silently=False)
|
||||||
msg = ungettext(
|
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),
|
_("Message has been sent to %i %s.") % (num, self.opts.verbose_name_plural),
|
||||||
num
|
num
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,6 +8,8 @@ from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
from orchestra.admin.decorators import action_with_confirmation
|
from orchestra.admin.decorators import action_with_confirmation
|
||||||
from orchestra.core import services
|
from orchestra.core import services
|
||||||
|
|
||||||
|
from . import settings
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@action_with_confirmation()
|
@action_with_confirmation()
|
||||||
|
@ -38,6 +40,7 @@ list_contacts.verbose_name = _("List contacts")
|
||||||
|
|
||||||
|
|
||||||
def service_report(modeladmin, request, queryset):
|
def service_report(modeladmin, request, queryset):
|
||||||
|
# TODO resources
|
||||||
accounts = []
|
accounts = []
|
||||||
fields = []
|
fields = []
|
||||||
# First we get related manager names to fire a prefetch related
|
# First we get related manager names to fire a prefetch related
|
||||||
|
@ -59,4 +62,4 @@ def service_report(modeladmin, request, queryset):
|
||||||
'accounts': accounts,
|
'accounts': accounts,
|
||||||
'date': timezone.now().today()
|
'date': timezone.now().today()
|
||||||
}
|
}
|
||||||
return render(request, 'admin/accounts/account/service_report.html', context)
|
return render(request, settings.ACCOUNTS_SERVICE_REPORT_TEMPLATE, context)
|
||||||
|
|
|
@ -47,3 +47,7 @@ ACCOUNTS_CREATE_RELATED = getattr(settings, 'ACCOUNTS_CREATE_RELATED', (
|
||||||
_("Designates whether to creates a related subdomain <username>.orchestra.lan or not."),
|
_("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')
|
||||||
|
|
|
@ -54,7 +54,10 @@
|
||||||
<li class="item-title">{{ opts.verbose_name_plural|capfirst }}</li>
|
<li class="item-title">{{ opts.verbose_name_plural|capfirst }}</li>
|
||||||
<ul>
|
<ul>
|
||||||
{% for obj in related %}
|
{% for obj in related %}
|
||||||
<li class="related"><a href="{{ obj|admin_url }}">{{ obj }}</a>{% if not obj|isactive %} ({% trans "disabled" %}){% endif %}</li>
|
<li class="related"><a href="{{ obj|admin_url }}">{{ obj }}</a>
|
||||||
|
{% if not obj|isactive %} ({% trans "disabled" %}){% endif %}
|
||||||
|
{{ obj.get_description|capfirst }}
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -159,7 +159,8 @@ class Bill(models.Model):
|
||||||
return now + relativedelta(months=1)
|
return now + relativedelta(months=1)
|
||||||
|
|
||||||
def close(self, payment=False):
|
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:
|
if payment is False:
|
||||||
payment = self.account.paymentsources.get_default()
|
payment = self.account.paymentsources.get_default()
|
||||||
if not self.due_on:
|
if not self.due_on:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
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 import services
|
||||||
from orchestra.core.validators import validate_ipv4_address, validate_ipv6_address, validate_ascii
|
from orchestra.core.validators import validate_ipv4_address, validate_ipv6_address, validate_ascii
|
||||||
|
@ -50,6 +50,15 @@ class Domain(models.Model):
|
||||||
def subdomains(self):
|
def subdomains(self):
|
||||||
return Domain.objects.filter(name__regex='\.%s$' % self.name)
|
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):
|
def get_absolute_url(self):
|
||||||
return 'http://%s' % self.name
|
return 'http://%s' % self.name
|
||||||
|
|
||||||
|
|
|
@ -37,8 +37,8 @@ class MailmanBackend(ServiceController):
|
||||||
[[ $(grep "^\s*%(address_domain)s\s*$" %(virtual_alias_domains)s) ]] || {
|
[[ $(grep "^\s*%(address_domain)s\s*$" %(virtual_alias_domains)s) ]] || {
|
||||||
echo "%(address_domain)s" >> %(virtual_alias_domains)s
|
echo "%(address_domain)s" >> %(virtual_alias_domains)s
|
||||||
UPDATED_VIRTUAL_ALIAS_DOMAINS=1
|
UPDATED_VIRTUAL_ALIAS_DOMAINS=1
|
||||||
}""" % context
|
}""") % context
|
||||||
))
|
)
|
||||||
|
|
||||||
def exclude_virtual_alias_domain(self, context):
|
def exclude_virtual_alias_domain(self, context):
|
||||||
address_domain = context['address_domain']
|
address_domain = context['address_domain']
|
||||||
|
@ -58,13 +58,11 @@ class MailmanBackend(ServiceController):
|
||||||
self.append(textwrap.dedent("""\
|
self.append(textwrap.dedent("""\
|
||||||
[[ ! -e %(mailman_root)s/lists/%(name)s ]] && {
|
[[ ! -e %(mailman_root)s/lists/%(name)s ]] && {
|
||||||
newlist --quiet --emailhost='%(domain)s' '%(name)s' '%(admin)s' '%(password)s'
|
newlist --quiet --emailhost='%(domain)s' '%(name)s' '%(admin)s' '%(password)s'
|
||||||
}""" % context))
|
}""") % context)
|
||||||
# Custom domain
|
# Custom domain
|
||||||
if mail_list.address:
|
if mail_list.address:
|
||||||
aliases = self.get_virtual_aliases(context)
|
context['aliases'] = self.get_virtual_aliases(context)
|
||||||
# Preserve indentation
|
# Preserve indentation
|
||||||
spaces = ' '*4
|
|
||||||
context['aliases'] = spaces + aliases.replace('\n', '\n'+spaces)
|
|
||||||
self.append(textwrap.dedent("""\
|
self.append(textwrap.dedent("""\
|
||||||
if [[ ! $(grep '\s\s*%(name)s\s*$' %(virtual_alias)s) ]]; then
|
if [[ ! $(grep '\s\s*%(name)s\s*$' %(virtual_alias)s) ]]; then
|
||||||
echo '# %(banner)s\n%(aliases)s
|
echo '# %(banner)s\n%(aliases)s
|
||||||
|
@ -78,23 +76,28 @@ class MailmanBackend(ServiceController):
|
||||||
' >> %(virtual_alias)s
|
' >> %(virtual_alias)s
|
||||||
UPDATED_VIRTUAL_ALIAS=1
|
UPDATED_VIRTUAL_ALIAS=1
|
||||||
fi
|
fi
|
||||||
fi""" % context
|
fi""") % context
|
||||||
))
|
)
|
||||||
self.append('echo "require_explicit_destination = 0" | '
|
self.append(
|
||||||
'%(mailman_root)s/bin/config_list -i /dev/stdin %(name)s' % context)
|
'echo "require_explicit_destination = 0" | '
|
||||||
|
'%(mailman_root)s/bin/config_list -i /dev/stdin %(name)s' % context
|
||||||
|
)
|
||||||
self.append(textwrap.dedent("""\
|
self.append(textwrap.dedent("""\
|
||||||
echo "host_name = '%(address_domain)s'" | \
|
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:
|
else:
|
||||||
# Cleanup shit
|
# Cleanup shit
|
||||||
self.append(textwrap.dedent("""\
|
self.append(textwrap.dedent("""\
|
||||||
if [[ ! $(grep '\s\s*%(name)s\s*$' %(virtual_alias)s) ]]; then
|
if [[ ! $(grep '\s\s*%(name)s\s*$' %(virtual_alias)s) ]]; then
|
||||||
sed -i "/^.*\s%(name)s\s*$/d" %(virtual_alias)s
|
sed -i "/^.*\s%(name)s\s*$/d" %(virtual_alias)s
|
||||||
fi""" % context
|
fi""") % context
|
||||||
))
|
)
|
||||||
# Update
|
# Update
|
||||||
if context['password'] is not None:
|
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)
|
self.include_virtual_alias_domain(context)
|
||||||
|
|
||||||
def delete(self, mail_list):
|
def delete(self, mail_list):
|
||||||
|
@ -102,8 +105,8 @@ class MailmanBackend(ServiceController):
|
||||||
self.exclude_virtual_alias_domain(context)
|
self.exclude_virtual_alias_domain(context)
|
||||||
self.append(textwrap.dedent("""\
|
self.append(textwrap.dedent("""\
|
||||||
sed -i -e '/^.*\s%(name)s\(%(address_regex)s\)\s*$/d' \\
|
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)
|
self.append("rmlist -a %(name)s" % context)
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
|
@ -111,8 +114,8 @@ class MailmanBackend(ServiceController):
|
||||||
self.append(textwrap.dedent("""
|
self.append(textwrap.dedent("""
|
||||||
[[ $UPDATED_VIRTUAL_ALIAS == 1 ]] && { postmap %(virtual_alias)s; }
|
[[ $UPDATED_VIRTUAL_ALIAS == 1 ]] && { postmap %(virtual_alias)s; }
|
||||||
[[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]] && { /etc/init.d/postfix reload; }
|
[[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]] && { /etc/init.d/postfix reload; }
|
||||||
""" % context
|
""") % context
|
||||||
))
|
)
|
||||||
|
|
||||||
def get_context_files(self):
|
def get_context_files(self):
|
||||||
return {
|
return {
|
||||||
|
@ -163,7 +166,7 @@ class MailmanTraffic(ServiceMonitor):
|
||||||
| tr '\\n' '+' \\
|
| tr '\\n' '+' \\
|
||||||
| xargs -i echo {} )
|
| xargs -i echo {} )
|
||||||
echo ${OBJECT_ID} $(( ${SIZE}*${SUBSCRIBERS} ))
|
echo ${OBJECT_ID} $(( ${SIZE}*${SUBSCRIBERS} ))
|
||||||
}""" % current_date))
|
}""") % current_date)
|
||||||
|
|
||||||
def monitor(self, mail_list):
|
def monitor(self, mail_list):
|
||||||
context = self.get_context(mail_list)
|
context = self.get_context(mail_list)
|
||||||
|
|
|
@ -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()
|
|
@ -11,6 +11,8 @@ from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin
|
||||||
from orchestra.admin.utils import admin_link, change_url
|
from orchestra.admin.utils import admin_link, change_url
|
||||||
from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
|
from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
|
||||||
|
|
||||||
|
from . import settings
|
||||||
|
from .actions import SendMailboxEmail
|
||||||
from .filters import HasMailboxListFilter, HasForwardListFilter, HasAddressListFilter
|
from .filters import HasMailboxListFilter, HasForwardListFilter, HasAddressListFilter
|
||||||
from .forms import MailboxCreationForm, MailboxChangeForm, AddressForm
|
from .forms import MailboxCreationForm, MailboxChangeForm, AddressForm
|
||||||
from .models import Mailbox, Address, Autoresponse
|
from .models import Mailbox, Address, Autoresponse
|
||||||
|
@ -71,6 +73,13 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo
|
||||||
display_addresses.short_description = _("Addresses")
|
display_addresses.short_description = _("Addresses")
|
||||||
display_addresses.allow_tags = True
|
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):
|
def get_fieldsets(self, request, obj=None):
|
||||||
fieldsets = super(MailboxAdmin, self).get_fieldsets(request, obj=obj)
|
fieldsets = super(MailboxAdmin, self).get_fieldsets(request, obj=obj)
|
||||||
if obj and obj.filtering == obj.CUSTOM:
|
if obj and obj.filtering == obj.CUSTOM:
|
||||||
|
|
|
@ -89,10 +89,10 @@ class PasswdVirtualUserBackend(ServiceController):
|
||||||
context = {
|
context = {
|
||||||
'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH
|
'virtual_mailbox_maps': settings.MAILBOXES_VIRTUAL_MAILBOX_MAPS_PATH
|
||||||
}
|
}
|
||||||
self.append(
|
self.append(textwrap.dedent("""\
|
||||||
"[[ $UPDATED_VIRTUAL_MAILBOX_MAPS == 1 ]] && { postmap %(virtual_mailbox_maps)s; }"
|
[[ $UPDATED_VIRTUAL_MAILBOX_MAPS == 1 ]] && {
|
||||||
% context
|
postmap %(virtual_mailbox_maps)s
|
||||||
)
|
}""" % context))
|
||||||
|
|
||||||
def get_context(self, mailbox):
|
def get_context(self, mailbox):
|
||||||
context = {
|
context = {
|
||||||
|
@ -123,8 +123,7 @@ class PostfixAddressBackend(ServiceController):
|
||||||
[[ $(grep "^\s*%(domain)s\s*$" %(virtual_alias_domains)s) ]] || {
|
[[ $(grep "^\s*%(domain)s\s*$" %(virtual_alias_domains)s) ]] || {
|
||||||
echo "%(domain)s" >> %(virtual_alias_domains)s
|
echo "%(domain)s" >> %(virtual_alias_domains)s
|
||||||
UPDATED_VIRTUAL_ALIAS_DOMAINS=1
|
UPDATED_VIRTUAL_ALIAS_DOMAINS=1
|
||||||
}""" % context
|
}""") % context)
|
||||||
))
|
|
||||||
|
|
||||||
def exclude_virtual_alias_domain(self, context):
|
def exclude_virtual_alias_domain(self, context):
|
||||||
domain = context['domain']
|
domain = context['domain']
|
||||||
|
@ -151,8 +150,7 @@ class PostfixAddressBackend(ServiceController):
|
||||||
sed -i "s/^%(email)s\s.*$/${LINE}/" %(virtual_alias_maps)s
|
sed -i "s/^%(email)s\s.*$/${LINE}/" %(virtual_alias_maps)s
|
||||||
UPDATED_VIRTUAL_ALIAS_MAPS=1
|
UPDATED_VIRTUAL_ALIAS_MAPS=1
|
||||||
fi
|
fi
|
||||||
fi""" % context
|
fi""") % context)
|
||||||
))
|
|
||||||
else:
|
else:
|
||||||
logger.warning("Address %i is empty" % address.pk)
|
logger.warning("Address %i is empty" % address.pk)
|
||||||
self.append('sed -i "/^%(email)s\s/d" %(virtual_alias_maps)s')
|
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
|
if [[ $(grep "^%(email)s\s" %(virtual_alias_maps)s) ]]; then
|
||||||
sed -i "/^%(email)s\s.*$/d" %(virtual_alias_maps)s
|
sed -i "/^%(email)s\s.*$/d" %(virtual_alias_maps)s
|
||||||
UPDATED_VIRTUAL_ALIAS_MAPS=1
|
UPDATED_VIRTUAL_ALIAS_MAPS=1
|
||||||
fi""" % context
|
fi""") % context)
|
||||||
))
|
|
||||||
|
|
||||||
def save(self, address):
|
def save(self, address):
|
||||||
context = self.get_context(address)
|
context = self.get_context(address)
|
||||||
|
@ -181,8 +178,8 @@ class PostfixAddressBackend(ServiceController):
|
||||||
self.append(textwrap.dedent("""
|
self.append(textwrap.dedent("""
|
||||||
[[ $UPDATED_VIRTUAL_ALIAS_MAPS == 1 ]] && { postmap %(virtual_alias_maps)s; }
|
[[ $UPDATED_VIRTUAL_ALIAS_MAPS == 1 ]] && { postmap %(virtual_alias_maps)s; }
|
||||||
[[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]] && { /etc/init.d/postfix reload; }
|
[[ $UPDATED_VIRTUAL_ALIAS_DOMAINS == 1 ]] && { /etc/init.d/postfix reload; }
|
||||||
""" % context
|
""") % context
|
||||||
))
|
)
|
||||||
|
|
||||||
def get_context_files(self):
|
def get_context_files(self):
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -78,6 +78,11 @@ class Mailbox(models.Model):
|
||||||
else:
|
else:
|
||||||
address.save()
|
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):
|
class Address(models.Model):
|
||||||
name = models.CharField(_("name"), max_length=64, blank=True,
|
name = models.CharField(_("name"), max_length=64, blank=True,
|
||||||
|
|
|
@ -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_MAILDIRSIZE_PATH = getattr(settings, 'MAILBOXES_MAILDIRSIZE_PATH', '%(home)s/Maildir/maildirsize')
|
||||||
|
|
||||||
|
|
||||||
|
MAILBOXES_LOCAL_ADDRESS_DOMAIN = getattr(settings, 'MAILBOXES_LOCAL_ADDRESS_DOMAIN',
|
||||||
|
'orchestra.lan')
|
||||||
|
|
|
@ -60,6 +60,9 @@ class Miscellaneous(models.Model):
|
||||||
except type(self).account.field.rel.to.DoesNotExist:
|
except type(self).account.field.rel.to.DoesNotExist:
|
||||||
return self.is_active
|
return self.is_active
|
||||||
|
|
||||||
|
def get_description(self):
|
||||||
|
return ' '.join((str(self.amount), self.service.description or self.service.verbose_name))
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.identifier:
|
if self.identifier:
|
||||||
self.identifier = self.identifier.strip()
|
self.identifier = self.identifier.strip()
|
||||||
|
|
|
@ -10,6 +10,14 @@ from orchestra import plugins
|
||||||
from . import methods
|
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):
|
class ServiceBackend(plugins.Plugin):
|
||||||
"""
|
"""
|
||||||
Service management backend base class
|
Service management backend base class
|
||||||
|
@ -27,7 +35,7 @@ class ServiceBackend(plugins.Plugin):
|
||||||
ignore_fields = []
|
ignore_fields = []
|
||||||
actions = []
|
actions = []
|
||||||
|
|
||||||
__metaclass__ = plugins.PluginMount
|
__metaclass__ = ServiceMount
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return type(self).__name__
|
return type(self).__name__
|
||||||
|
|
|
@ -50,10 +50,11 @@ def SSH(backend, log, server, cmds, async=False):
|
||||||
key = settings.ORCHESTRATION_SSH_KEY_PATH
|
key = settings.ORCHESTRATION_SSH_KEY_PATH
|
||||||
try:
|
try:
|
||||||
ssh.connect(addr, username='root', key_filename=key, timeout=10)
|
ssh.connect(addr, username='root', key_filename=key, timeout=10)
|
||||||
except socket.error:
|
except socket.error, e:
|
||||||
logger.error('%s timed out on %s' % (backend, server))
|
logger.error('%s timed out on %s' % (backend, addr))
|
||||||
log.state = log.TIMEOUT
|
log.state = log.TIMEOUT
|
||||||
log.save(update_fields=['state'])
|
log.stderr = str(e)
|
||||||
|
log.save(update_fields=['state', 'stderr'])
|
||||||
return
|
return
|
||||||
transport = ssh.get_transport()
|
transport = ssh.get_transport()
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ STATE_COLORS = {
|
||||||
class PaymentSourceAdmin(SelectPluginAdminMixin, AccountAdminMixin, admin.ModelAdmin):
|
class PaymentSourceAdmin(SelectPluginAdminMixin, AccountAdminMixin, admin.ModelAdmin):
|
||||||
list_display = ('label', 'method', 'number', 'account_link', 'is_active')
|
list_display = ('label', 'method', 'number', 'account_link', 'is_active')
|
||||||
list_filter = ('method', 'is_active')
|
list_filter = ('method', 'is_active')
|
||||||
|
search_fields = ('account__username', 'account__full_name', 'data')
|
||||||
plugin = PaymentMethod
|
plugin = PaymentMethod
|
||||||
plugin_field = 'method'
|
plugin_field = 'method'
|
||||||
|
|
||||||
|
|
|
@ -119,23 +119,27 @@ class Transaction(models.Model):
|
||||||
if amount >= self.bill.total:
|
if amount >= self.bill.total:
|
||||||
raise ValidationError(_("New transactions can not be allocated for this bill."))
|
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):
|
def mark_as_processed(self):
|
||||||
assert self.state == self.WAITTING_PROCESSING
|
self.check_state(self.WAITTING_PROCESSING)
|
||||||
self.state = self.WAITTING_EXECUTION
|
self.state = self.WAITTING_EXECUTION
|
||||||
self.save(update_fields=['state'])
|
self.save(update_fields=['state'])
|
||||||
|
|
||||||
def mark_as_executed(self):
|
def mark_as_executed(self):
|
||||||
assert self.state == self.WAITTING_EXECUTION
|
self.check_state(self.WAITTING_EXECUTION)
|
||||||
self.state = self.EXECUTED
|
self.state = self.EXECUTED
|
||||||
self.save(update_fields=['state'])
|
self.save(update_fields=['state'])
|
||||||
|
|
||||||
def mark_as_secured(self):
|
def mark_as_secured(self):
|
||||||
assert self.state == self.EXECUTED
|
self.check_state(self.EXECUTED)
|
||||||
self.state = self.SECURED
|
self.state = self.SECURED
|
||||||
self.save(update_fields=['state'])
|
self.save(update_fields=['state'])
|
||||||
|
|
||||||
def mark_as_rejected(self):
|
def mark_as_rejected(self):
|
||||||
assert self.state == self.EXECUTED
|
self.check_state(self.EXECUTED)
|
||||||
self.state = self.REJECTED
|
self.state = self.REJECTED
|
||||||
self.save(update_fields=['state'])
|
self.save(update_fields=['state'])
|
||||||
|
|
||||||
|
@ -167,22 +171,26 @@ class TransactionProcess(models.Model):
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return '#%i' % self.id
|
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):
|
def mark_as_executed(self):
|
||||||
assert self.state == self.CREATED
|
self.check_state(self.CREATED)
|
||||||
self.state = self.EXECUTED
|
self.state = self.EXECUTED
|
||||||
for transaction in self.transactions.all():
|
for transaction in self.transactions.all():
|
||||||
transaction.mark_as_executed()
|
transaction.mark_as_executed()
|
||||||
self.save(update_fields=['state'])
|
self.save(update_fields=['state'])
|
||||||
|
|
||||||
def abort(self):
|
def abort(self):
|
||||||
assert self.state in [self.CREATED, self.EXCECUTED]
|
self.check_state(self.CREATED, self.EXCECUTED)
|
||||||
self.state = self.ABORTED
|
self.state = self.ABORTED
|
||||||
for transaction in self.transaction.all():
|
for transaction in self.transaction.all():
|
||||||
transaction.mark_as_aborted()
|
transaction.mark_as_aborted()
|
||||||
self.save(update_fields=['state'])
|
self.save(update_fields=['state'])
|
||||||
|
|
||||||
def commit(self):
|
def commit(self):
|
||||||
assert self.state in [self.CREATED, self.EXECUTED]
|
self.check_state(self.CREATED, self.EXECUTED)
|
||||||
self.state = self.COMMITED
|
self.state = self.COMMITED
|
||||||
for transaction in self.transactions.processing():
|
for transaction in self.transactions.processing():
|
||||||
transaction.mark_as_secured()
|
transaction.mark_as_secured()
|
||||||
|
|
|
@ -9,33 +9,32 @@ from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
def run_monitor(modeladmin, request, queryset):
|
def run_monitor(modeladmin, request, queryset):
|
||||||
""" Resource and ResourceData run monitors """
|
""" Resource and ResourceData run monitors """
|
||||||
referer = request.META.get('HTTP_REFERER')
|
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]
|
async = modeladmin.model.monitor.func_defaults[0]
|
||||||
results = []
|
logs = set()
|
||||||
for resource in queryset:
|
for resource in queryset:
|
||||||
result = resource.monitor()
|
results = resource.monitor()
|
||||||
if not async:
|
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"))
|
modeladmin.log_change(request, resource, _("Run monitors"))
|
||||||
num = len(queryset)
|
|
||||||
if async:
|
if async:
|
||||||
|
num = len(queryset)
|
||||||
link = reverse('admin:djcelery_taskstate_changelist')
|
link = reverse('admin:djcelery_taskstate_changelist')
|
||||||
msg = ungettext(
|
msg = ungettext(
|
||||||
_("One selected resource has been <a href='%s'>scheduled for monitoring</a>.") % link,
|
_("One selected resource has been <a href='%s'>scheduled for monitoring</a>.") % link,
|
||||||
_("%s selected resource have been <a href='%s'>scheduled for monitoring</a>.") % (num, link),
|
_("%s selected resource have been <a href='%s'>scheduled for monitoring</a>.") % (num, link),
|
||||||
num)
|
num)
|
||||||
else:
|
else:
|
||||||
if len(results) == 1:
|
num = len(logs)
|
||||||
log = results[0].log
|
if num == 1:
|
||||||
link = reverse('admin:orchestration_backendlog_change', args=(log.pk,))
|
log = logs.pop()
|
||||||
msg = _("One selected resource has <a href='%s'>been monitored</a>.") % link
|
link = reverse('admin:orchestration_backendlog_change', args=(log,))
|
||||||
elif len(results) >= 1:
|
msg = _("One related monitor has <a href='%s'>been executed</a>.") % link
|
||||||
logs = [str(result.log.pk) for result in results]
|
elif num >= 1:
|
||||||
link = reverse('admin:orchestration_backendlog_changelist')
|
link = reverse('admin:orchestration_backendlog_changelist')
|
||||||
link += '?id__in=%s' % ','.join(logs)
|
link += '?id__in=%s' % ','.join(logs)
|
||||||
msg = _("%s selected resources have <a href='%s'>been monitored</a>.") % (num, link)
|
msg = _("%s related monitors have <a href='%s'>been executed</a>.") % (num, link)
|
||||||
else:
|
else:
|
||||||
msg = _("No related monitors have been executed.")
|
msg = _("No related monitors have been executed.")
|
||||||
modeladmin.message_user(request, mark_safe(msg))
|
modeladmin.message_user(request, mark_safe(msg))
|
||||||
|
|
|
@ -119,8 +119,7 @@ class Resource(models.Model):
|
||||||
def get_model_path(self, monitor):
|
def get_model_path(self, monitor):
|
||||||
""" returns a model path between self.content_type and monitor.model """
|
""" returns a model path between self.content_type and monitor.model """
|
||||||
resource_model = self.content_type.model_class()
|
resource_model = self.content_type.model_class()
|
||||||
model_path = ServiceMonitor.get_backend(monitor).model
|
monitor_model = ServiceMonitor.get_backend(monitor).model_class()
|
||||||
monitor_model = get_model(model_path)
|
|
||||||
return get_model_field_path(monitor_model, resource_model)
|
return get_model_field_path(monitor_model, resource_model)
|
||||||
|
|
||||||
def sync_periodic_task(self):
|
def sync_periodic_task(self):
|
||||||
|
@ -223,6 +222,7 @@ class ResourceData(models.Model):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
fields = '__'.join(path)
|
fields = '__'.join(path)
|
||||||
|
monitor_model = ServiceMonitor.get_backend(monitor).model_class()
|
||||||
objects = monitor_model.objects.filter(**{fields: self.object_id})
|
objects = monitor_model.objects.filter(**{fields: self.object_id})
|
||||||
pks = objects.values_list('id', flat=True)
|
pks = objects.values_list('id', flat=True)
|
||||||
ct = ContentType.objects.get_for_model(monitor_model)
|
ct = ContentType.objects.get_for_model(monitor_model)
|
||||||
|
|
|
@ -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(
|
|
||||||
'<a href="http://[\.\-\w]+/wp-admin/network/site-info\.php\?id=([0-9]+)"\s+'
|
|
||||||
'class="edit">%(name)s</a>' % context
|
|
||||||
)
|
|
||||||
content = session.get(search).content
|
|
||||||
ids = regex.search(content).groups()
|
|
||||||
if len(ids) > 1:
|
|
||||||
raise ValueError("Multiple matches")
|
|
||||||
|
|
||||||
delete = re.compile('<span class="delete">(.*)</span>')
|
|
||||||
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)
|
|
|
@ -1,6 +0,0 @@
|
||||||
from .options import SoftwareService
|
|
||||||
|
|
||||||
|
|
||||||
class DokuwikiService(SoftwareService):
|
|
||||||
verbose_name = "Dowkuwiki"
|
|
||||||
icon = 'saas/icons/Dokuwiki.png'
|
|
|
@ -1,6 +0,0 @@
|
||||||
from .options import SoftwareService
|
|
||||||
|
|
||||||
|
|
||||||
class DrupalService(SoftwareService):
|
|
||||||
verbose_name = "Drupal"
|
|
||||||
icon = 'saas/icons/Drupal.png'
|
|
|
@ -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',)
|
|
|
@ -2,31 +2,8 @@ from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
SAAS_ENABLED_SERVICES = getattr(settings, 'SAAS_ENABLED_SERVICES', (
|
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.moodle.MoodleService',
|
||||||
'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',
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
|
@ -66,12 +66,16 @@ class SystemUser(models.Model):
|
||||||
def has_shell(self):
|
def has_shell(self):
|
||||||
return self.shell not in settings.SYSTEMUSERS_DISABLED_SHELLS
|
return self.shell not in settings.SYSTEMUSERS_DISABLED_SHELLS
|
||||||
|
|
||||||
|
def get_description(self):
|
||||||
|
return self.get_shell_display()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.home:
|
if not self.home:
|
||||||
self.home = self.get_base_home()
|
self.home = self.get_base_home()
|
||||||
super(SystemUser, self).save(*args, **kwargs)
|
super(SystemUser, self).save(*args, **kwargs)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
if self.home:
|
||||||
self.home = os.path.normpath(self.home)
|
self.home = os.path.normpath(self.home)
|
||||||
if self.directory:
|
if self.directory:
|
||||||
directory_error = None
|
directory_error = None
|
||||||
|
@ -119,10 +123,7 @@ class SystemUser(models.Model):
|
||||||
return os.path.normpath(settings.SYSTEMUSERS_HOME % context)
|
return os.path.normpath(settings.SYSTEMUSERS_HOME % context)
|
||||||
|
|
||||||
def get_home(self):
|
def get_home(self):
|
||||||
return os.path.join(
|
return os.path.join(self.home, self.directory)
|
||||||
self.home or self.get_base_home(),
|
|
||||||
self.directory
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
services.register(SystemUser)
|
services.register(SystemUser)
|
||||||
|
|
|
@ -4,12 +4,12 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.apps.orchestration import ServiceController
|
from orchestra.apps.orchestration import ServiceController
|
||||||
|
|
||||||
from . import SaaSServiceMixin
|
|
||||||
from .. import settings
|
from .. import settings
|
||||||
|
|
||||||
|
|
||||||
class DokuWikiMuBackend(SaaSServiceMixin, ServiceController):
|
class DokuWikiMuBackend(ServiceController):
|
||||||
verbose_name = _("DokuWiki multisite")
|
verbose_name = _("DokuWiki multisite")
|
||||||
|
model = 'webapps.WebApp'
|
||||||
|
|
||||||
def save(self, webapp):
|
def save(self, webapp):
|
||||||
context = self.get_context(webapp)
|
context = self.get_context(webapp)
|
|
@ -1,30 +1,32 @@
|
||||||
import os
|
import os
|
||||||
|
import textwrap
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.apps.orchestration import ServiceController
|
from orchestra.apps.orchestration import ServiceController
|
||||||
|
|
||||||
from . import SaaSServiceMixin
|
|
||||||
from .. import settings
|
from .. import settings
|
||||||
|
|
||||||
|
|
||||||
class DrupalMuBackend(SaaSServiceMixin, ServiceController):
|
class DrupalMuBackend(ServiceController):
|
||||||
verbose_name = _("Drupal multisite")
|
verbose_name = _("Drupal multisite")
|
||||||
|
model = 'webapps.WebApp'
|
||||||
|
|
||||||
def save(self, webapp):
|
def save(self, webapp):
|
||||||
context = self.get_context(webapp)
|
context = self.get_context(webapp)
|
||||||
self.append("mkdir %(drupal_path)s" % context)
|
self.append(textwrap.dedent("""\
|
||||||
self.append("chown -R www-data %(drupal_path)s" % context)
|
mkdir %(drupal_path)s
|
||||||
self.append(
|
chown -R www-data %(drupal_path)s
|
||||||
"# the following assumes settings.php to be previously configured\n"
|
|
||||||
"REGEX='^\s*$databases\[.default.\]\[.default.\]\[.prefix.\]'\n"
|
# the following assumes settings.php to be previously configured
|
||||||
"CONFIG='$databases[\'default\'][\'default\'][\'prefix\'] = \'%(app_name)s_\';'\n"
|
REGEX='^\s*$databases\[.default.\]\[.default.\]\[.prefix.\]'
|
||||||
"if [[ ! $(grep $REGEX %(drupal_settings)s) ]]; then\n"
|
CONFIG='$databases[\'default\'][\'default\'][\'prefix\'] = \'%(app_name)s_\';'
|
||||||
" echo $CONFIG >> %(drupal_settings)s\n"
|
if [[ ! $(grep $REGEX %(drupal_settings)s) ]]; then
|
||||||
"fi" % context
|
echo $CONFIG >> %(drupal_settings)s
|
||||||
|
fi""") % context
|
||||||
)
|
)
|
||||||
|
|
||||||
def selete(self, webapp):
|
def delete(self, webapp):
|
||||||
context = self.get_context(webapp)
|
context = self.get_context(webapp)
|
||||||
self.append("rm -fr %(app_path)s" % context)
|
self.append("rm -fr %(app_path)s" % context)
|
||||||
|
|
|
@ -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'<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):
|
||||||
|
# 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)
|
|
@ -29,10 +29,29 @@ class WebApp(models.Model):
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.get_name()
|
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
|
@cached
|
||||||
def get_options(self):
|
def get_options(self):
|
||||||
return { opt.name: opt.value for opt in self.options.all() }
|
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):
|
def get_name(self):
|
||||||
return self.name or settings.WEBAPPS_BLANK_NAME
|
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
|
return settings.WEBAPPS_FPM_START_PORT + self.account_id
|
||||||
|
|
||||||
def get_directive(self):
|
def get_directive(self):
|
||||||
directive = settings.WEBAPPS_TYPES[self.type]['directive']
|
directive = self.app_type['directive']
|
||||||
args = directive[1:] if len(directive) > 1 else ()
|
args = directive[1:] if len(directive) > 1 else ()
|
||||||
return directive[0], args
|
return directive[0], args
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,26 @@ WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', {
|
||||||
'help_text': _("This creates a Webalizer application under "
|
'help_text': _("This creates a Webalizer application under "
|
||||||
"~/webapps/<app_name>-<site_name>")
|
"~/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.<br>"
|
||||||
|
"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.<br>"
|
||||||
|
"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.<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")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -282,3 +302,22 @@ WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', {
|
||||||
r'^[^ ]+$'
|
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')
|
||||||
|
|
|
@ -50,8 +50,8 @@ class Apache2Backend(ServiceController):
|
||||||
} || {
|
} || {
|
||||||
echo -e '%(apache_conf)s' > %(sites_available)s
|
echo -e '%(apache_conf)s' > %(sites_available)s
|
||||||
UPDATED=1
|
UPDATED=1
|
||||||
}""" % context
|
}""") % context
|
||||||
))
|
)
|
||||||
self.enable_or_disable(site)
|
self.enable_or_disable(site)
|
||||||
|
|
||||||
def delete(self, site):
|
def delete(self, site):
|
||||||
|
@ -92,7 +92,7 @@ class Apache2Backend(ServiceController):
|
||||||
Options +ExecCGI
|
Options +ExecCGI
|
||||||
AddHandler fcgid-script .php
|
AddHandler fcgid-script .php
|
||||||
FcgidWrapper %(fcgid_path)s\
|
FcgidWrapper %(fcgid_path)s\
|
||||||
""" % context)
|
""") % context
|
||||||
for option in content.webapp.options.filter(name__startswith='Fcgid'):
|
for option in content.webapp.options.filter(name__startswith='Fcgid'):
|
||||||
fcgid += " %s %s\n" % (option.name, option.value)
|
fcgid += " %s %s\n" % (option.name, option.value)
|
||||||
fcgid += "</Directory>\n"
|
fcgid += "</Directory>\n"
|
||||||
|
@ -231,7 +231,7 @@ class Apache2Traffic(ServiceMonitor):
|
||||||
END_DATE=$(date '+%%Y%%m%%d%%H%%M%%S' -d '%(current_date)s')
|
END_DATE=$(date '+%%Y%%m%%d%%H%%M%%S' -d '%(current_date)s')
|
||||||
LOG_FILE="$3"
|
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}" '
|
| awk -v ini="${INI_DATE}" -v end="${END_DATE}" '
|
||||||
BEGIN {
|
BEGIN {
|
||||||
sum = 0
|
sum = 0
|
||||||
|
|
|
@ -12,6 +12,7 @@ from . import settings
|
||||||
|
|
||||||
|
|
||||||
class Website(models.Model):
|
class Website(models.Model):
|
||||||
|
""" Models a web site, also known as virtual host """
|
||||||
name = models.CharField(_("name"), max_length=128, unique=True,
|
name = models.CharField(_("name"), max_length=128, unique=True,
|
||||||
validators=[validators.validate_name])
|
validators=[validators.validate_name])
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||||
|
|
|
@ -122,8 +122,8 @@ AUTHENTICATION_BACKENDS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
#TODO Email config
|
# Email config
|
||||||
#EMAIL_BACKEND = 'djcelery_email.backends.CeleryEmailBackend'
|
EMAIL_BACKEND = 'djcelery_email.backends.CeleryEmailBackend'
|
||||||
|
|
||||||
|
|
||||||
#################################
|
#################################
|
||||||
|
|
Loading…
Reference in New Issue