Commit a5be38e8 by Bach Dániel

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

Conflicts:
	circle/vm/tasks/local_agent_tasks.py
parents ab68f725 0bfdb2a3
...@@ -431,9 +431,18 @@ LOGIN_REDIRECT_URL = "/" ...@@ -431,9 +431,18 @@ LOGIN_REDIRECT_URL = "/"
AGENT_DIR = get_env_variable( AGENT_DIR = get_env_variable(
'DJANGO_AGENT_DIR', join(unicode(expanduser("~")), 'agent')) 'DJANGO_AGENT_DIR', join(unicode(expanduser("~")), 'agent'))
# AGENT_DIR is the root directory for the agent.
# The directory structure SHOULD be:
# /home/username/agent
# |-- agent-linux
# | |-- .git
# | +-- ...
# |-- agent-win
# | +-- agent-win-%(version).exe
#
try: try:
git_env = {'GIT_DIR': join(AGENT_DIR, '.git')} git_env = {'GIT_DIR': join(join(AGENT_DIR, "agent-linux"), '.git')}
AGENT_VERSION = check_output( AGENT_VERSION = check_output(
('git', 'log', '-1', r'--pretty=format:%h', 'HEAD'), env=git_env) ('git', 'log', '-1', r'--pretty=format:%h', 'HEAD'), env=git_env)
except: except:
......
...@@ -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={
...@@ -744,6 +752,20 @@ class VmRenewForm(forms.Form): ...@@ -744,6 +752,20 @@ class VmRenewForm(forms.Form):
return helper return helper
class VmMigrateForm(forms.Form):
live_migration = forms.BooleanField(
required=False, initial=True, label=_("live migration"))
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
default = kwargs.pop('default')
super(VmMigrateForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'to_node', forms.ModelChoiceField(
queryset=choices, initial=default, required=False,
widget=forms.RadioSelect(), label=_("Node")))
class VmStateChangeForm(forms.Form): class VmStateChangeForm(forms.Form):
interrupt = forms.BooleanField(required=False, label=_( interrupt = forms.BooleanField(required=False, label=_(
...@@ -788,6 +810,12 @@ class VmCreateDiskForm(forms.Form): ...@@ -788,6 +810,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:
...@@ -839,13 +867,42 @@ class VmDiskResizeForm(forms.Form): ...@@ -839,13 +867,42 @@ class VmDiskResizeForm(forms.Form):
helper.form_tag = False helper.form_tag = False
if self.disk: if self.disk:
helper.layout = Layout( helper.layout = Layout(
HTML(_("<label>Disk:</label> %s") % self.disk), HTML(_("<label>Disk:</label> %s") % escape(self.disk)),
Field('disk'), Field('size')) Field('disk'), Field('size'))
return helper 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
...@@ -854,6 +911,18 @@ class VmDownloadDiskForm(forms.Form): ...@@ -854,6 +911,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;
...@@ -1012,3 +1012,7 @@ textarea[name="new_members"] { ...@@ -1012,3 +1012,7 @@ textarea[name="new_members"] {
.disk-resize-btn { .disk-resize-btn {
margin-right: 5px; margin-right: 5px;
} }
#vm-migrate-node-list li {
cursor: pointer;
}
...@@ -411,6 +411,17 @@ $(function () { ...@@ -411,6 +411,17 @@ $(function () {
$(this).removeClass("btn-default").addClass("btn-primary"); $(this).removeClass("btn-default").addClass("btn-primary");
return false; return false;
}); });
// vm migrate select for node
$(document).on("click", "#vm-migrate-node-list li", function(e) {
var li = $(this).closest('li');
if (li.find('input').attr('disabled'))
return true;
$('#vm-migrate-node-list li').removeClass('panel-primary');
li.addClass('panel-primary').find('input').prop("checked", true);
return true;
});
}); });
function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { function generateVmHTML(pk, name, host, icon, _status, fav, is_last) {
...@@ -445,7 +456,7 @@ function generateNodeHTML(name, icon, _status, url, is_last) { ...@@ -445,7 +456,7 @@ function generateNodeHTML(name, icon, _status, url, is_last) {
function generateNodeTagHTML(name, icon, _status, label , url) { function generateNodeTagHTML(name, icon, _status, label , url) {
return '<a href="' + url + '" class="label ' + label + '" >' + return '<a href="' + url + '" class="label ' + label + '" >' +
'<i class="' + icon + '" title="' + _status + '"></i> ' + name + '<i class="fa ' + icon + '" title="' + _status + '"></i> ' + name +
'</a> '; '</a> ';
} }
......
...@@ -16,15 +16,6 @@ $(function() { ...@@ -16,15 +16,6 @@ $(function() {
$('#confirmation-modal').on('hidden.bs.modal', function() { $('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove(); $('#confirmation-modal').remove();
}); });
$('#vm-migrate-node-list li').click(function(e) {
var li = $(this).closest('li');
if (li.find('input').attr('disabled'))
return true;
$('#vm-migrate-node-list li').removeClass('panel-primary');
li.addClass('panel-primary').find('input').attr('checked', true);
return false;
});
$('#vm-migrate-node-list li input:checked').closest('li').addClass('panel-primary'); $('#vm-migrate-node-list li input:checked').closest('li').addClass('panel-primary');
} }
}); });
...@@ -51,7 +42,8 @@ $(function() { ...@@ -51,7 +42,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 */
...@@ -404,6 +405,7 @@ function checkNewActivity(runs) { ...@@ -404,6 +405,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');
}, },
......
{% 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 }}) - {{ d.size|filesize }}
{% 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 %}
<i class="fa fa-times"></i>{% if long_remove %} {% trans "Remove" %}{% endif %} <span class="operation-wrapper">
</a> <a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}"
{% if op.resize_disk %} class="btn btn-xs btn-{{ op.resize_disk.effect }} pull-right operation disk-resize-btn
<span class="operation-wrapper"> {% if op.resize_disk.disabled %}disabled{% endif %}">
<a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}" <i class="fa fa-{{ op.resize_disk.icon }}"></i> {% trans "Resize" %}
class="btn btn-xs btn-warning pull-right operation disk-resize-btn"> </a>
<i class="fa fa-arrows-alt"></i> {% trans "Resize" %} </span>
</a>
</span>
{% endif %}
{% endif %} {% endif %}
<div style="clear: both;"></div> <div style="clear: both;"></div>
{% if request.user.is_superuser %}
<small>{% trans "File name" %}: {{ d.filename }}</small>
{% endif %}
{% extends "dashboard/mass-operate.html" %} {% extends "dashboard/mass-operate.html" %}
{% load i18n %} {% load i18n %}
{% load sizefieldtags %} {% load sizefieldtags %}
{% load crispy_forms_tags %}
{% block formfields %} {% block formfields %}
...@@ -11,20 +12,20 @@ ...@@ -11,20 +12,20 @@
<label for="migrate-to-none"> <label for="migrate-to-none">
<strong>{% trans "Reschedule" %}</strong> <strong>{% trans "Reschedule" %}</strong>
</label> </label>
<input id="migrate-to-none" type="radio" name="node" value="" style="float: right;" checked="checked"> <input id="migrate-to-none" type="radio" name="to_node" value="" style="float: right;" checked="checked">
<span class="vm-migrate-node-property"> <span class="vm-migrate-node-property">
{% trans "This option will reschedule each virtual machine to the optimal node." %} {% trans "This option will reschedule each virtual machine to the optimal node." %}
</span> </span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
</div> </div>
</li> </li>
{% for n in nodes %} {% for n in form.fields.to_node.queryset.all %}
<li class="panel panel-default mass-migrate-node"> <li class="panel panel-default mass-migrate-node">
<div class="panel-body"> <div class="panel-body">
<label for="migrate-to-{{n.pk}}"> <label for="migrate-to-{{n.pk}}">
<strong>{{ n }}</strong> <strong>{{ n }}</strong>
</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="to_node" value="{{ n.pk }}" style="float: right;"/>
<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>
...@@ -32,5 +33,6 @@ ...@@ -32,5 +33,6 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{{ form.live_migration|as_crispy_field }}
<hr /> <hr />
{% endblock %} {% endblock %}
{% extends "dashboard/operate.html" %} {% extends "dashboard/operate.html" %}
{% load i18n %} {% load i18n %}
{% load sizefieldtags %} {% load sizefieldtags %}
{% load crispy_forms_tags %}
{% block question %} {% block question %}
<p> <p>
...@@ -13,24 +14,27 @@ Choose a compute node to migrate {{obj}} to. ...@@ -13,24 +14,27 @@ 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 %} {% with current=object.node.pk recommended=form.fields.to_node.initial.pk %}
{% for n in nodes %} {% for n in form.fields.to_node.queryset.all %}
<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> <div class="label label-primary">
{{n.get_status_display}}</div> <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 recommended == 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="to_node" value="{{ n.pk }}" style="float: right;"
{% if current == n.pk %}disabled="disabled"{% endif %} {% if current == n.pk %}disabled="disabled"{% endif %}
{% if recommended == 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>
</div></li> </li>
{% endfor %} {% endfor %}
{% endwith %} {% endwith %}
</ul> </ul>
{{ form.live_migration|as_crispy_field }}
{% endblock %} {% endblock %}
{% load i18n %} {% load i18n %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<div class="pull-right toolbar"> <div class="pull-right toolbar">
<div class="btn-group"> <div class="btn-group">
...@@ -7,9 +7,10 @@ ...@@ -7,9 +7,10 @@
data-container="body"><i class="fa fa-dashboard"></i></a> data-container="body"><i class="fa fa-dashboard"></i></a>
<a href="#index-list-view" data-index-box="node" class="btn btn-default btn-xs disabled" <a href="#index-list-view" data-index-box="node" class="btn btn-default btn-xs disabled"
data-container="body"><i class="fa fa-list"></i></a> data-container="body"><i class="fa fa-list"></i></a>
</div> </div>
<span class="btn btn-default btn-xs infobtn" title="{% trans "List of compute nodes, also called worker nodes or hypervisors, which run the virtual machines." %}"><i class="fa fa-info-circle"></i></span> <span class="btn btn-default btn-xs infobtn" title="{% trans "List of compute nodes, also called worker nodes or hypervisors, which run the virtual machines." %}">
<i class="fa fa-info-circle"></i>
</span>
</div> </div>
<h3 class="no-margin"> <h3 class="no-margin">
<i class="fa fa-sitemap"></i> {% trans "Nodes" %} <i class="fa fa-sitemap"></i> {% trans "Nodes" %}
...@@ -28,50 +29,55 @@ ...@@ -28,50 +29,55 @@
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
<div href="#" class="list-group-item list-group-footer"> </div><!-- #node-list-view -->
<div class="row">
<div class="col-sm-6 col-xs-6 input-group input-group-sm">
<input id="dashboard-node-search-input" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary" title="search"><i class="fa fa-search"></i></button>
</div>
</div>
<div class="col-sm-6 text-right">
<a class="btn btn-primary btn-xs" href="{% url "dashboard.views.node-list" %}">
<i class="fa fa-chevron-circle-right"></i>
{% if more_nodes > 0 %}
{% blocktrans with count=more_nodes %}<strong>{{count}}</strong> more{% endblocktrans %}
{% else %}
{% trans "list" %}
{% endif %}
</a>
<a class="btn btn-success btn-xs node-create" href="{% url "dashboard.views.node-create" %}"><i class="fa fa-plus-circle"></i> {% trans "new" %}</a>
</div>
</div>
</div>
</div>
<div class="panel-body" id="node-graph-view" style="display: none"> <div class="panel-body" id="node-graph-view" style="display: none; min-height: 204px;">
<p class="pull-right"> <input class="knob" data-fgColor="chartreuse" data-thickness=".4" data-width="60" data-height="60" data-readOnly="true" value="{% widthratio node_num.running sum_node_num 100 %}"></p> <p class="pull-right">
<p><span class="big"><big>{{ node_num.running }}</big> running </span> <input class="knob" data-fgColor="chartreuse"
+ <big>{{ node_num.missing }}</big> missing + <br><big>{{ node_num.disabled }}</big> disabled + <big>{{ node_num.offline }}</big> offline</p> data-thickness=".4" data-width="60" data-height="60" data-readOnly="true"
<ul class="list-inline" id="dashboard-node-taglist"> value="{% widthratio node_num.running sum_node_num 100 %}">
{% for i in nodes %} </p>
<a href="{{ i.get_absolute_url }}" class="label {{i.get_status_label}}" > <p>
<i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i> {{ i.name }}</a> <span class="big">
{% endfor %} <big>{{ node_num.running }}</big> running
</ul> </span>
+ <big>{{ node_num.missing }}</big>
missing + <br><big>{{ node_num.disabled }}</big> disabled + <big>{{ node_num.offline }}</big> offline
</p>
<ul class="list-inline" id="dashboard-node-taglist">
{% for i in nodes %}
<a href="{{ i.get_absolute_url }}" class="label {{i.get_status_label}}" >
<i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i> {{ i.name }}</a>
{% endfor %}
</ul>
<div class="clearfix"></div> <div class="clearfix"></div>
<div class="row"> </div>
<div class="col-sm-6 text-right pull-right">
{% if more_nodes >= 0 %} <div href="#" class="list-group-item list-group-footer">
<a class="btn btn-primary btn-xs" href="{% url "dashboard.views.node-list" %}"> <div class="row">
<i class="fa fa-chevron-circle-right"></i> {% blocktrans with count=more_nodes %}<strong>{{count}}</strong> more{% endblocktrans %} <div class="col-sm-6 col-xs-6 input-group input-group-sm">
</a> <input id="dashboard-node-search-input" type="text" class="form-control"
{% endif %} placeholder="{% trans "Search..." %}" />
<a class="btn btn-success btn-xs node-create" href="{% url "dashboard.views.node-create" %}"><i class="fa fa-plus-circle"></i> {% trans "new" %}</a> <div class="input-group-btn">
<button type="submit" class="btn btn-primary" title="{% trans "Search" %}" data-container="body">
<i class="fa fa-search"></i>
</button>
</div> </div>
</div> </div>
</div> <div class="col-sm-6 text-right">
<a class="btn btn-primary btn-xs" href="{% url "dashboard.views.node-list" %}">
<i class="fa fa-chevron-circle-right"></i>
{% if more_nodes > 0 %}
{% blocktrans with count=more_nodes %}<strong>{{count}}</strong> more{% endblocktrans %}
{% else %}
{% trans "list" %}
{% endif %}
</a>
<a class="btn btn-success btn-xs node-create" href="{% url "dashboard.views.node-create" %}">
<i class="fa fa-plus-circle"></i> {% trans "new" %}
</a>
</div>
</div>
</div>
</div> </div>
...@@ -58,6 +58,26 @@ ...@@ -58,6 +58,26 @@
<dt>{% trans "resultant state" %}</dt> <dt>{% trans "resultant state" %}</dt>
<dd>{{object.resultant_state|default:'n/a'}}</dd> <dd>{{object.resultant_state|default:'n/a'}}</dd>
<dt>{% trans "subactivities" %}</dt>
{% for s in object.children.all %}
<dd>
<span{% if s.result %} title="{{ s.result|get_text:user }}"{% endif %}>
<a href="{{ s.get_absolute_url }}">
{{ s.readable_name|get_text:user|capfirst }}</a></span> &ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
<i class="fa fa-refresh fa-spin" class="sub-activity-loading-icon"></i>
{% endif %}
{% if s.has_failed %}
<div class="label label-danger">{% trans "failed" %}</div>
{% endif %}
</dd>
{% empty %}
<dd>{% trans "none" %}</dd>
{% endfor %}
</div>
</div> </div>
</div> </div>
</div> </div>
......
{% load i18n %} {% load i18n %}
<div id="node-list-column-vm"> <div id="node-list-column-vm">
<a class="real-link" href="{% url "dashboard.views.vm-list" %}?s=node:{{ record.name }}"> <a class="real-link" href="{% url "dashboard.views.vm-list" %}?s=node_exact:{{ record.name }}">
{{ value }} {{ value }}
</a> </a>
</div> </div>
...@@ -86,7 +86,13 @@ ...@@ -86,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>
......
...@@ -34,6 +34,13 @@ from ..views import AclUpdateView ...@@ -34,6 +34,13 @@ from ..views import AclUpdateView
from .. import views from .. import views
class QuerySet(list):
model = MagicMock()
def get(self, *args, **kwargs):
return self.pop()
class ViewUserTestCase(unittest.TestCase): class ViewUserTestCase(unittest.TestCase):
def test_404(self): def test_404(self):
...@@ -145,58 +152,66 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -145,58 +152,66 @@ class VmOperationViewTestCase(unittest.TestCase):
view.as_view()(request, pk=1234).render() view.as_view()(request, pk=1234).render()
def test_migrate(self): def test_migrate(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=True) request = FakeRequestFactory(
POST={'to_node': 1, 'live_migration': True}, superuser=True)
view = vm_ops['migrate'] view = vm_ops['migrate']
node = MagicMock(pk=1, name='node1')
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
patch('dashboard.views.util.messages') as msg, \ patch('dashboard.views.util.messages') as msg, \
patch('dashboard.views.vm.get_object_or_404') as go4: patch.object(view, 'get_form_kwargs') as form_kwargs:
inst = MagicMock(spec=Instance) inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance" inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst) inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock() inst.migrate.async = MagicMock()
inst.has_level.return_value = True inst.has_level.return_value = True
form_kwargs.return_value = {
'default': 100, 'choices': QuerySet([node])}
go.return_value = inst go.return_value = inst
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)['location'] assert view.as_view()(request, pk=1234)['location']
assert not msg.error.called assert not msg.error.called
assert go4.called inst.migrate.async.assert_called_once_with(
to_node=node, live_migration=True, user=request.user)
def test_migrate_failed(self):