Commit df7e52cf by Kálmán Viktor

Merge branch 'master' into feature-new-tour

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/views.py
parents ad5e4e06 23000834
...@@ -39,3 +39,4 @@ circle/static_collected ...@@ -39,3 +39,4 @@ circle/static_collected
# jsi18n files # jsi18n files
jsi18n jsi18n
scripts.rc
...@@ -26,6 +26,17 @@ from .models import activity_context, has_suffix, humanize_exception ...@@ -26,6 +26,17 @@ from .models import activity_context, has_suffix, humanize_exception
logger = getLogger(__name__) logger = getLogger(__name__)
class SubOperationMixin(object):
required_perms = ()
def create_activity(self, parent, user, kwargs):
if not parent:
raise TypeError("SubOperation can only be called with "
"parent_activity specified.")
return super(SubOperationMixin, self).create_activity(
parent, user, kwargs)
class Operation(object): class Operation(object):
"""Base class for VM operations. """Base class for VM operations.
""" """
...@@ -36,6 +47,10 @@ class Operation(object): ...@@ -36,6 +47,10 @@ class Operation(object):
abortable = False abortable = False
has_percentage = False has_percentage = False
@classmethod
def get_activity_code_suffix(cls):
return cls.id
def __call__(self, **kwargs): def __call__(self, **kwargs):
return self.call(**kwargs) return self.call(**kwargs)
...@@ -232,7 +247,7 @@ class OperatedMixin(object): ...@@ -232,7 +247,7 @@ class OperatedMixin(object):
operation could be found. operation could be found.
""" """
for op in getattr(self, operation_registry_name, {}).itervalues(): for op in getattr(self, operation_registry_name, {}).itervalues():
if has_suffix(activity_code, op.activity_code_suffix): if has_suffix(activity_code, op.get_activity_code_suffix()):
return op(self) return op(self)
else: else:
return None return None
...@@ -273,3 +288,4 @@ def register_operation(op_cls, op_id=None, target_cls=None): ...@@ -273,3 +288,4 @@ def register_operation(op_cls, op_id=None, target_cls=None):
setattr(target_cls, operation_registry_name, dict()) setattr(target_cls, operation_registry_name, dict())
getattr(target_cls, operation_registry_name)[op_id] = op_cls getattr(target_cls, operation_registry_name)[op_id] = op_cls
return op_cls
...@@ -27,9 +27,7 @@ class OperationTestCase(TestCase): ...@@ -27,9 +27,7 @@ class OperationTestCase(TestCase):
class AbortEx(Exception): class AbortEx(Exception):
pass pass
op = Operation(MagicMock()) op = TestOp(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
op.async_operation = MagicMock( op.async_operation = MagicMock(
apply_async=MagicMock(side_effect=AbortEx)) apply_async=MagicMock(side_effect=AbortEx))
...@@ -44,9 +42,7 @@ class OperationTestCase(TestCase): ...@@ -44,9 +42,7 @@ class OperationTestCase(TestCase):
class AbortEx(Exception): class AbortEx(Exception):
pass pass
op = Operation(MagicMock()) op = TestOp(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
with patch.object(Operation, 'create_activity', side_effect=AbortEx): with patch.object(Operation, 'create_activity', side_effect=AbortEx):
with patch.object(Operation, 'check_precond') as chk_pre: with patch.object(Operation, 'check_precond') as chk_pre:
try: try:
...@@ -55,9 +51,7 @@ class OperationTestCase(TestCase): ...@@ -55,9 +51,7 @@ class OperationTestCase(TestCase):
self.assertTrue(chk_pre.called) self.assertTrue(chk_pre.called)
def test_auth_check_on_non_system_call(self): def test_auth_check_on_non_system_call(self):
op = Operation(MagicMock()) op = TestOp(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
user = MagicMock() user = MagicMock()
with patch.object(Operation, 'check_auth') as check_auth: with patch.object(Operation, 'check_auth') as check_auth:
with patch.object(Operation, 'check_precond'), \ with patch.object(Operation, 'check_precond'), \
...@@ -67,9 +61,7 @@ class OperationTestCase(TestCase): ...@@ -67,9 +61,7 @@ class OperationTestCase(TestCase):
check_auth.assert_called_with(user) check_auth.assert_called_with(user)
def test_no_auth_check_on_system_call(self): def test_no_auth_check_on_system_call(self):
op = Operation(MagicMock()) op = TestOp(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
with patch.object(Operation, 'check_auth', side_effect=AssertionError): with patch.object(Operation, 'check_auth', side_effect=AssertionError):
with patch.object(Operation, 'check_precond'), \ with patch.object(Operation, 'check_precond'), \
patch.object(Operation, 'create_activity'), \ patch.object(Operation, 'create_activity'), \
...@@ -77,39 +69,25 @@ class OperationTestCase(TestCase): ...@@ -77,39 +69,25 @@ class OperationTestCase(TestCase):
op.call(system=True) op.call(system=True)
def test_no_exception_for_more_arguments_when_operation_takes_kwargs(self): def test_no_exception_for_more_arguments_when_operation_takes_kwargs(self):
class KwargOp(Operation): op = TestOp(MagicMock())
activity_code_suffix = 'test' with patch.object(TestOp, 'create_activity'), \
id = 'test' patch.object(TestOp, '_exec_op'):
def _operation(self, **kwargs):
pass
op = KwargOp(MagicMock())
with patch.object(KwargOp, 'create_activity'), \
patch.object(KwargOp, '_exec_op'):
op.call(system=True, foo=42) op.call(system=True, foo=42)
def test_exception_for_unexpected_arguments(self): def test_exception_for_unexpected_arguments(self):
class TestOp(Operation):
activity_code_suffix = 'test'
id = 'test'
def _operation(self):
pass
op = TestOp(MagicMock()) op = TestOp(MagicMock())
with patch.object(TestOp, 'create_activity'), \ with patch.object(TestOp, 'create_activity'), \
patch.object(TestOp, '_exec_op'): patch.object(TestOp, '_exec_op'):
self.assertRaises(TypeError, op.call, system=True, foo=42) self.assertRaises(TypeError, op.call, system=True, bar=42)
def test_exception_for_missing_arguments(self): def test_exception_for_missing_arguments(self):
class TestOp(Operation):
activity_code_suffix = 'test'
id = 'test'
def _operation(self, foo):
pass
op = TestOp(MagicMock()) op = TestOp(MagicMock())
with patch.object(TestOp, 'create_activity'): with patch.object(TestOp, 'create_activity'):
self.assertRaises(TypeError, op.call, system=True) self.assertRaises(TypeError, op.call, system=True)
class TestOp(Operation):
id = 'test'
def _operation(self, foo):
pass
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
from __future__ import absolute_import from __future__ import absolute_import
from datetime import timedelta from datetime import timedelta
from urlparse import urlparse
from django.contrib.auth.forms import ( from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm, AuthenticationForm, PasswordResetForm, SetPasswordForm,
...@@ -39,6 +40,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm ...@@ -39,6 +40,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.forms.widgets import TextInput, HiddenInput from django.forms.widgets import TextInput, HiddenInput
from django.template import Context from django.template import Context
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from sizefield.widgets import FileSizeWidget from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
...@@ -79,6 +81,12 @@ class VmSaveForm(forms.Form): ...@@ -79,6 +81,12 @@ class VmSaveForm(forms.Form):
helper.form_tag = False helper.form_tag = False
return helper return helper
def __init__(self, *args, **kwargs):
default = kwargs.pop('default', None)
super(VmSaveForm, self).__init__(*args, **kwargs)
if default:
self.fields['name'].initial = default
class VmCustomizeForm(forms.Form): class VmCustomizeForm(forms.Form):
name = forms.CharField(widget=forms.TextInput(attrs={ name = forms.CharField(widget=forms.TextInput(attrs={
...@@ -524,11 +532,7 @@ class TemplateForm(forms.ModelForm): ...@@ -524,11 +532,7 @@ class TemplateForm(forms.ModelForm):
value = field.widget.value_from_datadict( value = field.widget.value_from_datadict(
self.data, self.files, self.add_prefix(name)) self.data, self.files, self.add_prefix(name))
try: try:
if isinstance(field, forms.FileField): value = field.clean(value)
initial = self.initial.get(name, field.initial)
value = field.clean(value, initial)
else:
value = field.clean(value)
self.cleaned_data[name] = value self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name): if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)() value = getattr(self, 'clean_%s' % name)()
...@@ -544,13 +548,14 @@ class TemplateForm(forms.ModelForm): ...@@ -544,13 +548,14 @@ class TemplateForm(forms.ModelForm):
else: else:
self.cleaned_data[name] = getattr(old, name) self.cleaned_data[name] = getattr(old, name)
if "req_traits" not in self.allowed_fields:
self.cleaned_data['req_traits'] = self.instance.req_traits.all()
def save(self, commit=True): def save(self, commit=True):
data = self.cleaned_data data = self.cleaned_data
self.instance.max_ram_size = data.get('ram_size') self.instance.max_ram_size = data.get('ram_size')
instance = super(TemplateForm, self).save(commit=False) instance = super(TemplateForm, self).save(commit=True)
if commit:
instance.save()
# create and/or delete InterfaceTemplates # create and/or delete InterfaceTemplates
networks = InterfaceTemplate.objects.filter( networks = InterfaceTemplate.objects.filter(
...@@ -755,6 +760,7 @@ class VmStateChangeForm(forms.Form): ...@@ -755,6 +760,7 @@ class VmStateChangeForm(forms.Form):
"but don't interrupt any tasks.")) "but don't interrupt any tasks."))
new_state = forms.ChoiceField(Instance.STATUS, label=_( new_state = forms.ChoiceField(Instance.STATUS, label=_(
"New status")) "New status"))
reset_node = forms.BooleanField(required=False, label=_("Reset node"))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
show_interrupt = kwargs.pop('show_interrupt') show_interrupt = kwargs.pop('show_interrupt')
...@@ -772,6 +778,17 @@ class VmStateChangeForm(forms.Form): ...@@ -772,6 +778,17 @@ class VmStateChangeForm(forms.Form):
return helper return helper
class RedeployForm(forms.Form):
with_emergency_change_state = forms.BooleanField(
required=False, initial=True, label=_("use emergency state change"))
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class VmCreateDiskForm(forms.Form): class VmCreateDiskForm(forms.Form):
name = forms.CharField(max_length=100, label=_("Name")) name = forms.CharField(max_length=100, label=_("Name"))
size = forms.CharField( size = forms.CharField(
...@@ -779,6 +796,12 @@ class VmCreateDiskForm(forms.Form): ...@@ -779,6 +796,12 @@ class VmCreateDiskForm(forms.Form):
help_text=_('Size of disk to create in bytes or with units ' help_text=_('Size of disk to create in bytes or with units '
'like MB or GB.')) 'like MB or GB.'))
def __init__(self, *args, **kwargs):
default = kwargs.pop('default', None)
super(VmCreateDiskForm, self).__init__(*args, **kwargs)
if default:
self.fields['name'].initial = default
def clean_size(self): def clean_size(self):
size_in_bytes = self.cleaned_data.get("size") size_in_bytes = self.cleaned_data.get("size")
if not size_in_bytes.isdigit() and len(size_in_bytes) > 0: if not size_in_bytes.isdigit() and len(size_in_bytes) > 0:
...@@ -793,8 +816,79 @@ class VmCreateDiskForm(forms.Form): ...@@ -793,8 +816,79 @@ class VmCreateDiskForm(forms.Form):
return helper return helper
class VmDiskResizeForm(forms.Form):
size = forms.CharField(
widget=FileSizeWidget, initial=(10 << 30), label=_('Size'),
help_text=_('Size to resize the disk in bytes or with units '
'like MB or GB.'))
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.disk = kwargs.pop('default')
super(VmDiskResizeForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'disk', forms.ModelChoiceField(
queryset=choices, initial=self.disk, required=True,
empty_label=None, label=_('Disk')))
if self.disk:
self.fields['disk'].widget = HiddenInput()
self.fields['size'].initial += self.disk.size
def clean(self):
cleaned_data = super(VmDiskResizeForm, self).clean()
size_in_bytes = self.cleaned_data.get("size")
disk = self.cleaned_data.get('disk')
if not size_in_bytes.isdigit() and len(size_in_bytes) > 0:
raise forms.ValidationError(_("Invalid format, you can use "
" GB or MB!"))
if int(size_in_bytes) < int(disk.size):
raise forms.ValidationError(_("Disk size must be greater than the "
"actual size."))
return cleaned_data
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
if self.disk:
helper.layout = Layout(
HTML(_("<label>Disk:</label> %s") % escape(self.disk)),
Field('disk'), Field('size'))
return helper
class VmDiskRemoveForm(forms.Form):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.disk = kwargs.pop('default')
super(VmDiskRemoveForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'disk', forms.ModelChoiceField(
queryset=choices, initial=self.disk, required=True,
empty_label=None, label=_('Disk')))
if self.disk:
self.fields['disk'].widget = HiddenInput()
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
if self.disk:
helper.layout = Layout(
AnyTag(
"div",
HTML(_("<label>Disk:</label> %s") % escape(self.disk)),
css_class="form-group",
),
Field("disk"),
)
return helper
class VmDownloadDiskForm(forms.Form): class VmDownloadDiskForm(forms.Form):
name = forms.CharField(max_length=100, label=_("Name")) name = forms.CharField(max_length=100, label=_("Name"), required=False)
url = forms.CharField(label=_('URL'), validators=[URLValidator(), ]) url = forms.CharField(label=_('URL'), validators=[URLValidator(), ])
@property @property
...@@ -803,6 +897,18 @@ class VmDownloadDiskForm(forms.Form): ...@@ -803,6 +897,18 @@ class VmDownloadDiskForm(forms.Form):
helper.form_tag = False helper.form_tag = False
return helper return helper
def clean(self):
cleaned_data = super(VmDownloadDiskForm, self).clean()
if not cleaned_data['name']:
if cleaned_data['url']:
cleaned_data['name'] = urlparse(
cleaned_data['url']).path.split('/')[-1]
if not cleaned_data['name']:
raise forms.ValidationError(
_("Could not find filename in URL, "
"please specify a name explicitly."))
return cleaned_data
class VmAddInterfaceForm(forms.Form): class VmAddInterfaceForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
......
...@@ -528,7 +528,7 @@ footer a, footer a:hover, footer a:visited { ...@@ -528,7 +528,7 @@ footer a, footer a:hover, footer a:visited {
} }
#dashboard-template-list a small { #dashboard-template-list a small {
max-width: 50%; max-width: 45%;
float: left; float: left;
padding-top: 2px; padding-top: 2px;
text-overflow: ellipsis; text-overflow: ellipsis;
...@@ -991,3 +991,42 @@ textarea[name="new_members"] { ...@@ -991,3 +991,42 @@ textarea[name="new_members"] {
.introjs-button:hover { .introjs-button:hover {
color: #428bca; color: #428bca;
} }
#vm-info-pane {
margin-bottom: 20px;
}
.node-list-table tbody>tr>td, .node-list-table thead>tr>th {
vertical-align: middle;
}
.node-list-table thead>tr>th,
.node-list-table .enabled, .node-list-table .priority,
.node-list-table .overcommit, .node-list-table .number_of_VMs {
text-align: center;
}
.node-list-table-thin {
width: 10px;
}
.node-list-table-monitor {
width: 250px;
}
.graph-images img {
max-width: 100%;
}
#vm-list-table td.state,
#vm-list-table td.memory {
white-space: nowrap;
}
#vm-list-table td {
vertical-align: middle;
}
.disk-resize-btn {
margin-right: 5px;
}
...@@ -234,6 +234,7 @@ $(function () { ...@@ -234,6 +234,7 @@ $(function () {
'host': result[i].host, 'host': result[i].host,
'icon': result[i].icon, 'icon': result[i].icon,
'status': result[i].status, 'status': result[i].status,
'owner': result[i].owner,
}); });
} }
}); });
...@@ -251,7 +252,7 @@ $(function () { ...@@ -251,7 +252,7 @@ $(function () {
search_result.sort(compareVmByFav); search_result.sort(compareVmByFav);
for(var i=0; i<5 && i<search_result.length; i++) for(var i=0; i<5 && i<search_result.length; i++)
html += generateVmHTML(search_result[i].pk, search_result[i].name, html += generateVmHTML(search_result[i].pk, search_result[i].name,
search_result[i].host, search_result[i].icon, search_result[i].owner ? search_result[i].owner : search_result[i].host, search_result[i].icon,
search_result[i].status, search_result[i].fav, search_result[i].status, search_result[i].fav,
(search_result.length < 5)); (search_result.length < 5));
if(search_result.length == 0) if(search_result.length == 0)
...@@ -396,6 +397,20 @@ $(function () { ...@@ -396,6 +397,20 @@ $(function () {
clientInstalledAction(connectUri); clientInstalledAction(connectUri);
return false; return false;
}); });
/* change graphs */
$(".graph-buttons a").click(function() {
var time = $(this).data("graph-time");
$(".graph-images img").each(function() {
var src = $(this).prop("src");
var new_src = src.substring(0, src.lastIndexOf("/") + 1) + time;
$(this).prop("src", new_src);
});
// change the buttons too
$(".graph-buttons a").removeClass("btn-primary").addClass("btn-default");
$(this).removeClass("btn-default").addClass("btn-primary");
return false;
});
}); });
function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { function generateVmHTML(pk, name, host, icon, _status, fav, is_last) {
...@@ -603,7 +618,7 @@ function addModalConfirmation(func, data) { ...@@ -603,7 +618,7 @@ function addModalConfirmation(func, data) {
} }
function clientInstalledAction(location) { function clientInstalledAction(location) {
setCookie('downloaded_client', true, 365 * 24 * 60 * 60, "/"); setCookie('downloaded_client', true, 365 * 24 * 60 * 60 * 1000, "/");
window.location.href = location; window.location.href = location;
$('#confirmation-modal').modal("hide"); $('#confirmation-modal').modal("hide");
} }
......
...@@ -51,7 +51,8 @@ $(function() { ...@@ -51,7 +51,8 @@ $(function() {
if(data.success) { if(data.success) {
$('a[href="#activity"]').trigger("click"); $('a[href="#activity"]').trigger("click");
if(data.with_reload) { if(data.with_reload) {
location.reload(); // when the activity check stops the page will reload
reload_vm_detail = true;
} }
/* if there are messages display them */ /* if there are messages display them */
......
var show_all = false; var show_all = false;
var in_progress = false; var in_progress = false;
var activity_hash = 5; var activity_hash = 5;
var reload_vm_detail = false;
$(function() { $(function() {
/* do we need to check for new activities */ /* do we need to check for new activities */
...@@ -419,6 +420,7 @@ function checkNewActivity(runs) { ...@@ -419,6 +420,7 @@ function checkNewActivity(runs) {
); );
} else { } else {
in_progress = false; in_progress = false;
if(reload_vm_detail) location.reload();
} }
$('a[href="#activity"] i').removeClass('fa-spin'); $('a[href="#activity"] i').removeClass('fa-spin');
}, },
......
...@@ -19,8 +19,7 @@ from __future__ import absolute_import ...@@ -19,8 +19,7 @@ from __future__ import absolute_import
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django_tables2 import Table, A from django_tables2 import Table, A
from django_tables2.columns import (TemplateColumn, Column, BooleanColumn, from django_tables2.columns import TemplateColumn, Column, LinkColumn
LinkColumn)
from vm.models import Node, InstanceTemplate, Lease from vm.models import Node, InstanceTemplate, Lease
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
...@@ -40,8 +39,10 @@ class NodeListTable(Table): ...@@ -40,8 +39,10 @@ class NodeListTable(Table):
attrs={'th': {'class': 'node-list-table-thin'}}, attrs={'th': {'class': 'node-list-table-thin'}},
) )
enabled = BooleanColumn( get_status_display = Column(
verbose_name=_("Status"),
attrs={'th': {'class': 'node-list-table-thin'}}, attrs={'th': {'class': 'node-list-table-thin'}},
order_by=("enabled", "schedule_enabled"),
) )
name = TemplateColumn( name = TemplateColumn(
...@@ -66,20 +67,12 @@ class NodeListTable(Table): ...@@ -66,20 +67,12 @@ class NodeListTable(Table):
orderable=False, orderable=False,
) )
actions = TemplateColumn(
verbose_name=_("Actions"),
attrs={'th': {'class': 'node-list-table-thin'}},
template_code=('{% include "dashboard/node-list/column-'
'actions.html" with btn_size="btn-xs" %}'),
orderable=False,
)
class Meta: class Meta:
model = Node model = Node
attrs = {'class': ('table table-bordered table-striped table-hover ' attrs = {'class': ('table table-bordered table-striped table-hover '
'node-list-table')} 'node-list-table')}
fields = ('pk', 'name', 'host', 'enabled', 'priority', 'overcommit', fields = ('pk', 'name', 'host', 'get_status_display', 'priority',
'number_of_VMs', ) 'overcommit', 'number_of_VMs', )
class GroupListTable(Table): class GroupListTable(Table):
......
<img src="{{ STATIC_URL}}dashboard/img/logo.png" style="height: 25px;"/>
<img src="{{ STATIC_URL}}local-logo.png" style="padding-left: 2px; height: 25px;"/>
{% load i18n %} {% load i18n %}
{% load sizefieldtags %} {% load sizefieldtags %}
<i class="fa {% if d.is_downloading %}fa-refresh fa-spin{% else %}fa-file{% if d.failed %}" style="color: #d9534f;{% endif %}{% endif %}"></i> <i class="fa fa-file"></i>
{{ d.name }} (#{{ d.id }}) - {{ d.name }} (#{{ d.id }})
{% if not d.is_downloading %}
{% if not d.failed %} {% if op.remove_disk %}
{% if d.size %}{{ d.size|filesize }}{% endif %} <span class="operation-wrapper">
{% else %} <a href="{{ op.remove_disk.get_url }}?disk={{d.pk}}"
<div class="label label-danger"{% if user.is_superuser %} title="{{ d.get_latest_activity_result }}"{% endif %}>{% trans "failed" %}</div> class="btn btn-xs btn-{{ op.remove_disk.effect}} pull-right operation disk-remove-btn
{% endif %} {% if op.resize_disk.disabled %}disabled{% endif %}">
{% else %}<span class="disk-list-disk-percentage" data-disk-pk="{{ d.pk }}">{{ d.get_download_percentage }}</span>%{% endif %} <i class="fa fa-{{ op.remove_disk.icon }}"></i> {% trans "Remove" %}
{% if is_owner != False %} </a>
<a href="{% url "dashboard.views.disk-remove" pk=d.pk %}?next={{ request.path }}" </span>
data-disk-pk="{{ d.pk }}" class="btn btn-xs btn-danger pull-right disk-remove" {% endif %}
{% if not long_remove %}title="{% trans "Remove" %}"{% endif %} {% if op.resize_disk %}
> <span class="operation-wrapper">
<i class="fa fa-times"></i>{% if long_remove %} {% trans "Remove" %}{% endif %} <a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}"
</a> class="btn btn-xs btn-{{ op.resize_disk.effect }} pull-right operation disk-resize-btn
{% if op.resize_disk.disabled %}disabled{% endif %}">
<i class="fa fa-{{ op.resize_disk.icon }}"></i> {% trans "Resize" %}
</a>
</span>
{% endif %} {% endif %}
<div style="clear: both;"></div> <div style="clear: both;"></div>
{% for o in graph_time_options %}
<a class="btn btn-xs
btn-{% if graph_time == o.time %}primary{% else %}default{% endif %}"
href="?graph_time={{ o.time }}"
data-graph-time="{{ o.time }}">
{{ o.name }}
</a>
{% endfor %}
...@@ -13,17 +13,19 @@ Choose a compute node to migrate {{obj}} to. ...@@ -13,17 +13,19 @@ Choose a compute node to migrate {{obj}} to.
{% block formfields %} {% block formfields %}
<ul id="vm-migrate-node-list" class="list-unstyled"> <ul id="vm-migrate-node-list" class="list-unstyled">
{% with current=object.node.pk selected=object.select_node.pk %} {% with current=object.node.pk %}
{% for n in nodes %} {% for n in nodes %}
<li class="panel panel-default"><div class="panel-body"> <li class="panel panel-default"><div class="panel-body">
<label for="migrate-to-{{n.pk}}"> <label for="migrate-to-{{n.pk}}">
<strong>{{ n }}</strong> <strong>{{ n }}</strong>
<div class="label label-primary"><i class="fa {{n.get_status_icon}}"></i>
{{n.get_status_display}}</div>
{% if current == n.pk %}<div class="label label-info">{% trans "current" %}</div>{% endif %} {% 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 %} {% if recommended == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %}
</label> </label>
<input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;" <input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;"
{% if current == n.pk %}disabled="disabled"{% endif %} {% if current == n.pk %}disabled="disabled"{% endif %}
{% if selected == n.pk %}checked="checked"{% endif %} /> {% if recommended == n.pk %}checked="checked"{% endif %} />
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span> <span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span> <span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
{% block navbar-brand %} {% block navbar-brand %}
<a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;"> <a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;">
<img src="{{ STATIC_URL}}dashboard/img/logo.png" style="height: 25px;"/> {% include "branding.html" %}
</a> </a>
{% endblock %} {% endblock %}
......
{% load i18n %}
<div class="modal fade" id="confirmation-modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
{% if text %}
{{ text }}
{% else %}
{%blocktrans with object=object%}
Are you sure you want to flush <strong>{{ object }}</strong>?
{%endblocktrans%}
{% endif %}
<div class="pull-right">
<form action="{% url "dashboard.views.flush-node" pk=node.pk %}?next={{next}}" method="POST">
{% csrf_token %}
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button>
<input type="hidden" name="flush" value=""/>
<button class="btn btn-warning">{% trans "Yes" %}</button>
</form>
</div>
<div class="clearfix"></div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load i18n %} {% load i18n %}
{% block title-page %}{{ group.name }} | {% trans "group" %}{% endblock %}
{% block content %} {% block content %}
<div class="body-content"> <div class="body-content">
<div class="page-header"> <div class="page-header">
......
...@@ -25,7 +25,10 @@ ...@@ -25,7 +25,10 @@
<i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i> <i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i>
{{ i.name }} {{ i.name }}
</span> </span>
<small class="text-muted"> {{ i.short_hostname }}</small> <small class="text-muted">
{% if i.owner == request.user %}{{ i.short_hostname }}
{% else %}{{i.owner.profile.get_display_name}}{% endif %}
</small>
<div class="pull-right dashboard-vm-favourite" data-vm="{{ i.pk }}"> <div class="pull-right dashboard-vm-favourite" data-vm="{{ i.pk }}">
{% if i.fav %} {% if i.fav %}
<i class="fa fa-star text-primary title-favourite" title="{% trans "Unfavourite" %}"></i> <i class="fa fa-star text-primary title-favourite" title="{% trans "Unfavourite" %}"></i>
......
...@@ -6,13 +6,12 @@ ...@@ -6,13 +6,12 @@
{% block content %} {% block content %}
<div class="body-content"> <div class="body-content">
<div class="page-header"> <div class="page-header">
<div class="pull-right" id="ops">
{% include "dashboard/vm-detail/_operations.html" %}
</div>
<div class="pull-right" style="padding-top: 15px;"> <div class="pull-right" style="padding-top: 15px;">
<a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs node-details-rename-button"><i class="fa fa-pencil"></i></a> <a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs node-details-rename-button"><i class="fa fa-pencil"></i></a>
<a title="{% trans "Flush" %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-flush" href="{% url "dashboard.views.flush-node" pk=node.pk %}"><i class="fa fa-cloud-upload"></i></a>
<a title="{% trans "Enable" %}" style="display:{% if node.enabled %}none{% else %}inline-block{% endif %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-enable" href="{% url "dashboard.views.status-node" pk=node.pk %}?next={{ request.path }}"><i class="fa fa-check"></i></a>
<a title="{% trans "Disable" %}" style="display:{% if not node.enabled %}none{% else %}inline-block{% endif %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-enable" href="{% url "dashboard.views.status-node" pk=node.pk %}?next={{ request.path }}"><i class="fa fa-ban"></i></a>
<a title="{% trans "Delete" %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-delete" href="{% url "dashboard.views.delete-node" pk=node.pk %}"><i class="fa fa-trash-o"></i></a> <a title="{% trans "Delete" %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-delete" href="{% url "dashboard.views.delete-node" pk=node.pk %}"><i class="fa fa-trash-o"></i></a>
<a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs node-details-help-button"><i class="fa fa-question"></i></a>
</div> </div>
<h1> <h1>
<div id="node-details-rename"> <div id="node-details-rename">
...@@ -26,42 +25,34 @@ ...@@ -26,42 +25,34 @@
{{ node.name }} {{ node.name }}
</div> </div>
</h1> </h1>
<div class="node-details-help js-hidden">
<ul style="list-style: none;">
<li>
<strong>{% trans "Rename" %}:</strong>
{% trans "Change the name of the node." %}
</li>
<li>
<strong>{% trans "Flush" %}:</strong>
{% trans "Disable node and move all instances to other one." %}
</li>
<li>
<strong>{% trans "Enable" %}:</strong>
{% trans "Enables node." %}
</li>
<li>
<strong>{% trans "Disable" %}:</strong>
{% trans "Disables node." %}
</li>
<li>
<strong>{% trans "Delete" %}:</strong>
{% trans "Remove node and it's host." %}
</li>
</ul>
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-2" id="node-info-pane"> <div class="col-md-2" id="node-info-pane">
<div id="node-info-data" class="big"> <div id="node-info-data" class="big">
<span id="node-details-state" class="label <span id="node-details-state" class="label
{% if node.state == 'ONLINE' %}label-success {% if node.state == 'ACTIVE' %}label-success
{% elif node.state == 'MISSING' %}label-danger {% elif node.state == 'PASSIVE' %}label-warning
{% elif node.state == 'DISABLED' %}label-warning {% else %}label-danger{% endif %}">
{% elif node.state == 'OFFLINE' %}label-warning{% endif %}">
<i class="fa {{ node.get_status_icon }}"></i> {{ node.get_status_display|upper }} <i class="fa {{ node.get_status_icon }}"></i> {{ node.get_status_display|upper }}
</span> </span>
</div> </div>
<div>
{% if node.enabled %}
<span class="label label-success">{% trans "Enabled" %}</span>
{% if node.schedule_enabled %}
<span class="label label-success">{% trans "Schedule enabled" %}</span>
{% else %}
<span class="label label-warning">{% trans "Schedule disabled" %}</span>
{% endif %}
{% else %}
<span class="label label-warning">{% trans "Disabled" %}</span>
{% endif %}
{% if node.online %}
<span class="label label-success">{% trans "Online" %}</span>
{% else %}
<span class="label label-warning">{% trans "Offline" %}</span>
{% endif %}
</div>
</div> </div>
<div class="col-md-10" id="node-detail-pane"> <div class="col-md-10" id="node-detail-pane">
<div class="panel panel-default" id="node-detail-panel"> <div class="panel panel-default" id="node-detail-panel">
......
...@@ -30,15 +30,22 @@ ...@@ -30,15 +30,22 @@
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{% if graphite_enabled %} {% if graphite_enabled %}
<img src="{% url "dashboard.views.node-graph" node.pk "cpu" "6h" %}" style="width:100%"/> <div class="text-center graph-buttons">
<img src="{% url "dashboard.views.node-graph" node.pk "memory" "6h" %}" style="width:100%"/> {% include "dashboard/_graph-time-buttons.html" %}
<img src="{% url "dashboard.views.node-graph" node.pk "network" "6h" %}" style="width:100%"/> </div>
<div class="graph-images text-center">
<img src="{% url "dashboard.views.node-graph" node.pk "cpu" graph_time %}"/>
<img src="{% url "dashboard.views.node-graph" node.pk "memory" graph_time %}"/>
<img src="{% url "dashboard.views.node-graph" node.pk "network" graph_time %}"/>
<img src="{% url "dashboard.views.node-graph" node.pk "vm" graph_time %}"/>
<img src="{% url "dashboard.views.node-graph" node.pk "alloc" graph_time %}"/>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<style>
.form-group {
margin: 0px;
}
</style> <style>
.form-group {
margin: 0px;
}
</style>
...@@ -21,25 +21,23 @@ ...@@ -21,25 +21,23 @@
</div> </div>
</div> </div>
<style> <div class="row">
.node-list-table tbody>tr>td, .node-list-table thead>tr>th { <div class="col-md-12">
vertical-align: middle; <div class="panel panel-default">
} <div class="panel-heading">
<div class="pull-right graph-buttons">
.node-list-table thead>tr>th, {% include "dashboard/_graph-time-buttons.html" %}
.node-list-table .enabled, .node-list-table .priority, </div>
.node-list-table .overcommit, .node-list-table .number_of_VMs { <h3 class="no-margin"><i class="fa fa-area-chart"></i> {% trans "Graphs" %}</h3>
text-align: center; </div>
} <div class="text-center graph-images">
<img src="{% url "dashboard.views.node-list-graph" "alloc" graph_time %}"/>
.node-list-table-thin { <img src="{% url "dashboard.views.node-list-graph" "vm" graph_time %}"/>
width: 10px; </div>
} </div>
</div><!-- -col-md-12 -->
</div><!-- .row -->
.node-list-table-monitor {
width: 250px;
}
</style>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
......
{% load i18n %}
<div class="btn-group">
<button type="button" class="btn {{ btn_size }} btn-warning nojs-dropdown-toogle dropdown-toggle" data-toggle="dropdown">Action
<i class="fa fa-caret-down"></i>
</button>
<ul class="dropdown-menu nojs-dropdown-toogle" role="menu">
<li>
<a href="#" class="node-details-rename-button">
<i class="fa fa-pencil"></i> {% trans "Rename" %}
</a>
</li>
<li>
<a data-node-pk="{{ record.pk }}" class="real-link node-flush" href="{% url "dashboard.views.flush-node" pk=record.pk %}">
<i class="fa fa-cloud-upload"></i> {% trans "Flush" %}
</a>
</li>
<li>
<a style={% if record.enabled %}"display:none"{% else %}"display:block"{% endif %} data-node-pk="{{ record.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=record.pk %}?next={{ request.path }}">
<i class="fa fa-check"></i> {% trans "Enable" %}
</a>
</li>
<li>
<a style={% if record.enabled %}"display:block"{% else %}"display:none"{% endif %} data-node-pk="{{ record.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=record.pk %}?next={{ request.path }}">
<i class="fa fa-times"></i> {% trans "Disable" %}
</a>
</li>
<li>
<a data-node-pk="{{ record.pk }}" class="real-link node-delete" href="{% url "dashboard.views.delete-node" pk=record.pk %}?next={{ request.path }}">
<i class="fa fa-trash-o"></i> {% trans "Delete" %}
</a>
</li>
</ul>
</div>
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
{% load sizefieldtags %} {% load sizefieldtags %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{% trans "Edit template" %}{% endblock %} {% block title-page %}{{ form.name.value }} | {% trans "template" %}{% endblock %}
{% block content %} {% block content %}
...@@ -23,18 +23,15 @@ ...@@ -23,18 +23,15 @@
{% csrf_token %} {% csrf_token %}
{{ form.name|as_crispy_field }} {{ form.name|as_crispy_field }}
<a {% if form.parent.value %}
href="{% url "dashboard.views.template-detail" pk=form.parent.value %}" <strong>{% trans "Parent template" %}:</strong>
{% else %} {% if parent %}
disabled %} <a href="{% url "dashboard.views.template-detail" pk=parent.pk %}">
{% endif %} {{ parent.name }}
class="btn btn-default pull-right" style="margin-top: 24px;"> </a>
{% trans "Visit" %} {% else %}
<i class="fa fa-arrow-circle-right"></i> -
</a> {% endif %}
<div style="width: 80%;">
{{ form.parent|as_crispy_field }}
</div>
<fieldset class="resources-sliders"> <fieldset class="resources-sliders">
<legend>{% trans "Resource configuration" %}</legend> <legend>{% trans "Resource configuration" %}</legend>
...@@ -89,7 +86,13 @@ ...@@ -89,7 +86,13 @@
{% endif %} {% endif %}
{% for d in disks %} {% for d in disks %}
<li> <li>
{% include "dashboard/_disk-list-element.html" %} <i class="fa fa-file"></i>
{{ d.name }} (#{{ d.id }}) -
<a href="{% url "dashboard.views.disk-remove" pk=d.pk %}?next={{ request.path }}"
data-disk-pk="{{ d.pk }}" class="btn btn-xs btn-danger pull-right disk-remove"
{% if not long_remove %}title="{% trans "Remove" %}"{% endif %}>
<i class="fa fa-times"></i>{% if long_remove %} {% trans "Remove" %}{% endif %}
</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
......
...@@ -58,7 +58,10 @@ ...@@ -58,7 +58,10 @@
<div class="input-group vm-details-home-name"> <div class="input-group vm-details-home-name">
<input id="vm-details-rename-name" class="form-control input-sm" name="new_name" type="text" value="{{ instance.name }}"/> <input id="vm-details-rename-name" class="form-control input-sm" name="new_name" type="text" value="{{ instance.name }}"/>
<span class="input-group-btn"> <span class="input-group-btn">
<button type="submit" class="btn btn-sm vm-details-rename-submit">{% trans "Rename" %}</button> <button type="submit" class="btn btn-sm vm-details-rename-submit
{% if not is_operator %}disabled{% endif %}">
{% trans "Rename" %}
</button>
</span> </span>
</div> </div>
</form> </form>
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}"> <span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
<i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-{{a.icon}}{% endif %}"></i> <i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-{{a.icon}}{% endif %}"></i>
</span> </span>
{% spaceless %}
<strong{% if a.result %} title="{{ a.result|get_text:user }}"{% endif %}> <strong{% if a.result %} title="{{ a.result|get_text:user }}"{% endif %}>
<a href="{{ a.get_absolute_url }}"> <a href="{{ a.get_absolute_url }}">
{% if a.times > 1 %}({{ a.times }}x){% endif %} {% if a.times > 1 %}({{ a.times }}x){% endif %}
...@@ -16,7 +17,7 @@ ...@@ -16,7 +17,7 @@
- {{ a.percentage }}% - {{ a.percentage }}%
{% endif %} {% endif %}
</strong> </strong>
{% if a.times < 2%}{{ a.started|date:"Y-m-d H:i" }}{% endif %}{% if a.user %}, {% endspaceless %}{% if a.times < 2%} {{ a.started|date:"Y-m-d H:i" }}{% endif %}{% if a.user %},
<a class="no-style-link" href="{% url "dashboard.views.profile" username=a.user.username %}"> <a class="no-style-link" href="{% url "dashboard.views.profile" username=a.user.username %}">
{% include "dashboard/_display-name.html" with user=a.user show_org=True %} {% include "dashboard/_display-name.html" with user=a.user show_org=True %}
</a> </a>
......
...@@ -11,7 +11,8 @@ ...@@ -11,7 +11,8 @@
<span class="input-group-addon">/</span> <span class="input-group-addon">/</span>
<select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select> <select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select>
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="btn btn-success btn-sm">{% trans "Add" %}</button> <button type="submit" class="btn btn-success btn-sm
{% if not is_operator %}disabled{% endif %}">{% trans "Add" %}</button>
</div> </div>
</div> </div>
</form> </form>
......
...@@ -6,7 +6,9 @@ ...@@ -6,7 +6,9 @@
<dd><i class="fa fa-{{ os_type_icon }}"></i> {{ instance.system }}</dd> <dd><i class="fa fa-{{ os_type_icon }}"></i> {{ instance.system }}</dd>
<dt style="margin-top: 5px;"> <dt style="margin-top: 5px;">
{% trans "Name" %}: {% trans "Name" %}:
<a href="#" class="vm-details-home-edit-name-click"><i class="fa fa-pencil"></i></a> {% if is_operator %}
<a href="#" class="vm-details-home-edit-name-click"><i class="fa fa-pencil"></i></a>
{% endif %}
</dt> </dt>
<dd> <dd>
<div class="vm-details-home-edit-name-click"> <div class="vm-details-home-edit-name-click">
...@@ -18,8 +20,9 @@ ...@@ -18,8 +20,9 @@
<div class="input-group"> <div class="input-group">
<input type="text" name="new_name" value="{{ instance.name }}" class="form-control input-sm"/> <input type="text" name="new_name" value="{{ instance.name }}" class="form-control input-sm"/>
<span class="input-group-btn"> <span class="input-group-btn">
<button type="submit" class="btn btn-success btn-sm vm-details-rename-submit"> <button type="submit" class="btn btn-success btn-sm vm-details-rename-submit
<i class="fa fa-pencil"></i> {% trans "Rename" %} {% if not is_operator %}disabled{% endif %}" title="{% trans "Rename" %}">
<i class="fa fa-pencil"></i>
</button> </button>
</span> </span>
</div> </div>
...@@ -28,7 +31,9 @@ ...@@ -28,7 +31,9 @@
</dd> </dd>
<dt style="margin-top: 5px;"> <dt style="margin-top: 5px;">
{% trans "Description" %}: {% trans "Description" %}:
<a href="#" class="vm-details-home-edit-description-click"><i class="fa fa-pencil"></i></a> {% if is_operator %}
<a href="#" class="vm-details-home-edit-description-click"><i class="fa fa-pencil"></i></a>
{% endif %}
</dt> </dt>
<dd> <dd>
{% csrf_token %} {% csrf_token %}
...@@ -38,7 +43,8 @@ ...@@ -38,7 +43,8 @@
<div id="vm-details-home-description" class="js-hidden"> <div id="vm-details-home-description" class="js-hidden">
<form method="POST"> <form method="POST">
<textarea name="new_description" class="form-control">{{ instance.description }}</textarea> <textarea name="new_description" class="form-control">{{ instance.description }}</textarea>
<button type="submit" class="btn btn-xs btn-success vm-details-description-submit"> <button type="submit" class="btn btn-xs btn-success vm-details-description-submit
{% if not is_operator %}disabled{% endif %}">
<i class="fa fa-pencil"></i> {% trans "Update" %} <i class="fa fa-pencil"></i> {% trans "Update" %}
</button> </button>
</form> </form>
...@@ -58,9 +64,17 @@ ...@@ -58,9 +64,17 @@
</h4> </h4>
<dl> <dl>
<dt>{% trans "Suspended at:" %}</dt> <dt>{% trans "Suspended at:" %}</dt>
<dd><i class="fa fa-moon-o"></i> {{ instance.time_of_suspend|timeuntil }}</dd> <dd>
<span title="{{ instance.time_of_suspend }}">
<i class="fa fa-moon-o"></i> {{ instance.time_of_suspend|timeuntil }}
</span>
</dd>
<dt>{% trans "Destroyed at:" %}</dt> <dt>{% trans "Destroyed at:" %}</dt>
<dd><i class="fa fa-times"></i> {{ instance.time_of_delete|timeuntil }}</dd> <dd>
<span title="{{ instance.time_of_delete }}">
<i class="fa fa-times"></i> {{ instance.time_of_delete|timeuntil }}
</span>
</dd>
</dl> </dl>
<div style="font-weight: bold;">{% trans "Tags" %}</div> <div style="font-weight: bold;">{% trans "Tags" %}</div>
...@@ -70,11 +84,13 @@ ...@@ -70,11 +84,13 @@
{% for t in instance.tags.all %} {% for t in instance.tags.all %}
<div class="label label-primary label-tag" style="display: inline-block"> <div class="label label-primary label-tag" style="display: inline-block">
{{ t }} {{ t }}
<a href="#" class="vm-details-remove-tag"><i class="fa fa-times"></i></a> {% if is_operator %}
<a href="#" class="vm-details-remove-tag"><i class="fa fa-times"></i></a>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}
<small>{% trans "No tag added!" %}</small> <small>{% trans "No tag added." %}</small>
{% endif %} {% endif %}
</div> </div>
<form action="" method="POST"> <form action="" method="POST">
...@@ -85,11 +101,26 @@ ...@@ -85,11 +101,26 @@
<i class="fa fa-question"></i> <i class="fa fa-question"></i>
</div>--> </div>-->
<div class="input-group-btn"> <div class="input-group-btn">
<input type="submit" class="btn btn-default btn-sm input-tags" value="{% trans "Add tag" %}"/> <input type="submit" class="btn btn-default btn-sm input-tags
{% if not is_operator %}disabled{% endif %}" value="{% trans "Add tag" %}"/>
</div> </div>
</div> </div>
</form> </form>
</div><!-- id:vm-details-tags --> </div><!-- id:vm-details-tags -->
{% if request.user.is_superuser %}
<dl>
<dt>{% trans "Node" %}:</dt>
<dd>
{% if instance.node %}
<a href="{{ instance.node.get_absolute_url }}">
{{ instance.node.name }}
</a>
{% else %}
-
{% endif %}
</dd>
{% endif %}
</dl>
<dl> <dl>
<dt>{% trans "Template" %}:</dt> <dt>{% trans "Template" %}:</dt>
<dd> <dd>
...@@ -123,9 +154,14 @@ ...@@ -123,9 +154,14 @@
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
{% if graphite_enabled %} {% if graphite_enabled %}
<img src="{% url "dashboard.views.vm-graph" instance.pk "cpu" "6h" %}" style="width:100%"/> <div class="text-center graph-buttons">
<img src="{% url "dashboard.views.vm-graph" instance.pk "memory" "6h" %}" style="width:100%"/> {% include "dashboard/_graph-time-buttons.html" %}
<img src="{% url "dashboard.views.vm-graph" instance.pk "network" "6h" %}" style="width:100%"/> </div>
<div class="graph-images text-center">
<img src="{% url "dashboard.views.vm-graph" instance.pk "cpu" graph_time %}"/>
<img src="{% url "dashboard.views.vm-graph" instance.pk "memory" graph_time %}"/>
<img src="{% url "dashboard.views.vm-graph" instance.pk "network" graph_time %}"/>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
...@@ -21,11 +21,13 @@ ...@@ -21,11 +21,13 @@
<a href="{{ i.host.get_absolute_url }}" <a href="{{ i.host.get_absolute_url }}"
class="btn btn-default btn-xs">{% trans "edit" %}</a> class="btn btn-default btn-xs">{% trans "edit" %}</a>
{% endif %} {% endif %}
<a href="{% url "dashboard.views.interface-delete" pk=i.pk %}?next={{ request.path }}" {% if is_owner %}
class="btn btn-danger btn-xs interface-remove" <a href="{% url "dashboard.views.interface-delete" pk=i.pk %}?next={{ request.path }}"
data-interface-pk="{{ i.pk }}"> class="btn btn-danger btn-xs interface-remove"
{% trans "remove" %} data-interface-pk="{{ i.pk }}">
</a> {% trans "remove" %}
</a>
{% endif %}
</h3> </h3>
{% if i.host %} {% if i.host %}
<div class="row"> <div class="row">
......
...@@ -72,6 +72,10 @@ ...@@ -72,6 +72,10 @@
{% trans "Lease" as t %} {% trans "Lease" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="lease" %} {% include "dashboard/vm-list/header-link.html" with name=t sort="lease" %}
</th> </th>
<th data-sort="string" class="orderable sortable">
{% trans "Memory" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="ram_size" %}
</th>
{% if user.is_superuser %} {% if user.is_superuser %}
<th data-sort="string" class="orderable sortable"> <th data-sort="string" class="orderable sortable">
{% trans "IP address" as t %} {% trans "IP address" as t %}
...@@ -86,7 +90,9 @@ ...@@ -86,7 +90,9 @@
{% for i in object_list %} {% for i in object_list %}
<tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}"> <tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}">
<td class="pk"><div id="vm-{{i.pk}}">{{i.pk}}</div> </td> <td class="pk"><div id="vm-{{i.pk}}">{{i.pk}}</div> </td>
<td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">{{ i.name }}</a> </td> <td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">
{{ i.name }}</a>
</td>
<td class="state"> <td class="state">
<i class="fa fa-fw <i class="fa fa-fw
{% if show_acts_in_progress and i.is_in_status_change %} {% if show_acts_in_progress and i.is_in_status_change %}
...@@ -104,7 +110,12 @@ ...@@ -104,7 +110,12 @@
{# include "dashboard/_display-name.html" with user=i.owner show_org=True #} {# include "dashboard/_display-name.html" with user=i.owner show_org=True #}
</td> </td>
<td class="lease "data-sort-value="{{ i.lease.name }}"> <td class="lease "data-sort-value="{{ i.lease.name }}">
{{ i.lease.name }} <span title="{{ i.time_of_suspend|timeuntil }} | {{ i.time_of_delete|timeuntil }}">
{{ i.lease.name }}
</span>
</td>
<td class="memory "data-sort-value="{{ i.ram_size }}">
{{ i.ram_size }} MiB
</td> </td>
{% if user.is_superuser %} {% if user.is_superuser %}
<td class="ip_addr "data-sort-value="{{ i.ipv4 }}"> <td class="ip_addr "data-sort-value="{{ i.ipv4 }}">
......
...@@ -512,20 +512,20 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -512,20 +512,20 @@ class VmDetailTest(LoginMixin, TestCase):
self.login(c, "user2") self.login(c, "user2")
with patch.object(Instance, 'select_node', return_value=None), \ with patch.object(Instance, 'select_node', return_value=None), \
patch.object(WakeUpOperation, 'async') as new_wake_up, \ patch.object(WakeUpOperation, 'async') as new_wake_up, \
patch('vm.tasks.vm_tasks.wake_up.apply_async') as wuaa, \
patch.object(Instance.WrongStateError, 'send_message') as wro: patch.object(Instance.WrongStateError, 'send_message') as wro:
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
new_wake_up.side_effect = inst.wake_up new_wake_up.side_effect = inst.wake_up
inst._wake_up_vm = Mock()
inst.get_remote_queue_name = Mock(return_value='test') inst.get_remote_queue_name = Mock(return_value='test')
inst.status = 'SUSPENDED' inst.status = 'SUSPENDED'
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
with patch('dashboard.views.messages') as msg: with patch('dashboard.views.messages') as msg:
response = c.post("/dashboard/vm/1/op/wake_up/") response = c.post("/dashboard/vm/1/op/wake_up/")
assert not msg.error.called assert not msg.error.called
assert inst._wake_up_vm.called
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(inst.status, 'RUNNING') self.assertEqual(inst.status, 'RUNNING')
assert new_wake_up.called assert new_wake_up.called
assert wuaa.called
assert not wro.called assert not wro.called
def test_unpermitted_wake_up(self): def test_unpermitted_wake_up(self):
...@@ -1210,7 +1210,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1210,7 +1210,7 @@ class GroupDetailTest(LoginMixin, TestCase):
gp = self.g1.profile gp = self.g1.profile
acl_users = len(gp.get_users_with_level()) acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'user3', 'level': 'owner'}) {'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users, len(gp.get_users_with_level())) self.assertEqual(acl_users, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1221,7 +1221,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1221,7 +1221,7 @@ class GroupDetailTest(LoginMixin, TestCase):
gp = self.g1.profile gp = self.g1.profile
acl_users = len(gp.get_users_with_level()) acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'user3', 'level': 'owner'}) {'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users, len(gp.get_users_with_level())) self.assertEqual(acl_users, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1232,7 +1232,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1232,7 +1232,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'superuser') self.login(c, 'superuser')
acl_users = len(gp.get_users_with_level()) acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'user3', 'level': 'owner'}) {'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users + 1, len(gp.get_users_with_level())) self.assertEqual(acl_users + 1, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1243,7 +1243,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1243,7 +1243,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'user0') self.login(c, 'user0')
acl_users = len(gp.get_users_with_level()) acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'user3', 'level': 'owner'}) {'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users + 1, len(gp.get_users_with_level())) self.assertEqual(acl_users + 1, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1253,7 +1253,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1253,7 +1253,7 @@ class GroupDetailTest(LoginMixin, TestCase):
gp = self.g1.profile gp = self.g1.profile
acl_groups = len(gp.get_groups_with_level()) acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'group2', 'level': 'owner'}) {'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups, len(gp.get_groups_with_level())) self.assertEqual(acl_groups, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1264,7 +1264,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1264,7 +1264,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'user3') self.login(c, 'user3')
acl_groups = len(gp.get_groups_with_level()) acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'group2', 'level': 'owner'}) {'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups, len(gp.get_groups_with_level())) self.assertEqual(acl_groups, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1275,7 +1275,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1275,7 +1275,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'superuser') self.login(c, 'superuser')
acl_groups = len(gp.get_groups_with_level()) acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'group2', 'level': 'owner'}) {'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level())) self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1286,7 +1286,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1286,7 +1286,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'user0') self.login(c, 'user0')
acl_groups = len(gp.get_groups_with_level()) acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'group2', 'level': 'owner'}) {'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level())) self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
......
...@@ -25,11 +25,11 @@ from .views import ( ...@@ -25,11 +25,11 @@ from .views import (
GroupDetailView, GroupList, IndexView, GroupDetailView, GroupList, IndexView,
InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail, InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail,
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete, MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeFlushView, NodeGraphView, NodeList, NodeStatus, NodeDetailView, NodeList, NodeStatus,
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate, NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView, TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDetailView, TransferOwnershipView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmDetailVncTokenView, VmList,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveUserView, GroupRemoveUserView,
GroupRemoveFutureUserView, GroupRemoveFutureUserView,
...@@ -47,7 +47,10 @@ from .views import ( ...@@ -47,7 +47,10 @@ from .views import (
LeaseAclUpdateView, LeaseAclUpdateView,
toggle_template_tutorial, toggle_template_tutorial,
ClientCheck, TokenLogin, ClientCheck, TokenLogin,
VmGraphView, NodeGraphView, NodeListGraphView,
) )
from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops
autocomplete_light.autodiscover() autocomplete_light.autodiscover()
...@@ -75,8 +78,6 @@ urlpatterns = patterns( ...@@ -75,8 +78,6 @@ urlpatterns = patterns(
name="dashboard.views.template-list"), name="dashboard.views.template-list"),
url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(), url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(),
name="dashboard.views.template-delete"), name="dashboard.views.template-delete"),
url(r'^vm/', include('dashboard.vm.urls')),
url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(), url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'), name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(), url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
...@@ -113,8 +114,6 @@ urlpatterns = patterns( ...@@ -113,8 +114,6 @@ urlpatterns = patterns(
name="dashboard.views.delete-node"), name="dashboard.views.delete-node"),
url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.as_view(), url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.as_view(),
name="dashboard.views.status-node"), name="dashboard.views.status-node"),
url(r'^node/flush/(?P<pk>\d+)/$', NodeFlushView.as_view(),
name="dashboard.views.flush-node"),
url(r'^node/create/$', NodeCreate.as_view(), url(r'^node/create/$', NodeCreate.as_view(),
name='dashboard.views.node-create'), name='dashboard.views.node-create'),
...@@ -124,14 +123,18 @@ urlpatterns = patterns( ...@@ -124,14 +123,18 @@ urlpatterns = patterns(
name="dashboard.views.delete-group"), name="dashboard.views.delete-group"),
url(r'^group/list/$', GroupList.as_view(), url(r'^group/list/$', GroupList.as_view(),
name='dashboard.views.group-list'), name='dashboard.views.group-list'),
url((r'^vm/(?P<pk>\d+)/graph/(?P<metric>cpu|memory|network)/' url((r'^vm/(?P<pk>\d+)/graph/(?P<metric>[a-z]+)/'
r'(?P<time>[0-9]{1,2}[hdwy])$'), r'(?P<time>[0-9]{1,2}[hdwy])$'),
VmGraphView.as_view(), VmGraphView.as_view(),
name='dashboard.views.vm-graph'), name='dashboard.views.vm-graph'),
url((r'^node/(?P<pk>\d+)/graph/(?P<metric>cpu|memory|network)/' url((r'^node/(?P<pk>\d+)/graph/(?P<metric>[a-z]+)/'
r'(?P<time>[0-9]{1,2}[hdwy])$'), r'(?P<time>[0-9]{1,2}[hdwy])$'),
NodeGraphView.as_view(), NodeGraphView.as_view(),
name='dashboard.views.node-graph'), name='dashboard.views.node-graph'),
url((r'^node/graph/(?P<metric>[a-z]+)/'
r'(?P<time>[0-9]{1,2}[hdwy])$'),
NodeListGraphView.as_view(),
name='dashboard.views.node-list-graph'),
url(r'^group/(?P<pk>\d+)/$', GroupDetailView.as_view(), url(r'^group/(?P<pk>\d+)/$', GroupDetailView.as_view(),
name='dashboard.views.group-detail'), name='dashboard.views.group-detail'),
url(r'^group/(?P<pk>\d+)/update/$', GroupProfileUpdate.as_view(), url(r'^group/(?P<pk>\d+)/update/$', GroupProfileUpdate.as_view(),
...@@ -213,3 +216,21 @@ urlpatterns = patterns( ...@@ -213,3 +216,21 @@ urlpatterns = patterns(
url(r'^token-login/(?P<token>.*)/$', TokenLogin.as_view(), url(r'^token-login/(?P<token>.*)/$', TokenLogin.as_view(),
name="dashboard.views.token-login"), name="dashboard.views.token-login"),
) )
urlpatterns += patterns(
'',
*(url(r'^vm/(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems())
)
urlpatterns += patterns(
'',
*(url(r'^vm/mass_op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_mass_ops.iteritems())
)
urlpatterns += patterns(
'',
*(url(r'^node/(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in node_ops.iteritems())
)
This source diff could not be displayed because it is too large. You can view the blob instead.
# flake8: noqa
# from .node import Node
# __all__ = [ ]
from group import *
from index import *
from node import *
from store import *
from template import *
from user import *
from util import *
from vm import *
from graph import *
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, unicode_literals
import logging
import requests
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse, Http404
from django.utils.translation import ugettext_lazy as _
from django.views.generic import View
from braces.views import LoginRequiredMixin, SuperuserRequiredMixin
from vm.models import Instance, Node
logger = logging.getLogger(__name__)
def register_graph(metric_cls, graph_name, graphview_cls):
if not hasattr(graphview_cls, 'metrics'):
graphview_cls.metrics = {}
graphview_cls.metrics[graph_name] = metric_cls
class GraphViewBase(LoginRequiredMixin, View):
def create_class(self, cls):
return type(str(cls.__name__ + 'Metric'), (cls, self.base), {})
def get(self, request, pk, metric, time, *args, **kwargs):
graphite_url = settings.GRAPHITE_URL
if graphite_url is None:
raise Http404()
try:
metric = self.metrics[metric]
except KeyError:
raise Http404()
try:
instance = self.get_object(request, pk)
except self.model.DoesNotExist:
raise Http404()
metric = self.create_class(metric)(instance)
return HttpResponse(metric.get_graph(graphite_url, time),
mimetype="image/png")
def get_object(self, request, pk):
instance = self.model.objects.get(id=pk)
if not instance.has_level(request.user, 'user'):
raise PermissionDenied()
return instance
class Metric(object):
cacti_style = True
derivative = False
scale_to_seconds = None
metric_name = None
title = None
label = None
def __init__(self, obj, metric_name=None):
self.obj = obj
self.metric_name = (
metric_name or self.metric_name or self.__class__.__name__.lower())
def get_metric_name(self):
return self.metric_name
def get_label(self):
return self.label or self.get_metric_name()
def get_title(self):
return self.title or self.get_metric_name()
def get_minmax(self):
return (None, None)
def get_target(self):
target = '%s.%s' % (self.obj.metric_prefix, self.get_metric_name())
if self.derivative:
target = 'nonNegativeDerivative(%s)' % target
if self.scale_to_seconds:
target = 'scaleToSeconds(%s, %d)' % (target, self.scale_to_seconds)
target = 'alias(%s, "%s")' % (target, self.get_label())
if self.cacti_style:
target = 'cactiStyle(%s)' % target
return target
def get_graph(self, graphite_url, time, width=500, height=200):
params = {'target': self.get_target(),
'from': '-%s' % time,
'title': self.get_title().encode('UTF-8'),
'width': width,
'height': height}
ymin, ymax = self.get_minmax()
if ymin is not None:
params['yMin'] = ymin
if ymax is not None:
params['yMax'] = ymax
logger.debug('%s %s', graphite_url, params)
response = requests.get('%s/render/' % graphite_url, params=params)
return response.content
class VmMetric(Metric):
def get_title(self):
title = super(VmMetric, self).get_title()
return '%s (%s) - %s' % (self.obj.name, self.obj.vm_name, title)
class NodeMetric(Metric):
def get_title(self):
title = super(NodeMetric, self).get_title()
return '%s (%s) - %s' % (self.obj.name, self.obj.host.hostname, title)
class VmGraphView(GraphViewBase):
model = Instance
base = VmMetric
class NodeGraphView(SuperuserRequiredMixin, GraphViewBase):
model = Node
base = NodeMetric
def get_object(self, request, pk):
return self.model.objects.get(id=pk)
class NodeListGraphView(SuperuserRequiredMixin, GraphViewBase):
model = Node
base = Metric
def get_object(self, request, pk):
return Node.objects.filter(enabled=True)
def get(self, request, metric, time, *args, **kwargs):
return super(NodeListGraphView, self).get(request, None, metric, time)
class Ram(object):
metric_name = "memory.usage"
title = _("RAM usage (%)")
label = _("RAM usage (%)")
def get_minmax(self):
return (0, 105)
register_graph(Ram, 'memory', VmGraphView)
register_graph(Ram, 'memory', NodeGraphView)
class Cpu(object):
metric_name = "cpu.percent"
title = _("CPU usage (%)")
label = _("CPU usage (%)")
def get_minmax(self):
if isinstance(self.obj, Node):
return (0, 105)
else:
return (0, self.obj.num_cores * 100 + 5)
register_graph(Cpu, 'cpu', VmGraphView)
register_graph(Cpu, 'cpu', NodeGraphView)
class VmNetwork(object):
title = _("Network")
def get_minmax(self):
return (0, None)
def get_target(self):
metrics = []
for n in self.obj.interface_set.all():
params = (self.obj.metric_prefix, n.vlan.vid, n.vlan.name)
metrics.append(
'alias(scaleToSeconds(nonNegativeDerivative('
'%s.network.bytes_recv-%s), 10), "out - %s (bits/s)")' % (
params))
metrics.append(
'alias(scaleToSeconds(nonNegativeDerivative('
'%s.network.bytes_sent-%s), 10), "in - %s (bits/s)")' % (
params))
return 'group(%s)' % ','.join(metrics)
register_graph(VmNetwork, 'network', VmGraphView)
class NodeNetwork(object):
title = _("Network")
def get_minmax(self):
return (0, None)
def get_target(self):
return (
'aliasSub(scaleToSeconds(nonNegativeDerivative(%s.network.b*),'
'10), ".*\.bytes_(sent|recv)-([a-zA-Z0-9]+).*", "\\2 \\1")' % (
self.obj.metric_prefix))
register_graph(NodeNetwork, 'network', NodeGraphView)
class NodeVms(object):
metric_name = "vmcount"
title = _("Instance count")
label = _("instance count")
def get_minmax(self):
return (0, None)
register_graph(NodeVms, 'vm', NodeGraphView)
class NodeAllocated(object):
title = _("Allocated memory (bytes)")
def get_target(self):
prefix = self.obj.metric_prefix
if self.obj.online and self.obj.enabled:
ram_size = self.obj.ram_size
else:
ram_size = 0
used = 'alias(%s.memory.used_bytes, "used")' % prefix
allocated = 'alias(%s.memory.allocated, "allocated")' % prefix
max = 'threshold(%d, "max")' % ram_size
return 'cactiStyle(group(%s, %s, %s))' % (used, allocated, max)
def get_minmax(self):
return (0, None)
register_graph(NodeAllocated, 'alloc', NodeGraphView)
class NodeListAllocated(object):
title = _("Allocated memory (bytes)")
def get_target(self):
nodes = self.obj
used = ','.join('%s.memory.used_bytes' % n.metric_prefix
for n in nodes)
allocated = 'alias(sumSeries(%s), "allocated")' % ','.join(
'%s.memory.allocated' % n.metric_prefix for n in nodes)
max = 'threshold(%d, "max")' % sum(
n.ram_size for n in nodes if n.online)
return ('group(aliasSub(aliasByNode(stacked(group(%s)), 1), "$",'
'" (used)"), %s, %s)' % (used, allocated, max))
def get_minmax(self):
return (0, None)
register_graph(NodeListAllocated, 'alloc', NodeListGraphView)
class NodeListVms(object):
title = _("Instance count")
def get_target(self):
vmcount = ','.join('%s.vmcount' % n.metric_prefix for n in self.obj)
return 'group(aliasByNode(stacked(group(%s)), 1))' % vmcount
def get_minmax(self):
return (0, None)
register_graph(NodeListVms, 'vm', NodeListGraphView)
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals, absolute_import
import logging
from django.core.cache import get_cache
from django.conf import settings
from django.contrib.auth.models import Group
from django.views.generic import TemplateView
from braces.views import LoginRequiredMixin
from dashboard.models import GroupProfile
from vm.models import Instance, Node, InstanceTemplate
from ..store_api import Store
logger = logging.getLogger(__name__)
class IndexView(LoginRequiredMixin, TemplateView):
template_name = "dashboard/index.html"
def get_context_data(self, **kwargs):
user = self.request.user
context = super(IndexView, self).get_context_data(**kwargs)
# instances
favs = Instance.objects.filter(favourite__user=self.request.user)
instances = Instance.get_objects_with_level(
'user', user, disregard_superuser=True).filter(destroyed_at=None)
display = list(favs) + list(set(instances) - set(favs))
for d in display:
d.fav = True if d in favs else False
context.update({
'instances': display[:5],
'more_instances': instances.count() - len(instances[:5])
})
running = instances.filter(status='RUNNING')
stopped = instances.exclude(status__in=('RUNNING', 'NOSTATE'))
context.update({
'running_vms': running[:20],
'running_vm_num': running.count(),
'stopped_vm_num': stopped.count()
})
# nodes
if user.is_superuser:
nodes = Node.objects.all()
context.update({
'nodes': nodes[:5],
'more_nodes': nodes.count() - len(nodes[:5]),
'sum_node_num': nodes.count(),
'node_num': {
'running': Node.get_state_count(True, True),
'missing': Node.get_state_count(False, True),
'disabled': Node.get_state_count(True, False),
'offline': Node.get_state_count(False, False)
}
})
# groups
if user.has_module_perms('auth'):
profiles = GroupProfile.get_objects_with_level('operator', user)
groups = Group.objects.filter(groupprofile__in=profiles)
context.update({
'groups': groups[:5],
'more_groups': groups.count() - len(groups[:5]),
})
# template
if user.has_perm('vm.create_template'):
context['templates'] = InstanceTemplate.get_objects_with_level(
'operator', user, disregard_superuser=True).all()[:5]
# toplist
if settings.STORE_URL:
cache_key = "files-%d" % self.request.user.pk
cache = get_cache("default")
files = cache.get(cache_key)
if not files:
try:
store = Store(self.request.user)
toplist = store.toplist()
quota = store.get_quota()
files = {'toplist': toplist, 'quota': quota}
except Exception:
logger.exception("Unable to get tolist for %s",
unicode(self.request.user))
files = {'toplist': []}
cache.set(cache_key, files, 300)
context['files'] = files
else:
context['no_store'] = True
return context
class HelpView(TemplateView):
def get_context_data(self, *args, **kwargs):
ctx = super(HelpView, self).get_context_data(*args, **kwargs)
ctx.update({"saml": hasattr(settings, "SAML_CONFIG"),
"store": settings.STORE_URL})
return ctx
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals, absolute_import
import json
import logging
from os.path import join, normpath, dirname, basename
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.cache import get_cache
from django.core.exceptions import SuspiciousOperation
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from django.shortcuts import redirect, render_to_response, render
from django.template import RequestContext
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_GET, require_POST
from django.views.generic import TemplateView
from braces.views import LoginRequiredMixin
from ..store_api import Store, NoStoreException, NotOkException
logger = logging.getLogger(__name__)
class StoreList(LoginRequiredMixin, TemplateView):
template_name = "dashboard/store/list.html"
def get_context_data(self, **kwargs):
context = super(StoreList, self).get_context_data(**kwargs)
directory = self.request.GET.get("directory", "/")
directory = "/" if not len(directory) else directory
store = Store(self.request.user)
context['root'] = store.list(directory)
context['quota'] = store.get_quota()
context['up_url'] = self.create_up_directory(directory)
context['current'] = directory
context['next_url'] = "%s%s?directory=%s" % (
settings.DJANGO_URL.rstrip("/"),
reverse("dashboard.views.store-list"), directory)
return context
def get(self, *args, **kwargs):
try:
if self.request.is_ajax():
context = self.get_context_data(**kwargs)
return render_to_response(
"dashboard/store/_list-box.html",
RequestContext(self.request, context),
)
else:
return super(StoreList, self).get(*args, **kwargs)
except NoStoreException:
messages.warning(self.request, _("No store."))
except NotOkException:
messages.warning(self.request, _("Store has some problems now."
" Try again later."))
except Exception as e:
logger.critical("Something is wrong with store: %s", unicode(e))
messages.warning(self.request, _("Unknown store error."))
return redirect("/")
def create_up_directory(self, directory):
path = normpath(join('/', directory, '..'))
if not path.endswith("/"):
path += "/"
return path
@require_GET
@login_required
def store_download(request):
path = request.GET.get("path")
try:
url = Store(request.user).request_download(path)
except Exception:
messages.error(request, _("Something went wrong during download."))
logger.exception("Unable to download, "
"maybe it is already deleted")
return redirect(reverse("dashboard.views.store-list"))
return redirect(url)
@require_GET
@login_required
def store_upload(request):
directory = request.GET.get("directory", "/")
try:
action = Store(request.user).request_upload(directory)
except Exception:
logger.exception("Unable to upload")
messages.error(request, _("Unable to upload file."))
return redirect("/")
next_url = "%s%s?directory=%s" % (
settings.DJANGO_URL.rstrip("/"),
reverse("dashboard.views.store-list"), directory)
return render(request, "dashboard/store/upload.html",
{'directory': directory, 'action': action,
'next_url': next_url})
@require_GET
@login_required
def store_get_upload_url(request):
current_dir = request.GET.get("current_dir")
try:
url = Store(request.user).request_upload(current_dir)
except Exception:
logger.exception("Unable to upload")
messages.error(request, _("Unable to upload file."))
return redirect("/")
return HttpResponse(
json.dumps({'url': url}), content_type="application/json")
class StoreRemove(LoginRequiredMixin, TemplateView):
template_name = "dashboard/store/remove.html"
def get_context_data(self, *args, **kwargs):
context = super(StoreRemove, self).get_context_data(*args, **kwargs)
path = self.request.GET.get("path", "/")
if path == "/":
SuspiciousOperation()
context['path'] = path
context['is_dir'] = path.endswith("/")
if context['is_dir']:
context['directory'] = path
else:
context['directory'] = dirname(path)
context['name'] = basename(path)
return context
def get(self, *args, **kwargs):
try:
return super(StoreRemove, self).get(*args, **kwargs)
except NoStoreException:
return redirect("/")
def post(self, *args, **kwargs):
path = self.request.POST.get("path")
try:
Store(self.request.user).remove(path)
except Exception:
logger.exception("Unable to remove %s", path)
messages.error(self.request, _("Unable to remove %s.") % path)
return redirect("%s?directory=%s" % (
reverse("dashboard.views.store-list"),
dirname(dirname(path)),
))
@require_POST
@login_required
def store_new_directory(request):
path = request.POST.get("path")
name = request.POST.get("name")
try:
Store(request.user).new_folder(join(path, name))
except Exception:
logger.exception("Unable to create folder %s in %s for %s",
name, path, unicode(request.user))
messages.error(request, _("Unable to create folder."))
return redirect("%s?directory=%s" % (
reverse("dashboard.views.store-list"), path))
@require_POST
@login_required
def store_refresh_toplist(request):
cache_key = "files-%d" % request.user.pk
cache = get_cache("default")
try:
store = Store(request.user)
toplist = store.toplist()
quota = store.get_quota()
files = {'toplist': toplist, 'quota': quota}
except Exception:
logger.exception("Can't get toplist of %s", unicode(request.user))
files = {'toplist': []}
cache.set(cache_key, files, 300)
return redirect(reverse("dashboard.index"))
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django.conf.urls import patterns, url
from ..views import vm_ops, vm_mass_ops
urlpatterns = patterns(
'',
*(url(r'^(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems())
)
urlpatterns += patterns(
'',
*(url(r'^mass_op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_mass_ops.iteritems())
)
...@@ -119,11 +119,13 @@ def stop_portal(test=False): ...@@ -119,11 +119,13 @@ def stop_portal(test=False):
@roles('node') @roles('node')
def update_node(): def update_node():
"Update and restart nodes" "Update and restart nodes"
with _stopped("node", "agentdriver"): with _stopped("node", "agentdriver", "monitor-client"):
pull("~/vmdriver") pull("~/vmdriver")
pip("vmdriver", "~/vmdriver/requirements/production.txt") pip("vmdriver", "~/vmdriver/requirements/production.txt")
pull("~/agentdriver") pull("~/agentdriver")
pip("agentdriver", "~/agentdriver/requirements.txt") pip("agentdriver", "~/agentdriver/requirements.txt")
pull("~/monitor-client")
pip("monitor-client", "~/monitor-client/requirements.txt")
@parallel @parallel
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -6,7 +6,7 @@ msgid "" ...@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-09-03 18:49+0200\n" "POT-Creation-Date: 2014-09-24 12:19+0200\n"
"PO-Revision-Date: 2014-09-03 12:51+0200\n" "PO-Revision-Date: 2014-09-03 12:51+0200\n"
"Last-Translator: Mate Ory <ory.mate@ik.bme.hu>\n" "Last-Translator: Mate Ory <ory.mate@ik.bme.hu>\n"
"Language-Team: Hungarian <cloud@ik.bme.hu>\n" "Language-Team: Hungarian <cloud@ik.bme.hu>\n"
...@@ -38,9 +38,9 @@ msgstr "" ...@@ -38,9 +38,9 @@ msgstr ""
msgid "Select an option to proceed!" msgid "Select an option to proceed!"
msgstr "Válasszon a folytatáshoz." msgstr "Válasszon a folytatáshoz."
#: dashboard/static/dashboard/dashboard.js:258 #: dashboard/static/dashboard/dashboard.js:259
#: dashboard/static/dashboard/dashboard.js:306 #: dashboard/static/dashboard/dashboard.js:307
#: dashboard/static/dashboard/dashboard.js:316 #: dashboard/static/dashboard/dashboard.js:317
#: static_collected/all.047675ebf594.js:3633 #: static_collected/all.047675ebf594.js:3633
#: static_collected/all.047675ebf594.js:3681 #: static_collected/all.047675ebf594.js:3681
#: static_collected/all.047675ebf594.js:3691 #: static_collected/all.047675ebf594.js:3691
......
...@@ -61,6 +61,12 @@ celery.conf.update( ...@@ -61,6 +61,12 @@ celery.conf.update(
'schedule': timedelta(seconds=30), 'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.monitor'} 'options': {'queue': 'localhost.monitor'}
}, },
'monitor.allocated_memory': {
'task': 'monitor.tasks.local_periodic_tasks.'
'allocated_memory',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.monitor'}
},
} }
) )
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
from logging import getLogger from logging import getLogger
from django.db.models import Sum
from django.utils.translation import ugettext_noop from django.utils.translation import ugettext_noop
from common.models import HumanReadableException from common.models import HumanReadableException
...@@ -48,7 +47,7 @@ class NotEnoughMemoryException(SchedulerError): ...@@ -48,7 +47,7 @@ class NotEnoughMemoryException(SchedulerError):
class TraitsUnsatisfiableException(SchedulerError): class TraitsUnsatisfiableException(SchedulerError):
message = ugettext_noop( message = ugettext_noop(
"No node can satisfy the required traits of the " "No node can satisfy the required traits of the "
"new vitual machine currently.") "new virtual machine currently.")
def select_node(instance, nodes): def select_node(instance, nodes):
...@@ -56,7 +55,7 @@ def select_node(instance, nodes): ...@@ -56,7 +55,7 @@ def select_node(instance, nodes):
''' '''
# check required traits # check required traits
nodes = [n for n in nodes nodes = [n for n in nodes
if n.enabled and n.online if n.schedule_enabled and n.online
and has_traits(instance.req_traits.all(), n)] and has_traits(instance.req_traits.all(), n)]
if not nodes: if not nodes:
logger.warning('select_node: no usable node for %s', unicode(instance)) logger.warning('select_node: no usable node for %s', unicode(instance))
...@@ -95,8 +94,7 @@ def has_enough_ram(ram_size, node): ...@@ -95,8 +94,7 @@ def has_enough_ram(ram_size, node):
unused = total - used unused = total - used
overcommit = node.ram_size_with_overcommit overcommit = node.ram_size_with_overcommit
reserved = (node.instance_set.aggregate( reserved = node.allocated_ram
r=Sum('ram_size'))['r'] or 0) * 1024 * 1024
free = overcommit - reserved free = overcommit - reserved
retval = ram_size < unused and ram_size < free retval = ram_size < unused and ram_size < free
......
...@@ -107,3 +107,19 @@ def instance_per_template(): ...@@ -107,3 +107,19 @@ def instance_per_template():
time())) time()))
Client().send(metrics) Client().send(metrics)
@celery.task(ignore_result=True)
def allocated_memory():
graphite_string = lambda hostname, val, time: (
"circle.%s.memory.allocated %d %s" % (
hostname, val, time)
)
metrics = []
for n in Node.objects.all():
print n.allocated_ram
metrics.append(graphite_string(
n.host.hostname, n.allocated_ram, time()))
Client().send(metrics)
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
{% load staticfiles %} {% load staticfiles %}
{% get_current_language as lang %} {% get_current_language as lang %}
{% block title-site %}{% trans "Network" %} | CIRCLE{% endblock %}
{% block extra_link %} {% block extra_link %}
<link href='//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 href="{% static "network/network.css" %}" rel="stylesheet"> <link href="{% static "network/network.css" %}" rel="stylesheet">
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{% trans "Create" %} | {% trans "blacklist" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h2>{% trans "Create a blacklist item" %}</h2> <h2>{% trans "Create a blacklist item" %}</h2>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{{ form.ipv4.value }} - {{ form.type.value }} | {% trans "blacklist" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.blacklist_delete" pk=blacklist_pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this blaclist item" %}</a> <a href="{% url "network.blacklist_delete" pk=blacklist_pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this blaclist item" %}</a>
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
{% load l10n %} {% load l10n %}
{% load staticfiles %} {% load staticfiles %}
{% block title-page %}{% trans "Blacklist" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.blacklist_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new blacklist item" %}</a> <a href="{% url "network.blacklist_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new blacklist item" %}</a>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{% trans "Create" %} | {% trans "domain" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h2>{% trans "Create a new domain" %}<small></small></h2> <h2>{% trans "Create a new domain" %}<small></small></h2>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{{ form.name.value }} | {% trans "domain" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.domain_delete" pk=domain_pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this domain" %}</a> <a href="{% url "network.domain_delete" pk=domain_pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this domain" %}</a>
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
{% load l10n %} {% load l10n %}
{% load staticfiles %} {% load staticfiles %}
{% block title-page %}{% trans "Domains" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.domain_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new domain" %}</a> <a href="{% url "network.domain_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new domain" %}</a>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{% trans "Create" %} | {% trans "host group" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h2>{% trans "Create a new host group" %}</h2> <h2>{% trans "Create a new host group" %}</h2>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{{ form.name.value }} | {% trans "host group" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.group_delete" pk=group.pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this group" %}</a> <a href="{% url "network.group_delete" pk=group.pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this group" %}</a>
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
{% load l10n %} {% load l10n %}
{% load staticfiles %} {% load staticfiles %}
{% block title-page %}{% trans "Host groups" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.group_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new host group" %}</a> <a href="{% url "network.group_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new host group" %}</a>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{% trans "Create" %} | {% trans "host" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h2>{% trans "Create a new host" %}</h2> <h2>{% trans "Create a new host" %}</h2>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{{ form.hostname.value }} | {% trans "host" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.host_delete" pk=host_pk%}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this host" %}</a> <a href="{% url "network.host_delete" pk=host_pk%}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this host" %}</a>
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
{% load l10n %} {% load l10n %}
{% load staticfiles %} {% load staticfiles %}
{% block title-page %}{% trans "Hosts" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.host_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new host" %}</a> <a href="{% url "network.host_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new host" %}</a>
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
{% load l10n %} {% load l10n %}
{% load staticfiles %} {% load staticfiles %}
{% block title-page %}{% trans "Index" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{% trans "Create" %} | {% trans "record" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h2>{% trans "Create a new record" %}</h2> <h2>{% trans "Create a new record" %}</h2>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{{ record.fqdn }} | {% trans "record" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.record_delete" pk=record_pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this record" %}</a> <a href="{% url "network.record_delete" pk=record_pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this record" %}</a>
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
{% load l10n %} {% load l10n %}
{% load staticfiles %} {% load staticfiles %}
{% block title-page %}{% trans "Records" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.record_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new record" %}</a> <a href="{% url "network.record_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new record" %}</a>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{% trans "Create" %} | {% trans "rule" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h2>{% trans "Create a new rule" %}</h2> <h2>{% trans "Create a new rule" %}</h2>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{{ rule.pk }} | {% trans "rule" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.rule_delete" pk=rule.pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this rule" %}</a> <a href="{% url "network.rule_delete" pk=rule.pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this rule" %}</a>
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
{% load l10n %} {% load l10n %}
{% load staticfiles %} {% load staticfiles %}
{% block title-page %}{% trans "Rules" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.rule_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new rule" %}</a> <a href="{% url "network.rule_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new rule" %}</a>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{% trans "Create" %} | {% trans "switch port" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h2>{% trans "Create a new switch port" %}</h2> <h2>{% trans "Create a new switch port" %}</h2>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{{ switch_port_pk }} | {% trans "switch port" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.switch_port_delete" pk=switch_port_pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this switchport" %}</a> <a href="{% url "network.switch_port_delete" pk=switch_port_pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this switchport" %}</a>
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
{% load l10n %} {% load l10n %}
{% load staticfiles %} {% load staticfiles %}
{% block title-page %}{% trans "Switch ports" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.switch_port_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new switch port" %}</a> <a href="{% url "network.switch_port_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new switch port" %}</a>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{% trans "Create" %} | {% trans "vlan" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h2>{% trans "Create a new vlan" %}</h2> <h2>{% trans "Create a new vlan" %}</h2>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{{ form.name.value }} | {% trans "vlan" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.vlan_delete" vid=vlan_vid %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this vlan" %}</a> <a href="{% url "network.vlan_delete" vid=vlan_vid %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this vlan" %}</a>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{% trans "Create" %} | {% trans "vlan group" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h2>{% trans "Create a new vlan group" %}</h2> <h2>{% trans "Create a new vlan group" %}</h2>
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
{% load staticfiles %} {% load staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{{ form.name.value }} | {% trans "vlan group" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.vlan_group_delete" pk=vlangroup_pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this group" %}</a> <a href="{% url "network.vlan_group_delete" pk=vlangroup_pk %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this group" %}</a>
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
{% load l10n %} {% load l10n %}
{% load staticfiles %} {% load staticfiles %}
{% block title-page %}{% trans "Vlan groups" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.vlan_group_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new vlan group" %}</a> <a href="{% url "network.vlan_group_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new vlan group" %}</a>
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
{% load l10n %} {% load l10n %}
{% load staticfiles %} {% load staticfiles %}
{% block title-page %}{% trans "Vlans" %}{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.vlan_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new vlan" %}</a> <a href="{% url "network.vlan_create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new vlan" %}</a>
......
...@@ -657,7 +657,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -657,7 +657,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
context = super(VlanDetail, self).get_context_data(**kwargs) context = super(VlanDetail, self).get_context_data(**kwargs)
q = Host.objects.filter(interface__in=Interface.objects.filter( q = Host.objects.filter(interface__in=Interface.objects.filter(
vlan=self.object, instance__destroyed_at=None vlan=self.object
)) ))
context['host_list'] = SmallHostTable(q) context['host_list'] = SmallHostTable(q)
......
...@@ -104,7 +104,9 @@ class Disk(TimeStampedModel): ...@@ -104,7 +104,9 @@ class Disk(TimeStampedModel):
verbose_name_plural = _('disks') verbose_name_plural = _('disks')
permissions = ( permissions = (
('create_empty_disk', _('Can create an empty disk.')), ('create_empty_disk', _('Can create an empty disk.')),
('download_disk', _('Can download a disk.'))) ('download_disk', _('Can download a disk.')),
('resize_disk', _('Can resize a disk.'))
)
class DiskError(HumanReadableException): class DiskError(HumanReadableException):
admin_message = None admin_message = None
...@@ -474,7 +476,8 @@ class Disk(TimeStampedModel): ...@@ -474,7 +476,8 @@ class Disk(TimeStampedModel):
queue_name = self.get_remote_queue_name("storage", priority="slow") queue_name = self.get_remote_queue_name("storage", priority="slow")
remote = storage_tasks.merge.apply_async(kwargs={ remote = storage_tasks.merge.apply_async(kwargs={
"old_json": self.get_disk_desc(), "old_json": self.get_disk_desc(),
"new_json": disk.get_disk_desc()}, "new_json": disk.get_disk_desc(),
"parent_id": task.request.id},
queue=queue_name queue=queue_name
) # Timeout ) # Timeout
while True: while True:
......
...@@ -7,7 +7,6 @@ from .common import BaseResourceConfigModel ...@@ -7,7 +7,6 @@ from .common import BaseResourceConfigModel
from .common import Lease from .common import Lease
from .common import NamedBaseResourceConfig from .common import NamedBaseResourceConfig
from .common import Trait from .common import Trait
from .instance import InstanceActiveManager
from .instance import VirtualMachineDescModel from .instance import VirtualMachineDescModel
from .instance import InstanceTemplate from .instance import InstanceTemplate
from .instance import Instance from .instance import Instance
...@@ -19,7 +18,7 @@ from .network import Interface ...@@ -19,7 +18,7 @@ from .network import Interface
from .node import Node from .node import Node
__all__ = [ __all__ = [
'InstanceActivity', 'InstanceActiveManager', 'BaseResourceConfigModel', 'InstanceActivity', 'BaseResourceConfigModel',
'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate', 'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate',
'Instance', 'instance_activity', 'post_state_changed', 'pre_state_changed', 'Instance', 'instance_activity', 'post_state_changed', 'pre_state_changed',
'InterfaceTemplate', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease', 'InterfaceTemplate', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease',
......
...@@ -24,10 +24,10 @@ import requests ...@@ -24,10 +24,10 @@ import requests
from django.conf import settings from django.conf import settings
from django.db.models import ( from django.db.models import (
CharField, IntegerField, ForeignKey, BooleanField, ManyToManyField, CharField, IntegerField, ForeignKey, BooleanField, ManyToManyField,
FloatField, permalink, FloatField, permalink, Sum
) )
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.utils.translation import ugettext_lazy as _
from celery.exceptions import TimeoutError from celery.exceptions import TimeoutError
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
...@@ -37,7 +37,7 @@ from common.models import method_cache, WorkerNotFound, HumanSortField ...@@ -37,7 +37,7 @@ from common.models import method_cache, WorkerNotFound, HumanSortField
from common.operations import OperatedMixin from common.operations import OperatedMixin
from firewall.models import Host from firewall.models import Host
from ..tasks import vm_tasks from ..tasks import vm_tasks
from .activity import node_activity, NodeActivity from .activity import NodeActivity
from .common import Trait from .common import Trait
...@@ -72,6 +72,11 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -72,6 +72,11 @@ class Node(OperatedMixin, TimeStampedModel):
enabled = BooleanField(verbose_name=_('enabled'), default=False, enabled = BooleanField(verbose_name=_('enabled'), default=False,
help_text=_('Indicates whether the node can ' help_text=_('Indicates whether the node can '
'be used for hosting.')) 'be used for hosting.'))
schedule_enabled = BooleanField(verbose_name=_('schedule enabled'),
default=False, help_text=_(
'Indicates whether a vm can be '
'automatically scheduled to this '
'node.'))
traits = ManyToManyField(Trait, blank=True, traits = ManyToManyField(Trait, blank=True,
help_text=_("Declared traits."), help_text=_("Declared traits."),
verbose_name=_('traits')) verbose_name=_('traits'))
...@@ -109,13 +114,18 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -109,13 +114,18 @@ class Node(OperatedMixin, TimeStampedModel):
def get_info(self): def get_info(self):
return self.remote_query(vm_tasks.get_info, return self.remote_query(vm_tasks.get_info,
priority='fast', priority='fast',
default={'core_num': '', default={'core_num': 0,
'ram_size': '0', 'ram_size': 0,
'architecture': ''}) 'architecture': ''})
info = property(get_info) info = property(get_info)
@property @property
def allocated_ram(self):
return (self.instance_set.aggregate(
r=Sum('ram_size'))['r'] or 0) * 1024 * 1024
@property
def ram_size(self): def ram_size(self):
warn('Use Node.info["ram_size"]', DeprecationWarning) warn('Use Node.info["ram_size"]', DeprecationWarning)
return self.info['ram_size'] return self.info['ram_size']
...@@ -125,46 +135,30 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -125,46 +135,30 @@ class Node(OperatedMixin, TimeStampedModel):
warn('Use Node.info["core_num"]', DeprecationWarning) warn('Use Node.info["core_num"]', DeprecationWarning)
return self.info['core_num'] return self.info['core_num']
STATES = {False: {False: ('OFFLINE', _('offline')), STATES = {None: ({True: ('MISSING', _('missing')),
True: ('DISABLED', _('disabled'))}, False: ('OFFLINE', _('offline'))}),
True: {False: ('MISSING', _('missing')), False: {False: ('DISABLED', _('disabled'))},
True: ('ONLINE', _('online'))}} True: {False: ('PASSIVE', _('passive')),
True: ('ACTIVE', _('active'))}}
def get_state(self): def _get_state(self):
"""The state combined of online and enabled attributes. """The state tuple based on online and enabled attributes.
""" """
return self.STATES[self.enabled][self.online][0] if self.online:
return self.STATES[self.enabled][self.schedule_enabled]
state = property(get_state) else:
return self.STATES[None][self.enabled]
def get_status_display(self): def get_status_display(self):
return self.STATES[self.enabled][self.online][1] return self._get_state()[1]
def disable(self, user=None, base_activity=None): def get_state(self):
''' Disable the node.''' return self._get_state()[0]
if self.enabled:
if base_activity: state = property(get_state)
act_ctx = base_activity.sub_activity(
'disable', readable_name=ugettext_noop("disable node"))
else:
act_ctx = node_activity(
'disable', node=self, user=user,
readable_name=ugettext_noop("disable node"))
with act_ctx:
self.enabled = False
self.save()
def enable(self, user=None, base_activity=None): def enable(self, user=None, base_activity=None):
''' Enable the node. ''' raise NotImplementedError("Use activate or passivate instead.")
if self.enabled is not True:
if base_activity:
act_ctx = base_activity.sub_activity('enable')
else:
act_ctx = node_activity('enable', node=self, user=user)
with act_ctx:
self.enabled = True
self.save()
self.get_info(invalidate_cache=True)
@property @property
@node_available @node_available
...@@ -259,7 +253,7 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -259,7 +253,7 @@ class Node(OperatedMixin, TimeStampedModel):
@node_available @node_available
@method_cache(10) @method_cache(10)
def monitor_info(self): def monitor_info(self):
metrics = ('cpu.usage', 'memory.usage') metrics = ('cpu.percent', 'memory.usage')
prefix = 'circle.%s.' % self.host.hostname prefix = 'circle.%s.' % self.host.hostname
params = [('target', '%s%s' % (prefix, metric)) params = [('target', '%s%s' % (prefix, metric))
for metric in metrics] for metric in metrics]
...@@ -295,7 +289,7 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -295,7 +289,7 @@ class Node(OperatedMixin, TimeStampedModel):
@property @property
@node_available @node_available
def cpu_usage(self): def cpu_usage(self):
return self.monitor_info.get('cpu.usage') / 100 return self.monitor_info.get('cpu.percent') / 100
@property @property
@node_available @node_available
...@@ -309,10 +303,11 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -309,10 +303,11 @@ class Node(OperatedMixin, TimeStampedModel):
def get_status_icon(self): def get_status_icon(self):
return { return {
'OFFLINE': 'fa-minus-circle', 'DISABLED': 'fa-times-circle-o',
'DISABLED': 'fa-moon-o', 'OFFLINE': 'fa-times-circle',
'MISSING': 'fa-warning', 'MISSING': 'fa-warning',
'ONLINE': 'fa-play-circle'}.get(self.get_state(), 'PASSIVE': 'fa-play-circle-o',
'ACTIVE': 'fa-play-circle'}.get(self.get_state(),
'fa-question-circle') 'fa-question-circle')
def get_status_label(self): def get_status_label(self):
...@@ -354,7 +349,8 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -354,7 +349,8 @@ class Node(OperatedMixin, TimeStampedModel):
logger.info('Node %s update: instance %s missing from ' logger.info('Node %s update: instance %s missing from '
'libvirt', self, i['id']) 'libvirt', self, i['id'])
# Set state to STOPPED when instance is missing # Set state to STOPPED when instance is missing
self.instance_set.get(id=i['id']).vm_state_changed('STOPPED') self.instance_set.get(id=i['id']).vm_state_changed(
'STOPPED', None)
else: else:
if d != i['state']: if d != i['state']:
logger.info('Node %s update: instance %s state changed ' logger.info('Node %s update: instance %s state changed '
...@@ -363,9 +359,11 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -363,9 +359,11 @@ class Node(OperatedMixin, TimeStampedModel):
self.instance_set.get(id=i['id']).vm_state_changed(d) self.instance_set.get(id=i['id']).vm_state_changed(d)
del domains[i['id']] del domains[i['id']]
for i in domains.keys(): for id, state in domains.iteritems():
logger.info('Node %s update: domain %s in libvirt but not in db.', from .instance import Instance
self, i) logger.error('Node %s update: domain %s in libvirt but not in db.',
self, id)
Instance.objects.get(id=id).vm_state_changed(state, self)
@classmethod @classmethod
def get_state_count(cls, online, enabled): def get_state_count(cls, online, enabled):
...@@ -376,3 +374,12 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -376,3 +374,12 @@ class Node(OperatedMixin, TimeStampedModel):
@permalink @permalink
def get_absolute_url(self): def get_absolute_url(self):
return ('dashboard.views.node-detail', None, {'pk': self.id}) return ('dashboard.views.node-detail', None, {'pk': self.id})
def save(self, *args, **kwargs):
if not self.enabled:
self.schedule_enabled = False
super(Node, self).save(*args, **kwargs)
@property
def metric_prefix(self):
return 'circle.%s' % self.host.hostname
...@@ -27,6 +27,7 @@ from base64 import encodestring ...@@ -27,6 +27,7 @@ from base64 import encodestring
from StringIO import StringIO from StringIO import StringIO
from tarfile import TarFile, TarInfo from tarfile import TarFile, TarInfo
from django.conf import settings from django.conf import settings
from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_noop from django.utils.translation import ugettext_noop
from celery.result import TimeoutError from celery.result import TimeoutError
...@@ -97,8 +98,9 @@ def agent_started(vm, version=None): ...@@ -97,8 +98,9 @@ def agent_started(vm, version=None):
pass pass
for i in InstanceActivity.objects.filter( for i in InstanceActivity.objects.filter(
instance=instance, activity_code__endswith='.os_boot', (Q(activity_code__endswith='.os_boot') |
finished__isnull=True): Q(activity_code__endswith='.agent_wait')),
instance=instance, finished__isnull=True):
i.finish(True) i.finish(True)
if version and version != settings.AGENT_VERSION: if version and version != settings.AGENT_VERSION:
...@@ -107,6 +109,8 @@ def agent_started(vm, version=None): ...@@ -107,6 +109,8 @@ def agent_started(vm, version=None):
except TimeoutError: except TimeoutError:
pass pass
else: else:
act.sub_activity('agent_wait', readable_name=ugettext_noop(
"wait agent restarting"), interruptible=True)
return # agent is going to restart return # agent is going to restart
if not initialized: if not initialized:
......
...@@ -132,6 +132,11 @@ def migrate(params): ...@@ -132,6 +132,11 @@ def migrate(params):
pass pass
@celery.task(name='vmdriver.resize_disk')
def resize_disk(params):
pass
@celery.task(name='vmdriver.domain_info') @celery.task(name='vmdriver.domain_info')
def domain_info(params): def domain_info(params):
pass pass
......
...@@ -29,7 +29,8 @@ from ..models import ( ...@@ -29,7 +29,8 @@ from ..models import (
) )
from ..models.instance import find_unused_port, ActivityInProgressError from ..models.instance import find_unused_port, ActivityInProgressError
from ..operations import ( from ..operations import (
DeployOperation, DestroyOperation, FlushOperation, MigrateOperation, RemoteOperationMixin, DeployOperation, DestroyOperation, FlushOperation,
MigrateOperation,
) )
...@@ -89,7 +90,7 @@ class InstanceTestCase(TestCase): ...@@ -89,7 +90,7 @@ class InstanceTestCase(TestCase):
self.assertFalse(inst.save.called) self.assertFalse(inst.save.called)
def test_destroy_sets_destroyed(self): def test_destroy_sets_destroyed(self):
inst = Mock(destroyed_at=None, spec=Instance, inst = Mock(destroyed_at=None, spec=Instance, _delete_vm=Mock(),
InstanceDestroyedError=Instance.InstanceDestroyedError) InstanceDestroyedError=Instance.InstanceDestroyedError)
inst.node = MagicMock(spec=Node) inst.node = MagicMock(spec=Node)
inst.disks.all.return_value = [] inst.disks.all.return_value = []
...@@ -105,15 +106,15 @@ class InstanceTestCase(TestCase): ...@@ -105,15 +106,15 @@ class InstanceTestCase(TestCase):
inst.node = MagicMock(spec=Node) inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING' inst.status = 'RUNNING'
migrate_op = MigrateOperation(inst) migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr: with patch('vm.operations.vm_tasks.migrate') as migr, \
patch.object(RemoteOperationMixin, "_operation"):
act = MagicMock() act = MagicMock()
with patch.object(MigrateOperation, 'create_activity', with patch.object(MigrateOperation, 'create_activity',
return_value=act): return_value=act):
migrate_op(system=True) migrate_op(system=True)
migr.apply_async.assert_called() migr.apply_async.assert_called()
self.assertIn(call.sub_activity( inst.allocate_node.assert_called()
u'scheduling', readable_name=u'schedule'), act.mock_calls)
inst.select_node.assert_called() inst.select_node.assert_called()
def test_migrate_wo_scheduling(self): def test_migrate_wo_scheduling(self):
...@@ -122,7 +123,8 @@ class InstanceTestCase(TestCase): ...@@ -122,7 +123,8 @@ class InstanceTestCase(TestCase):
inst.node = MagicMock(spec=Node) inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING' inst.status = 'RUNNING'
migrate_op = MigrateOperation(inst) migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr: with patch('vm.operations.vm_tasks.migrate') as migr, \
patch.object(RemoteOperationMixin, "_operation"):
inst.select_node.side_effect = AssertionError inst.select_node.side_effect = AssertionError
act = MagicMock() act = MagicMock()
with patch.object(MigrateOperation, 'create_activity', with patch.object(MigrateOperation, 'create_activity',
...@@ -130,7 +132,7 @@ class InstanceTestCase(TestCase): ...@@ -130,7 +132,7 @@ class InstanceTestCase(TestCase):
migrate_op(to_node=inst.node, system=True) migrate_op(to_node=inst.node, system=True)
migr.apply_async.assert_called() migr.apply_async.assert_called()
self.assertNotIn(call.sub_activity(u'scheduling'), act.mock_calls) inst.allocate_node.assert_called()
def test_migrate_with_error(self): def test_migrate_with_error(self):
inst = Mock(destroyed_at=None, spec=Instance) inst = Mock(destroyed_at=None, spec=Instance)
...@@ -139,20 +141,21 @@ class InstanceTestCase(TestCase): ...@@ -139,20 +141,21 @@ class InstanceTestCase(TestCase):
inst.status = 'RUNNING' inst.status = 'RUNNING'
e = Exception('abc') e = Exception('abc')
setattr(e, 'libvirtError', '') setattr(e, 'libvirtError', '')
inst.migrate_vm.side_effect = e
migrate_op = MigrateOperation(inst) migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr: migrate_op.rollback = Mock()
with patch('vm.operations.vm_tasks.migrate') as migr, \
patch.object(RemoteOperationMixin, '_operation') as remop:
act = MagicMock() act = MagicMock()
remop.side_effect = e
with patch.object(MigrateOperation, 'create_activity', with patch.object(MigrateOperation, 'create_activity',
return_value=act): return_value=act):
self.assertRaises(Exception, migrate_op, system=True) self.assertRaises(Exception, migrate_op, system=True)
remop.assert_called()
migr.apply_async.assert_called() migr.apply_async.assert_called()
self.assertIn(call.sub_activity( self.assertIn(call.sub_activity(
u'scheduling', readable_name=u'schedule'), act.mock_calls) u'scheduling', readable_name=u'schedule'), act.mock_calls)
self.assertIn(call.sub_activity( migrate_op.rollback.assert_called()
u'rollback_net', readable_name=u'redeploy network (rollback)'),
act.mock_calls)
inst.select_node.assert_called() inst.select_node.assert_called()
def test_status_icon(self): def test_status_icon(self):
...@@ -208,8 +211,10 @@ class NodeTestCase(TestCase): ...@@ -208,8 +211,10 @@ class NodeTestCase(TestCase):
node = Mock(spec=Node) node = Mock(spec=Node)
node.online = True node.online = True
node.enabled = True node.enabled = True
node.schedule_enabled = True
node.STATES = Node.STATES node.STATES = Node.STATES
self.assertEqual(Node.get_state(node), "ONLINE") node._get_state = lambda: Node._get_state(node)
self.assertEqual(Node.get_state(node), "ACTIVE")
assert isinstance(Node.get_status_display(node), _("x").__class__) assert isinstance(Node.get_status_display(node), _("x").__class__)
...@@ -351,70 +356,45 @@ class InstanceActivityTestCase(TestCase): ...@@ -351,70 +356,45 @@ class InstanceActivityTestCase(TestCase):
self.assertTrue(InstanceActivity.is_abortable_for(iaobj, su)) self.assertTrue(InstanceActivity.is_abortable_for(iaobj, su))
def test_disable_enabled(self): def test_disable_enabled(self):
node = MagicMock(spec=Node, enabled=True) node = MagicMock(spec=Node, enabled=True, online=True)
with patch('vm.models.node.node_activity') as nac: node.instance_set.exists.return_value = False
na = MagicMock() Node._ops['disable'](node).check_precond()
nac.return_value = na
na.__enter__.return_value = MagicMock()
Node.disable(node)
self.assertFalse(node.enabled)
node.save.assert_called_once()
na.assert_called()
def test_disable_disabled(self): def test_disable_disabled(self):
node = MagicMock(spec=Node, enabled=False) node = MagicMock(spec=Node, enabled=False)
with patch('vm.models.node.node_activity') as nac: with self.assertRaises(Exception):
na = MagicMock() Node._ops['disable'](node).check_precond()
na.__enter__.side_effect = AssertionError
nac.return_value = na
Node.disable(node)
self.assertFalse(node.enabled)
def test_disable_enabled_sub(self):
node = MagicMock(spec=Node, enabled=True)
act = MagicMock()
subact = MagicMock()
act.sub_activity.return_value = subact
Node.disable(node, base_activity=act)
self.assertFalse(node.enabled)
subact.__enter__.assert_called()
def test_flush(self): def test_flush(self):
insts = [MagicMock(spec=Instance, migrate=MagicMock()), insts = [MagicMock(spec=Instance, migrate=MagicMock()),
MagicMock(spec=Instance, migrate=MagicMock())] MagicMock(spec=Instance, migrate=MagicMock())]
insts[0].name = insts[1].name = "x" insts[0].name = insts[1].name = "x"
node = MagicMock(spec=Node, enabled=True) node = MagicMock(spec=Node, enabled=True, schedule_enabled=True)
node.instance_set.all.return_value = insts node.instance_set.all.return_value = insts
user = MagicMock(spec=User) user = MagicMock(spec=User)
user.is_superuser = MagicMock(return_value=True) user.is_superuser = MagicMock(return_value=True)
flush_op = FlushOperation(node) with patch.object(FlushOperation, 'create_activity') as create_act, \
patch.object(
with patch.object(FlushOperation, 'create_activity') as create_act: Node._ops['passivate'], 'create_activity') as create_act2:
act = create_act.return_value = MagicMock() FlushOperation(node)(user=user)
node.schedule_enabled = True
flush_op(user=user)
create_act.assert_called() create_act.assert_called()
node.disable.assert_called_with(user, act) create_act2.assert_called()
for i in insts: for i in insts:
i.migrate.assert_called() i.migrate.assert_called()
user.is_superuser.assert_called() user.is_superuser.assert_called()
def test_flush_disabled_wo_user(self): def test_flush_disabled_wo_user(self):
insts = [MagicMock(spec=Instance, migrate=MagicMock()), insts = [MagicMock(spec=Instance, migrate=MagicMock()),
MagicMock(spec=Instance, migrate=MagicMock())] MagicMock(spec=Instance, migrate=MagicMock())]
insts[0].name = insts[1].name = "x" insts[0].name = insts[1].name = "x"
node = MagicMock(spec=Node, enabled=False) node = MagicMock(spec=Node, enabled=False, schedule_enabled=False)
node.instance_set.all.return_value = insts node.instance_set.all.return_value = insts
flush_op = FlushOperation(node) flush_op = FlushOperation(node)
with patch.object(FlushOperation, 'create_activity') as create_act: with patch.object(FlushOperation, 'create_activity') as create_act:
act = create_act.return_value = MagicMock() create_act.return_value = MagicMock()
flush_op(system=True) flush_op(system=True)
create_act.assert_called() create_act.assert_called()
node.disable.assert_called_with(None, act)
# ^ should be called, but real method no-ops if disabled
for i in insts: for i in insts:
i.migrate.assert_called() i.migrate.assert_called()
...@@ -16,9 +16,10 @@ ...@@ -16,9 +16,10 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django.test import TestCase from django.test import TestCase
from mock import MagicMock
from common.operations import operation_registry_name as op_reg_name from common.operations import operation_registry_name as op_reg_name
from vm.models import Instance, Node from vm.models import Instance, InstanceActivity, Node
from vm.operations import ( from vm.operations import (
DeployOperation, DestroyOperation, FlushOperation, MigrateOperation, DeployOperation, DestroyOperation, FlushOperation, MigrateOperation,
RebootOperation, ResetOperation, SaveAsTemplateOperation, RebootOperation, ResetOperation, SaveAsTemplateOperation,
...@@ -45,6 +46,21 @@ class MigrateOperationTestCase(TestCase): ...@@ -45,6 +46,21 @@ class MigrateOperationTestCase(TestCase):
def test_operation_registered(self): def test_operation_registered(self):
assert MigrateOperation.id in getattr(Instance, op_reg_name) assert MigrateOperation.id in getattr(Instance, op_reg_name)
def test_operation_wo_to_node_param(self):
class MigrateException(Exception):
pass
inst = MagicMock(spec=Instance)
act = MagicMock(spec=InstanceActivity)
op = MigrateOperation(inst)
op._get_remote_args = MagicMock(side_effect=MigrateException())
inst.select_node = MagicMock(return_value='test')
self.assertRaises(
MigrateException, op._operation,
act, to_node=None)
assert inst.select_node.called
op._get_remote_args.assert_called_once_with(to_node='test')
class RebootOperationTestCase(TestCase): class RebootOperationTestCase(TestCase):
def test_operation_registered(self): def test_operation_registered(self):
......
...@@ -3,13 +3,13 @@ anyjson==0.3.3 ...@@ -3,13 +3,13 @@ anyjson==0.3.3
billiard==3.3.0.17 billiard==3.3.0.17
bpython==0.12 bpython==0.12
celery==3.1.11 celery==3.1.11
Django==1.6.3 Django==1.6.7
django-autocomplete-light==1.4.14 django-autocomplete-light==1.4.14
django-braces==1.4.0 django-braces==1.4.0
django-celery==3.1.10 django-celery==3.1.10
django-crispy-forms==1.4.0 django-crispy-forms==1.4.0
django-model-utils==2.0.3 django-model-utils==2.0.3
django-sizefield==0.5 django-sizefield==0.6
django-sshkey==2.2.0 django-sshkey==2.2.0
django-statici18n==1.1 django-statici18n==1.1
django-tables2==0.15.0 django-tables2==0.15.0
......
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