views.py 124 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE.  If not, see <http://www.gnu.org/licenses/>.

18
from __future__ import unicode_literals, absolute_import
19

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

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

57
from django.forms.models import inlineformset_factory
58
from django_tables2 import SingleTableView
59 60
from braces.views import (LoginRequiredMixin, SuperuserRequiredMixin,
                          PermissionRequiredMixin)
61
from braces.views._access import AccessMixin
62
from celery.exceptions import TimeoutError
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, VmStateChangeForm,
71
    CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
72
    TraitsForm, RawDataForm, GroupPermissionForm, AclUserAddForm,
73
    VmResourcesForm, VmAddInterfaceForm, VmListSearchForm
74
)
75 76

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

92
from .store_api import Store, NoStoreException, NotOkException
Kálmán Viktor committed
93

Őry Máté committed
94
logger = logging.getLogger(__name__)
95
saml_available = hasattr(settings, "SAML_CONFIG")
96

97

98 99 100 101
def search_user(keyword):
    try:
        return User.objects.get(username=keyword)
    except User.DoesNotExist:
102 103 104 105
        try:
            return User.objects.get(profile__org_id=keyword)
        except User.DoesNotExist:
            return User.objects.get(email=keyword)
106 107


108 109 110 111 112 113 114 115 116 117 118 119 120
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())
121 122
            else:
                raise
123 124


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

        return newgroups


158
class FilterMixin(object):
159

160 161 162 163
    def get_queryset_filters(self):
        filters = {}
        for item in self.allowed_filters:
            if item in self.request.GET:
164 165 166 167 168
                filters[self.allowed_filters[item]] = (
                    self.request.GET[item].split(",")
                    if self.allowed_filters[item].endswith("__in") else
                    self.request.GET[item])

169
        return filters
170

171 172 173
    def get_queryset(self):
        return super(FilterMixin,
                     self).get_queryset().filter(**self.get_queryset_filters())
174 175


176
class IndexView(LoginRequiredMixin, TemplateView):
Kálmán Viktor committed
177
    template_name = "dashboard/index.html"
178

179
    def get_context_data(self, **kwargs):
180
        user = self.request.user
181
        context = super(IndexView, self).get_context_data(**kwargs)
182

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

195 196
        running = instances.filter(status='RUNNING')
        stopped = instances.exclude(status__in=('RUNNING', 'NOSTATE'))
197

198
        context.update({
199 200 201
            'running_vms': running[:20],
            'running_vm_num': running.count(),
            'stopped_vm_num': stopped.count()
202
        })
203

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

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

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

250
            context['files'] = files
251 252
        else:
            context['no_store'] = True
Kálmán Viktor committed
253

254 255
        return context

256

257
class CheckedDetailView(LoginRequiredMixin, DetailView):
258 259
    read_level = 'user'

260 261 262
    def get_has_level(self):
        return self.object.has_level

263 264
    def get_context_data(self, **kwargs):
        context = super(CheckedDetailView, self).get_context_data(**kwargs)
265
        if not self.get_has_level()(self.request.user, self.read_level):
266 267 268 269
            raise PermissionDenied()
        return context


270 271 272 273 274 275 276 277
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()
278 279
        if not request.user.has_perm('vm.access_console'):
            raise PermissionDenied()
280
        if self.object.node:
281 282 283 284
            with instance_activity(
                    code_suffix='console-accessed', instance=self.object,
                    user=request.user, readable_name=ugettext_noop(
                        "console access"), concurrency_check=False):
285 286 287 288 289
                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)
290 291 292 293
        else:
            raise Http404()


294
class VmDetailView(CheckedDetailView):
Kálmán Viktor committed
295
    template_name = "dashboard/vm-detail.html"
296
    model = Instance
297 298

    def get_context_data(self, **kwargs):
299
        context = super(VmDetailView, self).get_context_data(**kwargs)
300
        instance = context['instance']
