views.py 111 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 __future__ import unicode_literals, absolute_import
19

20
from collections import OrderedDict
21
from itertools import chain
22 23
from os import getenv
import json
Őry Máté committed
24
import logging
25
import re
26
import requests
27

28
from django.conf import settings
29
from django.contrib.auth.models import User, Group
Őry Máté committed
30
from django.contrib.auth.views import login, redirect_to_login
31
from django.contrib.messages.views import SuccessMessageMixin
32
from django.core.exceptions import (
33
    PermissionDenied, SuspiciousOperation,
34
)
35 36
from django.core import signing
from django.core.urlresolvers import reverse, reverse_lazy
37
from django.db.models import Count, Q
38
from django.http import HttpResponse, HttpResponseRedirect, Http404
Őry Máté committed
39
from django.shortcuts import redirect, render, get_object_or_404
40
from django.views.decorators.http import require_GET, require_POST
41
from django.views.generic.detail import SingleObjectMixin
42
from django.views.generic import (TemplateView, DetailView, View, DeleteView,
43
                                  UpdateView, CreateView, ListView)
44
from django.contrib import messages
45
from django.utils.translation import ugettext as _, ugettext_noop
46
from django.utils.translation import ungettext as __
47
from django.template.loader import render_to_string
48
from django.template import RequestContext
49

50
from django.forms.models import inlineformset_factory
51
from django_tables2 import SingleTableView
52 53
from braces.views import (LoginRequiredMixin, SuperuserRequiredMixin,
                          PermissionRequiredMixin)
54
from braces.views._access import AccessMixin
55
from celery.exceptions import TimeoutError
Kálmán Viktor committed
56

57 58
from django_sshkey.models import UserKey

59
from .forms import (
60
    CircleAuthenticationForm, HostForm, LeaseForm, MyProfileForm,
61
    NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm,
62
    UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
63
    VmSaveForm, UserKeyForm, VmRenewForm,
64
    CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
65 66
    TraitsForm, RawDataForm, GroupPermissionForm, AclUserAddForm,
    VmAddInterfaceForm,
67
)
68 69 70

from .tables import (
    NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable,
71
    GroupListTable, UserKeyListTable
72
)
73
from common.models import HumanReadableObject
Őry Máté committed
74 75 76 77
from vm.models import (
    Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
    InterfaceTemplate, Lease, Node, NodeActivity, Trait,
)
78
from storage.models import Disk
79
from firewall.models import Vlan, Host, Rule
80
from .models import Favourite, Profile, GroupProfile, FutureMember
81

Őry Máté committed
82
logger = logging.getLogger(__name__)
83
saml_available = hasattr(settings, "SAML_CONFIG")
84

85

86 87 88 89
def search_user(keyword):
    try:
        return User.objects.get(username=keyword)
    except User.DoesNotExist:
90 91 92 93
        try:
            return User.objects.get(profile__org_id=keyword)
        except User.DoesNotExist:
            return User.objects.get(email=keyword)
94 95


96 97 98 99 100 101 102 103 104 105 106 107 108
class RedirectToLoginMixin(AccessMixin):

    redirect_exception_classes = (PermissionDenied, )

    def dispatch(self, request, *args, **kwargs):
        try:
            return super(RedirectToLoginMixin, self).dispatch(
                request, *args, **kwargs)
        except self.redirect_exception_classes:
            if not request.user.is_authenticated():
                return redirect_to_login(request.get_full_path(),
                                         self.get_login_url(),
                                         self.get_redirect_field_name())
109 110
            else:
                raise
111 112


113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
class GroupCodeMixin(object):

    @classmethod
    def get_available_group_codes(cls, request):
        newgroups = []
        if saml_available:
            from djangosaml2.cache import StateCache, IdentityCache
            from djangosaml2.conf import get_config
            from djangosaml2.views import _get_subject_id
            from saml2.client import Saml2Client

            state = StateCache(request.session)
            conf = get_config(None, request)
            client = Saml2Client(conf, state_cache=state,
                                 identity_cache=IdentityCache(request.session),
                                 logger=logger)
            subject_id = _get_subject_id(request.session)
            identity = client.users.get_identity(subject_id,
                                                 check_not_on_or_after=False)
            if identity:
                attributes = identity[0]
                owneratrs = getattr(
                    settings, 'SAML_GROUP_OWNER_ATTRIBUTES', [])
136 137
                for group in chain(*[attributes[i]
                                     for i in owneratrs if i in attributes]):
138 139 140 141 142 143 144 145
                    try:
                        GroupProfile.search(group)
                    except Group.DoesNotExist:
                        newgroups.append(group)

        return newgroups


146
class FilterMixin(object):
147

148 149 150 151
    def get_queryset_filters(self):
        filters = {}
        for item in self.allowed_filters:
            if item in self.request.GET:
152 153 154 155 156
                filters[self.allowed_filters[item]] = (
                    self.request.GET[item].split(",")
                    if self.allowed_filters[item].endswith("__in") else
                    self.request.GET[item])

157
        return filters
158

