diff --git a/TODO.md b/TODO.md index 5bd5b8ca..47af81be 100644 --- a/TODO.md +++ b/TODO.md @@ -11,8 +11,6 @@ * env vars instead of multiple settings files: https://devcenter.heroku.com/articles/config-vars ? -# TODO Log changes from rest api (serialized objects) - * backend logs with hal logo * LAST version of this shit http://wkhtmltopdf.org/downloads.h otml @@ -175,9 +173,8 @@ require_once(‘/etc/moodles/’.$moodle_host.‘config.php’);``` moodle/drupl * autoexpand mailbox.filter according to filtering options (js) * allow empty metric pack for default rates? changes on rating algo -# IMPORTANT make sure no order is created for mailboxes that include disk? or just don't produce lines with cost == 0 or quantity 0 ? maybe minimal quantity for billing? like 0.1 ? or minimal price? per line or per bill? +# don't produce lines with cost == 0 or quantity 0 ? maybe minimal quantity for billing? like 0.1 ? or minimal price? per line or per bill? -* Improve performance of admin change lists with debug toolbar and prefech_related # DOMINI REGISTRE MIGRATION SCRIPTS # lines too long on invoice, double lines or cut, and make margin wider @@ -191,7 +188,7 @@ require_once(‘/etc/moodles/’.$moodle_host.‘config.php’);``` moodle/drupl # display subline links on billlines, to show that they exists. * update service orders on a celery task? because it take alot -# billline quantity eval('10x100') instead of miningless description '(10*100)' +# billline quantity eval('10x100') instead of miningless description '(10*100)' line.verbose_quantity # FIXME do more test, make sure billed until doesn't get uodated whhen services are billed with les metric, and don't upgrade billed_until when undoing under this circumstances * line 513: change threshold and one time service metric change should update last value if not billed, only record for recurring invoicing. postpay services should store the last metric for pricing period. @@ -211,12 +208,12 @@ require_once(‘/etc/moodles/’.$moodle_host.‘config.php’);``` moodle/drupl * modeladmin Default filter + search isn't working, prepend filter when searching -# IMPORTANT do all modles.py TODOs and create migrations for finished apps - * create service help templates based on urlqwargs with the most basic services. # TDOO Base price: domini propi (all domains) + extra for other domains +# IMPORTANT op.instance = copy.deepcopy(instance) ValueError: Cannot assign "": "SaaS" instance isn't saved in the database. +# Separate operation from models !! BackendOperation and Operation Translation ----------- @@ -258,7 +255,7 @@ https://code.djangoproject.com/ticket/24576 # FIXME what to do when deleting accounts? set fk null and fill a username charfield? issues, invoices.. we whant all this to go away? * implement delete All related services -# FIXME address name change does not remove old one :P +# FIXME address name change does not remove old one :P, readonly, perhaps we can regenerate all addresses using backend.prepare()? * read https://docs.djangoproject.com/en/dev/releases/1.8/ and fix deprecation warnings * remove admin object display_links , like contents webapps @@ -277,4 +274,5 @@ https://code.djangoproject.com/ticket/24576 * migrate to DRF3.x -* move all tests on django-orchestra/tests +* move all tests to django-orchestra/tests +* *natural keys: those fields that uniquely identify a service, list.name, website.name, webapp.name+account, make sure rest api can not edit thos things diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py index 8b66551c..26348067 100644 --- a/orchestra/admin/options.py +++ b/orchestra/admin/options.py @@ -244,7 +244,7 @@ class ChangePasswordAdminMixin(object): 'save_as': False, 'show_save': True, } - context.update(admin.site.each_context()) + context.update(admin.site.each_context(request)) return TemplateResponse(request, self.change_user_password_template, context, current_app=self.admin_site.name) diff --git a/orchestra/contrib/accounts/models.py b/orchestra/contrib/accounts/models.py index 2ba91360..ec64884c 100644 --- a/orchestra/contrib/accounts/models.py +++ b/orchestra/contrib/accounts/models.py @@ -6,7 +6,7 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from orchestra.contrib.orchestration.middlewares import OperationsMiddleware -from orchestra.contrib.orchestration.models import BackendOperation as Operation +from orchestra.contrib.orchestration import Operation from orchestra.core import services, accounts from orchestra.utils import send_email_template diff --git a/orchestra/contrib/databases/backends.py b/orchestra/contrib/databases/backends.py index 0cea6fc7..40199147 100644 --- a/orchestra/contrib/databases/backends.py +++ b/orchestra/contrib/databases/backends.py @@ -38,7 +38,7 @@ class MySQLBackend(ServiceController): if database.type != database.MYSQL: return context = self.get_context(database) - self.append("mysql -e 'DROP DATABASE `%(database)s`;' || exit_code=1" % context) + self.append("mysql -e 'DROP DATABASE `%(database)s`;' || exit_code=$?" % context) self.append("mysql mysql -e 'DELETE FROM db WHERE db = \"%(database)s\";'" % context) def commit(self): @@ -76,7 +76,7 @@ class MySQLUserBackend(ServiceController): return context = self.get_context(user) self.append(textwrap.dedent("""\ - mysql -e 'DROP USER "%(username)s"@"%(host)s";' \ + mysql -e 'DROP USER "%(username)s"@"%(host)s";' || exit_code=$? \ """) % context ) diff --git a/orchestra/contrib/domains/backends.py b/orchestra/contrib/domains/backends.py index 8d862629..9851009b 100644 --- a/orchestra/contrib/domains/backends.py +++ b/orchestra/contrib/domains/backends.py @@ -4,7 +4,7 @@ import textwrap from django.utils.translation import ugettext_lazy as _ from orchestra.contrib.orchestration import ServiceController, replace -from orchestra.contrib.orchestration.models import BackendOperation as Operation +from orchestra.contrib.orchestration import Operation from . import settings @@ -41,10 +41,11 @@ class Bind9MasterDomainBackend(ServiceController): def update_conf(self, context): self.append(textwrap.dedent("""\ - sed '/zone "%(name)s".*/,/^\s*};\s*$/!d' %(conf_path)s | diff -B -I"^\s*//" - <(echo '%(conf)s') || { + conf='%(conf)s' + sed '/zone "%(name)s".*/,/^\s*};\s*$/!d' %(conf_path)s | diff -B -I"^\s*//" - <(echo "${conf}") || { sed -i -e '/zone\s\s*"%(name)s".*/,/^\s*};/d' \\ -e 'N; /^\s*\\n\s*$/d; P; D' %(conf_path)s - echo '%(conf)s' >> %(conf_path)s + echo "${conf}" >> %(conf_path)s UPDATED=1 }""") % context ) @@ -80,7 +81,7 @@ class Bind9MasterDomainBackend(ServiceController): def get_servers(self, domain, backend): """ Get related server IPs from registered backend routes """ from orchestra.contrib.orchestration.manager import router - operation = Operation.create(backend, domain, Operation.SAVE) + operation = Operation(backend, domain, Operation.SAVE) servers = [] for server in router.get_servers(operation): servers.append(server.get_ip()) diff --git a/orchestra/contrib/mailboxes/admin.py b/orchestra/contrib/mailboxes/admin.py index a0871c38..58457646 100644 --- a/orchestra/contrib/mailboxes/admin.py +++ b/orchestra/contrib/mailboxes/admin.py @@ -107,7 +107,8 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): 'email', 'account_link', 'domain_link', 'display_mailboxes', 'display_forward', ) list_filter = (HasMailboxListFilter, HasForwardListFilter) - fields = ('account_link', ('name', 'domain'), 'mailboxes', 'forward') + fields = ('account_link', 'email_link', 'mailboxes', 'forward') + add_fields = ('account_link', ('name', 'domain'), 'mailboxes', 'forward') inlines = [AutoresponseInline] search_fields = ('name', 'domain__name', 'forward', 'mailboxes__name', 'account__username') readonly_fields = ('account_link', 'domain_link', 'email_link') diff --git a/orchestra/contrib/mailboxes/models.py b/orchestra/contrib/mailboxes/models.py index 16731218..90293eda 100644 --- a/orchestra/contrib/mailboxes/models.py +++ b/orchestra/contrib/mailboxes/models.py @@ -17,16 +17,14 @@ class Mailbox(models.Model): name = models.CharField(_("name"), max_length=64, unique=True, help_text=_("Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only."), validators=[ - RegexValidator(r'^[\w.@+-]+$', _("Enter a valid mailbox name.")) + RegexValidator(r'^[\w.@+-]+$', _("Enter a valid mailbox name.")), ]) password = models.CharField(_("password"), max_length=128) account = models.ForeignKey('accounts.Account', verbose_name=_("account"), related_name='mailboxes') filtering = models.CharField(max_length=16, default=settings.MAILBOXES_MAILBOX_DEFAULT_FILTERING, - choices=[ - (k, v[0]) for k,v in settings.MAILBOXES_MAILBOX_FILTERINGS.items() - ]) + choices=[(k, v[0]) for k,v in settings.MAILBOXES_MAILBOX_FILTERINGS.items()]) custom_filtering = models.TextField(_("filtering"), blank=True, validators=[validators.validate_sieve], help_text=_("Arbitrary email filtering in sieve language. " @@ -80,7 +78,7 @@ class Mailbox(models.Model): def get_local_address(self): if not settings.MAILBOXES_LOCAL_ADDRESS_DOMAIN: - raise AttributeError("Mailboxes do not have a defined local address domain") + raise AttributeError("Mailboxes do not have a defined local address domain.") return '@'.join((self.name, settings.MAILBOXES_LOCAL_ADDRESS_DOMAIN)) diff --git a/orchestra/contrib/miscellaneous/admin.py b/orchestra/contrib/miscellaneous/admin.py index 3ba411b1..465b0c57 100644 --- a/orchestra/contrib/miscellaneous/admin.py +++ b/orchestra/contrib/miscellaneous/admin.py @@ -42,7 +42,7 @@ class MiscServiceAdmin(ExtendedModelAdmin): num_instances.admin_order_field = 'instances__count' def get_queryset(self, request): - qs = super(MiscServiceAdmin, self).queryset(request) + qs = super(MiscServiceAdmin, self).get_queryset(request) return qs.annotate(models.Count('instances', distinct=True)) def formfield_for_dbfield(self, db_field, **kwargs): diff --git a/orchestra/contrib/orchestration/__init__.py b/orchestra/contrib/orchestration/__init__.py index 2ea5b1e3..297c2b1d 100644 --- a/orchestra/contrib/orchestration/__init__.py +++ b/orchestra/contrib/orchestration/__init__.py @@ -1 +1,60 @@ +import copy + from .backends import ServiceBackend, ServiceController, replace + + +class Operation(): + DELETE = 'delete' + SAVE = 'save' + MONITOR = 'monitor' + EXCEEDED = 'exceeded' + RECOVERY = 'recovery' + + def __str__(self): + return '%s.%s(%s)' % (self.backend, self.action, self.instance) + + def __hash__(self): + """ set() """ + return hash(self.backend) + hash(self.instance) + hash(self.action) + + def __eq__(self, operation): + """ set() """ + return hash(self) == hash(operation) + + def __init__(self, backend, instance, action, servers=None): + self.backend = backend + # instance should maintain any dynamic attribute until backend execution + # deep copy is prefered over copy otherwise objects will share same atributes (queryset cache) + self.instance = copy.deepcopy(instance) + self.action = action + self.servers = servers + + @classmethod + def execute(cls, operations, async=False): + from . import manager + scripts, block = manager.generate(operations) + return manager.execute(scripts, block=block, async=async) + + @classmethod + def execute_action(cls, instance, action): + backends = ServiceBackend.get_backends(instance=instance, action=action) + operations = [cls(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(self.instance) + + def create(self, log): + from .models import BackendOperation + return BackendOperation.objects.create( + log=log, + backend=self.backend.get_name(), + instance=self.instance, + action=self.action, + ) diff --git a/orchestra/contrib/orchestration/admin.py b/orchestra/contrib/orchestration/admin.py index 8ac7f856..62bf5f2d 100644 --- a/orchestra/contrib/orchestration/admin.py +++ b/orchestra/contrib/orchestration/admin.py @@ -1,10 +1,12 @@ -from django.contrib import admin +from django.contrib import admin, messages from django.utils.html import escape +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin.html import monospace_format from orchestra.admin.utils import admin_link, admin_date, admin_colored +from . import settings from .backends import ServiceBackend from .models import Server, Route, BackendLog, BackendOperation from .widgets import RouteBackendSelect @@ -66,6 +68,19 @@ class RouteAdmin(admin.ModelAdmin): if obj: form.base_fields['backend'].help_text = self.BACKEND_HELP_TEXT.get(obj.backend, '') return form + + def show_orchestration_disabled(self, request): + if settings.ORCHESTRATION_DISABLE_EXECUTION: + msg = _("Orchestration execution is disabled by ORCHESTRATION_DISABLE_EXECUTION setting.") + self.message_user(request, mark_safe(msg), messages.WARNING) + + def changelist_view(self, request, extra_context=None): + self.show_orchestration_disabled(request) + return super(RouteAdmin, self).changelist_view(request, extra_context) + + def changeform_view(self, request, object_id=None, form_url='', extra_context=None): + self.show_orchestration_disabled(request) + return super(RouteAdmin, self).changeform_view(request, object_id, form_url, extra_context) class BackendOperationInline(admin.TabularInline): diff --git a/orchestra/contrib/orchestration/management/commands/orchestrate.py b/orchestra/contrib/orchestration/management/commands/orchestrate.py index 24b6893e..9e4ffb3c 100644 --- a/orchestra/contrib/orchestration/management/commands/orchestrate.py +++ b/orchestra/contrib/orchestration/management/commands/orchestrate.py @@ -10,13 +10,13 @@ class Command(BaseCommand): help = 'Runs orchestration backends.' def add_arguments(self, parser): - parser.add_argument('model', nargs='+', - help='App label of an application to synchronize the - parser.add_argument('query', nargs='?', + parser.add_argument('model', + help='Label of a model to execute the orchestration.') + parser.add_argument('query', nargs='*', help='Query arguments for filter().') parser.add_argument('--noinput', action='store_false', dest='interactive', default=True, help='Tells Django to NOT prompt the user for input of any kind.') - parser.add_argument('--action', action='store', dest='database', + parser.add_argument('--action', action='store', dest='action', default='save', help='Executes action. Defaults to "save".') def handle(self, *args, **options): diff --git a/orchestra/contrib/orchestration/manager.py b/orchestra/contrib/orchestration/manager.py index 08b602e6..e2cfb735 100644 --- a/orchestra/contrib/orchestration/manager.py +++ b/orchestra/contrib/orchestration/manager.py @@ -9,10 +9,10 @@ from django.core.mail import mail_admins from orchestra.utils.python import import_class -from . import settings +from . import settings, Operation from .backends import ServiceBackend from .helpers import send_report -from .models import BackendLog, BackendOperation as Operation +from .models import BackendLog from .signals import pre_action, post_action @@ -95,6 +95,9 @@ def generate(operations): def execute(scripts, block=False, async=False): """ executes the operations on the servers """ + if settings.ORCHESTRATION_DISABLE_EXECUTION: + logger.info('Orchestration execution is dissabled by ORCHESTRATION_DISABLE_EXECUTION settings.') + return [] # Execute scripts on each server threads = [] executions = [] @@ -120,10 +123,9 @@ def execute(scripts, block=False, async=False): if hasattr(execution, 'log'): for operation in operations: logger.info("Executed %s" % str(operation)) - operation.log = execution.log - if operation.object_id: - # Not all backends are call with objects saved on the database - operation.save() + if operation.instance.pk: + # Not all backends are called with objects saved on the database + operation.create(execution.log) stdout = execution.log.stdout.strip() stdout and logger.debug('STDOUT %s', stdout) stderr = execution.log.stderr.strip() @@ -157,7 +159,7 @@ def collect(instance, action, **kwargs): candidates = [candidate] for candidate in candidates: # Check if a delete for candidate is in operations - delete_mock = Operation.create(backend_cls, candidate, Operation.DELETE) + delete_mock = Operation(backend_cls, candidate, Operation.DELETE) if delete_mock not in operations: # related objects with backend.model trigger save() instances.append((candidate, Operation.SAVE)) @@ -165,7 +167,7 @@ def collect(instance, action, **kwargs): # Maintain consistent state of operations based on save/delete behaviour # Prevent creating a deleted selected by deleting existing saves if iaction == Operation.DELETE: - save_mock = Operation.create(backend_cls, selected, Operation.SAVE) + save_mock = Operation(backend_cls, selected, Operation.SAVE) try: operations.remove(save_mock) except KeyError: @@ -185,7 +187,7 @@ def collect(instance, action, **kwargs): break if not execute: continue - operation = Operation.create(backend_cls, selected, iaction) + operation = Operation(backend_cls, selected, iaction) # Only schedule operations if the router gives servers to execute into servers = router.get_servers(operation, cache=route_cache) if servers: diff --git a/orchestra/contrib/orchestration/middlewares.py b/orchestra/contrib/orchestration/middlewares.py index eff520d0..3b1a96d2 100644 --- a/orchestra/contrib/orchestration/middlewares.py +++ b/orchestra/contrib/orchestration/middlewares.py @@ -8,9 +8,9 @@ from django.http.response import HttpResponseServerError from orchestra.utils.python import OrderedSet -from . import manager +from . import manager, Operation from .helpers import message_user -from .models import BackendLog, BackendOperation as Operation +from .models import BackendLog @receiver(post_save, dispatch_uid='orchestration.post_save_collector') diff --git a/orchestra/contrib/orchestration/models.py b/orchestra/contrib/orchestration/models.py index 0a16478d..14012d96 100644 --- a/orchestra/contrib/orchestration/models.py +++ b/orchestra/contrib/orchestration/models.py @@ -1,9 +1,9 @@ -import copy import socket from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.db import models +from django.utils.functional import cached_property from django.utils.module_loading import autodiscover_modules from django.utils.translation import ugettext_lazy as _ @@ -98,12 +98,6 @@ class BackendOperation(models.Model): """ Encapsulates an operation, storing its related object, the action and the backend. """ - DELETE = 'delete' - SAVE = 'save' - MONITOR = 'monitor' - EXCEEDED = 'exceeded' - RECOVERY = 'recovery' - log = models.ForeignKey('orchestration.BackendLog', related_name='operations') backend = models.CharField(_("backend"), max_length=256) action = models.CharField(_("action"), max_length=64) @@ -119,46 +113,7 @@ class BackendOperation(models.Model): def __str__(self): return '%s.%s(%s)' % (self.backend, self.action, self.instance) - def __hash__(self): - """ set() """ - backend = getattr(self, 'backend', self.backend) - return hash(backend) + hash(self.instance) + hash(self.action) - - def __eq__(self, operation): - """ set() """ - return hash(self) == hash(operation) - - @classmethod - def create(cls, backend, instance, action, servers=None): - op = cls(backend=backend.get_name(), instance=instance, action=action) - op.backend = backend - # instance should maintain any dynamic attribute until backend execution - # deep copy is prefered over copy otherwise objects will share same atributes (queryset cache) - op.instance = copy.deepcopy(instance) - op.servers = servers - return op - - @classmethod - def execute(cls, operations, async=False): - from . import manager - scripts, block = manager.generate(operations) - return manager.execute(scripts, block=block, async=async) - - @classmethod - def execute_action(cls, instance, action): - backends = ServiceBackend.get_backends(instance=instance, action=action) - 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(self.instance) - + @cached_property def backend_class(self): return ServiceBackend.get_backend(self.backend) @@ -187,7 +142,7 @@ class Route(models.Model): def __str__(self): return "%s@%s" % (self.backend, self.host) - @property + @cached_property def backend_class(self): return ServiceBackend.get_backend(self.backend) diff --git a/orchestra/contrib/orchestration/settings.py b/orchestra/contrib/orchestration/settings.py index dff4e6d8..49188751 100644 --- a/orchestra/contrib/orchestration/settings.py +++ b/orchestra/contrib/orchestration/settings.py @@ -23,3 +23,8 @@ ORCHESTRATION_ROUTER = getattr(settings, 'ORCHESTRATION_ROUTER', ORCHESTRATION_TEMP_SCRIPT_PATH = getattr(settings, 'ORCHESTRATION_TEMP_SCRIPT_PATH', '/dev/shm' ) + + +ORCHESTRATION_DISABLE_EXECUTION = getattr(settings, 'ORCHESTRATION_DISABLE_EXECUTION', + False +) diff --git a/orchestra/contrib/orchestration/tests/test_route.py b/orchestra/contrib/orchestration/tests/test_route.py index e7ef0282..f8f1f9de 100644 --- a/orchestra/contrib/orchestration/tests/test_route.py +++ b/orchestra/contrib/orchestration/tests/test_route.py @@ -1,7 +1,7 @@ from orchestra.utils.tests import BaseTestCase -from .. import backends -from ..models import Route, Server, BackendOperation as Operation +from .. import backends, Operation +from ..models import Route, Server class RouterTests(BaseTestCase): diff --git a/orchestra/contrib/orders/models.py b/orchestra/contrib/orders/models.py index 993607bf..79489ddc 100644 --- a/orchestra/contrib/orders/models.py +++ b/orchestra/contrib/orders/models.py @@ -241,6 +241,7 @@ class MetricStorage(models.Model): value = models.DecimalField(_("value"), max_digits=16, decimal_places=2) created_on = models.DateField(_("created"), auto_now_add=True) # default=lambda: timezone.now()) + # TODO time field? updated_on = models.DateTimeField(_("updated")) class Meta: diff --git a/orchestra/contrib/plans/models.py b/orchestra/contrib/plans/models.py index 8495dd3f..06fdd632 100644 --- a/orchestra/contrib/plans/models.py +++ b/orchestra/contrib/plans/models.py @@ -1,6 +1,7 @@ from django.core.validators import ValidationError from django.db import models from django.db.models import Q +from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from orchestra.core import services, accounts @@ -14,6 +15,9 @@ from . import rating class Plan(models.Model): name = models.CharField(_("name"), max_length=32, unique=True, validators=[validate_name]) verbose_name = models.CharField(_("verbose_name"), max_length=128, blank=True) + # TODO is_active = models.BooleanField(_("active"), default=True, +# help_text=_("Designates whether this account should be treated as active. " +# "Unselect this instead of deleting accounts.")) is_default = models.BooleanField(_("default"), default=False, help_text=_("Designates whether this plan is used by default or not.")) is_combinable = models.BooleanField(_("combinable"), default=True, @@ -42,6 +46,10 @@ class ContractedPlan(models.Model): def __str__(self): return str(self.plan) + @cached_property + def active(self): + return self.plan.is_active and self.account.is_active + def clean(self): if not self.pk and not self.plan.allow_multiple: if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists(): diff --git a/orchestra/contrib/resources/tasks.py b/orchestra/contrib/resources/tasks.py index 29cd5e2d..39b95e09 100644 --- a/orchestra/contrib/resources/tasks.py +++ b/orchestra/contrib/resources/tasks.py @@ -1,6 +1,6 @@ from celery import shared_task -from orchestra.contrib.orchestration.models import BackendOperation as Operation +from orchestra.contrib.orchestration import Operation from orchestra.models.utils import get_model_field_path from .backends import ServiceMonitor diff --git a/orchestra/contrib/saas/admin.py b/orchestra/contrib/saas/admin.py index 159d8094..2d24e240 100644 --- a/orchestra/contrib/saas/admin.py +++ b/orchestra/contrib/saas/admin.py @@ -12,6 +12,7 @@ from .services import SoftwareService class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin): list_display = ('name', 'service', 'display_site_domain', 'account_link', 'is_active') list_filter = ('service', 'is_active') + search_fields = ('name', 'account__username') change_readonly_fields = ('service',) plugin = SoftwareService plugin_field = 'service' diff --git a/orchestra/contrib/saas/backends/bscw.py b/orchestra/contrib/saas/backends/bscw.py index b45d0a99..742daac9 100644 --- a/orchestra/contrib/saas/backends/bscw.py +++ b/orchestra/contrib/saas/backends/bscw.py @@ -18,8 +18,9 @@ class BSCWBackend(ServiceController): self.append(textwrap.dedent("""\ if [[ $(%(bsadmin)s register %(email)s) ]]; then echo 'ValidationError: email-exists' - elif [[ $(%(bsadmin)s users -n %(username)s) ]]; then - echo 'ValidationError: username-exists' + fi + if [[ $(%(bsadmin)s users -n %(username)s) ]]; then + echo 'ValidationError: user-exists' fi""") % context ) diff --git a/orchestra/contrib/saas/models.py b/orchestra/contrib/saas/models.py index 79a70730..94088071 100644 --- a/orchestra/contrib/saas/models.py +++ b/orchestra/contrib/saas/models.py @@ -52,6 +52,8 @@ class SaaS(models.Model): return self.is_active and self.account.is_active def clean(self): + if not self.pk: + self.name = self.name.lower() self.data = self.service_instance.clean_data() def get_site_domain(self): diff --git a/orchestra/contrib/saas/services/options.py b/orchestra/contrib/saas/services/options.py index c1d956b5..3b3523d3 100644 --- a/orchestra/contrib/saas/services/options.py +++ b/orchestra/contrib/saas/services/options.py @@ -4,7 +4,7 @@ from django.core.validators import RegexValidator from django.utils.translation import ugettext_lazy as _ from orchestra import plugins -from orchestra.contrib.orchestration.models import BackendOperation as Operation +from orchestra.contrib.orchestration import Operation from orchestra.core import validators from orchestra.forms import widgets from orchestra.plugins.forms import PluginDataForm @@ -109,7 +109,7 @@ class SoftwareService(plugins.Plugin): errors = {} if 'user-exists' in log.stdout: errors['name'] = _("User with this username already exists.") - elif 'email-exists' in log.stdout: + if 'email-exists' in log.stdout: errors['email'] = _("User with this email address already exists.") if errors: raise ValidationError(errors) diff --git a/orchestra/contrib/saas/services/wordpress.py b/orchestra/contrib/saas/services/wordpress.py index a7b2a96d..58078ac5 100644 --- a/orchestra/contrib/saas/services/wordpress.py +++ b/orchestra/contrib/saas/services/wordpress.py @@ -7,6 +7,10 @@ from .options import SoftwareService, SoftwareServiceForm class WordPressForm(SoftwareServiceForm): email = forms.EmailField(label=_("Email"), widget=forms.TextInput(attrs={'size':'40'})) + + def __init__(self, *args, **kwargs): + super(WordPressForm, self).__init__(*args, **kwargs) + self.fields['name'].label = _("Site name") class WordPressDataSerializer(serializers.Serializer): @@ -18,5 +22,5 @@ class WordPressService(SoftwareService): form = WordPressForm serializer = WordPressDataSerializer icon = 'orchestra/icons/apps/WordPress.png' - site_name_base_domain = 'blogs.orchestra.lan' + site_base_domain = 'blogs.orchestra.lan' change_readonly_fileds = ('email',) diff --git a/orchestra/contrib/services/actions.py b/orchestra/contrib/services/actions.py index eeebe155..9d9556fd 100644 --- a/orchestra/contrib/services/actions.py +++ b/orchestra/contrib/services/actions.py @@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin.utils import get_object_from_url + @transaction.atomic def update_orders(modeladmin, request, queryset, extra_context=None): if not queryset: diff --git a/orchestra/contrib/services/handlers.py b/orchestra/contrib/services/handlers.py index 35137f26..f49661e6 100644 --- a/orchestra/contrib/services/handlers.py +++ b/orchestra/contrib/services/handlers.py @@ -53,7 +53,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): bool(self.matches(obj)) except Exception as exception: name = type(exception).__name__ - raise ValidationError(': '.join((name, exception))) + raise ValidationError(': '.join((name, str(exception)))) def validate_metric(self, service): try: @@ -64,7 +64,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): bool(self.get_metric(obj)) except Exception as exception: name = type(exception).__name__ - raise ValidationError(': '.join((name, exception))) + raise ValidationError(': '.join((name, str(exception)))) def get_content_type(self): if not self.model: diff --git a/orchestra/contrib/systemusers/actions.py b/orchestra/contrib/systemusers/actions.py index e656a0b7..5b815c83 100644 --- a/orchestra/contrib/systemusers/actions.py +++ b/orchestra/contrib/systemusers/actions.py @@ -4,7 +4,7 @@ from django.core.exceptions import PermissionDenied from django.utils.translation import ungettext, ugettext_lazy as _ from orchestra.admin.decorators import action_with_confirmation -from orchestra.contrib.orchestration.models import BackendOperation as Operation +from orchestra.contrib.orchestration import Operation class GrantPermissionForm(forms.Form): diff --git a/orchestra/contrib/systemusers/backends.py b/orchestra/contrib/systemusers/backends.py index ef1db43d..4c00b594 100644 --- a/orchestra/contrib/systemusers/backends.py +++ b/orchestra/contrib/systemusers/backends.py @@ -74,7 +74,7 @@ class UNIXUserBackend(ServiceController): 'shell': user.shell, 'mainuser': user.username if user.is_main else user.account.username, 'home': user.get_home(), - 'base_home': self.get_base_home(), + 'base_home': user.get_base_home(), } return replace(context, "'", '"') @@ -111,7 +111,7 @@ class Exim4Traffic(ServiceMonitor): script_executable = '/usr/bin/python' def prepare(self): - mainlog = settings.LISTS_MAILMAN_POST_LOG_PATH + mainlog = settings.SYSTEMUSERS_MAIL_LOG_PATH context = { 'current_date': self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z"), 'mainlogs': str((mainlog, mainlog+'.1')), diff --git a/orchestra/contrib/systemusers/models.py b/orchestra/contrib/systemusers/models.py index d483b9a1..95b64ed8 100644 --- a/orchestra/contrib/systemusers/models.py +++ b/orchestra/contrib/systemusers/models.py @@ -40,7 +40,6 @@ class SystemUser(models.Model): groups = models.ManyToManyField('self', blank=True, symmetrical=False, help_text=_("A new group will be created for the user. " "Which additional groups would you like them to be a member of?")) -# is_main = models.BooleanField(_("is main"), default=False) is_active = models.BooleanField(_("active"), default=True, help_text=_("Designates whether this account should be treated as active. " "Unselect this instead of deleting accounts.")) diff --git a/orchestra/contrib/systemusers/settings.py b/orchestra/contrib/systemusers/settings.py index fb9bd1f8..ee42bd08 100644 --- a/orchestra/contrib/systemusers/settings.py +++ b/orchestra/contrib/systemusers/settings.py @@ -32,6 +32,10 @@ SYSTEMUSERS_FTP_LOG_PATH = getattr(settings, 'SYSTEMUSERS_FTP_LOG_PATH', ) +SYSTEMUSERS_MAIL_LOG_PATH = getattr(settings, 'SYSTEMUSERS_MAIL_LOG_PATH', + '/var/log/exim4/mainlog' +) + SYSTEMUSERS_DEFAULT_GROUP_MEMBERS = getattr(settings, 'SYSTEMUSERS_DEFAULT_GROUP_MEMBERS', ('www-data',) ) diff --git a/orchestra/contrib/webapps/options.py b/orchestra/contrib/webapps/options.py index 84b6f0ea..0b9cae8d 100644 --- a/orchestra/contrib/webapps/options.py +++ b/orchestra/contrib/webapps/options.py @@ -55,7 +55,7 @@ class PHPAppOption(AppOption): def validate(self): super(PHPAppOption, self).validate() if self.deprecated: - php_version = self.instance.webapp.type_instance.get_php_version() + php_version = self.instance.webapp.type_instance.get_php_version_number() if php_version and php_version > self.deprecated: raise ValidationError( _("This option is deprecated since PHP version %s.") % str(self.deprecated) diff --git a/orchestra/contrib/websites/backends/apache.py b/orchestra/contrib/websites/backends/apache.py index 390bf730..d3128b4e 100644 --- a/orchestra/contrib/websites/backends/apache.py +++ b/orchestra/contrib/websites/backends/apache.py @@ -163,9 +163,9 @@ class Apache2Backend(ServiceController): return [(location, directives)] def get_ssl(self, directives): - cert = directives.get('ssl_cert') - key = directives.get('ssl_key') - ca = directives.get('ssl_ca') + cert = directives.get('ssl-cert') + key = directives.get('ssl-key') + ca = directives.get('ssl-ca') if not (cert and key): cert = [settings.WEBSITES_DEFAULT_SSL_CERT] key = [settings.WEBSITES_DEFAULT_SSL_KEY] @@ -181,11 +181,11 @@ class Apache2Backend(ServiceController): def get_security(self, directives): security = [] - for rules in directives.get('sec_rule_remove', []): + for rules in directives.get('sec-rule-remove', []): for rule in rules.value.split(): sec_rule = "SecRuleRemoveById %i" % int(rule) security.append(('', sec_rule)) - for location in directives.get('sec_engine', []): + for location in directives.get('sec-engine', []): sec_rule = textwrap.dedent("""\ SecRuleEngine off diff --git a/orchestra/contrib/websites/directives.py b/orchestra/contrib/websites/directives.py index 92af8a3b..bb22f304 100644 --- a/orchestra/contrib/websites/directives.py +++ b/orchestra/contrib/websites/directives.py @@ -80,7 +80,7 @@ class Proxy(SiteDirective): class ErrorDocument(SiteDirective): - name = 'error_document' + name = 'error-document' verbose_name = _("ErrorDocumentRoot") help_text = _("<error code> <URL/path/message>
" " 500 http://foo.example.com/cgi-bin/tester
" @@ -93,7 +93,7 @@ class ErrorDocument(SiteDirective): class SSLCA(SiteDirective): - name = 'ssl_ca' + name = 'ssl-ca' verbose_name = _("SSL CA") help_text = _("Filesystem path of the CA certificate file.") regex = r'^[^ ]+$' @@ -102,7 +102,7 @@ class SSLCA(SiteDirective): class SSLCert(SiteDirective): - name = 'ssl_cert' + name = 'ssl-cert' verbose_name = _("SSL cert") help_text = _("Filesystem path of the certificate file.") regex = r'^[^ ]+$' @@ -111,7 +111,7 @@ class SSLCert(SiteDirective): class SSLKey(SiteDirective): - name = 'ssl_key' + name = 'ssl-key' verbose_name = _("SSL key") help_text = _("Filesystem path of the key file.") regex = r'^[^ ]+$' @@ -120,7 +120,7 @@ class SSLKey(SiteDirective): class SecRuleRemove(SiteDirective): - name = 'sec_rule_remove' + name = 'sec-rule-remove' verbose_name = _("SecRuleRemoveById") help_text = _("Space separated ModSecurity rule IDs.") regex = r'^[0-9\s]+$' @@ -128,7 +128,7 @@ class SecRuleRemove(SiteDirective): class SecEngine(SiteDirective): - name = 'sec_engine' + name = 'sec-engine' verbose_name = _("SecRuleEngine Off") help_text = _("URL path with disabled modsecurity engine.") regex = r'^/[^ ]*$' diff --git a/orchestra/contrib/websites/models.py b/orchestra/contrib/websites/models.py index 407bbbb2..d1985b90 100644 --- a/orchestra/contrib/websites/models.py +++ b/orchestra/contrib/websites/models.py @@ -46,6 +46,10 @@ class Website(models.Model): context = self.get_settings_context() return settings.WEBSITES_UNIQUE_NAME_FORMAT % context + @cached_property + def active(self): + return self.is_active and self.account.is_active + def get_settings_context(self): """ format settings strings """ return {