views.py 115 KB
Newer Older
1
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
2

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
#
# 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/>.

19
from __future__ import unicode_literals, absolute_import
20

21
from collections import OrderedDict
22
from itertools import chain
23
from os import getenv
Őry Máté committed
24
from os.path import join, normpath, dirname
25
import os
26
import json
Őry Máté committed
27
import logging
28
import re
29
import requests
30

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

58
from django.forms.models import inlineformset_factory
59
from django_tables2 import SingleTableView
60 61
from braces.views import (LoginRequiredMixin, SuperuserRequiredMixin,
                          PermissionRequiredMixin)
62
from braces.views._access import AccessMixin
Kálmán Viktor committed
63

64 65
from django_sshkey.models import UserKey

66
from .forms import (
67
    CircleAuthenticationForm, HostForm, LeaseForm, MyProfileForm,
68
    NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm,
69
    UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
70
    VmSaveForm, UserKeyForm, VmRenewForm,
71
    CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
72
    TraitsForm, RawDataForm, GroupPermissionForm
73
)
74 75 76

from .tables import (
    NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable,
77
    GroupListTable, UserKeyListTable
78
)
Őry Máté committed
79 80 81 82
from vm.models import (
    Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
    InterfaceTemplate, Lease, Node, NodeActivity, Trait,
)
83
from storage.models import Disk
84
from firewall.models import Vlan, Host, Rule
85
from .models import Favourite, Profile, GroupProfile, FutureMember
86

87
from .store_api import Store, NoStoreException
Kálmán Viktor committed
88

Őry Máté committed
89
logger = logging.getLogger(__name__)
90
saml_available = hasattr(settings, "SAML_CONFIG")
91

92

93 94 95 96
def search_user(keyword):
    try:
        return User.objects.get(username=keyword)
    except User.DoesNotExist:
97 98 99 100
        try:
            return User.objects.get(profile__org_id=keyword)
        except User.DoesNotExist:
            return User.objects.get(email=keyword)
101 102


103 104 105 106 107 108 109 110 111 112 113 114 115
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())
116 117
            else:
                raise
118 119


120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
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', [])
143 144
                for group in chain(*[attributes[i]
                                     for i in owneratrs if i in attributes]):
145 146 147 148 149 150 151 152
                    try:
                        GroupProfile.search(group)
                    except Group.DoesNotExist:
                        newgroups.append(group)

        return newgroups


153
class FilterMixin(object):
154

155 156 157 158
    def get_queryset_filters(self):
        filters = {}
        for item in self.allowed_filters:
            if item in self.request.GET:
159 160 161 162 163
                filters[self.allowed_filters[item]] = (
                    self.request.GET[item].split(",")
                    if self.allowed_filters[item].endswith("__in") else
                    self.request.GET[item])

164
        return filters
165

166 167 168
    def get_queryset(self):
        return super(FilterMixin,
                     self).get_queryset().filter(**self.get_queryset_filters())
169 170


171
class IndexView(LoginRequiredMixin, TemplateView):
Kálmán Viktor committed
172
    template_name = "dashboard/index.html"
173

174
    def get_context_data(self, **kwargs):
175
        user = self.request.user
176
        context = super(IndexView, self).get_context_data(**kwargs)
177

178
        # instances
179
        favs = Instance.objects.filter(favourite__user=self.request.user)
180
        instances = Instance.get_objects_with_level(
181
            'user', user, disregard_superuser=True).filter(destroyed_at=None)
182 183 184
        display = list(favs) + list(set(instances) - set(favs))
        for d in display:
            d.fav = True if d in favs else False
185
        context.update({
186
            'instances': display[:5],
187
            'more_instances': instances.count() - len(instances[:5])
188 189
        })

190 191
        running = instances.filter(status='RUNNING')
        stopped = instances.exclude(status__in=('RUNNING', 'NOSTATE'))
192

193
        context.update({
194 195 196
            'running_vms': running[:20],
            'running_vm_num': running.count(),
            'stopped_vm_num': stopped.count()
197
        })
198

