views.py 109 KB
Newer Older
1
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
2

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
#
# 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/>.

19
from __future__ import unicode_literals, absolute_import
20

21
from itertools import chain
22
from os import getenv
23
import os
24
import json
Őry Máté committed
25
import logging
26
import re
27
import requests
28

29
from django.conf import settings
30
from django.contrib.auth.models import User, Group
Őry Máté committed
31
from django.contrib.auth.views import login, redirect_to_login
32
from django.contrib.auth.decorators import login_required
33
from django.contrib.messages import warning
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
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 51
from django.contrib import messages
from django.utils.translation import ugettext as _
52
from django.utils.translation import ungettext as __
53
from django.template.loader import render_to_string
54
from django.template import RequestContext
55

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

62 63
from django_sshkey.models import UserKey

64
from .forms import (
65
    CircleAuthenticationForm, HostForm, LeaseForm, MyProfileForm,
66
    NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm,
67
    UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
68
    VmSaveForm, UserKeyForm,
69
    CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
70
)
71 72 73

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

Kálmán Viktor committed
84 85
from dashboard import store_api

Őry Máté committed
86
logger = logging.getLogger(__name__)
87
saml_available = hasattr(settings, "SAML_CONFIG")
88

89

90 91 92 93
def search_user(keyword):
    try:
        return User.objects.get(username=keyword)
    except User.DoesNotExist:
94 95 96 97
        try:
            return User.objects.get(profile__org_id=keyword)
        except User.DoesNotExist:
            return User.objects.get(email=keyword)
98 99


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

        return newgroups


133
class FilterMixin(object):
134

135 136 137 138
    def get_queryset_filters(self):
        filters = {}
        for item in self.allowed_filters:
            if item in self.request.GET:
139 140 141 142 143
                filters[self.allowed_filters[item]] = (
                    self.request.GET[item].split(",")
                    if self.allowed_filters[item].endswith("__in") else
                    self.request.GET[item])

144
        return filters
145

146 147 148
    def get_queryset(self):
        return super(FilterMixin,
                     self).get_queryset().filter(**self.get_queryset_filters())
149 150


151
class IndexView(LoginRequiredMixin, TemplateView):
Kálmán Viktor committed
152
    template_name = "dashboard/index.html"
153

154
    def get_context_data(self, **kwargs):
155
        user = self.request.user
156
        context = super(IndexView, self).get_context_data(**kwargs)
157

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

170 171
        running = instances.filter(status='RUNNING')
        stopped = instances.exclude(status__in=('RUNNING', 'NOSTATE'))
172

173
        context.update({
174 175 176
            'running_vms': running[:20],
            'running_vm_num': running.count(),
            'stopped_vm_num': stopped.count()
177
        })
178

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

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

Kálmán Viktor committed
208 209 210 211 212 213 214 215 216
        # toplist
        cache = get_cache("default")
        toplist = cache.get("toplist-test")
        if not toplist:
            toplist = store_api.process_list(store_api.toplist("test"))
            cache.set("toplist-test", toplist, 300)

        context['toplist'] = toplist

217 218
        return context

219

220
def get_vm_acl_data(obj):
221 222 223 224 225 226 227 228 229
    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])}


230 231 232 233 234 235 236 237 238 239 240
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])}


241
class CheckedDetailView(LoginRequiredMixin, DetailView):
242 243
    read_level = 'user'

244 245 246
    def get_has_level(self):
        return self.object.has_level

247 248
    def get_context_data(self, **kwargs):
        context = super(CheckedDetailView, self).get_context_data(**kwargs)
249
        if not self.get_has_level()(self.request.user, self.read_level):
250 251 252 253
            raise PermissionDenied()
        return context


254 255 256 257 258 259 260 261 262
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()
        if self.object.node:
263 264 265 266 267 268 269 270
            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)
271 272 273 274
        else:
            raise Http404()


275
class VmDetailView(CheckedDetailView):
Kálmán Viktor committed
276
    template_name = "dashboard/vm-detail.html"
277
    model = Instance
278 279

    def get_context_data(self, **kwargs):
280
        context = super(VmDetailView, self).get_context_data(**kwargs)
281
        instance = context['instance']
282
        ops = get_operations(instance, self.request.user)
283
        context.update({
Bach Dániel committed
284
            'graphite_enabled': settings.GRAPHITE_URL is not None,
285
            'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
286
                                    kwargs={'pk': self.object.pk}),
287 288
            'ops': ops,
            'op': {i.op: i for i in ops},
289
        })
290 291

        # activity data
292
        context['activities'] = self.object.get_activities(self.request.user)
293

294
        context['vlans'] = Vlan.get_objects_with_level(
295
            'user', self.request.user
296
        ).exclude(  # exclude already added interfaces
297 298 299
            pk__in=Interface.objects.filter(
                instance=self.get_object()).values_list("vlan", flat=True)
        ).all()
300
        context['acl'] = get_vm_acl_data(instance)
301 302
        context['os_type_icon'] = instance.os_type.replace("unknown",
                                                           "question")
303 304 305
        # ipv6 infos
        context['ipv6_host'] = instance.get_connect_host(use_ipv6=True)
        context['ipv6_port'] = instance.get_connect_port(use_ipv6=True)
306
        return context
Kálmán Viktor committed
307

308 309 310 311 312
    def post(self, request, *args, **kwargs):
        if (request.POST.get('ram-size') and request.POST.get('cpu-count')
                and request.POST.get('cpu-priority')):
            return self.__set_resources(request)

313 314 315
        options = {
            'change_password': self.__change_password,
            'new_name': self.__set_name,
316
            'new_description': self.__set_description,
317 318
            'new_tag': self.__add_tag,
            'to_remove': self.__remove_tag,
319 320
            'port': self.__add_port,
            'new_network_vlan': self.__new_network,
321
            'abort_operation': self.__abort_operation,
322 323 324 325
        }
        for k, v in options.iteritems():
            if request.POST.get(k) is not None:
                return v(request)
Kálmán Viktor committed
326

327 328 329 330
    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
331

332
        self.object.change_password(user=request.user)
333
        messages.success(request, _("Password changed."))
334
        if request.is_ajax():
335
            return HttpResponse("Success.")
336 337 338
        else:
            return redirect(reverse_lazy("dashboard.views.detail",
                                         kwargs={'pk': self.object.pk}))
339

340 341 342 343
    def __set_resources(self, request):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
344 345
        if not request.user.has_perm('vm.change_resources'):
            raise PermissionDenied()
346 347 348 349

        resources = {
            'num_cores': request.POST.get('cpu-count'),
            'ram_size': request.POST.get('ram-size'),
350
            'max_ram_size': request.POST.get('ram-size'),  # TODO: max_ram
351 352 353 354
            'priority': request.POST.get('cpu-priority')
        }
        Instance.objects.filter(pk=self.object.pk).update(**resources)

355
        success_message = _("Resources successfully updated.")
356 357 358 359 360 361 362 363 364 365 366
        if request.is_ajax():
            response = {'message': success_message}
            return HttpResponse(
                json.dumps(response),
                content_type="application/json"
            )
        else:
            messages.success(request, success_message)
            return redirect(reverse_lazy("dashboard.views.detail",
                                         kwargs={'pk': self.object.pk}))

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

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

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

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

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

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

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

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

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

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

489 490 491 492 493
    def __new_network(self, request):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()

494
        vlan = get_object_or_404(Vlan, pk=request.POST.get("new_network_vlan"))
495 496
        if not vlan.has_level(request.user, 'user'):
            raise PermissionDenied()
497
        try:
498
            self.object.add_interface(vlan=vlan, user=request.user)
499
            messages.success(request, _("Successfully added new interface."))
500 501 502 503 504 505 506
        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}))

507
    def __abort_operation(self, request):
Kálmán Viktor committed
508 509 510 511
        self.object = self.get_object()

        activity = get_object_or_404(InstanceActivity,
                                     pk=request.POST.get("activity"))
512 513
        if not activity.is_abortable_for(request.user):
            raise PermissionDenied()
Kálmán Viktor committed
514 515 516
        activity.abort()
        return redirect("%s#activity" % self.object.get_absolute_url())

517

518
class OperationView(DetailView):
519

520
    template_name = 'dashboard/operate.html'
521
    show_in_toolbar = True
522

523 524 525
    @property
    def name(self):
        return self.get_op().name
526

527 528 529
    @property
    def description(self):
        return self.get_op().description
530

531 532 533
    @classmethod
    def get_urlname(cls):
        return 'dashboard.vm.op.%s' % cls.op
534

535 536
    def get_url(self):
        return reverse(self.get_urlname(), args=(self.get_object().pk, ))
537

538 539 540 541 542
    def get_wrapper_template_name(self):
        if self.request.is_ajax():
            return 'dashboard/_modal.html'
        else:
            return 'dashboard/_base.html'
543

544 545 546
    @classmethod
    def get_op_by_object(cls, obj):
        return getattr(obj, cls.op)
547

548 549 550 551
    def get_op(self):
        if not hasattr(self, '_opobj'):
            setattr(self, '_opobj', getattr(self.get_object(), self.op))
        return self._opobj
552

553
    def get_context_data(self, **kwargs):
554
        ctx = super(OperationView, self).get_context_data(**kwargs)
555 556 557
        ctx['op'] = self.get_op()
        ctx['url'] = self.request.path
        return ctx
558

559 560 561 562 563 564 565
    def get(self, request, *args, **kwargs):
        self.get_op().check_auth(request.user)
        response = super(OperationView, self).get(request, *args, **kwargs)
        response.render()
        response.content = render_to_string(self.get_wrapper_template_name(),
                                            {'body': response.content})
        return response
566

567
    def post(self, request, extra=None, *args, **kwargs):
568
        self.object = self.get_object()
569 570
        if extra is None:
            extra = {}
571
        try:
572
            self.get_op().async(user=request.user, **extra)
573 574 575
        except Exception as e:
            messages.error(request, _('Could not start operation.'))
            logger.error(e)
576
        return redirect("%s#activity" % self.object.get_absolute_url())
577

578 579 580 581
    @classmethod
    def factory(cls, op, icon='cog'):
        return type(str(cls.__name__ + op),
                    (cls, ), {'op': op, 'icon': icon})
582

583 584 585 586 587 588 589 590 591 592
    @classmethod
    def bind_to_object(cls, instance):
        v = cls()
        v.get_object = lambda: instance
        return v


class VmOperationView(OperationView):

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

595

596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619
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)
            return super(FormOperationMixin, self).post(
                request, extra, *args, **kwargs)
        else:
            return self.get(request)


620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
class VmCreateDiskView(FormOperationMixin, VmOperationView):

    op = 'create_disk'
    form_class = VmCreateDiskForm
    show_in_toolbar = False
    icon = 'hdd'


class VmDownloadDiskView(FormOperationMixin, VmOperationView):

    op = 'download_disk'
    form_class = VmDownloadDiskForm
    show_in_toolbar = False
    icon = 'download'


636 637 638 639 640 641 642
class VmMigrateView(VmOperationView):

    op = 'migrate'
    icon = 'truck'
    template_name = 'dashboard/_vm-migrate.html'

    def get_context_data(self, **kwargs):
643
        ctx = super(VmMigrateView, self).get_context_data(**kwargs)
644 645 646 647 648
        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):
649 650
        if extra is None:
            extra = {}
651 652 653 654 655 656 657
        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)


658
class VmSaveView(FormOperationMixin, VmOperationView):
659 660 661

    op = 'save_as_template'
    icon = 'save'
662
    form_class = VmSaveForm
663

664 665 666
vm_ops = {
    'reset': VmOperationView.factory(op='reset', icon='bolt'),
    'deploy': VmOperationView.factory(op='deploy', icon='play'),
667
    'migrate': VmMigrateView,
668 669 670
    'reboot': VmOperationView.factory(op='reboot', icon='refresh'),
    'shut_off': VmOperationView.factory(op='shut_off', icon='ban-circle'),
    'shutdown': VmOperationView.factory(op='shutdown', icon='off'),
671
    'save_as_template': VmSaveView,
672 673 674
    'destroy': VmOperationView.factory(op='destroy', icon='remove'),
    'sleep': VmOperationView.factory(op='sleep', icon='moon'),
    'wake_up': VmOperationView.factory(op='wake_up', icon='sun'),
675 676
    'create_disk': VmCreateDiskView,
    'download_disk': VmDownloadDiskView,
677 678 679 680 681 682 683 684 685 686
}


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()
687 688 689
        except Exception as e:
            logger.debug('Not showing operation %s for %s: %s',
                         k, instance, unicode(e))
690 691 692
        else:
            ops.append(v.bind_to_object(instance))
    return ops
693

Kálmán Viktor committed
694

695
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
696 697
    template_name = "dashboard/node-detail.html"
    model = Node
698 699
    form = None
    form_class = TraitForm
700

701 702 703
    def get_context_data(self, form=None, **kwargs):
        if form is None:
            form = self.form_class()
704
        context = super(NodeDetailView, self).get_context_data(**kwargs)
705 706
        instances = Instance.active.filter(node=self.object)
        context['table'] = NodeVmListTable(instances)
707 708 709 710
        na = NodeActivity.objects.filter(
            node=self.object, parent=None
        ).order_by('-started').select_related()
        context['activities'] = na
711
        context['trait_form'] = form
712
        context['graphite_enabled'] = (
Bach Dániel committed
713
            settings.GRAPHITE_URL is not None)
714 715
        return context

716 717 718
    def post(self, request, *args, **kwargs):
        if request.POST.get('new_name'):
            return self.__set_name(request)
719 720
        if request.POST.get('to_remove'):
            return self.__remove_trait(request)
721 722
        return redirect(reverse_lazy("dashboard.views.node-detail",
                                     kwargs={'pk': self.get_object().pk}))
723 724 725 726 727 728 729

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

730
        success_message = _("Node successfully renamed.")
731 732 733 734 735
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
                'node_pk': self.object.pk
736 737 738 739 740 741 742 743 744 745
            }
            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}))

746 747 748 749
    def __remove_trait(self, request):
        try:
            to_remove = request.POST.get('to_remove')
            self.object = self.get_object()
750
            self.object.traits.remove(to_remove)
751 752 753 754 755 756 757
            message = u"Success"
        except:  # note this won't really happen
            message = u"Not success"

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': message}),
758
                content_type="application/json"
759
            )
760
        else:
761
            return redirect(self.object.get_absolute_url())
762

763

764
class GroupDetailView(CheckedDetailView):
765 766
    template_name = "dashboard/group-detail.html"
    model = Group
767
    read_level = 'operator'
768 769 770

    def get_has_level(self):
        return self.object.profile.has_level
771 772 773

    def get_context_data(self, **kwargs):
        context = super(GroupDetailView, self).get_context_data(**kwargs)
774 775 776
        context['group'] = self.object
        context['users'] = self.object.user_set.all()
        context['acl'] = get_group_acl_data(self.object)
777 778
        context['group_profile_form'] = GroupProfileUpdate.get_form_object(
            self.request, self.object.profile)
779 780 781
        return context

    def post(self, request, *args, **kwargs):
782 783 784
        self.object = self.get_object()
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
785 786
        if request.POST.get('new_name'):
            return self.__set_name(request)
787
        if request.POST.get('list-new-name'):
788
            return self.__add_user(request)
789
        if request.POST.get('list-new-namelist'):
790
            return self.__add_list(request)
791 792 793 794
        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}))
795 796

    def __add_user(self, request):
797
        name = request.POST['list-new-name']
798 799 800
        self.__add_username(request, name)
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
801 802

    def __add_username(self, request, name):
803
        if not name:
804
            return
805 806
        try:
            entity = User.objects.get(username=name)
807
            self.object.user_set.add(entity)
808 809
        except User.DoesNotExist:
            warning(request, _('User "%s" not found.') % name)
810

811
    def __add_list(self, request):
812 813
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
814 815 816
        userlist = request.POST.get('list-new-namelist').split('\r\n')
        for line in userlist:
            self.__add_username(request, line)
817 818
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
819 820 821 822 823 824

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

825
        success_message = _("Group successfully renamed.")
826 827 828 829
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
830
                'group_pk': self.object.pk
831 832 833 834 835 836 837 838 839 840 841
            }
            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}))


842
class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
843

844
    def post(self, request, *args, **kwargs):
845
        instance = self.get_object()
846 847
        if not (instance.has_level(request.user, "owner") or
                getattr(instance, 'owner', None) == request.user):
Őry Máté committed
848 849
            logger.warning('Tried to set permissions of %s by non-owner %s.',
                           unicode(instance), unicode(request.user))
850
            raise PermissionDenied()
851
        self.set_levels(request, instance)
852
        self.remove_levels(request, instance)
853
        self.add_levels(request, instance)
854
        return redirect("%s#access" % instance.get_absolute_url())
855 856

    def set_levels(self, request, instance):
857 858 859
        for key, value in request.POST.items():
            m = re.match('perm-([ug])-(\d+)', key)
            if m:
860 861
                typ, id = m.groups()
                entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
862
                if getattr(instance, "owner", None) == entity:
863 864 865
                    logger.info("Tried to set owner's acl level for %s by %s.",
                                unicode(instance), unicode(request.user))
                    continue
866
                instance.set_level(entity, value)
Őry Máté committed
867 868 869
                logger.info("Set %s's acl level for %s to %s by %s.",
                            unicode(entity), unicode(instance),
                            value, unicode(request.user))
870

871 872 873 874 875 876 877 878 879 880
    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 "
881
                            "you can transfer ownership.")
882 883 884 885 886 887 888
                    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))

889
    def add_levels(self, request, instance):
890 891
        name = request.POST['perm-new-name']
        value = request.POST['perm-new']
892 893 894 895 896 897
        if not name:
            return
        try:
            entity = User.objects.get(username=name)
        except User.DoesNotExist:
            entity = None
898 899
            try:
                entity = Group.objects.get(name=name)
900 901 902 903
            except Group.DoesNotExist:
                warning(request, _('User or group "%s" not found.') % name)
                return

904
        instance.set_level(entity, value)
Őry Máté committed
905 906 907
        logger.info("Set %s's new acl level for %s to %s by %s.",
                    unicode(entity), unicode(instance),
                    value, unicode(request.user))
908

Kálmán Viktor committed
909

910 911 912 913 914 915 916 917 918 919
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()
920 921 922 923 924 925 926 927 928

        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)
929
            self.remove_levels(request, template)
930 931 932 933 934

            post_for_disk = request.POST.copy()
            post_for_disk['perm-new'] = 'user'
            request.POST = post_for_disk
            for d in template.disks.all():
935
                self.set_levels(request, d)
936
                self.add_levels(request, d)
937
                self.remove_levels(request, d)
938

939
        return redirect(template)
940 941


942 943 944 945 946 947 948 949 950 951
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
952 953 954

        self.set_levels(request, instance)
        self.add_levels(request, instance)
955 956 957 958
        return redirect(reverse("dashboard.views.group-detail",
                                kwargs=self.kwargs))


959 960 961 962 963 964 965 966 967 968
class TemplateChoose(TemplateView):

    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)
969
        templates = InstanceTemplate.get_objects_with_level("user",
970 971 972 973
                                                            self.request.user)
        context.update({
            'box_title': _('Choose template'),
            'ajax_title': False,
974
            'template': "dashboard/_template-choose.html",
975
            'templates': templates.all(),
976 977 978
        })
        return context

979 980 981 982 983 984 985
    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"))
986
        elif template is None:
987
            messages.warning(request, _("Select an option to proceed."))
988
            return redirect(reverse("dashboard.views.template-choose"))
989 990 991 992 993 994 995 996
        else:
            template = get_object_or_404(InstanceTemplate, pk=template)

        instance = Instance.create_from_template(
            template=template, owner=request.user, is_base=True)

        return redirect(instance.get_absolute_url())

997

998 999 1000 1001
class TemplateCreate(SuccessMessageMixin, CreateView):
    model = InstanceTemplate
    form_class = TemplateForm

1002 1003
    def get_template_names(self):
        if self.request.is_ajax():
1004
            pass
1005 1006 1007 1008 1009 1010 1011
        else:
            return ['dashboard/nojs-wrapper.html']

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

        context.update({
1012
            'box_title': _("Create a new base VM"),
1013
            'template': "dashboard/_template-create.html",
1014
            'leases': Lease.objects.count()
1015 1016 1017
        })
        return context

1018
    def get(self, *args, **kwargs):
1019 1020
        if not self.request.user.has_perm('vm.create_template'):
            raise PermissionDenied()
1021

1022 1023 1024 1025
        return super(TemplateCreate, self).get(*args, **kwargs)

    def get_form_kwargs(self):
        kwargs = super(TemplateCreate, self).get_form_kwargs()
1026
        kwargs['user'] = self.request.user
1027 1028
        return kwargs

1029 1030 1031
    def post(self, request, *args, **kwargs):
        if not self.request.user.has_perm('vm.create_template'):
            raise PermissionDenied()
1032 1033

        form = self.form_class(request.POST, user=request.user)
