views.py 103 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 itertools import chain
21 22
from os import getenv
import json
Őry Máté committed
23
import logging
24
import re
25
from hashlib import md5
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.defaultfilters import title as title_filter
49
from django.template.loader import render_to_string
50
from django.template import RequestContext
51
from django.templatetags.static import static
52

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

59
from .forms import (
60
    CircleAuthenticationForm, DiskAddForm, HostForm, LeaseForm, MyProfileForm,
61
    NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm,
62
    UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
63
    CirclePasswordChangeForm
64
)
65 66 67 68

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

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

81

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


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

        return newgroups


125
class FilterMixin(object):
126

127 128 129 130 131 132
    def get_queryset_filters(self):
        filters = {}
        for item in self.allowed_filters:
            if item in self.request.GET:
                filters[self.allowed_filters[item]] = self.request.GET[item]
        return filters
133

134 135 136
    def get_queryset(self):
        return super(FilterMixin,
                     self).get_queryset().filter(**self.get_queryset_filters())
137 138


139
class IndexView(LoginRequiredMixin, TemplateView):
Kálmán Viktor committed
140
    template_name = "dashboard/index.html"
141

142
    def get_context_data(self, **kwargs):
143
        user = self.request.user
144
        context = super(IndexView, self).get_context_data(**kwargs)
145

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

158 159
        running = instances.filter(status='RUNNING')
        stopped = instances.exclude(status__in=('RUNNING', 'NOSTATE'))
160

161
        context.update({
162 163 164
            'running_vms': running[:20],
            'running_vm_num': running.count(),
            'stopped_vm_num': stopped.count()
165
        })
166

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

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

196 197
        return context

198

199
def get_vm_acl_data(obj):
200 201 202 203 204 205 206 207 208
    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])}


209 210 211 212 213 214 215 216 217 218 219
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])}


220
class CheckedDetailView(LoginRequiredMixin, DetailView):
221 222
    read_level = 'user'

223 224 225
    def get_has_level(self):
        return self.object.has_level

226 227
    def get_context_data(self, **kwargs):
        context = super(CheckedDetailView, self).get_context_data(**kwargs)
228
        if not self.get_has_level()(self.request.user, self.read_level):
229 230 231 232
            raise PermissionDenied()
        return context


233 234 235 236 237 238 239 240 241
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:
242 243 244 245 246 247 248 249
            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)
250 251 252 253
        else:
            raise Http404()


254
class VmDetailView(CheckedDetailView):
Kálmán Viktor committed
255
    template_name = "dashboard/vm-detail.html"
256
    model = Instance
257 258

    def get_context_data(self, **kwargs):
259
        context = super(VmDetailView, self).get_context_data(**kwargs)
260
        instance = context['instance']
261 262 263
        context.update({
            'graphite_enabled': VmGraphView.get_graphite_url() is not None,
            'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
264
                                    kwargs={'pk': self.object.pk}),
265
            'ops': get_operations(instance, self.request.user),
266
        })
267 268

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

271
        context['vlans'] = Vlan.get_objects_with_level(
272
            'user', self.request.user
273
        ).exclude(  # exclude already added interfaces
274 275 276
            pk__in=Interface.objects.filter(
                instance=self.get_object()).values_list("vlan", flat=True)
        ).all()
277
        context['acl'] = get_vm_acl_data(instance)
278
        context['forms'] = {
279
            'disk_add_form': DiskAddForm(
280
                user=self.request.user,
281
                is_template=False, object_pk=self.get_object().pk,
282
                prefix="disk"),
283
        }
284 285
        context['os_type_icon'] = instance.os_type.replace("unknown",
                                                           "question")
286 287 288
        # ipv6 infos
        context['ipv6_host'] = instance.get_connect_host(use_ipv6=True)
        context['ipv6_port'] = instance.get_connect_port(use_ipv6=True)
289
        return context
Kálmán Viktor committed
290

291 292 293 294 295
    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)