199 200 201 202
        # nodes
        if user.is_superuser:
            nodes = Node.objects.all()
            context.update({
203 204
                'nodes': nodes[:5],
                'more_nodes': nodes.count() - len(nodes[:5]),
205 206 207 208 209 210 211 212 213 214
                '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
215
        if user.has_module_perms('auth'):
216 217
            profiles = GroupProfile.get_objects_with_level('operator', user)
            groups = Group.objects.filter(groupprofile__in=profiles)
218 219 220 221
            context.update({
                'groups': groups[:5],
                'more_groups': groups.count() - len(groups[:5]),
            })
222 223 224 225 226 227

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

Kálmán Viktor committed
228
        # toplist
229
        if settings.STORE_URL:
230
            cache_key = "files-%d" % self.request.user.pk
231
            cache = get_cache("default")
232 233
            files = cache.get(cache_key)
            if not files:
234
                try:
235 236 237 238
                    store = Store(self.request.user)
                    toplist = store.toplist()
                    quota = store.get_quota()
                    files = {'toplist': toplist, 'quota': quota}
239 240 241
                except Exception:
                    logger.exception("Unable to get tolist for %s",
                                     unicode(self.request.user))
242 243
                    files = {'toplist': []}
                cache.set(cache_key, files, 300)
Kálmán Viktor committed
244

245
            context['files'] = files
246 247
        else:
            context['no_store'] = True
Kálmán Viktor committed
248

249 250
        return context

251

252
def get_vm_acl_data(obj):
253 254 255 256 257 258 259 260 261
    levels = obj.ACL_LEVELS
    users = obj.get_users_with_level()
    users = [{'user': u, 'level': l} for u, l in users]
    groups = obj.get_groups_with_level()
    groups = [{'group': g, 'level': l} for g, l in groups]
    return {'users': users, 'groups': groups, 'levels': levels,
            'url': reverse('dashboard.views.vm-acl', args=[obj.pk])}


262 263 264 265 266 267 268 269 270 271 272
def get_group_acl_data(obj):
    aclobj = obj.profile
    levels = aclobj.ACL_LEVELS
    users = aclobj.get_users_with_level()
    users = [{'user': u, 'level': l} for u, l in users]
    groups = aclobj.get_groups_with_level()
    groups = [{'group': g, 'level': l} for g, l in groups]
    return {'users': users, 'groups': groups, 'levels': levels,
            'url': reverse('dashboard.views.group-acl', args=[obj.pk])}


273
class CheckedDetailView(LoginRequiredMixin, DetailView):
274 275
    read_level = 'user'

276 277 278
    def get_has_level(self):
        return self.object.has_level

279 280
    def get_context_data(self, **kwargs):
        context = super(CheckedDetailView, self).get_context_data(**kwargs)
281
        if not self.get_has_level()(self.request.user, self.read_level):
282 283 284 285
            raise PermissionDenied()
        return context


286 287 288 289 290 291 292 293
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()
294 295
        if not request.user.has_perm('vm.access_console'):
            raise PermissionDenied()
296
        if self.object.node:
297 298 299 300
            with instance_activity(
                    code_suffix='console-accessed', instance=self.object,
                    user=request.user, readable_name=ugettext_noop(
                        "console access"), concurrency_check=False):
301 302 303 304 305
                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)
306 307 308 309
        else:
            raise Http404()


310
class VmDetailView(CheckedDetailView):
Kálmán Viktor committed
311
    template_name = "dashboard/vm-detail.html"
312
    model = Instance
313 314

    def get_context_data(self, **kwargs):
315
        context = super(VmDetailView, self).get_context_data(**kwargs)
316
        instance = context['instance']
317
        ops = get_operations(instance, self.request.user)
318
        context.update({
Bach Dániel committed
319
            'graphite_enabled': settings.GRAPHITE_URL is not None,
320
            'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
321
                                    kwargs={'pk': self.object.pk}),
322 323
            'ops': ops,
            'op': {i.op: i for i in ops},
324
        })
325 326

        # activity data
327 328
        context['activities'] = self.object.get_merged_activities(
            self.request.user)
329

330
        context['vlans'] = Vlan.get_objects_with_level(
331
            'user', self.request.user
332
        ).exclude(  # exclude already added interfaces
333 334 335
            pk__in=Interface.objects.filter(
                instance=self.get_object()).values_list("vlan", flat=True)
        ).all()
336
        context['acl'] = get_vm_acl_data(instance)
337 338
        context['os_type_icon'] = instance.os_type.replace("unknown",
                                                           "question")
339 340 341
        # ipv6 infos
        context['ipv6_host'] = instance.get_connect_host(use_ipv6=True)
        context['ipv6_port'] = instance.get_connect_port(use_ipv6=True)
342 343 344 345 346

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

348 349 350
        # resources change perm
        context['can_change_resources'] = self.request.user.has_perm(
            "vm.change_resources")
351

352
        return context
Kálmán Viktor committed
353

354
    def post(self, request, *args, **kwargs):
355 356 357
        options = {
            'change_password': self.__change_password,
            'new_name': self.__set_name,
358
            'new_description': self.__set_description,
359 360
            'new_tag': self.__add_tag,
            'to_remove': self.__remove_tag,
361 362
            'port': self.__add_port,
            'new_network_vlan': self.__new_network,
363
            'abort_operation': self.__abort_operation,
364 365 366 367
        }
        for k, v in options.iteritems():
            if request.POST.get(k) is not None:
                return v(request)
Kálmán Viktor committed
368

369 370
        raise Http404()

371 372 373 374
    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
375

376
        self.object.change_password(user=request.user)
377
        messages.success(request, _("Password changed."))
378
        if request.is_ajax():
379
            return HttpResponse("Success.")
380 381 382
        else:
            return redirect(reverse_lazy("dashboard.views.detail",
                                         kwargs={'pk': self.object.pk}))
383

384 385
    def __set_name(self, request):
        self.object = self.get_object()
386 387
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
388 389 390 391
        new_name = request.POST.get("new_name")
        Instance.objects.filter(pk=self.object.pk).update(
            **{'name': new_name})

392
        success_message = _("VM successfully renamed.")
393 394 395 396 397 398 399 400 401 402 403 404
        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)
405 406 407 408 409 410 411 412 413 414 415
            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})

416
        success_message = _("VM description successfully updated.")
417 418 419 420 421 422 423 424 425 426 427 428
        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())
429

Kálmán Viktor committed
430 431 432
    def __add_tag(self, request):
        new_tag = request.POST.get('new_tag')
        self.object = self.get_object()
433 434
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
Kálmán Viktor committed
435 436

        if len(new_tag) < 1:
437
            message = u"Please input something."
Kálmán Viktor committed
438
        elif len(new_tag) > 20:
439
            message = u"Tag name is too long."
Kálmán Viktor committed
440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
        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()
455 456
            if not self.object.has_level(request.user, 'owner'):
                raise PermissionDenied()
Kálmán Viktor committed
457 458 459 460 461 462 463 464 465 466 467

            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"
            )
468 469 470
        else:
            return redirect(reverse_lazy("dashboard.views.detail",
                            kwargs={'pk': self.object.pk}))
Kálmán Viktor committed
471

472 473
    def __add_port(self, request):
        object = self.get_object()
474 475
        if (not object.has_level(request.user, 'owner') or
                not request.user.has_perm('vm.config_ports')):
476
            raise PermissionDenied()
477 478 479 480 481 482

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

        try:
            error = None
483 484 485
            interfaces = object.interface_set.all()
            host = Host.objects.get(pk=request.POST.get("host_pk"),
                                    interface__in=interfaces)
486
            host.add_port(proto, private=port)
487 488 489 490 491
        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()
492
        except ValueError:
493
            error = _("There is a problem with your input.")
494
        except Exception as e:
Bach Dániel committed
495 496
            error = _("Unknown error.")
            logger.error(e)
497 498 499 500 501 502 503 504 505

        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}))

506 507 508 509 510
    def __new_network(self, request):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()

511
        vlan = get_object_or_404(Vlan, pk=request.POST.get("new_network_vlan"))
512 513
        if not vlan.has_level(request.user, 'user'):
            raise PermissionDenied()
514
        try:
515
            self.object.add_interface(vlan=vlan, user=request.user)
516
            messages.success(request, _("Successfully added new interface."))
517 518 519 520 521 522 523
        except Exception, e:
            error = u' '.join(e.messages)
            messages.error(request, error)

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

524
    def __abort_operation(self, request):
Kálmán Viktor committed
525 526 527 528
        self.object = self.get_object()

        activity = get_object_or_404(InstanceActivity,
                                     pk=request.POST.get("activity"))
529 530
        if not activity.is_abortable_for(request.user):
            raise PermissionDenied()
Kálmán Viktor committed
531 532 533
        activity.abort()
        return redirect("%s#activity" % self.object.get_absolute_url())

534

535
class VmTraitsUpdate(SuperuserRequiredMixin, UpdateView):
536 537 538 539 540 541 542
    form_class = TraitsForm
    model = Instance

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


543
class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
544 545 546 547 548 549 550
    form_class = RawDataForm
    model = Instance

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


551
class OperationView(RedirectToLoginMixin, DetailView):
552

553
    template_name = 'dashboard/operate.html'
554
    show_in_toolbar = True
555
    effect = None
556

557 558 559
    @property
    def name(self):
        return self.get_op().name
560

561 562 563
    @property
    def description(self):
        return self.get_op().description
564

565 566 567
    def is_preferred(self):
        return self.get_op().is_preferred()

568 569 570
    @classmethod
    def get_urlname(cls):
        return 'dashboard.vm.op.%s' % cls.op
571

572 573 574 575 576 577 578 579 580 581
    @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)
582

583
    def get_template_names(self):
584
        if self.request.is_ajax():
585
            return ['dashboard/_modal.html']
586
        else:
587
            return ['dashboard/_base.html']
588

589 590 591
    @classmethod
    def get_op_by_object(cls, obj):
        return getattr(obj, cls.op)
592

593 594 595 596
    def get_op(self):
        if not hasattr(self, '_opobj'):
            setattr(self, '_opobj', getattr(self.get_object(), self.op))
        return self._opobj
597

598
    def get_context_data(self, **kwargs):
599
        ctx = super(OperationView, self).get_context_data(**kwargs)
600
        ctx['op'] = self.get_op()
601
        ctx['opview'] = self
602 603 604 605
        url = self.request.path
        if self.request.GET:
            url += '?' + self.request.GET.urlencode()
        ctx['url'] = url
606
        ctx['template'] = super(OperationView, self).get_template_names()[0]
607
        return ctx
608

609 610 611 612
    def check_auth(self):
        logger.debug("OperationView.check_auth(%s)", unicode(self))
        self.get_op().check_auth(self.request.user)

613
    def get(self, request, *args, **kwargs):
614
        self.check_auth()
615
        return super(OperationView, self).get(request, *args, **kwargs)
616

617
    def post(self, request, extra=None, *args, **kwargs):
618
        self.check_auth()
619
        self.object = self.get_object()
620 621
        if extra is None:
            extra = {}
622
        try:
623
            self.get_op().async(user=request.user, **extra)
624 625
        except Exception as e:
            messages.error(request, _('Could not start operation.'))
626
            logger.exception(e)
627 628
        else:
            messages.success(request, _('Operation is started.'))
629
        return redirect("%s#activity" % self.object.get_absolute_url())
630

631
    @classmethod
632 633
    def factory(cls, op, icon='cog', effect='info', extra_bases=(), **kwargs):
        kwargs.update({'op': op, 'icon': icon, 'effect': effect})
634
        return type(str(cls.__name__ + op),
635
                    tuple(list(extra_bases) + [cls]), kwargs)
636

637
    @classmethod
638
    def bind_to_object(cls, instance, **kwargs):
639 640
        me = cls()
        me.get_object = lambda: instance
641
        for key, value in kwargs.iteritems():
642 643
            setattr(me, key, value)
        return me
644 645


646
class AjaxOperationMixin(object):
647

648
    def post(self, request, extra=None, *args, **kwargs):
649 650
        resp = super(AjaxOperationMixin, self).post(
            request, extra, *args, **kwargs)
651 652 653 654
        if request.is_ajax():
            store = messages.get_messages(request)
            store.used = True
            return HttpResponse(
655
                json.dumps({'success': True,
656 657 658 659 660 661
                            'messages': [unicode(m) for m in store]}),
                content_type="application=json"
            )
        else:
            return resp

662

663 664 665 666 667 668
class VmOperationView(AjaxOperationMixin, OperationView):

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


669 670 671 672
class FormOperationMixin(object):

    form_class = None

673 674 675
    def get_form_kwargs(self):
        return {}

676 677 678
    def get_context_data(self, **kwargs):
        ctx = super(FormOperationMixin, self).get_context_data(**kwargs)
        if self.request.method == 'POST':
679 680
            ctx['form'] = self.form_class(self.request.POST,
                                          **self.get_form_kwargs())
681
        else:
682
            ctx['form'] = self.form_class(**self.get_form_kwargs())
683 684 685 686 687
        return ctx

    def post(self, request, extra=None, *args, **kwargs):
        if extra is None:
            extra = {}
688
        form = self.form_class(self.request.POST, **self.get_form_kwargs())
689 690
        if form.is_valid():
            extra.update(form.cleaned_data)
