Added backend docstring as helptext

This commit is contained in:
Marc Aymerich 2015-04-23 19:46:23 +00:00
parent 1a78317028
commit eebaee1097
29 changed files with 293 additions and 116 deletions

View File

@ -286,3 +286,6 @@ https://code.djangoproject.com/ticket/24576
# Amend lines??? # Amend lines???
# Add icon on select contact view # Add icon on select contact view
# Determine the difference between data serializer used for validation and used for the rest API!
# Make PluginApiView that fills metadata and other stuff like modeladmin plugin support

View File

@ -1,6 +1,9 @@
import copy
from django.forms import widgets from django.forms import widgets
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.utils import model_meta
from ..core.validators import validate_password from ..core.validators import validate_password
@ -10,32 +13,47 @@ class SetPasswordSerializer(serializers.Serializer):
style={'widget': widgets.PasswordInput}, validators=[validate_password]) style={'widget': widgets.PasswordInput}, validators=[validate_password])
#class HyperlinkedModelSerializerOptions(serializers.HyperlinkedModelSerializerOptions):
# def __init__(self, meta):
# super(HyperlinkedModelSerializerOptions, self).__init__(meta)
# self.postonly_fields = getattr(meta, 'postonly_fields', ())
class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer): class HyperlinkedModelSerializer(serializers.HyperlinkedModelSerializer):
""" support for postonly_fields, fields whose value can only be set on post """ """ support for postonly_fields, fields whose value can only be set on post """
# _options_class = HyperlinkedModelSerializerOptions
def validate(self, attrs): def validate(self, attrs):
""" calls model.clean() """ """ calls model.clean() """
attrs = super(HyperlinkedModelSerializer, self).validate(attrs) attrs = super(HyperlinkedModelSerializer, self).validate(attrs)
instance = self.Meta.model(**attrs) validated_data = dict(attrs)
ModelClass = self.Meta.model
# Remove many-to-many relationships from validated_data.
info = model_meta.get_field_info(ModelClass)
for field_name, relation_info in info.relations.items():
if relation_info.to_many and (field_name in validated_data):
validated_data.pop(field_name)
if self.instance:
# on update: Merge provided fields with instance field
instance = copy.deepcopy(self.instance)
for key, value in validated_data.items():
setattr(instance, key, value)
else:
instance = ModelClass(**validated_data)
instance.clean() instance.clean()
return attrs return attrs
# TODO raise validationError instead of silently removing fields def post_only_cleanning(self, instance, validated_data):
def update(self, instance, validated_data): """ removes postonly_fields from attrs """
""" removes postonly_fields from attrs when not posting """
model_attrs = dict(**validated_data) model_attrs = dict(**validated_data)
if instance is not None: if instance is not None:
for attr, value in validated_data.items(): for attr, value in validated_data.items():
if attr in self.Meta.postonly_fields: if attr in self.Meta.postonly_fields:
model_attrs.pop(attr) model_attrs.pop(attr)
return model_attrs
def update(self, instance, validated_data):
""" removes postonly_fields from attrs when not posting """
model_attrs = self.post_only_cleanning(instance, validated_data)
return super(HyperlinkedModelSerializer, self).update(instance, model_attrs) return super(HyperlinkedModelSerializer, self).update(instance, model_attrs)
def partial_update(self, instance, validated_data):
""" removes postonly_fields from attrs when not posting """
model_attrs = self.post_only_cleanning(instance, validated_data)
return super(HyperlinkedModelSerializer, self).partial_update(instance, model_attrs)
class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer): class SetPasswordHyperlinkedSerializer(HyperlinkedModelSerializer):

View File

@ -8,13 +8,13 @@ from .serializers import DatabaseSerializer, DatabaseUserSerializer
class DatabaseViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet): class DatabaseViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
queryset = Database.objects.all() queryset = Database.objects.prefetch_related('users').all()
serializer_class = DatabaseSerializer serializer_class = DatabaseSerializer
filter_fields = ('name',) filter_fields = ('name',)
class DatabaseUserViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet): class DatabaseUserViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet):
queryset = DatabaseUser.objects.all() queryset = DatabaseUser.objects.prefetch_related('databases').all()
serializer_class = DatabaseUserSerializer serializer_class = DatabaseUserSerializer
filter_fields = ('username',) filter_fields = ('username',)

View File

