views.py 95.3 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.contrib.messages.views import SuccessMessageMixin
31
from django.core.exceptions import (
32
    PermissionDenied, SuspiciousOperation,
33
)
34 35
from django.core import signing
from django.core.urlresolvers import reverse, reverse_lazy
36
from django.db.models import Count
37
from django.http import HttpResponse, HttpResponseRedirect, Http404
Őry Máté committed
38
from django.shortcuts import redirect, render, get_object_or_404
39
from django.views.decorators.http import require_GET
40
from django.views.generic.detail import SingleObjectMixin
41
from django.views.generic import (TemplateView, DetailView, View, DeleteView,
42
                                  UpdateView, CreateView, ListView)
43 44
from django.contrib import messages
from django.utils.translation import ugettext as _
45
from django.utils.translation import ungettext as __
46
from django.template.defaultfilters import title as title_filter
47
from django.template.loader import render_to_string
48
from django.template import RequestContext
49

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

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

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

Őry Máté committed
75
logger = logging.getLogger(__name__)
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
class FilterMixin(object):

    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

    def get_queryset(self):
        return super(FilterMixin,
                     self).get_queryset().filter(**self.get_queryset_filters())


102
class IndexView(LoginRequiredMixin, TemplateView):
Kálmán Viktor committed
103
    template_name = "dashboard/index.html"
104

105
    def get_context_data(self, **kwargs):
106
        user = self.request.user
107
        context = super(IndexView, self).get_context_data(**kwargs)
108

109
        # instances
110
        favs = Instance.objects.filter(favourite__user=self.request.user)
111
        instances = Instance.get_objects_with_level(
112
            'user', user, disregard_superuser=True).filter(destroyed_at=None)
113 114 115
        display = list(favs) + list(set(instances) - set(favs))
        for d in display:
            d.fav = True if d in favs else False
116
        context.update({
117
            'instances': display[:5],
118
            'more_instances': instances.count() - len(instances[:5])
119 120
        })

121 122
        running = instances.filter(status='RUNNING')
        stopped = instances.exclude(status__in=('RUNNING', 'NOSTATE'))
123

124
        context.update({
125 126 127
            'running_vms': running[:20],
            'running_vm_num': running.count(),
            'stopped_vm_num': stopped.count()
128
        })
129

130 131 132 133
        # nodes
        if user.is_superuser:
            nodes = Node.objects.all()
            context.update({
134 135
                'nodes': nodes[:5],
                'more_nodes': nodes.count() - len(nodes[:5]),
136 137 138 139 140 141 142 143 144 145
                '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
146
        if user.has_module_perms('auth'):
147 148
            profiles = GroupProfile.get_objects_with_level('operator', user)
            groups = Group.objects.filter(groupprofile__in=profiles)
149 150 151 152
            context.update({
                'groups': groups[:5],
                'more_groups': groups.count() - len(groups[:5]),
            })
153 154 155 156 157 158

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

159 160
        return context

161

162
def get_vm_acl_data(obj):
163 164 165 166 167 168 169 170 171
    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])}


172 173 174 175 176 177 178 179 180 181 182
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])}


183
class CheckedDetailView(LoginRequiredMixin, DetailView):
184 185
    read_level = 'user'

186 187 188
    def get_has_level(self):
        return self.object.has_level

189 190
    def get_context_data(self, **kwargs):
        context = super(CheckedDetailView, self).get_context_data(**kwargs)
191
        if not self.get_has_level()(self.request.user, self.read_level):
192 193 194 195
            raise PermissionDenied()
        return context


196 197 198 199 200 201 202 203 204
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:
205 206 207 208 209 210 211 212
            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)
213 214 215 216
        else:
            raise Http404()


217
class VmDetailView(CheckedDetailView):
Kálmán Viktor committed
218
    template_name = "dashboard/vm-detail.html"
219
    model = Instance
220 221

    def get_context_data(self, **kwargs):
222
        context = super(VmDetailView, self).get_context_data(**kwargs)
223
        instance = context['instance']
224 225 226
        context.update({
            'graphite_enabled': VmGraphView.get_graphite_url() is not None,
            'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
227
                                    kwargs={'pk': self.object.pk}),
228
            'ops': get_operations(instance, self.request.user),
229
        })
230 231

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

234
        context['vlans'] = Vlan.get_objects_with_level(
235
            'user', self.request.user
236
        ).exclude(  # exclude already added interfaces
237 238 239
            pk__in=Interface.objects.filter(
                instance=self.get_object()).values_list("vlan", flat=True)
        ).all()
240
        context['acl'] = get_vm_acl_data(instance)
241
        context['forms'] = {
242
            'disk_add_form': DiskAddForm(
243
                user=self.request.user,
244
                is_template=False, object_pk=self.get_object().pk,
245
                prefix="disk"),
246
        }
247 248
        context['os_type_icon'] = instance.os_type.replace("unknown",
                                                           "question")
249
        return context
Kálmán Viktor committed
250

251 252 253 254 255
    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)

256 257 258
        options = {
            'change_password': self.__change_password,
            'new_name': self.__set_name,
259
            'new_description': self.__set_description,
260 261
            'new_tag': self.__add_tag,
            'to_remove': self.__remove_tag,
262 263
            'port': self.__add_port,
            'new_network_vlan': self.__new_network,
264
            'abort_operation': self.__abort_operation,
265 266 267 268
        }
        for k, v in options.iteritems():
            if request.POST.get(k) is not None:
                return v(request)
Kálmán Viktor committed
269

270 271 272 273
    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
274

275
        self.object.change_password(user=request.user)
276
        messages.success(request, _("Password changed."))
277
        if request.is_ajax():
278
            return HttpResponse("Success.")
279 280 281
        else:
            return redirect(reverse_lazy("dashboard.views.detail",
                                         kwargs={'pk': self.object.pk}))
282

283 284 285 286
    def __set_resources(self, request):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
287 288
        if not request.user.has_perm('vm.change_resources'):
            raise PermissionDenied()
289 290 291 292

        resources = {
            'num_cores': request.POST.get('cpu-count'),
            'ram_size': request.POST.get('ram-size'),
293
            'max_ram_size': request.POST.get('ram-size'),  # TODO: max_ram
294 295 296 297
            'priority': request.POST.get('cpu-priority')
        }
        Instance.objects.filter(pk=self.object.pk).update(**resources)

298
        success_message = _("Resources successfully updated.")
299 300 301 302 303 304 305 306 307 308 309
        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}))

310 311
    def __set_name(self, request):
        self.object = self.get_object()
312 313
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
314 315 316 317
        new_name = request.POST.get("new_name")
        Instance.objects.filter(pk=self.object.pk).update(
            **{'name': new_name})

318
        success_message = _("VM successfully renamed.")
319 320 321 322 323 324 325 326 327 328 329 330
        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)
331 332 333 334 335 336 337 338 339 340 341
            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})

342
        success_message = _("VM description successfully updated.")
343 344 345 346 347 348 349 350 351 352 353 354
        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())
355

Kálmán Viktor committed
356 357 358
    def __add_tag(self, request):
        new_tag = request.POST.get('new_tag')
        self.object = self.get_object()
359 360
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()
Kálmán Viktor committed
361 362

        if len(new_tag) < 1:
363
            message = u"Please input something."
Kálmán Viktor committed
364
        elif len(new_tag) > 20:
365
            message = u"Tag name is too long."
Kálmán Viktor committed
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
        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()
381 382
            if not self.object.has_level(request.user, 'owner'):
                raise PermissionDenied()
Kálmán Viktor committed
383 384 385 386 387 388 389 390 391 392 393

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

398 399
    def __add_port(self, request):
        object = self.get_object()
400 401
        if (not object.has_level(request.user, 'owner') or
                not request.user.has_perm('vm.config_ports')):
402
            raise PermissionDenied()
403 404 405 406 407 408

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

        try:
            error = None
409 410 411
            interfaces = object.interface_set.all()
            host = Host.objects.get(pk=request.POST.get("host_pk"),
                                    interface__in=interfaces)
412
            host.add_port(proto, private=port)
413 414 415 416 417
        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()
418
        except ValueError:
419
            error = _("There is a problem with your input.")
420
        except Exception as e:
Bach Dániel committed
421 422
            error = _("Unknown error.")
            logger.error(e)
423 424 425 426 427 428 429 430 431

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

432 433 434 435 436
    def __new_network(self, request):
        self.object = self.get_object()
        if not self.object.has_level(request.user, 'owner'):
            raise PermissionDenied()

437
        vlan = get_object_or_404(Vlan, pk=request.POST.get("new_network_vlan"))
438 439
        if not vlan.has_level(request.user, 'user'):
            raise PermissionDenied()
440
        try:
441
            self.object.add_interface(vlan=vlan, user=request.user)
442
            messages.success(request, _("Successfully added new interface."))