296 297 298
        options = {
            'change_password': self.__change_password,
            'new_name': self.__set_name,
299
            'new_description': self.__set_description,
300 301
            'new_tag': self.__add_tag,
            'to_remove': self.__remove_tag,
302 303
            'port': self.__add_port,
            'new_network_vlan': self.__new_network,
304
            'abort_operation': self.__abort_operation,
305 306 307 308
        }
        for k, v in options.iteritems():
            if request.POST.get(k) is not None:
                return v(request)
Kálmán Viktor committed
309

310 311 312 313
    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
314

315
        self.object.change_password(user=request.user)
316
        messages.success(request, _("Password changed."))
317
        if request.is_ajax():
318
            return HttpResponse("Success.")
319 320 321
        else:
            return redirect(reverse_lazy("dashboard.views.detail",
                                         kwargs={'pk': self.object.pk}))
322

323 324 325 326
    def __set_resources(self, request):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
327 328
        if not request.user.has_perm('vm.change_resources'):
            raise PermissionDenied()
329 330 331 332

        resources = {
            'num_cores': request.POST.get('cpu-count'),
            'ram_size': request.POST.get('ram-size'),
333
            'max_ram_size': request.POST.get('ram-size'),  # TODO: max_ram
334 335 336 337
            'priority': request.POST.get('cpu-priority')
        }
        Instance.objects.filter(pk=self.object.pk).update(**resources)

338
        success_message = _("Resources successfully updated.")
339 340 341 342 343 344 345 346 347 348 349
        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}))

350 351
    def __set_name(self, request):
        self.object = self.get_object()
352 353
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
354 355 356 357
        new_name = request.POST.get("new_name")
        Instance.objects.filter(pk=self.object.pk).update(
            **{'name': new_name})

358
        success_message = _("VM successfully renamed.")
359 360 361 362 363 364 365 366 367 368 369 370
        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)
371 372 373 374 375 376 377 378 379 380 381
            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})

382
        success_message = _("VM description successfully updated.")
383 384 385 386 387 388 389 390 391 392 393 394
        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())
395

Kálmán Viktor committed
396 397 398
    def __add_tag(self, request):
        new_tag = request.POST.get('new_tag')
        self.object = self.get_object()
399 400
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
Kálmán Viktor committed
401 402

        if len(new_tag) < 1:
403
            message = u"Please input something."
Kálmán Viktor committed
404
        elif len(new_tag) > 20:
405
            message = u"Tag name is too long."
Kálmán Viktor committed
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420
        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()
421 422
            if not self.object.has_level(request.user, 'owner'):
                raise PermissionDenied()
Kálmán Viktor committed
423 424 425 426 427 428 429 430 431 432 433

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

438 439
    def __add_port(self, request):
        object = self.get_object()
440 441
        if (not object.has_level(request.user, 'owner') or
                not request.user.has_perm('vm.config_ports')):
442
            raise PermissionDenied()
443 444 445 446 447 448

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

        try:
            error = None
449 450 451
            interfaces = object.interface_set.all()
            host = Host.objects.get(pk=request.POST.get("host_pk"),
                                    interface__in=interfaces)
452
            host.add_port(proto, private=port)
453 454 455 456 457
        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()
458
        except ValueError:
459
            error = _("There is a problem with your input.")
460
        except Exception as e:
Bach Dániel committed
461 462
            error = _("Unknown error.")
            logger.error(e)
463 464 465 466 467 468 469 470 471

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

472 473 474 475 476
    def __new_network(self, request):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()

477
        vlan = get_object_or_404(Vlan, pk=request.POST.get("new_network_vlan"))
478 479
        if not vlan.has_level(request.user, 'user'):
            raise PermissionDenied()
480
        try:
481
            self.object.add_interface(vlan=vlan, user=request.user)
482
            messages.success(request, _("Successfully added new interface."))
483 484 485 486 487 488 489
        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}))

490
    def __abort_operation(self, request):
Kálmán Viktor committed
491 492 493 494
        self.object = self.get_object()

        activity = get_object_or_404(InstanceActivity,
                                     pk=request.POST.get("activity"))
495 496
        if not activity.is_abortable_for(request.user):
            raise PermissionDenied()
Kálmán Viktor committed
497 498 499
        activity.abort()
        return redirect("%s#activity" % self.object.get_absolute_url())

500

501
class OperationView(DetailView):
502

503
    template_name = 'dashboard/operate.html'
504

505 506 507
    @property
    def name(self):
        return self.get_op().name
508

509 510 511
    @property
    def description(self):
        return self.get_op().description
512

513 514 515
    @classmethod
    def get_urlname(cls):
        return 'dashboard.vm.op.%s' % cls.op
516

517 518
    def get_url(self):
        return reverse(self.get_urlname(), args=(self.get_object().pk, ))
519

520 521 522 523 524
    def get_wrapper_template_name(self):
        if self.request.is_ajax():
            return 'dashboard/_modal.html'
        else:
            return 'dashboard/_base.html'
525

526 527 528
    @classmethod
    def get_op_by_object(cls, obj):
        return getattr(obj, cls.op)
529

530 531 532 533
    def get_op(self):
        if not hasattr(self, '_opobj'):
            setattr(self, '_opobj', getattr(self.get_object(), self.op))
        return self._opobj
534

535
    def get_context_data(self, **kwargs):
536
        ctx = super(OperationView, self).get_context_data(**kwargs)
537 538 539
        ctx['op'] = self.get_op()
        ctx['url'] = self.request.path
        return ctx
540

541 542 543 544 545 546 547
    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
548

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

560 561 562 563
    @classmethod
    def factory(cls, op, icon='cog'):
        return type(str(cls.__name__ + op),
                    (cls, ), {'op': op, 'icon': icon})
564

565 566 567 568 569 570 571 572 573 574
    @classmethod
    def bind_to_object(cls, instance):
        v = cls()
        v.get_object = lambda: instance
        return v


class VmOperationView(OperationView):

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

577

578 579 580 581 582 583 584
class VmMigrateView(VmOperationView):

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

    def get_context_data(self, **kwargs):
585
        ctx = super(VmMigrateView, self).get_context_data(**kwargs)
586 587 588 589 590
        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):
591 592
        if extra is None:
            extra = {}
593 594 595 596 597 598 599
        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)


600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619
class VmSaveView(VmOperationView):

    op = 'save_as_template'
    icon = 'save'
    template_name = 'dashboard/_vm-save.html'

    def get_context_data(self, **kwargs):
        ctx = super(VmSaveView, self).get_context_data(**kwargs)
        ctx['name'] = self.get_op()._rename(self.object.name)
        return ctx

    def post(self, request, extra=None, *args, **kwargs):
        if extra is None:
            extra = {}
        name = self.request.POST.get("name")
        if name:
            extra["name"] = name
        return super(VmSaveView, self).post(request, extra, *args, **kwargs)


620 621 622
vm_ops = {
    'reset': VmOperationView.factory(op='reset', icon='bolt'),
    'deploy': VmOperationView.factory(op='deploy', icon='play'),
623
    'migrate': VmMigrateView,
624 625 626
    'reboot': VmOperationView.factory(op='reboot', icon='refresh'),
    'shut_off': VmOperationView.factory(op='shut_off', icon='ban-circle'),
    'shutdown': VmOperationView.factory(op='shutdown', icon='off'),
627
    'save_as_template': VmSaveView,
628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645
    'destroy': VmOperationView.factory(op='destroy', icon='remove'),
    'sleep': VmOperationView.factory(op='sleep', icon='moon'),
    'wake_up': VmOperationView.factory(op='wake_up', icon='sun'),
}


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()
        except:
            pass  # unavailable
        else:
            ops.append(v.bind_to_object(instance))
    return ops
646

Kálmán Viktor committed
647

648
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
649 650
    template_name = "dashboard/node-detail.html"
    model = Node
651 652
    form = None
    form_class = TraitForm
653

654 655 656
    def get_context_data(self, form=None, **kwargs):
        if form is None:
            form = self.form_class()
657
        context = super(NodeDetailView, self).get_context_data(**kwargs)
658 659
        instances = Instance.active.filter(node=self.object)
        context['table'] = NodeVmListTable(instances)
660 661 662 663
        na = NodeActivity.objects.filter(
            node=self.object, parent=None
        ).order_by('-started').select_related()
        context['activities'] = na
664
        context['trait_form'] = form
665 666
        context['graphite_enabled'] = (
            NodeGraphView.get_graphite_url() is not None)
667 668
        return context

669 670 671
    def post(self, request, *args, **kwargs):
        if request.POST.get('new_name'):
            return self.__set_name(request)
672 673
        if request.POST.get('to_remove'):
            return self.__remove_trait(request)
674 675
        return redirect(reverse_lazy("dashboard.views.node-detail",
                                     kwargs={'pk': self.get_object().pk}))
676 677 678 679 680 681 682

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

683
        success_message = _("Node successfully renamed.")
684 685 686 687 688
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
                'node_pk': self.object.pk
689 690 691 692 693 694 695 696 697 698
            }
            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}))

699 700 701 702
    def __remove_trait(self, request):
        try:
            to_remove = request.POST.get('to_remove')
            self.object = self.get_object()
703
            self.object.traits.remove(to_remove)
704 705 706 707 708 709 710
            message = u"Success"
        except:  # note this won't really happen
            message = u"Not success"

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': message}),
711
                content_type="application/json"
712
            )
713
        else:
714
            return redirect(self.object.get_absolute_url())
715

716

717
class GroupDetailView(CheckedDetailView):
718 719
    template_name = "dashboard/group-detail.html"
    model = Group
720
    read_level = 'operator'
721 722 723

    def get_has_level(self):
        return self.object.profile.has_level
724 725 726

    def get_context_data(self, **kwargs):
        context = super(GroupDetailView, self).get_context_data(**kwargs)
727 728 729
        context['group'] = self.object
        context['users'] = self.object.user_set.all()
        context['acl'] = get_group_acl_data(self.object)
730 731
        context['group_profile_form'] = GroupProfileUpdate.get_form_object(
            self.request, self.object.profile)
732 733 734
        return context

    def post(self, request, *args, **kwargs):
735 736 737
        self.object = self.get_object()
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
738 739
        if request.POST.get('new_name'):
            return self.__set_name(request)
740
        if request.POST.get('list-new-name'):
741
            return self.__add_user(request)
742
        if request.POST.get('list-new-namelist'):
743
            return self.__add_list(request)
744 745 746 747
        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}))
748 749

    def __add_user(self, request):
750
        name = request.POST['list-new-name']
751 752 753
        self.__add_username(request, name)
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
754 755

    def __add_username(self, request, name):
756
        if not name:
757
            return
758 759
        try:
            entity = User.objects.get(username=name)
760
            self.object.user_set.add(entity)
761 762
        except User.DoesNotExist:
            warning(request, _('User "%s" not found.') % name)
763

764
    def __add_list(self, request):
765 766
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
767 768 769
        userlist = request.POST.get('list-new-namelist').split('\r\n')
        for line in userlist:
            self.__add_username(request, line)
770 771
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
772 773 774 775 776 777

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

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


795
class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
796

797
    def post(self, request, *args, **kwargs):
798
        instance = self.get_object()
799 800
        if not (instance.has_level(request.user, "owner") or
                getattr(instance, 'owner', None) == request.user):
Őry Máté committed
801 802
            logger.warning('Tried to set permissions of %s by non-owner %s.',
                           unicode(instance), unicode(request.user))
803
            raise PermissionDenied()
804
        self.set_levels(request, instance)
805
        self.remove_levels(request, instance)
806
        self.add_levels(request, instance)
807
        return redirect("%s#access" % instance.get_absolute_url())
808 809

    def set_levels(self, request, instance):
810 811 812
        for key, value in request.POST.items():
            m = re.match('perm-([ug])-(\d+)', key)
            if m:
813 814
                typ, id = m.groups()
                entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
815
                if getattr(instance, "owner", None) == entity:
816 817 818
                    logger.info("Tried to set owner's acl level for %s by %s.",
                                unicode(instance), unicode(request.user))
                    continue
819
                instance.set_level(entity, value)
Őry Máté committed
820 821 822
                logger.info("Set %s's acl level for %s to %s by %s.",
                            unicode(entity), unicode(instance),
                            value, unicode(request.user))
823

824 825 826 827 828 829 830 831 832 833
    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 "
834
                            "you can transfer ownership.")
835 836 837 838 839 840 841
                    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))

842
    def add_levels(self, request, instance):
843 844
        name = request.POST['perm-new-name']
        value = request.POST['perm-new']
845 846 847 848 849 850
        if not name:
            return
        try:
            entity = User.objects.get(username=name)
        except User.DoesNotExist:
            entity = None
851 852
            try:
                entity = Group.objects.get(name=name)
853 854 855 856
            except Group.DoesNotExist:
                warning(request, _('User or group "%s" not found.') % name)
                return

857
        instance.set_level(entity, value)
Őry Máté committed
858 859 860
        logger.info("Set %s's new acl level for %s to %s by %s.",
                    unicode(entity), unicode(instance),
                    value, unicode(request.user))
861

Kálmán Viktor committed
862

863 864 865 866 867 868 869 870 871 872
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()
873 874 875 876 877 878 879 880 881

        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)
882
            self.remove_levels(request, template)
883 884 885 886 887 888

            post_for_disk = request.POST.copy()
            post_for_disk['perm-new'] = 'user'
            request.POST = post_for_disk
            for d in template.disks.all():
                self.add_levels(request, d)
889

890
        return redirect(template)
891 892


893 894 895 896 897 898 899 900 901 902
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
903 904 905

        self.set_levels(request, instance)
        self.add_levels(request, instance)
906 907 908 909
        return redirect(reverse("dashboard.views.group-detail",
                                kwargs=self.kwargs))


910 911 912 913 914 915 916 917 918 919
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)
920
        templates = InstanceTemplate.get_objects_with_level("user",
921 922 923 924
                                                            self.request.user)
        context.update({
            'box_title': _('Choose template'),
            'ajax_title': False,
925
            'template': "dashboard/_template-choose.html",
926
            'templates': templates.all(),
927 928 929
        })
        return context

930 931 932 933 934 935 936
    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"))
937
        elif template is None:
938
            messages.warning(request, _("Select an option to proceed."))
939
            return redirect(reverse("dashboard.views.template-choose"))
940 941 942 943 944 945 946 947
        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())

948

949 950 951 952
class TemplateCreate(SuccessMessageMixin, CreateView):
    model = InstanceTemplate
    form_class = TemplateForm

953 954
    def get_template_names(self):
        if self.request.is_ajax():
955
            pass
956 957 958 959 960 961 962
        else:
            return ['dashboard/nojs-wrapper.html']

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

        context.update({
963
            'box_title': _("Create a new base VM"),
964
            'template': "dashboard/_template-create.html",
965
            'leases': Lease.objects.count()
966 967 968
        })
        return context

969
    def get(self, *args, **kwargs):
970 971
        if not self.request.user.has_perm('vm.create_template'):
            raise PermissionDenied()
972

973 974 975 976
        return super(TemplateCreate, self).get(*args, **kwargs)

    def get_form_kwargs(self):
        kwargs = super(TemplateCreate, self).get_form_kwargs()
977
        kwargs['user'] = self.request.user
978 979
        return kwargs

980 981 982
    def post(self, request, *args, **kwargs):
        if not self.request.user.has_perm('vm.create_template'):
            raise PermissionDenied()
983 984

        form = self.form_class(request.POST, user=request.user)
985 986
        if not form.is_valid():
            return self.get(request, form, *args, **kwargs)
987
        else:
988
            post = form.cleaned_data
989 990
            networks = self.__create_networks(post.pop("networks"),
                                              request.user)
991
            post.pop("parent")
992
            post['max_ram_size'] = post['ram_size']
993 994 995 996 997 998 999
            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)
1000

1001
            return redirect("%s#resources" % inst.get_absolute_url())
1002

1003 1004
        return super(TemplateCreate, self).post(self, request, args, kwargs)

1005
    def __create_networks(self, vlans, user):
1006 1007
        networks = []
        for v in vlans:
1008 1009
            if not v.has_level(user, "user"):
                raise PermissionDenied()
1010 1011 1012
            networks.append(InterfaceTemplate(vlan=v, managed=v.managed))
        return networks

1013 1014 1015 1016
    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-list")


1017
class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
1018
    model = InstanceTemplate
1019 1020
    template_name = "dashboard/template-edit.html"
    form_class = TemplateForm
1021
    success_message = _("Successfully modified template.")
1022 1023

    def get(self, request, *args, **kwargs):
1024
        template = self.get_object()
1025
        if not template.has_level(request.user, 'user'):
1026
            raise PermissionDenied()
1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047
        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:
1048 1049
            return super(TemplateDetail, self).get(request, *args, **kwargs)

1050
    def get_context_data(self, **kwargs):
1051
        obj = self.get_object()
1052
        context = super(TemplateDetail, self).get_context_data(**kwargs)
1053 1054
        context['acl'] = get_vm_acl_data(obj)
        context['disks'] = obj.disks.all()
1055
        context['disk_add_form'] = DiskAddForm(
1056
            user=self.request.user,
1057
            is_template=True,
1058
            object_pk=obj.pk,
1059 1060
            prefix="disk",
        )
1061 1062
        return context

1063 1064 1065 1066
    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-detail",
                            kwargs=self.kwargs)

1067 1068 1069 1070 1071 1072 1073
    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()
1074 1075 1076
        for network in self.get_object().interface_set.all():
            if not network.vlan.has_level(request.user, "user"):
                raise PermissionDenied()
1077 1078
        return super(TemplateDetail, self).post(self, request, args, kwargs)

1079 1080 1081 1082 1083
    def get_form_kwargs(self):
        kwargs = super(TemplateDetail, self).get_form_kwargs()
        kwargs['user'] = self.request.user
        return kwargs

1084

1085
class TemplateList(LoginRequiredMixin, SingleTableView):
1086 1087 1088 1089 1090 1091 1092
    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)
1093 1094
        context['lease_table'] = LeaseListTable(Lease.objects.all(),
                                                request=self.request)
1095
        return context
1096

1097 1098 1099 1100 1101 1102
    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()

1103

1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122
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()

        object.delete()
        success_url = self.get_success_url()
1123
        success_message = _("Template successfully deleted.")
1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134

        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)


1135
class VmList(LoginRequiredMixin, FilterMixin, ListView):
Kálmán Viktor committed
1136
    template_name = "dashboard/vm-list.html"
1137 1138 1139 1140 1141
    allowed_filters = {
        'name': "name__icontains",
        'node': "node__name__icontains",
        'status': "status__iexact",
        'tags': "tags__name__in",  # note: use it as ?tags[]=a,b
Kálmán Viktor committed
1142
        'owner': "owner__username",
1143
    }
1144

1145 1146
    def get(self, *args, **kwargs):
        if self.request.is_ajax():
1147 1148 1149 1150
            favs = Instance.objects.filter(
                favourite__user=self.request.user).values_list('pk', flat=True)
            instances = Instance.get_objects_with_level(
                'user', self.request.user).filter(
1151
                destroyed_at=None).all()
1152 1153 1154
            instances = [{
                'pk': i.pk,
                'name': i.name,
1155 1156 1157
                'icon': i.get_status_icon(),
                'host': "" if not i.primary_host else i.primary_host.hostname,
                'status': i.get_status_display(),
1158
                'fav': i.pk in favs} for i in instances]
1159
            return HttpResponse(
1160
                json.dumps(list(instances)),  # instances is ValuesQuerySet
1161 1162 1163 1164 1165
                content_type="application/json",
            )
        else:
            return super(VmList, self).get(*args, **kwargs)

1166
    def get_queryset(self):
1167
        logger.debug('VmList.get_queryset() called. User: %s',
1168
                     unicode(self.request.user))
1169
        queryset = Instance.get_objects_with_level(
1170
            'user', self.request.user).filter(destroyed_at=None)
1171

1172
        self.create_fake_get()
1173 1174 1175 1176
        sort = self.request.GET.get("sort")
        # remove "-" that means descending order
        # also check if the column name is valid
        if (sort and
1177
            (sort[1:] if sort[0] == "-" else sort)
1178
                in [i.name for i in Instance._meta.fields] + ["pk"]):
1179
            queryset = queryset.order_by(sort)
1180 1181
        return queryset.filter(**self.get_queryset_filters()
                               ).select_related('owner', 'node')