# 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 itertools import chain from os import getenv import os 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, redirect_to_login from django.contrib.auth.decorators import login_required from django.contrib.messages import warning from django.contrib.messages.views import SuccessMessageMixin from django.core.exceptions import ( PermissionDenied, SuspiciousOperation, ) from django.core import signing from django.core.urlresolvers import reverse, reverse_lazy from django.db.models import Count 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 _ from django.utils.translation import ungettext as __ 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 django_sshkey.models import UserKey from .forms import ( CircleAuthenticationForm, HostForm, LeaseForm, MyProfileForm, NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm, UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm, VmSaveForm, UserKeyForm, CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm, ) from .tables import ( NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable, GroupListTable, UserKeyListTable ) 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 from dashboard import store_api 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 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()) 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).all()[:5] return context def get_vm_acl_data(obj): levels = obj.ACL_LEVELS users = obj.get_users_with_level() users = [{'user': u, 'level': l} for u, l in users] groups = obj.get_groups_with_level() groups = [{'group': g, 'level': l} for g, l in groups] return {'users': users, 'groups': groups, 'levels': levels, 'url': reverse('dashboard.views.vm-acl', args=[obj.pk])} def get_group_acl_data(obj): aclobj = obj.profile levels = aclobj.ACL_LEVELS users = aclobj.get_users_with_level() users = [{'user': u, 'level': l} for u, l in users] groups = aclobj.get_groups_with_level() groups = [{'group': g, 'level': l} for g, l in groups] return {'users': users, 'groups': groups, 'levels': levels, 'url': reverse('dashboard.views.group-acl', args=[obj.pk])} 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 self.object.node: with instance_activity(code_suffix='console-accessed', instance=self.object, user=request.user, concurrency_check=False): port = self.object.vnc_port host = str(self.object.node.host.ipv4) value = signing.dumps({'host': host, 'port': port}, key=getenv("PROXY_SECRET", 'asdasd')), return HttpResponse('vnc/?d=%s' % value) 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'] ops = get_operations(instance, self.request.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}, }) # activity data context['activities'] = self.object.get_activities(self.request.user) 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'] = get_vm_acl_data(instance) 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) return context def post(self, request, *args, **kwargs): if (request.POST.get('ram-size') and request.POST.get('cpu-count') and request.POST.get('cpu-priority')): return self.__set_resources(request) options = { 'change_password': self.__change_password, 'new_name': self.__set_name, 'new_description': self.__set_description, 'new_tag': self.__add_tag, 'to_remove': self.__remove_tag, 'port': self.__add_port, 'new_network_vlan': self.__new_network, 'abort_operation': self.__abort_operation, } for k, v in options.iteritems(): if request.POST.get(k) is not None: return v(request) def __change_password(self, request): self.object = self.get_object() if not self.object.has_level(request.user, 'owner'): raise PermissionDenied() self.object.change_password(user=request.user) messages.success(request, _("Password changed.")) if request.is_ajax(): return HttpResponse("Success.") else: return redirect(reverse_lazy("dashboard.views.detail", kwargs={'pk': self.object.pk})) def __set_resources(self, request): self.object = self.get_object() if not self.object.has_level(request.user, 'owner'): raise PermissionDenied() if not request.user.has_perm('vm.change_resources'): raise PermissionDenied() resources = { 'num_cores': request.POST.get('cpu-count'), 'ram_size': request.POST.get('ram-size'), 'max_ram_size': request.POST.get('ram-size'), # TODO: max_ram 'priority': request.POST.get('cpu-priority') } Instance.objects.filter(pk=self.object.pk).update(**resources) success_message = _("Resources successfully updated.") if request.is_ajax(): response = {'message': success_message} return HttpResponse( json.dumps(response), content_type="application/json" ) else: messages.success(request, success_message) return redirect(reverse_lazy("dashboard.views.detail", kwargs={'pk': self.object.pk})) 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 __new_network(self, request): self.object = self.get_object() if not self.object.has_level(request.user, 'owner'): raise PermissionDenied() vlan = get_object_or_404(Vlan, pk=request.POST.get("new_network_vlan")) if not vlan.has_level(request.user, 'user'): raise PermissionDenied() try: self.object.add_interface(vlan=vlan, user=request.user) messages.success(request, _("Successfully added new interface.")) except Exception, e: error = u' '.join(e.messages) messages.error(request, error) return redirect("%s#network" % reverse_lazy( "dashboard.views.detail", kwargs={'pk': self.object.pk})) 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 OperationView(DetailView): template_name = 'dashboard/operate.html' show_in_toolbar = True @property def name(self): return self.get_op().name @property def description(self): return self.get_op().description @classmethod def get_urlname(cls): return 'dashboard.vm.op.%s' % cls.op def get_url(self): return reverse(self.get_urlname(), args=(self.get_object().pk, )) def get_wrapper_template_name(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 def get_context_data(self, **kwargs): ctx = super(OperationView, self).get_context_data(**kwargs) ctx['op'] = self.get_op() ctx['url'] = self.request.path return ctx def get(self, request, *args, **kwargs): self.get_op().check_auth(request.user) response = super(OperationView, self).get(request, *args, **kwargs) response.render() response.content = render_to_string(self.get_wrapper_template_name(), {'body': response.content}) return response def post(self, request, extra=None, *args, **kwargs): self.object = self.get_object() if extra is None: extra = {} try: self.get_op().async(user=request.user, **extra) except Exception as e: messages.error(request, _('Could not start operation.')) logger.error(e) return redirect("%s#activity" % self.object.get_absolute_url()) @classmethod def factory(cls, op, icon='cog'): return type(str(cls.__name__ + op), (cls, ), {'op': op, 'icon': icon}) @classmethod def bind_to_object(cls, instance): v = cls() v.get_object = lambda: instance return v class VmOperationView(OperationView): model = Instance context_object_name = 'instance' # much simpler to mock object class FormOperationMixin(object): form_class = None 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) else: ctx['form'] = self.form_class() return ctx def post(self, request, extra=None, *args, **kwargs): if extra is None: extra = {} form = self.form_class(self.request.POST) if form.is_valid(): extra.update(form.cleaned_data) return super(FormOperationMixin, self).post( request, extra, *args, **kwargs) else: return self.get(request) class VmCreateDiskView(FormOperationMixin, VmOperationView): op = 'create_disk' form_class = VmCreateDiskForm show_in_toolbar = False icon = 'hdd' class VmDownloadDiskView(FormOperationMixin, VmOperationView): op = 'download_disk' form_class = VmDownloadDiskForm show_in_toolbar = False icon = 'download' class VmMigrateView(VmOperationView): op = 'migrate' icon = 'truck' 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' form_class = VmSaveForm vm_ops = { 'reset': VmOperationView.factory(op='reset', icon='bolt'), 'deploy': VmOperationView.factory(op='deploy', icon='play'), 'migrate': VmMigrateView, 'reboot': VmOperationView.factory(op='reboot', icon='refresh'), 'shut_off': VmOperationView.factory(op='shut_off', icon='ban-circle'), 'shutdown': VmOperationView.factory(op='shutdown', icon='off'), 'save_as_template': VmSaveView, 'destroy': VmOperationView.factory(op='destroy', icon='remove'), 'sleep': VmOperationView.factory(op='sleep', icon='moon'), 'wake_up': VmOperationView.factory(op='wake_up', icon='sun'), 'create_disk': VmCreateDiskView, 'download_disk': VmDownloadDiskView, } 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 Exception as e: logger.debug('Not showing operation %s for %s: %s', k, instance, unicode(e)) else: ops.append(v.bind_to_object(instance)) return ops 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) instances = Instance.active.filter(node=self.object) context['table'] = NodeVmListTable(instances) 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['acl'] = get_group_acl_data(self.object) context['group_profile_form'] = GroupProfileUpdate.get_form_object( self.request, self.object.profile) 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('list-new-name'): return self.__add_user(request) if request.POST.get('list-new-namelist'): return self.__add_list(request) if (request.POST.get('list-new-name') is not None) and \ (request.POST.get('list-new-namelist') is not None): return redirect(reverse_lazy("dashboard.views.group-detail", kwargs={'pk': self.get_object().pk})) def __add_user(self, request): name = request.POST['list-new-name'] 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 = User.objects.get(username=name) self.object.user_set.add(entity) except User.DoesNotExist: warning(request, _('User "%s" not found.') % name) def __add_list(self, request): if not self.get_has_level()(request.user, 'operator'): raise PermissionDenied() userlist = request.POST.get('list-new-namelist').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 AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin): def post(self, request, *args, **kwargs): instance = self.get_object() if not (instance.has_level(request.user, "owner") or getattr(instance, 'owner', None) == request.user): logger.warning('Tried to set permissions of %s by non-owner %s.', unicode(instance), unicode(request.user)) raise PermissionDenied() self.set_levels(request, instance) self.remove_levels(request, instance) self.add_levels(request, instance) return redirect("%s#access" % instance.get_absolute_url()) def set_levels(self, request, instance): for key, value in request.POST.items(): m = re.match('perm-([ug])-(\d+)', key) if m: typ, id = m.groups() entity = {'u': User, 'g': Group}[typ].objects.get(id=id) if getattr(instance, "owner", None) == entity: logger.info("Tried to set owner's acl level for %s by %s.", unicode(instance), unicode(request.user)) continue instance.set_level(entity, value) logger.info("Set %s's acl level for %s to %s by %s.", unicode(entity), unicode(instance), value, unicode(request.user)) def remove_levels(self, request, instance): for key, value in request.POST.items(): if key.startswith("remove"): typ = key[7:8] # len("remove-") id = key[9:] # len("remove-x-") entity = {'u': User, 'g': Group}[typ].objects.get(id=id) if getattr(instance, "owner", None) == entity: logger.info("Tried to remove owner from %s by %s.", unicode(instance), unicode(request.user)) msg = _("The original owner cannot be removed, however " "you can transfer ownership.") messages.warning(request, msg) continue instance.set_level(entity, None) logger.info("Revoked %s's access to %s by %s.", unicode(entity), unicode(instance), unicode(request.user)) def add_levels(self, request, instance): name = request.POST['perm-new-name'] value = request.POST['perm-new'] if not name: return try: entity = User.objects.get(username=name) except User.DoesNotExist: entity = None try: entity = Group.objects.get(name=name) except Group.DoesNotExist: warning(request, _('User or group "%s" not found.') % name) return instance.set_level(entity, value) logger.info("Set %s's new acl level for %s to %s by %s.", unicode(entity), unicode(instance), value, unicode(request.user)) class TemplateAclUpdateView(AclUpdateView): model = InstanceTemplate def post(self, request, *args, **kwargs): template = self.get_object() if not (template.has_level(request.user, "owner") or getattr(template, 'owner', None) == request.user): logger.warning('Tried to set permissions of %s by non-owner %s.', unicode(template), unicode(request.user)) raise PermissionDenied() name = request.POST['perm-new-name'] if (User.objects.filter(username=name).count() + Group.objects.filter(name=name).count() < 1 and len(name) > 0): warning(request, _('User or group "%s" not found.') % name) else: self.set_levels(request, template) self.add_levels(request, template) self.remove_levels(request, template) post_for_disk = request.POST.copy() post_for_disk['perm-new'] = 'user' request.POST = post_for_disk for d in template.disks.all(): self.set_levels(request, d) self.add_levels(request, d) self.remove_levels(request, d) return redirect(template) class GroupAclUpdateView(AclUpdateView): model = Group def post(self, request, *args, **kwargs): instance = self.get_object().profile if not (instance.has_level(request.user, "owner") or getattr(instance, 'owner', None) == request.user): logger.warning('Tried to set permissions of %s by non-owner %s.', unicode(instance), unicode(request.user)) raise PermissionDenied() self.set_levels(request, instance) self.add_levels(request, instance) return redirect(reverse("dashboard.views.group-detail", kwargs=self.kwargs)) class TemplateChoose(TemplateView): def get_template_names(self): if self.request.is_ajax(): return ['dashboard/modal-wrapper.html'] else: return ['dashboard/nojs-wrapper.html'] def get_context_data(self, *args, **kwargs): context = super(TemplateChoose, self).get_context_data(*args, **kwargs) templates = InstanceTemplate.get_objects_with_level("user", self.request.user) context.update({ 'box_title': _('Choose template'), 'ajax_title': False, '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) 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) context.update({ 'box_title': _("Create a new base VM"), 'template': "dashboard/_template-create.html", 'leases': Lease.objects.count() }) return context def get(self, *args, **kwargs): if not self.request.user.has_perm('vm.create_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_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()) return super(TemplateCreate, self).post(self, request, args, kwargs) 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'] = get_vm_acl_data(obj) context['disks'] = obj.disks.all() 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() for disk in self.get_object().disks.all(): if not disk.has_level(request.user, 'user'): raise PermissionDenied() for network in self.get_object().interface_set.all(): if not network.vlan.has_level(request.user, "user"): 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, SingleTableView): template_name = "dashboard/template-list.html" model = InstanceTemplate table_class = TemplateListTable table_pagination = False def get_context_data(self, *args, **kwargs): context = super(TemplateList, self).get_context_data(*args, **kwargs) context['lease_table'] = LeaseListTable(Lease.objects.all(), request=self.request) return context def get_queryset(self): logger.debug('TemplateList.get_queryset() called. User: %s', unicode(self.request.user)) return InstanceTemplate.get_objects_with_level( 'user', self.request.user).all() 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", } def get(self, *args, **kwargs): if self.request.is_ajax(): 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': "" if not i.primary_host else i.primary_host.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", ) else: return super(VmList, self).get(*args, **kwargs) def get_queryset(self): logger.debug('VmList.get_queryset() called. User: %s', unicode(self.request.user)) queryset = Instance.get_objects_with_level( 'user', self.request.user).filter(destroyed_at=None) 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()).select_related('owner', 'node' ).distinct() def create_fake_get(self): """ Updates the 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} """ s = self.request.GET.get("s") 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 fake = self.request.GET.copy() for k, v in got.iteritems(): fake[k] = v self.request.GET = fake 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' 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["member_pk"]) 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 GroupRemoveAclUserView(GroupRemoveUserView): def remove_member(self, pk): container = self.get_object().profile container.set_level(User.objects.get(pk=pk), None) def get_success_message(self): return _("Acl user successfully removed from group.") class GroupRemoveAclGroupView(GroupRemoveUserView): def get_context_data(self, **kwargs): context = super(GroupRemoveUserView, self).get_context_data(**kwargs) try: context['member'] = Group.objects.get(pk=self.member_pk) except User.DoesNotExist: raise Http404() return context def remove_member(self, pk): container = self.get_object().profile container.set_level(Group.objects.get(pk=pk), None) def get_success_message(self): return _("Acl group 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): form_error = form is not None template = (form.template.pk if form_error else request.GET.get("template")) templates = InstanceTemplate.get_objects_with_level('user', request.user) if form is None and template: form = self.form_class(user=request.user, template=templates.get(pk=template)) context = self.get_context_data(**kwargs) if template: context.update({ 'template': 'dashboard/_vm-create-2.html', 'box_title': _('Customize VM'), 'ajax_title': False, 'vm_create_form': form, 'template_o': templates.get(pk=template), }) else: context.update({ 'template': 'dashboard/_vm-create-1.html', 'box_title': _('Create a VM'), 'ajax_title': False, '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() instances = [Instance.create_from_template( template=template, owner=user)] return self.__deploy(request, instances) def __create_customized(self, request, *args, **kwargs): user = request.user form = self.form_class( request.POST, user=request.user, template=InstanceTemplate.objects.get( pk=request.POST.get("template") ) ) if not form.is_valid(): return self.get(request, form, *args, **kwargs) post = form.cleaned_data template = InstanceTemplate.objects.get(pk=post['template']) # permission check if not template.has_level(user, 'user'): raise PermissionDenied() if request.user.has_perm('vm.set_resources'): ikwargs = { 'name': post['name'], 'num_cores': post['cpu_count'], 'ram_size': post['ram_size'], 'priority': post['cpu_priority'], } networks = [InterfaceTemplate(vlan=l, managed=l.managed) for l in post['networks']] disks = post['disks'] ikwargs.update({ 'template': template, 'owner': user, 'networks': networks, 'disks': disks, }) amount = post['amount'] instances = Instance.mass_create_from_template(amount=amount, **ikwargs) return self.__deploy(request, instances) else: raise PermissionDenied() def __deploy(self, request, instances, *args, **kwargs): for i in instances: i.deploy.async(user=request.user) if len(instances) > 1: messages.success(request, __( "Successfully created %(count)d VM.", # this should not happen "Successfully created %(count)d VMs.", len(instances)) % { 'count': len(instances)}) path = reverse("dashboard.index") 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 # limit chekcs try: limit = user.profile.instance_limit except Exception as e: logger.debug('No profile or instance limit: %s', e) else: current = Instance.active.filter(owner=user).count() logger.debug('current use: %d, limit: %d', current, limit) if limit < current: 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, }) 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 VmDelete(LoginRequiredMixin, DeleteView): model = Instance 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'] def get_success_url(self): next = self.request.POST.get('next') if next: return next else: return reverse_lazy('dashboard.index') def get_context_data(self, **kwargs): object = self.get_object() if not object.has_level(self.request.user, 'owner'): raise PermissionDenied() context = super(VmDelete, self).get_context_data(**kwargs) return context # 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.has_level(request.user, 'owner'): raise PermissionDenied() object.destroy.async(user=request.user) success_url = self.get_success_url() success_message = _("VM 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) 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 VmMassDelete(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): vms = request.GET.getlist('v[]') objects = Instance.objects.filter(pk__in=vms) return render(request, "dashboard/confirm/mass-delete.html", {'objects': objects}) def post(self, request, *args, **kwargs): vms = request.POST.getlist('vms') names = [] if vms is not None: for i in Instance.objects.filter(pk__in=vms): if not i.has_level(request.user, 'owner'): logger.info('Tried to delete instance #%d without owner ' 'permission by %s.', i.pk, unicode(request.user)) # no need for rollback or proper error message, this can't # normally happen: raise PermissionDenied() try: i.destroy.async(user=request.user) names.append(i.name) except Exception as e: logger.error(e) success_message = __( "Mass delete complete, the following VM was deleted: %s.", "Mass delete complete, the following VMs were deleted: %s.", len(names)) % u', '.join(names) # we can get this only via AJAX ... if request.is_ajax(): return HttpResponse( json.dumps({'message': success_message}), content_type="application/json" ) else: messages.success(request, success_message) next = request.GET.get('next') return redirect(next if next else reverse_lazy('dashboard.index')) class LeaseCreate(LoginRequiredMixin, SuperuserRequiredMixin, SuccessMessageMixin, CreateView): model = Lease form_class = LeaseForm 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 LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin, SuccessMessageMixin, UpdateView): model = Lease form_class = LeaseForm template_name = "dashboard/lease-edit.html" success_message = _("Successfully modified lease.") 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, 'owner'): raise PermissionDenied() response = {} only_status = request.GET.get("only_status", "false") response['human_readable_status'] = instance.get_status_display() response['status'] = instance.status response['icon'] = instance.get_status_icon() if only_status == "false": # instance activity context = { 'instance': instance, 'activities': instance.get_activities(request.user), '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), ) 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(LoginRequiredMixin, DetailView): model = Instance template_name = 'dashboard/vm-detail/tx-owner.html' def post(self, request, *args, **kwargs): 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( _('Ownership offer'), 'dashboard/notifications/ownership-offer.html', {'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 AbstractVmFunctionView(AccessMixin, View): """Abstract instance-action view. User can do the action with a valid token or if has at least required_level ACL level for the instance. Children should at least implement/add template_name, success_message, url_name, and do_action(). """ token_max_age = 3 * 24 * 3600 required_level = 'owner' success_message = _("Failed to perform requested action.") @classmethod def check_acl(cls, instance, user): if not instance.has_level(user, cls.required_level): raise PermissionDenied() @classmethod def get_salt(cls): return unicode(cls) @classmethod def get_token(cls, instance, user, *args): t = tuple([getattr(i, 'pk', i) for i in [instance, user] + list(args)]) return signing.dumps(t, salt=cls.get_salt(), compress=True) @classmethod def get_token_url(cls, instance, user, *args): key = cls.get_token(instance, user, *args) args = (instance.pk, key) + args return reverse(cls.url_name, args=args) # this wont work, CBVs suck: reverse(cls.as_view(), args=args) def get_template_names(self): return [self.template_name] def get(self, request, pk, key=None, *args, **kwargs): class LoginNeeded(Exception): pass pk = int(pk) instance = get_object_or_404(Instance, pk=pk) try: if key: logger.debug('Confirm dialog for token %s.', key) try: self.validate_key(pk, key) except signing.SignatureExpired: messages.error(request, _( 'The token has expired, please log in.')) raise LoginNeeded() self.key = key else: if not request.user.is_authenticated(): raise LoginNeeded() self.check_acl(instance, request.user) except LoginNeeded: return redirect_to_login(request.get_full_path(), self.get_login_url(), self.get_redirect_field_name()) except SuspiciousOperation as e: messages.error(request, _('This token is invalid.')) logger.warning('This token %s is invalid. %s', key, unicode(e)) raise PermissionDenied() return render(request, self.get_template_names(), self.get_context(instance)) def post(self, request, pk, key=None, *args, **kwargs): class LoginNeeded(Exception): pass pk = int(pk) instance = get_object_or_404(Instance, pk=pk) try: if not request.user.is_authenticated() and key: try: user = self.validate_key(pk, key) except signing.SignatureExpired: messages.error(request, _( 'The token has expired, please log in.')) raise LoginNeeded() self.key = key else: user = request.user self.check_acl(instance, request.user) except LoginNeeded: return redirect_to_login(request.get_full_path(), self.get_login_url(), self.get_redirect_field_name()) except SuspiciousOperation as e: messages.error(request, _('This token is invalid.')) logger.warning('This token %s is invalid. %s', key, unicode(e)) raise PermissionDenied() if self.do_action(instance, user): messages.success(request, self.success_message) else: messages.error(request, self.fail_message) return HttpResponseRedirect(instance.get_absolute_url()) 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 def do_action(self, instance, user): # noqa raise NotImplementedError('Please override do_action(instance, user)') def get_context(self, instance): context = {'instance': instance} if getattr(self, 'key', None) is not None: context['key'] = self.key return context class VmRenewView(AbstractVmFunctionView): """User can renew an instance.""" template_name = 'dashboard/confirm/base-renew.html' success_message = _("Virtual machine is successfully renewed.") url_name = 'dashboard.views.vm-renew' def get_context(self, instance): context = super(VmRenewView, self).get_context(instance) (context['time_of_suspend'], context['time_of_delete']) = instance.get_renew_times() return context def do_action(self, instance, user): instance.renew(user=user) logger.info('Instance %s renewed by %s.', unicode(instance), unicode(user)) return True 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( _('Ownership accepted'), 'dashboard/notifications/ownership-accepted.html', {'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.name 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) # we need to convert it to list, otherwise it's gonna be # similar to a QuerySet and update everything to # read status after get n = 10 if self.request.is_ajax() else 1000 context['notifications'] = list( self.request.user.notification_set.values()[: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(request, authentication_form=authentication_form, extra_context=extra_context) set_language_cookie(request, response) return response 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()), } table = UserKeyListTable( UserKey.objects.filter(user=self.request.user), request=self.request) table.page = None context['userkey_table'] = 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() if not disk.has_level(request.user, 'owner'): raise PermissionDenied() disk = self.get_object() app = disk.get_appliance() 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.has_level(request.user, 'owner'): raise PermissionDenied() return HttpResponse( json.dumps({ 'percentage': disk.get_download_percentage(), 'failed': disk.failed }), content_type="application/json", ) class InstanceActivityDetail(SuperuserRequiredMixin, DetailView): model = InstanceActivity context_object_name = 'instanceactivity' # much simpler to mock object template_name = 'dashboard/instanceactivity_detail.html' def get_context_data(self, **kwargs): ctx = super(InstanceActivityDetail, self).get_context_data(**kwargs) ctx['activities'] = self.object.instance.get_activities( self.request.user) 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: 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(instance=instance, 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) 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 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 context['root'] = self.clean_directory_list(directory) context['up_url'] = self.create_up_directory(directory) context['current'] = directory context['next_url'] = "%s%s?directory=%s" % ( settings.DJANGO_URL[:-1], reverse("dashboard.views.store-list"), directory) return context def get(self, *args, **kwargs): 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) def create_up_directory(self, directory): cut = -2 if directory.endswith("/") else -1 return "/".join(directory.split("/")[:cut]) + "/" def clean_directory_list(self, directory): from datetime import datetime from sizefield.utils import filesizeformat content = store_api.listfolder("test", directory) for d in content: d['human_readable_date'] = datetime.utcfromtimestamp(float( d['MTIME'])) delta = (datetime.utcnow() - d['human_readable_date'] ).total_seconds() d['is_new'] = delta < 5 and delta > 0 d['human_readable_size'] = ( "directory" if d['TYPE'] == "D" else filesizeformat(float(d['SIZE']))) d['path'] = d['DIR'] if len(d['path']) == 1 and d['path'][0] == ".": d['path'] = "/" else: d['path'] = "/" + d['path'] + "/" d['path'] += d['NAME'] if d['TYPE'] == "D": d['path'] += "/" return sorted(content, key=lambda k: k['TYPE']) @require_GET @login_required def store_download(request): path = request.GET.get("path") url = store_api.requestdownload("test", path) return redirect(url) @require_GET @login_required def store_upload(request): directory = request.GET.get("directory") action = store_api.requestupload("test", directory) next_url = "%s%s?directory=%s" % ( settings.DJANGO_URL[:-1], 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") url = store_api.requestupload("test", current_dir) 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'] = os.path.dirname(path) context['name'] = os.path.basename(path) return context def post(self, *args, **kwargs): path = self.request.POST.get("path") store_api.requestremove("test", path) if path.endswith("/"): return redirect("%s?directory=%s" % ( reverse("dashboard.views.store-list"), os.path.dirname(os.path.dirname(path)), )) else: return redirect("%s?directory=%s" % ( reverse("dashboard.views.store-list"), os.path.dirname(path), )) @require_POST @login_required def store_new_directory(request): path = request.POST.get("path") name = request.POST.get("name") store_api.requestnewfolder("test", path + name) return redirect("%s?directory=%s" % ( reverse("dashboard.views.store-list"), path))