1034 1035
        if not form.is_valid():
            return self.get(request, form, *args, **kwargs)
1036
        else:
1037
            post = form.cleaned_data
1038 1039
            networks = self.__create_networks(post.pop("networks"),
                                              request.user)
1040
            post.pop("parent")
1041
            post['max_ram_size'] = post['ram_size']
1042 1043 1044 1045 1046 1047 1048
            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)
1049

1050
            return redirect("%s#resources" % inst.get_absolute_url())
1051

1052 1053
        return super(TemplateCreate, self).post(self, request, args, kwargs)

1054
    def __create_networks(self, vlans, user):
1055 1056
        networks = []
        for v in vlans:
1057 1058
            if not v.has_level(user, "user"):
                raise PermissionDenied()
1059 1060 1061
            networks.append(InterfaceTemplate(vlan=v, managed=v.managed))
        return networks

1062 1063 1064 1065
    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-list")


1066
class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
1067
    model = InstanceTemplate
1068 1069
    template_name = "dashboard/template-edit.html"
    form_class = TemplateForm
1070
    success_message = _("Successfully modified template.")
1071 1072

    def get(self, request, *args, **kwargs):
1073
        template = self.get_object()
1074
        if not template.has_level(request.user, 'user'):
1075
            raise PermissionDenied()
1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096
        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:
1097 1098
            return super(TemplateDetail, self).get(request, *args, **kwargs)

1099
    def get_context_data(self, **kwargs):
1100
        obj = self.get_object()
1101
        context = super(TemplateDetail, self).get_context_data(**kwargs)
1102 1103
        context['acl'] = get_vm_acl_data(obj)
        context['disks'] = obj.disks.all()
1104 1105
        return context

1106 1107 1108 1109
    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-detail",
                            kwargs=self.kwargs)

1110 1111 1112 1113 1114 1115 1116
    def post(self, request, *args, **kwargs):
        template = self.get_object()
        if not template.has_level(request.user, 'owner'):
            raise PermissionDenied()
        for disk in self.get_object().disks.all():
            if not disk.has_level(request.user, 'user'):
                raise PermissionDenied()
1117 1118 1119
        for network in self.get_object().interface_set.all():
            if not network.vlan.has_level(request.user, "user"):
                raise PermissionDenied()
1120 1121
        return super(TemplateDetail, self).post(self, request, args, kwargs)

1122 1123 1124 1125 1126
    def get_form_kwargs(self):
        kwargs = super(TemplateDetail, self).get_form_kwargs()
        kwargs['user'] = self.request.user
        return kwargs

1127

1128
class TemplateList(LoginRequiredMixin, SingleTableView):
1129 1130 1131 1132 1133 1134 1135
    template_name = "dashboard/template-list.html"
    model = InstanceTemplate
    table_class = TemplateListTable
    table_pagination = False

    def get_context_data(self, *args, **kwargs):
        context = super(TemplateList, self).get_context_data(*args, **kwargs)
1136 1137
        context['lease_table'] = LeaseListTable(Lease.objects.all(),
                                                request=self.request)
1138
        return context
1139

1140 1141 1142 1143 1144 1145
    def get_queryset(self):
        logger.debug('TemplateList.get_queryset() called. User: %s',
                     unicode(self.request.user))
        return InstanceTemplate.get_objects_with_level(
            'user', self.request.user).all()

1146

1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163
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()

1164
        object.destroy_disks()
1165 1166
        object.delete()
        success_url = self.get_success_url()
1167
        success_message = _("Template successfully deleted.")
1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178

        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)


1179
class VmList(LoginRequiredMixin, FilterMixin, ListView):
Kálmán Viktor committed
1180
    template_name = "dashboard/vm-list.html"
1181 1182 1183 1184
    allowed_filters = {
        'name': "name__icontains",
        'node': "node__name__icontains",
        'status': "status__iexact",
1185 1186
        'tags[]': "tags__name__in",
        'tags': "tags__name__in",  # for search string
Kálmán Viktor committed
1187
        'owner': "owner__username",
1188
    }
1189

1190 1191
    def get(self, *args, **kwargs):
        if self.request.is_ajax():
1192 1193 1194 1195
            favs = Instance.objects.filter(
                favourite__user=self.request.user).values_list('pk', flat=True)
            instances = Instance.get_objects_with_level(