159 160 161
    def get_queryset(self):
        return super(FilterMixin,
                     self).get_queryset().filter(**self.get_queryset_filters())
162 163


164
class IndexView(LoginRequiredMixin, TemplateView):
Kálmán Viktor committed
165
    template_name = "dashboard/index.html"
166

167
    def get_context_data(self, **kwargs):
168
        user = self.request.user
169
        context = super(IndexView, self).get_context_data(**kwargs)
170

171
        # instances
172
        favs = Instance.objects.filter(favourite__user=self.request.user)
173
        instances = Instance.get_objects_with_level(
174
            'user', user, disregard_superuser=True).filter(destroyed_at=None)
175 176 177
        display = list(favs) + list(set(instances) - set(favs))
        for d in display:
            d.fav = True if d in favs else False
178
        context.update({
179
            'instances': display[:5],
180
            'more_instances': instances.count() - len(instances[:5])
181 182
        })

183 184
        running = instances.filter(status='RUNNING')
        stopped = instances.exclude(status__in=('RUNNING', 'NOSTATE'))
185

186
        context.update({
187 188 189
            'running_vms': running[:20],
            'running_vm_num': running.count(),
            'stopped_vm_num': stopped.count()
190
        })
191

192 193 194 195
        # nodes
        if user.is_superuser:
            nodes = Node.objects.all()
            context.update({
196 197
                'nodes': nodes[:5],
                'more_nodes': nodes.count() - len(nodes[:5]),
198 199 200 201 202 203 204 205 206 207
                'sum_node_num': nodes.count(),
                'node_num': {
                    'running': Node.get_state_count(True, True),
                    'missing': Node.get_state_count(False, True),
                    'disabled': Node.get_state_count(True, False),
                    'offline': Node.get_state_count(False, False)
                }
            })

        # groups
208
        if user.has_module_perms('auth'):
209 210
            profiles = GroupProfile.get_objects_with_level('operator', user)
            groups = Group.objects.filter(groupprofile__in=profiles)
211 212 213 214
            context.update({
                'groups': groups[:5],
                'more_groups': groups.count() - len(groups[:5]),
            })
215 216 217 218 219 220

        # template
        if user.has_perm('vm.create_template'):
            context['templates'] = InstanceTemplate.get_objects_with_level(
                'operator', user).all()[:5]

221 222
        return context

223

224
class CheckedDetailView(LoginRequiredMixin, DetailView):
225 226
    read_level = 'user'

227 228 229
    def get_has_level(self):
        return self.object.has_level

230 231
    def get_context_data(self, **kwargs):
        context = super(CheckedDetailView, self).get_context_data(**kwargs)
232
        if not self.get_has_level()(self.request.user, self.read_level):
233 234 235 236
            raise PermissionDenied()
        return context


237 238 239 240 241 242 243 244
class VmDetailVncTokenView(CheckedDetailView):
    template_name = "dashboard/vm-detail.html"
    model = Instance

    def get(self, request, **kwargs):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'operator'):
            raise PermissionDenied()
245 246
        if not request.user.has_perm('vm.access_console'):
            raise PermissionDenied()
247
        if self.object.node:
248 249 250 251
            with instance_activity(
                    code_suffix='console-accessed', instance=self.object,
                    user=request.user, readable_name=ugettext_noop(
                        "console access"), concurrency_check=False):
252 253 254 255 256
                port = self.object.vnc_port
                host = str(self.object.node.host.ipv4)
                value = signing.dumps({'host': host, 'port': port},
                                      key=getenv("PROXY_SECRET", 'asdasd')),
                return HttpResponse('vnc/?d=%s' % value)
257 258 259 260
        else:
            raise Http404()


261
class VmDetailView(CheckedDetailView):
Kálmán Viktor committed
262
    template_name = "dashboard/vm-detail.html"
263
    model = Instance
264 265

    def get_context_data(self, **kwargs):
266
        context = super(VmDetailView, self).get_context_data(**kwargs)
267
        instance = context['instance']
268
        ops = get_operations(instance, self.request.user)
269
        context.update({
Bach Dániel committed
270
            'graphite_enabled': settings.GRAPHITE_URL is not None,
271
            'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
272
                                    kwargs={'pk': self.object.pk}),
273 274
            'ops': ops,
            'op': {i.op: i for i in ops},
275
        })
276 277

        # activity data
278 279
        context['activities'] = self.object.get_merged_activities(
            self.request.user)
280

281
        context['vlans'] = Vlan.get_objects_with_level(
282
            'user', self.request.user
283
        ).exclude(  # exclude already added interfaces
284 285 286
            pk__in=Interface.objects.filter(
                instance=self.get_object()).values_list("vlan", flat=True)
        ).all()
287 288
        context['acl'] = AclUpdateView.get_acl_data(
            instance, self.request.user, 'dashboard.views.vm-acl')
289
        context['aclform'] = AclUserAddForm()
290 291
        context['os_type_icon'] = instance.os_type.replace("unknown",
                                                           "question")
