Commit cf578809 by Kálmán Viktor

Merge branch 'master' into feature-template-wizard

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/urls.py
parents 8a94925d dd0e44ed
......@@ -21,6 +21,13 @@ _build
celerybeat-schedule
.coverage
*,cover
coverage.xml
# Gettext object file:
*.mo
\ No newline at end of file
*.mo
# saml
circle/attribute-maps/
circle/remote_metadata.xml
circle/samlcert.key
circle/samlcert.pem
from collections import deque
from contextlib import contextmanager
from hashlib import sha224
from logging import getLogger
from time import time
......@@ -32,6 +33,18 @@ def activitycontextimpl(act, on_abort=None, on_commit=None):
act.finish(succeeded=True, event_handler=on_commit)
activity_context = contextmanager(activitycontextimpl)
activity_code_separator = '.'
def join_activity_code(*args):
"""Join the specified parts into an activity code.
"""
return activity_code_separator.join(args)
class ActivityModel(TimeStampedModel):
activity_code = CharField(max_length=100, verbose_name=_('activity code'))
parent = ForeignKey('self', blank=True, null=True, related_name='children')
......
from logging import getLogger
from .models import activity_context
from django.core.exceptions import PermissionDenied
logger = getLogger(__name__)
class Operation(object):
"""Base class for VM operations.
"""
async_queue = 'localhost.man'
required_perms = ()
def __call__(self, **kwargs):
return self.call(**kwargs)
def __init__(self, subject):
"""Initialize a new operation bound to the specified subject.
"""
self.subject = subject
def __unicode__(self):
return self.name
def __prelude(self, kwargs):
"""This method contains the shared prelude of call and async.
"""
skip_checks = kwargs.setdefault('system', False)
user = kwargs.setdefault('user', None)
parent_activity = kwargs.pop('parent_activity', None)
if not skip_checks:
self.check_auth(user)
self.check_precond()
return self.create_activity(parent=parent_activity, user=user)
def _exec_op(self, activity, user, **kwargs):
"""Execute the operation inside the specified activity's context.
"""
with activity_context(activity, on_abort=self.on_abort,
on_commit=self.on_commit):
return self._operation(activity=activity, user=user,
**kwargs)
def _operation(self, activity, user, system, **kwargs):
"""This method is the operation's particular implementation.
Deriving classes should implement this method.
"""
raise NotImplementedError
def async(self, **kwargs):
"""Execute the operation asynchronously.
Only a quick, preliminary check is ran before creating the associated
activity and queuing the job.
The returned value is the handle for the asynchronous job.
For more information, check the synchronous call's documentation.
"""
logger.info("%s called asynchronously with the following parameters: "
"%r", self.__class__.__name__, kwargs)
activity = self.__prelude(kwargs)
return self.async_operation.apply_async(args=(self.id,
self.subject.pk,
activity.pk),
kwargs=kwargs,
queue=self.async_queue)
def call(self, **kwargs):
"""Execute the operation (synchronously).
Anticipated keyword arguments:
* parent_activity: Parent activity for the operation. If this argument
is present, the operation's activity will be created
as a child activity of it.
* system: Indicates that the operation is invoked by the system, not a
User. If this argument is present and has a value of True,
then authorization checks are skipped.
* user: The User invoking the operation. If this argument is not
present, it'll be provided with a default value of None.
"""
logger.info("%s called (synchronously) with the following parameters: "
"%r", self.__class__.__name__, kwargs)
activity = self.__prelude(kwargs)
return self._exec_op(activity=activity, **kwargs)
def check_precond(self):
pass
def check_auth(self, user):
if not user.has_perms(self.required_perms):
raise PermissionDenied("%s doesn't have the required permissions."
% user)
def create_activity(self, parent, user):
raise NotImplementedError
def on_abort(self, activity, error):
"""This method is called when the operation aborts (i.e. raises an
exception).
"""
pass
def on_commit(self, activity):
"""This method is called when the operation executes successfully.
"""
pass
operation_registry_name = '_ops'
class OperatedMixin(object):
def __getattr__(self, name):
# NOTE: __getattr__ is only called if the attribute doesn't already
# exist in your __dict__
cls = self.__class__
ops = getattr(cls, operation_registry_name, {})
op = ops.get(name)
if op:
return op(self)
else:
raise AttributeError("%r object has no attribute %r" %
(self.__class__.__name__, name))
def register_operation(target_cls, op_cls, op_id=None):
"""Register the specified operation with the target class.
You can optionally specify an ID to be used for the registration;
otherwise, the operation class' 'id' attribute will be used.
"""
if op_id is None:
op_id = op_cls.id
if not issubclass(target_cls, OperatedMixin):
raise TypeError("%r is not a subclass of %r" %
(target_cls.__name__, OperatedMixin.__name__))
if not hasattr(target_cls, operation_registry_name):
setattr(target_cls, operation_registry_name, dict())
getattr(target_cls, operation_registry_name)[op_id] = op_cls
from mock import MagicMock, patch
from django.test import TestCase
from ..operations import Operation
class OperationTestCase(TestCase):
def test_activity_created_before_async_job(self):
class AbortEx(Exception):
pass
op = Operation(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
op.async_operation = MagicMock(
apply_async=MagicMock(side_effect=AbortEx))
with patch.object(Operation, 'check_precond'):
with patch.object(Operation, 'create_activity') as create_act:
try:
op.async(system=True)
except AbortEx:
self.assertTrue(create_act.called)
def test_check_precond_called_before_create_activity(self):
class AbortEx(Exception):
pass
op = Operation(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
with patch.object(Operation, 'create_activity', side_effect=AbortEx):
with patch.object(Operation, 'check_precond') as chk_pre:
try:
op.call(system=True)
except AbortEx:
self.assertTrue(chk_pre.called)
def test_auth_check_on_non_system_call(self):
op = Operation(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
user = MagicMock()
with patch.object(Operation, 'check_auth') as check_auth:
with patch.object(Operation, 'check_precond'), \
patch.object(Operation, 'create_activity'), \
patch.object(Operation, '_exec_op'):
op.call(user=user)
check_auth.assert_called_with(user)
def test_no_auth_check_on_system_call(self):
op = Operation(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
with patch.object(Operation, 'check_auth', side_effect=AssertionError):
with patch.object(Operation, 'check_precond'), \
patch.object(Operation, 'create_activity'), \
patch.object(Operation, '_exec_op'):
op.call(system=True)
......@@ -33,7 +33,6 @@
"hostname": "devenv",
"modified_at": "2014-02-24T15:55:01.412Z",
"location": "",
"pub_ipv4": null,
"mac": "11:22:33:44:55:66",
"shared_ip": false,
"ipv4": "10.7.0.96",
......
......@@ -37,6 +37,7 @@ class VmCustomizeForm(forms.Form):
cpu_priority = forms.IntegerField()
cpu_count = forms.IntegerField()
ram_size = forms.IntegerField()
amount = forms.IntegerField(min_value=0, initial=1)
disks = forms.ModelMultipleChoiceField(
queryset=None, required=True)
......@@ -72,12 +73,21 @@ class VmCustomizeForm(forms.Form):
self.initial['template'] = self.template.pk
self.initial['customized'] = self.template.pk
# set widget for amount
self.fields['amount'].widget = NumberInput()
self.helper = FormHelper(self)
self.helper.form_show_labels = False
# don't show labels for the sliders
self.helper.form_show_labels = True
self.fields['cpu_count'].label = ""
self.fields['ram_size'].label = ""
self.fields['cpu_priority'].label = ""
self.helper.layout = Layout(
Field("template", type="hidden"),
Field("customized", type="hidden"),
Div( # buttons
Div(
Div(
AnyTag( # tip: don't try to use Button class
"button",
......@@ -88,16 +98,17 @@ class VmCustomizeForm(forms.Form):
HTML(" Start"),
css_id="vm-create-customized-start",
css_class="btn btn-success",
style="float: right; margin-top: 24px;",
),
css_class="col-sm-11 text-right",
Field("name", style="max-width: 350px;"),
css_class="col-sm-12",
),
css_class="row",
),
Div(
Div(
Field("name"),
css_class="col-sm-5",
Field("amount", min="1", style="max-width: 60px;"),
css_class="col-sm-10",
),
css_class="row",
),
......@@ -189,32 +200,36 @@ class VmCustomizeForm(forms.Form):
HTML(_("No disks are added!")),
css_id="vm-create-disk-list",
),
AnyTag(
"h3",
Div(
AnyTag(
"select",
css_class="form-control",
css_id="vm-create-disk-add-select",
),
Div(
AnyTag(
"a",
AnyTag(
"i",
css_class="icon-plus-sign",
),
href="#",
css_id="vm-create-disk-add-button",
css_class="btn btn-success",
),
css_class="input-group-btn"
),
css_class="input-group",
style="max-width: 330px;",
),
css_id="vm-create-disk-add",
Div(
HTML(""),
style="clear: both;",
),
# AnyTag(
# "h3",
# Div(
# AnyTag(
# "select",
# css_class="form-control",
# css_id="vm-create-disk-add-select",
# ),
# Div(
# AnyTag(
# "a",
# AnyTag(
# "i",
# css_class="icon-plus-sign",
# ),
# href="#",
# css_id="vm-create-disk-add-button",
# css_class="btn btn-success",
# ),
# css_class="input-group-btn"
# ),
# css_class="input-group",
# style="max-width: 330px;",
# ),
# css_id="vm-create-disk-add",
# ),
css_class="no-js-hidden",
),
css_class="col-sm-8",
......
......@@ -79,6 +79,14 @@ html {
color: #fff;
}
.timeline .activity-active .timeline-icon {
background-color: black!important;
}
.timeline a {
color: black;
}
.timeline-icon.timeline-warning {
border-color: #c09853;
border-style: solid;
......@@ -100,6 +108,10 @@ html {
border-left: 3px solid green;
}
.sub-activity-active {
border-left: 8px solid black;
}
.sub-activity-failed {
border-left: 3px solid #d9534f;
}
......@@ -410,7 +422,7 @@ footer a, footer a:hover, footer a:visited {
color: white;
text-decoration: underline;
}
.template-disk-list {
list-style: none;
padding-left: 0;
......@@ -472,3 +484,35 @@ footer a, footer a:hover, footer a:visited {
#vm-details-resources-form {
padding: 5px; /* it's nice this way in the tour */
}
.index-vm-list-name {
display: inline-block;
max-width: 70%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
float: left;
}
#dashboard-vm-list a small {
padding-left: 10px;
}
.index-template-list-name {
display: inline-block;
max-width: 50%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
float: left;
}
#dashboard-template-list a small {
max-width: 50%;
float: left;
padding-top: 2px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding-left: 10px;
}
......@@ -3,7 +3,7 @@ $(function () {
var template = $(this).data("template");
$.ajax({
type: 'GET',
url: '/dashboard/vm/create/',
url: '/dashboard/vm/create/' + (typeof template === "undefined" ? '' : '?template=' + template),
success: function(data) {
$('body').append(data);
vmCreateLoaded();
......@@ -12,9 +12,6 @@ $(function () {
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
});
if(template) {
$('#vm-create-template-select option[value="' + template + '"]').prop("selected", true).trigger("change");
}
}
});
return false;
......@@ -209,6 +206,9 @@ $(function () {
'name': result[i].name.toLowerCase(),
'state': result[i].state,
'fav': result[i].fav,
'host': result[i].host,
'icon': result[i].icon,
'status': result[i].status,
});
}
});
......@@ -225,7 +225,9 @@ $(function () {
}
search_result.sort(compareVmByFav);
for(var i=0; i<5 && i<search_result.length; i++)
html += generateVmHTML(search_result[i].pk, search_result[i].name, search_result[i].fav);
html += generateVmHTML(search_result[i].pk, search_result[i].name,
search_result[i].host, search_result[i].icon,
search_result[i].status, search_result[i].fav);
if(search_result.length == 0)
html += '<div class="list-group-item">No result</div>';
$("#dashboard-vm-list").html(html);
......@@ -251,21 +253,33 @@ $(function () {
});
});
function generateVmHTML(pk, name, fav) {
return '<a href="/dashboard/vm/' + pk + '/" class="list-group-item">' +
'<i class="icon-play-sign"></i> ' + name +
'<div class="pull-right dashboard-vm-favourite" data-vm="' + pk +'">' +
'<i class="title-favourite icon-star' + (fav ? "" : "-empty") + ' text-primary" title="" data-original-title="' +
(fav ? "Un": "Mark as ") + 'favourite"></i>' +
'</div>' +
'</a>';
function generateVmHTML(pk, name, host, icon, _status, fav) {
return '<a href="/dashboard/vm/' + pk + '/" class="list-group-item">' +
'<span class="index-vm-list-name">' +
'<i class="' + icon + '" title="' + _status + '"></i> ' + name +
'</span>' +
'<small class="text-muted"> ' + host + '</small>' +
'<div class="pull-right dashboard-vm-favourite" data-vm="' + pk + '">' +
(fav ? '<i class="icon-star text-primary title-favourite" title="Unfavourite"></i>' :
'<i class="icon-star-empty text-primary title-favourite" title="Mark as favorite"></i>' ) +
'</div>' +
'<div style="clear: both;"></div>' +
'</a>';
}
/* copare vm-s by fav, pk order */
function compareVmByFav(a, b) {
if(a.fav)
if(a.fav && b.fav) {
return a.pk < b.pk ? -1 : 1;
}
else if(a.fav && !b.fav) {
return -1;
else
}
else if(!a.fav && b.fav) {
return 1;
}
else
return a.pk < b.pk ? -1 : 1;
}
function addSliderMiscs() {
......
......@@ -14,7 +14,6 @@ function vmCreateLoaded() {
$(".customize-vm").click(function() {
var template = $(this).data("template-pk");
console.log(template);
$.get("/dashboard/vm/create/?template=" + template, function(data) {
var r = $('#create-modal'); r.next('div').remove(); r.remove();
......@@ -142,7 +141,6 @@ function vmCustomizeLoaded() {
text = raw_text.replace("unmanaged -", "&#xf0c1;");
}
var html = '<option data-managed="' + (managed ? 1 : 0) + '" value="' + pk + '">' + text + '</option>';
if($('#vm-create-network-list span').length < 1) {
$("#vm-create-network-list").html("");
......@@ -152,8 +150,14 @@ function vmCustomizeLoaded() {
} else {
$('#vm-create-network-add-select').append(html);
}
});
// if all networks are added add a dummy and disable the add button
if($("#vm-create-network-add-select option").length < 1) {
$("#vm-create-network-add-select").html('<option value="-1">No more networks!</option>');
$('#vm-create-network-add-button').attr('disabled', true);
}
/* build up network list */
$('#vm-create-network-add-vlan option').each(function() {
......@@ -197,24 +201,14 @@ function vmCustomizeLoaded() {
/* remove disk */
// event for disk remove button (icon, X)
$('body').on('click', '.vm-create-remove-disk', function() {
var disk_pk = ($(this).parent('span').prop('id')).replace('vlan-', '')
var disk_pk = ($(this).parent('span').prop('id')).replace('disk-', '')
$(this).parent('span').fadeOut(500, function() {
/* if ther are no more disks disabled the add button */
if($('#vm-create-disk-add-select option')[0].value == -1) {
$('#vm-create-disk-add-button').attr('disabled', false);
$('#vm-create-disk-add-select').html('');
}
/* remove the disk label */
$(this).remove();
var disk_name = $(this).text();
$('#vm-create-disk-add-select').append($('<option>', {
value: disk_pk,
text: disk_name
}));
/* remove the selection from the multiple select */
$('#vm-create-disk-add-form option[value="' + disk_pk + '"]').prop('selected', false);
if ($('#vm-create-disk-list').children('span').length < 1) {
......@@ -257,7 +251,11 @@ function vmCustomizeLoaded() {
data: $('form').serialize(),
success: function(data, textStatus, xhr) {
if(data.redirect) {
window.location.replace(data.redirect + '#activity');
/* it won't redirect to the same page */
if(window.location.pathname == data.redirect) {
window.location.reload();
}
window.location.href = data.redirect + '#activity';
}
else {
var r = $('#create-modal'); r.next('div').remove(); r.remove();
......@@ -295,5 +293,6 @@ function vmCreateNetworkLabel(pk, name, managed) {
function vmCreateDiskLabel(pk, name) {
return '<span id="vlan-' + pk + '" class="label label-primary"><i class="icon-file"></i> ' + name + ' <a href="#" class="hover-black vm-create-remove-disk"><i class="icon-remove-sign"></i></a></span> ';
var style = "float: left; margin: 5px 5px 5px 0;";
return '<span id="disk-' + pk + '" class="label label-primary" style="' + style + '"><i class="icon-file"></i> ' + name + ' <a href="#" class="hover-black vm-create-remove-disk"><i class="icon-remove-sign"></i></a></span> ';
}
......@@ -199,24 +199,24 @@ function decideActivityRefresh() {
return check;
}
function checkNewActivity(only_state, runs) {
// set default only_state to false
only_state = typeof only_state !== 'undefined' ? only_state : false;
function checkNewActivity(only_status, runs) {
// set default only_status to false
only_status = typeof only_status !== 'undefined' ? only_status : false;
var instance = location.href.split('/'); instance = instance[instance.length - 2];
$.ajax({
type: 'GET',
url: '/dashboard/vm/' + instance + '/activity/',
data: {'only_state': only_state},
data: {'only_status': only_status},
success: function(data) {
if(!only_state) {
if(!only_status) {
$("#activity-timeline").html(data['activities']);
$("[title]").tooltip();
}
$("#vm-details-state i").prop("class", data['icon']);
$("#vm-details-state span").html(data['state']);
if(data['state'] == "RUNNING") {
$("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
if(data['status'] == "RUNNING") {
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
} else {
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
......@@ -224,7 +224,7 @@ function checkNewActivity(only_state, runs) {
if(runs > 0 && decideActivityRefresh()) {
setTimeout(
function() {checkNewActivity(only_state, runs + 1)},
function() {checkNewActivity(only_status, runs + 1)},
1000 + Math.exp(runs * 0.05)
);
}
......
......@@ -58,12 +58,17 @@
</li>
</ul>
<div style="margin-top: 20px; padding: 0 15px; width: 100%">
<a class="btn btn-primary btn-xs customize-vm" data-template-pk="{{ t.pk }}" href="{% url "dashboard.views.vm-create" %}?template={{ t.pk }}"><i class="icon-wrench"></i> Customize</a>
{% if perms.vm_set_resources %}
<a class="btn btn-primary btn-xs customize-vm" data-template-pk="{{ t.pk }}" href="{% url "dashboard.views.vm-create" %}?template={{ t.pk }}"><i class="icon-wrench"></i> {% trans "Customize" %}</a>
{% endif %}
<form class="pull-right text-right" method="POST" action="{% url "dashboard.views.vm-create" %}">
{% csrf_token %}
<input type="hidden" name="template" value="{{ t.pk }}"/>
<button class="btn btn-success btn-xs vm-create-start" data-template-pk="{{ t.pk }}" type="submit"><i class="icon-play"></i> Start</button>
<button class="btn btn-success btn-xs vm-create-start" data-template-pk="{{ t.pk }}" type="submit">
<i class="icon-play"></i> {% trans "Start" %}
</button>
</form>
<div style="clear: both;"></div>
</div>
</div>
</div>
......
......@@ -12,7 +12,7 @@
{%endblocktrans%}
{% endif %}
<div class="pull-right">
<form action="" method="POST">
<form action="{% url "dashboard.views.status-node" pk=object.pk %}" method="POST">
{% csrf_token %}
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button>
<input type="hidden" name="change_status" value=""/>
......
......@@ -16,7 +16,7 @@
<div class="list-group" id="node-list-view">
{% for i in nodes %}
<a href="{% url "dashboard.views.node-detail" pk=i.pk %}" class="list-group-item">
<i class="icon-{% if i.enabled == True %}play-sign{% else %}pause{% endif %}"></i> {{ i.name }} <div class="pull-right"><i class="icon-star text-primary" title="Mark as favorite."></i></div>
<i class="icon-{% if i.enabled == True %}play-sign{% else %}pause{% endif %}"></i> {{ i.name }}
</a>
{% endfor %}
<div href="#" class="list-group-item list-group-footer">
......
......@@ -7,11 +7,15 @@
<h3 class="no-margin"><i class="icon-puzzle-piece"></i> {% trans "Templates" %}
</h3>
</div>
<div class="list-group" id="vm-list-view">
<div class="list-group" id="dashboard-template-list">
{% for t in templates %}
<a href="{% url "dashboard.views.template-detail" pk=t.pk %}" class="list-group-item">
<i class="icon-{{ t.os_type }}"></i> {{ t.name }} <small class="text-muted">{{ t.system }}</small>
<span class="index-template-list-name">
<i class="icon-{{ t.os_type }}"></i> {{ t.name }}
</span>
<small class="text-muted index-template-list-system">{{ t.system }}</small>
<div class="pull-right vm-create" data-template="{{ t.pk }}"><i title="{% trans "Start vm instance" %}" class="icon-play"></i></div>
<div class="clearfix"></div>
</a>
{% empty %}
<div class="alert alert-warning" style="margin: 10px;">
......
......@@ -16,8 +16,11 @@
<div id="dashboard-vm-list">
{% for i in instances %}
<a href="{{ i.get_absolute_url }}" class="list-group-item">
<i class="{{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i> {{ i.name }}
<small class="text-muted">{{ i.primary_host.hostname }}</small>
<span class="index-vm-list-name">
<i class="{{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i>
{{ i.name }}
</span>
<small class="text-muted"> {{ i.primary_host.hostname }}</small>
<div class="pull-right dashboard-vm-favourite" data-vm="{{ i.pk }}">
{% if i.fav %}
<i class="icon-star text-primary title-favourite" title="{% trans "Unfavourite" %}"></i>
......@@ -25,6 +28,7 @@
<i class="icon-star-empty text-primary title-favourite" title="{% trans "Mark as favorite" %}"></i>
{% endif %}