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

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

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

55
from .forms import (
56
    CircleAuthenticationForm, DiskAddForm, HostForm, LeaseForm, MyProfileForm,
57
    NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm,
58
    UserCreationForm, GroupProfileUpdateForm,
59
    CirclePasswordChangeForm
60
)
61 62 63 64

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

Őry Máté committed
74
logger = logging.getLogger(__name__)
75
saml_available = hasattr(settings, "SAML_CONFIG")
76

77

78 79 80 81
def search_user(keyword):
    try:
        return User.objects.get(username=keyword)
    except User.DoesNotExist:
82 83 84 85
        try:
            return User.objects.get(profile__org_id=keyword)
        except User.DoesNotExist:
            return User.objects.get(email=keyword)
86 87


88 89 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 115 116 117 118 119 120 121 122 123 124 125
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', [])
                groups = []
                for i in owneratrs:
                    try:
                        groups += attributes[i]
                    except KeyError:
                        pass
                for group in groups:
                    try:
                        GroupProfile.search(group)
                    except Group.DoesNotExist:
                        newgroups.append(group)

        return newgroups


126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
# github.com/django/django/blob/stable/1.6.x/django/contrib/messages/views.py
class SuccessMessageMixin(object):
    """
    Adds a success message on successful form submission.
    """
    success_message = ''

    def form_valid(self, form):
        response = super(SuccessMessageMixin, self).form_valid(form)
        success_message = self.get_success_message(form.cleaned_data)
        if success_message:
            messages.success(self.request, success_message)
        return response

    def get_success_message(self, cleaned_data):
        return self.success_message % cleaned_data


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

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

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

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

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

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

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

201 202
        return context

203

204
def get_vm_acl_data(obj):
205 206 207 208 209 210 211 212 213
    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])}


214 215 216 217 218 219 220 221 222 223 224
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])}


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

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

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


238 239 240 241 242 243 244 245 246
class VmDetailVncTokenView(CheckedDetailView):
    template_name = "dashboard/vm-detail.html"
    model = Instance

    def get(self, request, **kwargs):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'operator'):
            raise PermissionDenied()
        if self.object.node:
247 248 249 250 251 252 253 254
            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)
255 256 257 258
        else:
            raise Http404()


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

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

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

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

293 294 295 296 297
    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)

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

312 313 314 315
    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
316

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

502

503
class OperationView(DetailView):
504

505
    template_name = 'dashboard/operate.html'
506

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

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

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

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

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

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

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

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

543 544 545 546 547 548 549
    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
550

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

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

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


class VmOperationView(OperationView):

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

579

580 581 582 583 584 585 586
class VmMigrateView(VmOperationView):

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

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


602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621
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)


622 623 624
vm_ops = {
    'reset': VmOperationView.factory(op='reset', icon='bolt'),
    'deploy': VmOperationView.factory(op='deploy', icon='play'),
625
    'migrate': VmMigrateView,
626 627 628
    'reboot': VmOperationView.factory(op='reboot', icon='refresh'),
    'shut_off': VmOperationView.factory(op='shut_off', icon='ban-circle'),
    'shutdown': VmOperationView.factory(op='shutdown', icon='off'),
629
    'save_as_template': VmSaveView,
630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647
    '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
648

Kálmán Viktor committed
649

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

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

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

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

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

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

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

718

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

    def get_has_level(self):
        return self.object.profile.has_level
726 727 728

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

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

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

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

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

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

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


797
class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
798

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

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

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

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

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

Kálmán Viktor committed
864

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

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

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

892
        return redirect(template)
893 894


895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916
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()
        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, instance)
            self.add_levels(request, instance)
        return redirect(reverse("dashboard.views.group-detail",
                                kwargs=self.kwargs))


917 918 919 920 921 922 923 924 925 926
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)
927
        templates = InstanceTemplate.get_objects_with_level("user",
928 929 930 931
                                                            self.request.user)
        context.update({
            'box_title': _('Choose template'),
            'ajax_title': False,
932
            'template': "dashboard/_template-choose.html",
933
            'templates': templates.all(),
934 935 936
        })
        return context

937 938 939 940 941 942 943
    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"))
944
        elif template is None:
945
            messages.warning(request, _("Select an option to proceed."))
946
            return redirect(reverse("dashboard.views.template-choose"))
947 948 949 950 951 952 953 954
        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())

955

956 957 958 959
class TemplateCreate(SuccessMessageMixin, CreateView):
    model = InstanceTemplate
    form_class = TemplateForm

960 961
    def get_template_names(self):
        if self.request.is_ajax():
962
            pass
963 964 965 966 967 968 969
        else:
            return ['dashboard/nojs-wrapper.html']

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

        context.update({
970
            'box_title': _("Create a new base VM"),
971
            'template': "dashboard/_template-create.html",
972
            'leases': Lease.objects.count()
973 974 975
        })
        return context

