2014-10-03 17:37:36 +00:00
|
|
|
import socket
|
|
|
|
|
2015-05-01 17:23:22 +00:00
|
|
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
2014-05-08 16:59:35 +00:00
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
from django.db import models
|
2015-09-20 10:57:13 +00:00
|
|
|
from django.utils.encoding import force_text
|
2015-04-07 15:14:49 +00:00
|
|
|
from django.utils.functional import cached_property
|
2014-10-07 13:08:59 +00:00
|
|
|
from django.utils.module_loading import autodiscover_modules
|
2014-05-08 16:59:35 +00:00
|
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
|
2014-10-03 17:37:36 +00:00
|
|
|
from orchestra.core.validators import validate_ip_address, ValidationError
|
2015-05-14 13:28:54 +00:00
|
|
|
from orchestra.models.fields import NullableCharField, MultiSelectField
|
2014-10-07 13:08:59 +00:00
|
|
|
#from orchestra.utils.apps import autodiscover
|
2014-05-08 16:59:35 +00:00
|
|
|
|
2015-03-04 21:06:16 +00:00
|
|
|
from . import settings
|
2014-05-08 16:59:35 +00:00
|
|
|
from .backends import ServiceBackend
|
|
|
|
|
|
|
|
|
|
|
|
class Server(models.Model):
|
|
|
|
""" Machine runing daemons (services) """
|
|
|
|
name = models.CharField(_("name"), max_length=256, unique=True)
|
2014-07-10 15:19:06 +00:00
|
|
|
address = NullableCharField(_("address"), max_length=256, blank=True,
|
2015-04-05 10:46:24 +00:00
|
|
|
null=True, unique=True, help_text=_("IP address or domain name"))
|
2014-05-08 16:59:35 +00:00
|
|
|
description = models.TextField(_("description"), blank=True)
|
|
|
|
os = models.CharField(_("operative system"), max_length=32,
|
2015-04-05 10:46:24 +00:00
|
|
|
choices=settings.ORCHESTRATION_OS_CHOICES,
|
|
|
|
default=settings.ORCHESTRATION_DEFAULT_OS)
|
2014-05-08 16:59:35 +00:00
|
|
|
|
2015-04-02 16:14:55 +00:00
|
|
|
def __str__(self):
|
2015-05-05 19:42:55 +00:00
|
|
|
return self.name or str(self.address)
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
def get_address(self):
|
|
|
|
if self.address:
|
|
|
|
return self.address
|
|
|
|
return self.name
|
2014-10-03 17:37:36 +00:00
|
|
|
|
|
|
|
def get_ip(self):
|
|
|
|
if self.address:
|
|
|
|
return self.address
|
|
|
|
try:
|
|
|
|
validate_ip_address(self.name)
|
|
|
|
except ValidationError:
|
|
|
|
return socket.gethostbyname(self.name)
|
|
|
|
else:
|
|
|
|
return self.name
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
|
|
|
|
class BackendLog(models.Model):
|
|
|
|
RECEIVED = 'RECEIVED'
|
|
|
|
TIMEOUT = 'TIMEOUT'
|
|
|
|
STARTED = 'STARTED'
|
|
|
|
SUCCESS = 'SUCCESS'
|
|
|
|
FAILURE = 'FAILURE'
|
|
|
|
ERROR = 'ERROR'
|
|
|
|
REVOKED = 'REVOKED'
|
2015-04-16 15:46:26 +00:00
|
|
|
ABORTED = 'ABORTED'
|
2015-05-05 19:42:55 +00:00
|
|
|
NOTHING = 'NOTHING'
|
2015-03-11 16:32:33 +00:00
|
|
|
# Special state for mocked backendlogs
|
|
|
|
EXCEPTION = 'EXCEPTION'
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
STATES = (
|
|
|
|
(RECEIVED, RECEIVED),
|
|
|
|
(TIMEOUT, TIMEOUT),
|
|
|
|
(STARTED, STARTED),
|
|
|
|
(SUCCESS, SUCCESS),
|
|
|
|
(FAILURE, FAILURE),
|
|
|
|
(ERROR, ERROR),
|
2015-04-16 15:46:26 +00:00
|
|
|
(ABORTED, ABORTED),
|
2014-05-08 16:59:35 +00:00
|
|
|
(REVOKED, REVOKED),
|
2015-05-05 19:42:55 +00:00
|
|
|
(NOTHING, NOTHING),
|
2014-05-08 16:59:35 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
backend = models.CharField(_("backend"), max_length=256)
|
2015-05-01 18:05:34 +00:00
|
|
|
state = models.CharField(_("state"), max_length=16, choices=STATES, default=RECEIVED)
|
|
|
|
server = models.ForeignKey(Server, verbose_name=_("server"), related_name='execution_logs')
|
2014-05-08 16:59:35 +00:00
|
|
|
script = models.TextField(_("script"))
|
2014-07-17 16:09:24 +00:00
|
|
|
stdout = models.TextField(_("stdout"))
|
2015-05-01 18:05:34 +00:00
|
|
|
stderr = models.TextField(_("stderr"))
|
2014-05-08 16:59:35 +00:00
|
|
|
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,
|
2015-04-05 10:46:24 +00:00
|
|
|
help_text="Celery task ID when used as execution backend")
|
2015-07-09 10:19:30 +00:00
|
|
|
created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
|
2014-09-26 15:05:20 +00:00
|
|
|
updated_at = models.DateTimeField(_("updated"), auto_now=True)
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
class Meta:
|
2014-09-24 20:09:41 +00:00
|
|
|
get_latest_by = 'id'
|
2014-05-08 16:59:35 +00:00
|
|
|
|
2015-04-02 16:14:55 +00:00
|
|
|
def __str__(self):
|
2014-11-16 18:39:31 +00:00
|
|
|
return "%s@%s" % (self.backend, self.server)
|
|
|
|
|
2014-05-08 16:59:35 +00:00
|
|
|
@property
|
|
|
|
def execution_time(self):
|
2014-09-30 14:46:29 +00:00
|
|
|
return (self.updated_at-self.created_at).total_seconds()
|
2014-09-10 16:53:09 +00:00
|
|
|
|
2015-05-08 14:05:57 +00:00
|
|
|
@property
|
|
|
|
def has_finished(self):
|
|
|
|
return self.state not in (self.STARTED, self.RECEIVED)
|
|
|
|
|
2015-05-12 12:38:40 +00:00
|
|
|
@property
|
|
|
|
def is_success(self):
|
|
|
|
return self.state in (self.SUCCESS, self.NOTHING)
|
|
|
|
|
2014-09-10 16:53:09 +00:00
|
|
|
def backend_class(self):
|
|
|
|
return ServiceBackend.get_backend(self.backend)
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
|
2015-05-13 12:16:51 +00:00
|
|
|
class BackendOperationQuerySet(models.QuerySet):
|
|
|
|
def create(self, **kwargs):
|
|
|
|
instance = kwargs.get('instance')
|
2015-09-20 10:57:13 +00:00
|
|
|
if instance and 'instance_repr' not in kwargs:
|
|
|
|
kwargs['instance_repr'] = force_text(instance)[:256]
|
2015-05-13 12:16:51 +00:00
|
|
|
return super(BackendOperationQuerySet, self).create(**kwargs)
|
|
|
|
|
|
|
|
|
2014-05-08 16:59:35 +00:00
|
|
|
class BackendOperation(models.Model):
|
|
|
|
"""
|
|
|
|
Encapsulates an operation, storing its related object, the action and the backend.
|
|
|
|
"""
|
|
|
|
log = models.ForeignKey('orchestration.BackendLog', related_name='operations')
|
2014-07-10 15:19:06 +00:00
|
|
|
backend = models.CharField(_("backend"), max_length=256)
|
2014-07-09 16:17:43 +00:00
|
|
|
action = models.CharField(_("action"), max_length=64)
|
2014-05-08 16:59:35 +00:00
|
|
|
content_type = models.ForeignKey(ContentType)
|
2015-05-13 12:16:51 +00:00
|
|
|
object_id = models.PositiveIntegerField(null=True)
|
|
|
|
instance_repr = models.CharField(_("instance representation"), max_length=256)
|
2014-09-26 15:05:20 +00:00
|
|
|
|
2015-05-01 17:23:22 +00:00
|
|
|
instance = GenericForeignKey('content_type', 'object_id')
|
2015-05-13 12:16:51 +00:00
|
|
|
objects = BackendOperationQuerySet.as_manager()
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _("Operation")
|
|
|
|
verbose_name_plural = _("Operations")
|
|
|
|
|
2015-04-02 16:14:55 +00:00
|
|
|
def __str__(self):
|
2014-10-10 17:17:20 +00:00
|
|
|
return '%s.%s(%s)' % (self.backend, self.action, self.instance)
|
2014-05-08 16:59:35 +00:00
|
|
|
|
2015-04-07 15:14:49 +00:00
|
|
|
@cached_property
|
2014-07-10 15:19:06 +00:00
|
|
|
def backend_class(self):
|
|
|
|
return ServiceBackend.get_backend(self.backend)
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
|
2014-10-07 13:08:59 +00:00
|
|
|
autodiscover_modules('backends')
|
2014-05-08 16:59:35 +00:00
|
|
|
|
|
|
|
|
2015-09-04 10:22:14 +00:00
|
|
|
class RouteQuerySet(models.QuerySet):
|
|
|
|
def get_for_operation(self, operation, **kwargs):
|
|
|
|
cache = kwargs.get('cache', {})
|
|
|
|
if not cache:
|
|
|
|
for route in self.filter(is_active=True).select_related('host'):
|
|
|
|
for action in route.backend_class.get_actions():
|
|
|
|
key = (route.backend, action)
|
|
|
|
try:
|
|
|
|
cache[key].append(route)
|
|
|
|
except KeyError:
|
|
|
|
cache[key] = [route]
|
|
|
|
routes = []
|
|
|
|
backend_cls = operation.backend
|
|
|
|
key = (backend_cls.get_name(), operation.action)
|
|
|
|
try:
|
|
|
|
target_routes = cache[key]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
for route in target_routes:
|
|
|
|
if route.matches(operation.instance):
|
|
|
|
routes.append(route)
|
|
|
|
return routes
|
|
|
|
|
|
|
|
|
2014-05-08 16:59:35 +00:00
|
|
|
class Route(models.Model):
|
|
|
|
"""
|
|
|
|
Defines the routing that determine in which server a backend is executed
|
|
|
|
"""
|
|
|
|
backend = models.CharField(_("backend"), max_length=256,
|
2015-04-05 10:46:24 +00:00
|
|
|
choices=ServiceBackend.get_choices())
|
2014-05-08 16:59:35 +00:00
|
|
|
host = models.ForeignKey(Server, verbose_name=_("host"))
|
|
|
|
match = models.CharField(_("match"), max_length=256, blank=True, default='True',
|
2015-04-05 10:46:24 +00:00
|
|
|
help_text=_("Python expression used for selecting the targe host, "
|
|
|
|
"<em>instance</em> referes to the current object."))
|
2015-05-06 14:39:25 +00:00
|
|
|
async = models.BooleanField(default=False,
|
|
|
|
help_text=_("Whether or not block the request/response cycle waitting this backend to "
|
2015-05-06 19:30:13 +00:00
|
|
|
"finish its execution. Usually you want slave servers to run asynchronously."))
|
2015-05-14 13:28:54 +00:00
|
|
|
async_actions = MultiSelectField(max_length=256, blank=True,
|
|
|
|
help_text=_("Specify individual actions to be executed asynchronoulsy."))
|
2014-05-08 16:59:35 +00:00
|
|
|
# method = models.CharField(_("method"), max_lenght=32, choices=method_choices,
|
|
|
|
# default=MethodBackend.get_default())
|
2014-09-30 10:20:11 +00:00
|
|
|
is_active = models.BooleanField(_("active"), default=True)
|
2014-05-08 16:59:35 +00:00
|
|
|
|
2015-09-04 10:22:14 +00:00
|
|
|
objects = RouteQuerySet.as_manager()
|
2015-05-14 13:28:54 +00:00
|
|
|
|
2014-05-08 16:59:35 +00:00
|
|
|
class Meta:
|
|
|
|
unique_together = ('backend', 'host')
|
|
|
|
|
2015-04-02 16:14:55 +00:00
|
|
|
def __str__(self):
|
2014-05-08 16:59:35 +00:00
|
|
|
return "%s@%s" % (self.backend, self.host)
|
|
|
|
|
2015-04-07 15:14:49 +00:00
|
|
|
@cached_property
|
2015-03-29 16:10:07 +00:00
|
|
|
def backend_class(self):
|
|
|
|
return ServiceBackend.get_backend(self.backend)
|
2014-05-08 16:59:35 +00:00
|
|
|
|
2015-03-29 16:10:07 +00:00
|
|
|
def clean(self):
|
|
|
|
if not self.match:
|
|
|
|
self.match = 'True'
|
|
|
|
if self.backend:
|
2015-04-02 16:14:55 +00:00
|
|
|
backend_model = self.backend_class.model_class()
|
2015-03-29 16:10:07 +00:00
|
|
|
try:
|
|
|
|
obj = backend_model.objects.all()[0]
|
|
|
|
except IndexError:
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
bool(self.matches(obj))
|
2015-04-01 15:49:21 +00:00
|
|
|
except Exception as exception:
|
2015-03-29 16:10:07 +00:00
|
|
|
name = type(exception).__name__
|
2015-06-09 11:16:36 +00:00
|
|
|
raise ValidationError(': '.join((name, str(exception))))
|
2015-03-29 16:10:07 +00:00
|
|
|
|
2015-05-14 13:28:54 +00:00
|
|
|
def action_is_async(self, action):
|
|
|
|
return action in self.async_actions
|
|
|
|
|
2014-07-17 16:09:24 +00:00
|
|
|
def matches(self, instance):
|
|
|
|
safe_locals = {
|
2014-10-04 09:29:18 +00:00
|
|
|
'instance': instance,
|
|
|
|
'obj': instance,
|
|
|
|
instance._meta.model_name: instance,
|
2014-07-17 16:09:24 +00:00
|
|
|
}
|
|
|
|
return eval(self.match, safe_locals)
|
|
|
|
|
2014-05-08 16:59:35 +00:00
|
|
|
def enable(self):
|
|
|
|
self.is_active = True
|
|
|
|
self.save()
|
|
|
|
|
|
|
|
def disable(self):
|
|
|
|
self.is_active = False
|
|
|
|
self.save()
|