@ -9,6 +9,12 @@ from . import settings
class MySQLBackend(ServiceController): class MySQLBackend(ServiceController):
"""
Simple backend for creating MySQL databases using `CREATE DATABASE` statement.
DATABASES_DEFAULT_HOST = %s
"""
format_docstring = (settings.DATABASES_DEFAULT_HOST,)
verbose_name = "MySQL database" verbose_name = "MySQL database"
model = 'databases.Database' model = 'databases.Database'
default_route_match = "database.type == 'mysql'" default_route_match = "database.type == 'mysql'"
@ -54,6 +60,11 @@ class MySQLBackend(ServiceController):
class MySQLUserBackend(ServiceController): class MySQLUserBackend(ServiceController):
"""
Simple backend for creating MySQL users using `CREATE USER` statement.
DATABASES_DEFAULT_HOST = %s
""" % settings.DATABASES_DEFAULT_HOST
verbose_name = "MySQL user" verbose_name = "MySQL user"
model = 'databases.DatabaseUser' model = 'databases.DatabaseUser'
default_route_match = "databaseuser.type == 'mysql'" default_route_match = "databaseuser.type == 'mysql'"
@ -93,6 +104,10 @@ class MySQLUserBackend(ServiceController):
class MysqlDisk(ServiceMonitor): class MysqlDisk(ServiceMonitor):
"""
du -bs <database_path>
Implements triggers for resource limit exceeded and recovery, disabling insert and create privileges.
"""
model = 'databases.Database' model = 'databases.Database'
verbose_name = _("MySQL disk") verbose_name = _("MySQL disk")

View File

