views.py 134 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
32
from django.contrib.auth.views import login as login_view, redirect_to_login
33
from django.contrib.auth.decorators import login_required
34
from django.contrib.auth import login
35
from django.contrib.messages.views import SuccessMessageMixin
36
from django.core.exceptions import (
37
    PermissionDenied, SuspiciousOperation,
38
)
Kálmán Viktor committed
39
from django.core.cache import get_cache
40 41
from django.core import signing
from django.core.urlresolvers import reverse, reverse_lazy
42
from django.db.models import Count, Q
43
from django.http import HttpResponse, HttpResponseRedirect, Http404
44 45 46
from django.shortcuts import (
    redirect, render, get_object_or_404, render_to_response,
)
47
from django.views.decorators.http import require_GET, require_POST
48
from django.views.generic.detail import SingleObjectMixin
49
from django.views.generic import (TemplateView, DetailView, View, DeleteView,
50
                                  UpdateView, CreateView, ListView)
51
from django.contrib import messages
52 53 54
from django.utils.translation import (
    ugettext as _, ugettext_noop, ungettext_lazy
)
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
63
from celery.exceptions import TimeoutError
Kálmán Viktor committed
64

65 66
from django_sshkey.models import UserKey

67
from .forms import (
68
    CircleAuthenticationForm, HostForm, LeaseForm, MyProfileForm,
69
    NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm,
70
    UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
71
    VmSaveForm, UserKeyForm, VmRenewForm, VmStateChangeForm,
72
    CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
73
    TraitsForm, RawDataForm, GroupPermissionForm, AclUserOrGroupAddForm,
74
    VmResourcesForm, VmAddInterfaceForm, VmListSearchForm,
75 76
    TemplateListSearchForm, ConnectCommandForm,
    TransferOwnershipForm, AddGroupMemberForm
77
)
78 79

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

96
from .store_api import Store, NoStoreException, NotOkException
Kálmán Viktor committed
97

Őry Máté committed
98
logger = logging.getLogger(__name__)
99
saml_available = hasattr(settings, "SAML_CONFIG")
100

101

102 103 104 105
def search_user(keyword):
    try:
        return User.objects.get(username=keyword)
    except User.DoesNotExist:
106 107 108 109
        try:
            return User.objects.get(profile__org_id=keyword)
        except User.DoesNotExist:
            return User.objects.get(email=keyword)
110 111


112 113 114 115 116 117 118 119 120 121 122 123 124
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())
125 126
            else:
                raise
127 128


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

        return newgroups


162
class FilterMixin(object):
163

164 165 166 167
    def get_queryset_filters(self):
        filters = {}
        for item in self.allowed_filters:
            if item in self.request.GET:
168 169 170 171 172
                filters[self.allowed_filters[item]] = (
                    self.request.GET[item].split(",")
                    if self.allowed_filters[item].endswith("__in") else
                    self.request.GET[item])

173
        return filters
174

175 176 177
    def get_queryset(self):
        return super(FilterMixin,
                     self).get_queryset().filter(**self.get_queryset_filters())
178

179
    def create_fake_get(self):
180 181 182
        self.request.GET = self._parse_get(self.request.GET)

    def _parse_get(self, GET_dict):
183
        """
184
        Returns a new dict from request's GET dict to filter the vm list
185 186 187 188 189 190 191 192 193
        For example: "name:xy node:1" updates the GET dict
                     to resemble this URL ?name=xy&node=1

        "name:xy node:1".split(":") becomes ["name", "xy node", "1"]
        we pop the the first element and use it as the first dict key
        then we iterate over the rest of the list and split by the last
        whitespace, the first part of this list will be the previous key's
        value, then last part of the list will be the next key.
        The final dict looks like this: {'name': xy, 'node':1}
194 195

        >>> f = FilterMixin()
196
        >>> o = f._parse_get({'s': "hello"}).items()
197
        >>> sorted(o) # doctest: +ELLIPSIS
198 199
        [(u'name', u'hello'), (...)]
        >>> o = f._parse_get({'s': "name:hello owner:test"}).items()
200
        >>> sorted(o) # doctest: +ELLIPSIS
201 202
        [(u'name', u'hello'), (u'owner', u'test'), (...)]
        >>> o = f._parse_get({'s': "name:hello ws node:node 3 oh"}).items()
203
        >>> sorted(o) # doctest: +ELLIPSIS
204
        [(u'name', u'hello ws'), (u'node', u'node 3 oh'), (...)]
205
        """
206 207
        s = GET_dict.get("s")
        fake = GET_dict.copy()
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
        if s:
            s = s.split(":")
            if len(s) < 2:  # if there is no ':' in the string, filter by name
                got = {'name': s[0]}
            else:
                latest = s.pop(0)
                got = {'%s' % latest: None}
                for i in s[:-1]:
                    new = i.rsplit(" ", 1)
                    got[latest] = new[0]
                    latest = new[1] if len(new) > 1 else None
                got[latest] = s[-1]

            # generate a new GET request, that is kinda fake
            for k, v in got.iteritems():
                fake[k] = v
224
        return fake
225 226 227 228 229

    def create_acl_queryset(self, model):
        cleaned_data = self.search_form.cleaned_data
        stype = cleaned_data.get('stype', "all")
        superuser = stype == "all"
230
        shared = stype == "shared" or stype == "all"
231 232 233 234 235 236 237
        level = "owner" if stype == "owned" else "user"
        queryset = model.get_objects_with_level(
            level, self.request.user,
            group_also=shared, disregard_superuser=not superuser,
        )
        return queryset

238

239
class IndexView(LoginRequiredMixin, TemplateView):
Kálmán Viktor committed
240
    template_name = "dashboard/index.html"
241

242
    def get_context_data(self, **kwargs):
243
        user = self.request.user
244
        context = super(IndexView, self).get_context_data(**kwargs)
245

246
        # instances
247
        favs = Instance.objects.filter(favourite__user=self.request.user)
248
        instances = Instance.get_objects_with_level(
249
            'user', user, disregard_superuser=True).filter(destroyed_at=None)
250 251 252
        display = list(favs) + list(set(instances) - set(favs))
        for d in display:
            d.fav = True if d in favs else False
253
        context.update({
254
            'instances': display[:5],
255
            'more_instances': instances.count() - len(instances[:5])
256 257
        })

258 259
        running = instances.filter(status='RUNNING')
        stopped = instances.exclude(status__in=('RUNNING', 'NOSTATE'))
260

261
        context.update({
262 263 264
            'running_vms': running[:20],
            'running_vm_num': running.count(),
            'stopped_vm_num': stopped.count()
265
        })
266

267 268 269 270
        # nodes
        if user.is_superuser:
            nodes = Node.objects.all()
            context.update({
271 272
                'nodes': nodes[:5],
                'more_nodes': nodes.count() - len(nodes[:5]),
273 274 275 276 277 278 279 280 281 282
                '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
283
        if user.has_module_perms('auth'):
284 285
            profiles = GroupProfile.get_objects_with_level('operator', user)
            groups = Group.objects.filter(groupprofile__in=profiles)
286 287 288 289
            context.update({
                'groups': groups[:5],
                'more_groups': groups.count() - len(groups[:5]),
            })
290 291 292 293

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

Kálmán Viktor committed
296
        # toplist
297
        if settings.STORE_URL:
298
            cache_key = "files-%d" % self.request.user.pk
299
            cache = get_cache("default")
300 301
            files = cache.get(cache_key)
            if not files:
302
                try:
303 304 305 306
                    store = Store(self.request.user)
                    toplist = store.toplist()
                    quota = store.get_quota()
                    files = {'toplist': toplist, 'quota': quota}
307 308 309
                except Exception:
                    logger.exception("Unable to get tolist for %s",
                                     unicode(self.request.user))
310 311
                    files = {'toplist': []}
                cache.set(cache_key, files, 300)
Kálmán Viktor committed
312

313
            context['files'] = files
314 315
        else:
            context['no_store'] = True
Kálmán Viktor committed
316

317 318
        return context

319

320
class CheckedDetailView(LoginRequiredMixin, DetailView):
321 322
    read_level = 'user'

323 324 325
    def get_has_level(self):
        return self.object.has_level

326 327
    def get_context_data(self, **kwargs):
        context = super(CheckedDetailView, self).get_context_data(**kwargs)
328
        if not self.get_has_level()(self.request.user, self.read_level):
329 330 331 332
            raise PermissionDenied()
        return context


333 334 335 336 337 338 339 340
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()
341 342
        if not request.user.has_perm('vm.access_console'):
            raise PermissionDenied()
343
        if self.object.node:
344 345 346 347
            with instance_activity(
                    code_suffix='console-accessed', instance=self.object,
                    user=request.user, readable_name=ugettext_noop(
                        "console access"), concurrency_check=False):
348 349 350 351 352
                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)
353 354 355 356
        else:
            raise Http404()


357
class VmDetailView(CheckedDetailView):
Kálmán Viktor committed
358
    template_name = "dashboard/vm-detail.html"
359
    model = Instance
360 361

    def get_context_data(self, **kwargs):
362
        context = super(VmDetailView, self).get_context_data(**kwargs)
363
        instance = context['instance']
364 365
        user = self.request.user
        ops = get_operations(instance, user)
366
        context.update({
Bach Dániel committed
367
            'graphite_enabled': settings.GRAPHITE_URL is not None,
368
            'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
369
                                    kwargs={'pk': self.object.pk}),
370 371
            'ops': ops,
            'op': {i.op: i for i in ops},
372
            'connect_commands': user.profile.get_connect_commands(instance)
373
        })
374 375

        # activity data
376
        activities = instance.get_merged_activities(user)
377 378
        show_show_all = len(activities) > 10
        activities = activities[:10]
379
        context['activities'] = _format_activities(activities)
380
        context['show_show_all'] = show_show_all
381 382
        latest = instance.get_latest_activity_in_progress()
        context['is_new_state'] = (latest and
383
                                   latest.resultant_state is not None and
384
                                   instance.status != latest.resultant_state)
385

386
        context['vlans'] = Vlan.get_objects_with_level(
387
            'user', self.request.user
388
        ).exclude(  # exclude already added interfaces
389 390 391
            pk__in=Interface.objects.filter(
                instance=self.get_object()).values_list("vlan", flat=True)
        ).all()
392 393
        context['acl'] = AclUpdateView.get_acl_data(
            instance, self.request.user, 'dashboard.views.vm-acl')
394
        context['aclform'] = AclUserOrGroupAddForm()
395 396
        context['os_type_icon'] = instance.os_type.replace("unknown",
                                                           "question")
397 398 399
        # ipv6 infos
        context['ipv6_host'] = instance.get_connect_host(use_ipv6=True)
        context['ipv6_port'] = instance.get_connect_port(use_ipv6=True)
400 401

        # resources forms
402
        can_edit = (
403
            instance.has_level(user, "owner")
404 405 406
            and self.request.user.has_perm("vm.change_resources"))
        context['resources_form'] = VmResourcesForm(
            can_edit=can_edit, instance=instance)
407

408 409 410
        if self.request.user.is_superuser:
            context['traits_form'] = TraitsForm(instance=instance)
            context['raw_data_form'] = RawDataForm(instance=instance)
411

412 413 414
        # resources change perm
        context['can_change_resources'] = self.request.user.has_perm(
            "vm.change_resources")
415

416
        # client info
Csók Tamás committed
417 418
        context['client_download'] = self.request.COOKIES.get(
            'downloaded_client')
419 420
        # can link template
        context['can_link_template'] = (
Kálmán Viktor committed
421
            instance.template and instance.template.has_level(user, "operator")
422 423
        )

424
        return context
Kálmán Viktor committed
425

426
    def post(self, request, *args, **kwargs):
427 428
        options = {
            'new_name': self.__set_name,
429
            'new_description': self.__set_description,
430 431
            'new_tag': self.__add_tag,
            'to_remove': self.__remove_tag,
432
            'port': self.__add_port,
433
            'abort_operation': self.__abort_operation,
434 435 436 437
        }
        for k, v in options.iteritems():
            if request.POST.get(k) is not None:
                return v(request)
438 439
        raise Http404()

440 441
    def __set_name(self, request):
        self.object = self.get_object()
442 443
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
444 445 446 447
        new_name = request.POST.get("new_name")
        Instance.objects.filter(pk=self.object.pk).update(
            **{'name': new_name})

448
        success_message = _("VM successfully renamed.")
449 450 451 452 453 454 455 456 457 458 459 460
        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)
461 462 463 464 465 466 467 468 469 470 471
            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})

472
        success_message = _("VM description successfully updated.")
473 474 475 476 477 478 479 480 481 482 483 484
        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())
485

Kálmán Viktor committed
486 487 488
    def __add_tag(self, request):
        new_tag = request.POST.get('new_tag')
        self.object = self.get_object()
489 490
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
Kálmán Viktor committed
491 492

        if len(new_tag) < 1:
493
            message = u"Please input something."
Kálmán Viktor committed
494
        elif len(new_tag) > 20:
495
            message = u"Tag name is too long."
Kálmán Viktor committed
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510
        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()
511 512
            if not self.object.has_level(request.user, 'owner'):
                raise PermissionDenied()
Kálmán Viktor committed
513 514 515 516 517 518 519 520 521 522 523

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

528 529
    def __add_port(self, request):
        object = self.get_object()
530 531
        if (not object.has_level(request.user, 'owner') or
                not request.user.has_perm('vm.config_ports')):
532
            raise PermissionDenied()
533 534 535 536 537 538

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

        try:
            error = None
539 540 541
            interfaces = object.interface_set.all()
            host = Host.objects.get(pk=request.POST.get("host_pk"),
                                    interface__in=interfaces)
542
            host.add_port(proto, private=port)
543 544 545 546 547
        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()
548
        except ValueError:
549
            error = _("There is a problem with your input.")
550
        except Exception as e:
Bach Dániel committed
551 552
            error = _("Unknown error.")
            logger.error(e)
553 554 555 556 557 558 559 560 561

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

562
    def __abort_operation(self, request):
Kálmán Viktor committed
563 564 565 566
        self.object = self.get_object()

        activity = get_object_or_404(InstanceActivity,
                                     pk=request.POST.get("activity"))
567 568
        if not activity.is_abortable_for(request.user):
            raise PermissionDenied()
Kálmán Viktor committed
569 570 571
        activity.abort()
        return redirect("%s#activity" % self.object.get_absolute_url())

572

573
class VmTraitsUpdate(SuperuserRequiredMixin, UpdateView):
574 575 576 577 578 579 580
    form_class = TraitsForm
    model = Instance

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


581
class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
582 583
    form_class = RawDataForm
    model = Instance
584
    template_name = 'dashboard/vm-detail/raw_data.html'
585 586 587 588 589

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


590
class OperationView(RedirectToLoginMixin, DetailView):
591

592
    template_name = 'dashboard/operate.html'
593
    show_in_toolbar = True
594
    effect = None
595
    wait_for_result = None
596
    with_reload = False
597

598 599 600
    @property
    def name(self):
        return self.get_op().name
601

602 603 604
    @property
    def description(self):
        return self.get_op().description
605

606 607 608
    def is_preferred(self):
        return self.get_op().is_preferred()

609 610 611
    @classmethod
    def get_urlname(cls):
        return 'dashboard.vm.op.%s' % cls.op
612

613 614 615 616 617 618 619 620 621 622
    @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)
623

624
    def get_template_names(self):
625
        if self.request.is_ajax():
626
            return ['dashboard/_modal.html']
627
        else:
628
            return ['dashboard/_base.html']
629

630 631 632
    @classmethod
    def get_op_by_object(cls, obj):
        return getattr(obj, cls.op)
633

634 635 636 637
    def get_op(self):
        if not hasattr(self, '_opobj'):
            setattr(self, '_opobj', getattr(self.get_object(), self.op))
        return self._opobj
638

639 640 641 642
    @classmethod
    def get_operation_class(cls):
        return cls.model.get_operation_class(cls.op)

643
    def get_context_data(self, **kwargs):
644
        ctx = super(OperationView, self).get_context_data(**kwargs)
645
        ctx['op'] = self.get_op()
646
        ctx['opview'] = self
647 648 649 650
        url = self.request.path
        if self.request.GET:
            url += '?' + self.request.GET.urlencode()
        ctx['url'] = url
651
        ctx['template'] = super(OperationView, self).get_template_names()[0]
652
        return ctx
653

654 655 656 657
    def check_auth(self):
        logger.debug("OperationView.check_auth(%s)", unicode(self))
        self.get_op().check_auth(self.request.user)

658 659 660 661
    @classmethod
    def check_perms(cls, user):
        cls.get_operation_class().check_perms(user)

662
    def get(self, request, *args, **kwargs):
663
        self.check_auth()
664
        return super(OperationView, self).get(request, *args, **kwargs)
665

666
    def get_response_data(self, result, done, extra=None, **kwargs):
667 668 669 670 671 672
        """Return serializable data to return to agents requesting json
        response to POST"""

        if extra is None:
            extra = {}
        extra["success"] = not isinstance(result, Exception)
673 674 675
        extra["done"] = done
        if isinstance(result, HumanReadableObject):
            extra["message"] = result.get_user_text()
676 677
        return extra

678
    def post(self, request, extra=None, *args, **kwargs):
679
        self.check_auth()
680
        self.object = self.get_object()
681 682
        if extra is None:
            extra = {}
683
        result = None
684
        done = False
685
        try:
686
            task = self.get_op().async(user=request.user, **extra)
687 688 689 690
        except HumanReadableException as e:
            e.send_message(request)
            logger.exception("Could not start operation")
            result = e
691 692
        except Exception as e:
            messages.error(request, _('Could not start operation.'))
693
            logger.exception("Could not start operation")
694
            result = e
695
        else:
696 697 698 699 700 701 702 703
            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)
704 705 706 707
                except HumanReadableException as e:
                    e.send_message(request)
                    logger.exception(e)
                    result = e
708 709 710 711 712
                except Exception as e:
                    messages.error(request, _('Operation failed.'))
                    logger.debug("Operation failed.", exc_info=True)
                    result = e
                else:
713
                    done = True
714
                    messages.success(request, _('Operation succeeded.'))
715
            if result is None and not done:
716
                messages.success(request, _('Operation is started.'))
717 718

        if "/json" in request.META.get("HTTP_ACCEPT", ""):
719 720
            data = self.get_response_data(result, done,
                                          post_extra=extra, **kwargs)
721 722 723 724
            return HttpResponse(json.dumps(data),
                                content_type="application/json")
        else:
            return redirect("%s#activity" % self.object.get_absolute_url())
725

726
    @classmethod
727 728
    def factory(cls, op, icon='cog', effect='info', extra_bases=(), **kwargs):
        kwargs.update({'op': op, 'icon': icon, 'effect': effect})
729
        return type(str(cls.__name__ + op),
730
                    tuple(list(extra_bases) + [cls]), kwargs)
731

732
    @classmethod
733
    def bind_to_object(cls, instance, **kwargs):
734 735
        me = cls()
        me.get_object = lambda: instance
736
        for key, value in kwargs.iteritems():
737 738
            setattr(me, key, value)
        return me
739 740


741
class AjaxOperationMixin(object):
742

743
    def post(self, request, extra=None, *args, **kwargs):
744 745
        resp = super(AjaxOperationMixin, self).post(
            request, extra, *args, **kwargs)
746
        if request.is_ajax():
747
            if not self.with_reload:
748 749
                store = messages.get_messages(request)
                store.used = True
750 751
            else:
                store = []
752
            return HttpResponse(
753
                json.dumps({'success': True,
754
                            'with_reload': self.with_reload,
755 756 757 758 759 760
                            'messages': [unicode(m) for m in store]}),
                content_type="application=json"
            )
        else:
            return resp

761

762 763 764 765 766 767
class VmOperationView(AjaxOperationMixin, OperationView):

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


768 769 770 771
class FormOperationMixin(object):

    form_class = None

772 773 774
    def get_form_kwargs(self):
        return {}

775 776 777
    def get_context_data(self, **kwargs):
        ctx = super(FormOperationMixin, self).get_context_data(**kwargs)
        if self.request.method == 'POST':
778 779
            ctx['form'] = self.form_class(self.request.POST,
                                          **self.get_form_kwargs())
780
        else:
781
            ctx['form'] = self.form_class(**self.get_form_kwargs())
782 783 784 785 786
        return ctx

    def post(self, request, extra=None, *args, **kwargs):
        if extra is None:
            extra = {}
787
        form = self.form_class(self.request.POST, **self.get_form_kwargs())
788 789
        if form.is_valid():
            extra.update(form.cleaned_data)
790
            resp = super(FormOperationMixin, self).post(
791
                request, extra, *args, **kwargs)
792 793
            if request.is_ajax():
                return HttpResponse(
794 795
                    json.dumps({
                        'success': True,
796 797
                        'with_reload': self.with_reload}),
                    content_type="application=json")
798 799
            else:
                return resp
800 801 802 803
        else:
            return self.get(request)


804 805 806
class RequestFormOperationMixin(FormOperationMixin):

    def get_form_kwargs(self):
807
        val = super(RequestFormOperationMixin, self).get_form_kwargs()
808 809 810 811
        val.update({'request': self.request})
        return val


812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830
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


831 832 833 834 835
class VmCreateDiskView(FormOperationMixin, VmOperationView):

    op = 'create_disk'
    form_class = VmCreateDiskForm
    show_in_toolbar = False
836
    icon = 'hdd-o'
837
    effect = "success"
838
    is_disk_operation = True
839 840 841 842 843 844 845 846


class VmDownloadDiskView(FormOperationMixin, VmOperationView):

    op = 'download_disk'
    form_class = VmDownloadDiskForm
    show_in_toolbar = False
    icon = 'download'
847
    effect = "success"
848
    is_disk_operation = True
849 850


851 852 853 854
class VmMigrateView(VmOperationView):

    op = 'migrate'
    icon = 'truck'
855
    effect = 'info'
856 857 858
    template_name = 'dashboard/_vm-migrate.html'

    def get_context_data(self, **kwargs):
859
        ctx = super(VmMigrateView, self).get_context_data(**kwargs)
860 861 862 863 864
        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):
865 866
        if extra is None:
            extra = {}
867 868 869 870 871 872 873
        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)


874
class VmSaveView(FormOperationMixin, VmOperationView):
875 876 877

    op = 'save_as_template'
    icon = 'save'
878
    effect = 'info'
879
    form_class = VmSaveForm
880

881 882 883 884 885

class VmResourcesChangeView(VmOperationView):
    op = 'resources_change'
    icon = "save"
    show_in_toolbar = False
886
    wait_for_result = 0.5
887 888 889 890 891

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

892
        instance = get_object_or_404(Instance, pk=kwargs['pk'])
893

894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910
        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:
911 912
            extra = form.cleaned_data
            extra['max_ram_size'] = extra['ram_size']
913 914
            return super(VmResourcesChangeView, self).post(request, extra,
                                                           *args, **kwargs)
915 916


917 918 919 920 921 922
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
923
    redirect_exception_classes = (PermissionDenied, SuspiciousOperation, )
924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957

    @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
958
                    self.request.token_user = True
959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993
        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


994 995
class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):

996 997 998 999
    op = 'renew'
    icon = 'calendar'
    effect = 'info'
    show_in_toolbar = False
1000
    form_class = VmRenewForm
1001
    wait_for_result = 0.5
1002 1003 1004 1005 1006

    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:
1007 1008
            choices = (choices.distinct() |
                       Lease.objects.filter(pk=default.pk).distinct())
1009 1010 1011 1012

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

1014 1015
    def get_response_data(self, result, done, extra=None, **kwargs):
        extra = super(VmRenewView, self).get_response_data(result, done,
1016 1017 1018 1019 1020
                                                           extra, **kwargs)
        extra["new_suspend_time"] = unicode(self.get_op().
                                            instance.time_of_suspend)
        return extra