301 302
        user = self.request.user
        ops = get_operations(instance, user)
303
        context.update({
Bach Dániel committed
304
            'graphite_enabled': settings.GRAPHITE_URL is not None,
305
            'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
306
                                    kwargs={'pk': self.object.pk}),
307 308
            'ops': ops,
            'op': {i.op: i for i in ops},
309
        })
310 311

        # activity data
312
        activities = instance.get_merged_activities(user)
313 314
        show_show_all = len(activities) > 10
        activities = activities[:10]
315
        context['activities'] = _format_activities(activities)
316
        context['show_show_all'] = show_show_all
317 318
        latest = instance.get_latest_activity_in_progress()
        context['is_new_state'] = (latest and
319
                                   latest.resultant_state is not None and
320
                                   instance.status != latest.resultant_state)
321

322
        context['vlans'] = Vlan.get_objects_with_level(
323
            'user', self.request.user
324
        ).exclude(  # exclude already added interfaces
325 326 327
            pk__in=Interface.objects.filter(
                instance=self.get_object()).values_list("vlan", flat=True)
        ).all()
328 329
        context['acl'] = AclUpdateView.get_acl_data(
            instance, self.request.user, 'dashboard.views.vm-acl')
330
        context['aclform'] = AclUserAddForm()
331 332
        context['os_type_icon'] = instance.os_type.replace("unknown",
                                                           "question")
333 334 335
        # ipv6 infos
        context['ipv6_host'] = instance.get_connect_host(use_ipv6=True)
        context['ipv6_port'] = instance.get_connect_port(use_ipv6=True)
336 337

        # resources forms
338 339 340 341 342
        can_edit = (
            instance in Instance.get_objects_with_level("owner", user)
            and self.request.user.has_perm("vm.change_resources"))
        context['resources_form'] = VmResourcesForm(
            can_edit=can_edit, instance=instance)
343

344 345 346
        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
        options = {
            'new_name': self.__set_name,
357
            'new_description': self.__set_description,
358 359
            'new_tag': self.__add_tag,
            'to_remove': self.__remove_tag,
360
            'port': self.__add_port,
361
            'abort_operation': self.__abort_operation,
362 363 364 365
        }
        for k, v in options.iteritems():
            if request.POST.get(k) is not None:
                return v(request)
366 367
        raise Http404()

368 369
    def __set_name(self, request):
        self.object = self.get_object()
370 371
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
372 373 374 375
        new_name = request.POST.get("new_name")
        Instance.objects.filter(pk=self.object.pk).update(
            **{'name': new_name})

376
        success_message = _("VM successfully renamed.")
377 378 379 380 381 382 383 384 385 386 387 388
        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)
389 390 391 392 393 394 395 396 397 398 399
            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})

400
        success_message = _("VM description successfully updated.")
401 402 403 404 405 406 407 408 409 410 411 412
        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())
413

Kálmán Viktor committed
414 415 416
    def __add_tag(self, request):
        new_tag = request.POST.get('new_tag')
        self.object = self.get_object()
417 418
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
Kálmán Viktor committed
419 420

        if len(new_tag) < 1:
421
            message = u"Please input something."
Kálmán Viktor committed
422
        elif len(new_tag) > 20:
423
            message = u"Tag name is too long."
Kálmán Viktor committed
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
        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()
439 440
            if not self.object.has_level(request.user, 'owner'):
                raise PermissionDenied()
Kálmán Viktor committed
441 442 443 444 445 446 447 448 449 450 451

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

456 457
    def __add_port(self, request):
        object = self.get_object()
458 459
        if (not object.has_level(request.user, 'owner') or
                not request.user.has_perm('vm.config_ports')):
460
            raise PermissionDenied()
461 462 463 464 465 466

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

        try:
            error = None
467 468 469
            interfaces = object.interface_set.all()
            host = Host.objects.get(pk=request.POST.get("host_pk"),
                                    interface__in=interfaces)
470
            host.add_port(proto, private=port)
471 472 473 474 475
        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()
476
        except ValueError:
477
            error = _("There is a problem with your input.")
478
        except Exception as e:
Bach Dániel committed
479 480
            error = _("Unknown error.")
            logger.error(e)
481 482 483 484 485 486 487 488 489

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

490
    def __abort_operation(self, request):
Kálmán Viktor committed
491 492 493 494
        self.object = self.get_object()

        activity = get_object_or_404(InstanceActivity,
                                     pk=request.POST.get("activity"))
495 496
        if not activity.is_abortable_for(request.user):
            raise PermissionDenied()
Kálmán Viktor committed
497 498 499
        activity.abort()
        return redirect("%s#activity" % self.object.get_absolute_url())

500

501
class VmTraitsUpdate(SuperuserRequiredMixin, UpdateView):
502 503 504 505 506 507 508
    form_class = TraitsForm
    model = Instance

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


509
class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
510 511
    form_class = RawDataForm
    model = Instance
512
    template_name = 'dashboard/vm-detail/raw_data.html'
513 514 515 516 517

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


518
class OperationView(RedirectToLoginMixin, DetailView):
519

520
    template_name = 'dashboard/operate.html'
521
    show_in_toolbar = True
522
    effect = None
523
    wait_for_result = None
524
    with_reload = False
525

526 527 528
    @property
    def name(self):
        return self.get_op().name
529

530 531 532
    @property
    def description(self):
        return self.get_op().description
533

534 535 536
    def is_preferred(self):
        return self.get_op().is_preferred()

537 538 539
    @classmethod
    def get_urlname(cls):
        return 'dashboard.vm.op.%s' % cls.op
540

541 542 543 544 545 546 547 548 549 550
    @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)
551

552
    def get_template_names(self):
553
        if self.request.is_ajax():
554
            return ['dashboard/_modal.html']
555
        else:
556
            return ['dashboard/_base.html']
557

558 559 560
    @classmethod
    def get_op_by_object(cls, obj):
        return getattr(obj, cls.op)
561

562 563 564 565
    def get_op(self):
        if not hasattr(self, '_opobj'):
            setattr(self, '_opobj', getattr(self.get_object(), self.op))
        return self._opobj
566

567 568 569 570
    @classmethod
    def get_operation_class(cls):
        return cls.model.get_operation_class(cls.op)

571
    def get_context_data(self, **kwargs):
572
        ctx = super(OperationView, self).get_context_data(**kwargs)
573
        ctx['op'] = self.get_op()
574
        ctx['opview'] = self
575 576 577 578
        url = self.request.path
        if self.request.GET:
            url += '?' + self.request.GET.urlencode()
        ctx['url'] = url
579
        ctx['template'] = super(OperationView, self).get_template_names()[0]
580
        return ctx
581

582 583 584 585
    def check_auth(self):
        logger.debug("OperationView.check_auth(%s)", unicode(self))
        self.get_op().check_auth(self.request.user)

586 587 588 589
    @classmethod
    def check_perms(cls, user):
        cls.get_operation_class().check_perms(user)

590
    def get(self, request, *args, **kwargs):
591
        self.check_auth()
592
        return super(OperationView, self).get(request, *args, **kwargs)
593

594
    def get_response_data(self, result, done, extra=None, **kwargs):
595 596 597 598 599 600
        """Return serializable data to return to agents requesting json
        response to POST"""

        if extra is None:
            extra = {}
        extra["success"] = not isinstance(result, Exception)
601 602 603
        extra["done"] = done
        if isinstance(result, HumanReadableObject):
            extra["message"] = result.get_user_text()
604 605
        return extra

606
    def post(self, request, extra=None, *args, **kwargs):
607
        self.check_auth()
608
        self.object = self.get_object()
609 610
        if extra is None:
            extra = {}
