views.py 109 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 23
from os import getenv
import json
Őry Máté committed
24
import logging
25
import re
26
import requests
27

28
from django.conf import settings
29
from django.contrib.auth.models import User, Group
Őry Máté committed
30
from django.contrib.auth.views import login, redirect_to_login
31
from django.contrib.messages import warning
32
from django.contrib.messages.views import SuccessMessageMixin
33
from django.core.exceptions import (
34
    PermissionDenied, SuspiciousOperation,
35
)
36 37
from django.core import signing
from django.core.urlresolvers import reverse, reverse_lazy
38
from django.db.models import Count
39
from django.http import HttpResponse, HttpResponseRedirect, Http404
Őry Máté committed
40
from django.shortcuts import redirect, render, get_object_or_404
41
from django.views.decorators.http import require_GET, require_POST
42
from django.views.generic.detail import SingleObjectMixin
43
from django.views.generic import (TemplateView, DetailView, View, DeleteView,
44
                                  UpdateView, CreateView, ListView)
45 46
from django.contrib import messages
from django.utils.translation import ugettext as _
47
from django.utils.translation import ungettext as __
48
from django.template.loader import render_to_string
49
from django.template import RequestContext
50

51
from django.forms.models import inlineformset_factory
52
from django_tables2 import SingleTableView
53 54
from braces.views import (LoginRequiredMixin, SuperuserRequiredMixin,
                          PermissionRequiredMixin)
55
from braces.views._access import AccessMixin
Kálmán Viktor committed
56

57 58
from django_sshkey.models import UserKey

59
from .forms import (
60
    CircleAuthenticationForm, HostForm, LeaseForm, MyProfileForm,
61
    NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm,
62
    UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
63
    VmSaveForm, UserKeyForm,
64
    CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
65
    TraitsForm, RawDataForm, GroupPermissionForm
66
)
67 68 69

from .tables import (
    NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable,
70
    GroupListTable, UserKeyListTable
71
)
Őry Máté committed
72 73 74 75
from vm.models import (
    Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
    InterfaceTemplate, Lease, Node, NodeActivity, Trait,
)
76
from storage.models import Disk
77
from firewall.models import Vlan, Host, Rule
78
from .models import Favourite, Profile, GroupProfile, FutureMember
79

Őry Máté committed
80
logger = logging.getLogger(__name__)
81
saml_available = hasattr(settings, "SAML_CONFIG")
82

83

84 85 86 87
def search_user(keyword):
    try:
        return User.objects.get(username=keyword)
    except User.DoesNotExist:
88 89 90 91
        try:
            return User.objects.get(profile__org_id=keyword)
        except User.DoesNotExist:
            return User.objects.get(email=keyword)
92 93


94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
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', [])
117 118
                for group in chain(*[attributes[i]
                                     for i in owneratrs if i in attributes]):
119 120 121 122 123 124 125 126
                    try:
                        GroupProfile.search(group)
                    except Group.DoesNotExist:
                        newgroups.append(group)

        return newgroups


127
class FilterMixin(object):
128

129 130 131 132
    def get_queryset_filters(self):
        filters = {}
        for item in self.allowed_filters:
            if item in self.request.GET:
133 134 135 136 137
                filters[self.allowed_filters[item]] = (
                    self.request.GET[item].split(",")
                    if self.allowed_filters[item].endswith("__in") else
                    self.request.GET[item])

138
        return filters
139

140 141 142
    def get_queryset(self):
        return super(FilterMixin,
                     self).get_queryset().filter(**self.get_queryset_filters())
143 144


145
class IndexView(LoginRequiredMixin, TemplateView):
Kálmán Viktor committed
146
    template_name = "dashboard/index.html"
147

148
    def get_context_data(self, **kwargs):
149
        user = self.request.user
150
        context = super(IndexView, self).get_context_data(**kwargs)
151

152
        # instances
153
        favs = Instance.objects.filter(favourite__user=self.request.user)
154
        instances = Instance.get_objects_with_level(
155
            'user', user, disregard_superuser=True).filter(destroyed_at=None)