292 293 294
        # ipv6 infos
        context['ipv6_host'] = instance.get_connect_host(use_ipv6=True)
        context['ipv6_port'] = instance.get_connect_port(use_ipv6=True)
295 296 297 298 299

        # resources forms
        if self.request.user.is_superuser:
            context['traits_form'] = TraitsForm(instance=instance)
            context['raw_data_form'] = RawDataForm(instance=instance)
300

301 302 303
        # resources change perm
        context['can_change_resources'] = self.request.user.has_perm(
            "vm.change_resources")
304

305
        return context
Kálmán Viktor committed
306

307
    def post(self, request, *args, **kwargs):
308 309 310
        options = {
            'change_password': self.__change_password,
            'new_name': self.__set_name,
311
            'new_description': self.__set_description,
312 313
            'new_tag': self.__add_tag,
            'to_remove': self.__remove_tag,
314
            'port': self.__add_port,
315
            'abort_operation': self.__abort_operation,
316 317 318 319
        }
        for k, v in options.iteritems():
            if request.POST.get(k) is not None:
                return v(request)
320
        raise Http404()
Kálmán Viktor committed
321

322 323
        raise Http404()

324 325 326 327
    def __change_password(self, request):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
Kálmán Viktor committed
328

329
        self.object.change_password(user=request.user)
330
        messages.success(request, _("Password changed."))
331
        if request.is_ajax():
332
            return HttpResponse("Success.")
333 334 335
        else:
            return redirect(reverse_lazy("dashboard.views.detail",
                                         kwargs={'pk': self.object.pk}))
336

337 338
    def __set_name(self, request):
        self.object = self.get_object()
339 340
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
341 342 343 344
        new_name = request.POST.get("new_name")
        Instance.objects.filter(pk=self.object.pk).update(
            **{'name': new_name})

345
        success_message = _("VM successfully renamed.")
346 347 348 349 350 351 352 353 354 355 356 357
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
                'vm_pk': self.object.pk
            }
            return HttpResponse(
                json.dumps(response),
                content_type="application/json"
            )
        else:
            messages.success(request, success_message)
358 359 360 361 362 363 364 365 366 367 368
            return redirect(self.object.get_absolute_url())

    def __set_description(self, request):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()

        new_description = request.POST.get("new_description")
        Instance.objects.filter(pk=self.object.pk).update(
            **{'description': new_description})

369
        success_message = _("VM description successfully updated.")
370 371 372 373 374 375 376 377 378 379 380 381
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_description': new_description,
            }
            return HttpResponse(
                json.dumps(response),
                content_type="application/json"
            )
        else:
            messages.success(request, success_message)
            return redirect(self.object.get_absolute_url())
382

Kálmán Viktor committed
383 384 385
    def __add_tag(self, request):
        new_tag = request.POST.get('new_tag')
        self.object = self.get_object()
386 387
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
Kálmán Viktor committed
388 389

        if len(new_tag) < 1:
390
            message = u"Please input something."
Kálmán Viktor committed
391
        elif len(new_tag) > 20:
392
            message = u"Tag name is too long."
Kálmán Viktor committed
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
        else:
            self.object.tags.add(new_tag)

        try:
            messages.error(request, message)
        except:
            pass

        return redirect(reverse_lazy("dashboard.views.detail",
                                     kwargs={'pk': self.object.pk}))

    def __remove_tag(self, request):
        try:
            to_remove = request.POST.get('to_remove')
            self.object = self.get_object()
408 409
            if not self.object.has_level(request.user, 'owner'):
                raise PermissionDenied()
Kálmán Viktor committed
410 411 412 413 414 415 416 417 418 419 420

            self.object.tags.remove(to_remove)
            message = u"Success"
        except:  # note this won't really happen
            message = u"Not success"

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': message}),
                content_type="application=json"
            )
421 422 423
        else:
            return redirect(reverse_lazy("dashboard.views.detail",
                            kwargs={'pk': self.object.pk}))
Kálmán Viktor committed
424

425 426
    def __add_port(self, request):
        object = self.get_object()
427 428
        if (not object.has_level(request.user, 'owner') or
                not request.user.has_perm('vm.config_ports')):
429
            raise PermissionDenied()
430 431 432 433 434 435

        port = request.POST.get("port")
        proto = request.POST.get("proto")

        try:
            error = None
436 437 438
            interfaces = object.interface_set.all()
            host = Host.objects.get(pk=request.POST.get("host_pk"),
                                    interface__in=interfaces)
439
            host.add_port(proto, private=port)
440 441 442 443 444
        except Host.DoesNotExist:
            logger.error('Tried to add port to nonexistent host %d. User: %s. '
                         'Instance: %s', request.POST.get("host_pk"),
                         unicode(request.user), object)
            raise PermissionDenied()
445
        except ValueError:
446
            error = _("There is a problem with your input.")
447
        except Exception as e:
Bach Dániel committed
448 449
            error = _("Unknown error.")
            logger.error(e)
