diff --git a/TODO.md b/TODO.md index c7d5a89e..54d7a587 100644 --- a/TODO.md +++ b/TODO.md @@ -30,8 +30,8 @@ * LAST version of this shit http://wkhtmltopdf.org/downloads.html * translations - from django.utils import translation - with translation.override('en'): + from django.utils import translation + with translation.override('en'): * help_text on readonly_fields specialy Bill.state. (eg. A bill is in OPEN state when bla bla ) @@ -190,7 +190,6 @@ Multi-tenant WebApps * forms autocomplete="off", doesn't work in chrome - ln -s /proc/self/fd /dev/fd @@ -204,3 +203,6 @@ POST INSTALL ssh-keygen ssh-copy-id root@ + +* symbolicLink webapp (link stuff from other places) + diff --git a/orchestra/apps/accounts/admin.py b/orchestra/apps/accounts/admin.py index 9ceda689..d67d9406 100644 --- a/orchestra/apps/accounts/admin.py +++ b/orchestra/apps/accounts/admin.py @@ -161,6 +161,22 @@ class AccountAdminMixin(object): account_link.allow_tags = True account_link.admin_order_field = 'account__username' + def get_fields(self, request, obj=None): + """ remove account or account_link depending on the case """ + fields = super(AccountAdminMixin, self).get_fields(request, obj) + fields = list(fields) + if obj is not None or getattr(self, 'account_id', None): + try: + fields.remove('account') + except ValueError: + pass + else: + try: + fields.remove('account_link') + except ValueError: + pass + return fields + def get_readonly_fields(self, request, obj=None): """ provide account for filter_by_account_fields """ if obj: diff --git a/orchestra/apps/databases/backends.py b/orchestra/apps/databases/backends.py index 967a6654..df1454c8 100644 --- a/orchestra/apps/databases/backends.py +++ b/orchestra/apps/databases/backends.py @@ -11,6 +11,7 @@ from . import settings class MySQLBackend(ServiceController): verbose_name = "MySQL database" model = 'databases.Database' + default_route_match = "database.type == 'mysql'" def save(self, database): if database.type != database.MYSQL: @@ -53,6 +54,7 @@ class MySQLBackend(ServiceController): class MySQLUserBackend(ServiceController): verbose_name = "MySQL user" model = 'databases.DatabaseUser' + default_route_match = "databaseuser.type == 'mysql'" def save(self, user): if user.type != user.MYSQL: diff --git a/orchestra/apps/databases/models.py b/orchestra/apps/databases/models.py index c907a8ee..76972437 100644 --- a/orchestra/apps/databases/models.py +++ b/orchestra/apps/databases/models.py @@ -37,7 +37,9 @@ class Database(models.Model): return users.order_by('id').first().databaseuser -Database.users.through._meta.unique_together = (('database', 'databaseuser'),) +Database.users.through._meta.unique_together = ( + ('database', 'databaseuser'), +) class DatabaseUser(models.Model): diff --git a/orchestra/apps/domains/backends.py b/orchestra/apps/domains/backends.py index 137c5f9a..396a167f 100644 --- a/orchestra/apps/domains/backends.py +++ b/orchestra/apps/domains/backends.py @@ -4,6 +4,7 @@ import textwrap from django.utils.translation import ugettext_lazy as _ from orchestra.apps.orchestration import ServiceController +from orchestra.apps.orchestration.models import BackendOperation as Operation from orchestra.utils.python import AttrDict from . import settings @@ -75,10 +76,11 @@ class Bind9MasterDomainBackend(ServiceController): self.append('[[ $UPDATED == 1 ]] && service bind9 reload') def get_servers(self, domain, backend): - from orchestra.apps.orchestration.models import Route - operation = AttrDict(backend=backend, action='save', instance=domain) + """ Get related server IPs from registered backend routes """ + from orchestra.apps.orchestration.manager import router + operation = Operation.create(backend_cls=backend, action=Operation.SAVE, instance=domain) servers = [] - for server in Route.get_servers(operation): + for server in router.get_servers(operation): servers.append(server.get_ip()) return servers diff --git a/orchestra/apps/domains/models.py b/orchestra/apps/domains/models.py index 6d71592b..58e41f2e 100644 --- a/orchestra/apps/domains/models.py +++ b/orchestra/apps/domains/models.py @@ -96,7 +96,14 @@ class Domain(models.Model): def render_zone(self): origin = self.origin zone = origin.render_records() + tail = [] for subdomain in origin.get_subdomains(): + if subdomain.name.startswith('*'): + # This subdomains needs to be rendered last in order to avoid undesired matches + tail.append(subdomain) + else: + zone += subdomain.render_records() + for subdomain in sorted(tail, key=lambda x: len(x.name), reverse=True): zone += subdomain.render_records() return zone diff --git a/orchestra/apps/orchestration/admin.py b/orchestra/apps/orchestration/admin.py index 160507f5..94b4a14e 100644 --- a/orchestra/apps/orchestration/admin.py +++ b/orchestra/apps/orchestration/admin.py @@ -3,13 +3,12 @@ from django.contrib import admin from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ -from orchestra.forms.widgets import DynamicHelpTextSelect from orchestra.admin.html import monospace_format from orchestra.admin.utils import admin_link, admin_date, admin_colored from .backends import ServiceBackend from .models import Server, Route, BackendLog, BackendOperation - +from .widgets import RouteBackendSelect STATE_COLORS = { BackendLog.RECEIVED: 'darkorange', @@ -22,18 +21,22 @@ STATE_COLORS = { } + class RouteAdmin(admin.ModelAdmin): list_display = [ 'id', 'backend', 'host', 'match', 'display_model', 'display_actions', 'is_active' ] - list_editable = ['backend', 'host', 'match', 'is_active'] + list_editable = ['host', 'match', 'is_active'] list_filter = ['host', 'is_active', 'backend'] BACKEND_HELP_TEXT = { backend: "This backend operates over '%s'" % ServiceBackend.get_backend(backend).model for backend, __ in ServiceBackend.get_plugin_choices() } + DEFAULT_MATCH = { + backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends(active=False) + } def display_model(self, route): try: @@ -54,7 +57,7 @@ class RouteAdmin(admin.ModelAdmin): def formfield_for_dbfield(self, db_field, **kwargs): """ Provides dynamic help text on backend form field """ if db_field.name == 'backend': - kwargs['widget'] = DynamicHelpTextSelect('this.id', self.BACKEND_HELP_TEXT) + kwargs['widget'] = RouteBackendSelect('this.id', self.BACKEND_HELP_TEXT, self.DEFAULT_MATCH) return super(RouteAdmin, self).formfield_for_dbfield(db_field, **kwargs) def get_form(self, request, obj=None, **kwargs): diff --git a/orchestra/apps/orchestration/backends.py b/orchestra/apps/orchestration/backends.py index 0d2c81ad..904cc846 100644 --- a/orchestra/apps/orchestration/backends.py +++ b/orchestra/apps/orchestration/backends.py @@ -34,6 +34,7 @@ class ServiceBackend(plugins.Plugin): type = 'task' # 'sync' ignore_fields = [] actions = [] + default_route_match = 'True' __metaclass__ = ServiceMount diff --git a/orchestra/apps/orchestration/manager.py b/orchestra/apps/orchestration/manager.py index 0ec144cb..c1605c83 100644 --- a/orchestra/apps/orchestration/manager.py +++ b/orchestra/apps/orchestration/manager.py @@ -10,6 +10,7 @@ from .helpers import send_report logger = logging.getLogger(__name__) +router = import_class(settings.ORCHESTRATION_ROUTER) def as_task(execute): @@ -41,17 +42,17 @@ def close_connection(execute): def execute(operations, async=False): """ generates and executes the operations on the servers """ - router = import_class(settings.ORCHESTRATION_ROUTER) scripts = {} cache = {} # Generate scripts per server+backend for operation in operations: logger.debug("Queued %s" % str(operation)) - servers = router.get_servers(operation, cache=cache) - for server in servers: + if operation.servers is None: + operation.servers = router.get_servers(operation, cache=cache) + for server in operation.servers: key = (server, operation.backend) if key not in scripts: - scripts[key] = (operation.backend(), [operation]) + scripts[key] = (operation.backend, [operation]) scripts[key][0].prepare() else: scripts[key][1].append(operation) diff --git a/orchestra/apps/orchestration/middlewares.py b/orchestra/apps/orchestration/middlewares.py index e8074546..16ee19ca 100644 --- a/orchestra/apps/orchestration/middlewares.py +++ b/orchestra/apps/orchestration/middlewares.py @@ -7,6 +7,7 @@ from django.http.response import HttpResponseServerError from orchestra.utils.python import OrderedSet +from .manager import router from .backends import ServiceBackend from .helpers import message_user from .models import BackendLog @@ -51,6 +52,16 @@ class OperationsMiddleware(object): return request.pending_operations return set() + @classmethod + def get_route_cache(cls): + """ chache the routes to save sql queries """ + if hasattr(cls.thread_locals, 'request'): + request = cls.thread_locals.request + if not hasattr(request, 'route_cache'): + request.route_cache = {} + return request.route_cache + return {} + @classmethod def collect(cls, action, **kwargs): """ Collects all pending operations derived from model signals """ @@ -58,13 +69,14 @@ class OperationsMiddleware(object): if request is None: return pending_operations = cls.get_pending_operations() - for backend in ServiceBackend.get_backends(): + route_cache = cls.get_route_cache() + for backend_cls in ServiceBackend.get_backends(): # Check if there exists a related instance to be executed for this backend instances = [] - if backend.is_main(kwargs['instance']): + if backend_cls.is_main(kwargs['instance']): instances = [(kwargs['instance'], action)] else: - candidate = backend.get_related(kwargs['instance']) + candidate = backend_cls.get_related(kwargs['instance']) if candidate: if candidate.__class__.__name__ == 'ManyRelatedManager': if 'pk_set' in kwargs: @@ -76,7 +88,7 @@ class OperationsMiddleware(object): candidates = [candidate] for candidate in candidates: # Check if a delete for candidate is in pending_operations - delete_mock = Operation.create(backend, candidate, Operation.DELETE) + delete_mock = Operation.create(backend_cls, candidate, Operation.DELETE) if delete_mock not in pending_operations: # related objects with backend.model trigger save() instances.append((candidate, Operation.SAVE)) @@ -84,7 +96,7 @@ class OperationsMiddleware(object): # Maintain consistent state of pending_operations based on save/delete behaviour # Prevent creating a deleted instance by deleting existing saves if iaction == Operation.DELETE: - save_mock = Operation.create(backend, instance, Operation.SAVE) + save_mock = Operation.create(backend_cls, instance, Operation.SAVE) try: pending_operations.remove(save_mock) except KeyError: @@ -97,17 +109,23 @@ class OperationsMiddleware(object): if update_fields != []: execute = False for field in update_fields: - if field not in backend.ignore_fields: + if field not in backend_cls.ignore_fields: execute = True break if not execute: continue - operation = Operation.create(backend, instance, iaction) - if iaction != Operation.DELETE: - # usually we expect to be using last object state, - # except when we are deleting it - pending_operations.discard(operation) - pending_operations.add(operation) + operation = Operation.create(backend_cls, instance, iaction) + # Only schedule operations if the router gives servers to execute into + servers = router.get_servers(operation, cache=route_cache) + if servers: + operation.servers = servers + if iaction != Operation.DELETE: + # usually we expect to be using last object state, + # except when we are deleting it + pending_operations.discard(operation) + elif iaction == Operation.DELETE: + operation.preload_context() + pending_operations.add(operation) def process_request(self, request): """ Store request on a thread local variable """ diff --git a/orchestra/apps/orchestration/models.py b/orchestra/apps/orchestration/models.py index 797a10b4..788cb3cc 100644 --- a/orchestra/apps/orchestration/models.py +++ b/orchestra/apps/orchestration/models.py @@ -11,7 +11,7 @@ from orchestra.core.validators import validate_ip_address, ValidationError from orchestra.models.fields import NullableCharField #from orchestra.utils.apps import autodiscover -from . import settings, manager +from . import settings from .backends import ServiceBackend @@ -119,36 +119,43 @@ class BackendOperation(models.Model): def __hash__(self): """ set() """ - backend = getattr(self, 'backend', self.backend) - return hash(backend) + hash(self.instance) + hash(self.action) + backend_cls = type(self.backend) + return hash(backend_cls) + hash(self.instance) + hash(self.action) def __eq__(self, operation): """ set() """ return hash(self) == hash(operation) @classmethod - def create(cls, backend, instance, action): - op = cls(backend=backend.get_name(), instance=instance, action=action) - op.backend = backend + def create(cls, backend_cls, instance, action, servers=None): + op = cls(backend=backend_cls.get_name(), instance=instance, action=action) + op.backend = backend_cls() # instance should maintain any dynamic attribute until backend execution # deep copy is prefered over copy otherwise objects will share same atributes (queryset cache) op.instance = copy.deepcopy(instance) - if action == cls.DELETE: - # Heuristic, running get_context will prevent most of related objects do not exist errors - if hasattr(backend, 'get_context'): - backend().get_context(op.instance) + op.servers = servers return op @classmethod def execute(cls, operations, async=False): + from . import manager return manager.execute(operations, async=async) @classmethod def execute_action(cls, instance, action): backends = ServiceBackend.get_backends(instance=instance, action=action) - operations = [cls.create(backend, instance, action) for backend in backends] + operations = [cls.create(backend_cls, instance, action) for backend_cls in backends] return cls.execute(operations) + def preload_context(self): + """ + Heuristic + Running get_context will prevent most of related objects do not exist errors + """ + if self.action == self.DELETE: + if hasattr(self.backend, 'get_context'): + self.backend.get_context(op.instance) + def backend_class(self): return ServiceBackend.get_backend(self.backend) @@ -187,14 +194,14 @@ class Route(models.Model): def get_servers(cls, operation, **kwargs): cache = kwargs.get('cache', {}) servers = [] - backend = operation.backend - key = (backend.get_name(), operation.action) + backend_cls = type(operation.backend) + key = (backend_cls.get_name(), operation.action) try: routes = cache[key] except KeyError: cache[key] = [] - for route in cls.objects.filter(is_active=True, backend=backend.get_name()): - for action in backend.get_actions(): + for route in cls.objects.filter(is_active=True, backend=backend_cls.get_name()): + for action in backend_cls.get_actions(): _key = (route.backend, action) try: cache[_key].append(route) diff --git a/orchestra/apps/orchestration/widgets.py b/orchestra/apps/orchestration/widgets.py new file mode 100644 index 00000000..576de4e8 --- /dev/null +++ b/orchestra/apps/orchestration/widgets.py @@ -0,0 +1,24 @@ +import textwrap + +from orchestra.forms.widgets import DynamicHelpTextSelect + + +class RouteBackendSelect(DynamicHelpTextSelect): + """ Updates matches input field based on selected backend """ + def __init__(self, target, help_text, route_matches, *args, **kwargs): + kwargs['attrs'] = { + 'onfocus': "this.oldvalue = this.value;", + } + self.route_matches = route_matches + super(RouteBackendSelect, self).__init__(target, help_text, *args, **kwargs) + + def get_dynamic_help_text(self, target, help_text): + help_text = super(RouteBackendSelect, self).get_dynamic_help_text(target, help_text) + return help_text + textwrap.dedent("""\ + routematches = {route_matches}; + match = $("#id_match"); + if ( this.oldvalue == "" || match.value == routematches[this.oldvalue]) + match.value = routematches[this.options[this.selectedIndex].value]; + this.oldvalue = this.value; + """.format(route_matches=self.route_matches) + ) diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py index 86111dad..834c34ee 100644 --- a/orchestra/apps/payments/models.py +++ b/orchestra/apps/payments/models.py @@ -33,25 +33,30 @@ class PaymentSource(models.Model): def method_class(self): return PaymentMethod.get_plugin(self.method) + @cached_property + def service_instance(self): + """ Per request lived method_instance """ + return self.method_class() + @cached_property def label(self): - return self.method_class().get_label(self.data) + return self.method_instance.get_label(self.data) @cached_property def number(self): - return self.method_class().get_number(self.data) + return self.method_instance.get_number(self.data) def get_bill_context(self): - method = self.method_class() + method = self.method_instance return { 'message': method.get_bill_message(self), } def get_due_delta(self): - return self.method_class().due_delta + return self.method_instance.due_delta def clean(self): - self.data = self.method_class().clean_data(self.data) + self.data = self.method_instance.clean_data(self.data) class TransactionQuerySet(models.QuerySet): diff --git a/orchestra/apps/saas/admin.py b/orchestra/apps/saas/admin.py index ed125b4d..4d1f33eb 100644 --- a/orchestra/apps/saas/admin.py +++ b/orchestra/apps/saas/admin.py @@ -13,6 +13,7 @@ class SaaSAdmin(SelectPluginAdminMixin, AccountAdminMixin, admin.ModelAdmin): list_filter = ('service',) plugin = SoftwareService plugin_field = 'service' + plugin_title = 'Software as a Service' def display_site_name(self, saas): site_name = saas.get_site_name() @@ -20,15 +21,6 @@ class SaaSAdmin(SelectPluginAdminMixin, AccountAdminMixin, admin.ModelAdmin): display_site_name.short_description = _("Site name") display_site_name.allow_tags = True display_site_name.admin_order_field = 'site_name' - - def get_fields(self, request, obj=None): - fields = super(SaaSAdmin, self).get_fields(request, obj) - fields = list(fields) - # TODO do it in AccountAdminMixin? - if obj is not None: - fields.remove('account') - else: - fields.remove('account_link') - return fields + admin.site.register(SaaS, SaaSAdmin) diff --git a/orchestra/apps/saas/models.py b/orchestra/apps/saas/models.py index 00c5fa8f..9f51aeb1 100644 --- a/orchestra/apps/saas/models.py +++ b/orchestra/apps/saas/models.py @@ -35,11 +35,16 @@ class SaaS(models.Model): def service_class(self): return SoftwareService.get_plugin(self.service) + @cached_property + def service_instance(self): + """ Per request lived service_instance """ + return self.service_class() + def get_site_name(self): - return self.service_class().get_site_name(self) + return self.service_instance.get_site_name(self) def clean(self): - self.data = self.service_class().clean_data(self) + self.data = self.service_instance.clean_data(self) def set_password(self, password): self.password = password diff --git a/orchestra/apps/saas/services/bscw.py b/orchestra/apps/saas/services/bscw.py index 722c22bf..877c61c4 100644 --- a/orchestra/apps/saas/services/bscw.py +++ b/orchestra/apps/saas/services/bscw.py @@ -22,7 +22,7 @@ class BSCWService(SoftwareService): verbose_name = "BSCW" form = BSCWForm serializer = BSCWDataSerializer - icon = 'saas/icons/BSCW.png' + icon = 'orchestra/icons/apps/BSCW.png' # TODO override from settings site_name = 'bascw.orchestra.lan' change_readonly_fileds = ('email',) diff --git a/orchestra/apps/saas/services/gitlab.py b/orchestra/apps/saas/services/gitlab.py index d9d2f581..250b4cff 100644 --- a/orchestra/apps/saas/services/gitlab.py +++ b/orchestra/apps/saas/services/gitlab.py @@ -3,4 +3,4 @@ from .options import SoftwareService class GitLabService(SoftwareService): verbose_name = "GitLab" - icon = 'saas/icons/gitlab.png' + icon = 'orchestra/icons/apps/gitlab.png' diff --git a/orchestra/apps/saas/services/moodle.py b/orchestra/apps/saas/services/moodle.py index 97277108..d2c69995 100644 --- a/orchestra/apps/saas/services/moodle.py +++ b/orchestra/apps/saas/services/moodle.py @@ -17,4 +17,4 @@ class MoodleService(SoftwareService): verbose_name = "Moodle" form = MoodleForm description_field = 'site_name' - icon = 'saas/icons/Moodle.png' + icon = 'orchestra/icons/apps/Moodle.png' diff --git a/orchestra/apps/saas/services/options.py b/orchestra/apps/saas/services/options.py index 9439c13b..bf6380e4 100644 --- a/orchestra/apps/saas/services/options.py +++ b/orchestra/apps/saas/services/options.py @@ -29,12 +29,6 @@ class SoftwareServiceForm(PluginDataForm): super(SoftwareServiceForm, self).__init__(*args, **kwargs) self.is_change = bool(self.instance and self.instance.pk) if self.is_change: - for field in self.plugin.change_readonly_fileds + ('username',): - value = getattr(self.instance, field, None) or self.instance.data[field] - self.fields[field].required = False - self.fields[field].widget = widgets.ReadOnlyWidget(value) - self.fields[field].help_text = None - site_name = self.instance.get_site_name() self.fields['password1'].required = False self.fields['password1'].widget = forms.HiddenInput() @@ -79,7 +73,7 @@ class SoftwareService(plugins.Plugin): site_name = None site_name_base_domain = 'orchestra.lan' icon = 'orchestra/icons/apps.png' - change_readonly_fileds = () + change_readonly_fileds = ('username',) class_verbose_name = _("Software as a Service") @classmethod @@ -98,6 +92,10 @@ class SoftwareService(plugins.Plugin): raise ValidationError(serializer.errors) return serializer.data + @classmethod + def get_change_readonly_fileds(cls): + return cls.change_readonly_fileds + ('username',) + def get_site_name(self, saas): return self.site_name or '.'.join((saas.site_name, self.site_name_base_domain)) diff --git a/orchestra/apps/saas/services/phplist.py b/orchestra/apps/saas/services/phplist.py index 3cb4a90f..346da96a 100644 --- a/orchestra/apps/saas/services/phplist.py +++ b/orchestra/apps/saas/services/phplist.py @@ -11,4 +11,4 @@ class PHPListForm(SoftwareServiceForm): class PHPListService(SoftwareService): verbose_name = "phpList" form = PHPListForm - icon = 'saas/icons/Phplist.png' + icon = 'orchestra/icons/apps/Phplist.png' diff --git a/orchestra/apps/systemusers/backends.py b/orchestra/apps/systemusers/backends.py index 158d5614..4aaee319 100644 --- a/orchestra/apps/systemusers/backends.py +++ b/orchestra/apps/systemusers/backends.py @@ -115,7 +115,7 @@ class FTPTraffic(ServiceMonitor): USERNAME="$3" LOG_FILE="$4" { - grep "UPLOAD\|DOWNLOAD" ${LOG_FILE} \\ + grep " bytes, " ${LOG_FILE} \\ | grep " \\[${USERNAME}\\] " \\ | awk -v ini="${INI_DATE}" -v end="${END_DATE}" ' BEGIN { @@ -135,7 +135,7 @@ class FTPTraffic(ServiceMonitor): } { # Fri Jul 1 13:23:17 2014 split($4, time, ":") - day = sprintf("%02d", $3) + day = sprintf("%%02d", $3) # line_date = year month day hour minute second line_date = $5 months[$2] day time[1] time[2] time[3] if ( line_date > ini && line_date < end) { diff --git a/orchestra/apps/systemusers/forms.py b/orchestra/apps/systemusers/forms.py index 78318565..c86ae3c4 100644 --- a/orchestra/apps/systemusers/forms.py +++ b/orchestra/apps/systemusers/forms.py @@ -7,6 +7,7 @@ from orchestra.forms import UserCreationForm, UserChangeForm from . import settings from .models import SystemUser + class SystemUserFormMixin(object): MOCK_USERNAME = '' diff --git a/orchestra/apps/webapps/admin.py b/orchestra/apps/webapps/admin.py index 068d5b90..78ef8806 100644 --- a/orchestra/apps/webapps/admin.py +++ b/orchestra/apps/webapps/admin.py @@ -7,8 +7,10 @@ from orchestra.admin import ExtendedModelAdmin from orchestra.admin.utils import change_url from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.forms.widgets import DynamicHelpTextSelect +from orchestra.plugins.admin import SelectPluginAdminMixin -from . import settings +from . import settings, options +from .applications import App from .models import WebApp, WebAppOption @@ -17,8 +19,7 @@ class WebAppOptionInline(admin.TabularInline): extra = 1 OPTIONS_HELP_TEXT = { - k: str(unicode(v[1])) if len(v) == 3 else '' - for k, v in settings.WEBAPPS_OPTIONS.iteritems() + op.name: str(unicode(op.help_text)) for op in options.get_enabled().values() } class Media: @@ -30,6 +31,12 @@ class WebAppOptionInline(admin.TabularInline): if db_field.name == 'value': kwargs['widget'] = forms.TextInput(attrs={'size':'100'}) if db_field.name == 'name': + if self.parent_object: + plugin = self.parent_object.type_class + else: + request = kwargs['request'] + plugin = App.get_plugin(request.GET['type']) + kwargs['choices'] = plugin.get_options_choices() # Help text based on select widget kwargs['widget'] = DynamicHelpTextSelect( 'this.id.replace("name", "value")', self.OPTIONS_HELP_TEXT @@ -37,20 +44,22 @@ class WebAppOptionInline(admin.TabularInline): return super(WebAppOptionInline, self).formfield_for_dbfield(db_field, **kwargs) -class WebAppAdmin(AccountAdminMixin, ExtendedModelAdmin): +class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin): list_display = ('name', 'type', 'display_websites', 'account_link') list_filter = ('type',) - add_fields = ('account', 'name', 'type') - fields = ('account_link', 'name', 'type') +# add_fields = ('account', 'name', 'type') +# fields = ('account_link', 'name', 'type') inlines = [WebAppOptionInline] readonly_fields = ('account_link',) change_readonly_fields = ('name', 'type') list_prefetch_related = ('content_set__website',) + plugin = App + plugin_field = 'type' + plugin_title = _("Web application type") - TYPE_HELP_TEXT = { - k: str(unicode(v.get('help_text', ''))) - for k, v in settings.WEBAPPS_TYPES.iteritems() - } +# TYPE_HELP_TEXT = { +# app.get_name(): str(unicode(app.help_text)) for app in App.get_plugins() +# } def display_websites(self, webapp): websites = [] @@ -68,14 +77,14 @@ class WebAppAdmin(AccountAdminMixin, ExtendedModelAdmin): display_websites.short_description = _("web sites") display_websites.allow_tags = True - def formfield_for_dbfield(self, db_field, **kwargs): - """ Make value input widget bigger """ - if db_field.name == 'type': - # Help text based on select widget - kwargs['widget'] = DynamicHelpTextSelect( - 'this.id.replace("name", "value")', self.TYPE_HELP_TEXT - ) - kwargs['help_text'] = self.TYPE_HELP_TEXT.get(db_field.default, '') - return super(WebAppAdmin, self).formfield_for_dbfield(db_field, **kwargs) +# def formfield_for_dbfield(self, db_field, **kwargs): +# """ Make value input widget bigger """ +# if db_field.name == 'type': +# # Help text based on select widget +# kwargs['widget'] = DynamicHelpTextSelect( +# 'this.id.replace("name", "value")', self.TYPE_HELP_TEXT +# ) +# kwargs['help_text'] = self.TYPE_HELP_TEXT.get(db_field.default, '') +# return super(WebAppAdmin, self).formfield_for_dbfield(db_field, **kwargs) admin.site.register(WebApp, WebAppAdmin) diff --git a/orchestra/apps/webapps/backends/__init__.py b/orchestra/apps/webapps/backends/__init__.py index eb8d92fb..fb6e432b 100644 --- a/orchestra/apps/webapps/backends/__init__.py +++ b/orchestra/apps/webapps/backends/__init__.py @@ -12,16 +12,8 @@ class WebAppServiceMixin(object): return settings.WEBAPPS_TYPES[webapp.type]['directive'][0] == self.directive def create_webapp_dir(self, context): - self.append(textwrap.dedent(""" - path="" - for dir in $(echo %(app_path)s | tr "/" "\n"); do - path="${path}/${dir}" - [ -d $path ] || { - mkdir "${path}" - chown %(user)s:%(group)s "${path}" - } - done - """ % context)) + self.append("mkdir -p %(app_path)s" % context) + self.append("chown %(user)s:%(group)s %(app_path)s" % context) def get_php_init_vars(self, webapp, per_account=False): """ @@ -54,7 +46,7 @@ class WebAppServiceMixin(object): return { 'user': webapp.get_username(), 'group': webapp.get_groupname(), - 'app_name': webapp.get_name(), + 'app_name': webapp.name, 'type': webapp.type, 'app_path': webapp.get_path().rstrip('/'), 'banner': self.get_banner(), diff --git a/orchestra/apps/webapps/backends/phpfcgid.py b/orchestra/apps/webapps/backends/phpfcgid.py index a811572c..65e15427 100644 --- a/orchestra/apps/webapps/backends/phpfcgid.py +++ b/orchestra/apps/webapps/backends/phpfcgid.py @@ -13,6 +13,7 @@ class PHPFcgidBackend(WebAppServiceMixin, ServiceController): """ Per-webapp fcgid application """ verbose_name = _("PHP-Fcgid") directive = 'fcgi' + default_route_match = "webapp.type.endswith('-fcgi')" def save(self, webapp): if not self.valid_directive(webapp): diff --git a/orchestra/apps/webapps/backends/phpfpm.py b/orchestra/apps/webapps/backends/phpfpm.py index 72f48f43..6841140d 100644 --- a/orchestra/apps/webapps/backends/phpfpm.py +++ b/orchestra/apps/webapps/backends/phpfpm.py @@ -14,6 +14,7 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController): """ Per-webapp php application """ verbose_name = _("PHP-FPM") directive = 'fpm' + default_route_match = "webapp.type.endswith('-fpm')" def save(self, webapp): if not self.valid_directive(webapp): @@ -26,7 +27,8 @@ class PHPFPMBackend(WebAppServiceMixin, ServiceController): } || { echo -e '%(fpm_config)s' > %(fpm_path)s UPDATEDFPM=1 - }""" % context)) + }""" % context + )) def delete(self, webapp): if not self.valid_directive(webapp): diff --git a/orchestra/apps/webapps/backends/static.py b/orchestra/apps/webapps/backends/static.py index f06c7eef..67d9a526 100644 --- a/orchestra/apps/webapps/backends/static.py +++ b/orchestra/apps/webapps/backends/static.py @@ -8,6 +8,7 @@ from . import WebAppServiceMixin class StaticBackend(WebAppServiceMixin, ServiceController): verbose_name = _("Static") directive = 'static' + default_route_match = "webapp.type == 'static'" def save(self, webapp): if not self.valid_directive(webapp): diff --git a/orchestra/apps/webapps/backends/symboliclink.py b/orchestra/apps/webapps/backends/symboliclink.py new file mode 100644 index 00000000..f97f5c3a --- /dev/null +++ b/orchestra/apps/webapps/backends/symboliclink.py @@ -0,0 +1,27 @@ +from django.utils.translation import ugettext_lazy as _ + +from orchestra.apps.orchestration import ServiceController + +from . import WebAppServiceMixin + + +class SymbolicLinkBackend(WebAppServiceMixin, ServiceController): + verbose_name = _("Symbolic link webapp") + model = 'webapps.WebApp' + default_route_match = "webapp.type == 'symbolic-link'" + + def save(self, webapp): + context = self.get_context(webapp) + self.append("ln -s '%(link_path)s' %(app_path)s" % context) + self.append("chown -h %(user)s:%(group)s %(app_path)s" % context) + + def delete(self, webapp): + context = self.get_context(webapp) + self.delete_webapp_dir(context) + + def get_context(self, webapp): + context = super(SymbolicLinkBackend, self).get_context(webapp) + context.update({ + 'link_path': webapp.data['path'], + }) + return context diff --git a/orchestra/apps/webapps/backends/wordpress.py b/orchestra/apps/webapps/backends/wordpress.py new file mode 100644 index 00000000..e9c4ab24 --- /dev/null +++ b/orchestra/apps/webapps/backends/wordpress.py @@ -0,0 +1,48 @@ +import textwrap + +from django.utils.translation import ugettext_lazy as _ + +from orchestra.apps.orchestration import ServiceController + +from .. import settings + +from . import WebAppServiceMixin + + +class WordPressBackend(WebAppServiceMixin, ServiceController): + verbose_name = _("Wordpress") + model = 'webapps.WebApp' + default_route_match = "webapp.type == 'wordpress'" + + def save(self, webapp): + context = self.get_context(webapp) + self.create_webapp_dir(context) + self.append(textwrap.dedent("""\ + # Check if directory is empty befor doing anything + if [[ ! $(ls -A %(app_path)s) ]]; then + wget http://wordpress.org/latest.tar.gz -O - --no-check-certificate \\ + | tar -xzvf - -C %(app_path)s --strip-components=1 + cp %(app_path)s/wp-config-sample.php %(app_path)s/wp-config.php + sed -i "s/database_name_here/%(db_name)s/" %(app_path)s/wp-config.php + sed -i "s/username_here/%(db_user)s/" %(app_path)s/wp-config.php + sed -i "s/password_here/%(db_pass)s/" %(app_path)s/wp-config.php + sed -i "s/localhost/%(db_host)s/" %(app_path)s/wp-config.php + mkdir %(app_path)s/wp-content/uploads + chmod 750 %(app_path)s/wp-content/uploads + chown -R %(user)s:%(group)s %(app_path)s + fi""" % context + )) + + def delete(self, webapp): + context = self.get_context(webapp) + self.delete_webapp_dir(context) + + def get_context(self, webapp): + context = super(WordPressBackend, self).get_context(webapp) + context.update({ + 'db_name': webapp.data['db_name'], + 'db_user': webapp.data['db_user'], + 'db_pass': webapp.data['db_pass'], + 'db_host': settings.WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST, + }) + return context diff --git a/orchestra/apps/webapps/backends/wordpressmu.py b/orchestra/apps/webapps/backends/wordpressmu.py index 87a5786c..690d1b38 100644 --- a/orchestra/apps/webapps/backends/wordpressmu.py +++ b/orchestra/apps/webapps/backends/wordpressmu.py @@ -11,6 +11,7 @@ from .. import settings class WordpressMuBackend(ServiceController): verbose_name = _("Wordpress multisite") model = 'webapps.WebApp' + default_route_match = "webapp.type == 'wordpress-mu'" @property def script(self): @@ -85,7 +86,7 @@ class WordpressMuBackend(ServiceController): self.validate_response(response) def delete_blog(self, webapp, server): - # OH, I've enjoied so much coding this methods that I want to thanks + # OH, I've enjoied so much coding this methods that I want to thank # the wordpress team for the excellent software they are producing session = requests.Session() self.login(session) diff --git a/orchestra/apps/webapps/migrations/0001_initial.py b/orchestra/apps/webapps/migrations/0001_initial.py new file mode 100644 index 00000000..c6c501b0 --- /dev/null +++ b/orchestra/apps/webapps/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import orchestra.core.validators +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='WebApp', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=128, verbose_name='name', validators=[orchestra.core.validators.validate_name])), + ('type', models.CharField(max_length=32, verbose_name='type', choices=[(b'dokuwiki-mu', b'DokuWiki (SaaS)'), (b'drupal-mu', b'Drupdal (SaaS)'), (b'php4-fcgi', b'PHP 4 FCGI'), (b'php5.2-fcgi', b'PHP 5.2 FCGI'), (b'php5.5-fpm', b'PHP 5.5 FPM'), (b'static', b'Static'), (b'symlink', b'Symbolic link'), (b'webalizer', b'Webalizer'), (b'wordpress', b'WordPress'), (b'wordpress-mu', b'WordPress (SaaS)')])), + ('account', models.ForeignKey(related_name='webapps', verbose_name='Account', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Web App', + 'verbose_name_plural': 'Web Apps', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='WebAppOption', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=128, verbose_name='name', choices=[(b'PHP-allow_url_fopen', 'PHP - allow_url_fopen'), (b'PHP-allow_url_include', 'PHP - Allow URL include'), (b'PHP-auto_append_file', 'PHP - Auto append file'), (b'PHP-auto_prepend_file', 'PHP - Auto prepend file'), (b'PHP-date.timezone', 'PHP - date.timezone'), (b'PHP-default_socket_timeout', 'PHP - Default socket timeout'), (b'PHP-display_errors', 'PHP - Display errors'), (b'PHP-extension', 'PHP - Extension'), (b'PHP-magic_quotes_gpc', 'PHP - Magic quotes GPC'), (b'PHP-magic_quotes_runtime', 'PHP - Magic quotes runtime'), (b'PHP-magic_quotes_sybase', 'PHP - Magic quotes sybase'), (b'PHP-max_execution_time', 'PHP - Max execution time'), (b'PHP-max_input_time', 'PHP - Max input time'), (b'PHP-max_input_vars', 'PHP - Max input vars'), (b'PHP-memory_limit', 'PHP - Memory limit'), (b'PHP-mysql.connect_timeout', 'PHP - Mysql connect timeout'), (b'PHP-output_buffering', 'PHP - output_buffering'), (b'PHP-post_max_size', 'PHP - Post max size'), (b'PHP-register_globals', 'PHP - Register globals'), (b'PHP-safe_mode', 'PHP - Safe mode'), (b'PHP-sendmail_path', 'PHP - sendmail_path'), (b'PHP-session.auto_start', 'PHP - session.auto_start'), (b'PHP-session.bug_compat_warn', 'PHP - session.bug_compat_warn'), (b'PHP-suhosin.executor.include.whitelist', 'PHP - suhosin.executor.include.whitelist'), (b'PHP-suhosin.get.max_vars', 'PHP - Suhosin GET max vars'), (b'PHP-suhosin.post.max_vars', 'PHP - Suhosin POST max vars'), (b'PHP-suhosin.request.max_vars', 'PHP - Suhosin request max vars'), (b'PHP-suhosin.session.encrypt', 'PHP - suhosin.session.encrypt'), (b'PHP-suhosin.simulation', 'PHP - Suhosin simulation'), (b'PHP-upload_max_filesize', 'PHP - upload_max_filesize'), (b'PHP-zend_extension', 'PHP - zend_extension'), (b'php-enabled_functions', 'PHP - Enabled functions'), (b'processes', 'Number of processes'), (b'public-root', 'Public root'), (b'timeout', 'Process timeout')])), + ('value', models.CharField(max_length=256, verbose_name='value')), + ('webapp', models.ForeignKey(related_name='options', verbose_name='Web application', to='webapps.WebApp')), + ], + options={ + 'verbose_name': 'option', + 'verbose_name_plural': 'options', + }, + bases=(models.Model,), + ), + migrations.AlterUniqueTogether( + name='webappoption', + unique_together=set([('webapp', 'name')]), + ), + migrations.AlterUniqueTogether( + name='webapp', + unique_together=set([('name', 'account')]), + ), + ] diff --git a/orchestra/apps/webapps/migrations/0002_webapp_data.py b/orchestra/apps/webapps/migrations/0002_webapp_data.py new file mode 100644 index 00000000..868b478d --- /dev/null +++ b/orchestra/apps/webapps/migrations/0002_webapp_data.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapps', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='webapp', + name='data', + field=jsonfield.fields.JSONField(default={}, help_text='Extra information dependent of each service.', verbose_name='data'), + preserve_default=False, + ), + ] diff --git a/orchestra/apps/webapps/migrations/__init__.py b/orchestra/apps/webapps/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/webapps/models.py b/orchestra/apps/webapps/models.py index b24063d4..6c0dd08b 100644 --- a/orchestra/apps/webapps/models.py +++ b/orchestra/apps/webapps/models.py @@ -2,23 +2,25 @@ import re from django.core.exceptions import ValidationError from django.db import models +from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ +from jsonfield import JSONField from orchestra.core import validators, services -from orchestra.utils import tuple_setting_to_choices, dict_setting_to_choices from orchestra.utils.functional import cached -from . import settings +from . import settings, options +from .types import AppType class WebApp(models.Model): """ Represents a web application """ name = models.CharField(_("name"), max_length=128, validators=[validators.validate_name]) type = models.CharField(_("type"), max_length=32, - choices=dict_setting_to_choices(settings.WEBAPPS_TYPES), - default=settings.WEBAPPS_DEFAULT_TYPE) + choices=AppType.get_plugin_choices()) account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), related_name='webapps') + data = JSONField(_("data"), help_text=_("Extra information dependent of each service.")) class Meta: unique_together = ('name', 'account') @@ -31,21 +33,25 @@ class WebApp(models.Model): def get_description(self): return self.get_type_display() + @cached_property + def type_class(self): + return AppType.get_plugin(self.type) + + @cached_property + def type_instance(self): + """ Per request lived type_instance """ + return self.type_class() + 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."), - }) + apptype = self.type_instance + apptype.validate(self) + self.data = apptype.clean_data(self) @cached 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): @@ -81,7 +87,7 @@ class WebAppOption(models.Model): webapp = models.ForeignKey(WebApp, verbose_name=_("Web application"), related_name='options') name = models.CharField(_("name"), max_length=128, - choices=tuple_setting_to_choices(settings.WEBAPPS_OPTIONS)) + choices=((op.name, op.verbose_name) for op in options.get_enabled().values())) value = models.CharField(_("value"), max_length=256) class Meta: @@ -93,16 +99,24 @@ class WebAppOption(models.Model): return self.name def clean(self): - """ validates name and value according to WEBAPPS_OPTIONS """ - regex = settings.WEBAPPS_OPTIONS[self.name][-1] - if not re.match(regex, self.value): - raise ValidationError({ - 'value': ValidationError(_("'%(value)s' does not match %(regex)s."), - params={ - 'value': self.value, - 'regex': regex - }), - }) + option = options.get_enabled()[self.name] + option.validate(self) services.register(WebApp) + + +# Admin bulk deletion doesn't call model.delete(), we use signals instead of model method overriding + +from django.db.models.signals import pre_save, pre_delete +from django.dispatch import receiver + +@receiver(pre_save, sender=WebApp, dispatch_uid='webapps.type.save') +def type_save(sender, *args, **kwargs): + instance = kwargs['instance'] + instance.type_instance.save(instance) + +@receiver(pre_delete, sender=WebApp, dispatch_uid='webapps.type.delete') +def type_delete(sender, *args, **kwargs): + instance = kwargs['instance'] + instance.type_instance.delete(instance) diff --git a/orchestra/apps/webapps/options.py b/orchestra/apps/webapps/options.py new file mode 100644 index 00000000..b7d89d91 --- /dev/null +++ b/orchestra/apps/webapps/options.py @@ -0,0 +1,300 @@ +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ + +from orchestra.utils.python import import_class + +from . import settings + + +class AppOption(object): + def __init__(self, name, *args, **kwargs): + self.name = name + self.verbose_name = kwargs.pop('verbose_name', name) + self.help_text = kwargs.pop('help_text', '') + for k,v in kwargs.iteritems(): + setattr(self, k, v) + + def validate(self, webapp): + if self.regex and not re.match(self.regex, webapp.value): + raise ValidationError({ + 'value': ValidationError(_("'%(value)s' does not match %(regex)s."), + params={ + 'value': webapp.value, + 'regex': self.regex + }), + }) + + +public_root = AppOption('public-root', + verbose_name=_("Public root"), + help_text=_("Document root relative to webapps/<webapp>/"), + regex=r'[^ ]+' +) + +timeout = AppOption('timeout', + # FCGID FcgidIOTimeout + # FPM pm.request_terminate_timeout + # PHP max_execution_time ini + verbose_name=_("Process timeout"), + help_text=_("Maximum time in seconds allowed for a request to complete (a number between 0 and 999)."), + regex=r'^[0-9]{1,3}$', +) + +processes = AppOption('processes', + # FCGID MaxProcesses + # FPM pm.max_children + verbose_name=_("Number of processes"), + help_text=_("Maximum number of children that can be alive at the same time (a number between 0 and 9)."), + regex=r'^[0-9]$', +) + +php_enabled_functions = AppOption('php-enabled_functions', + verbose_name=_("Enabled functions"), + help_text = ' '.join(settings.WEBAPPS_PHP_DISABLED_FUNCTIONS), + regex=r'^[\w\.,-]+$' +) + +php_allow_url_include = AppOption('PHP-allow_url_include', + verbose_name=_("Allow URL include"), + help_text=_("Allows the use of URL-aware fopen wrappers with include, include_once, require, " + "require_once (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_allow_url_fopen = AppOption('PHP-allow_url_fopen', + verbose_name=_("Allow URL fopen"), + help_text=_("Enables the URL-aware fopen wrappers that enable accessing URL object like files (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_auto_append_file = AppOption('PHP-auto_append_file', + verbose_name=_("Auto append file"), + help_text=_("Specifies the name of a file that is automatically parsed after the main file."), + regex=r'^[\w\.,-/]+$' +) + +php_auto_prepend_file = AppOption('PHP-auto_prepend_file', + verbose_name=_("Auto prepend file"), + help_text=_("Specifies the name of a file that is automatically parsed before the main file."), + regex=r'^[\w\.,-/]+$' +) + +php_date_timezone = AppOption('PHP-date.timezone', + verbose_name=_("date.timezone"), + help_text=_("Sets the default timezone used by all date/time functions (Timezone string 'Europe/London')."), + regex=r'^\w+/\w+$' +) + +php_default_socket_timeout = AppOption('PHP-default_socket_timeout', + verbose_name=_("Default socket timeout"), + help_text=_("Number between 0 and 999."), + regex=r'^[0-9]{1,3}$' +) + +php_display_errors = AppOption('PHP-display_errors', + verbose_name=_("Display errors"), + help_text=_("Determines whether errors should be printed to the screen as part of the output or " + "if they should be hidden from the user (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_extension = AppOption('PHP-extension', + verbose_name=_("Extension"), + regex=r'^[^ ]+$' +) + +php_magic_quotes_gpc = AppOption('PHP-magic_quotes_gpc', + verbose_name=_("Magic quotes GPC"), + help_text=_("Sets the magic_quotes state for GPC (Get/Post/Cookie) operations (On or Off) " + "DEPRECATED as of PHP 5.3.0."), + regex=r'^(On|Off|on|off)$', + deprecated=5.3 +) + +php_magic_quotes_runtime = AppOption('PHP-magic_quotes_runtime', + verbose_name=_("Magic quotes runtime"), + help_text=_("Functions that return data from any sort of external source will have quotes escaped " + "with a backslash (On or Off) DEPRECATED as of PHP 5.3.0."), + regex=r'^(On|Off|on|off)$', + deprecated=5.3 +) + +php_magic_quotes_sybase = AppOption('PHP-magic_quotes_sybase', + verbose_name=_("Magic quotes sybase"), + help_text=_("Single-quote is escaped with a single-quote instead of a backslash (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_max_execution_time = AppOption('PHP-max_execution_time', + verbose_name=_("Max execution time"), + help_text=_("Maximum time in seconds a script is allowed to run before it is terminated by " + "the parser (Integer between 0 and 999)."), + regex=r'^[0-9]{1,3}$' +) + +php_max_input_time = AppOption('PHP-max_input_time', + verbose_name=_("Max input time"), + help_text=_("Maximum time in seconds a script is allowed to parse input data, like POST and GET " + "(Integer between 0 and 999)."), + regex=r'^[0-9]{1,3}$' +) + +php_max_input_vars = AppOption('PHP-max_input_vars', + verbose_name=_("Max input vars"), + help_text=_("How many input variables may be accepted (limit is applied to $_GET, $_POST " + "and $_COOKIE superglobal separately) (Integer between 0 and 9999)."), + regex=r'^[0-9]{1,4}$' +) + +php_memory_limit = AppOption('PHP-memory_limit', + verbose_name=_("Memory limit"), + help_text=_("This sets the maximum amount of memory in bytes that a script is allowed to allocate " + "(Value between 0M and 999M)."), + regex=r'^[0-9]{1,3}M$' +) + +php_mysql_connect_timeout = AppOption('PHP-mysql.connect_timeout', + verbose_name=_("Mysql connect timeout"), + help_text=_("Number between 0 and 999."), + regex=r'^([0-9]){1,3}$' +) + +php_output_buffering = AppOption('PHP-output_buffering', + verbose_name=_("Output buffering"), + help_text=_("Turn on output buffering (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_register_globals = AppOption('PHP-register_globals', + verbose_name=_("Register globals"), + help_text=_("Whether or not to register the EGPCS (Environment, GET, POST, Cookie, Server) " + "variables as global variables (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_post_max_size = AppOption('PHP-post_max_size', + verbose_name=_("Post max size"), + help_text=_("Sets max size of post data allowed (Value between 0M and 999M)."), + regex=r'^[0-9]{1,3}M$' +) + +php_sendmail_path = AppOption('PHP-sendmail_path', + verbose_name=_("sendmail_path"), + help_text=_("Where the sendmail program can be found."), + regex=r'^[^ ]+$' +) + +php_session_bug_compat_warn = AppOption('PHP-session.bug_compat_warn', + verbose_name=_("session.bug_compat_warn"), + help_text=_("Enables an PHP bug on session initialization for legacy behaviour (On or Off)."), + regex=r'^(On|Off|on|off)$' +) + +php_session_auto_start = AppOption('PHP-session.auto_start', + verbose_name=_("session.auto_start"), + help_text=_("Specifies whether the session module starts a session automatically on request " + "startup (On or Off)."), + regex=r'^(On|Off|on|off)$' +) +php_safe_mode = AppOption('PHP-safe_mode', + verbose_name=_("Safe mode"), + help_text=_("Whether to enable PHP's safe mode (On or Off) DEPRECATED as of PHP 5.3.0"), + regex=r'^(On|Off|on|off)$', + deprecated=5.3 +) +php_suhosin_post_max_vars = AppOption('PHP-suhosin.post.max_vars', + verbose_name=_("Suhosin POST max vars"), + help_text=_("Number between 0 and 9999."), + regex=r'^[0-9]{1,4}$' +) +php_suhosin_get_max_vars = AppOption('PHP-suhosin.get.max_vars', + verbose_name=_("Suhosin GET max vars"), + help_text=_("Number between 0 and 9999."), + regex=r'^[0-9]{1,4}$' +) +php_suhosin_request_max_vars = AppOption('PHP-suhosin.request.max_vars', + verbose_name=_("Suhosin request max vars"), + help_text=_("Number between 0 and 9999."), + regex=r'^[0-9]{1,4}$' +) +php_suhosin_session_encrypt = AppOption('PHP-suhosin.session.encrypt', + verbose_name=_("suhosin.session.encrypt"), + help_text=_("On or Off"), + regex=r'^(On|Off|on|off)$' +) +php_suhosin_simulation = AppOption('PHP-suhosin.simulation', + verbose_name=_("Suhosin simulation"), + help_text=_("On or Off"), + regex=r'^(On|Off|on|off)$' +) +php_suhosin_executor_include_whitelist = AppOption('PHP-suhosin.executor.include.whitelist', + verbose_name=_("suhosin.executor.include.whitelist"), + regex=r'.*$' +) +php_upload_max_filesize = AppOption('PHP-upload_max_filesize', + verbose_name=_("upload_max_filesize"), + help_text=_("Value between 0M and 999M."), + regex=r'^[0-9]{1,3}M$' +) +php_zend_extension = AppOption('PHP-post_max_size', + verbose_name=_("zend_extension"), + regex=r'^[^ ]+$' +) + + +filesystem = [ + public_root, +] + +process = [ + timeout, + processes, +] + +php = [ + php_enabled_functions, + php_allow_url_include, + php_allow_url_fopen, + php_auto_append_file, + php_auto_prepend_file, + php_date_timezone, + php_default_socket_timeout, + php_display_errors, + php_extension, + php_magic_quotes_gpc, + php_magic_quotes_runtime, + php_magic_quotes_sybase, + php_max_execution_time, + php_max_input_time, + php_max_input_vars, + php_memory_limit, + php_mysql_connect_timeout, + php_output_buffering, + php_register_globals, + php_post_max_size, + php_sendmail_path, + php_session_bug_compat_warn, + php_session_auto_start, + php_safe_mode, + php_suhosin_post_max_vars, + php_suhosin_get_max_vars, + php_suhosin_request_max_vars, + php_suhosin_session_encrypt, + php_suhosin_simulation, + php_suhosin_executor_include_whitelist, + php_upload_max_filesize, + php_zend_extension, +] + + +_enabled = None + +def get_enabled(): + global _enabled + if _enabled is None: + from . import settings + _enabled = {} + for op in settings.WEBAPPS_ENABLED_OPTIONS: + op = import_class(op) + _enabled[op.name] = op + return _enabled diff --git a/orchestra/apps/webapps/settings.py b/orchestra/apps/webapps/settings.py index 8aa03469..bfb109aa 100644 --- a/orchestra/apps/webapps/settings.py +++ b/orchestra/apps/webapps/settings.py @@ -21,67 +21,26 @@ WEBAPPS_FCGID_PATH = getattr(settings, 'WEBAPPS_FCGID_PATH', '/home/httpd/fcgid/%(user)s/%(app_name)s-wrapper') -WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', { - 'php5.5': { - 'verbose_name': "PHP 5.5 fpm", -# 'fpm', ('unix:/var/run/%(user)s-%(app_name)s.sock|fcgi://127.0.0.1%(app_path)s',), - 'directive': ('fpm', 'fcgi://{}%(app_path)s'.format(WEBAPPS_FPM_LISTEN)), - 'help_text': _("This creates a PHP5.5 application under ~/webapps/<app_name>
" - "PHP-FPM will be used to execute PHP files.") - }, - 'php5.2': { - 'verbose_name': "PHP 5.2 fcgi", - 'directive': ('fcgi', WEBAPPS_FCGID_PATH), - 'help_text': _("This creates a PHP5.2 application under ~/webapps/<app_name>
" - "Apache-mod-fcgid will be used to execute PHP files.") - }, - 'php4': { - 'verbose_name': "PHP 4 fcgi", - 'directive': ('fcgi', WEBAPPS_FCGID_PATH,), - 'help_text': _("This creates a PHP4 application under ~/webapps/<app_name>
" - "Apache-mod-fcgid will be used to execute PHP files.") - }, - 'static': { - 'verbose_name': _("Static"), - 'directive': ('static',), - 'help_text': _("This creates a Static application under ~/webapps/<app_name>
" - "Apache2 will be used to serve static content and execute CGI files.") - }, - 'webalizer': { - 'verbose_name': "Webalizer", - 'directive': ('static', '%(app_path)s%(site_name)s'), - 'help_text': _("This creates a Webalizer application under " - "~/webapps/<app_name>-<site_name>") - }, - 'wordpress-mu': { - 'verbose_name': _("Wordpress (SaaS)"), - 'directive': ('fpm', 'fcgi://127.0.0.1:8990/home/httpd/wordpress-mu/'), - 'help_text': _("This creates a Wordpress site on a multi-tenant Wordpress server.
" - "By default this blog is accessible via <app_name>.blogs.orchestra.lan") - }, - 'dokuwiki-mu': { - 'verbose_name': _("DokuWiki (SaaS)"), - 'directive': ('alias', '/home/httpd/wikifarm/farm/'), - 'help_text': _("This create a Dokuwiki wiki into a shared Dokuwiki server.
" - "By default this wiki is accessible via <app_name>.wikis.orchestra.lan") - }, - 'drupal-mu': { - 'verbose_name': _("Drupdal (SaaS)"), - 'directive': ('fpm', 'fcgi://127.0.0.1:8991/home/httpd/drupal-mu/'), - 'help_text': _("This creates a Drupal site into a multi-tenant Drupal server.
" - "The installation will be completed after visiting " - "http://<app_name>.drupal.orchestra.lan/install.php?profile=standard
" - "By default this site will be accessible via <app_name>.drupal.orchestra.lan") - } -}) +WEBAPPS_TYPES = getattr(settings, 'WEBAPPS_TYPES', ( + 'orchestra.apps.webapps.types.Php55App', + 'orchestra.apps.webapps.types.Php52App', + 'orchestra.apps.webapps.types.Php4App', + 'orchestra.apps.webapps.types.StaticApp', + 'orchestra.apps.webapps.types.WebalizerApp', + 'orchestra.apps.webapps.types.WordPressMuApp', + 'orchestra.apps.webapps.types.DokuWikiMuApp', + 'orchestra.apps.webapps.types.DrupalMuApp', + 'orchestra.apps.webapps.types.SymbolicLinkApp', + 'orchestra.apps.webapps.types.WordPressApp', +)) -WEBAPPS_TYPES_OVERRIDE = getattr(settings, 'WEBAPPS_TYPES_OVERRIDE', {}) -for webapp_type, value in WEBAPPS_TYPES_OVERRIDE.iteritems(): - if value is None: - WEBAPPS_TYPES.pop(webapp_type, None) - else: - WEBAPPS_TYPES[webapp_type] = value +#WEBAPPS_TYPES_OVERRIDE = getattr(settings, 'WEBAPPS_TYPES_OVERRIDE', {}) +#for webapp_type, value in WEBAPPS_TYPES_OVERRIDE.iteritems(): +# if value is None: +# WEBAPPS_TYPES.pop(webapp_type, None) +# else: +# WEBAPPS_TYPES[webapp_type] = value WEBAPPS_DEFAULT_TYPE = getattr(settings, 'WEBAPPS_DEFAULT_TYPE', 'php5.5') @@ -116,204 +75,43 @@ WEBAPPS_PHP_DISABLED_FUNCTIONS = getattr(settings, 'WEBAPPS_PHP_DISABLED_FUNCTIO ]) -WEBAPPS_OPTIONS = getattr(settings, 'WEBAPPS_OPTIONS', { - # { name: ( verbose_name, [help_text], validation_regex ) } - # Filesystem - 'public-root': ( - _("Public root"), - _("Document root relative to webapps/<webapp>/"), - r'[^ ]+', - ), - # Processes - 'timeout': ( - _("Process timeout"), - _("Maximum time in seconds allowed for a request to complete " - "(a number between 0 and 999)."), - # FCGID FcgidIOTimeout - # FPM pm.request_terminate_timeout - # PHP max_execution_time ini - r'^[0-9]{1,3}$', - ), - 'processes': ( - _("Number of processes"), - _("Maximum number of children that can be alive at the same time " - "(a number between 0 and 9)."), - # FCGID MaxProcesses - # FPM pm.max_children - r'^[0-9]$', - ), - # PHP - 'php-enabled_functions': ( - _("PHP - Enabled functions"), - ' '.join(WEBAPPS_PHP_DISABLED_FUNCTIONS), - r'^[\w\.,-]+$' - ), - 'PHP-allow_url_include': ( - _("PHP - Allow URL include"), - _("Allows the use of URL-aware fopen wrappers with include, include_once, require, " - "require_once (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-allow_url_fopen': ( - _("PHP - allow_url_fopen"), - _("Enables the URL-aware fopen wrappers that enable accessing URL object like files " - "(On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-auto_append_file': ( - _("PHP - Auto append file"), - _("Specifies the name of a file that is automatically parsed after the main file."), - r'^[\w\.,-/]+$' - ), - 'PHP-auto_prepend_file': ( - _("PHP - Auto prepend file"), - _("Specifies the name of a file that is automatically parsed before the main file."), - r'^[\w\.,-/]+$' - ), - 'PHP-date.timezone': ( - _("PHP - date.timezone"), - _("Sets the default timezone used by all date/time functions " - "(Timezone string 'Europe/London')."), - r'^\w+/\w+$' - ), - 'PHP-default_socket_timeout': ( - _("PHP - Default socket timeout"), - _("Number between 0 and 999."), - r'^[0-9]{1,3}$' - ), - 'PHP-display_errors': ( - _("PHP - Display errors"), - _("determines whether errors should be printed to the screen as part of the output or " - "if they should be hidden from the user (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-extension': ( - _("PHP - Extension"), - r'^[^ ]+$' - ), - 'PHP-magic_quotes_gpc': ( - _("PHP - Magic quotes GPC"), - _("Sets the magic_quotes state for GPC (Get/Post/Cookie) operations (On or Off) " - "DEPRECATED as of PHP 5.3.0."), - r'^(On|Off|on|off)$' - ), - 'PHP-magic_quotes_runtime': ( - _("PHP - Magic quotes runtime"), - _("Functions that return data from any sort of external source will have quotes escaped " - "with a backslash (On or Off) DEPRECATED as of PHP 5.3.0."), - r'^(On|Off|on|off)$' - ), - 'PHP-magic_quotes_sybase': ( - _("PHP - Magic quotes sybase"), - _("Single-quote is escaped with a single-quote instead of a backslash (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-max_execution_time': ( - _("PHP - Max execution time"), - _("Maximum time in seconds a script is allowed to run before it is terminated by " - "the parser (Integer between 0 and 999)."), - r'^[0-9]{1,3}$' - ), - 'PHP-max_input_time': ( - _("PHP - Max input time"), - _("Maximum time in seconds a script is allowed to parse input data, like POST and GET " - "(Integer between 0 and 999)."), - r'^[0-9]{1,3}$' - ), - 'PHP-max_input_vars': ( - _("PHP - Max input vars"), - _("How many input variables may be accepted (limit is applied to $_GET, $_POST and $_COOKIE superglobal separately) " - "(Integer between 0 and 9999)."), - r'^[0-9]{1,4}$' - ), - 'PHP-memory_limit': ( - _("PHP - Memory limit"), - _("This sets the maximum amount of memory in bytes that a script is allowed to allocate " - "(Value between 0M and 999M)."), - r'^[0-9]{1,3}M$' - ), - 'PHP-mysql.connect_timeout': ( - _("PHP - Mysql connect timeout"), - _("Number between 0 and 999."), - r'^([0-9]){1,3}$' - ), - 'PHP-output_buffering': ( - _("PHP - output_buffering"), - _("Turn on output buffering (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-register_globals': ( - _("PHP - Register globals"), - _("Whether or not to register the EGPCS (Environment, GET, POST, Cookie, Server) " - "variables as global variables (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-post_max_size': ( - _("PHP - Post max size"), - _("Sets max size of post data allowed (Value between 0M and 999M)."), - r'^[0-9]{1,3}M$' - ), - 'PHP-sendmail_path': ( - _("PHP - sendmail_path"), - _("Where the sendmail program can be found."), - r'^[^ ]+$' - ), - 'PHP-session.bug_compat_warn': ( - _("PHP - session.bug_compat_warn"), - _("Enables an PHP bug on session initialization for legacy behaviour (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-session.auto_start': ( - _("PHP - session.auto_start"), - _("Specifies whether the session module starts a session automatically on request " - "startup (On or Off)."), - r'^(On|Off|on|off)$' - ), - 'PHP-safe_mode': ( - _("PHP - Safe mode"), - _("Whether to enable PHP's safe mode (On or Off) DEPRECATED as of PHP 5.3.0"), - r'^(On|Off|on|off)$' - ), - 'PHP-suhosin.post.max_vars': ( - _("PHP - Suhosin POST max vars"), - _("Number between 0 and 9999."), - r'^[0-9]{1,4}$' - ), - 'PHP-suhosin.get.max_vars': ( - _("PHP - Suhosin GET max vars"), - _("Number between 0 and 9999."), - r'^[0-9]{1,4}$' - ), - 'PHP-suhosin.request.max_vars': ( - _("PHP - Suhosin request max vars"), - _("Number between 0 and 9999."), - r'^[0-9]{1,4}$' - ), - 'PHP-suhosin.session.encrypt': ( - _("PHP - suhosin.session.encrypt"), - _("On or Off"), - r'^(On|Off|on|off)$' - ), - 'PHP-suhosin.simulation': ( - _("PHP - Suhosin simulation"), - _("On or Off"), - r'^(On|Off|on|off)$' - ), - 'PHP-suhosin.executor.include.whitelist': ( - _("PHP - suhosin.executor.include.whitelist"), - r'.*$' - ), - 'PHP-upload_max_filesize': ( - _("PHP - upload_max_filesize"), - _("Value between 0M and 999M."), - r'^[0-9]{1,3}M$' - ), - 'PHP-zend_extension': ( - _("PHP - zend_extension"), - r'^[^ ]+$' - ), -}) - +WEBAPPS_ENABLED_OPTIONS = getattr(settings, 'WEBAPPS_ENABLED_OPTIONS', ( + 'orchestra.apps.webapps.options.public_root', + 'orchestra.apps.webapps.options.timeout', + 'orchestra.apps.webapps.options.processes', + 'orchestra.apps.webapps.options.php_enabled_functions', + 'orchestra.apps.webapps.options.php_allow_url_include', + 'orchestra.apps.webapps.options.php_allow_url_fopen', + 'orchestra.apps.webapps.options.php_auto_append_file', + 'orchestra.apps.webapps.options.php_auto_prepend_file', + 'orchestra.apps.webapps.options.php_date_timezone', + 'orchestra.apps.webapps.options.php_default_socket_timeout', + 'orchestra.apps.webapps.options.php_display_errors', + 'orchestra.apps.webapps.options.php_extension', + 'orchestra.apps.webapps.options.php_magic_quotes_gpc', + 'orchestra.apps.webapps.options.php_magic_quotes_runtime', + 'orchestra.apps.webapps.options.php_magic_quotes_sybase', + 'orchestra.apps.webapps.options.php_max_execution_time', + 'orchestra.apps.webapps.options.php_max_input_time', + 'orchestra.apps.webapps.options.php_max_input_vars', + 'orchestra.apps.webapps.options.php_memory_limit', + 'orchestra.apps.webapps.options.php_mysql_connect_timeout', + 'orchestra.apps.webapps.options.php_output_buffering', + 'orchestra.apps.webapps.options.php_register_globals', + 'orchestra.apps.webapps.options.php_post_max_size', + 'orchestra.apps.webapps.options.php_sendmail_path', + 'orchestra.apps.webapps.options.php_session_bug_compat_warn', + 'orchestra.apps.webapps.options.php_session_auto_start', + 'orchestra.apps.webapps.options.php_safe_mode', + 'orchestra.apps.webapps.options.php_suhosin_post_max_vars', + 'orchestra.apps.webapps.options.php_suhosin_get_max_vars', + 'orchestra.apps.webapps.options.php_suhosin_request_max_vars', + 'orchestra.apps.webapps.options.php_suhosin_session_encrypt', + 'orchestra.apps.webapps.options.php_suhosin_simulation', + 'orchestra.apps.webapps.options.php_suhosin_executor_include_whitelist', + 'orchestra.apps.webapps.options.php_upload_max_filesize', + 'orchestra.apps.webapps.options.php_zend_extension', +)) WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD = getattr(settings, 'WEBAPPS_WORDPRESSMU_ADMIN_PASSWORD', @@ -332,3 +130,7 @@ WEBAPPS_DOKUWIKIMU_FARM_PATH = getattr(settings, 'WEBAPPS_DOKUWIKIMU_FARM_PATH', WEBAPPS_DRUPAL_SITES_PATH = getattr(settings, 'WEBAPPS_DRUPAL_SITES_PATH', '/home/httpd/htdocs/drupal-mu/sites/%(site_name)s') + + +WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST = getattr(settings, 'WEBAPPS_DEFAULT_MYSQL_DATABASE_HOST', + 'mysql.orchestra.lan') diff --git a/orchestra/apps/webapps/types.py b/orchestra/apps/webapps/types.py new file mode 100644 index 00000000..87815836 --- /dev/null +++ b/orchestra/apps/webapps/types.py @@ -0,0 +1,309 @@ +from django import forms +from django.core.exceptions import ValidationError +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ + +from orchestra import plugins +from orchestra.plugins.forms import PluginDataForm +from orchestra.core import validators +from orchestra.forms import widgets +from orchestra.utils.functional import cached +from orchestra.utils.python import import_class + +from . import options, settings + + +class AppType(plugins.Plugin): + name = None + verbose_name = "" + help_text= "" + form = PluginDataForm + change_form = None + serializer = None + icon = 'orchestra/icons/apps.png' + unique_name = False + options = ( + ('Process', options.process), + ('PHP', options.php), + ('File system', options.filesystem), + ) + + @classmethod + @cached + def get_plugins(cls): + plugins = [] + for cls in settings.WEBAPPS_TYPES: + plugins.append(import_class(cls)) + return plugins + + @classmethod + def clean_data(cls, webapp): + """ model clean, uses cls.serizlier by default """ + if cls.serializer: + serializer = cls.serializer(data=webapp.data) + if not serializer.is_valid(): + raise ValidationError(serializer.errors) + return serializer.data + return {} + + def get_form(self): + self.form.plugin = self + self.form.plugin_field = 'type' + return self.form + + def get_change_form(self): + form = self.change_form or self.form + form.plugin = self + form.plugin_field = 'type' + return form + + def get_serializer(self): + self.serializer.plugin = self + return self.serializer + + def validate(self, instance): + """ Unique name validation """ + if self.unique_name: + if not instance.pk and Webapp.objects.filter(name=instance.name, type=instance.type).exists(): + raise ValidationError({ + 'name': _("A WordPress blog with this name already exists."), + }) + + def get_options(self): + pass + + @classmethod + def get_options_choices(cls): + enabled = options.get_enabled().values() + yield (None, '-------') + for option in cls.options: + if hasattr(option, '__iter__'): + yield (option[0], [(op.name, op.verbose_name) for op in option[1] if op in enabled]) + elif option in enabled: + yield (option.name, option.verbose_name) + + + def save(self, instance): + pass + + def delete(self, instance): + pass + + def get_related_objects(self, instance): + pass + + +class Php55App(AppType): + name = 'php5.5-fpm' + verbose_name = "PHP 5.5 FPM" +# 'fpm', ('unix:/var/run/%(user)s-%(app_name)s.sock|fcgi://127.0.0.1%(app_path)s',), + directive = ('fpm', 'fcgi://{}%(app_path)s'.format(settings.WEBAPPS_FPM_LISTEN)) + help_text = _("This creates a PHP5.5 application under ~/webapps/<app_name>
" + "PHP-FPM will be used to execute PHP files.") + options = ( + ('Process', options.process), + ('PHP', [op for op in options.php if getattr(op, 'deprecated', 99) > 5.5]), + ('File system', options.filesystem), + ) + icon = 'orchestra/icons/apps/PHPFPM.png' + + +class Php52App(AppType): + name = 'php5.2-fcgi' + verbose_name = "PHP 5.2 FCGI" + directive = ('fcgi', settings.WEBAPPS_FCGID_PATH) + help_text = _("This creates a PHP5.2 application under ~/webapps/<app_name>
" + "Apache-mod-fcgid will be used to execute PHP files.") + icon = 'orchestra/icons/apps/PHPFCGI.png' + + +class Php4App(AppType): + name = 'php4-fcgi' + verbose_name = "PHP 4 FCGI" + directive = ('fcgi', settings.WEBAPPS_FCGID_PATH) + help_text = _("This creates a PHP4 application under ~/webapps/<app_name>
" + "Apache-mod-fcgid will be used to execute PHP files.") + icon = 'orchestra/icons/apps/PHPFCGI.png' + + +class StaticApp(AppType): + name = 'static' + verbose_name = "Static" + directive = ('static',) + help_text = _("This creates a Static application under ~/webapps/<app_name>
" + "Apache2 will be used to serve static content and execute CGI files.") + icon = 'orchestra/icons/apps/Static.png' + options = ( + ('File system', options.filesystem), + ) + +class WebalizerApp(AppType): + name = 'webalizer' + verbose_name = "Webalizer" + directive = ('static', '%(app_path)s%(site_name)s') + help_text = _("This creates a Webalizer application under " + "~/webapps/<app_name>-<site_name>") + icon = 'orchestra/icons/apps/Stats.png' + options = () + + +class WordPressMuApp(AppType): + name = 'wordpress-mu' + verbose_name = "WordPress (SaaS)" + directive = ('fpm', 'fcgi://127.0.0.1:8990/home/httpd/wordpress-mu/') + help_text = _("This creates a WordPress site on a multi-tenant WordPress server.
" + "By default this blog is accessible via <app_name>.blogs.orchestra.lan") + icon = 'orchestra/icons/apps/WordPressMu.png' + unique_name = True + options = () + + +class DokuWikiMuApp(AppType): + name = 'dokuwiki-mu' + verbose_name = "DokuWiki (SaaS)" + directive = ('alias', '/home/httpd/wikifarm/farm/') + help_text = _("This create a DokuWiki wiki into a shared DokuWiki server.
" + "By default this wiki is accessible via <app_name>.wikis.orchestra.lan") + icon = 'orchestra/icons/apps/DokuWikiMu.png' + unique_name = True + options = () + + +class MoodleMuApp(AppType): + name = 'moodle-mu' + verbose_name = "Moodle (SaaS)" + directive = ('alias', '/home/httpd/wikifarm/farm/') + help_text = _("This create a Moodle site into a shared Moodle server.
" + "By default this wiki is accessible via <app_name>.moodle.orchestra.lan") + icon = 'orchestra/icons/apps/MoodleMu.png' + unique_name = True + options = () + + +class DrupalMuApp(AppType): + name = 'drupal-mu' + verbose_name = "Drupdal (SaaS)" + directive = ('fpm', 'fcgi://127.0.0.1:8991/home/httpd/drupal-mu/') + help_text = _("This creates a Drupal site into a multi-tenant Drupal server.
" + "The installation will be completed after visiting " + "http://<app_name>.drupal.orchestra.lan/install.php?profile=standard
" + "By default this site will be accessible via <app_name>.drupal.orchestra.lan") + icon = 'orchestra/icons/apps/DrupalMu.png' + unique_name = True + options = () + + +from rest_framework import serializers +from orchestra.forms import widgets +class SymbolicLinkForm(PluginDataForm): + path = forms.CharField(label=_("Path"), widget=forms.TextInput(attrs={'size':'100'}), + help_text=_("Path for the origin of the symbolic link.")) + + +class SymbolicLinkSerializer(serializers.Serializer): + path = serializers.CharField(label=_("Path")) + + +class SymbolicLinkApp(AppType): + name = 'symbolic-link' + verbose_name = "Symbolic link" + form = SymbolicLinkForm + serializer = SymbolicLinkSerializer + icon = 'orchestra/icons/apps/SymbolicLink.png' + change_readonly_fileds = ('path',) + + +class WordPressForm(PluginDataForm): + db_name = forms.CharField(label=_("Database name"), + help_text=_("Database used for this webapp.")) + db_user = forms.CharField(label=_("Database user"),) + db_pass = forms.CharField(label=_("Database user password"), + help_text=_("Initial database password.")) + + +class WordPressSerializer(serializers.Serializer): + db_name = serializers.CharField(label=_("Database name"), required=False) + db_user = serializers.CharField(label=_("Database user"), required=False) + db_pass = serializers.CharField(label=_("Database user password"), required=False) + + +from orchestra.apps.databases.models import Database, DatabaseUser +from orchestra.utils.python import random_ascii + + +class WordPressApp(AppType): + name = 'wordpress' + verbose_name = "WordPress" + icon = 'orchestra/icons/apps/WordPress.png' + change_form = WordPressForm + serializer = WordPressSerializer + change_readonly_fileds = ('db_name', 'db_user', 'db_pass',) + help_text = _("Visit http://<domain.lan>/wp-admin/install.php to finish the installation.") + + def get_db_name(self, webapp): + db_name = 'wp_%s_%s' % (webapp.name, webapp.account) + # Limit for mysql database names + return db_name[:65] + + def get_db_user(self, webapp): + db_name = self.get_db_name(webapp) + # Limit for mysql user names + return db_name[:17] + + def get_db_pass(self): + return random_ascii(10) + + def validate(self, webapp): + create = not webapp.pk + if create: + db = Database(name=self.get_db_name(webapp), account=webapp.account) + user = DatabaseUser(username=self.get_db_user(webapp), password=self.get_db_pass(), + account=webapp.account) + for obj in (db, user): + try: + obj.full_clean() + except ValidationError, e: + raise ValidationError({ + 'name': e.messages, + }) + + def save(self, webapp): + create = not webapp.pk + if create: + db_name = self.get_db_name(webapp) + db_user = self.get_db_user(webapp) + db_pass = self.get_db_pass() + db = Database.objects.create(name=db_name, account=webapp.account) + user = DatabaseUser(username=db_user, account=webapp.account) + user.set_password(db_pass) + user.save() + db.users.add(user) + webapp.data = { + 'db_name': db_name, + 'db_user': db_user, + 'db_pass': db_pass, + } + else: + # Trigger related backends + for related in self.get_related(webapp): + related.save() + + def delete(self, webapp): + for related in self.get_related(webapp): + related.delete() + + def get_related(self, webapp): + related = [] + try: + db_user = DatabaseUser.objects.get(username=webapp.data.get('db_user')) + except DatabaseUser.DoesNotExist: + pass + else: + related.append(db_user) + try: + db = Database.objects.get(name=webapp.data.get('db_name')) + except Database.DoesNotExist: + pass + else: + related.append(db) + return related diff --git a/orchestra/apps/websites/admin.py b/orchestra/apps/websites/admin.py index 45b4fac1..71c4242c 100644 --- a/orchestra/apps/websites/admin.py +++ b/orchestra/apps/websites/admin.py @@ -10,7 +10,7 @@ from orchestra.admin.utils import admin_link, change_url from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin from orchestra.forms.widgets import DynamicHelpTextSelect -from . import settings +from . import settings, options from .forms import WebsiteAdminForm from .models import Content, Website, WebsiteOption @@ -20,8 +20,7 @@ class WebsiteOptionInline(admin.TabularInline): extra = 1 OPTIONS_HELP_TEXT = { - k: str(unicode(v[1])) if len(v) == 3 else '' - for k, v in settings.WEBSITES_OPTIONS.iteritems() + op.name: str(unicode(op.help_text)) for op in options.get_enabled().values() } # class Media: diff --git a/orchestra/apps/websites/models.py b/orchestra/apps/websites/models.py index ea6ace91..9bc2a59f 100644 --- a/orchestra/apps/websites/models.py +++ b/orchestra/apps/websites/models.py @@ -5,10 +5,9 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from orchestra.core import validators, services -from orchestra.utils import tuple_setting_to_choices from orchestra.utils.functional import cached -from . import settings +from . import settings, options class Website(models.Model): @@ -78,15 +77,16 @@ class Website(models.Model): path = settings.WEBSITES_WEBSITE_WWW_ERROR_LOG_PATH % context return path.replace('//', '/') + class WebsiteOption(models.Model): website = models.ForeignKey(Website, verbose_name=_("web site"), related_name='options') name = models.CharField(_("name"), max_length=128, - choices=tuple_setting_to_choices(settings.WEBSITES_OPTIONS)) + choices=((op.name, op.verbose_name) for op in options.get_enabled().values())) value = models.CharField(_("value"), max_length=256) class Meta: - unique_together = ('website', 'name') +# unique_together = ('website', 'name') verbose_name = _("option") verbose_name_plural = _("options") @@ -94,16 +94,8 @@ class WebsiteOption(models.Model): return self.name def clean(self): - """ validates name and value according to WEBSITES_WEBSITEOPTIONS """ - regex = settings.WEBSITES_OPTIONS[self.name][-1] - if not re.match(regex, self.value): - raise ValidationError({ - 'value': ValidationError(_("'%(value)s' does not match %(regex)s."), - params={ - 'value': self.value, - 'regex': regex - }), - }) + option = options.get_enabled()[self.name] + option.validate(self) class Content(models.Model): diff --git a/orchestra/apps/websites/options.py b/orchestra/apps/websites/options.py new file mode 100644 index 00000000..c768f054 --- /dev/null +++ b/orchestra/apps/websites/options.py @@ -0,0 +1,127 @@ +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ + +from orchestra.utils.python import import_class + +from . import settings + + +# TODO multiple and unique validation support in the formset +class SiteOption(object): + unique = True + + def __init__(self, name, *args, **kwargs): + self.name = name + self.verbose_name = kwargs.pop('verbose_name', name) + self.help_text = kwargs.pop('help_text', '') + for k,v in kwargs.iteritems(): + setattr(self, k, v) + + def validate(self, website): + if self.regex and not re.match(self.regex, website.value): + raise ValidationError({ + 'value': ValidationError(_("'%(value)s' does not match %(regex)s."), + params={ + 'value': website.value, + 'regex': self.regex + }), + }) + + +directory_protection = SiteOption('directory_protection', + verbose_name=_("Directory protection"), + help_text=_("Space separated ..."), + regex=r'^([\w/_]+)\s+(\".*\")\s+([\w/_\.]+)$', +) + +redirect = SiteOption('redirect', + verbose_name=_("Redirection"), + help_text=_("<website path> <destination URL>"), + regex=r'^[^ ]+\s[^ ]+$', +) + +proxy = SiteOption('proxy', + verbose_name=_("Proxy"), + help_text=_("<website path> <target URL>"), + regex=r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$', +) + +ssl_ca = SiteOption('ssl_ca', + verbose_name=_("SSL CA"), + help_text=_("Filesystem path of the CA certificate file."), + regex=r'^[^ ]+$' +) + +ssl_cert = SiteOption('ssl_cert', + verbose_name=_("SSL cert"), + help_text=_("Filesystem path of the certificate file."), + regex=r'^[^ ]+$', +) + +ssl_key = SiteOption('ssl_key', + verbose_name=_("SSL key"), + help_text=_("Filesystem path of the key file."), + regex=r'^[^ ]+$', +) + +sec_rule_remove = SiteOption('sec_rule_remove', + verbose_name=_("SecRuleRemoveById"), + help_text=_("Space separated ModSecurity rule IDs."), + regex=r'^[0-9\s]+$', +) + +sec_engine = SiteOption('sec_engine', + verbose_name=_("Modsecurity engine"), + help_text=_("On or Off, defaults to On"), + regex=r'^(On|Off)$', +) + +user_group = SiteOption('user_group', + verbose_name=_("SuexecUserGroup"), + help_text=_("user [group], username and optional groupname."), + # TODO validate existing user/group + regex=r'^[\w/_]+(\s[\w/_]+)*$', +) + +error_document = SiteOption('error_document', + verbose_name=_("ErrorDocumentRoot"), + help_text=_("<error code> <URL/path/message>
" + " 500 http://foo.example.com/cgi-bin/tester
" + " 404 /cgi-bin/bad_urls.pl
" + " 401 /subscription_info.html
" + " 403 \"Sorry can't allow you access today\""), + regex=r'[45]0[0-9]\s.*', +) + + +ssl = [ + ssl_ca, + ssl_cert, + ssl_key, +] + +sec = [ + sec_rule_remove, + sec_engine, +] + +httpd = [ + directory_protection, + redirect, + proxy, + user_group, + error_document, +] + + +_enabled = None + +def get_enabled(): + global _enabled + if _enabled is None: + from . import settings + _enabled = {} + for op in settings.WEBSITES_ENABLED_OPTIONS: + op = import_class(op) + _enabled[op.name] = op + return _enabled diff --git a/orchestra/apps/websites/settings.py b/orchestra/apps/websites/settings.py index 7c85c017..1f167224 100644 --- a/orchestra/apps/websites/settings.py +++ b/orchestra/apps/websites/settings.py @@ -6,12 +6,20 @@ WEBSITES_UNIQUE_NAME_FORMAT = getattr(settings, 'WEBSITES_UNIQUE_NAME_FORMAT', '%(account)s-%(name)s') +# TODO 'http', 'https', 'https-only', 'http and https' and rename to PROTOCOL WEBSITES_PORT_CHOICES = getattr(settings, 'WEBSITES_PORT_CHOICES', ( (80, 'HTTP'), (443, 'HTTPS'), )) +WEBSITES_PROTOCOL_CHOICES = getattr(settings, 'WEBSITES_PROTOCOL_CHOICES', ( + ('http', 'HTTP'), + ('https', 'HTTPS'), + ('http-https', 'HTTP and HTTPS), + ('https-only', 'HTTPS only'), +)) + WEBSITES_DEFAULT_PORT = getattr(settings, 'WEBSITES_DEFAULT_PORT', 80) @@ -21,65 +29,18 @@ WEBSITES_DEFAULT_IP = getattr(settings, 'WEBSITES_DEFAULT_IP', '*') WEBSITES_DOMAIN_MODEL = getattr(settings, 'WEBSITES_DOMAIN_MODEL', 'domains.Domain') -WEBSITES_OPTIONS = getattr(settings, 'WEBSITES_OPTIONS', { - # { name: ( verbose_name, [help_text], validation_regex ) } - 'directory_protection': ( - _("HTTPD - Directory protection"), - _("Space separated ..."), - r'^([\w/_]+)\s+(\".*\")\s+([\w/_\.]+)$', - ), - 'redirect': ( - _("HTTPD - Redirection"), - _("<website path> <destination URL>"), - r'^[^ ]+\s[^ ]+$', - ), - 'proxy': ( - _("HTTPD - Proxy"), - _("<website path> <target URL>"), - r'^[^ ]+\shttp[^ ]+(timeout=[0-9]{1,3}|retry=[0-9]|\s)*$', - ), - 'ssl_ca': ( - "HTTPD - SSL CA", - _("Filesystem path of the CA certificate file."), - r'^[^ ]+$' - ), - 'ssl_cert': ( - _("HTTPD - SSL cert"), - _("Filesystem path of the certificate file."), - r'^[^ ]+$' - ), - 'ssl_key': ( - _("HTTPD - SSL key"), - _("Filesystem path of the key file."), - r'^[^ ]+$', - ), - 'sec_rule_remove': ( - "HTTPD - SecRuleRemoveById", - _("Space separated ModSecurity rule IDs."), - r'^[0-9\s]+$', - ), - 'sec_engine': ( - "HTTPD - Modsecurity engine", - _("On or Off, defaults to On"), - r'^(On|Off)$', - ), - 'user_group': ( - "HTTPD - SuexecUserGroup", - _("user [group], username and optional groupname."), - # TODO validate existing user/group - r'^[\w/_]+(\s[\w/_]+)*$', - ), - # TODO backend support - 'error_document': ( - "HTTPD - ErrorDocumentRoot", - _("<error code> <URL/path/message>
" - " 500 http://foo.example.com/cgi-bin/tester
" - " 404 /cgi-bin/bad_urls.pl
" - " 401 /subscription_info.html
" - " 403 \"Sorry can't allow you access today\""), - r'[45]0[0-9]\s.*', - ) -}) +WEBSITES_ENABLED_OPTIONS = getattr(settings, 'WEBSITES_ENABLED_OPTIONS', ( + 'orchestra.apps.websites.options.directory_protection', + 'orchestra.apps.websites.options.redirect', + 'orchestra.apps.websites.options.proxy', + 'orchestra.apps.websites.options.ssl_ca', + 'orchestra.apps.websites.options.ssl_cert', + 'orchestra.apps.websites.options.ssl_key', + 'orchestra.apps.websites.options.sec_rule_remove', + 'orchestra.apps.websites.options.sec_engine', + 'orchestra.apps.websites.options.user_group', + 'orchestra.apps.websites.options.error_document', +)) WEBSITES_BASE_APACHE_CONF = getattr(settings, 'WEBSITES_BASE_APACHE_CONF', diff --git a/orchestra/forms/widgets.py b/orchestra/forms/widgets.py index b4673b2a..5ffbe0e1 100644 --- a/orchestra/forms/widgets.py +++ b/orchestra/forms/widgets.py @@ -69,10 +69,11 @@ def paddingCheckboxSelectMultiple(padding): class DynamicHelpTextSelect(forms.Select): def __init__(self, target, help_text, *args, **kwargs): help_text = self.get_dynamic_help_text(target, help_text) - attrs = { + attrs = kwargs.get('attrs', {}) + attrs.update({ 'onClick': help_text, 'onChange': help_text, - } + }) attrs.update(kwargs.get('attrs', {})) kwargs['attrs'] = attrs super(DynamicHelpTextSelect, self).__init__(*args, **kwargs) @@ -81,8 +82,8 @@ class DynamicHelpTextSelect(forms.Select): return textwrap.dedent("""\ siteoptions = {help_text}; valueelement = $("#" + {target}); + help_text = siteoptions[this.options[this.selectedIndex].value] || "" valueelement.parent().find('p').remove(); - valueelement.parent().append( - "

" + siteoptions[this.options[this.selectedIndex].value] + "

" - );""".format(target=target, help_text=str(help_text)) + valueelement.parent().append("

" + help_text + "

");\ + """.format(target=target, help_text=str(help_text)) ) diff --git a/orchestra/plugins/admin.py b/orchestra/plugins/admin.py index a21d4dca..4435fadd 100644 --- a/orchestra/plugins/admin.py +++ b/orchestra/plugins/admin.py @@ -1,7 +1,6 @@ from django.conf.urls import patterns, url from django.contrib.admin.utils import unquote from django.shortcuts import render, redirect -from django.utils.text import camel_case_to_spaces from django.utils.translation import ugettext_lazy as _ from orchestra.admin.utils import wrap_admin_view @@ -11,14 +10,29 @@ from orchestra.utils.functional import cached class SelectPluginAdminMixin(object): plugin = None plugin_field = None + plugin_title = None def get_form(self, request, obj=None, **kwargs): if obj: - self.form = getattr(obj, '%s_class' % self.plugin_field)().get_form() + plugin = getattr(obj, '%s_instance' % self.plugin_field) + self.form = getattr(plugin, 'get_change_form', plugin.get_form)() else: - self.form = self.plugin.get_plugin(self.plugin_value)().get_form() + plugin = self.plugin.get_plugin(self.plugin_value)() + self.form = plugin.get_form() return super(SelectPluginAdminMixin, self).get_form(request, obj=obj, **kwargs) + def get_fields(self, request, obj=None): + """ Try to maintain original field ordering """ + fields = super(SelectPluginAdminMixin, self).get_fields(request, obj=obj) + head_fields = list(self.get_readonly_fields(request, obj)) + head, tail = [], [] + for field in fields: + if field in head_fields: + head.append(field) + else: + tail.append(field) + return head + tail + def get_urls(self): """ Hooks select account url """ urls = super(SelectPluginAdminMixin, self).get_urls() @@ -34,6 +48,7 @@ class SelectPluginAdminMixin(object): def select_plugin_view(self, request): opts = self.model._meta context = { + 'plugin_title': self.plugin_title or 'Plugins', 'opts': opts, 'app_label': opts.app_label, 'field': self.plugin_field, @@ -52,8 +67,9 @@ class SelectPluginAdminMixin(object): self.plugin_value = plugin_value if not plugin_value: self.plugin_value = self.plugin.get_plugins()[0].get_name() + plugin = self.plugin.get_plugin(self.plugin_value) context = { - 'title': _("Add new %s") % camel_case_to_spaces(self.plugin_value), + 'title': _("Add new %s") % plugin.verbose_name, } context.update(extra_context or {}) return super(SelectPluginAdminMixin, self).add_view(request, form_url=form_url, @@ -62,9 +78,9 @@ class SelectPluginAdminMixin(object): def change_view(self, request, object_id, form_url='', extra_context=None): obj = self.get_object(request, unquote(object_id)) - plugin_value = getattr(obj, self.plugin_field) + plugin = getattr(obj, '%s_class' % self.plugin_field) context = { - 'title': _("Change %s") % camel_case_to_spaces(str(plugin_value)), + 'title': _("Change %s") % plugin.verbose_name, } context.update(extra_context or {}) return super(SelectPluginAdminMixin, self).change_view(request, object_id, diff --git a/orchestra/plugins/forms.py b/orchestra/plugins/forms.py index 6467ca75..4bd1937b 100644 --- a/orchestra/plugins/forms.py +++ b/orchestra/plugins/forms.py @@ -1,23 +1,32 @@ from django import forms +from orchestra.forms.widgets import ReadOnlyWidget + class PluginDataForm(forms.ModelForm): data = forms.CharField(widget=forms.HiddenInput, required=False) def __init__(self, *args, **kwargs): super(PluginDataForm, self).__init__(*args, **kwargs) - # TODO remove it well - try: - self.fields[self.plugin_field].widget = forms.HiddenInput() - except KeyError: - pass + if self.plugin_field in self.fields: + value = self.plugin.get_name() + display = '%s change' % unicode(self.plugin.verbose_name) + self.fields[self.plugin_field].widget = ReadOnlyWidget(value, display) + self.fields[self.plugin_field].help_text = getattr(self.plugin, 'help_text', '') instance = kwargs.get('instance') if instance: for field in self.declared_fields: initial = self.fields[field].initial self.fields[field].initial = instance.data.get(field, initial) + if self.instance.pk: + for field in self.plugin.get_change_readonly_fileds(): + value = getattr(self.instance, field, None) or self.instance.data[field] + self.fields[field].required = False + self.fields[field].widget = ReadOnlyWidget(value) +# self.fields[field].help_text = None def clean(self): + # TODO clean all filed within data??? data = {} for field in self.declared_fields: try: diff --git a/orchestra/plugins/options.py b/orchestra/plugins/options.py index f6e87a69..3f2387b1 100644 --- a/orchestra/plugins/options.py +++ b/orchestra/plugins/options.py @@ -6,10 +6,11 @@ class Plugin(object): # Used on select plugin view class_verbose_name = None icon = None + change_readonly_fileds = () @classmethod def get_name(cls): - return cls.__name__ + return getattr(cls, 'name', cls.__name__) @classmethod def get_plugins(cls): @@ -39,6 +40,10 @@ class Plugin(object): verbose = plugin.get_verbose_name() choices.append((plugin.get_name(), verbose)) return sorted(choices, key=lambda e: e[1]) + + @classmethod + def get_change_readonly_fileds(cls): + return cls.change_readonly_fileds class PluginModelAdapter(Plugin): diff --git a/orchestra/apps/saas/static/saas/icons/BSCW.png b/orchestra/static/orchestra/icons/apps/BSCW.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/BSCW.png rename to orchestra/static/orchestra/icons/apps/BSCW.png diff --git a/orchestra/static/orchestra/icons/apps/DokuWikiMu.png b/orchestra/static/orchestra/icons/apps/DokuWikiMu.png new file mode 100644 index 00000000..38d91789 Binary files /dev/null and b/orchestra/static/orchestra/icons/apps/DokuWikiMu.png differ diff --git a/orchestra/static/orchestra/icons/apps/DokuWikiMu.svg b/orchestra/static/orchestra/icons/apps/DokuWikiMu.svg new file mode 100644 index 00000000..02282eed --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/DokuWikiMu.svg @@ -0,0 +1,630 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/apps/saas/static/saas/icons/Dokuwiki.png b/orchestra/static/orchestra/icons/apps/Dokuwiki.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Dokuwiki.png rename to orchestra/static/orchestra/icons/apps/Dokuwiki.png diff --git a/orchestra/apps/saas/static/saas/icons/Dokuwiki.svg b/orchestra/static/orchestra/icons/apps/Dokuwiki.svg similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Dokuwiki.svg rename to orchestra/static/orchestra/icons/apps/Dokuwiki.svg diff --git a/orchestra/apps/saas/static/saas/icons/Drupal.png b/orchestra/static/orchestra/icons/apps/Drupal.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Drupal.png rename to orchestra/static/orchestra/icons/apps/Drupal.png diff --git a/orchestra/apps/saas/static/saas/icons/Drupal.svg b/orchestra/static/orchestra/icons/apps/Drupal.svg similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Drupal.svg rename to orchestra/static/orchestra/icons/apps/Drupal.svg diff --git a/orchestra/static/orchestra/icons/apps/DrupalMu.png b/orchestra/static/orchestra/icons/apps/DrupalMu.png new file mode 100644 index 00000000..b8ca4053 Binary files /dev/null and b/orchestra/static/orchestra/icons/apps/DrupalMu.png differ diff --git a/orchestra/static/orchestra/icons/apps/DrupalMu.svg b/orchestra/static/orchestra/icons/apps/DrupalMu.svg new file mode 100644 index 00000000..11b394f2 --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/DrupalMu.svg @@ -0,0 +1,160 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/apps/saas/static/saas/icons/Moodle.png b/orchestra/static/orchestra/icons/apps/Moodle.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Moodle.png rename to orchestra/static/orchestra/icons/apps/Moodle.png diff --git a/orchestra/apps/saas/static/saas/icons/Moodle.svg b/orchestra/static/orchestra/icons/apps/Moodle.svg similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Moodle.svg rename to orchestra/static/orchestra/icons/apps/Moodle.svg diff --git a/orchestra/static/orchestra/icons/apps/MoodleMu.png b/orchestra/static/orchestra/icons/apps/MoodleMu.png new file mode 100644 index 00000000..3881b0ee Binary files /dev/null and b/orchestra/static/orchestra/icons/apps/MoodleMu.png differ diff --git a/orchestra/static/orchestra/icons/apps/MoodleMu.svg b/orchestra/static/orchestra/icons/apps/MoodleMu.svg new file mode 100644 index 00000000..3493ba5b --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/MoodleMu.svg @@ -0,0 +1,538 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/apps/PHP.svg b/orchestra/static/orchestra/icons/apps/PHP.svg new file mode 100644 index 00000000..ffa40a38 --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/PHP.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/orchestra/static/orchestra/icons/apps/PHPFCGI.png b/orchestra/static/orchestra/icons/apps/PHPFCGI.png new file mode 100644 index 00000000..02ea63ca Binary files /dev/null and b/orchestra/static/orchestra/icons/apps/PHPFCGI.png differ diff --git a/orchestra/static/orchestra/icons/apps/PHPFCGI.svg b/orchestra/static/orchestra/icons/apps/PHPFCGI.svg new file mode 100644 index 00000000..677aac42 --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/PHPFCGI.svg @@ -0,0 +1,182 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FCGI + diff --git a/orchestra/static/orchestra/icons/apps/PHPFPM.png b/orchestra/static/orchestra/icons/apps/PHPFPM.png new file mode 100644 index 00000000..71d3fca7 Binary files /dev/null and b/orchestra/static/orchestra/icons/apps/PHPFPM.png differ diff --git a/orchestra/static/orchestra/icons/apps/PHPFPM.svg b/orchestra/static/orchestra/icons/apps/PHPFPM.svg new file mode 100644 index 00000000..82cd01ea --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/PHPFPM.svg @@ -0,0 +1,182 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FPM + diff --git a/orchestra/apps/saas/static/saas/icons/Phplist.png b/orchestra/static/orchestra/icons/apps/Phplist.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Phplist.png rename to orchestra/static/orchestra/icons/apps/Phplist.png diff --git a/orchestra/apps/saas/static/saas/icons/Phplist.svg b/orchestra/static/orchestra/icons/apps/Phplist.svg similarity index 100% rename from orchestra/apps/saas/static/saas/icons/Phplist.svg rename to orchestra/static/orchestra/icons/apps/Phplist.svg diff --git a/orchestra/static/orchestra/icons/apps/Python.svg b/orchestra/static/orchestra/icons/apps/Python.svg new file mode 100644 index 00000000..6f9736f4 --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/Python.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/orchestra/static/orchestra/icons/apps/Static.png b/orchestra/static/orchestra/icons/apps/Static.png new file mode 100644 index 00000000..9328f1a6 Binary files /dev/null and b/orchestra/static/orchestra/icons/apps/Static.png differ diff --git a/orchestra/static/orchestra/icons/apps/Static.svg b/orchestra/static/orchestra/icons/apps/Static.svg new file mode 100644 index 00000000..699c9c32 --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/Static.svg @@ -0,0 +1,1365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + HTML Document + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + HTML + hypertext + web + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/apps/Stats.png b/orchestra/static/orchestra/icons/apps/Stats.png new file mode 100644 index 00000000..525a2bb9 Binary files /dev/null and b/orchestra/static/orchestra/icons/apps/Stats.png differ diff --git a/orchestra/static/orchestra/icons/apps/Stats.svg b/orchestra/static/orchestra/icons/apps/Stats.svg new file mode 100644 index 00000000..aceaa252 --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/Stats.svg @@ -0,0 +1,394 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/static/orchestra/icons/apps/SymbolicLink.png b/orchestra/static/orchestra/icons/apps/SymbolicLink.png new file mode 100644 index 00000000..d3b23940 Binary files /dev/null and b/orchestra/static/orchestra/icons/apps/SymbolicLink.png differ diff --git a/orchestra/static/orchestra/icons/apps/SymbolicLink.svg b/orchestra/static/orchestra/icons/apps/SymbolicLink.svg new file mode 100644 index 00000000..5a1b8d2d --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/SymbolicLink.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Symbolic Link + + + emblem + symbolic + link + pointer + io + file + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/apps/saas/static/saas/icons/WordPress.png b/orchestra/static/orchestra/icons/apps/WordPress.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/WordPress.png rename to orchestra/static/orchestra/icons/apps/WordPress.png diff --git a/orchestra/apps/saas/static/saas/icons/WordPress.svg b/orchestra/static/orchestra/icons/apps/WordPress.svg similarity index 100% rename from orchestra/apps/saas/static/saas/icons/WordPress.svg rename to orchestra/static/orchestra/icons/apps/WordPress.svg diff --git a/orchestra/static/orchestra/icons/apps/WordPressMu.png b/orchestra/static/orchestra/icons/apps/WordPressMu.png new file mode 100644 index 00000000..dc604112 Binary files /dev/null and b/orchestra/static/orchestra/icons/apps/WordPressMu.png differ diff --git a/orchestra/static/orchestra/icons/apps/WordPressMu.svg b/orchestra/static/orchestra/icons/apps/WordPressMu.svg new file mode 100644 index 00000000..aa829da5 --- /dev/null +++ b/orchestra/static/orchestra/icons/apps/WordPressMu.svg @@ -0,0 +1,127 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/apps/saas/static/saas/icons/gitlab.png b/orchestra/static/orchestra/icons/apps/gitlab.png similarity index 100% rename from orchestra/apps/saas/static/saas/icons/gitlab.png rename to orchestra/static/orchestra/icons/apps/gitlab.png diff --git a/orchestra/static/orchestra/icons/saas.svg b/orchestra/static/orchestra/icons/saas.svg index 5eb15d98..b10f711b 100644 --- a/orchestra/static/orchestra/icons/saas.svg +++ b/orchestra/static/orchestra/icons/saas.svg @@ -16,7 +16,7 @@ width="48" height="48" sodipodi:docname="saas.svg" - inkscape:export-filename="/home/glic3/orchestra/django-orchestra/orchestra/static/orchestra/icons/saas.png" + inkscape:export-filename="/home/glic3rinu/orchestra/django-orchestra/orchestra/static/orchestra/icons/saas.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90"> {% if plugin.icon %}
-

Software as a Service

+

{{ plugin_title }}

    {% for plugin in plugins %} diff --git a/orchestra/utils/humanize.py b/orchestra/utils/humanize.py index c8b0c5cd..4fa44170 100644 --- a/orchestra/utils/humanize.py +++ b/orchestra/utils/humanize.py @@ -174,7 +174,6 @@ UNITS_CONVERSIONS = { } def unit_to_bytes(unit): - unit = unit.upper() for bytes, units in UNITS_CONVERSIONS.iteritems(): if unit in units: return bytes diff --git a/orchestra/utils/python.py b/orchestra/utils/python.py index bf1a68fa..0bfb997c 100644 --- a/orchestra/utils/python.py +++ b/orchestra/utils/python.py @@ -1,4 +1,6 @@ import collections +import random +import string def import_class(cls): @@ -8,6 +10,10 @@ def import_class(cls): return getattr(module, cls) +def random_ascii(length): + return ''.join([random.choice(string.hexdigits) for i in range(0, length)]).lower() + + class OrderedSet(collections.MutableSet): def __init__(self, iterable=None): self.end = end = [] diff --git a/orchestra/utils/tests.py b/orchestra/utils/tests.py index 6fc9f7c1..89f78d40 100644 --- a/orchestra/utils/tests.py +++ b/orchestra/utils/tests.py @@ -1,7 +1,5 @@ import datetime import os -import string -import random from functools import wraps from django.conf import settings @@ -15,9 +13,8 @@ from xvfbwrapper import Xvfb from orchestra.apps.accounts.models import Account +from .python import random_ascii -def random_ascii(length): - return ''.join([random.choice(string.hexdigits) for i in range(0, length)]).lower() class AppDependencyMixin(object):