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
import re
28
import requests
29

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

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

64 65
from django_sshkey.models import UserKey

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

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

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

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

99

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


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


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

        return newgroups


160
class FilterMixin(object):
161

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

171
        return filters
172

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

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

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

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

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

236

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

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

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

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

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

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

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

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

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

315 316
        return context

317

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

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

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


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


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

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

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

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

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

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

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

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

422
        return context
Kálmán Viktor committed
423

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

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

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

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

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

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

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

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

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

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

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

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

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

570

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

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


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

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


588
class OperationView(RedirectToLoginMixin, DetailView):
589

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


739
class AjaxOperationMixin(object):
740

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

759

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

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


766 767 768 769
class FormOperationMixin(object):

    form_class = None

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

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

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


802 803 804
class RequestFormOperationMixin(FormOperationMixin):

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


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


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

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


class VmDownloadDiskView(FormOperationMixin, VmOperationView):

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


849 850 851 852
class VmMigrateView(VmOperationView):

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

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


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

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

879 880 881 882 883

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

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

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

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


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

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


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

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

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

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

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

1019

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


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


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

Kálmán Viktor committed
1093

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

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

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

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

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

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

1164
    def post(self, request, extra=None, *args, **kwargs):
1165
        self.check_auth()
1166 1167
        if extra is None:
            extra = {}
1168
        self._call_operations(extra)
1169 1170 1171 1172 1173
        if request.is_ajax():
            store = messages.get_messages(request)
            store.used = True
            return HttpResponse(
                json.dumps({'messages': [unicode(m) for m in store]}),
1174
                content_type="application/json"
1175 1176 1177
            )
        else:
            return redirect(reverse("dashboard.views.vm-list"))
1178

1179 1180 1181
    @classmethod
    def factory(cls, vm_op, extra_bases=(), **kwargs):
        return type(str(cls.__name__ + vm_op.op),
1182
                    tuple(list(extra_bases) + [cls, vm_op]), kwargs)
1183

1184

1185
class MassMigrationView(MassOperationView, VmMigrateView):
1186
    template_name = 'dashboard/_vm-mass-migrate.html'
1187

1188
vm_mass_ops = OrderedDict([
1189 1190 1191 1192 1193 1194
    ('deploy', MassOperationView.factory(vm_ops['deploy'])),
    ('wake_up', MassOperationView.factory(vm_ops['wake_up'])),
    ('sleep', MassOperationView.factory(vm_ops['sleep'])),
    ('reboot', MassOperationView.factory(vm_ops['reboot'])),
    ('reset', MassOperationView.factory(vm_ops['reset'])),
    ('shut_off', MassOperationView.factory(vm_ops['shut_off'])),
1195
    ('migrate', MassMigrationView),
1196
    ('destroy', MassOperationView.factory(vm_ops['destroy'])),
1197 1198 1199
])


1200
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
1201 1202
    template_name = "dashboard/node-detail.html"
    model = Node
1203 1204
    form = None
    form_class = TraitForm
1205

1206 1207 1208
    def get_context_data(self, form=None, **kwargs):
        if form is None:
            form = self.form_class()
1209
        context = super(NodeDetailView, self).get_context_data(**kwargs)
1210 1211 1212 1213
        na = NodeActivity.objects.filter(
            node=self.object, parent=None
        ).order_by('-started').select_related()
        context['activities'] = na
1214
        context['trait_form'] = form
1215
        context['graphite_enabled'] = (
Bach Dániel committed
1216
            settings.GRAPHITE_URL is not None)
1217 1218
        return context

1219 1220 1221
    def post(self, request, *args, **kwargs):
        if request.POST.get('new_name'):
            return self.__set_name(request)
1222 1223
        if request.POST.get('to_remove'):
            return self.__remove_trait(request)
1224 1225
        return redirect(reverse_lazy("dashboard.views.node-detail",
                                     kwargs={'pk': self.get_object().pk}))
1226 1227 1228 1229 1230 1231 1232

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

1233
        success_message = _("Node successfully renamed.")
1234 1235 1236 1237 1238
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
                'node_pk': self.object.pk
1239 1240 1241 1242 1243 1244 1245 1246 1247 1248
            }
            return HttpResponse(
                json.dumps(response),
                content_type="application/json"
            )
        else:
            messages.success(request, success_message)
            return redirect(reverse_lazy("dashboard.views.node-detail",
                                         kwargs={'pk': self.object.pk}))

1249 1250 1251 1252
    def __remove_trait(self, request):
        try:
            to_remove = request.POST.get('to_remove')
            self.object = self.get_object()
1253
            self.object.traits.remove(to_remove)
1254 1255 1256 1257 1258 1259 1260
            message = u"Success"
        except:  # note this won't really happen
            message = u"Not success"

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': message}),
1261
                content_type="application/json"
1262
            )
1263
        else:
1264
            return redirect(self.object.get_absolute_url())
1265

1266

1267
class GroupDetailView(CheckedDetailView):
1268 1269
    template_name = "dashboard/group-detail.html"
    model = Group
1270
    read_level = 'operator'
1271 1272 1273

    def get_has_level(self):
        return self.object.profile.has_level
1274 1275 1276

    def get_context_data(self, **kwargs):
        context = super(GroupDetailView, self).get_context_data(**kwargs)
1277 1278
        context['group'] = self.object
        context['users'] = self.object.user_set.all()
1279 1280
        context['future_users'] = FutureMember.objects.filter(
            group=self.object)
1281 1282 1283
        context['acl'] = AclUpdateView.get_acl_data(
            self.object.profile, self.request.user,
            'dashboard.views.group-acl')
1284
        context['aclform'] = AclUserAddForm()
1285 1286
        context['group_profile_form'] = GroupProfileUpdate.get_form_object(
            self.request, self.object.profile)
1287 1288 1289 1290 1291

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

1292 1293 1294
        return context

    def post(self, request, *args, **kwargs):
1295 1296 1297
        self.object = self.get_object()
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
1298

1299 1300
        if request.POST.get('new_name'):
            return self.__set_name(request)
1301
        if request.POST.get('list-new-name'):
1302
            return self.__add_user(request)
1303
        if request.POST.get('list-new-namelist'):
1304
            return self.__add_list(request)
1305 1306 1307 1308
        if (request.POST.get('list-new-name') is not None) and \
                (request.POST.get('list-new-namelist') is not None):
            return redirect(reverse_lazy("dashboard.views.group-detail",
                                         kwargs={'pk': self.get_object().pk}))
1309 1310

    def __add_user(self, request):
1311
        name = request.POST['list-new-name']
1312 1313 1314
        self.__add_username(request, name)
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
1315 1316

    def __add_username(self, request, name):
1317
        if not name:
1318
            return
1319
        try:
1320
            entity = search_user(name)
1321
            self.object.user_set.add(entity)
1322
        except User.DoesNotExist:
1323 1324 1325 1326
            if saml_available:
                FutureMember.objects.get_or_create(org_id=name,
                                                   group=self.object)
            else:
1327
                messages.warning(request, _('User "%s" not found.') % name)
1328

1329
    def __add_list(self, request):
1330 1331
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
1332 1333 1334
        userlist = request.POST.get('list-new-namelist').split('\r\n')
        for line in userlist:
            self.__add_username(request, line)
1335 1336
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
1337 1338 1339 1340 1341 1342

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

1343
        success_message = _("Group successfully renamed.")
1344 1345 1346 1347
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
1348
                'group_pk': self.object.pk
1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359
            }
            return HttpResponse(
                json.dumps(response),
                content_type="application/json"
            )
        else:
            messages.success(request, success_message)
            return redirect(reverse_lazy("dashboard.views.group-detail",
                                         kwargs={'pk': self.object.pk}))


1360 1361 1362 1363 1364 1365 1366
class GroupPermissionsView(SuperuserRequiredMixin, UpdateView):
    model = Group
    form_class = GroupPermissionForm
    slug_field = "pk"
    slug_url_kwarg = "group_pk"

    def get_success_url(self):
1367
        return "%s#group-detail-permissions" % (
1368 1369 1370
            self.get_object().groupprofile.get_absolute_url())


1371
class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386
    def send_success_message(self, whom, old_level, new_level):
        if old_level and new_level:
            msg = _("Acl user/group %(w)s successfully modified.")
        elif not old_level and new_level:
            msg = _("Acl user/group %(w)s successfully added.")
        elif old_level and not new_level:
            msg = _("Acl user/group %(w)s successfully removed.")
        if msg:
            messages.success(self.request, msg % {'w': whom})

    def get_level(self, whom):
        for u, level in self.acl_data:
            if u == whom:
                return level
        return None
1387

1388 1389 1390
    @classmethod
    def get_acl_data(cls, obj, user, url):
        levels = obj.ACL_LEVELS
1391 1392
        allowed_levels = list(l for l in OrderedDict(levels)
                              if cls.has_next_level(user, obj, l))
1393 1394
        is_owner = 'owner' in allowed_levels

1395 1396
        allowed_users = cls.get_allowed_users(user)
        allowed_groups = cls.get_allowed_groups(user)
1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413

        user_levels = list(
            {'user': u, 'level': l} for u, l in obj.get_users_with_level()
            if is_owner or u == user or u in allowed_users)

        group_levels = list(
            {'group': g, 'level': l} for g, l in obj.get_groups_with_level()
            if is_owner or g in allowed_groups)

        return {'users': user_levels,
                'groups': group_levels,
                'levels': levels,
                'allowed_levels': allowed_levels,
                'url': reverse(url, args=[obj.pk])}

    @classmethod
    def has_next_level(self, user, instance, level):
1414 1415 1416 1417 1418
        levels = OrderedDict(instance.ACL_LEVELS).keys()
        next_levels = dict(zip([None] + levels, levels + levels[-1:]))
        # {None: 'user', 'user': 'operator', 'operator: 'owner',
        #  'owner: 'owner'}
        next_level = next_levels[level]
1419
        return instance.has_level(user, next_level)
1420

1421
    @classmethod
1422 1423
    def get_allowed_groups(cls, user):
        if user.has_perm('dashboard.use_autocomplete'):
1424 1425 1426 1427 1428 1429
            return Group.objects.all()
        else:
            profiles = GroupProfile.get_objects_with_level('owner', user)
            return Group.objects.filter(groupprofile__in=profiles).distinct()

    @classmethod
1430 1431
    def get_allowed_users(cls, user):
        if user.has_perm('dashboard.use_autocomplete'):
1432 1433
            return User.objects.all()
        else:
1434
            groups = cls.get_allowed_groups(user)
1435 1436 1437 1438 1439
            return User.objects.filter(
                Q(groups__in=groups) | Q(pk=user.pk)).distinct()

    def check_auth(self, whom, old_level, new_level):
        if isinstance(whom, Group):
1440 1441
            if (not self.is_owner and whom not in
                    AclUpdateView.get_allowed_groups(self.request.user)):
1442 1443
                return False
        elif isinstance(whom, User):
1444 1445
            if (not self.is_owner and whom not in
                    AclUpdateView.get_allowed_users(self.request.user)):