450 451 452 453 454 455 456 457 458

        if request.is_ajax():
            pass
        else:
            if error:
                messages.error(request, error)
            return redirect(reverse_lazy("dashboard.views.detail",
                                         kwargs={'pk': self.get_object().pk}))

459
    def __abort_operation(self, request):
Kálmán Viktor committed
460 461 462 463
        self.object = self.get_object()

        activity = get_object_or_404(InstanceActivity,
                                     pk=request.POST.get("activity"))
464 465
        if not activity.is_abortable_for(request.user):
            raise PermissionDenied()
Kálmán Viktor committed
466 467 468
        activity.abort()
        return redirect("%s#activity" % self.object.get_absolute_url())

469

470
class VmTraitsUpdate(SuperuserRequiredMixin, UpdateView):
471 472 473 474 475 476 477
    form_class = TraitsForm
    model = Instance

    def get_success_url(self):
        return self.get_object().get_absolute_url() + "#resources"


478
class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
479 480 481 482 483 484 485
    form_class = RawDataForm
    model = Instance

    def get_success_url(self):
        return self.get_object().get_absolute_url() + "#resources"


486
class OperationView(RedirectToLoginMixin, DetailView):
487

488
    template_name = 'dashboard/operate.html'
489
    show_in_toolbar = True
490
    effect = None
491
    wait_for_result = None
492

493 494 495
    @property
    def name(self):
        return self.get_op().name
496

497 498 499
    @property
    def description(self):
        return self.get_op().description
500

501 502 503
    def is_preferred(self):
        return self.get_op().is_preferred()

504 505 506
    @classmethod
    def get_urlname(cls):
        return 'dashboard.vm.op.%s' % cls.op
507

508 509 510 511 512 513 514 515 516 517
    @classmethod
    def get_instance_url(cls, pk, key=None, *args, **kwargs):
        url = reverse(cls.get_urlname(), args=(pk, ) + args, kwargs=kwargs)
        if key is None:
            return url
        else:
            return "%s?k=%s" % (url, key)

    def get_url(self, **kwargs):
        return self.get_instance_url(self.get_object().pk, **kwargs)
518

519
    def get_template_names(self):
520
        if self.request.is_ajax():
521
            return ['dashboard/_modal.html']
522
        else:
523
            return ['dashboard/_base.html']
524

525 526 527
    @classmethod
    def get_op_by_object(cls, obj):
        return getattr(obj, cls.op)
528

529 530 531 532
    def get_op(self):
        if not hasattr(self, '_opobj'):
            setattr(self, '_opobj', getattr(self.get_object(), self.op))
        return self._opobj
533

534
    def get_context_data(self, **kwargs):
535
        ctx = super(OperationView, self).get_context_data(**kwargs)
536
        ctx['op'] = self.get_op()
537
        ctx['opview'] = self
538 539 540 541
        url = self.request.path
        if self.request.GET:
            url += '?' + self.request.GET.urlencode()
        ctx['url'] = url
542
        ctx['template'] = super(OperationView, self).get_template_names()[0]
543
        return ctx
544

545 546 547 548
    def check_auth(self):
        logger.debug("OperationView.check_auth(%s)", unicode(self))
        self.get_op().check_auth(self.request.user)

549
    def get(self, request, *args, **kwargs):
550
        self.check_auth()
551
        return super(OperationView, self).get(request, *args, **kwargs)
552

553
    def get_response_data(self, result, done, extra=None, **kwargs):
554 555 556 557 558 559
        """Return serializable data to return to agents requesting json
        response to POST"""

        if extra is None:
            extra = {}
        extra["success"] = not isinstance(result, Exception)
560 561 562
        extra["done"] = done
        if isinstance(result, HumanReadableObject):
            extra["message"] = result.get_user_text()
563 564
        return extra

565
    def post(self, request, extra=None, *args, **kwargs):
566
        self.check_auth()
567
        self.object = self.get_object()
568 569
        if extra is None:
            extra = {}
570
        result = None
571
        done = False
572
        try:
573
            task = self.get_op().async(user=request.user, **extra)
574 575
        except Exception as e:
            messages.error(request, _('Could not start operation.'))
576
            logger.exception(e)
577
            result = e
578
        else:
579 580 581 582 583 584 585 586 587 588 589 590 591
            wait = self.wait_for_result
            if wait:
                try:
                    result = task.get(timeout=wait,
                                      interval=min((wait / 5, .5)))
                except TimeoutError:
                    logger.debug("Result didn't arrive in %ss",
                                 self.wait_for_result, exc_info=True)
                except Exception as e:
                    messages.error(request, _('Operation failed.'))
                    logger.debug("Operation failed.", exc_info=True)
                    result = e
                else:
592
                    done = True
593
                    messages.success(request, _('Operation succeeded.'))
594
            if result is None and not done:
595
                messages.success(request, _('Operation is started.'))
596 597

        if "/json" in request.META.get("HTTP_ACCEPT", ""):
598 599
            data = self.get_response_data(result, done,
                                          post_extra=extra, **kwargs)
600 601 602 603
            return HttpResponse(json.dumps(data),
                                content_type="application/json")
        else:
            return redirect("%s#activity" % self.object.get_absolute_url())
