Split Operation and BackendOperation

This commit is contained in:
Marc Aymerich 2015-04-07 15:14:49 +00:00
parent 4a6d29ebd7
commit bac2b94d70
34 changed files with 173 additions and 114 deletions

16
TODO.md
View File

@ -11,8 +11,6 @@
* env vars instead of multiple settings files: https://devcenter.heroku.com/articles/config-vars ? * 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 * backend logs with hal logo
* LAST version of this shit http://wkhtmltopdf.org/downloads.h otml * 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) * autoexpand mailbox.filter according to filtering options (js)
* allow empty metric pack for default rates? changes on rating algo * 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 # DOMINI REGISTRE MIGRATION SCRIPTS
# lines too long on invoice, double lines or cut, and make margin wider # 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. # display subline links on billlines, to show that they exists.
* update service orders on a celery task? because it take alot * 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 # 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. * 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 * 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. * create service help templates based on urlqwargs with the most basic services.
# TDOO Base price: domini propi (all domains) + extra for other domains # TDOO Base price: domini propi (all domains) + extra for other domains
# IMPORTANT op.instance = copy.deepcopy(instance) ValueError: Cannot assign "<SaaS: blog@WordPressService>": "SaaS" instance isn't saved in the database.
# Separate operation from models !! BackendOperation and Operation
Translation 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? # 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 * 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 * read https://docs.djangoproject.com/en/dev/releases/1.8/ and fix deprecation warnings
* remove admin object display_links , like contents webapps * remove admin object display_links , like contents webapps
@ -277,4 +274,5 @@ https://code.djangoproject.com/ticket/24576
* migrate to DRF3.x * 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

View File

@ -244,7 +244,7 @@ class ChangePasswordAdminMixin(object):
'save_as': False, 'save_as': False,
'show_save': True, 'show_save': True,
} }
context.update(admin.site.each_context()) context.update(admin.site.each_context(request))
return TemplateResponse(request, return TemplateResponse(request,
self.change_user_password_template, self.change_user_password_template,
context, current_app=self.admin_site.name) context, current_app=self.admin_site.name)

View File

@ -6,7 +6,7 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.orchestration.middlewares import OperationsMiddleware 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.core import services, accounts
from orchestra.utils import send_email_template from orchestra.utils import send_email_template

View File

@ -38,7 +38,7 @@ class MySQLBackend(ServiceController):
if database.type != database.MYSQL: if database.type != database.MYSQL:
return return
context = self.get_context(database) 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) self.append("mysql mysql -e 'DELETE FROM db WHERE db = \"%(database)s\";'" % context)
def commit(self): def commit(self):
@ -76,7 +76,7 @@ class MySQLUserBackend(ServiceController):
return return
context = self.get_context(user) context = self.get_context(user)
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
mysql -e 'DROP USER "%(username)s"@"%(host)s";' \ mysql -e 'DROP USER "%(username)s"@"%(host)s";' || exit_code=$? \
""") % context """) % context
) )

View File