611
        result = None
612
        done = False
613
        try:
614
            task = self.get_op().async(user=request.user, **extra)
615 616 617 618
        except HumanReadableException as e:
            e.send_message(request)
            logger.exception("Could not start operation")
            result = e
619 620
        except Exception as e:
            messages.error(request, _('Could not start operation.'))
621
            logger.exception("Could not start operation")
622
            result = e
623
        else:
624 625 626 627 628 629 630 631
            wait = self.wait_for_result
            if wait:
                try:
                    result = task.get(timeout=wait,
                                      interval=min((wait / 5, .5)))
                except TimeoutError:
                    logger.debug("Result didn't arrive in %ss",
                                 self.wait_for_result, exc_info=True)
632 633 634 635
                except HumanReadableException as e:
                    e.send_message(request)
                    logger.exception(e)
                    result = e
636 637 638 639 640
                except Exception as e:
                    messages.error(request, _('Operation failed.'))
                    logger.debug("Operation failed.", exc_info=True)
                    result = e
                else:
641
                    done = True
642
                    messages.success(request, _('Operation succeeded.'))
643
            if result is None and not done:
644
                messages.success(request, _('Operation is started.'))
645 646

        if "/json" in request.META.get("HTTP_ACCEPT", ""):
647 648
            data = self.get_response_data(result, done,
                                          post_extra=extra, **kwargs)
649 650 651 652
            return HttpResponse(json.dumps(data),
                                content_type="application/json")
        else:
            return redirect("%s#activity" % self.object.get_absolute_url())
653

654
    @classmethod
655 656
    def factory(cls, op, icon='cog', effect='info', extra_bases=(), **kwargs):
        kwargs.update({'op': op, 'icon': icon, 'effect': effect})
657
        return type(str(cls.__name__ + op),
658
                    tuple(list(extra_bases) + [cls]), kwargs)
659

660
    @classmethod
661
    def bind_to_object(cls, instance, **kwargs):
662 663
        me = cls()
        me.get_object = lambda: instance
664
        for key, value in kwargs.iteritems():
665 666
            setattr(me, key, value)
        return me
667 668


669
class AjaxOperationMixin(object):
670

671
    def post(self, request, extra=None, *args, **kwargs):
672 673
        resp = super(AjaxOperationMixin, self).post(
            request, extra, *args, **kwargs)
674
        if request.is_ajax():
675
            if not self.with_reload:
676 677
                store = messages.get_messages(request)
                store.used = True
678 679
            else:
                store = []
680
            return HttpResponse(
681
                json.dumps({'success': True,
682
                            'with_reload': self.with_reload,
683 684 685 686 687 688
                            'messages': [unicode(m) for m in store]}),
                content_type="application=json"
            )
        else:
            return resp

689

690 691 692 693 694 695
class VmOperationView(AjaxOperationMixin, OperationView):

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


696 697 698 699
class FormOperationMixin(object):

    form_class = None

700 701 702
    def get_form_kwargs(self):
        return {}

703 704 705
    def get_context_data(self, **kwargs):
        ctx = super(FormOperationMixin, self).get_context_data(**kwargs)
        if self.request.method == 'POST':
706 707
            ctx['form'] = self.form_class(self.request.POST,
                                          **self.get_form_kwargs())
708
        else:
709
            ctx['form'] = self.form_class(**self.get_form_kwargs())
710 711 712 713 714
        return ctx

    def post(self, request, extra=None, *args, **kwargs):
        if extra is None:
            extra = {}
715
        form = self.form_class(self.request.POST, **self.get_form_kwargs())
716 717
        if form.is_valid():
            extra.update(form.cleaned_data)
718
            resp = super(FormOperationMixin, self).post(
719
                request, extra, *args, **kwargs)
720 721
            if request.is_ajax():
                return HttpResponse(
722 723
                    json.dumps({
                        'success': True,
724 725
                        'with_reload': self.with_reload}),
                    content_type="application=json")