156 157 158
        display = list(favs) + list(set(instances) - set(favs))
        for d in display:
            d.fav = True if d in favs else False
159
        context.update({
160
            'instances': display[:5],
161
            'more_instances': instances.count() - len(instances[:5])
162 163
        })

164 165
        running = instances.filter(status='RUNNING')
        stopped = instances.exclude(status__in=('RUNNING', 'NOSTATE'))
166

167
        context.update({
168 169 170
            'running_vms': running[:20],
            'running_vm_num': running.count(),
            'stopped_vm_num': stopped.count()
171
        })
172

173 174 175 176
        # nodes
        if user.is_superuser:
            nodes = Node.objects.all()
            context.update({
177 178
                'nodes': nodes[:5],
                'more_nodes': nodes.count() - len(nodes[:5]),
179 180 181 182 183 184 185 186 187 188
                '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
189
        if user.has_module_perms('auth'):
190 191
            profiles = GroupProfile.get_objects_with_level('operator', user)
            groups = Group.objects.filter(groupprofile__in=profiles)
192 193 194 195
            context.update({
                'groups': groups[:5],
                'more_groups': groups.count() - len(groups[:5]),
            })
196 197 198 199 200 201

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

202 203
        return context

204

205
def get_vm_acl_data(obj):
206 207 208 209 210 211 212 213 214
    levels = obj.ACL_LEVELS
    users = obj.get_users_with_level()
    users = [{'user': u, 'level': l} for u, l in users]
    groups = obj.get_groups_with_level()
    groups = [{'group': g, 'level': l} for g, l in groups]
    return {'users': users, 'groups': groups, 'levels': levels,
            'url': reverse('dashboard.views.vm-acl', args=[obj.pk])}


215 216 217 218 219 220 221 222 223 224 225
def get_group_acl_data(obj):
    aclobj = obj.profile
    levels = aclobj.ACL_LEVELS
    users = aclobj.get_users_with_level()
    users = [{'user': u, 'level': l} for u, l in users]
    groups = aclobj.get_groups_with_level()
    groups = [{'group': g, 'level': l} for g, l in groups]
    return {'users': users, 'groups': groups, 'levels': levels,
            'url': reverse('dashboard.views.group-acl', args=[obj.pk])}


226
class CheckedDetailView(LoginRequiredMixin, DetailView):
227 228
    read_level = 'user'

229 230 231
    def get_has_level(self):
        return self.object.has_level

232 233
    def get_context_data(self, **kwargs):
        context = super(CheckedDetailView, self).get_context_data(**kwargs)
234
        if not self.get_has_level()(self.request.user, self.read_level):
235 236 237 238
            raise PermissionDenied()
        return context


239 240 241 242 243 244 245 246
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()
247 248
        if not request.user.has_perm('vm.access_console'):
            raise PermissionDenied()
249
        if self.object.node:
250 251 252 253 254 255 256 257
            with instance_activity(code_suffix='console-accessed',
                                   instance=self.object, user=request.user,
                                   concurrency_check=False):
                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)
258 259 260 261
        else:
            raise Http404()


262
class VmDetailView(CheckedDetailView):
Kálmán Viktor committed
263
    template_name = "dashboard/vm-detail.html"
264
    model = Instance
265 266

    def get_context_data(self, **kwargs):
267
        context = super(VmDetailView, self).get_context_data(**kwargs)
268
        instance = context['instance']
269
        ops = get_operations(instance, self.request.user)
270
        context.update({
Bach Dániel committed
271
            'graphite_enabled': settings.GRAPHITE_URL is not None,
272
            'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
273
                                    kwargs={'pk': self.object.pk}),
274 275
            'ops': ops,
            'op': {i.op: i for i in ops},
276
        })
277 278

        # activity data
279 280
        context['activities'] = self.object.get_merged_activities(
            self.request.user)
281

282
        context['vlans'] = Vlan.get_objects_with_level(
283
            'user', self.request.user
284
        ).exclude(  # exclude already added interfaces
285 286 287
            pk__in=Interface.objects.filter(
                instance=self.get_object()).values_list("vlan", flat=True)
        ).all()