1021

1022 1023 1024 1025 1026 1027 1028 1029 1030 1031
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
1032 1033
        active_activities = InstanceActivity.objects.filter(
            finished__isnull=True, instance=inst)
1034 1035
        show_interrupt = active_activities.exists()
        val = super(VmStateChangeView, self).get_form_kwargs()
1036
        val.update({'show_interrupt': show_interrupt, 'status': inst.status})
1037 1038 1039
        return val


1040 1041
vm_ops = OrderedDict([
    ('deploy', VmOperationView.factory(
1042
        op='deploy', icon='play', effect='success')),
1043
    ('wake_up', VmOperationView.factory(
1044
        op='wake_up', icon='sun-o', effect='success')),
1045
    ('sleep', VmOperationView.factory(
1046
        extra_bases=[TokenOperationView],
1047
        op='sleep', icon='moon-o', effect='info')),
1048 1049 1050
    ('migrate', VmMigrateView),
    ('save_as_template', VmSaveView),
    ('reboot', VmOperationView.factory(
1051
        op='reboot', icon='refresh', effect='warning')),
1052
    ('reset', VmOperationView.factory(
1053
        op='reset', icon='bolt', effect='warning')),
1054
    ('shutdown', VmOperationView.factory(
1055
        op='shutdown', icon='power-off', effect='warning')),
1056
    ('shut_off', VmOperationView.factory(
1057
        op='shut_off', icon='ban', effect='warning')),
1058 1059
    ('recover', VmOperationView.factory(
        op='recover', icon='medkit', effect='warning')),
1060
    ('nostate', VmStateChangeView),
1061
    ('destroy', VmOperationView.factory(
1062
        extra_bases=[TokenOperationView],
1063
        op='destroy', icon='times', effect='danger')),
1064 1065
    ('create_disk', VmCreateDiskView),
    ('download_disk', VmDownloadDiskView),
1066
    ('add_interface', VmAddInterfaceView),
1067
    ('renew', VmRenewView),
1068
    ('resources_change', VmResourcesChangeView),
1069 1070
    ('password_reset', VmOperationView.factory(
        op='password_reset', icon='unlock', effect='warning',
1071
        show_in_toolbar=False, wait_for_result=0.5, with_reload=True)),
1072 1073 1074 1075
    ('mount_store', VmOperationView.factory(
        op='mount_store', icon='briefcase', effect='info',
        show_in_toolbar=False,
    )),
1076
])
1077 1078 1079 1080 1081 1082 1083 1084 1085


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()
1086
        except PermissionDenied as e:
1087 1088
            logger.debug('Not showing operation %s for %s: %s',
                         k, instance, unicode(e))
1089 1090
        except Exception:
            ops.append(v.bind_to_object(instance, disabled=True))
1091 1092 1093
        else:
            ops.append(v.bind_to_object(instance))
    return ops
1094

Kálmán Viktor committed
1095

1096 1097 1098
class MassOperationView(OperationView):
    template_name = 'dashboard/mass-operate.html'

1099
    def check_auth(self):
1100
        self.get_op().check_perms(self.request.user)
1101 1102 1103 1104
        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)
1105

1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121
    @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)
1122 1123
        instances = self.get_object()
        ctx['instances'] = self._get_operable_instances(
1124
            instances, self.request.user)
1125
        ctx['vm_count'] = sum(1 for i in ctx['instances'] if not i.disabled)
1126 1127
        return ctx

1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143
    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})
1144 1145

    def get_object(self):
1146 1147
        vms = getattr(self.request, self.request.method).getlist("vm")
        return Instance.objects.filter(pk__in=vms)
1148

1149
    def _get_operable_instances(self, instances, user):
1150 1151
        for i in instances:
            try:
1152 1153 1154
                op = self.get_op(i)
                op.check_auth(user)
                op.check_precond()
1155 1156 1157 1158 1159
            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"
1160 1161
            except Exception as e:
                i.disabled = fetch_human_exception(e)
1162
            else: