django-orchestra/orchestra/contrib/resources/models.py

357 lines
14 KiB
Python
Raw Permalink Normal View History

2014-07-10 15:19:06 +00:00
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
2014-07-08 15:19:15 +00:00
from django.contrib.contenttypes.models import ContentType
2014-10-06 14:57:02 +00:00
from django.apps import apps
2014-07-11 22:08:16 +00:00
from django.db import models
2014-09-22 15:59:53 +00:00
from django.utils import timezone
2014-10-27 13:29:02 +00:00
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
2015-05-13 14:27:24 +00:00
from djcelery.models import PeriodicTask
2014-07-08 15:19:15 +00:00
from orchestra.core import validators
2014-07-25 13:27:31 +00:00
from orchestra.models import queryset, fields
2014-11-13 15:34:00 +00:00
from orchestra.models.utils import get_model_field_path
2014-07-08 15:19:15 +00:00
2015-05-13 14:27:24 +00:00
from . import tasks
2014-07-09 16:17:43 +00:00
from .backends import ServiceMonitor
2015-04-08 14:41:09 +00:00
from .aggregations import Aggregation
2014-10-23 15:38:46 +00:00
from .validators import validate_scale
2014-07-09 16:17:43 +00:00
2014-07-08 15:19:15 +00:00
2014-07-25 13:27:31 +00:00
class ResourceQuerySet(models.QuerySet):
group_by = queryset.group_by
2014-07-08 15:19:15 +00:00
class Resource(models.Model):
2014-07-09 16:17:43 +00:00
"""
Defines a resource, a resource is basically an interpretation of data
gathered by a Monitor
"""
2014-07-09 16:17:43 +00:00
LAST = 'LAST'
MONTHLY_SUM = 'MONTHLY_SUM'
MONTHLY_AVG = 'MONTHLY_AVG'
2014-07-08 15:19:15 +00:00
PERIODS = (
2014-07-09 16:17:43 +00:00
(LAST, _("Last")),
(MONTHLY_SUM, _("Monthly sum")),
(MONTHLY_AVG, _("Monthly avg")),
2014-07-08 15:19:15 +00:00
)
2014-10-07 13:08:59 +00:00
_related = set() # keeps track of related models for resource cleanup
2014-07-16 15:20:16 +00:00
name = models.CharField(_("name"), max_length=32,
2015-05-13 14:27:24 +00:00
help_text=_("Required. 32 characters or fewer. Lowercase letters, "
"digits and hyphen only."),
validators=[validators.validate_name])
2014-07-16 15:20:16 +00:00
verbose_name = models.CharField(_("verbose name"), max_length=256)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE,
2015-04-05 10:46:24 +00:00
help_text=_("Model where this resource will be hooked."))
2015-04-08 14:41:09 +00:00
aggregation = models.CharField(_("aggregation"), max_length=16,
choices=Aggregation.get_choices(), default=Aggregation.get_choices()[0][0],
2015-04-05 10:46:24 +00:00
help_text=_("Method used for aggregating this resource monitored data."))
2014-09-26 15:05:20 +00:00
on_demand = models.BooleanField(_("on demand"), default=False,
2015-04-05 10:46:24 +00:00
help_text=_("If enabled the resource will not be pre-allocated, "
"but allocated under the application demand"))
2014-07-09 16:17:43 +00:00
default_allocation = models.PositiveIntegerField(_("default allocation"),
2015-04-05 10:46:24 +00:00
null=True, blank=True,
help_text=_("Default allocation value used when this is not an "
"on demand resource"))
2014-07-16 15:20:16 +00:00
unit = models.CharField(_("unit"), max_length=16,
2015-04-05 10:46:24 +00:00
help_text=_("The unit in which this resource is represented. "
"For example GB, KB or subscribers"))
2014-10-23 15:38:46 +00:00
scale = models.CharField(_("scale"), max_length=32, validators=[validate_scale],
2015-04-05 10:46:24 +00:00
help_text=_("Scale in which this resource monitoring resoults should "
"be prorcessed to match with unit. e.g. <tt>10**9</tt>"))
disable_trigger = models.BooleanField(_("disable trigger"), default=True,
2015-04-05 10:46:24 +00:00
help_text=_("Disables monitors exeeded and recovery triggers"))
2015-05-03 17:44:46 +00:00
crontab = models.ForeignKey('djcelery.CrontabSchedule', verbose_name=_("crontab"),
2016-04-27 08:35:13 +00:00
null=True, blank=True, on_delete=models.SET_NULL,
2015-04-05 10:46:24 +00:00
help_text=_("Crontab for periodic execution. "
"Leave it empty to disable periodic monitoring"))
2014-07-25 13:27:31 +00:00
monitors = fields.MultiSelectField(_("monitors"), max_length=256, blank=True,
2015-04-05 10:46:24 +00:00
choices=ServiceMonitor.get_choices(),
help_text=_("Monitor backends used for monitoring this resource."))
2014-09-30 10:20:11 +00:00
is_active = models.BooleanField(_("active"), default=True)
2014-07-25 13:27:31 +00:00
objects = ResourceQuerySet.as_manager()
2014-07-16 15:20:16 +00:00
class Meta:
unique_together = (
('name', 'content_type'),
('verbose_name', 'content_type')
)
2015-04-02 16:14:55 +00:00
def __str__(self):
2015-10-05 13:31:08 +00:00
return "%s-%s" % (self.content_type, self.name)
@cached_property
2015-04-08 14:41:09 +00:00
def aggregation_class(self):
return Aggregation.get(self.aggregation)
@cached_property
2015-04-08 14:41:09 +00:00
def aggregation_instance(self):
""" Per request lived type_instance """
2015-04-08 14:41:09 +00:00
return self.aggregation_class(self)
2014-10-23 15:38:46 +00:00
def clean(self):
self.verbose_name = self.verbose_name.strip()
if self.on_demand and self.default_allocation:
raise validators.ValidationError({
'default_allocation': _("Default allocation can not be set for 'on demand' services")
})
# Validate that model path exists between ct and each monitor.model
monitor_errors = []
for monitor in self.monitors:
try:
self.get_model_path(monitor)
except (RuntimeError, LookupError):
2015-05-01 17:23:22 +00:00
model = apps.get_model(ServiceMonitor.get_backend(monitor).model)
2014-11-24 20:09:44 +00:00
monitor_errors.append(model._meta.model_name)
if monitor_errors:
2014-11-24 20:09:44 +00:00
model_name = self.content_type.model_class()._meta.model_name
raise validators.ValidationError({
'monitors': [
_("Path does not exists between '%s' and '%s'") % (
2014-11-24 20:09:44 +00:00
error,
model_name,
) for error in monitor_errors
]})
2014-07-10 10:03:22 +00:00
def save(self, *args, **kwargs):
super(Resource, self).save(*args, **kwargs)
2015-05-13 14:27:24 +00:00
# This only works on tests (multiprocessing used on real deployments)
2014-10-09 17:04:12 +00:00
apps.get_app_config('resources').reload_relations()
2015-09-18 11:29:52 +00:00
def sync_periodic_task(self, delete=False):
""" sync periodic task on save/delete resource operations """
2015-10-05 13:31:08 +00:00
name = 'monitor.%s' % self
2015-09-18 11:29:52 +00:00
if delete or not self.crontab or not self.is_active:
PeriodicTask.objects.filter(name=name).delete()
elif self.pk:
2015-05-13 14:27:24 +00:00
try:
task = PeriodicTask.objects.get(name=name)
except PeriodicTask.DoesNotExist:
2015-05-13 14:28:41 +00:00
if self.is_active:
2015-05-13 14:27:24 +00:00
PeriodicTask.objects.create(
name=name,
task='resources.Monitor',
2015-05-13 14:28:41 +00:00
args=[self.pk],
crontab=self.crontab
2015-05-13 14:27:24 +00:00
)
else:
2015-05-13 14:28:41 +00:00
if task.crontab != self.crontab:
task.crontab = self.crontab
2015-05-13 14:27:24 +00:00
task.save(update_fields=['crontab'])
def get_model_path(self, monitor):
""" returns a model path between self.content_type and monitor.model """
resource_model = self.content_type.model_class()
2014-11-27 19:17:26 +00:00
monitor_model = ServiceMonitor.get_backend(monitor).model_class()
return get_model_field_path(monitor_model, resource_model)
def get_scale(self):
return eval(self.scale)
2014-11-10 15:40:51 +00:00
def get_verbose_name(self):
return self.verbose_name or self.name
def monitor(self, run_async=True):
if run_async:
return tasks.monitor.apply_async(self.pk)
return tasks.monitor(self.pk)
2014-07-10 10:03:22 +00:00
class ResourceDataQuerySet(models.QuerySet):
def get_or_create(self, obj, resource):
ct = ContentType.objects.get_for_model(type(obj))
try:
return self.get(
content_type=ct,
object_id=obj.pk,
resource=resource
), False
except self.model.DoesNotExist:
return self.create(
content_object=obj,
resource=resource,
allocated=resource.default_allocation
), True
2014-07-10 10:03:22 +00:00
class ResourceData(models.Model):
""" Stores computed resource usage and allocation """
resource = models.ForeignKey(Resource, on_delete=models.CASCADE, related_name='dataset', verbose_name=_("resource"))
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, verbose_name=_("content type"))
2016-02-19 10:11:28 +00:00
object_id = models.PositiveIntegerField(_("object id"))
2014-11-17 14:17:33 +00:00
used = models.DecimalField(_("used"), max_digits=16, decimal_places=3, null=True,
2015-04-05 10:46:24 +00:00
editable=False)
2014-11-09 10:16:07 +00:00
updated_at = models.DateTimeField(_("updated"), null=True, editable=False)
2015-08-05 22:58:35 +00:00
allocated = models.PositiveIntegerField(_("allocated"), null=True, blank=True)
content_object_repr = models.CharField(_("content object representation"), max_length=256,
editable=False)
2014-07-10 15:19:06 +00:00
content_object = GenericForeignKey()
objects = ResourceDataQuerySet.as_manager()
2014-07-10 10:03:22 +00:00
class Meta:
unique_together = ('resource', 'content_type', 'object_id')
verbose_name_plural = _("resource data")
2016-02-19 10:11:28 +00:00
index_together = (
('content_type', 'object_id'),
)
2023-12-30 09:00:54 +00:00
# def __str__(self):
# return "%s: %s" % (self.resource, self.content_object)
2015-04-02 16:14:55 +00:00
def __str__(self):
2023-12-30 09:00:54 +00:00
return "%s" % (self.content_object)
2014-10-06 14:57:02 +00:00
@property
def unit(self):
return self.resource.unit
2015-10-07 22:05:00 +00:00
@property
def verbose_name(self):
return self.resource.verbose_name
2014-07-10 10:03:22 +00:00
def get_used(self):
2015-04-01 15:49:21 +00:00
resource = self.resource
total = 0
has_result = False
for monitor, dataset in self.get_monitor_datasets():
2015-07-16 13:07:15 +00:00
dataset = resource.aggregation_instance.filter(dataset)
2015-04-08 14:41:09 +00:00
usage = resource.aggregation_instance.compute_usage(dataset)
if usage is not None:
has_result = True
total += usage
return float(total)/resource.get_scale() if has_result else None
2014-09-22 15:59:53 +00:00
def update(self, current=None):
if current is None:
current = self.get_used()
self.used = current or 0
2014-09-26 15:05:20 +00:00
self.updated_at = timezone.now()
self.content_object_repr = str(self.content_object)
self.save(update_fields=('used', 'updated_at', 'content_object_repr'))
def monitor(self, run_async=False):
ids = (self.object_id,)
if run_async:
return tasks.monitor.delay(self.resource_id, ids=ids)
return tasks.monitor(self.resource_id, ids=ids)
2014-11-13 15:34:00 +00:00
def get_monitor_datasets(self):
resource = self.resource
for monitor in resource.monitors:
path = resource.get_model_path(monitor)
if path == []:
2014-11-13 15:34:00 +00:00
dataset = MonitorData.objects.filter(
monitor=monitor,
content_type=self.content_type_id,
2015-07-16 13:07:15 +00:00
object_id=self.object_id,
2014-11-13 15:34:00 +00:00
)
else:
fields = '__'.join(path)
2014-11-27 19:17:26 +00:00
monitor_model = ServiceMonitor.get_backend(monitor).model_class()
2014-11-13 15:34:00 +00:00
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,
2015-07-16 13:07:15 +00:00
object_id__in=pks,
2014-11-13 15:34:00 +00:00
)
yield monitor, dataset
2014-07-08 15:19:15 +00:00
2015-04-09 14:32:10 +00:00
class MonitorDataQuerySet(models.QuerySet):
group_by = queryset.group_by
2014-07-08 15:19:15 +00:00
class MonitorData(models.Model):
2014-07-09 16:17:43 +00:00
""" Stores monitored data """
2015-07-27 12:55:35 +00:00
monitor = models.CharField(_("monitor"), max_length=256, db_index=True,
choices=ServiceMonitor.get_choices())
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, verbose_name=_("content type"))
2016-02-19 10:11:28 +00:00
object_id = models.PositiveIntegerField(_("object id"))
2015-07-27 12:55:35 +00:00
created_at = models.DateTimeField(_("created"), default=timezone.now, db_index=True)
2014-07-16 15:20:16 +00:00
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
2015-08-05 22:58:35 +00:00
state = models.DecimalField(_("state"), max_digits=16, decimal_places=2, null=True,
help_text=_("Optional field used to store current state needed for diff-based monitoring."))
content_object_repr = models.CharField(_("content object representation"), max_length=256,
editable=False)
2014-07-10 15:19:06 +00:00
content_object = GenericForeignKey()
2015-04-09 14:32:10 +00:00
objects = MonitorDataQuerySet.as_manager()
2024-02-03 21:43:27 +00:00
launch_id = models.PositiveIntegerField(_("launch id"), blank=True, null=True)
2014-07-09 16:17:43 +00:00
class Meta:
2014-09-24 20:09:41 +00:00
get_latest_by = 'id'
2014-07-09 16:17:43 +00:00
verbose_name_plural = _("monitor data")
2016-02-19 10:11:28 +00:00
index_together = (
('content_type', 'object_id'),
)
2015-04-02 16:14:55 +00:00
def __str__(self):
2014-07-08 15:19:15 +00:00
return str(self.monitor)
2014-10-27 13:29:02 +00:00
@cached_property
def unit(self):
return self.resource.unit
2014-07-10 10:03:22 +00:00
def create_resource_relation():
2014-07-18 15:32:27 +00:00
class ResourceHandler(object):
""" account.resources.web """
def __getattr__(self, attr):
2014-07-22 21:47:01 +00:00
""" get or build ResourceData """
2015-10-07 22:05:00 +00:00
if attr.startswith('_'):
raise AttributeError
2014-11-09 10:16:07 +00:00
try:
return self.obj.__resource_cache[attr]
except AttributeError:
self.obj.__resource_cache = {}
except KeyError:
pass
2014-07-21 15:43:36 +00:00
try:
rdata = self.obj.resource_set.get(resource__name=attr)
2014-07-21 15:43:36 +00:00
except ResourceData.DoesNotExist:
model = self.obj._meta.model_name
2014-11-13 15:34:00 +00:00
resource = Resource.objects.get(
content_type__model=model,
name=attr,
is_active=True
)
rdata = ResourceData(
2014-11-13 15:34:00 +00:00
content_object=self.obj,
2015-08-05 22:58:35 +00:00
content_object_repr=str(self.obj),
2014-11-13 15:34:00 +00:00
resource=resource,
allocated=resource.default_allocation
)
self.obj.__resource_cache[attr] = rdata
return rdata
2014-07-18 15:32:27 +00:00
def __get__(self, obj, cls):
2014-07-25 15:17:50 +00:00
""" proxy handled object """
2014-07-18 15:32:27 +00:00
self.obj = obj
return self
2015-10-07 22:05:00 +00:00
def __iter__(self):
return iter(self.obj.resource_set.all())
2014-10-07 13:08:59 +00:00
# Clean previous state
for related in Resource._related:
try:
delattr(related, 'resource_set')
delattr(related, 'resources')
except AttributeError:
pass
else:
related._meta.private_fields = [
field for field in related._meta.private_fields if field.remote_field.model != ResourceData
2014-10-07 13:08:59 +00:00
]
2015-04-02 16:14:55 +00:00
for ct, resources in Resource.objects.group_by('content_type').items():
2014-07-25 13:27:31 +00:00
model = ct.model_class()
2015-03-23 15:36:51 +00:00
relation = GenericRelation('resources.ResourceData')
2014-07-18 15:32:27 +00:00
model.add_to_class('resource_set', relation)
model.resources = ResourceHandler()
2014-10-07 13:08:59 +00:00
Resource._related.add(model)