Commit 7bb23000 by Kálmán Viktor

Merge branch 'master' into issue-sliders

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/views.py
parents 216378f7 b548b1ce
......@@ -10,6 +10,18 @@ class AclUserAutocomplete(autocomplete_light.AutocompleteGenericBase):
('^name', 'groupprofile__org_id'),
)
autocomplete_js_attributes = {'placeholder': _("Name of group or user")}
choice_html_format = u'<span data-value="%s"><span>%s</span> %s</span>'
def choice_html(self, choice):
try:
name = choice.get_full_name()
except AttributeError:
name = _('group')
if name:
name = u'(%s)' % name
return self.choice_html_format % (
self.choice_value(choice), self.choice_label(choice), name)
def choices_for_request(self):
user = self.request.user
......
......@@ -54,6 +54,7 @@ from .models import Profile, GroupProfile
from circle.settings.base import LANGUAGES, MAX_NODE_RAM
from django.utils.translation import string_concat
from .virtvalidator import domain_validator
LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")"))
for l in LANGUAGES)
......@@ -908,7 +909,8 @@ class VmRenewForm(forms.Form):
self.fields['lease'] = forms.ModelChoiceField(queryset=choices,
initial=default,
required=True,
required=False,
empty_label=None,
label=_('Length'))
if len(choices) < 2:
self.fields['lease'].widget = HiddenInput()
......@@ -952,6 +954,25 @@ class VmDownloadDiskForm(forms.Form):
return helper
class VmAddInterfaceForm(forms.Form):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
super(VmAddInterfaceForm, self).__init__(*args, **kwargs)
field = forms.ModelChoiceField(
queryset=choices, required=True, label=_('Vlan'))
if not choices:
field.widget.attrs['disabled'] = 'disabled'
field.empty_label = _('No more networks.')
self.fields['vlan'] = field
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class CircleAuthenticationForm(AuthenticationForm):
# fields: username, password
......@@ -1236,6 +1257,9 @@ class TraitsForm(forms.ModelForm):
class RawDataForm(forms.ModelForm):
raw_data = forms.CharField(validators=[domain_validator],
widget=forms.Textarea(attrs={'rows': 5}),
required=False)
class Meta:
model = Instance
......
......@@ -70,7 +70,7 @@ class Notification(TimeStampedModel):
def send(cls, user, subject, template, context,
valid_until=None, subject_context=None):
hro = create_readable(template, user=user, **context)
subject = create_readable(subject, subject_context or context)
subject = create_readable(subject, **(subject_context or context))
return cls.objects.create(to=user,
subject_data=subject.to_dict(),
message_data=hro.to_dict(),
......
......@@ -716,6 +716,10 @@ textarea[name="list-new-namelist"] {
margin-top: -6px;
}
#show-all-activities-container {
margin: 20px 0 0 10px;
}
.vm-resources-sliders .row {
margin-bottom: 15px;
}
......@@ -3,7 +3,7 @@
$(function() {
/* vm operations */
$('#ops, #vm-details-resources-disk, #vm-details-renew-op').on('click', '.operation.btn', function(e) {
$('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface').on('click', '.operation.btn', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin');
$.ajax({
......@@ -50,6 +50,9 @@ $(function() {
*/
if(data.success) {
$('a[href="#activity"]').trigger("click");
if(data.with_reload) {
location.reload();
}
/* if there are messages display them */
if(data.messages && data.messages.length > 0) {
......
var show_all = false;
var in_progress = false;
$(function() {
/* do we need to check for new activities */
if(decideActivityRefresh()) {
checkNewActivity(false, 1);
if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
}
$('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('fa-spin');
checkNewActivity(false, 1);
if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
});
$("#activity-refresh").on("click", "#show-all-activities", function() {
$(this).find("i").addClass("fa-spinner fa-spin");
show_all = !show_all;
$('a[href="#activity"]').trigger("click");
return false;
});
/* save resources */
......@@ -134,11 +151,6 @@ $(function() {
return false;
});
/* show help */
$(".vm-details-help-button").click(function() {
$(".vm-details-help").stop().slideToggle();
});
/* for interface remove buttons */
$('.interface-remove').click(function() {
var interface_pk = $(this).data('interface-pk');
......@@ -320,7 +332,7 @@ function removePort(data) {
function decideActivityRefresh() {
var check = false;
/* if something is still spinning */
if($('.timeline .activity:first i:first').hasClass('fa-spin'))
if($('.timeline .activity i').hasClass('fa-spin'))
check = true;
/* if there is only one activity */
if($('#activity-timeline div[class="activity"]').length < 2)
......@@ -340,25 +352,25 @@ function changeHTML(html) {
return html.replace(/data-original-title/g, "title").replace(/title=""/g, "").replace(/\//g, '').replace(/ /g, '');
}
function checkNewActivity(only_status, runs) {
// set default only_status to false
only_status = typeof only_status !== 'undefined' ? only_status : false;
function checkNewActivity(runs) {
var instance = location.href.split('/'); instance = instance[instance.length - 2];
$.ajax({
type: 'GET',
url: '/dashboard/vm/' + instance + '/activity/',
data: {'only_status': only_status},
data: {'show_all': show_all},
success: function(data) {
if(!only_status) {
if(show_all) { /* replace on longer string freezes the spinning stuff */
$("#activity-refresh").html(data['activities']);
} else {
a = unescapeHTML(data['activities']);
b = changeHTML($("#activity-timeline").html());
b = changeHTML($("#activity-refresh").html());
if(a != b)
$("#activity-timeline").html(data['activities']);
$("#activity-refresh").html(data['activities']);
}
$("#ops").html(data['ops']);
$("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip();
}
$("#vm-details-state i").prop("class", "fa " + data['icon']);
$("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
......@@ -378,14 +390,16 @@ function checkNewActivity(only_status, runs) {
if(runs > 0 && decideActivityRefresh()) {
setTimeout(
function() {checkNewActivity(only_status, runs + 1)},
function() {checkNewActivity(runs + 1)},
1000 + Math.exp(runs * 0.05)
);
} else {
in_progress = false;
}
$('a[href="#activity"] i').removeClass('fa-spin');
},
error: function() {
in_progress = false;
}
});
}
......@@ -81,4 +81,11 @@
{% block extra_etc %}
{% endblock %}
<script>
yourlabs.TextWidget.prototype.getValue = function(choice) {
return choice.children().html();
}
</script>
</html>
......@@ -18,8 +18,8 @@
{% endblock %}
{% block navbar %}
<ul class="nav navbar-nav pull-right">
{% if user.is_authenticated and user.pk and not request.token_user %}
<ul class="nav navbar-nav pull-right">
<li class="dropdown" id="notification-button">
<a href="{% url "dashboard.views.notifications" %}" style="color: white; font-size: 12px;"
class="dropdown-toggle" data-toggle="dropdown">
......@@ -32,9 +32,8 @@
<li>{% trans "Loading..." %}</li>
</ul>
</li>
</ul>
</ul>
{% if user.is_authenticated and user.pk %}
<a class="navbar-brand pull-right" href="{% url "logout" %}?next={% url "login" %}" style="color: white; font-size: 10px;">
<i class="fa fa-sign-out"></i> {% trans "Log out" %}
</a>
......@@ -48,7 +47,7 @@
<a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;"><i class="fa fa-cogs"></i> {% trans "Admin" %}</a>
{% endif %}
{% else %}
<a class="navbar-brand pull-right" href="{% url "login" %}?next={% url "dashboard.index" %}" style="color: white; font-size: 10px;"><i class="fa fa-sign-in"></i> {% trans "Log in " %}</a>
<a class="navbar-brand pull-right" href="{% url "login" %}?next={{ request.path }}" style="color: white; font-size: 10px;"><i class="fa fa-sign-in"></i> {% trans "Log in " %}</a>
{% endif %}
{% endblock %}
......
......@@ -7,7 +7,7 @@
<div class="page-header">
<div class="pull-right" style="padding-top: 15px;">
<a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs group-details-rename-button"><i class="fa fa-pencil"></i></a>
<a title="{% trans "Delete" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=group.pk %}"><i class="fa fa-trash"></i></a>
<a title="{% trans "Delete" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=group.pk %}"><i class="fa fa-trash-o"></i></a>
<a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs group-details-help-button"><i class="fa fa-question"></i></a>
</div>
<h1>
......
......@@ -14,17 +14,17 @@
</h1>
</div>
<div class="row">
<div class="col-md-4" id="vm-info-pane">
<div class="col-md-5" id="vm-info-pane">
<div class="big">
<span id="vm-activity-state" class="label label-{% if object.get_status_id == 'wait' %}info{% else %}{% if object.succeeded %}success{% else %}error{% endif %}{% endif %}">
<span>{{ object.get_status_id|upper }}</span>
</span>
</div>
<div id="vm-activity-context" class="timeline">
{% include "dashboard/vm-detail/_activity-timeline.html" with active=object %}
</div>
</div>
<div class="col-md-8">
<div class="col-md-7">
<div class="panel panel-default">
<!--<div class="panel-heading"><h2 class="panel-title">{% trans "Activity" %}</h2></div> -->
<div class="panel-body">
......
......@@ -64,7 +64,7 @@
<i class="fa fa-desktop"></i>
{% trans "Virtual machines owned by the user" %} ({{ instances_owned|length }})
</h4>
<ul class="dashboard-profile-vm-list">
<ul class="dashboard-profile-vm-list fa-ul">
{% for i in instances_owned %}
<li>
<a href="{{ i.get_absolute_url }}">
......@@ -85,7 +85,7 @@
<i class="fa fa-desktop"></i>
{% trans "Virtual machines with access" %} ({{ instances_with_access|length }})
</h4>
<ul class="dashboard-profile-vm-list">
<ul class="dashboard-profile-vm-list fa-ul">
{% for i in instances_with_access %}
<li>
<a href="{{ i.get_absolute_url }}">
......
......@@ -98,17 +98,12 @@
</div>
</dd>
<dd style="font-size: 10px; text-align: right; padding-top: 8px;">
<a id="vm-details-pw-change" href="#">{% trans "Generate new password!" %}</a>
</dd>
<div id="vm-details-pw-confirm"> {% comment %} TODO Couldn't this use a modal? {% endcomment%}
<dt>
{% trans "Are you sure?" %}
</dt>
<dd>
<a href="#" class="vm-details-pw-confirm-choice label label-success" data-choice="1" data-vm="{{ instance.pk }}">{% trans "Yes" %}</a> /
<a href="#" class="vm-details-pw-confirm-choice label label-danger" data-choice="0">{% trans "No" %}</a>
</dd>
<div id="vm-details-pw-reset">
{% with op=op.password_reset %}{% if op %}
<a href="{{op.get_url}}" class="btn operation btn-default btn-xs" {% if op.disabled %}disabled{% endif %}>{% trans "Generate new password!" %}</a>
{% endif %}{% endwith %}
</div>
</dd>
</dl>
<div class="input-group" id="dashboard-vm-details-connect-command">
......
{% load i18n %}
<div id="activity-timeline" class="timeline">
{% for a in activities %}
<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" data-activity-id="{{ a.pk }}">
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
......@@ -7,7 +10,7 @@
<strong{% if a.result %} title="{{ a.result.get_user_text }}"{% endif %}>
<a href="{{ a.get_absolute_url }}">
{% if a.times > 1 %}({{ a.times }}x){% endif %}
{{ a.readable_name.get_user_text }}</a>
{{ a.readable_name.get_user_text|capfirst }}</a>
{% if a.has_percent %}
- {{ a.percentage }}%
......@@ -32,7 +35,7 @@
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}{% if s.pk == active.pk %} sub-activity-active{% endif %}">
<span{% if s.result %} title="{{ s.result.get_user_text }}"{% endif %}>
<a href="{{ s.get_absolute_url }}">
{{ s.readable_name.get_user_text }}</a></span> &ndash;
{{ s.readable_name.get_user_text|capfirst }}</a></span> &ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
......@@ -47,3 +50,16 @@
{% endif %}
</div>
{% endfor %}
</div>
{% if show_show_all %}
<div id="show-all-activities-container">
<a id="show-all-activities" href="#">
{% if activities|length > 10 %}
{% trans "Show less activities" %} <i class="fa fa-angle-double-up"></i>
{% else %}
{% trans "Show all activities" %} <i class="fa fa-angle-double-down"></i>
{% endif %}
</a>
</div>
{% endif %}
......@@ -2,6 +2,6 @@
<h3>{% trans "Activity" %}</h3>
<div id="activity-timeline" class="timeline">
<div id="activity-refresh">
{% include "dashboard/vm-detail/_activity-timeline.html" %}
</div>
{% load i18n %}
{% load network_tags %}
<h2>
<a href="#" id="vm-details-network-add" class="btn btn-success pull-right no-js-hidden">
<i class="fa fa-plus"></i> {% trans "add interface" %}
</a>
<div id="vm-details-add-interface">
{% with op=op.add_interface %}{% if op %}
<a href="{{op.get_url}}" class="btn btn-{{op.effect}} operation pull-right"
{% if op.disabled %}disabled{% endif %}>
<i class="fa fa-{{op.icon}}"></i> {% trans "add interface" %}</a>
{% endif %}{% endwith %}
</div>
{% trans "Interfaces" %}
</h2>
<div class="js-hidden row" id="vm-details-network-add-form">
<div class="col-md-12">
<div>
<hr />
<h3>
{% trans "Add new network interface!" %}
</h3>
<form method="POST" action="">
{% csrf_token %}
<div class="input-group" style="max-width: 330px;">
<select name="new_network_vlan" class="form-control font-awesome-font">
{% for v in vlans %}
<option value="{{ v.pk }}">
{% if v.managed %}
&#xf0ac;
{% else %}
&#xf0c1;
{% endif %}
{{ v.name }}
</option>
{% empty %}
<option value="-1">No more networks!</option>
{% endfor %}
</select>
<div class="input-group-btn">
<button {% if vlans|length == 0 %}disabled{% endif %}
type="submit" class="btn btn-success"><i class="fa fa-plus-circle"></i></button>
</div>
</div>
</form>
<hr />
</div>
</div>
</div>
{% for i in instance.interface_set.all %}
<div>
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title-page %}{% trans "Edit raw data" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-7">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin"><i class="fa fa-time"></i> {% trans "Edit raw data" %}</h3>
</div>
<div class="panel-body">
{% with form=form %}
{% include "display-form-errors.html" %}
{% endwith %}
{% crispy form %}
</div>
</div>
</div>
</div>
{% endblock %}
......@@ -280,6 +280,7 @@ class RenewViewTest(unittest.TestCase):
view = vm_ops['renew']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
......@@ -288,7 +289,10 @@ class RenewViewTest(unittest.TestCase):
inst.has_level.return_value = True
go.return_value = inst
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234).render().status_code == 200
assert view.as_view()(request, pk=1234)
assert not msg.error.called
assert inst.renew.async.called_with(user=request.user, lease=None)
assert inst.renew.async.return_value.get.called
# success would redirect
def test_renew_by_owner_w_param(self):
......
......@@ -24,8 +24,9 @@ from django.contrib.auth.models import User, Group
from django.contrib.auth.models import Permission
from django.contrib.auth import authenticate
from dashboard.views import VmAddInterfaceView
from vm.models import Instance, InstanceTemplate, Lease, Node, Trait
from vm.operations import WakeUpOperation
from vm.operations import WakeUpOperation, AddInterfaceOperation
from ..models import Profile
from firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch
......@@ -142,33 +143,21 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]})
self.assertEqual(response.status_code, 302)
def test_permitted_password_change(self):
c = Client()
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
inst.node = Node.objects.all()[0]
inst.save()
password = inst.pw
response = c.post("/dashboard/vm/1/", {'change_password': True})
self.assertTrue(Instance.get_remote_queue_name.called)
self.assertEqual(response.status_code, 302)
self.assertNotEqual(password, Instance.objects.get(pk=1).pw)
def test_unpermitted_password_change(self):
c = Client()
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
password = inst.pw
response = c.post("/dashboard/vm/1/", {'change_password': True})
response = c.post("/dashboard/vm/1/op/password_reset/")
self.assertEqual(response.status_code, 403)
self.assertEqual(password, Instance.objects.get(pk=1).pw)
def test_unpermitted_network_add_wo_perm(self):
c = Client()
self.login(c, "user2")
response = c.post("/dashboard/vm/1/", {'new_network_vlan': 1})
response = c.post("/dashboard/vm/1/op/add_interface/",
{'vlan': 1})
self.assertEqual(response.status_code, 403)
def test_unpermitted_network_add_wo_vlan_perm(self):
......@@ -176,8 +165,18 @@ class VmDetailTest(LoginMixin, TestCase):
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
response = c.post("/dashboard/vm/1/", {'new_network_vlan': 1})
self.assertEqual(response.status_code, 403)
interface_count = inst.interface_set.count()
with patch.object(AddInterfaceOperation, 'async') as async:
async.side_effect = inst.add_interface.call
with patch.object(VmAddInterfaceView, 'get_form_kwargs',
autospec=True) as get_form_kwargs:
get_form_kwargs.return_value = {'choices': Vlan.objects.all()}
response = c.post("/dashboard/vm/1/op/add_interface/",
{'vlan': 1})
self.assertEqual(response.status_code, 302)
assert async.called
self.assertEqual(inst.interface_set.count(), interface_count)
def test_permitted_network_add(self):
c = Client()
......@@ -187,9 +186,12 @@ class VmDetailTest(LoginMixin, TestCase):
vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u1, 'user')
interface_count = inst.interface_set.count()
response = c.post("/dashboard/vm/1/",
{'new_network_vlan': 1})
with patch.object(AddInterfaceOperation, 'async') as mock_method:
mock_method.side_effect = inst.add_interface
response = c.post("/dashboard/vm/1/op/add_interface/",
{'vlan': 1})
self.assertEqual(response.status_code, 302)
assert mock_method.called
self.assertEqual(inst.interface_set.count(), interface_count + 1)
def test_permitted_network_delete(self):
......@@ -401,8 +403,7 @@ class VmDetailTest(LoginMixin, TestCase):
inst.set_level(self.u2, 'owner')
vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user')
response = c.post("/dashboard/vm/1/",
{'new_network_vlan': 1})
inst.add_interface(user=self.u2, vlan=vlan)
host = Host.objects.get(
interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get(
......@@ -421,8 +422,7 @@ class VmDetailTest(LoginMixin, TestCase):
inst.set_level(self.u2, 'owner')
vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user')
response = c.post("/dashboard/vm/1/",
{'new_network_vlan': 1})
inst.add_interface(user=self.u2, vlan=vlan)
host = Host.objects.get(
interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get(
......
......@@ -20,6 +20,7 @@ from __future__ import unicode_literals, absolute_import
from collections import OrderedDict
from itertools import chain
from os import getenv
from urlparse import urljoin
import json
import logging
import re
......@@ -52,6 +53,7 @@ from django_tables2 import SingleTableView
from braces.views import (LoginRequiredMixin, SuperuserRequiredMixin,
PermissionRequiredMixin)
from braces.views._access import AccessMixin
from celery.exceptions import TimeoutError
from django_sshkey.models import UserKey
......@@ -62,13 +64,14 @@ from .forms import (
VmSaveForm, UserKeyForm, VmRenewForm,
CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
TraitsForm, RawDataForm, GroupPermissionForm, AclUserAddForm,
VmResourcesForm,
VmResourcesForm, VmAddInterfaceForm,
)
from .tables import (
NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable,
GroupListTable, UserKeyListTable
)
from common.models import HumanReadableObject
from vm.models import (
Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
InterfaceTemplate, Lease, Node, NodeActivity, Trait,
......@@ -273,8 +276,11 @@ class VmDetailView(CheckedDetailView):
})
# activity data
context['activities'] = self.object.get_merged_activities(
self.request.user)
activities = instance.get_merged_activities(self.request.user)
show_show_all = len(activities) > 10
activities = activities[:10]
context['activities'] = activities
context['show_show_all'] = show_show_all
context['vlans'] = Vlan.get_objects_with_level(
'user', self.request.user
......@@ -306,13 +312,11 @@ class VmDetailView(CheckedDetailView):
def post(self, request, *args, **kwargs):
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():
......@@ -320,19 +324,6 @@ class VmDetailView(CheckedDetailView):
return v(request)
raise Http404()
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_name(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
......@@ -455,24 +446,6 @@ class VmDetailView(CheckedDetailView):
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()
......@@ -495,6 +468,7 @@ class VmTraitsUpdate(SuperuserRequiredMixin, UpdateView):
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"
......@@ -505,6 +479,7 @@ class OperationView(RedirectToLoginMixin, DetailView):
template_name = 'dashboard/operate.html'
show_in_toolbar = True
effect = None
wait_for_result = None
@property
def name(self):
......@@ -566,18 +541,56 @@ class OperationView(RedirectToLoginMixin, DetailView):
self.check_auth()
return super(OperationView, self).get(request, *args, **kwargs)
def get_response_data(self, result, done, extra=None, **kwargs):
"""Return serializable data to return to agents requesting json
response to POST"""
if extra is None:
extra = {}
extra["success"] = not isinstance(result, Exception)
extra["done"] = done
if isinstance(result, HumanReadableObject):
extra["message"] = result.get_user_text()
return extra
def post(self, request, extra=None, *args, **kwargs):
self.check_auth()
self.object = self.get_object()
if extra is None:
extra = {}
result = None
done = False
try:
self.get_op().async(user=request.user, **extra)
task = self.get_op().async(user=request.user, **extra)
except Exception as e:
messages.error(request, _('Could not start operation.'))
logger.exception(e)
result = e
else:
wait = self.wait_for_result
if wait:
try:
result = task.get(timeout=wait,
interval=min((wait / 5, .5)))
except TimeoutError:
logger.debug("Result didn't arrive in %ss",
self.wait_for_result, exc_info=True)
except Exception as e:
messages.error(request, _('Operation failed.'))
logger.debug("Operation failed.", exc_info=True)
result = e
else:
done = True
messages.success(request, _('Operation succeeded.'))
if result is None and not done:
messages.success(request, _('Operation is started.'))
if "/json" in request.META.get("HTTP_ACCEPT", ""):
data = self.get_response_data(result, done,
post_extra=extra, **kwargs)
return HttpResponse(json.dumps(data),
content_type="application/json")
else:
return redirect("%s#activity" % self.object.get_absolute_url())
@classmethod
......@@ -605,6 +618,7 @@ class AjaxOperationMixin(object):
store.used = True
return HttpResponse(
json.dumps({'success': True,
'with_reload': getattr(self, 'with_reload', False),
'messages': [unicode(m) for m in store]}),
content_type="application=json"
)
......@@ -644,7 +658,9 @@ class FormOperationMixin(object):
request, extra, *args, **kwargs)
if request.is_ajax():
return HttpResponse(
json.dumps({'success': True}),
json.dumps({
'success': True,
'with_reload': getattr(self, 'with_reload', False)}),
content_type="application=json"
)
else:
......@@ -661,12 +677,32 @@ class RequestFormOperationMixin(FormOperationMixin):
return val
class VmAddInterfaceView(FormOperationMixin, VmOperationView):
op = 'add_interface'
form_class = VmAddInterfaceForm
show_in_toolbar = False
icon = 'globe'
effect = 'success'
with_reload = True
def get_form_kwargs(self):
inst = self.get_op().instance
choices = Vlan.get_objects_with_level(
"user", self.request.user).exclude(
vm_interface__instance__in=[inst])
val = super(VmAddInterfaceView, self).get_form_kwargs()
val.update({'choices': choices})
return val
class VmCreateDiskView(FormOperationMixin, VmOperationView):
op = 'create_disk'
form_class = VmCreateDiskForm
show_in_toolbar = False
icon = 'hdd-o'
effect = "success"
is_disk_operation = True
......@@ -676,6 +712,7 @@ class VmDownloadDiskView(FormOperationMixin, VmOperationView):
form_class = VmDownloadDiskForm
show_in_toolbar = False
icon = 'download'
effect = "success"
is_disk_operation = True
......@@ -773,6 +810,7 @@ class TokenOperationView(OperationView):
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())
......@@ -815,6 +853,7 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
effect = 'info'
show_in_toolbar = False
form_class = VmRenewForm
wait_for_result = 0.5
def get_form_kwargs(self):
choices = Lease.get_objects_with_level("user", self.request.user)
......@@ -827,6 +866,13 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
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
vm_ops = OrderedDict([
('deploy', VmOperationView.factory(
......@@ -855,8 +901,12 @@ vm_ops = OrderedDict([
op='destroy', icon='times', effect='danger')),
('create_disk', VmCreateDiskView),
('download_disk', VmDownloadDiskView),
('add_interface', VmAddInterfaceView),
('renew', VmRenewView),
('resources_change', VmResourcesChangeView),
('password_reset', VmOperationView.factory(
op='password_reset', icon='unlock', effect='warning',
show_in_toolbar=False, wait_for_result=0.5, with_reload=True)),
])
......@@ -2387,15 +2437,20 @@ def vm_activity(request, pk):
raise PermissionDenied()
response = {}
only_status = request.GET.get("only_status", "false")
show_all = request.GET.get("show_all", "false") == "true"
activities = instance.get_merged_activities(request.user)
show_show_all = len(activities) > 10
if not show_all:
activities = activities[:10]
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_merged_activities(request.user),
'activities': activities,
'show_show_all': show_show_all,
'ops': get_operations(instance, request.user),
}
......@@ -3070,3 +3125,7 @@ class UserKeyCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView):
kwargs = super(UserKeyCreate, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def absolute_url(url):
return urljoin(settings.DJANGO_URL, url)
from django.core.exceptions import ValidationError
from lxml import etree as ET
import logging
rng_file = "/usr/share/libvirt/schemas/domain.rng"
# Mandatory xml elements dor parsing
header = "<domain type='kvm'><name>validator</name>\
<memory unit='KiB'>1024</memory>\
<os><type>hvm</type></os>"
footer = "</domain>"
logger = logging.getLogger()
def domain_validator(value):
xml = header + value + footer
try:
parsed_xml = ET.fromstring(xml)
except Exception as e:
raise ValidationError(e.message)
try:
relaxng = ET.RelaxNG(file=rng_file)
except:
logger.critical("%s RelaxNG libvirt domain schema file "
"is missing for validation.", rng_file)
else:
try:
relaxng.assertValid(parsed_xml)
except Exception as e:
raise ValidationError(e.message)
......@@ -13,6 +13,7 @@ from .instance import InstanceTemplate
from .instance import Instance
from .instance import post_state_changed
from .instance import pre_state_changed
from .instance import pwgen
from .network import InterfaceTemplate
from .network import Interface
from .node import Node
......@@ -22,5 +23,5 @@ __all__ = [
'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate',
'Instance', 'instance_activity', 'post_state_changed', 'pre_state_changed',
'InterfaceTemplate', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease',
'node_activity',
'node_activity', 'pwgen'
]
......@@ -642,6 +642,13 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
:param again: Notify already notified owners.
"""
notification_msg = ugettext_noop(
'Your instance <a href="%(url)s">%(instance)s</a> is going to '
'expire. It will be suspended at %(suspend)s and destroyed at '
'%(delete)s. Please, either <a href="%(token)s">renew</a> '
'or <a href="%(url)s">destroy</a> it now.')
if not again and self._is_notified_about_expiration():
return False
success, failed = [], []
......@@ -666,20 +673,26 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
readable_name=ugettext_noop(
"notify owner about expiration"),
on_commit=on_commit):
from dashboard.views import VmRenewView
from dashboard.views import VmRenewView, absolute_url
level = self.get_level_object("owner")
for u, ulevel in self.get_users_with_level(level__pk=level.pk):
try:
token = VmRenewView.get_token_url(self, u)
u.profile.notify(
_('%s expiring soon') % unicode(self),
'dashboard/notifications/vm-expiring.html',
{'instance': self, 'token': token}, valid_until=min(
self.time_of_delete, self.time_of_suspend))
ugettext_noop('%(instance)s expiring soon'),
notification_msg, url=self.get_absolute_url(),
instance=self, suspend=self.time_of_suspend,
token=token, delete=self.time_of_delete)
except Exception as e:
failed.append((u, e))
else:
success.append(u)
if self.status == "RUNNING":
token = absolute_url(
VmRenewView.get_token_url(self, self.owner))
queue = self.get_remote_queue_name("agent")
agent_tasks.send_expiration.apply_async(
queue=queue, args=(self.vm_name, token))
return True
def is_expiring(self, threshold=0.1):
......@@ -719,24 +732,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
timezone.now() + lease.suspend_interval,
timezone.now() + lease.delete_interval)
def change_password(self, user=None):
"""Generate new password for the vm
:param self: The virtual machine.
:param user: The user who's issuing the command.
"""
self.pw = pwgen()
with instance_activity(code_suffix='change_password', instance=self,
readable_name=ugettext_noop("change password"),
user=user):
queue = self.get_remote_queue_name("agent")
agent_tasks.change_password.apply_async(queue=queue,
args=(self.vm_name,
self.pw))
self.save()
def select_node(self):
"""Returns the node the VM should be deployed or migrated to.
"""
......
......@@ -33,8 +33,9 @@ from .tasks.local_tasks import (
)
from .models import (
Instance, InstanceActivity, InstanceTemplate, Interface, Node,
NodeActivity,
NodeActivity, pwgen
)
from .tasks import agent_tasks
logger = getLogger(__name__)
......@@ -94,12 +95,21 @@ class AddInterfaceOperation(InstanceOperation):
"the VM.")
required_perms = ()
def rollback(self, net, activity):
with activity.sub_activity(
'destroying_net',
readable_name=ugettext_noop("destroy network (rollback)")):
net.destroy()
net.delete()
def check_precond(self):
super(AddInterfaceOperation, self).check_precond()
if self.instance.status not in ['STOPPED', 'PENDING', 'RUNNING']:
raise self.instance.WrongStateError(self.instance)
def _operation(self, activity, user, system, vlan, managed=None):
if not vlan.has_level(user, 'user'):
raise PermissionDenied()
if managed is None:
managed = vlan.managed
......@@ -107,12 +117,15 @@ class AddInterfaceOperation(InstanceOperation):
managed=managed, owner=user, vlan=vlan)
if self.instance.is_running:
try:
with activity.sub_activity('attach_network'):
self.instance.attach_network(net)
except Exception as e:
if hasattr(e, 'libvirtError'):
self.rollback(net, activity)
raise
net.deploy()
return net
def get_activity_name(self, kwargs):
return create_readable(ugettext_noop("add %(vlan)s interface"),
vlan=kwargs['vlan'])
......@@ -867,3 +880,27 @@ class ResourcesOperation(InstanceOperation):
register_operation(ResourcesOperation)
class PasswordResetOperation(InstanceOperation):
activity_code_suffix = 'Password reset'
id = 'password_reset'
name = _("password reset")
description = _("Password reset")
acl_level = "owner"
required_perms = ()
def check_precond(self):
super(PasswordResetOperation, self).check_precond()
if self.instance.status not in ["RUNNING"]:
raise self.instance.WrongStateError(self.instance)
def _operation(self):
self.instance.pw = pwgen()
queue = self.instance.get_remote_queue_name("agent")
agent_tasks.change_password.apply_async(
queue=queue, args=(self.instance.vm_name, self.instance.pw))
self.instance.save()
register_operation(PasswordResetOperation)
......@@ -71,3 +71,8 @@ def del_keys(vm, keys):
@celery.task(name='agent.get_keys')
def get_keys(vm):
pass
@celery.task(name='agent.send_expiration')
def send_expiration(vm, url):
pass
......@@ -35,3 +35,4 @@ South==0.8.4
sqlparse==0.1.11
pika==0.9.13
Fabric==1.9.0
lxml==3.3.5
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment