# 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 .
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.contrib.auth.decorators import login_required
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, HttpResponseRedirect
from django.shortcuts import redirect, get_object_or_404
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
)
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from common.models import (
create_readable, HumanReadableException, fetch_human_exception,
)
from firewall.models import Vlan, Host, Rule
from manager.scheduler import SchedulerError
from storage.models import Disk
from vm.models import (
Instance, InstanceActivity, Node, Lease,
InstanceTemplate, InterfaceTemplate, Interface,
)
from .util import (
CheckedDetailView, AjaxOperationMixin, OperationView, AclUpdateView,
FormOperationMixin, FilterMixin, GraphMixin,
TransferOwnershipConfirmView, TransferOwnershipView,
)
from ..forms import (
AclUserOrGroupAddForm, VmResourcesForm, TraitsForm, RawDataForm,
VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm,
VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm,
VmDiskResizeForm, RedeployForm, VmDiskRemoveForm,
VmMigrateForm, VmDeployForm,
VmPortRemoveForm, VmPortAddForm,
VmRemoveInterfaceForm,
)
from request.models import TemplateAccessType, LeaseType
from request.forms import LeaseRequestForm, TemplateRequestForm
from ..models import Favourite
from manager.scheduler import has_traits
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 self.object.activity(
code_suffix='console-accessed', 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(GraphMixin, 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
is_operator = instance.has_level(user, "operator")
is_owner = instance.has_level(user, "owner")
ops = get_operations(instance, user)
hide_tutorial = self.request.COOKIES.get(
"hide_tutorial_for_%s" % instance.pk) == "True"
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),
'hide_tutorial': hide_tutorial,
'fav': instance.favourite_set.filter(user=user).exists(),
})
# 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 is_operator
# is operator/owner
context['is_operator'] = is_operator
context['is_owner'] = is_owner
# operation also allows RUNNING (if with_shutdown is present)
context['save_resources_enabled'] = instance.status not in ("RUNNING",
"PENDING")
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,
'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, "operator"):
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, "operator"):
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, "operator"):
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, "operator"):
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 __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 HttpResponseRedirect("%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 VmRemoveInterfaceView(FormOperationMixin, VmOperationView):
op = 'remove_interface'
form_class = VmRemoveInterfaceForm
show_in_toolbar = False
wait_for_result = 0.5
icon = 'times'
effect = "danger"
with_reload = True
def get_form_kwargs(self):
instance = self.get_op().instance
choices = instance.interface_set.all()
interface_pk = self.request.GET.get('interface')
if interface_pk:
try:
default = choices.get(pk=interface_pk)
except (ValueError, Interface.DoesNotExist):
raise Http404()
else:
default = None
val = super(VmRemoveInterfaceView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
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 VmDiskModifyView(FormOperationMixin, VmOperationView):
show_in_toolbar = False
with_reload = True
icon = 'arrows-alt'
effect = "success"
def get_form_kwargs(self):
choices = self.get_op().instance.disks
disk_pk = self.request.GET.get('disk')
if disk_pk:
try:
default = choices.get(pk=disk_pk)
except (ValueError, Disk.DoesNotExist):
raise Http404()
else:
default = None
val = super(VmDiskModifyView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
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
with_reload = True
def get_form_kwargs(self):
op = self.get_op()
val = super(VmCreateDiskView, self).get_form_kwargs()
num = op.instance.disks.count() + 1
val['default'] = "%s %d" % (op.instance.name, num)
return val
class VmDownloadDiskView(FormOperationMixin, VmOperationView):
op = 'download_disk'
form_class = VmDownloadDiskForm
show_in_toolbar = False
icon = 'download'
effect = "success"
is_disk_operation = True
with_reload = True
class VmMigrateView(FormOperationMixin, VmOperationView):
op = 'migrate'
icon = 'truck'
effect = 'info'
template_name = 'dashboard/_vm-migrate.html'
form_class = VmMigrateForm
def get_form_kwargs(self):
online = (n.pk for n in Node.objects.filter(enabled=True) if n.online)
choices = Node.objects.filter(pk__in=online)
default = None
inst = self.get_object()
try:
if isinstance(inst, Instance):
default = inst.select_node()
except SchedulerError:
logger.exception("scheduler error:")
val = super(VmMigrateView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
return val
def get_context_data(self, *args, **kwargs):
ctx = super(VmMigrateView, self).get_context_data(*args, **kwargs)
inst = self.get_object()
if isinstance(inst, Instance):
nodes_w_traits = [
n.pk for n in Node.objects.filter(enabled=True)
if n.online and
has_traits(inst.req_traits.all(), n)
]
ctx['nodes_w_traits'] = nodes_w_traits
return ctx
class VmPortRemoveView(FormOperationMixin, VmOperationView):
template_name = 'dashboard/_vm-remove-port.html'
op = 'remove_port'
show_in_toolbar = False
with_reload = True
wait_for_result = 0.5
icon = 'times'
effect = "danger"
form_class = VmPortRemoveForm
def get_form_kwargs(self):
instance = self.get_op().instance
choices = Rule.portforwards().filter(
host__interface__instance=instance)
rule_pk = self.request.GET.get('rule')
if rule_pk:
try:
default = choices.get(pk=rule_pk)
except (ValueError, Rule.DoesNotExist):
raise Http404()
else:
default = None
val = super(VmPortRemoveView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
return val
class VmPortAddView(FormOperationMixin, VmOperationView):
op = 'add_port'
show_in_toolbar = False
with_reload = True
wait_for_result = 0.5
icon = 'plus'
effect = "success"
form_class = VmPortAddForm
def get_form_kwargs(self):
instance = self.get_op().instance
choices = Host.objects.filter(interface__instance=instance)
host_pk = self.request.GET.get('host')
if host_pk:
try:
default = choices.get(pk=host_pk)
except (ValueError, Host.DoesNotExist):
raise Http404()
else:
default = None
val = super(VmPortAddView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
return val
class VmSaveView(FormOperationMixin, VmOperationView):
op = 'save_as_template'
icon = 'save'
effect = 'info'
form_class = VmSaveForm
def get_form_kwargs(self):
op = self.get_op()
val = super(VmSaveView, self).get_form_kwargs()
val['default'] = op._rename(op.instance.name)
obj = self.get_object()
if obj.template and obj.template.has_level(
self.request.user, "owner"):
val['clone'] = True
return val
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, "%s: %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 HttpResponseRedirect(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 = 'success'
show_in_toolbar = False
form_class = VmRenewForm
wait_for_result = 0.5
template_name = 'dashboard/_vm-renew.html'
with_reload = True
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
def get_context_data(self, **kwargs):
context = super(VmRenewView, self).get_context_data(**kwargs)
context['lease_request_form'] = LeaseRequestForm(request=self.request)
context['lease_types'] = LeaseType.objects.exists()
return context
class VmStateChangeView(FormOperationMixin, VmOperationView):
op = 'emergency_change_state'
icon = 'legal'
effect = 'danger'
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
class RedeployView(FormOperationMixin, VmOperationView):
op = 'redeploy'
icon = 'stethoscope'
effect = 'danger'
show_in_toolbar = True
form_class = RedeployForm
wait_for_result = 0.5
class VmDeployView(FormOperationMixin, VmOperationView):
op = 'deploy'
icon = 'play'
effect = 'success'
form_class = VmDeployForm
def get_form_kwargs(self):
kwargs = super(VmDeployView, self).get_form_kwargs()
if self.request.user.is_superuser:
online = (n.pk for n in
Node.objects.filter(enabled=True) if n.online)
kwargs['choices'] = Node.objects.filter(pk__in=online)
kwargs['instance'] = self.get_object()
return kwargs
vm_ops = OrderedDict([
('deploy', VmDeployView),
('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='plug', effect='warning')),
('recover', VmOperationView.factory(
op='recover', icon='medkit', effect='warning')),
('nostate', VmStateChangeView),
('redeploy', RedeployView),
('destroy', VmOperationView.factory(
extra_bases=[TokenOperationView],
op='destroy', icon='times', effect='danger')),
('create_disk', VmCreateDiskView),
('download_disk', VmDownloadDiskView),
('resize_disk', VmDiskModifyView.factory(
op='resize_disk', form_class=VmDiskResizeForm,
icon='arrows-alt', effect="warning")),
('remove_disk', VmDiskModifyView.factory(
op='remove_disk', form_class=VmDiskRemoveForm,
icon='times', effect="danger")),
('add_interface', VmAddInterfaceView),
('remove_interface', VmRemoveInterfaceView),
('remove_port', VmPortRemoveView),
('add_port', VmPortAddView),
('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,
)),
('install_keys', VmOperationView.factory(
op='install_keys', icon='key', 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 = {}
if hasattr(self, 'form_class'):
form = self.form_class(self.request.POST, **self.get_form_kwargs())
if form.is_valid():
extra.update(form.cleaned_data)
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",
'node_exact': "node__name",
'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,
'url': reverse('dashboard.views.detail', args=[i.pk]),
'name': i.name,
'icon': i.get_status_icon(),
'host': i.short_hostname,
'status': i.get_status_display(),
'owner': (i.owner.profile.get_display_name()
if i.owner != self.request.user else None),
'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)
filters, excludes = self.get_queryset_filters()
return queryset.filter(**filters).exclude(**excludes).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.html']
else:
return ['dashboard/nojs-wrapper.html']
def get_template(self, request, pk):
try:
template = InstanceTemplate.objects.get(
pk=int(pk))
except (ValueError, InstanceTemplate.DoesNotExist):
raise Http404()
if not template.has_level(request.user, 'user'):
raise PermissionDenied()
return template
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 = self.get_template(request, template_pk)
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(),
'template_access_types': TemplateAccessType.objects.exists(),
'form': TemplateRequestForm(request=request),
})
return self.render_to_response(context)
def __create_normal(self, request, template, *args, **kwargs):
instances = [Instance.create_from_template(
template=template,
owner=request.user)]
return self.__deploy(request, instances)
def __create_customized(self, request, template, *args, **kwargs):
user = request.user
# no form yet, using POST directly:
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
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):
# workaround EncodeError: dictionary changed size during iteration
user = User.objects.get(pk=request.user.pk)
for i in instances:
i.deploy.async(user=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 HttpResponseRedirect("%s#activity" % path)
def post(self, request, *args, **kwargs):
user = request.user
if not request.user.has_perm('vm.create_vm'):
raise PermissionDenied()
template = self.get_template(request, request.POST.get("template"))
# 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, template, *args, **kwargs)
@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, content_type="image/png")
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
@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 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, 'user'):
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 TransferInstanceOwnershipConfirmView(TransferOwnershipConfirmView):
template = "dashboard/confirm/transfer-instance-ownership.html"
model = Instance
def change_owner(self, instance, new_owner):
with instance.activity(
code_suffix='ownership-transferred',
readable_name=ugettext_noop("transfer ownership"),
concurrency_check=False, user=new_owner):
super(TransferInstanceOwnershipConfirmView, self).change_owner(
instance, new_owner)
class TransferInstanceOwnershipView(TransferOwnershipView):
confirm_view = TransferInstanceOwnershipConfirmView
model = Instance
notification_msg = ugettext_noop(
'%(owner)s offered you to take the ownership of '
'his/her virtual machine called %(instance)s. '
'Accept')
token_url = 'dashboard.views.vm-transfer-ownership-confirm'
template = "dashboard/vm-detail/tx-owner.html"
@login_required
def toggle_template_tutorial(request, pk):
hidden = request.POST.get("hidden", "").lower() == "true"
instance = get_object_or_404(Instance, pk=pk)
response = HttpResponseRedirect(instance.get_absolute_url())
response.set_cookie( # for a week
"hide_tutorial_for_%s" % pk, hidden, 7 * 24 * 60 * 60)
return response