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): ...@@ -10,6 +10,18 @@ class AclUserAutocomplete(autocomplete_light.AutocompleteGenericBase):
('^name', 'groupprofile__org_id'), ('^name', 'groupprofile__org_id'),
) )
autocomplete_js_attributes = {'placeholder': _("Name of group or user")} 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): def choices_for_request(self):
user = self.request.user user = self.request.user
......
...@@ -54,6 +54,7 @@ from .models import Profile, GroupProfile ...@@ -54,6 +54,7 @@ from .models import Profile, GroupProfile
from circle.settings.base import LANGUAGES, MAX_NODE_RAM from circle.settings.base import LANGUAGES, MAX_NODE_RAM
from django.utils.translation import string_concat from django.utils.translation import string_concat
from .virtvalidator import domain_validator
LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")")) LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")"))
for l in LANGUAGES) for l in LANGUAGES)
...@@ -908,7 +909,8 @@ class VmRenewForm(forms.Form): ...@@ -908,7 +909,8 @@ class VmRenewForm(forms.Form):
self.fields['lease'] = forms.ModelChoiceField(queryset=choices, self.fields['lease'] = forms.ModelChoiceField(queryset=choices,
initial=default, initial=default,
required=True, required=False,
empty_label=None,
label=_('Length')) label=_('Length'))
if len(choices) < 2: if len(choices) < 2:
self.fields['lease'].widget = HiddenInput() self.fields['lease'].widget = HiddenInput()
...@@ -952,6 +954,25 @@ class VmDownloadDiskForm(forms.Form): ...@@ -952,6 +954,25 @@ class VmDownloadDiskForm(forms.Form):
return helper 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): class CircleAuthenticationForm(AuthenticationForm):
# fields: username, password # fields: username, password
...@@ -1236,6 +1257,9 @@ class TraitsForm(forms.ModelForm): ...@@ -1236,6 +1257,9 @@ class TraitsForm(forms.ModelForm):
class RawDataForm(forms.ModelForm): class RawDataForm(forms.ModelForm):
raw_data = forms.CharField(validators=[domain_validator],
widget=forms.Textarea(attrs={'rows': 5}),
required=False)
class Meta: class Meta:
model = Instance model = Instance
......
...@@ -70,7 +70,7 @@ class Notification(TimeStampedModel): ...@@ -70,7 +70,7 @@ class Notification(TimeStampedModel):
def send(cls, user, subject, template, context, def send(cls, user, subject, template, context,
valid_until=None, subject_context=None): valid_until=None, subject_context=None):
hro = create_readable(template, user=user, **context) 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, return cls.objects.create(to=user,
subject_data=subject.to_dict(), subject_data=subject.to_dict(),
message_data=hro.to_dict(), message_data=hro.to_dict(),
......
...@@ -716,6 +716,10 @@ textarea[name="list-new-namelist"] { ...@@ -716,6 +716,10 @@ textarea[name="list-new-namelist"] {
margin-top: -6px; margin-top: -6px;
} }
#show-all-activities-container {
margin: 20px 0 0 10px;
}
.vm-resources-sliders .row { .vm-resources-sliders .row {
margin-bottom: 15px; margin-bottom: 15px;
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
$(function() { $(function() {
/* vm operations */ /* 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'); var icon = $(this).children("i").addClass('fa-spinner fa-spin');
$.ajax({ $.ajax({
...@@ -50,6 +50,9 @@ $(function() { ...@@ -50,6 +50,9 @@ $(function() {
*/ */
if(data.success) { if(data.success) {
$('a[href="#activity"]').trigger("click"); $('a[href="#activity"]').trigger("click");
if(data.with_reload) {
location.reload();
}
/* if there are messages display them */ /* if there are messages display them */
if(data.messages && data.messages.length > 0) { if(data.messages && data.messages.length > 0) {
......
var show_all = false;
var in_progress = false;
$(function() { $(function() {
/* do we need to check for new activities */ /* do we need to check for new activities */
if(decideActivityRefresh()) { if(decideActivityRefresh()) {
checkNewActivity(false, 1); if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
} }
$('a[href="#activity"]').click(function(){ $('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('fa-spin'); $('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 */ /* save resources */
...@@ -134,11 +151,6 @@ $(function() { ...@@ -134,11 +151,6 @@ $(function() {
return false; return false;
}); });
/* show help */
$(".vm-details-help-button").click(function() {
$(".vm-details-help").stop().slideToggle();
});
/* for interface remove buttons */ /* for interface remove buttons */
$('.interface-remove').click(function() { $('.interface-remove').click(function() {
var interface_pk = $(this).data('interface-pk'); var interface_pk = $(this).data('interface-pk');
...@@ -320,12 +332,12 @@ function removePort(data) { ...@@ -320,12 +332,12 @@ function removePort(data) {
function decideActivityRefresh() { function decideActivityRefresh() {
var check = false; var check = false;
/* if something is still spinning */ /* if something is still spinning */
if($('.timeline .activity:first i:first').hasClass('fa-spin')) if($('.timeline .activity i').hasClass('fa-spin'))
check = true; check = true;
/* if there is only one activity */ /* if there is only one activity */
if($('#activity-timeline div[class="activity"]').length < 2) if($('#activity-timeline div[class="activity"]').length < 2)
check = true; check = true;
return check; return check;
} }
...@@ -340,25 +352,25 @@ function changeHTML(html) { ...@@ -340,25 +352,25 @@ function changeHTML(html) {
return html.replace(/data-original-title/g, "title").replace(/title=""/g, "").replace(/\//g, '').replace(/ /g, ''); return html.replace(/data-original-title/g, "title").replace(/title=""/g, "").replace(/\//g, '').replace(/ /g, '');
} }
function checkNewActivity(only_status, runs) { function checkNewActivity(runs) {
// set default only_status to false
only_status = typeof only_status !== 'undefined' ? only_status : false;
var instance = location.href.split('/'); instance = instance[instance.length - 2]; var instance = location.href.split('/'); instance = instance[instance.length - 2];
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/dashboard/vm/' + instance + '/activity/', url: '/dashboard/vm/' + instance + '/activity/',
data: {'only_status': only_status}, data: {'show_all': show_all},
success: function(data) { 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']); a = unescapeHTML(data['activities']);
b = changeHTML($("#activity-timeline").html()); b = changeHTML($("#activity-refresh").html());
if(a != b) 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();
} }
$("#ops").html(data['ops']);
$("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip();
$("#vm-details-state i").prop("class", "fa " + data['icon']); $("#vm-details-state i").prop("class", "fa " + data['icon']);
$("#vm-details-state span").html(data['human_readable_status'].toUpperCase()); $("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
...@@ -378,14 +390,16 @@ function checkNewActivity(only_status, runs) { ...@@ -378,14 +390,16 @@ function checkNewActivity(only_status, runs) {
if(runs > 0 && decideActivityRefresh()) { if(runs > 0 && decideActivityRefresh()) {
setTimeout( setTimeout(
function() {checkNewActivity(only_status, runs + 1)}, function() {checkNewActivity(runs + 1)},
1000 + Math.exp(runs * 0.05) 1000 + Math.exp(runs * 0.05)
); );
} else {
in_progress = false;
} }
$('a[href="#activity"] i').removeClass('fa-spin'); $('a[href="#activity"] i').removeClass('fa-spin');
}, },
error: function() { error: function() {
in_progress = false;
} }
}); });
} }
...@@ -81,4 +81,11 @@ ...@@ -81,4 +81,11 @@
{% block extra_etc %} {% block extra_etc %}
{% endblock %} {% endblock %}
<script>
yourlabs.TextWidget.prototype.getValue = function(choice) {
return choice.children().html();
}
</script>
</html> </html>
...@@ -18,23 +18,22 @@ ...@@ -18,23 +18,22 @@
{% endblock %} {% endblock %}
{% block navbar %} {% block navbar %}
{% 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">
{% trans "Notifications" %}
{% if NEW_NOTIFICATIONS_COUNT > 0 %}
<span class="badge badge-pulse">{{ NEW_NOTIFICATIONS_COUNT }}</span>
{% endif %}
</a>
<ul class="dropdown-menu notification-messages">
<li>{% trans "Loading..." %}</li>
</ul>
</li>
</ul>
<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">
{% trans "Notifications" %}
{% if NEW_NOTIFICATIONS_COUNT > 0 %}
<span class="badge badge-pulse">{{ NEW_NOTIFICATIONS_COUNT }}</span>
{% endif %}
</a>
<ul class="dropdown-menu notification-messages">
<li>{% trans "Loading..." %}</li>
</ul>
</li>
</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;"> <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" %} <i class="fa fa-sign-out"></i> {% trans "Log out" %}
</a> </a>
...@@ -48,7 +47,7 @@ ...@@ -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> <a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;"><i class="fa fa-cogs"></i> {% trans "Admin" %}</a>
{% endif %} {% endif %}
{% else %} {% 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 %} {% endif %}
{% endblock %} {% endblock %}
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<div class="page-header"> <div class="page-header">
<div class="pull-right" style="padding-top: 15px;"> <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 "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> <a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs group-details-help-button"><i class="fa fa-question"></i></a>
</div> </div>
<h1> <h1>
......
...@@ -14,17 +14,17 @@ ...@@ -14,17 +14,17 @@
</h1> </h1>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-4" id="vm-info-pane"> <div class="col-md-5" id="vm-info-pane">
<div class="big"> <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 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>{{ object.get_status_id|upper }}</span>
</span> </span>
</div> </div>
<div id="vm-activity-context" class="timeline">
{% include "dashboard/vm-detail/_activity-timeline.html" with active=object %} {% include "dashboard/vm-detail/_activity-timeline.html" with active=object %}
</div>
</div> </div>
<div class="col-md-8"> <div class="col-md-7">
<div class="panel panel-default"> <div class="panel panel-default">
<!--<div class="panel-heading"><h2 class="panel-title">{% trans "Activity" %}</h2></div> --> <!--<div class="panel-heading"><h2 class="panel-title">{% trans "Activity" %}</h2></div> -->
<div class="panel-body"> <div class="panel-body">
......
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
<i class="fa fa-desktop"></i> <i class="fa fa-desktop"></i>
{% trans "Virtual machines owned by the user" %} ({{ instances_owned|length }}) {% trans "Virtual machines owned by the user" %} ({{ instances_owned|length }})
</h4> </h4>
<ul class="dashboard-profile-vm-list"> <ul class="dashboard-profile-vm-list fa-ul">
{% for i in instances_owned %} {% for i in instances_owned %}
<li> <li>
<a href="{{ i.get_absolute_url }}"> <a href="{{ i.get_absolute_url }}">
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
<i class="fa fa-desktop"></i> <i class="fa fa-desktop"></i>
{% trans "Virtual machines with access" %} ({{ instances_with_access|length }}) {% trans "Virtual machines with access" %} ({{ instances_with_access|length }})
</h4> </h4>
<ul class="dashboard-profile-vm-list"> <ul class="dashboard-profile-vm-list fa-ul">
{% for i in instances_with_access %} {% for i in instances_with_access %}
<li> <li>
<a href="{{ i.get_absolute_url }}"> <a href="{{ i.get_absolute_url }}">
......
...@@ -98,17 +98,12 @@ ...@@ -98,17 +98,12 @@
</div> </div>
</dd> </dd>
<dd style="font-size: 10px; text-align: right; padding-top: 8px;"> <dd style="font-size: 10px; text-align: right; padding-top: 8px;">
<a id="vm-details-pw-change" href="#">{% trans "Generate new password!" %}</a> <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> </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>
</dl> </dl>
<div class="input-group" id="dashboard-vm-details-connect-command"> <div class="input-group" id="dashboard-vm-details-connect-command">
......
{% load i18n %} {% load i18n %}
<div id="activity-timeline" class="timeline">
{% for a in activities %} {% for a in activities %}
<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" data-activity-id="{{ a.pk }}"> <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 %}"> <span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
...@@ -7,7 +10,7 @@ ...@@ -7,7 +10,7 @@
<strong{% if a.result %} title="{{ a.result.get_user_text }}"{% endif %}> <strong{% if a.result %} title="{{ a.result.get_user_text }}"{% endif %}>
<a href="{{ a.get_absolute_url }}"> <a href="{{ a.get_absolute_url }}">
{% if a.times > 1 %}({{ a.times }}x){% endif %} {% 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 %} {% if a.has_percent %}
- {{ a.percentage }}% - {{ a.percentage }}%
...@@ -32,7 +35,7 @@ ...@@ -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 %}"> <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 %}> <span{% if s.result %} title="{{ s.result.get_user_text }}"{% endif %}>
<a href="{{ s.get_absolute_url }}"> <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 %} {% if s.finished %}
{{ s.finished|time:"H:i:s" }} {{ s.finished|time:"H:i:s" }}
{% else %} {% else %}
...@@ -47,3 +50,16 @@ ...@@ -47,3 +50,16 @@
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% 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 @@ ...@@ -2,6 +2,6 @@
<h3>{% trans "Activity" %}</h3> <h3>{% trans "Activity" %}</h3>
<div id="activity-timeline" class="timeline"> <div id="activity-refresh">
{% include "dashboard/vm-detail/_activity-timeline.html" %} {% include "dashboard/vm-detail/_activity-timeline.html" %}
</div> </div>
{% load i18n %} {% load i18n %}
{% load network_tags %} {% load network_tags %}
<h2> <h2>
<a href="#" id="vm-details-network-add" class="btn btn-success pull-right no-js-hidden"> <div id="vm-details-add-interface">
<i class="fa fa-plus"></i> {% trans "add interface" %} {% with op=op.add_interface %}{% if op %}
</a> <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" %} {% trans "Interfaces" %}
</h2> </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 %} {% for i in instance.interface_set.all %}
<div> <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): ...@@ -280,6 +280,7 @@ class RenewViewTest(unittest.TestCase):
view = vm_ops['renew'] view = vm_ops['renew']
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4: patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance) inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance" inst._meta.object_name = "Instance"
...@@ -288,7 +289,10 @@ class RenewViewTest(unittest.TestCase): ...@@ -288,7 +289,10 @@ class RenewViewTest(unittest.TestCase):
inst.has_level.return_value = True inst.has_level.return_value = True
go.return_value = inst go.return_value = inst
go4.return_value = MagicMock() 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 # success would redirect
def test_renew_by_owner_w_param(self): def test_renew_by_owner_w_param(self):
......
...@@ -24,8 +24,9 @@ from django.contrib.auth.models import User, Group ...@@ -24,8 +24,9 @@ from django.contrib.auth.models import User, Group
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from dashboard.views import VmAddInterfaceView
from vm.models import Instance, InstanceTemplate, Lease, Node, Trait 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 ..models import Profile
from firewall.models import Vlan, Host, VlanGroup from firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch from mock import Mock, patch
...@@ -142,33 +143,21 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -142,33 +143,21 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]}) response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]})
self.assertEqual(response.status_code, 302) 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): def test_unpermitted_password_change(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner') inst.set_level(self.u1, 'owner')
password = inst.pw 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(response.status_code, 403)
self.assertEqual(password, Instance.objects.get(pk=1).pw) self.assertEqual(password, Instance.objects.get(pk=1).pw)
def test_unpermitted_network_add_wo_perm(self): def test_unpermitted_network_add_wo_perm(self):
c = Client() c = Client()
self.login(c, "user2") 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) self.assertEqual(response.status_code, 403)
def test_unpermitted_network_add_wo_vlan_perm(self): def test_unpermitted_network_add_wo_vlan_perm(self):
...@@ -176,8 +165,18 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -176,8 +165,18 @@ class VmDetailTest(LoginMixin, TestCase):
self.login(c, "user2") self.login(c, "user2")
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
response = c.post("/dashboard/vm/1/", {'new_network_vlan': 1}) interface_count = inst.interface_set.count()
self.assertEqual(response.status_code, 403)
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): def test_permitted_network_add(self):
c = Client() c = Client()
...@@ -187,9 +186,12 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -187,9 +186,12 @@ class VmDetailTest(LoginMixin, TestCase):
vlan = Vlan.objects.get(id=1) vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u1, 'user') vlan.set_level(self.u1, 'user')
interface_count = inst.interface_set.count() interface_count = inst.interface_set.count()
response = c.post("/dashboard/vm/1/", with patch.object(AddInterfaceOperation, 'async') as mock_method:
{'new_network_vlan': 1}) mock_method.side_effect = inst.add_interface
response = c.post("/dashboard/vm/1/op/add_interface/",
{'vlan': 1})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
assert mock_method.called
self.assertEqual(inst.interface_set.count(), interface_count + 1) self.assertEqual(inst.interface_set.count(), interface_count + 1)
def test_permitted_network_delete(self): def test_permitted_network_delete(self):
...@@ -401,8 +403,7 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -401,8 +403,7 @@ class VmDetailTest(LoginMixin, TestCase):
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
vlan = Vlan.objects.get(id=1) vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user') vlan.set_level(self.u2, 'user')
response = c.post("/dashboard/vm/1/", inst.add_interface(user=self.u2, vlan=vlan)
{'new_network_vlan': 1})
host = Host.objects.get( host = Host.objects.get(
interface__in=inst.interface_set.all()) interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get( self.u2.user_permissions.add(Permission.objects.get(
...@@ -421,8 +422,7 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -421,8 +422,7 @@ class VmDetailTest(LoginMixin, TestCase):
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
vlan = Vlan.objects.get(id=1) vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user') vlan.set_level(self.u2, 'user')
response = c.post("/dashboard/vm/1/", inst.add_interface(user=self.u2, vlan=vlan)
{'new_network_vlan': 1})
host = Host.objects.get( host = Host.objects.get(
interface__in=inst.interface_set.all()) interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get( self.u2.user_permissions.add(Permission.objects.get(
......
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 ...@@ -13,6 +13,7 @@ from .instance import InstanceTemplate
from .instance import Instance from .instance import Instance
from .instance import post_state_changed from .instance import post_state_changed
from .instance import pre_state_changed from .instance import pre_state_changed
from .instance import pwgen
from .network import InterfaceTemplate from .network import InterfaceTemplate
from .network import Interface from .network import Interface
from .node import Node from .node import Node
...@@ -22,5 +23,5 @@ __all__ = [ ...@@ -22,5 +23,5 @@ __all__ = [
'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate', 'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate',
'Instance', 'instance_activity', 'post_state_changed', 'pre_state_changed', 'Instance', 'instance_activity', 'post_state_changed', 'pre_state_changed',
'InterfaceTemplate', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease', 'InterfaceTemplate', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease',
'node_activity', 'node_activity', 'pwgen'
] ]
...@@ -642,6 +642,13 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -642,6 +642,13 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
:param again: Notify already notified owners. :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(): if not again and self._is_notified_about_expiration():
return False return False
success, failed = [], [] success, failed = [], []
...@@ -666,20 +673,26 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -666,20 +673,26 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
readable_name=ugettext_noop( readable_name=ugettext_noop(
"notify owner about expiration"), "notify owner about expiration"),
on_commit=on_commit): on_commit=on_commit):
from dashboard.views import VmRenewView from dashboard.views import VmRenewView, absolute_url
level = self.get_level_object("owner") level = self.get_level_object("owner")
for u, ulevel in self.get_users_with_level(level__pk=level.pk): for u, ulevel in self.get_users_with_level(level__pk=level.pk):
try: try:
token = VmRenewView.get_token_url(self, u) token = VmRenewView.get_token_url(self, u)
u.profile.notify( u.profile.notify(
_('%s expiring soon') % unicode(self), ugettext_noop('%(instance)s expiring soon'),
'dashboard/notifications/vm-expiring.html', notification_msg, url=self.get_absolute_url(),
{'instance': self, 'token': token}, valid_until=min( instance=self, suspend=self.time_of_suspend,
self.time_of_delete, self.time_of_suspend)) token=token, delete=self.time_of_delete)
except Exception as e: except Exception as e:
failed.append((u, e)) failed.append((u, e))
else: else:
success.append(u) 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 return True
def is_expiring(self, threshold=0.1): def is_expiring(self, threshold=0.1):
...@@ -719,24 +732,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -719,24 +732,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
timezone.now() + lease.suspend_interval, timezone.now() + lease.suspend_interval,
timezone.now() + lease.delete_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): def select_node(self):
"""Returns the node the VM should be deployed or migrated to. """Returns the node the VM should be deployed or migrated to.
""" """
......
...@@ -33,8 +33,9 @@ from .tasks.local_tasks import ( ...@@ -33,8 +33,9 @@ from .tasks.local_tasks import (
) )
from .models import ( from .models import (
Instance, InstanceActivity, InstanceTemplate, Interface, Node, Instance, InstanceActivity, InstanceTemplate, Interface, Node,
NodeActivity, NodeActivity, pwgen
) )
from .tasks import agent_tasks
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -94,12 +95,21 @@ class AddInterfaceOperation(InstanceOperation): ...@@ -94,12 +95,21 @@ class AddInterfaceOperation(InstanceOperation):
"the VM.") "the VM.")
required_perms = () 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): def check_precond(self):
super(AddInterfaceOperation, self).check_precond() super(AddInterfaceOperation, self).check_precond()
if self.instance.status not in ['STOPPED', 'PENDING', 'RUNNING']: if self.instance.status not in ['STOPPED', 'PENDING', 'RUNNING']:
raise self.instance.WrongStateError(self.instance) raise self.instance.WrongStateError(self.instance)
def _operation(self, activity, user, system, vlan, managed=None): def _operation(self, activity, user, system, vlan, managed=None):
if not vlan.has_level(user, 'user'):
raise PermissionDenied()
if managed is None: if managed is None:
managed = vlan.managed managed = vlan.managed
...@@ -107,12 +117,15 @@ class AddInterfaceOperation(InstanceOperation): ...@@ -107,12 +117,15 @@ class AddInterfaceOperation(InstanceOperation):
managed=managed, owner=user, vlan=vlan) managed=managed, owner=user, vlan=vlan)
if self.instance.is_running: if self.instance.is_running:
with activity.sub_activity('attach_network'): try:
self.instance.attach_network(net) 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() net.deploy()
return net
def get_activity_name(self, kwargs): def get_activity_name(self, kwargs):
return create_readable(ugettext_noop("add %(vlan)s interface"), return create_readable(ugettext_noop("add %(vlan)s interface"),
vlan=kwargs['vlan']) vlan=kwargs['vlan'])
...@@ -867,3 +880,27 @@ class ResourcesOperation(InstanceOperation): ...@@ -867,3 +880,27 @@ class ResourcesOperation(InstanceOperation):
register_operation(ResourcesOperation) 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): ...@@ -71,3 +71,8 @@ def del_keys(vm, keys):
@celery.task(name='agent.get_keys') @celery.task(name='agent.get_keys')
def get_keys(vm): def get_keys(vm):
pass pass
@celery.task(name='agent.send_expiration')
def send_expiration(vm, url):
pass
...@@ -35,3 +35,4 @@ South==0.8.4 ...@@ -35,3 +35,4 @@ South==0.8.4
sqlparse==0.1.11 sqlparse==0.1.11
pika==0.9.13 pika==0.9.13
Fabric==1.9.0 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