operations.py 7.64 KB
Newer Older
1
from inspect import getargspec
2 3
from logging import getLogger

4 5 6 7 8
from .models import activity_context

from django.core.exceptions import PermissionDenied


9 10 11
logger = getLogger(__name__)


12 13 14 15 16
class Operation(object):
    """Base class for VM operations.
    """
    async_queue = 'localhost.man'
    required_perms = ()
17
    do_not_call_in_templates = True
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

    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, kwargs):
        """This method contains the shared prelude of call and async.
        """
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
        defaults = {'parent_activity': None, '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')
        parent_activity = auxargs.pop('parent_activity')

        # check for unexpected keyword arguments
        argspec = getargspec(self._operation)
        if argspec.keywords is None:  # _operation doesn't take ** args
            unexpected_kwargs = set(arg for arg in auxargs
                                    if arg not in argspec.args)
            if unexpected_kwargs:
                raise TypeError("Operation got unexpected keyword arguments: "
                                "%s" % ", ".join(unexpected_kwargs))
52

53
        if not skip_auth_check:
54 55 56
            self.check_auth(user)
        self.check_precond()

57 58 59 60 61
        activity = self.create_activity(parent=parent_activity, user=user)

        return activity, allargs, auxargs

    def _exec_op(self, allargs, auxargs):
62 63
        """Execute the operation inside the specified activity's context.
        """
64 65 66 67 68 69 70 71 72 73
        # 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)

        with activity_context(allargs['activity'], on_abort=self.on_abort,
74
                              on_commit=self.on_commit):
75
            return self._operation(**arguments)
76

77
    def _operation(self, **kwargs):
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
        """This method is the operation's particular implementation.

        Deriving classes should implement this method.
        """
        raise NotImplementedError

    def async(self, **kwargs):
        """Execute the operation asynchronously.

        Only a quick, preliminary check is ran before creating the associated
        activity and queuing the job.

        The returned value is the handle for the asynchronous job.

        For more information, check the synchronous call's documentation.
        """
94 95 96
        logger.info("%s called asynchronously on %s with the following "
                    "parameters: %r", self.__class__.__name__, self.subject,
                    kwargs)
97 98 99 100
        activity, allargs, auxargs = self.__prelude(kwargs)
        return self.async_operation.apply_async(
            args=(self.id, self.subject.pk, activity.pk, allargs, auxargs, ),
            queue=self.async_queue)
101 102 103 104 105

    def call(self, **kwargs):
        """Execute the operation (synchronously).

        Anticipated keyword arguments:
106 107 108
        * 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.
109 110 111
        * 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.
112 113
        * user: The User invoking the operation. If this argument is not
                present, it'll be provided with a default value of None.
114
        """
115 116 117
        logger.info("%s called (synchronously) on %s with the following "
                    "parameters: %r", self.__class__.__name__, self.subject,
                    kwargs)
118 119 120
        activity, allargs, auxargs = self.__prelude(kwargs)
        allargs['activity'] = activity
        return self._exec_op(allargs, auxargs)
121 122 123 124 125 126 127 128 129

    def check_precond(self):
        pass

    def check_auth(self, user):
        if not user.has_perms(self.required_perms):
            raise PermissionDenied("%s doesn't have the required permissions."
                                   % user)

130
    def create_activity(self, parent, user):
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
        raise NotImplementedError

    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__
        cls = self.__class__
        ops = getattr(cls, operation_registry_name, {})
        op = ops.get(name)
        if op:
            return op(self)
        else:
            raise AttributeError("%r object has no attribute %r" %
                                 (self.__class__.__name__, name))

161 162 163 164 165 166 167 168 169 170 171 172 173
    def get_available_operations(self, user):
        """Yield Operations that match permissions of user and preconditions.
        """
        for name in getattr(self, operation_registry_name, {}):
            try:
                op = getattr(self, name)
                op.check_auth(user)
                op.check_precond()
            except:
                pass  # unavailable
            else:
                yield op

174

175
def register_operation(op_cls, op_id=None, target_cls=None):
176 177 178 179 180 181
    """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:
182 183 184 185 186 187 188 189
        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.")
190

191 192 193 194 195 196 197 198 199 200 201
    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.")

202 203 204 205 206 207 208 209
    if not issubclass(target_cls, OperatedMixin):
        raise TypeError("%r is not a subclass of %r" %
                        (target_cls.__name__, OperatedMixin.__name__))

    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