288
        context['acl'] = get_vm_acl_data(instance)
289 290
        context['os_type_icon'] = instance.os_type.replace("unknown",
                                                           "question")
291 292 293
        # ipv6 infos
        context['ipv6_host'] = instance.get_connect_host(use_ipv6=True)
        context['ipv6_port'] = instance.get_connect_port(use_ipv6=True)
294 295 296 297 298

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

300 301 302
        # resources change perm
        context['can_change_resources'] = self.request.user.has_perm(
            "vm.change_resources")
303

304
        return context
Kálmán Viktor committed
305

306
    def post(self, request, *args, **kwargs):
307 308 309
        options = {
            'change_password': self.__change_password,
            'new_name': self.__set_name,
310
            'new_description': self.__set_description,
311 312
            'new_tag': self.__add_tag,
            'to_remove': self.__remove_tag,
313 314
            'port': self.__add_port,
            'new_network_vlan': self.__new_network,
315
            'abort_operation': self.__abort_operation,
316 317 318 319
        }
        for k, v in options.iteritems():
            if request.POST.get(k) is not None:
                return v(request)
Kálmán Viktor committed
320

321 322 323 324
    def __change_password(self, request):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
Kálmán Viktor committed
325

326
        self.object.change_password(user=request.user)
327
        messages.success(request, _("Password changed."))
328
        if request.is_ajax():
329
            return HttpResponse("Success.")
330 331 332
        else:
            return redirect(reverse_lazy("dashboard.views.detail",
                                         kwargs={'pk': self.object.pk}))
333

334 335
    def __set_name(self, request):
        self.object = self.get_object()
336 337
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
338 339 340 341
        new_name = request.POST.get("new_name")
        Instance.objects.filter(pk=self.object.pk).update(
            **{'name': new_name})

342
        success_message = _("VM successfully renamed.")
343 344 345 346 347 348 349 350 351 352 353 354
        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)
355 356 357 358 359 360 361 362 363 364 365
            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})

366
        success_message = _("VM description successfully updated.")
367 368 369 370 371 372 373 374 375 376 377 378
        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())
379

Kálmán Viktor committed
380 381 382
    def __add_tag(self, request):
        new_tag = request.POST.get('new_tag')
        self.object = self.get_object()
383 384
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
Kálmán Viktor committed
385 386

        if len(new_tag) < 1:
387
            message = u"Please input something."
Kálmán Viktor committed
388
        elif len(new_tag) > 20:
389
            message = u"Tag name is too long."
Kálmán Viktor committed
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
        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()
405 406
            if not self.object.has_level(request.user, 'owner'):
                raise PermissionDenied()
Kálmán Viktor committed
407 408 409 410 411 412 413 414 415 416 417

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

422 423
    def __add_port(self, request):
        object = self.get_object()
424 425
        if (not object.has_level(request.user, 'owner') or
                not request.user.has_perm('vm.config_ports')):
426
            raise PermissionDenied()
427 428 429 430 431 432

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

        try:
            error = None
433 434 435
            interfaces = object.interface_set.all()
            host = Host.objects.get(pk=request.POST.get("host_pk"),
                                    interface__in=interfaces)
436
            host.add_port(proto, private=port)
437 438 439 440 441
        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()
442
        except ValueError:
443
            error = _("There is a problem with your input.")
444
        except Exception as e:
Bach Dániel committed
445 446
            error = _("Unknown error.")
            logger.error(e)
447 448 449 450 451 452 453 454 455

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

456 457 458 459 460
    def __new_network(self, request):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()

461
        vlan = get_object_or_404(Vlan, pk=request.POST.get("new_network_vlan"))
462 463
        if not vlan.has_level(request.user, 'user'):
            raise PermissionDenied()
464
        try:
465
            self.object.add_interface(vlan=vlan, user=request.user)
466
            messages.success(request, _("Successfully added new interface."))