443 444 445 446 447 448 449
        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}))

450
    def __abort_operation(self, request):
Kálmán Viktor committed
451 452 453 454
        self.object = self.get_object()

        activity = get_object_or_404(InstanceActivity,
                                     pk=request.POST.get("activity"))
455 456
        if not activity.is_abortable_for(request.user):
            raise PermissionDenied()
Kálmán Viktor committed
457 458 459
        activity.abort()
        return redirect("%s#activity" % self.object.get_absolute_url())

460

461
class OperationView(DetailView):
462

463
    template_name = 'dashboard/operate.html'
464

465 466 467
    @property
    def name(self):
        return self.get_op().name
468

469 470 471
    @property
    def description(self):
        return self.get_op().description
472

473 474 475
    @classmethod
    def get_urlname(cls):
        return 'dashboard.vm.op.%s' % cls.op
476

477 478
    def get_url(self):
        return reverse(self.get_urlname(), args=(self.get_object().pk, ))
479

480 481 482 483 484
    def get_wrapper_template_name(self):
        if self.request.is_ajax():
            return 'dashboard/_modal.html'
        else:
            return 'dashboard/_base.html'
485

486 487 488
    @classmethod
    def get_op_by_object(cls, obj):
        return getattr(obj, cls.op)
489

490 491 492 493
    def get_op(self):
        if not hasattr(self, '_opobj'):
            setattr(self, '_opobj', getattr(self.get_object(), self.op))
        return self._opobj
494

495
    def get_context_data(self, **kwargs):
496
        ctx = super(OperationView, self).get_context_data(**kwargs)
497 498 499
        ctx['op'] = self.get_op()
        ctx['url'] = self.request.path
        return ctx
500

501 502 503 504 505 506 507
    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
508

509
    def post(self, request, extra=None, *args, **kwargs):
510
        self.object = self.get_object()
511 512
        if extra is None:
            extra = {}
513
        try:
514
            self.get_op().async(user=request.user, **extra)
515 516 517
        except Exception as e:
            messages.error(request, _('Could not start operation.'))
            logger.error(e)
518
        return redirect("%s#activity" % self.object.get_absolute_url())
519

520 521 522 523
    @classmethod
    def factory(cls, op, icon='cog'):
        return type(str(cls.__name__ + op),
                    (cls, ), {'op': op, 'icon': icon})
524

525 526 527 528 529 530 531 532 533 534
    @classmethod
    def bind_to_object(cls, instance):
        v = cls()
        v.get_object = lambda: instance
        return v


class VmOperationView(OperationView):

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

537

538 539 540 541 542 543 544
class VmMigrateView(VmOperationView):

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

    def get_context_data(self, **kwargs):
545
        ctx = super(VmMigrateView, self).get_context_data(**kwargs)
546 547 548 549 550
        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):
551 552
        if extra is None:
            extra = {}
553 554 555 556 557 558 559
        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)


560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579
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)


580 581 582
vm_ops = {
    'reset': VmOperationView.factory(op='reset', icon='bolt'),
    'deploy': VmOperationView.factory(op='deploy', icon='play'),
583
    'migrate': VmMigrateView,
584 585 586
    'reboot': VmOperationView.factory(op='reboot', icon='refresh'),
    'shut_off': VmOperationView.factory(op='shut_off', icon='ban-circle'),
    'shutdown': VmOperationView.factory(op='shutdown', icon='off'),
587
    'save_as_template': VmSaveView,
588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605
    '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
606

Kálmán Viktor committed
607

608
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
609 610
    template_name = "dashboard/node-detail.html"
    model = Node
611 612
    form = None
    form_class = TraitForm
613

614 615 616
    def get_context_data(self, form=None, **kwargs):
        if form is None:
            form = self.form_class()
617
        context = super(NodeDetailView, self).get_context_data(**kwargs)
618 619
        instances = Instance.active.filter(node=self.object)
        context['table'] = NodeVmListTable(instances)
620 621 622 623
        na = NodeActivity.objects.filter(
            node=self.object, parent=None
        ).order_by('-started').select_related()
        context['activities'] = na
624
        context['trait_form'] = form
625 626
        context['graphite_enabled'] = (
            NodeGraphView.get_graphite_url() is not None)
627 628
        return context

629 630 631
    def post(self, request, *args, **kwargs):
        if request.POST.get('new_name'):
            return self.__set_name(request)
632 633
        if request.POST.get('to_remove'):
            return self.__remove_trait(request)
634 635
        return redirect(reverse_lazy("dashboard.views.node-detail",
                                     kwargs={'pk': self.get_object().pk}))
636 637 638 639 640 641 642

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

643
        success_message = _("Node successfully renamed.")
644 645 646 647 648
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
                'node_pk': self.object.pk
649 650 651 652 653 654 655 656 657 658
            }
            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}))

