Commit 5cbfba57 by Kálmán Viktor

Merge remote-tracking branch 'origin/master' into issue-24

Conflicts:
	circle/dashboard/urls.py
parents 391c72e1 99986b1b
============= ============
cirecle-cloud circle-cloud
============= ============
This is the Django based controller and web portal of the CIRCLE Cloud. This is the Django based controller and web portal of the CIRCLE Cloud.
\ No newline at end of file
[
{
"pk": 1,
"model": "firewall.vlan",
"fields": {
"comment": "",
"ipv6_template": "2001:7:2:4031:%(b)d:%(c)d:%(d)d:0",
"domain": 1,
"dhcp_pool": "",
"managed": true,
"name": "pub",
"vid": 3066,
"created_at": "2014-02-19T17:00:17.358Z",
"modified_at": "2014-02-19T17:00:17.358Z",
"owner": null,
"snat_ip": null,
"snat_to": [],
"network6": null,
"network4": "10.7.0.93/16",
"reverse_domain": "%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa",
"network_type": "public",
"description": ""
}
},
{
"pk": 1,
"model": "firewall.host",
"fields": {
"comment": "",
"vlan": 1,
"reverse": "",
"created_at": "2014-02-19T17:03:45.365Z",
"hostname": "devenv",
"modified_at": "2014-02-24T15:55:01.412Z",
"location": "",
"pub_ipv4": null,
"mac": "11:22:33:44:55:66",
"shared_ip": false,
"ipv4": "10.7.0.96",
"groups": [],
"ipv6": null,
"owner": 1,
"description": ""
}
},
{
"pk": 1,
"model": "firewall.domain",
"fields": {
"description": "",
"created_at": "2014-02-19T17:00:08.819Z",
"modified_at": "2014-02-19T17:00:08.819Z",
"ttl": 600,
"owner": 1,
"name": "test.ik.bme.hu"
}
},
{
"pk": 1,
"model": "vm.node",
"fields": {
"name": "devenv",
"created": "2014-02-19T17:03:45.322Z",
"overcommit": 1.0,
"enabled": true,
"modified": "2014-02-19T21:11:34.671Z",
"priority": 1,
"traits": [],
"host": 1
}
}
]
...@@ -332,8 +332,6 @@ a.hover-black { ...@@ -332,8 +332,6 @@ a.hover-black {
} }
<<<<<<< HEAD
.notification-messages { .notification-messages {
padding: 10px 8px; padding: 10px 8px;
width: 350px; width: 350px;
......
...@@ -4,16 +4,29 @@ $(function() { ...@@ -4,16 +4,29 @@ $(function() {
/* vm migrate */ /* vm migrate */
$('.vm-migrate').click(function(e) { $('.vm-migrate').click(function(e) {
var icon = $(this).children("i");
var vm = $(this).data("vm-pk"); var vm = $(this).data("vm-pk");
icon.removeClass("icon-truck").addClass("icon-spinner icon-spin");
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/dashboard/vm/' + vm + '/migrate/', url: '/dashboard/vm/' + vm + '/migrate/',
success: function(data) { success: function(data) {
icon.addClass("icon-truck").removeClass("icon-spinner icon-spin");
$('body').append(data); $('body').append(data);
$('#create-modal').modal('show'); $('#create-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() { $('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove(); $('#create-modal').remove();
}); });
$('#vm-migrate-node-list li').click(function(e) {
var li = $(this).closest('li');
if (li.find('input').attr('disabled'))
return true;
$('#vm-migrate-node-list li').removeClass('panel-primary');
li.addClass('panel-primary').find('input').attr('checked', true);
return false;
});
$('#vm-migrate-node-list li input:checked').closest('li').addClass('panel-primary');
} }
}); });
return false; return false;
......
$(function() {
"use strict";
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"input.js", "display.js", "jsunzip.js", "rfb.js"]);
var rfb;
function updateState(rfb, state, oldstate, msg) {
$('#_console .btn-toolbar button').attr('disabled', !(state === "normal"));
rfb.sendKey(0xffe3); // press and release ctrl to kill screensaver
if (typeof(msg) !== 'undefined') {
$('#noVNC_status').html(msg);
}
}
$('a[data-toggle$="pill"][href!="#console"]').click(function() {
if (rfb) {
rfb.disconnect();
rfb = 0;
}
$("#vm-info-pane").fadeIn();
$("#vm-detail-pane").removeClass("col-md-12");
});
$('#sendCtrlAltDelButton').click(function() {
rfb.sendCtrlAltDel(); return false;});
$('#sendPasswordButton').click(function() {
var pw = $("#vm-details-pw-input").val();
for (var i=0; i < pw.length; i++) {
rfb.sendKey(pw.charCodeAt(i));
} return false;});
$("body").on("click", 'a[href$="console"]', function() {
var host, port, password, path;
$("#vm-info-pane").hide();
$("#vm-detail-pane").addClass("col-md-12");
WebUtil.init_logging('warn');
host = window.location.hostname;
if (window.location.port == 8080) {
port = 9999;
} else {
port = window.location.port == "" ? "443" : window.location.port;
}
password = '';
$('#_console .btn-toolbar button').attr('disabled', true);
$('#noVNC_status').html('Retreiving authorization token.');
$.get(VNC_URL, function(data) {
if (data.indexOf('vnc') != 0) {
$('#noVNC_status').html('No authorization token received.');
}
else {
rfb = new RFB({'target': $D('noVNC_canvas'),
'encrypt': (window.location.protocol === "https:"),
'true_color': true,
'local_cursor': true,
'shared': true,
'view_only': false,
'updateState': updateState});
rfb.connect(host, port, password, data);
}
}).fail(function(){
$('#noVNC_status').html("Can't connect to console.");
});
});
if (window.location.hash == "#console")
window.onscriptsload = function(){$('a[href$="console"]').click();};
});
...@@ -131,7 +131,11 @@ $(function() { ...@@ -131,7 +131,11 @@ $(function() {
location.reload(); location.reload();
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
if (xhr.status == 500) {
addMessage("Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
} }
}); });
} else { } else {
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
</span> </span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
<div class="notification-message-text"> <div class="notification-message-text">
{{ n.message }} {{ n.message|safe }}
</div> </div>
</li> </li>
{% empty %} {% empty %}
......
{% load i18n %} {% load i18n %}
{% load sizefieldtags %} {% load sizefieldtags %}
<form method="POST" action="{% url "dashboard.views.vm-migrate" pk=vm %}"> <form method="POST" action="{% url "dashboard.views.vm-migrate" pk=vm.pk %}">
{% csrf_token %} {% csrf_token %}
<ul id="vm-migrate-node-list"> <ul id="vm-migrate-node-list">
{% with current=vm.node.pk selected=vm.select_node.pk %}
{% for n in nodes %} {% for n in nodes %}
<li> <li class="panel panel-default"><div class="panel-body">
<label for="migrate-to-{{n.pk}}">
<strong>{{ n }}</strong> <strong>{{ n }}</strong>
<input type="radio" name="node" value="{{ n.pk }}" style="float: right;"/> {% if current == n.pk %}<div class="label label-info">{% trans "current" %}</div>{% endif %}
{% if selected == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %}
</label>
<input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;"
{% if current == n.pk %}disabled="disabled"{% endif %}
{% if selected == n.pk %}checked="checked"{% endif %} />
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span> <span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span> <span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
</li> </div></li>
{% endfor %} {% endfor %}
{% endwith %}
</ul> </ul>
<button type="submit" class="btn btn-primary btn-sm"><i class="icon-truck"></i> Migrate</button> <button type="submit" class="btn btn-primary btn-sm"><i class="icon-truck"></i> Migrate</button>
</form> </form>
{%load i18n%}
{%blocktrans with instance=instance.name user=user.name%}
Your ownership offer of {{instance}} has been accepted by {{user}}.
{%endblocktrans%}
{%load i18n%}
{%blocktrans with instance=instance.name user=user.name%}
{{user}} offered you to take the ownership of his/her virtual machine
called {{instance}}.{%endblocktrans%}
<a href="{{token}}" class="btn btn-success btn-small">{%trans "Accept"%}</a>
...@@ -80,7 +80,7 @@ ...@@ -80,7 +80,7 @@
<dt>Password:</dt> <dt>Password:</dt>
<dd> <dd>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control input-sm input-tags" value="{{ instance.pw }}"/> <input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags" value="{{ instance.pw }}"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show"> <span class="input-group-addon input-tags" id="vm-details-pw-show">
<i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i> <i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i>
</span> </span>
...@@ -113,8 +113,8 @@ ...@@ -113,8 +113,8 @@
<i class="icon-tasks icon-2x"></i><br> <i class="icon-tasks icon-2x"></i><br>
{% trans "Resources" %}</a> {% trans "Resources" %}</a>
</li> </li>
<li {% if instance.state != "RUNNING" %}class="disabled"{% endif %}> <li {% if not instance.is_console_available %}class="disabled"{% endif %}>
<a href="#{% if instance.state == "RUNNING" %}console" data-toggle="pill{% endif %}" data-target="#_console" class="text-center"> <a href="#console" data-toggle="pill" data-target="#_console" class="text-center">
<i class="icon-desktop icon-2x"></i><br> <i class="icon-desktop icon-2x"></i><br>
{% trans "Console" %}</a></li> {% trans "Console" %}</a></li>
<li> <li>
...@@ -152,4 +152,5 @@ ...@@ -152,4 +152,5 @@
{% block extra_js %} {% block extra_js %}
<script src="{{ STATIC_URL }}dashboard/vm-details.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-details.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-common.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-common.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-console.js"></script>
{% endblock %} {% endblock %}
...@@ -4,13 +4,16 @@ ...@@ -4,13 +4,16 @@
<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="{% if not a.finished %} icon-refresh icon-spin {% else %}icon-plus{% endif %}"></i> <i class="{% if not a.finished %} icon-refresh icon-spin {% else %}icon-plus{% endif %}"></i>
</span> </span>
<strong>{{ a.get_readable_name }}</strong> <strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% endif %}>
{{ a.started|date:"Y-m-d H:i" }}, {{ a.user }} {{ a.get_readable_name }}
</strong>
{{ a.started|date:"Y-m-d H:i" }}{% if a.user %}, {{ a.user }}{% endif %}
{% if a.children.count > 0 %} {% if a.children.count > 0 %}
<div class="sub-timeline"> <div class="sub-timeline">
{% for s in a.children.all %} {% for s in a.children.all %}
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}"> <div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}">
{{ s.get_readable_name }} - <span{% if user.is_superuser and s.result %} title="{{ s.result }}"{% endif %}>
{{ s.get_readable_name }}</span> &ndash;
{% if s.finished %} {% if s.finished %}
{{ s.finished|time:"H:i:s" }} {{ s.finished|time:"H:i:s" }}
{% else %} {% else %}
......
{% load i18n %} {% load i18n %}
<h3>{% trans "Owner" %}</h3> <h3>{% trans "Owner" %}</h3>
<p> <p>
{% if user == instance.owner %}
{% blocktrans %}You are the current owner of this instance.{% endblocktrans %} {% blocktrans %}You are the current owner of this instance.{% endblocktrans %}
<a href="#" class="btn btn-link">{% trans "Transfer ownership..." %}</a> {% else %}
{% blocktrans with owner=instance.owner %}
The current owner of this instance is {{owner}}.
{% endblocktrans %}
{% endif %}
{% if user == instance.owner or user.is_superuser %}
<a href="{% url "dashboard.views.vm-transfer-ownership" instance.pk %}"
class="btn btn-link">{% trans "Transfer ownership..." %}</a>
{% endif %}
</p> </p>
<h3>{% trans "Permissions"|capfirst %}</h3> <h3>{% trans "Permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %} <form action="{{acl.url}}" method="post">{% csrf_token %}
......
<div class="btn-toolbar"> <div class="btn-toolbar">
<button id="sendCtrlAltDelButton" class="btn btn-danger small" href="#">Send CtrlAltDel</button> <button id="sendCtrlAltDelButton" class="btn btn-danger small">Send CtrlAltDel</button>
<button id="sendPasswordButton" class="btn btn-default small" href="#">Type password</button> <button id="sendPasswordButton" class="btn btn-default small">Type password</button>
</div> </div>
<div class="alert alert-info" id="noVNC_status"> <div class="alert alert-info" id="noVNC_status">
</div> </div>
...@@ -10,68 +10,6 @@ ...@@ -10,68 +10,6 @@
<script src="{{ STATIC_URL }}dashboard/novnc/util.js"></script> <script src="{{ STATIC_URL }}dashboard/novnc/util.js"></script>
<script> <script>
"use strict";
var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/'; var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/';
var VNC_URL = "{{ vnc_url }}";
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", </script>
"input.js", "display.js", "jsunzip.js", "rfb.js"]);
var rfb;
function updateState(rfb, state, oldstate, msg) {
var s, sb, cad
s = $('#noVNC_status')[0];
cad = $('#sendCtrlAltDelButton')[0];
if (state === "normal") { cad.disabled = false; }
else { cad.disabled = true; }
if (typeof(msg) !== 'undefined') {
s.innerHTML = msg;
}
}
$('a[data-toggle$="pill"][href!="#console"]').click(function() {
if (rfb) {
rfb.disconnect();
rfb = 0;
}
$("#vm-info-pane").fadeIn();
$("#vm-detail-pane").removeClass("col-md-12");
});
$('#sendCtrlAltDelButton').click(function() {
rfb.sendCtrlAltDel(); return false;});
$('#sendPasswordButton').click(function() {
var pw = '{{instance.pw}}';
for (var i=0; i < pw.length; i++) {
rfb.sendKey(pw.charCodeAt(i));
} return false;});
$("body").on("click", 'a[href$="console"]', function() {
var host, port, password, path;
$("#vm-info-pane").hide();
$("#vm-detail-pane").addClass("col-md-12");
WebUtil.init_logging('warn');
host = window.location.hostname;
if (window.location.port == 8080) {
port = 9999;
} else {
port = window.location.port == "" ? "443" : window.location.port;
}
password = '';
path = 'vnc/?d={{ vnc_url }}';
rfb = new RFB({'target': $D('noVNC_canvas'),
'encrypt': (window.location.protocol === "https:"),
'true_color': true,
'local_cursor': true,
'shared': true,
'view_only': false,
'updateState': updateState});
rfb.connect(host, port, password, path);
});
</script>
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
<div class="pull-right"> <div class="pull-right">
<form action="" method="POST"> <form action="" method="POST">
{% csrf_token %} {% csrf_token %}
E-mail address or identifier of user:
<input name="name"> <input name="name">
<input type="submit"> <input type="submit">
</form> </form>
......
...@@ -15,7 +15,7 @@ class NotificationTestCase(TestCase): ...@@ -15,7 +15,7 @@ class NotificationTestCase(TestCase):
def test_notification_send(self): def test_notification_send(self):
c1 = self.u1.notification_set.count() c1 = self.u1.notification_set.count()
c2 = self.u1.notification_set.count() c2 = self.u2.notification_set.count()
profile = self.u1.profile profile = self.u1.profile
msg = profile.notify('subj', msg = profile.notify('subj',
'dashboard/test_message.txt', 'dashboard/test_message.txt',
......
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.exceptions import SuspiciousOperation
from vm.models import Instance, InstanceTemplate, Lease from vm.models import Instance, InstanceTemplate, Lease, Node
from ..models import Profile
from storage.models import Disk from storage.models import Disk
from firewall.models import Vlan from firewall.models import Vlan
class VmDetailTest(TestCase): class LoginMixin(object):
def login(self, client, username, password='password'):
response = client.post('/accounts/login/', {'username': username,
'password': password})
self.assertNotEqual(response.status_code, 403)
class VmDetailTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json'] fixtures = ['test-vm-fixture.json']
def setUp(self): def setUp(self):
...@@ -32,11 +41,6 @@ class VmDetailTest(TestCase): ...@@ -32,11 +41,6 @@ class VmDetailTest(TestCase):
self.us.delete() self.us.delete()
self.g1.delete() self.g1.delete()
def login(self, client, username, password='password'):
response = client.post('/accounts/login/', {'username': username,
'password': password})
self.assertNotEqual(response.status_code, 403)
def test_404_vm_page(self): def test_404_vm_page(self):
c = Client() c = Client()
self.login(c, 'user1') self.login(c, 'user1')
...@@ -242,3 +246,99 @@ class VmDetailTest(TestCase): ...@@ -242,3 +246,99 @@ class VmDetailTest(TestCase):
response = c.get("/dashboard/notifications/") response = c.get("/dashboard/notifications/")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
assert self.u1.notification_set.get().status == 'read' assert self.u1.notification_set.get().status == 'read'
class VmDetailVncTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json']
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
def test_permitted_vm_console(self):
c = Client()
self.login(c, 'user1')
inst = Instance.objects.get(pk=1)
inst.node = Node.objects.all()[0]
inst.save()
inst.set_level(self.u1, 'operator')
response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 200)
def test_not_permitted_vm_console(self):
c = Client()
self.login(c, 'user1')
inst = Instance.objects.get(pk=1)
inst.node = Node.objects.all()[0]
inst.save()
inst.set_level(self.u1, 'user')
response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 403)
class TransferOwnershipViewTest(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()
Profile.objects.create(user=self.u1)
self.u2 = User.objects.create(username='user2', is_staff=True)
self.u2.set_password('password')
self.u2.save()
Profile.objects.create(user=self.u2)
self.us = User.objects.create(username='superuser', is_superuser=True)
self.us.set_password('password')
self.us.save()
Profile.objects.create(user=self.us)
inst = Instance.objects.get(pk=1)
inst.owner = self.u1
inst.save()
def test_non_owner_offer(self):
c2 = self.u2.notification_set.count()
c = Client()
self.login(c, 'user2')
with self.assertRaises(SuspiciousOperation):
c.post('/dashboard/vm/1/tx/')
self.assertEqual(self.u2.notification_set.count(), c2)
def test_owned_offer(self):
c2 = self.u2.notification_set.count()
c = Client()
self.login(c, 'user1')
response = c.get('/dashboard/vm/1/tx/')
assert response.status_code == 200
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
self.assertEqual(self.u2.notification_set.count(), c2 + 1)
def test_transfer(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
c = Client()
self.login(c, 'user2')
response = c.post(url)
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk)
def test_transfer_token_used_by_others(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
response = c.post(url) # token is for user2
assert response.status_code == 403
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u1.pk)
def test_transfer_by_superuser(self):
c = Client()
self.login(c, 'superuser')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
c = Client()
self.login(c, 'user2')
response = c.post(url)
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk)
...@@ -9,7 +9,7 @@ from .views import ( ...@@ -9,7 +9,7 @@ from .views import (
FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete, FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete,
VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete, VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete,
GroupAclUpdateView, GroupUserDelete, NotificationView, NodeGraphView, GroupAclUpdateView, GroupUserDelete, NotificationView, NodeGraphView,
VmMigrateView, DiskAddView VmMigrateView, DiskAddVie, VmDetailVncTokenView,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -37,6 +37,8 @@ urlpatterns = patterns( ...@@ -37,6 +37,8 @@ urlpatterns = patterns(
name='dashboard.views.remove-port'), name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(), url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
name='dashboard.views.detail'), name='dashboard.views.detail'),
url(r'^vm/(?P<pk>\d+)/vnctoken/$', VmDetailVncTokenView.as_view(),
name='dashboard.views.detail-vnc'),
url(r'^vm/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Instance), url(r'^vm/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Instance),
name='dashboard.views.vm-acl'), name='dashboard.views.vm-acl'),
url(r'^vm/(?P<pk>\d+)/tx/$', TransferOwnershipView.as_view(), url(r'^vm/(?P<pk>\d+)/tx/$', TransferOwnershipView.as_view(),
...@@ -55,7 +57,7 @@ urlpatterns = patterns( ...@@ -55,7 +57,7 @@ urlpatterns = patterns(
url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'), url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'),
url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(), url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(),
name='dashboard.views.node-detail'), name='dashboard.views.node-detail'),
url(r'^tx/$', TransferOwnershipConfirmView.as_view(), url(r'^tx/(?P<key>.*)/?$', TransferOwnershipConfirmView.as_view(),
name='dashboard.views.vm-transfer-ownership-confirm'), name='dashboard.views.vm-transfer-ownership-confirm'),
url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(), url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(),
name="dashboard.views.delete-node"), name="dashboard.views.delete-node"),
......
...@@ -37,7 +37,7 @@ from vm.models import (Instance, InstanceTemplate, InterfaceTemplate, ...@@ -37,7 +37,7 @@ from vm.models import (Instance, InstanceTemplate, InterfaceTemplate,
InstanceActivity, Node, instance_activity, Lease, InstanceActivity, Node, instance_activity, Lease,
Interface, NodeActivity) Interface, NodeActivity)
from firewall.models import Vlan, Host, Rule from firewall.models import Vlan, Host, Rule
from dashboard.models import Favourite from dashboard.models import Favourite, Profile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -148,6 +148,25 @@ class CheckedDetailView(LoginRequiredMixin, DetailView): ...@@ -148,6 +148,25 @@ class CheckedDetailView(LoginRequiredMixin, DetailView):
return context return context
class VmDetailVncTokenView(CheckedDetailView):
template_name = "dashboard/vm-detail.html"
model = Instance
def get(self, request, **kwargs):
self.object = self.get_object()
if not self.object.has_level(request.user, 'operator'):
raise PermissionDenied()
if self.object.node:
port = self.object.vnc_port
host = str(self.object.node.host.ipv4)
value = signing.dumps({'host': host,
'port': port},
key=getenv("PROXY_SECRET", 'asdasd')),
return HttpResponse('vnc/?d=%s' % value)
else:
raise Http404()
class VmDetailView(CheckedDetailView): class VmDetailView(CheckedDetailView):
template_name = "dashboard/vm-detail.html" template_name = "dashboard/vm-detail.html"
model = Instance model = Instance
...@@ -155,15 +174,10 @@ class VmDetailView(CheckedDetailView): ...@@ -155,15 +174,10 @@ class VmDetailView(CheckedDetailView):
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']
if instance.node:
port = instance.vnc_port
host = str(instance.node.host.ipv4)
value = signing.dumps({'host': host,
'port': port},
key=getenv("PROXY_SECRET", 'asdasd')),
context.update({ context.update({
'graphite_enabled': VmGraphView.get_graphite_url() is not None, 'graphite_enabled': VmGraphView.get_graphite_url() is not None,
'vnc_url': '%s' % value 'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
kwargs={'pk': self.object.pk})
}) })
# activity data # activity data
...@@ -1464,7 +1478,12 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView): ...@@ -1464,7 +1478,12 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
try: try:
new_owner = User.objects.get(username=request.POST['name']) new_owner = User.objects.get(username=request.POST['name'])
except User.DoesNotExist: except User.DoesNotExist:
raise Http404() new_owner = User.objects.get(email=request.POST['name'])
except User.DoesNotExist:
new_owner = User.objects.get(profile__org_id=request.POST['name'])
except User.DoesNotExist:
messages.error(request, _('Can not find specified user.'))
return self.get(request, *args, **kwargs)
except KeyError: except KeyError:
raise SuspiciousOperation() raise SuspiciousOperation()
...@@ -1475,29 +1494,41 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView): ...@@ -1475,29 +1494,41 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
token = signing.dumps((obj.pk, new_owner.pk), token = signing.dumps((obj.pk, new_owner.pk),
salt=TransferOwnershipConfirmView.get_salt()) salt=TransferOwnershipConfirmView.get_salt())
return HttpResponse("%s?key=%s" % ( token_path = reverse(
reverse('dashboard.views.vm-transfer-ownership-confirm'), token), 'dashboard.views.vm-transfer-ownership-confirm', args=[token])
content_type="text/plain") try:
new_owner.profile.notify(
_('Ownership offer'),
'dashboard/notifications/ownership-offer.html',
{'instance': obj, 'token': token_path})
except Profile.DoesNotExist:
messages.error(request, _('Can not notify selected user.'))
else:
messages.success(request,
_('User %s is notified about the offer.') % (
unicode(new_owner), ))
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': obj.pk}))
class TransferOwnershipConfirmView(LoginRequiredMixin, View): class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"""User can accept an ownership offer."""
max_age = 3 * 24 * 3600 max_age = 3 * 24 * 3600
success_message = _("Ownership successfully transferred.") success_message = _("Ownership successfully transferred to you.")
@classmethod @classmethod
def get_salt(cls): def get_salt(cls):
return unicode(cls) return unicode(cls)
def get(self, request, *args, **kwargs): def get(self, request, key, *args, **kwargs):
"""Confirm ownership transfer based on token. """Confirm ownership transfer based on token.
""" """
try:
key = request.GET['key']
logger.debug('Confirm dialog for token %s.', key) logger.debug('Confirm dialog for token %s.', key)
try:
instance, new_owner = self.get_instance(key, request.user) instance, new_owner = self.get_instance(key, request.user)
except KeyError: except PermissionDenied:
raise Http404()
except PermissionDenied():
messages.error(request, _('This token is for an other user.')) messages.error(request, _('This token is for an other user.'))
raise raise
except SuspiciousOperation: except SuspiciousOperation:
...@@ -1507,16 +1538,10 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): ...@@ -1507,16 +1538,10 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"dashboard/confirm/base-transfer-ownership.html", "dashboard/confirm/base-transfer-ownership.html",
dictionary={'instance': instance, 'key': key}) dictionary={'instance': instance, 'key': key})
def post(self, request, *args, **kwargs): def post(self, request, key, *args, **kwargs):
"""Really transfer ownership based on token. """Really transfer ownership based on token.
""" """
try:
key = request.POST['key']
instance, owner = self.get_instance(key, request.user) instance, owner = self.get_instance(key, request.user)
except KeyError:
logger.debug('Posted to %s without key field.',
unicode(self.__class__))
raise SuspiciousOperation()
old = instance.owner old = instance.owner
with instance_activity(code_suffix='ownership-transferred', with instance_activity(code_suffix='ownership-transferred',
...@@ -1527,6 +1552,11 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): ...@@ -1527,6 +1552,11 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
messages.success(request, self.success_message) messages.success(request, self.success_message)
logger.info('Ownership of %s transferred from %s to %s.', logger.info('Ownership of %s transferred from %s to %s.',
unicode(instance), unicode(old), unicode(request.user)) unicode(instance), unicode(old), unicode(request.user))
if old.profile:
old.profile.notify(
_('Ownership accepted'),
'dashboard/notifications/ownership-accepted.html',
{'instance': instance})
return HttpResponseRedirect(instance.get_absolute_url()) return HttpResponseRedirect(instance.get_absolute_url())
def get_instance(self, key, user): def get_instance(self, key, user):
...@@ -1536,15 +1566,7 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): ...@@ -1536,15 +1566,7 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
instance, new_owner = ( instance, new_owner = (
signing.loads(key, max_age=self.max_age, signing.loads(key, max_age=self.max_age,
salt=self.get_salt())) salt=self.get_salt()))
except signing.BadSignature as e: except (signing.BadSignature, ValueError, TypeError) as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e))
raise SuspiciousOperation()
except ValueError as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e))
raise SuspiciousOperation()
except TypeError as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s', logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e)) key, unicode(user), unicode(e))
raise SuspiciousOperation() raise SuspiciousOperation()
...@@ -1634,6 +1656,14 @@ class VmGraphView(GraphViewBase): ...@@ -1634,6 +1656,14 @@ class VmGraphView(GraphViewBase):
class NodeGraphView(SuperuserRequiredMixin, GraphViewBase): class NodeGraphView(SuperuserRequiredMixin, GraphViewBase):
metrics = {
'cpu': ('cactiStyle(alias(derivative(%s.cpu.times),'
'"cpu usage (%%)"))'),
'memory': ('cactiStyle(alias(%s.memory.usage,'
'"memory usage (%%)"))'),
'network': ('cactiStyle(aliasByMetric('
'derivative(%s.network.bytes_*)))'),
}
model = Node model = Node
def get_prefix(self, instance): def get_prefix(self, instance):
...@@ -1689,7 +1719,7 @@ class VmMigrateView(SuperuserRequiredMixin, TemplateView): ...@@ -1689,7 +1719,7 @@ class VmMigrateView(SuperuserRequiredMixin, TemplateView):
'template': 'dashboard/_vm-migrate.html', 'template': 'dashboard/_vm-migrate.html',
'box_title': _('Migrate %(name)s' % {'name': vm.name}), 'box_title': _('Migrate %(name)s' % {'name': vm.name}),
'ajax_title': True, 'ajax_title': True,
'vm': kwargs['pk'], 'vm': vm,
'nodes': [n for n in Node.objects.filter(enabled=True) 'nodes': [n for n in Node.objects.filter(enabled=True)
if n.state == "ONLINE"] if n.state == "ONLINE"]
}) })
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from itertools import islice from itertools import islice, chain
import logging import logging
from netaddr import IPSet from netaddr import IPSet, EUI
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
...@@ -298,13 +298,28 @@ class Vlan(AclBase, models.Model): ...@@ -298,13 +298,28 @@ class Vlan(AclBase, models.Model):
def prefix6(self): def prefix6(self):
return self.network6.prefixlen return self.network6.prefixlen
def get_next_address(self, used_v4):
try:
last_address = list(used_v4)[-1]
except IndexError:
return []
next_address = last_address + 1
if next_address in self.network4.iter_hosts():
logger.debug("Found unused IPv4 address %s after %s.",
next_address, last_address)
return [next_address]
else:
return []
def get_new_address(self): def get_new_address(self):
hosts = Host.objects.filter(vlan=self) hosts = self.host_set
used_v4 = IPSet(hosts.values_list('ipv4', flat=True)) used_v4 = IPSet(hosts.values_list('ipv4', flat=True))
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))
for ipv4 in islice(self.network4.iter_hosts(), 10000): for ipv4 in chain(self.get_next_address(used_v4),
islice(self.network4.iter_hosts(), 10000)):
ipv4 = str(ipv4) ipv4 = str(ipv4)
if ipv4 not in used_v4: if ipv4 not in used_v4:
logger.debug("Found unused IPv4 address %s.", ipv4) logger.debug("Found unused IPv4 address %s.", ipv4)
...@@ -691,6 +706,17 @@ class Host(models.Model): ...@@ -691,6 +706,17 @@ class Host(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return ('network.host', None, {'pk': self.pk}) return ('network.host', None, {'pk': self.pk})
@property
def eui(self):
return EUI(self.mac)
@property
def hw_vendor(self):
try:
return self.eui.oui.registration().org
except:
return None
class Firewall(models.Model): class Firewall(models.Model):
name = models.CharField(max_length=20, unique=True, name = models.CharField(max_length=20, unique=True,
......
from django.test import TestCase from django.test import TestCase
from admin import HostAdmin from django.contrib.auth.models import User
from ..admin import HostAdmin
from firewall.models import Vlan, Domain, Host
from django.forms import ValidationError
class MockInstance: class MockInstance:
...@@ -36,3 +39,46 @@ class HostAdminTestCase(TestCase): ...@@ -36,3 +39,46 @@ class HostAdminTestCase(TestCase):
MockGroup("korte"), MockGroup("szilva")]) MockGroup("korte"), MockGroup("szilva")])
l = HostAdmin.list_groups(instance) l = HostAdmin.list_groups(instance)
self.assertEqual(l, "alma, korte, szilva") self.assertEqual(l, "alma, korte, szilva")
class GetNewAddressTestCase(TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.save()
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',
network6='2001:738:2001:4031::/80', domain=d,
owner=self.u1)
self.vlan.save()
self.vlan.host_set.all().delete()
for i in [1] + 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()
def test_new_addr_w_empty_vlan(self):
self.vlan.host_set.all().delete()
self.vlan.get_new_address()
def test_all_addr_in_use(self):
for i in (2, 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()
self.assertRaises(ValidationError, self.vlan.get_new_address)
def test_all_addr_in_use_w_ipv6(self):
Host(hostname='h-x', mac='01:02:03:04:05:06',
ipv4='10.0.0.6', ipv6='2001:738:2001:4031:0:0:2:0',
vlan=self.vlan, owner=self.u1).save()
self.assertRaises(ValidationError, self.vlan.get_new_address)
def test_new_addr_last(self):
self.assertEqual(self.vlan.get_new_address()['ipv4'], '10.0.0.6')
def test_new_addr_w_overflow(self):
Host(hostname='h-6', mac='01:02:03:04:05:06',
ipv4='10.0.0.6', vlan=self.vlan, owner=self.u1).save()
self.assertEqual(self.vlan.get_new_address()['ipv4'], '10.0.0.2')
...@@ -36,6 +36,9 @@ class GroupTable(Table): ...@@ -36,6 +36,9 @@ class GroupTable(Table):
class HostTable(Table): class HostTable(Table):
hostname = LinkColumn('network.host', args=[A('pk')]) hostname = LinkColumn('network.host', args=[A('pk')])
mac = TemplateColumn(
template_name="network/columns/mac.html"
)
class Meta: class Meta:
model = Host model = Host
......
{% load i18n %}
<span title="{% blocktrans with vendor=record.hw_vendor|default:"n/a" %}Vendor: {{vendor}}{% endblocktrans %}">{{ record.mac }}</span>
...@@ -48,3 +48,9 @@ class create_from_url(AbortableTask): ...@@ -48,3 +48,9 @@ class create_from_url(AbortableTask):
task_uuid=create_from_url.request.id, task_uuid=create_from_url.request.id,
abortable_task=self, abortable_task=self,
user=user) user=user)
@celery.task
def create_empty(Disk, instance, params, user):
Disk.create_empty(instance, params, user,
task_uuid=create_empty.request.id)
from storage.models import DataStore from storage.models import DataStore
import os import os
from django.utils import timezone
from datetime import timedelta
from manager.mancelery import celery from manager.mancelery import celery
import logging import logging
from storage.tasks import remote_tasks from storage.tasks import remote_tasks
...@@ -21,10 +19,8 @@ def garbage_collector(timeout=15): ...@@ -21,10 +19,8 @@ def garbage_collector(timeout=15):
:type timeoit: int :type timeoit: int
""" """
for ds in DataStore.objects.all(): for ds in DataStore.objects.all():
time_before = timezone.now() - timedelta(days=1)
file_list = os.listdir(ds.path) file_list = os.listdir(ds.path)
disk_list = [disk.filename for disk in disk_list = ds.get_deletable_disks()
ds.disk_set.filter(destroyed__lt=time_before)]
queue_name = ds.get_remote_queue_name('storage') queue_name = ds.get_remote_queue_name('storage')
for i in set(file_list).intersection(disk_list): for i in set(file_list).intersection(disk_list):
logger.info("Image: %s at Datastore: %s moved to trash folder." % logger.info("Image: %s at Datastore: %s moved to trash folder." %
......
"""
This file demonstrates writing tests using the unittest module. These will pass
when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.assertEqual(1 + 1, 2)
from datetime import timedelta
from django.test import TestCase
from django.utils import timezone
from ..models import Disk, DataStore
old = timezone.now() - timedelta(days=2)
new = timezone.now() - timedelta(hours=2)
class DiskTestCase(TestCase):
n = 0
def setUp(self):
self.ds = DataStore.objects.create(path="/datastore",
hostname="devenv", name="default")
def _disk(self, destroyed=None, base=None):
self.n += 1
n = "d%d" % self.n
return Disk.objects.create(name=n, filename=n, base=base, size=1,
destroyed=destroyed, datastore=self.ds)
def test_deletable_not_destroyed(self):
d = self._disk()
assert not d.is_deletable()
def test_deletable_newly_destroyed(self):
d = self._disk(destroyed=new)
assert not d.is_deletable()
def test_deletable_no_child(self):
d = self._disk(destroyed=old)
assert d.is_deletable()
def test_deletable_child_not_destroyed(self):
d = self._disk()
self._disk(base=d, destroyed=old)
self._disk(base=d)
assert not d.is_deletable()
def test_deletable_child_newly_destroyed(self):
d = self._disk(destroyed=old)
self._disk(base=d, destroyed=new)
self._disk(base=d)
assert not d.is_deletable()
...@@ -274,6 +274,9 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -274,6 +274,9 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
act = None act = None
return 'NOSTATE' if act is None else act.resultant_state return 'NOSTATE' if act is None else act.resultant_state
def is_console_available(self):
return self.state in ('RUNNING', )
def manual_state_change(self, new_state, reason=None, user=None): def manual_state_change(self, new_state, reason=None, user=None):
# TODO cancel concurrent activity (if exists) # TODO cancel concurrent activity (if exists)
act = InstanceActivity.create(code_suffix='manual_state_change', act = InstanceActivity.create(code_suffix='manual_state_change',
...@@ -584,10 +587,13 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -584,10 +587,13 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
self.pw)) self.pw))
self.save() self.save()
def __schedule_vm(self, act): def select_node(self):
"""Schedule the virtual machine. """Returns the node the VM should be deployed or migrated to.
"""
return scheduler.select_node(self, Node.objects.all())
:param self: The virtual machine. def __schedule_vm(self, act):
"""Schedule the virtual machine as part of a higher level activity.
:param act: Parent activity. :param act: Parent activity.
""" """
...@@ -597,7 +603,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -597,7 +603,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
# Schedule # Schedule
if self.node is None: if self.node is None:
self.node = scheduler.select_node(self, Node.objects.all()) self.node = self.select_node()
self.save() self.save()
......
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