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.
\ No newline at end of file
......@@ -151,9 +151,9 @@ class AclBase(Model):
return True
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))
object_levels = (self.object_level_set.select_related(
object_levels = (self.object_level_set.filter(**kwargs).select_related(
'users', 'level').all())
users = []
for object_level in object_levels:
......
......@@ -339,7 +339,6 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
'django.contrib.auth.backends.ModelBackend',
'djangosaml2.backends.Saml2Backend',
)
LOGIN_URL = '/saml2/login/'
remote_metadata = join(SITE_ROOT, 'remote_metadata.xml')
if not isfile(remote_metadata):
......@@ -388,6 +387,8 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
'DJANGO_SAML_GROUP_OWNER_ATTRIBUTES', '').split(',')
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(
'DJANGO_SAML_ORG_ID_ATTRIBUTE')
LOGIN_REDIRECT_URL = "/"
......@@ -29,3 +29,12 @@ CACHES = {
'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
from django.core.urlresolvers import reverse
from circle.settings.base import get_env_variable
from dashboard.views import circle_login
from dashboard.forms import CirclePasswordResetForm, CircleSetPasswordForm
admin.autodiscover()
......@@ -23,6 +25,19 @@ urlpatterns = patterns(
url(r'^admin/', include(admin.site.urls)),
url(r'^network/', include('network.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')),
)
......
......@@ -14,6 +14,10 @@ from model_utils.models import TimeStampedModel
logger = getLogger(__name__)
class WorkerNotFound(Exception):
pass
def activitycontextimpl(act, on_abort=None, on_commit=None):
try:
yield act
......@@ -60,6 +64,7 @@ class ActivityModel(TimeStampedModel):
if not self.finished:
self.finished = timezone.now()
self.succeeded = succeeded
if result is not None:
self.result = result
if event_handler is not None:
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 @@
}
},
{
"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,
"model": "firewall.domain",
"fields": {
......@@ -1462,7 +1491,6 @@
"modified": "2014-01-24T00:58:19.654Z",
"system": "",
"priority": 20,
"state": "READY",
"access_method": "ssh",
"raw_data": "",
"arch": "x86_64",
......
......@@ -5,9 +5,15 @@ from django.conf import settings
from django.contrib.auth.models import User, Group
from django.contrib.auth.signals import user_logged_in
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 acl.models import AclBase
......@@ -20,6 +26,34 @@ class Favourite(Model):
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):
user = OneToOneField(User)
preferred_language = CharField(verbose_name=_('preferred language'),
......@@ -31,6 +65,10 @@ class Profile(Model):
help_text=_('Unique identifier of the person, e.g. a student number.'))
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):
ACL_LEVELS = (
......
......@@ -331,3 +331,61 @@ a.hover-black {
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 () {
if (window.location.hash) {
if(window.location.hash.substring(1,4) == "ipv")
$("a[href=#network]").tab('show');
if(window.location.hash == "activity")
checkNewActivity(false, 1);
$("a[href=" + window.location.hash +"]").tab('show');
}
......@@ -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) {
......
/* 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 = [];
var disks = [];
$(function() {
vmCreateLoaded();
vmCustomizeLoaded();
});
function vmCreateLoaded() {
$('.vm-create-advanced').hide();
$('.vm-create-advanced-btn').click(function() {
$('.vm-create-advanced').stop().slideToggle();
if ($('.vm-create-advanced-icon').hasClass('icon-caret-down')) {
$('.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');
$(".vm-create-template-details").hide();
$(".vm-create-template-summary").click(function() {
$(this).next(".vm-create-template-details").slideToggle();
});
$(".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() {
vmCreateTemplateChange(this);
if (xhr.status == 500) {
addMessage("500 Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
}
});
return false;
});
}
function vmCustomizeLoaded() {
/* network thingies */
/* add network */
......@@ -86,15 +134,24 @@ function vmCreateLoaded() {
/* copy networks from hidden select */
$('#vm-create-network-add-vlan option').each(function() {
var managed = $(this).text().indexOf("mana") == 0;
var text = $(this).text();
var raw_text = $(this).text();
var pk = $(this).val();
if(managed) {
text = text.replace("managed -", "&#xf0ac;");
text = raw_text.replace("managed -", "&#xf0ac;");
} else {
text = text.replace("unmanaged -", "&#xf0c1;");
text = raw_text.replace("unmanaged -", "&#xf0c1;");
}
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);
}
});
......@@ -168,8 +225,20 @@ function vmCreateLoaded() {
});
/* 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 */
$('#vm-create-disk-add-select option').each(function() {
......@@ -179,8 +248,8 @@ function vmCreateLoaded() {
});
});
/* add button */
$('#vm-create-submit').click(function() {
/* start vm button clicks */
$('#vm-create-customized-start').click(function() {
$.ajax({
url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')},
......@@ -219,97 +288,6 @@ function vmCreateLoaded() {
$('.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) {
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() {
if(decideActivityRefresh()) {
checkNewActivity(false, 1);
}
$('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('icon-spin');
checkNewActivity(false,0);
});
/* save resources */
$('#vm-details-resources-save').click(function() {
......@@ -131,7 +135,11 @@ $(function() {
location.reload();
},
error: function(xhr, textStatus, error) {
if (xhr.status == 500) {
addMessage("Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
}
});
} else {
......@@ -152,6 +160,11 @@ $(function() {
$("#vm-details-disk-add-for-form").html($("#vm-details-disk-add-form").html());
return false;
});
/* show help */
$(".vm-details-help-button").click(function() {
$(".vm-details-help").stop().slideToggle();
});
});
......@@ -198,17 +211,23 @@ function checkNewActivity(only_state, runs) {
success: function(data) {
if(!only_state) {
$("#activity-timeline").html(data['activities']);
$("[title]").tooltip();
}
$("#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()) {
console.log("szia");
if(runs > 0 && decideActivityRefresh()) {
setTimeout(
function() {checkNewActivity(only_state, runs + 1)},
1000 + runs * 250
1000 + Math.exp(runs * 0.05)
);
}
$('a[href="#activity"] i').removeClass('icon-spin');
},
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 @@
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<a class="navbar-brand" href="/dashboard/">{% block header-site %}CIRCLE{% endblock %}</a>
<!-- temporarily -->
<div class="navbar-header">
<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="/admin/" style="color: white; font-size: 10px;">Admin</a>
{% if user.is_authenticated %}
......@@ -32,7 +43,8 @@
{% else %}
<a class="navbar-brand pull-right" href="{% url "login" %}?next={% url "dashboard.index" %}" style="color: white; font-size: 10px;">Login</a>
{% endif %}
</div>
</div><!-- .collapse .navbar-collapse -->
</div><!-- navbar navbar-inverse navbar-fixed-top -->
<div class="container">
{% 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 @@
{% 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 %}
<div class="body-content dashboard-index">
<div class="row">
......@@ -17,9 +28,11 @@
{% include "dashboard/index-groups.html" %}
</div>
{% comment %}
<div class="col-lg-4 col-sm-6">
{% include "dashboard/index-files.html" %}
</div>
{% endcomment %}
{% if perms.vm.create_template %}
<div class="col-lg-4 col-sm-6">
......
<div class="modal fade" id="create-modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
{% if box_title and ajax_title %}
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{{ box_title }}</h4>
</div>
{% endif %}
<div class="modal-body">
{% include template %}
</div>
......
......@@ -8,10 +8,10 @@
<div class="row">
<div class="col-md-4" id="node-info-pane">
<div id="node-info-data" class="big">
<span id="node-status-label" class="label {% if node.state == 'Online' %}label-success
{% elif node.state == 'Missing' %}label-danger
{% elif node.state == 'Disabled' %}label-warning
{% elif node.state == 'Offline' %}label-warning
<span id="node-details-state" class="label {% if node.state == 'ONLINE' %}label-success
{% elif node.state == 'MISSING' %}label-danger
{% elif node.state == 'DISABLED' %}label-warning
{% elif node.state == 'OFFLINE' %}label-warning
{% endif %}">{{ node.state }}</span>
<div class="btn-group">
{% with node as record %}
......@@ -42,10 +42,10 @@
</ul>
<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" 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="virtualmachines">{% include "dashboard/node-detail-vm.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="activity">{% include "dashboard/node-detail/activity.html" %}</div>
<div class="tab-pane" id="virtualmachines">{% include "dashboard/node-detail/vm.html" %}</div>
</div>
</div>
</div>
......
{% load i18n %}
<h3>{% trans "Activity" %}</h3>
<style>
.sub-timeline {
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">
{% for a in activities %}
<div class="activity" data-activity-id="{{ a.pk }}">
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
<i class="{% if not a.finished %} icon-refresh icon-spin {% else %}icon-plus{% endif %}"></i>
</span>
<strong>{{ a.get_readable_name }}</strong>
{{ a.started|date:"Y-m-d. H:i" }}, {{ a.user }}
</span>
<strong>{{ a.get_readable_name }}</strong>
{{ a.started|date:"Y-m-d H:i" }}, {{ a.user }}
{% if a.children.count > 0 %}
<div class="sub-timeline">
{% 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 }} -
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
<i class="icon-refresh icon-spin" class="sub-activity-loading-icon"></i>
{% endif %}
{% if s.has_failed %}
<div class="label label-danger">{% trans "failed" %}</div>
{% endif %}
</div>
{% endfor %}
</div>
{% 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>
{% block extra_js %}
<script src="{{ STATIC_URL }}dashboard/vm-details.js"></script>
{% endblock %}
{% endfor %}
{% load i18n %}
<h3>{% trans "Activity" %}</h3>
<div id="activity-timeline" class="timeline">
{% include "dashboard/node-detail/_activity-timeline.html" %}
</div>
......@@ -35,10 +35,11 @@
</div><!-- id:node-details-traits -->
</div>
<div class="col-md-8">
<img src="/static/grafikon.png" style="width:45%"/>
<img src="/static/grafikon.png" style="width:45%"/>
<img src="/static/grafikon.png" style="width:45%"/>
<img src="/static/grafikon.png" style="width:45%"/>
{% if graphite_enabled %}
<img src="{% url "dashboard.views.node-graph" node.pk "cpu" "6h" %}" style="width:100%"/>
<img src="{% url "dashboard.views.node-graph" node.pk "memory" "6h" %}" style="width:100%"/>
<img src="{% url "dashboard.views.node-graph" node.pk "network" "6h" %}" style="width:100%"/>
{% endif %}
</div>
</div>
<style>
......
......@@ -15,7 +15,7 @@
{% endblock %}
{% 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>
{% endif %}
{% 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" %}
{% load i18n %}
{% load sizefieldtags %}
{% load crispy_forms_tags %}
{% block content %}
......@@ -22,7 +23,7 @@
<div class="col-md-4">
<div class="panel panel-default">
<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 class="panel-body">
<form action="{% url "dashboard.views.template-acl" pk=object.pk %}" method="post">{% csrf_token %}
......@@ -64,8 +65,35 @@
</form>
</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><!-- .col-md-4 -->
</div><!-- .row -->
<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 @@
<div class="body-content">
<div class="page-header">
<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 %}">
{% 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>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% 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>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% 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>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% 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>
</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 %}">
{% 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>
</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 "Help" %}" href="#" class="btn btn-default btn-xs vm-details-help-button"><i class="icon-question"></i></a>
</div>
<h1>
<div id="vm-details-rename">
......@@ -46,7 +64,49 @@
<div id="vm-details-h1-name">
{{ instance.name }}
</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 class="row">
<div class="col-md-4" id="vm-info-pane">
......@@ -69,7 +129,7 @@
<dt>Password:</dt>
<dd>
<div class="input-group">
<input type="text" class="form-control input-sm input-tags" value="{{ instance.pw }}"/>
<input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags" value="{{ instance.pw }}"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show">
<i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i>
</span>
......@@ -102,8 +162,11 @@
<i class="icon-tasks icon-2x"></i><br>
{% trans "Resources" %}</a>
</li>
<li {% if instance.state != "RUNNING" %}class="disabled"{% endif %}>
<a href="#{% if instance.state == "RUNNING" %}console" data-toggle="pill{% endif %}" data-target="#_console" class="text-center">
<li {% if not instance.is_console_available %}class="disabled">
<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>
{% trans "Console" %}</a></li>
<li>
......@@ -137,3 +200,9 @@
</div>
{% 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 @@
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
<i class="{% if not a.finished %} icon-refresh icon-spin {% else %}icon-plus{% endif %}"></i>
</span>
<strong>{{ a.get_readable_name }}</strong>
{{ a.started|date:"Y-m-d H:i" }}, {{ a.user }}
<strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% endif %}>
{{ a.get_readable_name }}
</strong>
{{ a.started|date:"Y-m-d H:i" }}{% if a.user %}, {{ a.user }}{% endif %}
{% if a.children.count > 0 %}
<div class="sub-timeline">
{% for s in a.children.all %}
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}">
{{ s.get_readable_name }} -
<span{% if user.is_superuser and s.result %} title="{{ s.result }}"{% endif %}>
{{ s.get_readable_name }}</span> &ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
......
{% load i18n %}
<h3>{% trans "Owner" %}</h3>
<p>
{% if user == instance.owner %}
{% blocktrans %}You are the current owner of this instance.{% endblocktrans %}
<a href="#" class="btn btn-link">{% trans "Transfer ownership..." %}</a>
{% else %}
{% blocktrans with owner=instance.owner %}
The current owner of this instance is {{owner}}.
{% endblocktrans %}
{% endif %}
{% if user == instance.owner or user.is_superuser %}
<a href="{% url "dashboard.views.vm-transfer-ownership" instance.pk %}"
class="btn btn-link">{% trans "Transfer ownership..." %}</a>
{% endif %}
</p>
<h3>{% trans "Permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %}
......
......@@ -5,7 +5,3 @@
<div id="activity-timeline" class="timeline">
{% include "dashboard/vm-detail/_activity-timeline.html" %}
</div>
{% block extra_js %}
<script src="{{ STATIC_URL }}dashboard/vm-details.js"></script>
{% endblock %}
<div class="btn-toolbar">
<button id="sendCtrlAltDelButton" class="btn btn-danger small" href="#">Send CtrlAltDel</button>
<button id="sendPasswordButton" class="btn btn-default small" href="#">Type password</button>
<button id="sendCtrlAltDelButton" class="btn btn-danger small">Send CtrlAltDel</button>
<button id="sendPasswordButton" class="btn btn-default small">Type password</button>
</div>
<div class="alert alert-info" id="noVNC_status">
</div>
......@@ -10,68 +10,6 @@
<script src="{{ STATIC_URL }}dashboard/novnc/util.js"></script>
<script>
"use strict";
var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/';
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"input.js", "display.js", "jsunzip.js", "rfb.js"]);
var rfb;
function updateState(rfb, state, oldstate, msg) {
var s, sb, cad
s = $('#noVNC_status')[0];
cad = $('#sendCtrlAltDelButton')[0];
if (state === "normal") { cad.disabled = false; }
else { cad.disabled = true; }
if (typeof(msg) !== 'undefined') {
s.innerHTML = msg;
}
}
$('a[data-toggle$="pill"][href!="#console"]').click(function() {
if (rfb) {
rfb.disconnect();
rfb = 0;
}
$("#vm-info-pane").fadeIn();
$("#vm-detail-pane").removeClass("col-md-12");
});
$('#sendCtrlAltDelButton').click(function() {
rfb.sendCtrlAltDel(); return false;});
$('#sendPasswordButton').click(function() {
var pw = '{{instance.pw}}';
for (var i=0; i < pw.length; i++) {
rfb.sendKey(pw.charCodeAt(i));
} return false;});
$('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>
var VNC_URL = "{{ vnc_url }}";
</script>
......@@ -8,6 +8,16 @@
<dd><small>{{ instance.description }}</small></dd>
</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 id="vm-details-tags" style="margin-bottom: 20px;">
<div id="vm-details-tags-list">
......@@ -37,8 +47,10 @@
</div><!-- id:vm-details-tags -->
</div>
<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 "memory" "6h" %}" style="width:100%"/>
<img src="{% url "dashboard.views.vm-graph" instance.pk "network" "6h" %}" style="width:100%"/>
{% endif %}
</div>
</div>
......@@ -52,12 +52,15 @@
</a>
</div>
</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 %}
<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>
{% endfor %}
</div>
......@@ -67,7 +70,7 @@
<div class="col-md-12">
<div>
<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 %}
</form>
<hr />
......
......@@ -13,6 +13,7 @@
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
E-mail address or identifier of user:
<input name="name">
<input type="submit">
</form>
......
......@@ -72,5 +72,6 @@
{% endblock %}
{% 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 %}
<a class="btn btn-default btn-xs" title data-original-title="Migrate">
<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">
<i class="icon-truck"></i>
</a>
<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
from vm.models import Instance
from .views import (
IndexView, VmDetailView, VmList, VmCreate, TemplateDetail, AclUpdateView,
VmDelete, VmMassDelete, vm_activity, NodeList, NodeDetailView, PortDelete,
TransferOwnershipView, TransferOwnershipConfirmView, NodeDelete,
TemplateList, LeaseDetail, NodeCreate, LeaseCreate, TemplateCreate,
FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete,
VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete,
GroupAclUpdateView, GroupUserDelete, NodeAddTraitView,
AclUpdateView, DiskAddView, FavouriteView, GroupAclUpdateView, GroupDelete,
GroupDetailView, GroupList, GroupUserDelete, IndexView, LeaseCreate,
LeaseDelete, LeaseDetail, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeGraphView, NodeList, NodeStatus, NotificationView,
PortDelete, TemplateAclUpdateView, TemplateCreate, TemplateDelete,
TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
VmRenewView,
)
urlpatterns = patterns(
......@@ -36,6 +38,8 @@ urlpatterns = patterns(
name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
name='dashboard.views.detail'),
url(r'^vm/(?P<pk>\d+)/vnctoken/$', VmDetailVncTokenView.as_view(),
name='dashboard.views.detail-vnc'),
url(r'^vm/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Instance),
name='dashboard.views.vm-acl'),
url(r'^vm/(?P<pk>\d+)/tx/$', TransferOwnershipView.as_view(),
......@@ -48,13 +52,17 @@ urlpatterns = patterns(
url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'),
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/(?P<pk>\d+)/$', NodeDetailView.as_view(),
name='dashboard.views.node-detail'),
url(r'^node/(?P<pk>\d+)/add-trait/$', NodeAddTraitView.as_view(),
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'),
url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(),
name="dashboard.views.delete-node"),
......@@ -73,10 +81,20 @@ urlpatterns = patterns(
r'(?P<time>[0-9]{1,2}[hdwy])$'),
VmGraphView.as_view(),
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(),
name='dashboard.views.group-detail'),
url(r'^group/(?P<pk>\d+)/acl/$', GroupAclUpdateView.as_view(),
name='dashboard.views.group-acl'),
url(r'^groupuser/delete/(?P<pk>\d+)/$', GroupUserDelete.as_view(),
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 -*-
from itertools import islice
from itertools import islice, chain
import logging
from netaddr import IPSet
from netaddr import IPSet, EUI
from django.contrib.auth.models import User
from django.db import models
......@@ -298,13 +298,28 @@ class Vlan(AclBase, models.Model):
def prefix6(self):
return self.network6.prefixlen
def get_next_address(self, used_v4):
try:
last_address = list(used_v4)[-1]
except IndexError:
return []
next_address = last_address + 1
if next_address in self.network4.iter_hosts():
logger.debug("Found unused IPv4 address %s after %s.",
next_address, last_address)
return [next_address]
else:
return []
def get_new_address(self):
hosts = Host.objects.filter(vlan=self)
hosts = self.host_set
used_v4 = IPSet(hosts.values_list('ipv4', flat=True))
used_v6 = IPSet(hosts.exclude(ipv6__isnull=True)
.values_list('ipv6', flat=True))
for ipv4 in islice(self.network4.iter_hosts(), 10000):
for ipv4 in chain(self.get_next_address(used_v4),
islice(self.network4.iter_hosts(), 10000)):
ipv4 = str(ipv4)
if ipv4 not in used_v4:
logger.debug("Found unused IPv4 address %s.", ipv4)
......@@ -616,6 +631,8 @@ class Host(models.Model):
if self.shared_ip and public:
res = Record.objects.filter(type='A',
address=self.pub_ipv4)
if res.count() < 1:
return unicode(self.pub_ipv4)
else:
res = self.record_set.filter(type='A',
address=self.ipv4)
......@@ -691,6 +708,17 @@ class Host(models.Model):
def get_absolute_url(self):
return ('network.host', None, {'pk': self.pk})
@property
def eui(self):
return EUI(self.mac)
@property
def hw_vendor(self):
try:
return self.eui.oui.registration().org
except:
return None
class Firewall(models.Model):
name = models.CharField(max_length=20, unique=True,
......
from django.test import TestCase
from admin import HostAdmin
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',
'vm.tasks.local_periodic_tasks',
'vm.tasks.local_agent_tasks',
'storage.tasks.local_tasks',
'firewall.tasks.local_tasks'])
'storage.tasks.periodic_tasks',
'firewall.tasks.local_tasks', ])
celery.conf.update(
CELERY_TASK_RESULT_EXPIRES=300,
......@@ -27,11 +28,21 @@ celery.conf.update(
'schedule': timedelta(seconds=5),
'options': {'queue': 'localhost.man'}
},
'vm.periodic_tasks': {
'vm.update_domain_states': {
'task': 'vm.tasks.local_periodic_tasks.update_domain_states',
'schedule': timedelta(seconds=10),
'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):
''' Select a node for hosting an instance based on its requirements.
'''
# 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:
raise TraitsUnsatisfiableException()
......@@ -51,7 +52,7 @@ def has_enough_ram(ram_size, node):
ram_size mebibytes of memory; otherwise, false.
"""
total = node.ram_size
used = (node.ram_usage() / 100) * total
used = (node.ram_usage / 100) * total
unused = total - used
overcommit = node.ram_size_with_overcommit
......@@ -66,7 +67,7 @@ def free_cpu_time(node):
Higher values indicate more idle time.
"""
activity = node.cpu_usage() / 100
activity = node.cpu_usage / 100
inactivity = 1 - activity
cores = node.num_cores
return cores * inactivity
......@@ -36,6 +36,9 @@ class GroupTable(Table):
class HostTable(Table):
hostname = LinkColumn('network.host', args=[A('pk')])
mac = TemplateColumn(
template_name="network/columns/mac.html"
)
class Meta:
model = Host
......
......@@ -8,7 +8,7 @@
<meta charset="utf-8">
<title>{% block title %}Firewall GUI{% endblock %}</title>
<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-theme.min.css">
<link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet" />
......@@ -79,7 +79,7 @@
</footer>
</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="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.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.utils.translation import ugettext_lazy as _
from .models import Disk, DataStore
from .models import Disk, DataStore, DiskActivity
class DiskAdmin(contrib.admin.ModelAdmin):
......@@ -13,4 +13,5 @@ class DataStoreAdmin(contrib.admin.ModelAdmin):
contrib.admin.site.register(Disk, DiskAdmin)
contrib.admin.site.register(DiskActivity)
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 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
......@@ -14,3 +40,23 @@ def destroy(disk, user):
@celery.task
def restore(disk, 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):
pass
@celery.task(name='storagedriver.list_files')
def list_files(dir):
pass
@celery.task(name='storagedriver.create')
def create(disk_desc):
pass
@celery.task(name='storagedriver.download')
def download(disk_desc, url):
pass
@celery.task(name='storagedriver.delete')
def delete(path):
pass
......@@ -34,3 +44,18 @@ def get(path):
@celery.task(name='storagedriver.merge')
def merge(src_disk_desc, dst_disk_desc):
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 staticfiles %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title %}{% trans "Login" %}{% endblock %}
{% 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 %}
{{ form }}
<input type="submit" value="LOGIN" />
</form>
{% crispy 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 %}
{% 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):
app_label = 'vm'
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
def create(cls, code_suffix, node, task_uuid=None, user=None):
act = cls(activity_code='vm.Node.' + code_suffix,
......
......@@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _
from firewall.models import Vlan, Host
from ..tasks import net_tasks
from .activity import instance_activity
logger = getLogger(__name__)
......@@ -105,7 +106,7 @@ class Interface(Model):
self.host.delete()
@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.
"""
if managed:
......@@ -115,9 +116,19 @@ class Interface(Model):
host.mac = str(cls.generate_mac(instance, vlan))
host.hostname = instance.vm_name
# 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()
host.ipv4 = addresses['ipv4']
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
if vlan.network_type == 'public':
host.shared_ip = False
......
......@@ -11,15 +11,16 @@ from celery.exceptions import TimeoutError
from model_utils.models import TimeStampedModel
from taggit.managers import TaggableManager
from common.models import method_cache
from common.models import method_cache, WorkerNotFound
from firewall.models import Host
from ..tasks import vm_tasks
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 GraphiteHandler
from django.utils import timezone
logger = getLogger(__name__)
......@@ -54,53 +55,64 @@ class Node(TimeStampedModel):
def __unicode__(self):
return self.name
@property
@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)
except WorkerNotFound:
return False
online = property(get_online)
@property
@method_cache(300)
def num_cores(self):
def get_num_cores(self):
"""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
def state(self):
"""Node state.
"""The state combined of online and enabled attributes.
"""
if self.enabled and self.online:
return 'Online'
return 'ONLINE'
elif self.enabled and not self.online:
return 'Missing'
return 'MISSING'
elif not self.enabled and self.online:
return 'Disabled'
return 'DISABLED'
else:
return 'Offline'
return 'OFFLINE'
def disable(self, user=None):
''' Disable the node.'''
if self.enabled is True:
with node_activity(code_suffix='disable', node=self, user=user):
self.enabled = False
self.save()
def enable(self, user=None):
''' Enable the node. '''
if self.enabled is not True:
with node_activity(code_suffix='enable', node=self, user=user):
self.enabled = True
self.save()
self.get_num_cores(invalidate_cache=True)
self.get_ram_size(invalidate_cache=True)
@property
@method_cache(300)
def ram_size(self):
def get_ram_size(self):
"""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
def ram_size_with_overcommit(self):
......@@ -110,25 +122,77 @@ class Node(TimeStampedModel):
@method_cache(30)
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.
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):
self.node_online()
return self.host.hostname + "." + queue_id
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):
"""Query the given task, and get the result.
If the result is not ready in timeout secs, return default value or
raise a TimeoutError."""
If the result is not ready or worker not reachable
in timeout secs, return default value or raise a
TimeoutError or WorkerNotFound exception.
"""
try:
r = task.apply_async(
queue=self.get_remote_queue_name('vm'), expires=timeout + 60)
try:
return r.get(timeout=timeout)
except TimeoutError:
except (TimeoutError, WorkerNotFound):
if raise_:
raise
else:
......@@ -171,7 +235,16 @@ class Node(TimeStampedModel):
def ram_usage(self):
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):
"""Update state of Instances running on this Node.
Query state of all libvirt domains, and notify Instances by their
vm_state_changed hook.
"""
domains = {}
domain_list = self.remote_query(vm_tasks.list_domains_info, timeout=5)
if domain_list is None:
......@@ -194,6 +267,8 @@ class Node(TimeStampedModel):
except KeyError:
logger.info('Node %s update: instance %s missing from '
'libvirt', self, i['id'])
# Set state to STOPPED when instance is missing
self.instance_set.get(id=i['id']).vm_state_changed('STOPPED')
else:
if d != i['state']:
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 vm.models import Node
from vm.models import Node, Instance
logger = logging.getLogger(__name__)
@celery.task(ignore_result=True)
......@@ -7,3 +13,44 @@ def update_domain_states():
nodes = Node.objects.filter(enabled=True).all()
for node in nodes:
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):
@celery.task
def shut_off(instance, user):
instance.shut_off(task_uuid=shut_off.request.id, user=user)
@celery.task
def destroy(instance, user):
instance.destroy(task_uuid=destroy.request.id, user=user)
@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):
instance.sleep(task_uuid=sleep.request.id, user=user)
......
......@@ -2,12 +2,15 @@ from manager.mancelery import celery
def check_queue(node_hostname, queue_id):
drivers = ['vmdriver', 'netdriver']
drivers = ['vmdriver', 'netdriver', 'agentdriver']
worker_list = [node_hostname + "." + d for d in drivers]
queue_name = node_hostname + "." + queue_id
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
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 queue in worker:
if queue['name'] == queue_name:
......
from django.test import TestCase
from mock import Mock
from ..models.common import (
Lease
)
from ..models.instance import (
InstanceTemplate, Instance
find_unused_port, InstanceTemplate, Instance
)
from ..models.network import (
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):
......@@ -20,6 +33,13 @@ class TemplateTestCase(TestCase):
# 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):
def test_interface_create(self):
......
description "IK Cloud Django Development Server"
description "CIRCLE mancelery"
start on runlevel [2345]
stop on runlevel [!2345]
respawn
respawn limit 30 30
setgid cloud
setuid cloud
chdir /home/cloud/circle/circle
script
. /home/cloud/.virtualenvs/circle/local/bin/postactivate
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
cd /home/cloud/circle/circle
. /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
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