691
            resp = super(FormOperationMixin, self).post(
692
                request, extra, *args, **kwargs)
693 694
            if request.is_ajax():
                return HttpResponse(
695
                    json.dumps({'success': True}),
696 697 698 699
                    content_type="application=json"
                )
            else:
                return resp
700 701 702 703
        else:
            return self.get(request)


704 705 706 707 708 709 710 711
class RequestFormOperationMixin(FormOperationMixin):

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


712 713 714 715 716
class VmCreateDiskView(FormOperationMixin, VmOperationView):

    op = 'create_disk'
    form_class = VmCreateDiskForm
    show_in_toolbar = False
717
    icon = 'hdd-o'
718
    is_disk_operation = True
719 720 721 722 723 724 725 726


class VmDownloadDiskView(FormOperationMixin, VmOperationView):

    op = 'download_disk'
    form_class = VmDownloadDiskForm
    show_in_toolbar = False
    icon = 'download'
727
    is_disk_operation = True
728 729


730 731 732 733
class VmMigrateView(VmOperationView):

    op = 'migrate'
    icon = 'truck'
734
    effect = 'info'
735 736 737
    template_name = 'dashboard/_vm-migrate.html'

    def get_context_data(self, **kwargs):
738
        ctx = super(VmMigrateView, self).get_context_data(**kwargs)
739 740 741 742 743
        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):
744 745
        if extra is None:
            extra = {}
746 747 748 749 750 751 752
        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)


753
class VmSaveView(FormOperationMixin, VmOperationView):
754 755 756

    op = 'save_as_template'
    icon = 'save'
757
    effect = 'info'
758
    form_class = VmSaveForm
759

760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782

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)


783 784 785 786 787 788
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
789
    redirect_exception_classes = (PermissionDenied, SuspiciousOperation, )
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 854 855 856 857 858

    @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


859 860
class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):

861 862 863 864
    op = 'renew'
    icon = 'calendar'
    effect = 'info'
    show_in_toolbar = False
865 866 867 868 869 870
    form_class = VmRenewForm

    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:
871 872
            choices = (choices.distinct() |
                       Lease.objects.filter(pk=default.pk).distinct())
873 874 875 876

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


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


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()
916
        except PermissionDenied as e:
917 918
            logger.debug('Not showing operation %s for %s: %s',
                         k, instance, unicode(e))
919 920
        except Exception:
            ops.append(v.bind_to_object(instance, disabled=True))
921 922 923
        else:
            ops.append(v.bind_to_object(instance))
    return ops
924

Kálmán Viktor committed
925

926
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
927 928
    template_name = "dashboard/node-detail.html"
    model = Node
929 930
    form = None
    form_class = TraitForm
931

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

947 948 949
    def post(self, request, *args, **kwargs):
        if request.POST.get('new_name'):
            return self.__set_name(request)
950 951
        if request.POST.get('to_remove'):
            return self.__remove_trait(request)
952 953
        return redirect(reverse_lazy("dashboard.views.node-detail",
                                     kwargs={'pk': self.get_object().pk}))
954 955 956 957 958 959 960

    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})

961
        success_message = _("Node successfully renamed.")
962 963 964 965 966
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
                'node_pk': self.object.pk
967 968 969 970 971 972 973 974 975 976
            }
            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}))

977 978 979 980
    def __remove_trait(self, request):
        try:
            to_remove = request.POST.get('to_remove')
            self.object = self.get_object()
981
            self.object.traits.remove(to_remove)
982 983 984 985 986 987 988
            message = u"Success"
        except:  # note this won't really happen
            message = u"Not success"

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': message}),
989
                content_type="application/json"
990
            )
991
        else:
992
            return redirect(self.object.get_absolute_url())
993

994

995
class GroupDetailView(CheckedDetailView):
996 997
    template_name = "dashboard/group-detail.html"
    model = Group
998
    read_level = 'operator'
999 1000 1001

    def get_has_level(self):
        return self.object.profile.has_level
1002 1003 1004

    def get_context_data(self, **kwargs):
        context = super(GroupDetailView, self).get_context_data(**kwargs)
1005 1006
        context['group'] = self.object
        context['users'] = self.object.user_set.all()
1007 1008
        context['future_users'] = FutureMember.objects.filter(
            group=self.object)
1009
        context['acl'] = get_group_acl_data(self.object)
1010 1011
        context['group_profile_form'] = GroupProfileUpdate.get_form_object(
            self.request, self.object.profile)
