# Copyright 2014 Budapest University of Technology and Economics (BME IK) # # This file is part of CIRCLE Cloud. # # CIRCLE is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) # any later version. # # CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along # with CIRCLE. If not, see <http://www.gnu.org/licenses/>. from inspect import getargspec from logging import getLogger from django.core.exceptions import PermissionDenied, ImproperlyConfigured from django.utils.translation import ugettext_noop from .models import (activity_context, has_suffix, humanize_exception, HumanReadableObject) logger = getLogger(__name__) class Operation(object): """Base class for VM operations. """ required_perms = None superuser_required = False do_not_call_in_templates = True abortable = False has_percentage = False def __call__(self, **kwargs): return self.call(**kwargs) def __init__(self, subject): """Initialize a new operation bound to the specified subject. """ self.subject = subject def __unicode__(self): return self.name def __prelude(self, request, kwargs): """This method contains the shared prelude of call and async. """ defaults = {'system': False, 'user': None} allargs = dict(defaults, **kwargs) # all arguments auxargs = allargs.copy() # auxiliary (i.e. only for _operation) args # NOTE: consumed items should be removed from auxargs, and no new items # should be added to it skip_auth_check = auxargs.pop('system') user = auxargs.pop('user') if user is None: # parent was a system call skip_auth_check = True # check for unexpected keyword arguments argspec = getargspec(self._operation) if argspec.keywords is None: # _operation doesn't take ** args unexpected_kwargs = set(auxargs) - set(argspec.args) if unexpected_kwargs: raise TypeError("Operation got unexpected keyword arguments: " "%s" % ", ".join(unexpected_kwargs)) if not skip_auth_check: self.check_auth(user, request) self.check_precond() return allargs, auxargs def _exec_op(self, request, allargs, auxargs): """Execute the operation inside the specified activity's context. """ # compile arguments for _operation argspec = getargspec(self._operation) if argspec.keywords is not None: # _operation takes ** args arguments = allargs.copy() else: # _operation doesn't take ** args arguments = {k: v for (k, v) in allargs.iteritems() if k in argspec.args} arguments.update(auxargs) return self._operation(request, **arguments) def _operation(self, **kwargs): """This method is the operation's particular implementation. Deriving classes should implement this method. """ raise NotImplementedError def call(self, request, **kwargs): """Execute the operation (synchronously). Anticipated keyword arguments: * parent_activity: Parent activity for the operation. If this argument is present, the operation's activity will be created as a child activity of it. * system: Indicates that the operation is invoked by the system, not a User. If this argument is present and has a value of True, then authorization checks are skipped. * user: The User invoking the operation. If this argument is not present, it'll be provided with a default value of None. """ allargs, auxargs = self.__prelude(request, kwargs) logger.info("%s called (synchronously) on %s with the following " "parameters: %r", self.__class__.__name__, self.subject, kwargs) return self._exec_op(request, allargs, auxargs) def check_precond(self): pass @classmethod def check_perms(cls, user): """Check if user is permitted to run this operation on any instance """ if cls.required_perms is None: raise ImproperlyConfigured( "Set required_perms to () if none needed.") if not user.has_perms(cls.required_perms): raise humanize_exception(ugettext_noop( "You don't have the required permissions."), PermissionDenied()) if cls.superuser_required and not user.is_superuser: raise humanize_exception(ugettext_noop( "Superuser privileges are required."), PermissionDenied()) def check_auth(self, user): """Check if user is permitted to run this operation on this instance """ #TODO: implement pass def on_abort(self, activity, error): """This method is called when the operation aborts (i.e. raises an exception). """ pass def on_commit(self, activity): """This method is called when the operation executes successfully. """ pass operation_registry_name = '_ops' class OperatedMixin(object): def __getattr__(self, name): # NOTE: __getattr__ is only called if the attribute doesn't already # exist in your __dict__ return self.get_operation_class(name)(self) @classmethod def get_operation_class(cls, name): ops = getattr(cls, operation_registry_name, {}) op = ops.get(name) if op: return op else: raise AttributeError("%r object has no attribute %r" % (cls.__name__, name)) def get_available_operations(self, user): """Yield Operations that match permissions of user and preconditions. """ for name in getattr(self, operation_registry_name, {}): op = getattr(self, name) try: op.check_auth(user) op.check_precond() except: pass # unavailable else: yield op def get_operation_from_activity_code(self, activity_code): """Get an instance of the Operation corresponding to the specified activity code. :returns: A bound instance of an operation, or None if no matching operation could be found. """ for op in getattr(self, operation_registry_name, {}).itervalues(): if has_suffix(activity_code, op.get_activity_code_suffix()): return op(self) else: return None def register_operation(op_cls, op_id=None, target_cls=None): """Register the specified operation with the target class. You can optionally specify an ID to be used for the registration; otherwise, the operation class' 'id' attribute will be used. """ if op_id is None: try: op_id = op_cls.id except AttributeError: raise NotImplementedError("Operations should specify an 'id' " "attribute designating the name the " "operation can be called by on its " "host. Alternatively, provide the name " "in the 'op_id' parameter to this call.") if target_cls is None: try: target_cls = op_cls.host_cls except AttributeError: raise NotImplementedError("Operations should specify a 'host_cls' " "attribute designating the host class " "the operation should be registered to. " "Alternatively, provide the host class " "in the 'target_cls' parameter to this " "call.") assert not hasattr(target_cls, op_id), ( "target class already has an attribute with this id") if not hasattr(target_cls, operation_registry_name): setattr(target_cls, operation_registry_name, dict()) getattr(target_cls, operation_registry_name)[op_id] = op_cls def get_operation_class(cls, name): ops = getattr(cls, operation_registry_name, {}) op = ops.get(name) if op: return op else: raise AttributeError("%r object has no attribute %r" % (cls.__name__, name)) def __getattr__(self, name): # NOTE: __getattr__ is only called if the attribute doesn't already # exist in your __dict__ return get_operation_class(type(self), name)(self) target_cls.__getattr__ = __getattr__ return op_cls