@ -11,6 +11,18 @@ from . import settings
class Bind9MasterDomainBackend(ServiceController): class Bind9MasterDomainBackend(ServiceController):
"""
Bind9 zone and config generation.
It auto-discovers slave Bind9 servers based on your routing configuration or you can use DOMAINS_SLAVES to explicitly configure the slaves.
DOMAINS_SLAVES = %s
DOMAINS_MASTERS_PATH = '%s'
"""
format_docstring = (
str(settings.DOMAINS_SLAVES),
settings.DOMAINS_MASTERS_PATH,
)
verbose_name = _("Bind9 master domain") verbose_name = _("Bind9 master domain")
model = 'domains.Domain' model = 'domains.Domain'
related_models = ( related_models = (
@ -121,6 +133,12 @@ class Bind9MasterDomainBackend(ServiceController):
class Bind9SlaveDomainBackend(Bind9MasterDomainBackend): class Bind9SlaveDomainBackend(Bind9MasterDomainBackend):
"""
Generate the configuartion for slave servers
It auto-discover the master server based on your routing configuration or you can use DOMAINS_MASTERS to explicitly configure the master.
DOMAINS_MASTERS = %s
""" % str(settings.DOMAINS_MASTERS)
verbose_name = _("Bind9 slave domain") verbose_name = _("Bind9 slave domain")
related_models = ( related_models = (
('domains.Domain', 'origin'), ('domains.Domain', 'origin'),

View File

@ -10,6 +10,19 @@ from .models import List
class MailmanBackend(ServiceController): class MailmanBackend(ServiceController):
"""
Mailman backend based on `newlist`, it handles custom domains.
LISTS_VIRTUAL_ALIAS_PATH = '%s'
LISTS_VIRTUAL_ALIAS_DOMAINS_PATH = '%s'
LISTS_DEFAULT_DOMAIN = '%s'
LISTS_MAILMAN_ROOT_DIR = '%s'
"""
format_docstring = (
settings.LISTS_VIRTUAL_ALIAS_PATH,
settings.LISTS_VIRTUAL_ALIAS_DOMAINS_PATH,
settings.LISTS_DEFAULT_DOMAIN,
settings.LISTS_MAILMAN_ROOT_DIR,
)
verbose_name = "Mailman" verbose_name = "Mailman"
model = 'lists.List' model = 'lists.List'
addresses = [ addresses = [
@ -149,75 +162,15 @@ class MailmanBackend(ServiceController):
return replace(context, "'", '"') return replace(context, "'", '"')
class MailmanTrafficBash(ServiceMonitor):
model = 'lists.List'
resource = ServiceMonitor.TRAFFIC
verbose_name = _("Mailman traffic (Bash)")
def prepare(self):
super(MailmanTraffic, self).prepare()
context = {
'mailman_log': '%s{,.1}' % settings.LISTS_MAILMAN_POST_LOG_PATH,
'current_date': self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z"),
}
self.append(textwrap.dedent("""\
function monitor () {
OBJECT_ID=$1
# Dates convertions are done server-side because of timezone discrepancies
INI_DATE=$(date "+%%Y%%m%%d%%H%%M%%S" -d "$2")
END_DATE=$(date '+%%Y%%m%%d%%H%%M%%S' -d '%(current_date)s')
LIST_NAME="$3"
MAILMAN_LOG=%(mailman_log)s
SUBSCRIBERS=$(list_members ${LIST_NAME} | wc -l)
{
{ grep " post to ${LIST_NAME} " ${MAILMAN_LOG} || echo '\\r'; } \\
| awk -v ini="${INI_DATE}" -v end="${END_DATE}" -v subs="${SUBSCRIBERS}" '
BEGIN {
sum = 0
months["Jan"] = "01"
months["Feb"] = "02"
months["Mar"] = "03"
months["Apr"] = "04"
months["May"] = "05"
months["Jun"] = "06"
months["Jul"] = "07"
months["Aug"] = "08"
months["Sep"] = "09"
months["Oct"] = "10"
months["Nov"] = "11"
months["Dec"] = "12"
} {
# Mar 01 08:29:02 2015
month = months[$1]
day = $2
year = $4
split($3, time, ":")
line_date = year month day time[1] time[2] time[3]
if ( line_date > ini && line_date < end)
sum += substr($11, 6, length($11)-6)
} END {
print sum * subs
}' || [[ $? == 1 ]] && true
} | xargs echo ${OBJECT_ID}
}""") % context)
def monitor(self, mail_list):
context = self.get_context(mail_list)
self.append(
'monitor %(object_id)i "%(last_date)s" "%(list_name)s"' % context
)
def get_context(self, mail_list):
context = {
'list_name': mail_list.name,
'object_id': mail_list.pk,
'last_date': self.get_last_date(mail_list.pk).strftime("%Y-%m-%d %H:%M:%S %Z"),
}
return replace(context, "'", '"')
class MailmanTraffic(ServiceMonitor): class MailmanTraffic(ServiceMonitor):
"""
Parses mailman log file looking for email size and multiples it by `list_members` count.
LISTS_MAILMAN_POST_LOG_PATH = '%s'
"""
format_docstring = (
settings.LISTS_MAILMAN_POST_LOG_PATH,
)
model = 'lists.List' model = 'lists.List'
resource = ServiceMonitor.TRAFFIC resource = ServiceMonitor.TRAFFIC
verbose_name = _("Mailman traffic") verbose_name = _("Mailman traffic")
@ -235,13 +188,13 @@ class MailmanTraffic(ServiceMonitor):
import sys import sys
from datetime import datetime from datetime import datetime
from dateutil import tz from dateutil import tz
def to_local_timezone(date, tzlocal=tz.tzlocal()): def to_local_timezone(date, tzlocal=tz.tzlocal()):
date = datetime.strptime(date, '%Y-%m-%d %H:%M:%S %Z') date = datetime.strptime(date, '%Y-%m-%d %H:%M:%S %Z')
date = date.replace(tzinfo=tz.tzutc()) date = date.replace(tzinfo=tz.tzutc())
date = date.astimezone(tzlocal) date = date.astimezone(tzlocal)
return date return date
postlogs = {postlogs} postlogs = {postlogs}
# Use local timezone # Use local timezone
end_date = to_local_timezone('{current_date}') end_date = to_local_timezone('{current_date}')
@ -261,13 +214,13 @@ class MailmanTraffic(ServiceMonitor):
'Nov': '11', 'Nov': '11',
'Dec': '12', 'Dec': '12',
}} }}
def prepare(object_id, list_name, ini_date): def prepare(object_id, list_name, ini_date):
global lists global lists
ini_date = to_local_timezone(ini_date) ini_date = to_local_timezone(ini_date)
ini_date = int(ini_date.strftime('%Y%m%d%H%M%S')) ini_date = int(ini_date.strftime('%Y%m%d%H%M%S'))
lists[list_name] = [ini_date, object_id, 0] lists[list_name] = [ini_date, object_id, 0]
def monitor(lists, end_date, months, postlogs): def monitor(lists, end_date, months, postlogs):
for postlog in postlogs: for postlog in postlogs:
try: try:
@ -318,6 +271,9 @@ class MailmanTraffic(ServiceMonitor):
class MailmanSubscribers(ServiceMonitor): class MailmanSubscribers(ServiceMonitor):
"""
Monitors number of list subscribers via `list_members`
"""
model = 'lists.List' model = 'lists.List'
verbose_name = _("Mailman subscribers") verbose_name = _("Mailman subscribers")

View File

@ -8,13 +8,12 @@ from .serializers import AddressSerializer, MailboxSerializer
class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet): class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
queryset = Address.objects.all() queryset = Address.objects.select_related('domain').prefetch_related('mailboxes').all()
serializer_class = AddressSerializer serializer_class = AddressSerializer
class MailboxViewSet(LogApiMixin, SetPasswordApiMixin, AccountApiMixin, viewsets.ModelViewSet): class MailboxViewSet(LogApiMixin, SetPasswordApiMixin, AccountApiMixin, viewsets.ModelViewSet):
queryset = Mailbox.objects.all() queryset = Mailbox.objects.prefetch_related('addresses__domain').all()
serializer_class = MailboxSerializer serializer_class = MailboxSerializer

View File

@ -20,6 +20,10 @@ logger = logging.getLogger(__name__)
class UNIXUserMaildirBackend(ServiceController): class UNIXUserMaildirBackend(ServiceController):
"""
Assumes that all system users on this servers all mail accounts.
If you want to have system users AND mailboxes on the same server you should consider using virtual mailboxes
"""
verbose_name = _("UNIX maildir user") verbose_name = _("UNIX maildir user")
model = 'mailboxes.Mailbox' model = 'mailboxes.Mailbox'
@ -74,6 +78,9 @@ class UNIXUserMaildirBackend(ServiceController):
class DovecotPostfixPasswdVirtualUserBackend(ServiceController): class DovecotPostfixPasswdVirtualUserBackend(ServiceController):
"""
WARNING: This backends is not fully implemented
"""
verbose_name = _("Dovecot-Postfix virtualuser") verbose_name = _("Dovecot-Postfix virtualuser")
model = 'mailboxes.Mailbox' model = 'mailboxes.Mailbox'
# TODO related_models = ('resources__content_type') ?? needed for updating disk usage from resource.data # TODO related_models = ('resources__content_type') ?? needed for updating disk usage from resource.data
@ -176,6 +183,17 @@ class DovecotPostfixPasswdVirtualUserBackend(ServiceController):
class PostfixAddressBackend(ServiceController): class PostfixAddressBackend(ServiceController):
"""
Addresses based on Postfix virtual alias domains.
<tt>MAILBOXES_LOCAL_DOMAIN = '%s'
MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH = '%s'
MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH = '%s'</tt>
"""
format_docstring = (
settings.MAILBOXES_LOCAL_DOMAIN,
settings.MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH,
settings.MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH,
)
verbose_name = _("Postfix address") verbose_name = _("Postfix address")
model = 'mailboxes.Address' model = 'mailboxes.Address'
related_models = ( related_models = (
@ -267,6 +285,9 @@ class PostfixAddressBackend(ServiceController):
class AutoresponseBackend(ServiceController): class AutoresponseBackend(ServiceController):
"""
WARNING: not implemented
"""
verbose_name = _("Mail autoresponse") verbose_name = _("Mail autoresponse")
model = 'mailboxes.Autoresponse' model = 'mailboxes.Autoresponse'
@ -274,9 +295,13 @@ class AutoresponseBackend(ServiceController):
class DovecotMaildirDisk(ServiceMonitor): class DovecotMaildirDisk(ServiceMonitor):
""" """
Maildir disk usage based on Dovecot maildirsize file Maildir disk usage based on Dovecot maildirsize file
http://wiki2.dovecot.org/Quota/Maildir http://wiki2.dovecot.org/Quota/Maildir
MAILBOXES_MAILDIRSIZE_PATH = '%s'
""" """
format_docstring = (
settings.MAILBOXES_MAILDIRSIZE_PATH,
)
model = 'mailboxes.Mailbox' model = 'mailboxes.Mailbox'
resource = ServiceMonitor.DISK resource = ServiceMonitor.DISK
verbose_name = _("Dovecot Maildir size") verbose_name = _("Dovecot Maildir size")
@ -304,9 +329,13 @@ class DovecotMaildirDisk(ServiceMonitor):
class PostfixMailscannerTraffic(ServiceMonitor): class PostfixMailscannerTraffic(ServiceMonitor):
""" """
A high-performance log parser A high-performance log parser.
Reads the mail.log file only once, for all users Reads the mail.log file only once, for all users.
MAILBOXES_MAIL_LOG_PATH = '%s'
""" """
format_docstring = (
settings.MAILBOXES_MAIL_LOG_PATH,
)
model = 'mailboxes.Mailbox' model = 'mailboxes.Mailbox'
resource = ServiceMonitor.TRAFFIC resource = ServiceMonitor.TRAFFIC
verbose_name = _("Postfix-Mailscanner traffic") verbose_name = _("Postfix-Mailscanner traffic")
@ -460,8 +489,3 @@ class PostfixMailscannerTraffic(ServiceMonitor):
'last_date': self.get_last_date(mailbox.pk).strftime("%Y-%m-%d %H:%M:%S %Z"), 'last_date': self.get_last_date(mailbox.pk).strftime("%Y-%m-%d %H:%M:%S %Z"),
} }
return replace(context, "'", '"') return replace(context, "'", '"')

View File

@ -25,10 +25,10 @@ class RelatedAddressSerializer(AccountSerializerMixin, serializers.HyperlinkedMo
class Meta: class Meta:
model = Address model = Address
fields = ('url', 'name', 'domain', 'forward') fields = ('url', 'name', 'domain', 'forward')
#
def from_native(self, data, files=None): # def from_native(self, data, files=None):
queryset = self.opts.model.objects.filter(account=self.account) # queryset = self.opts.model.objects.filter(account=self.account)
return get_object_or_404(queryset, name=data['name']) # return get_object_or_404(queryset, name=data['name'])
class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer): class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer):