467 468 469 470 471 472 473
        except Exception, e:
            error = u' '.join(e.messages)
            messages.error(request, error)

        return redirect("%s#network" % reverse_lazy(
            "dashboard.views.detail", kwargs={'pk': self.object.pk}))

474
    def __abort_operation(self, request):
Kálmán Viktor committed
475 476 477 478
        self.object = self.get_object()

        activity = get_object_or_404(InstanceActivity,
                                     pk=request.POST.get("activity"))
479 480
        if not activity.is_abortable_for(request.user):
            raise PermissionDenied()
Kálmán Viktor committed
481 482 483
        activity.abort()
        return redirect("%s#activity" % self.object.get_absolute_url())

484

485
class VmTraitsUpdate(SuperuserRequiredMixin, UpdateView):
486 487 488 489 490 491 492
    form_class = TraitsForm
    model = Instance

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


493
class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
494 495 496 497 498 499 500
    form_class = RawDataForm
    model = Instance

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


501
class OperationView(DetailView):
502

503
    template_name = 'dashboard/operate.html'
504
    show_in_toolbar = True
505
    effect = None
506

507 508 509
    @property
    def name(self):
        return self.get_op().name
510

511 512 513
    @property
    def description(self):
        return self.get_op().description
514

515 516 517
    def is_preferred(self):
        return self.get_op().is_preferred()

518 519 520
    @classmethod
    def get_urlname(cls):
        return 'dashboard.vm.op.%s' % cls.op
521

522 523
    def get_url(self):
        return reverse(self.get_urlname(), args=(self.get_object().pk, ))
524

525
    def get_template_names(self):
526
        if self.request.is_ajax():
527
            return ['dashboard/_modal.html']
528
        else:
529
            return ['dashboard/_base.html']
530

531 532 533
    @classmethod
    def get_op_by_object(cls, obj):
        return getattr(obj, cls.op)
534

535 536 537 538
    def get_op(self):
        if not hasattr(self, '_opobj'):
            setattr(self, '_opobj', getattr(self.get_object(), self.op))
        return self._opobj
539

540
    def get_context_data(self, **kwargs):
541
        ctx = super(OperationView, self).get_context_data(**kwargs)
542
        ctx['op'] = self.get_op()
543
        ctx['opview'] = self
544
        ctx['url'] = self.request.path
545
        ctx['template'] = super(OperationView, self).get_template_names()[0]
546
        return ctx
547

548 549
    def get(self, request, *args, **kwargs):
        self.get_op().check_auth(request.user)
550
        return super(OperationView, self).get(request, *args, **kwargs)
551

552
    def post(self, request, extra=None, *args, **kwargs):
553
        self.object = self.get_object()
554 555
        if extra is None:
            extra = {}
556
        try:
557
            self.get_op().async(user=request.user, **extra)
558 559 560
        except Exception as e:
            messages.error(request, _('Could not start operation.'))
            logger.error(e)
561
        return redirect("%s#activity" % self.object.get_absolute_url())
562

563
    @classmethod
564
    def factory(cls, op, icon='cog', effect='info'):
565
        return type(str(cls.__name__ + op),
566
                    (cls, ), {'op': op, 'icon': icon, 'effect': effect})
567

568
    @classmethod
569
    def bind_to_object(cls, instance, **kwargs):
570 571
        v = cls()
        v.get_object = lambda: instance
572 573
        for key, value in kwargs.iteritems():
            setattr(v, key, value)
574 575 576 577 578 579
        return v


class VmOperationView(OperationView):

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

582 583 584
    def post(self, request, extra=None, *args, **kwargs):
        resp = super(VmOperationView, self).post(request, extra, *args,
                                                 **kwargs)
585 586 587 588
        if request.is_ajax():
            store = messages.get_messages(request)
            store.used = True
            return HttpResponse(
589
                json.dumps({'success': True,
590 591 592 593 594 595
                            'messages': [unicode(m) for m in store]}),
                content_type="application=json"
            )
        else:
            return resp

596

