From b93be150c2b19c9f1886c6b6fbf052be987efc4a Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Fri, 14 Nov 2014 16:52:54 +0000 Subject: [PATCH] Initial implementation of backend arbitrary action execution --- orchestra/apps/orchestration/backends.py | 23 ++++++++++++++++++--- orchestra/apps/orchestration/methods.py | 13 +++++------- orchestra/apps/orchestration/middlewares.py | 2 ++ orchestra/apps/orchestration/models.py | 6 ++++++ orchestra/apps/systemusers/actions.py | 4 +++- orchestra/apps/systemusers/backends.py | 5 +++++ 6 files changed, 41 insertions(+), 12 deletions(-) diff --git a/orchestra/apps/orchestration/backends.py b/orchestra/apps/orchestration/backends.py index 2efa8eee..effa64db 100644 --- a/orchestra/apps/orchestration/backends.py +++ b/orchestra/apps/orchestration/backends.py @@ -64,8 +64,24 @@ class ServiceBackend(plugins.Plugin): return None @classmethod - def get_backends(cls): - return cls.get_plugins() + def get_backends(cls, instance=None, action=None): + backends = cls.get_plugins() + included = [] + # Filter for instance or action + if 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) + backends = included + return backends @classmethod def get_backend(cls, name): @@ -126,6 +142,7 @@ class ServiceController(ServiceBackend): @classmethod def get_backends(cls): """ filter controller classes """ + backends = super(ServiceController, cls).get_backends() return [ - plugin for plugin in cls.plugins if ServiceController in plugin.__mro__ + backend for backend in backends if ServiceController in backend.__mro__ ] diff --git a/orchestra/apps/orchestration/methods.py b/orchestra/apps/orchestration/methods.py index 263d2dbb..ec7b3fab 100644 --- a/orchestra/apps/orchestration/methods.py +++ b/orchestra/apps/orchestration/methods.py @@ -19,8 +19,6 @@ transports = {} def BashSSH(backend, log, server, cmds): - from .models import BackendLog - script = '\n'.join(['set -e', 'set -o pipefail'] + cmds + ['exit 0']) script = script.replace('\r', '') digest = hashlib.md5(script).hexdigest() @@ -48,7 +46,7 @@ def BashSSH(backend, log, server, cmds): ssh.connect(addr, username='root', key_filename=settings.ORCHESTRATION_SSH_KEY_PATH, timeout=10) except socket.error: logger.error('%s timed out on %s' % (backend, server)) - log.state = BackendLog.TIMEOUT + log.state = log.TIMEOUT log.save(update_fields=['state']) return transport = ssh.get_transport() @@ -92,11 +90,11 @@ def BashSSH(backend, log, server, cmds): if channel.exit_status_ready(): break log.exit_code = exit_code = channel.recv_exit_status() - log.state = BackendLog.SUCCESS if exit_code == 0 else BackendLog.FAILURE + log.state = log.SUCCESS if exit_code == 0 else log.FAILURE logger.debug('%s execution state on %s is %s' % (backend, server, log.state)) log.save() except: - log.state = BackendLog.ERROR + log.state = log.ERROR log.traceback = ExceptionInfo(sys.exc_info()).traceback logger.error('Exception while executing %s on %s' % (backend, server)) logger.debug(log.traceback) @@ -109,7 +107,6 @@ def BashSSH(backend, log, server, cmds): def Python(backend, log, server, cmds): - from .models import BackendLog script = [ str(cmd.func.func_name) + str(cmd.args) for cmd in cmds ] script = json.dumps(script, indent=4).replace('"', '') log.script = '\n'.join([log.script, script]) @@ -121,10 +118,10 @@ def Python(backend, log, server, cmds): stdout += str(result) except: log.exit_code = 1 - log.state = BackendLog.FAILURE + log.state = log.FAILURE log.traceback = ExceptionInfo(sys.exc_info()).traceback else: log.exit_code = 0 - log.state = BackendLog.SUCCESS + log.state = log.SUCCESS log.stdout += stdout log.save() diff --git a/orchestra/apps/orchestration/middlewares.py b/orchestra/apps/orchestration/middlewares.py index cd31a1f4..19be9b8d 100644 --- a/orchestra/apps/orchestration/middlewares.py +++ b/orchestra/apps/orchestration/middlewares.py @@ -50,6 +50,7 @@ class OperationsMiddleware(object): return pending_operations = cls.get_pending_operations() for backend in ServiceBackend.get_backends(): + # Check if there exists a related instance to be executed for this backend instance = None if backend.is_main(kwargs['instance']): instance = kwargs['instance'] @@ -61,6 +62,7 @@ class OperationsMiddleware(object): instance = candidate # related objects with backend.model trigger save() action = Operation.SAVE + # Maintain consistent state of pending_operations based on save/delete behaviour if instance is not None: # Prevent creating a deleted instance by deleting existing saves if action == Operation.DELETE: diff --git a/orchestra/apps/orchestration/models.py b/orchestra/apps/orchestration/models.py index 171eb4a0..8203562c 100644 --- a/orchestra/apps/orchestration/models.py +++ b/orchestra/apps/orchestration/models.py @@ -138,6 +138,12 @@ class BackendOperation(models.Model): def execute(cls, operations): return manager.execute(operations) + @classmethod + def execute_action(cls, instance, action): + backends = ServiceBackend.get_backends(instance=instance, action=action) + operations = [cls.create(backend, instance, action) for backend in backends] + return cls.execute(operations) + def backend_class(self): return ServiceBackend.get_backend(self.backend) diff --git a/orchestra/apps/systemusers/actions.py b/orchestra/apps/systemusers/actions.py index a938e16a..b5a0b5d2 100644 --- a/orchestra/apps/systemusers/actions.py +++ b/orchestra/apps/systemusers/actions.py @@ -10,6 +10,7 @@ from django.utils.translation import ungettext, ugettext_lazy as _ from orchestra.admin.decorators import action_with_confirmation from orchestra.admin.utils import change_url +from orchestra.apps.orchestration.models import BackendOperation as Operation class GrantPermissionForm(forms.Form): @@ -22,7 +23,8 @@ class GrantPermissionForm(forms.Form): @action_with_confirmation(extra_context=dict(form=GrantPermissionForm())) def grant_permission(modeladmin, request, queryset): + user = queryset.get() + log = Operation.execute_action(user, 'grant_permission') # TODO - pass grant_permission.url_name = 'grant-permission' grant_permission.verbose_name = _("Grant permission") diff --git a/orchestra/apps/systemusers/backends.py b/orchestra/apps/systemusers/backends.py index c2a2ef30..c150d721 100644 --- a/orchestra/apps/systemusers/backends.py +++ b/orchestra/apps/systemusers/backends.py @@ -12,6 +12,7 @@ from . import settings class SystemUserBackend(ServiceController): verbose_name = _("System user") model = 'systemusers.SystemUser' + actions = ('save', 'delete', 'grant_permission') def save(self, user): context = self.get_context(user) @@ -40,6 +41,10 @@ class SystemUserBackend(ServiceController): self.append("groupdel %(username)s || true" % context) self.delete_home(context, user) + def grant_permission(self, user): + context = self.get_context(user) + # TODO setacl + def delete_home(self, context, user): if user.is_main: # TODO delete instead of this shit