diff --git a/TODO.md b/TODO.md index 404c8ae4..72fe818d 100644 --- a/TODO.md +++ b/TODO.md @@ -155,9 +155,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * Subdomain saving should not trigger bind slave * multiple files monitoring -* prevent adding local email addresses on account.contacts account.email - -* Resource monitoring without ROUTE alert or explicit error * Domain validation has to be done with injected records and subdomains @@ -202,6 +199,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * webapp backend option compatibility check? -* Miscellaneous service construct form for specific data, fields, validation, uniquenes.. etc (domain usecase) - * miscellaneous.indentifier.endswith(('.org', '.es', '.cat')) + +* miscservic icon miscellaneous icon + scissors diff --git a/orchestra/apps/mailboxes/models.py b/orchestra/apps/mailboxes/models.py index 708d32d9..d58d2419 100644 --- a/orchestra/apps/mailboxes/models.py +++ b/orchestra/apps/mailboxes/models.py @@ -1,5 +1,5 @@ from django.contrib.auth.hashers import make_password -from django.core.validators import RegexValidator +from django.core.validators import RegexValidator, ValidationError from django.db import models from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ @@ -109,6 +109,17 @@ class Address(models.Model): # destinations.append(self.forward) # return ' '.join(destinations) + def clean(self): + if self.account: + errors = [] + for mailbox in self.get_forward_mailboxes(): + if mailbox.account == self.account: + errors.append(ValidationError( + _("Please use mailboxes field for '%s' mailbox.") % mailbox + )) + if errors: + raise ValidationError({'forward': errors}) + def get_forward_mailboxes(self): for forward in self.forward.split(): if '@' not in forward: diff --git a/orchestra/apps/mailboxes/validators.py b/orchestra/apps/mailboxes/validators.py index 57a714e5..483d7b69 100644 --- a/orchestra/apps/mailboxes/validators.py +++ b/orchestra/apps/mailboxes/validators.py @@ -13,7 +13,7 @@ from . import settings def validate_emailname(value): - msg = _("'%s' is not a correct email name" % value) + msg = _("'%s' is not a correct email name." % value) if '@' in value: raise ValidationError(msg) value += '@localhost' @@ -26,20 +26,27 @@ def validate_emailname(value): def validate_forward(value): """ space separated mailboxes or emails """ from .models import Mailbox + errors = [] destinations = [] for destination in value.split(): if destination in destinations: - raise ValidationError(_("'%s' is already present.") % destination) + errors.append(ValidationError( + _("'%s' is already present.") % destination + )) destinations.append(destination) - msg = _("'%s' is not an existent mailbox" % destination) if '@' in destination: - if not destination[-1].isalpha(): - raise ValidationError(msg) - EmailValidator()(destination) - else: - if not Mailbox.objects.filter(user__username=destination).exists(): - raise ValidationError(msg) - validate_emailname(destination) + try: + EmailValidator()(destination) + except ValidationError: + errors.append(ValidationError( + _("'%s' is not a valid email address.") % destination + )) + elif not Mailbox.objects.filter(name=destination).exists(): + errors.append(ValidationError( + _("'%s' is not an existent mailbox.") % destination + )) + if errors: + raise ValidationError(errors) def validate_sieve(value): diff --git a/orchestra/apps/miscellaneous/admin.py b/orchestra/apps/miscellaneous/admin.py index 37747033..986a56cd 100644 --- a/orchestra/apps/miscellaneous/admin.py +++ b/orchestra/apps/miscellaneous/admin.py @@ -1,3 +1,4 @@ +from django import forms from django.contrib import admin from django.core.urlresolvers import reverse from django.db import models @@ -5,24 +6,27 @@ from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin +from orchestra.admin.utils import admin_link from orchestra.apps.accounts.admin import AccountAdminMixin +from orchestra.apps.plugins import PluginModelAdapter +from orchestra.apps.plugins.admin import SelectPluginAdminMixin +from . import settings from .models import MiscService, Miscellaneous -from orchestra.apps.plugins.admin import SelectPluginAdminMixin, PluginAdapter - - -class MiscServicePlugin(PluginAdapter): +class MiscServicePlugin(PluginModelAdapter): model = MiscService name_field = 'name' class MiscServiceAdmin(ExtendedModelAdmin): - list_display = ('name', 'verbose_name', 'num_instances', 'has_amount', 'is_active') - list_editable = ('has_amount', 'is_active') - list_filter = ('has_amount', 'is_active') - fields = ('verbose_name', 'name', 'description', 'has_amount', 'is_active') + list_display = ( + 'name', 'verbose_name', 'num_instances', 'has_identifier', 'has_amount', 'is_active' + ) + list_editable = ('is_active',) + list_filter = ('has_identifier', 'has_amount', 'is_active') + fields = ('verbose_name', 'name', 'description', 'has_identifier', 'has_amount', 'is_active') prepopulated_fields = {'name': ('verbose_name',)} change_readonly_fields = ('name',) @@ -38,13 +42,29 @@ class MiscServiceAdmin(ExtendedModelAdmin): def get_queryset(self, request): qs = super(MiscServiceAdmin, self).queryset(request) return qs.annotate(models.Count('instances', distinct=True)) + + def formfield_for_dbfield(self, db_field, **kwargs): + """ Make value input widget bigger """ + if db_field.name == 'description': + kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2}) + return super(MiscServiceAdmin, self).formfield_for_dbfield(db_field, **kwargs) class MiscellaneousAdmin(AccountAdminMixin, SelectPluginAdminMixin, admin.ModelAdmin): - list_display = ('service', 'amount', 'active', 'account_link') + list_display = ('__unicode__', 'service_link', 'amount', 'dispaly_active', 'account_link') + list_filter = ('service__name', 'is_active') + list_select_related = ('service', 'account') plugin_field = 'service' plugin = MiscServicePlugin + service_link = admin_link('service') + + def dispaly_active(self, instance): + return instance.active + dispaly_active.short_description = _("Active") + dispaly_active.boolean = True + dispaly_active.admin_order_field = 'is_active' + def get_service(self, obj): if obj is None: return self.plugin.get_plugin(self.plugin_value)().instance @@ -58,20 +78,28 @@ class MiscellaneousAdmin(AccountAdminMixin, SelectPluginAdminMixin, admin.ModelA service = self.get_service(obj) if service.has_amount: fields.insert(-1, 'amount') -# if service.has_identifier: -# fields.insert(1, 'identifier') + if service.has_identifier: + fields.insert(1, 'identifier') return fields - def get_form(self, request, obj=None, **kwargs): form = super(SelectPluginAdminMixin, self).get_form(request, obj=obj, **kwargs) service = self.get_service(obj) def clean_identifier(self, service=service): + identifier = self.cleaned_data['identifier'] validator = settings.MISCELLANEOUS_IDENTIFIER_VALIDATORS.get(service.name, None) if validator: - validator(self.cleaned_data['identifier']) + validator(identifier) + return identifier + form.clean_identifier = clean_identifier return form + + def formfield_for_dbfield(self, db_field, **kwargs): + """ Make value input widget bigger """ + if db_field.name == 'description': + kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 4}) + return super(MiscellaneousAdmin, self).formfield_for_dbfield(db_field, **kwargs) admin.site.register(MiscService, MiscServiceAdmin) diff --git a/orchestra/apps/miscellaneous/models.py b/orchestra/apps/miscellaneous/models.py index 365710fe..8309f212 100644 --- a/orchestra/apps/miscellaneous/models.py +++ b/orchestra/apps/miscellaneous/models.py @@ -14,9 +14,9 @@ class MiscService(models.Model): help_text=_("Human readable name")) description = models.TextField(_("description"), blank=True, help_text=_("Optional description")) -# has_identifier = models.BooleanField(_("has identifier"), default=True, -# help_text=_("Designates if this service has a unique text field that " -# "identifies it or not.")) + has_identifier = models.BooleanField(_("has identifier"), default=True, + help_text=_("Designates if this service has a unique text field that " + "identifies it or not.")) has_amount = models.BooleanField(_("has amount"), default=False, help_text=_("Designates whether this service has amount " "property or not.")) @@ -39,8 +39,8 @@ class Miscellaneous(models.Model): related_name='instances') account = models.ForeignKey('accounts.Account', verbose_name=_("account"), related_name='miscellaneous') -# identifier = NullableCharField(_("identifier"), max_length=256, null=True, unique=True, -# help_text=_("A unique identifier for this service.")) + identifier = NullableCharField(_("identifier"), max_length=256, null=True, unique=True, + help_text=_("A unique identifier for this service.")) description = models.TextField(_("description"), blank=True) amount = models.PositiveIntegerField(_("amount"), default=1) is_active = models.BooleanField(_("active"), default=True, @@ -51,8 +51,7 @@ class Miscellaneous(models.Model): verbose_name_plural = _("miscellaneous") def __unicode__(self): -# return self.identifier or str(self.service) - return "{0}-{1}".format(str(self.service), str(self.account)) + return self.identifier or str(self.service) @cached_property def active(self): @@ -62,8 +61,8 @@ class Miscellaneous(models.Model): return self.is_active def clean(self): -# if self.identifier: -# self.identifier = self.identifier.strip() + if self.identifier: + self.identifier = self.identifier.strip() self.description = self.description.strip() diff --git a/orchestra/apps/miscellaneous/settings.py b/orchestra/apps/miscellaneous/settings.py index 429d07e4..8162dd97 100644 --- a/orchestra/apps/miscellaneous/settings.py +++ b/orchestra/apps/miscellaneous/settings.py @@ -1,5 +1,5 @@ from django.conf import settings -MISCELLANEOUS_IDENTIFIER_VALIDATORS = getattr(settings, MISCELLANEOUS_IDENTIFIER_VALIDATORS, {}) +MISCELLANEOUS_IDENTIFIER_VALIDATORS = getattr(settings, 'MISCELLANEOUS_IDENTIFIER_VALIDATORS', {}) # MISCELLANEOUS_IDENTIFIER_VALIDATORS = { miscservice__name: validator_function } diff --git a/orchestra/apps/plugins/admin.py b/orchestra/apps/plugins/admin.py index 878f7cb2..a21d4dca 100644 --- a/orchestra/apps/plugins/admin.py +++ b/orchestra/apps/plugins/admin.py @@ -74,35 +74,3 @@ class SelectPluginAdminMixin(object): if not change: setattr(obj, self.plugin_field, self.plugin_value) obj.save() - - -class PluginAdapter(object): - """ Adapter class for using model classes as plugins """ - - model = None - name_field = None - - def __init__(self, instance): - self.instance = instance - - @classmethod - @cached - def get_plugins(cls): - plugins = [] - for instance in cls.model.objects.filter(is_active=True): - plugins.append(cls(instance)) - return plugins - - @classmethod - def get_plugin(cls, name): - return cls(cls.model.objects.get(**{cls.name_field:name})) - - @property - def verbose_name(self): - return self.instance.verbose_name or str(getattr(self.instance, self.name_field)) - - def get_name(self): - return getattr(self.instance, self.name_field) - - def __call__(self): - return self diff --git a/orchestra/apps/plugins/options.py b/orchestra/apps/plugins/options.py index 47080434..f6e87a69 100644 --- a/orchestra/apps/plugins/options.py +++ b/orchestra/apps/plugins/options.py @@ -41,6 +41,27 @@ class Plugin(object): return sorted(choices, key=lambda e: e[1]) +class PluginModelAdapter(Plugin): + """ Adapter class for using model classes as plugins """ + model = None + name_field = None + + @classmethod + def get_plugins(cls): + plugins = [] + for instance in cls.model.objects.filter(is_active=True): + attributes = { + 'instance': instance, + 'verbose_name': instance.verbose_name + } + plugins.append(type('PluginAdapter', (cls,), attributes)) + return plugins + + @classmethod + def get_name(cls): + return getattr(cls.instance, cls.name_field) + + class PluginMount(type): def __init__(cls, name, bases, attrs): if not attrs.get('abstract', False): diff --git a/orchestra/apps/plugins/templates/admin/plugins/select_plugin.html b/orchestra/apps/plugins/templates/admin/plugins/select_plugin.html index 12ea44c6..02818163 100644 --- a/orchestra/apps/plugins/templates/admin/plugins/select_plugin.html +++ b/orchestra/apps/plugins/templates/admin/plugins/select_plugin.html @@ -20,7 +20,7 @@ {% for plugin in plugins %}
  • {{ plugin.get_name }} - {{ plugin.verbose_name }}
  • + {{ plugin.get_verbose_name }} {% endfor %} @@ -28,7 +28,7 @@ {% else %} {% endif %} diff --git a/orchestra/apps/resources/admin.py b/orchestra/apps/resources/admin.py index b848dbbb..7c696f38 100644 --- a/orchestra/apps/resources/admin.py +++ b/orchestra/apps/resources/admin.py @@ -1,13 +1,15 @@ from django.contrib import admin, messages +from django.contrib.admin.utils import unquote from django.contrib.contenttypes import generic from django.core.urlresolvers import reverse from django.utils.functional import cached_property from django.utils.safestring import mark_safe -from django.utils.translation import ugettext, ugettext_lazy as _ +from django.utils.translation import ungettext, ugettext, ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin from orchestra.admin.filters import UsedContentTypeFilter from orchestra.admin.utils import insertattr, get_modeladmin, admin_link, admin_date +from orchestra.apps.orchestration.models import Route from orchestra.core import services from orchestra.utils import database_ready @@ -36,7 +38,7 @@ class ResourceAdmin(ExtendedModelAdmin): 'fields': ('monitors', 'crontab'), }), ) - change_readonly_fields = ('name', 'content_type', 'period') + change_readonly_fields = ('name', 'content_type') prepopulated_fields = {'name': ('verbose_name',)} def add_view(self, request, **kwargs): @@ -48,6 +50,25 @@ class ResourceAdmin(ExtendedModelAdmin): ))) return super(ResourceAdmin, self).add_view(request, **kwargs) + def change_view(self, request, object_id, form_url='', extra_context=None): + """ Remaind user when monitor routes are not configured """ + if request.method == 'GET': + resource = self.get_object(request, unquote(object_id)) + backends = Route.objects.values_list('backend', flat=True) + not_routed = [] + for monitor in resource.monitors: + if monitor not in backends: + not_routed.append(monitor) + if not_routed: + messages.warning(request, ungettext( + _("%(not_routed)s monitor doesn't have any configured route."), + _("%(not_routed)s monitors don't have any configured route."), + len(not_routed), + ) % { + 'not_routed': ', '.join(not_routed) + }) + return super(ResourceAdmin, self).changeform_view(request, object_id, form_url, extra_context) + def save_model(self, request, obj, form, change): super(ResourceAdmin, self).save_model(request, obj, form, change) model = obj.content_type.model_class() @@ -70,25 +91,21 @@ class ResourceAdmin(ExtendedModelAdmin): class ResourceDataAdmin(ExtendedModelAdmin): list_display = ( - 'id', 'resource_link', 'content_object_link', 'used', 'allocated', 'display_unit', + 'id', 'resource_link', 'content_object_link', 'display_used', 'allocated', 'display_unit', 'display_updated' ) list_filter = ('resource',) - add_fields = ('resource', 'content_type', 'object_id', 'used', 'updated_at', 'allocated') fields = ( - 'resource_link', 'content_type', 'content_object_link', 'used', 'display_updated', + 'resource_link', 'content_type', 'content_object_link', 'display_used', 'display_updated', 'allocated', 'display_unit' ) - readonly_fields = ('display_unit',) - change_readonly_fields = ( - 'resource_link', 'content_type', 'content_object_link', 'used', 'display_updated', - 'display_unit' - ) + readonly_fields = fields actions = (run_monitor,) change_view_actions = actions ordering = ('-updated_at',) + list_select_related = ('resource',) prefetch_related = ('content_object',) - + resource_link = admin_link('resource') content_object_link = admin_link('content_object') display_updated = admin_date('updated_at', short_description=_("Updated")) @@ -97,6 +114,24 @@ class ResourceDataAdmin(ExtendedModelAdmin): return data.unit display_unit.short_description = _("Unit") display_unit.admin_order_field = 'resource__unit' + + def display_used(self, data): + if not data.used: + return '' + ids = [] + for dataset in data.get_monitor_datasets(): + if isinstance(dataset, MonitorData): + ids.append(dataset.id) + else: + ids += dataset.values_list('id', flat=True) + url = reverse('admin:resources_monitordata_changelist') + url += '?id__in=%s' % ','.join(map(str, ids)) + return '%s' % (url, data.used) + display_used.short_description = _("Used") + display_used.allow_tags = True + + def has_add_permission(self, *args, **kwargs): + return False class MonitorDataAdmin(ExtendedModelAdmin): diff --git a/orchestra/apps/resources/forms.py b/orchestra/apps/resources/forms.py index 080aff95..09bcdd07 100644 --- a/orchestra/apps/resources/forms.py +++ b/orchestra/apps/resources/forms.py @@ -5,8 +5,8 @@ from orchestra.forms.widgets import ShowTextWidget, ReadOnlyWidget class ResourceForm(forms.ModelForm): - verbose_name = forms.CharField(label=_("Name"), widget=ShowTextWidget(bold=True), - required=False) + verbose_name = forms.CharField(label=_("Name"), required=False, + widget=ShowTextWidget(bold=True)) allocated = forms.IntegerField(label=_("Allocated")) unit = forms.CharField(label=_("Unit"), widget=ShowTextWidget(), required=False) diff --git a/orchestra/apps/resources/helpers.py b/orchestra/apps/resources/helpers.py index 682d19e9..e2ab03b1 100644 --- a/orchestra/apps/resources/helpers.py +++ b/orchestra/apps/resources/helpers.py @@ -1,52 +1,27 @@ import datetime -from django.contrib.contenttypes.models import ContentType -from django.db.models.loading import get_model -from django.utils import timezone - -from orchestra.models.utils import get_model_field_path - -from .backends import ServiceMonitor - def compute_resource_usage(data): """ Computes MonitorData.used based on related monitors """ - from .models import MonitorData resource = data.resource - today = timezone.now() result = 0 has_result = False - for monitor in resource.monitors: - # Get related dataset - resource_model = data.content_type.model_class() - monitor_model = get_model(ServiceMonitor.get_backend(monitor).model) - if resource_model == monitor_model: - dataset = MonitorData.objects.filter(monitor=monitor, - content_type=data.content_type_id, object_id=data.object_id) - else: - path = get_model_field_path(monitor_model, resource_model) - fields = '__'.join(path) - objects = monitor_model.objects.filter(**{fields: data.object_id}) - pks = objects.values_list('id', flat=True) - ct = ContentType.objects.get_for_model(monitor_model) - dataset = MonitorData.objects.filter(monitor=monitor, content_type=ct, object_id__in=pks) - # Process dataset according to resource.period + for dataset in data.get_monitor_datasets(): if resource.period == resource.MONTHLY_AVG: - try: - last = dataset.latest() - except MonitorData.DoesNotExist: - continue - has_result = True - epoch = datetime(year=today.year, month=today.month, day=1, tzinfo=timezone.utc) + last = dataset.latest() + epoch = datetime( + year=today.year, + month=today.month, + day=1, + tzinfo=timezone.utc + ) total = (last.created_at-epoch).total_seconds() - dataset = dataset.filter(created_at__year=today.year, created_at__month=today.month) ini = epoch for data in dataset: slot = (data.created_at-ini).total_seconds() result += data.value * slot/total ini = data.created_at elif resource.period == resource.MONTHLY_SUM: - dataset = dataset.filter(created_at__year=today.year, created_at__month=today.month) # FIXME Aggregation of 0s returns None! django bug? # value = dataset.aggregate(models.Sum('value'))['value__sum'] values = dataset.values_list('value', flat=True) @@ -54,10 +29,7 @@ def compute_resource_usage(data): has_result = True result += sum(values) elif resource.period == resource.LAST: - try: - result += dataset.latest().value - except MonitorData.DoesNotExist: - continue + dataset.value has_result = True else: raise NotImplementedError("%s support not implemented" % data.period) diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py index e6458b46..6710d4f1 100644 --- a/orchestra/apps/resources/models.py +++ b/orchestra/apps/resources/models.py @@ -2,6 +2,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio from django.contrib.contenttypes.models import ContentType from django.apps import apps from django.db import models +from django.db.models.loading import get_model from django.utils import timezone from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ @@ -9,6 +10,7 @@ from djcelery.models import PeriodicTask, CrontabSchedule from orchestra.core import validators from orchestra.models import queryset, fields +from orchestra.models.utils import get_model_field_path from orchestra.utils.paths import get_project_root from orchestra.utils.system import run @@ -88,36 +90,38 @@ class Resource(models.Model): def save(self, *args, **kwargs): created = not self.pk super(Resource, self).save(*args, **kwargs) - # Create Celery periodic task - name = 'monitor.%s' % str(self) - try: - task = PeriodicTask.objects.get(name=name) - except PeriodicTask.DoesNotExist: - if self.is_active: - PeriodicTask.objects.create( - name=name, - task='resources.Monitor', - args=[self.pk], - crontab=self.crontab - ) - else: - if not self.is_active: - task.delete() - elif task.crontab != self.crontab: - task.crontab = self.crontab - task.save(update_fields=['crontab']) + self.sync_periodic_task() # This only work on tests (multiprocessing used on real deployments) apps.get_app_config('resources').reload_relations() - run('touch %s/wsgi.py' % get_project_root()) + run('sleep 2 && touch %s/wsgi.py' % get_project_root(), async=True, display=True) def delete(self, *args, **kwargs): super(Resource, self).delete(*args, **kwargs) name = 'monitor.%s' % str(self) - PeriodicTask.objects.filter( - name=name, - task='resources.Monitor', - args=[self.pk] - ).delete() + + def sync_periodic_task(self): + name = 'monitor.%s' % str(self) + if self.pk and self.crontab: + try: + task = PeriodicTask.objects.get(name=name) + except PeriodicTask.DoesNotExist: + if self.is_active: + PeriodicTask.objects.create( + name=name, + task='resources.Monitor', + args=[self.pk], + crontab=self.crontab + ) + else: + if task.crontab != self.crontab: + task.crontab = self.crontab + task.save(update_fields=['crontab']) + else: + PeriodicTask.objects.filter( + name=name, + task='resources.Monitor', + args=[self.pk] + ).delete() def get_scale(self): return eval(self.scale) @@ -146,10 +150,17 @@ class ResourceData(models.Model): def get_or_create(cls, obj, resource): ct = ContentType.objects.get_for_model(type(obj)) try: - return cls.objects.get(content_type=ct, object_id=obj.pk, resource=resource) + return cls.objects.get( + content_type=ct, + object_id=obj.pk, + resource=resource + ) except cls.DoesNotExist: - return cls.objects.create(content_object=obj, resource=resource, - allocated=resource.default_allocation) + return cls.objects.create( + content_object=obj, + resource=resource, + allocated=resource.default_allocation + ) @property def unit(self): @@ -167,6 +178,47 @@ class ResourceData(models.Model): def monitor(self): tasks.monitor(self.resource_id, ids=(self.object_id,)) + + def get_monitor_datasets(self): + resource = self.resource + today = timezone.now() + datasets = [] + for monitor in resource.monitors: + resource_model = self.content_type.model_class() + model_path = ServiceMonitor.get_backend(monitor).model + monitor_model = get_model(model_path) + if resource_model == monitor_model: + dataset = MonitorData.objects.filter( + monitor=monitor, + content_type=self.content_type_id, + object_id=self.object_id + ) + else: + path = get_model_field_path(monitor_model, resource_model) + fields = '__'.join(path) + objects = monitor_model.objects.filter(**{fields: self.object_id}) + pks = objects.values_list('id', flat=True) + ct = ContentType.objects.get_for_model(monitor_model) + dataset = MonitorData.objects.filter( + monitor=monitor, + content_type=ct, + object_id__in=pks + ) + if resource.period in (resource.MONTHLY_AVG, resource.MONTHLY_SUM): + datasets.append( + dataset.filter( + created_at__year=today.year, + created_at__month=today.month + ) + ) + elif resource.period == resource.LAST: + try: + datasets.append(dataset.latest()) + except MonitorData.DoesNotExist: + continue + else: + raise NotImplementedError("%s support not implemented" % self.period) + return datasets class MonitorData(models.Model): @@ -207,10 +259,16 @@ def create_resource_relation(): data = self.obj.resource_set.get(resource__name=attr) except ResourceData.DoesNotExist: model = self.obj._meta.model_name - resource = Resource.objects.get(content_type__model=model, name=attr, - is_active=True) - data = ResourceData(content_object=self.obj, resource=resource, - allocated=resource.default_allocation) + resource = Resource.objects.get( + content_type__model=model, + name=attr, + is_active=True + ) + data = ResourceData( + content_object=self.obj, + resource=resource, + allocated=resource.default_allocation + ) self.obj.__resource_cache[attr] = data return data diff --git a/orchestra/apps/websites/backends/apache.py b/orchestra/apps/websites/backends/apache.py index c5b56863..037ee559 100644 --- a/orchestra/apps/websites/backends/apache.py +++ b/orchestra/apps/websites/backends/apache.py @@ -161,11 +161,13 @@ class Apache2Backend(ServiceController): context = self.get_context(site) self.append("ls -l %(sites_enabled)s > /dev/null; DISABLED=$?" % context) if site.is_active: - self.append("if [[ $DISABLED ]]; then a2ensite %(site_unique_name)s.conf;\n" - "else UPDATED=0; fi" % context) + self.append( + "if [[ $DISABLED ]]; then a2ensite %(site_unique_name)s.conf;\n" + "else UPDATED=0; fi" % context) else: - self.append("if [[ ! $DISABLED ]]; then a2dissite %(site_unique_name)s.conf;\n" - "else UPDATED=0; fi" % context) + self.append( + "if [[ ! $DISABLED ]]; then a2dissite %(site_unique_name)s.conf;\n" + "else UPDATED=0; fi" % context) def get_username(self, site): option = site.options.filter(name='user_group').first() @@ -258,7 +260,7 @@ class Apache2Traffic(ServiceMonitor): def monitor(self, site): context = self.get_context(site) - self.append('monitor {object_id} $(date "+%Y%m%d%H%M%S" -d "{last_date}") "{log_file}"'.format(**context)) + self.append('monitor {object_id} $(date "+%Y%m%d%H%M%S" -d "{last_date}") {log_file}'.format(**context)) def get_context(self, site): return { diff --git a/orchestra/apps/websites/models.py b/orchestra/apps/websites/models.py index 29645152..64377f51 100644 --- a/orchestra/apps/websites/models.py +++ b/orchestra/apps/websites/models.py @@ -52,6 +52,9 @@ class Website(models.Model): def get_www_log_path(self): context = { + 'user_home': self.account.main_systemuser.get_home(), + 'username': self.account.username, + 'name': self.name, 'unique_name': self.unique_name } return settings.WEBSITES_WEBSITE_WWW_LOG_PATH % context diff --git a/orchestra/apps/websites/settings.py b/orchestra/apps/websites/settings.py index 53a7d19e..477c56f4 100644 --- a/orchestra/apps/websites/settings.py +++ b/orchestra/apps/websites/settings.py @@ -82,4 +82,5 @@ WEBSITES_WEBALIZER_PATH = getattr(settings, 'WEBSITES_WEBALIZER_PATH', WEBSITES_WEBSITE_WWW_LOG_PATH = getattr(settings, 'WEBSITES_WEBSITE_WWW_LOG_PATH', + # %(user_home)s %(name)s %(unique_name)s %(username)s '/var/log/apache2/virtual/%(unique_name)s') diff --git a/orchestra/conf/base_settings.py b/orchestra/conf/base_settings.py index b9f45ec3..e74de7c1 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -168,6 +168,7 @@ FLUENT_DASHBOARD_APP_GROUPS = ( 'orchestra.apps.resources.models.Monitor', 'orchestra.apps.services.models.Service', 'orchestra.apps.services.models.Plan', + 'orchestra.apps.miscellaneous.models.MiscService', ), 'collapsible': True, }), @@ -201,6 +202,7 @@ FLUENT_DASHBOARD_APP_ICONS = { 'payments/transaction': 'transaction.png', 'payments/transactionprocess': 'transactionprocess.png', 'issues/ticket': 'Ticket_star.png', + 'miscellaneous/miscservice': 'Misc-Misc-Box-icon.png', # Administration 'djcelery/taskstate': 'taskstate.png', 'orchestration/server': 'vps.png', diff --git a/orchestra/models/fields.py b/orchestra/models/fields.py index 537362e9..e3cf40e8 100644 --- a/orchestra/models/fields.py +++ b/orchestra/models/fields.py @@ -28,19 +28,21 @@ class MultiSelectField(models.CharField): return ','.join(value) def to_python(self, value): - if value is not None: + if value: if isinstance(value, list) and value[0].startswith('('): # Workaround unknown bug on default model values # [u"('SUPPORT'", u" 'ADMIN'", u" 'BILLING'", u" 'TECH'", u" 'ADDS'", u" 'EMERGENCY')"] value = list(eval(', '.join(value))) - return value if isinstance(value, list) else value.split(',') - return '' + if isinstance(value, list): + return value + return value.split(',') + return [] def contribute_to_class(self, cls, name): super(MultiSelectField, self).contribute_to_class(cls, name) if self.choices: def func(self, field=name, choices=dict(self.choices)): - return ','.join([ choices.get(value, value) for value in getattr(self, field) ]) + return ','.join([choices.get(value, value) for value in getattr(self, field)]) setattr(cls, 'get_%s_display' % self.name, func) def validate(self, value, model_instance): diff --git a/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.png b/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.png index 7d64b79f..f7c6e0a4 100644 Binary files a/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.png and b/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.png differ diff --git a/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.svg b/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.svg index 644f1826..c65c2ee2 100644 --- a/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.svg +++ b/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.svg @@ -16,7 +16,7 @@ width="48" height="48" sodipodi:docname="Misc-Misc-Box-icon.svg" - inkscape:export-filename="/home/glic3rinu/orchestra/django-orchestra/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.png" + inkscape:export-filename="/home/glic3/production_orchestra/django-orchestra/orchestra/static/orchestra/icons/Misc-Misc-Box-icon.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90"> + id="defs6"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/orchestra/utils/system.py b/orchestra/utils/system.py index 3c4fa77f..a51434f5 100644 --- a/orchestra/utils/system.py +++ b/orchestra/utils/system.py @@ -46,8 +46,8 @@ def read_async(fd): return u'' -def run(command, display=False, error_codes=[0], silent=False, stdin=''): - """ Subprocess wrapper for running commands """ +def runiterator(command, display=False, error_codes=[0], silent=False, stdin=''): + """ Subprocess wrapper for running commands concurrently """ if display: sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command) @@ -56,6 +56,7 @@ def run(command, display=False, error_codes=[0], silent=False, stdin=''): p.stdin.write(stdin) p.stdin.close() + yield make_async(p.stdout) make_async(p.stderr) @@ -77,22 +78,39 @@ def run(command, display=False, error_codes=[0], silent=False, stdin=''): if display and stderrPiece: sys.stderr.write(stderrPiece) - stdout += stdoutPiece.decode("utf8") - stderr += stderrPiece.decode("utf8") - returnCode = p.poll() + return_code = p.poll() + state = _AttributeUnicode(stdoutPiece.decode("utf8")) + state.stderr = stderrPiece.decode("utf8") + state.return_code = return_code + yield state - if returnCode != None: - break + if return_code != None: + p.stdout.close() + p.stderr.close() + raise StopIteration + + +def run(command, display=False, error_codes=[0], silent=False, stdin='', async=False): + iterator = runiterator(command, display, error_codes, silent, stdin) + iterator.next() + if async: + return iterator + + stdout = '' + stderr = '' + for state in iterator: + stdout += state.stdout + stderr += state.stderr + + return_code = state.return_code out = _AttributeUnicode(stdout.strip()) - err = _AttributeUnicode(stderr.strip()) - p.stdout.close() - p.stderr.close() + err = stderr.strip() out.failed = False - out.return_code = returnCode + out.return_code = return_code out.stderr = err - if p.returncode not in error_codes: + if return_code not in error_codes: out.failed = True msg = "\nrun() encountered an error (return code %s) while executing '%s'\n" msg = msg % (p.returncode, command)