From 113ce3d5f1311ead6d104e193aa03f8407db2b76 Mon Sep 17 00:00:00 2001
From: Őry Máté <ory.mate@cloud.bme.hu>
Date: Thu, 18 Sep 2014 14:48:21 +0200
Subject: [PATCH] dashboard: refactor views

(tests coming soon)
---
 circle/dashboard/views.py          | 3746 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 circle/dashboard/views/__init__.py |   13 +++++++++++++
 circle/dashboard/views/group.py    |  440 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 circle/dashboard/views/index.py    |  123 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 circle/dashboard/views/node.py     |  373 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 circle/dashboard/views/store.py    |  206 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 circle/dashboard/views/template.py |  397 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 circle/dashboard/views/user.py     |  488 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 circle/dashboard/views/util.py     |  579 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 circle/dashboard/views/vm.py       | 1417 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 10 files changed, 4036 insertions(+), 3746 deletions(-)
 delete mode 100644 circle/dashboard/views.py
 create mode 100644 circle/dashboard/views/__init__.py
 create mode 100644 circle/dashboard/views/group.py
 create mode 100644 circle/dashboard/views/index.py
 create mode 100644 circle/dashboard/views/node.py
 create mode 100644 circle/dashboard/views/store.py
 create mode 100644 circle/dashboard/views/template.py
 create mode 100644 circle/dashboard/views/user.py
 create mode 100644 circle/dashboard/views/util.py
 create mode 100644 circle/dashboard/views/vm.py

diff --git a/circle/dashboard/views.py b/circle/dashboard/views.py
deleted file mode 100644
index f91ca70..0000000
--- a/circle/dashboard/views.py
+++ /dev/null
@@ -1,3746 +0,0 @@
-# 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/>.
-
-from __future__ import unicode_literals, absolute_import
-
-from collections import OrderedDict
-from itertools import chain
-from os import getenv
-from os.path import join, normpath, dirname, basename
-from urlparse import urljoin
-import json
-import logging
-import re
-import requests
-
-from django.conf import settings
-from django.contrib.auth.models import User, Group
-from django.contrib.auth.views import login as login_view, redirect_to_login
-from django.contrib.auth.decorators import login_required
-from django.contrib.auth import login
-from django.contrib.messages.views import SuccessMessageMixin
-from django.core.exceptions import (
-    PermissionDenied, SuspiciousOperation,
-)
-from django.core.cache import get_cache
-from django.core import signing
-from django.core.urlresolvers import reverse, reverse_lazy
-from django.db.models import Count, Q
-from django.http import HttpResponse, HttpResponseRedirect, Http404
-from django.shortcuts import (
-    redirect, render, get_object_or_404, render_to_response,
-)
-from django.views.decorators.http import require_GET, require_POST
-from django.views.generic.detail import SingleObjectMixin
-from django.views.generic import (TemplateView, DetailView, View, DeleteView,
-                                  UpdateView, CreateView, ListView)
-from django.contrib import messages
-from django.utils.translation import (
-    ugettext as _, ugettext_noop, ungettext_lazy
-)
-from django.template.loader import render_to_string
-from django.template import RequestContext
-
-from django.forms.models import inlineformset_factory
-from django_tables2 import SingleTableView
-from braces.views import (LoginRequiredMixin, SuperuserRequiredMixin,
-                          PermissionRequiredMixin)
-from braces.views._access import AccessMixin
-from celery.exceptions import TimeoutError
-
-from django_sshkey.models import UserKey
-
-from .forms import (
-    CircleAuthenticationForm, HostForm, LeaseForm, MyProfileForm,
-    NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm,
-    UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
-    VmSaveForm, UserKeyForm, VmRenewForm, VmStateChangeForm,
-    CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
-    TraitsForm, RawDataForm, GroupPermissionForm, AclUserOrGroupAddForm,
-    VmResourcesForm, VmAddInterfaceForm, VmListSearchForm,
-    TemplateListSearchForm, ConnectCommandForm,
-    TransferOwnershipForm, AddGroupMemberForm
-)
-
-from .tables import (
-    NodeListTable, TemplateListTable, LeaseListTable,
-    GroupListTable, UserKeyListTable, ConnectCommandListTable,
-)
-from common.models import (
-    HumanReadableObject, HumanReadableException, fetch_human_exception,
-    create_readable,
-)
-from vm.models import (
-    Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
-    InterfaceTemplate, Lease, Node, NodeActivity, Trait,
-)
-from storage.models import Disk
-from firewall.models import Vlan, Host, Rule
-from .models import (Favourite, Profile, GroupProfile, FutureMember,
-                     ConnectCommand, create_profile)
-
-from .store_api import Store, NoStoreException, NotOkException
-
-logger = logging.getLogger(__name__)
-saml_available = hasattr(settings, "SAML_CONFIG")
-
-
-def search_user(keyword):
-    try:
-        return User.objects.get(username=keyword)
-    except User.DoesNotExist:
-        try:
-            return User.objects.get(profile__org_id=keyword)
-        except User.DoesNotExist:
-            return User.objects.get(email=keyword)
-
-
-class RedirectToLoginMixin(AccessMixin):
-
-    redirect_exception_classes = (PermissionDenied, )
-
-    def dispatch(self, request, *args, **kwargs):
-        try:
-            return super(RedirectToLoginMixin, self).dispatch(
-                request, *args, **kwargs)
-        except self.redirect_exception_classes:
-            if not request.user.is_authenticated():
-                return redirect_to_login(request.get_full_path(),
-                                         self.get_login_url(),
-                                         self.get_redirect_field_name())
-            else:
-                raise
-
-
-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', [])
-                for group in chain(*[attributes[i]
-                                     for i in owneratrs if i in attributes]):
-                    try:
-                        GroupProfile.search(group)
-                    except Group.DoesNotExist:
-                        newgroups.append(group)
-
-        return newgroups
-
-
-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].split(",")
-                    if self.allowed_filters[item].endswith("__in") else
-                    self.request.GET[item])
-
-        return filters
-
-    def get_queryset(self):
-        return super(FilterMixin,
-                     self).get_queryset().filter(**self.get_queryset_filters())
-
-    def create_fake_get(self):
-        self.request.GET = self._parse_get(self.request.GET)
-
-    def _parse_get(self, GET_dict):
-        """
-        Returns a new dict from request's GET dict to filter the vm list
-        For example: "name:xy node:1" updates the GET dict
-                     to resemble this URL ?name=xy&node=1
-
-        "name:xy node:1".split(":") becomes ["name", "xy node", "1"]
-        we pop the the first element and use it as the first dict key
-        then we iterate over the rest of the list and split by the last
-        whitespace, the first part of this list will be the previous key's
-        value, then last part of the list will be the next key.
-        The final dict looks like this: {'name': xy, 'node':1}
-
-        >>> f = FilterMixin()
-        >>> o = f._parse_get({'s': "hello"}).items()
-        >>> sorted(o) # doctest: +ELLIPSIS
-        [(u'name', u'hello'), (...)]
-        >>> o = f._parse_get({'s': "name:hello owner:test"}).items()
-        >>> sorted(o) # doctest: +ELLIPSIS
-        [(u'name', u'hello'), (u'owner', u'test'), (...)]
-        >>> o = f._parse_get({'s': "name:hello ws node:node 3 oh"}).items()
-        >>> sorted(o) # doctest: +ELLIPSIS
-        [(u'name', u'hello ws'), (u'node', u'node 3 oh'), (...)]
-        """
-        s = GET_dict.get("s")
-        fake = GET_dict.copy()
-        if s:
-            s = s.split(":")
-            if len(s) < 2:  # if there is no ':' in the string, filter by name
-                got = {'name': s[0]}
-            else:
-                latest = s.pop(0)
-                got = {'%s' % latest: None}
-                for i in s[:-1]:
-                    new = i.rsplit(" ", 1)
-                    got[latest] = new[0]
-                    latest = new[1] if len(new) > 1 else None
-                got[latest] = s[-1]
-
-            # generate a new GET request, that is kinda fake
-            for k, v in got.iteritems():
-                fake[k] = v
-        return fake
-
-    def create_acl_queryset(self, model):
-        cleaned_data = self.search_form.cleaned_data
-        stype = cleaned_data.get('stype', "all")
-        superuser = stype == "all"
-        shared = stype == "shared" or stype == "all"
-        level = "owner" if stype == "owned" else "user"
-        queryset = model.get_objects_with_level(
-            level, self.request.user,
-            group_also=shared, disregard_superuser=not superuser,
-        )
-        return queryset
-
-
-class IndexView(LoginRequiredMixin, TemplateView):
-    template_name = "dashboard/index.html"
-
-    def get_context_data(self, **kwargs):
-        user = self.request.user
-        context = super(IndexView, self).get_context_data(**kwargs)
-
-        # instances
-        favs = Instance.objects.filter(favourite__user=self.request.user)
-        instances = Instance.get_objects_with_level(
-            'user', user, disregard_superuser=True).filter(destroyed_at=None)
-        display = list(favs) + list(set(instances) - set(favs))
-        for d in display:
-            d.fav = True if d in favs else False
-        context.update({
-            'instances': display[:5],
-            'more_instances': instances.count() - len(instances[:5])
-        })
-
-        running = instances.filter(status='RUNNING')
-        stopped = instances.exclude(status__in=('RUNNING', 'NOSTATE'))
-
-        context.update({
-            'running_vms': running[:20],
-            'running_vm_num': running.count(),
-            'stopped_vm_num': stopped.count()
-        })
-
-        # nodes
-        if user.is_superuser:
-            nodes = Node.objects.all()
-            context.update({
-                'nodes': nodes[:5],
-                'more_nodes': nodes.count() - len(nodes[:5]),
-                '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
-        if user.has_module_perms('auth'):
-            profiles = GroupProfile.get_objects_with_level('operator', user)
-            groups = Group.objects.filter(groupprofile__in=profiles)
-            context.update({
-                'groups': groups[:5],
-                'more_groups': groups.count() - len(groups[:5]),
-            })
-
-        # template
-        if user.has_perm('vm.create_template'):
-            context['templates'] = InstanceTemplate.get_objects_with_level(
-                'operator', user, disregard_superuser=True).all()[:5]
-
-        # toplist
-        if settings.STORE_URL:
-            cache_key = "files-%d" % self.request.user.pk
-            cache = get_cache("default")
-            files = cache.get(cache_key)
-            if not files:
-                try:
-                    store = Store(self.request.user)
-                    toplist = store.toplist()
-                    quota = store.get_quota()
-                    files = {'toplist': toplist, 'quota': quota}
-                except Exception:
-                    logger.exception("Unable to get tolist for %s",
-                                     unicode(self.request.user))
-                    files = {'toplist': []}
-                cache.set(cache_key, files, 300)
-
-            context['files'] = files
-        else:
-            context['no_store'] = True
-
-        return context
-
-
-class CheckedDetailView(LoginRequiredMixin, DetailView):
-    read_level = 'user'
-
-    def get_has_level(self):
-        return self.object.has_level
-
-    def get_context_data(self, **kwargs):
-        context = super(CheckedDetailView, self).get_context_data(**kwargs)
-        if not self.get_has_level()(self.request.user, self.read_level):
-            raise PermissionDenied()
-        return context
-
-
-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 not request.user.has_perm('vm.access_console'):
-            raise PermissionDenied()
-        if self.object.node:
-            with instance_activity(
-                    code_suffix='console-accessed', instance=self.object,
-                    user=request.user, readable_name=ugettext_noop(
-                        "console access"), 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)
-        else:
-            raise Http404()
-
-
-class VmDetailView(CheckedDetailView):
-    template_name = "dashboard/vm-detail.html"
-    model = Instance
-
-    def get_context_data(self, **kwargs):
-        context = super(VmDetailView, self).get_context_data(**kwargs)
-        instance = context['instance']
-        user = self.request.user
-        ops = get_operations(instance, user)
-        context.update({
-            'graphite_enabled': settings.GRAPHITE_URL is not None,
-            'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
-                                    kwargs={'pk': self.object.pk}),
-            'ops': ops,
-            'op': {i.op: i for i in ops},
-            'connect_commands': user.profile.get_connect_commands(instance)
-        })
-
-        # activity data
-        activities = instance.get_merged_activities(user)
-        show_show_all = len(activities) > 10
-        activities = activities[:10]
-        context['activities'] = _format_activities(activities)
-        context['show_show_all'] = show_show_all
-        latest = instance.get_latest_activity_in_progress()
-        context['is_new_state'] = (latest and
-                                   latest.resultant_state is not None and
-                                   instance.status != latest.resultant_state)
-
-        context['vlans'] = Vlan.get_objects_with_level(
-            'user', self.request.user
-        ).exclude(  # exclude already added interfaces
-            pk__in=Interface.objects.filter(
-                instance=self.get_object()).values_list("vlan", flat=True)
-        ).all()
-        context['acl'] = AclUpdateView.get_acl_data(
-            instance, self.request.user, 'dashboard.views.vm-acl')
-        context['aclform'] = AclUserOrGroupAddForm()
-        context['os_type_icon'] = instance.os_type.replace("unknown",
-                                                           "question")
-        # ipv6 infos
-        context['ipv6_host'] = instance.get_connect_host(use_ipv6=True)
-        context['ipv6_port'] = instance.get_connect_port(use_ipv6=True)
-
-        # resources forms
-        can_edit = (
-            instance.has_level(user, "owner")
-            and self.request.user.has_perm("vm.change_resources"))
-        context['resources_form'] = VmResourcesForm(
-            can_edit=can_edit, instance=instance)
-
-        if self.request.user.is_superuser:
-            context['traits_form'] = TraitsForm(instance=instance)
-            context['raw_data_form'] = RawDataForm(instance=instance)
-
-        # resources change perm
-        context['can_change_resources'] = self.request.user.has_perm(
-            "vm.change_resources")
-
-        # client info
-        context['client_download'] = self.request.COOKIES.get(
-            'downloaded_client')
-        # can link template
-        context['can_link_template'] = (
-            instance.template and instance.template.has_level(user, "operator")
-        )
-
-        return context
-
-    def post(self, request, *args, **kwargs):
-        options = {
-            'new_name': self.__set_name,
-            'new_description': self.__set_description,
-            'new_tag': self.__add_tag,
-            'to_remove': self.__remove_tag,
-            'port': self.__add_port,
-            'abort_operation': self.__abort_operation,
-        }
-        for k, v in options.iteritems():
-            if request.POST.get(k) is not None:
-                return v(request)
-        raise Http404()
-
-    def __set_name(self, request):
-        self.object = self.get_object()
-        if not self.object.has_level(request.user, 'owner'):
-            raise PermissionDenied()
-        new_name = request.POST.get("new_name")
-        Instance.objects.filter(pk=self.object.pk).update(
-            **{'name': new_name})
-
-        success_message = _("VM successfully renamed.")
-        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)
-            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})
-
-        success_message = _("VM description successfully updated.")
-        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())
-
-    def __add_tag(self, request):
-        new_tag = request.POST.get('new_tag')
-        self.object = self.get_object()
-        if not self.object.has_level(request.user, 'owner'):
-            raise PermissionDenied()
-
-        if len(new_tag) < 1:
-            message = u"Please input something."
-        elif len(new_tag) > 20:
-            message = u"Tag name is too long."
-        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()
-            if not self.object.has_level(request.user, 'owner'):
-                raise PermissionDenied()
-
-            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"
-            )
-        else:
-            return redirect(reverse_lazy("dashboard.views.detail",
-                            kwargs={'pk': self.object.pk}))
-
-    def __add_port(self, request):
-        object = self.get_object()
-        if (not object.has_level(request.user, 'owner') or
-                not request.user.has_perm('vm.config_ports')):
-            raise PermissionDenied()
-
-        port = request.POST.get("port")
-        proto = request.POST.get("proto")
-
-        try:
-            error = None
-            interfaces = object.interface_set.all()
-            host = Host.objects.get(pk=request.POST.get("host_pk"),
-                                    interface__in=interfaces)
-            host.add_port(proto, private=port)
-        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()
-        except ValueError:
-            error = _("There is a problem with your input.")
-        except Exception as e:
-            error = _("Unknown error.")
-            logger.error(e)
-
-        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}))
-
-    def __abort_operation(self, request):
-        self.object = self.get_object()
-
-        activity = get_object_or_404(InstanceActivity,
-                                     pk=request.POST.get("activity"))
-        if not activity.is_abortable_for(request.user):
-            raise PermissionDenied()
-        activity.abort()
-        return redirect("%s#activity" % self.object.get_absolute_url())
-
-
-class VmTraitsUpdate(SuperuserRequiredMixin, UpdateView):
-    form_class = TraitsForm
-    model = Instance
-
-    def get_success_url(self):
-        return self.get_object().get_absolute_url() + "#resources"
-
-
-class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
-    form_class = RawDataForm
-    model = Instance
-    template_name = 'dashboard/vm-detail/raw_data.html'
-
-    def get_success_url(self):
-        return self.get_object().get_absolute_url() + "#resources"
-
-
-class OperationView(RedirectToLoginMixin, DetailView):
-
-    template_name = 'dashboard/operate.html'
-    show_in_toolbar = True
-    effect = None
-    wait_for_result = None
-    with_reload = False
-
-    @property
-    def name(self):
-        return self.get_op().name
-
-    @property
-    def description(self):
-        return self.get_op().description
-
-    def is_preferred(self):
-        return self.get_op().is_preferred()
-
-    @classmethod
-    def get_urlname(cls):
-        return 'dashboard.vm.op.%s' % cls.op
-
-    @classmethod
-    def get_instance_url(cls, pk, key=None, *args, **kwargs):
-        url = reverse(cls.get_urlname(), args=(pk, ) + args, kwargs=kwargs)
-        if key is None:
-            return url
-        else:
-            return "%s?k=%s" % (url, key)
-
-    def get_url(self, **kwargs):
-        return self.get_instance_url(self.get_object().pk, **kwargs)
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/_modal.html']
-        else:
-            return ['dashboard/_base.html']
-
-    @classmethod
-    def get_op_by_object(cls, obj):
-        return getattr(obj, cls.op)
-
-    def get_op(self):
-        if not hasattr(self, '_opobj'):
-            setattr(self, '_opobj', getattr(self.get_object(), self.op))
-        return self._opobj
-
-    @classmethod
-    def get_operation_class(cls):
-        return cls.model.get_operation_class(cls.op)
-
-    def get_context_data(self, **kwargs):
-        ctx = super(OperationView, self).get_context_data(**kwargs)
-        ctx['op'] = self.get_op()
-        ctx['opview'] = self
-        url = self.request.path
-        if self.request.GET:
-            url += '?' + self.request.GET.urlencode()
-        ctx['url'] = url
-        ctx['template'] = super(OperationView, self).get_template_names()[0]
-        return ctx
-
-    def check_auth(self):
-        logger.debug("OperationView.check_auth(%s)", unicode(self))
-        self.get_op().check_auth(self.request.user)
-
-    @classmethod
-    def check_perms(cls, user):
-        cls.get_operation_class().check_perms(user)
-
-    def get(self, request, *args, **kwargs):
-        self.check_auth()
-        return super(OperationView, self).get(request, *args, **kwargs)
-
-    def get_response_data(self, result, done, extra=None, **kwargs):
-        """Return serializable data to return to agents requesting json
-        response to POST"""
-
-        if extra is None:
-            extra = {}
-        extra["success"] = not isinstance(result, Exception)
-        extra["done"] = done
-        if isinstance(result, HumanReadableObject):
-            extra["message"] = result.get_user_text()
-        return extra
-
-    def post(self, request, extra=None, *args, **kwargs):
-        self.check_auth()
-        self.object = self.get_object()
-        if extra is None:
-            extra = {}
-        result = None
-        done = False
-        try:
-            task = self.get_op().async(user=request.user, **extra)
-        except HumanReadableException as e:
-            e.send_message(request)
-            logger.exception("Could not start operation")
-            result = e
-        except Exception as e:
-            messages.error(request, _('Could not start operation.'))
-            logger.exception("Could not start operation")
-            result = e
-        else:
-            wait = self.wait_for_result
-            if wait:
-                try:
-                    result = task.get(timeout=wait,
-                                      interval=min((wait / 5, .5)))
-                except TimeoutError:
-                    logger.debug("Result didn't arrive in %ss",
-                                 self.wait_for_result, exc_info=True)
-                except HumanReadableException as e:
-                    e.send_message(request)
-                    logger.exception(e)
-                    result = e
-                except Exception as e:
-                    messages.error(request, _('Operation failed.'))
-                    logger.debug("Operation failed.", exc_info=True)
-                    result = e
-                else:
-                    done = True
-                    messages.success(request, _('Operation succeeded.'))
-            if result is None and not done:
-                messages.success(request, _('Operation is started.'))
-
-        if "/json" in request.META.get("HTTP_ACCEPT", ""):
-            data = self.get_response_data(result, done,
-                                          post_extra=extra, **kwargs)
-            return HttpResponse(json.dumps(data),
-                                content_type="application/json")
-        else:
-            return redirect("%s#activity" % self.object.get_absolute_url())
-
-    @classmethod
-    def factory(cls, op, icon='cog', effect='info', extra_bases=(), **kwargs):
-        kwargs.update({'op': op, 'icon': icon, 'effect': effect})
-        return type(str(cls.__name__ + op),
-                    tuple(list(extra_bases) + [cls]), kwargs)
-
-    @classmethod
-    def bind_to_object(cls, instance, **kwargs):
-        me = cls()
-        me.get_object = lambda: instance
-        for key, value in kwargs.iteritems():
-            setattr(me, key, value)
-        return me
-
-
-class AjaxOperationMixin(object):
-
-    def post(self, request, extra=None, *args, **kwargs):
-        resp = super(AjaxOperationMixin, self).post(
-            request, extra, *args, **kwargs)
-        if request.is_ajax():
-            if not self.with_reload:
-                store = messages.get_messages(request)
-                store.used = True
-            else:
-                store = []
-            return HttpResponse(
-                json.dumps({'success': True,
-                            'with_reload': self.with_reload,
-                            'messages': [unicode(m) for m in store]}),
-                content_type="application=json"
-            )
-        else:
-            return resp
-
-
-class VmOperationView(AjaxOperationMixin, OperationView):
-
-    model = Instance
-    context_object_name = 'instance'  # much simpler to mock object
-
-
-class FormOperationMixin(object):
-
-    form_class = None
-
-    def get_form_kwargs(self):
-        return {}
-
-    def get_context_data(self, **kwargs):
-        ctx = super(FormOperationMixin, self).get_context_data(**kwargs)
-        if self.request.method == 'POST':
-            ctx['form'] = self.form_class(self.request.POST,
-                                          **self.get_form_kwargs())
-        else:
-            ctx['form'] = self.form_class(**self.get_form_kwargs())
-        return ctx
-
-    def post(self, request, extra=None, *args, **kwargs):
-        if extra is None:
-            extra = {}
-        form = self.form_class(self.request.POST, **self.get_form_kwargs())
-        if form.is_valid():
-            extra.update(form.cleaned_data)
-            resp = super(FormOperationMixin, self).post(
-                request, extra, *args, **kwargs)
-            if request.is_ajax():
-                return HttpResponse(
-                    json.dumps({
-                        'success': True,
-                        'with_reload': self.with_reload}),
-                    content_type="application=json")
-            else:
-                return resp
-        else:
-            return self.get(request)
-
-
-class RequestFormOperationMixin(FormOperationMixin):
-
-    def get_form_kwargs(self):
-        val = super(RequestFormOperationMixin, self).get_form_kwargs()
-        val.update({'request': self.request})
-        return val
-
-
-class VmAddInterfaceView(FormOperationMixin, VmOperationView):
-
-    op = 'add_interface'
-    form_class = VmAddInterfaceForm
-    show_in_toolbar = False
-    icon = 'globe'
-    effect = 'success'
-    with_reload = True
-
-    def get_form_kwargs(self):
-        inst = self.get_op().instance
-        choices = Vlan.get_objects_with_level(
-            "user", self.request.user).exclude(
-            vm_interface__instance__in=[inst])
-        val = super(VmAddInterfaceView, self).get_form_kwargs()
-        val.update({'choices': choices})
-        return val
-
-
-class VmCreateDiskView(FormOperationMixin, VmOperationView):
-
-    op = 'create_disk'
-    form_class = VmCreateDiskForm
-    show_in_toolbar = False
-    icon = 'hdd-o'
-    effect = "success"
-    is_disk_operation = True
-
-
-class VmDownloadDiskView(FormOperationMixin, VmOperationView):
-
-    op = 'download_disk'
-    form_class = VmDownloadDiskForm
-    show_in_toolbar = False
-    icon = 'download'
-    effect = "success"
-    is_disk_operation = True
-
-
-class VmMigrateView(VmOperationView):
-
-    op = 'migrate'
-    icon = 'truck'
-    effect = 'info'
-    template_name = 'dashboard/_vm-migrate.html'
-
-    def get_context_data(self, **kwargs):
-        ctx = super(VmMigrateView, self).get_context_data(**kwargs)
-        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):
-        if extra is None:
-            extra = {}
-        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)
-
-
-class VmSaveView(FormOperationMixin, VmOperationView):
-
-    op = 'save_as_template'
-    icon = 'save'
-    effect = 'info'
-    form_class = VmSaveForm
-
-
-class VmResourcesChangeView(VmOperationView):
-    op = 'resources_change'
-    icon = "save"
-    show_in_toolbar = False
-    wait_for_result = 0.5
-
-    def post(self, request, extra=None, *args, **kwargs):
-        if extra is None:
-            extra = {}
-
-        instance = get_object_or_404(Instance, pk=kwargs['pk'])
-
-        form = VmResourcesForm(request.POST, instance=instance)
-        if not form.is_valid():
-            for f in form.errors:
-                messages.error(request, "<strong>%s</strong>: %s" % (
-                    f, form.errors[f].as_text()
-                ))
-            if request.is_ajax():  # this is not too nice
-                store = messages.get_messages(request)
-                store.used = True
-                return HttpResponse(
-                    json.dumps({'success': False,
-                                'messages': [unicode(m) for m in store]}),
-                    content_type="application=json"
-                )
-            else:
-                return redirect(instance.get_absolute_url() + "#resources")
-        else:
-            extra = form.cleaned_data
-            extra['max_ram_size'] = extra['ram_size']
-            return super(VmResourcesChangeView, self).post(request, extra,
-                                                           *args, **kwargs)
-
-
-class TokenOperationView(OperationView):
-    """Abstract operation view with token support.
-
-    User can do the action with a valid token instead of logging in.
-    """
-    token_max_age = 3 * 24 * 3600
-    redirect_exception_classes = (PermissionDenied, SuspiciousOperation, )
-
-    @classmethod
-    def get_salt(cls):
-        return unicode(cls)
-
-    @classmethod
-    def get_token(cls, instance, user):
-        t = tuple([getattr(i, 'pk', i) for i in [instance, user]])
-        return signing.dumps(t, salt=cls.get_salt(), compress=True)
-
-    @classmethod
-    def get_token_url(cls, instance, user):
-        key = cls.get_token(instance, user)
-        return cls.get_instance_url(instance.pk, key)
-
-    def check_auth(self):
-        if 'k' in self.request.GET:
-            try:  # check if token is needed at all
-                return super(TokenOperationView, self).check_auth()
-            except Exception:
-                op = self.get_op()
-                pk = op.instance.pk
-                key = self.request.GET.get('k')
-
-                logger.debug("checking token supplied to %s",
-                             self.request.get_full_path())
-                try:
-                    user = self.validate_key(pk, key)
-                except signing.SignatureExpired:
-                    messages.error(self.request, _('The token has expired.'))
-                else:
-                    logger.info("Request user changed to %s at %s",
-                                user, self.request.get_full_path())
-                    self.request.user = user
-                    self.request.token_user = True
-        else:
-            logger.debug("no token supplied to %s",
-                         self.request.get_full_path())
-
-        return super(TokenOperationView, self).check_auth()
-
-    def validate_key(self, pk, key):
-        """Get object based on signed token.
-        """
-        try:
-            data = signing.loads(key, salt=self.get_salt())
-            logger.debug('Token data: %s', unicode(data))
-            instance, user = data
-            logger.debug('Extracted token data: instance: %s, user: %s',
-                         unicode(instance), unicode(user))
-        except (signing.BadSignature, ValueError, TypeError) as e:
-            logger.warning('Tried invalid token. Token: %s, user: %s. %s',
-                           key, unicode(self.request.user), unicode(e))
-            raise SuspiciousOperation()
-
-        try:
-            instance, user = signing.loads(key, max_age=self.token_max_age,
-                                           salt=self.get_salt())
-            logger.debug('Extracted non-expired token data: %s, %s',
-                         unicode(instance), unicode(user))
-        except signing.BadSignature as e:
-            raise signing.SignatureExpired()
-
-        if pk != instance:
-            logger.debug('pk (%d) != instance (%d)', pk, instance)
-            raise SuspiciousOperation()
-        user = User.objects.get(pk=user)
-        return user
-
-
-class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
-
-    op = 'renew'
-    icon = 'calendar'
-    effect = 'info'
-    show_in_toolbar = False
-    form_class = VmRenewForm
-    wait_for_result = 0.5
-
-    def get_form_kwargs(self):
-        choices = Lease.get_objects_with_level("user", self.request.user)
-        default = self.get_op().instance.lease
-        if default and default not in choices:
-            choices = (choices.distinct() |
-                       Lease.objects.filter(pk=default.pk).distinct())
-
-        val = super(VmRenewView, self).get_form_kwargs()
-        val.update({'choices': choices, 'default': default})
-        return val
-
-    def get_response_data(self, result, done, extra=None, **kwargs):
-        extra = super(VmRenewView, self).get_response_data(result, done,
-                                                           extra, **kwargs)
-        extra["new_suspend_time"] = unicode(self.get_op().
-                                            instance.time_of_suspend)
-        return extra
-
-
-class VmStateChangeView(FormOperationMixin, VmOperationView):
-    op = 'emergency_change_state'
-    icon = 'legal'
-    effect = 'danger'
-    show_in_toolbar = True
-    form_class = VmStateChangeForm
-    wait_for_result = 0.5
-
-    def get_form_kwargs(self):
-        inst = self.get_op().instance
-        active_activities = InstanceActivity.objects.filter(
-            finished__isnull=True, instance=inst)
-        show_interrupt = active_activities.exists()
-        val = super(VmStateChangeView, self).get_form_kwargs()
-        val.update({'show_interrupt': show_interrupt, 'status': inst.status})
-        return val
-
-
-vm_ops = OrderedDict([
-    ('deploy', VmOperationView.factory(
-        op='deploy', icon='play', effect='success')),
-    ('wake_up', VmOperationView.factory(
-        op='wake_up', icon='sun-o', effect='success')),
-    ('sleep', VmOperationView.factory(
-        extra_bases=[TokenOperationView],
-        op='sleep', icon='moon-o', effect='info')),
-    ('migrate', VmMigrateView),
-    ('save_as_template', VmSaveView),
-    ('reboot', VmOperationView.factory(
-        op='reboot', icon='refresh', effect='warning')),
-    ('reset', VmOperationView.factory(
-        op='reset', icon='bolt', effect='warning')),
-    ('shutdown', VmOperationView.factory(
-        op='shutdown', icon='power-off', effect='warning')),
-    ('shut_off', VmOperationView.factory(
-        op='shut_off', icon='ban', effect='warning')),
-    ('recover', VmOperationView.factory(
-        op='recover', icon='medkit', effect='warning')),
-    ('nostate', VmStateChangeView),
-    ('destroy', VmOperationView.factory(
-        extra_bases=[TokenOperationView],
-        op='destroy', icon='times', effect='danger')),
-    ('create_disk', VmCreateDiskView),
-    ('download_disk', VmDownloadDiskView),
-    ('add_interface', VmAddInterfaceView),
-    ('renew', VmRenewView),
-    ('resources_change', VmResourcesChangeView),
-    ('password_reset', VmOperationView.factory(
-        op='password_reset', icon='unlock', effect='warning',
-        show_in_toolbar=False, wait_for_result=0.5, with_reload=True)),
-    ('mount_store', VmOperationView.factory(
-        op='mount_store', icon='briefcase', effect='info',
-        show_in_toolbar=False,
-    )),
-])
-
-
-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 PermissionDenied as e:
-            logger.debug('Not showing operation %s for %s: %s',
-                         k, instance, unicode(e))
-        except Exception:
-            ops.append(v.bind_to_object(instance, disabled=True))
-        else:
-            ops.append(v.bind_to_object(instance))
-    return ops
-
-
-class MassOperationView(OperationView):
-    template_name = 'dashboard/mass-operate.html'
-
-    def check_auth(self):
-        self.get_op().check_perms(self.request.user)
-        for i in self.get_object():
-            if not i.has_level(self.request.user, "user"):
-                raise PermissionDenied(
-                    "You have no user access to instance %d" % i.pk)
-
-    @classmethod
-    def get_urlname(cls):
-        return 'dashboard.vm.mass-op.%s' % cls.op
-
-    @classmethod
-    def get_url(cls):
-        return reverse("dashboard.vm.mass-op.%s" % cls.op)
-
-    def get_op(self, instance=None):
-        if instance:
-            return getattr(instance, self.op)
-        else:
-            return Instance._ops[self.op]
-
-    def get_context_data(self, **kwargs):
-        ctx = super(MassOperationView, self).get_context_data(**kwargs)
-        instances = self.get_object()
-        ctx['instances'] = self._get_operable_instances(
-            instances, self.request.user)
-        ctx['vm_count'] = sum(1 for i in ctx['instances'] if not i.disabled)
-        return ctx
-
-    def _call_operations(self, extra):
-        request = self.request
-        user = request.user
-        instances = self.get_object()
-        for i in instances:
-            try:
-                self.get_op(i).async(user=user, **extra)
-            except HumanReadableException as e:
-                e.send_message(request)
-            except Exception as e:
-                # pre-existing errors should have been catched when the
-                # confirmation dialog was constructed
-                messages.error(request, _(
-                    "Failed to execute %(op)s operation on "
-                    "instance %(instance)s.") % {"op": self.name,
-                                                 "instance": i})
-
-    def get_object(self):
-        vms = getattr(self.request, self.request.method).getlist("vm")
-        return Instance.objects.filter(pk__in=vms)
-
-    def _get_operable_instances(self, instances, user):
-        for i in instances:
-            try:
-                op = self.get_op(i)
-                op.check_auth(user)
-                op.check_precond()
-            except PermissionDenied as e:
-                i.disabled = create_readable(
-                    _("You are not permitted to execute %(op)s on instance "
-                      "%(instance)s."), instance=i.pk, op=self.name)
-                i.disabled_icon = "lock"
-            except Exception as e:
-                i.disabled = fetch_human_exception(e)
-            else:
-                i.disabled = None
-        return instances
-
-    def post(self, request, extra=None, *args, **kwargs):
-        self.check_auth()
-        if extra is None:
-            extra = {}
-        self._call_operations(extra)
-        if request.is_ajax():
-            store = messages.get_messages(request)
-            store.used = True
-            return HttpResponse(
-                json.dumps({'messages': [unicode(m) for m in store]}),
-                content_type="application/json"
-            )
-        else:
-            return redirect(reverse("dashboard.views.vm-list"))
-
-    @classmethod
-    def factory(cls, vm_op, extra_bases=(), **kwargs):
-        return type(str(cls.__name__ + vm_op.op),
-                    tuple(list(extra_bases) + [cls, vm_op]), kwargs)
-
-
-class MassMigrationView(MassOperationView, VmMigrateView):
-    template_name = 'dashboard/_vm-mass-migrate.html'
-
-vm_mass_ops = OrderedDict([
-    ('deploy', MassOperationView.factory(vm_ops['deploy'])),
-    ('wake_up', MassOperationView.factory(vm_ops['wake_up'])),
-    ('sleep', MassOperationView.factory(vm_ops['sleep'])),
-    ('reboot', MassOperationView.factory(vm_ops['reboot'])),
-    ('reset', MassOperationView.factory(vm_ops['reset'])),
-    ('shut_off', MassOperationView.factory(vm_ops['shut_off'])),
-    ('migrate', MassMigrationView),
-    ('destroy', MassOperationView.factory(vm_ops['destroy'])),
-])
-
-
-class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
-    template_name = "dashboard/node-detail.html"
-    model = Node
-    form = None
-    form_class = TraitForm
-
-    def get_context_data(self, form=None, **kwargs):
-        if form is None:
-            form = self.form_class()
-        context = super(NodeDetailView, self).get_context_data(**kwargs)
-        na = NodeActivity.objects.filter(
-            node=self.object, parent=None
-        ).order_by('-started').select_related()
-        context['activities'] = na
-        context['trait_form'] = form
-        context['graphite_enabled'] = (
-            settings.GRAPHITE_URL is not None)
-        return context
-
-    def post(self, request, *args, **kwargs):
-        if request.POST.get('new_name'):
-            return self.__set_name(request)
-        if request.POST.get('to_remove'):
-            return self.__remove_trait(request)
-        return redirect(reverse_lazy("dashboard.views.node-detail",
-                                     kwargs={'pk': self.get_object().pk}))
-
-    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})
-
-        success_message = _("Node successfully renamed.")
-        if request.is_ajax():
-            response = {
-                'message': success_message,
-                'new_name': new_name,
-                'node_pk': self.object.pk
-            }
-            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}))
-
-    def __remove_trait(self, request):
-        try:
-            to_remove = request.POST.get('to_remove')
-            self.object = self.get_object()
-            self.object.traits.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"
-            )
-        else:
-            return redirect(self.object.get_absolute_url())
-
-
-class GroupDetailView(CheckedDetailView):
-    template_name = "dashboard/group-detail.html"
-    model = Group
-    read_level = 'operator'
-
-    def get_has_level(self):
-        return self.object.profile.has_level
-
-    def get_context_data(self, **kwargs):
-        context = super(GroupDetailView, self).get_context_data(**kwargs)
-        context['group'] = self.object
-        context['users'] = self.object.user_set.all()
-        context['future_users'] = FutureMember.objects.filter(
-            group=self.object)
-        context['acl'] = AclUpdateView.get_acl_data(
-            self.object.profile, self.request.user,
-            'dashboard.views.group-acl')
-        context['aclform'] = AclUserOrGroupAddForm()
-        context['addmemberform'] = AddGroupMemberForm()
-        context['group_profile_form'] = GroupProfileUpdate.get_form_object(
-            self.request, self.object.profile)
-
-        if self.request.user.is_superuser:
-            context['group_perm_form'] = GroupPermissionForm(
-                instance=self.object)
-
-        return context
-
-    def post(self, request, *args, **kwargs):
-        self.object = self.get_object()
-        if not self.get_has_level()(request.user, 'operator'):
-            raise PermissionDenied()
-
-        if request.POST.get('new_name'):
-            return self.__set_name(request)
-        if request.POST.get('new_member'):
-            return self.__add_user(request)
-        if request.POST.get('new_members'):
-            return self.__add_list(request)
-        return redirect(reverse_lazy("dashboard.views.group-detail",
-                                     kwargs={'pk': self.get_object().pk}))
-
-    def __add_user(self, request):
-        name = request.POST['new_member']
-        self.__add_username(request, name)
-        return redirect(reverse_lazy("dashboard.views.group-detail",
-                                     kwargs={'pk': self.object.pk}))
-
-    def __add_username(self, request, name):
-        if not name:
-            return
-        try:
-            entity = search_user(name)
-            self.object.user_set.add(entity)
-        except User.DoesNotExist:
-            if saml_available:
-                FutureMember.objects.get_or_create(org_id=name,
-                                                   group=self.object)
-            else:
-                messages.warning(request, _('User "%s" not found.') % name)
-
-    def __add_list(self, request):
-        userlist = request.POST.get('new_members').split('\r\n')
-        for line in userlist:
-            self.__add_username(request, line)
-        return redirect(reverse_lazy("dashboard.views.group-detail",
-                                     kwargs={'pk': self.object.pk}))
-
-    def __set_name(self, request):
-        new_name = request.POST.get("new_name")
-        Group.objects.filter(pk=self.object.pk).update(
-            **{'name': new_name})
-
-        success_message = _("Group successfully renamed.")
-        if request.is_ajax():
-            response = {
-                'message': success_message,
-                'new_name': new_name,
-                'group_pk': self.object.pk
-            }
-            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}))
-
-
-class GroupPermissionsView(SuperuserRequiredMixin, UpdateView):
-    model = Group
-    form_class = GroupPermissionForm
-    slug_field = "pk"
-    slug_url_kwarg = "group_pk"
-
-    def get_success_url(self):
-        return "%s#group-detail-permissions" % (
-            self.get_object().groupprofile.get_absolute_url())
-
-
-class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
-    def send_success_message(self, whom, old_level, new_level):
-        if old_level and new_level:
-            msg = _("Acl user/group %(w)s successfully modified.")
-        elif not old_level and new_level:
-            msg = _("Acl user/group %(w)s successfully added.")
-        elif old_level and not new_level:
-            msg = _("Acl user/group %(w)s successfully removed.")
-        if msg:
-            messages.success(self.request, msg % {'w': whom})
-
-    def get_level(self, whom):
-        for u, level in self.acl_data:
-            if u == whom:
-                return level
-        return None
-
-    @classmethod
-    def get_acl_data(cls, obj, user, url):
-        levels = obj.ACL_LEVELS
-        allowed_levels = list(l for l in OrderedDict(levels)
-                              if cls.has_next_level(user, obj, l))
-        is_owner = 'owner' in allowed_levels
-
-        allowed_users = cls.get_allowed_users(user)
-        allowed_groups = cls.get_allowed_groups(user)
-
-        user_levels = list(
-            {'user': u, 'level': l} for u, l in obj.get_users_with_level()
-            if is_owner or u == user or u in allowed_users)
-
-        group_levels = list(
-            {'group': g, 'level': l} for g, l in obj.get_groups_with_level()
-            if is_owner or g in allowed_groups)
-
-        return {'users': user_levels,
-                'groups': group_levels,
-                'levels': levels,
-                'allowed_levels': allowed_levels,
-                'url': reverse(url, args=[obj.pk])}
-
-    @classmethod
-    def has_next_level(self, user, instance, level):
-        levels = OrderedDict(instance.ACL_LEVELS).keys()
-        next_levels = dict(zip([None] + levels, levels + levels[-1:]))
-        # {None: 'user', 'user': 'operator', 'operator: 'owner',
-        #  'owner: 'owner'}
-        next_level = next_levels[level]
-        return instance.has_level(user, next_level)
-
-    @classmethod
-    def get_allowed_groups(cls, user):
-        if user.has_perm('dashboard.use_autocomplete'):
-            return Group.objects.all()
-        else:
-            profiles = GroupProfile.get_objects_with_level('owner', user)
-            return Group.objects.filter(groupprofile__in=profiles).distinct()
-
-    @classmethod
-    def get_allowed_users(cls, user):
-        if user.has_perm('dashboard.use_autocomplete'):
-            return User.objects.all()
-        else:
-            groups = cls.get_allowed_groups(user)
-            return User.objects.filter(
-                Q(groups__in=groups) | Q(pk=user.pk)).distinct()
-
-    def check_auth(self, whom, old_level, new_level):
-        if isinstance(whom, Group):
-            if (not self.is_owner and whom not in
-                    AclUpdateView.get_allowed_groups(self.request.user)):
-                return False
-        elif isinstance(whom, User):
-            if (not self.is_owner and whom not in
-                    AclUpdateView.get_allowed_users(self.request.user)):
-                return False
-        return (
-            AclUpdateView.has_next_level(self.request.user,
-                                         self.instance, new_level) and
-            AclUpdateView.has_next_level(self.request.user,
-                                         self.instance, old_level))
-
-    def set_level(self, whom, new_level):
-        user = self.request.user
-        old_level = self.get_level(whom)
-        if old_level == new_level:
-            return
-
-        if getattr(self.instance, "owner", None) == whom:
-            logger.info("Tried to set owner's acl level for %s by %s.",
-                        unicode(self.instance), unicode(user))
-            msg = _("The original owner cannot be removed, however "
-                    "you can transfer ownership.")
-            if not getattr(self, 'hide_messages', False):
-                messages.warning(self.request, msg)
-        elif self.check_auth(whom, old_level, new_level):
-            logger.info(
-                u"Set %s's acl level for %s to %s by %s.", unicode(whom),
-                unicode(self.instance), new_level, unicode(user))
-            if not getattr(self, 'hide_messages', False):
-                self.send_success_message(whom, old_level, new_level)
-            self.instance.set_level(whom, new_level)
-        else:
-            logger.warning(
-                u"Tried to set %s's acl_level for %s (%s->%s) by %s.",
-                unicode(whom), unicode(self.instance), old_level, new_level,
-                unicode(user))
-
-    def set_or_remove_levels(self):
-        for key, value in self.request.POST.items():
-            m = re.match('(perm|remove)-([ug])-(\d+)', key)
-            if m:
-                cmd, typ, id = m.groups()
-                if cmd == 'remove':
-                    value = None
-                entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
-                self.set_level(entity, value)
-
-    def add_levels(self):
-        name = self.request.POST.get('name', None)
-        level = self.request.POST.get('level', None)
-        if not name or not level:
-            return
-        try:
-            entity = search_user(name)
-            if self.instance.object_level_set.filter(users__in=[entity]):
-                messages.warning(
-                    self.request, _('User "%s" has already '
-                                    'access to this object.') % name)
-                return
-        except User.DoesNotExist:
-            entity = None
-            try:
-                entity = Group.objects.get(name=name)
-                if self.instance.object_level_set.filter(groups__in=[entity]):
-                    messages.warning(
-                        self.request, _('Group "%s" has already '
-                                        'access to this object.') % name)
-                    return
-            except Group.DoesNotExist:
-                messages.warning(
-                    self.request, _('User or group "%s" not found.') % name)
-                return
-        self.set_level(entity, level)
-
-    def post(self, request, *args, **kwargs):
-        self.instance = self.get_object()
-        self.is_owner = self.instance.has_level(request.user, 'owner')
-        self.acl_data = (self.instance.get_users_with_level() +
-                         self.instance.get_groups_with_level())
-        self.set_or_remove_levels()
-        self.add_levels()
-        return redirect("%s#access" % self.instance.get_absolute_url())
-
-
-class TemplateAclUpdateView(AclUpdateView):
-    model = InstanceTemplate
-
-
-class GroupAclUpdateView(AclUpdateView):
-    model = Group
-
-    def get_object(self):
-        return super(GroupAclUpdateView, self).get_object().profile
-
-
-class ClientCheck(LoginRequiredMixin, TemplateView):
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/_modal.html']
-        else:
-            return ['dashboard/nojs-wrapper.html']
-
-    def get_context_data(self, *args, **kwargs):
-        context = super(ClientCheck, self).get_context_data(*args, **kwargs)
-        context.update({
-            'box_title': _('About CIRCLE Client'),
-            'ajax_title': False,
-            'client_download_url': settings.CLIENT_DOWNLOAD_URL,
-            'template': "dashboard/_client-check.html",
-            'instance': get_object_or_404(
-                Instance, pk=self.request.GET.get('vm')),
-        })
-        if not context['instance'].has_level(self.request.user, 'operator'):
-            raise PermissionDenied()
-        return context
-
-    def post(self, request, *args, **kwargs):
-        instance = get_object_or_404(Instance, pk=request.POST.get('vm'))
-        if not instance.has_level(request.user, 'operator'):
-            raise PermissionDenied()
-        response = HttpResponseRedirect(instance.get_absolute_url())
-        response.set_cookie('downloaded_client', 'True', 365 * 24 * 60 * 60)
-        return response
-
-
-class TemplateChoose(LoginRequiredMixin, 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)
-        templates = InstanceTemplate.get_objects_with_level("user",
-                                                            self.request.user)
-        context.update({
-            'box_title': _('Choose template'),
-            'ajax_title': True,
-            'template': "dashboard/_template-choose.html",
-            'templates': templates.all(),
-        })
-        return context
-
-    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"))
-        elif template is None:
-            messages.warning(request, _("Select an option to proceed."))
-            return redirect(reverse("dashboard.views.template-choose"))
-        else:
-            template = get_object_or_404(InstanceTemplate, pk=template)
-
-        if not template.has_level(request.user, "user"):
-            raise PermissionDenied()
-
-        instance = Instance.create_from_template(
-            template=template, owner=request.user, is_base=True)
-
-        return redirect(instance.get_absolute_url())
-
-
-class TemplateCreate(SuccessMessageMixin, CreateView):
-    model = InstanceTemplate
-    form_class = TemplateForm
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            pass
-        else:
-            return ['dashboard/nojs-wrapper.html']
-
-    def get_context_data(self, *args, **kwargs):
-        context = super(TemplateCreate, self).get_context_data(*args, **kwargs)
-
-        num_leases = Lease.get_objects_with_level("operator",
-                                                  self.request.user).count()
-        can_create_leases = self.request.user.has_perm("create_leases")
-        context.update({
-            'box_title': _("Create a new base VM"),
-            'template': "dashboard/_template-create.html",
-            'show_lease_create': num_leases < 1 and can_create_leases
-        })
-        return context
-
-    def get(self, *args, **kwargs):
-        if not self.request.user.has_perm('vm.create_base_template'):
-            raise PermissionDenied()
-
-        return super(TemplateCreate, self).get(*args, **kwargs)
-
-    def get_form_kwargs(self):
-        kwargs = super(TemplateCreate, self).get_form_kwargs()
-        kwargs['user'] = self.request.user
-        return kwargs
-
-    def post(self, request, *args, **kwargs):
-        if not self.request.user.has_perm('vm.create_base_template'):
-            raise PermissionDenied()
-
-        form = self.form_class(request.POST, user=request.user)
-        if not form.is_valid():
-            return self.get(request, form, *args, **kwargs)
-        else:
-            post = form.cleaned_data
-            networks = self.__create_networks(post.pop("networks"),
-                                              request.user)
-            post.pop("parent")
-            post['max_ram_size'] = post['ram_size']
-            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)
-
-            return redirect("%s#resources" % inst.get_absolute_url())
-
-    def __create_networks(self, vlans, user):
-        networks = []
-        for v in vlans:
-            if not v.has_level(user, "user"):
-                raise PermissionDenied()
-            networks.append(InterfaceTemplate(vlan=v, managed=v.managed))
-        return networks
-
-    def get_success_url(self):
-        return reverse_lazy("dashboard.views.template-list")
-
-
-class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
-    model = InstanceTemplate
-    template_name = "dashboard/template-edit.html"
-    form_class = TemplateForm
-    success_message = _("Successfully modified template.")
-
-    def get(self, request, *args, **kwargs):
-        template = self.get_object()
-        if not template.has_level(request.user, 'user'):
-            raise PermissionDenied()
-        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:
-            return super(TemplateDetail, self).get(request, *args, **kwargs)
-
-    def get_context_data(self, **kwargs):
-        obj = self.get_object()
-        context = super(TemplateDetail, self).get_context_data(**kwargs)
-        context['acl'] = AclUpdateView.get_acl_data(
-            obj, self.request.user, 'dashboard.views.template-acl')
-        context['disks'] = obj.disks.all()
-        context['is_owner'] = obj.has_level(self.request.user, 'owner')
-        context['aclform'] = AclUserOrGroupAddForm()
-        return context
-
-    def get_success_url(self):
-        return reverse_lazy("dashboard.views.template-detail",
-                            kwargs=self.kwargs)
-
-    def post(self, request, *args, **kwargs):
-        template = self.get_object()
-        if not template.has_level(request.user, 'owner'):
-            raise PermissionDenied()
-        return super(TemplateDetail, self).post(self, request, args, kwargs)
-
-    def get_form_kwargs(self):
-        kwargs = super(TemplateDetail, self).get_form_kwargs()
-        kwargs['user'] = self.request.user
-        return kwargs
-
-
-class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView):
-    template_name = "dashboard/template-list.html"
-    model = InstanceTemplate
-    table_class = TemplateListTable
-    table_pagination = False
-
-    allowed_filters = {
-        'name': "name__icontains",
-        'tags[]': "tags__name__in",
-        'tags': "tags__name__in",  # for search string
-        'owner': "owner__username",
-        'ram': "ram_size",
-        'ram_size': "ram_size",
-        'cores': "num_cores",
-        'num_cores': "num_cores",
-        'access_method': "access_method__iexact",
-    }
-
-    def get_context_data(self, *args, **kwargs):
-        context = super(TemplateList, self).get_context_data(*args, **kwargs)
-        user = self.request.user
-        leases_w_operator = Lease.get_objects_with_level("operator", user)
-        context['lease_table'] = LeaseListTable(
-            leases_w_operator, request=self.request,
-            template="django_tables2/table_no_page.html",
-        )
-        context['show_lease_table'] = (
-            leases_w_operator.count() > 0 or
-            user.has_perm("vm.create_leases")
-        )
-
-        context['search_form'] = self.search_form
-
-        return context
-
-    def get(self, *args, **kwargs):
-        self.search_form = TemplateListSearchForm(self.request.GET)
-        self.search_form.full_clean()
-        return super(TemplateList, self).get(*args, **kwargs)
-
-    def create_acl_queryset(self, model):
-        queryset = super(TemplateList, self).create_acl_queryset(model)
-        sql = ("SELECT count(*) FROM vm_instance WHERE "
-               "vm_instance.template_id = vm_instancetemplate.id and "
-               "vm_instance.destroyed_at is null and "
-               "vm_instance.status = 'RUNNING'")
-        queryset = queryset.extra(select={'running': sql})
-        return queryset
-
-    def get_queryset(self):
-        logger.debug('TemplateList.get_queryset() called. User: %s',
-                     unicode(self.request.user))
-        qs = self.create_acl_queryset(InstanceTemplate)
-        self.create_fake_get()
-
-        try:
-            qs = qs.filter(**self.get_queryset_filters()).distinct()
-        except ValueError:
-            messages.error(self.request, _("Error during filtering."))
-
-        return qs.select_related("lease", "owner", "owner__profile")
-
-
-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.destroy_disks()
-        object.delete()
-        success_url = self.get_success_url()
-        success_message = _("Template successfully deleted.")
-
-        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)
-
-
-class VmList(LoginRequiredMixin, FilterMixin, ListView):
-    template_name = "dashboard/vm-list.html"
-    allowed_filters = {
-        'name': "name__icontains",
-        'node': "node__name__icontains",
-        'status': "status__iexact",
-        'tags[]': "tags__name__in",
-        'tags': "tags__name__in",  # for search string
-        'owner': "owner__username",
-        'template': "template__pk",
-    }
-
-    def get_context_data(self, *args, **kwargs):
-        context = super(VmList, self).get_context_data(*args, **kwargs)
-        context['ops'] = []
-        for k, v in vm_mass_ops.iteritems():
-            try:
-                v.check_perms(user=self.request.user)
-            except PermissionDenied:
-                pass
-            else:
-                context['ops'].append(v)
-        context['search_form'] = self.search_form
-        context['show_acts_in_progress'] = self.object_list.count() < 100
-        return context
-
-    def get(self, *args, **kwargs):
-        if self.request.is_ajax():
-            return self._create_ajax_request()
-        else:
-            self.search_form = VmListSearchForm(self.request.GET)
-            self.search_form.full_clean()
-            return super(VmList, self).get(*args, **kwargs)
-
-    def _create_ajax_request(self):
-        if self.request.GET.get("compact") is not None:
-            instances = Instance.get_objects_with_level(
-                "user", self.request.user).filter(destroyed_at=None)
-            statuses = {}
-            for i in instances:
-                statuses[i.pk] = {
-                    'status': i.get_status_display(),
-                    'icon': i.get_status_icon(),
-                    'in_status_change': i.is_in_status_change(),
-                }
-                if self.request.user.is_superuser:
-                    statuses[i.pk]['node'] = i.node.name if i.node else "-"
-            return HttpResponse(json.dumps(statuses),
-                                content_type="application/json")
-        else:
-            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(
-                destroyed_at=None).all()
-            instances = [{
-                'pk': i.pk,
-                'name': i.name,
-                'icon': i.get_status_icon(),
-                'host': i.short_hostname,
-                'status': i.get_status_display(),
-                'fav': i.pk in favs,
-            } for i in instances]
-            return HttpResponse(
-                json.dumps(list(instances)),  # instances is ValuesQuerySet
-                content_type="application/json",
-            )
-
-    def create_acl_queryset(self, model):
-        queryset = super(VmList, self).create_acl_queryset(model)
-        if not self.search_form.cleaned_data.get("include_deleted"):
-            queryset = queryset.filter(destroyed_at=None)
-        return queryset
-
-    def get_queryset(self):
-        logger.debug('VmList.get_queryset() called. User: %s',
-                     unicode(self.request.user))
-        queryset = self.create_acl_queryset(Instance)
-
-        self.create_fake_get()
-        sort = self.request.GET.get("sort")
-        # remove "-" that means descending order
-        # also check if the column name is valid
-        if (sort and
-            (sort[1:] if sort[0] == "-" else sort)
-                in [i.name for i in Instance._meta.fields] + ["pk"]):
-            queryset = queryset.order_by(sort)
-
-        return queryset.filter(
-            **self.get_queryset_filters()).prefetch_related(
-                "owner", "node", "owner__profile", "interface_set", "lease",
-                "interface_set__host").distinct()
-
-
-class NodeList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
-    template_name = "dashboard/node-list.html"
-    table_class = NodeListTable
-    table_pagination = False
-
-    def get(self, *args, **kwargs):
-        if self.request.is_ajax():
-            nodes = Node.objects.all()
-            nodes = [{
-                'name': i.name,
-                'icon': i.get_status_icon(),
-                'url': i.get_absolute_url(),
-                'label': i.get_status_label(),
-                'status': i.state.lower()} for i in nodes]
-
-            return HttpResponse(
-                json.dumps(list(nodes)),
-                content_type="application/json",
-            )
-        else:
-            return super(NodeList, self).get(*args, **kwargs)
-
-    def get_queryset(self):
-        return Node.objects.annotate(
-            number_of_VMs=Count('instance_set')).select_related('host')
-
-
-class GroupList(LoginRequiredMixin, SingleTableView):
-    template_name = "dashboard/group-list.html"
-    model = Group
-    table_class = GroupListTable
-    table_pagination = False
-
-    def get(self, *args, **kwargs):
-        if self.request.is_ajax():
-            groups = [{
-                'url': reverse("dashboard.views.group-detail",
-                               kwargs={'pk': i.pk}),
-                'name': i.name} for i in self.get_queryset()]
-            return HttpResponse(
-                json.dumps(list(groups)),
-                content_type="application/json",
-            )
-        else:
-            return super(GroupList, self).get(*args, **kwargs)
-
-    def get_queryset(self):
-        logger.debug('GroupList.get_queryset() called. User: %s',
-                     unicode(self.request.user))
-        profiles = GroupProfile.get_objects_with_level(
-            'operator', self.request.user)
-        groups = Group.objects.filter(groupprofile__in=profiles)
-        s = self.request.GET.get("s")
-        if s:
-            groups = groups.filter(name__icontains=s)
-        return groups
-
-
-class GroupRemoveUserView(CheckedDetailView, DeleteView):
-    model = Group
-    slug_field = 'pk'
-    slug_url_kwarg = 'group_pk'
-    read_level = 'operator'
-    member_key = 'member_pk'
-
-    def get_has_level(self):
-        return self.object.profile.has_level
-
-    def get_context_data(self, **kwargs):
-        context = super(GroupRemoveUserView, self).get_context_data(**kwargs)
-        try:
-            context['member'] = User.objects.get(pk=self.member_pk)
-        except User.DoesNotExist:
-            raise Http404()
-        return context
-
-    def get_success_url(self):
-        next = self.request.POST.get('next')
-        if next:
-            return next
-        else:
-            return reverse_lazy("dashboard.views.group-detail",
-                                kwargs={'pk': self.get_object().pk})
-
-    def get(self, request, member_pk, *args, **kwargs):
-        self.member_pk = member_pk
-        return super(GroupRemoveUserView, self).get(request, *args, **kwargs)
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/confirm/ajax-remove.html']
-        else:
-            return ['dashboard/confirm/base-remove.html']
-
-    def remove_member(self, pk):
-        container = self.get_object()
-        container.user_set.remove(User.objects.get(pk=pk))
-
-    def get_success_message(self):
-        return _("Member successfully removed from group.")
-
-    def delete(self, request, *args, **kwargs):
-        object = self.get_object()
-        if not object.profile.has_level(request.user, 'operator'):
-            raise PermissionDenied()
-        self.remove_member(kwargs[self.member_key])
-        success_url = self.get_success_url()
-        success_message = self.get_success_message()
-        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)
-
-
-class GroupRemoveFutureUserView(GroupRemoveUserView):
-
-    member_key = 'member_org_id'
-
-    def get(self, request, member_org_id, *args, **kwargs):
-        self.member_org_id = member_org_id
-        return super(GroupRemoveUserView, self).get(request, *args, **kwargs)
-
-    def get_context_data(self, **kwargs):
-        context = super(GroupRemoveUserView, self).get_context_data(**kwargs)
-        try:
-            context['member'] = FutureMember.objects.get(
-                org_id=self.member_org_id, group=self.get_object())
-        except FutureMember.DoesNotExist:
-            raise Http404()
-        return context
-
-    def remove_member(self, org_id):
-        FutureMember.objects.filter(org_id=org_id,
-                                    group=self.get_object()).delete()
-
-    def get_success_message(self):
-        return _("Future user successfully removed from group.")
-
-
-class GroupDelete(CheckedDetailView, DeleteView):
-
-    """This stuff deletes the group.
-    """
-    model = Group
-    template_name = "dashboard/confirm/base-delete.html"
-    read_level = 'operator'
-
-    def get_has_level(self):
-        return self.object.profile.has_level
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/confirm/ajax-delete.html']
-        else:
-            return ['dashboard/confirm/base-delete.html']
-
-    # github.com/django/django/blob/master/django/views/generic/edit.py#L245
-    def delete(self, request, *args, **kwargs):
-        object = self.get_object()
-        if not object.profile.has_level(request.user, 'owner'):
-            raise PermissionDenied()
-        object.delete()
-        success_url = self.get_success_url()
-        success_message = _("Group successfully deleted.")
-
-        if request.is_ajax():
-            if request.POST.get('redirect').lower() == "true":
-                messages.success(request, success_message)
-            return HttpResponse(
-                json.dumps({'message': success_message}),
-                content_type="application/json",
-            )
-        else:
-            messages.success(request, success_message)
-            return HttpResponseRedirect(success_url)
-
-    def get_success_url(self):
-        next = self.request.POST.get('next')
-        if next:
-            return next
-        else:
-            return reverse_lazy('dashboard.index')
-
-
-class VmCreate(LoginRequiredMixin, TemplateView):
-
-    form_class = VmCustomizeForm
-    form = None
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/modal-wrapper.html']
-        else:
-            return ['dashboard/nojs-wrapper.html']
-
-    def get(self, request, form=None, *args, **kwargs):
-        if not request.user.has_perm('vm.create_vm'):
-            raise PermissionDenied()
-
-        if form is None:
-            template_pk = request.GET.get("template")
-        else:
-            template_pk = form.template.pk
-
-        if template_pk:
-            template = get_object_or_404(InstanceTemplate, pk=template_pk)
-            if not template.has_level(request.user, 'user'):
-                raise PermissionDenied()
-            if form is None:
-                form = self.form_class(user=request.user, template=template)
-        else:
-            templates = InstanceTemplate.get_objects_with_level(
-                'user', request.user, disregard_superuser=True)
-
-        context = self.get_context_data(**kwargs)
-        if template_pk:
-            context.update({
-                'template': 'dashboard/_vm-create-2.html',
-                'box_title': _('Customize VM'),
-                'ajax_title': True,
-                'vm_create_form': form,
-                'template_o': template,
-            })
-        else:
-            context.update({
-                'template': 'dashboard/_vm-create-1.html',
-                'box_title': _('Create a VM'),
-                'ajax_title': True,
-                'templates': templates.all(),
-            })
-        return self.render_to_response(context)
-
-    def __create_normal(self, request, *args, **kwargs):
-        user = request.user
-        template = InstanceTemplate.objects.get(
-            pk=request.POST.get("template"))
-
-        # permission check
-        if not template.has_level(request.user, 'user'):
-            raise PermissionDenied()
-
-        args = {"template": template, "owner": user}
-        instances = [Instance.create_from_template(**args)]
-        return self.__deploy(request, instances)
-
-    def __create_customized(self, request, *args, **kwargs):
-        user = request.user
-        # no form yet, using POST directly:
-        template = get_object_or_404(InstanceTemplate,
-                                     pk=request.POST.get("template"))
-        form = self.form_class(
-            request.POST, user=request.user, template=template)
-        if not form.is_valid():
-            return self.get(request, form, *args, **kwargs)
-        post = form.cleaned_data
-
-        if not template.has_level(user, 'user'):
-            raise PermissionDenied()
-
-        ikwargs = {
-            'name': post['name'],
-            'template': template,
-            'owner': user,
-        }
-        amount = post.get("amount", 1)
-        if request.user.has_perm('vm.set_resources'):
-            networks = [InterfaceTemplate(vlan=l, managed=l.managed)
-                        for l in post['networks']]
-
-            ikwargs.update({
-                'num_cores': post['cpu_count'],
-                'ram_size': post['ram_size'],
-                'priority': post['cpu_priority'],
-                'max_ram_size': post['ram_size'],
-                'networks': networks,
-                'disks': list(template.disks.all()),
-            })
-
-        else:
-            pass
-
-        instances = Instance.mass_create_from_template(amount=amount,
-                                                       **ikwargs)
-        return self.__deploy(request, instances)
-
-    def __deploy(self, request, instances, *args, **kwargs):
-        for i in instances:
-            i.deploy.async(user=request.user)
-
-        if len(instances) > 1:
-            messages.success(request, ungettext_lazy(
-                "Successfully created %(count)d VM.",  # this should not happen
-                "Successfully created %(count)d VMs.", len(instances)) % {
-                'count': len(instances)})
-            path = "%s?stype=owned" % reverse("dashboard.views.vm-list")
-        else:
-            messages.success(request, _("VM successfully created."))
-            path = instances[0].get_absolute_url()
-
-        if request.is_ajax():
-            return HttpResponse(json.dumps({'redirect': path}),
-                                content_type="application/json")
-        else:
-            return redirect("%s#activity" % path)
-
-    def post(self, request, *args, **kwargs):
-        user = request.user
-
-        if not request.user.has_perm('vm.create_vm'):
-            raise PermissionDenied()
-
-        # limit chekcs
-        try:
-            limit = user.profile.instance_limit
-        except Exception as e:
-            logger.debug('No profile or instance limit: %s', e)
-        else:
-            try:
-                amount = int(request.POST.get("amount", 1))
-            except:
-                amount = limit  # TODO this should definitely use a Form
-            current = Instance.active.filter(owner=user).count()
-            logger.debug('current use: %d, limit: %d', current, limit)
-            if current + amount > limit:
-                messages.error(request,
-                               _('Instance limit (%d) exceeded.') % limit)
-                if request.is_ajax():
-                    return HttpResponse(json.dumps({'redirect': '/'}),
-                                        content_type="application/json")
-                else:
-                    return redirect('/')
-
-        create_func = (self.__create_normal if
-                       request.POST.get("customized") is None else
-                       self.__create_customized)
-
-        return create_func(request, *args, **kwargs)
-
-
-class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
-
-    form_class = HostForm
-    hostform = None
-
-    formset_class = inlineformset_factory(Host, Node, form=NodeForm, extra=1)
-    formset = None
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/modal-wrapper.html']
-        else:
-            return ['dashboard/nojs-wrapper.html']
-
-    def get(self, request, hostform=None, formset=None, *args, **kwargs):
-        if hostform is None:
-            hostform = self.form_class()
-        if formset is None:
-            formset = self.formset_class(instance=Host())
-        context = self.get_context_data(**kwargs)
-        context.update({
-            'template': 'dashboard/node-create.html',
-            'box_title': 'Create a Node',
-            'hostform': hostform,
-            'formset': formset,
-
-        })
-        return self.render_to_response(context)
-
-    # TODO handle not ajax posts
-    def post(self, request, *args, **kwargs):
-        if not self.request.user.is_authenticated():
-            raise PermissionDenied()
-
-        hostform = self.form_class(request.POST)
-        formset = self.formset_class(request.POST, Host())
-        if not hostform.is_valid():
-            return self.get(request, hostform, formset, *args, **kwargs)
-        hostform.setowner(request.user)
-        savedform = hostform.save(commit=False)
-        formset = self.formset_class(request.POST, instance=savedform)
-        if not formset.is_valid():
-            return self.get(request, hostform, formset, *args, **kwargs)
-
-        savedform.save()
-        nodemodel = formset.save()
-        messages.success(request, _('Node successfully created.'))
-        path = nodemodel[0].get_absolute_url()
-        if request.is_ajax():
-            return HttpResponse(json.dumps({'redirect': path}),
-                                content_type="application/json")
-        else:
-            return redirect(path)
-
-
-class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
-
-    form_class = GroupCreateForm
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/modal-wrapper.html']
-        else:
-            return ['dashboard/nojs-wrapper.html']
-
-    def get(self, request, form=None, *args, **kwargs):
-        if not request.user.has_module_perms('auth'):
-            raise PermissionDenied()
-        if form is None:
-            form = self.form_class(
-                new_groups=self.get_available_group_codes(request))
-        context = self.get_context_data(**kwargs)
-        context.update({
-            'template': 'dashboard/group-create.html',
-            'box_title': _('Create a Group'),
-            'form': form,
-            'ajax_title': True,
-        })
-        return self.render_to_response(context)
-
-    def post(self, request, *args, **kwargs):
-        if not request.user.has_module_perms('auth'):
-            raise PermissionDenied()
-        form = self.form_class(
-            request.POST, new_groups=self.get_available_group_codes(request))
-        if not form.is_valid():
-            return self.get(request, form, *args, **kwargs)
-        form.cleaned_data
-        savedform = form.save()
-        savedform.profile.set_level(request.user, 'owner')
-        messages.success(request, _('Group successfully created.'))
-        if request.is_ajax():
-            return HttpResponse(json.dumps({'redirect':
-                                savedform.profile.get_absolute_url()}),
-                                content_type="application/json")
-        else:
-            return redirect(savedform.profile.get_absolute_url())
-
-
-class GroupProfileUpdate(SuccessMessageMixin, GroupCodeMixin,
-                         LoginRequiredMixin, UpdateView):
-
-    form_class = GroupProfileUpdateForm
-    model = Group
-    success_message = _('Group is successfully updated.')
-
-    @classmethod
-    def get_available_group_codes(cls, request, extra=None):
-        result = super(GroupProfileUpdate, cls).get_available_group_codes(
-            request)
-        if extra and extra not in result:
-            result += [extra]
-        return result
-
-    def get_object(self):
-        group = super(GroupProfileUpdate, self).get_object()
-        profile = group.profile
-        if not profile.has_level(self.request.user, 'owner'):
-            raise PermissionDenied
-        else:
-            return profile
-
-    @classmethod
-    def get_form_object(cls, request, instance, *args, **kwargs):
-        kwargs['instance'] = instance
-        kwargs['new_groups'] = cls.get_available_group_codes(
-            request, instance.org_id)
-        kwargs['superuser'] = request.user.is_superuser
-        return cls.form_class(*args, **kwargs)
-
-    def get(self, request, form=None, *args, **kwargs):
-        self.object = self.get_object()
-        if form is None:
-            form = self.get_form_object(request, self.object)
-        return super(GroupProfileUpdate, self).get(
-            request, form, *args, **kwargs)
-
-    def post(self, request, *args, **kwargs):
-        if not request.user.has_module_perms('auth'):
-            raise PermissionDenied()
-        self.object = self.get_object()
-        form = self.get_form_object(request, self.object, self.request.POST)
-        if not form.is_valid():
-            return self.form_invalid(form)
-        form.save()
-        return self.form_valid(form)
-
-
-class NodeDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
-
-    """This stuff deletes the node.
-    """
-    model = Node
-    template_name = "dashboard/confirm/base-delete.html"
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/confirm/ajax-delete.html']
-        else:
-            return ['dashboard/confirm/base-delete.html']
-
-    # github.com/django/django/blob/master/django/views/generic/edit.py#L245
-    def delete(self, request, *args, **kwargs):
-        object = self.get_object()
-
-        object.delete()
-        success_url = self.get_success_url()
-        success_message = _("Node successfully deleted.")
-
-        if request.is_ajax():
-            if request.POST.get('redirect').lower() == "true":
-                messages.success(request, success_message)
-            return HttpResponse(
-                json.dumps({'message': success_message}),
-                content_type="application/json",
-            )
-        else:
-            messages.success(request, success_message)
-            return HttpResponseRedirect(success_url)
-
-    def get_success_url(self):
-        next = self.request.POST.get('next')
-        if next:
-            return next
-        else:
-            return reverse_lazy('dashboard.index')
-
-
-class NodeAddTraitView(SuperuserRequiredMixin, DetailView):
-    model = Node
-    template_name = "dashboard/node-add-trait.html"
-
-    def get_success_url(self):
-        next = self.request.GET.get('next')
-        if next:
-            return next
-        else:
-            return self.object.get_absolute_url()
-
-    def get_context_data(self, **kwargs):
-        self.object = self.get_object()
-        context = super(NodeAddTraitView, self).get_context_data(**kwargs)
-        context['form'] = (TraitForm(self.request.POST) if self.request.POST
-                           else TraitForm())
-        return context
-
-    def post(self, request, pk, *args, **kwargs):
-        context = self.get_context_data(**kwargs)
-        form = context['form']
-        if form.is_valid():
-            node = self.object
-            n = form.cleaned_data['name']
-            trait, created = Trait.objects.get_or_create(name=n)
-            node.traits.add(trait)
-            success_message = _("Trait successfully added to node.")
-            messages.success(request, success_message)
-            return redirect(self.get_success_url())
-        else:
-            return self.get(self, request, pk, *args, **kwargs)
-
-
-class NodeStatus(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
-    template_name = "dashboard/confirm/node-status.html"
-    model = Node
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/confirm/ajax-node-status.html']
-        else:
-            return ['dashboard/confirm/node-status.html']
-
-    def get_success_url(self):
-        next = self.request.GET.get('next')
-        if next:
-            return next
-        else:
-            return reverse_lazy("dashboard.views.node-detail",
-                                kwargs={'pk': self.object.pk})
-
-    def get_context_data(self, **kwargs):
-        context = super(NodeStatus, self).get_context_data(**kwargs)
-        if self.object.enabled:
-            context['status'] = "disable"
-        else:
-            context['status'] = "enable"
-        return context
-
-    def post(self, request, *args, **kwargs):
-        if request.POST.get('change_status') is not None:
-            return self.__set_status(request)
-        return redirect(reverse_lazy("dashboard.views.node-detail",
-                                     kwargs={'pk': self.get_object().pk}))
-
-    def __set_status(self, request):
-        self.object = self.get_object()
-        if not self.object.enabled:
-            self.object.enable(user=request.user)
-        else:
-            self.object.disable(user=request.user)
-        success_message = _("Node successfully changed status.")
-
-        if request.is_ajax():
-            response = {
-                'message': success_message,
-                'node_pk': self.object.pk
-            }
-            return HttpResponse(
-                json.dumps(response),
-                content_type="application/json"
-            )
-        else:
-            messages.success(request, success_message)
-            return redirect(self.get_success_url())
-
-
-class NodeFlushView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
-    template_name = "dashboard/confirm/node-flush.html"
-    model = Node
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/confirm/ajax-node-flush.html']
-        else:
-            return ['dashboard/confirm/node-flush.html']
-
-    def get_success_url(self):
-        next = self.request.GET.get('next')
-        if next:
-            return next
-        else:
-            return reverse_lazy("dashboard.views.node-detail",
-                                kwargs={'pk': self.object.pk})
-
-    def get_context_data(self, **kwargs):
-        context = super(NodeFlushView, self).get_context_data(**kwargs)
-        return context
-
-    def post(self, request, *args, **kwargs):
-        if request.POST.get('flush') is not None:
-            return self.__flush(request)
-        return redirect(reverse_lazy("dashboard.views.node-detail",
-                                     kwargs={'pk': self.get_object().pk}))
-
-    def __flush(self, request):
-        self.object = self.get_object()
-        self.object.flush.async(user=request.user)
-        success_message = _("Node successfully flushed.")
-        messages.success(request, success_message)
-        return redirect(self.get_success_url())
-
-
-class PortDelete(LoginRequiredMixin, DeleteView):
-    model = Rule
-    pk_url_kwarg = 'rule'
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/confirm/ajax-delete.html']
-        else:
-            return ['dashboard/confirm/base-delete.html']
-
-    def get_context_data(self, **kwargs):
-        context = super(PortDelete, self).get_context_data(**kwargs)
-        rule = kwargs.get('object')
-        instance = rule.host.interface_set.get().instance
-        context['title'] = _("Port delete confirmation")
-        context['text'] = _("Are you sure you want to close %(port)d/"
-                            "%(proto)s on %(vm)s?" % {'port': rule.dport,
-                                                      'proto': rule.proto,
-                                                      'vm': instance})
-        return context
-
-    def delete(self, request, *args, **kwargs):
-        rule = Rule.objects.get(pk=kwargs.get("rule"))
-        instance = rule.host.interface_set.get().instance
-        if not instance.has_level(request.user, 'owner'):
-            raise PermissionDenied()
-
-        super(PortDelete, self).delete(request, *args, **kwargs)
-
-        success_url = self.get_success_url()
-        success_message = _("Port successfully removed.")
-
-        if request.is_ajax():
-            return HttpResponse(
-                json.dumps({'message': success_message}),
-                content_type="application/json",
-            )
-        else:
-            messages.success(request, success_message)
-            return HttpResponseRedirect("%s#network" % success_url)
-
-    def get_success_url(self):
-        return reverse_lazy('dashboard.views.detail',
-                            kwargs={'pk': self.kwargs.get("pk")})
-
-
-class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin,
-                  SuccessMessageMixin, CreateView):
-    model = Lease
-    form_class = LeaseForm
-    permission_required = 'vm.create_leases'
-    template_name = "dashboard/lease-create.html"
-    success_message = _("Successfully created a new lease.")
-
-    def get_success_url(self):
-        return reverse_lazy("dashboard.views.template-list")
-
-
-class LeaseAclUpdateView(AclUpdateView):
-    model = Lease
-
-
-class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin,
-                  SuccessMessageMixin, UpdateView):
-    model = Lease
-    form_class = LeaseForm
-    template_name = "dashboard/lease-edit.html"
-    success_message = _("Successfully modified lease.")
-
-    def get_context_data(self, *args, **kwargs):
-        obj = self.get_object()
-        context = super(LeaseDetail, self).get_context_data(*args, **kwargs)
-        context['acl'] = AclUpdateView.get_acl_data(
-            obj, self.request.user, 'dashboard.views.lease-acl')
-        return context
-
-    def get_success_url(self):
-        return reverse_lazy("dashboard.views.lease-detail", kwargs=self.kwargs)
-
-
-class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
-    model = Lease
-
-    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 get_context_data(self, *args, **kwargs):
-        c = super(LeaseDelete, self).get_context_data(*args, **kwargs)
-        lease = self.get_object()
-        templates = lease.instancetemplate_set
-        if templates.count() > 0:
-            text = _("You can't delete this lease because some templates "
-                     "are still using it, modify these to proceed: ")
-
-            c['text'] = text + ", ".join("<strong>%s (#%d)</strong>"
-                                         "" % (o.name, o.pk)
-                                         for o in templates.all())
-            c['disable_submit'] = True
-        return c
-
-    def delete(self, request, *args, **kwargs):
-        object = self.get_object()
-
-        if (object.instancetemplate_set.count() > 0):
-            raise SuspiciousOperation()
-
-        object.delete()
-        success_url = self.get_success_url()
-        success_message = _("Lease successfully deleted.")
-
-        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)
-
-
-@require_GET
-def vm_activity(request, pk):
-    instance = Instance.objects.get(pk=pk)
-    if not instance.has_level(request.user, 'user'):
-        raise PermissionDenied()
-
-    response = {}
-    show_all = request.GET.get("show_all", "false") == "true"
-    activities = _format_activities(
-        instance.get_merged_activities(request.user))
-    show_show_all = len(activities) > 10
-    if not show_all:
-        activities = activities[:10]
-
-    response['connect_uri'] = instance.get_connect_uri()
-    response['human_readable_status'] = instance.get_status_display()
-    response['status'] = instance.status
-    response['icon'] = instance.get_status_icon()
-    latest = instance.get_latest_activity_in_progress()
-    response['is_new_state'] = (latest and latest.resultant_state is not None
-                                and instance.status != latest.resultant_state)
-
-    context = {
-        'instance': instance,
-        'activities': activities,
-        'show_show_all': show_show_all,
-        'ops': get_operations(instance, request.user),
-    }
-
-    response['activities'] = render_to_string(
-        "dashboard/vm-detail/_activity-timeline.html",
-        RequestContext(request, context),
-    )
-    response['ops'] = render_to_string(
-        "dashboard/vm-detail/_operations.html",
-        RequestContext(request, context),
-    )
-    response['disk_ops'] = render_to_string(
-        "dashboard/vm-detail/_disk-operations.html",
-        RequestContext(request, context),
-    )
-
-    return HttpResponse(
-        json.dumps(response),
-        content_type="application/json"
-    )
-
-
-class FavouriteView(TemplateView):
-
-    def post(self, *args, **kwargs):
-        user = self.request.user
-        vm = Instance.objects.get(pk=self.request.POST.get("vm"))
-        if not vm.has_level(user, 'user'):
-            raise PermissionDenied()
-        try:
-            Favourite.objects.get(instance=vm, user=user).delete()
-            return HttpResponse("Deleted.")
-        except Favourite.DoesNotExist:
-            Favourite(instance=vm, user=user).save()
-            return HttpResponse("Added.")
-
-
-class TransferOwnershipView(CheckedDetailView, DetailView):
-    model = Instance
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/_modal.html']
-        else:
-            return ['dashboard/nojs-wrapper.html']
-
-    def get_context_data(self, *args, **kwargs):
-        context = super(TransferOwnershipView, self).get_context_data(
-            *args, **kwargs)
-        context['form'] = TransferOwnershipForm()
-        context.update({
-            'box_title': _("Transfer ownership"),
-            'ajax_title': True,
-            'template': "dashboard/vm-detail/tx-owner.html",
-        })
-        return context
-
-    def post(self, request, *args, **kwargs):
-        form = TransferOwnershipForm(request.POST)
-        if not form.is_valid():
-            return self.get(request)
-        try:
-            new_owner = search_user(request.POST['name'])
-        except User.DoesNotExist:
-            messages.error(request, _('Can not find specified user.'))
-            return self.get(request, *args, **kwargs)
-        except KeyError:
-            raise SuspiciousOperation()
-
-        obj = self.get_object()
-        if not (obj.owner == request.user or
-                request.user.is_superuser):
-            raise PermissionDenied()
-
-        token = signing.dumps((obj.pk, new_owner.pk),
-                              salt=TransferOwnershipConfirmView.get_salt())
-        token_path = reverse(
-            'dashboard.views.vm-transfer-ownership-confirm', args=[token])
-        try:
-            new_owner.profile.notify(
-                ugettext_noop('Ownership offer'),
-                ugettext_noop('%(user)s offered you to take the ownership of '
-                              'his/her virtual machine called %(instance)s. '
-                              '<a href="%(token)s" '
-                              'class="btn btn-success btn-small">Accept</a>'),
-                {'instance': obj, 'token': token_path})
-        except Profile.DoesNotExist:
-            messages.error(request, _('Can not notify selected user.'))
-        else:
-            messages.success(request,
-                             _('User %s is notified about the offer.') % (
-                                 unicode(new_owner), ))
-
-        return redirect(reverse_lazy("dashboard.views.detail",
-                                     kwargs={'pk': obj.pk}))
-
-
-class TransferOwnershipConfirmView(LoginRequiredMixin, View):
-    """User can accept an ownership offer."""
-
-    max_age = 3 * 24 * 3600
-    success_message = _("Ownership successfully transferred to you.")
-
-    @classmethod
-    def get_salt(cls):
-        return unicode(cls)
-
-    def get(self, request, key, *args, **kwargs):
-        """Confirm ownership transfer based on token.
-        """
-        logger.debug('Confirm dialog for token %s.', key)
-        try:
-            instance, new_owner = self.get_instance(key, request.user)
-        except PermissionDenied:
-            messages.error(request, _('This token is for an other user.'))
-            raise
-        except SuspiciousOperation:
-            messages.error(request, _('This token is invalid or has expired.'))
-            raise PermissionDenied()
-        return render(request,
-                      "dashboard/confirm/base-transfer-ownership.html",
-                      dictionary={'instance': instance, 'key': key})
-
-    def post(self, request, key, *args, **kwargs):
-        """Really transfer ownership based on token.
-        """
-        instance, owner = self.get_instance(key, request.user)
-
-        old = instance.owner
-        with instance_activity(code_suffix='ownership-transferred',
-                               instance=instance, user=request.user):
-            instance.owner = request.user
-            instance.clean()
-            instance.save()
-        messages.success(request, self.success_message)
-        logger.info('Ownership of %s transferred from %s to %s.',
-                    unicode(instance), unicode(old), unicode(request.user))
-        if old.profile:
-            old.profile.notify(
-                ugettext_noop('Ownership accepted'),
-                ugettext_noop('Your ownership offer of %(instance)s has been '
-                              'accepted by %(user)s.'),
-                {'instance': instance})
-        return HttpResponseRedirect(instance.get_absolute_url())
-
-    def get_instance(self, key, user):
-        """Get object based on signed token.
-        """
-        try:
-            instance, new_owner = (
-                signing.loads(key, max_age=self.max_age,
-                              salt=self.get_salt()))
-        except (signing.BadSignature, ValueError, TypeError) as e:
-            logger.error('Tried invalid token. Token: %s, user: %s. %s',
-                         key, unicode(user), unicode(e))
-            raise SuspiciousOperation()
-
-        try:
-            instance = Instance.objects.get(id=instance)
-        except Instance.DoesNotExist as e:
-            logger.error('Tried token to nonexistent instance %d. '
-                         'Token: %s, user: %s. %s',
-                         instance, key, unicode(user), unicode(e))
-            raise Http404()
-
-        if new_owner != user.pk:
-            logger.error('%s (%d) tried the token for %s. Token: %s.',
-                         unicode(user), user.pk, new_owner, key)
-            raise PermissionDenied()
-        return (instance, new_owner)
-
-
-class GraphViewBase(LoginRequiredMixin, View):
-    def get(self, request, pk, metric, time, *args, **kwargs):
-        graphite_url = settings.GRAPHITE_URL
-        if graphite_url is None:
-            raise Http404()
-
-        if metric not in self.metrics.keys():
-            raise SuspiciousOperation()
-
-        try:
-            instance = self.get_object(request, pk)
-        except self.model.DoesNotExist:
-            raise Http404()
-
-        prefix = self.get_prefix(instance)
-        target = self.metrics[metric] % {'prefix': prefix}
-        title = self.get_title(instance, metric)
-        params = {'target': target,
-                  'from': '-%s' % time,
-                  'title': title.encode('UTF-8'),
-                  'width': '500',
-                  'height': '200'}
-        logger.debug('%s %s', graphite_url, params)
-        response = requests.get('%s/render/' % graphite_url, params=params)
-        return HttpResponse(response.content, mimetype="image/png")
-
-    def get_prefix(self, instance):
-        raise NotImplementedError("Subclass must implement abstract method")
-
-    def get_title(self, instance, metric):
-        raise NotImplementedError("Subclass must implement abstract method")
-
-    def get_object(self, request, pk):
-        instance = self.model.objects.get(id=pk)
-        if not instance.has_level(request.user, 'user'):
-            raise PermissionDenied()
-        return instance
-
-
-class VmGraphView(GraphViewBase):
-    metrics = {
-        'cpu': ('cactiStyle(alias(nonNegativeDerivative(%(prefix)s.cpu.usage),'
-                '"cpu usage (%%)"))'),
-        'memory': ('cactiStyle(alias(%(prefix)s.memory.usage,'
-                   '"memory usage (%%)"))'),
-        'network': (
-            'group('
-            'aliasSub(nonNegativeDerivative(%(prefix)s.network.bytes_recv*),'
-            ' ".*-(\d+)\\)", "out (vlan \\1)"),'
-            'aliasSub(nonNegativeDerivative(%(prefix)s.network.bytes_sent*),'
-            ' ".*-(\d+)\\)", "in (vlan \\1)"))'),
-    }
-    model = Instance
-
-    def get_prefix(self, instance):
-        return 'vm.%s' % instance.vm_name
-
-    def get_title(self, instance, metric):
-        return '%s (%s) - %s' % (instance.name, instance.vm_name, metric)
-
-
-class NodeGraphView(SuperuserRequiredMixin, GraphViewBase):
-    metrics = {
-        'cpu': ('cactiStyle(alias(nonNegativeDerivative(%(prefix)s.cpu.times),'
-                '"cpu usage (%%)"))'),
-        'memory': ('cactiStyle(alias(%(prefix)s.memory.usage,'
-                   '"memory usage (%%)"))'),
-        'network': ('cactiStyle(aliasByMetric('
-                    'nonNegativeDerivative(%(prefix)s.network.bytes_*)))'),
-    }
-    model = Node
-
-    def get_prefix(self, instance):
-        return 'circle.%s' % instance.host.hostname
-
-    def get_title(self, instance, metric):
-        return '%s - %s' % (instance.name, metric)
-
-    def get_object(self, request, pk):
-        return self.model.objects.get(id=pk)
-
-
-class NotificationView(LoginRequiredMixin, TemplateView):
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/_notifications-timeline.html']
-        else:
-            return ['dashboard/notifications.html']
-
-    def get_context_data(self, *args, **kwargs):
-        context = super(NotificationView, self).get_context_data(
-            *args, **kwargs)
-        n = 10 if self.request.is_ajax() else 1000
-        context['notifications'] = list(
-            self.request.user.notification_set.all()[:n])
-        return context
-
-    def get(self, *args, **kwargs):
-        response = super(NotificationView, self).get(*args, **kwargs)
-        un = self.request.user.notification_set.filter(status="new")
-        for u in un:
-            u.status = "read"
-            u.save()
-        return response
-
-
-def circle_login(request):
-    authentication_form = CircleAuthenticationForm
-    extra_context = {
-        'saml2': saml_available,
-    }
-    response = login_view(request, authentication_form=authentication_form,
-                          extra_context=extra_context)
-    set_language_cookie(request, response)
-    return response
-
-
-class TokenLogin(View):
-
-    token_max_age = 120  # seconds
-
-    @classmethod
-    def get_salt(cls):
-        return unicode(cls)
-
-    @classmethod
-    def get_token(cls, user, sudoer):
-        return signing.dumps((sudoer.pk, user.pk),
-                             salt=cls.get_salt(), compress=True)
-
-    @classmethod
-    def get_token_url(cls, user, sudoer):
-        key = cls.get_token(user, sudoer)
-        return reverse("dashboard.views.token-login", args=(key, ))
-
-    def get(self, request, token, *args, **kwargs):
-        try:
-            data = signing.loads(token, salt=self.get_salt(),
-                                 max_age=self.token_max_age)
-            logger.debug('TokenLogin token data: %s', unicode(data))
-            sudoer, user = data
-            logger.debug('Extracted TokenLogin data: sudoer: %s, user: %s',
-                         unicode(sudoer), unicode(user))
-        except (signing.BadSignature, ValueError, TypeError) as e:
-            logger.warning('Tried invalid TokenLogin token. '
-                           'Token: %s, user: %s. %s',
-                           token, unicode(self.request.user), unicode(e))
-            raise SuspiciousOperation()
-        sudoer = User.objects.get(pk=sudoer)
-        if not sudoer.is_superuser:
-            raise PermissionDenied()
-        user = User.objects.get(pk=user)
-        user.backend = 'django.contrib.auth.backends.ModelBackend'
-        logger.warning('%s %d logged in as user %s %d',
-                       unicode(sudoer), sudoer.pk, unicode(user), user.pk)
-        login(request, user)
-        messages.info(request, _("Logged in as user %s.") % unicode(user))
-        return redirect("/")
-
-
-class MyPreferencesView(UpdateView):
-    model = Profile
-
-    def get_context_data(self, *args, **kwargs):
-        context = super(MyPreferencesView, self).get_context_data(*args,
-                                                                  **kwargs)
-        context['forms'] = {
-            'change_password': CirclePasswordChangeForm(
-                user=self.request.user),
-            'change_language': MyProfileForm(instance=self.get_object()),
-        }
-        key_table = UserKeyListTable(
-            UserKey.objects.filter(user=self.request.user),
-            request=self.request)
-        key_table.page = None
-        context['userkey_table'] = key_table
-        cmd_table = ConnectCommandListTable(
-            self.request.user.command_set.all(),
-            request=self.request)
-        cmd_table.page = None
-        context['connectcommand_table'] = cmd_table
-        return context
-
-    def get_object(self, queryset=None):
-        if self.request.user.is_anonymous():
-            raise PermissionDenied()
-        try:
-            return self.request.user.profile
-        except Profile.DoesNotExist:
-            raise Http404(_("You don't have a profile."))
-
-    def post(self, request, *args, **kwargs):
-        self.ojbect = self.get_object()
-        redirect_response = HttpResponseRedirect(
-            reverse("dashboard.views.profile-preferences"))
-        if "preferred_language" in request.POST:
-            form = MyProfileForm(request.POST, instance=self.get_object())
-            if form.is_valid():
-                lang = form.cleaned_data.get("preferred_language")
-                set_language_cookie(self.request, redirect_response, lang)
-                form.save()
-        else:
-            form = CirclePasswordChangeForm(user=request.user,
-                                            data=request.POST)
-            if form.is_valid():
-                form.save()
-
-        if form.is_valid():
-            return redirect_response
-        else:
-            return self.get(request, form=form, *args, **kwargs)
-
-    def get(self, request, form=None, *args, **kwargs):
-        # if this is not here, it won't work
-        self.object = self.get_object()
-        context = self.get_context_data(*args, **kwargs)
-        if form is not None:
-            # a little cheating, users can't post invalid
-            # language selection forms (without modifying the HTML)
-            context['forms']['change_password'] = form
-        return self.render_to_response(context)
-
-
-class UnsubscribeFormView(SuccessMessageMixin, UpdateView):
-    model = Profile
-    form_class = UnsubscribeForm
-    template_name = "dashboard/unsubscribe.html"
-    success_message = _("Successfully modified subscription.")
-
-    def get_success_url(self):
-        if self.request.user.is_authenticated():
-            return super(UnsubscribeFormView, self).get_success_url()
-        else:
-            return self.request.path
-
-    @classmethod
-    def get_salt(cls):
-        return unicode(cls)
-
-    @classmethod
-    def get_token(cls, user):
-        return signing.dumps(user.pk, salt=cls.get_salt(), compress=True)
-
-    def get_object(self, queryset=None):
-        key = self.kwargs['token']
-        try:
-            pk = signing.loads(key, salt=self.get_salt(), max_age=48 * 3600)
-        except signing.SignatureExpired:
-            raise
-        except (signing.BadSignature, ValueError, TypeError) as e:
-            logger.warning('Tried invalid token. Token: %s, user: %s. %s',
-                           key, unicode(self.request.user), unicode(e))
-            raise Http404
-        else:
-            return (queryset or self.get_queryset()).get(user_id=pk)
-
-    def dispatch(self, request, *args, **kwargs):
-        try:
-            return super(UnsubscribeFormView, self).dispatch(
-                request, *args, **kwargs)
-        except signing.SignatureExpired:
-            return redirect('dashboard.views.profile-preferences')
-
-
-def set_language_cookie(request, response, lang=None):
-    if lang is None:
-        try:
-            lang = request.user.profile.preferred_language
-        except:
-            return
-
-    cname = getattr(settings, 'LANGUAGE_COOKIE_NAME', 'django_language')
-    response.set_cookie(cname, lang, 365 * 86400)
-
-
-class DiskRemoveView(DeleteView):
-    model = Disk
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/confirm/ajax-delete.html']
-        else:
-            return ['dashboard/confirm/base-delete.html']
-
-    def get_context_data(self, **kwargs):
-        context = super(DiskRemoveView, self).get_context_data(**kwargs)
-        disk = self.get_object()
-        app = disk.get_appliance()
-        context['title'] = _("Disk remove confirmation")
-        context['text'] = _("Are you sure you want to remove "
-                            "<strong>%(disk)s</strong> from "
-                            "<strong>%(app)s</strong>?" % {'disk': disk,
-                                                           'app': app}
-                            )
-        return context
-
-    def delete(self, request, *args, **kwargs):
-        disk = self.get_object()
-        app = disk.get_appliance()
-
-        if not app.has_level(request.user, 'owner'):
-            raise PermissionDenied()
-
-        app.remove_disk(disk=disk, user=request.user)
-        disk.destroy()
-
-        next_url = request.POST.get("next")
-        success_url = next_url if next_url else app.get_absolute_url()
-        success_message = _("Disk successfully removed.")
-
-        if request.is_ajax():
-            return HttpResponse(
-                json.dumps({'message': success_message}),
-                content_type="application/json",
-            )
-        else:
-            messages.success(request, success_message)
-            return HttpResponseRedirect("%s#resources" % success_url)
-
-
-@require_GET
-def get_disk_download_status(request, pk):
-    disk = Disk.objects.get(pk=pk)
-    if not disk.get_appliance().has_level(request.user, 'owner'):
-        raise PermissionDenied()
-
-    return HttpResponse(
-        json.dumps({
-            'percentage': disk.get_download_percentage(),
-            'failed': disk.failed
-        }),
-        content_type="application/json",
-    )
-
-
-def _get_activity_icon(act):
-    op = act.get_operation()
-    if op and op.id in vm_ops:
-        return vm_ops[op.id].icon
-    else:
-        return "cog"
-
-
-def _format_activities(acts):
-    for i in acts:
-        i.icon = _get_activity_icon(i)
-    return acts
-
-
-class InstanceActivityDetail(CheckedDetailView):
-    model = InstanceActivity
-    context_object_name = 'instanceactivity'  # much simpler to mock object
-    template_name = 'dashboard/instanceactivity_detail.html'
-
-    def get_has_level(self):
-        return self.object.instance.has_level
-
-    def get_context_data(self, **kwargs):
-        ctx = super(InstanceActivityDetail, self).get_context_data(**kwargs)
-        ctx['activities'] = _format_activities(
-            self.object.instance.get_activities(self.request.user))
-        ctx['icon'] = _get_activity_icon(self.object)
-        return ctx
-
-
-class UserCreationView(LoginRequiredMixin, PermissionRequiredMixin,
-                       CreateView):
-    form_class = UserCreationForm
-    model = User
-    template_name = 'dashboard/user-create.html'
-    permission_required = "auth.add_user"
-
-    def get_group(self, group_pk):
-        self.group = get_object_or_404(Group, pk=group_pk)
-        if not self.group.profile.has_level(self.request.user, 'owner'):
-            raise PermissionDenied()
-
-    def get(self, *args, **kwargs):
-        self.get_group(kwargs.pop('group_pk'))
-        return super(UserCreationView, self).get(*args, **kwargs)
-
-    def post(self, *args, **kwargs):
-        group_pk = kwargs.pop('group_pk')
-        self.get_group(group_pk)
-        ret = super(UserCreationView, self).post(*args, **kwargs)
-        if self.object:
-            create_profile(self.object)
-            self.object.groups.add(self.group)
-            return redirect(
-                reverse('dashboard.views.group-detail', args=[group_pk]))
-        else:
-            return ret
-
-
-class InterfaceDeleteView(DeleteView):
-    model = Interface
-
-    def get_template_names(self):
-        if self.request.is_ajax():
-            return ['dashboard/confirm/ajax-delete.html']
-        else:
-            return ['dashboard/confirm/base-delete.html']
-
-    def get_context_data(self, **kwargs):
-        context = super(InterfaceDeleteView, self).get_context_data(**kwargs)
-        interface = self.get_object()
-        context['text'] = _("Are you sure you want to remove this interface "
-                            "from <strong>%(vm)s</strong>?" %
-                            {'vm': interface.instance.name})
-        return context
-
-    def delete(self, request, *args, **kwargs):
-        self.object = self.get_object()
-        instance = self.object.instance
-
-        if not instance.has_level(request.user, "owner"):
-            raise PermissionDenied()
-
-        instance.remove_interface(interface=self.object, user=request.user)
-        success_url = self.get_success_url()
-        success_message = _("Interface successfully deleted.")
-
-        if request.is_ajax():
-            return HttpResponse(
-                json.dumps(
-                    {'message': success_message,
-                     'removed_network': {
-                         'vlan': self.object.vlan.name,
-                         'vlan_pk': self.object.vlan.pk,
-                         'managed': self.object.host is not None,
-                     }}),
-                content_type="application/json",
-            )
-        else:
-            messages.success(request, success_message)
-            return HttpResponseRedirect("%s#network" % success_url)
-
-    def get_success_url(self):
-        redirect = self.request.POST.get("next")
-        if redirect:
-            return redirect
-        self.object.instance.get_absolute_url()
-
-
-@require_GET
-def get_vm_screenshot(request, pk):
-    instance = get_object_or_404(Instance, pk=pk)
-    try:
-        image = instance.screenshot(user=request.user).getvalue()
-    except:
-        # TODO handle this better
-        raise Http404()
-
-    return HttpResponse(image, mimetype="image/png")
-
-
-class ProfileView(LoginRequiredMixin, DetailView):
-    template_name = "dashboard/profile.html"
-    model = User
-    slug_field = "username"
-    slug_url_kwarg = "username"
-
-    def get_context_data(self, **kwargs):
-        context = super(ProfileView, self).get_context_data(**kwargs)
-        user = self.get_object()
-        context['profile'] = user
-        context['avatar_url'] = user.profile.get_avatar_url()
-        context['instances_owned'] = Instance.get_objects_with_level(
-            "owner", user, disregard_superuser=True).filter(destroyed_at=None)
-        context['instances_with_access'] = Instance.get_objects_with_level(
-            "user", user, disregard_superuser=True
-        ).filter(destroyed_at=None).exclude(pk__in=context['instances_owned'])
-
-        group_profiles = GroupProfile.get_objects_with_level(
-            "operator", self.request.user)
-        groups = Group.objects.filter(groupprofile__in=group_profiles)
-        context['groups'] = user.groups.filter(pk__in=groups)
-
-        # permissions
-        # show groups only if the user is superuser, or have access
-        # to any of the groups the user belongs to
-        context['perm_group_list'] = (
-            self.request.user.is_superuser or len(context['groups']) > 0)
-        context['perm_email'] = (
-            context['perm_group_list'] or self.request.user == user)
-
-        # filter the virtual machine list
-        # if the logged in user is not superuser or not the user itself
-        # filter the list so only those virtual machines are shown that are
-        # originated from templates the logged in user is operator or higher
-        if not (self.request.user.is_superuser or self.request.user == user):
-            it = InstanceTemplate.get_objects_with_level("operator",
-                                                         self.request.user)
-            context['instances_owned'] = context['instances_owned'].filter(
-                template__in=it)
-            context['instances_with_access'] = context[
-                'instances_with_access'].filter(template__in=it)
-        if self.request.user.is_superuser:
-            context['login_token'] = TokenLogin.get_token_url(
-                user, self.request.user)
-        return context
-
-
-@require_POST
-def toggle_use_gravatar(request, **kwargs):
-    user = get_object_or_404(User, username=kwargs['username'])
-    if not request.user == user:
-        raise PermissionDenied()
-
-    profile = user.profile
-    profile.use_gravatar = not profile.use_gravatar
-    profile.save()
-
-    new_avatar_url = user.profile.get_avatar_url()
-    return HttpResponse(
-        json.dumps({'new_avatar_url': new_avatar_url}),
-        content_type="application/json",
-    )
-
-
-class UserKeyDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
-    model = UserKey
-    template_name = "dashboard/userkey-edit.html"
-    form_class = UserKeyForm
-    success_message = _("Successfully modified SSH key.")
-
-    def get(self, request, *args, **kwargs):
-        object = self.get_object()
-        if object.user != request.user:
-            raise PermissionDenied()
-        return super(UserKeyDetail, self).get(request, *args, **kwargs)
-
-    def get_success_url(self):
-        return reverse_lazy("dashboard.views.userkey-detail",
-                            kwargs=self.kwargs)
-
-    def post(self, request, *args, **kwargs):
-        object = self.get_object()
-        if object.user != request.user:
-            raise PermissionDenied()
-        return super(UserKeyDetail, self).post(self, request, args, kwargs)
-
-
-class UserKeyDelete(LoginRequiredMixin, DeleteView):
-    model = UserKey
-
-    def get_success_url(self):
-        return reverse("dashboard.views.profile-preferences")
-
-    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 object.user != request.user:
-            raise PermissionDenied()
-
-        object.delete()
-        success_url = self.get_success_url()
-        success_message = _("SSH key successfully deleted.")
-
-        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)
-
-
-class UserKeyCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView):
-    model = UserKey
-    form_class = UserKeyForm
-    template_name = "dashboard/userkey-create.html"
-    success_message = _("Successfully created a new SSH key.")
-
-    def get_success_url(self):
-        return reverse_lazy("dashboard.views.profile-preferences")
-
-    def get_form_kwargs(self):
-        kwargs = super(UserKeyCreate, self).get_form_kwargs()
-        kwargs['user'] = self.request.user
-        return kwargs
-
-
-class ConnectCommandDetail(LoginRequiredMixin, SuccessMessageMixin,
-                           UpdateView):
-    model = ConnectCommand
-    template_name = "dashboard/connect-command-edit.html"
-    form_class = ConnectCommandForm
-    success_message = _("Successfully modified command template.")
-
-    def get(self, request, *args, **kwargs):
-        object = self.get_object()
-        if object.user != request.user:
-            raise PermissionDenied()
-        return super(ConnectCommandDetail, self).get(request, *args, **kwargs)
-
-    def get_success_url(self):
-        return reverse_lazy("dashboard.views.connect-command-detail",
-                            kwargs=self.kwargs)
-
-    def post(self, request, *args, **kwargs):
-        object = self.get_object()
-        if object.user != request.user:
-            raise PermissionDenied()
-        return super(ConnectCommandDetail, self).post(request, args, kwargs)
-
-    def get_form_kwargs(self):
-        kwargs = super(ConnectCommandDetail, self).get_form_kwargs()
-        kwargs['user'] = self.request.user
-        return kwargs
-
-
-class ConnectCommandDelete(LoginRequiredMixin, DeleteView):
-    model = ConnectCommand
-
-    def get_success_url(self):
-        return reverse("dashboard.views.profile-preferences")
-
-    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 object.user != request.user:
-            raise PermissionDenied()
-
-        object.delete()
-        success_url = self.get_success_url()
-        success_message = _("Command template successfully deleted.")
-
-        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)
-
-
-class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin,
-                           CreateView):
-    model = ConnectCommand
-    form_class = ConnectCommandForm
-    template_name = "dashboard/connect-command-create.html"
-    success_message = _("Successfully created a new command template.")
-
-    def get_success_url(self):
-        return reverse_lazy("dashboard.views.profile-preferences")
-
-    def get_form_kwargs(self):
-        kwargs = super(ConnectCommandCreate, self).get_form_kwargs()
-        kwargs['user'] = self.request.user
-        return kwargs
-
-
-class HelpView(TemplateView):
-
-    def get_context_data(self, *args, **kwargs):
-        ctx = super(HelpView, self).get_context_data(*args, **kwargs)
-        ctx.update({"saml": hasattr(settings, "SAML_CONFIG"),
-                    "store": settings.STORE_URL})
-        return ctx
-
-
-class StoreList(LoginRequiredMixin, TemplateView):
-    template_name = "dashboard/store/list.html"
-
-    def get_context_data(self, **kwargs):
-        context = super(StoreList, self).get_context_data(**kwargs)
-        directory = self.request.GET.get("directory", "/")
-        directory = "/" if not len(directory) else directory
-
-        store = Store(self.request.user)
-        context['root'] = store.list(directory)
-        context['quota'] = store.get_quota()
-        context['up_url'] = self.create_up_directory(directory)
-        context['current'] = directory
-        context['next_url'] = "%s%s?directory=%s" % (
-            settings.DJANGO_URL.rstrip("/"),
-            reverse("dashboard.views.store-list"), directory)
-        return context
-
-    def get(self, *args, **kwargs):
-        try:
-            if self.request.is_ajax():
-                context = self.get_context_data(**kwargs)
-                return render_to_response(
-                    "dashboard/store/_list-box.html",
-                    RequestContext(self.request, context),
-                )
-            else:
-                return super(StoreList, self).get(*args, **kwargs)
-        except NoStoreException:
-            messages.warning(self.request, _("No store."))
-        except NotOkException:
-            messages.warning(self.request, _("Store has some problems now."
-                                             " Try again later."))
-        except Exception as e:
-            logger.critical("Something is wrong with store: %s", unicode(e))
-            messages.warning(self.request, _("Unknown store error."))
-        return redirect("/")
-
-    def create_up_directory(self, directory):
-        path = normpath(join('/', directory, '..'))
-        if not path.endswith("/"):
-            path += "/"
-        return path
-
-
-@require_GET
-@login_required
-def store_download(request):
-    path = request.GET.get("path")
-    try:
-        url = Store(request.user).request_download(path)
-    except Exception:
-        messages.error(request, _("Something went wrong during download."))
-        logger.exception("Unable to download, "
-                         "maybe it is already deleted")
-        return redirect(reverse("dashboard.views.store-list"))
-    return redirect(url)
-
-
-@require_GET
-@login_required
-def store_upload(request):
-    directory = request.GET.get("directory", "/")
-    try:
-        action = Store(request.user).request_upload(directory)
-    except Exception:
-        logger.exception("Unable to upload")
-        messages.error(request, _("Unable to upload file."))
-        return redirect("/")
-
-    next_url = "%s%s?directory=%s" % (
-        settings.DJANGO_URL.rstrip("/"),
-        reverse("dashboard.views.store-list"), directory)
-
-    return render(request, "dashboard/store/upload.html",
-                  {'directory': directory, 'action': action,
-                   'next_url': next_url})
-
-
-@require_GET
-@login_required
-def store_get_upload_url(request):
-    current_dir = request.GET.get("current_dir")
-    try:
-        url = Store(request.user).request_upload(current_dir)
-    except Exception:
-        logger.exception("Unable to upload")
-        messages.error(request, _("Unable to upload file."))
-        return redirect("/")
-    return HttpResponse(
-        json.dumps({'url': url}), content_type="application/json")
-
-
-class StoreRemove(LoginRequiredMixin, TemplateView):
-    template_name = "dashboard/store/remove.html"
-
-    def get_context_data(self, *args, **kwargs):
-        context = super(StoreRemove, self).get_context_data(*args, **kwargs)
-        path = self.request.GET.get("path", "/")
-        if path == "/":
-            SuspiciousOperation()
-
-        context['path'] = path
-        context['is_dir'] = path.endswith("/")
-        if context['is_dir']:
-            context['directory'] = path
-        else:
-            context['directory'] = dirname(path)
-            context['name'] = basename(path)
-
-        return context
-
-    def get(self, *args, **kwargs):
-        try:
-            return super(StoreRemove, self).get(*args, **kwargs)
-        except NoStoreException:
-            return redirect("/")
-
-    def post(self, *args, **kwargs):
-        path = self.request.POST.get("path")
-        try:
-            Store(self.request.user).remove(path)
-        except Exception:
-            logger.exception("Unable to remove %s", path)
-            messages.error(self.request, _("Unable to remove %s.") % path)
-
-        return redirect("%s?directory=%s" % (
-            reverse("dashboard.views.store-list"),
-            dirname(dirname(path)),
-        ))
-
-
-@require_POST
-@login_required
-def store_new_directory(request):
-    path = request.POST.get("path")
-    name = request.POST.get("name")
-
-    try:
-        Store(request.user).new_folder(join(path, name))
-    except Exception:
-        logger.exception("Unable to create folder %s in %s for %s",
-                         name, path, unicode(request.user))
-        messages.error(request, _("Unable to create folder."))
-    return redirect("%s?directory=%s" % (
-        reverse("dashboard.views.store-list"), path))
-
-
-@require_POST
-@login_required
-def store_refresh_toplist(request):
-    cache_key = "files-%d" % request.user.pk
-    cache = get_cache("default")
-    try:
-        store = Store(request.user)
-        toplist = store.toplist()
-        quota = store.get_quota()
-        files = {'toplist': toplist, 'quota': quota}
-    except Exception:
-        logger.exception("Can't get toplist of %s", unicode(request.user))
-        files = {'toplist': []}
-    cache.set(cache_key, files, 300)
-
-    return redirect(reverse("dashboard.index"))
-
-
-def absolute_url(url):
-    return urljoin(settings.DJANGO_URL, url)
diff --git a/circle/dashboard/views/__init__.py b/circle/dashboard/views/__init__.py
new file mode 100644
index 0000000..61df052
--- /dev/null
+++ b/circle/dashboard/views/__init__.py
@@ -0,0 +1,13 @@
+# flake8: noqa
+# from .node import Node
+
+# __all__ = [ ]
+
+from group import *
+from index import *
+from node import *
+from store import *
+from template import *
+from user import *
+from util import *
+from vm import *
diff --git a/circle/dashboard/views/group.py b/circle/dashboard/views/group.py
new file mode 100644
index 0000000..e2cad63
--- /dev/null
+++ b/circle/dashboard/views/group.py
@@ -0,0 +1,440 @@
+# 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/>.
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+from itertools import chain
+
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth.models import User, Group
+from django.contrib.messages.views import SuccessMessageMixin
+from django.core.exceptions import PermissionDenied
+from django.core.urlresolvers import reverse, reverse_lazy
+from django.http import HttpResponse, Http404
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from django.views.generic import UpdateView, DeleteView, TemplateView
+
+from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
+from django_tables2 import SingleTableView
+
+from ..forms import (
+    AddGroupMemberForm, AclUserOrGroupAddForm, GroupPermissionForm,
+    GroupCreateForm, GroupProfileUpdateForm,
+)
+from ..models import FutureMember, GroupProfile
+from ..tables import GroupListTable
+from .util import CheckedDetailView, AclUpdateView, search_user, saml_available
+
+logger = logging.getLogger(__name__)
+
+
+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', [])
+                for group in chain(*[attributes[i]
+                                     for i in owneratrs if i in attributes]):
+                    try:
+                        GroupProfile.search(group)
+                    except Group.DoesNotExist:
+                        newgroups.append(group)
+
+        return newgroups
+
+
+class GroupDetailView(CheckedDetailView):
+    template_name = "dashboard/group-detail.html"
+    model = Group
+    read_level = 'operator'
+
+    def get_has_level(self):
+        return self.object.profile.has_level
+
+    def get_context_data(self, **kwargs):
+        context = super(GroupDetailView, self).get_context_data(**kwargs)
+        context['group'] = self.object
+        context['users'] = self.object.user_set.all()
+        context['future_users'] = FutureMember.objects.filter(
+            group=self.object)
+        context['acl'] = AclUpdateView.get_acl_data(
+            self.object.profile, self.request.user,
+            'dashboard.views.group-acl')
+        context['aclform'] = AclUserOrGroupAddForm()
+        context['addmemberform'] = AddGroupMemberForm()
+        context['group_profile_form'] = GroupProfileUpdate.get_form_object(
+            self.request, self.object.profile)
+
+        if self.request.user.is_superuser:
+            context['group_perm_form'] = GroupPermissionForm(
+                instance=self.object)
+
+        return context
+
+    def post(self, request, *args, **kwargs):
+        self.object = self.get_object()
+        if not self.get_has_level()(request.user, 'operator'):
+            raise PermissionDenied()
+
+        if request.POST.get('new_name'):
+            return self.__set_name(request)
+        if request.POST.get('new_member'):
+            return self.__add_user(request)
+        if request.POST.get('new_members'):
+            return self.__add_list(request)
+        return redirect(reverse_lazy("dashboard.views.group-detail",
+                                     kwargs={'pk': self.get_object().pk}))
+
+    def __add_user(self, request):
+        name = request.POST['new_member']
+        self.__add_username(request, name)
+        return redirect(reverse_lazy("dashboard.views.group-detail",
+                                     kwargs={'pk': self.object.pk}))
+
+    def __add_username(self, request, name):
+        if not name:
+            return
+        try:
+            entity = search_user(name)
+            self.object.user_set.add(entity)
+        except User.DoesNotExist:
+            if saml_available:
+                FutureMember.objects.get_or_create(org_id=name,
+                                                   group=self.object)
+            else:
+                messages.warning(request, _('User "%s" not found.') % name)
+
+    def __add_list(self, request):
+        userlist = request.POST.get('new_members').split('\r\n')
+        for line in userlist:
+            self.__add_username(request, line)
+        return redirect(reverse_lazy("dashboard.views.group-detail",
+                                     kwargs={'pk': self.object.pk}))
+
+    def __set_name(self, request):
+        new_name = request.POST.get("new_name")
+        Group.objects.filter(pk=self.object.pk).update(
+            **{'name': new_name})
+
+        success_message = _("Group successfully renamed.")
+        if request.is_ajax():
+            response = {
+                'message': success_message,
+                'new_name': new_name,
+                'group_pk': self.object.pk
+            }
+            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}))
+
+
+class GroupPermissionsView(SuperuserRequiredMixin, UpdateView):
+    model = Group
+    form_class = GroupPermissionForm
+    slug_field = "pk"
+    slug_url_kwarg = "group_pk"
+
+    def get_success_url(self):
+        return "%s#group-detail-permissions" % (
+            self.get_object().groupprofile.get_absolute_url())
+
+
+class GroupAclUpdateView(AclUpdateView):
+    model = Group
+
+    def get_object(self):
+        return super(GroupAclUpdateView, self).get_object().profile
+
+
+class GroupList(LoginRequiredMixin, SingleTableView):
+    template_name = "dashboard/group-list.html"
+    model = Group
+    table_class = GroupListTable
+    table_pagination = False
+
+    def get(self, *args, **kwargs):
+        if self.request.is_ajax():
+            groups = [{
+                'url': reverse("dashboard.views.group-detail",
+                               kwargs={'pk': i.pk}),
+                'name': i.name} for i in self.get_queryset()]
+            return HttpResponse(
+                json.dumps(list(groups)),
+                content_type="application/json",
+            )
+        else:
+            return super(GroupList, self).get(*args, **kwargs)
+
+    def get_queryset(self):
+        logger.debug('GroupList.get_queryset() called. User: %s',
+                     unicode(self.request.user))
+        profiles = GroupProfile.get_objects_with_level(
+            'operator', self.request.user)
+        groups = Group.objects.filter(groupprofile__in=profiles)
+        s = self.request.GET.get("s")
+        if s:
+            groups = groups.filter(name__icontains=s)
+        return groups
+
+
+class GroupRemoveUserView(CheckedDetailView, DeleteView):
+    model = Group
+    slug_field = 'pk'
+    slug_url_kwarg = 'group_pk'
+    read_level = 'operator'
+    member_key = 'member_pk'
+
+    def get_has_level(self):
+        return self.object.profile.has_level
+
+    def get_context_data(self, **kwargs):
+        context = super(GroupRemoveUserView, self).get_context_data(**kwargs)
+        try:
+            context['member'] = User.objects.get(pk=self.member_pk)
+        except User.DoesNotExist:
+            raise Http404()
+        return context
+
+    def get_success_url(self):
+        next = self.request.POST.get('next')
+        if next:
+            return next
+        else:
+            return reverse_lazy("dashboard.views.group-detail",
+                                kwargs={'pk': self.get_object().pk})
+
+    def get(self, request, member_pk, *args, **kwargs):
+        self.member_pk = member_pk
+        return super(GroupRemoveUserView, self).get(request, *args, **kwargs)
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/confirm/ajax-remove.html']
+        else:
+            return ['dashboard/confirm/base-remove.html']
+
+    def remove_member(self, pk):
+        container = self.get_object()
+        container.user_set.remove(User.objects.get(pk=pk))
+
+    def get_success_message(self):
+        return _("Member successfully removed from group.")
+
+    def delete(self, request, *args, **kwargs):
+        object = self.get_object()
+        if not object.profile.has_level(request.user, 'operator'):
+            raise PermissionDenied()
+        self.remove_member(kwargs[self.member_key])
+        success_url = self.get_success_url()
+        success_message = self.get_success_message()
+        if request.is_ajax():
+            return HttpResponse(
+                json.dumps({'message': success_message}),
+                content_type="application/json",
+            )
+        else:
+            messages.success(request, success_message)
+            return redirect(success_url)
+
+
+class GroupRemoveFutureUserView(GroupRemoveUserView):
+
+    member_key = 'member_org_id'
+
+    def get(self, request, member_org_id, *args, **kwargs):
+        self.member_org_id = member_org_id
+        return super(GroupRemoveUserView, self).get(request, *args, **kwargs)
+
+    def get_context_data(self, **kwargs):
+        context = super(GroupRemoveUserView, self).get_context_data(**kwargs)
+        try:
+            context['member'] = FutureMember.objects.get(
+                org_id=self.member_org_id, group=self.get_object())
+        except FutureMember.DoesNotExist:
+            raise Http404()
+        return context
+
+    def remove_member(self, org_id):
+        FutureMember.objects.filter(org_id=org_id,
+                                    group=self.get_object()).delete()
+
+    def get_success_message(self):
+        return _("Future user successfully removed from group.")
+
+
+class GroupDelete(CheckedDetailView, DeleteView):
+
+    """This stuff deletes the group.
+    """
+    model = Group
+    template_name = "dashboard/confirm/base-delete.html"
+    read_level = 'operator'
+
+    def get_has_level(self):
+        return self.object.profile.has_level
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/confirm/ajax-delete.html']
+        else:
+            return ['dashboard/confirm/base-delete.html']
+
+    # github.com/django/django/blob/master/django/views/generic/edit.py#L245
+    def delete(self, request, *args, **kwargs):
+        object = self.get_object()
+        if not object.profile.has_level(request.user, 'owner'):
+            raise PermissionDenied()
+        object.delete()
+        success_url = self.get_success_url()
+        success_message = _("Group successfully deleted.")
+
+        if request.is_ajax():
+            if request.POST.get('redirect').lower() == "true":
+                messages.success(request, success_message)
+            return HttpResponse(
+                json.dumps({'message': success_message}),
+                content_type="application/json",
+            )
+        else:
+            messages.success(request, success_message)
+            return redirect(success_url)
+
+    def get_success_url(self):
+        next = self.request.POST.get('next')
+        if next:
+            return next
+        else:
+            return reverse_lazy('dashboard.index')
+
+
+class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
+
+    form_class = GroupCreateForm
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/modal-wrapper.html']
+        else:
+            return ['dashboard/nojs-wrapper.html']
+
+    def get(self, request, form=None, *args, **kwargs):
+        if not request.user.has_module_perms('auth'):
+            raise PermissionDenied()
+        if form is None:
+            form = self.form_class(
+                new_groups=self.get_available_group_codes(request))
+        context = self.get_context_data(**kwargs)
+        context.update({
+            'template': 'dashboard/group-create.html',
+            'box_title': _('Create a Group'),
+            'form': form,
+            'ajax_title': True,
+        })
+        return self.render_to_response(context)
+
+    def post(self, request, *args, **kwargs):
+        if not request.user.has_module_perms('auth'):
+            raise PermissionDenied()
+        form = self.form_class(
+            request.POST, new_groups=self.get_available_group_codes(request))
+        if not form.is_valid():
+            return self.get(request, form, *args, **kwargs)
+        form.cleaned_data
+        savedform = form.save()
+        savedform.profile.set_level(request.user, 'owner')
+        messages.success(request, _('Group successfully created.'))
+        if request.is_ajax():
+            return HttpResponse(json.dumps({'redirect':
+                                savedform.profile.get_absolute_url()}),
+                                content_type="application/json")
+        else:
+            return redirect(savedform.profile.get_absolute_url())
+
+
+class GroupProfileUpdate(SuccessMessageMixin, GroupCodeMixin,
+                         LoginRequiredMixin, UpdateView):
+
+    form_class = GroupProfileUpdateForm
+    model = Group
+    success_message = _('Group is successfully updated.')
+
+    @classmethod
+    def get_available_group_codes(cls, request, extra=None):
+        result = super(GroupProfileUpdate, cls).get_available_group_codes(
+            request)
+        if extra and extra not in result:
+            result += [extra]
+        return result
+
+    def get_object(self):
+        group = super(GroupProfileUpdate, self).get_object()
+        profile = group.profile
+        if not profile.has_level(self.request.user, 'owner'):
+            raise PermissionDenied
+        else:
+            return profile
+
+    @classmethod
+    def get_form_object(cls, request, instance, *args, **kwargs):
+        kwargs['instance'] = instance
+        kwargs['new_groups'] = cls.get_available_group_codes(
+            request, instance.org_id)
+        kwargs['superuser'] = request.user.is_superuser
+        return cls.form_class(*args, **kwargs)
+
+    def get(self, request, form=None, *args, **kwargs):
+        self.object = self.get_object()
+        if form is None:
+            form = self.get_form_object(request, self.object)
+        return super(GroupProfileUpdate, self).get(
+            request, form, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        if not request.user.has_module_perms('auth'):
+            raise PermissionDenied()
+        self.object = self.get_object()
+        form = self.get_form_object(request, self.object, self.request.POST)
+        if not form.is_valid():
+            return self.form_invalid(form)
+        form.save()
+        return self.form_valid(form)
diff --git a/circle/dashboard/views/index.py b/circle/dashboard/views/index.py
new file mode 100644
index 0000000..9f7bba0
--- /dev/null
+++ b/circle/dashboard/views/index.py
@@ -0,0 +1,123 @@
+# 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/>.
+from __future__ import unicode_literals, absolute_import
+
+import logging
+
+from django.core.cache import get_cache
+from django.conf import settings
+from django.contrib.auth.models import Group
+from django.views.generic import TemplateView
+
+from braces.views import LoginRequiredMixin
+
+from dashboard.models import GroupProfile
+from vm.models import Instance, Node, InstanceTemplate
+
+from ..store_api import Store
+
+logger = logging.getLogger(__name__)
+
+
+class IndexView(LoginRequiredMixin, TemplateView):
+    template_name = "dashboard/index.html"
+
+    def get_context_data(self, **kwargs):
+        user = self.request.user
+        context = super(IndexView, self).get_context_data(**kwargs)
+
+        # instances
+        favs = Instance.objects.filter(favourite__user=self.request.user)
+        instances = Instance.get_objects_with_level(
+            'user', user, disregard_superuser=True).filter(destroyed_at=None)
+        display = list(favs) + list(set(instances) - set(favs))
+        for d in display:
+            d.fav = True if d in favs else False
+        context.update({
+            'instances': display[:5],
+            'more_instances': instances.count() - len(instances[:5])
+        })
+
+        running = instances.filter(status='RUNNING')
+        stopped = instances.exclude(status__in=('RUNNING', 'NOSTATE'))
+
+        context.update({
+            'running_vms': running[:20],
+            'running_vm_num': running.count(),
+            'stopped_vm_num': stopped.count()
+        })
+
+        # nodes
+        if user.is_superuser:
+            nodes = Node.objects.all()
+            context.update({
+                'nodes': nodes[:5],
+                'more_nodes': nodes.count() - len(nodes[:5]),
+                '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
+        if user.has_module_perms('auth'):
+            profiles = GroupProfile.get_objects_with_level('operator', user)
+            groups = Group.objects.filter(groupprofile__in=profiles)
+            context.update({
+                'groups': groups[:5],
+                'more_groups': groups.count() - len(groups[:5]),
+            })
+
+        # template
+        if user.has_perm('vm.create_template'):
+            context['templates'] = InstanceTemplate.get_objects_with_level(
+                'operator', user, disregard_superuser=True).all()[:5]
+
+        # toplist
+        if settings.STORE_URL:
+            cache_key = "files-%d" % self.request.user.pk
+            cache = get_cache("default")
+            files = cache.get(cache_key)
+            if not files:
+                try:
+                    store = Store(self.request.user)
+                    toplist = store.toplist()
+                    quota = store.get_quota()
+                    files = {'toplist': toplist, 'quota': quota}
+                except Exception:
+                    logger.exception("Unable to get tolist for %s",
+                                     unicode(self.request.user))
+                    files = {'toplist': []}
+                cache.set(cache_key, files, 300)
+
+            context['files'] = files
+        else:
+            context['no_store'] = True
+
+        return context
+
+
+class HelpView(TemplateView):
+
+    def get_context_data(self, *args, **kwargs):
+        ctx = super(HelpView, self).get_context_data(*args, **kwargs)
+        ctx.update({"saml": hasattr(settings, "SAML_CONFIG"),
+                    "store": settings.STORE_URL})
+        return ctx
diff --git a/circle/dashboard/views/node.py b/circle/dashboard/views/node.py
new file mode 100644
index 0000000..19f8547
--- /dev/null
+++ b/circle/dashboard/views/node.py
@@ -0,0 +1,373 @@
+# 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/>.
+from __future__ import unicode_literals, absolute_import
+
+import json
+
+from django.conf import settings
+from django.contrib import messages
+from django.core.exceptions import PermissionDenied
+from django.core.urlresolvers import reverse_lazy
+from django.db.models import Count
+from django.forms.models import inlineformset_factory
+from django.http import HttpResponse
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from django.views.generic import DetailView, TemplateView, DeleteView
+
+from braces.views import LoginRequiredMixin, SuperuserRequiredMixin
+from django_tables2 import SingleTableView
+
+from firewall.models import Host
+from vm.models import Node, NodeActivity, Trait
+
+from ..forms import TraitForm, HostForm, NodeForm
+from ..tables import NodeListTable
+from .util import GraphViewBase
+
+
+class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
+    template_name = "dashboard/node-detail.html"
+    model = Node
+    form = None
+    form_class = TraitForm
+
+    def get_context_data(self, form=None, **kwargs):
+        if form is None:
+            form = self.form_class()
+        context = super(NodeDetailView, self).get_context_data(**kwargs)
+        na = NodeActivity.objects.filter(
+            node=self.object, parent=None
+        ).order_by('-started').select_related()
+        context['activities'] = na
+        context['trait_form'] = form
+        context['graphite_enabled'] = (
+            settings.GRAPHITE_URL is not None)
+        return context
+
+    def post(self, request, *args, **kwargs):
+        if request.POST.get('new_name'):
+            return self.__set_name(request)
+        if request.POST.get('to_remove'):
+            return self.__remove_trait(request)
+        return redirect(reverse_lazy("dashboard.views.node-detail",
+                                     kwargs={'pk': self.get_object().pk}))
+
+    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})
+
+        success_message = _("Node successfully renamed.")
+        if request.is_ajax():
+            response = {
+                'message': success_message,
+                'new_name': new_name,
+                'node_pk': self.object.pk
+            }
+            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}))
+
+    def __remove_trait(self, request):
+        try:
+            to_remove = request.POST.get('to_remove')
+            self.object = self.get_object()
+            self.object.traits.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"
+            )
+        else:
+            return redirect(self.object.get_absolute_url())
+
+
+class NodeList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
+    template_name = "dashboard/node-list.html"
+    table_class = NodeListTable
+    table_pagination = False
+
+    def get(self, *args, **kwargs):
+        if self.request.is_ajax():
+            nodes = Node.objects.all()
+            nodes = [{
+                'name': i.name,
+                'icon': i.get_status_icon(),
+                'url': i.get_absolute_url(),
+                'label': i.get_status_label(),
+                'status': i.state.lower()} for i in nodes]
+
+            return HttpResponse(
+                json.dumps(list(nodes)),
+                content_type="application/json",
+            )
+        else:
+            return super(NodeList, self).get(*args, **kwargs)
+
+    def get_queryset(self):
+        return Node.objects.annotate(
+            number_of_VMs=Count('instance_set')).select_related('host')
+
+
+class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
+
+    form_class = HostForm
+    hostform = None
+
+    formset_class = inlineformset_factory(Host, Node, form=NodeForm, extra=1)
+    formset = None
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/modal-wrapper.html']
+        else:
+            return ['dashboard/nojs-wrapper.html']
+
+    def get(self, request, hostform=None, formset=None, *args, **kwargs):
+        if hostform is None:
+            hostform = self.form_class()
+        if formset is None:
+            formset = self.formset_class(instance=Host())
+        context = self.get_context_data(**kwargs)
+        context.update({
+            'template': 'dashboard/node-create.html',
+            'box_title': 'Create a Node',
+            'hostform': hostform,
+            'formset': formset,
+
+        })
+        return self.render_to_response(context)
+
+    # TODO handle not ajax posts
+    def post(self, request, *args, **kwargs):
+        if not self.request.user.is_authenticated():
+            raise PermissionDenied()
+
+        hostform = self.form_class(request.POST)
+        formset = self.formset_class(request.POST, Host())
+        if not hostform.is_valid():
+            return self.get(request, hostform, formset, *args, **kwargs)
+        hostform.setowner(request.user)
+        savedform = hostform.save(commit=False)
+        formset = self.formset_class(request.POST, instance=savedform)
+        if not formset.is_valid():
+            return self.get(request, hostform, formset, *args, **kwargs)
+
+        savedform.save()
+        nodemodel = formset.save()
+        messages.success(request, _('Node successfully created.'))
+        path = nodemodel[0].get_absolute_url()
+        if request.is_ajax():
+            return HttpResponse(json.dumps({'redirect': path}),
+                                content_type="application/json")
+        else:
+            return redirect(path)
+
+
+class NodeDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
+
+    """This stuff deletes the node.
+    """
+    model = Node
+    template_name = "dashboard/confirm/base-delete.html"
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/confirm/ajax-delete.html']
+        else:
+            return ['dashboard/confirm/base-delete.html']
+
+    # github.com/django/django/blob/master/django/views/generic/edit.py#L245
+    def delete(self, request, *args, **kwargs):
+        object = self.get_object()
+
+        object.delete()
+        success_url = self.get_success_url()
+        success_message = _("Node successfully deleted.")
+
+        if request.is_ajax():
+            if request.POST.get('redirect').lower() == "true":
+                messages.success(request, success_message)
+            return HttpResponse(
+                json.dumps({'message': success_message}),
+                content_type="application/json",
+            )
+        else:
+            messages.success(request, success_message)
+            return redirect(success_url)
+
+    def get_success_url(self):
+        next = self.request.POST.get('next')
+        if next:
+            return next
+        else:
+            return reverse_lazy('dashboard.index')
+
+
+class NodeAddTraitView(SuperuserRequiredMixin, DetailView):
+    model = Node
+    template_name = "dashboard/node-add-trait.html"
+
+    def get_success_url(self):
+        next = self.request.GET.get('next')
+        if next:
+            return next
+        else:
+            return self.object.get_absolute_url()
+
+    def get_context_data(self, **kwargs):
+        self.object = self.get_object()
+        context = super(NodeAddTraitView, self).get_context_data(**kwargs)
+        context['form'] = (TraitForm(self.request.POST) if self.request.POST
+                           else TraitForm())
+        return context
+
+    def post(self, request, pk, *args, **kwargs):
+        context = self.get_context_data(**kwargs)
+        form = context['form']
+        if form.is_valid():
+            node = self.object
+            n = form.cleaned_data['name']
+            trait, created = Trait.objects.get_or_create(name=n)
+            node.traits.add(trait)
+            success_message = _("Trait successfully added to node.")
+            messages.success(request, success_message)
+            return redirect(self.get_success_url())
+        else:
+            return self.get(self, request, pk, *args, **kwargs)
+
+
+class NodeStatus(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
+    template_name = "dashboard/confirm/node-status.html"
+    model = Node
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/confirm/ajax-node-status.html']
+        else:
+            return ['dashboard/confirm/node-status.html']
+
+    def get_success_url(self):
+        next = self.request.GET.get('next')
+        if next:
+            return next
+        else:
+            return reverse_lazy("dashboard.views.node-detail",
+                                kwargs={'pk': self.object.pk})
+
+    def get_context_data(self, **kwargs):
+        context = super(NodeStatus, self).get_context_data(**kwargs)
+        if self.object.enabled:
+            context['status'] = "disable"
+        else:
+            context['status'] = "enable"
+        return context
+
+    def post(self, request, *args, **kwargs):
+        if request.POST.get('change_status') is not None:
+            return self.__set_status(request)
+        return redirect(reverse_lazy("dashboard.views.node-detail",
+                                     kwargs={'pk': self.get_object().pk}))
+
+    def __set_status(self, request):
+        self.object = self.get_object()
+        if not self.object.enabled:
+            self.object.enable(user=request.user)
+        else:
+            self.object.disable(user=request.user)
+        success_message = _("Node successfully changed status.")
+
+        if request.is_ajax():
+            response = {
+                'message': success_message,
+                'node_pk': self.object.pk
+            }
+            return HttpResponse(
+                json.dumps(response),
+                content_type="application/json"
+            )
+        else:
+            messages.success(request, success_message)
+            return redirect(self.get_success_url())
+
+
+class NodeFlushView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
+    template_name = "dashboard/confirm/node-flush.html"
+    model = Node
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/confirm/ajax-node-flush.html']
+        else:
+            return ['dashboard/confirm/node-flush.html']
+
+    def get_success_url(self):
+        next = self.request.GET.get('next')
+        if next:
+            return next
+        else:
+            return reverse_lazy("dashboard.views.node-detail",
+                                kwargs={'pk': self.object.pk})
+
+    def get_context_data(self, **kwargs):
+        context = super(NodeFlushView, self).get_context_data(**kwargs)
+        return context
+
+    def post(self, request, *args, **kwargs):
+        if request.POST.get('flush') is not None:
+            return self.__flush(request)
+        return redirect(reverse_lazy("dashboard.views.node-detail",
+                                     kwargs={'pk': self.get_object().pk}))
+
+    def __flush(self, request):
+        self.object = self.get_object()
+        self.object.flush.async(user=request.user)
+        success_message = _("Node successfully flushed.")
+        messages.success(request, success_message)
+        return redirect(self.get_success_url())
+
+
+class NodeGraphView(SuperuserRequiredMixin, GraphViewBase):
+    metrics = {
+        'cpu': ('cactiStyle(alias(nonNegativeDerivative(%(prefix)s.cpu.times),'
+                '"cpu usage (%%)"))'),
+        'memory': ('cactiStyle(alias(%(prefix)s.memory.usage,'
+                   '"memory usage (%%)"))'),
+        'network': ('cactiStyle(aliasByMetric('
+                    'nonNegativeDerivative(%(prefix)s.network.bytes_*)))'),
+    }
+    model = Node
+
+    def get_prefix(self, instance):
+        return 'circle.%s' % instance.host.hostname
+
+    def get_title(self, instance, metric):
+        return '%s - %s' % (instance.name, metric)
+
+    def get_object(self, request, pk):
+        return self.model.objects.get(id=pk)
diff --git a/circle/dashboard/views/store.py b/circle/dashboard/views/store.py
new file mode 100644
index 0000000..a7debdc
--- /dev/null
+++ b/circle/dashboard/views/store.py
@@ -0,0 +1,206 @@
+# 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/>.
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+from os.path import join, normpath, dirname, basename
+
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.core.cache import get_cache
+from django.core.exceptions import SuspiciousOperation
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse
+from django.shortcuts import redirect, render_to_response, render
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from django.views.decorators.http import require_GET, require_POST
+from django.views.generic import TemplateView
+
+from braces.views import LoginRequiredMixin
+
+from ..store_api import Store, NoStoreException, NotOkException
+
+logger = logging.getLogger(__name__)
+
+
+class StoreList(LoginRequiredMixin, TemplateView):
+    template_name = "dashboard/store/list.html"
+
+    def get_context_data(self, **kwargs):
+        context = super(StoreList, self).get_context_data(**kwargs)
+        directory = self.request.GET.get("directory", "/")
+        directory = "/" if not len(directory) else directory
+
+        store = Store(self.request.user)
+        context['root'] = store.list(directory)
+        context['quota'] = store.get_quota()
+        context['up_url'] = self.create_up_directory(directory)
+        context['current'] = directory
+        context['next_url'] = "%s%s?directory=%s" % (
+            settings.DJANGO_URL.rstrip("/"),
+            reverse("dashboard.views.store-list"), directory)
+        return context
+
+    def get(self, *args, **kwargs):
+        try:
+            if self.request.is_ajax():
+                context = self.get_context_data(**kwargs)
+                return render_to_response(
+                    "dashboard/store/_list-box.html",
+                    RequestContext(self.request, context),
+                )
+            else:
+                return super(StoreList, self).get(*args, **kwargs)
+        except NoStoreException:
+            messages.warning(self.request, _("No store."))
+        except NotOkException:
+            messages.warning(self.request, _("Store has some problems now."
+                                             " Try again later."))
+        except Exception as e:
+            logger.critical("Something is wrong with store: %s", unicode(e))
+            messages.warning(self.request, _("Unknown store error."))
+        return redirect("/")
+
+    def create_up_directory(self, directory):
+        path = normpath(join('/', directory, '..'))
+        if not path.endswith("/"):
+            path += "/"
+        return path
+
+
+@require_GET
+@login_required
+def store_download(request):
+    path = request.GET.get("path")
+    try:
+        url = Store(request.user).request_download(path)
+    except Exception:
+        messages.error(request, _("Something went wrong during download."))
+        logger.exception("Unable to download, "
+                         "maybe it is already deleted")
+        return redirect(reverse("dashboard.views.store-list"))
+    return redirect(url)
+
+
+@require_GET
+@login_required
+def store_upload(request):
+    directory = request.GET.get("directory", "/")
+    try:
+        action = Store(request.user).request_upload(directory)
+    except Exception:
+        logger.exception("Unable to upload")
+        messages.error(request, _("Unable to upload file."))
+        return redirect("/")
+
+    next_url = "%s%s?directory=%s" % (
+        settings.DJANGO_URL.rstrip("/"),
+        reverse("dashboard.views.store-list"), directory)
+
+    return render(request, "dashboard/store/upload.html",
+                  {'directory': directory, 'action': action,
+                   'next_url': next_url})
+
+
+@require_GET
+@login_required
+def store_get_upload_url(request):
+    current_dir = request.GET.get("current_dir")
+    try:
+        url = Store(request.user).request_upload(current_dir)
+    except Exception:
+        logger.exception("Unable to upload")
+        messages.error(request, _("Unable to upload file."))
+        return redirect("/")
+    return HttpResponse(
+        json.dumps({'url': url}), content_type="application/json")
+
+
+class StoreRemove(LoginRequiredMixin, TemplateView):
+    template_name = "dashboard/store/remove.html"
+
+    def get_context_data(self, *args, **kwargs):
+        context = super(StoreRemove, self).get_context_data(*args, **kwargs)
+        path = self.request.GET.get("path", "/")
+        if path == "/":
+            SuspiciousOperation()
+
+        context['path'] = path
+        context['is_dir'] = path.endswith("/")
+        if context['is_dir']:
+            context['directory'] = path
+        else:
+            context['directory'] = dirname(path)
+            context['name'] = basename(path)
+
+        return context
+
+    def get(self, *args, **kwargs):
+        try:
+            return super(StoreRemove, self).get(*args, **kwargs)
+        except NoStoreException:
+            return redirect("/")
+
+    def post(self, *args, **kwargs):
+        path = self.request.POST.get("path")
+        try:
+            Store(self.request.user).remove(path)
+        except Exception:
+            logger.exception("Unable to remove %s", path)
+            messages.error(self.request, _("Unable to remove %s.") % path)
+
+        return redirect("%s?directory=%s" % (
+            reverse("dashboard.views.store-list"),
+            dirname(dirname(path)),
+        ))
+
+
+@require_POST
+@login_required
+def store_new_directory(request):
+    path = request.POST.get("path")
+    name = request.POST.get("name")
+
+    try:
+        Store(request.user).new_folder(join(path, name))
+    except Exception:
+        logger.exception("Unable to create folder %s in %s for %s",
+                         name, path, unicode(request.user))
+        messages.error(request, _("Unable to create folder."))
+    return redirect("%s?directory=%s" % (
+        reverse("dashboard.views.store-list"), path))
+
+
+@require_POST
+@login_required
+def store_refresh_toplist(request):
+    cache_key = "files-%d" % request.user.pk
+    cache = get_cache("default")
+    try:
+        store = Store(request.user)
+        toplist = store.toplist()
+        quota = store.get_quota()
+        files = {'toplist': toplist, 'quota': quota}
+    except Exception:
+        logger.exception("Can't get toplist of %s", unicode(request.user))
+        files = {'toplist': []}
+    cache.set(cache_key, files, 300)
+
+    return redirect(reverse("dashboard.index"))
diff --git a/circle/dashboard/views/template.py b/circle/dashboard/views/template.py
new file mode 100644
index 0000000..13d8f7f
--- /dev/null
+++ b/circle/dashboard/views/template.py
@@ -0,0 +1,397 @@
+# 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/>.
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+
+from django.contrib import messages
+from django.contrib.auth.models import User
+from django.contrib.messages.views import SuccessMessageMixin
+from django.core.urlresolvers import reverse, reverse_lazy
+from django.core.exceptions import PermissionDenied, SuspiciousOperation
+from django.http import HttpResponse
+from django.shortcuts import redirect, get_object_or_404
+from django.utils.translation import ugettext as _
+from django.views.generic import (
+    TemplateView, CreateView, DeleteView, UpdateView,
+)
+
+from braces.views import (
+    LoginRequiredMixin, PermissionRequiredMixin, SuperuserRequiredMixin,
+)
+from django_tables2 import SingleTableView
+
+from vm.models import InstanceTemplate, InterfaceTemplate, Instance, Lease
+
+from ..forms import (
+    TemplateForm, TemplateListSearchForm, AclUserOrGroupAddForm, LeaseForm,
+)
+from ..tables import TemplateListTable, LeaseListTable
+
+from .util import AclUpdateView, FilterMixin
+
+logger = logging.getLogger(__name__)
+
+
+class TemplateChoose(LoginRequiredMixin, 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)
+        templates = InstanceTemplate.get_objects_with_level("user",
+                                                            self.request.user)
+        context.update({
+            'box_title': _('Choose template'),
+            'ajax_title': True,
+            'template': "dashboard/_template-choose.html",
+            'templates': templates.all(),
+        })
+        return context
+
+    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"))
+        elif template is None:
+            messages.warning(request, _("Select an option to proceed."))
+            return redirect(reverse("dashboard.views.template-choose"))
+        else:
+            template = get_object_or_404(InstanceTemplate, pk=template)
+
+        if not template.has_level(request.user, "user"):
+            raise PermissionDenied()
+
+        instance = Instance.create_from_template(
+            template=template, owner=request.user, is_base=True)
+
+        return redirect(instance.get_absolute_url())
+
+
+class TemplateCreate(SuccessMessageMixin, CreateView):
+    model = InstanceTemplate
+    form_class = TemplateForm
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            pass
+        else:
+            return ['dashboard/nojs-wrapper.html']
+
+    def get_context_data(self, *args, **kwargs):
+        context = super(TemplateCreate, self).get_context_data(*args, **kwargs)
+
+        num_leases = Lease.get_objects_with_level("operator",
+                                                  self.request.user).count()
+        can_create_leases = self.request.user.has_perm("create_leases")
+        context.update({
+            'box_title': _("Create a new base VM"),
+            'template': "dashboard/_template-create.html",
+            'show_lease_create': num_leases < 1 and can_create_leases
+        })
+        return context
+
+    def get(self, *args, **kwargs):
+        if not self.request.user.has_perm('vm.create_base_template'):
+            raise PermissionDenied()
+
+        return super(TemplateCreate, self).get(*args, **kwargs)
+
+    def get_form_kwargs(self):
+        kwargs = super(TemplateCreate, self).get_form_kwargs()
+        kwargs['user'] = self.request.user
+        return kwargs
+
+    def post(self, request, *args, **kwargs):
+        if not self.request.user.has_perm('vm.create_base_template'):
+            raise PermissionDenied()
+
+        form = self.form_class(request.POST, user=request.user)
+        if not form.is_valid():
+            return self.get(request, form, *args, **kwargs)
+        else:
+            post = form.cleaned_data
+            networks = self.__create_networks(post.pop("networks"),
+                                              request.user)
+            post.pop("parent")
+            post['max_ram_size'] = post['ram_size']
+            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)
+
+            return redirect("%s#resources" % inst.get_absolute_url())
+
+    def __create_networks(self, vlans, user):
+        networks = []
+        for v in vlans:
+            if not v.has_level(user, "user"):
+                raise PermissionDenied()
+            networks.append(InterfaceTemplate(vlan=v, managed=v.managed))
+        return networks
+
+    def get_success_url(self):
+        return reverse_lazy("dashboard.views.template-list")
+
+
+class TemplateAclUpdateView(AclUpdateView):
+    model = InstanceTemplate
+
+
+class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView):
+    template_name = "dashboard/template-list.html"
+    model = InstanceTemplate
+    table_class = TemplateListTable
+    table_pagination = False
+
+    allowed_filters = {
+        'name': "name__icontains",
+        'tags[]': "tags__name__in",
+        'tags': "tags__name__in",  # for search string
+        'owner': "owner__username",
+        'ram': "ram_size",
+        'ram_size': "ram_size",
+        'cores': "num_cores",
+        'num_cores': "num_cores",
+        'access_method': "access_method__iexact",
+    }
+
+    def get_context_data(self, *args, **kwargs):
+        context = super(TemplateList, self).get_context_data(*args, **kwargs)
+        user = self.request.user
+        leases_w_operator = Lease.get_objects_with_level("operator", user)
+        context['lease_table'] = LeaseListTable(
+            leases_w_operator, request=self.request,
+            template="django_tables2/table_no_page.html",
+        )
+        context['show_lease_table'] = (
+            leases_w_operator.count() > 0 or
+            user.has_perm("vm.create_leases")
+        )
+
+        context['search_form'] = self.search_form
+
+        return context
+
+    def get(self, *args, **kwargs):
+        self.search_form = TemplateListSearchForm(self.request.GET)
+        self.search_form.full_clean()
+        return super(TemplateList, self).get(*args, **kwargs)
+
+    def create_acl_queryset(self, model):
+        queryset = super(TemplateList, self).create_acl_queryset(model)
+        sql = ("SELECT count(*) FROM vm_instance WHERE "
+               "vm_instance.template_id = vm_instancetemplate.id and "
+               "vm_instance.destroyed_at is null and "
+               "vm_instance.status = 'RUNNING'")
+        queryset = queryset.extra(select={'running': sql})
+        return queryset
+
+    def get_queryset(self):
+        logger.debug('TemplateList.get_queryset() called. User: %s',
+                     unicode(self.request.user))
+        qs = self.create_acl_queryset(InstanceTemplate)
+        self.create_fake_get()
+
+        try:
+            qs = qs.filter(**self.get_queryset_filters()).distinct()
+        except ValueError:
+            messages.error(self.request, _("Error during filtering."))
+
+        return qs.select_related("lease", "owner", "owner__profile")
+
+
+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.destroy_disks()
+        object.delete()
+        success_url = self.get_success_url()
+        success_message = _("Template successfully deleted.")
+
+        if request.is_ajax():
+            return HttpResponse(
+                json.dumps({'message': success_message}),
+                content_type="application/json",
+            )
+        else:
+            messages.success(request, success_message)
+            return redirect(success_url)
+
+
+class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
+    model = InstanceTemplate
+    template_name = "dashboard/template-edit.html"
+    form_class = TemplateForm
+    success_message = _("Successfully modified template.")
+
+    def get(self, request, *args, **kwargs):
+        template = self.get_object()
+        if not template.has_level(request.user, 'user'):
+            raise PermissionDenied()
+        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:
+            return super(TemplateDetail, self).get(request, *args, **kwargs)
+
+    def get_context_data(self, **kwargs):
+        obj = self.get_object()
+        context = super(TemplateDetail, self).get_context_data(**kwargs)
+        context['acl'] = AclUpdateView.get_acl_data(
+            obj, self.request.user, 'dashboard.views.template-acl')
+        context['disks'] = obj.disks.all()
+        context['is_owner'] = obj.has_level(self.request.user, 'owner')
+        context['aclform'] = AclUserOrGroupAddForm()
+        return context
+
+    def get_success_url(self):
+        return reverse_lazy("dashboard.views.template-detail",
+                            kwargs=self.kwargs)
+
+    def post(self, request, *args, **kwargs):
+        template = self.get_object()
+        if not template.has_level(request.user, 'owner'):
+            raise PermissionDenied()
+        return super(TemplateDetail, self).post(self, request, args, kwargs)
+
+    def get_form_kwargs(self):
+        kwargs = super(TemplateDetail, self).get_form_kwargs()
+        kwargs['user'] = self.request.user
+        return kwargs
+
+
+class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin,
+                  SuccessMessageMixin, CreateView):
+    model = Lease
+    form_class = LeaseForm
+    permission_required = 'vm.create_leases'
+    template_name = "dashboard/lease-create.html"
+    success_message = _("Successfully created a new lease.")
+
+    def get_success_url(self):
+        return reverse_lazy("dashboard.views.template-list")
+
+
+class LeaseAclUpdateView(AclUpdateView):
+    model = Lease
+
+
+class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin,
+                  SuccessMessageMixin, UpdateView):
+    model = Lease
+    form_class = LeaseForm
+    template_name = "dashboard/lease-edit.html"
+    success_message = _("Successfully modified lease.")
+
+    def get_context_data(self, *args, **kwargs):
+        obj = self.get_object()
+        context = super(LeaseDetail, self).get_context_data(*args, **kwargs)
+        context['acl'] = AclUpdateView.get_acl_data(
+            obj, self.request.user, 'dashboard.views.lease-acl')
+        return context
+
+    def get_success_url(self):
+        return reverse_lazy("dashboard.views.lease-detail", kwargs=self.kwargs)
+
+
+class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
+    model = Lease
+
+    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 get_context_data(self, *args, **kwargs):
+        c = super(LeaseDelete, self).get_context_data(*args, **kwargs)
+        lease = self.get_object()
+        templates = lease.instancetemplate_set
+        if templates.count() > 0:
+            text = _("You can't delete this lease because some templates "
+                     "are still using it, modify these to proceed: ")
+
+            c['text'] = text + ", ".join("<strong>%s (#%d)</strong>"
+                                         "" % (o.name, o.pk)
+                                         for o in templates.all())
+            c['disable_submit'] = True
+        return c
+
+    def delete(self, request, *args, **kwargs):
+        object = self.get_object()
+
+        if (object.instancetemplate_set.count() > 0):
+            raise SuspiciousOperation()
+
+        object.delete()
+        success_url = self.get_success_url()
+        success_message = _("Lease successfully deleted.")
+
+        if request.is_ajax():
+            return HttpResponse(
+                json.dumps({'message': success_message}),
+                content_type="application/json",
+            )
+        else:
+            messages.success(request, success_message)
+            return redirect(success_url)
diff --git a/circle/dashboard/views/user.py b/circle/dashboard/views/user.py
new file mode 100644
index 0000000..3c532d3
--- /dev/null
+++ b/circle/dashboard/views/user.py
@@ -0,0 +1,488 @@
+# 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/>.
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth import login
+from django.contrib.auth.models import User, Group
+from django.contrib.auth.views import login as login_view
+from django.contrib.messages.views import SuccessMessageMixin
+from django.core import signing
+from django.core.exceptions import (
+    PermissionDenied, SuspiciousOperation,
+)
+from django.core.urlresolvers import reverse, reverse_lazy
+from django.http import HttpResponse, HttpResponseRedirect, Http404
+from django.shortcuts import redirect, get_object_or_404
+from django.utils.translation import ugettext as _
+from django.views.decorators.http import require_POST
+from django.views.generic import (
+    TemplateView, DetailView, View, DeleteView, UpdateView, CreateView,
+)
+from django_sshkey.models import UserKey
+
+from braces.views import LoginRequiredMixin, PermissionRequiredMixin
+
+from vm.models import Instance, InstanceTemplate
+
+from ..forms import (
+    CircleAuthenticationForm, MyProfileForm, UserCreationForm, UnsubscribeForm,
+    UserKeyForm, CirclePasswordChangeForm, ConnectCommandForm,
+)
+from ..models import Profile, GroupProfile, ConnectCommand, create_profile
+from ..tables import UserKeyListTable, ConnectCommandListTable
+
+from .util import saml_available
+
+
+logger = logging.getLogger(__name__)
+
+
+class NotificationView(LoginRequiredMixin, TemplateView):
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/_notifications-timeline.html']
+        else:
+            return ['dashboard/notifications.html']
+
+    def get_context_data(self, *args, **kwargs):
+        context = super(NotificationView, self).get_context_data(
+            *args, **kwargs)
+        n = 10 if self.request.is_ajax() else 1000
+        context['notifications'] = list(
+            self.request.user.notification_set.all()[:n])
+        return context
+
+    def get(self, *args, **kwargs):
+        response = super(NotificationView, self).get(*args, **kwargs)
+        un = self.request.user.notification_set.filter(status="new")
+        for u in un:
+            u.status = "read"
+            u.save()
+        return response
+
+
+def circle_login(request):
+    authentication_form = CircleAuthenticationForm
+    extra_context = {
+        'saml2': saml_available,
+    }
+    response = login_view(request, authentication_form=authentication_form,
+                          extra_context=extra_context)
+    set_language_cookie(request, response)
+    return response
+
+
+class TokenLogin(View):
+
+    token_max_age = 120  # seconds
+
+    @classmethod
+    def get_salt(cls):
+        return unicode(cls)
+
+    @classmethod
+    def get_token(cls, user, sudoer):
+        return signing.dumps((sudoer.pk, user.pk),
+                             salt=cls.get_salt(), compress=True)
+
+    @classmethod
+    def get_token_url(cls, user, sudoer):
+        key = cls.get_token(user, sudoer)
+        return reverse("dashboard.views.token-login", args=(key, ))
+
+    def get(self, request, token, *args, **kwargs):
+        try:
+            data = signing.loads(token, salt=self.get_salt(),
+                                 max_age=self.token_max_age)
+            logger.debug('TokenLogin token data: %s', unicode(data))
+            sudoer, user = data
+            logger.debug('Extracted TokenLogin data: sudoer: %s, user: %s',
+                         unicode(sudoer), unicode(user))
+        except (signing.BadSignature, ValueError, TypeError) as e:
+            logger.warning('Tried invalid TokenLogin token. '
+                           'Token: %s, user: %s. %s',
+                           token, unicode(self.request.user), unicode(e))
+            raise SuspiciousOperation()
+        sudoer = User.objects.get(pk=sudoer)
+        if not sudoer.is_superuser:
+            raise PermissionDenied()
+        user = User.objects.get(pk=user)
+        user.backend = 'django.contrib.auth.backends.ModelBackend'
+        logger.warning('%s %d logged in as user %s %d',
+                       unicode(sudoer), sudoer.pk, unicode(user), user.pk)
+        login(request, user)
+        messages.info(request, _("Logged in as user %s.") % unicode(user))
+        return redirect("/")
+
+
+class MyPreferencesView(UpdateView):
+    model = Profile
+
+    def get_context_data(self, *args, **kwargs):
+        context = super(MyPreferencesView, self).get_context_data(*args,
+                                                                  **kwargs)
+        context['forms'] = {
+            'change_password': CirclePasswordChangeForm(
+                user=self.request.user),
+            'change_language': MyProfileForm(instance=self.get_object()),
+        }
+        key_table = UserKeyListTable(
+            UserKey.objects.filter(user=self.request.user),
+            request=self.request)
+        key_table.page = None
+        context['userkey_table'] = key_table
+        cmd_table = ConnectCommandListTable(
+            self.request.user.command_set.all(),
+            request=self.request)
+        cmd_table.page = None
+        context['connectcommand_table'] = cmd_table
+        return context
+
+    def get_object(self, queryset=None):
+        if self.request.user.is_anonymous():
+            raise PermissionDenied()
+        try:
+            return self.request.user.profile
+        except Profile.DoesNotExist:
+            raise Http404(_("You don't have a profile."))
+
+    def post(self, request, *args, **kwargs):
+        self.ojbect = self.get_object()
+        redirect_response = HttpResponseRedirect(
+            reverse("dashboard.views.profile-preferences"))
+        if "preferred_language" in request.POST:
+            form = MyProfileForm(request.POST, instance=self.get_object())
+            if form.is_valid():
+                lang = form.cleaned_data.get("preferred_language")
+                set_language_cookie(self.request, redirect_response, lang)
+                form.save()
+        else:
+            form = CirclePasswordChangeForm(user=request.user,
+                                            data=request.POST)
+            if form.is_valid():
+                form.save()
+
+        if form.is_valid():
+            return redirect_response
+        else:
+            return self.get(request, form=form, *args, **kwargs)
+
+    def get(self, request, form=None, *args, **kwargs):
+        # if this is not here, it won't work
+        self.object = self.get_object()
+        context = self.get_context_data(*args, **kwargs)
+        if form is not None:
+            # a little cheating, users can't post invalid
+            # language selection forms (without modifying the HTML)
+            context['forms']['change_password'] = form
+        return self.render_to_response(context)
+
+
+class UnsubscribeFormView(SuccessMessageMixin, UpdateView):
+    model = Profile
+    form_class = UnsubscribeForm
+    template_name = "dashboard/unsubscribe.html"
+    success_message = _("Successfully modified subscription.")
+
+    def get_success_url(self):
+        if self.request.user.is_authenticated():
+            return super(UnsubscribeFormView, self).get_success_url()
+        else:
+            return self.request.path
+
+    @classmethod
+    def get_salt(cls):
+        return unicode(cls)
+
+    @classmethod
+    def get_token(cls, user):
+        return signing.dumps(user.pk, salt=cls.get_salt(), compress=True)
+
+    def get_object(self, queryset=None):
+        key = self.kwargs['token']
+        try:
+            pk = signing.loads(key, salt=self.get_salt(), max_age=48 * 3600)
+        except signing.SignatureExpired:
+            raise
+        except (signing.BadSignature, ValueError, TypeError) as e:
+            logger.warning('Tried invalid token. Token: %s, user: %s. %s',
+                           key, unicode(self.request.user), unicode(e))
+            raise Http404
+        else:
+            return (queryset or self.get_queryset()).get(user_id=pk)
+
+    def dispatch(self, request, *args, **kwargs):
+        try:
+            return super(UnsubscribeFormView, self).dispatch(
+                request, *args, **kwargs)
+        except signing.SignatureExpired:
+            return redirect('dashboard.views.profile-preferences')
+
+
+def set_language_cookie(request, response, lang=None):
+    if lang is None:
+        try:
+            lang = request.user.profile.preferred_language
+        except:
+            return
+
+    cname = getattr(settings, 'LANGUAGE_COOKIE_NAME', 'django_language')
+    response.set_cookie(cname, lang, 365 * 86400)
+
+
+class UserCreationView(LoginRequiredMixin, PermissionRequiredMixin,
+                       CreateView):
+    form_class = UserCreationForm
+    model = User
+    template_name = 'dashboard/user-create.html'
+    permission_required = "auth.add_user"
+
+    def get_group(self, group_pk):
+        self.group = get_object_or_404(Group, pk=group_pk)
+        if not self.group.profile.has_level(self.request.user, 'owner'):
+            raise PermissionDenied()
+
+    def get(self, *args, **kwargs):
+        self.get_group(kwargs.pop('group_pk'))
+        return super(UserCreationView, self).get(*args, **kwargs)
+
+    def post(self, *args, **kwargs):
+        group_pk = kwargs.pop('group_pk')
+        self.get_group(group_pk)
+        ret = super(UserCreationView, self).post(*args, **kwargs)
+        if self.object:
+            create_profile(self.object)
+            self.object.groups.add(self.group)
+            return redirect(
+                reverse('dashboard.views.group-detail', args=[group_pk]))
+        else:
+            return ret
+
+
+class ProfileView(LoginRequiredMixin, DetailView):
+    template_name = "dashboard/profile.html"
+    model = User
+    slug_field = "username"
+    slug_url_kwarg = "username"
+
+    def get_context_data(self, **kwargs):
+        context = super(ProfileView, self).get_context_data(**kwargs)
+        user = self.get_object()
+        context['profile'] = user
+        context['avatar_url'] = user.profile.get_avatar_url()
+        context['instances_owned'] = Instance.get_objects_with_level(
+            "owner", user, disregard_superuser=True).filter(destroyed_at=None)
+        context['instances_with_access'] = Instance.get_objects_with_level(
+            "user", user, disregard_superuser=True
+        ).filter(destroyed_at=None).exclude(pk__in=context['instances_owned'])
+
+        group_profiles = GroupProfile.get_objects_with_level(
+            "operator", self.request.user)
+        groups = Group.objects.filter(groupprofile__in=group_profiles)
+        context['groups'] = user.groups.filter(pk__in=groups)
+
+        # permissions
+        # show groups only if the user is superuser, or have access
+        # to any of the groups the user belongs to
+        context['perm_group_list'] = (
+            self.request.user.is_superuser or len(context['groups']) > 0)
+        context['perm_email'] = (
+            context['perm_group_list'] or self.request.user == user)
+
+        # filter the virtual machine list
+        # if the logged in user is not superuser or not the user itself
+        # filter the list so only those virtual machines are shown that are
+        # originated from templates the logged in user is operator or higher
+        if not (self.request.user.is_superuser or self.request.user == user):
+            it = InstanceTemplate.get_objects_with_level("operator",
+                                                         self.request.user)
+            context['instances_owned'] = context['instances_owned'].filter(
+                template__in=it)
+            context['instances_with_access'] = context[
+                'instances_with_access'].filter(template__in=it)
+        if self.request.user.is_superuser:
+            context['login_token'] = TokenLogin.get_token_url(
+                user, self.request.user)
+        return context
+
+
+@require_POST
+def toggle_use_gravatar(request, **kwargs):
+    user = get_object_or_404(User, username=kwargs['username'])
+    if not request.user == user:
+        raise PermissionDenied()
+
+    profile = user.profile
+    profile.use_gravatar = not profile.use_gravatar
+    profile.save()
+
+    new_avatar_url = user.profile.get_avatar_url()
+    return HttpResponse(
+        json.dumps({'new_avatar_url': new_avatar_url}),
+        content_type="application/json",
+    )
+
+
+class UserKeyDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
+    model = UserKey
+    template_name = "dashboard/userkey-edit.html"
+    form_class = UserKeyForm
+    success_message = _("Successfully modified SSH key.")
+
+    def get(self, request, *args, **kwargs):
+        object = self.get_object()
+        if object.user != request.user:
+            raise PermissionDenied()
+        return super(UserKeyDetail, self).get(request, *args, **kwargs)
+
+    def get_success_url(self):
+        return reverse_lazy("dashboard.views.userkey-detail",
+                            kwargs=self.kwargs)
+
+    def post(self, request, *args, **kwargs):
+        object = self.get_object()
+        if object.user != request.user:
+            raise PermissionDenied()
+        return super(UserKeyDetail, self).post(self, request, args, kwargs)
+
+
+class UserKeyDelete(LoginRequiredMixin, DeleteView):
+    model = UserKey
+
+    def get_success_url(self):
+        return reverse("dashboard.views.profile-preferences")
+
+    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 object.user != request.user:
+            raise PermissionDenied()
+
+        object.delete()
+        success_url = self.get_success_url()
+        success_message = _("SSH key successfully deleted.")
+
+        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)
+
+
+class UserKeyCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView):
+    model = UserKey
+    form_class = UserKeyForm
+    template_name = "dashboard/userkey-create.html"
+    success_message = _("Successfully created a new SSH key.")
+
+    def get_success_url(self):
+        return reverse_lazy("dashboard.views.profile-preferences")
+
+    def get_form_kwargs(self):
+        kwargs = super(UserKeyCreate, self).get_form_kwargs()
+        kwargs['user'] = self.request.user
+        return kwargs
+
+
+class ConnectCommandDetail(LoginRequiredMixin, SuccessMessageMixin,
+                           UpdateView):
+    model = ConnectCommand
+    template_name = "dashboard/connect-command-edit.html"
+    form_class = ConnectCommandForm
+    success_message = _("Successfully modified command template.")
+
+    def get(self, request, *args, **kwargs):
+        object = self.get_object()
+        if object.user != request.user:
+            raise PermissionDenied()
+        return super(ConnectCommandDetail, self).get(request, *args, **kwargs)
+
+    def get_success_url(self):
+        return reverse_lazy("dashboard.views.connect-command-detail",
+                            kwargs=self.kwargs)
+
+    def post(self, request, *args, **kwargs):
+        object = self.get_object()
+        if object.user != request.user:
+            raise PermissionDenied()
+        return super(ConnectCommandDetail, self).post(request, args, kwargs)
+
+    def get_form_kwargs(self):
+        kwargs = super(ConnectCommandDetail, self).get_form_kwargs()
+        kwargs['user'] = self.request.user
+        return kwargs
+
+
+class ConnectCommandDelete(LoginRequiredMixin, DeleteView):
+    model = ConnectCommand
+
+    def get_success_url(self):
+        return reverse("dashboard.views.profile-preferences")
+
+    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 object.user != request.user:
+            raise PermissionDenied()
+
+        object.delete()
+        success_url = self.get_success_url()
+        success_message = _("Command template successfully deleted.")
+
+        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)
+
+
+class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin,
+                           CreateView):
+    model = ConnectCommand
+    form_class = ConnectCommandForm
+    template_name = "dashboard/connect-command-create.html"
+    success_message = _("Successfully created a new command template.")
+
+    def get_success_url(self):
+        return reverse_lazy("dashboard.views.profile-preferences")
+
+    def get_form_kwargs(self):
+        kwargs = super(ConnectCommandCreate, self).get_form_kwargs()
+        kwargs['user'] = self.request.user
+        return kwargs
diff --git a/circle/dashboard/views/util.py b/circle/dashboard/views/util.py
new file mode 100644
index 0000000..47c6dbb
--- /dev/null
+++ b/circle/dashboard/views/util.py
@@ -0,0 +1,579 @@
+# 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/>.
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+import re
+from collections import OrderedDict
+from urlparse import urljoin
+
+import requests
+
+from django.conf import settings
+from django.contrib.auth.models import User, Group
+from django.core.exceptions import PermissionDenied, SuspiciousOperation
+from django.core.urlresolvers import reverse
+from django.contrib import messages
+from django.contrib.auth.views import redirect_to_login
+from django.db.models import Q
+from django.http import HttpResponse, Http404
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from django.views.generic import DetailView, View
+from django.views.generic.detail import SingleObjectMixin
+
+from braces.views import LoginRequiredMixin
+from braces.views._access import AccessMixin
+from celery.exceptions import TimeoutError
+
+from common.models import HumanReadableException, HumanReadableObject
+from ..models import GroupProfile
+
+logger = logging.getLogger(__name__)
+saml_available = hasattr(settings, "SAML_CONFIG")
+
+
+class RedirectToLoginMixin(AccessMixin):
+
+    redirect_exception_classes = (PermissionDenied, )
+
+    def dispatch(self, request, *args, **kwargs):
+        try:
+            return super(RedirectToLoginMixin, self).dispatch(
+                request, *args, **kwargs)
+        except self.redirect_exception_classes:
+            if not request.user.is_authenticated():
+                return redirect_to_login(request.get_full_path(),
+                                         self.get_login_url(),
+                                         self.get_redirect_field_name())
+            else:
+                raise
+
+
+def search_user(keyword):
+    try:
+        return User.objects.get(username=keyword)
+    except User.DoesNotExist:
+        try:
+            return User.objects.get(profile__org_id=keyword)
+        except User.DoesNotExist:
+            return User.objects.get(email=keyword)
+
+
+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].split(",")
+                    if self.allowed_filters[item].endswith("__in") else
+                    self.request.GET[item])
+
+        return filters
+
+    def get_queryset(self):
+        return super(FilterMixin,
+                     self).get_queryset().filter(**self.get_queryset_filters())
+
+    def create_fake_get(self):
+        self.request.GET = self._parse_get(self.request.GET)
+
+    def _parse_get(self, GET_dict):
+        """
+        Returns a new dict from request's GET dict to filter the vm list
+        For example: "name:xy node:1" updates the GET dict
+                     to resemble this URL ?name=xy&node=1
+
+        "name:xy node:1".split(":") becomes ["name", "xy node", "1"]
+        we pop the the first element and use it as the first dict key
+        then we iterate over the rest of the list and split by the last
+        whitespace, the first part of this list will be the previous key's
+        value, then last part of the list will be the next key.
+        The final dict looks like this: {'name': xy, 'node':1}
+
+        >>> f = FilterMixin()
+        >>> o = f._parse_get({'s': "hello"}).items()
+        >>> sorted(o) # doctest: +ELLIPSIS
+        [(u'name', u'hello'), (...)]
+        >>> o = f._parse_get({'s': "name:hello owner:test"}).items()
+        >>> sorted(o) # doctest: +ELLIPSIS
+        [(u'name', u'hello'), (u'owner', u'test'), (...)]
+        >>> o = f._parse_get({'s': "name:hello ws node:node 3 oh"}).items()
+        >>> sorted(o) # doctest: +ELLIPSIS
+        [(u'name', u'hello ws'), (u'node', u'node 3 oh'), (...)]
+        """
+        s = GET_dict.get("s")
+        fake = GET_dict.copy()
+        if s:
+            s = s.split(":")
+            if len(s) < 2:  # if there is no ':' in the string, filter by name
+                got = {'name': s[0]}
+            else:
+                latest = s.pop(0)
+                got = {'%s' % latest: None}
+                for i in s[:-1]:
+                    new = i.rsplit(" ", 1)
+                    got[latest] = new[0]
+                    latest = new[1] if len(new) > 1 else None
+                got[latest] = s[-1]
+
+            # generate a new GET request, that is kinda fake
+            for k, v in got.iteritems():
+                fake[k] = v
+        return fake
+
+    def create_acl_queryset(self, model):
+        cleaned_data = self.search_form.cleaned_data
+        stype = cleaned_data.get('stype', "all")
+        superuser = stype == "all"
+        shared = stype == "shared" or stype == "all"
+        level = "owner" if stype == "owned" else "user"
+        queryset = model.get_objects_with_level(
+            level, self.request.user,
+            group_also=shared, disregard_superuser=not superuser,
+        )
+        return queryset
+
+
+class CheckedDetailView(LoginRequiredMixin, DetailView):
+    read_level = 'user'
+
+    def get_has_level(self):
+        return self.object.has_level
+
+    def get_context_data(self, **kwargs):
+        context = super(CheckedDetailView, self).get_context_data(**kwargs)
+        if not self.get_has_level()(self.request.user, self.read_level):
+            raise PermissionDenied()
+        return context
+
+
+class OperationView(RedirectToLoginMixin, DetailView):
+
+    template_name = 'dashboard/operate.html'
+    show_in_toolbar = True
+    effect = None
+    wait_for_result = None
+    with_reload = False
+
+    @property
+    def name(self):
+        return self.get_op().name
+
+    @property
+    def description(self):
+        return self.get_op().description
+
+    def is_preferred(self):
+        return self.get_op().is_preferred()
+
+    @classmethod
+    def get_urlname(cls):
+        return 'dashboard.vm.op.%s' % cls.op
+
+    @classmethod
+    def get_instance_url(cls, pk, key=None, *args, **kwargs):
+        url = reverse(cls.get_urlname(), args=(pk, ) + args, kwargs=kwargs)
+        if key is None:
+            return url
+        else:
+            return "%s?k=%s" % (url, key)
+
+    def get_url(self, **kwargs):
+        return self.get_instance_url(self.get_object().pk, **kwargs)
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/_modal.html']
+        else:
+            return ['dashboard/_base.html']
+
+    @classmethod
+    def get_op_by_object(cls, obj):
+        return getattr(obj, cls.op)
+
+    def get_op(self):
+        if not hasattr(self, '_opobj'):
+            setattr(self, '_opobj', getattr(self.get_object(), self.op))
+        return self._opobj
+
+    @classmethod
+    def get_operation_class(cls):
+        return cls.model.get_operation_class(cls.op)
+
+    def get_context_data(self, **kwargs):
+        ctx = super(OperationView, self).get_context_data(**kwargs)
+        ctx['op'] = self.get_op()
+        ctx['opview'] = self
+        url = self.request.path
+        if self.request.GET:
+            url += '?' + self.request.GET.urlencode()
+        ctx['url'] = url
+        ctx['template'] = super(OperationView, self).get_template_names()[0]
+        return ctx
+
+    def check_auth(self):
+        logger.debug("OperationView.check_auth(%s)", unicode(self))
+        self.get_op().check_auth(self.request.user)
+
+    @classmethod
+    def check_perms(cls, user):
+        cls.get_operation_class().check_perms(user)
+
+    def get(self, request, *args, **kwargs):
+        self.check_auth()
+        return super(OperationView, self).get(request, *args, **kwargs)
+
+    def get_response_data(self, result, done, extra=None, **kwargs):
+        """Return serializable data to return to agents requesting json
+        response to POST"""
+
+        if extra is None:
+            extra = {}
+        extra["success"] = not isinstance(result, Exception)
+        extra["done"] = done
+        if isinstance(result, HumanReadableObject):
+            extra["message"] = result.get_user_text()
+        return extra
+
+    def post(self, request, extra=None, *args, **kwargs):
+        self.check_auth()
+        self.object = self.get_object()
+        if extra is None:
+            extra = {}
+        result = None
+        done = False
+        try:
+            task = self.get_op().async(user=request.user, **extra)
+        except HumanReadableException as e:
+            e.send_message(request)
+            logger.exception("Could not start operation")
+            result = e
+        except Exception as e:
+            messages.error(request, _('Could not start operation.'))
+            logger.exception("Could not start operation")
+            result = e
+        else:
+            wait = self.wait_for_result
+            if wait:
+                try:
+                    result = task.get(timeout=wait,
+                                      interval=min((wait / 5, .5)))
+                except TimeoutError:
+                    logger.debug("Result didn't arrive in %ss",
+                                 self.wait_for_result, exc_info=True)
+                except HumanReadableException as e:
+                    e.send_message(request)
+                    logger.exception(e)
+                    result = e
+                except Exception as e:
+                    messages.error(request, _('Operation failed.'))
+                    logger.debug("Operation failed.", exc_info=True)
+                    result = e
+                else:
+                    done = True
+                    messages.success(request, _('Operation succeeded.'))
+            if result is None and not done:
+                messages.success(request, _('Operation is started.'))
+
+        if "/json" in request.META.get("HTTP_ACCEPT", ""):
+            data = self.get_response_data(result, done,
+                                          post_extra=extra, **kwargs)
+            return HttpResponse(json.dumps(data),
+                                content_type="application/json")
+        else:
+            return redirect("%s#activity" % self.object.get_absolute_url())
+
+    @classmethod
+    def factory(cls, op, icon='cog', effect='info', extra_bases=(), **kwargs):
+        kwargs.update({'op': op, 'icon': icon, 'effect': effect})
+        return type(str(cls.__name__ + op),
+                    tuple(list(extra_bases) + [cls]), kwargs)
+
+    @classmethod
+    def bind_to_object(cls, instance, **kwargs):
+        me = cls()
+        me.get_object = lambda: instance
+        for key, value in kwargs.iteritems():
+            setattr(me, key, value)
+        return me
+
+
+class AjaxOperationMixin(object):
+
+    def post(self, request, extra=None, *args, **kwargs):
+        resp = super(AjaxOperationMixin, self).post(
+            request, extra, *args, **kwargs)
+        if request.is_ajax():
+            if not self.with_reload:
+                store = messages.get_messages(request)
+                store.used = True
+            else:
+                store = []
+            return HttpResponse(
+                json.dumps({'success': True,
+                            'with_reload': self.with_reload,
+                            'messages': [unicode(m) for m in store]}),
+                content_type="application=json"
+            )
+        else:
+            return resp
+
+
+class FormOperationMixin(object):
+
+    form_class = None
+
+    def get_form_kwargs(self):
+        return {}
+
+    def get_context_data(self, **kwargs):
+        ctx = super(FormOperationMixin, self).get_context_data(**kwargs)
+        if self.request.method == 'POST':
+            ctx['form'] = self.form_class(self.request.POST,
+                                          **self.get_form_kwargs())
+        else:
+            ctx['form'] = self.form_class(**self.get_form_kwargs())
+        return ctx
+
+    def post(self, request, extra=None, *args, **kwargs):
+        if extra is None:
+            extra = {}
+        form = self.form_class(self.request.POST, **self.get_form_kwargs())
+        if form.is_valid():
+            extra.update(form.cleaned_data)
+            resp = super(FormOperationMixin, self).post(
+                request, extra, *args, **kwargs)
+            if request.is_ajax():
+                return HttpResponse(
+                    json.dumps({
+                        'success': True,
+                        'with_reload': self.with_reload}),
+                    content_type="application=json")
+            else:
+                return resp
+        else:
+            return self.get(request)
+
+
+class RequestFormOperationMixin(FormOperationMixin):
+
+    def get_form_kwargs(self):
+        val = super(RequestFormOperationMixin, self).get_form_kwargs()
+        val.update({'request': self.request})
+        return val
+
+
+class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
+    def send_success_message(self, whom, old_level, new_level):
+        if old_level and new_level:
+            msg = _("Acl user/group %(w)s successfully modified.")
+        elif not old_level and new_level:
+            msg = _("Acl user/group %(w)s successfully added.")
+        elif old_level and not new_level:
+            msg = _("Acl user/group %(w)s successfully removed.")
+        if msg:
+            messages.success(self.request, msg % {'w': whom})
+
+    def get_level(self, whom):
+        for u, level in self.acl_data:
+            if u == whom:
+                return level
+        return None
+
+    @classmethod
+    def get_acl_data(cls, obj, user, url):
+        levels = obj.ACL_LEVELS
+        allowed_levels = list(l for l in OrderedDict(levels)
+                              if cls.has_next_level(user, obj, l))
+        is_owner = 'owner' in allowed_levels
+
+        allowed_users = cls.get_allowed_users(user)
+        allowed_groups = cls.get_allowed_groups(user)
+
+        user_levels = list(
+            {'user': u, 'level': l} for u, l in obj.get_users_with_level()
+            if is_owner or u == user or u in allowed_users)
+
+        group_levels = list(
+            {'group': g, 'level': l} for g, l in obj.get_groups_with_level()
+            if is_owner or g in allowed_groups)
+
+        return {'users': user_levels,
+                'groups': group_levels,
+                'levels': levels,
+                'allowed_levels': allowed_levels,
+                'url': reverse(url, args=[obj.pk])}
+
+    @classmethod
+    def has_next_level(self, user, instance, level):
+        levels = OrderedDict(instance.ACL_LEVELS).keys()
+        next_levels = dict(zip([None] + levels, levels + levels[-1:]))
+        # {None: 'user', 'user': 'operator', 'operator: 'owner',
+        #  'owner: 'owner'}
+        next_level = next_levels[level]
+        return instance.has_level(user, next_level)
+
+    @classmethod
+    def get_allowed_groups(cls, user):
+        if user.has_perm('dashboard.use_autocomplete'):
+            return Group.objects.all()
+        else:
+            profiles = GroupProfile.get_objects_with_level('owner', user)
+            return Group.objects.filter(groupprofile__in=profiles).distinct()
+
+    @classmethod
+    def get_allowed_users(cls, user):
+        if user.has_perm('dashboard.use_autocomplete'):
+            return User.objects.all()
+        else:
+            groups = cls.get_allowed_groups(user)
+            return User.objects.filter(
+                Q(groups__in=groups) | Q(pk=user.pk)).distinct()
+
+    def check_auth(self, whom, old_level, new_level):
+        if isinstance(whom, Group):
+            if (not self.is_owner and whom not in
+                    AclUpdateView.get_allowed_groups(self.request.user)):
+                return False
+        elif isinstance(whom, User):
+            if (not self.is_owner and whom not in
+                    AclUpdateView.get_allowed_users(self.request.user)):
+                return False
+        return (
+            AclUpdateView.has_next_level(self.request.user,
+                                         self.instance, new_level) and
+            AclUpdateView.has_next_level(self.request.user,
+                                         self.instance, old_level))
+
+    def set_level(self, whom, new_level):
+        user = self.request.user
+        old_level = self.get_level(whom)
+        if old_level == new_level:
+            return
+
+        if getattr(self.instance, "owner", None) == whom:
+            logger.info("Tried to set owner's acl level for %s by %s.",
+                        unicode(self.instance), unicode(user))
+            msg = _("The original owner cannot be removed, however "
+                    "you can transfer ownership.")
+            if not getattr(self, 'hide_messages', False):
+                messages.warning(self.request, msg)
+        elif self.check_auth(whom, old_level, new_level):
+            logger.info(
+                u"Set %s's acl level for %s to %s by %s.", unicode(whom),
+                unicode(self.instance), new_level, unicode(user))
+            if not getattr(self, 'hide_messages', False):
+                self.send_success_message(whom, old_level, new_level)
+            self.instance.set_level(whom, new_level)
+        else:
+            logger.warning(
+                u"Tried to set %s's acl_level for %s (%s->%s) by %s.",
+                unicode(whom), unicode(self.instance), old_level, new_level,
+                unicode(user))
+
+    def set_or_remove_levels(self):
+        for key, value in self.request.POST.items():
+            m = re.match('(perm|remove)-([ug])-(\d+)', key)
+            if m:
+                cmd, typ, id = m.groups()
+                if cmd == 'remove':
+                    value = None
+                entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
+                self.set_level(entity, value)
+
+    def add_levels(self):
+        name = self.request.POST.get('name', None)
+        level = self.request.POST.get('level', None)
+        if not name or not level:
+            return
+        try:
+            entity = search_user(name)
+            if self.instance.object_level_set.filter(users__in=[entity]):
+                messages.warning(
+                    self.request, _('User "%s" has already '
+                                    'access to this object.') % name)
+                return
+        except User.DoesNotExist:
+            entity = None
+            try:
+                entity = Group.objects.get(name=name)
+                if self.instance.object_level_set.filter(groups__in=[entity]):
+                    messages.warning(
+                        self.request, _('Group "%s" has already '
+                                        'access to this object.') % name)
+                    return
+            except Group.DoesNotExist:
+                messages.warning(
+                    self.request, _('User or group "%s" not found.') % name)
+                return
+        self.set_level(entity, level)
+
+    def post(self, request, *args, **kwargs):
+        self.instance = self.get_object()
+        self.is_owner = self.instance.has_level(request.user, 'owner')
+        self.acl_data = (self.instance.get_users_with_level() +
+                         self.instance.get_groups_with_level())
+        self.set_or_remove_levels()
+        self.add_levels()
+        return redirect("%s#access" % self.instance.get_absolute_url())
+
+
+class GraphViewBase(LoginRequiredMixin, View):
+    def get(self, request, pk, metric, time, *args, **kwargs):
+        graphite_url = settings.GRAPHITE_URL
+        if graphite_url is None:
+            raise Http404()
+
+        if metric not in self.metrics.keys():
+            raise SuspiciousOperation()
+
+        try:
+            instance = self.get_object(request, pk)
+        except self.model.DoesNotExist:
+            raise Http404()
+
+        prefix = self.get_prefix(instance)
+        target = self.metrics[metric] % {'prefix': prefix}
+        title = self.get_title(instance, metric)
+        params = {'target': target,
+                  'from': '-%s' % time,
+                  'title': title.encode('UTF-8'),
+                  'width': '500',
+                  'height': '200'}
+        logger.debug('%s %s', graphite_url, params)
+        response = requests.get('%s/render/' % graphite_url, params=params)
+        return HttpResponse(response.content, mimetype="image/png")
+
+    def get_prefix(self, instance):
+        raise NotImplementedError("Subclass must implement abstract method")
+
+    def get_title(self, instance, metric):
+        raise NotImplementedError("Subclass must implement abstract method")
+
+    def get_object(self, request, pk):
+        instance = self.model.objects.get(id=pk)
+        if not instance.has_level(request.user, 'user'):
+            raise PermissionDenied()
+        return instance
+
+
+def absolute_url(url):
+    return urljoin(settings.DJANGO_URL, url)
diff --git a/circle/dashboard/views/vm.py b/circle/dashboard/views/vm.py
new file mode 100644
index 0000000..5712cfe
--- /dev/null
+++ b/circle/dashboard/views/vm.py
@@ -0,0 +1,1417 @@
+# 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/>.
+from __future__ import unicode_literals, absolute_import
+
+import json
+import logging
+from collections import OrderedDict
+from os import getenv
+
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth.models import User
+from django.core import signing
+from django.core.exceptions import PermissionDenied, SuspiciousOperation
+from django.core.urlresolvers import reverse, reverse_lazy
+from django.http import HttpResponse, Http404
+from django.shortcuts import redirect, get_object_or_404, render
+from django.template import RequestContext
+from django.template.loader import render_to_string
+from django.utils.translation import (
+    ugettext as _, ugettext_noop, ungettext_lazy,
+)
+from django.views.decorators.http import require_GET
+from django.views.generic import (
+    UpdateView, ListView, TemplateView, DeleteView, DetailView, View,
+)
+
+from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
+
+from common.models import (
+    create_readable, HumanReadableException, fetch_human_exception,
+)
+from firewall.models import Vlan, Host, Rule
+from storage.models import Disk
+from vm.models import (
+    Instance, instance_activity, InstanceActivity, Node, Lease,
+    InstanceTemplate, InterfaceTemplate, Interface,
+)
+from .util import (
+    CheckedDetailView, AjaxOperationMixin, OperationView, AclUpdateView,
+    FormOperationMixin, FilterMixin, GraphViewBase, search_user,
+)
+from ..forms import (
+    AclUserOrGroupAddForm, VmResourcesForm, TraitsForm, RawDataForm,
+    VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm,
+    VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm,
+    TransferOwnershipForm,
+)
+from ..models import Favourite, Profile
+
+logger = logging.getLogger(__name__)
+
+
+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 not request.user.has_perm('vm.access_console'):
+            raise PermissionDenied()
+        if self.object.node:
+            with instance_activity(
+                    code_suffix='console-accessed', instance=self.object,
+                    user=request.user, readable_name=ugettext_noop(
+                        "console access"), 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)
+        else:
+            raise Http404()
+
+
+class VmDetailView(CheckedDetailView):
+    template_name = "dashboard/vm-detail.html"
+    model = Instance
+
+    def get_context_data(self, **kwargs):
+        context = super(VmDetailView, self).get_context_data(**kwargs)
+        instance = context['instance']
+        user = self.request.user
+        ops = get_operations(instance, user)
+        context.update({
+            'graphite_enabled': settings.GRAPHITE_URL is not None,
+            'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
+                                    kwargs={'pk': self.object.pk}),
+            'ops': ops,
+            'op': {i.op: i for i in ops},
+            'connect_commands': user.profile.get_connect_commands(instance)
+        })
+
+        # activity data
+        activities = instance.get_merged_activities(user)
+        show_show_all = len(activities) > 10
+        activities = activities[:10]
+        context['activities'] = _format_activities(activities)
+        context['show_show_all'] = show_show_all
+        latest = instance.get_latest_activity_in_progress()
+        context['is_new_state'] = (latest and
+                                   latest.resultant_state is not None and
+                                   instance.status != latest.resultant_state)
+
+        context['vlans'] = Vlan.get_objects_with_level(
+            'user', self.request.user
+        ).exclude(  # exclude already added interfaces
+            pk__in=Interface.objects.filter(
+                instance=self.get_object()).values_list("vlan", flat=True)
+        ).all()
+        context['acl'] = AclUpdateView.get_acl_data(
+            instance, self.request.user, 'dashboard.views.vm-acl')
+        context['aclform'] = AclUserOrGroupAddForm()
+        context['os_type_icon'] = instance.os_type.replace("unknown",
+                                                           "question")
+        # ipv6 infos
+        context['ipv6_host'] = instance.get_connect_host(use_ipv6=True)
+        context['ipv6_port'] = instance.get_connect_port(use_ipv6=True)
+
+        # resources forms
+        can_edit = (
+            instance.has_level(user, "owner")
+            and self.request.user.has_perm("vm.change_resources"))
+        context['resources_form'] = VmResourcesForm(
+            can_edit=can_edit, instance=instance)
+
+        if self.request.user.is_superuser:
+            context['traits_form'] = TraitsForm(instance=instance)
+            context['raw_data_form'] = RawDataForm(instance=instance)
+
+        # resources change perm
+        context['can_change_resources'] = self.request.user.has_perm(
+            "vm.change_resources")
+
+        # client info
+        context['client_download'] = self.request.COOKIES.get(
+            'downloaded_client')
+        # can link template
+        context['can_link_template'] = (
+            instance.template and instance.template.has_level(user, "operator")
+        )
+
+        return context
+
+    def post(self, request, *args, **kwargs):
+        options = {
+            'new_name': self.__set_name,
+            'new_description': self.__set_description,
+            'new_tag': self.__add_tag,
+            'to_remove': self.__remove_tag,
+            'port': self.__add_port,
+            'abort_operation': self.__abort_operation,
+        }
+        for k, v in options.iteritems():
+            if request.POST.get(k) is not None:
+                return v(request)
+        raise Http404()
+
+    def __set_name(self, request):
+        self.object = self.get_object()
+        if not self.object.has_level(request.user, 'owner'):
+            raise PermissionDenied()
+        new_name = request.POST.get("new_name")
+        Instance.objects.filter(pk=self.object.pk).update(
+            **{'name': new_name})
+
+        success_message = _("VM successfully renamed.")
+        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)
+            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})
+
+        success_message = _("VM description successfully updated.")
+        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())
+
+    def __add_tag(self, request):
+        new_tag = request.POST.get('new_tag')
+        self.object = self.get_object()
+        if not self.object.has_level(request.user, 'owner'):
+            raise PermissionDenied()
+
+        if len(new_tag) < 1:
+            message = u"Please input something."
+        elif len(new_tag) > 20:
+            message = u"Tag name is too long."
+        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()
+            if not self.object.has_level(request.user, 'owner'):
+                raise PermissionDenied()
+
+            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"
+            )
+        else:
+            return redirect(reverse_lazy("dashboard.views.detail",
+                            kwargs={'pk': self.object.pk}))
+
+    def __add_port(self, request):
+        object = self.get_object()
+        if (not object.has_level(request.user, 'owner') or
+                not request.user.has_perm('vm.config_ports')):
+            raise PermissionDenied()
+
+        port = request.POST.get("port")
+        proto = request.POST.get("proto")
+
+        try:
+            error = None
+            interfaces = object.interface_set.all()
+            host = Host.objects.get(pk=request.POST.get("host_pk"),
+                                    interface__in=interfaces)
+            host.add_port(proto, private=port)
+        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()
+        except ValueError:
+            error = _("There is a problem with your input.")
+        except Exception as e:
+            error = _("Unknown error.")
+            logger.error(e)
+
+        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}))
+
+    def __abort_operation(self, request):
+        self.object = self.get_object()
+
+        activity = get_object_or_404(InstanceActivity,
+                                     pk=request.POST.get("activity"))
+        if not activity.is_abortable_for(request.user):
+            raise PermissionDenied()
+        activity.abort()
+        return redirect("%s#activity" % self.object.get_absolute_url())
+
+
+class VmTraitsUpdate(SuperuserRequiredMixin, UpdateView):
+    form_class = TraitsForm
+    model = Instance
+
+    def get_success_url(self):
+        return self.get_object().get_absolute_url() + "#resources"
+
+
+class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
+    form_class = RawDataForm
+    model = Instance
+    template_name = 'dashboard/vm-detail/raw_data.html'
+
+    def get_success_url(self):
+        return self.get_object().get_absolute_url() + "#resources"
+
+
+class VmOperationView(AjaxOperationMixin, OperationView):
+
+    model = Instance
+    context_object_name = 'instance'  # much simpler to mock object
+
+
+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 PermissionDenied as e:
+            logger.debug('Not showing operation %s for %s: %s',
+                         k, instance, unicode(e))
+        except Exception:
+            ops.append(v.bind_to_object(instance, disabled=True))
+        else:
+            ops.append(v.bind_to_object(instance))
+    return ops
+
+
+class VmAddInterfaceView(FormOperationMixin, VmOperationView):
+
+    op = 'add_interface'
+    form_class = VmAddInterfaceForm
+    show_in_toolbar = False
+    icon = 'globe'
+    effect = 'success'
+    with_reload = True
+
+    def get_form_kwargs(self):
+        inst = self.get_op().instance
+        choices = Vlan.get_objects_with_level(
+            "user", self.request.user).exclude(
+            vm_interface__instance__in=[inst])
+        val = super(VmAddInterfaceView, self).get_form_kwargs()
+        val.update({'choices': choices})
+        return val
+
+
+class VmCreateDiskView(FormOperationMixin, VmOperationView):
+
+    op = 'create_disk'
+    form_class = VmCreateDiskForm
+    show_in_toolbar = False
+    icon = 'hdd-o'
+    effect = "success"
+    is_disk_operation = True
+
+
+class VmDownloadDiskView(FormOperationMixin, VmOperationView):
+
+    op = 'download_disk'
+    form_class = VmDownloadDiskForm
+    show_in_toolbar = False
+    icon = 'download'
+    effect = "success"
+    is_disk_operation = True
+
+
+class VmMigrateView(VmOperationView):
+
+    op = 'migrate'
+    icon = 'truck'
+    effect = 'info'
+    template_name = 'dashboard/_vm-migrate.html'
+
+    def get_context_data(self, **kwargs):
+        ctx = super(VmMigrateView, self).get_context_data(**kwargs)
+        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):
+        if extra is None:
+            extra = {}
+        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)
+
+
+class VmSaveView(FormOperationMixin, VmOperationView):
+
+    op = 'save_as_template'
+    icon = 'save'
+    effect = 'info'
+    form_class = VmSaveForm
+
+
+class VmResourcesChangeView(VmOperationView):
+    op = 'resources_change'
+    icon = "save"
+    show_in_toolbar = False
+    wait_for_result = 0.5
+
+    def post(self, request, extra=None, *args, **kwargs):
+        if extra is None:
+            extra = {}
+
+        instance = get_object_or_404(Instance, pk=kwargs['pk'])
+
+        form = VmResourcesForm(request.POST, instance=instance)
+        if not form.is_valid():
+            for f in form.errors:
+                messages.error(request, "<strong>%s</strong>: %s" % (
+                    f, form.errors[f].as_text()
+                ))
+            if request.is_ajax():  # this is not too nice
+                store = messages.get_messages(request)
+                store.used = True
+                return HttpResponse(
+                    json.dumps({'success': False,
+                                'messages': [unicode(m) for m in store]}),
+                    content_type="application=json"
+                )
+            else:
+                return redirect(instance.get_absolute_url() + "#resources")
+        else:
+            extra = form.cleaned_data
+            extra['max_ram_size'] = extra['ram_size']
+            return super(VmResourcesChangeView, self).post(request, extra,
+                                                           *args, **kwargs)
+
+
+class TokenOperationView(OperationView):
+    """Abstract operation view with token support.
+
+    User can do the action with a valid token instead of logging in.
+    """
+    token_max_age = 3 * 24 * 3600
+    redirect_exception_classes = (PermissionDenied, SuspiciousOperation, )
+
+    @classmethod
+    def get_salt(cls):
+        return unicode(cls)
+
+    @classmethod
+    def get_token(cls, instance, user):
+        t = tuple([getattr(i, 'pk', i) for i in [instance, user]])
+        return signing.dumps(t, salt=cls.get_salt(), compress=True)
+
+    @classmethod
+    def get_token_url(cls, instance, user):
+        key = cls.get_token(instance, user)
+        return cls.get_instance_url(instance.pk, key)
+
+    def check_auth(self):
+        if 'k' in self.request.GET:
+            try:  # check if token is needed at all
+                return super(TokenOperationView, self).check_auth()
+            except Exception:
+                op = self.get_op()
+                pk = op.instance.pk
+                key = self.request.GET.get('k')
+
+                logger.debug("checking token supplied to %s",
+                             self.request.get_full_path())
+                try:
+                    user = self.validate_key(pk, key)
+                except signing.SignatureExpired:
+                    messages.error(self.request, _('The token has expired.'))
+                else:
+                    logger.info("Request user changed to %s at %s",
+                                user, self.request.get_full_path())
+                    self.request.user = user
+                    self.request.token_user = True
+        else:
+            logger.debug("no token supplied to %s",
+                         self.request.get_full_path())
+
+        return super(TokenOperationView, self).check_auth()
+
+    def validate_key(self, pk, key):
+        """Get object based on signed token.
+        """
+        try:
+            data = signing.loads(key, salt=self.get_salt())
+            logger.debug('Token data: %s', unicode(data))
+            instance, user = data
+            logger.debug('Extracted token data: instance: %s, user: %s',
+                         unicode(instance), unicode(user))
+        except (signing.BadSignature, ValueError, TypeError) as e:
+            logger.warning('Tried invalid token. Token: %s, user: %s. %s',
+                           key, unicode(self.request.user), unicode(e))
+            raise SuspiciousOperation()
+
+        try:
+            instance, user = signing.loads(key, max_age=self.token_max_age,
+                                           salt=self.get_salt())
+            logger.debug('Extracted non-expired token data: %s, %s',
+                         unicode(instance), unicode(user))
+        except signing.BadSignature as e:
+            raise signing.SignatureExpired()
+
+        if pk != instance:
+            logger.debug('pk (%d) != instance (%d)', pk, instance)
+            raise SuspiciousOperation()
+        user = User.objects.get(pk=user)
+        return user
+
+
+class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
+
+    op = 'renew'
+    icon = 'calendar'
+    effect = 'info'
+    show_in_toolbar = False
+    form_class = VmRenewForm
+    wait_for_result = 0.5
+
+    def get_form_kwargs(self):
+        choices = Lease.get_objects_with_level("user", self.request.user)
+        default = self.get_op().instance.lease
+        if default and default not in choices:
+            choices = (choices.distinct() |
+                       Lease.objects.filter(pk=default.pk).distinct())
+
+        val = super(VmRenewView, self).get_form_kwargs()
+        val.update({'choices': choices, 'default': default})
+        return val
+
+    def get_response_data(self, result, done, extra=None, **kwargs):
+        extra = super(VmRenewView, self).get_response_data(result, done,
+                                                           extra, **kwargs)
+        extra["new_suspend_time"] = unicode(self.get_op().
+                                            instance.time_of_suspend)
+        return extra
+
+
+class VmStateChangeView(FormOperationMixin, VmOperationView):
+    op = 'emergency_change_state'
+    icon = 'legal'
+    effect = 'danger'
+    show_in_toolbar = True
+    form_class = VmStateChangeForm
+    wait_for_result = 0.5
+
+    def get_form_kwargs(self):
+        inst = self.get_op().instance
+        active_activities = InstanceActivity.objects.filter(
+            finished__isnull=True, instance=inst)
+        show_interrupt = active_activities.exists()
+        val = super(VmStateChangeView, self).get_form_kwargs()
+        val.update({'show_interrupt': show_interrupt, 'status': inst.status})
+        return val
+
+
+vm_ops = OrderedDict([
+    ('deploy', VmOperationView.factory(
+        op='deploy', icon='play', effect='success')),
+    ('wake_up', VmOperationView.factory(
+        op='wake_up', icon='sun-o', effect='success')),
+    ('sleep', VmOperationView.factory(
+        extra_bases=[TokenOperationView],
+        op='sleep', icon='moon-o', effect='info')),
+    ('migrate', VmMigrateView),
+    ('save_as_template', VmSaveView),
+    ('reboot', VmOperationView.factory(
+        op='reboot', icon='refresh', effect='warning')),
+    ('reset', VmOperationView.factory(
+        op='reset', icon='bolt', effect='warning')),
+    ('shutdown', VmOperationView.factory(
+        op='shutdown', icon='power-off', effect='warning')),
+    ('shut_off', VmOperationView.factory(
+        op='shut_off', icon='ban', effect='warning')),
+    ('recover', VmOperationView.factory(
+        op='recover', icon='medkit', effect='warning')),
+    ('nostate', VmStateChangeView),
+    ('destroy', VmOperationView.factory(
+        extra_bases=[TokenOperationView],
+        op='destroy', icon='times', effect='danger')),
+    ('create_disk', VmCreateDiskView),
+    ('download_disk', VmDownloadDiskView),
+    ('add_interface', VmAddInterfaceView),
+    ('renew', VmRenewView),
+    ('resources_change', VmResourcesChangeView),
+    ('password_reset', VmOperationView.factory(
+        op='password_reset', icon='unlock', effect='warning',
+        show_in_toolbar=False, wait_for_result=0.5, with_reload=True)),
+    ('mount_store', VmOperationView.factory(
+        op='mount_store', icon='briefcase', effect='info',
+        show_in_toolbar=False,
+    )),
+])
+
+
+def _get_activity_icon(act):
+    op = act.get_operation()
+    if op and op.id in vm_ops:
+        return vm_ops[op.id].icon
+    else:
+        return "cog"
+
+
+def _format_activities(acts):
+    for i in acts:
+        i.icon = _get_activity_icon(i)
+    return acts
+
+
+class MassOperationView(OperationView):
+    template_name = 'dashboard/mass-operate.html'
+
+    def check_auth(self):
+        self.get_op().check_perms(self.request.user)
+        for i in self.get_object():
+            if not i.has_level(self.request.user, "user"):
+                raise PermissionDenied(
+                    "You have no user access to instance %d" % i.pk)
+
+    @classmethod
+    def get_urlname(cls):
+        return 'dashboard.vm.mass-op.%s' % cls.op
+
+    @classmethod
+    def get_url(cls):
+        return reverse("dashboard.vm.mass-op.%s" % cls.op)
+
+    def get_op(self, instance=None):
+        if instance:
+            return getattr(instance, self.op)
+        else:
+            return Instance._ops[self.op]
+
+    def get_context_data(self, **kwargs):
+        ctx = super(MassOperationView, self).get_context_data(**kwargs)
+        instances = self.get_object()
+        ctx['instances'] = self._get_operable_instances(
+            instances, self.request.user)
+        ctx['vm_count'] = sum(1 for i in ctx['instances'] if not i.disabled)
+        return ctx
+
+    def _call_operations(self, extra):
+        request = self.request
+        user = request.user
+        instances = self.get_object()
+        for i in instances:
+            try:
+                self.get_op(i).async(user=user, **extra)
+            except HumanReadableException as e:
+                e.send_message(request)
+            except Exception as e:
+                # pre-existing errors should have been catched when the
+                # confirmation dialog was constructed
+                messages.error(request, _(
+                    "Failed to execute %(op)s operation on "
+                    "instance %(instance)s.") % {"op": self.name,
+                                                 "instance": i})
+
+    def get_object(self):
+        vms = getattr(self.request, self.request.method).getlist("vm")
+        return Instance.objects.filter(pk__in=vms)
+
+    def _get_operable_instances(self, instances, user):
+        for i in instances:
+            try:
+                op = self.get_op(i)
+                op.check_auth(user)
+                op.check_precond()
+            except PermissionDenied as e:
+                i.disabled = create_readable(
+                    _("You are not permitted to execute %(op)s on instance "
+                      "%(instance)s."), instance=i.pk, op=self.name)
+                i.disabled_icon = "lock"
+            except Exception as e:
+                i.disabled = fetch_human_exception(e)
+            else:
+                i.disabled = None
+        return instances
+
+    def post(self, request, extra=None, *args, **kwargs):
+        self.check_auth()
+        if extra is None:
+            extra = {}
+        self._call_operations(extra)
+        if request.is_ajax():
+            store = messages.get_messages(request)
+            store.used = True
+            return HttpResponse(
+                json.dumps({'messages': [unicode(m) for m in store]}),
+                content_type="application/json"
+            )
+        else:
+            return redirect(reverse("dashboard.views.vm-list"))
+
+    @classmethod
+    def factory(cls, vm_op, extra_bases=(), **kwargs):
+        return type(str(cls.__name__ + vm_op.op),
+                    tuple(list(extra_bases) + [cls, vm_op]), kwargs)
+
+
+class MassMigrationView(MassOperationView, VmMigrateView):
+    template_name = 'dashboard/_vm-mass-migrate.html'
+
+
+vm_mass_ops = OrderedDict([
+    ('deploy', MassOperationView.factory(vm_ops['deploy'])),
+    ('wake_up', MassOperationView.factory(vm_ops['wake_up'])),
+    ('sleep', MassOperationView.factory(vm_ops['sleep'])),
+    ('reboot', MassOperationView.factory(vm_ops['reboot'])),
+    ('reset', MassOperationView.factory(vm_ops['reset'])),
+    ('shut_off', MassOperationView.factory(vm_ops['shut_off'])),
+    ('migrate', MassMigrationView),
+    ('destroy', MassOperationView.factory(vm_ops['destroy'])),
+])
+
+
+class VmList(LoginRequiredMixin, FilterMixin, ListView):
+    template_name = "dashboard/vm-list.html"
+    allowed_filters = {
+        'name': "name__icontains",
+        'node': "node__name__icontains",
+        'status': "status__iexact",
+        'tags[]': "tags__name__in",
+        'tags': "tags__name__in",  # for search string
+        'owner': "owner__username",
+        'template': "template__pk",
+    }
+
+    def get_context_data(self, *args, **kwargs):
+        context = super(VmList, self).get_context_data(*args, **kwargs)
+        context['ops'] = []
+        for k, v in vm_mass_ops.iteritems():
+            try:
+                v.check_perms(user=self.request.user)
+            except PermissionDenied:
+                pass
+            else:
+                context['ops'].append(v)
+        context['search_form'] = self.search_form
+        context['show_acts_in_progress'] = self.object_list.count() < 100
+        return context
+
+    def get(self, *args, **kwargs):
+        if self.request.is_ajax():
+            return self._create_ajax_request()
+        else:
+            self.search_form = VmListSearchForm(self.request.GET)
+            self.search_form.full_clean()
+            return super(VmList, self).get(*args, **kwargs)
+
+    def _create_ajax_request(self):
+        if self.request.GET.get("compact") is not None:
+            instances = Instance.get_objects_with_level(
+                "user", self.request.user).filter(destroyed_at=None)
+            statuses = {}
+            for i in instances:
+                statuses[i.pk] = {
+                    'status': i.get_status_display(),
+                    'icon': i.get_status_icon(),
+                    'in_status_change': i.is_in_status_change(),
+                }
+                if self.request.user.is_superuser:
+                    statuses[i.pk]['node'] = i.node.name if i.node else "-"
+            return HttpResponse(json.dumps(statuses),
+                                content_type="application/json")
+        else:
+            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(
+                destroyed_at=None).all()
+            instances = [{
+                'pk': i.pk,
+                'name': i.name,
+                'icon': i.get_status_icon(),
+                'host': i.short_hostname,
+                'status': i.get_status_display(),
+                'fav': i.pk in favs,
+            } for i in instances]
+            return HttpResponse(
+                json.dumps(list(instances)),  # instances is ValuesQuerySet
+                content_type="application/json",
+            )
+
+    def create_acl_queryset(self, model):
+        queryset = super(VmList, self).create_acl_queryset(model)
+        if not self.search_form.cleaned_data.get("include_deleted"):
+            queryset = queryset.filter(destroyed_at=None)
+        return queryset
+
+    def get_queryset(self):
+        logger.debug('VmList.get_queryset() called. User: %s',
+                     unicode(self.request.user))
+        queryset = self.create_acl_queryset(Instance)
+
+        self.create_fake_get()
+        sort = self.request.GET.get("sort")
+        # remove "-" that means descending order
+        # also check if the column name is valid
+        if (sort and
+            (sort[1:] if sort[0] == "-" else sort)
+                in [i.name for i in Instance._meta.fields] + ["pk"]):
+            queryset = queryset.order_by(sort)
+
+        return queryset.filter(
+            **self.get_queryset_filters()).prefetch_related(
+                "owner", "node", "owner__profile", "interface_set", "lease",
+                "interface_set__host").distinct()
+
+
+class VmCreate(LoginRequiredMixin, TemplateView):
+
+    form_class = VmCustomizeForm
+    form = None
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/modal-wrapper.html']
+        else:
+            return ['dashboard/nojs-wrapper.html']
+
+    def get(self, request, form=None, *args, **kwargs):
+        if not request.user.has_perm('vm.create_vm'):
+            raise PermissionDenied()
+
+        if form is None:
+            template_pk = request.GET.get("template")
+        else:
+            template_pk = form.template.pk
+
+        if template_pk:
+            template = get_object_or_404(InstanceTemplate, pk=template_pk)
+            if not template.has_level(request.user, 'user'):
+                raise PermissionDenied()
+            if form is None:
+                form = self.form_class(user=request.user, template=template)
+        else:
+            templates = InstanceTemplate.get_objects_with_level(
+                'user', request.user, disregard_superuser=True)
+
+        context = self.get_context_data(**kwargs)
+        if template_pk:
+            context.update({
+                'template': 'dashboard/_vm-create-2.html',
+                'box_title': _('Customize VM'),
+                'ajax_title': True,
+                'vm_create_form': form,
+                'template_o': template,
+            })
+        else:
+            context.update({
+                'template': 'dashboard/_vm-create-1.html',
+                'box_title': _('Create a VM'),
+                'ajax_title': True,
+                'templates': templates.all(),
+            })
+        return self.render_to_response(context)
+
+    def __create_normal(self, request, *args, **kwargs):
+        user = request.user
+        template = InstanceTemplate.objects.get(
+            pk=request.POST.get("template"))
+
+        # permission check
+        if not template.has_level(request.user, 'user'):
+            raise PermissionDenied()
+
+        args = {"template": template, "owner": user}
+        instances = [Instance.create_from_template(**args)]
+        return self.__deploy(request, instances)
+
+    def __create_customized(self, request, *args, **kwargs):
+        user = request.user
+        # no form yet, using POST directly:
+        template = get_object_or_404(InstanceTemplate,
+                                     pk=request.POST.get("template"))
+        form = self.form_class(
+            request.POST, user=request.user, template=template)
+        if not form.is_valid():
+            return self.get(request, form, *args, **kwargs)
+        post = form.cleaned_data
+
+        if not template.has_level(user, 'user'):
+            raise PermissionDenied()
+
+        ikwargs = {
+            'name': post['name'],
+            'template': template,
+            'owner': user,
+        }
+        amount = post.get("amount", 1)
+        if request.user.has_perm('vm.set_resources'):
+            networks = [InterfaceTemplate(vlan=l, managed=l.managed)
+                        for l in post['networks']]
+
+            ikwargs.update({
+                'num_cores': post['cpu_count'],
+                'ram_size': post['ram_size'],
+                'priority': post['cpu_priority'],
+                'max_ram_size': post['ram_size'],
+                'networks': networks,
+                'disks': list(template.disks.all()),
+            })
+
+        else:
+            pass
+
+        instances = Instance.mass_create_from_template(amount=amount,
+                                                       **ikwargs)
+        return self.__deploy(request, instances)
+
+    def __deploy(self, request, instances, *args, **kwargs):
+        for i in instances:
+            i.deploy.async(user=request.user)
+
+        if len(instances) > 1:
+            messages.success(request, ungettext_lazy(
+                "Successfully created %(count)d VM.",  # this should not happen
+                "Successfully created %(count)d VMs.", len(instances)) % {
+                'count': len(instances)})
+            path = "%s?stype=owned" % reverse("dashboard.views.vm-list")
+        else:
+            messages.success(request, _("VM successfully created."))
+            path = instances[0].get_absolute_url()
+
+        if request.is_ajax():
+            return HttpResponse(json.dumps({'redirect': path}),
+                                content_type="application/json")
+        else:
+            return redirect("%s#activity" % path)
+
+    def post(self, request, *args, **kwargs):
+        user = request.user
+
+        if not request.user.has_perm('vm.create_vm'):
+            raise PermissionDenied()
+
+        # limit chekcs
+        try:
+            limit = user.profile.instance_limit
+        except Exception as e:
+            logger.debug('No profile or instance limit: %s', e)
+        else:
+            try:
+                amount = int(request.POST.get("amount", 1))
+            except:
+                amount = limit  # TODO this should definitely use a Form
+            current = Instance.active.filter(owner=user).count()
+            logger.debug('current use: %d, limit: %d', current, limit)
+            if current + amount > limit:
+                messages.error(request,
+                               _('Instance limit (%d) exceeded.') % limit)
+                if request.is_ajax():
+                    return HttpResponse(json.dumps({'redirect': '/'}),
+                                        content_type="application/json")
+                else:
+                    return redirect('/')
+
+        create_func = (self.__create_normal if
+                       request.POST.get("customized") is None else
+                       self.__create_customized)
+
+        return create_func(request, *args, **kwargs)
+
+
+class VmGraphView(GraphViewBase):
+    metrics = {
+        'cpu': ('cactiStyle(alias(nonNegativeDerivative(%(prefix)s.cpu.usage),'
+                '"cpu usage (%%)"))'),
+        'memory': ('cactiStyle(alias(%(prefix)s.memory.usage,'
+                   '"memory usage (%%)"))'),
+        'network': (
+            'group('
+            'aliasSub(nonNegativeDerivative(%(prefix)s.network.bytes_recv*),'
+            ' ".*-(\d+)\\)", "out (vlan \\1)"),'
+            'aliasSub(nonNegativeDerivative(%(prefix)s.network.bytes_sent*),'
+            ' ".*-(\d+)\\)", "in (vlan \\1)"))'),
+    }
+    model = Instance
+
+    def get_prefix(self, instance):
+        return 'vm.%s' % instance.vm_name
+
+    def get_title(self, instance, metric):
+        return '%s (%s) - %s' % (instance.name, instance.vm_name, metric)
+
+
+@require_GET
+def get_vm_screenshot(request, pk):
+    instance = get_object_or_404(Instance, pk=pk)
+    try:
+        image = instance.screenshot(user=request.user).getvalue()
+    except:
+        # TODO handle this better
+        raise Http404()
+
+    return HttpResponse(image, mimetype="image/png")
+
+
+class InterfaceDeleteView(DeleteView):
+    model = Interface
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/confirm/ajax-delete.html']
+        else:
+            return ['dashboard/confirm/base-delete.html']
+
+    def get_context_data(self, **kwargs):
+        context = super(InterfaceDeleteView, self).get_context_data(**kwargs)
+        interface = self.get_object()
+        context['text'] = _("Are you sure you want to remove this interface "
+                            "from <strong>%(vm)s</strong>?" %
+                            {'vm': interface.instance.name})
+        return context
+
+    def delete(self, request, *args, **kwargs):
+        self.object = self.get_object()
+        instance = self.object.instance
+
+        if not instance.has_level(request.user, "owner"):
+            raise PermissionDenied()
+
+        instance.remove_interface(interface=self.object, user=request.user)
+        success_url = self.get_success_url()
+        success_message = _("Interface successfully deleted.")
+
+        if request.is_ajax():
+            return HttpResponse(
+                json.dumps(
+                    {'message': success_message,
+                     'removed_network': {
+                         'vlan': self.object.vlan.name,
+                         'vlan_pk': self.object.vlan.pk,
+                         'managed': self.object.host is not None,
+                     }}),
+                content_type="application/json",
+            )
+        else:
+            messages.success(request, success_message)
+            return redirect("%s#network" % success_url)
+
+    def get_success_url(self):
+        redirect = self.request.POST.get("next")
+        if redirect:
+            return redirect
+        self.object.instance.get_absolute_url()
+
+
+class InstanceActivityDetail(CheckedDetailView):
+    model = InstanceActivity
+    context_object_name = 'instanceactivity'  # much simpler to mock object
+    template_name = 'dashboard/instanceactivity_detail.html'
+
+    def get_has_level(self):
+        return self.object.instance.has_level
+
+    def get_context_data(self, **kwargs):
+        ctx = super(InstanceActivityDetail, self).get_context_data(**kwargs)
+        ctx['activities'] = _format_activities(
+            self.object.instance.get_activities(self.request.user))
+        ctx['icon'] = _get_activity_icon(self.object)
+        return ctx
+
+
+class DiskRemoveView(DeleteView):
+    model = Disk
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/confirm/ajax-delete.html']
+        else:
+            return ['dashboard/confirm/base-delete.html']
+
+    def get_context_data(self, **kwargs):
+        context = super(DiskRemoveView, self).get_context_data(**kwargs)
+        disk = self.get_object()
+        app = disk.get_appliance()
+        context['title'] = _("Disk remove confirmation")
+        context['text'] = _("Are you sure you want to remove "
+                            "<strong>%(disk)s</strong> from "
+                            "<strong>%(app)s</strong>?" % {'disk': disk,
+                                                           'app': app}
+                            )
+        return context
+
+    def delete(self, request, *args, **kwargs):
+        disk = self.get_object()
+        app = disk.get_appliance()
+
+        if not app.has_level(request.user, 'owner'):
+            raise PermissionDenied()
+
+        app.remove_disk(disk=disk, user=request.user)
+        disk.destroy()
+
+        next_url = request.POST.get("next")
+        success_url = next_url if next_url else app.get_absolute_url()
+        success_message = _("Disk successfully removed.")
+
+        if request.is_ajax():
+            return HttpResponse(
+                json.dumps({'message': success_message}),
+                content_type="application/json",
+            )
+        else:
+            messages.success(request, success_message)
+            return redirect("%s#resources" % success_url)
+
+
+@require_GET
+def get_disk_download_status(request, pk):
+    disk = Disk.objects.get(pk=pk)
+    if not disk.get_appliance().has_level(request.user, 'owner'):
+        raise PermissionDenied()
+
+    return HttpResponse(
+        json.dumps({
+            'percentage': disk.get_download_percentage(),
+            'failed': disk.failed
+        }),
+        content_type="application/json",
+    )
+
+
+class PortDelete(LoginRequiredMixin, DeleteView):
+    model = Rule
+    pk_url_kwarg = 'rule'
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/confirm/ajax-delete.html']
+        else:
+            return ['dashboard/confirm/base-delete.html']
+
+    def get_context_data(self, **kwargs):
+        context = super(PortDelete, self).get_context_data(**kwargs)
+        rule = kwargs.get('object')
+        instance = rule.host.interface_set.get().instance
+        context['title'] = _("Port delete confirmation")
+        context['text'] = _("Are you sure you want to close %(port)d/"
+                            "%(proto)s on %(vm)s?" % {'port': rule.dport,
+                                                      'proto': rule.proto,
+                                                      'vm': instance})
+        return context
+
+    def delete(self, request, *args, **kwargs):
+        rule = Rule.objects.get(pk=kwargs.get("rule"))
+        instance = rule.host.interface_set.get().instance
+        if not instance.has_level(request.user, 'owner'):
+            raise PermissionDenied()
+
+        super(PortDelete, self).delete(request, *args, **kwargs)
+
+        success_url = self.get_success_url()
+        success_message = _("Port successfully removed.")
+
+        if request.is_ajax():
+            return HttpResponse(
+                json.dumps({'message': success_message}),
+                content_type="application/json",
+            )
+        else:
+            messages.success(request, success_message)
+            return redirect("%s#network" % success_url)
+
+    def get_success_url(self):
+        return reverse_lazy('dashboard.views.detail',
+                            kwargs={'pk': self.kwargs.get("pk")})
+
+
+class ClientCheck(LoginRequiredMixin, TemplateView):
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/_modal.html']
+        else:
+            return ['dashboard/nojs-wrapper.html']
+
+    def get_context_data(self, *args, **kwargs):
+        context = super(ClientCheck, self).get_context_data(*args, **kwargs)
+        context.update({
+            'box_title': _('About CIRCLE Client'),
+            'ajax_title': False,
+            'client_download_url': settings.CLIENT_DOWNLOAD_URL,
+            'template': "dashboard/_client-check.html",
+            'instance': get_object_or_404(
+                Instance, pk=self.request.GET.get('vm')),
+        })
+        if not context['instance'].has_level(self.request.user, 'operator'):
+            raise PermissionDenied()
+        return context
+
+    def post(self, request, *args, **kwargs):
+        instance = get_object_or_404(Instance, pk=request.POST.get('vm'))
+        if not instance.has_level(request.user, 'operator'):
+            raise PermissionDenied()
+        response = redirect(instance.get_absolute_url())
+        response.set_cookie('downloaded_client', 'True', 365 * 24 * 60 * 60)
+        return response
+
+
+@require_GET
+def vm_activity(request, pk):
+    instance = Instance.objects.get(pk=pk)
+    if not instance.has_level(request.user, 'user'):
+        raise PermissionDenied()
+
+    response = {}
+    show_all = request.GET.get("show_all", "false") == "true"
+    activities = _format_activities(
+        instance.get_merged_activities(request.user))
+    show_show_all = len(activities) > 10
+    if not show_all:
+        activities = activities[:10]
+
+    response['connect_uri'] = instance.get_connect_uri()
+    response['human_readable_status'] = instance.get_status_display()
+    response['status'] = instance.status
+    response['icon'] = instance.get_status_icon()
+    latest = instance.get_latest_activity_in_progress()
+    response['is_new_state'] = (latest and latest.resultant_state is not None
+                                and instance.status != latest.resultant_state)
+
+    context = {
+        'instance': instance,
+        'activities': activities,
+        'show_show_all': show_show_all,
+        'ops': get_operations(instance, request.user),
+    }
+
+    response['activities'] = render_to_string(
+        "dashboard/vm-detail/_activity-timeline.html",
+        RequestContext(request, context),
+    )
+    response['ops'] = render_to_string(
+        "dashboard/vm-detail/_operations.html",
+        RequestContext(request, context),
+    )
+    response['disk_ops'] = render_to_string(
+        "dashboard/vm-detail/_disk-operations.html",
+        RequestContext(request, context),
+    )
+
+    return HttpResponse(
+        json.dumps(response),
+        content_type="application/json"
+    )
+
+
+class FavouriteView(TemplateView):
+
+    def post(self, *args, **kwargs):
+        user = self.request.user
+        vm = Instance.objects.get(pk=self.request.POST.get("vm"))
+        if not vm.has_level(user, 'user'):
+            raise PermissionDenied()
+        try:
+            Favourite.objects.get(instance=vm, user=user).delete()
+            return HttpResponse("Deleted.")
+        except Favourite.DoesNotExist:
+            Favourite(instance=vm, user=user).save()
+            return HttpResponse("Added.")
+
+
+class TransferOwnershipView(CheckedDetailView, DetailView):
+    model = Instance
+
+    def get_template_names(self):
+        if self.request.is_ajax():
+            return ['dashboard/_modal.html']
+        else:
+            return ['dashboard/nojs-wrapper.html']
+
+    def get_context_data(self, *args, **kwargs):
+        context = super(TransferOwnershipView, self).get_context_data(
+            *args, **kwargs)
+        context['form'] = TransferOwnershipForm()
+        context.update({
+            'box_title': _("Transfer ownership"),
+            'ajax_title': True,
+            'template': "dashboard/vm-detail/tx-owner.html",
+        })
+        return context
+
+    def post(self, request, *args, **kwargs):
+        form = TransferOwnershipForm(request.POST)
+        if not form.is_valid():
+            return self.get(request)
+        try:
+            new_owner = search_user(request.POST['name'])
+        except User.DoesNotExist:
+            messages.error(request, _('Can not find specified user.'))
+            return self.get(request, *args, **kwargs)
+        except KeyError:
+            raise SuspiciousOperation()
+
+        obj = self.get_object()
+        if not (obj.owner == request.user or
+                request.user.is_superuser):
+            raise PermissionDenied()
+
+        token = signing.dumps((obj.pk, new_owner.pk),
+                              salt=TransferOwnershipConfirmView.get_salt())
+        token_path = reverse(
+            'dashboard.views.vm-transfer-ownership-confirm', args=[token])
+        try:
+            new_owner.profile.notify(
+                ugettext_noop('Ownership offer'),
+                ugettext_noop('%(user)s offered you to take the ownership of '
+                              'his/her virtual machine called %(instance)s. '
+                              '<a href="%(token)s" '
+                              'class="btn btn-success btn-small">Accept</a>'),
+                {'instance': obj, 'token': token_path})
+        except Profile.DoesNotExist:
+            messages.error(request, _('Can not notify selected user.'))
+        else:
+            messages.success(request,
+                             _('User %s is notified about the offer.') % (
+                                 unicode(new_owner), ))
+
+        return redirect(reverse_lazy("dashboard.views.detail",
+                                     kwargs={'pk': obj.pk}))
+
+
+class TransferOwnershipConfirmView(LoginRequiredMixin, View):
+    """User can accept an ownership offer."""
+
+    max_age = 3 * 24 * 3600
+    success_message = _("Ownership successfully transferred to you.")
+
+    @classmethod
+    def get_salt(cls):
+        return unicode(cls)
+
+    def get(self, request, key, *args, **kwargs):
+        """Confirm ownership transfer based on token.
+        """
+        logger.debug('Confirm dialog for token %s.', key)
+        try:
+            instance, new_owner = self.get_instance(key, request.user)
+        except PermissionDenied:
+            messages.error(request, _('This token is for an other user.'))
+            raise
+        except SuspiciousOperation:
+            messages.error(request, _('This token is invalid or has expired.'))
+            raise PermissionDenied()
+        return render(request,
+                      "dashboard/confirm/base-transfer-ownership.html",
+                      dictionary={'instance': instance, 'key': key})
+
+    def post(self, request, key, *args, **kwargs):
+        """Really transfer ownership based on token.
+        """
+        instance, owner = self.get_instance(key, request.user)
+
+        old = instance.owner
+        with instance_activity(code_suffix='ownership-transferred',
+                               instance=instance, user=request.user):
+            instance.owner = request.user
+            instance.clean()
+            instance.save()
+        messages.success(request, self.success_message)
+        logger.info('Ownership of %s transferred from %s to %s.',
+                    unicode(instance), unicode(old), unicode(request.user))
+        if old.profile:
+            old.profile.notify(
+                ugettext_noop('Ownership accepted'),
+                ugettext_noop('Your ownership offer of %(instance)s has been '
+                              'accepted by %(user)s.'),
+                {'instance': instance})
+        return redirect(instance.get_absolute_url())
+
+    def get_instance(self, key, user):
+        """Get object based on signed token.
+        """
+        try:
+            instance, new_owner = (
+                signing.loads(key, max_age=self.max_age,
+                              salt=self.get_salt()))
+        except (signing.BadSignature, ValueError, TypeError) as e:
+            logger.error('Tried invalid token. Token: %s, user: %s. %s',
+                         key, unicode(user), unicode(e))
+            raise SuspiciousOperation()
+
+        try:
+            instance = Instance.objects.get(id=instance)
+        except Instance.DoesNotExist as e:
+            logger.error('Tried token to nonexistent instance %d. '
+                         'Token: %s, user: %s. %s',
+                         instance, key, unicode(user), unicode(e))
+            raise Http404()
+
+        if new_owner != user.pk:
+            logger.error('%s (%d) tried the token for %s. Token: %s.',
+                         unicode(user), user.pk, new_owner, key)
+            raise PermissionDenied()
+        return (instance, new_owner)
--
libgit2 0.26.0