Commit 01f0294e by Czémán Arnold

Merge branch 'master' into issue_439

parents d989daf2 3cff165b
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
"bootstrap": "~3.2.0", "bootstrap": "~3.2.0",
"fontawesome": "~4.3.0", "fontawesome": "~4.3.0",
"jquery": "~2.1.1", "jquery": "~2.1.1",
"no-vnc": "*", "no-vnc": "0.5.1",
"jquery-knob": "~1.2.9", "jquery-knob": "~1.2.9",
"jquery-simple-slider": "https://github.com/BME-IK/jquery-simple-slider.git", "jquery-simple-slider": "https://github.com/BME-IK/jquery-simple-slider.git",
"bootbox": "~4.3.0", "bootbox": "~4.3.0",
......
...@@ -232,6 +232,14 @@ class ActivityModel(TimeStampedModel): ...@@ -232,6 +232,14 @@ class ActivityModel(TimeStampedModel):
else: else:
return code return code
def get_status_id(self):
if self.succeeded is None:
return 'wait'
elif self.succeeded:
return 'success'
else:
return 'failed'
@celery.task() @celery.task()
def compute_cached(method, instance, memcached_seconds, def compute_cached(method, instance, memcached_seconds,
......
...@@ -1223,7 +1223,7 @@ class MyProfileForm(forms.ModelForm): ...@@ -1223,7 +1223,7 @@ class MyProfileForm(forms.ModelForm):
class Meta: class Meta:
fields = ('preferred_language', 'email_notifications', fields = ('preferred_language', 'email_notifications',
'use_gravatar', ) 'desktop_notifications', 'use_gravatar', )
model = Profile model = Profile
@property @property
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0003_message'),
]
operations = [
migrations.AddField(
model_name='profile',
name='desktop_notifications',
field=models.BooleanField(default=False, help_text='Whether user wants to get desktop notification when an activity has finished and the window is not in focus.', verbose_name='Desktop notifications'),
),
]
...@@ -184,6 +184,10 @@ class Profile(Model): ...@@ -184,6 +184,10 @@ class Profile(Model):
email_notifications = BooleanField( email_notifications = BooleanField(
verbose_name=_("Email notifications"), default=True, verbose_name=_("Email notifications"), default=True,
help_text=_('Whether user wants to get digested email notifications.')) help_text=_('Whether user wants to get digested email notifications.'))
desktop_notifications = BooleanField(
verbose_name=_("Desktop notifications"), default=False,
help_text=_('Whether user wants to get desktop notification when an '
'activity has finished and the window is not in focus.'))
smb_password = CharField( smb_password = CharField(
max_length=20, max_length=20,
verbose_name=_('Samba password'), verbose_name=_('Samba password'),
......
...@@ -169,6 +169,9 @@ $(function() { ...@@ -169,6 +169,9 @@ $(function() {
); );
} else { } else {
in_progress = false; in_progress = false;
if(document.hasFocus() === false && userWantNotifications()){
sendNotification(generateMessageFromLastActivity());
}
if(reload_vm_detail) location.reload(); if(reload_vm_detail) location.reload();
if(runs > 1) addConnectText(); if(runs > 1) addConnectText();
} }
...@@ -181,18 +184,49 @@ $(function() { ...@@ -181,18 +184,49 @@ $(function() {
} }
}); });
// Notification init
$(function(){
if(userWantNotifications())
Notification.requestPermission();
});
function generateMessageFromLastActivity(){
var ac = $("div.activity").first();
var error = ac.children(".timeline-icon-failed").length;
var sign = (error === 1) ? "❌ " : "✓ ";
var msg = ac.children("strong").text().replace(/\s+/g, " ");
return sign + msg;
}
function sendNotification(message) {
var options = { icon: "/static/dashboard/img/favicon.png"};
if (Notification.permission === "granted") {
var notification = new Notification(message, options);
}
else if (Notification.permission !== "denied") {
Notification.requestPermission(function (permission) {
if (permission === "granted") {
var notification = new Notification(message, options);
}
});
}
}
function userWantNotifications(){
var dn = $("#user-options").data("desktop_notifications");
return dn === "True";
}
function addConnectText() { function addConnectText() {
var activities = $(".timeline .activity"); var activities = $(".timeline .activity");
if(activities.length > 1) { if(activities.length > 1) {
if(activities.eq(0).data("activity-code") == "vm.Instance.wake_up" || if(activities.eq(0).data("activity-code") == "vm.Instance.wake_up" ||
activities.eq(0).data("activity-code") == "vm.Instance.agent") { activities.eq(0).data("activity-code") == "vm.Instance.agent") {
$("#vm-detail-successfull-boot").slideDown(500); $("#vm-detail-successful-boot").slideDown(500);
} }
} }
} }
String.prototype.hashCode = function() { String.prototype.hashCode = function() {
var hash = 0, i, chr, len; var hash = 0, i, chr, len;
if (this.length === 0) return hash; if (this.length === 0) return hash;
......
...@@ -557,3 +557,11 @@ $(function () { ...@@ -557,3 +557,11 @@ $(function () {
"alert-" + $(this).val()); "alert-" + $(this).val());
}); });
}); });
/* select all in template list */
$(function() {
$("#manage-access-select-all").click(function(e) {
var inputs = $(this).closest("table").find('input[type="checkbox"]');
inputs.prop("checked", !inputs.prop("checked"));
});
});
...@@ -284,7 +284,7 @@ a.hover-black { ...@@ -284,7 +284,7 @@ a.hover-black {
} }
.hover-black:hover { .hover-black:hover {
color: black /*#d9534f*/; color: black; /*#d9534f*/
text-decoration: none; text-decoration: none;
} }
...@@ -1285,9 +1285,16 @@ textarea[name="new_members"] { ...@@ -1285,9 +1285,16 @@ textarea[name="new_members"] {
} }
} }
#vm-detail-successfull-boot { #vm-detail-successful-boot {
margin-bottom: 20px; margin-bottom: 20px;
display: none; display: none;
.label {
width: 100%;
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
} }
#vm-detail-access-help { #vm-detail-access-help {
...@@ -1523,3 +1530,7 @@ textarea[name="new_members"] { ...@@ -1523,3 +1530,7 @@ textarea[name="new_members"] {
text-align: center; text-align: center;
width: 100%; width: 100%;
} }
#manage-access-select-all {
cursor: pointer;
}
...@@ -14,5 +14,4 @@ ...@@ -14,5 +14,4 @@
({% trans "username" %}: {{ user.username }}) ({% trans "username" %}: {{ user.username }})
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<th></th> <th></th>
<th>{% trans "Who" %}</th> <th>{% trans "Who" %}</th>
<th>{% trans "What" %}</th> <th>{% trans "What" %}</th>
<th><i class="fa fa-times"></i></th> <th><i id="manage-access-select-all" class="fa fa-times"></i></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<a class="btn btn-default" href="{{object.get_absolute_url}}" data-dismiss="modal"> <a class="btn btn-default" href="{{object.get_absolute_url}}" data-dismiss="modal">
{% trans "Cancel" %} {% trans "Cancel" %}
</a> </a>
{% if lease_types and not request.token_user %} {% if object.active and lease_types and not request.token_user %}
<a class="btn btn-primary" id="vm-renew-request-lease-button" <a class="btn btn-primary" id="vm-renew-request-lease-button"
href="{% url "request.views.request-lease" vm_pk=object.pk %}"> href="{% url "request.views.request-lease" vm_pk=object.pk %}">
<i class="fa fa-forward"></i> <i class="fa fa-forward"></i>
......
...@@ -12,6 +12,9 @@ ...@@ -12,6 +12,9 @@
{% block navbar %} {% block navbar %}
{% if request.user.is_authenticated and request.user.pk and not request.token_user %} {% if request.user.is_authenticated and request.user.pk and not request.token_user %}
<span id="user-options" data-desktop_notifications="{{ request.user.profile.desktop_notifications }}"><span>
<ul class="nav navbar-nav navbar-right" id="dashboard-menu"> <ul class="nav navbar-nav navbar-right" id="dashboard-menu">
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
{% if ADMIN_ENABLED %} {% if ADMIN_ENABLED %}
......
...@@ -56,6 +56,11 @@ ...@@ -56,6 +56,11 @@
<span class="label label-warning">{% trans "Offline" %}</span> <span class="label label-warning">{% trans "Offline" %}</span>
{% endif %} {% endif %}
</div> </div>
<div>
{% for k, v in queues.iteritems %}
<span class="label label-{% if v %}success{% else %}danger{% endif %}">{{ k }}</span>
{% endfor %}
</div>
</div> </div>
<div class="col-md-10" id="node-detail-pane"> <div class="col-md-10" id="node-detail-pane">
<div class="panel panel-default" id="node-detail-panel"> <div class="panel panel-default" id="node-detail-panel">
......
...@@ -5,10 +5,11 @@ ...@@ -5,10 +5,11 @@
{% for a in activities %} {% for a in activities %}
<div class="activity" data-activity-id="{{ a.pk }}"> <div class="activity" 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 %}">
<i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-plus{% endif %}"></i> <i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-{{a.icon}}{% endif %}"></i>
</span> </span>
<strong title="{{ a.result.get_admin_text }}"> <strong title="{{ a.result.get_admin_text }}">
{{ a.readable_name.get_admin_text|capfirst }} <a href="{{ a.get_absolute_url }}">
{{ a.readable_name.get_admin_text|capfirst }}</a>
</strong> </strong>
<span title="{{ a.started }}">{{ a.started|arrowfilter:LANGUAGE_CODE }}</span>{% if a.user %}, {{ a.user }}{% endif %} <span title="{{ a.started }}">{{ a.started|arrowfilter:LANGUAGE_CODE }}</span>{% if a.user %}, {{ a.user }}{% endif %}
...@@ -19,7 +20,8 @@ ...@@ -19,7 +20,8 @@
<div data-activity-id="{{ s.pk }}" <div data-activity-id="{{ s.pk }}"
class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}"> class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}">
<span title="{{ s.result.get_admin_text }}"> <span title="{{ s.result.get_admin_text }}">
{{ s.readable_name|get_text:user }} <a href="{{ s.get_absolute_url }}">
{{ s.readable_name|get_text:user }}</a>
</span> </span>
&ndash; &ndash;
{% if s.finished %} {% if s.finished %}
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load hro %}
{% block content %}
<div class="body-content">
<div class="page-header">
<h1>
<div class="pull-right" id="vm-activity-state">
<span class="label label-{% if object.get_status_id == 'wait' %}info{% else %}{% if object.succeeded %}success{% else %}danger{% endif %}{% endif %}">
<span>{{ object.get_status_id|upper }}</span>
</span>
</div>
<i class="fa fa-{{icon}}"></i>
{{ object.node.name }}: {{object.readable_name|get_text:user}}
</h1>
</div>
<div class="row">
<div class="col-md-6" id="vm-info-pane">
{% include "dashboard/vm-detail/_activity-timeline.html" with active=object %}
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-body">
<dl>
<dt>{% trans "activity code" %}</dt>
<dd>{{object.activity_code}}</dd>
<dt>{% trans "node" %}</dt>
<dd><a href="{{object.node.get_absolute_url}}">{{object.node}}</a></dd>
<dt>{% trans "time" %}</dt>
<dd>{{object.started|default:'n/a'}} → {{object.finished|default:'n/a'}}</dd>
<dt>{% trans "user" %}</dt>
<dd>
<a href="{{ object.user.profile.get_absolute_url }}">
{{object.user|default:'(system)'}}</a></dd>
<dt>{% trans "type" %}</dt>
<dd>
{% if object.parent %}
{% blocktrans with url=object.parent.get_absolute_url name=object.parent %}
subactivity of <a href="{{url}}">{{name}}</a>
{% endblocktrans %}
{% else %}{% trans "top level activity" %}{% endif %}
</dd>
<dt>{% trans "task uuid" %}</dt>
<dd>{{ object.task_uuid|default:'n/a' }}</dd>
<dt>{% trans "status" %}</dt>
<dd id="activity_status">{{ object.get_status_id }}</dd>
<dt>{% trans "result" %}</dt>
<dd><textarea class="form-control" id="activity_result_text">{{object.result|get_text:user}}</textarea></dd>
<dt>{% trans "subactivities" %}</dt>
{% for s in object.children.all %}
<dd>
<span{% if s.result %} title="{{ s.result|get_text:user }}"{% endif %}>
<a href="{{ s.get_absolute_url }}">
{{ s.readable_name|get_text:user|capfirst }}</a></span> &ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
<i class="fa fa-refresh fa-spin" class="sub-activity-loading-icon"></i>
{% endif %}
{% if s.has_failed %}
<div class="label label-danger">{% trans "failed" %}</div>
{% endif %}
</dd>
{% empty %}
<dd>{% trans "none" %}</dd>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
...@@ -132,7 +132,7 @@ ...@@ -132,7 +132,7 @@
<dd> <dd>
<div class="input-group"> <div class="input-group">
<input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags" <input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags"
value="{{ instance.pw }}" spellcheck="false"/> value="{{ instance.pw }}" spellcheck="false" autocomplete="new-password"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show" <span class="input-group-addon input-tags" id="vm-details-pw-show"
title="{% trans "Show password" %}" data-container="body"> title="{% trans "Show password" %}" data-container="body">
<i class="fa fa-eye" id="vm-details-pw-eye"></i> <i class="fa fa-eye" id="vm-details-pw-eye"></i>
...@@ -192,11 +192,11 @@ ...@@ -192,11 +192,11 @@
{% endif %} {% endif %}
</div> </div>
<div class="col-md-8" id="vm-detail-pane"> <div class="col-md-8" id="vm-detail-pane">
<div class="big" id="vm-detail-successfull-boot"> <div class="big" id="vm-detail-successful-boot">
<span class="label label-info" data-status="{{ instance.status }}"> <div class="label label-info" data-status="{{ instance.status }}">
<i class="fa fa-check"></i> <i class="fa fa-check"></i>
{% trans "The virtual machine successfully started, you can connect now." %} {% trans "The virtual machine successfully started, you can connect now." %}
</span> </div>
</div> </div>
<div class="panel panel-default" id="vm-detail-panel"> <div class="panel panel-default" id="vm-detail-panel">
<ul class="nav nav-pills panel-heading"> <ul class="nav nav-pills panel-heading">
......
...@@ -59,8 +59,8 @@ ...@@ -59,8 +59,8 @@
{% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %} {% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %}
<span id="vm-details-renew-op"> <span id="vm-details-renew-op">
{% with op=op.renew %}{% if op %} {% with op=op.renew %}{% if op %}
<a href="{{op.get_url}}" class="btn btn-{{op.effect}} btn-xs <a href="{{op.get_url}}" class="btn btn-xs operation operation-{{ op.op }}
operation operation-{{op.op}}"> {% if op.disabled %}btn-default disabled{% else %}btn-{{op.effect}}{% endif %}">
<i class="fa fa-{{op.icon}}"></i> <i class="fa fa-{{op.icon}}"></i>
{{op.name}} {{op.name}}
</a> </a>
......
...@@ -270,33 +270,6 @@ class VmDetailTest(LoginMixin, MockCeleryMixin, TestCase): ...@@ -270,33 +270,6 @@ class VmDetailTest(LoginMixin, MockCeleryMixin, TestCase):
self.assertEqual(InstanceTemplate.objects.get(id=1).raw_data, self.assertEqual(InstanceTemplate.objects.get(id=1).raw_data,
"<devices></devices>") "<devices></devices>")
def test_permitted_lease_delete_w_template_using_it(self):
c = Client()
self.login(c, 'superuser')
leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/1/")
self.assertEqual(response.status_code, 400)
self.assertEqual(leases, Lease.objects.count())
def test_permitted_lease_delete_w_template_not_using_it(self):
c = Client()
self.login(c, 'superuser')
lease = Lease.objects.create(name="yay")
leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/%d/" % lease.pk)
self.assertEqual(response.status_code, 302)
self.assertEqual(leases - 1, Lease.objects.count())
def test_unpermitted_lease_delete(self):
c = Client()
self.login(c, 'user1')
leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/1/")
# redirect to the login page
self.assertEqual(response.status_code, 403)
self.assertEqual(leases, Lease.objects.count())
def test_notification_read(self): def test_notification_read(self):
c = Client() c = Client()
self.login(c, "user1") self.login(c, "user1")
...@@ -615,6 +588,12 @@ class NodeDetailTest(LoginMixin, MockCeleryMixin, TestCase): ...@@ -615,6 +588,12 @@ class NodeDetailTest(LoginMixin, MockCeleryMixin, TestCase):
node = Node.objects.get(pk=1) node = Node.objects.get(pk=1)
trait, created = Trait.objects.get_or_create(name='testtrait') trait, created = Trait.objects.get_or_create(name='testtrait')
node.traits.add(trait) node.traits.add(trait)
self.patcher = patch("vm.tasks.vm_tasks.get_queues", return_value={
'x': [{'name': "devenv.vm.fast"}],
'y': [{'name': "devenv.vm.slow"}],
'z': [{'name': "devenv.net.fast"}],
})
self.patcher.start()
def tearDown(self): def tearDown(self):
super(NodeDetailTest, self).tearDown() super(NodeDetailTest, self).tearDown()
...@@ -622,6 +601,7 @@ class NodeDetailTest(LoginMixin, MockCeleryMixin, TestCase): ...@@ -622,6 +601,7 @@ class NodeDetailTest(LoginMixin, MockCeleryMixin, TestCase):
self.u2.delete() self.u2.delete()
self.us.delete() self.us.delete()
self.g1.delete() self.g1.delete()
self.patcher.stop()
def test_404_superuser_node_page(self): def test_404_superuser_node_page(self):
c = Client() c = Client()
...@@ -629,6 +609,12 @@ class NodeDetailTest(LoginMixin, MockCeleryMixin, TestCase): ...@@ -629,6 +609,12 @@ class NodeDetailTest(LoginMixin, MockCeleryMixin, TestCase):
response = c.get('/dashboard/node/25555/') response = c.get('/dashboard/node/25555/')
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
def test_200_superuser_node_page(self):
c = Client()
self.login(c, 'superuser')
response = c.get('/dashboard/node/1/')
self.assertEqual(response.status_code, 200)
def test_302_user_node_page(self): def test_302_user_node_page(self):
c = Client() c = Client()
self.login(c, 'user1') self.login(c, 'user1')
...@@ -1758,3 +1744,76 @@ class SshKeyTest(LoginMixin, TestCase): ...@@ -1758,3 +1744,76 @@ class SshKeyTest(LoginMixin, TestCase):
resp = c.post("/dashboard/sshkey/delete/1/") resp = c.post("/dashboard/sshkey/delete/1/")
self.assertEqual(403, resp.status_code) self.assertEqual(403, resp.status_code)
class LeaseDetailTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', ]
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
self.u2 = User.objects.create(username='user2', is_staff=True)
self.u2.set_password('password')
self.u2.save()
self.us = User.objects.create(username='superuser', is_superuser=True)
self.us.set_password('password')
self.us.save()
def tearDown(self):
super(LeaseDetailTest, self).tearDown()
self.u1.delete()
self.u2.delete()
self.us.delete()
def test_anon_view(self):
c = Client()
response = c.get("/dashboard/lease/1/")
self.assertEqual(response.status_code, 302)
def test_unpermitted_view(self):
c = Client()
self.login(c, 'user1')
response = c.get("/dashboard/lease/1/")
self.assertEqual(response.status_code, 302)
def test_operator_view(self):
c = Client()
self.login(c, 'user2')
lease = Lease.objects.get()
lease.set_level(self.u2, "owner")
response = c.get("/dashboard/lease/1/")
self.assertEqual(response.status_code, 200)
def test_superuser_view(self):
c = Client()
self.login(c, 'superuser')
response = c.get("/dashboard/lease/1/")
self.assertEqual(response.status_code, 200)
def test_permitted_lease_delete_w_template_using_it(self):
c = Client()
self.login(c, 'superuser')
leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/1/")
self.assertEqual(response.status_code, 400)
self.assertEqual(leases, Lease.objects.count())
def test_permitted_lease_delete_w_template_not_using_it(self):
c = Client()
self.login(c, 'superuser')
lease = Lease.objects.create(name="yay")
leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/%d/" % lease.pk)
self.assertEqual(response.status_code, 302)
self.assertEqual(leases - 1, Lease.objects.count())
def test_unpermitted_lease_delete(self):
c = Client()
self.login(c, 'user1')
leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/1/")
# redirect to the login page
self.assertEqual(response.status_code, 403)
self.assertEqual(leases, Lease.objects.count())
...@@ -25,7 +25,7 @@ from .views import ( ...@@ -25,7 +25,7 @@ from .views import (
GroupDetailView, GroupList, IndexView, GroupDetailView, GroupList, IndexView,
InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail, InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail,
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete, MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeList, NodeDetailView, NodeList, NodeActivityDetail,
NotificationView, TemplateAclUpdateView, TemplateCreate, NotificationView, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TemplateDelete, TemplateDetail, TemplateList,
vm_activity, VmCreate, VmDetailView, vm_activity, VmCreate, VmDetailView,
...@@ -136,6 +136,8 @@ urlpatterns = patterns( ...@@ -136,6 +136,8 @@ urlpatterns = patterns(
name='dashboard.views.node-activity-list'), name='dashboard.views.node-activity-list'),
url(r'^node/create/$', NodeCreate.as_view(), url(r'^node/create/$', NodeCreate.as_view(),
name='dashboard.views.node-create'), name='dashboard.views.node-create'),
url(r'^node/activity/(?P<pk>\d+)/$', NodeActivityDetail.as_view(),
name='dashboard.views.node-activity'),
url(r'^favourite/$', FavouriteView.as_view(), url(r'^favourite/$', FavouriteView.as_view(),
name='dashboard.views.favourite'), name='dashboard.views.favourite'),
......
...@@ -37,6 +37,7 @@ from django_tables2 import SingleTableView ...@@ -37,6 +37,7 @@ from django_tables2 import SingleTableView
from firewall.models import Host from firewall.models import Host
from vm.models import Node, NodeActivity, Trait from vm.models import Node, NodeActivity, Trait
from vm.tasks.vm_tasks import check_queue
from ..forms import TraitForm, HostForm, NodeForm from ..forms import TraitForm, HostForm, NodeForm
from ..tables import NodeListTable from ..tables import NodeListTable
...@@ -81,6 +82,20 @@ node_ops = OrderedDict([ ...@@ -81,6 +82,20 @@ node_ops = OrderedDict([
]) ])
def _get_activity_icon(act):
op = act.get_operation()
if op and op.id in node_ops:
return node_ops[op.id].icon
else:
return "cog"
def _format_activities(acts):
for i in acts:
i.icon = _get_activity_icon(i)
return acts
class NodeDetailView(LoginRequiredMixin, class NodeDetailView(LoginRequiredMixin,
GraphMixin, DetailView): GraphMixin, DetailView):
template_name = "dashboard/node-detail.html" template_name = "dashboard/node-detail.html"
...@@ -103,10 +118,17 @@ class NodeDetailView(LoginRequiredMixin, ...@@ -103,10 +118,17 @@ class NodeDetailView(LoginRequiredMixin,
context['ops'] = get_operations(self.object, self.request.user) context['ops'] = get_operations(self.object, self.request.user)
context['op'] = {i.op: i for i in context['ops']} context['op'] = {i.op: i for i in context['ops']}
context['show_show_all'] = len(na) > 10 context['show_show_all'] = len(na) > 10
context['activities'] = na[:10] context['activities'] = _format_activities(na[:10])
context['trait_form'] = form context['trait_form'] = form
context['graphite_enabled'] = ( context['graphite_enabled'] = (
settings.GRAPHITE_URL is not None) settings.GRAPHITE_URL is not None)
node_hostname = self.object.host.hostname
context['queues'] = {
'vmcelery.fast': check_queue(node_hostname, "vm", "fast"),
'vmcelery.slow': check_queue(node_hostname, "vm", "slow"),
'netcelery.fast': check_queue(node_hostname, "net", "fast"),
}
return context return context
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
...@@ -298,8 +320,8 @@ class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View): ...@@ -298,8 +320,8 @@ class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View):
show_all = request.GET.get("show_all", "false") == "true" show_all = request.GET.get("show_all", "false") == "true"
node = Node.objects.get(pk=pk) node = Node.objects.get(pk=pk)
activities = NodeActivity.objects.filter( activities = _format_activities(NodeActivity.objects.filter(
node=node, parent=None).order_by('-started').select_related() node=node, parent=None).order_by('-started').select_related())
show_show_all = len(activities) > 10 show_show_all = len(activities) > 10
if not show_all: if not show_all:
...@@ -316,3 +338,18 @@ class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View): ...@@ -316,3 +338,18 @@ class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View):
json.dumps(response), json.dumps(response),
content_type="application/json" content_type="application/json"
) )
class NodeActivityDetail(LoginRequiredMixin, SuperuserRequiredMixin,
DetailView):
model = NodeActivity
context_object_name = 'nodeactivity' # much simpler to mock object
template_name = 'dashboard/nodeactivity_detail.html'
def get_context_data(self, **kwargs):
ctx = super(NodeActivityDetail, self).get_context_data(**kwargs)
ctx['activities'] = _format_activities(NodeActivity.objects.filter(
node=self.object.node, parent=None
).order_by('-started').select_related())
ctx['icon'] = _get_activity_icon(self.object)
return ctx
...@@ -200,6 +200,8 @@ class MyPreferencesView(UpdateView): ...@@ -200,6 +200,8 @@ class MyPreferencesView(UpdateView):
data=request.POST) data=request.POST)
if form.is_valid(): if form.is_valid():
form.save() form.save()
messages.success(self.request,
_("Password successfully changed."))
if form.is_valid(): if form.is_valid():
return redirect_response return redirect_response
......
...@@ -105,6 +105,19 @@ class VmDetailView(GraphMixin, CheckedDetailView): ...@@ -105,6 +105,19 @@ class VmDetailView(GraphMixin, CheckedDetailView):
template_name = "dashboard/vm-detail.html" template_name = "dashboard/vm-detail.html"
model = Instance model = Instance
def get(self, *args, **kwargs):
if self.request.is_ajax():
return JsonResponse(self.get_json_data())
else:
return super(VmDetailView, self).get(*args, **kwargs)
def get_json_data(self):
instance = self.get_object()
return {"status": instance.status,
"host": instance.get_connect_host(),
"port": instance.get_connect_port(),
"password": instance.pw}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(VmDetailView, self).get_context_data(**kwargs) context = super(VmDetailView, self).get_context_data(**kwargs)
instance = context['instance'] instance = context['instance']
......
...@@ -499,7 +499,11 @@ class Vlan(AclBase, models.Model): ...@@ -499,7 +499,11 @@ class Vlan(AclBase, models.Model):
def get_new_address(self): def get_new_address(self):
hosts = self.host_set hosts = self.host_set
used_v4 = IPSet(hosts.values_list('ipv4', flat=True)) used_ext_addrs = Host.objects.filter(
external_ipv4__isnull=False).values_list(
'external_ipv4', flat=True)
used_v4 = IPSet(hosts.values_list('ipv4', flat=True)).union(
used_ext_addrs).union([self.network4.ip])
used_v6 = IPSet(hosts.exclude(ipv6__isnull=True) used_v6 = IPSet(hosts.exclude(ipv6__isnull=True)
.values_list('ipv6', flat=True)) .values_list('ipv6', flat=True))
......
...@@ -77,13 +77,13 @@ class GetNewAddressTestCase(MockCeleryMixin, TestCase): ...@@ -77,13 +77,13 @@ class GetNewAddressTestCase(MockCeleryMixin, TestCase):
d = Domain(name='example.org', owner=self.u1) d = Domain(name='example.org', owner=self.u1)
d.save() d.save()
# /29 = .1-.6 = 6 hosts/subnet + broadcast + network id # /29 = .1-.6 = 6 hosts/subnet + broadcast + network id
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/29', self.vlan = Vlan(vid=1, name='test', network4='10.0.0.1/29',
network6='2001:738:2001:4031::/80', domain=d, network6='2001:738:2001:4031::/80', domain=d,
owner=self.u1) owner=self.u1)
self.vlan.clean() self.vlan.clean()
self.vlan.save() self.vlan.save()
self.vlan.host_set.all().delete() self.vlan.host_set.all().delete()
for i in [1] + range(3, 6): for i in range(3, 6):
Host(hostname='h-%d' % i, mac='01:02:03:04:05:%02d' % i, Host(hostname='h-%d' % i, mac='01:02:03:04:05:%02d' % i,
ipv4='10.0.0.%d' % i, vlan=self.vlan, ipv4='10.0.0.%d' % i, vlan=self.vlan,
owner=self.u1).save() owner=self.u1).save()
...@@ -102,6 +102,15 @@ class GetNewAddressTestCase(MockCeleryMixin, TestCase): ...@@ -102,6 +102,15 @@ class GetNewAddressTestCase(MockCeleryMixin, TestCase):
owner=self.u1).save() owner=self.u1).save()
self.assertRaises(ValidationError, self.vlan.get_new_address) self.assertRaises(ValidationError, self.vlan.get_new_address)
def test_all_addr_in_use2(self):
Host(hostname='h-xd', mac='01:02:03:04:05:06',
ipv4='10.0.0.6', vlan=self.vlan,
owner=self.u1).save()
Host(hostname='h-arni', mac='01:02:03:04:05:02',
ipv4='100.0.0.1', vlan=self.vlan, external_ipv4='10.0.0.2',
owner=self.u1).save()
self.assertRaises(ValidationError, self.vlan.get_new_address)
def test_new_addr(self): def test_new_addr(self):
used_v4 = IPSet(self.vlan.host_set.values_list('ipv4', flat=True)) used_v4 = IPSet(self.vlan.host_set.values_list('ipv4', flat=True))
assert self.vlan.get_new_address()['ipv4'] not in used_v4 assert self.vlan.get_new_address()['ipv4'] not in used_v4
...@@ -114,7 +123,7 @@ class HostGetHostnameTestCase(MockCeleryMixin, TestCase): ...@@ -114,7 +123,7 @@ class HostGetHostnameTestCase(MockCeleryMixin, TestCase):
self.d = Domain(name='example.org', owner=self.u1) self.d = Domain(name='example.org', owner=self.u1)
self.d.save() self.d.save()
Record.objects.all().delete() Record.objects.all().delete()
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/24', self.vlan = Vlan(vid=1, name='test', network4='10.0.0.1/24',
network6='2001:738:2001:4031::/80', domain=self.d, network6='2001:738:2001:4031::/80', domain=self.d,
owner=self.u1, network_type='portforward', owner=self.u1, network_type='portforward',
snat_ip='10.1.1.1') snat_ip='10.1.1.1')
...@@ -194,13 +203,13 @@ class ReloadTestCase(MockCeleryMixin, TestCase): ...@@ -194,13 +203,13 @@ class ReloadTestCase(MockCeleryMixin, TestCase):
self.u1 = User.objects.create(username='user1') self.u1 = User.objects.create(username='user1')
self.u1.save() self.u1.save()
d = Domain.objects.create(name='example.org', owner=self.u1) d = Domain.objects.create(name='example.org', owner=self.u1)
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/29', self.vlan = Vlan(vid=1, name='test', network4='10.0.0.1/29',
snat_ip='152.66.243.99', snat_ip='152.66.243.99',
network6='2001:738:2001:4031::/80', domain=d, network6='2001:738:2001:4031::/80', domain=d,
owner=self.u1, network_type='portforward', owner=self.u1, network_type='portforward',
dhcp_pool='manual') dhcp_pool='manual')
self.vlan.save() self.vlan.save()
self.vlan2 = Vlan(vid=2, name='pub', network4='10.1.0.0/29', self.vlan2 = Vlan(vid=2, name='pub', network4='10.1.0.1/29',
network6='2001:738:2001:4032::/80', domain=d, network6='2001:738:2001:4032::/80', domain=d,
owner=self.u1, network_type='public') owner=self.u1, network_type='public')
self.vlan2.save() self.vlan2.save()
......
...@@ -73,8 +73,11 @@ class InitialFromFileMixin(object): ...@@ -73,8 +73,11 @@ class InitialFromFileMixin(object):
) )
def clean_message(self): def clean_message(self):
def comp(x):
return "".join(x.strip().splitlines())
message = self.cleaned_data['message'] message = self.cleaned_data['message']
if message.strip() == self.initial['message'].strip(): if comp(message) == comp(self.initial['message']):
raise ValidationError(_("Fill in the message."), code="invalid") raise ValidationError(_("Fill in the message."), code="invalid")
return message.strip() return message.strip()
......
...@@ -38,6 +38,7 @@ class RequestTable(Table): ...@@ -38,6 +38,7 @@ class RequestTable(Table):
template_name="request/columns/user.html", template_name="request/columns/user.html",
verbose_name=_("User"), verbose_name=_("User"),
) )
created = Column(verbose_name=_("Date"))
type = TemplateColumn( type = TemplateColumn(
template_name="request/columns/type.html", template_name="request/columns/type.html",
verbose_name=_("Type"), verbose_name=_("Type"),
...@@ -48,7 +49,7 @@ class RequestTable(Table): ...@@ -48,7 +49,7 @@ class RequestTable(Table):
template = "django_tables2/with_pagination.html" template = "django_tables2/with_pagination.html"
attrs = {'class': ('table table-bordered table-striped table-hover'), attrs = {'class': ('table table-bordered table-striped table-hover'),
'id': "request-list-table"} 'id': "request-list-table"}
fields = ("pk", "status", "type", "user", ) fields = ("pk", "status", "type", "created", "user", )
order_by = ("-pk", ) order_by = ("-pk", )
empty_text = _("No more requests.") empty_text = _("No more requests.")
per_page = 10 per_page = 10
......
...@@ -38,6 +38,9 @@ ...@@ -38,6 +38,9 @@
<pre>{{ object.message }}</pre> <pre>{{ object.message }}</pre>
</p> </p>
<hr /> <hr />
<div class="pull-right">
<strong>{% trans "Submitted" %}:</strong> {{ object.created }}
</div>
{% if object.type == "lease" %} {% if object.type == "lease" %}
<dl> <dl>
<dt>{% trans "VM name" %}</dt> <dt>{% trans "VM name" %}</dt>
......
...@@ -208,6 +208,12 @@ class VmRequestMixin(LoginRequiredMixin, object): ...@@ -208,6 +208,12 @@ class VmRequestMixin(LoginRequiredMixin, object):
user = self.request.user user = self.request.user
if not vm.has_level(user, self.user_level): if not vm.has_level(user, self.user_level):
raise PermissionDenied() raise PermissionDenied()
if vm.destroyed_at:
message = _("Instance %(instance)s has already been destroyed.")
messages.error(self.request, message % {'instance': vm.name})
return redirect(vm.get_absolute_url())
return super(VmRequestMixin, self).dispatch(*args, **kwargs) return super(VmRequestMixin, self).dispatch(*args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
......
...@@ -135,14 +135,6 @@ class InstanceActivity(ActivityModel): ...@@ -135,14 +135,6 @@ class InstanceActivity(ActivityModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dashboard.views.vm-activity', args=[self.pk]) return reverse('dashboard.views.vm-activity', args=[self.pk])
def get_status_id(self):
if self.succeeded is None:
return 'wait'
elif self.succeeded:
return 'success'
else:
return 'failed'
def has_percentage(self): def has_percentage(self):
op = self.instance.get_operation_from_activity_code(self.activity_code) op = self.instance.get_operation_from_activity_code(self.activity_code)
return (self.task_uuid and op and op.has_percentage and return (self.task_uuid and op and op.has_percentage and
...@@ -215,6 +207,13 @@ class NodeActivity(ActivityModel): ...@@ -215,6 +207,13 @@ class NodeActivity(ActivityModel):
app_label = 'vm' app_label = 'vm'
db_table = 'vm_nodeactivity' db_table = 'vm_nodeactivity'
def get_operation(self):
return self.node.get_operation_from_activity_code(
self.activity_code)
def get_absolute_url(self):
return reverse('dashboard.views.node-activity', args=[self.pk])
def __unicode__(self): def __unicode__(self):
if self.parent: if self.parent:
return '{}({})->{}'.format(self.parent.activity_code, return '{}({})->{}'.format(self.parent.activity_code,
......
...@@ -448,12 +448,17 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -448,12 +448,17 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
if new_node is False: # None would be a valid value if new_node is False: # None would be a valid value
new_node = self.node new_node = self.node
# log state change # log state change
if new_node:
msg = ugettext_noop("vm state changed to %(state)s on %(node)s")
else:
msg = ugettext_noop("vm state changed to %(state)s")
try: try:
act = InstanceActivity.create( act = InstanceActivity.create(
code_suffix='vm_state_changed', code_suffix='vm_state_changed',
readable_name=create_readable( readable_name=create_readable(msg, state=new_state,
ugettext_noop("vm state changed to %(state)s on %(node)s"), node=new_node),
state=new_state, node=new_node),
instance=self) instance=self)
except ActivityInProgressError: except ActivityInProgressError:
pass # discard state change if another activity is in progress. pass # discard state change if another activity is in progress.
...@@ -676,7 +681,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -676,7 +681,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
with self.activity('notification_about_expiration', with self.activity('notification_about_expiration',
readable_name=ugettext_noop( readable_name=ugettext_noop(
"notify owner about expiration"), "notify owner about expiration"),
on_commit=on_commit): on_commit=on_commit, concurrency_check=False):
from dashboard.views import VmRenewView, absolute_url 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):
......
...@@ -160,6 +160,8 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -160,6 +160,8 @@ class Node(OperatedMixin, TimeStampedModel):
""" """
try: try:
self.get_remote_queue_name("vm", "fast") self.get_remote_queue_name("vm", "fast")
self.get_remote_queue_name("vm", "slow")
self.get_remote_queue_name("net", "fast")
except: except:
return False return False
else: else:
......
...@@ -861,7 +861,9 @@ class ShutOffOperation(InstanceOperation): ...@@ -861,7 +861,9 @@ class ShutOffOperation(InstanceOperation):
def _operation(self, activity): def _operation(self, activity):
# Shutdown networks # Shutdown networks
with activity.sub_activity('shutdown_net'): with activity.sub_activity('shutdown_net',
readable_name=ugettext_noop(
"shutdown network")):
self.instance.shutdown_net() self.instance.shutdown_net()
self.instance._delete_vm(parent_activity=activity) self.instance._delete_vm(parent_activity=activity)
......
...@@ -57,7 +57,7 @@ def get_queues(): ...@@ -57,7 +57,7 @@ def get_queues():
inspect = celery.control.inspect() inspect = celery.control.inspect()
inspect.timeout = 0.5 inspect.timeout = 0.5
result = inspect.active_queues() result = inspect.active_queues()
logger.debug('Queue list of length %d cached.', len(result)) logger.debug('Queue list of length %d cached.', result and len(result))
cache.set(key, result, 10) cache.set(key, result, 10)
return result return result
......
[Unit]
Description=CIRCLE portal
After=network.target
[Service]
User=cloud
Group=cloud
WorkingDirectory=/home/cloud/circle/circle
ExecStart=/bin/bash -c "source /etc/profile; workon circle; exec /home/cloud/.virtualenvs/circle/bin/uwsgi --chdir=/home/cloud/circle/circle -H /home/cloud/.virtualenvs/circle --socket /tmp/uwsgi.sock --wsgi-file circle/wsgi.py --chmod-socket=666"
Restart=always
[Install]
WantedBy=multi-user.target
amqp==1.4.6 amqp==1.4.6
anyjson==0.3.3 anyjson==0.3.3
arrow==0.6.0 arrow==0.7.0
billiard==3.3.0.20 billiard==3.3.0.20
bpython==0.14.1 bpython==0.14.1
celery==3.1.18 celery==3.1.18
Django==1.8.2 Django==1.8.12
django-appconf==1.0.1 django-appconf==1.0.1
django-autocomplete-light==2.1.1 django-autocomplete-light==2.1.1
django-braces==1.8.0 django-braces==1.8.0
django-crispy-forms==1.4.0 django-crispy-forms==1.6.0
django-model-utils==2.2 django-model-utils==2.2
djangosaml2==0.13.0 djangosaml2==0.13.0
django-sizefield==0.7 django-sizefield==0.7
......
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