views.py 131 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 28
import re

29
from django.conf import settings
30
from django.contrib.auth.models import User, Group
31
from django.contrib.auth.views import login as login_view, redirect_to_login
32
from django.contrib.auth.decorators import login_required
33
from django.contrib.auth import login
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, AclUserOrGroupAddForm,
73
    VmResourcesForm, VmAddInterfaceForm, VmListSearchForm,
74 75
    TemplateListSearchForm, ConnectCommandForm,
    TransferOwnershipForm, AddGroupMemberForm
76
)
77 78

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

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

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

100

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


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


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

        return newgroups


161
class FilterMixin(object):
162

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

172
        return filters
173

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

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

    def _parse_get(self, GET_dict):
182
        """
183
        Returns a new dict from request's GET dict to filter the vm list
184 185 186 187 188 189 190 191 192
        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}
193 194

        >>> f = FilterMixin()
195
        >>> o = f._parse_get({'s': "hello"}).items()
196
        >>> sorted(o) # doctest: +ELLIPSIS
197 198
        [(u'name', u'hello'), (...)]
        >>> o = f._parse_get({'s': "name:hello owner:test"}).items()
199
        >>> sorted(o) # doctest: +ELLIPSIS
200 201
        [(u'name', u'hello'), (u'owner', u'test'), (...)]
        >>> o = f._parse_get({'s': "name:hello ws node:node 3 oh"}).items()
202
        >>> sorted(o) # doctest: +ELLIPSIS
203
        [(u'name', u'hello ws'), (u'node', u'node 3 oh'), (...)]
204
        """
205 206
        s = GET_dict.get("s")
        fake = GET_dict.copy()
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
        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
223
        return fake
224 225 226 227 228

    def create_acl_queryset(self, model):
        cleaned_data = self.search_form.cleaned_data
        stype = cleaned_data.get('stype', "all")
        superuser = stype == "all"
229
        shared = stype == "shared" or stype == "all"
230 231 232 233 234 235 236
        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

237

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

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

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

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

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

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

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

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

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

316 317
        return context

318

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

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

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


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


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

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

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

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

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

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

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

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

423
        return context
Kálmán Viktor committed
424

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

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

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

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

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

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

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

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

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

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

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

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

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

571

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

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


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

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


589
class OperationView(RedirectToLoginMixin, DetailView):
590

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


740
class AjaxOperationMixin(object):
741

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

760

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

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


767 768 769 770
class FormOperationMixin(object):

    form_class = None

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

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

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


803 804 805
class RequestFormOperationMixin(FormOperationMixin):

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


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


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

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


class VmDownloadDiskView(FormOperationMixin, VmOperationView):

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


850 851 852 853
class VmMigrateView(VmOperationView):

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

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


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

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

880 881 882 883 884

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

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

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

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


916 917 918 919 920 921
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
922
    redirect_exception_classes = (PermissionDenied, SuspiciousOperation, )
923 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

    @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
957
                    self.request.token_user = True
958 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
        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


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

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

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

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

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

1020

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


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


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

Kálmán Viktor committed
1094

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

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

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

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

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

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