207 lines
6.4 KiB
Python
207 lines
6.4 KiB
Python
from functools import partial
|
|
|
|
from django.apps import apps
|
|
from django.utils import timezone
|
|
from django.utils.functional import cached_property
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from orchestra import plugins
|
|
|
|
from . import methods
|
|
|
|
|
|
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)
|
|
super(ServiceMount, cls).__init__(name, bases, attrs)
|
|
|
|
|
|
class ServiceBackend(plugins.Plugin):
|
|
"""
|
|
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'
|
|
function_method = methods.Python
|
|
type = 'task' # 'sync'
|
|
ignore_fields = []
|
|
actions = []
|
|
default_route_match = 'True'
|
|
|
|
__metaclass__ = ServiceMount
|
|
|
|
def __unicode__(self):
|
|
return type(self).__name__
|
|
|
|
def __str__(self):
|
|
return unicode(self)
|
|
|
|
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)
|
|
|
|
@classmethod
|
|
def get_actions(cls):
|
|
return [ action for action in cls.actions if action in dir(cls) ]
|
|
|
|
@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
|
|
|
|
@classmethod
|
|
def get_backend(cls, name):
|
|
return cls.get_plugin(name)
|
|
|
|
@classmethod
|
|
def model_class(cls):
|
|
return apps.get_model(cls.model)
|
|
|
|
@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
|
|
return list(scripts.iteritems())
|
|
|
|
def get_banner(self):
|
|
time = timezone.now().strftime("%h %d, %Y %I:%M:%S")
|
|
return "Generated by Orchestra at %s" % time
|
|
|
|
def execute(self, server, async=False):
|
|
from .models import BackendLog
|
|
scripts = self.scripts
|
|
state = BackendLog.STARTED
|
|
if not scripts:
|
|
state = BackendLog.SUCCESS
|
|
log = BackendLog.objects.create(backend=self.get_name(), state=state, server=server)
|
|
for method, commands in scripts:
|
|
method(log, server, commands, async)
|
|
if log.state != BackendLog.SUCCESS:
|
|
break
|
|
return log
|
|
|
|
def append(self, *cmd):
|
|
# aggregate commands acording to its execution method
|
|
if isinstance(cmd[0], basestring):
|
|
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]))
|
|
else:
|
|
self.cmd_section[-1][1].append(cmd)
|
|
|
|
def prepare(self):
|
|
"""
|
|
hook for executing something at the beging
|
|
define functions or initialize state
|
|
"""
|
|
self.append(
|
|
'set -e\n'
|
|
'set -o pipefail'
|
|
)
|
|
|
|
def commit(self):
|
|
"""
|
|
hook for executing something at the end
|
|
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 0')
|
|
|
|
|
|
class ServiceController(ServiceBackend):
|
|
actions = ('save', 'delete')
|
|
abstract = True
|
|
|
|
@classmethod
|
|
def get_verbose_name(cls):
|
|
return _("[S] %s") % super(ServiceController, cls).get_verbose_name()
|
|
|
|
@classmethod
|
|
def get_backends(cls):
|
|
""" filter controller classes """
|
|
backends = super(ServiceController, cls).get_backends()
|
|
return [
|
|
backend for backend in backends if ServiceController in backend.__mro__
|
|
]
|