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