diff --git a/TODO.md b/TODO.md index 7b1e1e6b..403fd06c 100644 --- a/TODO.md +++ b/TODO.md @@ -346,10 +346,12 @@ TODO mount the filesystem with "nosuid" option # Deprecate restart/start/stop services (do touch wsgi.py and fuck celery) # orchestrate async stdout stderr (inspired on pangea managemengt commands) -# orchestra-beat support for uwsgi cron - -# message.log if len() == 1: return changeform +orchestra-beat support for uwsgi cron make django admin taskstate uncollapse fucking traceback, ( if exists ?) # form for custom message on admin save "comment & save"? + +# backend.context and backned.instance provided when an action is called? like forms.cleaned_data: do it on manager.generation(backend.context = backend.get_context()) or in backend.__getattr__ ? also backend.head,tail,content switching on manager.generate()? + +# replace return_code by exit_code everywhere diff --git a/orchestra/contrib/mailer/README.md b/orchestra/contrib/mailer/README.md index 8c442f35..1b7eae19 100644 --- a/orchestra/contrib/mailer/README.md +++ b/orchestra/contrib/mailer/README.md @@ -1,4 +1,4 @@ -This is a simlified clone of [django-mailer](https://github.com/pinax/django-mailer). +This is a simplified clone of [django-mailer](https://github.com/pinax/django-mailer). Using `orchestra.contrib.mailer.backends.EmailBackend` as your email backend will have the following effects: * E-mails sent with Django's `send_mass_mail()` will be queued and sent by an out-of-band perioic task. diff --git a/orchestra/contrib/orchestration/__init__.py b/orchestra/contrib/orchestration/__init__.py index 86219de0..57b18b26 100644 --- a/orchestra/contrib/orchestration/__init__.py +++ b/orchestra/contrib/orchestration/__init__.py @@ -1,3 +1,4 @@ +import collections import copy from .backends import ServiceBackend, ServiceController, replace @@ -35,19 +36,31 @@ class Operation(): @classmethod def execute(cls, operations, serialize=False, async=None): from . import manager - scripts, oserialize = manager.generate(operations) - return manager.execute(scripts, serialize=(serialize or oserialize), async=async) + scripts, backend_serialize = manager.generate(operations) + return manager.execute(scripts, serialize=(serialize or backend_serialize), 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] + def create_for_action(cls, instances, action): + if not isinstance(instances, collections.Iterable): + instances = [instances] + operations = [] + for instance in instances: + backends = ServiceBackend.get_backends(instance=instance, action=action) + for backend_cls in backends: + operations.append( + cls(backend_cls, instance, action) + ) + return operations + + @classmethod + def execute_action(cls, instances, action): + """ instances can be an object or an iterable for batch processing """ + operations = cls.create_for_action(instances, action) return cls.execute(operations) def preload_context(self): """ - Heuristic - Running get_context will prevent most of related objects do not exist errors + 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'): diff --git a/orchestra/contrib/orchestration/management/commands/orchestrate.py b/orchestra/contrib/orchestration/management/commands/orchestrate.py index defeb4c4..3eb204d6 100644 --- a/orchestra/contrib/orchestration/management/commands/orchestrate.py +++ b/orchestra/contrib/orchestration/management/commands/orchestrate.py @@ -1,3 +1,4 @@ +import time from django.core.management.base import BaseCommand, CommandError from django.apps import apps @@ -96,9 +97,22 @@ class Command(BaseCommand): return break if not dry: - logs = manager.execute(scripts, serialize=serialize) - for log in logs: - self.stdout.write(log.stdout) - self.stderr.write(log.stderr) + logs = manager.execute(scripts, serialize=serialize, async=True) + running = list(logs) + stdout = 0 + stderr = 0 + while running: + for log in running: + cstdout = len(log.stdout) + cstderr = len(log.stderr) + if cstdout > stdout: + self.stdout.write(log.stdout[stdout:]) + stdout = cstdout + if cstderr > stderr: + self.stderr.write(log.stderr[stderr:]) + stderr = cstderr + if log.has_finished: + running.remove(log) + time.sleep(0.1) for log in logs: self.stdout.write(' '.join((log.backend, log.state))) diff --git a/orchestra/contrib/orchestration/manager.py b/orchestra/contrib/orchestration/manager.py index 9bcafb26..c46de751 100644 --- a/orchestra/contrib/orchestration/manager.py +++ b/orchestra/contrib/orchestration/manager.py @@ -123,9 +123,14 @@ def execute(scripts, serialize=False, async=None): 'async': async, } log = backend.create_log(*args, **kwargs) + # TODO Perform this shit outside of the current transaction in a non-hacky way + #t = threading.Thread(target=backend.create_log, args=args, kwargs=kwargs) + #t.start() + #log = t.join() + # End of hack kwargs['log'] = log task = keep_log(backend.execute, log, operations) - logger.debug('%s is going to be executed on %s' % (backend, route.host)) + logger.debug('%s is going to be executed on %s.' % (backend, route.host)) if serialize: # Execute one backend at a time, no need for threads task(*args, **kwargs) @@ -181,7 +186,7 @@ def collect(instance, action, **kwargs): if update_fields is not None: # TODO remove this, django does not execute post_save if update_fields=[]... # Maybe open a ticket at Djangoproject ? - # "update_fileds=[]" is a convention for explicitly executing backend + # INITIAL INTENTION: "update_fileds=[]" is a convention for explicitly executing backend # i.e. account.disable() if update_fields != []: execute = False diff --git a/orchestra/contrib/orchestration/models.py b/orchestra/contrib/orchestration/models.py index 4090fc7a..b8281b78 100644 --- a/orchestra/contrib/orchestration/models.py +++ b/orchestra/contrib/orchestration/models.py @@ -92,8 +92,13 @@ class BackendLog(models.Model): def execution_time(self): return (self.updated_at-self.created_at).total_seconds() + @property + def has_finished(self): + return self.state not in (self.STARTED, self.RECEIVED) + def backend_class(self): return ServiceBackend.get_backend(self.backend) + class BackendOperation(models.Model): diff --git a/orchestra/contrib/plans/admin.py b/orchestra/contrib/plans/admin.py index 7bb9440f..1ae7de53 100644 --- a/orchestra/contrib/plans/admin.py +++ b/orchestra/contrib/plans/admin.py @@ -28,7 +28,7 @@ class ContractedPlanAdmin(AccountAdminMixin, admin.ModelAdmin): list_display = ('plan', 'account_link') list_filter = ('plan__name',) list_select_related = ('plan', 'account') - + search_fields = ('account__username', 'plan__name', 'id') admin.site.register(Plan, PlanAdmin) admin.site.register(ContractedPlan, ContractedPlanAdmin) diff --git a/orchestra/contrib/plans/apps.py b/orchestra/contrib/plans/apps.py index 153501c9..45d30773 100644 --- a/orchestra/contrib/plans/apps.py +++ b/orchestra/contrib/plans/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -from orchestra.core import administration, accounts +from orchestra.core import administration, accounts, services from orchestra.core.translations import ModelTranslation @@ -11,5 +11,6 @@ class PlansConfig(AppConfig): def ready(self): from .models import Plan, ContractedPlan accounts.register(ContractedPlan, icon='ContractedPack.png') + services.register(ContractedPlan, menu=False, dashboard=False) administration.register(Plan, icon='Pack.png') ModelTranslation.register(Plan, ('verbose_name',)) diff --git a/orchestra/contrib/plans/models.py b/orchestra/contrib/plans/models.py index 4710f252..4e5d68f0 100644 --- a/orchestra/contrib/plans/models.py +++ b/orchestra/contrib/plans/models.py @@ -68,9 +68,11 @@ class RateQuerySet(models.QuerySet): class Rate(models.Model): STEP_PRICE = 'STEP_PRICE' MATCH_PRICE = 'MATCH_PRICE' + BEST_PRICE = 'BEST_PRICE' RATE_METHODS = { STEP_PRICE: rating.step_price, MATCH_PRICE: rating.match_price, + BEST_PRICE: rating.best_price, } service = models.ForeignKey('services.Service', verbose_name=_("service"), diff --git a/orchestra/contrib/plans/rating.py b/orchestra/contrib/plans/rating.py index 53467b3b..70c9d80f 100644 --- a/orchestra/contrib/plans/rating.py +++ b/orchestra/contrib/plans/rating.py @@ -152,3 +152,9 @@ def match_price(rates, metric): match_price.verbose_name = _("Match price") match_price.help_text = _("Only the rate with a) inmediate inferior metric and b) lower price is applied. " "Nominal price will be used when initial block is missing.") + + +def best_price(rates, metric): + pass +best_price.verbose_name = _("Best price") +best_price.help_text = _("Produces the best possible price given all active rating lines.") diff --git a/orchestra/contrib/services/handlers.py b/orchestra/contrib/services/handlers.py index 0055fef4..c8a2e67b 100644 --- a/orchestra/contrib/services/handlers.py +++ b/orchestra/contrib/services/handlers.py @@ -49,7 +49,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): return try: bool(getattr(self, method)(obj)) - except Exception as exception: + except Exception as exc: raise ValidationError(format_exception(exc)) def validate_match(self, service): @@ -124,8 +124,8 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): safe_locals = self.get_expression_context(instance) try: return eval(self.metric, safe_locals) - except Exception as error: - raise type(error)("%s on '%s'" %(error, self.service)) + except Exception as exc: + raise type(exc)("%s on '%s'" %(exc, self.service)) def get_order_description(self, instance): safe_locals = self.get_expression_context(instance) diff --git a/orchestra/contrib/systemusers/actions.py b/orchestra/contrib/systemusers/actions.py index 5b815c83..208a05bb 100644 --- a/orchestra/contrib/systemusers/actions.py +++ b/orchestra/contrib/systemusers/actions.py @@ -1,25 +1,56 @@ +import os + from django import forms from django.contrib import messages, admin from django.core.exceptions import PermissionDenied +from django.template.response import TemplateResponse from django.utils.translation import ungettext, ugettext_lazy as _ from orchestra.admin.decorators import action_with_confirmation -from orchestra.contrib.orchestration import Operation +from orchestra.contrib.orchestration.middlewares import OperationsMiddleware + +from .forms import GrantPermissionForm -class GrantPermissionForm(forms.Form): - base_path = forms.ChoiceField(label=_("Grant access to"), choices=(('hola', 'hola'),), - help_text=_("User will be granted access to this directory.")) - path_extension = forms.CharField(label='', required=False) - read_only = forms.BooleanField(label=_("Read only"), initial=False, required=False, - help_text=_("Designates whether the permissions granted will be read-only or read/write.")) - - -@action_with_confirmation(extra_context=dict(form=GrantPermissionForm())) def grant_permission(modeladmin, request, queryset): - user = queryset.get() - log = Operation.execute_action(user, 'grant_permission') - # TODO + account_id = None + for user in queryset: + account_id = account_id or user.account_id + if user.account_id != account_id: + messages.error("Users from the same account should be selected.") + user = queryset[0] + if request.method == 'POST': + form = GrantPermissionForm(user, request.POST) + if form.is_valid(): + cleaned_data = form.cleaned_data + to = os.path.join(cleaned_data['base_path'], cleaned_data['path_extension']) + ro = cleaned_data['read_only'] + for user in queryset: + user.grant_to = to + user.grant_ro = ro + OperationsMiddleware.collect('grant_permission', instance=user) + context = { + 'type': _("read-only") if ro else _("read-write"), + 'to': to, + } + msg = _("Granted %(type)s permissions on %(to)s") % context + modeladmin.log_change(request, user, msg) + return + opts = modeladmin.model._meta + app_label = opts.app_label + context = { + 'title': _("Grant permission"), + 'action_name': _("Grant permission"), + 'action_value': 'grant_permission', + 'queryset': queryset, + 'opts': opts, + 'obj': user, + 'app_label': app_label, + 'action_checkbox_name': admin.helpers.ACTION_CHECKBOX_NAME, + 'form': GrantPermissionForm(user), + } + return TemplateResponse(request, 'admin/systemusers/systemuser/grant_permission.html', + context, current_app=modeladmin.admin_site.name) grant_permission.url_name = 'grant-permission' grant_permission.verbose_name = _("Grant permission") diff --git a/orchestra/contrib/systemusers/backends.py b/orchestra/contrib/systemusers/backends.py index 1914f874..3603a8ad 100644 --- a/orchestra/contrib/systemusers/backends.py +++ b/orchestra/contrib/systemusers/backends.py @@ -66,8 +66,16 @@ class UNIXUserBackend(ServiceController): self.append("rm -fr %(base_home)s" % context) def grant_permission(self, user): + # TODO context = self.get_context(user) - # TODO setacl + context.update({ + 'to': user.grant_to, + 'ro': user.grant_ro, + }) + if user.ro: + self.append('echo "acl add read permissions for %(user)s to %(to)s"' % context) + else: + self.append('echo "acl add read-write permissions for %(user)s to %(to)s"' % context) def get_groups(self, user): if user.is_main: diff --git a/orchestra/contrib/systemusers/forms.py b/orchestra/contrib/systemusers/forms.py index 0eeee379..a6682a86 100644 --- a/orchestra/contrib/systemusers/forms.py +++ b/orchestra/contrib/systemusers/forms.py @@ -1,6 +1,7 @@ import textwrap from django import forms +from django.utils.translation import ngettext, ugettext_lazy as _ from orchestra.forms import UserCreationForm, UserChangeForm @@ -34,6 +35,8 @@ class SystemUserFormMixin(object): self.fields['directory'].widget = forms.HiddenInput() elif self.instance.pk and (self.instance.get_base_home() == self.instance.home): self.fields['directory'].widget = forms.HiddenInput() + else: + self.fields['directory'].widget = forms.TextInput(attrs={'size':'70'}) if not self.instance.pk or not self.instance.is_main: # Some javascript for hidde home/directory inputs when convinient self.fields['shell'].widget.attrs = { @@ -74,3 +77,23 @@ class SystemUserCreationForm(SystemUserFormMixin, UserCreationForm): class SystemUserChangeForm(SystemUserFormMixin, UserChangeForm): pass + + +class GrantPermissionForm(forms.Form): + base_path = forms.ChoiceField(label=_("Grant access to"), choices=(), + help_text=_("User will be granted access to this directory.")) + path_extension = forms.CharField(label=_("Path extension"), required=False, initial='', + widget=forms.TextInput(attrs={'size':'70'}), help_text=_("Relative to chosen home.")) + read_only = forms.BooleanField(label=_("Read only"), initial=False, required=False, + help_text=_("Designates whether the permissions granted will be read-only or read/write.")) + + def __init__(self, *args, **kwargs): + instance = args[0] + super_args = [] + if len(args) > 1: + super_args.append(args[1]) + super(GrantPermissionForm, self).__init__(*super_args, **kwargs) + related_users = type(instance).objects.filter(account=instance.account_id) + self.fields['base_path'].choices = ( + (user.get_base_home(), user.get_base_home()) for user in related_users + ) diff --git a/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/grant_permission.html b/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/grant_permission.html new file mode 100644 index 00000000..26063212 --- /dev/null +++ b/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/grant_permission.html @@ -0,0 +1,65 @@ +{% extends "admin/base_site.html" %} +{% load i18n l10n %} +{% load url from future %} +{% load admin_urls static utils %} + +{% block extrastyle %} +{{ block.super }} + + +{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +
+
+ Grant permissions to these system users: {% for user in queryset %}{{ user.username }}{% if not forloop.last %},{% endif %}{% endfor %}. + +
{% csrf_token %} +
+
+
+ {{ form.path_extension.errors }} + + {{ form.base_path }} +

{{ form.base_path.help_text|safe }}

+
+
+ {{ form.path_extension.errors }} + + {{ form.path_extension }} +

{{ form.path_extension.help_text|safe }}

+
+
+
+ {{ form.read_only }} +

{{ form.read_only.help_text|safe }}

+
+
+
+ {% for obj in queryset %} + + {% endfor %} + + + +
+
+{% endblock %} +