1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481
                return False
        return (
            AclUpdateView.has_next_level(self.request.user,
                                         self.instance, new_level) and
            AclUpdateView.has_next_level(self.request.user,
                                         self.instance, old_level))

    def set_level(self, whom, new_level):
        user = self.request.user
        old_level = self.get_level(whom)
        if old_level == new_level:
            return

        if getattr(self.instance, "owner", None) == whom:
            logger.info("Tried to set owner's acl level for %s by %s.",
                        unicode(self.instance), unicode(user))
            msg = _("The original owner cannot be removed, however "
                    "you can transfer ownership.")
            if not getattr(self, 'hide_messages', False):
                messages.warning(self.request, msg)
        elif self.check_auth(whom, old_level, new_level):
            logger.info(
                u"Set %s's acl level for %s to %s by %s.", unicode(whom),
                unicode(self.instance), new_level, unicode(user))
            if not getattr(self, 'hide_messages', False):
                self.send_success_message(whom, old_level, new_level)
            self.instance.set_level(whom, new_level)
        else:
            logger.warning(
                u"Tried to set %s's acl_level for %s (%s->%s) by %s.",
                unicode(whom), unicode(self.instance), old_level, new_level,
                unicode(user))

    def set_or_remove_levels(self):
        for key, value in self.request.POST.items():
            m = re.match('(perm|remove)-([ug])-(\d+)', key)
1482
            if m:
1483 1484 1485
                cmd, typ, id = m.groups()
                if cmd == 'remove':
                    value = None
1486
                entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
1487 1488 1489
                self.set_level(entity, value)

    def add_levels(self):
1490 1491 1492
        name = self.request.POST.get('name', None)
        level = self.request.POST.get('level', None)
        if not name or not level:
1493 1494
            return
        try:
1495
            entity = search_user(name)
1496 1497 1498 1499 1500
            if self.instance.object_level_set.filter(users__in=[entity]):
                messages.warning(
                    self.request, _('User "%s" has already '
                                    'access to this object.') % name)
                return
1501 1502
        except User.DoesNotExist:
            entity = None
1503 1504
            try:
                entity = Group.objects.get(name=name)
1505 1506 1507 1508 1509
                if self.instance.object_level_set.filter(groups__in=[entity]):
                    messages.warning(
                        self.request, _('Group "%s" has already '
                                        'access to this object.') % name)
                    return
1510
            except Group.DoesNotExist:
1511
                messages.warning(
Bach Dániel committed
1512
                    self.request, _('User or group "%s" not found.') % name)
1513
                return
1514
        self.set_level(entity, level)
1515

1516
    def post(self, request, *args, **kwargs):
Bach Dániel committed
1517
        self.instance = self.get_object()
1518
        self.is_owner = self.instance.has_level(request.user, 'owner')
Bach Dániel committed
1519 1520 1521 1522 1523
        self.acl_data = (self.instance.get_users_with_level() +
                         self.instance.get_groups_with_level())
        self.set_or_remove_levels()
        self.add_levels()
        return redirect("%s#access" % self.instance.get_absolute_url())
1524

Kálmán Viktor committed
1525

1526 1527 1528 1529
class TemplateAclUpdateView(AclUpdateView):
    model = InstanceTemplate


1530 1531 1532
class GroupAclUpdateView(AclUpdateView):
    model = Group

1533 1534
    def get_object(self):
        return super(GroupAclUpdateView, self).get_object().profile
1535 1536


1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549
class ClientCheck(LoginRequiredMixin, TemplateView):

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/_modal.html']
        else:
            return ['dashboard/nojs-wrapper.html']

    def get_context_data(self, *args, **kwargs):
        context = super(ClientCheck, self).get_context_data(*args, **kwargs)
        context.update({
            'box_title': _('About CIRCLE Client'),
            'ajax_title': False,
1550
            'client_download_url': settings.CLIENT_DOWNLOAD_URL,
1551
            'template': "dashboard/_client-check.html",
Csók Tamás committed
1552 1553
            'instance': get_object_or_404(
                Instance, pk=self.request.GET.get('vm')),
1554
        })
Csók Tamás committed
1555 1556
        if not context['instance'].has_level(self.request.user, 'operator'):
            raise PermissionDenied()
1557 1558 1559 1560
        return context

    def post(self, request, *args, **kwargs):
        instance = get_object_or_404(Instance, pk=request.POST.get('vm'))
Csók Tamás committed
1561 1562
        if not instance.has_level(request.user, 'operator'):
            raise PermissionDenied()
1563
        response = HttpResponseRedirect(instance.get_absolute_url())
1564 1565 1566
        response.set_cookie('downloaded_client', 'True', 365 * 24 * 60 * 60)
        return response

Csók Tamás committed
1567

1568
class TemplateChoose(LoginRequiredMixin, TemplateView):
1569 1570 1571 1572 1573 1574 1575 1576 1577

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/modal-wrapper.html']
        else:
            return ['dashboard/nojs-wrapper.html']

    def get_context_data(self, *args, **kwargs):
        context = super(TemplateChoose, self).get_context_data(*args, **kwargs)
1578
        templates = InstanceTemplate.get_objects_with_level("user",
1579 1580 1581
                                                            self.request.user)
        context.update({
            'box_title': _('Choose template'),
1582
            'ajax_title': True,
1583
            'template': "dashboard/_template-choose.html",
1584
            'templates': templates.all(),
1585 1586 1587
        })
        return context

1588 1589 1590 1591 1592 1593 1594
    def post(self, request, *args, **kwargs):
        if not request.user.has_perm('vm.create_template'):
            raise PermissionDenied()

        template = request.POST.get("parent")
        if template == "base_vm":
            return redirect(reverse("dashboard.views.template-create"))
1595
        elif template is None:
1596
            messages.warning(request, _("Select an option to proceed."))
1597
            return redirect(reverse("dashboard.views.template-choose"))
1598 1599 1600
        else:
            template = get_object_or_404(InstanceTemplate, pk=template)

1601
        if not template.has_level(request.user, "user"):
1602 1603
            raise PermissionDenied()

1604 1605 1606 1607 1608
        instance = Instance.create_from_template(
            template=template, owner=request.user, is_base=True)

        return redirect(instance.get_absolute_url())

1609

1610 1611 1612 1613
class TemplateCreate(SuccessMessageMixin, CreateView):
    model = InstanceTemplate
    form_class = TemplateForm

1614 1615
    def get_template_names(self):
        if self.request.is_ajax():
1616
            pass
1617 1618 1619 1620 1621 1622
        else:
            return ['dashboard/nojs-wrapper.html']

    def get_context_data(self, *args, **kwargs):
        context = super(TemplateCreate, self).get_context_data(*args, **kwargs)

1623
        num_leases = Lease.get_objects_with_level("operator",
1624 1625
                                                  self.request.user).count()
        can_create_leases = self.request.user.has_perm("create_leases")
1626
        context.update({
1627
            'box_title': _("Create a new base VM"),
1628
            'template': "dashboard/_template-create.html",
1629
            'show_lease_create': num_leases < 1 and can_create_leases
1630 1631 1632
        })
        return context

1633
    def get(self, *args, **kwargs):
1634
        if not self.request.user.has_perm('vm.create_base_template'):
1635
            raise PermissionDenied()
1636

1637 1638 1639 1640
        return super(TemplateCreate, self).get(*args, **kwargs)

    def get_form_kwargs(self):
        kwargs = super(TemplateCreate, self).get_form_kwargs()
1641
        kwargs['user'] = self.request.user
1642 1643
        return kwargs

1644
    def post(self, request, *args, **kwargs):
1645
        if not self.request.user.has_perm('vm.create_base_template'):
1646
            raise PermissionDenied()
1647 1648

        form = self.form_class(request.POST, user=request.user)
1649 1650
        if not form.is_valid():
            return self.get(request, form, *args, **kwargs)
1651
        else:
1652
            post = form.cleaned_data
1653 1654
            networks = self.__create_networks(post.pop("networks"),
                                              request.user)
1655
            post.pop("parent")
1656
            post['max_ram_size'] = post['ram_size']
1657 1658 1659 1660 1661 1662 1663
            req_traits = post.pop("req_traits")
            tags = post.pop("tags")
            post['pw'] = User.objects.make_random_password()
            post['is_base'] = True
            inst = Instance.create(params=post, disks=[],
                                   networks=networks,
                                   tags=tags, req_traits=req_traits)
1664

1665
            return redirect("%s#resources" % inst.get_absolute_url())
1666

1667
    def __create_networks(self, vlans, user):
1668 1669
        networks = []
        for v in vlans:
1670 1671
            if not v.has_level(user, "user"):
                raise PermissionDenied()
1672 1673 1674
            networks.append(InterfaceTemplate(vlan=v, managed=v.managed))
        return networks

1675 1676 1677 1678
    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-list")


1679
class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
1680
    model = InstanceTemplate
1681 1682
    template_name = "dashboard/template-edit.html"
    form_class = TemplateForm
1683
    success_message = _("Successfully modified template.")
1684 1685

    def get(self, request, *args, **kwargs):
1686
        template = self.get_object()
1687
        if not template.has_level(request.user, 'user'):
1688
            raise PermissionDenied()
1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709
        if request.is_ajax():
            template = {
                'num_cores': template.num_cores,
                'ram_size': template.ram_size,
                'priority': template.priority,
                'arch': template.arch,
                'description': template.description,
                'system': template.system,
                'name': template.name,
                'disks': [{'pk': d.pk, 'name': d.name}
                          for d in template.disks.all()],
                'network': [
                    {'vlan_pk': i.vlan.pk, 'vlan': i.vlan.name,
                     'managed': i.managed}
                    for i in InterfaceTemplate.objects.filter(
                        template=self.get_object()).all()
                ]
            }
            return HttpResponse(json.dumps(template),
                                content_type="application/json")
        else:
1710 1711
            return super(TemplateDetail, self).get(request, *args, **kwargs)

1712
    def get_context_data(self, **kwargs):
1713
        obj = self.get_object()
1714
        context = super(TemplateDetail, self).get_context_data(**kwargs)
1715 1716
        context['acl'] = AclUpdateView.get_acl_data(
            obj, self.request.user, 'dashboard.views.template-acl')
1717
        context['disks'] = obj.disks.all()
1718
        context['is_owner'] = obj.has_level(self.request.user, 'owner')
1719
        context['aclform'] = AclUserAddForm()
1720 1721
        return context

1722 1723 1724 1725
    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-detail",
                            kwargs=self.kwargs)

1726 1727 1728 1729 1730 1731
    def post(self, request, *args, **kwargs):
        template = self.get_object()
        if not template.has_level(request.user, 'owner'):
            raise PermissionDenied()
        return super(TemplateDetail, self).post(self, request, args, kwargs)

1732 1733 1734 1735 1736
    def get_form_kwargs(self):
        kwargs = super(TemplateDetail, self).get_form_kwargs()
        kwargs['user'] = self.request.user
        return kwargs

1737

1738
class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView):
1739 1740 1741 1742 1743
    template_name = "dashboard/template-list.html"
    model = InstanceTemplate
    table_class = TemplateListTable
    table_pagination = False