659 660 661 662
    def __remove_trait(self, request):
        try:
            to_remove = request.POST.get('to_remove')
            self.object = self.get_object()
663
            self.object.traits.remove(to_remove)
664 665 666 667 668 669 670
            message = u"Success"
        except:  # note this won't really happen
            message = u"Not success"

        if request.is_ajax():
            return HttpResponse(
                json.dumps({'message': message}),
671
                content_type="application/json"
672
            )
673
        else:
674
            return redirect(self.object.get_absolute_url())
675

676

677
class GroupDetailView(CheckedDetailView):
678 679
    template_name = "dashboard/group-detail.html"
    model = Group
680
    read_level = 'operator'
681 682 683

    def get_has_level(self):
        return self.object.profile.has_level
684 685 686

    def get_context_data(self, **kwargs):
        context = super(GroupDetailView, self).get_context_data(**kwargs)
687 688 689
        context['group'] = self.object
        context['users'] = self.object.user_set.all()
        context['acl'] = get_group_acl_data(self.object)
690 691 692
        return context

    def post(self, request, *args, **kwargs):
693 694 695
        self.object = self.get_object()
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
696 697
        if request.POST.get('new_name'):
            return self.__set_name(request)
698
        if request.POST.get('list-new-name'):
699
            return self.__add_user(request)
700
        if request.POST.get('list-new-namelist'):
701
            return self.__add_list(request)
702 703 704 705
        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}))
706 707

    def __add_user(self, request):
708
        name = request.POST['list-new-name']
709 710 711
        self.__add_username(request, name)
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
712 713

    def __add_username(self, request, name):
714
        if not name:
715
            return
716 717
        try:
            entity = User.objects.get(username=name)
718
            self.object.user_set.add(entity)
719 720
        except User.DoesNotExist:
            warning(request, _('User "%s" not found.') % name)
721

722
    def __add_list(self, request):
723 724
        if not self.get_has_level()(request.user, 'operator'):
            raise PermissionDenied()
725 726 727
        userlist = request.POST.get('list-new-namelist').split('\r\n')
        for line in userlist:
            self.__add_username(request, line)
728 729
        return redirect(reverse_lazy("dashboard.views.group-detail",
                                     kwargs={'pk': self.object.pk}))
730 731 732 733 734 735

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

736
        success_message = _("Group successfully renamed.")
737 738 739 740
        if request.is_ajax():
            response = {
                'message': success_message,
                'new_name': new_name,
741
                'group_pk': self.object.pk
742 743 744 745 746 747 748 749 750 751 752
            }
            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}))


753
class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
754

755
    def post(self, request, *args, **kwargs):
756
        instance = self.get_object()
757 758
        if not (instance.has_level(request.user, "owner") or
                getattr(instance, 'owner', None) == request.user):
Őry Máté committed
759 760
            logger.warning('Tried to set permissions of %s by non-owner %s.',
                           unicode(instance), unicode(request.user))
761
            raise PermissionDenied()
762
        self.set_levels(request, instance)
763
        self.remove_levels(request, instance)
764
        self.add_levels(request, instance)
765
        return redirect("%s#access" % instance.get_absolute_url())
766 767

    def set_levels(self, request, instance):
768 769 770
        for key, value in request.POST.items():
            m = re.match('perm-([ug])-(\d+)', key)
            if m:
771 772
                typ, id = m.groups()
                entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
773
                if getattr(instance, "owner", None) == entity:
774 775 776
                    logger.info("Tried to set owner's acl level for %s by %s.",
                                unicode(instance), unicode(request.user))
                    continue
777
                instance.set_level(entity, value)
Őry Máté committed
778 779 780
                logger.info("Set %s's acl level for %s to %s by %s.",
                            unicode(entity), unicode(instance),
                            value, unicode(request.user))
781

782 783 784 785 786 787 788 789 790 791
    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 "
792
                            "you can transfer ownership.")
793 794 795 796 797 798 799
                    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))

800
    def add_levels(self, request, instance):
801 802
        name = request.POST['perm-new-name']
        value = request.POST['perm-new']
