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 230 231 232 233 234 235 236 237 238 239 240
        if settings.STORE_URL:
            cache_key = "toplist-%d" % self.request.user.pk
            cache = get_cache("default")
            toplist = cache.get(cache_key)
            if not toplist:
                try:
                    toplist = Store(self.request.user).toplist()
                except Exception:
                    logger.exception("Unable to get tolist for %s",
                                     unicode(self.request.user))
                    toplist = []
                cache.set(cache_key, toplist, 300)
Kálmán Viktor committed
241

242 243 244
            context['toplist'] = toplist
        else:
            context['no_store'] = True
Kálmán Viktor committed
245

246 247
        return context

248

249
def get_vm_acl_data(obj):
250 251 252 253 254 255 256 257 258
    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])}


259 260 261 262 263 264 265 266 267 268 269
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])}


270
class CheckedDetailView(LoginRequiredMixin, DetailView):
271 272
    read_level = 'user'

273 274 275
    def get_has_level(self):
        return self.object.has_level

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


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


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

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

        # activity data
324 325
        context['activities'] = self.object.get_merged_activities(
            self.request.user)
326

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

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

345 346 347
        # resources change perm
        context['can_change_resources'] = self.request.user.has_perm(
            "vm.change_resources")
348

349
        return context
Kálmán Viktor committed
350

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

366 367
        raise Http404()

368 369 370 371
    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
372

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

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

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

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

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

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

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

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

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

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

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

503 504 505 506 507
    def __new_network(self, request):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()

508
        vlan = get_object_or_404(Vlan, pk=request.POST.get("new_network_vlan"))
509 510
        if not vlan.has_level(request.user, 'user'):
            raise PermissionDenied()
511
        try:
512
            self.object.add_interface(vlan=vlan, user=request.user)
513
            messages.success(request, _("Successfully added new interface."))
514 515 516 517 518 519 520
        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}))

521
    def __abort_operation(self, request):
Kálmán Viktor committed
522 523 524 525
        self.object = self.get_object()

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

531

532
class VmTraitsUpdate(SuperuserRequiredMixin, UpdateView):
533 534 535 536 537 538 539
    form_class = TraitsForm
    model = Instance

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


540
class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
541 542 543 544 545 546 547
    form_class = RawDataForm
    model = Instance

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


548
class OperationView(RedirectToLoginMixin, DetailView):
549

550
    template_name = 'dashboard/operate.html'
551
    show_in_toolbar = True
552
    effect = None
553

554 555 556
    @property
    def name(self):
        return self.get_op().name
557

558 559 560
    @property
    def description(self):
        return self.get_op().description
561

562 563 564
    def is_preferred(self):
        return self.get_op().is_preferred()

565 566 567
    @classmethod
    def get_urlname(cls):
        return 'dashboard.vm.op.%s' % cls.op
568

569 570 571 572 573 574 575 576 577 578
    @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)
579

580
    def get_template_names(self):
581
        if self.request.is_ajax():
582
            return ['dashboard/_modal.html']
583
        else:
584
            return ['dashboard/_base.html']
585

586 587 588
    @classmethod
    def get_op_by_object(cls, obj):
        return getattr(obj, cls.op)
589

590 591 592 593
    def get_op(self):
        if not hasattr(self, '_opobj'):
            setattr(self, '_opobj', getattr(self.get_object(), self.op))
        return self._opobj
594

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

606 607 608 609
    def check_auth(self):
        logger.debug("OperationView.check_auth(%s)", unicode(self))
        self.get_op().check_auth(self.request.user)

610
    def get(self, request, *args, **kwargs):
611
        self.check_auth()
612
        return super(OperationView, self).get(request, *args, **kwargs)
613

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

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

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


643
class AjaxOperationMixin(object):
644

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

659

660 661 662 663 664 665
class VmOperationView(AjaxOperationMixin, OperationView):

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


666 667 668 669
class FormOperationMixin(object):

    form_class = None

670 671 672
    def get_form_kwargs(self):
        return {}

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

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


701 702 703 704 705 706 707 708
class RequestFormOperationMixin(FormOperationMixin):

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


709 710 711 712 713
class VmCreateDiskView(FormOperationMixin, VmOperationView):

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


class VmDownloadDiskView(FormOperationMixin, VmOperationView):

    op = 'download_disk'
    form_class = VmDownloadDiskForm
    show_in_toolbar = False
    icon = 'download'
724
    is_disk_operation = True
725 726


727 728 729 730
class VmMigrateView(VmOperationView):

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

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


750
class VmSaveView(FormOperationMixin, VmOperationView):
751 752 753

    op = 'save_as_template'
    icon = 'save'
754
    effect = 'info'
755
    form_class = VmSaveForm
756

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

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)


780 781 782 783 784 785
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
786
    redirect_exception_classes = (PermissionDenied, SuspiciousOperation, )
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 854 855

    @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


856 857
class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):

858 859 860 861
    op = 'renew'
    icon = 'calendar'
    effect = 'info'
    show_in_toolbar = False
862 863 864 865 866 867
    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:
868 869
            choices = (choices.distinct() |
                       Lease.objects.filter(pk=default.pk).distinct())
870 871 872 873

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


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


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

Kálmán Viktor committed
922

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

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

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

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

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

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

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

991

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

    def get_has_level(self):
        return self.object.profile.has_level
999 1000 1001

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

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

1014 1015 1016
        return context

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

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

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

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

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

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

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


1082 1083 1084 1085 1086 1087 1088
class GroupPermissionsView(SuperuserRequiredMixin, UpdateView):
    model = Group
    form_class = GroupPermissionForm
    slug_field = "pk"
    slug_url_kwarg = "group_pk"

    def get_success_url(self):
1089
        return "%s#group-detail-permissions" % (
1090 1091 1092
            self.get_object().groupprofile.get_absolute_url())


1093
class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
1094

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

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

1122 1123 1124 1125 1126 1127 1128 1129 1130 1131
    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 "
1132
                            "you can transfer ownership.")
1133 1134 1135 1136 1137 1138 1139
                    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))

1140
    def add_levels(self, request, instance):
1141 1142
        name = request.POST['perm-new-name']
        value = request.POST['perm-new']
1143 1144 1145
        if not name:
            return
        try:
1146
            entity = search_user(name)
1147 1148
        except User.DoesNotExist:
            entity = None
1149 1150
            try:
                entity = Group.objects.get(name=name)
1151 1152 1153 1154
            except Group.DoesNotExist:
                warning(request, _('User or group "%s" not found.') % name)
                return

1155
        instance.set_level(entity, value)
Őry Máté committed
1156 1157 1158
        logger.info("Set %s's new acl level for %s to %s by %s.",
                    unicode(entity), unicode(instance),
                    value, unicode(request.user))
1159

Kálmán Viktor committed
1160

1161 1162 1163 1164 1165 1166 1167 1168