Commit bc48c0ba by Bach Dániel

Merge remote-tracking branch 'origin/master' into issue-289

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
parents 2a45d4dc dc674ca8
...@@ -273,3 +273,4 @@ def register_operation(op_cls, op_id=None, target_cls=None): ...@@ -273,3 +273,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
...@@ -524,11 +524,7 @@ class TemplateForm(forms.ModelForm): ...@@ -524,11 +524,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 +540,14 @@ class TemplateForm(forms.ModelForm): ...@@ -544,13 +540,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(
...@@ -793,6 +790,48 @@ class VmCreateDiskForm(forms.Form): ...@@ -793,6 +790,48 @@ 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") % self.disk),
Field('disk'), Field('size'))
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"))
url = forms.CharField(label=_('URL'), validators=[URLValidator(), ]) url = forms.CharField(label=_('URL'), validators=[URLValidator(), ])
......
...@@ -977,3 +977,33 @@ textarea[name="new_members"] { ...@@ -977,3 +977,33 @@ textarea[name="new_members"] {
#vm-info-pane { #vm-info-pane {
margin-bottom: 20px; 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 tbody td:nth-child(3) {
white-space: nowrap;
}
#vm-list-table td {
vertical-align: middle;
}
...@@ -397,6 +397,20 @@ $(function () { ...@@ -397,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) {
......
...@@ -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):
......
...@@ -11,11 +11,17 @@ ...@@ -11,11 +11,17 @@
{% endif %} {% endif %}
{% else %}<span class="disk-list-disk-percentage" data-disk-pk="{{ d.pk }}">{{ d.get_download_percentage }}</span>%{% endif %} {% else %}<span class="disk-list-disk-percentage" data-disk-pk="{{ d.pk }}">{{ d.get_download_percentage }}</span>%{% endif %}
{% if is_owner != False %} {% if is_owner != False %}
<a href="{% url "dashboard.views.disk-remove" pk=d.pk %}?next={{ request.path }}" <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" data-disk-pk="{{ d.pk }}" class="btn btn-xs btn-danger pull-right disk-remove"
{% if not long_remove %}title="{% trans "Remove" %}"{% endif %} {% if not long_remove %}title="{% trans "Remove" %}"{% endif %}>
> <i class="fa fa-times"></i>{% if long_remove %} {% trans "Remove" %}{% endif %}
<i class="fa fa-times"></i>{% if long_remove %} {% trans "Remove" %}{% endif %} </a>
</a> {% if op.resize_disk %}
<span class="operation-wrapper">
<a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}" class="btn btn-xs btn-warning pull-right operation">
<i class="fa fa-arrows-alt"></i> {% trans "Resize" %}
</a>
</span>
{% endif %}
{% 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 %}
...@@ -18,6 +18,8 @@ Choose a compute node to migrate {{obj}} to. ...@@ -18,6 +18,8 @@ Choose a compute node to migrate {{obj}} to.
<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 selected == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %}
</label> </label>
......
{% 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>
...@@ -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>
...@@ -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>
......
...@@ -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>
......
...@@ -137,9 +137,14 @@ ...@@ -137,9 +137,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>
...@@ -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,
...@@ -46,7 +46,10 @@ from .views import ( ...@@ -46,7 +46,10 @@ from .views import (
GroupPermissionsView, GroupPermissionsView,
LeaseAclUpdateView, LeaseAclUpdateView,
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()
...@@ -74,8 +77,6 @@ urlpatterns = patterns( ...@@ -74,8 +77,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(),
...@@ -110,8 +111,6 @@ urlpatterns = patterns( ...@@ -110,8 +111,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'),
...@@ -121,14 +120,18 @@ urlpatterns = patterns( ...@@ -121,14 +120,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(),
...@@ -210,3 +213,21 @@ urlpatterns = patterns( ...@@ -210,3 +213,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())
)
...@@ -11,3 +11,4 @@ from template import * ...@@ -11,3 +11,4 @@ from template import *
from user import * from user import *
from util import * from util import *
from vm 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)
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import json import json
from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
...@@ -37,10 +38,43 @@ from vm.models import Node, NodeActivity, Trait ...@@ -37,10 +38,43 @@ from vm.models import Node, NodeActivity, Trait
from ..forms import TraitForm, HostForm, NodeForm from ..forms import TraitForm, HostForm, NodeForm
from ..tables import NodeListTable from ..tables import NodeListTable
from .util import GraphViewBase from .util import AjaxOperationMixin, OperationView, GraphMixin
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView): def get_operations(instance, user):
ops = []
for k, v in node_ops.iteritems():
try:
op = v.get_op_by_object(instance)
op.check_auth(user)
op.check_precond()
except Exception:
ops.append(v.bind_to_object(instance, disabled=True))
else:
ops.append(v.bind_to_object(instance))
return ops
class NodeOperationView(AjaxOperationMixin, OperationView):
model = Node
context_object_name = 'node' # much simpler to mock object
node_ops = OrderedDict([
('activate', NodeOperationView.factory(
op='activate', icon='play-circle', effect='success')),
('passivate', NodeOperationView.factory(
op='passivate', icon='play-circle-o', effect='info')),
('disable', NodeOperationView.factory(
op='disable', icon='times-circle-o', effect='danger')),
('flush', NodeOperationView.factory(
op='flush', icon='paint-brush', effect='danger')),
])
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin,
GraphMixin, DetailView):
template_name = "dashboard/node-detail.html" template_name = "dashboard/node-detail.html"
model = Node model = Node
form = None form = None
...@@ -53,6 +87,8 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView): ...@@ -53,6 +87,8 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
na = NodeActivity.objects.filter( na = NodeActivity.objects.filter(
node=self.object, parent=None node=self.object, parent=None
).order_by('-started').select_related() ).order_by('-started').select_related()
context['ops'] = get_operations(self.object, self.request.user)
context['op'] = {i.op: i for i in context['ops']}
context['activities'] = na context['activities'] = na
context['trait_form'] = form context['trait_form'] = form
context['graphite_enabled'] = ( context['graphite_enabled'] = (
...@@ -107,7 +143,8 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView): ...@@ -107,7 +143,8 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
return redirect(self.object.get_absolute_url()) return redirect(self.object.get_absolute_url())
class NodeList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView): class NodeList(LoginRequiredMixin, SuperuserRequiredMixin,
GraphMixin, SingleTableView):
template_name = "dashboard/node-list.html" template_name = "dashboard/node-list.html"
table_class = NodeListTable table_class = NodeListTable
table_pagination = False table_pagination = False
...@@ -314,60 +351,3 @@ class NodeStatus(LoginRequiredMixin, SuperuserRequiredMixin, DetailView): ...@@ -314,60 +351,3 @@ class NodeStatus(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
else: else:
messages.success(request, success_message) messages.success(request, success_message)
return redirect(self.get_success_url()) return redirect(self.get_success_url())
class NodeFlushView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
template_name = "dashboard/confirm/node-flush.html"
model = Node
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-node-flush.html']
else:
return ['dashboard/confirm/node-flush.html']
def get_success_url(self):
next = self.request.GET.get('next')
if next:
return next
else:
return reverse_lazy("dashboard.views.node-detail",
kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super(NodeFlushView, self).get_context_data(**kwargs)
return context
def post(self, request, *args, **kwargs):
if request.POST.get('flush') is not None:
return self.__flush(request)
return redirect(reverse_lazy("dashboard.views.node-detail",
kwargs={'pk': self.get_object().pk}))
def __flush(self, request):
self.object = self.get_object()
self.object.flush.async(user=request.user)
success_message = _("Node successfully flushed.")
messages.success(request, success_message)
return redirect(self.get_success_url())
class NodeGraphView(SuperuserRequiredMixin, GraphViewBase):
metrics = {
'cpu': ('cactiStyle(alias(nonNegativeDerivative(%(prefix)s.cpu.times),'
'"cpu usage (%%)"))'),
'memory': ('cactiStyle(alias(%(prefix)s.memory.usage,'
'"memory usage (%%)"))'),
'network': ('cactiStyle(aliasByMetric('
'nonNegativeDerivative(%(prefix)s.network.bytes_*)))'),
}
model = Node
def get_prefix(self, instance):
return 'circle.%s' % instance.host.hostname
def get_title(self, instance, metric):
return '%s - %s' % (instance.name, metric)
def get_object(self, request, pk):
return self.model.objects.get(id=pk)
...@@ -300,6 +300,7 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -300,6 +300,7 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
context['disks'] = obj.disks.all() context['disks'] = obj.disks.all()
context['is_owner'] = obj.has_level(self.request.user, 'owner') context['is_owner'] = obj.has_level(self.request.user, 'owner')
context['aclform'] = AclUserOrGroupAddForm() context['aclform'] = AclUserOrGroupAddForm()
context['parent'] = obj.parent
return context return context
def get_success_url(self): def get_success_url(self):
......
...@@ -285,6 +285,25 @@ class ProfileView(LoginRequiredMixin, DetailView): ...@@ -285,6 +285,25 @@ class ProfileView(LoginRequiredMixin, DetailView):
slug_field = "username" slug_field = "username"
slug_url_kwarg = "username" slug_url_kwarg = "username"
def get(self, *args, **kwargs):
user = self.request.user
target = self.get_object()
# get the list of groups where the user is operator
user_g_w_op = GroupProfile.get_objects_with_level("operator", user)
# get the list of groups the "target" (the profile) is member of
target_groups = GroupProfile.objects.filter(
group__in=target.groups.all())
intersection = set(user_g_w_op).intersection(target_groups)
# if the intersection of the 2 lists is empty the logged in user
# has no permission to check the target's profile
# (except if the user want to see his own profile)
if len(intersection) < 1 and target != user:
raise PermissionDenied
return super(ProfileView, self).get(*args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ProfileView, self).get_context_data(**kwargs) context = super(ProfileView, self).get_context_data(**kwargs)
user = self.get_object() user = self.get_object()
......
...@@ -22,18 +22,16 @@ import re ...@@ -22,18 +22,16 @@ import re
from collections import OrderedDict from collections import OrderedDict
from urlparse import urljoin from urlparse import urljoin
import requests
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponse, Http404, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView, View from django.views.generic import DetailView, View
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
...@@ -407,7 +405,8 @@ class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin): ...@@ -407,7 +405,8 @@ class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
is_owner = 'owner' in allowed_levels is_owner = 'owner' in allowed_levels
allowed_users = cls.get_allowed_users(user) allowed_users = cls.get_allowed_users(user)
allowed_groups = cls.get_allowed_groups(user) allowed_groups = (set(cls.get_allowed_groups(user)) |
set(user.groups.all()))
user_levels = list( user_levels = list(
{'user': u, 'level': l} for u, l in obj.get_users_with_level() {'user': u, 'level': l} for u, l in obj.get_users_with_level()
...@@ -537,43 +536,29 @@ class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin): ...@@ -537,43 +536,29 @@ class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
return redirect("%s#access" % self.instance.get_absolute_url()) return redirect("%s#access" % self.instance.get_absolute_url())
class GraphViewBase(LoginRequiredMixin, View): class GraphMixin(object):
def get(self, request, pk, metric, time, *args, **kwargs): graph_time_options = [
graphite_url = settings.GRAPHITE_URL {'time': "1h", 'name': _("1 hour")},
if graphite_url is None: {'time': "6h", 'name': _("6 hours")},
raise Http404() {'time': "1d", 'name': _("1 day")},
{'time': "1w", 'name': _("1 week")},
if metric not in self.metrics.keys(): {'time': "30d", 'name': _("1 month")},
raise SuspiciousOperation() {'time': "26w", 'name': _("6 months")},
]
try: default_graph_time = "6h"
instance = self.get_object(request, pk)
except self.model.DoesNotExist: def get_context_data(self, *args, **kwargs):
raise Http404() context = super(GraphMixin, self).get_context_data(*args, **kwargs)
graph_time = self.request.GET.get("graph_time",
prefix = self.get_prefix(instance) self.default_graph_time)
target = self.metrics[metric] % {'prefix': prefix} if not re.match("^[0-9]{1,2}[hdwy]$", graph_time):
title = self.get_title(instance, metric) messages.warning(self.request, _("Bad graph time format, "
params = {'target': target, "available periods are: "
'from': '-%s' % time, "h, d, w, and y."))
'title': title.encode('UTF-8'), graph_time = self.default_graph_time
'width': '500', context['graph_time'] = graph_time
'height': '200'} context['graph_time_options'] = self.graph_time_options
logger.debug('%s %s', graphite_url, params) return context
response = requests.get('%s/render/' % graphite_url, params=params)
return HttpResponse(response.content, mimetype="image/png")
def get_prefix(self, instance):
raise NotImplementedError("Subclass must implement abstract method")
def get_title(self, instance, metric):
raise NotImplementedError("Subclass must implement abstract method")
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
def absolute_url(url): def absolute_url(url):
......
...@@ -52,13 +52,13 @@ from vm.models import ( ...@@ -52,13 +52,13 @@ from vm.models import (
) )
from .util import ( from .util import (
CheckedDetailView, AjaxOperationMixin, OperationView, AclUpdateView, CheckedDetailView, AjaxOperationMixin, OperationView, AclUpdateView,
FormOperationMixin, FilterMixin, GraphViewBase, search_user, FormOperationMixin, FilterMixin, search_user, GraphMixin,
) )
from ..forms import ( from ..forms import (
AclUserOrGroupAddForm, VmResourcesForm, TraitsForm, RawDataForm, AclUserOrGroupAddForm, VmResourcesForm, TraitsForm, RawDataForm,
VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm, VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm,
VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm, VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm,
TransferOwnershipForm, TransferOwnershipForm, VmDiskResizeForm,
) )
from ..models import Favourite, Profile from ..models import Favourite, Profile
...@@ -89,7 +89,7 @@ class VmDetailVncTokenView(CheckedDetailView): ...@@ -89,7 +89,7 @@ class VmDetailVncTokenView(CheckedDetailView):
raise Http404() raise Http404()
class VmDetailView(CheckedDetailView): class VmDetailView(GraphMixin, CheckedDetailView):
template_name = "dashboard/vm-detail.html" template_name = "dashboard/vm-detail.html"
model = Instance model = Instance
...@@ -365,6 +365,30 @@ class VmAddInterfaceView(FormOperationMixin, VmOperationView): ...@@ -365,6 +365,30 @@ class VmAddInterfaceView(FormOperationMixin, VmOperationView):
return val return val
class VmDiskResizeView(FormOperationMixin, VmOperationView):
op = 'resize_disk'
form_class = VmDiskResizeForm
show_in_toolbar = False
icon = 'arrows-alt'
effect = "success"
def get_form_kwargs(self):
choices = self.get_op().instance.disks
disk_pk = self.request.GET.get('disk')
if disk_pk:
try:
default = choices.get(pk=disk_pk)
except (ValueError, Disk.DoesNotExist):
raise Http404()
else:
default = None
val = super(VmDiskResizeView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
return val
class VmCreateDiskView(FormOperationMixin, VmOperationView): class VmCreateDiskView(FormOperationMixin, VmOperationView):
op = 'create_disk' op = 'create_disk'
...@@ -395,7 +419,7 @@ class VmMigrateView(VmOperationView): ...@@ -395,7 +419,7 @@ class VmMigrateView(VmOperationView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super(VmMigrateView, self).get_context_data(**kwargs) ctx = super(VmMigrateView, self).get_context_data(**kwargs)
ctx['nodes'] = [n for n in Node.objects.filter(enabled=True) ctx['nodes'] = [n for n in Node.objects.filter(enabled=True)
if n.state == "ONLINE"] if n.online]
return ctx return ctx
def post(self, request, extra=None, *args, **kwargs): def post(self, request, extra=None, *args, **kwargs):
...@@ -601,6 +625,7 @@ vm_ops = OrderedDict([ ...@@ -601,6 +625,7 @@ vm_ops = OrderedDict([
op='destroy', icon='times', effect='danger')), op='destroy', icon='times', effect='danger')),
('create_disk', VmCreateDiskView), ('create_disk', VmCreateDiskView),
('download_disk', VmDownloadDiskView), ('download_disk', VmDownloadDiskView),
('resize_disk', VmDiskResizeView),
('add_interface', VmAddInterfaceView), ('add_interface', VmAddInterfaceView),
('renew', VmRenewView), ('renew', VmRenewView),
('resources_change', VmResourcesChangeView), ('resources_change', VmResourcesChangeView),
...@@ -986,28 +1011,6 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -986,28 +1011,6 @@ class VmCreate(LoginRequiredMixin, TemplateView):
return create_func(request, *args, **kwargs) return create_func(request, *args, **kwargs)
class VmGraphView(GraphViewBase):
metrics = {
'cpu': ('cactiStyle(alias(nonNegativeDerivative(%(prefix)s.cpu.usage),'
'"cpu usage (%%)"))'),
'memory': ('cactiStyle(alias(%(prefix)s.memory.usage,'
'"memory usage (%%)"))'),
'network': (
'group('
'aliasSub(nonNegativeDerivative(%(prefix)s.network.bytes_recv*),'
' ".*-(\d+)\\)", "out (vlan \\1)"),'
'aliasSub(nonNegativeDerivative(%(prefix)s.network.bytes_sent*),'
' ".*-(\d+)\\)", "in (vlan \\1)"))'),
}
model = Instance
def get_prefix(self, instance):
return 'vm.%s' % instance.vm_name
def get_title(self, instance, metric):
return '%s (%s) - %s' % (instance.name, instance.vm_name, metric)
@require_GET @require_GET
def get_vm_screenshot(request, pk): def get_vm_screenshot(request, pk):
instance = get_object_or_404(Instance, pk=pk) instance = get_object_or_404(Instance, pk=pk)
......
# 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-16 12:15+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
...@@ -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)
...@@ -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:
......
...@@ -790,6 +790,15 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -790,6 +790,15 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
else: else:
raise raise
def resize_disk_live(self, disk, size, timeout=15):
queue_name = self.get_remote_queue_name('vm', 'slow')
result = vm_tasks.resize_disk.apply_async(
args=[self.vm_name, disk.path, size],
queue=queue_name).get(timeout=timeout)
disk.size = size
disk.save()
return result
def deploy_disks(self): def deploy_disks(self):
"""Deploy all associated disks. """Deploy all associated disks.
""" """
...@@ -1009,3 +1018,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -1009,3 +1018,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
latest = self.get_latest_activity_in_progress() latest = self.get_latest_activity_in_progress()
return (latest and latest.resultant_state is not None return (latest and latest.resultant_state is not None
and self.status != latest.resultant_state) and self.status != latest.resultant_state)
@property
def metric_prefix(self):
return 'vm.%s' % self.vm_name
...@@ -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'))
...@@ -116,6 +121,11 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -116,6 +121,11 @@ class Node(OperatedMixin, TimeStampedModel):
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):
...@@ -379,3 +374,12 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -379,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
...@@ -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
......
...@@ -205,8 +205,10 @@ class NodeTestCase(TestCase): ...@@ -205,8 +205,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__)
...@@ -348,70 +350,45 @@ class InstanceActivityTestCase(TestCase): ...@@ -348,70 +350,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()
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