803 804 805 806 807 808
        if not name:
            return
        try:
            entity = User.objects.get(username=name)
        except User.DoesNotExist:
            entity = None
809 810
            try:
                entity = Group.objects.get(name=name)
811 812 813 814
            except Group.DoesNotExist:
                warning(request, _('User or group "%s" not found.') % name)
                return

815
        instance.set_level(entity, value)
Őry Máté committed
816 817 818
        logger.info("Set %s's new acl level for %s to %s by %s.",
                    unicode(entity), unicode(instance),
                    value, unicode(request.user))
819

Kálmán Viktor committed
820

821 822 823 824 825 826 827 828 829 830
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()
831 832 833 834 835 836 837 838 839

        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)
840
            self.remove_levels(request, template)
841 842 843 844 845 846

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

848
        return redirect(template)
849 850


851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872
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))


873 874 875 876 877 878 879 880 881 882
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)
883
        templates = InstanceTemplate.get_objects_with_level("user",
884 885 886 887
                                                            self.request.user)
        context.update({
            'box_title': _('Choose template'),
            'ajax_title': False,
888
            'template': "dashboard/_template-choose.html",
889
            'templates': templates.all(),
890 891 892
        })
        return context

893 894 895 896 897 898 899
    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"))
900
        elif template is None:
901
            messages.warning(request, _("Select an option to proceed."))
902
            return redirect(reverse("dashboard.views.template-choose"))
903 904 905 906 907 908 909 910
        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())

911

912 913 914 915
class TemplateCreate(SuccessMessageMixin, CreateView):
    model = InstanceTemplate
    form_class = TemplateForm

916 917
    def get_template_names(self):
        if self.request.is_ajax():
918
            pass
919 920 921 922 923 924 925
        else:
            return ['dashboard/nojs-wrapper.html']

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

        context.update({
926
            'box_title': _("Create a new base VM"),
927
            'template': "dashboard/_template-create.html",
928
            'leases': Lease.objects.count()
929 930 931
        })
        return context

932
    def get(self, *args, **kwargs):
933 934
        if not self.request.user.has_perm('vm.create_template'):
            raise PermissionDenied()
935

936 937 938 939
        return super(TemplateCreate, self).get(*args, **kwargs)

    def get_form_kwargs(self):
        kwargs = super(TemplateCreate, self).get_form_kwargs()
940
        kwargs['user'] = self.request.user
941 942
        return kwargs

943 944 945
    def post(self, request, *args, **kwargs):
        if not self.request.user.has_perm('vm.create_template'):
            raise PermissionDenied()
946 947

        form = self.form_class(request.POST, user=request.user)
948 949
        if not form.is_valid():
            return self.get(request, form, *args, **kwargs)
950
        else:
951
            post = form.cleaned_data
952 953
            networks = self.__create_networks(post.pop("networks"),
                                              request.user)
954
            post.pop("parent")
955
            post['max_ram_size'] = post['ram_size']
956 957 958 959 960 961 962
            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)
963

964
            return redirect("%s#resources" % inst.get_absolute_url())
965

966 967
        return super(TemplateCreate, self).post(self, request, args, kwargs)

968
    def __create_networks(self, vlans, user):
969 970
        networks = []
        for v in vlans:
971 972
            if not v.has_level(user, "user"):
                raise PermissionDenied()
973 974 975
            networks.append(InterfaceTemplate(vlan=v, managed=v.managed))
        return networks

976 977 978 979
    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-list")


980
class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
981
    model = InstanceTemplate
982 983
    template_name = "dashboard/template-edit.html"
    form_class = TemplateForm
984
    success_message = _("Successfully modified template.")
985 986

    def get(self, request, *args, **kwargs):
987
        template = self.get_object()
988
        if not template.has_level(request.user, 'user'):
989
            raise PermissionDenied()
990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010
        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:
1011 1012
            return super(TemplateDetail, self).get(request, *args, **kwargs)

1013
    def get_context_data(self, **kwargs):
1014
        obj = self.get_object()
1015
        context = super(TemplateDetail, self).get_context_data(**kwargs)
1016 1017
        context['acl'] = get_vm_acl_data(obj)
        context['disks'] = obj.disks.all()
1018
        context['disk_add_form'] = DiskAddForm(
1019
            user=self.request.user,
1020
            is_template=True,
1021
            object_pk=obj.pk,
1022 1023
            prefix="disk",
        )
1024 1025
        return context

