from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import ugettext_lazy as _ from orchestra.utils.apps import autodiscover from orchestra.utils.functional import cached from . import settings, manager 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")) description = models.TextField(_("description"), blank=True) os = models.CharField(_("operative system"), max_length=32, choices=settings.ORCHESTRATION_OS_CHOICES, default=settings.ORCHESTRATION_DEFAULT_OS) def __unicode__(self): return self.name def get_address(self): if self.address: return self.address return self.name class BackendLog(models.Model): RECEIVED = 'RECEIVED' TIMEOUT = 'TIMEOUT' STARTED = 'STARTED' SUCCESS = 'SUCCESS' FAILURE = 'FAILURE' ERROR = 'ERROR' REVOKED = 'REVOKED' STATES = ( (RECEIVED, RECEIVED), (TIMEOUT, TIMEOUT), (STARTED, STARTED), (SUCCESS, SUCCESS), (FAILURE, FAILURE), (ERROR, ERROR), (REVOKED, REVOKED), ) backend = models.CharField(_("backend"), max_length=256) state = models.CharField(_("state"), max_length=16, choices=STATES, default=RECEIVED) server = models.ForeignKey(Server, verbose_name=_("server"), related_name='execution_logs') script = models.TextField(_("script")) stdout = models.TextField() stderr = models.TextField() traceback = models.TextField(_("traceback")) exit_code = models.IntegerField(_("exit code"), null=True) task_id = models.CharField(_("task ID"), max_length=36, unique=True, null=True, help_text="Celery task ID") created = models.DateTimeField(_("created"), auto_now_add=True) last_update = models.DateTimeField(_("last update"), auto_now=True) class Meta: get_latest_by = 'created' @property def execution_time(self): return (self.last_update-self.created).total_seconds() class BackendOperation(models.Model): """ Encapsulates an operation, storing its related object, the action and the backend. """ DELETE = 'delete' SAVE = 'save' 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) action = models.CharField(_("action"), max_length=64) content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField() instance = generic.GenericForeignKey('content_type', 'object_id') class Meta: verbose_name = _("Operation") verbose_name_plural = _("Operations") def __unicode__(self): return '%s.%s(%s)' % (self.backend_class, self.action, self.instance) def __hash__(self): """ set() """ backend = getattr(self, 'backend', self.backend_class) return hash(backend) + hash(self.instance) + hash(self.action) def __eq__(self, operation): """ set() """ return hash(self) == hash(operation) @classmethod def create(cls, backend, instance, action): op = cls(backend_class=backend.get_name(), instance=instance, action=action) op.backend = backend return op @classmethod def execute(cls, operations): return manager.execute(operations) autodiscover('backends') class Route(models.Model): """ Defines the routing that determine in which server a backend is executed """ backend = models.CharField(_("backend"), max_length=256, choices=ServiceBackend.get_choices()) host = models.ForeignKey(Server, verbose_name=_("host")) match = models.CharField(_("match"), max_length=256, blank=True, default='True', help_text=_("Python expression used for selecting the targe host, " "instance referes to the current object.")) # async = models.BooleanField(default=False) # method = models.CharField(_("method"), max_lenght=32, choices=method_choices, # default=MethodBackend.get_default()) is_active = models.BooleanField(_("is active"), default=True) class Meta: unique_together = ('backend', 'host') def __unicode__(self): return "%s@%s" % (self.backend, self.host) # def clean(self): # backend, method = self.get_backend_class(), self.get_method_class() # if not backend.type in method.types: # msg = _("%s backend is not compatible with %s method") # raise ValidationError(msg % (self.backend, self.method) @classmethod @cached def get_routing_table(cls): table = {} for route in cls.objects.filter(is_active=True): for action in route.backend_class().get_actions(): key = (route.backend, action) try: table[key].append(route) except KeyError: table[key] = [route] return table @classmethod def get_servers(cls, operation): table = cls.get_routing_table() servers = [] key = (operation.backend.get_name(), operation.action) try: routes = table[key] except KeyError: return servers safe_locals = { 'instance': operation.instance } for route in routes: if eval(route.match, safe_locals): servers.append(route.host) return servers def backend_class(self): return ServiceBackend.get_backend(self.backend) # def method_class(self): # for method in MethodBackend.get_backends(): # if method.get_name() == self.method: # return method # raise ValueError('This method is not registered') def enable(self): self.is_active = True self.save() def disable(self): self.is_active = False self.save()