1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755
    allowed_filters = {
        'name': "name__icontains",
        'tags[]': "tags__name__in",
        'tags': "tags__name__in",  # for search string
        'owner': "owner__username",
        'ram': "ram_size",
        'ram_size': "ram_size",
        'cores': "num_cores",
        'num_cores': "num_cores",
        'access_method': "access_method__iexact",
    }

1756 1757
    def get_context_data(self, *args, **kwargs):
        context = super(TemplateList, self).get_context_data(*args, **kwargs)
1758 1759 1760 1761 1762 1763
        context['lease_table'] = LeaseListTable(
            Lease.get_objects_with_level("user", self.request.user),
            request=self.request)

        context['search_form'] = self.search_form

1764
        return context
1765

1766 1767 1768 1769 1770
    def get(self, *args, **kwargs):
        self.search_form = TemplateListSearchForm(self.request.GET)
        self.search_form.full_clean()
        return super(TemplateList, self).get(*args, **kwargs)

1771 1772 1773 1774 1775 1776 1777 1778 1779
    def create_acl_queryset(self, model):
        queryset = super(TemplateList, self).create_acl_queryset(model)
        sql = ("SELECT count(*) FROM vm_instance WHERE "
               "vm_instance.template_id = vm_instancetemplate.id and "
               "vm_instance.destroyed_at is null and "
               "vm_instance.status = 'RUNNING'")
        queryset = queryset.extra(select={'running': sql})
        return queryset

1780 1781 1782
    def get_queryset(self):
        logger.debug('TemplateList.get_queryset() called. User: %s',
                     unicode(self.request.user))
1783
        qs = self.create_acl_queryset(InstanceTemplate)
1784 1785 1786
        self.create_fake_get()

        try:
1787
            qs = qs.filter(**self.get_queryset_filters()).distinct()
1788 1789
        except ValueError:
            messages.error(self.request, _("Error during filtering."))
1790 1791

        return qs.select_related("lease", "owner", "owner__profile")
1792

1793

1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810
class TemplateDelete(LoginRequiredMixin, DeleteView):
    model = InstanceTemplate

    def get_success_url(self):
        return reverse("dashboard.views.template-list")

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/confirm/ajax-delete.html']
        else:
            return ['dashboard/confirm/base-delete.html']

    def delete(self, request, *args, **kwargs):
        object = self.get_object()
        if not object.has_level(request.user, 'owner'):
            raise PermissionDenied()

1811
        object.destroy_disks()
1812 1813
        object.delete()
        success_url = self.get_success_url()
1814
        success_message = _("Template successfully deleted.")
1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': success_message}),
                content_type="application/json",
            )
        else:
            messages.success(request, success_message)
            return HttpResponseRedirect(success_url)


1826
class VmList(LoginRequiredMixin, FilterMixin, ListView):
Kálmán Viktor committed
1827
    template_name = "dashboard/vm-list.html"
1828 1829 1830 1831
    allowed_filters = {
        'name': "name__icontains",
        'node': "node__name__icontains",
        'status': "status__iexact",
1832 1833
        'tags[]': "tags__name__in",
        'tags': "tags__name__in",  # for search string
Kálmán Viktor committed
1834
        'owner': "owner__username",
1835
        'template': "template__pk",
1836
    }
1837

1838 1839
    def get_context_data(self, *args, **kwargs):
        context = super(VmList, self).get_context_data(*args, **kwargs)
1840 1841 1842
        context['ops'] = []
        for k, v in vm_mass_ops.iteritems():
            try:
1843
                v.check_perms(user=self.request.user)
1844 1845 1846 1847
            except PermissionDenied:
                pass
            else:
                context['ops'].append(v)
Kálmán Viktor committed
1848
        context['search_form'] = self.search_form
1849
        context['show_acts_in_progress'] = self.object_list.count() < 100
1850 1851
        return context

1852 1853
    def get(self, *args, **kwargs):
        if self.request.is_ajax():
1854 1855
            return self._create_ajax_request()
        else:
1856 1857
            self.search_form = VmListSearchForm(self.request.GET)
            self.search_form.full_clean()
1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870
            return super(VmList, self).get(*args, **kwargs)

    def _create_ajax_request(self):
        if self.request.GET.get("compact") is not None:
            instances = Instance.get_objects_with_level(
                "user", self.request.user).filter(destroyed_at=None)
            statuses = {}
            for i in instances:
                statuses[i.pk] = {
                    'status': i.get_status_display(),
                    'icon': i.get_status_icon(),
                    'in_status_change': i.is_in_status_change(),
                }
1871 1872
                if self.request.user.is_superuser:
                    statuses[i.pk]['node'] = i.node.name if i.node else "-"
1873 1874 1875
            return HttpResponse(json.dumps(statuses),
                                content_type="application/json")
        else:
1876 1877 1878 1879
            favs = Instance.objects.filter(
                favourite__user=self.request.user).values_list('pk', flat=True)
            instances = Instance.get_objects_with_level(
                'user', self.request.user).filter(
1880
                destroyed_at=None).all()
1881 1882 1883
            instances = [{
                'pk': i.pk,
                'name': i.name,
1884 1885 1886
                'icon': i.get_status_icon(),
                'host': "" if not i.primary_host else i.primary_host.hostname,
                'status': i.get_status_display(),
1887 1888
                'fav': i.pk in favs,
            } for i in instances]
1889
            return HttpResponse(
1890
                json.dumps(list(instances)),  # instances is ValuesQuerySet
1891 1892 1893
                content_type="application/json",
            )

1894 1895 1896 1897 1898 1899
    def create_acl_queryset(self, model):
        queryset = super(VmList, self).create_acl_queryset(model)
        if not self.search_form.cleaned_data.get("include_deleted"):
            queryset = queryset.filter(destroyed_at=None)
        return queryset

1900
    def get_queryset(self):
1901
        logger.debug('VmList.get_queryset() called. User: %s',
1902
                     unicode(self.request.user))
1903
        queryset = self.create_acl_queryset(Instance)
1904

1905
        self.create_fake_get()
1906 1907 1908 1909
        sort = self.request.GET.get("sort")
        # remove "-" that means descending order
        # also check if the column name is valid
        if (sort and
1910
            (sort[1:] if sort[0] == "-" else sort)
1911
                in [i.name for i in Instance._meta.fields] + ["pk"]):
1912
            queryset = queryset.order_by(sort)
1913 1914

        return queryset.filter(
1915
            **self.get_queryset_filters()).prefetch_related(
1916 1917
                "owner", "node", "owner__profile", "interface_set", "lease",
                "interface_set__host").distinct()
1918

1919

1920
class NodeList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
1921 1922 1923
    template_name = "dashboard/node-list.html"
    table_class = NodeListTable
    table_pagination = False
1924

1925 1926 1927 1928 1929
    def get(self, *args, **kwargs):
        if self.request.is_ajax():
            nodes = Node.objects.all()
            nodes = [{
                'name': i.name,
1930 1931
                'icon': i.get_status_icon(),
                'url': i.get_absolute_url(),
1932
                'label': i.get_status_label(),
1933
                'status': i.state.lower()} for i in nodes]
1934 1935

            return HttpResponse(
1936
                json.dumps(list(nodes)),
1937 1938 1939 1940 1941
                content_type="application/json",
            )
        else:
            return super(NodeList, self).get(*args, **kwargs)

1942 1943 1944 1945
    def get_queryset(self):
        return Node.objects.annotate(
            number_of_VMs=Count('instance_set')).select_related('host')

Őry Máté committed
1946

1947
class GroupList(LoginRequiredMixin, SingleTableView):
1948 1949 1950 1951 1952
    template_name = "dashboard/group-list.html"
    model = Group
    table_class = GroupListTable
    table_pagination = False

1953 1954 1955 1956 1957
    def get(self, *args, **kwargs):
        if self.request.is_ajax():
            groups = [{
                'url': reverse("dashboard.views.group-detail",
                               kwargs={'pk': i.pk}),
1958
                'name': i.name} for i in self.get_queryset()]
1959 1960 1961 1962 1963 1964
            return HttpResponse(
                json.dumps(list(groups)),
                content_type="application/json",
            )
        else:
            return super(GroupList, self).get(*args, **kwargs)
1965

1966 1967 1968
    def get_queryset(self):
        logger.debug('GroupList.get_queryset() called. User: %s',
                     unicode(self.request.user))
1969 1970 1971
        profiles = GroupProfile.get_objects_with_level(
            'operator', self.request.user)
        groups = Group.objects.filter(groupprofile__in=profiles)
1972 1973 1974 1975
        s = self.request.GET.get("s")
        if s:
            groups = groups.filter(name__icontains=s)
        return groups
1976

1977

1978
class GroupRemoveUserView(CheckedDetailView, DeleteView):
1979 1980 1981
    model = Group
    slug_field = 'pk'
    slug_url_kwarg = 'group_pk'
1982
    read_level = 'operator'
1983
    member_key = 'member_pk'
1984

1985 1986 1987
    def get_has_level(self):
        return self.object.profile.has_level

1988 1989 1990
    def get_context_data(self, **kwargs):
        context = super(GroupRemoveUserView, self).get_context_data(**kwargs)
        try:
1991
            context['member'] = User.objects.get(pk=self.member_pk)
1992 1993 1994 1995
        except User.DoesNotExist:
            raise Http404()
        return context

1996 1997 1998 1999 2000
    def get_success_url(self):
        next = self.request.POST.get('next')
        if next:
            return next
        else:
2001 2002
            return reverse_lazy("dashboard.views.group-detail",
                                kwargs={'pk': self.get_object().pk})
2003

2004 2005
    def get(self, request, member_pk, *args, **kwargs):
        self.member_pk = member_pk
2006
        return super(GroupRemoveUserView, self).get(request, *args, **kwargs)
2007 2008 2009

    def get_template_names(self):
        if self.request.is_ajax():
2010
            return ['dashboard/confirm/ajax-remove.html']
2011
        else:
2012
            return ['dashboard/confirm/base-remove.html']
2013

2014
    def remove_member(self, pk):
2015
        container = self.get_object()
2016
        container.user_set.remove(User.objects.get(pk=pk))
2017

2018
    def get_success_message(self):
2019
        return _("Member successfully removed from group.")
2020 2021 2022

    def delete(self, request, *args, **kwargs):
        object = self.get_object()
2023 2024
        if not object.profile.has_level(request.user, 'operator'):
            raise PermissionDenied()
2025
        self.remove_member(kwargs[self.member_key])
2026
        success_url = self.get_success_url()
2027
        success_message = self.get_success_message()
2028 2029 2030 2031 2032 2033 2034 2035 2036 2037
        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': success_message}),
                content_type="application/json",
            )
        else:
            messages.success(request, success_message)
            return HttpResponseRedirect(success_url)