976
    def get(self, *args, **kwargs):
977 978
        if not self.request.user.has_perm('vm.create_template'):
            raise PermissionDenied()
979

980 981 982 983
        return super(TemplateCreate, self).get(*args, **kwargs)

    def get_form_kwargs(self):
        kwargs = super(TemplateCreate, self).get_form_kwargs()
984
        kwargs['user'] = self.request.user
985 986
        return kwargs

987 988 989
    def post(self, request, *args, **kwargs):
        if not self.request.user.has_perm('vm.create_template'):
            raise PermissionDenied()
990 991

        form = self.form_class(request.POST, user=request.user)
992 993
        if not form.is_valid():
            return self.get(request, form, *args, **kwargs)
994
        else:
995
            post = form.cleaned_data
996 997
            networks = self.__create_networks(post.pop("networks"),
                                              request.user)
998
            post.pop("parent")
999
            post['max_ram_size'] = post['ram_size']
1000 1001 1002 1003 1004 1005 1006
            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)
1007

1008
            return redirect("%s#resources" % inst.get_absolute_url())
1009

1010 1011
        return super(TemplateCreate, self).post(self, request, args, kwargs)

1012
    def __create_networks(self, vlans, user):
1013 1014
        networks = []
        for v in vlans:
1015 1016
            if not v.has_level(user, "user"):
                raise PermissionDenied()
1017 1018 1019
            networks.append(InterfaceTemplate(vlan=v, managed=v.managed))
        return networks

1020 1021 1022 1023
    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-list")


1024
class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
1025
    model = InstanceTemplate
1026 1027
    template_name = "dashboard/template-edit.html"
    form_class = TemplateForm
1028
    success_message = _("Successfully modified template.")
1029 1030

    def get(self, request, *args, **kwargs):
1031
        template = self.get_object()
1032
        if not template.has_level(request.user, 'user'):
1033
            raise PermissionDenied()
1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054
        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:
1055 1056
            return super(TemplateDetail, self).get(request, *args, **kwargs)

1057
    def get_context_data(self, **kwargs):
1058
        obj = self.get_object()
1059
        context = super(TemplateDetail, self).get_context_data(**kwargs)
1060 1061
        context['acl'] = get_vm_acl_data(obj)
        context['disks'] = obj.disks.all()
1062
        context['disk_add_form'] = DiskAddForm(
1063
            user=self.request.user,
1064
            is_template=True,
1065
            object_pk=obj.pk,
1066 1067
            prefix="disk",
        )
1068 1069
        return context

1070 1071 1072 1073
    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-detail",
                            kwargs=self.kwargs)

1074 1075 1076 1077 1078 1079 1080
    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()
1081 1082 1083
        for network in self.get_object().interface_set.all():
            if not network.vlan.has_level(request.user, "user"):
                raise PermissionDenied()
1084 1085
        return super(TemplateDetail, self).post(self, request, args, kwargs)

1086 1087 1088 1089 1090
    def get_form_kwargs(self):
        kwargs = super(TemplateDetail, self).get_form_kwargs()
        kwargs['user'] = self.request.user
        return kwargs

1091

1092
class TemplateList(LoginRequiredMixin, SingleTableView):
1093 1094 1095 1096 1097 1098 1099
    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)
1100 1101
        context['lease_table'] = LeaseListTable(Lease.objects.all(),
                                                request=self.request)
1102
        return context
1103

1104 1105 1106 1107 1108 1109
    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()

1110

1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129
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()
1130
        success_message = _("Template successfully deleted.")
1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141

        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)


1142
class VmList(LoginRequiredMixin, ListView):
Kálmán Viktor committed
1143
    template_name = "dashboard/vm-list.html"
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 1173
        s = self.request.GET.get("s")
        if s:
            queryset = queryset.filter(name__icontains=s)
1174 1175 1176 1177 1178

        sort = self.request.GET.get("sort")
        # remove "-" that means descending order
        # also check if the column name is valid
        if (sort and
1179
            (sort[1:] if sort[0] == "-" else sort)
1180
                in [i.name for i in Instance._meta.fields] + ["pk"]):
1181
            queryset = queryset.order_by(sort)
1182
        return queryset.select_related('owner', 'node')
1183

1184

1185
class NodeList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
1186 1187 1188
    template_name = "dashboard/node-list.html"
    table_class = NodeListTable
    table_pagination = False
1189

1190 1191 1192 1193 1194
    def get(self, *args, **kwargs):
        if self.request.is_ajax():
            nodes = Node.objects.all()
            nodes = [{
                'name': i.name,
1195 1196
                'icon': i.get_status_icon(),
                'url': i.get_absolute_url(),
1197
                'label': i.get_status_label(),
1198
                'status': i.state.lower()} for i in nodes]
1199 1200

            return HttpResponse(
1201
                json.dumps(list(nodes)),
1202 1203 1204 1205