1026 1027 1028 1029
    def get_success_url(self):
        return reverse_lazy("dashboard.views.template-detail",
                            kwargs=self.kwargs)

1030 1031 1032 1033 1034 1035 1036
    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()
1037 1038 1039
        for network in self.get_object().interface_set.all():
            if not network.vlan.has_level(request.user, "user"):
                raise PermissionDenied()
1040 1041
        return super(TemplateDetail, self).post(self, request, args, kwargs)

1042 1043 1044 1045 1046
    def get_form_kwargs(self):
        kwargs = super(TemplateDetail, self).get_form_kwargs()
        kwargs['user'] = self.request.user
        return kwargs

1047

1048
class TemplateList(LoginRequiredMixin, SingleTableView):
1049 1050 1051 1052 1053 1054 1055
    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)
1056 1057
        context['lease_table'] = LeaseListTable(Lease.objects.all(),
                                                request=self.request)
1058
        return context
1059

1060 1061 1062 1063 1064 1065
    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()

1066

1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085
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()
1086
        success_message = _("Template successfully deleted.")
1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097

        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)


1098
class VmList(LoginRequiredMixin, FilterMixin, ListView):
Kálmán Viktor committed
1099
    template_name = "dashboard/vm-list.html"
1100 1101 1102 1103 1104 1105
    allowed_filters = {
        'name': "name__icontains",
        'node': "node__name__icontains",
        'status': "status__iexact",
        'tags': "tags__name__in",  # note: use it as ?tags[]=a,b
    }
1106

1107 1108
    def get(self, *args, **kwargs):
        if self.request.is_ajax():
1109 1110 1111 1112
            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(
1113
                destroyed_at=None).all()
1114 1115 1116
            instances = [{
                'pk': i.pk,
                'name': i.name,
1117 1118 1119
                'icon': i.get_status_icon(),
                'host': "" if not i.primary_host else i.primary_host.hostname,
                'status': i.get_status_display(),
1120
                'fav': i.pk in favs} for i in instances]
1121
            return HttpResponse(
1122
                json.dumps(list(instances)),  # instances is ValuesQuerySet
1123 1124 1125 1126 1127
                content_type="application/json",
            )
        else:
            return super(VmList, self).get(*args, **kwargs)

1128
    def get_queryset(self):
1129
        logger.debug('VmList.get_queryset() called. User: %s',
1130
                     unicode(self.request.user))
1131
        queryset = Instance.get_objects_with_level(
1132
            'user', self.request.user).filter(destroyed_at=None)
1133 1134 1135
        s = self.request.GET.get("s")
        if s:
            queryset = queryset.filter(name__icontains=s)
1136 1137 1138 1139 1140

        sort = self.request.GET.get("sort")
        # remove "-" that means descending order
        # also check if the column name is valid
        if (sort and
1141
            (sort[1:] if sort[0] == "-" else sort)
1142
                in [i.name for i in Instance._meta.fields] + ["pk"]):
1143
            queryset = queryset.order_by(sort)
1144 1145
        return queryset.filter(**self.get_queryset_filters()
                               ).select_related('owner', 'node')
1146

1147

1148
class NodeList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
1149 1150 1151
    template_name = "dashboard/node-list.html"
    table_class = NodeListTable
    table_pagination = False
1152

1153 1154 1155 1156 1157
    def get(self, *args, **kwargs):
        if self.request.is_ajax():
            nodes = Node.objects.all()
            nodes = [{
                'name': i.name,
1158 1159
                'icon': i.get_status_icon(),
                'url': i.get_absolute_url(),
1160
                'label': i.get_status_label(),
1161
                'status': i.state.lower()} for i in nodes]
1162 1163

            return HttpResponse(
1164
                json.dumps(list(nodes)),
1165 1166 1167 1168 1169
                content_type="application/json",
            )
        else:
            return super(NodeList, self).get(*args, **kwargs)

1170 1171 1172 1173
    def get_queryset(self):
        return Node.objects.annotate(
            number_of_VMs=Count('instance_set')).select_related('host')

Őry Máté committed
1174

1175
class GroupList(LoginRequiredMixin, SingleTableView):
1176 1177 1178 1179 1180
    template_name = "dashboard/group-list.html"
    model = Group
    table_class = GroupListTable
    table_pagination = False

1181 1182 1183 1184 1185
    def get(self, *args, **kwargs):
        if self.request.is_ajax():
            groups = [{
                'url': reverse("dashboard.views.group-detail",
                               kwargs={'pk': i.pk}),