2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062
class GroupRemoveFutureUserView(GroupRemoveUserView):

    member_key = 'member_org_id'

    def get(self, request, member_org_id, *args, **kwargs):
        self.member_org_id = member_org_id
        return super(GroupRemoveUserView, self).get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super(GroupRemoveUserView, self).get_context_data(**kwargs)
        try:
            context['member'] = FutureMember.objects.get(
                org_id=self.member_org_id, group=self.get_object())
        except FutureMember.DoesNotExist:
            raise Http404()
        return context

    def remove_member(self, org_id):
        FutureMember.objects.filter(org_id=org_id,
                                    group=self.get_object()).delete()

    def get_success_message(self):
        return _("Future user successfully removed from group.")


2063
class GroupDelete(CheckedDetailView, DeleteView):
2064 2065 2066 2067 2068

    """This stuff deletes the group.
    """
    model = Group
    template_name = "dashboard/confirm/base-delete.html"
2069 2070 2071 2072
    read_level = 'operator'

    def get_has_level(self):
        return self.object.profile.has_level
2073 2074 2075 2076 2077 2078 2079 2080 2081 2082

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/confirm/ajax-delete.html']
        else:
            return ['dashboard/confirm/base-delete.html']

    # github.com/django/django/blob/master/django/views/generic/edit.py#L245
    def delete(self, request, *args, **kwargs):
        object = self.get_object()
2083
        if not object.profile.has_level(request.user, 'owner'):
2084
            raise PermissionDenied()
2085 2086
        object.delete()
        success_url = self.get_success_url()
2087
        success_message = _("Group successfully deleted.")
2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107

        if request.is_ajax():
            if request.POST.get('redirect').lower() == "true":
                messages.success(request, success_message)
            return HttpResponse(
                json.dumps({'message': success_message}),
                content_type="application/json",
            )
        else:
            messages.success(request, success_message)
            return HttpResponseRedirect(success_url)

    def get_success_url(self):
        next = self.request.POST.get('next')
        if next:
            return next
        else:
            return reverse_lazy('dashboard.index')


2108
class VmCreate(LoginRequiredMixin, TemplateView):
2109

2110
    form_class = VmCustomizeForm
2111 2112
    form = None

2113 2114
    def get_template_names(self):
        if self.request.is_ajax():
2115
            return ['dashboard/modal-wrapper.html']
2116
        else:
2117
            return ['dashboard/nojs-wrapper.html']
2118

2119
    def get(self, request, form=None, *args, **kwargs):
2120 2121 2122
        if not request.user.has_perm('vm.create_vm'):
            raise PermissionDenied()

2123
        form_error = form is not None
2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134
        template_pk = (form.template.pk if form_error
                       else request.GET.get("template"))
        if template_pk:
            template = get_object_or_404(InstanceTemplate, pk=template_pk)
            if not template.has_level(request.user, 'user'):
                raise PermissionDenied()
            if form is None:
                form = self.form_class(user=request.user, template=template)
        else:
            templates = InstanceTemplate.get_objects_with_level(
                'user', request.user, disregard_superuser=True)
2135

2136
        context = self.get_context_data(**kwargs)
2137
        if template_pk:
2138 2139 2140
            context.update({
                'template': 'dashboard/_vm-create-2.html',
                'box_title': _('Customize VM'),
2141
                'ajax_title': True,
2142
                'vm_create_form': form,
2143
                'template_o': template,
2144 2145 2146 2147 2148
            })
        else:
            context.update({
                'template': 'dashboard/_vm-create-1.html',
                'box_title': _('Create a VM'),
2149
                'ajax_title': True,
2150 2151
                'templates': templates.all(),
            })
2152
        return self.render_to_response(context)
2153

2154 2155 2156 2157 2158 2159 2160 2161 2162
    def __create_normal(self, request, *args, **kwargs):
        user = request.user
        template = InstanceTemplate.objects.get(
            pk=request.POST.get("template"))

        # permission check
        if not template.has_level(request.user, 'user'):
            raise PermissionDenied()

Őry Máté committed
2163 2164
        args = {"template": template, "owner": user}
        instances = [Instance.create_from_template(**args)]
2165
        return self.__deploy(request, instances)
2166 2167 2168

    def __create_customized(self, request, *args, **kwargs):
        user = request.user
2169 2170 2171
        # no form yet, using POST directly:
        template = get_object_or_404(InstanceTemplate,
                                     pk=request.POST.get("template"))
2172
        form = self.form_class(
2173
            request.POST, user=request.user, template=template)
2174 2175 2176 2177
        if not form.is_valid():
            return self.get(request, form, *args, **kwargs)
        post = form.cleaned_data

2178
        if not template.has_level(user, 'user'):
2179
            raise PermissionDenied()
2180

2181 2182 2183 2184 2185 2186
        ikwargs = {
            'name': post['name'],
            'template': template,
            'owner': user,
        }
        amount = post.get("amount", 1)
2187
        if request.user.has_perm('vm.set_resources'):
2188 2189
            networks = [InterfaceTemplate(vlan=l, managed=l.managed)
                        for l in post['networks']]
2190 2191

            ikwargs.update({
2192 2193 2194 2195
                'num_cores': post['cpu_count'],
                'ram_size': post['ram_size'],
                'priority': post['cpu_priority'],
                'max_ram_size': post['ram_size'],
2196
                'networks': networks,
Bach Dániel committed
2197
                'disks': list(template.disks.all()),
2198 2199
            })

2200
        else:
2201 2202 2203 2204 2205
            pass

        instances = Instance.mass_create_from_template(amount=amount,
                                                       **ikwargs)
        return self.__deploy(request, instances)
2206

2207 2208
    def __deploy(self, request, instances, *args, **kwargs):
        for i in instances:
Dudás Ádám committed
2209
            i.deploy.async(user=request.user)
2210 2211

        if len(instances) > 1:
2212
            messages.success(request, ungettext_lazy(
2213 2214 2215
                "Successfully created %(count)d VM.",  # this should not happen
                "Successfully created %(count)d VMs.", len(instances)) % {
                'count': len(instances)})
2216
            path = "%s?stype=owned" % reverse("dashboard.views.vm-list")
2217
        else:
2218
            messages.success(request, _("VM successfully created."))
2219 2220
            path = instances[0].get_absolute_url()

2221
        if request.is_ajax():
2222 2223
            return HttpResponse(json.dumps({'redirect': path}),
                                content_type="application/json")
2224
        else:
2225 2226 2227 2228 2229
            return redirect("%s#activity" % path)

    def post(self, request, *args, **kwargs):
        user = request.user

2230 2231 2232
        if not request.user.has_perm('vm.create_vm'):
            raise PermissionDenied()

2233 2234 2235 2236 2237 2238
        # limit chekcs
        try:
            limit = user.profile.instance_limit
        except Exception as e:
            logger.debug('No profile or instance limit: %s', e)
        else:
Őry Máté committed
2239 2240 2241 2242
            try:
                amount = int(request.POST.get("amount", 1))
            except:
                amount = limit  # TODO this should definitely use a Form
2243 2244
            current = Instance.active.filter(owner=user).count()
            logger.debug('current use: %d, limit: %d', current, limit)
Őry Máté committed
2245
            if current + amount > limit:
2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258
                messages.error(request,
                               _('Instance limit (%d) exceeded.') % limit)
                if request.is_ajax():
                    return HttpResponse(json.dumps({'redirect': '/'}),
                                        content_type="application/json")
                else:
                    return redirect('/')

        create_func = (self.__create_normal if
                       request.POST.get("customized") is None else
                       self.__create_customized)

        return create_func(request, *args, **kwargs)
Kálmán Viktor committed
2259

2260

2261
class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
2262

2263 2264 2265 2266 2267 2268
    form_class = HostForm
    hostform = None

    formset_class = inlineformset_factory(Host, Node, form=NodeForm, extra=1)
    formset = None

2269 2270 2271 2272 2273 2274
    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/modal-wrapper.html']
        else:
            return ['dashboard/nojs-wrapper.html']

2275 2276 2277 2278 2279
    def get(self, request, hostform=None, formset=None, *args, **kwargs):
        if hostform is None:
            hostform = self.form_class()
        if formset is None:
            formset = self.formset_class(instance=Host())
2280 2281 2282
        context = self.get_context_data(**kwargs)
        context.update({
            'template': 'dashboard/node-create.html',
2283 2284 2285 2286
            'box_title': 'Create a Node',
            'hostform': hostform,
            'formset': formset,

2287 2288 2289 2290 2291
        })
        return self.render_to_response(context)

    # TODO handle not ajax posts
    def post(self, request, *args, **kwargs):
2292 2293
        if not self.request.user.is_authenticated():
            raise PermissionDenied()
2294

2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306
        hostform = self.form_class(request.POST)
        formset = self.formset_class(request.POST, Host())
        if not hostform.is_valid():
            return self.get(request, hostform, formset, *args, **kwargs)
        hostform.setowner(request.user)
        savedform = hostform.save(commit=False)
        formset = self.formset_class(request.POST, instance=savedform)
        if not formset.is_valid():
            return self.get(request, hostform, formset, *args, **kwargs)

        savedform.save()
        nodemodel = formset.save()
2307
        messages.success(request, _('Node successfully created.'))
2308
        path = nodemodel[0].get_absolute_url()
2309
        if request.is_ajax():
2310 2311
            return HttpResponse(json.dumps({'redirect': path}),
                                content_type="application/json")
2312
        else:
2313
            return redirect(path)
2314 2315


2316
class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
2317 2318 2319 2320 2321 2322 2323 2324 2325 2326

    form_class = GroupCreateForm

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/modal-wrapper.html']
        else:
            return ['dashboard/nojs-wrapper.html']

    def get(self, request, form=None, *args, **kwargs):
2327 2328
        if not request.user.has_module_perms('auth'):
            raise PermissionDenied()
2329
        if form is None:
2330 2331
            form = self.form_class(
                new_groups=self.get_available_group_codes(request))
2332 2333 2334
        context = self.get_context_data(**kwargs)
        context.update({
            'template': 'dashboard/group-create.html',
2335
            'box_title': _('Create a Group'),
2336
            'form': form,
2337
            'ajax_title': True,
2338 2339 2340 2341
        })
        return self.render_to_response(context)

    def post(self, request, *args, **kwargs):
2342 2343
        if not request.user.has_module_perms('auth'):
            raise PermissionDenied()
2344 2345
        form = self.form_class(
            request.POST, new_groups=self.get_available_group_codes(request))
2346 2347 2348 2349
        if not form.is_valid():
            return self.get(request, form, *args, **kwargs)
        form.cleaned_data
        savedform = form.save()
2350
        savedform.profile.set_level(request.user, 'owner')
2351
        messages.success(request, _('Group successfully created.'))
2352 2353 2354 2355 2356 2357 2358 2359
        if request.is_ajax():
            return HttpResponse(json.dumps({'redirect':
                                savedform.profile.get_absolute_url()}),
                                content_type="application/json")
        else:
            return redirect(savedform.profile.get_absolute_url())


2360 2361
class GroupProfileUpdate(SuccessMessageMixin, GroupCodeMixin,
                         LoginRequiredMixin, UpdateView):
2362 2363 2364

    form_class = GroupProfileUpdateForm
    model = Group
2365
    success_message = _('Group is successfully updated.')
2366 2367 2368 2369 2370

    @classmethod
    def get_available_group_codes(cls, request, extra=None):
        result = super(GroupProfileUpdate, cls).get_available_group_codes(
            request)
2371
        if extra and extra not in result:
2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387
            result += [extra]
        return result

    def get_object(self):
        group = super(GroupProfileUpdate, self).get_object()
        profile = group.profile
        if not profile.has_level(self.request.user, 'owner'):
            raise PermissionDenied
        else:
            return profile

    @classmethod
    def get_form_object(cls, request, instance, *args, **kwargs):
        kwargs['instance'] = instance
        kwargs['new_groups'] = cls.get_available_group_codes(
            request, instance.org_id)
2388
        kwargs['superuser'] = request.user.is_superuser
2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403
        return cls.form_class(*args, **kwargs)

    def get(self, request, form=None, *args, **kwargs):
        self.object = self.get_object()
        if form is None:
            form = self.get_form_object(request, self.object)
        return super(GroupProfileUpdate, self).get(
            request, form, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        if not request.user.has_module_perms('auth'):
            raise PermissionDenied()
        self.object = self.get_object()
        form = self.get_form_object(request, self.object, self.request.POST)
        if not form.is_valid():
2404
            return self.form_invalid(form)
2405
        form.save()
2406
        return self.form_valid(form)
2407 2408


2409
class NodeDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427

    """This stuff deletes the node.
    """
    model = Node
    template_name = "dashboard/confirm/base-delete.html"

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/confirm/ajax-delete.html']
        else:
            return ['dashboard/confirm/base-delete.html']

    # github.com/django/django/blob/master/django/views/generic/edit.py#L245
    def delete(self, request, *args, **kwargs):
        object = self.get_object()

        object.delete()
        success_url = self.get_success_url()
2428
        success_message = _("Node successfully deleted.")
2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446

        if request.is_ajax():
            if request.POST.get('redirect').lower() == "true":
                messages.success(request, success_message)
            return HttpResponse(
                json.dumps({'message': success_message}),
                content_type="application/json",
            )
        else:
            messages.success(request, success_message)
            return HttpResponseRedirect(success_url)

    def get_success_url(self):
        next = self.request.POST.get('next')
        if next:
            return next
        else:
            return reverse_lazy('dashboard.index')
2447

2448

2449 2450 2451 2452 2453 2454 2455 2456 2457
class NodeAddTraitView(SuperuserRequiredMixin, DetailView):
    model = Node
    template_name = "dashboard/node-add-trait.html"

    def get_success_url(self):
        next = self.request.GET.get('next')
        if next:
            return next
        else:
2458
            return self.object.get_absolute_url()
2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481

    def get_context_data(self, **kwargs):
        self.object = self.get_object()
        context = super(NodeAddTraitView, self).get_context_data(**kwargs)
        context['form'] = (TraitForm(self.request.POST) if self.request.POST
                           else TraitForm())
        return context

    def post(self, request, pk, *args, **kwargs):
        context = self.get_context_data(**kwargs)
        form = context['form']
        if form.is_valid():
            node = self.object
            n = form.cleaned_data['name']
            trait, created = Trait.objects.get_or_create(name=n)
            node.traits.add(trait)
            success_message = _("Trait successfully added to node.")
            messages.success(request, success_message)
            return redirect(self.get_success_url())
        else:
            return self.get(self, request, pk, *args, **kwargs)


2482 2483 2484 2485
class NodeStatus(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
    template_name = "dashboard/confirm/node-status.html"
    model = Node

2486 2487 2488 2489 2490 2491
    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/confirm/ajax-node-status.html']
        else:
            return ['dashboard/confirm/node-status.html']

2492 2493 2494 2495 2496 2497 2498 2499 2500 2501
    def get_success_url(self):
        next = self.request.GET.get('next')
        if next:
            return next
        else:
            return reverse_lazy("dashboard.views.node-detail",
                                kwargs={'pk': self.object.pk})

    def get_context_data(self, **kwargs):
        context = super(NodeStatus, self).get_context_data(**kwargs)
2502 2503 2504 2505
        if self.object.enabled:
            context['status'] = "disable"
        else:
            context['status'] = "enable"
2506 2507 2508
        return context

    def post(self, request, *args, **kwargs):
2509
        if request.POST.get('change_status') is not None:
2510
            return self.__set_status(request)
2511 2512
        return redirect(reverse_lazy("dashboard.views.node-detail",
                                     kwargs={'pk': self.get_object().pk}))
2513 2514 2515

    def __set_status(self, request):
        self.object = self.get_object()
2516
        if not self.object.enabled:
2517
            self.object.enable(user=request.user)
2518
        else:
2519
            self.object.disable(user=request.user)
2520
        success_message = _("Node successfully changed status.")
2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535

        if request.is_ajax():
            response = {
                'message': success_message,
                'node_pk': self.object.pk
            }
            return HttpResponse(
                json.dumps(response),
                content_type="application/json"
            )
        else:
            messages.success(request, success_message)
            return redirect(self.get_success_url())


2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565
class NodeFlushView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
    template_name = "dashboard/confirm/node-flush.html"
    model = Node

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/confirm/ajax-node-flush.html']
        else:
            return ['dashboard/confirm/node-flush.html']

    def get_success_url(self):
        next = self.request.GET.get('next')
        if next:
            return next
        else:
            return reverse_lazy("dashboard.views.node-detail",
                                kwargs={'pk': self.object.pk})

    def get_context_data(self, **kwargs):
        context = super(NodeFlushView, self).get_context_data(**kwargs)
        return context

    def post(self, request, *args, **kwargs):
        if request.POST.get('flush') is not None:
            return self.__flush(request)
        return redirect(reverse_lazy("dashboard.views.node-detail",
                                     kwargs={'pk': self.get_object().pk}))

    def __flush(self, request):
        self.object = self.get_object()
2566
        self.object.flush.async(user=request.user)
2567
        success_message = _("Node successfully flushed.")
2568 2569
        messages.success(request, success_message)
        return redirect(self.get_success_url())
2570 2571


2572
class PortDelete(LoginRequiredMixin, DeleteView):
2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601
    model = Rule
    pk_url_kwarg = 'rule'

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/confirm/ajax-delete.html']
        else:
            return ['dashboard/confirm/base-delete.html']

    def get_context_data(self, **kwargs):
        context = super(PortDelete, self).get_context_data(**kwargs)
        rule = kwargs.get('object')
        instance = rule.host.interface_set.get().instance
        context['title'] = _("Port delete confirmation")
        context['text'] = _("Are you sure you want to close %(port)d/"
                            "%(proto)s on %(vm)s?" % {'port': rule.dport,
                                                      'proto': rule.proto,
                                                      'vm': instance})
        return context

    def delete(self, request, *args, **kwargs):
        rule = Rule.objects.get(pk=kwargs.get("rule"))
        instance = rule.host.interface_set.get().instance
        if not instance.has_level(request.user, 'owner'):
            raise PermissionDenied()

        super(PortDelete, self).delete(request, *args, **kwargs)

        success_url = self.get_success_url()
2602
        success_message = _("Port successfully removed.")
2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': success_message}),
                content_type="application/json",
            )
        else:
            messages.success(request, success_message)
            return HttpResponseRedirect("%s#network" % success_url)

    def get_success_url(self):
        return reverse_lazy('dashboard.views.detail',
                            kwargs={'pk': self.kwargs.get("pk")})


2618
class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin,
2619
                  SuccessMessageMixin, CreateView):
2620 2621
    model = Lease
    form_class = LeaseForm
2622
    permission_required = 'vm.create_leases'
2623
    template_name = "dashboard/lease-create.html"
2624
    success_message = _("Successfully created a new lease.")
2625 2626 2627 2628 2629

    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-list")


2630 2631 2632 2633
class LeaseAclUpdateView(AclUpdateView):
    model = Lease


2634 2635
class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin,
                  SuccessMessageMixin, UpdateView):
2636 2637 2638
    model = Lease
    form_class = LeaseForm
    template_name = "dashboard/lease-edit.html"
2639
    success_message = _("Successfully modified lease.")
2640

2641 2642 2643
    def get_context_data(self, *args, **kwargs):
        obj = self.get_object()
        context = super(LeaseDetail, self).get_context_data(*args, **kwargs)
2644 2645
        context['acl'] = AclUpdateView.get_acl_data(
            obj, self.request.user, 'dashboard.views.lease-acl')
2646 2647
        return context

2648 2649 2650 2651
    def get_success_url(self):
        return reverse_lazy("dashboard.views.lease-detail", kwargs=self.kwargs)


2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663
class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
    model = Lease

    def get_success_url(self):
        return reverse("dashboard.views.template-list")

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/confirm/ajax-delete.html']
        else:
            return ['dashboard/confirm/base-delete.html']

2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677
    def get_context_data(self, *args, **kwargs):
        c = super(LeaseDelete, self).get_context_data(*args, **kwargs)
        lease = self.get_object()
        templates = lease.instancetemplate_set
        if templates.count() > 0:
            text = _("You can't delete this lease because some templates "
                     "are still using it, modify these to proceed: ")

            c['text'] = text + ", ".join("<strong>%s (#%d)</strong>"
                                         "" % (o.name, o.pk)
                                         for o in templates.all())
            c['disable_submit'] = True
        return c

2678 2679 2680
    def delete(self, request, *args, **kwargs):
        object = self.get_object()

2681 2682 2683
        if (object.instancetemplate_set.count() > 0):
            raise SuspiciousOperation()

2684 2685
        object.delete()
        success_url = self.get_success_url()
2686
        success_message = _("Lease successfully deleted.")
2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': success_message}),
                content_type="application/json",
            )
        else:
            messages.success(request, success_message)
            return HttpResponseRedirect(success_url)


2698
@require_GET
2699
def vm_activity(request, pk):
2700
    instance = Instance.objects.get(pk=pk)
2701
    if not instance.has_level(request.user, 'user'):
2702 2703
        raise PermissionDenied()

2704
    response = {}
2705
    show_all = request.GET.get("show_all", "false") == "true"
2706 2707
    activities = _format_activities(
        instance.get_merged_activities(request.user))
2708 2709 2710
    show_show_all = len(activities) > 10
    if not show_all:
        activities = activities[:10]
2711

2712
    response['connect_uri'] = instance.get_connect_uri()
2713 2714
    response['human_readable_status'] = instance.get_status_display()
    response['status'] = instance.status
2715
    response['icon'] = instance.get_status_icon()
2716
    latest = instance.get_latest_activity_in_progress()
2717 2718
    response['is_new_state'] = (latest and latest.resultant_state is not None
                                and instance.status != latest.resultant_state)
2719

2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738
    context = {
        'instance': instance,
        'activities': activities,
        'show_show_all': show_show_all,
        'ops': get_operations(instance, request.user),
    }

    response['activities'] = render_to_string(
        "dashboard/vm-detail/_activity-timeline.html",
        RequestContext(request, context),
    )
    response['ops'] = render_to_string(
        "dashboard/vm-detail/_operations.html",
        RequestContext(request, context),
    )
    response['disk_ops'] = render_to_string(
        "dashboard/vm-detail/_disk-operations.html",
        RequestContext(request, context),
    )
