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 {
}
<<<<<<< HEAD
.notification-messages {
padding: 10px 8px;
width: 350px;
......
......@@ -4,16 +4,29 @@ $(function() {
/* vm migrate */
$('.vm-migrate').click(function(e) {
var icon = $(this).children("i");
var vm = $(this).data("vm-pk");
icon.removeClass("icon-truck").addClass("icon-spinner icon-spin");
$.ajax({
type: 'GET',
url: '/dashboard/vm/' + vm + '/migrate/',
success: function(data) {
success: function(data) {
icon.addClass("icon-truck").removeClass("icon-spinner icon-spin");
$('body').append(data);
$('#create-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() {
$('#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;
......
$(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() {
location.reload();
},
error: function(xhr, textStatus, error) {
if (xhr.status == 500) {
addMessage("Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
}
});
} else {
......
......@@ -10,7 +10,7 @@
</span>
<div style="clear: both;"></div>
<div class="notification-message-text">
{{ n.message }}
{{ n.message|safe }}
</div>
</li>
{% empty %}
......
{% load i18n %}
{% 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 %}
<ul id="vm-migrate-node-list">
{% for n in nodes %}
<li>
<strong>{{ n }}</strong>
<input type="radio" name="node" value="{{ n.pk }}" style="float: right;"/>
<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>
<div style="clear: both;"></div>
</li>
{% endfor %}
{% with current=vm.node.pk selected=vm.select_node.pk %}
{% for n in nodes %}
<li class="panel panel-default"><div class="panel-body">
<label for="migrate-to-{{n.pk}}">
<strong>{{ n }}</strong>
{% 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 "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
<div style="clear: both;"></div>
</div></li>
{% endfor %}
{% endwith %}
</ul>
<button type="submit" class="btn btn-primary btn-sm"><i class="icon-truck"></i> Migrate</button>
</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 @@
<dt>Password:</dt>
<dd>
<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">
<i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i>
</span>
......@@ -113,8 +113,8 @@
<i class="icon-tasks icon-2x"></i><br>
{% trans "Resources" %}</a>
</li>
<li {% if instance.state != "RUNNING" %}class="disabled"{% endif %}>
<a href="#{% if instance.state == "RUNNING" %}console" data-toggle="pill{% endif %}" data-target="#_console" class="text-center">
<li {% if not instance.is_console_available %}class="disabled"{% endif %}>
<a href="#console" data-toggle="pill" data-target="#_console" class="text-center">
<i class="icon-desktop icon-2x"></i><br>
{% trans "Console" %}</a></li>
<li>
......@@ -152,4 +152,5 @@
{% block extra_js %}
<script src="{{ STATIC_URL }}dashboard/vm-details.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-common.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-console.js"></script>
{% endblock %}
......@@ -4,13 +4,16 @@
<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>
</span>
<strong>{{ a.get_readable_name }}</strong>
{{ a.started|date:"Y-m-d H:i" }}, {{ a.user }}
<strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% endif %}>
{{ a.get_readable_name }}
</strong>
{{ a.started|date:"Y-m-d H:i" }}{% if a.user %}, {{ a.user }}{% endif %}
{% if a.children.count > 0 %}
<div class="sub-timeline">
{% for s in a.children.all %}
<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 %}
{{ s.finished|time:"H:i:s" }}
{% else %}
......
{% load i18n %}
<h3>{% trans "Owner" %}</h3>
<p>
{% if user == instance.owner %}
{% 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>
<h3>{% trans "Permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %}
......
<div class="btn-toolbar">
<button id="sendCtrlAltDelButton" class="btn btn-danger small" href="#">Send CtrlAltDel</button>
<button id="sendPasswordButton" class="btn btn-default small" href="#">Type password</button>
<button id="sendCtrlAltDelButton" class="btn btn-danger small">Send CtrlAltDel</button>
<button id="sendPasswordButton" class="btn btn-default small">Type password</button>
</div>
<div class="alert alert-info" id="noVNC_status">
</div>
......@@ -10,68 +10,6 @@
<script src="{{ STATIC_URL }}dashboard/novnc/util.js"></script>
<script>
"use strict";
var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/';
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) {
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>
var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/';
var VNC_URL = "{{ vnc_url }}";
</script>
......@@ -13,6 +13,7 @@
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
E-mail address or identifier of user:
<input name="name">
<input type="submit">
</form>
......
......@@ -15,7 +15,7 @@ class NotificationTestCase(TestCase):
def test_notification_send(self):
c1 = self.u1.notification_set.count()
c2 = self.u1.notification_set.count()
c2 = self.u2.notification_set.count()
profile = self.u1.profile
msg = profile.notify('subj',
'dashboard/test_message.txt',
......
from django.test import TestCase
from django.test.client import Client
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 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']
def setUp(self):
......@@ -32,11 +41,6 @@ class VmDetailTest(TestCase):
self.us.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):
c = Client()
self.login(c, 'user1')
......@@ -242,3 +246,99 @@ class VmDetailTest(TestCase):
response = c.get("/dashboard/notifications/")
self.assertEqual(response.status_code, 200)
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 (
FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete,
VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete,
GroupAclUpdateView, GroupUserDelete, NotificationView, NodeGraphView,
VmMigrateView, DiskAddView
VmMigrateView, DiskAddVie, VmDetailVncTokenView,
)
urlpatterns = patterns(
......@@ -37,6 +37,8 @@ urlpatterns = patterns(
name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
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),
name='dashboard.views.vm-acl'),
url(r'^vm/(?P<pk>\d+)/tx/$', TransferOwnershipView.as_view(),
......@@ -55,7 +57,7 @@ urlpatterns = patterns(
url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'),
url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(),
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'),
url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(),
name="dashboard.views.delete-node"),
......
......@@ -37,7 +37,7 @@ from vm.models import (Instance, InstanceTemplate, InterfaceTemplate,
InstanceActivity, Node, instance_activity, Lease,
Interface, NodeActivity)
from firewall.models import Vlan, Host, Rule
from dashboard.models import Favourite
from dashboard.models import Favourite, Profile
logger = logging.getLogger(__name__)
......@@ -148,6 +148,25 @@ class CheckedDetailView(LoginRequiredMixin, DetailView):
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):
template_name = "dashboard/vm-detail.html"
model = Instance
......@@ -155,16 +174,11 @@ class VmDetailView(CheckedDetailView):
def get_context_data(self, **kwargs):
context = super(VmDetailView, self).get_context_data(**kwargs)
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({
'graphite_enabled': VmGraphView.get_graphite_url() is not None,
'vnc_url': '%s' % value
})
context.update({
'graphite_enabled': VmGraphView.get_graphite_url() is not None,
'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
kwargs={'pk': self.object.pk})
})
# activity data
ia = InstanceActivity.objects.filter(
......@@ -1464,7 +1478,12 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
try:
new_owner = User.objects.get(username=request.POST['name'])
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:
raise SuspiciousOperation()
......@@ -1475,29 +1494,41 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
token = signing.dumps((obj.pk, new_owner.pk),
salt=TransferOwnershipConfirmView.get_salt())
return HttpResponse("%s?key=%s" % (
reverse('dashboard.views.vm-transfer-ownership-confirm'), token),
content_type="text/plain")
token_path = reverse(
'dashboard.views.vm-transfer-ownership-confirm', args=[token])
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):
"""User can accept an ownership offer."""
max_age = 3 * 24 * 3600
success_message = _("Ownership successfully transferred.")
success_message = _("Ownership successfully transferred to you.")
@classmethod
def get_salt(cls):
return unicode(cls)
def get(self, request, *args, **kwargs):
def get(self, request, key, *args, **kwargs):
"""Confirm ownership transfer based on token.
"""
logger.debug('Confirm dialog for token %s.', key)
try:
key = request.GET['key']
logger.debug('Confirm dialog for token %s.', key)
instance, new_owner = self.get_instance(key, request.user)
except KeyError:
raise Http404()
except PermissionDenied():
except PermissionDenied:
messages.error(request, _('This token is for an other user.'))
raise
except SuspiciousOperation:
......@@ -1507,16 +1538,10 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"dashboard/confirm/base-transfer-ownership.html",
dictionary={'instance': instance, 'key': key})
def post(self, request, *args, **kwargs):
def post(self, request, key, *args, **kwargs):
"""Really transfer ownership based on token.
"""
try:
key = request.POST['key']
instance, owner = self.get_instance(key, request.user)
except KeyError:
logger.debug('Posted to %s without key field.',
unicode(self.__class__))
raise SuspiciousOperation()
instance, owner = self.get_instance(key, request.user)
old = instance.owner
with instance_activity(code_suffix='ownership-transferred',
......@@ -1527,6 +1552,11 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
messages.success(request, self.success_message)
logger.info('Ownership of %s transferred from %s to %s.',
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())
def get_instance(self, key, user):
......@@ -1536,15 +1566,7 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
instance, new_owner = (
signing.loads(key, max_age=self.max_age,
salt=self.get_salt()))
except signing.BadSignature 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:
except (signing.BadSignature, ValueError, TypeError) as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e))
raise SuspiciousOperation()
......@@ -1634,6 +1656,14 @@ class VmGraphView(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
def get_prefix(self, instance):
......@@ -1689,7 +1719,7 @@ class VmMigrateView(SuperuserRequiredMixin, TemplateView):
'template': 'dashboard/_vm-migrate.html',
'box_title': _('Migrate %(name)s' % {'name': vm.name}),
'ajax_title': True,
'vm': kwargs['pk'],
'vm': vm,
'nodes': [n for n in Node.objects.filter(enabled=True)
if n.state == "ONLINE"]
})
......
# -*- coding: utf-8 -*-
from itertools import islice
from itertools import islice, chain
import logging
from netaddr import IPSet
from netaddr import IPSet, EUI
from django.contrib.auth.models import User
from django.db import models
......@@ -298,13 +298,28 @@ class Vlan(AclBase, models.Model):
def prefix6(self):
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):
hosts = Host.objects.filter(vlan=self)
hosts = self.host_set
used_v4 = IPSet(hosts.values_list('ipv4', flat=True))
used_v6 = IPSet(hosts.exclude(ipv6__isnull=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)
if ipv4 not in used_v4:
logger.debug("Found unused IPv4 address %s.", ipv4)
......@@ -691,6 +706,17 @@ class Host(models.Model):
def get_absolute_url(self):
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):
name = models.CharField(max_length=20, unique=True,
......
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:
......@@ -36,3 +39,46 @@ class HostAdminTestCase(TestCase):
MockGroup("korte"), MockGroup("szilva")])
l = HostAdmin.list_groups(instance)
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):
class HostTable(Table):
hostname = LinkColumn('network.host', args=[A('pk')])
mac = TemplateColumn(
template_name="network/columns/mac.html"
)
class Meta:
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):
task_uuid=create_from_url.request.id,
abortable_task=self,
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
import os
from django.utils import timezone
from datetime import timedelta
from manager.mancelery import celery
import logging
from storage.tasks import remote_tasks
......@@ -21,10 +19,8 @@ def garbage_collector(timeout=15):
:type timeoit: int
"""
for ds in DataStore.objects.all():
time_before = timezone.now() - timedelta(days=1)
file_list = os.listdir(ds.path)
disk_list = [disk.filename for disk in
ds.disk_set.filter(destroyed__lt=time_before)]
disk_list = ds.get_deletable_disks()
queue_name = ds.get_remote_queue_name('storage')
for i in set(file_list).intersection(disk_list):
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):
act = None
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):
# TODO cancel concurrent activity (if exists)
act = InstanceActivity.create(code_suffix='manual_state_change',
......@@ -584,10 +587,13 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
self.pw))
self.save()
def __schedule_vm(self, act):
"""Schedule the virtual machine.
def select_node(self):
"""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.
"""
......@@ -597,7 +603,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
# Schedule
if self.node is None:
self.node = scheduler.select_node(self, Node.objects.all())
self.node = self.select_node()
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