726 727
            else:
                return resp
728 729 730 731
        else:
            return self.get(request)


732 733 734
class RequestFormOperationMixin(FormOperationMixin):

    def get_form_kwargs(self):
735
        val = super(RequestFormOperationMixin, self).get_form_kwargs()
736 737 738 739
        val.update({'request': self.request})
        return val


740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758
class VmAddInterfaceView(FormOperationMixin, VmOperationView):

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

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


759 760 761 762 763
class VmCreateDiskView(FormOperationMixin, VmOperationView):

    op = 'create_disk'
    form_class = VmCreateDiskForm
    show_in_toolbar = False
764
    icon = 'hdd-o'
765
    effect = "success"
766
    is_disk_operation = True
767 768 769 770 771 772 773 774


class VmDownloadDiskView(FormOperationMixin, VmOperationView):

    op = 'download_disk'
    form_class = VmDownloadDiskForm
    show_in_toolbar = False
    icon = 'download'
775
    effect = "success"
776
    is_disk_operation = True
777 778


779 780 781 782
class VmMigrateView(VmOperationView):

    op = 'migrate'
    icon = 'truck'
783
    effect = 'info'
784 785 786
    template_name = 'dashboard/_vm-migrate.html'

    def get_context_data(self, **kwargs):
787
        ctx = super(VmMigrateView, self).get_context_data(**kwargs)
788 789 790 791 792
        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):
793 794
        if extra is None:
            extra = {}
795 796 797 798 799 800 801
        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)


802
class VmSaveView(FormOperationMixin, VmOperationView):
803 804 805

    op = 'save_as_template'
    icon = 'save'
806
    effect = 'info'
807
    form_class = VmSaveForm
808

809 810 811 812 813

class VmResourcesChangeView(VmOperationView):
    op = 'resources_change'
    icon = "save"
    show_in_toolbar = False
814
    wait_for_result = 0.5
815 816 817 818 819

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

820
        instance = get_object_or_404(Instance, pk=kwargs['pk'])
821

822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838
        form = VmResourcesForm(request.POST, instance=instance)
        if not form.is_valid():
            for f in form.errors:
                messages.error(request, "<strong>%s</strong>: %s" % (
                    f, form.errors[f].as_text()
                ))
            if request.is_ajax():  # this is not too nice
                store = messages.get_messages(request)
                store.used = True
                return HttpResponse(
                    json.dumps({'success': False,
                                'messages': [unicode(m) for m in store]}),
                    content_type="application=json"
                )
            else:
                return redirect(instance.get_absolute_url() + "#resources")
        else:
839 840
            extra = form.cleaned_data
            extra['max_ram_size'] = extra['ram_size']
841 842
            return super(VmResourcesChangeView, self).post(request, extra,
                                                           *args, **kwargs)
843 844


845 846 847 848 849 850
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
851
    redirect_exception_classes = (PermissionDenied, SuspiciousOperation, )
852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885

    @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
886
                    self.request.token_user = True
887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921
        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


922 923
class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):

924 925 926 927
    op = 'renew'
    icon = 'calendar'
    effect = 'info'
    show_in_toolbar = False
928
    form_class = VmRenewForm
929
    wait_for_result = 0.5
930 931 932 933 934

    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:
935 936
            choices = (choices.distinct() |
                       Lease.objects.filter(pk=default.pk).distinct())
937 938 939 940

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

942 943
    def get_response_data(self, result, done, extra=None, **kwargs):
        extra = super(VmRenewView, self).get_response_data(result, done,
944 945 946 947 948
                                                           extra, **kwargs)
        extra["new_suspend_time"] = unicode(self.get_op().
                                            instance.time_of_suspend)
        return extra

949