604

605
    @classmethod
606 607
    def factory(cls, op, icon='cog', effect='info', extra_bases=(), **kwargs):
        kwargs.update({'op': op, 'icon': icon, 'effect': effect})
608
        return type(str(cls.__name__ + op),
609
                    tuple(list(extra_bases) + [cls]), kwargs)
610

611
    @classmethod
612
    def bind_to_object(cls, instance, **kwargs):
613 614
        me = cls()
        me.get_object = lambda: instance
615
        for key, value in kwargs.iteritems():
616 617
            setattr(me, key, value)
        return me
618 619


620
class AjaxOperationMixin(object):
621

622
    def post(self, request, extra=None, *args, **kwargs):
623 624
        resp = super(AjaxOperationMixin, self).post(
            request, extra, *args, **kwargs)
625 626 627 628
        if request.is_ajax():
            store = messages.get_messages(request)
            store.used = True
            return HttpResponse(
629
                json.dumps({'success': True,
630 631 632 633 634 635
                            'messages': [unicode(m) for m in store]}),
                content_type="application=json"
            )
        else:
            return resp

636

637 638 639 640 641 642
class VmOperationView(AjaxOperationMixin, OperationView):

    model = Instance
    context_object_name = 'instance'  # much simpler to mock object


643 644 645 646
class FormOperationMixin(object):

    form_class = None

647 648 649
    def get_form_kwargs(self):
        return {}

650 651 652
    def get_context_data(self, **kwargs):
        ctx = super(FormOperationMixin, self).get_context_data(**kwargs)
        if self.request.method == 'POST':
653 654
            ctx['form'] = self.form_class(self.request.POST,
                                          **self.get_form_kwargs())
655
        else:
656
            ctx['form'] = self.form_class(**self.get_form_kwargs())
657 658 659 660 661
        return ctx

    def post(self, request, extra=None, *args, **kwargs):
        if extra is None:
            extra = {}
662
        form = self.form_class(self.request.POST, **self.get_form_kwargs())
663 664
        if form.is_valid():
            extra.update(form.cleaned_data)
665
            resp = super(FormOperationMixin, self).post(
666
                request, extra, *args, **kwargs)
667 668
            if request.is_ajax():
                return HttpResponse(
669 670 671
                    json.dumps({
                        'success': True,
                        'with_reload': getattr(self, 'with_reload', False)}),
672 673 674 675
                    content_type="application=json"
                )
            else:
                return resp
676 677 678 679
        else:
            return self.get(request)


680 681 682 683 684 685 686 687
class RequestFormOperationMixin(FormOperationMixin):

    def get_form_kwargs(self):
        val = super(FormOperationMixin, self).get_form_kwargs()
        val.update({'request': self.request})
        return val


688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706
class VmAddInterfaceView(FormOperationMixin, VmOperationView):

    op = 'add_interface'
    form_class = VmAddInterfaceForm
    show_in_toolbar = False
    icon = 'globe'
    effect = 'success'
    with_reload = True

    def get_form_kwargs(self):
        inst = self.get_op().instance
        choices = Vlan.get_objects_with_level(
            "user", self.request.user).exclude(
            vm_interface__instance__in=[inst])
        val = super(VmAddInterfaceView, self).get_form_kwargs()
        val.update({'choices': choices})
        return val


707 708 709 710 711
class VmCreateDiskView(FormOperationMixin, VmOperationView):

    op = 'create_disk'
    form_class = VmCreateDiskForm
    show_in_toolbar = False
712
    icon = 'hdd-o'
713
    is_disk_operation = True
714 715 716 717 718 719 720 721


class VmDownloadDiskView(FormOperationMixin, VmOperationView):

    op = 'download_disk'
    form_class = VmDownloadDiskForm
    show_in_toolbar = False
    icon = 'download'
722
    is_disk_operation = True
723 724


725 726 727 728
class VmMigrateView(VmOperationView):

    op = 'migrate'
    icon = 'truck'
729
    effect = 'info'
730 731 732
    template_name = 'dashboard/_vm-migrate.html'

    def get_context_data(self, **kwargs):
733
        ctx = super(VmMigrateView, self).get_context_data(**kwargs)
734 735 736 737 738
        ctx['nodes'] = [n for n in Node.objects.filter(enabled=True)
                        if n.state == "ONLINE"]
        return ctx

    def post(self, request, extra=None, *args, **kwargs):
739 740
        if extra is None:
            extra = {}
741 742 743 744 745 746 747
        node = self.request.POST.get("node")
        if node:
            node = get_object_or_404(Node, pk=node)
            extra["to_node"] = node
        return super(VmMigrateView, self).post(request, extra, *args, **kwargs)


748
class VmSaveView(FormOperationMixin, VmOperationView):
749 750 751

    op = 'save_as_template'
    icon = 'save'
752
    effect = 'info'
753
    form_class = VmSaveForm
754

755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777

