operations.py 8.81 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# 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/>.

18
from inspect import getargspec
19 20
from logging import getLogger

21
from django.core.exceptions import PermissionDenied, ImproperlyConfigured
22
from django.utils.translation import ugettext_noop
23

24 25
from .models import (activity_context, has_suffix, humanize_exception,
                     HumanReadableObject)
26

27 28
logger = getLogger(__name__)

29 30 31
class Operation(object):
    """Base class for VM operations.
    """
32
    required_perms = None
33
    superuser_required = False
34
    do_not_call_in_templates = True
Kálmán Viktor committed
35
    abortable = False
36
    has_percentage = False
37 38 39 40 41 42 43 44 45 46 47 48

    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

49
    def __prelude(self, request, kwargs):
50 51
        """This method contains the shared prelude of call and async.
        """
52 53 54
        self._operation.im_self.get_from_os(request)

        defaults = {'system': False, 'user': None}
55 56 57 58 59 60 61 62

        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')
63 64
        if user is None:  # parent was a system call
            skip_auth_check = True
65 66 67 68

        # check for unexpected keyword arguments
        argspec = getargspec(self._operation)
        if argspec.keywords is None:  # _operation doesn't take ** args
69
            unexpected_kwargs = set(auxargs) - set(argspec.args)
70 71 72
            if unexpected_kwargs:
                raise TypeError("Operation got unexpected keyword arguments: "
                                "%s" % ", ".join(unexpected_kwargs))
73

74
        if not skip_auth_check:
75
            self.check_auth(user, request)
76 77
        self.check_precond()

78
        return allargs, auxargs
79

80
    def _exec_op(self, request, allargs, auxargs):
81 82
        """Execute the operation inside the specified activity's context.
        """
83 84 85 86 87 88 89 90 91
        # 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)

92
        return self._operation(request, **arguments)
93

94
    def _operation(self, **kwargs):
95 96 97 98 99 100
        """This method is the operation's particular implementation.

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

101
    def call(self, request, **kwargs):
102 103 104
        """Execute the operation (synchronously).

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

    def check_precond(self):
        pass

123 124 125 126 127 128
    @classmethod
    def check_perms(cls, user):
        """Check if user is permitted to run this operation on any instance
        """

        if cls.required_perms is None:
129 130
            raise ImproperlyConfigured(
                "Set required_perms to () if none needed.")
131
        if not user.has_perms(cls.required_perms):
132 133 134
            raise humanize_exception(ugettext_noop(
                "You don't have the required permissions."),
                PermissionDenied())
135 136 137
        if cls.superuser_required and not user.is_superuser:
            raise humanize_exception(ugettext_noop(
                "Superuser privileges are required."), PermissionDenied())
138

139 140 141
    def check_auth(self, user):
        """Check if user is permitted to run this operation on this instance
        """
142 143
        #TODO: implement
        pass
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163

    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__
164 165 166 167
        return self.get_operation_class(name)(self)

    @classmethod
    def get_operation_class(cls, name):
168 169 170
        ops = getattr(cls, operation_registry_name, {})
        op = ops.get(name)
        if op:
171
            return op
172 173
        else:
            raise AttributeError("%r object has no attribute %r" %
174
                                 (cls.__name__, name))
175

176 177 178 179
    def get_available_operations(self, user):
        """Yield Operations that match permissions of user and preconditions.
        """
        for name in getattr(self, operation_registry_name, {}):
180
            op = getattr(self, name)
181 182 183 184 185 186 187 188
            try:
                op.check_auth(user)
                op.check_precond()
            except:
                pass  # unavailable
            else:
                yield op

189
    def get_operation_from_activity_code(self, activity_code):
190 191 192 193 194 195 196
        """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():
197
            if has_suffix(activity_code, op.get_activity_code_suffix()):
198
                return op(self)
199 200 201
        else:
            return None

202

203
def register_operation(op_cls, op_id=None, target_cls=None):
204 205 206 207 208 209
    """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:
210 211 212 213 214 215 216 217
        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.")
218

219 220 221 222 223 224 225 226 227 228 229
    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.")

230 231
    assert not hasattr(target_cls, op_id), (
        "target class already has an attribute with this id")
232 233 234 235 236 237 238 239
    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
240
    return op_cls