2739 2740 2741 2742 2743

    return HttpResponse(
        json.dumps(response),
        content_type="application/json"
    )
2744 2745


2746 2747 2748 2749 2750
class FavouriteView(TemplateView):

    def post(self, *args, **kwargs):
        user = self.request.user
        vm = Instance.objects.get(pk=self.request.POST.get("vm"))
2751 2752
        if not vm.has_level(user, 'user'):
            raise PermissionDenied()
2753 2754
        try:
            Favourite.objects.get(instance=vm, user=user).delete()
2755
            return HttpResponse("Deleted.")
2756 2757
        except Favourite.DoesNotExist:
            Favourite(instance=vm, user=user).save()
2758
            return HttpResponse("Added.")
2759 2760


2761 2762
class TransferOwnershipView(LoginRequiredMixin, DetailView):
    model = Instance
2763
    template_name = 'dashboard/vm-detail/tx-owner.html'
2764 2765 2766

    def post(self, request, *args, **kwargs):
        try:
2767
            new_owner = search_user(request.POST['name'])
2768 2769 2770
        except User.DoesNotExist:
            messages.error(request, _('Can not find specified user.'))
            return self.get(request, *args, **kwargs)
2771 2772 2773 2774 2775 2776 2777 2778 2779 2780
        except KeyError:
            raise SuspiciousOperation()

        obj = self.get_object()
        if not (obj.owner == request.user or
                request.user.is_superuser):
            raise PermissionDenied()

        token = signing.dumps((obj.pk, new_owner.pk),
                              salt=TransferOwnershipConfirmView.get_salt())
2781 2782 2783 2784
        token_path = reverse(
            'dashboard.views.vm-transfer-ownership-confirm', args=[token])
        try:
            new_owner.profile.notify(
2785 2786 2787 2788 2789
                ugettext_noop('Ownership offer'),
                ugettext_noop('%(user)s offered you to take the ownership of '
                              'his/her virtual machine called %(instance)s. '
                              '<a href="%(token)s" '
                              'class="btn btn-success btn-small">Accept</a>'),
2790 2791 2792 2793 2794 2795 2796 2797 2798 2799
                {'instance': obj, 'token': token_path})
        except Profile.DoesNotExist:
            messages.error(request, _('Can not notify selected user.'))
        else:
            messages.success(request,
                             _('User %s is notified about the offer.') % (
                                 unicode(new_owner), ))

        return redirect(reverse_lazy("dashboard.views.detail",
                                     kwargs={'pk': obj.pk}))
2800 2801


2802
class TransferOwnershipConfirmView(LoginRequiredMixin, View):
2803 2804
    """User can accept an ownership offer."""

2805
    max_age = 3 * 24 * 3600
2806
    success_message = _("Ownership successfully transferred to you.")
2807 2808 2809 2810 2811

    @classmethod
    def get_salt(cls):
        return unicode(cls)

2812
    def get(self, request, key, *args, **kwargs):
2813 2814
        """Confirm ownership transfer based on token.
        """
2815
        logger.debug('Confirm dialog for token %s.', key)
2816 2817
        try:
            instance, new_owner = self.get_instance(key, request.user)
2818
        except PermissionDenied:
2819 2820 2821 2822 2823 2824 2825 2826 2827
            messages.error(request, _('This token is for an other user.'))
            raise
        except SuspiciousOperation:
            messages.error(request, _('This token is invalid or has expired.'))
            raise PermissionDenied()
        return render(request,
                      "dashboard/confirm/base-transfer-ownership.html",
                      dictionary={'instance': instance, 'key': key})

2828
    def post(self, request, key, *args, **kwargs):
2829 2830
        """Really transfer ownership based on token.
        """
2831
        instance, owner = self.get_instance(key, request.user)
2832 2833 2834 2835 2836 2837 2838 2839 2840 2841

        old = instance.owner
        with instance_activity(code_suffix='ownership-transferred',
                               instance=instance, user=request.user):
            instance.owner = request.user
            instance.clean()
            instance.save()
        messages.success(request, self.success_message)
        logger.info('Ownership of %s transferred from %s to %s.',
                    unicode(instance), unicode(old), unicode(request.user))
2842 2843
        if old.profile:
            old.profile.notify(
2844 2845 2846
                ugettext_noop('Ownership accepted'),
                ugettext_noop('Your ownership offer of %(instance)s has been '
                              'accepted by %(user)s.'),
2847
                {'instance': instance})
2848 2849 2850 2851 2852 2853 2854 2855 2856
        return HttpResponseRedirect(instance.get_absolute_url())

    def get_instance(self, key, user):
        """Get object based on signed token.
        """
        try:
            instance, new_owner = (
                signing.loads(key, max_age=self.max_age,
                              salt=self.get_salt()))
2857
        except (signing.BadSignature, ValueError, TypeError) as e:
2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874
            logger.error('Tried invalid token. Token: %s, user: %s. %s',
                         key, unicode(user), unicode(e))
            raise SuspiciousOperation()

        try:
            instance = Instance.objects.get(id=instance)
        except Instance.DoesNotExist as e:
            logger.error('Tried token to nonexistent instance %d. '
                         'Token: %s, user: %s. %s',
                         instance, key, unicode(user), unicode(e))
            raise Http404()

        if new_owner != user.pk:
            logger.error('%s (%d) tried the token for %s. Token: %s.',
                         unicode(user), user.pk, new_owner, key)
            raise PermissionDenied()
        return (instance, new_owner)
2875 2876


2877 2878
class GraphViewBase(LoginRequiredMixin, View):
    def get(self, request, pk, metric, time, *args, **kwargs):
Bach Dániel committed
2879
        graphite_url = settings.GRAPHITE_URL
2880
        if graphite_url is None:
2881
            raise Http404()
2882

2883
        if metric not in self.metrics.keys():
2884
            raise SuspiciousOperation()
2885

2886
        try:
2887 2888
            instance = self.get_object(request, pk)
        except self.model.DoesNotExist:
2889 2890
            raise Http404()

2891
        prefix = self.get_prefix(instance)
2892
        target = self.metrics[metric] % {'prefix': prefix}
2893
        title = self.get_title(instance, metric)
2894 2895 2896 2897 2898
        params = {'target': target,
                  'from': '-%s' % time,
                  'title': title.encode('UTF-8'),
                  'width': '500',
                  'height': '200'}
2899 2900
        logger.debug('%s %s', graphite_url, params)
        response = requests.get('%s/render/' % graphite_url, params=params)
2901
        return HttpResponse(response.content, mimetype="image/png")
2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916

    def get_prefix(self, instance):
        raise NotImplementedError("Subclass must implement abstract method")

    def get_title(self, instance, metric):
        raise NotImplementedError("Subclass must implement abstract method")

    def get_object(self, request, pk):
        instance = self.model.objects.get(id=pk)
        if not instance.has_level(request.user, 'user'):
            raise PermissionDenied()
        return instance


class VmGraphView(GraphViewBase):
2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928
    metrics = {
        'cpu': ('cactiStyle(alias(nonNegativeDerivative(%(prefix)s.cpu.usage),'
                '"cpu usage (%%)"))'),
        'memory': ('cactiStyle(alias(%(prefix)s.memory.usage,'
                   '"memory usage (%%)"))'),
        'network': (
            'group('
            'aliasSub(nonNegativeDerivative(%(prefix)s.network.bytes_recv*),'
            ' ".*-(\d+)\\)", "out (vlan \\1)"),'
            'aliasSub(nonNegativeDerivative(%(prefix)s.network.bytes_sent*),'
            ' ".*-(\d+)\\)", "in (vlan \\1)"))'),
    }
2929 2930 2931 2932 2933 2934 2935
    model = Instance

    def get_prefix(self, instance):
        return 'vm.%s' % instance.vm_name

    def get_title(self, instance, metric):
        return '%s (%s) - %s' % (instance.name, instance.vm_name, metric)
2936 2937 2938


class NodeGraphView(SuperuserRequiredMixin, GraphViewBase):
2939
    metrics = {
2940
        'cpu': ('cactiStyle(alias(nonNegativeDerivative(%(prefix)s.cpu.times),'
2941
                '"cpu usage (%%)"))'),
2942
        'memory': ('cactiStyle(alias(%(prefix)s.memory.usage,'
2943 2944
                   '"memory usage (%%)"))'),
        'network': ('cactiStyle(aliasByMetric('
2945
                    'nonNegativeDerivative(%(prefix)s.network.bytes_*)))'),
2946
    }
2947 2948 2949
    model = Node

    def get_prefix(self, instance):
2950
        return 'circle.%s' % instance.host.hostname
2951 2952 2953 2954 2955 2956

    def get_title(self, instance, metric):
        return '%s - %s' % (instance.name, metric)

    def get_object(self, request, pk):
        return self.model.objects.get(id=pk)
2957 2958 2959


class NotificationView(LoginRequiredMixin, TemplateView):
2960 2961 2962 2963 2964 2965

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/_notifications-timeline.html']
        else:
            return ['dashboard/notifications.html']
2966 2967 2968 2969

    def get_context_data(self, *args, **kwargs):
        context = super(NotificationView, self).get_context_data(
            *args, **kwargs)
2970
        n = 10 if self.request.is_ajax() else 1000
2971
        context['notifications'] = list(
2972
            self.request.user.notification_set.all()[:n])
2973 2974 2975 2976 2977 2978 2979 2980 2981
        return context

    def get(self, *args, **kwargs):
        response = super(NotificationView, self).get(*args, **kwargs)
        un = self.request.user.notification_set.filter(status="new")
        for u in un:
            u.status = "read"
            u.save()
        return response
2982 2983


2984 2985 2986
def circle_login(request):
    authentication_form = CircleAuthenticationForm
    extra_context = {
2987
        'saml2': saml_available,
2988
    }
2989 2990 2991 2992
    response = login(request, authentication_form=authentication_form,
                     extra_context=extra_context)
    set_language_cookie(request, response)
    return response
2993 2994


2995 2996
class MyPreferencesView(UpdateView):
    model = Profile
2997 2998 2999 3000 3001 3002 3003 3004 3005

    def get_context_data(self, *args, **kwargs):
        context = super(MyPreferencesView, self).get_context_data(*args,
                                                                  **kwargs)
        context['forms'] = {
            'change_password': CirclePasswordChangeForm(
                user=self.request.user),
            'change_language': MyProfileForm(instance=self.get_object()),
        }
3006
        key_table = UserKeyListTable(
3007 3008
            UserKey.objects.filter(user=self.request.user),
            request=self.request)
3009 3010 3011 3012 3013 3014 3015
        key_table.page = None
        context['userkey_table'] = key_table
        cmd_table = ConnectCommandListTable(
            self.request.user.command_set.all(),
            request=self.request)
        cmd_table.page = None
        context['connectcommand_table'] = cmd_table
3016
        return context