@ -4,7 +4,7 @@ import textwrap
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.orchestration import ServiceController, replace 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 from . import settings
@ -41,10 +41,11 @@ class Bind9MasterDomainBackend(ServiceController):
def update_conf(self, context): def update_conf(self, context):
self.append(textwrap.dedent("""\ 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' \\ sed -i -e '/zone\s\s*"%(name)s".*/,/^\s*};/d' \\
-e 'N; /^\s*\\n\s*$/d; P; D' %(conf_path)s -e 'N; /^\s*\\n\s*$/d; P; D' %(conf_path)s
echo '%(conf)s' >> %(conf_path)s echo "${conf}" >> %(conf_path)s
UPDATED=1 UPDATED=1
}""") % context }""") % context
) )
@ -80,7 +81,7 @@ class Bind9MasterDomainBackend(ServiceController):
def get_servers(self, domain, backend): def get_servers(self, domain, backend):
""" Get related server IPs from registered backend routes """ """ Get related server IPs from registered backend routes """
from orchestra.contrib.orchestration.manager import router from orchestra.contrib.orchestration.manager import router
operation = Operation.create(backend, domain, Operation.SAVE) operation = Operation(backend, domain, Operation.SAVE)
servers = [] servers = []
for server in router.get_servers(operation): for server in router.get_servers(operation):
servers.append(server.get_ip()) servers.append(server.get_ip())

View File

@ -107,7 +107,8 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
'email', 'account_link', 'domain_link', 'display_mailboxes', 'display_forward', 'email', 'account_link', 'domain_link', 'display_mailboxes', 'display_forward',
) )
list_filter = (HasMailboxListFilter, HasForwardListFilter) 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] inlines = [AutoresponseInline]
search_fields = ('name', 'domain__name', 'forward', 'mailboxes__name', 'account__username') search_fields = ('name', 'domain__name', 'forward', 'mailboxes__name', 'account__username')
readonly_fields = ('account_link', 'domain_link', 'email_link') readonly_fields = ('account_link', 'domain_link', 'email_link')

View File

@ -17,16 +17,14 @@ class Mailbox(models.Model):
name = models.CharField(_("name"), max_length=64, unique=True, name = models.CharField(_("name"), max_length=64, unique=True,
help_text=_("Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only."), help_text=_("Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only."),
validators=[ validators=[
RegexValidator(r'^[\w.@+-]+$', _("Enter a valid mailbox name.")) RegexValidator(r'^[\w.@+-]+$', _("Enter a valid mailbox name.")),
]) ])
password = models.CharField(_("password"), max_length=128) password = models.CharField(_("password"), max_length=128)
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='mailboxes') related_name='mailboxes')
filtering = models.CharField(max_length=16, filtering = models.CharField(max_length=16,
default=settings.MAILBOXES_MAILBOX_DEFAULT_FILTERING, default=settings.MAILBOXES_MAILBOX_DEFAULT_FILTERING,
choices=[ choices=[(k, v[0]) for k,v in settings.MAILBOXES_MAILBOX_FILTERINGS.items()])
(k, v[0]) for k,v in settings.MAILBOXES_MAILBOX_FILTERINGS.items()
])
custom_filtering = models.TextField(_("filtering"), blank=True, custom_filtering = models.TextField(_("filtering"), blank=True,
validators=[validators.validate_sieve], validators=[validators.validate_sieve],
help_text=_("Arbitrary email filtering in sieve language. " help_text=_("Arbitrary email filtering in sieve language. "
@ -80,7 +78,7 @@ class Mailbox(models.Model):
def get_local_address(self): def get_local_address(self):
if not settings.MAILBOXES_LOCAL_ADDRESS_DOMAIN: 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)) return '@'.join((self.name, settings.MAILBOXES_LOCAL_ADDRESS_DOMAIN))

View File

@ -42,7 +42,7 @@ class MiscServiceAdmin(ExtendedModelAdmin):
num_instances.admin_order_field = 'instances__count' num_instances.admin_order_field = 'instances__count'
def get_queryset(self, request): 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)) return qs.annotate(models.Count('instances', distinct=True))
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):

View File

@ -1 +1,60 @@
import copy
from .backends import ServiceBackend, ServiceController, replace 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,
)

View File

@ -1,10 +1,12 @@
from django.contrib import admin from django.contrib import admin, messages
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin.html import monospace_format from orchestra.admin.html import monospace_format
from orchestra.admin.utils import admin_link, admin_date, admin_colored from orchestra.admin.utils import admin_link, admin_date, admin_colored
from . import settings
from .backends import ServiceBackend from .backends import ServiceBackend
from .models import Server, Route, BackendLog, BackendOperation from .models import Server, Route, BackendLog, BackendOperation
from .widgets import RouteBackendSelect from .widgets import RouteBackendSelect
@ -66,6 +68,19 @@ class RouteAdmin(admin.ModelAdmin):
if obj: if obj:
form.base_fields['backend'].help_text = self.BACKEND_HELP_TEXT.get(obj.backend, '') form.base_fields['backend'].help_text = self.BACKEND_HELP_TEXT.get(obj.backend, '')
return form return form
def show_orchestration_disabled(self, request):
if settings.ORCHESTRATION_DISABLE_EXECUTION:
msg = _("Orchestration execution is disabled by <tt>ORCHESTRATION_DISABLE_EXECUTION</tt> 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): class BackendOperationInline(admin.TabularInline):

View File

@ -10,13 +10,13 @@ class Command(BaseCommand):
help = 'Runs orchestration backends.' help = 'Runs orchestration backends.'
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument('model', nargs='+', parser.add_argument('model',
help='App label of an application to synchronize the help='Label of a model to execute the orchestration.')
parser.add_argument('query', nargs='?', parser.add_argument('query', nargs='*',
help='Query arguments for filter().') help='Query arguments for filter().')
parser.add_argument('--noinput', action='store_false', dest='interactive', default=True, parser.add_argument('--noinput', action='store_false', dest='interactive', default=True,
help='Tells Django to NOT prompt the user for input of any kind.') 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".') default='save', help='Executes action. Defaults to "save".')
def handle(self, *args, **options): def handle(self, *args, **options):

View File

@ -9,10 +9,10 @@ from django.core.mail import mail_admins
from orchestra.utils.python import import_class from orchestra.utils.python import import_class
from . import settings from . import settings, Operation
from .backends import ServiceBackend from .backends import ServiceBackend
from .helpers import send_report from .helpers import send_report
from .models import BackendLog, BackendOperation as Operation from .models import BackendLog
from .signals import pre_action, post_action from .signals import pre_action, post_action
@ -95,6 +95,9 @@ def generate(operations):
def execute(scripts, block=False, async=False): def execute(scripts, block=False, async=False):
""" executes the operations on the servers """ """ 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 # Execute scripts on each server
threads = [] threads = []
executions = [] executions = []
@ -120,10 +123,9 @@ def execute(scripts, block=False, async=False):
if hasattr(execution, 'log'): if hasattr(execution, 'log'):
for operation in operations: for operation in operations:
logger.info("Executed %s" % str(operation)) logger.info("Executed %s" % str(operation))
operation.log = execution.log if operation.instance.pk:
if operation.object_id: # Not all backends are called with objects saved on the database
# Not all backends are call with objects saved on the database operation.create(execution.log)
operation.save()
stdout = execution.log.stdout.strip() stdout = execution.log.stdout.strip()
stdout and logger.debug('STDOUT %s', stdout) stdout and logger.debug('STDOUT %s', stdout)
stderr = execution.log.stderr.strip() stderr = execution.log.stderr.strip()
@ -157,7 +159,7 @@ def collect(instance, action, **kwargs):
candidates = [candidate] candidates = [candidate]
for candidate in candidates: for candidate in candidates:
# Check if a delete for candidate is in operations # 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: if delete_mock not in operations:
# related objects with backend.model trigger save() # related objects with backend.model trigger save()
instances.append((candidate, Operation.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 # Maintain consistent state of operations based on save/delete behaviour
# Prevent creating a deleted selected by deleting existing saves # Prevent creating a deleted selected by deleting existing saves
if iaction == Operation.DELETE: if iaction == Operation.DELETE:
save_mock = Operation.create(backend_cls, selected, Operation.SAVE) save_mock = Operation(backend_cls, selected, Operation.SAVE)
try: try:
operations.remove(save_mock) operations.remove(save_mock)
except KeyError: except KeyError:
@ -185,7 +187,7 @@ def collect(instance, action, **kwargs):
break break
if not execute: if not execute:
continue 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 # Only schedule operations if the router gives servers to execute into
servers = router.get_servers(operation, cache=route_cache) servers = router.get_servers(operation, cache=route_cache)
if servers: if servers:

View File

@ -8,9 +8,9 @@ from django.http.response import HttpResponseServerError
from orchestra.utils.python import OrderedSet from orchestra.utils.python import OrderedSet
from . import manager from . import manager, Operation
from .helpers import message_user 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') @receiver(post_save, dispatch_uid='orchestration.post_save_collector')

View File

@ -1,9 +1,9 @@
import copy
import socket import socket
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.utils.functional import cached_property
from django.utils.module_loading import autodiscover_modules from django.utils.module_loading import autodiscover_modules
from django.utils.translation import ugettext_lazy as _ 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. 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') log = models.ForeignKey('orchestration.BackendLog', related_name='operations')
backend = models.CharField(_("backend"), max_length=256) backend = models.CharField(_("backend"), max_length=256)
action = models.CharField(_("action"), max_length=64) action = models.CharField(_("action"), max_length=64)
@ -119,46 +113,7 @@ class BackendOperation(models.Model):
def __str__(self): def __str__(self):
return '%s.%s(%s)' % (self.backend, self.action, self.instance) return '%s.%s(%s)' % (self.backend, self.action, self.instance)
def __hash__(self): @cached_property
""" 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)
def backend_class(self): def backend_class(self):
return ServiceBackend.get_backend(self.backend) return ServiceBackend.get_backend(self.backend)
@ -187,7 +142,7 @@ class Route(models.Model):
def __str__(self): def __str__(self):
return "%s@%s" % (self.backend, self.host) return "%s@%s" % (self.backend, self.host)
@property @cached_property
def backend_class(self): def backend_class(self):
return ServiceBackend.get_backend(self.backend) return ServiceBackend.get_backend(self.backend)

View File

@ -23,3 +23,8 @@ ORCHESTRATION_ROUTER = getattr(settings, 'ORCHESTRATION_ROUTER',
ORCHESTRATION_TEMP_SCRIPT_PATH = getattr(settings, 'ORCHESTRATION_TEMP_SCRIPT_PATH', ORCHESTRATION_TEMP_SCRIPT_PATH = getattr(settings, 'ORCHESTRATION_TEMP_SCRIPT_PATH',
'/dev/shm' '/dev/shm'
) )
ORCHESTRATION_DISABLE_EXECUTION = getattr(settings, 'ORCHESTRATION_DISABLE_EXECUTION',
False
)

View File

@ -1,7 +1,7 @@
from orchestra.utils.tests import BaseTestCase from orchestra.utils.tests import BaseTestCase
from .. import backends from .. import backends, Operation
from ..models import Route, Server, BackendOperation as Operation from ..models import Route, Server
class RouterTests(BaseTestCase): class RouterTests(BaseTestCase):

View File

@ -241,6 +241,7 @@ class MetricStorage(models.Model):
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2) value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
created_on = models.DateField(_("created"), auto_now_add=True) created_on = models.DateField(_("created"), auto_now_add=True)
# default=lambda: timezone.now()) # default=lambda: timezone.now())
# TODO time field?
updated_on = models.DateTimeField(_("updated")) updated_on = models.DateTimeField(_("updated"))
class Meta: class Meta:

View File

@ -1,6 +1,7 @@
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.core import services, accounts from orchestra.core import services, accounts
@ -14,6 +15,9 @@ from . import rating
class Plan(models.Model): class Plan(models.Model):
name = models.CharField(_("name"), max_length=32, unique=True, validators=[validate_name]) name = models.CharField(_("name"), max_length=32, unique=True, validators=[validate_name])
verbose_name = models.CharField(_("verbose_name"), max_length=128, blank=True) 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, is_default = models.BooleanField(_("default"), default=False,
help_text=_("Designates whether this plan is used by default or not.")) help_text=_("Designates whether this plan is used by default or not."))
is_combinable = models.BooleanField(_("combinable"), default=True, is_combinable = models.BooleanField(_("combinable"), default=True,
@ -42,6 +46,10 @@ class ContractedPlan(models.Model):
def __str__(self): def __str__(self):
return str(self.plan) return str(self.plan)
@cached_property
def active(self):
return self.plan.is_active and self.account.is_active
def clean(self): def clean(self):
if not self.pk and not self.plan.allow_multiple: if not self.pk and not self.plan.allow_multiple:
if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists(): if ContractedPlan.objects.filter(plan=self.plan, account=self.account).exists():

View File

@ -1,6 +1,6 @@
from celery import shared_task 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 orchestra.models.utils import get_model_field_path
from .backends import ServiceMonitor from .backends import ServiceMonitor

View File

@ -12,6 +12,7 @@ from .services import SoftwareService
class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin): class SaaSAdmin(SelectPluginAdminMixin, ChangePasswordAdminMixin, AccountAdminMixin, ExtendedModelAdmin):
list_display = ('name', 'service', 'display_site_domain', 'account_link', 'is_active') list_display = ('name', 'service', 'display_site_domain', 'account_link', 'is_active')
list_filter = ('service', 'is_active') list_filter = ('service', 'is_active')
search_fields = ('name', 'account__username')
change_readonly_fields = ('service',) change_readonly_fields = ('service',)
plugin = SoftwareService plugin = SoftwareService
plugin_field = 'service' plugin_field = 'service'

View File

@ -18,8 +18,9 @@ class BSCWBackend(ServiceController):
self.append(textwrap.dedent("""\ self.append(textwrap.dedent("""\
if [[ $(%(bsadmin)s register %(email)s) ]]; then if [[ $(%(bsadmin)s register %(email)s) ]]; then
echo 'ValidationError: email-exists' echo 'ValidationError: email-exists'
elif [[ $(%(bsadmin)s users -n %(username)s) ]]; then fi
echo 'ValidationError: username-exists' if [[ $(%(bsadmin)s users -n %(username)s) ]]; then
echo 'ValidationError: user-exists'
fi""") % context fi""") % context
) )

View File

@ -52,6 +52,8 @@ class SaaS(models.Model):
return self.is_active and self.account.is_active return self.is_active and self.account.is_active
def clean(self): def clean(self):
if not self.pk:
self.name = self.name.lower()
self.data = self.service_instance.clean_data() self.data = self.service_instance.clean_data()
def get_site_domain(self): def get_site_domain(self):

View File

@ -4,7 +4,7 @@ from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra import plugins 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.core import validators
from orchestra.forms import widgets from orchestra.forms import widgets
from orchestra.plugins.forms import PluginDataForm from orchestra.plugins.forms import PluginDataForm
@ -109,7 +109,7 @@ class SoftwareService(plugins.Plugin):
errors = {} errors = {}
if 'user-exists' in log.stdout: if 'user-exists' in log.stdout:
errors['name'] = _("User with this username already exists.") 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.") errors['email'] = _("User with this email address already exists.")
if errors: if errors:
raise ValidationError(errors) raise ValidationError(errors)

View File

@ -7,6 +7,10 @@ from .options import SoftwareService, SoftwareServiceForm
class WordPressForm(SoftwareServiceForm): class WordPressForm(SoftwareServiceForm):
email = forms.EmailField(label=_("Email"), widget=forms.TextInput(attrs={'size':'40'})) 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): class WordPressDataSerializer(serializers.Serializer):
@ -18,5 +22,5 @@ class WordPressService(SoftwareService):
form = WordPressForm form = WordPressForm
serializer = WordPressDataSerializer serializer = WordPressDataSerializer
icon = 'orchestra/icons/apps/WordPress.png' icon = 'orchestra/icons/apps/WordPress.png'
site_name_base_domain = 'blogs.orchestra.lan' site_base_domain = 'blogs.orchestra.lan'
change_readonly_fileds = ('email',) change_readonly_fileds = ('email',)

View File

@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _
from orchestra.admin.utils import get_object_from_url from orchestra.admin.utils import get_object_from_url
@transaction.atomic @transaction.atomic
def update_orders(modeladmin, request, queryset, extra_context=None): def update_orders(modeladmin, request, queryset, extra_context=None):
if not queryset: if not queryset:

View File

@ -53,7 +53,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
bool(self.matches(obj)) bool(self.matches(obj))
except Exception as exception: except Exception as exception:
name = type(exception).__name__ name = type(exception).__name__
raise ValidationError(': '.join((name, exception))) raise ValidationError(': '.join((name, str(exception))))
def validate_metric(self, service): def validate_metric(self, service):
try: try:
@ -64,7 +64,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
bool(self.get_metric(obj)) bool(self.get_metric(obj))
except Exception as exception: except Exception as exception:
name = type(exception).__name__ name = type(exception).__name__
raise ValidationError(': '.join((name, exception))) raise ValidationError(': '.join((name, str(exception))))
def get_content_type(self): def get_content_type(self):
if not self.model: if not self.model:

View File

@ -4,7 +4,7 @@ from django.core.exceptions import PermissionDenied
from django.utils.translation import ungettext, ugettext_lazy as _ from django.utils.translation import ungettext, ugettext_lazy as _
from orchestra.admin.decorators import action_with_confirmation from orchestra.admin.decorators import action_with_confirmation
from orchestra.contrib.orchestration.models import BackendOperation as Operation from orchestra.contrib.orchestration import Operation
class GrantPermissionForm(forms.Form): class GrantPermissionForm(forms.Form):

View File

@ -74,7 +74,7 @@ class UNIXUserBackend(ServiceController):
'shell': user.shell, 'shell': user.shell,
'mainuser': user.username if user.is_main else user.account.username, 'mainuser': user.username if user.is_main else user.account.username,
'home': user.get_home(), 'home': user.get_home(),
'base_home': self.get_base_home(), 'base_home': user.get_base_home(),
} }
return replace(context, "'", '"') return replace(context, "'", '"')
@ -111,7 +111,7 @@ class Exim4Traffic(ServiceMonitor):
script_executable = '/usr/bin/python' script_executable = '/usr/bin/python'
def prepare(self): def prepare(self):
mainlog = settings.LISTS_MAILMAN_POST_LOG_PATH mainlog = settings.SYSTEMUSERS_MAIL_LOG_PATH
context = { context = {
'current_date': self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z"), 'current_date': self.current_date.strftime("%Y-%m-%d %H:%M:%S %Z"),
'mainlogs': str((mainlog, mainlog+'.1')), 'mainlogs': str((mainlog, mainlog+'.1')),

View File

@ -40,7 +40,6 @@ class SystemUser(models.Model):
groups = models.ManyToManyField('self', blank=True, symmetrical=False, groups = models.ManyToManyField('self', blank=True, symmetrical=False,
help_text=_("A new group will be created for the user. " help_text=_("A new group will be created for the user. "
"Which additional groups would you like them to be a member of?")) "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, is_active = models.BooleanField(_("active"), default=True,
help_text=_("Designates whether this account should be treated as active. " help_text=_("Designates whether this account should be treated as active. "
"Unselect this instead of deleting accounts.")) "Unselect this instead of deleting accounts."))

View File

@ -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', SYSTEMUSERS_DEFAULT_GROUP_MEMBERS = getattr(settings, 'SYSTEMUSERS_DEFAULT_GROUP_MEMBERS',
('www-data',) ('www-data',)
) )

View File

@ -55,7 +55,7 @@ class PHPAppOption(AppOption):
def validate(self): def validate(self):
super(PHPAppOption, self).validate() super(PHPAppOption, self).validate()
if self.deprecated: 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: if php_version and php_version > self.deprecated:
raise ValidationError( raise ValidationError(
_("This option is deprecated since PHP version %s.") % str(self.deprecated) _("This option is deprecated since PHP version %s.") % str(self.deprecated)

View File

@ -163,9 +163,9 @@ class Apache2Backend(ServiceController):
return [(location, directives)] return [(location, directives)]
def get_ssl(self, directives): def get_ssl(self, directives):
cert = directives.get('ssl_cert') cert = directives.get('ssl-cert')
key = directives.get('ssl_key') key = directives.get('ssl-key')
ca = directives.get('ssl_ca') ca = directives.get('ssl-ca')
if not (cert and key): if not (cert and key):
cert = [settings.WEBSITES_DEFAULT_SSL_CERT] cert = [settings.WEBSITES_DEFAULT_SSL_CERT]
key = [settings.WEBSITES_DEFAULT_SSL_KEY] key = [settings.WEBSITES_DEFAULT_SSL_KEY]
@ -181,11 +181,11 @@ class Apache2Backend(ServiceController):
def get_security(self, directives): def get_security(self, directives):
security = [] security = []
for rules in directives.get('sec_rule_remove', []): for rules in directives.get('sec-rule-remove', []):
for rule in rules.value.split(): for rule in rules.value.split():
sec_rule = "SecRuleRemoveById %i" % int(rule) sec_rule = "SecRuleRemoveById %i" % int(rule)
security.append(('', sec_rule)) security.append(('', sec_rule))
for location in directives.get('sec_engine', []): for location in directives.get('sec-engine', []):
sec_rule = textwrap.dedent("""\ sec_rule = textwrap.dedent("""\
<Location %s> <Location %s>
SecRuleEngine off SecRuleEngine off

View File

@ -80,7 +80,7 @@ class Proxy(SiteDirective):
class ErrorDocument(SiteDirective): class ErrorDocument(SiteDirective):
name = 'error_document' name = 'error-document'
verbose_name = _("ErrorDocumentRoot") verbose_name = _("ErrorDocumentRoot")
help_text = _("&lt;error code&gt; &lt;URL/path/message&gt;<br>" help_text = _("&lt;error code&gt; &lt;URL/path/message&gt;<br>"
"<tt>&nbsp;500 http://foo.example.com/cgi-bin/tester</tt><br>" "<tt>&nbsp;500 http://foo.example.com/cgi-bin/tester</tt><br>"
@ -93,7 +93,7 @@ class ErrorDocument(SiteDirective):
class SSLCA(SiteDirective): class SSLCA(SiteDirective):
name = 'ssl_ca' name = 'ssl-ca'
verbose_name = _("SSL CA") verbose_name = _("SSL CA")
help_text = _("Filesystem path of the CA certificate file.") help_text = _("Filesystem path of the CA certificate file.")
regex = r'^[^ ]+$' regex = r'^[^ ]+$'
@ -102,7 +102,7 @@ class SSLCA(SiteDirective):
class SSLCert(SiteDirective): class SSLCert(SiteDirective):
name = 'ssl_cert' name = 'ssl-cert'
verbose_name = _("SSL cert") verbose_name = _("SSL cert")
help_text = _("Filesystem path of the certificate file.") help_text = _("Filesystem path of the certificate file.")
regex = r'^[^ ]+$' regex = r'^[^ ]+$'
@ -111,7 +111,7 @@ class SSLCert(SiteDirective):
class SSLKey(SiteDirective): class SSLKey(SiteDirective):
name = 'ssl_key' name = 'ssl-key'
verbose_name = _("SSL key") verbose_name = _("SSL key")
help_text = _("Filesystem path of the key file.") help_text = _("Filesystem path of the key file.")
regex = r'^[^ ]+$' regex = r'^[^ ]+$'
@ -120,7 +120,7 @@ class SSLKey(SiteDirective):
class SecRuleRemove(SiteDirective): class SecRuleRemove(SiteDirective):
name = 'sec_rule_remove' name = 'sec-rule-remove'
verbose_name = _("SecRuleRemoveById") verbose_name = _("SecRuleRemoveById")
help_text = _("Space separated ModSecurity rule IDs.") help_text = _("Space separated ModSecurity rule IDs.")
regex = r'^[0-9\s]+$' regex = r'^[0-9\s]+$'
@ -128,7 +128,7 @@ class SecRuleRemove(SiteDirective):
class SecEngine(SiteDirective): class SecEngine(SiteDirective):
name = 'sec_engine' name = 'sec-engine'
verbose_name = _("SecRuleEngine Off") verbose_name = _("SecRuleEngine Off")
help_text = _("URL path with disabled modsecurity engine.") help_text = _("URL path with disabled modsecurity engine.")
regex = r'^/[^ ]*$' regex = r'^/[^ ]*$'

View File

@ -46,6 +46,10 @@ class Website(models.Model):
context = self.get_settings_context() context = self.get_settings_context()
return settings.WEBSITES_UNIQUE_NAME_FORMAT % 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): def get_settings_context(self):
""" format settings strings """ """ format settings strings """
return { return {