class VmResourcesChangeView(VmOperationView):
    op = 'resources_change'
    icon = "save"
    show_in_toolbar = False

    def post(self, request, extra=None, *args, **kwargs):
        if extra is None:
            extra = {}

        resources = {
            'num_cores': "cpu-count",
            'priority': "cpu-priority",
            'ram_size': "ram-size",
            "max_ram_size": "ram-size",  # TODO
        }
        for k, v in resources.iteritems():
            extra[k] = request.POST.get(v)

        return super(VmResourcesChangeView, self).post(request, extra,
                                                       *args, **kwargs)


778 779 780 781 782 783
class TokenOperationView(OperationView):
    """Abstract operation view with token support.

    User can do the action with a valid token instead of logging in.
    """
    token_max_age = 3 * 24 * 3600
784
    redirect_exception_classes = (PermissionDenied, SuspiciousOperation, )
785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853

    @classmethod
    def get_salt(cls):
        return unicode(cls)

    @classmethod
    def get_token(cls, instance, user):
        t = tuple([getattr(i, 'pk', i) for i in [instance, user]])
        return signing.dumps(t, salt=cls.get_salt(), compress=True)

    @classmethod
    def get_token_url(cls, instance, user):
        key = cls.get_token(instance, user)
        return cls.get_instance_url(instance.pk, key)

    def check_auth(self):
        if 'k' in self.request.GET:
            try:  # check if token is needed at all
                return super(TokenOperationView, self).check_auth()
            except Exception:
                op = self.get_op()
                pk = op.instance.pk
                key = self.request.GET.get('k')

                logger.debug("checking token supplied to %s",
                             self.request.get_full_path())
                try:
                    user = self.validate_key(pk, key)
                except signing.SignatureExpired:
                    messages.error(self.request, _('The token has expired.'))
                else:
                    logger.info("Request user changed to %s at %s",
                                user, self.request.get_full_path())
                    self.request.user = user
        else:
            logger.debug("no token supplied to %s",
                         self.request.get_full_path())

        return super(TokenOperationView, self).check_auth()

    def validate_key(self, pk, key):
        """Get object based on signed token.
        """
        try:
            data = signing.loads(key, salt=self.get_salt())
            logger.debug('Token data: %s', unicode(data))
            instance, user = data
            logger.debug('Extracted token data: instance: %s, user: %s',
                         unicode(instance), unicode(user))
        except (signing.BadSignature, ValueError, TypeError) as e:
            logger.warning('Tried invalid token. Token: %s, user: %s. %s',
                           key, unicode(self.request.user), unicode(e))
            raise SuspiciousOperation()

        try:
            instance, user = signing.loads(key, max_age=self.token_max_age,
                                           salt=self.get_salt())
            logger.debug('Extracted non-expired token data: %s, %s',
                         unicode(instance), unicode(user))
        except signing.BadSignature as e:
            raise signing.SignatureExpired()

        if pk != instance:
            logger.debug('pk (%d) != instance (%d)', pk, instance)
            raise SuspiciousOperation()
        user = User.objects.get(pk=user)
        return user


854 855
class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):

856 857 858 859
    op = 'renew'
    icon = 'calendar'
    effect = 'info'
    show_in_toolbar = False
860
    form_class = VmRenewForm
861
    wait_for_result = 0.5
862 863 864 865 866

    def get_form_kwargs(self):
        choices = Lease.get_objects_with_level("user", self.request.user)
        default = self.get_op().instance.lease
        if default and default not in choices:
867 868
            choices = (choices.distinct() |
                       Lease.objects.filter(pk=default.pk).distinct())
869 870 871 872

        val = super(VmRenewView, self).get_form_kwargs()
        val.update({'choices': choices, 'default': default})
        return val
873

874 875
    def get_response_data(self, result, done, extra=None, **kwargs):
        extra = super(VmRenewView, self).get_response_data(result, done,
876 877 878 879 880
                                                           extra, **kwargs)
        extra["new_suspend_time"] = unicode(self.get_op().
                                            instance.time_of_suspend)
        return extra

881

882 883
vm_ops = OrderedDict([
    ('deploy', VmOperationView.factory(
884
        op='deploy', icon='play', effect='success')),
885
    ('wake_up', VmOperationView.factory(
886
        op='wake_up', icon='sun-o', effect='success')),
887
    ('sleep', VmOperationView.factory(
888
        extra_bases=[TokenOperationView],
889
        op='sleep', icon='moon-o', effect='info')),
890 891 892
    ('migrate', VmMigrateView),
    ('save_as_template', VmSaveView),
    ('reboot', VmOperationView.factory(
893
        op='reboot', icon='refresh', effect='warning')),
894
    ('reset', VmOperationView.factory(
895
        op='reset', icon='bolt', effect='warning')),
896
    ('shutdown', VmOperationView.factory(
897
        op='shutdown', icon='power-off', effect='warning')),
898
    ('shut_off', VmOperationView.factory(
899
        op='shut_off', icon='ban', effect='warning')),
900 901
    ('recover', VmOperationView.factory(
        op='recover', icon='medkit', effect='warning')),
902
    ('nostate', VmOperationView.factory(
903
        op='emergency_change_state', icon='legal', effect='danger')),
904
    ('destroy', VmOperationView.factory(
905
        extra_bases=[TokenOperationView],
906
        op='destroy', icon='times', effect='danger')),
907 908
    ('create_disk', VmCreateDiskView),
    ('download_disk', VmDownloadDiskView),
909
    ('add_interface', VmAddInterfaceView),
910
    ('renew', VmRenewView),
911
    ('resources_change', VmResourcesChangeView),
912
])
913 914 915 916 917 918 919 920 921