597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614
class FormOperationMixin(object):

    form_class = None

    def get_context_data(self, **kwargs):
        ctx = super(FormOperationMixin, self).get_context_data(**kwargs)
        if self.request.method == 'POST':
            ctx['form'] = self.form_class(self.request.POST)
        else:
            ctx['form'] = self.form_class()
        return ctx

    def post(self, request, extra=None, *args, **kwargs):
        if extra is None:
            extra = {}
        form = self.form_class(self.request.POST)
        if form.is_valid():
            extra.update(form.cleaned_data)
615
            resp = super(FormOperationMixin, self).post(
616
                request, extra, *args, **kwargs)
617 618
            if request.is_ajax():
                return HttpResponse(
619
                    json.dumps({'success': True}),
620 621 622 623
                    content_type="application=json"
                )
            else:
                return resp
624 625 626 627
        else:
            return self.get(request)


628 629 630 631 632
class VmCreateDiskView(FormOperationMixin, VmOperationView):

    op = 'create_disk'
    form_class = VmCreateDiskForm
    show_in_toolbar = False
633
    icon = 'hdd-o'
634
    is_disk_operation = True
635 636 637 638 639 640 641 642


class VmDownloadDiskView(FormOperationMixin, VmOperationView):

    op = 'download_disk'
    form_class = VmDownloadDiskForm
    show_in_toolbar = False
    icon = 'download'
643
    is_disk_operation = True
644 645


646 647 648 649
class VmMigrateView(VmOperationView):

    op = 'migrate'
    icon = 'truck'
650
    effect = 'info'
651 652 653
    template_name = 'dashboard/_vm-migrate.html'

    def get_context_data(self, **kwargs):
654
        ctx = super(VmMigrateView, self).get_context_data(**kwargs)
655 656 657 658 659
        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):
660 661
        if extra is None:
            extra = {}
662 663 664 665 666 667 668
        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)


669
class VmSaveView(FormOperationMixin, VmOperationView):
670 671 672

    op = 'save_as_template'
    icon = 'save'
673
    effect = 'info'
674
    form_class = VmSaveForm
675

676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698

class VmResourcesChangeView(VmOperationView):
    op = 'resources_change'
    icon = "save"
    show_in_toolbar = False

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

        resources = {
            'num_cores': "cpu-count",
            'priority': "cpu-priority",
            'ram_size': "ram-size",
            "max_ram_size": "ram-size",  # TODO
        }
        for k, v in resources.iteritems():
            extra[k] = request.POST.get(v)

        return super(VmResourcesChangeView, self).post(request, extra,
                                                       *args, **kwargs)


699 700
vm_ops = OrderedDict([
    ('deploy', VmOperationView.factory(
701
        op='deploy', icon='play', effect='success')),
702
    ('wake_up', VmOperationView.factory(
703
        op='wake_up', icon='sun-o', effect='success')),
704
    ('sleep', VmOperationView.factory(
705
        op='sleep', icon='moon-o', effect='info')),
706 707 708
    ('migrate', VmMigrateView),
    ('save_as_template', VmSaveView),
    ('reboot', VmOperationView.factory(
709
        op='reboot', icon='refresh', effect='warning')),
710
    ('reset', VmOperationView.factory(
711
        op='reset', icon='bolt', effect='warning')),
712
    ('shutdown', VmOperationView.factory(
713
        op='shutdown', icon='power-off', effect='warning')),
714
    ('shut_off', VmOperationView.factory(
715
        op='shut_off', icon='ban', effect='warning')),
716 717
    ('recover', VmOperationView.factory(
        op='recover', icon='medkit', effect='warning')),
718
    ('destroy', VmOperationView.factory(
719
        op='destroy', icon='times', effect='danger')),
720 721 722
    ('create_disk', VmCreateDiskView),
    ('download_disk', VmDownloadDiskView),
])
723 724 725 726 727 728 729 730 731


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()
732
        except PermissionDenied as e:
733 734
            logger.debug('Not showing operation %s for %s: %s',
                         k, instance, unicode(e))
