django-orchestra-test/orchestra/contrib/orchestration/backends.py

213 lines
6.7 KiB
Python
Raw Normal View History

2015-04-05 18:02:36 +00:00
import re
2014-05-08 16:59:35 +00:00
from functools import partial
from django.apps import apps
2014-07-11 14:48:46 +00:00
from django.utils import timezone
2014-10-11 16:21:51 +00:00
from django.utils.translation import ugettext_lazy as _
2014-05-08 16:59:35 +00:00
from orchestra import plugins
2014-05-08 16:59:35 +00:00
from . import methods
2015-04-05 18:02:36 +00:00
def replace(context, pattern, repl):
""" applies replace to all context str values """
2015-04-05 18:02:36 +00:00
for key, value in context.items():
if isinstance(value, str):
context[key] = value.replace(pattern, repl)
return context
2014-11-27 19:17:26 +00:00
class ServiceMount(plugins.PluginMount):
def __init__(cls, name, bases, attrs):
# Make sure backends specify a model attribute
if not (attrs.get('abstract', False) or name == 'ServiceBackend' or cls.model):
raise AttributeError("'%s' does not have a defined model attribute." % cls)
2014-11-27 19:17:26 +00:00
super(ServiceMount, cls).__init__(name, bases, attrs)
2015-04-02 16:14:55 +00:00
class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
2014-05-08 16:59:35 +00:00
"""
Service management backend base class
It uses the _unit of work_ design principle, which allows bulk operations to
be conviniently supported. Each backend generates the configuration for all
the changes of all modified objects, reloading the daemon just once.
"""
model = None
related_models = () # ((model, accessor__attribute),)
script_method = methods.SSH
script_executable = '/bin/bash'
2014-05-08 16:59:35 +00:00
function_method = methods.Python
type = 'task' # 'sync'
2014-05-08 16:59:35 +00:00
ignore_fields = []
2014-07-09 16:17:43 +00:00
actions = []
2015-03-04 21:06:16 +00:00
default_route_match = 'True'
# Force the backend manager to block in multiple backend executions executing them synchronously
block = False
2014-05-08 16:59:35 +00:00
def __str__(self):
2015-04-02 16:14:55 +00:00
return type(self).__name__
2014-05-08 16:59:35 +00:00
def __init__(self):
self.head = []
self.content = []
self.tail = []
def __getattribute__(self, attr):
""" Select head, content or tail section depending on the method name """
IGNORE_ATTRS = (
'append',
'cmd_section',
'head',
'tail',
'content',
'script_method',
'function_method'
)
if attr == 'prepare':
self.cmd_section = self.head
elif attr == 'commit':
self.cmd_section = self.tail
elif attr not in IGNORE_ATTRS:
self.cmd_section = self.content
return super(ServiceBackend, self).__getattribute__(attr)
2014-05-08 16:59:35 +00:00
2014-07-09 16:17:43 +00:00
@classmethod
def get_actions(cls):
return [ action for action in cls.actions if action in dir(cls) ]
2014-05-08 16:59:35 +00:00
@classmethod
def get_name(cls):
return cls.__name__
@classmethod
def is_main(cls, obj):
opts = obj._meta
return cls.model == '%s.%s' % (opts.app_label, opts.object_name)
@classmethod
def get_related(cls, obj):
opts = obj._meta
model = '%s.%s' % (opts.app_label, opts.object_name)
for rel_model, field in cls.related_models:
if rel_model == model:
related = obj
for attribute in field.split('__'):
related = getattr(related, attribute)
return related
return None
@classmethod
def get_backends(cls, instance=None, action=None, active=True):
from .models import Route
backends = cls.get_plugins()
included = []
if active:
active_backends = Route.objects.filter(is_active=True).values_list('backend', flat=True)
# Filter for instance or action
for backend in backends:
if active and backend.get_name() not in active_backends:
continue
include = True
if instance:
opts = instance._meta
if backend.model != '.'.join((opts.app_label, opts.object_name)):
include = False
if include and action:
if action not in backend.get_actions():
include = False
if include:
included.append(backend)
return included
2014-05-08 16:59:35 +00:00
2014-07-10 10:03:22 +00:00
@classmethod
def get_backend(cls, name):
return cls.get(name)
2014-05-08 16:59:35 +00:00
2014-11-14 16:12:56 +00:00
@classmethod
def model_class(cls):
return apps.get_model(cls.model)
2014-11-14 16:12:56 +00:00
@property
def scripts(self):
""" group commands based on their method """
if not self.content:
return []
scripts = {}
for method, cmd in self.content:
scripts[method] = []
for method, commands in self.head + self.content + self.tail:
try:
scripts[method] += commands
except KeyError:
pass
2015-04-02 16:14:55 +00:00
return list(scripts.items())
2014-05-08 16:59:35 +00:00
def get_banner(self):
2015-03-10 21:51:10 +00:00
time = timezone.now().strftime("%h %d, %Y %I:%M:%S %Z")
2014-10-04 09:29:18 +00:00
return "Generated by Orchestra at %s" % time
2014-05-08 16:59:35 +00:00
def execute(self, server, async=False):
2014-05-08 16:59:35 +00:00
from .models import BackendLog
scripts = self.scripts
state = BackendLog.STARTED
if not scripts:
state = BackendLog.SUCCESS
2014-05-08 16:59:35 +00:00
log = BackendLog.objects.create(backend=self.get_name(), state=state, server=server)
for method, commands in scripts:
method(log, server, commands, async)
2014-05-08 16:59:35 +00:00
if log.state != BackendLog.SUCCESS:
break
return log
def append(self, *cmd):
# aggregate commands acording to its execution method
2015-04-02 16:14:55 +00:00
if isinstance(cmd[0], str):
2014-05-08 16:59:35 +00:00
method = self.script_method
cmd = cmd[0]
else:
method = self.function_method
cmd = partial(*cmd)
if not self.cmd_section or self.cmd_section[-1][0] != method:
self.cmd_section.append((method, [cmd]))
2014-05-08 16:59:35 +00:00
else:
self.cmd_section[-1][1].append(cmd)
2014-05-08 16:59:35 +00:00
2014-07-25 15:17:50 +00:00
def prepare(self):
"""
hook for executing something at the beging
define functions or initialize state
"""
self.append(
'set -e\n'
'set -o pipefail\n'
'exit_code=0;'
)
2014-07-25 15:17:50 +00:00
2014-05-08 16:59:35 +00:00
def commit(self):
"""
hook for executing something at the end
2014-05-08 16:59:35 +00:00
apply the configuration, usually reloading a service
reloading a service is done in a separated method in order to reload
the service once in bulk operations
"""
self.append('exit $exit_code')
2014-07-09 16:17:43 +00:00
class ServiceController(ServiceBackend):
actions = ('save', 'delete')
2014-10-11 16:21:51 +00:00
abstract = True
@classmethod
def get_verbose_name(cls):
return _("[S] %s") % super(ServiceController, cls).get_verbose_name()
2014-07-09 16:17:43 +00:00
@classmethod
def get_backends(cls):
""" filter controller classes """
backends = super(ServiceController, cls).get_backends()
2014-07-16 15:20:16 +00:00
return [
backend for backend in backends if ServiceController in backend.__mro__
2014-07-16 15:20:16 +00:00
]