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.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)