1012 1013 1014 1015 1016

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

1017 1018 1019
        return context

    def post(self, request, *args, **kwargs):
1020 1021 1022
        self.object = self.get_object()
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
1023

1024 1025
        if request.POST.get('new_name'):
            return self.__set_name(request)
1026
        if request.POST.get('list-new-name'):
1027
            return self.__add_user(request)
1028
        if request.POST.get('list-new-namelist'):
1029
            return self.__add_list(request)
1030 1031 1032 1033
        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}))
1034 1035

    def __add_user(self, request):
1036
        name = request.POST['list-new-name']
1037 1038 1039
        self.__add_username(request, name)
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
1040 1041

    def __add_username(self, request, name):
1042
        if not name:
1043
            return
1044
        try:
1045
            entity = search_user(name)
1046
            self.object.user_set.add(entity)
1047
        except User.DoesNotExist:
1048 1049 1050 1051 1052
            if saml_available:
                FutureMember.objects.get_or_create(org_id=name,
                                                   group=self.object)
            else:
                warning(request, _('User "%s" not found.') % name)
1053

1054
    def __add_list(self, request):
1055 1056
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
1057 1058 1059
        userlist = request.POST.get('list-new-namelist').split('\r\n')
        for line in userlist:
            self.__add_username(request, line)
1060 1061
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
1062 1063 1064 1065 1066 1067

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

1068
        success_message = _("Group successfully renamed.")
1069 1070 1071 1072
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
1073
                'group_pk': self.object.pk
1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084
            }
            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}))


1085 1086 1087 1088 1089 1090 1091
class GroupPermissionsView(SuperuserRequiredMixin, UpdateView):
    model = Group
    form_class = GroupPermissionForm
    slug_field = "pk"
    slug_url_kwarg = "group_pk"

    def get_success_url(self):
1092
        return "%s#group-detail-permissions" % (
1093 1094 1095
            self.get_object().groupprofile.get_absolute_url())


1096
class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
1097

1098
    def post(self, request, *args, **kwargs):
1099
        instance = self.get_object()
1100 1101
        if not (instance.has_level(request.user, "owner") or
                getattr(instance, 'owner', None) == request.user):
Őry Máté committed
1102 1103
            logger.warning('Tried to set permissions of %s by non-owner %s.',
                           unicode(instance), unicode(request.user))
1104
            raise PermissionDenied()
1105
        self.set_levels(request, instance)
1106
        self.remove_levels(request, instance)
1107
        self.add_levels(request, instance)
1108
        return redirect("%s#access" % instance.get_absolute_url())
1109 1110

    def set_levels(self, request, instance):
1111 1112 1113
        for key, value in request.POST.items():
            m = re.match('perm-([ug])-(\d+)', key)
            if m:
1114 1115
                typ, id = m.groups()
                entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
1116
                if getattr(instance, "owner", None) == entity:
1117 1118 1119
                    logger.info("Tried to set owner's acl level for %s by %s.",
                                unicode(instance), unicode(request.user))
                    continue
1120
                instance.set_level(entity, value)
Őry Máté committed
1121 1122 1123
                logger.info("Set %s's acl level for %s to %s by %s.",
                            unicode(entity), unicode(instance),
                            value, unicode(request.user))
1124

1125 1126 1127 1128 1129 1130 1131 1132 1133 1134
    def remove_levels(self, request, instance):
        for key, value in request.POST.items():
            if key.startswith("remove"):
                typ = key[7:8]  # len("remove-")
                id = key[9:]  # len("remove-x-")
                entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
                if getattr(instance, "owner", None) == entity:
                    logger.info("Tried to remove owner from %s by %s.",
                                unicode(instance), unicode(request.user))
                    msg = _("The original owner cannot be removed, however "
1135
                            "you can transfer ownership.")
1136 1137 1138 1139 1140 1141 1142
                    messages.warning(request, msg)
                    continue
                instance.set_level(entity, None)
                logger.info("Revoked %s's access to %s by %s.",
                            unicode(entity), unicode(instance),
                            unicode(request.user))

1143
    def add_levels(self, request, instance):
1144 1145
        name = request.POST['perm-new-name']
        value = request.POST['perm-new']
1146 1147 1148
        if not name:
            return
        try:
1149
            entity = search_user(name)
1150 1151
        except User.DoesNotExist:
            entity = None
1152 1153
            try:
                entity = Group.objects.get(name=name)