View File

@ -25,9 +25,7 @@ class Operation():
self.backend = backend self.backend = backend
# instance should maintain any dynamic attribute until backend execution # instance should maintain any dynamic attribute until backend execution
# deep copy is prefered over copy otherwise objects will share same atributes (queryset cache) # deep copy is prefered over copy otherwise objects will share same atributes (queryset cache)
print('aa', getattr(instance, 'password', 'NOOOO'), id(instance))
self.instance = copy.deepcopy(instance) self.instance = copy.deepcopy(instance)
print('aa', getattr(self.instance, 'password', 'NOOOO'), id(self.instance))
self.action = action self.action = action
self.servers = servers self.servers = servers

View File

@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.admin.html import monospace_format from orchestra.admin.html import monospace_format
from orchestra.admin.utils import admin_link, admin_date, admin_colored from orchestra.admin.utils import admin_link, admin_date, admin_colored
from . import settings from . import settings, helpers
from .backends import ServiceBackend from .backends import ServiceBackend
from .models import Server, Route, BackendLog, BackendOperation from .models import Server, Route, BackendLog, BackendOperation
from .widgets import RouteBackendSelect from .widgets import RouteBackendSelect
@ -31,10 +31,7 @@ class RouteAdmin(admin.ModelAdmin):
list_filter = ('host', 'is_active', 'backend') list_filter = ('host', 'is_active', 'backend')
ordering = ('backend',) ordering = ('backend',)
BACKEND_HELP_TEXT = { BACKEND_HELP_TEXT = helpers.get_backends_help_text(ServiceBackend.get_backends())
backend: "This backend operates over '%s'" % ServiceBackend.get_backend(backend).model
for backend, __ in ServiceBackend.get_choices()
}
DEFAULT_MATCH = { DEFAULT_MATCH = {
backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends() backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends()
} }

View File

@ -45,6 +45,7 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
default_route_match = 'True' default_route_match = 'True'
# Force the backend manager to block in multiple backend executions executing them synchronously # Force the backend manager to block in multiple backend executions executing them synchronously
block = False block = False
format_docstring = ()
def __str__(self): def __str__(self):
return type(self).__name__ return type(self).__name__

View File

@ -1,3 +1,5 @@
import textwrap
from django.contrib import messages from django.contrib import messages
from django.core.mail import mail_admins from django.core.mail import mail_admins
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@ -6,6 +8,36 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ungettext, ugettext_lazy as _ from django.utils.translation import ungettext, ugettext_lazy as _
def get_backends_help_text(backends):
help_texts = {}
for backend in backends:
context = {
'model': backend.model,
'related_models': str(backend.related_models),
'script_executable': backend.script_executable,
'script_method': str(backend.script_method),
'function_method': str(backend.script_method),
'actions': ', '.join(backend.actions),
}
help_text = textwrap.dedent("""
- Model: '%(model)s'<br>
- Related models: %(related_models)s<br>
- Script executable: %(script_executable)s<br>
- Script method: %(script_method)s<br>
- Function method: %(function_method)s<br>
- Actions: %(actions)s<br>"""
) % context
docstring = backend.__doc__
if docstring:
try:
docstring = (docstring % backend.format_docstring).strip().splitlines()
except TypeError as e:
raise TypeError(str(backend) + str(e))
help_text += '<br>' + '<br>'.join(docstring)
help_texts[backend.get_name()] = help_text
return help_texts
def send_report(method, args, log): def send_report(method, args, log):
server = args[0] server = args[0]
backend = method.__self__.__class__.__name__ backend = method.__self__.__class__.__name__

View File

