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">
{% for n in nodes %} {% with current=vm.node.pk selected=vm.select_node.pk %}
<li> {% for n in nodes %}
<strong>{{ n }}</strong> <li class="panel panel-default"><div class="panel-body">
<input type="radio" name="node" value="{{ n.pk }}" style="float: right;"/> <label for="migrate-to-{{n.pk}}">
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span> <strong>{{ n }}</strong>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span> {% if current == n.pk %}<div class="label label-info">{% trans "current" %}</div>{% endif %}
<div style="clear: both;"></div> {% if selected == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %}
</li> </label>
{% endfor %} <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> </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 VNC_URL = "{{ vnc_url }}";
var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/'; </script>
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>
...@@ -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,16 +174,11 @@ class VmDetailView(CheckedDetailView): ...@@ -155,16 +174,11 @@ 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: context.update({
port = instance.vnc_port 'graphite_enabled': VmGraphView.get_graphite_url() is not None,
host = str(instance.node.host.ipv4) 'vnc_url': reverse_lazy("dashboard.views.detail-vnc",
value = signing.dumps({'host': host, kwargs={'pk': self.object.pk})
'port': port}, })
key=getenv("PROXY_SECRET", 'asdasd')),
context.update({
'graphite_enabled': VmGraphView.get_graphite_url() is not None,
'vnc_url': '%s' % value
})
# activity data # activity data
ia = InstanceActivity.objects.filter( ia = InstanceActivity.objects.filter(
...@@ -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.
""" """
logger.debug('Confirm dialog for token %s.', key)
try: try:
key = request.GET['key']
logger.debug('Confirm dialog for token %s.', key)
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: instance, owner = self.get_instance(key, request.user)
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()
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>
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
from contextlib import contextmanager from contextlib import contextmanager
import logging import logging
from os.path import join
import uuid import uuid
from django.db.models import (Model, BooleanField, CharField, DateTimeField, from django.db.models import (Model, BooleanField, CharField, DateTimeField,
...@@ -10,6 +11,7 @@ from django.utils import timezone ...@@ -10,6 +11,7 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from sizefield.models import FileSizeField from sizefield.models import FileSizeField
from datetime import timedelta
from acl.models import AclBase from acl.models import AclBase
from .tasks import local_tasks, remote_tasks from .tasks import local_tasks, remote_tasks
...@@ -36,14 +38,20 @@ class DataStore(Model): ...@@ -36,14 +38,20 @@ class DataStore(Model):
def __unicode__(self): def __unicode__(self):
return u'%s (%s)' % (self.name, self.path) return u'%s (%s)' % (self.name, self.path)
def get_remote_queue_name(self, queue_id): def get_remote_queue_name(self, queue_id, check_worker=True):
logger.debug("Checking for storage queue %s.%s", logger.debug("Checking for storage queue %s.%s",
self.hostname, queue_id) self.hostname, queue_id)
if local_tasks.check_queue(self.hostname, queue_id): if not check_worker or local_tasks.check_queue(self.hostname,
queue_id):
return self.hostname + '.' + queue_id return self.hostname + '.' + queue_id
else: else:
raise WorkerNotFound() raise WorkerNotFound()
def get_deletable_disks(self):
return [disk.filename for disk in
self.disk_set.filter(
destroyed__isnull=False) if disk.is_deletable()]
class Disk(AclBase, TimeStampedModel): class Disk(AclBase, TimeStampedModel):
...@@ -100,10 +108,12 @@ class Disk(AclBase, TimeStampedModel): ...@@ -100,10 +108,12 @@ class Disk(AclBase, TimeStampedModel):
@property @property
def path(self): def path(self):
return self.datastore.path + '/' + self.filename """Get the path where the files are stored."""
return join(self.datastore.path, self.filename)
@property @property
def format(self): def format(self):
"""Returns the proper file format for different type of images."""
return { return {
'qcow2-norm': 'qcow2', 'qcow2-norm': 'qcow2',
'qcow2-snap': 'qcow2', 'qcow2-snap': 'qcow2',
...@@ -114,6 +124,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -114,6 +124,7 @@ class Disk(AclBase, TimeStampedModel):
@property @property
def device_type(self): def device_type(self):
"""Returns the proper device prefix for different file format."""
return { return {
'qcow2-norm': 'vd', 'qcow2-norm': 'vd',
'qcow2-snap': 'vd', 'qcow2-snap': 'vd',
...@@ -122,7 +133,28 @@ class Disk(AclBase, TimeStampedModel): ...@@ -122,7 +133,28 @@ class Disk(AclBase, TimeStampedModel):
'raw-rw': 'vd', 'raw-rw': 'vd',
}[self.type] }[self.type]
def is_deletable(self):
"""Returns whether the file can be deleted.
Checks if all children and the disk itself is destroyed.
"""
yesterday = timezone.now() - timedelta(days=1)
return (self.destroyed is not None
and self.destroyed < yesterday) and not self.has_active_child()
def has_active_child(self):
"""Returns if disk has children that are not destroyed.
"""
return any((not i.is_deletable() for i in self.derivatives.all()))
def is_in_use(self): def is_in_use(self):
"""Returns if disk is attached to an active VM.
'In use' means the disk is attached to a VM which is not STOPPED, as
any other VMs leave the disk in an inconsistent state.
"""
return any([i.state != 'STOPPED' for i in self.instance_set.all()]) return any([i.state != 'STOPPED' for i in self.instance_set.all()])
def get_exclusive(self): def get_exclusive(self):
...@@ -139,7 +171,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -139,7 +171,7 @@ class Disk(AclBase, TimeStampedModel):
if self.type not in type_mapping.keys(): if self.type not in type_mapping.keys():
raise self.WrongDiskTypeError(self.type) raise self.WrongDiskTypeError(self.type)
filename = self.filename if self.type == 'iso' else str(uuid.uuid4()) filename = self.filename if self.type == 'iso' else None
new_type = type_mapping[self.type] new_type = type_mapping[self.type]
return Disk.objects.create(base=self, datastore=self.datastore, return Disk.objects.create(base=self, datastore=self.datastore,
...@@ -147,6 +179,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -147,6 +179,7 @@ class Disk(AclBase, TimeStampedModel):
size=self.size, type=new_type) size=self.size, type=new_type)
def get_vmdisk_desc(self): def get_vmdisk_desc(self):
"""Serialize disk object to the vmdriver."""
return { return {
'source': self.path, 'source': self.path,
'driver_type': self.format, 'driver_type': self.format,
...@@ -156,6 +189,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -156,6 +189,7 @@ class Disk(AclBase, TimeStampedModel):
} }
def get_disk_desc(self): def get_disk_desc(self):
"""Serialize disk object to the storage driver."""
return { return {
'name': self.filename, 'name': self.filename,
'dir': self.datastore.path, 'dir': self.datastore.path,
...@@ -165,20 +199,26 @@ class Disk(AclBase, TimeStampedModel): ...@@ -165,20 +199,26 @@ class Disk(AclBase, TimeStampedModel):
'type': 'snapshot' if self.type == 'qcow2-snap' else 'normal' 'type': 'snapshot' if self.type == 'qcow2-snap' else 'normal'
} }
def get_remote_queue_name(self, queue_id): def get_remote_queue_name(self, queue_id='storage', check_worker=True):
"""Returns the proper queue name based on the datastore."""
if self.datastore: if self.datastore:
return self.datastore.get_remote_queue_name(queue_id) return self.datastore.get_remote_queue_name(queue_id, check_worker)
else: else:
return None return None
def __unicode__(self): def __unicode__(self):
return u"%s (#%d)" % (self.name, self.id) return u"%s (#%d)" % (self.name, self.id or 0)
def clean(self, *args, **kwargs): def clean(self, *args, **kwargs):
if self.size == "" and self.base: if self.size == "" and self.base:
self.size = self.base.size self.size = self.base.size
super(Disk, self).clean(*args, **kwargs) super(Disk, self).clean(*args, **kwargs)
def save(self, *args, **kwargs):
if self.filename is None:
self.generate_filename()
return super(Disk, self).save(*args, **kwargs)
def deploy(self, user=None, task_uuid=None, timeout=15): def deploy(self, user=None, task_uuid=None, timeout=15):
"""Reify the disk model on the associated data store. """Reify the disk model on the associated data store.
...@@ -231,29 +271,79 @@ class Disk(AclBase, TimeStampedModel): ...@@ -231,29 +271,79 @@ class Disk(AclBase, TimeStampedModel):
return local_tasks.deploy.apply_async(args=[self, user], return local_tasks.deploy.apply_async(args=[self, user],
queue="localhost.man") queue="localhost.man")
def generate_filename(self):
"""Generate a unique filename and set it on the object.
"""
self.filename = str(uuid.uuid4())
@classmethod @classmethod
def create_empty(cls, params={}, user=None): def create_empty(cls, instance=None, user=None, **kwargs):
disk = cls() """Create empty Disk object.
disk.__dict__.update(params)
disk.save() :param instance: Instance or template attach the Disk to.
return disk :type instance: vm.models.Instance or InstanceTemplate or NoneType
:param user: Creator of the disk.
:type user: django.contrib.auth.User
:return: Disk object without a real image, to be .deploy()ed later.
"""
with disk_activity(code_suffix="create", user=user) as act:
disk = cls(**kwargs)
if disk.filename is None:
disk.generate_filename()
disk.save()
act.disk = disk
act.save()
if instance:
instance.disks.add(disk)
return disk
@classmethod
def create_from_url_async(cls, url, instance=None, user=None, **kwargs):
"""Create disk object and download data from url asynchrnously.
:param url: URL of image to download.
:type url: string
:param instance: Instance or template attach the Disk to.
:type instance: vm.models.Instance or InstanceTemplate or NoneType
:param user: owner of the disk
:type user: django.contrib.auth.User
:return: Task
:rtype: AsyncResult
"""
kwargs.update({'cls': cls, 'url': url,
'instance': instance, 'user': user})
return local_tasks.create_from_url.apply_async(kwargs=kwargs,
queue='localhost.man')
@classmethod @classmethod
def create_from_url_async(cls, url, params=None, user=None): def create_from_url(cls, url, instance=None, user=None,
return local_tasks.create_from_url.apply_async(kwargs={ task_uuid=None, abortable_task=None, **kwargs):
'cls': cls, 'url': url, 'params': params, 'user': user}, """Create disk object and download data from url synchronusly.
queue='localhost.man')
:param url: image url to download.
def create_from_url(cls, url, params={}, user=None, task_uuid=None, :type url: url
abortable_task=None): :param instance: Instance or template attach the Disk to.
disk = cls() :type instance: vm.models.Instance or InstanceTemplate or NoneType
disk.filename = str(uuid.uuid4()) :param user: owner of the disk
:type user: django.contrib.auth.User
:param task_uuid: TODO
:param abortable_task: TODO
:return: The created Disk object
:rtype: Disk
"""
kwargs.setdefault('name', url.split('/')[-1])
disk = cls(**kwargs)
disk.generate_filename()
disk.type = "iso" disk.type = "iso"
disk.size = 1 disk.size = 1
disk.datastore = DataStore.objects.all()[0] # TODO get proper datastore
if params: disk.datastore = DataStore.objects.get()
disk.__dict__.update(params)
disk.save() disk.save()
if instance:
instance.disks.add(disk)
queue_name = disk.get_remote_queue_name('storage') queue_name = disk.get_remote_queue_name('storage')
def __on_abort(activity, error): def __on_abort(activity, error):
...@@ -284,6 +374,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -284,6 +374,7 @@ class Disk(AclBase, TimeStampedModel):
disk.size = size disk.size = size
disk.ready = True disk.ready = True
disk.save() disk.save()
return disk
def destroy(self, user=None, task_uuid=None): def destroy(self, user=None, task_uuid=None):
if self.destroyed: if self.destroyed:
...@@ -303,7 +394,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -303,7 +394,7 @@ class Disk(AclBase, TimeStampedModel):
queue='localhost.man') queue='localhost.man')
def restore(self, user=None, task_uuid=None): def restore(self, user=None, task_uuid=None):
"""Restore destroyed disk. """Recover destroyed disk from trash if possible.
""" """
# TODO # TODO
pass pass
...@@ -328,12 +419,11 @@ class Disk(AclBase, TimeStampedModel): ...@@ -328,12 +419,11 @@ class Disk(AclBase, TimeStampedModel):
with disk_activity(code_suffix='save_as', disk=self, with disk_activity(code_suffix='save_as', disk=self,
task_uuid=task_uuid, user=user, timeout=300): task_uuid=task_uuid, user=user, timeout=300):
filename = str(uuid.uuid4())
new_type, new_base = mapping[self.type] new_type, new_base = mapping[self.type]
disk = Disk.objects.create(base=new_base, datastore=self.datastore, disk = Disk.objects.create(base=new_base, datastore=self.datastore,
filename=filename, name=self.name, name=self.name, size=self.size,
size=self.size, type=new_type) type=new_type)
queue_name = self.get_remote_queue_name('storage') queue_name = self.get_remote_queue_name('storage')
remote_tasks.merge.apply_async(args=[self.get_disk_desc(), remote_tasks.merge.apply_async(args=[self.get_disk_desc(),
......
...@@ -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