735 736
        except Exception:
            ops.append(v.bind_to_object(instance, disabled=True))
737 738 739
        else:
            ops.append(v.bind_to_object(instance))
    return ops
740

Kálmán Viktor committed
741

742
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
743 744
    template_name = "dashboard/node-detail.html"
    model = Node
745 746
    form = None
    form_class = TraitForm
747

748 749 750
    def get_context_data(self, form=None, **kwargs):
        if form is None:
            form = self.form_class()
751
        context = super(NodeDetailView, self).get_context_data(**kwargs)
752 753
        instances = Instance.active.filter(node=self.object)
        context['table'] = NodeVmListTable(instances)
754 755 756 757
        na = NodeActivity.objects.filter(
            node=self.object, parent=None
        ).order_by('-started').select_related()
        context['activities'] = na
758
        context['trait_form'] = form
759
        context['graphite_enabled'] = (
Bach Dániel committed
760
            settings.GRAPHITE_URL is not None)
761 762
        return context

763 764 765
    def post(self, request, *args, **kwargs):
        if request.POST.get('new_name'):
            return self.__set_name(request)
766 767
        if request.POST.get('to_remove'):
            return self.__remove_trait(request)
768 769
        return redirect(reverse_lazy("dashboard.views.node-detail",
                                     kwargs={'pk': self.get_object().pk}))
770 771 772 773 774 775 776

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

777
        success_message = _("Node successfully renamed.")
778 779 780 781 782
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
                'node_pk': self.object.pk
783 784 785 786 787 788 789 790 791 792
            }
            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}))

793 794 795 796
    def __remove_trait(self, request):
        try:
            to_remove = request.POST.get('to_remove')
            self.object = self.get_object()
797
            self.object.traits.remove(to_remove)
798 799 800 801 802 803 804
            message = u"Success"
        except:  # note this won't really happen
            message = u"Not success"

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': message}),
805
                content_type="application/json"
806
            )
807
        else:
808
            return redirect(self.object.get_absolute_url())
809

810

811
class GroupDetailView(CheckedDetailView):
812 813
    template_name = "dashboard/group-detail.html"
    model = Group
814
    read_level = 'operator'
815 816 817

    def get_has_level(self):
        return self.object.profile.has_level
818 819 820

    def get_context_data(self, **kwargs):
        context = super(GroupDetailView, self).get_context_data(**kwargs)
821 822
        context['group'] = self.object
        context['users'] = self.object.user_set.all()
823 824
        context['future_users'] = FutureMember.objects.filter(
            group=self.object)
825
        context['acl'] = get_group_acl_data(self.object)
826 827
        context['group_profile_form'] = GroupProfileUpdate.get_form_object(
            self.request, self.object.profile)
828 829 830 831 832

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

833 834 835
        return context

    def post(self, request, *args, **kwargs):
836 837 838
        self.object = self.get_object()
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
839

840 841
        if request.POST.get('new_name'):
            return self.__set_name(request)
842
        if request.POST.get('list-new-name'):
843
            return self.__add_user(request)
844
        if request.POST.get('list-new-namelist'):
845
            return self.__add_list(request)
846 847 848 849
        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}))
850 851

    def __add_user(self, request):
852
        name = request.POST['list-new-name']
853 854 855
        self.__add_username(request, name)
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
856 857

    def __add_username(self, request, name):
858
        if not name:
859
            return
860
        try:
861
            entity = search_user(name)
862
            self.object.user_set.add(entity)
863
        except User.DoesNotExist:
864 865 866 867 868
            if saml_available:
                FutureMember.objects.get_or_create(org_id=name,
                                                   group=self.object)
            else:
                warning(request, _('User "%s" not found.') % name)
869

870
    def __add_list(self, request):
871 872
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
873 874 875
        userlist = request.POST.get('list-new-namelist').split('\r\n')
        for line in userlist:
            self.__add_username(request, line)
876 877
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
878 879 880 881 882 883

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

884
        success_message = _("Group successfully renamed.")
885 886 887 888
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
889
                'group_pk': self.object.pk
