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

245 lines
7.7 KiB
Python
Raw Normal View History

import textwrap
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'
2015-05-06 14:39:25 +00:00
# Don't wait for the backend to finish before continuing with request/response
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
serialize = False
2015-04-24 11:39:20 +00:00
doc_settings = None
2015-05-05 19:42:55 +00:00
# By default backend will not run if actions do not generate insctructions,
# If your backend uses prepare() or commit() only then you should set force_empty_action_execution = True
force_empty_action_execution = 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',
2015-05-05 19:42:55 +00:00
'function_method',
'set_head',
'set_tail',
'set_content',
'actions',
)
if attr == 'prepare':
2015-05-05 19:42:55 +00:00
self.set_head()
elif attr == 'commit':
2015-05-05 19:42:55 +00:00
self.set_tail()
elif attr not in IGNORE_ATTRS and attr in self.actions:
self.set_content()
return super(ServiceBackend, self).__getattribute__(attr)
2014-05-08 16:59:35 +00:00
2015-05-05 19:42:55 +00:00
def set_head(self):
self.cmd_section = self.head
def set_tail(self):
self.cmd_section = self.tail
def set_content(self):
self.cmd_section = self.content
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)
2016-03-11 12:19:34 +00:00
if type(related).__name__ == 'RelatedManager':
return related.all()
return [related]
return []
2014-05-08 16:59:35 +00:00
@classmethod
2015-04-14 15:22:01 +00:00
def get_backends(cls, instance=None, action=None):
backends = cls.get_plugins()
included = []
# Filter for instance or action
for backend in backends:
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-10-05 13:31:08 +00:00
now = timezone.localtime(timezone.now())
time = 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
2015-05-06 14:39:25 +00:00
def create_log(self, server, **kwargs):
2014-05-08 16:59:35 +00:00
from .models import BackendLog
2015-05-06 14:39:25 +00:00
state = BackendLog.RECEIVED
run = bool(self.scripts) or (self.force_empty_action_execution or bool(self.content))
2015-05-05 19:42:55 +00:00
if not run:
state = BackendLog.NOTHING
using = kwargs.pop('using', None)
manager = BackendLog.objects
if using:
manager = manager.using(using)
log = manager.create(backend=self.get_name(), state=state, server=server)
2015-05-06 14:39:25 +00:00
return log
def execute(self, server, async=False, log=None):
from .models import BackendLog
if log is None:
log = self.create_log(server)
run = log.state != BackendLog.NOTHING
2015-05-05 19:42:55 +00:00
if run:
2015-05-06 14:39:25 +00:00
scripts = self.scripts
2015-05-05 19:42:55 +00:00
for method, commands in scripts:
method(log, server, commands, async)
if log.state != BackendLog.SUCCESS:
break
2014-05-08 16:59:35 +00:00
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
def get_context(self, obj):
return {}
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(textwrap.dedent("""\
set -e
set -o pipefail
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
2015-05-22 13:15:06 +00:00
apply the configuration, usually reloading a service
reloading a service is done in a separated method in order to reload
2014-05-08 16:59:35 +00:00
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 [
2015-04-14 15:22:01 +00:00
backend for backend in backends if issubclass(backend, ServiceController)
2014-07-16 15:20:16 +00:00
]