950 951 952 953 954 955 956 957 958 959
class VmStateChangeView(FormOperationMixin, VmOperationView):
    op = 'emergency_change_state'
    icon = 'legal'
    effect = 'danger'
    show_in_toolbar = True
    form_class = VmStateChangeForm
    wait_for_result = 0.5

    def get_form_kwargs(self):
        inst = self.get_op().instance
960 961
        active_activities = InstanceActivity.objects.filter(
            finished__isnull=True, instance=inst)
962 963
        show_interrupt = active_activities.exists()
        val = super(VmStateChangeView, self).get_form_kwargs()
964
        val.update({'show_interrupt': show_interrupt, 'status': inst.status})
965 966 967
        return val


968 969
vm_ops = OrderedDict([
    ('deploy', VmOperationView.factory(
970
        op='deploy', icon='play', effect='success')),
971
    ('wake_up', VmOperationView.factory(
972
        op='wake_up', icon='sun-o', effect='success')),
973
    ('sleep', VmOperationView.factory(
974
        extra_bases=[TokenOperationView],
975
        op='sleep', icon='moon-o', effect='info')),
976 977 978
    ('migrate', VmMigrateView),
    ('save_as_template', VmSaveView),
    ('reboot', VmOperationView.factory(
979
        op='reboot', icon='refresh', effect='warning')),
980
    ('reset', VmOperationView.factory(
981
        op='reset', icon='bolt', effect='warning')),
982
    ('shutdown', VmOperationView.factory(
983
        op='shutdown', icon='power-off', effect='warning')),
984
    ('shut_off', VmOperationView.factory(
985
        op='shut_off', icon='ban', effect='warning')),
986 987
    ('recover', VmOperationView.factory(
        op='recover', icon='medkit', effect='warning')),
988
    ('nostate', VmStateChangeView),
989
    ('destroy', VmOperationView.factory(
990
        extra_bases=[TokenOperationView],
991
        op='destroy', icon='times', effect='danger')),
992 993
    ('create_disk', VmCreateDiskView),
    ('download_disk', VmDownloadDiskView),
994
    ('add_interface', VmAddInterfaceView),
995
    ('renew', VmRenewView),
996
    ('resources_change', VmResourcesChangeView),
997 998
    ('password_reset', VmOperationView.factory(
        op='password_reset', icon='unlock', effect='warning',
999
        show_in_toolbar=False, wait_for_result=0.5, with_reload=True)),
1000 1001 1002 1003
    ('mount_store', VmOperationView.factory(
        op='mount_store', icon='briefcase', effect='info',
        show_in_toolbar=False,
    )),
1004
])
1005 1006 1007 1008 1009 1010 1011 1012 1013


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()
1014
        except PermissionDenied as e:
1015 1016
            logger.debug('Not showing operation %s for %s: %s',
                         k, instance, unicode(e))
1017 1018
        except Exception:
            ops.append(v.bind_to_object(instance, disabled=True))
1019 1020 1021
        else:
            ops.append(v.bind_to_object(instance))
    return ops
1022

Kálmán Viktor committed
1023

1024 1025 1026
class MassOperationView(OperationView):
    template_name = 'dashboard/mass-operate.html'

1027
    def check_auth(self):
1028
        self.get_op().check_perms(self.request.user)
1029 1030 1031 1032
        for i in self.get_object():
            if not i.has_level(self.request.user, "user"):
                raise PermissionDenied(
                    "You have no user access to instance %d" % i.pk)
1033

1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049
    @classmethod
    def get_urlname(cls):
        return 'dashboard.vm.mass-op.%s' % cls.op

    @classmethod
    def get_url(cls):
        return reverse("dashboard.vm.mass-op.%s" % cls.op)

    def get_op(self, instance=None):
        if instance:
            return getattr(instance, self.op)
        else:
            return Instance._ops[self.op]

    def get_context_data(self, **kwargs):
        ctx = super(MassOperationView, self).get_context_data(**kwargs)
1050 1051
        instances = self.get_object()
        ctx['instances'] = self._get_operable_instances(
1052
            instances, self.request.user)