def get_operations(instance, user):
    ops = []
    for k, v in vm_ops.iteritems():
        try:
            op = v.get_op_by_object(instance)
            op.check_auth(user)
            op.check_precond()
922
        except PermissionDenied as e:
923 924
            logger.debug('Not showing operation %s for %s: %s',
                         k, instance, unicode(e))
925 926
        except Exception:
            ops.append(v.bind_to_object(instance, disabled=True))
927 928 929
        else:
            ops.append(v.bind_to_object(instance))
    return ops
930

Kálmán Viktor committed
931

932
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
933 934
    template_name = "dashboard/node-detail.html"
    model = Node
935 936
    form = None
    form_class = TraitForm
937

938 939 940
    def get_context_data(self, form=None, **kwargs):
        if form is None:
            form = self.form_class()
941
        context = super(NodeDetailView, self).get_context_data(**kwargs)
942 943
        instances = Instance.active.filter(node=self.object)
        context['table'] = NodeVmListTable(instances)
944 945 946 947
        na = NodeActivity.objects.filter(
            node=self.object, parent=None
        ).order_by('-started').select_related()
        context['activities'] = na
948
        context['trait_form'] = form
949
        context['graphite_enabled'] = (
Bach Dániel committed
950
            settings.GRAPHITE_URL is not None)
951 952
        return context

953 954 955
    def post(self, request, *args, **kwargs):
        if request.POST.get('new_name'):
            return self.__set_name(request)
956 957
        if request.POST.get('to_remove'):
            return self.__remove_trait(request)
958 959
        return redirect(reverse_lazy("dashboard.views.node-detail",
                                     kwargs={'pk': self.get_object().pk}))
960 961 962 963 964 965 966

    def __set_name(self, request):
        self.object = self.get_object()
        new_name = request.POST.get("new_name")
        Node.objects.filter(pk=self.object.pk).update(
            **{'name': new_name})

967
        success_message = _("Node successfully renamed.")
968 969 970 971 972
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
                'node_pk': self.object.pk
973 974 975 976 977 978 979 980 981 982
            }
            return HttpResponse(
                json.dumps(response),
                content_type="application/json"
            )
        else:
            messages.success(request, success_message)
            return redirect(reverse_lazy("dashboard.views.node-detail",
                                         kwargs={'pk': self.object.pk}))

983 984 985 986
    def __remove_trait(self, request):
        try:
            to_remove = request.POST.get('to_remove')
            self.object = self.get_object()
987
            self.object.traits.remove(to_remove)
988 989 990 991 992 993 994
            message = u"Success"
        except:  # note this won't really happen
            message = u"Not success"

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': message}),
995
                content_type="application/json"
996
            )
997
        else:
998
            return redirect(self.object.get_absolute_url())
999

1000

1001
class GroupDetailView(CheckedDetailView):
1002 1003
    template_name = "dashboard/group-detail.html"
    model = Group
1004
    read_level = 'operator'
1005 1006 1007

    def get_has_level(self):
        return self.object.profile.has_level
1008 1009 1010

    def get_context_data(self, **kwargs):
        context = super(GroupDetailView, self).get_context_data(**kwargs)
1011 1012
        context['group'] = self.object
        context['users'] = self.object.user_set.all()
1013 1014
        context['future_users'] = FutureMember.objects.filter(
            group=self.object)
1015 1016 1017
        context['acl'] = AclUpdateView.get_acl_data(
            self.object.profile, self.request.user,
            'dashboard.views.group-acl')
1018
        context['aclform'] = AclUserAddForm()
1019 1020
        context['group_profile_form'] = GroupProfileUpdate.get_form_object(
            self.request, self.object.profile)
1021 1022 1023 1024 1025

        if self.request.user.is_superuser:
            context['group_perm_form'] = GroupPermissionForm(
                instance=self.object)

1026 1027 1028
        return context

    def post(self, request, *args, **kwargs):
1029 1030 1031
        self.object = self.get_object()
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
1032

1033 1034
        if request.POST.get('new_name'):
            return self.__set_name(request)
1035
        if request.POST.get('list-new-name'):
1036
            return self.__add_user(request)
1037
        if request.POST.get('list-new-namelist'):
1038
            return self.__add_list(request)
1039 1040 1041 1042
        if (request.POST.get('list-new-name') is not None) and \
                (request.POST.get('list-new-namelist') is not None):
            return redirect(reverse_lazy("dashboard.views.group-detail",
                                         kwargs={'pk': self.get_object().pk}))
