Improvements on resource monitoring

This commit is contained in:
Marc 2014-07-10 15:19:06 +00:00
parent cc445559d0
commit 53a135a1d9
15 changed files with 137 additions and 68 deletions

View File

@ -48,5 +48,4 @@ Remember that, as always with QuerySets, any subsequent chained methods which im
* passlib; nano /usr/local/lib/python2.7/dist-packages/passlib/ext/django/utils.py SortedDict -> collections.OrderedDict
* pip install pyinotify
* Backend.operations dynamically generated based on defined methods
* create custom field that returns backend python objects

View File

@ -71,6 +71,8 @@ class AccountAdmin(ExtendedModelAdmin):
def save_model(self, request, obj, form, change):
""" Save user and account, they are interdependent """
if change:
return super(AccountAdmin, self).save_model(request, obj, form, change)
obj.user.save()
obj.user_id = obj.user.pk
obj.save()

View File

@ -22,7 +22,9 @@ class DomainViewSet(AccountApiMixin, viewsets.ModelViewSet):
@link()
def view_zone(self, request, pk=None):
domain = self.get_object()
return Response({'zone': domain.render_zone()})
return Response({
'zone': domain.render_zone()
})
def metadata(self, request):
ret = super(DomainViewSet, self).metadata(request)

View File

@ -69,7 +69,7 @@ class ServiceBackend(object):
@classmethod
def get_backend(cls, name):
for backend in ServiceMonitor.get_backends():
for backend in ServiceBackend.get_backends():
if backend.get_name() == name:
return backend
raise KeyError('This backend is not registered')

View File

@ -38,7 +38,7 @@ def message_user(request, logs):
errors = total-successes
if errors:
msg = 'backends have' if errors > 1 else 'backend has'
msg = _("%d out of %d {0} fail to executed".format(msg))
msg = _("%d out of %d {0} fail to execute".format(msg))
messages.warning(request, msg % (errors, total))
else:
msg = 'backends have' if successes > 1 else 'backend has'

View File

