From e98f5004116f3a49ed4e04c4bb8f88e666473353 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 13 Nov 2014 15:34:00 +0000 Subject: [PATCH] Improvements on resources --- TODO.md | 7 +- orchestra/apps/mailboxes/models.py | 13 +- orchestra/apps/mailboxes/validators.py | 27 +- orchestra/apps/miscellaneous/admin.py | 54 +- orchestra/apps/miscellaneous/models.py | 17 +- orchestra/apps/miscellaneous/settings.py | 2 +- orchestra/apps/plugins/admin.py | 32 - orchestra/apps/plugins/options.py | 21 + .../admin/plugins/select_plugin.html | 4 +- orchestra/apps/resources/admin.py | 57 +- orchestra/apps/resources/forms.py | 4 +- orchestra/apps/resources/helpers.py | 46 +- orchestra/apps/resources/models.py | 120 +- orchestra/apps/websites/backends/apache.py | 12 +- orchestra/apps/websites/models.py | 3 + orchestra/apps/websites/settings.py | 1 + orchestra/conf/base_settings.py | 2 + orchestra/models/fields.py | 10 +- .../orchestra/icons/Misc-Misc-Box-icon.png | Bin 3795 -> 4933 bytes .../orchestra/icons/Misc-Misc-Box-icon.svg | 1256 ++++++----------- orchestra/utils/system.py | 42 +- 21 files changed, 715 insertions(+), 1015 deletions(-) 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 7d64b79fcef00dd24d0a2aa82191d3d70ff483a9..f7c6e0a4a915af4bfb8b2479f28fb81e9591b7cf 100644 GIT binary patch delta 4868 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 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)