1053
        ctx['vm_count'] = sum(1 for i in ctx['instances'] if not i.disabled)
1054 1055
        return ctx

1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071
    def _call_operations(self, extra):
        request = self.request
        user = request.user
        instances = self.get_object()
        for i in instances:
            try:
                self.get_op(i).async(user=user, **extra)
            except HumanReadableException as e:
                e.send_message(request)
            except Exception as e:
                # pre-existing errors should have been catched when the
                # confirmation dialog was constructed
                messages.error(request, _(
                    "Failed to execute %(op)s operation on "
                    "instance %(instance)s.") % {"op": self.name,
                                                 "instance": i})
1072 1073

    def get_object(self):
1074 1075
        vms = getattr(self.request, self.request.method).getlist("vm")
        return Instance.objects.filter(pk__in=vms)
1076

1077
    def _get_operable_instances(self, instances, user):
1078 1079
        for i in instances:
            try:
1080 1081 1082
                op = self.get_op(i)
                op.check_auth(user)
                op.check_precond()
1083 1084 1085 1086 1087
            except PermissionDenied as e:
                i.disabled = create_readable(
                    _("You are not permitted to execute %(op)s on instance "
                      "%(instance)s."), instance=i.pk, op=self.name)
                i.disabled_icon = "lock"
1088 1089
            except Exception as e:
                i.disabled = fetch_human_exception(e)
1090
            else:
1091
                i.disabled = None
1092
        return instances
1093

1094
    def post(self, request, extra=None, *args, **kwargs):
1095
        self.check_auth()
1096 1097
        if extra is None:
            extra = {}
1098
        self._call_operations(extra)
1099 1100 1101 1102 1103
        if request.is_ajax():
            store = messages.get_messages(request)
            store.used = True
            return HttpResponse(
                json.dumps({'messages': [unicode(m) for m in store]}),
1104
                content_type="application/json"
1105 1106 1107
            )
        else:
            return redirect(reverse("dashboard.views.vm-list"))
1108

1109 1110 1111
    @classmethod
    def factory(cls, vm_op, extra_bases=(), **kwargs):
        return type(str(cls.__name__ + vm_op.op),
1112
                    tuple(list(extra_bases) + [cls, vm_op]), kwargs)
1113

1114

1115
class MassMigrationView(MassOperationView, VmMigrateView):
1116
    template_name = 'dashboard/_vm-mass-migrate.html'
1117

1118
vm_mass_ops = OrderedDict([
1119 1120 1121 1122 1123 1124
    ('deploy', MassOperationView.factory(vm_ops['deploy'])),
    ('wake_up', MassOperationView.factory(vm_ops['wake_up'])),
    ('sleep', MassOperationView.factory(vm_ops['sleep'])),
    ('reboot', MassOperationView.factory(vm_ops['reboot'])),
    ('reset', MassOperationView.factory(vm_ops['reset'])),
    ('shut_off', MassOperationView.factory(vm_ops['shut_off'])),
1125
    ('migrate', MassMigrationView),
1126
    ('destroy', MassOperationView.factory(vm_ops['destroy'])),
1127 1128 1129
])


1130
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
1131 1132
    template_name = "dashboard/node-detail.html"
    model = Node
1133 1134
    form = None
    form_class = TraitForm
1135

1136 1137 1138
    def get_context_data(self, form=None, **kwargs):
        if form is None:
            form = self.form_class()
1139
        context = super(NodeDetailView, self).get_context_data(**kwargs)
1140 1141 1142 1143
        na = NodeActivity.objects.filter(
            node=self.object, parent=None
        ).order_by('-started').select_related()
        context['activities'] = na
1144
        context['trait_form'] = form
1145
        context['graphite_enabled'] = (
Bach Dániel committed
1146
            settings.GRAPHITE_URL is not None)
1147 1148
        return context