@ -40,12 +40,12 @@ def BashSSH(backend, log, server, cmds):
channel = transport.open_session()
sftp = paramiko.SFTPClient.from_transport(transport)
sftp.put(path, path)
sftp.put(path, "%s.remote" % path)
sftp.close()
os.remove(path)
context = {
'path': path,
'path': "%s.remote" % path,
'digest': digest
}
cmd = (

View File

@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orchestra.models.fields import NullableCharField
from orchestra.utils.apps import autodiscover
from orchestra.utils.functional import cached
@ -14,9 +15,8 @@ from .backends import ServiceBackend
class Server(models.Model):
""" Machine runing daemons (services) """
name = models.CharField(_("name"), max_length=256, unique=True)
# TODO unique address with blank=True (nullablecharfield)
address = models.CharField(_("address"), max_length=256, blank=True,
help_text=_("IP address or domain name"))
address = NullableCharField(_("address"), max_length=256, blank=True,
null=True, unique=True, help_text=_("IP address or domain name"))
description = models.TextField(_("description"), blank=True)
os = models.CharField(_("operative system"), max_length=32,
choices=settings.ORCHESTRATION_OS_CHOICES,
@ -82,8 +82,7 @@ class BackendOperation(models.Model):
MONITOR = 'monitor'
log = models.ForeignKey('orchestration.BackendLog', related_name='operations')
# TODO backend and backend_class() (like content_type)
backend_class = models.CharField(_("backend"), max_length=256)
backend = models.CharField(_("backend"), max_length=256)
action = models.CharField(_("action"), max_length=64)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
@ -94,11 +93,11 @@ class BackendOperation(models.Model):
verbose_name_plural = _("Operations")
def __unicode__(self):
return '%s.%s(%s)' % (self.backend_class, self.action, self.instance)
return '%s.%s(%s)' % (self.backend, self.action, self.instance)
def __hash__(self):
""" set() """
backend = getattr(self, 'backend', self.backend_class)
backend = getattr(self, 'backend', self.backend)
return hash(backend) + hash(self.instance) + hash(self.action)
def __eq__(self, operation):
@ -107,7 +106,7 @@ class BackendOperation(models.Model):
@classmethod
def create(cls, backend, instance, action):
op = cls(backend_class=backend.get_name(), instance=instance, action=action)
op = cls(backend=backend.get_name(), instance=instance, action=action)
op.backend = backend
return op
@ -115,6 +114,9 @@ class BackendOperation(models.Model):
def execute(cls, operations):
return manager.execute(operations)
def backend_class(self):
return ServiceBackend.get_backend(self.backend)
autodiscover('backends')

View File

@ -1,8 +1,9 @@
from django.contrib import admin
from django.contrib import admin, messages
from django.contrib.contenttypes import generic
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.filters import UsedContentTypeFilter
from orchestra.admin.utils import insertattr, get_modeladmin
from orchestra.core import services
@ -12,14 +13,34 @@ from .forms import ResourceForm
from .models import Resource, ResourceData, MonitorData
class ResourceAdmin(admin.ModelAdmin):
# TODO warning message server/celery should be restarted when creating things
class ResourceAdmin(ExtendedModelAdmin):
list_display = (
'name', 'verbose_name', 'content_type', 'period', 'ondemand',
'default_allocation', 'disable_trigger'
'default_allocation', 'disable_trigger', 'crontab',
)
list_filter = (UsedContentTypeFilter, 'period', 'ondemand', 'disable_trigger')
fieldsets = (
(None, {
'fields': ('name', 'content_type', 'period'),
}),
(_("Configuration"), {
'fields': ('verbose_name', 'default_allocation', 'ondemand',
'disable_trigger', 'is_active'),
}),
(_("Monitoring"), {
'fields': ('monitors', 'crontab'),
}),
)
change_readonly_fields = ('name', 'content_type', 'period')
def add_view(self, request, **kwargs):
""" Warning user if the node is not fully configured """
if request.method == 'GET':
messages.warning(request, _(
"Restarting orchestra and celery is required to fully apply changes. "
"Remember that allocated values will be applied when objects are saved"
))
return super(ResourceAdmin, self).add_view(request, **kwargs)
def save_model(self, request, obj, form, change):
super(ResourceAdmin, self).save_model(request, obj, form, change)

View File

@ -1,4 +1,9 @@
import datetime
from django.contrib.contenttypes.models import ContentType
from orchestra.apps.orchestration import ServiceBackend
from orchestra.utils.functional import cached
class ServiceMonitor(ServiceBackend):
@ -14,14 +19,34 @@ class ServiceMonitor(ServiceBackend):
""" filter monitor classes """
return [plugin for plugin in cls.plugins if ServiceMonitor in plugin.__mro__]
def store(self, stdout):
@cached
def get_last_date(self, obj):
from .models import MonitorData
try:
# TODO replace
#return MonitorData.objects.filter(content_object=obj).latest().date
ct = ContentType.objects.get(app_label=obj._meta.app_label, model=obj._meta.model_name)
return MonitorData.objects.filter(content_type=ct, object_id=obj.pk).latest().date
except MonitorData.DoesNotExist:
return self.get_current_date() - datetime.timedelta(days=1)
@cached
def get_current_date(self):
return datetime.datetime.now()
def store(self, log):
""" object_id value """
for line in stdout.readlines():
from .models import MonitorData
name = self.get_name()
app_label, model_name = self.model.split('.')
ct = ContentType.objects.get(app_label=app_label, model=model_name.lower())
for line in log.stdout.splitlines():
line = line.strip()
object_id, value = line.split()
# TODO date
MonitorHistory.store(self.model, object_id, value, date)
MonitorData.objects.create(monitor=name, object_id=object_id,
content_type=ct, value=value, date=self.get_current_date())
def execute(self, server):
log = super(MonitorBackend, self).execute(server)
log = super(ServiceMonitor, self).execute(server)
self.store(log)
return log

View File

@ -21,7 +21,6 @@ class ResourceForm(forms.ModelForm):
super(ResourceForm, self).__init__(*args, **kwargs)
if self.resource:
self.fields['verbose_name'].initial = self.resource.verbose_name
self.fields['used'].initial = self.resource.get_used() # TODO
if self.resource.ondemand:
self.fields['allocated'].required = False
self.fields['allocated'].widget = ReadOnlyWidget(None, '')

View File

@ -1,7 +1,7 @@
import datetime
from django.db import models
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core import validators
from django.utils.translation import ugettext_lazy as _
@ -34,17 +34,22 @@ class Resource(models.Model):
validators=[validators.RegexValidator(r'^[a-z0-9_\-]+$',
_('Enter a valid name.'), 'invalid')])
verbose_name = models.CharField(_("verbose name"), max_length=256, unique=True)
content_type = models.ForeignKey(ContentType) # TODO filter by servicE?
period = models.CharField(_("period"), max_length=16, choices=PERIODS,
default=LAST)
ondemand = models.BooleanField(_("on demand"), default=False)
content_type = models.ForeignKey(ContentType,
help_text=_("Model where this resource will be hooked"))
period = models.CharField(_("period"), max_length=16, choices=PERIODS, default=LAST,
help_text=_("Operation used for aggregating this resource monitored data."))
ondemand = models.BooleanField(_("on demand"), default=False,
help_text=_("If enabled the resource will not be pre-allocated, "
"but allocated under the application demand"))
default_allocation = models.PositiveIntegerField(_("default allocation"),
help_text=_("Default allocation value used when this is not an "
"on demand resource"),
null=True, blank=True)
is_active = models.BooleanField(_("is active"), default=True)
disable_trigger = models.BooleanField(_("disable trigger"), default=False)
disable_trigger = models.BooleanField(_("disable trigger"), default=False,
help_text=_("Disables monitor's resource exeeded and recovery triggers"))
crontab = models.ForeignKey(CrontabSchedule, verbose_name=_("crontab"),
help_text=_("Crontab for periodic execution"))
# TODO create custom field that returns backend python objects
monitors = MultiSelectField(_("monitors"), max_length=256,
choices=ServiceMonitor.get_choices())
@ -58,10 +63,13 @@ class Resource(models.Model):
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:
if not self.is_active:
task.delete()
elif task.crontab != self.crontab:
task.crontab = self.crontab
task.save()
@ -97,7 +105,7 @@ class ResourceData(models.Model):
last_update = models.DateTimeField(null=True)
allocated = models.PositiveIntegerField(null=True)
content_object = generic.GenericForeignKey()
content_object = GenericForeignKey()
class Meta:
unique_together = ('resource', 'content_type', 'object_id')
@ -159,7 +167,7 @@ class MonitorData(models.Model):
date = models.DateTimeField(auto_now_add=True)
value = models.PositiveIntegerField()
content_object = generic.GenericForeignKey()
content_object = GenericForeignKey()
class Meta:
get_latest_by = 'date'
@ -170,7 +178,7 @@ class MonitorData(models.Model):
def create_resource_relation():
relation = generic.GenericRelation('resources.ResourceData')
relation = GenericRelation('resources.ResourceData')
for resources in Resource.group_by_content_type():
model = resources[0].content_type.model_class()
model.add_to_class('resources', relation)

View File

@ -7,6 +7,9 @@ from .models import Resource, ResourceData
class ResourceSerializer(serializers.ModelSerializer):
# TODO required allocation serializers (like resource form)
# TODO create missing ResourceData (like resource form)
# TODO make default allocation available on OPTIONS (like resource form)
name = serializers.SerializerMethodField('get_name')
class Meta:

View File

@ -12,6 +12,7 @@ from . import settings
class MailSystemUserBackend(ServiceController):
verbose_name = _("Mail system user")
model = 'mail.Mailbox'
# TODO related_models = ('resources__content_type') ??
DEFAULT_GROUP = 'postfix'

View File

@ -185,8 +185,8 @@ class Apache2Traffic(ServiceMonitor):
context = self.get_context(site)
self.append("""
awk 'BEGIN {
ini = "%(start_date)s";
end = "%(end_date)s";
ini = "%(last_date)s";
end = "%(current_date)s";
months["Jan"] = "01";
months["Feb"] = "02";
@ -218,14 +218,15 @@ class Apache2Traffic(ServiceMonitor):
print sum;
}' %(log_file)s | {
read value
echo %(site_name)s $value
echo %(site_id)s $value
}
""" % context)
def get_context(self, site):
# TODO log timezone!!
return {
'log_file': os.path.join(settings.WEBSITES_BASE_APACHE_LOGS, site.unique_name),
'start_date': '',
'end_date': '',
'site_name': '',
'last_date': self.get_last_date(site).strftime("%Y%m%d%H%M%S"),
'current_date': self.get_current_date().strftime("%Y%m%d%H%M%S"),
'site_id': site.pk,
}

View File

@ -52,6 +52,12 @@ class MultiSelectField(models.CharField):
return [ value for value,__ in arr_choices ]
class NullableCharField(models.CharField):
def get_db_prep_value(self, value, connection=None, prepared=False):
return value or None
if isinstalled('south'):
from south.modelsinspector import add_introspection_rules
add_introspection_rules([], ["^orchestra\.models\.fields\.MultiSelectField"])
add_introspection_rules([], ["^orchestra\.models\.fields\.NullableCharField"])