890 891 892 893 894 895 896 897 898 899 900
            }
            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}))


901 902 903 904 905 906 907
class GroupPermissionsView(SuperuserRequiredMixin, UpdateView):
    model = Group
    form_class = GroupPermissionForm
    slug_field = "pk"
    slug_url_kwarg = "group_pk"

    def get_success_url(self):
908
        return "%s#group-detail-permissions" % (
909 910 911
            self.get_object().groupprofile.get_absolute_url())


912
class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
913

914
    def post(self, request, *args, **kwargs):
915
        instance = self.get_object()
916 917
        if not (instance.has_level(request.user, "owner") or
                getattr(instance, 'owner', None) == request.user):
Őry Máté committed
918 919
            logger.warning('Tried to set permissions of %s by non-owner %s.',
                           unicode(instance), unicode(request.user))
920
            raise PermissionDenied()
921
        self.set_levels(request, instance)
922
        self.remove_levels(request, instance)
923
        self.add_levels(request, instance)
924
        return redirect("%s#access" % instance.get_absolute_url())
925 926

    def set_levels(self, request, instance):
927 928 929
        for key, value in request.POST.items():
            m = re.match('perm-([ug])-(\d+)', key)
            if m:
930 931
                typ, id = m.groups()
                entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
932
                if getattr(instance, "owner", None) == entity:
933 934 935
                    logger.info("Tried to set owner's acl level for %s by %s.",
                                unicode(instance), unicode(request.user))
                    continue
936
                instance.set_level(entity, value)
Őry Máté committed
937 938 939
                logger.info("Set %s's acl level for %s to %s by %s.",
                            unicode(entity), unicode(instance),
                            value, unicode(request.user))
940

941 942 943 944 945 946 947 948 949 950
    def remove_levels(self, request, instance):
        for key, value in request.POST.items():
            if key.startswith("remove"):
                typ = key[7:8]  # len("remove-")
                id = key[9:]  # len("remove-x-")
                entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
                if getattr(instance, "owner", None) == entity:
                    logger.info("Tried to remove owner from %s by %s.",
                                unicode(instance), unicode(request.user))
                    msg = _("The original owner cannot be removed, however "
951
                            "you can transfer ownership.")
952 953 954 955 956 957 958
                    messages.warning(request, msg)
                    continue
                instance.set_level(entity, None)
                logger.info("Revoked %s's access to %s by %s.",
                            unicode(entity), unicode(instance),
                            unicode(request.user))

959
    def add_levels(self, request, instance):
960 961
        name = request.POST['perm-new-name']
        value = request.POST['perm-new']
962 963 964
        if not name:
            return
        try:
965
            entity = search_user(name)
966 967
        except User.DoesNotExist:
            entity = None
968 969
            try:
                entity = Group.objects.get(name=name)
970 971 972 973
            except Group.DoesNotExist:
                warning(request, _('User or group "%s" not found.') % name)
                return

974
        instance.set_level(entity, value)
Őry Máté committed
975 976 977
        logger.info("Set %s's new acl level for %s to %s by %s.",
                    unicode(entity), unicode(instance),
                    value, unicode(request.user))
978

Kálmán Viktor committed
979

980 981 982 983 984 985 986 987 988 989
class TemplateAclUpdateView(AclUpdateView):
    model = InstanceTemplate

    def post(self, request, *args, **kwargs):
        template = self.get_object()
        if not (template.has_level(request.user, "owner") or
                getattr(template, 'owner', None) == request.user):
            logger.warning('Tried to set permissions of %s by non-owner %s.',
                           unicode(template), unicode(request.user))
            raise PermissionDenied()
990 991 992 993 994 995 996 997 998

        name = request.POST['perm-new-name']
        if (User.objects.filter(username=name).count() +
                Group.objects.filter(name=name).count() < 1
                and len(name) > 0):
            warning(request, _('User or group "%s" not found.') % name)
        else:
            self.set_levels(request, template)
            self.add_levels(request, template)
999
            self.remove_levels(request, template)
