diff --git a/TODO.md b/TODO.md index d8d7ab82..b39e84ac 100644 --- a/TODO.md +++ b/TODO.md @@ -286,3 +286,6 @@ https://code.djangoproject.com/ticket/24576 # Amend lines??? # 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 diff --git a/orchestra/api/serializers.py b/orchestra/api/serializers.py index 55cfaa9d..e554c906 100644 --- a/orchestra/api/serializers.py +++ b/orchestra/api/serializers.py @@ -1,6 +1,9 @@ +import copy + from django.forms import widgets from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from rest_framework.utils import model_meta from ..core.validators import validate_password @@ -10,32 +13,47 @@ class SetPasswordSerializer(serializers.Serializer): 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): """ support for postonly_fields, fields whose value can only be set on post """ -# _options_class = HyperlinkedModelSerializerOptions def validate(self, attrs): """ calls model.clean() """ 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() return attrs - # TODO raise validationError instead of silently removing fields - def update(self, instance, validated_data): - """ removes postonly_fields from attrs when not posting """ + def post_only_cleanning(self, instance, validated_data): + """ removes postonly_fields from attrs """ model_attrs = dict(**validated_data) if instance is not None: for attr, value in validated_data.items(): if attr in self.Meta.postonly_fields: 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) + + 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): diff --git a/orchestra/contrib/databases/api.py b/orchestra/contrib/databases/api.py index 3bc92cee..808d02c0 100644 --- a/orchestra/contrib/databases/api.py +++ b/orchestra/contrib/databases/api.py @@ -8,13 +8,13 @@ from .serializers import DatabaseSerializer, DatabaseUserSerializer class DatabaseViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet): - queryset = Database.objects.all() + queryset = Database.objects.prefetch_related('users').all() serializer_class = DatabaseSerializer filter_fields = ('name',) class DatabaseUserViewSet(LogApiMixin, AccountApiMixin, SetPasswordApiMixin, viewsets.ModelViewSet): - queryset = DatabaseUser.objects.all() + queryset = DatabaseUser.objects.prefetch_related('databases').all() serializer_class = DatabaseUserSerializer filter_fields = ('username',) diff --git a/orchestra/contrib/databases/backends.py b/orchestra/contrib/databases/backends.py index 37bc0229..d076f15e 100644 --- a/orchestra/contrib/databases/backends.py +++ b/orchestra/contrib/databases/backends.py @@ -9,6 +9,12 @@ from . import settings 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" model = 'databases.Database' default_route_match = "database.type == 'mysql'" @@ -54,6 +60,11 @@ class MySQLBackend(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" model = 'databases.DatabaseUser' default_route_match = "databaseuser.type == 'mysql'" @@ -93,6 +104,10 @@ class MySQLUserBackend(ServiceController): class MysqlDisk(ServiceMonitor): + """ + du -bs + Implements triggers for resource limit exceeded and recovery, disabling insert and create privileges. + """ model = 'databases.Database' verbose_name = _("MySQL disk") diff --git a/orchestra/contrib/domains/backends.py b/orchestra/contrib/domains/backends.py index 13762b4e..35e4848d 100644 --- a/orchestra/contrib/domains/backends.py +++ b/orchestra/contrib/domains/backends.py @@ -11,6 +11,18 @@ from . import settings 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") model = 'domains.Domain' related_models = ( @@ -121,6 +133,12 @@ class Bind9MasterDomainBackend(ServiceController): 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") related_models = ( ('domains.Domain', 'origin'), diff --git a/orchestra/contrib/lists/backends.py b/orchestra/contrib/lists/backends.py index 3eb577b0..1ffa5dc0 100644 --- a/orchestra/contrib/lists/backends.py +++ b/orchestra/contrib/lists/backends.py @@ -10,6 +10,19 @@ from .models import List 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" model = 'lists.List' addresses = [ @@ -149,75 +162,15 @@ class MailmanBackend(ServiceController): 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): + """ + 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' resource = ServiceMonitor.TRAFFIC verbose_name = _("Mailman traffic") @@ -235,13 +188,13 @@ class MailmanTraffic(ServiceMonitor): import sys from datetime import datetime from dateutil import tz - + def to_local_timezone(date, tzlocal=tz.tzlocal()): date = datetime.strptime(date, '%Y-%m-%d %H:%M:%S %Z') date = date.replace(tzinfo=tz.tzutc()) date = date.astimezone(tzlocal) return date - + postlogs = {postlogs} # Use local timezone end_date = to_local_timezone('{current_date}') @@ -261,13 +214,13 @@ class MailmanTraffic(ServiceMonitor): 'Nov': '11', 'Dec': '12', }} - + def prepare(object_id, list_name, ini_date): global lists ini_date = to_local_timezone(ini_date) ini_date = int(ini_date.strftime('%Y%m%d%H%M%S')) lists[list_name] = [ini_date, object_id, 0] - + def monitor(lists, end_date, months, postlogs): for postlog in postlogs: try: @@ -318,6 +271,9 @@ class MailmanTraffic(ServiceMonitor): class MailmanSubscribers(ServiceMonitor): + """ + Monitors number of list subscribers via `list_members` + """ model = 'lists.List' verbose_name = _("Mailman subscribers") diff --git a/orchestra/contrib/mailboxes/api.py b/orchestra/contrib/mailboxes/api.py index d03a892e..2d498f1e 100644 --- a/orchestra/contrib/mailboxes/api.py +++ b/orchestra/contrib/mailboxes/api.py @@ -8,13 +8,12 @@ from .serializers import AddressSerializer, MailboxSerializer class AddressViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet): - queryset = Address.objects.all() + queryset = Address.objects.select_related('domain').prefetch_related('mailboxes').all() serializer_class = AddressSerializer - class MailboxViewSet(LogApiMixin, SetPasswordApiMixin, AccountApiMixin, viewsets.ModelViewSet): - queryset = Mailbox.objects.all() + queryset = Mailbox.objects.prefetch_related('addresses__domain').all() serializer_class = MailboxSerializer diff --git a/orchestra/contrib/mailboxes/backends.py b/orchestra/contrib/mailboxes/backends.py index bed6ab45..bdf61131 100644 --- a/orchestra/contrib/mailboxes/backends.py +++ b/orchestra/contrib/mailboxes/backends.py @@ -20,6 +20,10 @@ logger = logging.getLogger(__name__) 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") model = 'mailboxes.Mailbox' @@ -74,6 +78,9 @@ class UNIXUserMaildirBackend(ServiceController): class DovecotPostfixPasswdVirtualUserBackend(ServiceController): + """ + WARNING: This backends is not fully implemented + """ verbose_name = _("Dovecot-Postfix virtualuser") model = 'mailboxes.Mailbox' # TODO related_models = ('resources__content_type') ?? needed for updating disk usage from resource.data @@ -176,6 +183,17 @@ class DovecotPostfixPasswdVirtualUserBackend(ServiceController): class PostfixAddressBackend(ServiceController): + """ + Addresses based on Postfix virtual alias domains. + MAILBOXES_LOCAL_DOMAIN = '%s' + MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH = '%s' + MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH = '%s' + """ + format_docstring = ( + settings.MAILBOXES_LOCAL_DOMAIN, + settings.MAILBOXES_VIRTUAL_ALIAS_DOMAINS_PATH, + settings.MAILBOXES_VIRTUAL_ALIAS_MAPS_PATH, + ) verbose_name = _("Postfix address") model = 'mailboxes.Address' related_models = ( @@ -267,6 +285,9 @@ class PostfixAddressBackend(ServiceController): class AutoresponseBackend(ServiceController): + """ + WARNING: not implemented + """ verbose_name = _("Mail autoresponse") model = 'mailboxes.Autoresponse' @@ -274,9 +295,13 @@ class AutoresponseBackend(ServiceController): class DovecotMaildirDisk(ServiceMonitor): """ Maildir disk usage based on Dovecot maildirsize file - http://wiki2.dovecot.org/Quota/Maildir + + MAILBOXES_MAILDIRSIZE_PATH = '%s' """ + format_docstring = ( + settings.MAILBOXES_MAILDIRSIZE_PATH, + ) model = 'mailboxes.Mailbox' resource = ServiceMonitor.DISK verbose_name = _("Dovecot Maildir size") @@ -304,9 +329,13 @@ class DovecotMaildirDisk(ServiceMonitor): class PostfixMailscannerTraffic(ServiceMonitor): """ - A high-performance log parser - Reads the mail.log file only once, for all users + A high-performance log parser. + 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' resource = ServiceMonitor.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"), } return replace(context, "'", '"') - - - - - diff --git a/orchestra/contrib/mailboxes/serializers.py b/orchestra/contrib/mailboxes/serializers.py index 23cf2e7c..c7ab4c17 100644 --- a/orchestra/contrib/mailboxes/serializers.py +++ b/orchestra/contrib/mailboxes/serializers.py @@ -25,10 +25,10 @@ class RelatedAddressSerializer(AccountSerializerMixin, serializers.HyperlinkedMo class Meta: model = Address fields = ('url', 'name', 'domain', 'forward') - - def from_native(self, data, files=None): - queryset = self.opts.model.objects.filter(account=self.account) - return get_object_or_404(queryset, name=data['name']) +# +# def from_native(self, data, files=None): +# queryset = self.opts.model.objects.filter(account=self.account) +# return get_object_or_404(queryset, name=data['name']) class MailboxSerializer(AccountSerializerMixin, SetPasswordHyperlinkedSerializer): diff --git a/orchestra/contrib/orchestration/__init__.py b/orchestra/contrib/orchestration/__init__.py index e9769bd5..c4d3945d 100644 --- a/orchestra/contrib/orchestration/__init__.py +++ b/orchestra/contrib/orchestration/__init__.py @@ -25,9 +25,7 @@ class Operation(): self.backend = backend # instance should maintain any dynamic attribute until backend execution # 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) - print('aa', getattr(self.instance, 'password', 'NOOOO'), id(self.instance)) self.action = action self.servers = servers diff --git a/orchestra/contrib/orchestration/admin.py b/orchestra/contrib/orchestration/admin.py index 0c0d8c6d..eb5a9a88 100644 --- a/orchestra/contrib/orchestration/admin.py +++ b/orchestra/contrib/orchestration/admin.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin.html import monospace_format from orchestra.admin.utils import admin_link, admin_date, admin_colored -from . import settings +from . import settings, helpers from .backends import ServiceBackend from .models import Server, Route, BackendLog, BackendOperation from .widgets import RouteBackendSelect @@ -31,10 +31,7 @@ class RouteAdmin(admin.ModelAdmin): list_filter = ('host', 'is_active', 'backend') ordering = ('backend',) - BACKEND_HELP_TEXT = { - backend: "This backend operates over '%s'" % ServiceBackend.get_backend(backend).model - for backend, __ in ServiceBackend.get_choices() - } + BACKEND_HELP_TEXT = helpers.get_backends_help_text(ServiceBackend.get_backends()) DEFAULT_MATCH = { backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends() } diff --git a/orchestra/contrib/orchestration/backends.py b/orchestra/contrib/orchestration/backends.py index 7c74ed4c..e55d7097 100644 --- a/orchestra/contrib/orchestration/backends.py +++ b/orchestra/contrib/orchestration/backends.py @@ -45,6 +45,7 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount): default_route_match = 'True' # Force the backend manager to block in multiple backend executions executing them synchronously block = False + format_docstring = () def __str__(self): return type(self).__name__ diff --git a/orchestra/contrib/orchestration/helpers.py b/orchestra/contrib/orchestration/helpers.py index edb03318..0c55d224 100644 --- a/orchestra/contrib/orchestration/helpers.py +++ b/orchestra/contrib/orchestration/helpers.py @@ -1,3 +1,5 @@ +import textwrap + from django.contrib import messages from django.core.mail import mail_admins 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 _ +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'
+ - Related models: %(related_models)s
+ - Script executable: %(script_executable)s
+ - Script method: %(script_method)s
+ - Function method: %(function_method)s
+ - Actions: %(actions)s
""" + ) % 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 += '
' + '
'.join(docstring) + help_texts[backend.get_name()] = help_text + return help_texts + + def send_report(method, args, log): server = args[0] backend = method.__self__.__class__.__name__ diff --git a/orchestra/contrib/orchestration/settings.py b/orchestra/contrib/orchestration/settings.py index a6d3e845..0c248fa4 100644 --- a/orchestra/contrib/orchestration/settings.py +++ b/orchestra/contrib/orchestration/settings.py @@ -32,5 +32,5 @@ ORCHESTRATION_DISABLE_EXECUTION = getattr(settings, 'ORCHESTRATION_DISABLE_EXECU ORCHESTRATION_BACKEND_CLEANUP_DELTA = getattr(settings, 'ORCHESTRATION_BACKEND_CLEANUP_DELTA', - timedelta(days=40) + timedelta(days=15) ) diff --git a/orchestra/contrib/systemusers/backends.py b/orchestra/contrib/systemusers/backends.py index a4fe2d81..82e6dce8 100644 --- a/orchestra/contrib/systemusers/backends.py +++ b/orchestra/contrib/systemusers/backends.py @@ -10,6 +10,16 @@ from . import settings class UNIXUserBackend(ServiceController): + """ + Basic UNIX system user/group support based on `useradd`, `usermod`, `userdel` and `groupdel`. + SYSTEMUSERS_DEFAULT_GROUP_MEMBERS = '%s' + SYSTEMUSERS_MOVE_ON_DELETE_PATH = '%s' + """ + format_docstring = ( + settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS, + settings.SYSTEMUSERS_MOVE_ON_DELETE_PATH, + ) + verbose_name = _("UNIX user") model = 'systemusers.SystemUser' actions = ('save', 'delete', 'grant_permission') @@ -84,6 +94,9 @@ class UNIXUserBackend(ServiceController): class UNIXUserDisk(ServiceMonitor): + """ + `du -bs ` + """ model = 'systemusers.SystemUser' resource = ServiceMonitor.DISK verbose_name = _('UNIX user disk') @@ -109,6 +122,14 @@ class UNIXUserDisk(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' resource = ServiceMonitor.TRAFFIC verbose_name = _("Exim4 traffic") @@ -188,6 +209,13 @@ class Exim4Traffic(ServiceMonitor): class VsFTPdTraffic(ServiceMonitor): + """ + vsFTPd log parser. + SYSTEMUSERS_FTP_LOG_PATH = '%s' + """ + format_docstring = ( + settings.SYSTEMUSERS_FTP_LOG_PATH, + ) model = 'systemusers.SystemUser' resource = ServiceMonitor.TRAFFIC verbose_name = _('VsFTPd traffic') @@ -279,4 +307,3 @@ class VsFTPdTraffic(ServiceMonitor): 'username': user.username, } return replace(context, "'", '"') - diff --git a/orchestra/contrib/vps/backends.py b/orchestra/contrib/vps/backends.py index 7a83bd55..29d21415 100644 --- a/orchestra/contrib/vps/backends.py +++ b/orchestra/contrib/vps/backends.py @@ -3,6 +3,9 @@ from orchestra.contrib.resources import ServiceMonitor class OpenVZTraffic(ServiceMonitor): + """ + WARNING: Not fully implemeted + """ model = 'vps.VPS' resource = ServiceMonitor.TRAFFIC diff --git a/orchestra/contrib/webapps/api.py b/orchestra/contrib/webapps/api.py index 283a8fb3..61237167 100644 --- a/orchestra/contrib/webapps/api.py +++ b/orchestra/contrib/webapps/api.py @@ -5,11 +5,13 @@ from orchestra.contrib.accounts.api import AccountApiMixin from . import settings from .models import WebApp +from .options import AppOption from .serializers import WebAppSerializer +from .types import AppType class WebAppViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet): - queryset = WebApp.objects.all() + queryset = WebApp.objects.prefetch_related('options').all() serializer_class = WebAppSerializer filter_fields = ('name',) @@ -22,6 +24,31 @@ class WebAppViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet): metadata.data['settings'] = { 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 diff --git a/orchestra/contrib/webapps/backends/php.py b/orchestra/contrib/webapps/backends/php.py index c89ee6ef..24e15839 100644 --- a/orchestra/contrib/webapps/backends/php.py +++ b/orchestra/contrib/webapps/backends/php.py @@ -11,6 +11,29 @@ from .. import settings 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") default_route_match = "webapp.type.endswith('php')" MERGE = settings.WEBAPPS_MERGE_PHP_WEBAPPS diff --git a/orchestra/contrib/webapps/backends/python.py b/orchestra/contrib/webapps/backends/python.py index e665dd5e..9c384c75 100644 --- a/orchestra/contrib/webapps/backends/python.py +++ b/orchestra/contrib/webapps/backends/python.py @@ -13,9 +13,19 @@ from .. import settings class uWSGIPythonBackend(WebAppServiceMixin, ServiceController): """ Emperor mode - 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") default_route_match = "webapp.type.endswith('python')" diff --git a/orchestra/contrib/webapps/backends/static.py b/orchestra/contrib/webapps/backends/static.py index b5179be3..7becb8cb 100644 --- a/orchestra/contrib/webapps/backends/static.py +++ b/orchestra/contrib/webapps/backends/static.py @@ -6,6 +6,10 @@ from . import WebAppServiceMixin 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") default_route_match = "webapp.type == 'static'" diff --git a/orchestra/contrib/webapps/backends/symboliclink.py b/orchestra/contrib/webapps/backends/symboliclink.py index ec9c9459..add8eab1 100644 --- a/orchestra/contrib/webapps/backends/symboliclink.py +++ b/orchestra/contrib/webapps/backends/symboliclink.py @@ -8,6 +8,10 @@ from .php import PHPBackend 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") model = 'webapps.WebApp' default_route_match = "webapp.type == 'symbolic-link'" diff --git a/orchestra/contrib/webapps/backends/webalizer.py b/orchestra/contrib/webapps/backends/webalizer.py index fe5bab30..664603ae 100644 --- a/orchestra/contrib/webapps/backends/webalizer.py +++ b/orchestra/contrib/webapps/backends/webalizer.py @@ -7,7 +7,9 @@ from . import WebAppServiceMixin # TODO DEPRECATE 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") default_route_match = "webapp.type == 'webalizer'" diff --git a/orchestra/contrib/webapps/backends/wordpress.py b/orchestra/contrib/webapps/backends/wordpress.py index 4ada7fec..082ce091 100644 --- a/orchestra/contrib/webapps/backends/wordpress.py +++ b/orchestra/contrib/webapps/backends/wordpress.py @@ -11,6 +11,14 @@ from . import WebAppServiceMixin # Based on https://github.com/mtomic/wordpress-install/blob/master/wpinstall.php 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") model = 'webapps.WebApp' default_route_match = "webapp.type == 'wordpress-php'" diff --git a/orchestra/contrib/webapps/serializers.py b/orchestra/contrib/webapps/serializers.py index bed4100d..db289338 100644 --- a/orchestra/contrib/webapps/serializers.py +++ b/orchestra/contrib/webapps/serializers.py @@ -35,7 +35,7 @@ class WebAppSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): def update(self, instance, validated_data): options_data = validated_data.pop('options') - instance = super(WebAppSerializer, self).update(validated_data) + instance = super(WebAppSerializer, self).update(instance, validated_data) existing = {} for obj in instance.options.all(): existing[obj.name] = obj diff --git a/orchestra/contrib/webapps/settings.py b/orchestra/contrib/webapps/settings.py index a5af0c90..88a63cee 100644 --- a/orchestra/contrib/webapps/settings.py +++ b/orchestra/contrib/webapps/settings.py @@ -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', 500 ) diff --git a/orchestra/contrib/websites/api.py b/orchestra/contrib/websites/api.py index 54be3c66..af4d7fb3 100644 --- a/orchestra/contrib/websites/api.py +++ b/orchestra/contrib/websites/api.py @@ -9,7 +9,7 @@ from .serializers import WebsiteSerializer class WebsiteViewSet(LogApiMixin, AccountApiMixin, viewsets.ModelViewSet): - queryset = Website.objects.all() + queryset = Website.objects.prefetch_related('domains', 'content_set__webapp', 'directives').all() serializer_class = WebsiteSerializer filter_fields = ('name',) diff --git a/orchestra/contrib/websites/backends/apache.py b/orchestra/contrib/websites/backends/apache.py index f8f2d286..cb988946 100644 --- a/orchestra/contrib/websites/backends/apache.py +++ b/orchestra/contrib/websites/backends/apache.py @@ -40,7 +40,7 @@ class Apache2Backend(ServiceController): 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]) return Template(textwrap.dedent("""\ - + IncludeOptional /etc/apache2/site[s]-override/{{ site_unique_name }}.con[f] ServerName {{ server_name }}\ {% if server_alias %} @@ -59,7 +59,7 @@ class Apache2Backend(ServiceController): def render_redirect_https(self, context): context['port'] = self.HTTP_PORT return Template(textwrap.dedent(""" - + ServerName {{ server_name }}\ {% if server_alias %} ServerAlias {{ server_alias|join:' ' }}{% endif %}\ diff --git a/orchestra/contrib/websites/backends/webalizer.py b/orchestra/contrib/websites/backends/webalizer.py index 09a1d4cb..3947c94c 100644 --- a/orchestra/contrib/websites/backends/webalizer.py +++ b/orchestra/contrib/websites/backends/webalizer.py @@ -9,6 +9,9 @@ from .. import settings class WebalizerBackend(ServiceController): + """ + Creates webalizer conf file for each time a webalizer webapp is mounted on a website. + """ verbose_name = _("Webalizer Content") model = 'websites.Content' default_route_match = "content.webapp.type == 'webalizer'" diff --git a/orchestra/contrib/websites/serializers.py b/orchestra/contrib/websites/serializers.py index 70776571..ac8cfb7d 100644 --- a/orchestra/contrib/websites/serializers.py +++ b/orchestra/contrib/websites/serializers.py @@ -82,7 +82,7 @@ class WebsiteSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): def update(self, instance, validated_data): options_data = validated_data.pop('options') - instance = super(WebsiteSerializer, self).update(validated_data) + instance = super(WebsiteSerializer, self).update(instance, validated_data) existing = {} for obj in instance.options.all(): existing[obj.name] = obj