@ -32,5 +32,5 @@ ORCHESTRATION_DISABLE_EXECUTION = getattr(settings, 'ORCHESTRATION_DISABLE_EXECU
ORCHESTRATION_BACKEND_CLEANUP_DELTA = getattr(settings, 'ORCHESTRATION_BACKEND_CLEANUP_DELTA', ORCHESTRATION_BACKEND_CLEANUP_DELTA = getattr(settings, 'ORCHESTRATION_BACKEND_CLEANUP_DELTA',
timedelta(days=40) timedelta(days=15)
) )

View File

@ -10,6 +10,16 @@ from . import settings
class UNIXUserBackend(ServiceController): class UNIXUserBackend(ServiceController):
"""
Basic UNIX system user/group support based on `useradd`, `usermod`, `userdel` and `groupdel`.
<tt>SYSTEMUSERS_DEFAULT_GROUP_MEMBERS = '%s'
SYSTEMUSERS_MOVE_ON_DELETE_PATH = '%s'</tt>
"""
format_docstring = (
settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS,
settings.SYSTEMUSERS_MOVE_ON_DELETE_PATH,
)
verbose_name = _("UNIX user") verbose_name = _("UNIX user")
model = 'systemusers.SystemUser' model = 'systemusers.SystemUser'
actions = ('save', 'delete', 'grant_permission') actions = ('save', 'delete', 'grant_permission')
@ -84,6 +94,9 @@ class UNIXUserBackend(ServiceController):
class UNIXUserDisk(ServiceMonitor): class UNIXUserDisk(ServiceMonitor):
"""
`du -bs <home>`
"""
model = 'systemusers.SystemUser' model = 'systemusers.SystemUser'
resource = ServiceMonitor.DISK resource = ServiceMonitor.DISK
verbose_name = _('UNIX user disk') verbose_name = _('UNIX user disk')
@ -109,6 +122,14 @@ class UNIXUserDisk(ServiceMonitor):
class Exim4Traffic(ServiceMonitor): class Exim4Traffic(ServiceMonitor):
"""
Exim4 mainlog parser for mails sent on the webserver by system users (e.g. via PHP mail())
SYSTEMUSERS_MAIL_LOG_PATH = '%s'
"""
format_docstring = (
settings.SYSTEMUSERS_MAIL_LOG_PATH,
)
model = 'systemusers.SystemUser' model = 'systemusers.SystemUser'
resource = ServiceMonitor.TRAFFIC resource = ServiceMonitor.TRAFFIC
verbose_name = _("Exim4 traffic") verbose_name = _("Exim4 traffic")
@ -188,6 +209,13 @@ class Exim4Traffic(ServiceMonitor):
class VsFTPdTraffic(ServiceMonitor): class VsFTPdTraffic(ServiceMonitor):
"""
vsFTPd log parser.
SYSTEMUSERS_FTP_LOG_PATH = '%s'
"""
format_docstring = (
settings.SYSTEMUSERS_FTP_LOG_PATH,
)
model = 'systemusers.SystemUser' model = 'systemusers.SystemUser'
resource = ServiceMonitor.TRAFFIC resource = ServiceMonitor.TRAFFIC
verbose_name = _('VsFTPd traffic') verbose_name = _('VsFTPd traffic')
@ -279,4 +307,3 @@ class VsFTPdTraffic(ServiceMonitor):
'username': user.username, 'username': user.username,
} }
return replace(context, "'", '"') return replace(context, "'", '"')

View File

@ -3,6 +3,9 @@ from orchestra.contrib.resources import ServiceMonitor
class OpenVZTraffic(ServiceMonitor): class OpenVZTraffic(ServiceMonitor):
"""
WARNING: Not fully implemeted
"""
model = 'vps.VPS' model = 'vps.VPS'
resource = ServiceMonitor.TRAFFIC resource = ServiceMonitor.TRAFFIC

View File

@ -5,11 +5,13 @@ from orchestra.contrib.accounts.api import AccountApiMixin
from . import settings from . import settings
from .models import WebApp from .models import WebApp
from .options import AppOption
from .serializers import WebAppSerializer from .serializers import WebAppSerializer
from .types import AppType
class WebAppViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet): class WebAppViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
queryset = WebApp.objects.all() queryset = WebApp.objects.prefetch_related('options').all()
serializer_class = WebAppSerializer serializer_class = WebAppSerializer
filter_fields = ('name',) filter_fields = ('name',)
@ -22,6 +24,31 @@ class WebAppViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
metadata.data['settings'] = { metadata.data['settings'] = {
name.lower(): getattr(settings, name, None) for name in names name.lower(): getattr(settings, name, None) for name in names
} }
# AppTypes
meta = self.metadata_class()
app_types = {}
for app_type in AppType.get_plugins():
if app_type.serializer:
data = meta.get_serializer_info(app_type.serializer())
else:
data = {}
options = []
for group, option in app_type.get_options():
options += [opt.name for opt in option]
app_types[app_type.get_name()] = {
'data': data,
'options': options,
}
metadata.data['actions']['types'] = app_types
# Options
options = {}
for option in AppOption.get_plugins():
options[option.get_name()] = {
'verbose_name': option.get_verbose_name(),
'help_text': option.help_text,
'group': option.group,
}
metadata.data['actions']['options'] = options
return metadata return metadata

View File

@ -11,6 +11,29 @@ from .. import settings
class PHPBackend(WebAppServiceMixin, ServiceController): class PHPBackend(WebAppServiceMixin, ServiceController):
"""
PHP support for apache-mod-fcgid and php-fpm.
It handles switching between these two PHP process management systemes.
WEBAPPS_MERGE_PHP_WEBAPPS = %s
WEBAPPS_FPM_DEFAULT_MAX_CHILDREN = %s
WEBAPPS_PHP_CGI_BINARY_PATH = '%s'
WEBAPPS_PHP_CGI_RC_DIR = '%s'
WEBAPPS_PHP_CGI_INI_SCAN_DIR = '%s'
WEBAPPS_FCGID_CMD_OPTIONS_PATH = '%s'
WEBAPPS_PHPFPM_POOL_PATH = '%s'
WEBAPPS_PHP_MAX_REQUESTS = %s
"""
format_docstring = (
settings.WEBAPPS_MERGE_PHP_WEBAPPS,
settings.WEBAPPS_FPM_DEFAULT_MAX_CHILDREN,
settings.WEBAPPS_PHP_CGI_BINARY_PATH,
settings.WEBAPPS_PHP_CGI_RC_DIR,
settings.WEBAPPS_PHP_CGI_INI_SCAN_DIR,
settings.WEBAPPS_FCGID_CMD_OPTIONS_PATH,
settings.WEBAPPS_PHPFPM_POOL_PATH,
settings.WEBAPPS_PHP_MAX_REQUESTS,
)
verbose_name = _("PHP FPM/FCGID") verbose_name = _("PHP FPM/FCGID")
default_route_match = "webapp.type.endswith('php')" default_route_match = "webapp.type.endswith('php')"
MERGE = settings.WEBAPPS_MERGE_PHP_WEBAPPS MERGE = settings.WEBAPPS_MERGE_PHP_WEBAPPS

View File

@ -13,9 +13,19 @@ from .. import settings
class uWSGIPythonBackend(WebAppServiceMixin, ServiceController): class uWSGIPythonBackend(WebAppServiceMixin, ServiceController):
""" """
Emperor mode Emperor mode
http://uwsgi-docs.readthedocs.org/en/latest/Emperor.html http://uwsgi-docs.readthedocs.org/en/latest/Emperor.html
WEBAPPS_UWSGI_BASE_DIR = '%s'
WEBAPPS_PYTHON_MAX_REQUESTS = %s
WEBAPPS_PYTHON_DEFAULT_MAX_WORKERS = %s
WEBAPPS_PYTHON_DEFAULT_TIMEOUT = %s
""" """
format_docstring = (
settings.WEBAPPS_UWSGI_BASE_DIR,
settings.WEBAPPS_PYTHON_MAX_REQUESTS,
settings.WEBAPPS_PYTHON_DEFAULT_MAX_WORKERS,
settings.WEBAPPS_PYTHON_DEFAULT_TIMEOUT,
)
verbose_name = _("Python uWSGI") verbose_name = _("Python uWSGI")
default_route_match = "webapp.type.endswith('python')" default_route_match = "webapp.type.endswith('python')"

View File

@ -6,6 +6,10 @@ from . import WebAppServiceMixin
class StaticBackend(WebAppServiceMixin, ServiceController): class StaticBackend(WebAppServiceMixin, ServiceController):
"""
Static web pages.
Only creates the webapp dir and leaves the web server the decision to execute CGIs or not.
"""
verbose_name = _("Static") verbose_name = _("Static")
default_route_match = "webapp.type == 'static'" default_route_match = "webapp.type == 'static'"

View File

@ -8,6 +8,10 @@ from .php import PHPBackend
class SymbolicLinkBackend(PHPBackend, ServiceController): class SymbolicLinkBackend(PHPBackend, ServiceController):
"""
Same as PHPBackend but allows you to have the webapps on a directory diferent than the webapps dir.
"""
format_docstring = ()
verbose_name = _("Symbolic link webapp") verbose_name = _("Symbolic link webapp")
model = 'webapps.WebApp' model = 'webapps.WebApp'
default_route_match = "webapp.type == 'symbolic-link'" default_route_match = "webapp.type == 'symbolic-link'"

View File

@ -7,7 +7,9 @@ from . import WebAppServiceMixin
# TODO DEPRECATE # TODO DEPRECATE
class WebalizerAppBackend(WebAppServiceMixin, ServiceController): class WebalizerAppBackend(WebAppServiceMixin, ServiceController):
""" Needed for cleaning up webalizer main folder when webapp deleteion withou related contents """ """
Needed for cleaning up webalizer main folder when webapp deleteion withou related contents
"""
verbose_name = _("Webalizer App") verbose_name = _("Webalizer App")
default_route_match = "webapp.type == 'webalizer'" default_route_match = "webapp.type == 'webalizer'"

View File

@ -11,6 +11,14 @@ from . import WebAppServiceMixin
# Based on https://github.com/mtomic/wordpress-install/blob/master/wpinstall.php # Based on https://github.com/mtomic/wordpress-install/blob/master/wpinstall.php
class WordPressBackend(WebAppServiceMixin, ServiceController): class WordPressBackend(WebAppServiceMixin, ServiceController):
"""
Installs the latest version of WordPress available on www.wordpress.org
It fully configures the wp-config.php (keys included) and sets up the database with initial admin password.
WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST = '%s'
"""
format_docstring = (
settings.WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST,
)
verbose_name = _("Wordpress") verbose_name = _("Wordpress")
model = 'webapps.WebApp' model = 'webapps.WebApp'
default_route_match = "webapp.type == 'wordpress-php'" default_route_match = "webapp.type == 'wordpress-php'"

View File

@ -35,7 +35,7 @@ class WebAppSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
options_data = validated_data.pop('options') options_data = validated_data.pop('options')
instance = super(WebAppSerializer, self).update(validated_data) instance = super(WebAppSerializer, self).update(instance, validated_data)
existing = {} existing = {}
for obj in instance.options.all(): for obj in instance.options.all():
existing[obj.name] = obj existing[obj.name] = obj

View File

@ -114,6 +114,11 @@ WEBAPPS_UWSGI_SOCKET = getattr(settings, 'WEBAPPS_UWSGI_SOCKET',
) )
WEBAPPS_UWSGI_BASE_DIR = getattr(settings, 'WEBAPPS_UWSGI_BASE_DIR',
'/etc/uwsgi/'
)
WEBAPPS_PYTHON_MAX_REQUESTS = getattr(settings, 'WEBAPPS_PYTHON_MAX_REQUESTS', WEBAPPS_PYTHON_MAX_REQUESTS = getattr(settings, 'WEBAPPS_PYTHON_MAX_REQUESTS',
500 500
) )

View File

@ -9,7 +9,7 @@ from .serializers import WebsiteSerializer
class WebsiteViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet): class WebsiteViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet):
queryset = Website.objects.all() queryset = Website.objects.prefetch_related('domains', 'content_set__webapp', 'directives').all()
serializer_class = WebsiteSerializer serializer_class = WebsiteSerializer
filter_fields = ('name',) filter_fields = ('name',)

View File

@ -40,7 +40,7 @@ class Apache2Backend(ServiceController):
extra_conf = sorted(extra_conf, key=lambda a: len(a[0]), reverse=True) extra_conf = sorted(extra_conf, key=lambda a: len(a[0]), reverse=True)
context['extra_conf'] = '\n'.join([conf for location, conf in extra_conf]) context['extra_conf'] = '\n'.join([conf for location, conf in extra_conf])
return Template(textwrap.dedent("""\ return Template(textwrap.dedent("""\
<VirtualHost {% for ip in ips %}{{ ip }}:{{ port }} {% endfor %}> <VirtualHost{% for ip in ips %} {{ ip }}:{{ port }}{% endfor %}>
IncludeOptional /etc/apache2/site[s]-override/{{ site_unique_name }}.con[f] IncludeOptional /etc/apache2/site[s]-override/{{ site_unique_name }}.con[f]
ServerName {{ server_name }}\ ServerName {{ server_name }}\
{% if server_alias %} {% if server_alias %}
@ -59,7 +59,7 @@ class Apache2Backend(ServiceController):
def render_redirect_https(self, context): def render_redirect_https(self, context):
context['port'] = self.HTTP_PORT context['port'] = self.HTTP_PORT
return Template(textwrap.dedent(""" return Template(textwrap.dedent("""
<VirtualHost {% for ip in ips %}{{ ip }}:{{ port }} {% endfor %}> <VirtualHost{% for ip in ips %} {{ ip }}:{{ port }}{% endfor %}>
ServerName {{ server_name }}\ ServerName {{ server_name }}\
{% if server_alias %} {% if server_alias %}
ServerAlias {{ server_alias|join:' ' }}{% endif %}\ ServerAlias {{ server_alias|join:' ' }}{% endif %}\

View File

@ -9,6 +9,9 @@ from .. import settings
class WebalizerBackend(ServiceController): class WebalizerBackend(ServiceController):
"""
Creates webalizer conf file for each time a webalizer webapp is mounted on a website.
"""
verbose_name = _("Webalizer Content") verbose_name = _("Webalizer Content")
model = 'websites.Content' model = 'websites.Content'
default_route_match = "content.webapp.type == 'webalizer'" default_route_match = "content.webapp.type == 'webalizer'"

View File

@ -82,7 +82,7 @@ class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
options_data = validated_data.pop('options') options_data = validated_data.pop('options')
instance = super(WebsiteSerializer, self).update(validated_data) instance = super(WebsiteSerializer, self).update(instance, validated_data)
existing = {} existing = {}
for obj in instance.options.all(): for obj in instance.options.all():
existing[obj.name] = obj existing[obj.name] = obj