1043 1044

    def __add_user(self, request):
1045
        name = request.POST['list-new-name']
1046 1047 1048
        self.__add_username(request, name)
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
1049 1050

    def __add_username(self, request, name):
1051
        if not name:
1052
            return
1053
        try:
1054
            entity = search_user(name)
1055
            self.object.user_set.add(entity)
1056
        except User.DoesNotExist:
1057 1058 1059 1060
            if saml_available:
                FutureMember.objects.get_or_create(org_id=name,
                                                   group=self.object)
            else:
1061
                messages.warning(request, _('User "%s" not found.') % name)
1062

1063
    def __add_list(self, request):
1064 1065
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
1066 1067 1068
        userlist = request.POST.get('list-new-namelist').split('\r\n')
        for line in userlist:
            self.__add_username(request, line)
1069 1070
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
1071 1072 1073 1074 1075 1076

    def __set_name(self, request):
        new_name = request.POST.get("new_name")
        Group.objects.filter(pk=self.object.pk).update(
            **{'name': new_name})

1077
        success_message = _("Group successfully renamed.")
1078 1079 1080 1081
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
1082
                'group_pk': self.object.pk
1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093
            }
            return HttpResponse(
                json.dumps(response),
                content_type="application/json"
            )
        else:
            messages.success(request, success_message)
            return redirect(reverse_lazy("dashboard.views.group-detail",
                                         kwargs={'pk': self.object.pk}))


1094 1095 1096 1097 1098 1099 1100
class GroupPermissionsView(SuperuserRequiredMixin, UpdateView):
    model = Group
    form_class = GroupPermissionForm
    slug_field = "pk"
    slug_url_kwarg = "group_pk"

    def get_success_url(self):
1101
        return "%s#group-detail-permissions" % (
1102 1103 1104
            self.get_object().groupprofile.get_absolute_url())


1105
class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120
    def send_success_message(self, whom, old_level, new_level):
        if old_level and new_level:
            msg = _("Acl user/group %(w)s successfully modified.")
        elif not old_level and new_level:
            msg = _("Acl user/group %(w)s successfully added.")
        elif old_level and not new_level:
            msg = _("Acl user/group %(w)s successfully removed.")
        if msg:
            messages.success(self.request, msg % {'w': whom})

    def get_level(self, whom):
        for u, level in self.acl_data:
            if u == whom:
                return level
        return None
1121

1122 1123 1124
    @classmethod
    def get_acl_data(cls, obj, user, url):
        levels = obj.ACL_LEVELS
1125 1126
        allowed_levels = list(l for l in OrderedDict(levels)
                              if cls.has_next_level(user, obj, l))
1127 1128
        is_owner = 'owner' in allowed_levels

1129 1130
        allowed_users = cls.get_allowed_users(user)
        allowed_groups = cls.get_allowed_groups(user)
1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147

        user_levels = list(
            {'user': u, 'level': l} for u, l in obj.get_users_with_level()
            if is_owner or u == user or u in allowed_users)

        group_levels = list(
            {'group': g, 'level': l} for g, l in obj.get_groups_with_level()
            if is_owner or g in allowed_groups)

        return {'users': user_levels,
                'groups': group_levels,
                'levels': levels,
                'allowed_levels': allowed_levels,
                'url': reverse(url, args=[obj.pk])}

    @classmethod
    def has_next_level(self, user, instance, level):
1148 1149 1150 1151 1152
        levels = OrderedDict(instance.ACL_LEVELS).keys()
        next_levels = dict(zip([None] + levels, levels + levels[-1:]))
        # {None: 'user', 'user': 'operator', 'operator: 'owner',
        #  'owner: 'owner'}
        next_level = next_levels[level]
1153
        return instance.has_level(user, next_level)
1154

1155
    @classmethod
1156 1157
    def get_allowed_groups(cls, user):
        if user.has_perm('dashboard.use_autocomplete'):
1158 1159 1160 1161 1162 1163
            return Group.objects.all()
        else:
            profiles = GroupProfile.get_objects_with_level('owner', user)
            return Group.objects.filter(groupprofile__in=profiles).distinct()

    @classmethod
1164 1165
    def get_allowed_users(cls, user):
        if user.has_perm('dashboard.use_autocomplete'):
1166 1167
            return User.objects.all()
        else:
1168
            groups = cls.get_allowed_groups(user)
1169 1170 1171 1172 1173
            return User.objects.filter(
                Q(groups__in=groups) | Q(pk=user.pk)).distinct()

    def check_auth(self, whom, old_level, new_level):
        if isinstance(whom, Group):
1174 1175
            if (not self.is_owner and whom not in
                    AclUpdateView.get_allowed_groups(self.request.user)):
1176 1177
                return False
        elif isinstance(whom, User):
1178 1179
            if (not self.is_owner and whom not in
                    AclUpdateView.get_allowed_users(self.request.user)):
1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215
                return False
        return (
            AclUpdateView.has_next_level(self.request.user,
                                         self.instance, new_level) and
            AclUpdateView.