3017 3018 3019 3020 3021 3022 3023 3024 3025

    def get_object(self, queryset=None):
        if self.request.user.is_anonymous():
            raise PermissionDenied()
        try:
            return self.request.user.profile
        except Profile.DoesNotExist:
            raise Http404(_("You don't have a profile."))

3026 3027 3028
    def post(self, request, *args, **kwargs):
        self.ojbect = self.get_object()
        redirect_response = HttpResponseRedirect(
3029
            reverse("dashboard.views.profile-preferences"))
3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055
        if "preferred_language" in request.POST:
            form = MyProfileForm(request.POST, instance=self.get_object())
            if form.is_valid():
                lang = form.cleaned_data.get("preferred_language")
                set_language_cookie(self.request, redirect_response, lang)
                form.save()
        else:
            form = CirclePasswordChangeForm(user=request.user,
                                            data=request.POST)
            if form.is_valid():
                form.save()

        if form.is_valid():
            return redirect_response
        else:
            return self.get(request, form=form, *args, **kwargs)

    def get(self, request, form=None, *args, **kwargs):
        # if this is not here, it won't work
        self.object = self.get_object()
        context = self.get_context_data(*args, **kwargs)
        if form is not None:
            # a little cheating, users can't post invalid
            # language selection forms (without modifying the HTML)
            context['forms']['change_password'] = form
        return self.render_to_response(context)
3056 3057


3058 3059 3060 3061 3062 3063
class UnsubscribeFormView(SuccessMessageMixin, UpdateView):
    model = Profile
    form_class = UnsubscribeForm
    template_name = "dashboard/unsubscribe.html"
    success_message = _("Successfully modified subscription.")

3064 3065 3066 3067 3068 3069
    def get_success_url(self):
        if self.request.user.is_authenticated():
            return super(UnsubscribeFormView, self).get_success_url()
        else:
            return self.request.path

3070 3071 3072 3073 3074 3075
    @classmethod
    def get_salt(cls):
        return unicode(cls)

    @classmethod
    def get_token(cls, user):
3076
        return signing.dumps(user.pk, salt=cls.get_salt(), compress=True)
3077 3078

    def get_object(self, queryset=None):
3079 3080
        key = self.kwargs['token']
        try:
3081
            pk = signing.loads(key, salt=self.get_salt(), max_age=48 * 3600)
3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096
        except signing.SignatureExpired:
            raise
        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 Http404
        else:
            return (queryset or self.get_queryset()).get(user_id=pk)

    def dispatch(self, request, *args, **kwargs):
        try:
            return super(UnsubscribeFormView, self).dispatch(
                request, *args, **kwargs)
        except signing.SignatureExpired:
            return redirect('dashboard.views.profile-preferences')
3097 3098


3099 3100 3101 3102 3103 3104 3105 3106 3107
def set_language_cookie(request, response, lang=None):
    if lang is None:
        try:
            lang = request.user.profile.preferred_language
        except:
            return

    cname = getattr(settings, 'LANGUAGE_COOKIE_NAME', 'django_language')
    response.set_cookie(cname, lang, 365 * 86400)
3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134


class DiskRemoveView(DeleteView):
    model = Disk

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/confirm/ajax-delete.html']
        else:
            return ['dashboard/confirm/base-delete.html']

    def get_context_data(self, **kwargs):
        context = super(DiskRemoveView, self).get_context_data(**kwargs)
        disk = self.get_object()
        app = disk.get_appliance()
        context['title'] = _("Disk remove confirmation")
        context['text'] = _("Are you sure you want to remove "
                            "<strong>%(disk)s</strong> from "
                            "<strong>%(app)s</strong>?" % {'disk': disk,
                                                           'app': app}
                            )
        return context

    def delete(self, request, *args, **kwargs):
        disk = self.get_object()
        app = disk.get_appliance()

Bach Dániel committed
3135 3136 3137
        if not app.has_level(request.user, 'owner'):
            raise PermissionDenied()

3138
        app.remove_disk(disk=disk, user=request.user)
3139 3140 3141 3142
        disk.destroy()

        next_url = request.POST.get("next")
        success_url = next_url if next_url else app.get_absolute_url()
3143
        success_message = _("Disk successfully removed.")
3144 3145 3146 3147 3148 3149 3150 3151 3152

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': success_message}),
                content_type="application/json",
            )
        else:
            messages.success(request, success_message)
            return HttpResponseRedirect("%s#resources" % success_url)
3153 3154 3155 3156 3157


@require_GET
def get_disk_download_status(request, pk):
    disk = Disk.objects.get(pk=pk)
Bach Dániel committed
3158
    if not disk.get_appliance().has_level(request.user, 'owner'):
3159 3160 3161 3162 3163 3164 3165 3166 3167
        raise PermissionDenied()

    return HttpResponse(
        json.dumps({
            'percentage': disk.get_download_percentage(),
            'failed': disk.failed
        }),
        content_type="application/json",
    )
3168 3169


3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183
def _get_activity_icon(act):
    op = act.get_operation()
    if op and op.id in vm_ops:
        return vm_ops[op.id].icon
    else:
        return "cog"


def _format_activities(acts):
    for i in acts:
        i.icon = _get_activity_icon(i)
    return acts


3184
class InstanceActivityDetail(CheckedDetailView):
3185
    model = InstanceActivity
3186
    context_object_name = 'instanceactivity'  # much simpler to mock object
3187 3188
    template_name = 'dashboard/instanceactivity_detail.html'

3189 3190 3191
    def get_has_level(self):
        return self.object.instance.has_level

3192 3193
    def get_context_data(self, **kwargs):
        ctx = super(InstanceActivityDetail, self).get_context_data(**kwargs)
3194 3195 3196
        ctx['activities'] = _format_activities(
            self.object.instance.get_activities(self.request.user))
        ctx['icon'] = _get_activity_icon(self.object)
3197
        return ctx
3198 3199


3200 3201
class UserCreationView(LoginRequiredMixin, PermissionRequiredMixin,
                       CreateView):
3202 3203 3204
    form_class = UserCreationForm
    model = User
    template_name = 'dashboard/user-create.html'
3205
    permission_required = "auth.add_user"
3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227

    def get_group(self, group_pk):
        self.group = get_object_or_404(Group, pk=group_pk)
        if not self.group.profile.has_level(self.request.user, 'owner'):
            raise PermissionDenied()

    def get(self, *args, **kwargs):
        self.get_group(kwargs.pop('group_pk'))
        return super(UserCreationView, self).get(*args, **kwargs)

    def post(self, *args, **kwargs):
        group_pk = kwargs.pop('group_pk')
        self.get_group(group_pk)
        ret = super(UserCreationView, self).post(*args, **kwargs)
        if self.object:
            self.object.groups.add(self.group)
            return redirect(
                reverse('dashboard.views.group-detail', args=[group_pk]))
        else:
            return ret


3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246
class InterfaceDeleteView(DeleteView):
    model = Interface

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/confirm/ajax-delete.html']
        else:
            return ['dashboard/confirm/base-delete.html']

    def get_context_data(self, **kwargs):
        context = super(InterfaceDeleteView, self).get_context_data(**kwargs)
        interface = self.get_object()
        context['text'] = _("Are you sure you want to remove this interface "
                            "from <strong>%(vm)s</strong>?" %
                            {'vm': interface.instance.name})
        return context

    def delete(self, request, *args, **kwargs):
        self.object = self.get_object()
3247
        instance = self.object.instance
3248

3249
        if not instance.has_level(request.user, "owner"):
3250 3251
            raise PermissionDenied()

3252
        instance.remove_interface(interface=self.object, user=request.user)
3253
        success_url = self.get_success_url()
3254
        success_message = _("Interface successfully deleted.")
3255 3256 3257

        if request.is_ajax():
            return HttpResponse(
3258 3259 3260 3261 3262 3263 3264
                json.dumps(
                    {'message': success_message,
                     'removed_network': {
                         'vlan': self.object.vlan.name,
                         'vlan_pk': self.object.vlan.pk,
                         'managed': self.object.host is not None,
                     }}),
3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275
                content_type="application/json",
            )
        else:
            messages.success(request, success_message)
            return HttpResponseRedirect("%s#network" % success_url)

    def get_success_url(self):
        redirect = self.request.POST.get("next")
        if redirect:
            return redirect
        self.object.instance.get_absolute_url()
3276 3277 3278 3279 3280 3281


@require_GET
def get_vm_screenshot(request, pk):
    instance = get_object_or_404(Instance, pk=pk)
    try:
Kálmán Viktor committed
3282
        image = instance.screenshot(user=request.user).getvalue()
3283 3284 3285 3286 3287
    except:
        # TODO handle this better
        raise Http404()

    return HttpResponse(image, mimetype="image/png")
3288 3289


3290
class ProfileView(LoginRequiredMixin, DetailView):
3291 3292
    template_name = "dashboard/profile.html"
    model = User
3293 3294
    slug_field = "username"
    slug_url_kwarg = "username"
3295 3296 3297

    def get_context_data(self, **kwargs):
        context = super(ProfileView, self).get_context_data(**kwargs)
3298 3299
        user = self.get_object()
        context['profile'] = user
3300
        context['avatar_url'] = user.profile.get_avatar_url()
3301
        context['instances_owned'] = Instance.get_objects_with_level(
3302
            "owner", user, disregard_superuser=True).filter(destroyed_at=None)
3303
        context['instances_with_access'] = Instance.get_objects_with_level(
3304
            "user", user, disregard_superuser=True
3305
        ).filter(destroyed_at=None).exclude(pk__in=context['instances_owned'])
3306 3307 3308 3309

        group_profiles = GroupProfile.get_objects_with_level(
            "operator", self.request.user)
        groups = Group.objects.filter(groupprofile__in=group_profiles)
3310
        context['groups'] = user.groups.filter(pk__in=groups)
3311

3312 3313 3314 3315 3316
        # permissions
        # show groups only if the user is superuser, or have access
        # to any of the groups the user belongs to
        context['perm_group_list'] = (
            self.request.user.is_superuser or len(context['groups']) > 0)
3317 3318
        context['perm_email'] = (
            context['perm_group_list'] or self.request.user == user)
3319 3320 3321 3322 3323

        # filter the virtual machine list
        # if the logged in user is not superuser or not the user itself
        # filter the list so only those virtual machines are shown that are
        # originated from templates the logged in user is operator or higher
3324
        if not (self.request.user.is_superuser or self.request.user == user):
3325 3326 3327 3328 3329 3330
            it = InstanceTemplate.get_objects_with_level("operator",
                                                         self.request.user)
            context['instances_owned'] = context['instances_owned'].filter(
                template__in=it)
            context['instances_with_access'] = context[
                'instances_with_access'].filter(template__in=it)
3331 3332
        return context

3333 3334 3335

@require_POST
def toggle_use_gravatar(request, **kwargs):
3336
    user = get_object_or_404(User, username=kwargs['username'])
3337 3338 3339 3340 3341 3342 3343
    if not request.user == user:
        raise PermissionDenied()

    profile = user.profile
    profile.use_gravatar = not profile.use_gravatar
    profile.save()

3344
    new_avatar_url = user.profile.get_avatar_url()
3345 3346 3347 3348
    return HttpResponse(
        json.dumps({'new_avatar_url': new_avatar_url}),
        content_type="application/json",
    )
3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417


class UserKeyDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
    model = UserKey
    template_name = "dashboard/userkey-edit.html"
    form_class = UserKeyForm
    success_message = _("Successfully modified SSH key.")

    def get(self, request, *args, **kwargs):
        object = self.get_object()
        if object.user != request.user:
            raise PermissionDenied()
        return super(UserKeyDetail, self).get(request, *args, **kwargs)

    def get_success_url(self):
        return reverse_lazy("dashboard.views.userkey-detail",
                            kwargs=self.kwargs)

    def post(self, request, *args, **kwargs):
        object = self.get_object()
        if object.user != request.user:
            raise PermissionDenied()
        return super(UserKeyDetail, self).post(self, request, args, kwargs)


class UserKeyDelete(LoginRequiredMixin, DeleteView):
    model = UserKey

    def get_success_url(self):
        return reverse("dashboard.views.profile-preferences")

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/confirm/ajax-delete.html']
        else:
            return ['dashboard/confirm/base-delete.html']

    def delete(self, request, *args, **kwargs):
        object = self.get_object()
        if object.user != request.user:
            raise PermissionDenied()

        object.delete()
        success_url = self.get_success_url()
        success_message = _("SSH key successfully deleted.")

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': success_message}),
                content_type="application/json",
            )
        else:
            messages.success(request, success_message)
            return HttpResponseRedirect(success_url)


class UserKeyCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView):
    model = UserKey
    form_class = UserKeyForm
    template_name = "dashboard/userkey-create.html"
    success_message = _("Successfully created a new SSH key.")

    def get_success_url(self):
        return reverse_lazy("dashboard.views.profile-preferences")

    def get_form_kwargs(self):
        kwargs = super(UserKeyCreate, self).get_form_kwargs()
        kwargs['user'] = self.request.user
        return kwargs
Kálmán Viktor committed
3418 3419


3420 3421 3422
class ConnectCommandDetail(LoginRequiredMixin, SuccessMessageMixin,
                           UpdateView):
    model = ConnectCommand
3423
    template_name = "dashboard/connect-command-edit.html"
3424 3425 3426 3427 3428 3429 3430 3431 3432 3433
    form_class = ConnectCommandForm
    success_message = _("Successfully modified command template.")

    def get(self, request, *args, **kwargs):
        object = self.get_object()
        if object.user != request.user:
            raise PermissionDenied()
        return super(ConnectCommandDetail, self).get(request, *args, **kwargs)

    def get_success_url(self):
3434
        return reverse_lazy("dashboard.views.connect-command-detail",
3435 3436 3437 3438 3439 3440
                            kwargs=self.kwargs)

    def post(self, request, *args, **kwargs):
        object = self.get_object()
        if object.user != request.user:
            raise PermissionDenied()
3441
        return super(ConnectCommandDetail, self).post(request, args, kwargs)
3442

3443 3444 3445 3446 3447
    def get_form_kwargs(self):
        kwargs = super(ConnectCommandDetail, self).get_form_kwargs()
        kwargs['user'] = self.request.user
        return kwargs

3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483

class ConnectCommandDelete(LoginRequiredMixin, DeleteView):
    model = ConnectCommand

    def get_success_url(self):
        return reverse("dashboard.views.profile-preferences")

    def get_template_names(self):
        if self.request.is_ajax():
            return ['dashboard/confirm/ajax-delete.html']
        else:
            return ['dashboard/confirm/base-delete.html']

    def delete(self, request, *args, **kwargs):
        object = self.get_object()
        if object.user != request.user:
            raise PermissionDenied()

        object.delete()
        success_url = self.get_success_url()
        success_message = _("Command template successfully deleted.")

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': success_message}),
                content_type="application/json",
            )
        else:
            messages.success(request, success_message)
            return HttpResponseRedirect(success_url)


class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin,
                           CreateView):
    model = ConnectCommand
    form_class = ConnectCommandForm
3484
    template_name = "dashboard/connect-command-create.html"
3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495
    success_message = _("Successfully created a new command template.")

    def get_success_url(self):
        return reverse_lazy("dashboard.views.profile-preferences")

    def get_form_kwargs(self):
        kwargs = super(ConnectCommandCreate, self).get_form_kwargs()
        kwargs['user'] = self.request.user
        return kwargs


Őry Máté committed
3496 3497 3498 3499 3500 3501 3502 3503 3504
class HelpView(TemplateView):

    def get_context_data(self, *args, **kwargs):
        ctx = super(HelpView, self).get_context_data(*args, **kwargs)
        ctx.update({"saml": hasattr(settings, "SAML_CONFIG"),
                    "store": settings.STORE_URL})
        return ctx


Kálmán Viktor committed
3505 3506 3507
class StoreList(LoginRequiredMixin, TemplateView):
    template_name = "dashboard/store/list.html"

3508 3509
    def get_context_data(self, **kwargs):
        context = super(StoreList, self).get_context_data(**kwargs)
3510 3511
        directory = self.request.GET.get("directory", "/")
        directory = "/" if not len(directory) else directory
Kálmán Viktor committed
3512

3513 3514 3515
        store = Store(self.request.user)
        context['root'] = store.list(directory)
        context['quota'] = store.get_quota()
3516 3517
        context['up_url'] = self.create_up_directory(directory)
        context['current'] = directory
3518
        context['next_url'] = "%s%s?directory=%s" % (
Őry Máté committed
3519 3520
            settings.DJANGO_URL.rstrip("/"),
            reverse("dashboard.views.store-list"), directory)
Kálmán Viktor committed
3521
        return context
3522

3523
    def get(self, *args, **kwargs):
3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534
        try:
            if self.request.is_ajax():
                context = self.get_context_data(**kwargs)
                return render_to_response(
                    "dashboard/store/_list-box.html",
                    RequestContext(self.request, context),
                )
            else:
                return super(StoreList, self).get(*args, **kwargs)
        except NoStoreException:
            messages.warning(self.request, _("No store."))
3535 3536 3537
        except NotOkException:
            messages.warning(self.request, _("Store has some problems now."
                                             " Try again later."))
3538 3539 3540 3541
        except Exception as e:
            logger.critical("Something is wrong with store: %s", unicode(e))
            messages.warning(self.request, _("Unknown store error."))
        return redirect("/")
3542

3543
    def create_up_directory(self, directory):
3544 3545 3546 3547
        path = normpath(join('/', directory, '..'))
        if not path.endswith("/"):
            path += "/"
        return path
3548 3549 3550


@require_GET
3551
@login_required
3552 3553
def store_download(request):
    path = request.GET.get("path")
3554 3555
    try:
        url = Store(request.user).request_download(path)
3556
    except Exception:
3557 3558 3559 3560
        messages.error(request, _("Something went wrong during download."))
        logger.exception("Unable to download, "
                         "maybe it is already deleted")
        return redirect(reverse("dashboard.views.store-list"))
3561 3562 3563 3564
    return redirect(url)


@require_GET
3565
@login_required
3566
def store_upload(request):
Kálmán Viktor committed
3567
    directory = request.GET.get("directory", "/")
Őry Máté committed
3568 3569 3570 3571 3572 3573 3574
    try:
        action = Store(request.user).request_upload(directory)
    except Exception:
        logger.exception("Unable to upload")
        messages.error(request, _("Unable to upload file."))
        return redirect("/")

3575
    next_url = "%s%s?directory=%s" % (
Őry Máté committed
3576 3577
        settings.DJANGO_URL.rstrip("/"),
        reverse("dashboard.views.store-list"), directory)
3578 3579 3580 3581 3582 3583 3584

    return render(request, "dashboard/store/upload.html",
                  {'directory': directory, 'action': action,
                   'next_url': next_url})


@require_GET
3585
@login_required
3586
def store_get_upload_url(request):
3587
    current_dir = request.GET.get("current_dir")
Őry Máté committed
3588 3589 3590 3591 3592 3593
    try:
        url = Store(request.user).request_upload(current_dir)
    except Exception:
        logger.exception("Unable to upload")
        messages.error(request, _("Unable to upload file."))
        return redirect("/")
3594
    return HttpResponse(
3595
        json.dumps({'url': url}), content_type="application/json")
3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611


class StoreRemove(LoginRequiredMixin, TemplateView):
    template_name = "dashboard/store/remove.html"

    def get_context_data(self, *args, **kwargs):
        context = super(StoreRemove, self).get_context_data(*args, **kwargs)
        path = self.request.GET.get("path", "/")
        if path == "/":
            SuspiciousOperation()

        context['path'] = path
        context['is_dir'] = path.endswith("/")
        if context['is_dir']:
            context['directory'] = path
        else:
3612 3613
            context['directory'] = dirname(path)
            context['name'] = basename(path)
3614 3615 3616

        return context

3617 3618 3619 3620 3621 3622
    def get(self, *args, **kwargs):
        try:
            return super(StoreRemove, self).get(*args, **kwargs)
        except NoStoreException:
            return redirect("/")

3623 3624
    def post(self, *args, **kwargs):
        path = self.request.POST.get("path")
Őry Máté committed
3625 3626 3627 3628 3629
        try:
            Store(self.request.user).remove(path)
        except Exception:
            logger.exception("Unable to remove %s", path)
            messages.error(self.request, _("Unable to remove %s.") % path)
3630

Őry Máté committed
3631 3632 3633 3634
        return redirect("%s?directory=%s" % (
            reverse("dashboard.views.store-list"),
            dirname(dirname(path)),
        ))
3635 3636 3637 3638 3639 3640 3641 3642


@require_POST
@login_required
def store_new_directory(request):
    path = request.POST.get("path")
    name = request.POST.get("name")

Őry Máté committed
3643 3644 3645 3646 3647 3648
    try:
        Store(request.user).new_folder(join(path, name))
    except Exception:
        logger.exception("Unable to create folder %s in %s for %s",
                         name, path, unicode(request.user))
        messages.error(request, _("Unable to create folder."))
3649 3650
    return redirect("%s?directory=%s" % (
        reverse("dashboard.views.store-list"), path))
Kálmán Viktor committed
3651

3652

Kálmán Viktor committed
3653 3654 3655
@require_POST
@login_required
def store_refresh_toplist(request):
3656
    cache_key = "files-%d" % request.user.pk
Kálmán Viktor committed
3657
    cache = get_cache("default")
3658
    try:
3659 3660 3661 3662
        store = Store(request.user)
        toplist = store.toplist()
        quota = store.get_quota()
        files = {'toplist': toplist, 'quota': quota}
Őry Máté committed
3663 3664
    except Exception:
        logger.exception("Can't get toplist of %s", unicode(request.user))
3665 3666
        files = {'toplist': []}
    cache.set(cache_key, files, 300)
Kálmán Viktor committed
3667 3668

    return redirect(reverse("dashboard.index"))
3669 3670


3671 3672
def absolute_url(url):
    return urljoin(settings.DJANGO_URL, url)