1000 1001 1002 1003 1004

            post_for_disk = request.POST.copy()
            post_for_disk['perm-new'] = 'user'
            request.POST = post_for_disk
            for d in template.disks.all():
1005
                self.set_levels(request, d)
1006
                self.add_levels(request, d)
1007
                self.remove_levels(request, d)
1008

1009
        return redirect(template)
1010 1011


1012 1013 1014 1015 1016 1017 1018 1019 1020 1021
class GroupAclUpdateView(AclUpdateView):
    model = Group

    def post(self, request, *args, **kwargs):
        instance = self.get_object().profile
        if not (instance.has_level(request.user, "owner") or
                getattr(instance, 'owner', None) == request.user):
            logger.warning('Tried to set permissions of %s by non-owner %s.',
                           unicode(instance), unicode(request.user))
            raise PermissionDenied()
Kálmán Viktor committed
1022 1023 1024

        self.set_levels(request, instance)
        self.add_levels(request, instance)
1025 1026 1027 1028
        return redirect(reverse("dashboard.views.group-detail",
                                kwargs=self.kwargs))


1029
class TemplateChoose(LoginRequiredMixin, TemplateView):
1030 1031 1032 1033 1034 1035 1036 1037 1038

    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)
1039
        templates = InstanceTemplate.get_objects_with_level("user",
1040 1041 1042 1043
                                                            self.request.user)
        context.update({
            'box_title': _('Choose template'),
            'ajax_title': False,
1044
            'template': "dashboard/_template-choose.html",
1045
            'templates': templates.all(),
1046 1047 1048
        })
        return context

1049 1050 1051 1052 1053 1054 1055
    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"))
1056
        elif template is None:
1057
            messages.warning(request, _("Select an option to proceed."))
1058
            return redirect(reverse("dashboard.views.template-choose"))
1059 1060 1061
        else:
            template = get_object_or_404(InstanceTemplate, pk=template)

1062
        if not template.has_level(request.user, "user"):
1063 1064
            raise PermissionDenied()

1065 1066 1067 1068 1069
        instance = Instance.create_from_template(
            template=template, owner=request.user, is_base=True)

        return redirect(instance.get_absolute_url())

1070

1071 1072 1073 1074
class TemplateCreate(SuccessMessageMixin, CreateView):
    model = InstanceTemplate
    form_class = TemplateForm

1075 1076
    def get_template_names(self):
        if self.request.is_ajax():
1077
            pass
1078 1079 1080 1081 1082 1083 1084
        else:
            return ['dashboard/nojs-wrapper.html']

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

        context.update({
1085
            'box_title': _("Create a new base VM"),
1086
            'template': "dashboard/_template-create.html",
1087
            'leases': Lease.objects.count()
1088 1089 1090
        })
        return context

1091
    def get(self, *args, **kwargs):
1092
        if not self.request.user.has_perm('vm.create_base_template'):
1093
            raise PermissionDenied()
1094

1095 1096 1097 1098
        return super(TemplateCreate, self).get(*args, **kwargs)

    def get_form_kwargs(self):
        kwargs = super(TemplateCreate, self).get_form_kwargs()
1099
        kwargs['user'] = self.request.user
1100 1101
        return kwargs

1102
    def post(self, request, *args, **kwargs):
1103
        if not self.request.user.has_perm('vm.create_base_template'):
1104
            raise PermissionDenied()
1105 1106

        form = self.form_class(request.POST, user=request.user)
1107 1108
        if not form.is_valid():
            return self.get(request, form, *args, **kwargs)
1109
        else:
1110
            post = form.cleaned_data
1111 1112
            networks = self.__create_networks(post.pop("networks"),
                                              request.user)
1113
            post.pop("parent")
1114
            post['max_ram_size'] = post['ram_size']
1115 1116 1117 1118 1119 1120 1121
            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)
1122

1123
            return redirect("%s#resources" % inst.get_absolute_url())
1124

1125
    def __create_networks(self, vlans, user):
1126 1127
        networks = []
        for v in vlans: