Commit 7a501d0a by Őry Máté

Update to master

Merge remote-tracking branch 'origin/master' into feature-node-traits

Conflicts:
	circle/dashboard/forms.py
	circle/dashboard/urls.py
	circle/dashboard/views.py
parents 8a1af341 66a9ff8a
============= ============
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
...@@ -151,9 +151,9 @@ class AclBase(Model): ...@@ -151,9 +151,9 @@ class AclBase(Model):
return True return True
return False return False
def get_users_with_level(self): def get_users_with_level(self, **kwargs):
logger.debug('%s.get_users_with_level() called', unicode(self)) logger.debug('%s.get_users_with_level() called', unicode(self))
object_levels = (self.object_level_set.select_related( object_levels = (self.object_level_set.filter(**kwargs).select_related(
'users', 'level').all()) 'users', 'level').all())
users = [] users = []
for object_level in object_levels: for object_level in object_levels:
......
...@@ -339,7 +339,6 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': ...@@ -339,7 +339,6 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'djangosaml2.backends.Saml2Backend', 'djangosaml2.backends.Saml2Backend',
) )
LOGIN_URL = '/saml2/login/'
remote_metadata = join(SITE_ROOT, 'remote_metadata.xml') remote_metadata = join(SITE_ROOT, 'remote_metadata.xml')
if not isfile(remote_metadata): if not isfile(remote_metadata):
...@@ -388,6 +387,8 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': ...@@ -388,6 +387,8 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
'DJANGO_SAML_GROUP_OWNER_ATTRIBUTES', '').split(',') 'DJANGO_SAML_GROUP_OWNER_ATTRIBUTES', '').split(',')
SAML_CREATE_UNKNOWN_USER = True SAML_CREATE_UNKNOWN_USER = True
if get_env_variable('DJANGO_SAML_ORG_ID_ATTRIBUTE', False) != False: if get_env_variable('DJANGO_SAML_ORG_ID_ATTRIBUTE', False) is not False:
SAML_ORG_ID_ATTRIBUTE = get_env_variable( SAML_ORG_ID_ATTRIBUTE = get_env_variable(
'DJANGO_SAML_ORG_ID_ATTRIBUTE') 'DJANGO_SAML_ORG_ID_ATTRIBUTE')
LOGIN_REDIRECT_URL = "/"
...@@ -29,3 +29,12 @@ CACHES = { ...@@ -29,3 +29,12 @@ CACHES = {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache' 'BACKEND': 'django.core.cache.backends.dummy.DummyCache'
} }
} }
LOGGING['loggers']['djangosaml2'] = {'handlers': ['console'],
'level': 'CRITICAL'}
LOGGING['handlers']['console'] = {'level': 'WARNING',
'class': 'logging.StreamHandler',
'formatter': 'simple'}
for i in LOCAL_APPS:
LOGGING['loggers'][i] = {'handlers': ['console'], 'level': 'CRITICAL'}
...@@ -6,6 +6,8 @@ from django.shortcuts import redirect ...@@ -6,6 +6,8 @@ from django.shortcuts import redirect
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from circle.settings.base import get_env_variable from circle.settings.base import get_env_variable
from dashboard.views import circle_login
from dashboard.forms import CirclePasswordResetForm, CircleSetPasswordForm
admin.autodiscover() admin.autodiscover()
...@@ -23,6 +25,19 @@ urlpatterns = patterns( ...@@ -23,6 +25,19 @@ urlpatterns = patterns(
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),
url(r'^network/', include('network.urls')), url(r'^network/', include('network.urls')),
url(r'^dashboard/', include('dashboard.urls')), url(r'^dashboard/', include('dashboard.urls')),
url((r'^accounts/reset/(?P<uidb36>[0-9A-Za-z]{1,13})-'
'(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$'),
'django.contrib.auth.views.password_reset_confirm',
{'set_password_form': CircleSetPasswordForm},
name='accounts.password_reset_confirm'
),
url(r'^accounts/password/reset/$', ("django.contrib.auth.views."
"password_reset"),
{'password_reset_form': CirclePasswordResetForm},
name="accounts.password-reset",
),
url(r'^accounts/login/?$', circle_login, name="accounts.login"),
url(r'^accounts/', include('django.contrib.auth.urls')), url(r'^accounts/', include('django.contrib.auth.urls')),
) )
......
...@@ -14,6 +14,10 @@ from model_utils.models import TimeStampedModel ...@@ -14,6 +14,10 @@ from model_utils.models import TimeStampedModel
logger = getLogger(__name__) logger = getLogger(__name__)
class WorkerNotFound(Exception):
pass
def activitycontextimpl(act, on_abort=None, on_commit=None): def activitycontextimpl(act, on_abort=None, on_commit=None):
try: try:
yield act yield act
...@@ -60,6 +64,7 @@ class ActivityModel(TimeStampedModel): ...@@ -60,6 +64,7 @@ class ActivityModel(TimeStampedModel):
if not self.finished: if not self.finished:
self.finished = timezone.now() self.finished = timezone.now()
self.succeeded = succeeded self.succeeded = succeeded
if result is not None:
self.result = result self.result = result
if event_handler is not None: if event_handler is not None:
event_handler(self) event_handler(self)
......
[
{
"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
}
}
]
...@@ -1372,6 +1372,35 @@ ...@@ -1372,6 +1372,35 @@
} }
}, },
{ {
"pk": 12,
"model": "vm.instance",
"fields": {
"destroyed": null,
"disks": [],
"boot_menu": false,
"owner": 1,
"time_of_delete": null,
"max_ram_size": 200,
"pw": "ads",
"time_of_suspend": null,
"ram_size": 200,
"priority": 4,
"active_since": null,
"template": null,
"access_method": "nx",
"lease": 1,
"node": null,
"description": "",
"arch": "x86_64",
"name": "vanneve",
"created": "2013-09-16T09:05:59.991Z",
"raw_data": "",
"vnc_port": 1235,
"num_cores": 2,
"modified": "2013-10-14T07:27:38.192Z"
}
},
{
"pk": 1, "pk": 1,
"model": "firewall.domain", "model": "firewall.domain",
"fields": { "fields": {
...@@ -1462,7 +1491,6 @@ ...@@ -1462,7 +1491,6 @@
"modified": "2014-01-24T00:58:19.654Z", "modified": "2014-01-24T00:58:19.654Z",
"system": "", "system": "",
"priority": 20, "priority": 20,
"state": "READY",
"access_method": "ssh", "access_method": "ssh",
"raw_data": "", "raw_data": "",
"arch": "x86_64", "arch": "x86_64",
......
...@@ -5,9 +5,15 @@ from django.conf import settings ...@@ -5,9 +5,15 @@ from django.conf import settings
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.contrib.auth.signals import user_logged_in from django.contrib.auth.signals import user_logged_in
from django.db.models import ( from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField,
DateTimeField,
) )
from django.utils.translation import ugettext_lazy as _ from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _, override
from model_utils.models import TimeStampedModel
from model_utils.fields import StatusField
from model_utils import Choices
from vm.models import Instance from vm.models import Instance
from acl.models import AclBase from acl.models import AclBase
...@@ -20,6 +26,34 @@ class Favourite(Model): ...@@ -20,6 +26,34 @@ class Favourite(Model):
user = ForeignKey(User) user = ForeignKey(User)
class Notification(TimeStampedModel):
STATUS = Choices(('new', _('new')),
('delivered', _('delivered')),
('read', _('read')))
status = StatusField()
to = ForeignKey(User)
subject = CharField(max_length=128)
message = TextField()
valid_until = DateTimeField(null=True, default=None)
class Meta:
ordering = ['-created']
@classmethod
def send(cls, user, subject, template, context={}, valid_until=None):
try:
language = user.profile.preferred_language
except:
language = None
with override(language):
context['user'] = user
rendered = render_to_string(template, context)
subject = unicode(subject)
return cls.objects.create(to=user, subject=subject, message=rendered,
valid_until=valid_until)
class Profile(Model): class Profile(Model):
user = OneToOneField(User) user = OneToOneField(User)
preferred_language = CharField(verbose_name=_('preferred language'), preferred_language = CharField(verbose_name=_('preferred language'),
...@@ -31,6 +65,10 @@ class Profile(Model): ...@@ -31,6 +65,10 @@ class Profile(Model):
help_text=_('Unique identifier of the person, e.g. a student number.')) help_text=_('Unique identifier of the person, e.g. a student number.'))
instance_limit = IntegerField(default=5) instance_limit = IntegerField(default=5)
def notify(self, subject, template, context={}, valid_until=None):
return Notification.send(self.user, subject, template, context,
valid_until)
class GroupProfile(AclBase): class GroupProfile(AclBase):
ACL_LEVELS = ( ACL_LEVELS = (
......
...@@ -331,3 +331,61 @@ a.hover-black { ...@@ -331,3 +331,61 @@ a.hover-black {
display: block; display: block;
} }
.notification-messages {
padding: 10px 8px;
width: 350px;
}
.notification-message {
margin-bottom: 10px;
padding: 0 0 4px 0;
border-bottom: 1px dotted #D3D3D3;
}
.notification-messages .notification-message:last-child {
margin-bottom: 0px;
padding: 0px;
border-bottom: none;
}
.notification-message-text {
padding: 8px 15px;
display: none;
}
.notification-message .notification-message-subject {
cursor: pointer;
}
#vm-migrate-node-list {
list-style: none;
}
#vm-migrate-node-list li {
padding-bottom: 10px;
}
.vm-migrate-node-property {
display: block;
padding-left: 15px;
}
.vm-details-help {
max-width: 700px;
padding: 10px 10px 0px 10px;
margin-left: 50px;
/* fancy stuff
border: 1px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
border-radius: 8px;
*/
}
.vm-details-help li {
padding-bottom: 5px;
}
.vm-details-help ul {
padding-left: 0px;
}
...@@ -66,6 +66,8 @@ $(function () { ...@@ -66,6 +66,8 @@ $(function () {
if (window.location.hash) { if (window.location.hash) {
if(window.location.hash.substring(1,4) == "ipv") if(window.location.hash.substring(1,4) == "ipv")
$("a[href=#network]").tab('show'); $("a[href=#network]").tab('show');
if(window.location.hash == "activity")
checkNewActivity(false, 1);
$("a[href=" + window.location.hash +"]").tab('show'); $("a[href=" + window.location.hash +"]").tab('show');
} }
...@@ -206,6 +208,15 @@ $(function () { ...@@ -206,6 +208,15 @@ $(function () {
} }
}); });
/* notification message toggle */
$(document).on('click', ".notification-message-subject", function() {
$(".notification-message-text", $(this).parent()).slideToggle();
return false;
});
$("#notification-button a").click(function() {
$('.notification-messages').load("/dashboard/notifications/");
});
}); });
function generateVmHTML(pk, name, fav) { function generateVmHTML(pk, name, fav) {
......
/* for functions in both vm list and vm detail */
$(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) {
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();};
});
...@@ -2,24 +2,72 @@ var vlans = []; ...@@ -2,24 +2,72 @@ var vlans = [];
var disks = []; var disks = [];
$(function() { $(function() {
vmCreateLoaded(); vmCustomizeLoaded();
}); });
function vmCreateLoaded() { function vmCreateLoaded() {
$('.vm-create-advanced').hide(); $(".vm-create-template-details").hide();
$('.vm-create-advanced-btn').click(function() {
$('.vm-create-advanced').stop().slideToggle(); $(".vm-create-template-summary").click(function() {
if ($('.vm-create-advanced-icon').hasClass('icon-caret-down')) { $(this).next(".vm-create-template-details").slideToggle();
$('.vm-create-advanced-icon').removeClass('icon-caret-down').addClass('icon-caret-up'); });
} else {
$('.vm-create-advanced-icon').removeClass('icon-caret-up').addClass('icon-caret-down'); $(".customize-vm").click(function() {
var template = $(this).data("template-pk");
console.log(template);
$.get("/dashboard/vm/create/?template=" + template, function(data) {
var r = $('#create-modal'); r.next('div').remove(); r.remove();
$('body').append(data);
vmCreateLoaded();
addSliderMiscs();
$('#create-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
});
});
return false;
});
/* start vm button clicks */
$('.vm-create-start').click(function() {
template = $(this).data("template-pk");
$.ajax({
url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')},
type: 'POST',
data: {'template': template},
success: function(data, textStatus, xhr) {
if(data.redirect) {
window.location.replace(data.redirect + '#activity');
} }
else {
var r = $('#create-modal'); r.next('div').remove(); r.remove();
$('body').append(data);
vmCreateLoaded();
addSliderMiscs();
$('#create-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
}); });
}
},
error: function(xhr, textStatus, error) {
var r = $('#create-modal'); r.next('div').remove(); r.remove();
$('#vm-create-template-select').change(function() { if (xhr.status == 500) {
vmCreateTemplateChange(this); addMessage("500 Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
}
}); });
return false;
});
}
function vmCustomizeLoaded() {
/* network thingies */ /* network thingies */
/* add network */ /* add network */
...@@ -86,15 +134,24 @@ function vmCreateLoaded() { ...@@ -86,15 +134,24 @@ function vmCreateLoaded() {
/* copy networks from hidden select */ /* copy networks from hidden select */
$('#vm-create-network-add-vlan option').each(function() { $('#vm-create-network-add-vlan option').each(function() {
var managed = $(this).text().indexOf("mana") == 0; var managed = $(this).text().indexOf("mana") == 0;
var text = $(this).text(); var raw_text = $(this).text();
var pk = $(this).val(); var pk = $(this).val();
if(managed) { if(managed) {
text = text.replace("managed -", "&#xf0ac;"); text = raw_text.replace("managed -", "&#xf0ac;");
} else { } else {
text = text.replace("unmanaged -", "&#xf0c1;"); text = raw_text.replace("unmanaged -", "&#xf0c1;");
} }
var html = '<option data-managed="' + (managed ? 1 : 0) + '" value="' + pk + '">' + text + '</option>'; var html = '<option data-managed="' + (managed ? 1 : 0) + '" value="' + pk + '">' + text + '</option>';
if($('#vm-create-network-list span').length < 1) {
$("#vm-create-network-list").html("");
}
if($(this).is(":selected")) {
$("#vm-create-network-list").append(vmCreateNetworkLabel(pk, raw_text.replace("unmanaged -", "").replace("managed -", ""), managed));
} else {
$('#vm-create-network-add-select').append(html); $('#vm-create-network-add-select').append(html);
}
}); });
...@@ -168,8 +225,20 @@ function vmCreateLoaded() { ...@@ -168,8 +225,20 @@ function vmCreateLoaded() {
}); });
/* copy disks from hidden select */ /* copy disks from hidden select */
$('#vm-create-disk-add-select').html($('#vm-create-disk-add-form').html()); $('#vm-create-disk-add-form option').each(function() {
var text = $(this).text();
var pk = $(this).val();
var html = '<option value="' + pk + '">' + text + '</option>';
if($('#vm-create-disk-list span').length < 1) {
$("#vm-create-disk-list").html("");
}
if($(this).is(":selected")) {
$("#vm-create-disk-list").append(vmCreateDiskLabel(pk, text));
} else {
$('#vm-create-disk-add-select').append(html);
}
});
/* build up disk list */ /* build up disk list */
$('#vm-create-disk-add-select option').each(function() { $('#vm-create-disk-add-select option').each(function() {
...@@ -179,8 +248,8 @@ function vmCreateLoaded() { ...@@ -179,8 +248,8 @@ function vmCreateLoaded() {
}); });
}); });
/* add button */ /* start vm button clicks */
$('#vm-create-submit').click(function() { $('#vm-create-customized-start').click(function() {
$.ajax({ $.ajax({
url: '/dashboard/vm/create/', url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
...@@ -219,97 +288,6 @@ function vmCreateLoaded() { ...@@ -219,97 +288,6 @@ function vmCreateLoaded() {
$('.js-hidden').hide(); $('.js-hidden').hide();
} }
function vmCreateTemplateChange(new_this) {
this.value = new_this.value;
if(this.value < 0) return;
$.ajax({
url: '/dashboard/template/' + this.value,
type: 'GET',
success: function(data, textStatus, xhr) {
if(xhr.status == 200) {
// set sliders
$('#vm-cpu-priority-slider').slider("setValue", data['priority']);
$('#vm-cpu-count-slider').slider("setValue", data['num_cores']);
$('#vm-ram-size-slider').slider("setValue", data['ram_size']);
/* slider doesn't have change event ........................ */
refreshSliders();
/* clear selections */
$("#vm-create-network-add-vlan").find('option').prop('selected', false);
$('#vm-create-disk-add-form').find('option').prop('selected', false);
/* clear the network select */
$("#vm-create-network-add-select").html('');
/* append vlans from InterfaceTemplates */
$('#vm-create-network-list').html("");
var added_vlans = []
for(var n = 0; n<data['network'].length; n++) {
nn = data['network'][n]
$('#vm-create-network-list').append(
vmCreateNetworkLabel(nn.vlan_pk, nn.vlan, nn.managed)
);
$('#vm-create-network-add-vlan option[value="' + nn.vlan_pk + '"]').prop('selected', true);
added_vlans.push(nn.vlan_pk);
}
/* remove already added vlans from dropdown or add new ones */
$('#vm-create-network-add-select').html('');
// this is working because the vlans array already has the icon's hex code
for(var i=0; i < vlans.length; i++)
if(added_vlans.indexOf(vlans[i].pk) == -1) {
var html = '<option data-managed="' + (vlans[i].managed ? 1 : 0) + '" value="' + vlans[i].pk + '">' + vlans[i].name + '</option>';
$('#vm-create-network-add-select').append(html);
}
/* enable the network add button if there are not added vlans */
if(added_vlans.length != vlans.length) {
$('#vm-create-network-add-button').attr('disabled', false);
} else {
$('#vm-create-network-add-select').html('<option value="-1">No more networks!</option>');
$('#vm-create-network-add-button').attr('disabled', true);
}
/* if there are no added vlans print it out */
if(added_vlans.length < 1) {
$('#vm-create-network-list').html("Not added to any network!");
}
/* append disks */
$('#vm-create-disk-list').html('');
var added_disks = []
for(var d = 0; d<data['disks'].length; d++) {
dd = data['disks'][d]
$('#vm-create-disk-list').append(
vmCreateDiskLabel(dd.pk, dd.name)
);
$('#vm-create-disk-add-form option[value="' + dd.pk + '"]').prop('selected', true);
added_disks.push(dd.pk);
}
/* remove already added disks from dropdown or add new ones */
$('#vm-create-disk-add-select').html('');
for(var i=0; i < disks.length; i++)
if(added_disks.indexOf(disks[i].pk) == -1)
$('#vm-create-disk-add-select').append($('<option>', {
value: disks[i].pk,
text: disks[i].name
}));
/* enable the disk add button if there are not added disks */
if(added_disks.length != disks.length) {
$('#vm-create-disk-add-button').attr('disabled', false);
} else {
$('#vm-create-disk-add-select').html('<option value="-1">We are out of &lt;options&gt; hehe</option>');
$('#vm-create-disk-add-button').attr('disabled', true);
}
}
}
});
}
function vmCreateNetworkLabel(pk, name, managed) { function vmCreateNetworkLabel(pk, name, managed) {
return '<span id="vlan-' + pk + '" class="label label-' + (managed ? 'primary' : 'default') + '"><i class="icon-' + (managed ? 'globe' : 'link') + '"></i> ' + name + ' <a href="#" class="hover-black vm-create-remove-network"><i class="icon-remove-sign"></i></a></span> '; return '<span id="vlan-' + pk + '" class="label label-' + (managed ? 'primary' : 'default') + '"><i class="icon-' + (managed ? 'globe' : 'link') + '"></i> ' + name + ' <a href="#" class="hover-black vm-create-remove-network"><i class="icon-remove-sign"></i></a></span> ';
......
...@@ -3,6 +3,10 @@ $(function() { ...@@ -3,6 +3,10 @@ $(function() {
if(decideActivityRefresh()) { if(decideActivityRefresh()) {
checkNewActivity(false, 1); checkNewActivity(false, 1);
} }
$('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('icon-spin');
checkNewActivity(false,0);
});
/* save resources */ /* save resources */
$('#vm-details-resources-save').click(function() { $('#vm-details-resources-save').click(function() {
...@@ -131,7 +135,11 @@ $(function() { ...@@ -131,7 +135,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 {
...@@ -152,6 +160,11 @@ $(function() { ...@@ -152,6 +160,11 @@ $(function() {
$("#vm-details-disk-add-for-form").html($("#vm-details-disk-add-form").html()); $("#vm-details-disk-add-for-form").html($("#vm-details-disk-add-form").html());
return false; return false;
}); });
/* show help */
$(".vm-details-help-button").click(function() {
$(".vm-details-help").stop().slideToggle();
});
}); });
...@@ -198,17 +211,23 @@ function checkNewActivity(only_state, runs) { ...@@ -198,17 +211,23 @@ function checkNewActivity(only_state, runs) {
success: function(data) { success: function(data) {
if(!only_state) { if(!only_state) {
$("#activity-timeline").html(data['activities']); $("#activity-timeline").html(data['activities']);
$("[title]").tooltip();
} }
$("#vm-details-state").html(data['state']); $("#vm-details-state").html(data['state']);
if(data['state'] == "RUNNING") {
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
} else {
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
}
if(decideActivityRefresh()) { if(runs > 0 && decideActivityRefresh()) {
console.log("szia");
setTimeout( setTimeout(
function() {checkNewActivity(only_state, runs + 1)}, function() {checkNewActivity(only_state, runs + 1)},
1000 + runs * 250 1000 + Math.exp(runs * 0.05)
); );
} }
$('a[href="#activity"] i').removeClass('icon-spin');
}, },
error: function() { error: function() {
......
{% load i18n %}
{% load sizefieldtags %}
<i class="{% if d.is_downloading %}icon-refresh icon-spin{% else %}icon-file{% endif %}"></i>
{{ d.name }} (#{{ d.id }}) -
{% if not d.is_downloading %}
{% if d.ready %}
{{ d.size|filesize }}
{% else %}
<div class="label label-danger">failed</div>
{% endif %}
{% else %}<span class="disk-list-disk-percentage" data-disk-pk="{{ d.pk }}">{{ d.get_download_percentage }}</span>%{% endif %}
<div class="btn btn-xs btn-danger pull-right"><i class="icon-remove"></i> Remove</div>
{% load i18n %}
{% for n in notifications %}
<li class="notification-message">
<span class="notification-message-subject">
{% if n.status == "new" %}<i class="icon-envelope-alt"></i> {% endif %}
{{ n.subject }}
</span>
<span class="notification-message-date pull-right">
{{ n.created|timesince }}
</span>
<div style="clear: both;"></div>
<div class="notification-message-text">
{{ n.message|safe }}
</div>
</li>
{% empty %}
<li class="notification-message">
{% trans "You have no notifications." %}
</li>
{% endfor %}
{% load sizefieldtags %}
{% load i18n %}
<div class="vm-create-template-list">
{% for t in templates %}
<div class="vm-create-template">
<div class="vm-create-template-summary">
{{ t.name }}
<span class="pull-right"><i class="icon-{{ t.os_type }}"></i> {{ t.system }}</span>
</div>
<div class="vm-create-template-details">
<ul>
<li>
<i class="icon-gears"></i> {% trans "CPU" %}
<div class="progress pull-right">
<div class="progress-bar progress-bar-success" role="progressbar"
aria-valuenow="{{ t.num_cores }}" aria-valuemin="0" aria-valuemax="8" style="width: 80%">
<span class="progress-bar-text">{{ t.num_cores }} cores</span>
</div>
</div>
</li>
<li>
<i class="icon-ticket"></i> {% trans "Memory" %}
<div class="progress pull-right">
<div class="progress-bar progress-bar-info" role="progressbar"
aria-valuenow="{{ t.ram_size }}" aria-valuemin="0" aria-valuemax="4096"
style="width: 80%">
<span class="progress-bar-text">{{ t.ram_size }} MB</span>
</div>
</div>
</li>
<li>
<i class="icon-file"></i> {% trans "Disks" %}
<span style="float: right; text-align: right;">
{% for d in t.disks.all %}{{ d.name }} ({{ d.size|filesize }}){% if not forloop.last %}, {% endif %}{% endfor %}
</span>
<div style="clear: both;"></div>
</li>
<li>
<i class="icon-globe"></i> {% trans "Network" %}
<span style="float: right;">
{% for i in t.interface_set.all %}{{ i.vlan.name }}{% if not forloop.last %}, {% endif %}{% endfor %}
</span>
</li>
<li>
<i class="icon-tag"></i> {% trans "Type" %}: {{ t.lease.name }}
<span style="float: right;">
<i class="icon-pause"></i> {{ t.lease.get_readable_suspend_time }}
<i class="icon-remove"></i> {{ t.lease.get_readable_delete_time }}
</span>
</li>
<li>
<i class="icon-hand-right"></i> Description:
<span style="float: right; max-width: 350px;">
{{ t.description }}
</span>
<div class="clearfix"></div>
</li>
</ul>
<div style="margin-top: 20px; padding: 0 15px; width: 100%">
<a class="btn btn-primary btn-xs customize-vm" data-template-pk="{{ t.pk }}" href="{% url "dashboard.views.vm-create" %}?template={{ t.pk }}"><i class="icon-wrench"></i> Customize</a>
<form class="pull-right text-right" method="POST" action="{% url "dashboard.views.vm-create" %}">
{% csrf_token %}
<input type="hidden" name="template" value="{{ t.pk }}"/>
<button class="btn btn-success btn-xs vm-create-start" data-template-pk="{{ t.pk }}" type="submit"><i class="icon-play"></i> Start</button>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
<style>
.row {
margin-bottom: 15px;
}
.vm-create-template {
max-width: 800px;
border: 1px solid black;
border-bottom: none;
}
.vm-create-template-list .vm-create-template:last-child {
border-bottom: 1px solid black;
}
.vm-create-template-summary {
padding: 15px;
cursor: pointer;
}
.vm-create-template:nth-child(odd) .vm-create-template-summary {
background: #F5F5F5;
}
.vm-create-template-list .vm-create-template-summary:hover {
background: #D2D2D2;
}
.vm-create-template-details {
border-top: 1px dashed #D3D3D3;
padding: 15px;
}
.vm-create-template-details ul {
list-style: none;
padding: 0 15px;
}
.vm-create-template-details li {
border-bottom: 1px dotted #aaa;
padding: 5px 0px;
}
.progress {
position: relative;
width: 200px;
height: 12px;
margin-bottom: 0px;
margin-top: 5px;
}
.progress-bar-text {
position: absolute;
display: block;
width: 100%;
color: white;
/* outline */
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
font-size: 10px;
}
</style>
{% block "extra-js" %}
<script>
$('.progress-bar').each(function() {
var min = $(this).attr('aria-valuemin');
var max = $(this).attr('aria-valuemax');
var now = $(this).attr('aria-valuenow');
var siz = (now-min)*100/(max-min);
$(this).css('width', siz+'%');
});
</script>
{% endblock %}
{% load crispy_forms_tags %}
{% load sizefieldtags %}
{% crispy vm_create_form %}
<script src="/static/dashboard/vm-create.js"></script>
{% load i18n %}
{% load sizefieldtags %}
<form method="POST" action="{% url "dashboard.views.vm-migrate" pk=vm.pk %}">
{% csrf_token %}
<ul id="vm-migrate-node-list">
{% 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>
...@@ -23,8 +23,19 @@ ...@@ -23,8 +23,19 @@
<body> <body>
<div class="navbar navbar-inverse navbar-fixed-top"> <div class="navbar navbar-inverse navbar-fixed-top">
<a class="navbar-brand" href="/dashboard/">{% block header-site %}CIRCLE{% endblock %}</a> <div class="navbar-header">
<!-- temporarily --> <a class="navbar-brand" href="{% url "dashboard.index" %}">CIRCLE</a>
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div><!-- .navbar-header -->
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav pull-right">
{% block navbar-ul %}
{% endblock %}
</ul>
<a class="navbar-brand pull-right" href="/network/" style="color: white; font-size: 10px;">Network</a> <a class="navbar-brand pull-right" href="/network/" style="color: white; font-size: 10px;">Network</a>
<a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;">Admin</a> <a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;">Admin</a>
{% if user.is_authenticated %} {% if user.is_authenticated %}
...@@ -32,7 +43,8 @@ ...@@ -32,7 +43,8 @@
{% else %} {% else %}
<a class="navbar-brand pull-right" href="{% url "login" %}?next={% url "dashboard.index" %}" style="color: white; font-size: 10px;">Login</a> <a class="navbar-brand pull-right" href="{% url "login" %}?next={% url "dashboard.index" %}" style="color: white; font-size: 10px;">Login</a>
{% endif %} {% endif %}
</div> </div><!-- .collapse .navbar-collapse -->
</div><!-- navbar navbar-inverse navbar-fixed-top -->
<div class="container"> <div class="container">
{% block messages %} {% block messages %}
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{%blocktrans with instance=instance.name%}
Renewing <em>{{instance}}</em>
{%endblocktrans%}
</h3>
</div>
<div class="panel-body">
{%blocktrans with object=instance.name%}
Do you want to renew <strong>{{ object }}</strong>?
{%endblocktrans%}
{%blocktrans with suspend=time_of_suspend delete=time_of_delete|default:"n/a" %}
The instance will be suspended at <em>{{suspend}}</em>
and removed at <em>{{delete}}</em> if you renew it now.
{%endblocktrans%}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a class="btn btn-default"
href="{{instance.get_absolute_path}}">{% trans "Back" %}</a>
<button class="btn btn-danger">{% trans "Renew" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
...@@ -3,6 +3,17 @@ ...@@ -3,6 +3,17 @@
{% block title-page %}{% trans "Dashboard" %}{% endblock %} {% block title-page %}{% trans "Dashboard" %}{% endblock %}
{% block navbar-ul %}
<li class="dropdown" id="notification-button">
<a href="{% url "dashboard.views.notifications" %}" style="color: white; font-size: 12px;" class="dropdown-toggle" data-toggle="dropdown">
Notifications{% if new_notifications %} <span class="badge">{{ new_notifications }}</span>{% endif %}
</a>
<ul class="dropdown-menu notification-messages">
<li>{% trans "Loading..." %}</li>
</ul>
</li>
{% endblock %}
{% block content %} {% block content %}
<div class="body-content dashboard-index"> <div class="body-content dashboard-index">
<div class="row"> <div class="row">
...@@ -17,9 +28,11 @@ ...@@ -17,9 +28,11 @@
{% include "dashboard/index-groups.html" %} {% include "dashboard/index-groups.html" %}
</div> </div>
{% comment %}
<div class="col-lg-4 col-sm-6"> <div class="col-lg-4 col-sm-6">
{% include "dashboard/index-files.html" %} {% include "dashboard/index-files.html" %}
</div> </div>
{% endcomment %}
{% if perms.vm.create_template %} {% if perms.vm.create_template %}
<div class="col-lg-4 col-sm-6"> <div class="col-lg-4 col-sm-6">
......
<div class="modal fade" id="create-modal" tabindex="-1" role="dialog"> <div class="modal fade" id="create-modal" tabindex="-1" role="dialog">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
{% if box_title and ajax_title %}
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{{ box_title }}</h4> <h4 class="modal-title">{{ box_title }}</h4>
</div> </div>
{% endif %}
<div class="modal-body"> <div class="modal-body">
{% include template %} {% include template %}
</div> </div>
......
...@@ -8,10 +8,10 @@ ...@@ -8,10 +8,10 @@
<div class="row"> <div class="row">
<div class="col-md-4" id="node-info-pane"> <div class="col-md-4" id="node-info-pane">
<div id="node-info-data" class="big"> <div id="node-info-data" class="big">
<span id="node-status-label" class="label {% if node.state == 'Online' %}label-success <span id="node-details-state" class="label {% if node.state == 'ONLINE' %}label-success
{% elif node.state == 'Missing' %}label-danger {% elif node.state == 'MISSING' %}label-danger
{% elif node.state == 'Disabled' %}label-warning {% elif node.state == 'DISABLED' %}label-warning
{% elif node.state == 'Offline' %}label-warning {% elif node.state == 'OFFLINE' %}label-warning
{% endif %}">{{ node.state }}</span> {% endif %}">{{ node.state }}</span>
<div class="btn-group"> <div class="btn-group">
{% with node as record %} {% with node as record %}
...@@ -42,10 +42,10 @@ ...@@ -42,10 +42,10 @@
</ul> </ul>
<div id="panel-body" class="tab-content panel-body"> <div id="panel-body" class="tab-content panel-body">
<div class="tab-pane active" id="home">{% include "dashboard/node-detail-home.html" %}</div> <div class="tab-pane active" id="home">{% include "dashboard/node-detail/home.html" %}</div>
<div class="tab-pane" id="resources">{% include "dashboard/node-detail-resources.html" %}</div> <div class="tab-pane" id="resources">{% include "dashboard/node-detail/resources.html" %}</div>
<div class="tab-pane" id="activity">{% include "dashboard/node-detail-activity.html" %}</div> <div class="tab-pane" id="activity">{% include "dashboard/node-detail/activity.html" %}</div>
<div class="tab-pane" id="virtualmachines">{% include "dashboard/node-detail-vm.html" %}</div> <div class="tab-pane" id="virtualmachines">{% include "dashboard/node-detail/vm.html" %}</div>
</div> </div>
</div> </div>
</div> </div>
......
{% load i18n %} {% load i18n %}
<h3>{% trans "Activity" %}</h3> {% for a in activities %}
<style> <div class="activity" data-activity-id="{{ a.pk }}">
.sub-timeline { <span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
border-left: 3px solid green;
margin-left: 30px;
padding-left: 10px;
}
</style>
<div class="timeline">
{% for a in activity %}
<div class="activity" data-activity-id="{{ a.pk }}">
<span class="timeline-icon">
<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>{{ a.get_readable_name }}</strong>
{{ a.started|date:"Y-m-d. H:i" }}, {{ a.user }} {{ a.started|date:"Y-m-d H:i" }}, {{ a.user }}
{% 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"> <div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}">
{{ s.get_readable_name }} - {{ s.get_readable_name }} -
{% if s.finished %} {% if s.finished %}
{{ s.finished|time:"H:i:s" }} {{ s.finished|time:"H:i:s" }}
{% else %} {% else %}
<i class="icon-refresh icon-spin" class="sub-activity-loading-icon"></i> <i class="icon-refresh icon-spin" class="sub-activity-loading-icon"></i>
{% endif %} {% endif %}
{% if s.has_failed %}
<div class="label label-danger">{% trans "failed" %}</div>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
</div>
{% endfor %}
<div><span class="timeline-icon timeline-warning"><i class="icon-remove"></i></span> <strong>Removing</strong> 2013-11-21 15:32</div>
<div><span class="timeline-icon timeline-warning"><i class="icon-pause"></i></span> <strong>Suspending</strong> 2013-09-21 15:32</div>
<div><span class="timeline-icon"><i class="icon-ellipsis-vertical" ></i></span> <strong>(now)</strong></div>
<div><span class="timeline-icon"><i class="icon-truck"></i></span> <strong>Migrated to mega5</strong> 2013-04-21 15:32, ABC123</div>
<div><span class="timeline-icon"><i class="icon-refresh"></i></span> <strong>Forced reboot</strong> 2013-04-21 15:32, ABC123</div>
<div><span class="timeline-icon"><i class="icon-plus"></i></span> <strong>Created</strong> 2013-04-21 15:32, ABC123</div>
</div> </div>
{% endfor %}
{% block extra_js %}
<script src="{{ STATIC_URL }}dashboard/vm-details.js"></script>
{% endblock %}
{% load i18n %}
<h3>{% trans "Activity" %}</h3>
<div id="activity-timeline" class="timeline">
{% include "dashboard/node-detail/_activity-timeline.html" %}
</div>
...@@ -35,10 +35,11 @@ ...@@ -35,10 +35,11 @@
</div><!-- id:node-details-traits --> </div><!-- id:node-details-traits -->
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<img src="/static/grafikon.png" style="width:45%"/> {% if graphite_enabled %}
<img src="/static/grafikon.png" style="width:45%"/> <img src="{% url "dashboard.views.node-graph" node.pk "cpu" "6h" %}" style="width:100%"/>
<img src="/static/grafikon.png" style="width:45%"/> <img src="{% url "dashboard.views.node-graph" node.pk "memory" "6h" %}" style="width:100%"/>
<img src="/static/grafikon.png" style="width:45%"/> <img src="{% url "dashboard.views.node-graph" node.pk "network" "6h" %}" style="width:100%"/>
{% endif %}
</div> </div>
</div> </div>
<style> <style>
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
{% if template == "dashboard/vm-create.html" %} {% if template == "dashboard/_vm-create-1.html" %}
<script src="{{ STATIC_URL }}dashboard/vm-create.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-create.js"></script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin"><i class="icon-desktop"></i> {% trans "Notifications" %}</h3>
</div>
<div class="panel-body">
<ul style="list-style: none;">
{% include "dashboard/_notifications-timeline.html" %}
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{%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>
{%load i18n%}
{%blocktrans with instance=instance.name url=instance.get_absolute_url %}
Your instance <a href="{{url}}">{{instance}}</a> has been destroyed due to expiration.
{%endblocktrans%}
{%load i18n%}
{%blocktrans with instance=instance.name url=instance.get_absolute_url suspend=instance.time_of_suspend delete=instance.time_of_delete %}
Your instance <a href="{{url}}">{{instance}}</a> is going to expire.
It will be suspended at {{suspend}} and destroyed at {{delete}}.
{%endblocktrans%}
{%blocktrans with token=token url=instance.get_absolute_url %}
Please, either <a href="{{token}}">renew</a> or <a href="{{url}}">destroy</a>
it now.
{%endblocktrans%}
{%load i18n%}
{%blocktrans with instance=instance.name url=instance.get_absolute_url %}
Your instance <a href="{{url}}">{{instance}}</a> has been suspended due to expiration.
{%endblocktrans%}
{% extends "dashboard/base.html" %} {% extends "dashboard/base.html" %}
{% load i18n %} {% load i18n %}
{% load sizefieldtags %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block content %} {% block content %}
...@@ -22,7 +23,7 @@ ...@@ -22,7 +23,7 @@
<div class="col-md-4"> <div class="col-md-4">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="no-margin"><i class="icon-group"></i> {% trans "Manage access" %}</h3> <h4 class="no-margin"><i class="icon-group"></i> {% trans "Manage access" %}</h4>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form action="{% url "dashboard.views.template-acl" pk=object.pk %}" method="post">{% csrf_token %} <form action="{% url "dashboard.views.template-acl" pk=object.pk %}" method="post">{% csrf_token %}
...@@ -64,8 +65,35 @@ ...@@ -64,8 +65,35 @@
</form> </form>
</div> </div>
</div> </div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="no-margin"><i class="icon-file"></i> {% trans "Disk list" %}</h4>
</div>
<div class="panel-body">
<ul style="list-style: none; padding-left: 0;">
{% for d in disks %}
<li>
{% include "dashboard/_disk-list-element.html" %}
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="no-margin"><i class="icon-folder-open"></i> {% trans "Create new disk" %}</h4>
</div>
<div class="panel-body">
<form action="{% url "dashboard.views.disk-add" %}" method="POST">
{% crispy disk_add_form %}
</form>
</div>
</div> </div>
</div> </div><!-- .col-md-4 -->
</div><!-- .row -->
<style> <style>
......
This is a test for {{user.username}}.
Var: {{var}}.
{% load crispy_forms_tags %}
<style>
.row {
margin-bottom: 15px;
}
</style>
<form method="POST" action="/dashboard/vm/create/">
{% csrf_token %}
{% crispy vm_create_form %}
</form>
...@@ -5,35 +5,53 @@ ...@@ -5,35 +5,53 @@
<div class="body-content"> <div class="body-content">
<div class="page-header"> <div class="page-header">
<div class="pull-right" style="padding-top: 15px;"> <div class="pull-right" style="padding-top: 15px;">
<a title="Rename" href="#" class="btn btn-default btn-xs vm-details-rename-button"><i class="icon-pencil"></i></a>
<a title="Pause == sleep?" href="#" class="btn btn-default btn-xs"><i class="icon-pause"></i></a>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}"> <form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="sleep" value="dummy"/> <input type="hidden" name="sleep" />
<button title="{% trans "Sleep" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-moon"></i></button> <button title="{% trans "Sleep" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-moon"></i></button>
</form> </form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}"> <form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="deploy" value="dummy"/> <input type="hidden" name="deploy" />
<button title="{% trans "Deploy" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-play"></i></button> <button title="{% trans "Deploy" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-play"></i></button>
</form> </form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}"> <form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="wake_up" value="dummy"/> <input type="hidden" name="wake_up" />
<button title="{% trans "Wake up" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-sun"></i></button> <button title="{% trans "Wake up" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-sun"></i></button>
</form> </form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}"> <form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="shut_down" value="dummy"/> <input type="hidden" name="shut_down" />
<button title="{% trans "Shut down" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-off"></i></button> <button title="{% trans "Shut down" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-off"></i></button>
</form> </form>
<a title="Migrate" href="#" class="btn btn-default btn-xs"><i class="icon-truck"></i></a>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}"> <form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="save_as" value="dummy"/> <input type="hidden" name="reboot" />
<button title="{% trans "Reboot (ctrl + alt + del)" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-refresh"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="reset" />
<button title="{% trans "Reset (power cycle)" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-bolt"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="shut_off"/>
<button title="{% trans "Shut off" %}" class="btn btn-default btn-xs" type="submit">
<i class="icon-ban-circle"></i>
</button>
</form>
<a title="Migrate" data-vm-pk="{{ instance.pk }}" href="{% url "dashboard.views.vm-migrate" pk=instance.pk %}" class="btn btn-default btn-xs vm-migrate">
<i class="icon-truck"></i>
</a>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="save_as" />
<button title="{% trans "Save as template" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-save"></i></button> <button title="{% trans "Save as template" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-save"></i></button>
</form> </form>
<a title="{% trans "Destroy" %}" href="{% url "dashboard.views.delete-vm" pk=instance.pk %}" class="btn btn-default btn-xs vm-delete" data-vm-pk="{{ instance.pk }}"><i class="icon-remove"></i></a> <a title="{% trans "Destroy" %}" href="{% url "dashboard.views.delete-vm" pk=instance.pk %}" class="btn btn-default btn-xs vm-delete" data-vm-pk="{{ instance.pk }}"><i class="icon-remove"></i></a>
<a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs vm-details-help-button"><i class="icon-question"></i></a>
</div> </div>
<h1> <h1>
<div id="vm-details-rename"> <div id="vm-details-rename">
...@@ -46,7 +64,49 @@ ...@@ -46,7 +64,49 @@
<div id="vm-details-h1-name"> <div id="vm-details-h1-name">
{{ instance.name }} {{ instance.name }}
</div> </div>
<small>{{ instance.primary_host.get_fqdn }}</small></h1> <small>{{ instance.primary_host.get_fqdn }}</small>
</h1>
<div class="vm-details-help js-hidden">
<ul style="list-style: none;">
<li>
<strong>{% trans "Sleep" %}:</strong>
{% trans "Suspend virtual machine with memory dump." %}
</li>
<li>
<strong>{% trans "Wake up" %}:</strong>
{% trans "?" %}
</li>
<li>
<strong>{% trans "Shutdown" %}:</strong>
{% trans "Shutdown virtual machine with ACPI signal." %}
</li>
<li>
<strong>{% trans "Reboot (ctrl + alt + del)" %}:</strong>
{% trans "Reboot virtual machine with Ctrl+Alt+Del signal." %}
</li>
<li>
<strong>{% trans "Reset (power cycle)" %}:</strong>
{% trans "Reset virtual machine (reset button)" %}
</li>
<li>
<strong>{% trans "Shut off" %}:</strong>
{% trans "Shut off VM. (plug-out)" %}
</li>
<li>
<strong>{% trans "Migrate" %}:</strong>
{% trans "Live migrate running vm to another node." %}
</li>
<li>
<strong>{% trans "Save as template" %}:</strong>
{% trans "?" %}
</li>
<li>
<strong>{% trans "Destroy" %}:</strong>
{% trans "Remove virtual machine and its networks." %}
</li>
</ul>
</div>
<div style="clear: both;"></div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-4" id="vm-info-pane"> <div class="col-md-4" id="vm-info-pane">
...@@ -69,7 +129,7 @@ ...@@ -69,7 +129,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>
...@@ -102,8 +162,11 @@ ...@@ -102,8 +162,11 @@
<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">
<a href="#{% if instance.state == "RUNNING" %}console" data-toggle="pill{% endif %}" data-target="#_console" class="text-center"> <a href="#" data-toggle="pill_" data-target="#_console" class="text-center">
{% else %}>
<a href="#console" data-toggle="pill" data-target="#_console" class="text-center">
{% endif %}
<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>
...@@ -137,3 +200,9 @@ ...@@ -137,3 +200,9 @@
</div> </div>
{% endblock %} {% endblock %}
{% 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 @@ ...@@ -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 %}
......
...@@ -5,7 +5,3 @@ ...@@ -5,7 +5,3 @@
<div id="activity-timeline" class="timeline"> <div id="activity-timeline" class="timeline">
{% include "dashboard/vm-detail/_activity-timeline.html" %} {% include "dashboard/vm-detail/_activity-timeline.html" %}
</div> </div>
{% block extra_js %}
<script src="{{ STATIC_URL }}dashboard/vm-details.js"></script>
{% endblock %}
<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;});
$('a[href$="console"]').click(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>
...@@ -8,6 +8,16 @@ ...@@ -8,6 +8,16 @@
<dd><small>{{ instance.description }}</small></dd> <dd><small>{{ instance.description }}</small></dd>
</dl> </dl>
<h4>{% trans "Expiration" %} {% if instance.is_expiring %}<i class="icon-warning-sign text-danger"></i>{% endif %}
<a href="{% url "dashboard.views.vm-renew" instance.pk "" %}" class="btn btn-success btn-xs pull-right">{% trans "renew" %}</a>
</h4>
<dl>
<dt>{% trans "Suspended at:" %}</dt>
<dd><i class="icon-moon"></i> {{ instance.time_of_suspend|timeuntil }}</dd>
<dt>{% trans "Destroyed at:" %}</dt>
<dd><i class="icon-remove"></i> {{ instance.time_of_delete|timeuntil }}</dd>
</dl>
<div style="font-weight: bold;">{% trans "Tags" %}</div> <div style="font-weight: bold;">{% trans "Tags" %}</div>
<div id="vm-details-tags" style="margin-bottom: 20px;"> <div id="vm-details-tags" style="margin-bottom: 20px;">
<div id="vm-details-tags-list"> <div id="vm-details-tags-list">
...@@ -37,8 +47,10 @@ ...@@ -37,8 +47,10 @@
</div><!-- id:vm-details-tags --> </div><!-- id:vm-details-tags -->
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{% if graphite_enabled %}
<img src="{% url "dashboard.views.vm-graph" instance.pk "cpu" "6h" %}" style="width:100%"/> <img src="{% url "dashboard.views.vm-graph" instance.pk "cpu" "6h" %}" style="width:100%"/>
<img src="{% url "dashboard.views.vm-graph" instance.pk "memory" "6h" %}" style="width:100%"/> <img src="{% url "dashboard.views.vm-graph" instance.pk "memory" "6h" %}" style="width:100%"/>
<img src="{% url "dashboard.views.vm-graph" instance.pk "network" "6h" %}" style="width:100%"/> <img src="{% url "dashboard.views.vm-graph" instance.pk "network" "6h" %}" style="width:100%"/>
{% endif %}
</div> </div>
</div> </div>
...@@ -52,12 +52,15 @@ ...@@ -52,12 +52,15 @@
</a> </a>
</div> </div>
</h3> </h3>
<div class="row" id="vm-details-disk-add-for-form">
</div> <div class="row" id="vm-details-disk-add-for-form"></div>
{% if not instance.disks.all %}
{% trans "No disks are added!" %}
{% endif %}
{% for d in instance.disks.all %} {% for d in instance.disks.all %}
<h4 class="list-group-item-heading dashboard-vm-details-network-h3"> <h4 class="list-group-item-heading dashboard-vm-details-network-h3">
<i class="icon-file"></i> {{ d.name }} (#{{ d.id }}) - {{ d.size|filesize }} {% include "dashboard/_disk-list-element.html" %}
</h4> </h4>
{% endfor %} {% endfor %}
</div> </div>
...@@ -67,7 +70,7 @@ ...@@ -67,7 +70,7 @@
<div class="col-md-12"> <div class="col-md-12">
<div> <div>
<hr /> <hr />
<form method="POST" action="" style="max-width: 300px;"> <form method="POST" action="{% url "dashboard.views.disk-add" %}" style="max-width: 350px;">
{% crispy forms.disk_add_form %} {% crispy forms.disk_add_form %}
</form> </form>
<hr /> <hr />
......
...@@ -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>
......
...@@ -72,5 +72,6 @@ ...@@ -72,5 +72,6 @@
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<script src="{{ STATIC_URL}}dashboard/vm-list.js"></script> <script src="{{ STATIC_URL}}dashboard/vm-list.js"></script>
<script src="{{ STATIC_URL}}dashboard/vm-common.js"></script>
{% endblock %} {% endblock %}
<a href="{% url "dashboard.views.vm-migrate" pk=record.pk %}" class="btn btn-default btn-xs vm-migrate" data-vm-pk="{{ record.pk }}" title data-original-title="Migrate">
<a class="btn btn-default btn-xs" title data-original-title="Migrate">
<i class="icon-truck"></i> <i class="icon-truck"></i>
</a> </a>
<a id="vm-list-rename-button" class="btn btn-default btn-xs" title data-original-title="Rename"> <a id="vm-list-rename-button" class="btn btn-default btn-xs" title data-original-title="Rename">
......
from django.contrib.auth.models import User
from django.test import TestCase
from ..models import Profile
from ..views import search_user
class NotificationTestCase(TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
Profile.objects.get_or_create(user=self.u1)
self.u2 = User.objects.create(username='user2')
Profile.objects.get_or_create(user=self.u2)
def test_notification_send(self):
c1 = self.u1.notification_set.count()
c2 = self.u2.notification_set.count()
profile = self.u1.profile
msg = profile.notify('subj',
'dashboard/test_message.txt',
{'var': 'testme'})
assert self.u1.notification_set.count() == c1 + 1
assert self.u2.notification_set.count() == c2
assert 'user1' in msg.message
assert 'testme' in msg.message
assert msg in self.u1.notification_set.all()
class ProfileTestCase(TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
Profile.objects.get_or_create(user=self.u1)
self.u2 = User.objects.create(username='user2',
email='john@example.org')
Profile.objects.get_or_create(user=self.u2, org_id='apple')
def test_search_user_by_name(self):
self.assertEqual(search_user('user1'), self.u1)
self.assertEqual(search_user('user2'), self.u2)
def test_search_user_by_mail(self):
self.assertEqual(search_user('john@example.org'), self.u2)
def test_search_user_by_orgid(self):
self.assertEqual(search_user('apple'), self.u2)
def test_search_user_nonexist(self):
with self.assertRaises(User.DoesNotExist):
search_user('no-such-user')
...@@ -2,13 +2,15 @@ from django.conf.urls import patterns, url ...@@ -2,13 +2,15 @@ from django.conf.urls import patterns, url
from vm.models import Instance from vm.models import Instance
from .views import ( from .views import (
IndexView, VmDetailView, VmList, VmCreate, TemplateDetail, AclUpdateView, AclUpdateView, DiskAddView, FavouriteView, GroupAclUpdateView, GroupDelete,
VmDelete, VmMassDelete, vm_activity, NodeList, NodeDetailView, PortDelete, GroupDetailView, GroupList, GroupUserDelete, IndexView, LeaseCreate,
TransferOwnershipView, TransferOwnershipConfirmView, NodeDelete, LeaseDelete, LeaseDetail, NodeAddTraitView, NodeCreate, NodeDelete,
TemplateList, LeaseDetail, NodeCreate, LeaseCreate, TemplateCreate, NodeDetailView, NodeGraphView, NodeList, NodeStatus, NotificationView,
FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete, PortDelete, TemplateAclUpdateView, TemplateCreate, TemplateDelete,
VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
GroupAclUpdateView, GroupUserDelete, NodeAddTraitView, TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
VmRenewView,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -36,6 +38,8 @@ urlpatterns = patterns( ...@@ -36,6 +38,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(),
...@@ -48,13 +52,17 @@ urlpatterns = patterns( ...@@ -48,13 +52,17 @@ urlpatterns = patterns(
url(r'^vm/mass-delete/', VmMassDelete.as_view(), url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'), name='dashboard.view.mass-delete-vm'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity), url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity),
url(r'^vm/(?P<pk>\d+)/migrate/$', VmMigrateView.as_view(),
name='dashboard.views.vm-migrate'),
url(r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$', VmRenewView.as_view(),
name='dashboard.views.vm-renew'),
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'^node/(?P<pk>\d+)/add-trait/$', NodeAddTraitView.as_view(), url(r'^node/(?P<pk>\d+)/add-trait/$', NodeAddTraitView.as_view(),
name='dashboard.views.node-addtrait'), name='dashboard.views.node-addtrait'),
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"),
...@@ -73,10 +81,20 @@ urlpatterns = patterns( ...@@ -73,10 +81,20 @@ urlpatterns = patterns(
r'(?P<time>[0-9]{1,2}[hdwy])$'), r'(?P<time>[0-9]{1,2}[hdwy])$'),
VmGraphView.as_view(), VmGraphView.as_view(),
name='dashboard.views.vm-graph'), name='dashboard.views.vm-graph'),
url((r'^node/(?P<pk>\d+)/graph/(?P<metric>cpu|memory|network)/'
r'(?P<time>[0-9]{1,2}[hdwy])$'),
NodeGraphView.as_view(),
name='dashboard.views.node-graph'),
url(r'^group/(?P<pk>\d+)/$', GroupDetailView.as_view(), url(r'^group/(?P<pk>\d+)/$', GroupDetailView.as_view(),
name='dashboard.views.group-detail'), name='dashboard.views.group-detail'),
url(r'^group/(?P<pk>\d+)/acl/$', GroupAclUpdateView.as_view(), url(r'^group/(?P<pk>\d+)/acl/$', GroupAclUpdateView.as_view(),
name='dashboard.views.group-acl'), name='dashboard.views.group-acl'),
url(r'^groupuser/delete/(?P<pk>\d+)/$', GroupUserDelete.as_view(), url(r'^groupuser/delete/(?P<pk>\d+)/$', GroupUserDelete.as_view(),
name="dashboard.views.delete-groupuser"), name="dashboard.views.delete-groupuser"),
url(r'^notifications/$', NotificationView.as_view(),
name="dashboard.views.notifications"),
url(r'^disk/add/$', DiskAddView.as_view(),
name="dashboard.views.disk-add"),
) )
# -*- 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)
...@@ -616,6 +631,8 @@ class Host(models.Model): ...@@ -616,6 +631,8 @@ class Host(models.Model):
if self.shared_ip and public: if self.shared_ip and public:
res = Record.objects.filter(type='A', res = Record.objects.filter(type='A',
address=self.pub_ipv4) address=self.pub_ipv4)
if res.count() < 1:
return unicode(self.pub_ipv4)
else: else:
res = self.record_set.filter(type='A', res = self.record_set.filter(type='A',
address=self.ipv4) address=self.ipv4)
...@@ -691,6 +708,17 @@ class Host(models.Model): ...@@ -691,6 +708,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 admin import HostAdmin
class MockInstance:
def __init__(self, groups):
self.groups = MockGroups(groups)
class MockGroup:
def __init__(self, name):
self.name = name
class MockGroups:
def __init__(self, groups):
self.groups = groups
def all(self):
return self.groups
class HostAdminTestCase(TestCase):
def test_no_groups(self):
instance = MockInstance([])
l = HostAdmin.list_groups(instance)
self.assertEqual(l, "")
def test_sigle_group(self):
instance = MockInstance([MockGroup("alma")])
l = HostAdmin.list_groups(instance)
self.assertEqual(l, "alma")
def test_multiple_groups(self):
instance = MockInstance([MockGroup("alma"),
MockGroup("korte"), MockGroup("szilva")])
l = HostAdmin.list_groups(instance)
self.assertEqual(l, "alma, korte, szilva")
from django.test import TestCase
from django.contrib.auth.models import User
from ..admin import HostAdmin
from firewall.models import Vlan, Domain, Record, Host
from django.forms import ValidationError
class MockInstance:
def __init__(self, groups):
self.groups = MockGroups(groups)
class MockGroup:
def __init__(self, name):
self.name = name
class MockGroups:
def __init__(self, groups):
self.groups = groups
def all(self):
return self.groups
class HostAdminTestCase(TestCase):
def test_no_groups(self):
instance = MockInstance([])
l = HostAdmin.list_groups(instance)
self.assertEqual(l, "")
def test_sigle_group(self):
instance = MockInstance([MockGroup("alma")])
l = HostAdmin.list_groups(instance)
self.assertEqual(l, "alma")
def test_multiple_groups(self):
instance = MockInstance([MockGroup("alma"),
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')
class HostGetHostnameTestCase(TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.save()
self.d = Domain(name='example.org', owner=self.u1)
self.d.save()
Record.objects.all().delete()
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/24',
network6='2001:738:2001:4031::/80', domain=self.d,
owner=self.u1, network_type='portforward',
snat_ip='10.1.1.1')
self.vlan.save()
self.h = Host(hostname='h', mac='01:02:03:04:05:00', ipv4='10.0.0.1',
vlan=self.vlan, owner=self.u1, shared_ip=True,
pub_ipv4=self.vlan.snat_ip)
self.h.save()
def test_issue_93_wo_record(self):
self.assertEqual(self.h.get_hostname(proto='ipv4', public=True),
unicode(self.h.pub_ipv4))
def test_issue_93_w_record(self):
self.r = Record(name='vm', type='A', domain=self.d, owner=self.u1,
address=self.vlan.snat_ip)
self.r.save()
self.assertEqual(self.h.get_hostname(proto='ipv4', public=True),
self.r.fqdn)
...@@ -11,7 +11,8 @@ celery = Celery('manager', backend='amqp', ...@@ -11,7 +11,8 @@ celery = Celery('manager', backend='amqp',
'vm.tasks.local_periodic_tasks', 'vm.tasks.local_periodic_tasks',
'vm.tasks.local_agent_tasks', 'vm.tasks.local_agent_tasks',
'storage.tasks.local_tasks', 'storage.tasks.local_tasks',
'firewall.tasks.local_tasks']) 'storage.tasks.periodic_tasks',
'firewall.tasks.local_tasks', ])
celery.conf.update( celery.conf.update(
CELERY_TASK_RESULT_EXPIRES=300, CELERY_TASK_RESULT_EXPIRES=300,
...@@ -27,11 +28,21 @@ celery.conf.update( ...@@ -27,11 +28,21 @@ celery.conf.update(
'schedule': timedelta(seconds=5), 'schedule': timedelta(seconds=5),
'options': {'queue': 'localhost.man'} 'options': {'queue': 'localhost.man'}
}, },
'vm.periodic_tasks': { 'vm.update_domain_states': {
'task': 'vm.tasks.local_periodic_tasks.update_domain_states', 'task': 'vm.tasks.local_periodic_tasks.update_domain_states',
'schedule': timedelta(seconds=10), 'schedule': timedelta(seconds=10),
'options': {'queue': 'localhost.man'} 'options': {'queue': 'localhost.man'}
}, },
'vm.garbage_collector': {
'task': 'vm.tasks.local_periodic_tasks.garbage_collector',
'schedule': timedelta(minutes=10),
'options': {'queue': 'localhost.man'}
},
'storage.periodic_tasks': {
'task': 'storage.tasks.periodic_tasks.garbage_collector',
'schedule': timedelta(hours=1),
'options': {'queue': 'localhost.man'}
},
} }
) )
...@@ -23,7 +23,8 @@ def select_node(instance, nodes): ...@@ -23,7 +23,8 @@ def select_node(instance, nodes):
''' Select a node for hosting an instance based on its requirements. ''' Select a node for hosting an instance based on its requirements.
''' '''
# check required traits # check required traits
nodes = [n for n in nodes if has_traits(instance.req_traits.all(), n)] nodes = [n for n in nodes
if n.enabled and has_traits(instance.req_traits.all(), n)]
if not nodes: if not nodes:
raise TraitsUnsatisfiableException() raise TraitsUnsatisfiableException()
...@@ -51,7 +52,7 @@ def has_enough_ram(ram_size, node): ...@@ -51,7 +52,7 @@ def has_enough_ram(ram_size, node):
ram_size mebibytes of memory; otherwise, false. ram_size mebibytes of memory; otherwise, false.
""" """
total = node.ram_size total = node.ram_size
used = (node.ram_usage() / 100) * total used = (node.ram_usage / 100) * total
unused = total - used unused = total - used
overcommit = node.ram_size_with_overcommit overcommit = node.ram_size_with_overcommit
...@@ -66,7 +67,7 @@ def free_cpu_time(node): ...@@ -66,7 +67,7 @@ def free_cpu_time(node):
Higher values indicate more idle time. Higher values indicate more idle time.
""" """
activity = node.cpu_usage() / 100 activity = node.cpu_usage / 100
inactivity = 1 - activity inactivity = 1 - activity
cores = node.num_cores cores = node.num_cores
return cores * inactivity return cores * inactivity
...@@ -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
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>{% block title %}Firewall GUI{% endblock %}</title> <title>{% block title %}Firewall GUI{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href='http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,400&amp;subset=latin,latin-ext' rel='stylesheet' type='text/css'> <link href='//fonts.googleapis.com/css?family=Source+Sans+Pro:200,400&amp;subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
<link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet" /> <link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet" />
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
</footer> </footer>
</div><!-- .footer-container .container --> </div><!-- .footer-container .container -->
<script src="http://code.jquery.com/jquery-latest.js"></script> <script src="//code.jquery.com/jquery-latest.js"></script>
<script src="{% url "network.js_catalog" %}"></script> <script src="{% url "network.js_catalog" %}"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="{% static "js/bootbox.min.js" %}"></script> <script src="{% static "js/bootbox.min.js" %}"></script>
......
{% load i18n %}
<span title="{% blocktrans with vendor=record.hw_vendor|default:"n/a" %}Vendor: {{vendor}}{% endblocktrans %}">{{ record.mac }}</span>
from django import contrib from django import contrib
# from django.utils.translation import ugettext_lazy as _ # from django.utils.translation import ugettext_lazy as _
from .models import Disk, DataStore from .models import Disk, DataStore, DiskActivity
class DiskAdmin(contrib.admin.ModelAdmin): class DiskAdmin(contrib.admin.ModelAdmin):
...@@ -13,4 +13,5 @@ class DataStoreAdmin(contrib.admin.ModelAdmin): ...@@ -13,4 +13,5 @@ class DataStoreAdmin(contrib.admin.ModelAdmin):
contrib.admin.site.register(Disk, DiskAdmin) contrib.admin.site.register(Disk, DiskAdmin)
contrib.admin.site.register(DiskActivity)
contrib.admin.site.register(DataStore, DataStoreAdmin) contrib.admin.site.register(DataStore, DataStoreAdmin)
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding unique constraint on 'Disk', fields ['filename']
db.create_unique(u'storage_disk', ['filename'])
def backwards(self, orm):
# Removing unique constraint on 'Disk', fields ['filename']
db.delete_unique(u'storage_disk', ['filename'])
models = {
u'acl.level': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Level'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'weight': ('django.db.models.fields.IntegerField', [], {'null': 'True'})
},
u'acl.objectlevel': {
'Meta': {'unique_together': "(('content_type', 'object_id', 'level'),)", 'object_name': 'ObjectLevel'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['acl.Level']"}),
'object_id': ('django.db.models.fields.IntegerField', [], {}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False'})
},
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'storage.datastore': {
'Meta': {'ordering': "['name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "['name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'size': ('sizefield.models.FileSizeField', [], {}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
},
u'storage.diskactivity': {
'Meta': {'object_name': 'DiskActivity'},
'activity_code': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'disk': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_log'", 'to': u"orm['storage.Disk']"}),
'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['storage.DiskActivity']"}),
'result': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'started': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'succeeded': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'task_uuid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['storage']
\ No newline at end of file
from manager.mancelery import celery from manager.mancelery import celery
from celery.contrib.abortable import AbortableTask
@celery.task
def check_queue(storage, queue_id):
''' Celery inspect job to check for active workers at queue_id
return True/False
'''
drivers = ['storage', 'download']
worker_list = [storage + "." + d for d in drivers]
queue_name = storage + "." + queue_id
# v is List of List of queues dict
active_queues = celery.control.inspect(worker_list).active_queues()
if active_queues is not None:
node_workers = [v for k, v in active_queues.iteritems()]
for worker in node_workers:
for queue in worker:
if queue['name'] == queue_name:
return True
return False
@celery.task
def save_as(disk, timeout, user):
disk.save_disk_as(task_uuid=save_as.request.id, user=user,
disk=disk, timeout=timeout)
@celery.task @celery.task
...@@ -14,3 +40,23 @@ def destroy(disk, user): ...@@ -14,3 +40,23 @@ def destroy(disk, user):
@celery.task @celery.task
def restore(disk, user): def restore(disk, user):
disk.restore(task_uuid=restore.request.id, user=user) disk.restore(task_uuid=restore.request.id, user=user)
class CreateFromURLTask(AbortableTask):
def __init__(self):
self.bind(celery)
def run(self, **kwargs):
Disk = kwargs.pop('cls')
Disk.create_from_url(url=kwargs.pop('url'),
task_uuid=create_from_url.request.id,
abortable_task=self,
**kwargs)
create_from_url = CreateFromURLTask()
@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 manager.mancelery import celery
import logging
from storage.tasks import remote_tasks
logger = logging.getLogger(__name__)
@celery.task
def garbage_collector(timeout=15):
""" Garbage collector for disk images.
Moves 1 day old deleted images to trash folder.
If there is not enough free space on datastore (default 10%)
deletes oldest images from trash.
:param timeout: Seconds before TimeOut exception
:type timeoit: int
"""
for ds in DataStore.objects.all():
file_list = os.listdir(ds.path)
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." %
(i, ds.path))
remote_tasks.move_to_trash.apply_async(
args=[ds.path, i], queue=queue_name).get(timeout=timeout)
try:
remote_tasks.make_free_space.apply_async(
args=[ds.path], queue=queue_name).get(timeout=timeout)
except Exception as e:
logger.warning(str(e))
@celery.task
def list_orphan_disks(timeout=15):
"""List disk image files without Disk object in the database.
Exclude cloud-xxxxxxxx.dump format images.
:param timeout: Seconds before TimeOut exception
:type timeoit: int
"""
import re
for ds in DataStore.objects.all():
queue_name = ds.get_remote_queue_name('storage')
files = set(remote_tasks.list_files.apply_async(
args=[ds.path], queue=queue_name).get(timeout=timeout))
disks = set([disk.filename for disk in ds.disk_set.all()])
for i in files - disks:
if not re.match('cloud-[0-9]*\.dump', i):
logging.warning("Orphan disk: %s" % i)
@celery.task
def list_missing_disks(timeout=15):
"""List Disk objects without disk image files.
:param timeout: Seconds before TimeOut exception
:type timeoit: int
"""
for ds in DataStore.objects.all():
queue_name = ds.get_remote_queue_name('storage')
files = set(remote_tasks.list_files.apply_async(
args=[ds.path], queue=queue_name).get(timeout=timeout))
disks = set([disk.filename for disk in
ds.disk_set.filter(destroyed__isnull=True)])
for i in disks - files:
logging.critical("Image: %s is missing from %s datastore."
% (i, ds.path))
...@@ -6,11 +6,21 @@ def list(dir): ...@@ -6,11 +6,21 @@ def list(dir):
pass pass
@celery.task(name='storagedriver.list_files')
def list_files(dir):
pass
@celery.task(name='storagedriver.create') @celery.task(name='storagedriver.create')
def create(disk_desc): def create(disk_desc):
pass pass
@celery.task(name='storagedriver.download')
def download(disk_desc, url):
pass
@celery.task(name='storagedriver.delete') @celery.task(name='storagedriver.delete')
def delete(path): def delete(path):
pass pass
...@@ -34,3 +44,18 @@ def get(path): ...@@ -34,3 +44,18 @@ def get(path):
@celery.task(name='storagedriver.merge') @celery.task(name='storagedriver.merge')
def merge(src_disk_desc, dst_disk_desc): def merge(src_disk_desc, dst_disk_desc):
pass pass
@celery.task(name='storagedriver.make_free_space')
def make_free_space(datastore, percent):
pass
@celery.task(name='storagedriver.move_to_trash')
def move_to_trash(datastore, disk_path):
pass
@celery.task(name='storagedriver.get_storage_stat')
def get_storage_stat(path):
pass
"""
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
{% load i18n %}
{% load staticfiles %}
{% get_current_language as LANGUAGE_CODE %}
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock %} | CIRCLE</title>
<meta name="description" content="">
<meta name="author" content="">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet">
<style type="text/css">
html, body {
background-color: #eee;
}
body {
margin-top: 40px;
}
.container {
width: 600px;
}
ul, li {
list-style: none;
margin: 0;
padding: 0;
}
.container > .content {
background-color: #fff;
padding: 20px;
-webkit-border-radius: 10px 10px 10px 10px;
-moz-border-radius: 10px 10px 10px 10px;
border-radius: 10px 10px 10px 10px;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15);
-moz-box-shadow: 0 1px 2px rgba(0,0,0,.15);
box-shadow: 0 1px 2px rgba(0,0,0,.15);
}
.login-form-errors .alert {
margin-right: 30px;
margin-left: 30px;
}
.login-form {
margin-top: 40px;
padding: 0 10px;
}
.login-form form {
padding: 0 20px;
}
.input-group {
margin-bottom: 10px;
}
.input-group-addon {
width: 38px;
}
.form-group label {
margin-top: 20px;
}
#submit-password-button {
margin-top: 15px;
}
/* fix for crispy-forms' html */
.form-group {
margin-bottom: 0px;
}
.help-block {
display: none;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
{% block content %}{% endblock %}
</div>
</div> <!-- /container -->
</body>
</html>
{% extends "base.html" %} {% extends "registration/base.html" %}
{% load i18n %} {% load i18n %}
{% load staticfiles %} {% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Login" %}{% endblock %}
{% block content %} {% block content %}
<form action="" method="POST"> <div class="row">
{% if form.password.errors or form.username.errors %}
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
{% endif %}
<div class="col-sm-{% if saml2 %}6{% else %}12{% endif %}">
<div class="login-form">
<form action="" method="POST">
{% csrf_token %} {% csrf_token %}
{{ form }} {% crispy form %}
<input type="submit" value="LOGIN" /> </form>
</form> </div>
</div>
{% if saml2 %}
<div class="col-sm-6">
<h4 style="padding-top: 0; margin-top: 0;">{% trans "Login with SSO" %}</h4>
<a href="{% url "saml2_login" %}">{% trans "Click here!" %}</a>
</div>
{% endif %}
</div>
<div class="row">
<div class="col-sm-12">
<a class="pull-right" href="{% url "accounts.password-reset" %}">{% trans "Forgot your password?" %}</a>
</div>
</div>
{% endblock %} {% endblock %}
{% extends "registration/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Password reset complete" %}{% endblock %}
{% block content %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
<div class="col-sm-12">
<div class="alert alert-success">
{% trans "Password change successful!" %}
<a href="{% url "accounts.login" %}">{% trans "Click here to login" %}</a>
</div>
</div>
</div>
{% endblock %}
{% extends "registration/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Password reset confirm" %}{% endblock %}
{% block content %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
<div class="col-sm-12">
<div style="margin: 0 0 25px 0;">
{% blocktrans %}Please enter your new password twice so we can verify you typed it in correctly!{% endblocktrans %}
</div>
{% if form %}
{% crispy form %}
{% else %}
<div class="alert alert-warning">
{% url "accounts.password-reset" as url %}
{% blocktrans with url=url %}This token is expired, please <a href="{{ url }}">request</a> a new password reset link again!{% endblocktrans %}
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% extends "registration/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Password reset done" %}{% endblock %}
{% block content %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
<div class="col-sm-12">
<div class="pull-right"><a href="{% url "accounts.login" %}">{% trans "Back to login" %}</a></div>
{% trans "We have sent you an email about your next steps!" %}
</div>
</div>
{% endblock %}
{% extends "registration/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Password reset" %}{% endblock %}
{% block content %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
<div class="col-sm-12">
<div class="pull-right"><a href="{% url "accounts.login" %}">{% trans "Back to login" %}</a></div>
<h4 style="margin: 0 0 25px 0;">{% blocktrans %}Enter your email address to reset your password!{% endblocktrans %}</h4>
<form action="" method="POST">
{% csrf_token %}
{% crispy form %}
</form>
</div>
</div>
{% endblock %}
...@@ -98,6 +98,18 @@ class NodeActivity(ActivityModel): ...@@ -98,6 +98,18 @@ class NodeActivity(ActivityModel):
app_label = 'vm' app_label = 'vm'
db_table = 'vm_nodeactivity' db_table = 'vm_nodeactivity'
def __unicode__(self):
if self.parent:
return '{}({})->{}'.format(self.parent.activity_code,
self.node,
self.activity_code)
else:
return '{}({})'.format(self.activity_code,
self.node)
def get_readable_name(self):
return self.activity_code.split('.')[-1].replace('_', ' ').capitalize()
@classmethod @classmethod
def create(cls, code_suffix, node, task_uuid=None, user=None): def create(cls, code_suffix, node, task_uuid=None, user=None):
act = cls(activity_code='vm.Node.' + code_suffix, act = cls(activity_code='vm.Node.' + code_suffix,
......
...@@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _
from firewall.models import Vlan, Host from firewall.models import Vlan, Host
from ..tasks import net_tasks from ..tasks import net_tasks
from .activity import instance_activity
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -105,7 +106,7 @@ class Interface(Model): ...@@ -105,7 +106,7 @@ class Interface(Model):
self.host.delete() self.host.delete()
@classmethod @classmethod
def create(cls, instance, vlan, managed, owner=None): def create(cls, instance, vlan, managed, owner=None, base_activity=None):
"""Create a new interface for a VM instance to the specified VLAN. """Create a new interface for a VM instance to the specified VLAN.
""" """
if managed: if managed:
...@@ -115,9 +116,19 @@ class Interface(Model): ...@@ -115,9 +116,19 @@ class Interface(Model):
host.mac = str(cls.generate_mac(instance, vlan)) host.mac = str(cls.generate_mac(instance, vlan))
host.hostname = instance.vm_name host.hostname = instance.vm_name
# Get adresses from firewall # Get adresses from firewall
if base_activity is None:
act = instance_activity(code_suffix='allocating_ip',
instance=instance, user=owner)
else:
act = base_activity.sub_activity('allocating_ip')
with act as act:
addresses = vlan.get_new_address() addresses = vlan.get_new_address()
host.ipv4 = addresses['ipv4'] host.ipv4 = addresses['ipv4']
host.ipv6 = addresses['ipv6'] host.ipv6 = addresses['ipv6']
act.result = ('new addresses: ipv4: %(ip4)s, ipv6: %(ip6)s, '
'vlan: %(vlan)s' % {'ip4': host.ipv4,
'ip6': host.ipv6,
'vlan': vlan.name})
host.owner = owner host.owner = owner
if vlan.network_type == 'public': if vlan.network_type == 'public':
host.shared_ip = False host.shared_ip = False
......
...@@ -11,15 +11,16 @@ from celery.exceptions import TimeoutError ...@@ -11,15 +11,16 @@ from celery.exceptions import TimeoutError
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from common.models import method_cache from common.models import method_cache, WorkerNotFound
from firewall.models import Host from firewall.models import Host
from ..tasks import vm_tasks from ..tasks import vm_tasks
from .common import Trait from .common import Trait
from .activity import node_activity from .activity import node_activity, NodeActivity
from monitor.calvin.calvin import Query from monitor.calvin.calvin import Query
from monitor.calvin.calvin import GraphiteHandler from monitor.calvin.calvin import GraphiteHandler
from django.utils import timezone
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -54,53 +55,64 @@ class Node(TimeStampedModel): ...@@ -54,53 +55,64 @@ class Node(TimeStampedModel):
def __unicode__(self): def __unicode__(self):
return self.name return self.name
@property
@method_cache(10, 5) @method_cache(10, 5)
def online(self): def get_online(self):
"""Check if the node is online.
Runs a remote ping task if the worker is running.
"""
try:
return self.remote_query(vm_tasks.ping, timeout=1, default=False) return self.remote_query(vm_tasks.ping, timeout=1, default=False)
except WorkerNotFound:
return False
online = property(get_online)
@property
@method_cache(300) @method_cache(300)
def num_cores(self): def get_num_cores(self):
"""Number of CPU threads available to the virtual machines. """Number of CPU threads available to the virtual machines.
""" """
return self.remote_query(vm_tasks.get_core_num) return self.remote_query(vm_tasks.get_core_num, default=0)
num_cores = property(get_num_cores)
@property @property
def state(self): def state(self):
"""Node state. """The state combined of online and enabled attributes.
""" """
if self.enabled and self.online: if self.enabled and self.online:
return 'Online' return 'ONLINE'
elif self.enabled and not self.online: elif self.enabled and not self.online:
return 'Missing' return 'MISSING'
elif not self.enabled and self.online: elif not self.enabled and self.online:
return 'Disabled' return 'DISABLED'
else: else:
return 'Offline' return 'OFFLINE'
def disable(self, user=None): def disable(self, user=None):
''' Disable the node.''' ''' Disable the node.'''
if self.enabled is True:
with node_activity(code_suffix='disable', node=self, user=user): with node_activity(code_suffix='disable', node=self, user=user):
self.enabled = False self.enabled = False
self.save() self.save()
def enable(self, user=None): def enable(self, user=None):
''' Enable the node. ''' ''' Enable the node. '''
if self.enabled is not True:
with node_activity(code_suffix='enable', node=self, user=user): with node_activity(code_suffix='enable', node=self, user=user):
self.enabled = True self.enabled = True
self.save() self.save()
self.get_num_cores(invalidate_cache=True)
self.get_ram_size(invalidate_cache=True)
@property
@method_cache(300) @method_cache(300)
def ram_size(self): def get_ram_size(self):
"""Bytes of total memory in the node. """Bytes of total memory in the node.
""" """
return self.remote_query(vm_tasks.get_ram_size, default=0)
return self.remote_query(vm_tasks.get_ram_size) ram_size = property(get_ram_size)
@property @property
def ram_size_with_overcommit(self): def ram_size_with_overcommit(self):
...@@ -110,25 +122,77 @@ class Node(TimeStampedModel): ...@@ -110,25 +122,77 @@ class Node(TimeStampedModel):
@method_cache(30) @method_cache(30)
def get_remote_queue_name(self, queue_id): def get_remote_queue_name(self, queue_id):
""" Return the remote queue name """Return the name of the remote celery queue for this node.
throws Exception if there is no worker on the queue. throws Exception if there is no worker on the queue.
Until the cache provide reult there can be dead quques. Until the cache provide reult there can be dead queues.
""" """
if vm_tasks.check_queue(self.host.hostname, queue_id): if vm_tasks.check_queue(self.host.hostname, queue_id):
self.node_online()
return self.host.hostname + "." + queue_id return self.host.hostname + "." + queue_id
else: else:
raise Exception("Worker not found.") if self.enabled is True:
self.node_offline()
raise WorkerNotFound()
def node_online(self):
"""Create activity and log entry when node reappears.
"""
try:
act = self.activity_log.order_by('-pk')[0]
except IndexError:
pass # no monitoring activity at all
else:
logger.debug("The last activity was %s" % act)
if act.activity_code.endswith("offline"):
act = NodeActivity.create(code_suffix='monitor_succes_online',
node=self, user=None)
act.started = timezone.now()
act.finished = timezone.now()
act.succeeded = True
act.save()
logger.info("Node %s is ONLINE." % self.name)
self.get_num_cores(invalidate_cache=True)
self.get_ram_size(invalidate_cache=True)
def node_offline(self):
"""Called when a node disappears.
If the node is not already offline, record an activity and a log entry.
"""
try:
act = self.activity_log.order_by('-pk')[0]
except IndexError:
pass # no activity at all
else:
logger.debug("The last activity was %s" % act)
if act.activity_code.endswith("offline"):
return
act = NodeActivity.create(code_suffix='monitor_failed_offline',
node=self, user=None)
act.started = timezone.now()
act.finished = timezone.now()
act.succeeded = False
act.save()
logger.critical("Node %s is OFFLINE%s.", self.name,
", but enabled" if self.enabled else "")
# TODO: check if we should reschedule any VMs?
def remote_query(self, task, timeout=30, raise_=False, default=None): def remote_query(self, task, timeout=30, raise_=False, default=None):
"""Query the given task, and get the result. """Query the given task, and get the result.
If the result is not ready in timeout secs, return default value or If the result is not ready or worker not reachable
raise a TimeoutError.""" in timeout secs, return default value or raise a
TimeoutError or WorkerNotFound exception.
"""
try:
r = task.apply_async( r = task.apply_async(
queue=self.get_remote_queue_name('vm'), expires=timeout + 60) queue=self.get_remote_queue_name('vm'), expires=timeout + 60)
try:
return r.get(timeout=timeout) return r.get(timeout=timeout)
except TimeoutError: except (TimeoutError, WorkerNotFound):
if raise_: if raise_:
raise raise
else: else:
...@@ -171,7 +235,16 @@ class Node(TimeStampedModel): ...@@ -171,7 +235,16 @@ class Node(TimeStampedModel):
def ram_usage(self): def ram_usage(self):
return float(self.get_monitor_info()["memory.usage"]) / 100 return float(self.get_monitor_info()["memory.usage"]) / 100
@property
def byte_ram_usage(self):
return self.ram_usage * self.ram_size
def update_vm_states(self): def update_vm_states(self):
"""Update state of Instances running on this Node.
Query state of all libvirt domains, and notify Instances by their
vm_state_changed hook.
"""
domains = {} domains = {}
domain_list = self.remote_query(vm_tasks.list_domains_info, timeout=5) domain_list = self.remote_query(vm_tasks.list_domains_info, timeout=5)
if domain_list is None: if domain_list is None:
...@@ -194,6 +267,8 @@ class Node(TimeStampedModel): ...@@ -194,6 +267,8 @@ class Node(TimeStampedModel):
except KeyError: except KeyError:
logger.info('Node %s update: instance %s missing from ' logger.info('Node %s update: instance %s missing from '
'libvirt', self, i['id']) 'libvirt', self, i['id'])
# Set state to STOPPED when instance is missing
self.instance_set.get(id=i['id']).vm_state_changed('STOPPED')
else: else:
if d != i['state']: if d != i['state']:
logger.info('Node %s update: instance %s state changed ' logger.info('Node %s update: instance %s state changed '
......
import logging
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from manager.mancelery import celery from manager.mancelery import celery
from vm.models import Node from vm.models import Node, Instance
logger = logging.getLogger(__name__)
@celery.task(ignore_result=True) @celery.task(ignore_result=True)
...@@ -7,3 +13,44 @@ def update_domain_states(): ...@@ -7,3 +13,44 @@ def update_domain_states():
nodes = Node.objects.filter(enabled=True).all() nodes = Node.objects.filter(enabled=True).all()
for node in nodes: for node in nodes:
node.update_vm_states() node.update_vm_states()
@celery.task(ignore_result=True)
def garbage_collector(timeout=15):
"""Garbage collector for instances.
Suspends and destroys expired instances.
:param timeout: Seconds before TimeOut exception
:type timeout: int
"""
now = timezone.now()
for i in Instance.objects.filter(destroyed=None).all():
if i.time_of_delete and now < i.time_of_delete:
i.destroy_async()
logger.info("Expired instance %d destroyed.", i.pk)
try:
i.owner.profile.notify(
_('%s destroyed') % unicode(i),
'dashboard/notifications/vm-destroyed.html',
{'instance': i})
except Exception as e:
logger.debug('Could not notify owner of instance %d .%s',
i.pk, unicode(e))
elif (i.time_of_suspend and now < i.time_of_suspend and
i.state == 'RUNNING'):
i.sleep_async()
logger.info("Expired instance %d suspended." % i.pk)
try:
i.owner.profile.notify(
_('%s suspended') % unicode(i),
'dashboard/notifications/vm-suspended.html',
{'instance': i})
except Exception as e:
logger.debug('Could not notify owner of instance %d .%s',
i.pk, unicode(e))
elif i.is_expiring():
logger.debug("Instance %d expires soon." % i.pk)
i.notify_owners_about_expiration()
else:
logger.debug("Instance %d didn't expire." % i.pk)
...@@ -14,11 +14,22 @@ def redeploy(instance, user): ...@@ -14,11 +14,22 @@ def redeploy(instance, user):
@celery.task @celery.task
def shut_off(instance, user):
instance.shut_off(task_uuid=shut_off.request.id, user=user)
@celery.task
def destroy(instance, user): def destroy(instance, user):
instance.destroy(task_uuid=destroy.request.id, user=user) instance.destroy(task_uuid=destroy.request.id, user=user)
@celery.task @celery.task
def save_as_template(instance, name, user, params):
instance.save_as_template(name, task_uuid=save_as_template.request.id,
user=user, **params)
@celery.task
def sleep(instance, user): def sleep(instance, user):
instance.sleep(task_uuid=sleep.request.id, user=user) instance.sleep(task_uuid=sleep.request.id, user=user)
......
...@@ -2,12 +2,15 @@ from manager.mancelery import celery ...@@ -2,12 +2,15 @@ from manager.mancelery import celery
def check_queue(node_hostname, queue_id): def check_queue(node_hostname, queue_id):
drivers = ['vmdriver', 'netdriver'] drivers = ['vmdriver', 'netdriver', 'agentdriver']
worker_list = [node_hostname + "." + d for d in drivers] worker_list = [node_hostname + "." + d for d in drivers]
queue_name = node_hostname + "." + queue_id queue_name = node_hostname + "." + queue_id
inspect = celery.control.inspect(worker_list) inspect = celery.control.inspect(worker_list)
active_queues = inspect.active_queues()
if active_queues is None:
return False
# v is List of List of queues dict # v is List of List of queues dict
node_workers = [v for k, v in inspect.active_queues().iteritems()] node_workers = [v for k, v in active_queues.iteritems()]
for worker in node_workers: for worker in node_workers:
for queue in worker: for queue in worker:
if queue['name'] == queue_name: if queue['name'] == queue_name:
......
from django.test import TestCase from django.test import TestCase
from mock import Mock
from ..models.common import (
Lease
)
from ..models.instance import ( from ..models.instance import (
InstanceTemplate, Instance find_unused_port, InstanceTemplate, Instance
) )
from ..models.network import ( from ..models.network import (
Interface Interface
) )
from ..models.common import (
Lease
) class PortFinderTestCase(TestCase):
def test_find_unused_port_without_used_ports(self):
port = find_unused_port(port_range=(1000, 2000))
assert port is not None
def test_find_unused_port_with_fully_saturated_range(self):
r = (10, 20)
port = find_unused_port(port_range=r, used_ports=range(*r))
assert port is None
class TemplateTestCase(TestCase): class TemplateTestCase(TestCase):
...@@ -20,6 +33,13 @@ class TemplateTestCase(TestCase): ...@@ -20,6 +33,13 @@ class TemplateTestCase(TestCase):
# TODO add images & net # TODO add images & net
class InstanceTestCase(TestCase):
def test_is_running(self):
inst = Mock(state='RUNNING')
assert Instance.is_running.getter(inst)
class InterfaceTestCase(TestCase): class InterfaceTestCase(TestCase):
def test_interface_create(self): def test_interface_create(self):
......
description "IK Cloud Django Development Server" description "CIRCLE mancelery"
start on runlevel [2345] start on runlevel [2345]
stop on runlevel [!2345] stop on runlevel [!2345]
respawn respawn
respawn limit 30 30 respawn limit 30 30
setgid cloud
setuid cloud setuid cloud
chdir /home/cloud/circle/circle
script script
. /home/cloud/.virtualenvs/circle/local/bin/postactivate cd /home/cloud/circle/circle
exec /home/cloud/.virtualenvs/circle/bin/python manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 1 --logfile /tmp/mancelery.log . /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 1
end script end script
description "CIRCLE django dev server"
start on runlevel [2345]
stop on runlevel [!2345]
respawn
respawn limit 30 30
setgid cloud
setuid cloud
script
cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py runserver '[::]:8080'
end script
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