...
 
Commits (45)
......@@ -14,7 +14,7 @@
"bootstrap": "~3.2.0",
"fontawesome": "~4.3.0",
"jquery": "~2.1.1",
"no-vnc": "*",
"no-vnc": "0.5.1",
"jquery-knob": "~1.2.9",
"jquery-simple-slider": "https://github.com/BME-IK/jquery-simple-slider.git",
"bootbox": "~4.3.0",
......
......@@ -232,6 +232,14 @@ class ActivityModel(TimeStampedModel):
else:
return code
def get_status_id(self):
if self.succeeded is None:
return 'wait'
elif self.succeeded:
return 'success'
else:
return 'failed'
@celery.task()
def compute_cached(method, instance, memcached_seconds,
......
......@@ -1223,7 +1223,7 @@ class MyProfileForm(forms.ModelForm):
class Meta:
fields = ('preferred_language', 'email_notifications',
'use_gravatar', )
'desktop_notifications', 'use_gravatar', )
model = Profile
@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):
email_notifications = BooleanField(
verbose_name=_("Email notifications"), default=True,
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(
max_length=20,
verbose_name=_('Samba password'),
......
......@@ -169,6 +169,9 @@ $(function() {
);
} else {
in_progress = false;
if(document.hasFocus() === false && userWantNotifications()){
sendNotification(generateMessageFromLastActivity());
}
if(reload_vm_detail) location.reload();
if(runs > 1) addConnectText();
}
......@@ -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() {
var activities = $(".timeline .activity");
if(activities.length > 1) {
if(activities.eq(0).data("activity-code") == "vm.Instance.wake_up" ||
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() {
var hash = 0, i, chr, len;
if (this.length === 0) return hash;
......
......@@ -557,3 +557,11 @@ $(function () {
"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 {
}
.hover-black:hover {
color: black /*#d9534f*/;
color: black; /*#d9534f*/
text-decoration: none;
}
......@@ -1285,9 +1285,16 @@ textarea[name="new_members"] {
}
}
#vm-detail-successfull-boot {
#vm-detail-successful-boot {
margin-bottom: 20px;
display: none;
.label {
width: 100%;
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
#vm-detail-access-help {
......@@ -1523,3 +1530,7 @@ textarea[name="new_members"] {
text-align: center;
width: 100%;
}
#manage-access-select-all {
cursor: pointer;
}
......@@ -14,5 +14,4 @@
({% trans "username" %}: {{ user.username }})
{% endif %}
{% endif %}
{% endif %}
......@@ -6,7 +6,7 @@
<th></th>
<th>{% trans "Who" %}</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>
</thead>
<tbody>
......
......@@ -8,7 +8,7 @@
<a class="btn btn-default" href="{{object.get_absolute_url}}" data-dismiss="modal">
{% trans "Cancel" %}
</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"
href="{% url "request.views.request-lease" vm_pk=object.pk %}">
<i class="fa fa-forward"></i>
......
......@@ -12,6 +12,9 @@
{% block navbar %}
{% 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">
{% if request.user.is_superuser %}
{% if ADMIN_ENABLED %}
......
......@@ -56,6 +56,11 @@
<span class="label label-warning">{% trans "Offline" %}</span>
{% endif %}
</div>
<div>
{% for k, v in queues.iteritems %}
<span class="label label-{% if v %}success{% else %}danger{% endif %}">{{ k }}</span>
{% endfor %}
</div>
</div>
<div class="col-md-10" id="node-detail-pane">
<div class="panel panel-default" id="node-detail-panel">
......
......@@ -5,10 +5,11 @@
{% for a in activities %}
<div class="activity" data-activity-id="{{ a.pk }}">
<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>
<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>
<span title="{{ a.started }}">{{ a.started|arrowfilter:LANGUAGE_CODE }}</span>{% if a.user %}, {{ a.user }}{% endif %}
......@@ -19,7 +20,8 @@
<div data-activity-id="{{ s.pk }}"
class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}">
<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>
&ndash;
{% 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 @@
<dd>
<div class="input-group">
<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"
title="{% trans "Show password" %}" data-container="body">
<i class="fa fa-eye" id="vm-details-pw-eye"></i>
......@@ -192,11 +192,11 @@
{% endif %}
</div>
<div class="col-md-8" id="vm-detail-pane">
<div class="big" id="vm-detail-successfull-boot">
<span class="label label-info" data-status="{{ instance.status }}">
<div class="big" id="vm-detail-successful-boot">
<div class="label label-info" data-status="{{ instance.status }}">
<i class="fa fa-check"></i>
{% trans "The virtual machine successfully started, you can connect now." %}
</span>
</div>
</div>
<div class="panel panel-default" id="vm-detail-panel">
<ul class="nav nav-pills panel-heading">
......
......@@ -59,8 +59,8 @@
{% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %}
<span id="vm-details-renew-op">
{% with op=op.renew %}{% if op %}
<a href="{{op.get_url}}" class="btn btn-{{op.effect}} btn-xs
operation operation-{{op.op}}">
<a href="{{op.get_url}}" class="btn btn-xs operation operation-{{ op.op }}
{% if op.disabled %}btn-default disabled{% else %}btn-{{op.effect}}{% endif %}">
<i class="fa fa-{{op.icon}}"></i>
{{op.name}}
</a>
......
......@@ -270,33 +270,6 @@ class VmDetailTest(LoginMixin, MockCeleryMixin, TestCase):
self.assertEqual(InstanceTemplate.objects.get(id=1).raw_data,
"<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):
c = Client()
self.login(c, "user1")
......@@ -615,6 +588,12 @@ class NodeDetailTest(LoginMixin, MockCeleryMixin, TestCase):
node = Node.objects.get(pk=1)
trait, created = Trait.objects.get_or_create(name='testtrait')
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):
super(NodeDetailTest, self).tearDown()
......@@ -622,6 +601,7 @@ class NodeDetailTest(LoginMixin, MockCeleryMixin, TestCase):
self.u2.delete()
self.us.delete()
self.g1.delete()
self.patcher.stop()
def test_404_superuser_node_page(self):
c = Client()
......@@ -629,6 +609,12 @@ class NodeDetailTest(LoginMixin, MockCeleryMixin, TestCase):
response = c.get('/dashboard/node/25555/')
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):
c = Client()
self.login(c, 'user1')
......@@ -1758,3 +1744,76 @@ class SshKeyTest(LoginMixin, TestCase):
resp = c.post("/dashboard/sshkey/delete/1/")
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 (
GroupDetailView, GroupList, IndexView,
InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail,
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeList,
NodeDetailView, NodeList, NodeActivityDetail,
NotificationView, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList,
vm_activity, VmCreate, VmDetailView,
......@@ -133,6 +133,8 @@ urlpatterns = patterns(
name='dashboard.views.node-activity-list'),
url(r'^node/create/$', NodeCreate.as_view(),
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(),
name='dashboard.views.favourite'),
......
......@@ -37,6 +37,7 @@ from django_tables2 import SingleTableView
from firewall.models import Host
from vm.models import Node, NodeActivity, Trait
from vm.tasks.vm_tasks import check_queue
from ..forms import TraitForm, HostForm, NodeForm
from ..tables import NodeListTable
......@@ -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,
GraphMixin, DetailView):
template_name = "dashboard/node-detail.html"
......@@ -103,10 +118,17 @@ class NodeDetailView(LoginRequiredMixin,
context['ops'] = get_operations(self.object, self.request.user)
context['op'] = {i.op: i for i in context['ops']}
context['show_show_all'] = len(na) > 10
context['activities'] = na[:10]
context['activities'] = _format_activities(na[:10])
context['trait_form'] = form
context['graphite_enabled'] = (
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
def post(self, request, *args, **kwargs):
......@@ -298,8 +320,8 @@ class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View):
show_all = request.GET.get("show_all", "false") == "true"
node = Node.objects.get(pk=pk)
activities = NodeActivity.objects.filter(
node=node, parent=None).order_by('-started').select_related()
activities = _format_activities(NodeActivity.objects.filter(
node=node, parent=None).order_by('-started').select_related())
show_show_all = len(activities) > 10
if not show_all:
......@@ -316,3 +338,18 @@ class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View):
json.dumps(response),
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):
data=request.POST)
if form.is_valid():
form.save()
messages.success(self.request,
_("Password successfully changed."))
if form.is_valid():
return redirect_response
......
......@@ -104,6 +104,19 @@ class VmDetailView(GraphMixin, CheckedDetailView):
template_name = "dashboard/vm-detail.html"
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):
context = super(VmDetailView, self).get_context_data(**kwargs)
instance = context['instance']
......
......@@ -499,7 +499,11 @@ class Vlan(AclBase, models.Model):
def get_new_address(self):
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)
.values_list('ipv6', flat=True))
......
......@@ -77,13 +77,13 @@ class GetNewAddressTestCase(MockCeleryMixin, TestCase):
d = Domain(name='example.org', owner=self.u1)
d.save()
# /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,
owner=self.u1)
self.vlan.clean()
self.vlan.save()
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,
ipv4='10.0.0.%d' % i, vlan=self.vlan,
owner=self.u1).save()
......@@ -102,6 +102,15 @@ class GetNewAddressTestCase(MockCeleryMixin, TestCase):
owner=self.u1).save()
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):
used_v4 = IPSet(self.vlan.host_set.values_list('ipv4', flat=True))
assert self.vlan.get_new_address()['ipv4'] not in used_v4
......@@ -114,7 +123,7 @@ class HostGetHostnameTestCase(MockCeleryMixin, TestCase):
self.d = Domain(name='example.org', owner=self.u1)
self.d.save()
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,
owner=self.u1, network_type='portforward',
snat_ip='10.1.1.1')
......@@ -194,13 +203,13 @@ class ReloadTestCase(MockCeleryMixin, TestCase):
self.u1 = User.objects.create(username='user1')
self.u1.save()
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',
network6='2001:738:2001:4031::/80', domain=d,
owner=self.u1, network_type='portforward',
dhcp_pool='manual')
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,
owner=self.u1, network_type='public')
self.vlan2.save()
......
......@@ -73,8 +73,11 @@ class InitialFromFileMixin(object):
)
def clean_message(self):
def comp(x):
return "".join(x.strip().splitlines())
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")
return message.strip()
......
......@@ -38,6 +38,7 @@ class RequestTable(Table):
template_name="request/columns/user.html",
verbose_name=_("User"),
)
created = Column(verbose_name=_("Date"))
type = TemplateColumn(
template_name="request/columns/type.html",
verbose_name=_("Type"),
......@@ -48,7 +49,7 @@ class RequestTable(Table):
template = "django_tables2/with_pagination.html"
attrs = {'class': ('table table-bordered table-striped table-hover'),
'id': "request-list-table"}
fields = ("pk", "status", "type", "user", )
fields = ("pk", "status", "type", "created", "user", )
order_by = ("-pk", )
empty_text = _("No more requests.")
per_page = 10
......
......@@ -38,6 +38,9 @@
<pre>{{ object.message }}</pre>
</p>
<hr />
<div class="pull-right">
<strong>{% trans "Submitted" %}:</strong> {{ object.created }}
</div>
{% if object.type == "lease" %}
<dl>
<dt>{% trans "VM name" %}</dt>
......
......@@ -208,6 +208,12 @@ class VmRequestMixin(LoginRequiredMixin, object):
user = self.request.user
if not vm.has_level(user, self.user_level):
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)
def get_context_data(self, **kwargs):
......
......@@ -135,14 +135,6 @@ class InstanceActivity(ActivityModel):
def get_absolute_url(self):
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):
op = self.instance.get_operation_from_activity_code(self.activity_code)
return (self.task_uuid and op and op.has_percentage and
......@@ -215,6 +207,13 @@ class NodeActivity(ActivityModel):
app_label = 'vm'
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):
if self.parent:
return '{}({})->{}'.format(self.parent.activity_code,
......
......@@ -447,12 +447,17 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
if new_node is False: # None would be a valid value
new_node = self.node
# 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:
act = InstanceActivity.create(
code_suffix='vm_state_changed',
readable_name=create_readable(
ugettext_noop("vm state changed to %(state)s on %(node)s"),
state=new_state, node=new_node),
readable_name=create_readable(msg, state=new_state,
node=new_node),
instance=self)
except ActivityInProgressError:
pass # discard state change if another activity is in progress.
......@@ -675,7 +680,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
with self.activity('notification_about_expiration',
readable_name=ugettext_noop(
"notify owner about expiration"),
on_commit=on_commit):
on_commit=on_commit, concurrency_check=False):
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):
......
......@@ -160,6 +160,8 @@ class Node(OperatedMixin, TimeStampedModel):
"""
try:
self.get_remote_queue_name("vm", "fast")
self.get_remote_queue_name("vm", "slow")
self.get_remote_queue_name("net", "fast")
except:
return False
else:
......
......@@ -861,7 +861,9 @@ class ShutOffOperation(InstanceOperation):
def _operation(self, activity):
# 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._delete_vm(parent_activity=activity)
......
......@@ -57,7 +57,7 @@ def get_queues():
inspect = celery.control.inspect()
inspect.timeout = 0.5
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)
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
anyjson==0.3.3
arrow==0.6.0
arrow==0.7.0
billiard==3.3.0.20
bpython==0.14.1
celery==3.1.18
Django==1.8.2
Django==1.8.12
django-appconf==1.0.1
django-autocomplete-light==2.1.1
django-braces==1.8.0
django-crispy-forms==1.4.0
django-crispy-forms==1.6.0
django-model-utils==2.2
djangosaml2==0.13.0
django-sizefield==0.7
......