Commit 1a9d84cd by Kálmán Viktor

Merge branch 'master' into issue-364

Conflicts:
	circle/dashboard/static/dashboard/vm-common.js
	circle/dashboard/templates/dashboard/store/index-files.html
parents 29adb58a e70e03b7
...@@ -198,6 +198,7 @@ PIPELINE_JS = { ...@@ -198,6 +198,7 @@ PIPELINE_JS = {
"jquery-knob/dist/jquery.knob.min.js", "jquery-knob/dist/jquery.knob.min.js",
"jquery-simple-slider/js/simple-slider.js", "jquery-simple-slider/js/simple-slider.js",
"dashboard/dashboard.js", "dashboard/dashboard.js",
"dashboard/activity.js",
"dashboard/group-details.js", "dashboard/group-details.js",
"dashboard/group-list.js", "dashboard/group-list.js",
"dashboard/js/stupidtable.min.js", # no bower file "dashboard/js/stupidtable.min.js", # no bower file
......
...@@ -282,6 +282,8 @@ def register_operation(op_cls, op_id=None, target_cls=None): ...@@ -282,6 +282,8 @@ def register_operation(op_cls, op_id=None, target_cls=None):
"in the 'target_cls' parameter to this " "in the 'target_cls' parameter to this "
"call.") "call.")
assert not hasattr(target_cls, op_id), (
"target class already has an attribute with this id")
if not issubclass(target_cls, OperatedMixin): if not issubclass(target_cls, OperatedMixin):
raise TypeError("%r is not a subclass of %r" % raise TypeError("%r is not a subclass of %r" %
(target_cls.__name__, OperatedMixin.__name__)) (target_cls.__name__, OperatedMixin.__name__))
......
...@@ -898,7 +898,7 @@ class VmDownloadDiskForm(OperationForm): ...@@ -898,7 +898,7 @@ class VmDownloadDiskForm(OperationForm):
def clean(self): def clean(self):
cleaned_data = super(VmDownloadDiskForm, self).clean() cleaned_data = super(VmDownloadDiskForm, self).clean()
if not cleaned_data['name']: if not cleaned_data['name']:
if cleaned_data['url']: if cleaned_data.get('url'):
cleaned_data['name'] = urlparse( cleaned_data['name'] = urlparse(
cleaned_data['url']).path.split('/')[-1] cleaned_data['url']).path.split('/')[-1]
if not cleaned_data['name']: if not cleaned_data['name']:
...@@ -908,6 +908,36 @@ class VmDownloadDiskForm(OperationForm): ...@@ -908,6 +908,36 @@ class VmDownloadDiskForm(OperationForm):
return cleaned_data return cleaned_data
class VmRemoveInterfaceForm(OperationForm):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.interface = kwargs.pop('default')
super(VmRemoveInterfaceForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'interface', forms.ModelChoiceField(
queryset=choices, initial=self.interface, required=True,
empty_label=None, label=_('Interface')))
if self.interface:
self.fields['interface'].widget = HiddenInput()
@property
def helper(self):
helper = super(VmRemoveInterfaceForm, self).helper
if self.interface:
helper.layout = Layout(
AnyTag(
"div",
HTML(format_html(
_("<label>Vlan:</label> {0}"),
self.interface.vlan)),
css_class="form-group",
),
Field("interface"),
)
return helper
class VmAddInterfaceForm(OperationForm): class VmAddInterfaceForm(OperationForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices') choices = kwargs.pop('choices')
......
/* for functions in both vm list and vm detail */
$(function() { $(function() {
var in_progress = false;
var activity_hash = 5;
var show_all = false;
var reload_vm_detail = false;
/* do we need to check for new activities */
if(decideActivityRefresh()) {
if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
}
/* vm operations */ $('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('fa-spin');
if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
});
$("#activity-refresh").on("click", "#show-all-activities", function() {
$(this).find("i").addClass("fa-spinner fa-spin");
show_all = !show_all;
$('a[href="#activity"]').trigger("click");
return false;
});
/* operations */
$('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface, .operation-wrapper').on('click', '.operation', function(e) { $('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface, .operation-wrapper').on('click', '.operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin'); var icon = $(this).children("i").addClass('fa-spinner fa-spin');
...@@ -23,11 +48,9 @@ $(function() { ...@@ -23,11 +48,9 @@ $(function() {
}); });
/* if the operation fails show the modal again */ /* if the operation fails show the modal again */
$("body").on("click", "#op-form-send", function() { $("body").on("click", "#confirmation-modal #op-form-send", function() {
var url = $(this).closest("form").prop("action"); var url = $(this).closest("form").prop("action");
$(this).find("i").prop("class", "fa fa-fw fa-spinner fa-spin");
$.ajax({ $.ajax({
url: url, url: url,
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
...@@ -79,4 +102,91 @@ $(function() { ...@@ -79,4 +102,91 @@ $(function() {
return false; return false;
}); });
function decideActivityRefresh() {
var check = false;
/* if something is still spinning */
if($('.timeline .activity i').hasClass('fa-spin'))
check = true;
return check;
}
function checkNewActivity(runs) {
$.ajax({
type: 'GET',
url: $('a[href="#activity"]').attr('data-activity-url'),
data: {'show_all': show_all},
success: function(data) {
var new_activity_hash = (data.activities + "").hashCode();
if(new_activity_hash != activity_hash) {
$("#activity-refresh").html(data.activities);
}
activity_hash = new_activity_hash;
$("#ops").html(data.ops);
$("#disk-ops").html(data.disk_ops);
$("[title]").tooltip();
/* changing the status text */
var icon = $("#vm-details-state i");
if(data.is_new_state) {
if(!icon.hasClass("fa-spin"))
icon.prop("class", "fa fa-spinner fa-spin");
} else {
icon.prop("class", "fa " + data.icon);
}
var vm_state = $("#vm-details-state");
if (vm_state.length) {
vm_state.data("status", data['status']);
$("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
}
if(data['status'] == "RUNNING") {
if(data['connect_uri']) {
$("#dashboard-vm-details-connect-button").removeClass('disabled');
}
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
} else {
if(data['connect_uri']) {
$("#dashboard-vm-details-connect-button").addClass('disabled');
}
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
}
if(data.status == "STOPPED" || data.status == "PENDING") {
$(".change-resources-button").prop("disabled", false);
$(".change-resources-help").hide();
} else {
$(".change-resources-button").prop("disabled", true);
$(".change-resources-help").show();
}
if(runs > 0 && decideActivityRefresh()) {
setTimeout(
function() {checkNewActivity(runs + 1);},
1000 + Math.exp(runs * 0.05)
);
} else {
in_progress = false;
if(reload_vm_detail) location.reload();
}
$('a[href="#activity"] i').removeClass('fa-spin');
},
error: function() {
in_progress = false;
}
});
}
}); });
String.prototype.hashCode = function() {
var hash = 0, i, chr, len;
if (this.length == 0) return hash;
for (i = 0, len = this.length; i < len; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
...@@ -90,8 +90,11 @@ html { ...@@ -90,8 +90,11 @@ html {
} }
.list-group .list-group-footer { .list-group .list-group-footer {
padding-top: 5px; height: 41px;
padding-bottom: 5px; }
.list-group-footer .text-right {
padding-top: 4px;
} }
.big { .big {
...@@ -199,7 +202,7 @@ html { ...@@ -199,7 +202,7 @@ html {
} }
.dashboard-index .panel { .dashboard-index .panel {
height: 300px; height: 294px;
} }
#vm-details-rename, #vm-details-h1-name, #vm-details-rename , #vm-details-rename, #vm-details-h1-name, #vm-details-rename ,
...@@ -586,8 +589,8 @@ footer a, footer a:hover, footer a:visited { ...@@ -586,8 +589,8 @@ footer a, footer a:hover, footer a:visited {
} }
#dashboard-vm-list, #dashboard-node-list, #dashboard-group-list, #dashboard-vm-list, #dashboard-node-list, #dashboard-group-list,
#dashboard-template-list { #dashboard-template-list, #dashboard-files-toplist {
min-height: 204px; min-height: 200px;
} }
#group-detail-user-table td:first-child, #group-detail-user-table th:last-child, #group-detail-user-table td:first-child, #group-detail-user-table th:last-child,
...@@ -749,10 +752,6 @@ textarea[name="new_members"] { ...@@ -749,10 +752,6 @@ textarea[name="new_members"] {
margin-top: 8px; margin-top: 8px;
} }
#dashboard-files-toplist {
min-height: 204px;
}
#dashboard-files-toplist div.list-group-item { #dashboard-files-toplist div.list-group-item {
color: #555; color: #555;
} }
...@@ -957,6 +956,11 @@ textarea[name="new_members"] { ...@@ -957,6 +956,11 @@ textarea[name="new_members"] {
#vm-list-search, #vm-mass-ops { #vm-list-search, #vm-mass-ops {
margin-top: 8px; margin-top: 8px;
} }
.list-group-item {
border-bottom: 0px !important;
}
.list-group-item-last { .list-group-item-last {
border-bottom: 1px solid #ddd !important; border-bottom: 1px solid #ddd !important;
} }
......
...@@ -28,39 +28,3 @@ ...@@ -28,39 +28,3 @@
$(".group-details-help-button").click(function() { $(".group-details-help-button").click(function() {
$(".group-details-help").stop().slideToggle(); $(".group-details-help").stop().slideToggle();
}); });
/* for Node removes buttons */
$('.delete-from-group').click(function() {
var href = $(this).attr('href');
var tr = $(this).closest('tr');
var group = $(this).data('group_pk');
var member = $(this).data('member_pk');
var dir = window.location.pathname.indexOf('list') == -1;
addModalConfirmation(removeMember,
{ 'url': href,
'data': [],
'tr': tr,
'group_pk': group,
'member_pk': member,
'type': "user",
'redirect': dir});
return false;
});
function removeMember(data) {
$.ajax({
type: 'POST',
url: data.url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
data.tr.fadeOut(function() {
$(this).remove();});
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger');
}
});
}
$(function() { $(function() {
/* rename */ /* rename */
$("#group-list-rename-button, .group-details-rename-button").click(function() { $("#group-list-rename-button, .group-details-rename-button").click(function() {
$("#group-list-column-name", $(this).closest("tr")).hide(); $(".group-list-column-name", $(this).closest("tr")).hide();
$("#group-list-rename", $(this).closest("tr")).css('display', 'inline'); $("#group-list-rename", $(this).closest("tr")).css('display', 'inline');
$("#group-list-rename").find("input").select(); $("#group-list-rename").find("input").select();
}); });
...@@ -10,7 +10,7 @@ $(function() { ...@@ -10,7 +10,7 @@ $(function() {
$('.group-list-rename-submit').click(function() { $('.group-list-rename-submit').click(function() {
var row = $(this).closest("tr"); var row = $(this).closest("tr");
var name = $('#group-list-rename-name', row).val(); var name = $('#group-list-rename-name', row).val();
var url = '/dashboard/group/' + row.children("td:first-child").text().replace(" ", "") + '/'; var url = row.find(".group-list-column-name a").prop("href");
$.ajax({ $.ajax({
method: 'POST', method: 'POST',
url: url, url: url,
...@@ -18,7 +18,7 @@ $(function() { ...@@ -18,7 +18,7 @@ $(function() {
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) { success: function(data, textStatus, xhr) {
$("#group-list-column-name", row).html( $(".group-list-column-name", row).html(
$("<a/>", { $("<a/>", {
'class': "real-link", 'class': "real-link",
href: "/dashboard/group/" + data.group_pk + "/", href: "/dashboard/group/" + data.group_pk + "/",
......
$(function() {
nodeCreateLoaded();
});
function nodeCreateLoaded() {
/* no js compatibility */
$('.no-js-hidden').show();
$('.js-hidden').hide();
}
...@@ -30,20 +30,6 @@ $(function() { ...@@ -30,20 +30,6 @@ $(function() {
$(".node-details-help").stop().slideToggle(); $(".node-details-help").stop().slideToggle();
}); });
/* for Node removes buttons */
$('.node-enable').click(function() {
var node_pk = $(this).data('node-pk');
var dir = window.location.pathname.indexOf('list') == -1;
addModalConfirmation(changeNodeStatus,
{ 'url': '/dashboard/node/status/' + node_pk + '/',
'data': [],
'pk': node_pk,
'type': "node",
'redirect': dir});
return false;
});
// remove trait // remove trait
$('.node-details-remove-trait').click(function() { $('.node-details-remove-trait').click(function() {
var to_remove = $(this).data("trait-pk"); var to_remove = $(this).data("trait-pk");
...@@ -69,22 +55,3 @@ $(function() { ...@@ -69,22 +55,3 @@ $(function() {
}); });
}); });
function changeNodeStatus(data) {
$.ajax({
type: 'POST',
url: data.url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
if(!data.redirect) {
selected = [];
addMessage(re.message, 'success');
} else {
window.location.replace('/dashboard');
}
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger');
}
});
}
...@@ -9,49 +9,4 @@ $(function() { ...@@ -9,49 +9,4 @@ $(function() {
$('.false').closest("tr").addClass('danger'); $('.false').closest("tr").addClass('danger');
$('.true').closest("tr").removeClass('danger'); $('.true').closest("tr").removeClass('danger');
} }
function statuschangeSuccess(tr){
var tspan=tr.children('.enabled').children();
var buttons=tr.children('.actions').children('.btn-group').children('.dropdown-menu').children('li').children('.node-enable');
buttons.each(function(index){
if ($(this).css("display")=="block"){
$(this).css("display","none");
}
else{
$(this).css("display","block");
}
});
if(tspan.hasClass("false")){
tspan.removeClass("false");
tspan.addClass("true");
tspan.text("✔");
}
else{
tspan.removeClass("true");
tspan.addClass("false");
tspan.text("✘");
}
colortable();
}
$('#table_container').on('click','.node-enable',function() {
var tr= $(this).closest("tr");
var pk =$(this).attr('data-node-pk');
var url = $(this).attr('href');
$.ajax({
method: 'POST',
url: url,
data: {'change_status':''},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
statuschangeSuccess(tr);
},
error: function(xhr, textStatus, error) {
addMessage("Error!", "danger");
}
});
return false;
});
}); });
$(function() { $(function() {
/* for template removes buttons */
$('.template-delete').click(function() {
var template_pk = $(this).data('template-pk');
addModalConfirmationOrDisplayMessage(deleteTemplate,
{ 'url': '/dashboard/template/delete/' + template_pk + '/',
'data': [],
'template_pk': template_pk,
});
return false;
});
/* for lease removes buttons */
$('.lease-delete').click(function() {
var lease_pk = $(this).data('lease-pk');
addModalConfirmationOrDisplayMessage(deleteLease,
{ 'url': '/dashboard/lease/delete/' + lease_pk + '/',
'data': [],
'lease_pk': lease_pk,
});
return false;
});
/* template table sort */ /* template table sort */
var ttable = $(".template-list-table").stupidtable(); var ttable = $(".template-list-table").stupidtable();
...@@ -43,67 +21,3 @@ $(function() { ...@@ -43,67 +21,3 @@ $(function() {
event.preventDefault(); event.preventDefault();
}); });
}); });
// send POST request then delete the row in table
function deleteTemplate(data) {
$.ajax({
type: 'POST',
url: data.url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
addMessage(re.message, 'success');
$('a[data-template-pk="' + data.template_pk + '"]').closest('tr').fadeOut(function() {
$(this).remove();
});
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger');
}
});
}
// send POST request then delete the row in table
function deleteLease(data) {
$.ajax({
type: 'POST',
url: data.url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
addMessage(re.message, 'success');
$('a[data-lease-pk="' + data.lease_pk + '"]').closest('tr').fadeOut(function() {
$(this).remove();
});
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger');
}
});
}
function addModalConfirmationOrDisplayMessage(func, data) {
$.ajax({
type: 'GET',
url: data['url'],
data: jQuery.param(data['data']),
success: function(result) {
$('body').append(result);
$('#confirmation-modal').modal('show');
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
$('#confirmation-modal-button').click(function() {
func(data);
$('#confirmation-modal').modal('hide');
});
},
error: function(xhr, textStatus, error) {
if(xhr.status === 403) {
addMessage(gettext("Only the owners can delete the selected object."), "warning");
} else {
addMessage(gettext("An error occurred. (") + xhr.status + ")", 'danger')
}
}
});
}
...@@ -20,15 +20,15 @@ function vmCreateLoaded() { ...@@ -20,15 +20,15 @@ function vmCreateLoaded() {
var template = $(this).data("template-pk"); var template = $(this).data("template-pk");
$.get("/dashboard/vm/create/?template=" + template, function(data) { $.get("/dashboard/vm/create/?template=" + template, function(data) {
var r = $('#create-modal'); r.next('div').remove(); r.remove(); var r = $('#confirmation-modal'); r.next('div').remove(); r.remove();
$('body').append(data); $('body').append(data);
vmCreateLoaded(); vmCreateLoaded();
addSliderMiscs(); addSliderMiscs();
$('#create-modal').modal('show'); $('#confirmation-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() { $('#confirmation-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove(); $('#confirmation-modal').remove();
}); });
$("#create-modal").on("shown.bs.modal", function() { $("#confirmation-modal").on("shown.bs.modal", function() {
setDefaultSliderValues(); setDefaultSliderValues();
}); });
}); });
...@@ -48,18 +48,18 @@ function vmCreateLoaded() { ...@@ -48,18 +48,18 @@ function vmCreateLoaded() {
window.location.replace(data.redirect + '#activity'); window.location.replace(data.redirect + '#activity');
} }
else { else {
var r = $('#create-modal'); r.next('div').remove(); r.remove(); var r = $('#confirmation-modal'); r.next('div').remove(); r.remove();
$('body').append(data); $('body').append(data);
vmCreateLoaded(); vmCreateLoaded();
addSliderMiscs(); addSliderMiscs();
$('#create-modal').modal('show'); $('#confirmation-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() { $('#confirmation-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove(); $('#confirmation-modal').remove();
}); });
} }
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
var r = $('#create-modal'); r.next('div').remove(); r.remove(); var r = $('#confirmation-modal'); r.next('div').remove(); r.remove();
if (xhr.status == 500) { if (xhr.status == 500) {
addMessage("500 Internal Server Error", "danger"); addMessage("500 Internal Server Error", "danger");
...@@ -211,7 +211,7 @@ function vmCustomizeLoaded() { ...@@ -211,7 +211,7 @@ function vmCustomizeLoaded() {
}); });
/* start vm button clicks */ /* start vm button clicks */
$('#vm-create-customized-start').click(function() { $('#confirmation-modal #vm-create-customized-start').click(function() {
var error = false; var error = false;
$(".cpu-count-input, .ram-input, #id_name, #id_amount ").each(function() { $(".cpu-count-input, .ram-input, #id_name, #id_amount ").each(function() {
if(!$(this)[0].checkValidity()) { if(!$(this)[0].checkValidity()) {
...@@ -222,8 +222,6 @@ function vmCustomizeLoaded() { ...@@ -222,8 +222,6 @@ function vmCustomizeLoaded() {
$(this).find("i").prop("class", "fa fa-spinner fa-spin"); $(this).find("i").prop("class", "fa fa-spinner fa-spin");
if($("#create-modal")) return true;
$.ajax({ $.ajax({
url: '/dashboard/vm/create/', url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
...@@ -238,18 +236,18 @@ function vmCustomizeLoaded() { ...@@ -238,18 +236,18 @@ function vmCustomizeLoaded() {
window.location.href = data.redirect + '#activity'; window.location.href = data.redirect + '#activity';
} }
else { else {
var r = $('#create-modal'); r.next('div').remove(); r.remove(); var r = $('#confirmation-modal'); r.next('div').remove(); r.remove();
$('body').append(data); $('body').append(data);
vmCreateLoaded(); vmCreateLoaded();
addSliderMiscs(); addSliderMiscs();
$('#create-modal').modal('show'); $('#confirmation-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() { $('#confirmation-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove(); $('#confirmation-modal').remove();
}); });
} }
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
var r = $('#create-modal'); r.next('div').remove(); r.remove(); var r = $('#confirmation-modal'); r.next('div').remove(); r.remove();
if (xhr.status == 500) { if (xhr.status == 500) {
addMessage("500 Internal Server Error", "danger"); addMessage("500 Internal Server Error", "danger");
......
var show_all = false;
var in_progress = false;
var activity_hash = 5;
var Websock_native; // not sure var Websock_native; // not sure
var reload_vm_detail = false;
$(function() { $(function() {
/* do we need to check for new activities */
if(decideActivityRefresh()) {
if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
}
$('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('fa-spin');
if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
});
$("#activity-refresh").on("click", "#show-all-activities", function() {
$(this).find("i").addClass("fa-spinner fa-spin");
show_all = !show_all;
$('a[href="#activity"]').trigger("click");
return false;
});
/* save resources */ /* save resources */
$('#vm-details-resources-save').click(function(e) { $('#vm-details-resources-save').click(function(e) {
var error = false; var error = false;
...@@ -43,7 +16,7 @@ $(function() { ...@@ -43,7 +16,7 @@ $(function() {
var vm = $(this).data("vm"); var vm = $(this).data("vm");
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: "/dashboard/vm/" + vm + "/op/resources_change/", url: $(this).parent("form").prop('action'),
data: $('#vm-details-resources-form').serialize(), data: $('#vm-details-resources-form').serialize(),
success: function(data, textStatus, xhr) { success: function(data, textStatus, xhr) {
if(data.success) { if(data.success) {
...@@ -89,17 +62,6 @@ $(function() { ...@@ -89,17 +62,6 @@ $(function() {
return false; return false;
}); });
/* remove port */
$('.vm-details-remove-port').click(function() {
addModalConfirmation(removePort,
{
'url': $(this).prop("href"),
'data': [],
'rule': $(this).data("rule")
});
return false;
});
/* for js fallback */ /* for js fallback */
$("#vm-details-pw-show").parent("div").children("input").prop("type", "password"); $("#vm-details-pw-show").parent("div").children("input").prop("type", "password");
...@@ -123,80 +85,6 @@ $(function() { ...@@ -123,80 +85,6 @@ $(function() {
span.tooltip(); span.tooltip();
}); });
/* change password confirmation */
$("#vm-details-pw-change").click(function() {
$("#vm-details-pw-confirm").fadeIn();
return false;
});
/* change password */
$(".vm-details-pw-confirm-choice").click(function() {
choice = $(this).data("choice");
if(choice) {
pk = $(this).data("vm");
$.ajax({
type: 'POST',
url: "/dashboard/vm/" + pk + "/",
data: {'change_password': 'true'},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
location.reload();
},
error: function(xhr, textStatus, error) {
if (xhr.status == 500) {
addMessage("Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
}
});
} else {
$("#vm-details-pw-confirm").fadeOut();
}
return false;
});
/* add network button */
$("#vm-details-network-add").click(function() {
$("#vm-details-network-add-form").toggle();
return false;
});
/* add disk button */
$("#vm-details-disk-add").click(function() {
$("#vm-details-disk-add-for-form").html($("#vm-details-disk-add-form").html());
return false;
});
/* for interface remove buttons */
$('.interface-remove').click(function() {
var interface_pk = $(this).data('interface-pk');
addModalConfirmation(removeInterface,
{ 'url': '/dashboard/interface/' + interface_pk + '/delete/',
'data': [],
'pk': interface_pk,
'type': "interface",
});
return false;
});
/* removing interface post */
function removeInterface(data) {
$.ajax({
type: 'POST',
url: data.url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
/* remove the html element */
$('a[data-interface-pk="' + data.pk + '"]').closest("div").fadeOut();
location.reload();
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger');
}
});
}
/* rename */ /* rename */
$("#vm-details-h1-name, .vm-details-rename-button").click(function() { $("#vm-details-h1-name, .vm-details-rename-button").click(function() {
$("#vm-details-h1-name").hide(); $("#vm-details-h1-name").hide();
...@@ -336,109 +224,3 @@ $(function() { ...@@ -336,109 +224,3 @@ $(function() {
}); });
}); });
function removePort(data) {
$.ajax({
type: 'POST',
url: data.url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
$("a[data-rule=" + data.rule + "]").each(function() {
$(this).closest("tr").fadeOut(500, function() {
$(this).remove();
});
});
addMessage(re.message, "success");
},
error: function(xhr, textStatus, error) {
}
});
}
function decideActivityRefresh() {
var check = false;
/* if something is still spinning */
if($('.timeline .activity i').hasClass('fa-spin'))
check = true;
return check;
}
function checkNewActivity(runs) {
var instance = location.href.split('/'); instance = instance[instance.length - 2];
$.ajax({
type: 'GET',
url: '/dashboard/vm/' + instance + '/activity/',
data: {'show_all': show_all},
success: function(data) {
var new_activity_hash = (data.activities + "").hashCode();
if(new_activity_hash != activity_hash) {
$("#activity-refresh").html(data.activities);
}
activity_hash = new_activity_hash;
$("#ops").html(data.ops);
$("#disk-ops").html(data.disk_ops);
$("[title]").tooltip();
/* changing the status text */
var icon = $("#vm-details-state i");
if(data.is_new_state) {
if(!icon.hasClass("fa-spin"))
icon.prop("class", "fa fa-spinner fa-spin");
} else {
icon.prop("class", "fa " + data.icon);
}
$("#vm-details-state").data("status", data['status']);
$("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
if(data['status'] == "RUNNING") {
if(data['connect_uri']) {
$("#dashboard-vm-details-connect-button").removeClass('disabled');
}
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
} else {
if(data['connect_uri']) {
$("#dashboard-vm-details-connect-button").addClass('disabled');
}
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
}
if(data.status == "STOPPED" || data.status == "PENDING") {
$(".change-resources-button").prop("disabled", false);
$(".change-resources-help").hide();
} else {
$(".change-resources-button").prop("disabled", true);
$(".change-resources-help").show();
}
if(runs > 0 && decideActivityRefresh()) {
setTimeout(
function() {checkNewActivity(runs + 1);},
1000 + Math.exp(runs * 0.05)
);
} else {
in_progress = false;
if(reload_vm_detail) location.reload();
}
$('a[href="#activity"] i').removeClass('fa-spin');
},
error: function() {
in_progress = false;
}
});
}
String.prototype.hashCode = function() {
var hash = 0, i, chr, len;
if (this.length === 0) return hash;
for (i = 0, len = this.length; i < len; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
...@@ -19,7 +19,8 @@ from __future__ import absolute_import ...@@ -19,7 +19,8 @@ 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, LinkColumn from django_tables2.columns import (TemplateColumn, Column, LinkColumn,
BooleanColumn)
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 _
...@@ -67,12 +68,18 @@ class NodeListTable(Table): ...@@ -67,12 +68,18 @@ class NodeListTable(Table):
orderable=False, orderable=False,
) )
minion_online = BooleanColumn(
verbose_name=_("Minion online"),
attrs={'th': {'class': 'node-list-table-thin'}},
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', 'get_status_display', 'priority', fields = ('pk', 'name', 'host', 'get_status_display', 'priority',
'overcommit', 'number_of_VMs', ) 'minion_online', 'overcommit', 'number_of_VMs', )
class GroupListTable(Table): class GroupListTable(Table):
......
...@@ -64,7 +64,6 @@ ...@@ -64,7 +64,6 @@
<a href="{% url "info.support" %}">{% trans "Support" %}</a> <a href="{% url "info.support" %}">{% trans "Support" %}</a>
<span class="pull-right">{{ COMPANY_NAME }}</span> <span class="pull-right">{{ COMPANY_NAME }}</span>
</footer> </footer>
</body>
<script src="{% static "jquery/dist/jquery.min.js" %}"></script> <script src="{% static "jquery/dist/jquery.min.js" %}"></script>
<script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script> <script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script>
...@@ -78,4 +77,5 @@ ...@@ -78,4 +77,5 @@
{% block extra_etc %} {% block extra_etc %}
{% endblock %} {% endblock %}
</body>
</html> </html>
<img src="{{ STATIC_URL}}dashboard/img/logo.png" style="height: 25px;"/> <img src="{{ STATIC_URL}}dashboard/img/logo.png" alt="circle logo" style="height: 25px;"/>
<img src="{{ STATIC_URL}}local-logo.png" style="padding-left: 2px; height: 25px;"/> <img src="{{ STATIC_URL}}local-logo.png" alt="local logo" style="padding-left: 2px; height: 25px;"/>
...@@ -3,19 +3,25 @@ ...@@ -3,19 +3,25 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-body"> <div class="modal-body">
{% if text %} {% if member %}
{{ text|safe }} {% blocktrans with group=object member=member %}
Do you really want to remove <strong>{{ member }}</strong> from {{ group }}?
{% endblocktrans %}
{% else %} {% else %}
{%blocktrans with object=object%} {% blocktrans with object=object %}
Are you sure you want to delete <strong>{{ object }}</strong>? Are you sure you want to delete <strong>{{ object }}</strong>?
{%endblocktrans%} {% endblocktrans %}
{% endif %} {% endif %}
<br /> <br />
<div class="pull-right" style="margin-top: 15px;"> <div class="pull-right" style="margin-top: 15px;">
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button> <form action="{{ request.path }}" method="POST">
<button id="confirmation-modal-button" type="button" class="btn btn-danger" {% csrf_token %}
{% if disable_submit %}disabled{% endif %} <button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button>
>{% trans "Delete" %}</button> <input type="hidden" name="next" value="{{ request.GET.next }}"/>
<button class="btn btn-danger"
{% if disable_submit %}disabled{% endif %}
>{% trans "Delete" %}</button>
</form>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
......
{% 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 change <strong>{{ object }}</strong> status?
{%endblocktrans%}
{% endif %}
<div class="pull-right">
<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=""/>
<button class="btn btn-warning">{% blocktrans with status=status %}Yes, {{status}}{% endblocktrans %}</button>
</form>
</div>
<div class="clearfix"></div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
{% 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 remove <strong>{{ member }}</strong> from <strong>{{ object }}</strong>?
{%endblocktrans%}
{% endif %}
<br />
<div class="pull-right" style="margin-top: 15px;">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button id="confirmation-modal-button" type="button" class="btn btn-warning">Remove</button>
</div>
<div class="clearfix"></div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
...@@ -17,12 +17,18 @@ ...@@ -17,12 +17,18 @@
{% if text %} {% if text %}
{{ text|safe }} {{ text|safe }}
{% else %} {% else %}
{%blocktrans with object=object%} {% if member %}
Are you sure you want to delete <strong>{{ object }}</strong>? {% blocktrans with group=object member=member %}
{%endblocktrans%} Do you really want to remove <strong>{{ member }}</strong> from {{ group }}?
{% endblocktrans %}
{% else %}
{% blocktrans with object=object %}
Are you sure you want to delete <strong>{{ object }}</strong>?
{% endblocktrans %}
{% endif %}
{% endif %} {% endif %}
<div class="pull-right"> <div class="pull-right">
<form action="" method="POST"> <form action="{{ request.path }}" method="POST">
{% csrf_token %} {% csrf_token %}
<a class="btn btn-default">{% trans "Cancel" %}</a> <a class="btn btn-default">{% trans "Cancel" %}</a>
<input type="hidden" name="next" value="{{ request.GET.next }}"/> <input type="hidden" name="next" value="{{ request.GET.next }}"/>
......
{% extends "base.html" %}
{% load i18n %}
{% block title-site %}Dashboard | CIRCLE{% endblock %}
{% block content %}
{% blocktrans with group=object member=member %}
Do you really want to remove {{member}} from {{group}}?
{% endblocktrans %}
<form action="" method="POST">{% csrf_token %}
<input type="submit" value="{% trans "Remove" %}" />
</form>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{%blocktrans with instance=instance.name%}
Renewing <em>{{instance}}</em>
{%endblocktrans%}
</h3>
</div>
<div class="panel-body">
{%blocktrans with object=instance.name%}
Do you want to renew <strong>{{ object }}</strong>?
{%endblocktrans%}
{%blocktrans with suspend=time_of_suspend delete=time_of_delete|default:"n/a" %}
The instance will be suspended at <em>{{suspend}}</em>
and removed at <em>{{delete}}</em> if you renew it now.
{%endblocktrans%}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a class="btn btn-default"
href="{{instance.get_absolute_path}}">{% trans "Back" %}</a>
<button class="btn btn-danger">{% trans "Renew" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
{% 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">
{% trans "Are you sure you want to delete the following objects?" %}<br />
{% for o in objects %}
<strong>{{ o }}</strong>{% if not forloop.last %},{% endif %}
{% endfor %}
<div class="pull-right" style="margin-top: 40px;">
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button>
<button id="confirmation-modal-button" type="button" class="btn btn-danger">{% trans "Delete" %}</button>
</div>
<div class="clearfix"></div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{% if title %}
{{ title }}
{% else %}
Flush confirmation
{% endif %}
</h3>
</div>
<div class="panel-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="" method="POST">
{% csrf_token %}
<a class="btn btn-default">{% trans "Back" %}</a>
<input type="hidden" name="flush" value=""/>
<button class="btn btn-warning">{% trans "Yes" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{% if title %}
{{ title }}
{% else %}
{% trans "Status changing confirmation" %}
{% endif %}
</h3>
</div>
<div class="panel-body">
{% if text %}
{{ text }}
{% else %}
{%blocktrans with object=object%}
Are you sure you want to change <strong>{{ object }}</strong> status?
{%endblocktrans%}
{% endif %}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a class="btn btn-default">{% trans "Cancel" %}</a>
<button type="button" class="btn btn-default" data-dismiss="modal"></button>
<input type="hidden" name="change_status" value=""/>
<button class="btn btn-warning">{% blocktrans with status=status %}Yes, {{status}}{% endblocktrans %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
...@@ -7,6 +7,6 @@ ...@@ -7,6 +7,6 @@
<button type="submit" class="group-list-rename-submit btn btn-sm">{% trans "Rename" %}</button> <button type="submit" class="group-list-rename-submit btn btn-sm">{% trans "Rename" %}</button>
</form> </form>
</div> </div>
<div id="group-list-column-name"> <div class="group-list-column-name">
<a class="real-link" href="{% url "dashboard.views.group-detail" pk=record.pk %}">{{ record.name }}</a> <a class="real-link" href="{% url "dashboard.views.group-detail" pk=record.pk %}">{{ record.name }}</a>
</div> </div>
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
</div> </div>
<h3 class="no-margin"><i class="fa fa-group"></i> {% trans "Groups" %}</h3> <h3 class="no-margin"><i class="fa fa-group"></i> {% trans "Groups" %}</h3>
</div> </div>
<div class="list-group" id="vm-list-view"> <div class="list-group" id="group-list-view">
<div id="dashboard-group-list"> <div id="dashboard-group-list">
{% for i in groups %} {% for i in groups %}
<a href="{% url "dashboard.views.group-detail" pk=i.pk %}" class="list-group-item real-link <a href="{% url "dashboard.views.group-detail" pk=i.pk %}" class="list-group-item real-link
...@@ -15,14 +15,14 @@ ...@@ -15,14 +15,14 @@
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
<div href="#" class="list-group-item list-group-footer text-right"> <div class="list-group-item list-group-footer text-right">
<div class="row"> <div class="row">
<div class="col-xs-6"> <div class="col-xs-6">
<form action="{% url "dashboard.views.group-list" %}" method="GET" id="dashboard-group-search-form"> <form action="{% url "dashboard.views.group-list" %}" method="GET" id="dashboard-group-search-form">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input id="dashboard-group-search-input" name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" /> <input id="dashboard-group-search-input" name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary"><i class="fa fa-search"></i></button> <button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div> </div>
</div> </div>
</form> </form>
......
...@@ -29,9 +29,43 @@ ...@@ -29,9 +29,43 @@
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
<div class="list-group-item list-group-footer">
<div class="row">
<div class="col-xs-6">
<form action="{% url "dashboard.views.node-list" %}" method="GET"
id="dashboard-node-search-form">
<div class="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="btn btn-primary" title="{% trans "Search" %}" data-container="body">
<i class="fa fa-search"></i>
</button>
</div>
</div>
</form>
</div>
<div class="col-xs-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>
{% if request.user.is_superuser %}
<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>
{% endif %}
</div>
</div>
</div>
</div><!-- #node-list-view --> </div><!-- #node-list-view -->
<div class="panel-body" id="node-graph-view" style="display: none; min-height: 204px;"> <div class="panel-body" id="node-graph-view" style="display: none">
<p class="pull-right"> <p class="pull-right">
<input class="knob" data-fgColor="chartreuse" <input class="knob" data-fgColor="chartreuse"
data-thickness=".4" data-width="60" data-height="60" data-readOnly="true" data-thickness=".4" data-width="60" data-height="60" data-readOnly="true"
...@@ -46,45 +80,14 @@ ...@@ -46,45 +80,14 @@
</p> </p>
<ul class="list-inline" id="dashboard-node-taglist"> <ul class="list-inline" id="dashboard-node-taglist">
{% for i in nodes %} {% for i in nodes %}
<a href="{{ i.get_absolute_url }}" class="label {{i.get_status_label}}" > <li>
<i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i> {{ i.name }}</a> <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>
</li>
{% endfor %} {% endfor %}
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div href="#" class="list-group-item list-group-footer">
<div class="row">
<div class="col-xs-6">
<form action="{% url "dashboard.views.node-list" %}" method="GET"
id="dashboard-node-search-form">
<div class="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="btn btn-primary" title="{% trans "Search" %}" data-container="body">
<i class="fa fa-search"></i>
</button>
</div>
</div>
</form>
</div>
<div class="col-xs-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>
{% if request.user.is_superuser %}
<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>
{% endif %}
</div>
</div>
</div>
</div> </div>
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %} <h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %}
</h3> </h3>
</div> </div>
<div class="list-group" id="dashboard-template-list"> <div class="list-group" id="template-list-view">
<div id="dashboard-template-list"> <div id="dashboard-template-list">
{% for t in templates %} {% for t in templates %}
<a href="{% url "dashboard.views.template-detail" pk=t.pk %}" class="list-group-item <a href="{% url "dashboard.views.template-detail" pk=t.pk %}" class="list-group-item
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<i class="fa fa-{{ t.os_type }}"></i> {{ t.name }} <i class="fa fa-{{ t.os_type }}"></i> {{ t.name }}
</span> </span>
<small class="text-muted index-template-list-system">{{ t.system }}</small> <small class="text-muted index-template-list-system">{{ t.system }}</small>
<div class="pull-right vm-create" data-template="{{ t.pk }}"> <div href="{% url "dashboard.views.vm-create" %}?template={{ t.pk }}" class="pull-right vm-create">
<i data-container="body" title="{% trans "Start VM instance" %}" <i data-container="body" title="{% trans "Start VM instance" %}"
class="fa fa-play"></i> class="fa fa-play"></i>
</div> </div>
...@@ -32,15 +32,15 @@ ...@@ -32,15 +32,15 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<div href="#" class="list-group-item list-group-footer text-right"> <div class="list-group-item list-group-footer">
<p> <div class="text-right">
<a href="{% url "dashboard.views.template-list" %}" class="btn btn-primary btn-xs"> <a href="{% url "dashboard.views.template-list" %}" class="btn btn-primary btn-xs">
<i class="fa fa-chevron-circle-right"></i> {% trans "show all" %} <i class="fa fa-chevron-circle-right"></i> {% trans "show all" %}
</a> </a>
<a href="{% url "dashboard.views.template-choose" %}" class="btn btn-success btn-xs template-choose"> <a href="{% url "dashboard.views.template-choose" %}" class="btn btn-success btn-xs template-choose">
<i class="fa fa-plus-circle"></i> {% trans "new" %} <i class="fa fa-plus-circle"></i> {% trans "new" %}
</a> </a>
</p> </div>
</div> </div>
</div> </div>
</div> </div>
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<div href="#" class="list-group-item list-group-footer"> <div class="list-group-item list-group-footer">
<div class="row"> <div class="row">
<div class="col-xs-6"> <div class="col-xs-6">
<form action="{% url "dashboard.views.vm-list" %}" method="GET" id="dashboard-vm-search-form"> <form action="{% url "dashboard.views.vm-list" %}" method="GET" id="dashboard-vm-search-form">
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
<input id="dashboard-vm-search-input" type="text" class="form-control" name="s" <input id="dashboard-vm-search-input" type="text" class="form-control" name="s"
placeholder="{% trans "Search..." %}" /> placeholder="{% trans "Search..." %}" />
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary"><i class="fa fa-search"></i></button> <button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div> </div>
</div> </div>
</form> </form>
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
<p class="pull-right"> <p class="pull-right">
<input class="knob" data-fgColor="chartreuse" data-thickness=".4" data-max="{{ request.user.profile.instance_limit }}" data-width="100" data-height="100" data-readOnly="true" value="{{ instances|length|add:more_instances }}"> <input class="knob" data-fgColor="chartreuse" data-thickness=".4" data-max="{{ request.user.profile.instance_limit }}" data-width="100" data-height="100" data-readOnly="true" value="{{ instances|length|add:more_instances }}">
</p> </p>
<p><span class="bigbig">{% blocktrans with count=running_vm_num %}<big>{{ count }}</big> running{% endblocktrans %}</span> <span class="bigbig">{% blocktrans with count=running_vm_num %}<big>{{ count }}</big> running{% endblocktrans %}</span>
<ul class="list-inline" style="max-height: 95px; overflow: hidden;"> <ul class="list-inline" style="max-height: 95px; overflow: hidden;">
{% for vm in running_vms %} {% for vm in running_vms %}
<li style="display: inline-block; padding: 2px;"> <li style="display: inline-block; padding: 2px;">
...@@ -96,7 +96,6 @@ ...@@ -96,7 +96,6 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</p>
<div class="clearfix"></div> <div class="clearfix"></div>
<div> <div>
......
<div class="modal fade" id="create-modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
{% if box_title and ajax_title %}
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{{ box_title }}</h4>
</div>
{% endif %}
<div class="modal-body">
{% include template %}
</div>
<!--<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>-->
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
...@@ -80,7 +80,8 @@ ...@@ -80,7 +80,8 @@
</a> </a>
</li> </li>
<li> <li>
<a href="#activity" data-toggle="pill" class="text-center"> <a href="#activity" data-toggle="pill" class="text-center"
data-activity-url="{% url "dashboard.views.node-activity-list" node.pk %}">
<i class="fa fa-clock-o fa-2x"></i><br> <i class="fa fa-clock-o fa-2x"></i><br>
{% trans "Activity" %} {% trans "Activity" %}
</a> </a>
......
{% load i18n %} {% load i18n %}
{% load hro %} {% load hro %}
<div id="activity-timeline" class="timeline"> <div id="activity-timeline" class="timeline">
{% for a in activities %} {% for a in activities %}
<div class="activity" data-activity-id="{{ a.pk }}"> <div class="activity" data-activity-id="{{ a.pk }}">
<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-plus{% endif %}"></i> <i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-plus{% endif %}"></i>
</span> </span>
<strong title="{{ a.result.get_admin_text }}"> <strong title="{{ a.result.get_admin_text }}">
{{ a.readable_name.get_admin_text|capfirst }} {{ a.readable_name.get_admin_text|capfirst }}
</strong> </strong>
{{ a.started|date:"Y-m-d H:i" }}, {{ a.user }} {{ a.started|date:"Y-m-d H:i" }}{% if a.user %}, {{ a.user }}{% endif %}
{% if a.children.count > 0 %} {% if a.children.count > 0 %}
<div class="sub-timeline"> <div class="sub-timeline">
{% for s in a.children.all %} {% for s in a.children.all %}
<div data-activity-id="{{ s.pk }}" <div data-activity-id="{{ s.pk }}"
class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}">
> <span title="{{ s.result.get_admin_text }}">
{{ s.readable_name|get_text:user }} {{ s.readable_name|get_text:user }}
&ndash; </span>
{% if s.finished %} &ndash;
{{ s.finished|time:"H:i:s" }} {% if s.finished %}
{% else %} {{ s.finished|time:"H:i:s" }}
<i class="fa fa-refresh fa-spin" class="sub-activity-loading-icon"></i> {% else %}
{% endif %} <i class="fa fa-refresh fa-spin" class="sub-activity-loading-icon"></i>
{% if s.has_failed %} {% endif %}
<div title="{{ s.result.get_admin_text }}" class="label label-danger">{% trans "failed" %}</div> {% if s.has_failed %}
{% endif %} <div class="label label-danger">{% trans "failed" %}</div>
</div> {% endif %}
{% endfor %} </div>
</div> {% endfor %}
{% endif %} </div>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
...@@ -2,6 +2,6 @@ ...@@ -2,6 +2,6 @@
<h3>{% trans "Activity" %}</h3> <h3>{% trans "Activity" %}</h3>
<div id="activity-timeline-wrapper"> <div id="activity-refresh">
{% include "dashboard/node-detail/_activity-timeline.html" %} {% include "dashboard/node-detail/_activity-timeline.html" %}
</div> </div>
...@@ -7,8 +7,9 @@ ...@@ -7,8 +7,9 @@
<dt>{% trans "RAM size" %}:</dt> <dd>{% widthratio node.info.ram_size 1048576 1 %} MiB</dd> <dt>{% trans "RAM size" %}:</dt> <dd>{% widthratio node.info.ram_size 1048576 1 %} MiB</dd>
<dt>{% trans "Architecture" %}:</dt><dd>{{ node.info.architecture }}</dd> <dt>{% trans "Architecture" %}:</dt><dd>{{ node.info.architecture }}</dd>
<dt>{% trans "Host IP" %}:</dt><dd>{{ node.host.ipv4 }}</dd> <dt>{% trans "Host IP" %}:</dt><dd>{{ node.host.ipv4 }}</dd>
<dt>{% trans "Enabled" %}:</dt><dd>{{ node.enabled }}</dd> <dt>{% trans "Enabled" %}:</dt><dd>{{ node.enabled|yesno }}</dd>
<dt>{% trans "Host online" %}:</dt><dd> {{ node.online }}</dd> <dt>{% trans "Host online" %}:</dt><dd> {{ node.online|yesno }}</dd>
<dt>{% trans "Minion online" %}:</dt><dd> {{ node.minion_online|yesno }}</dd>
<dt>{% trans "Priority" %}:</dt><dd>{{ node.priority }}</dd> <dt>{% trans "Priority" %}:</dt><dd>{{ node.priority }}</dd>
<dt>{% trans "Driver Version:" %}</dt> <dt>{% trans "Driver Version:" %}</dt>
<dd> <dd>
......
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
class="list-group-item class="list-group-item
{% if forloop.last and files.toplist|length < 5 %}list-group-item-last{% endif %}"> {% if forloop.last and files.toplist|length < 5 %}list-group-item-last{% endif %}">
<i class="fa fa-{{ t.icon }} dashboard-toplist-icon"></i> <i class="fa fa-{{ t.icon }} dashboard-toplist-icon"></i>
<div class="store-list-item-name"> <div class="store-list-item-name">
{{ t.NAME }} {{ t.NAME }}
</div> </div>
<div style="clear: both;"></div> <div style="clear: both;"></div>
...@@ -53,20 +53,22 @@ ...@@ -53,20 +53,22 @@
{% trans "Your toplist is empty, upload something." %} {% trans "Your toplist is empty, upload something." %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<div class="list-group-item list-group-footer text-right no-hover"> <div class="list-group-item list-group-footer no-hover">
<form class="pull-left" method="POST" action="{% url "dashboard.views.store-refresh-toplist" %}"> <div class="text-right">
{% csrf_token %} <form class="pull-left" method="POST" action="{% url "dashboard.views.store-refresh-toplist" %}">
<button class="btn btn-success btn-xs" type="submit" title="{% trans "Refresh" %}"/> {% csrf_token %}
<i class="fa fa-refresh"></i> <button class="btn btn-success btn-xs" type="submit" title="{% trans "Refresh" %}"/>
</button> <i class="fa fa-refresh"></i>
</form> </button>
<a href="{% url "dashboard.views.store-list" %}" class="btn btn-primary btn-xs"> </form>
<i class="fa fa-chevron-circle-right"></i> {% trans "show my files" %} <a href="{% url "dashboard.views.store-list" %}" class="btn btn-primary btn-xs">
</a> <i class="fa fa-chevron-circle-right"></i> {% trans "show my files" %}
<a href="{% url "dashboard.views.store-upload" %}" class="btn btn-success btn-xs"> </a>
<i class="fa fa-cloud-upload"></i> {% trans "upload" %} <a href="{% url "dashboard.views.store-upload" %}" class="btn btn-success btn-xs">
</a> <i class="fa fa-cloud-upload"></i> {% trans "upload" %}
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<a href="{% url "dashboard.views.template-delete" pk=object.pk %}" <a href="{% url "dashboard.views.template-delete" pk=object.pk %}"
class="btn btn-xs btn-danger pull-right"> class="btn btn-xs btn-danger pull-right template-delete">
{% trans "Delete" %} {% trans "Delete" %}
</a> </a>
<h4 class="no-margin"><i class="fa fa-times"></i> {% trans "Delete template" %}</h4> <h4 class="no-margin"><i class="fa fa-times"></i> {% trans "Delete template" %}</h4>
......
...@@ -207,19 +207,25 @@ ...@@ -207,19 +207,25 @@
{% trans "Network" %}</a> {% trans "Network" %}</a>
</li> </li>
<li> <li>
<a href="#activity" data-toggle="pill" data-target="#_activity" class="text-center"> <a href="#activity" data-toggle="pill" data-target="#_activity" class="text-center"
data-activity-url="{% url "dashboard.views.vm-activity-list" instance.pk %}">
<i class="fa fa-clock-o fa-2x"></i><br> <i class="fa fa-clock-o fa-2x"></i><br>
{% trans "Activity" %}</a> {% trans "Activity" %}</a>
</li> </li>
</ul> </ul>
<div class="tab-content panel-body"> <div class="tab-content panel-body">
<div class="tab-pane active" id="_home">{% include "dashboard/vm-detail/home.html" %}</div> <div class="not-tab-pane active" id="_home">{% include "dashboard/vm-detail/home.html" %}</div>
<div class="tab-pane" id="_resources">{% include "dashboard/vm-detail/resources.html" %}</div> <hr class="js-hidden"/>
<div class="not-tab-pane" id="_resources">{% include "dashboard/vm-detail/resources.html" %}</div>
<div class="tab-pane" id="_console">{% include "dashboard/vm-detail/console.html" %}</div> <div class="tab-pane" id="_console">{% include "dashboard/vm-detail/console.html" %}</div>
<div class="tab-pane" id="_access">{% include "dashboard/vm-detail/access.html" %} </div> <hr class="js-hidden"/>
<div class="tab-pane" id="_network">{% include "dashboard/vm-detail/network.html" %}</div> <div class="not-tab-pane" id="_access">{% include "dashboard/vm-detail/access.html" %} </div>
<div class="tab-pane" id="_activity">{% include "dashboard/vm-detail/activity.html" %}</div> <hr class="js-hidden"/>
<div class="not-tab-pane" id="_network">{% include "dashboard/vm-detail/network.html" %}</div>
<hr class="js-hidden"/>
<div class="not-tab-pane" id="_activity">{% include "dashboard/vm-detail/activity.html" %}</div>
<hr class="js-hidden"/>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -21,13 +21,14 @@ ...@@ -21,13 +21,14 @@
<a href="{{ i.host.get_absolute_url }}" <a href="{{ i.host.get_absolute_url }}"
class="btn btn-default btn-xs">{% trans "edit" %}</a> class="btn btn-default btn-xs">{% trans "edit" %}</a>
{% endif %} {% endif %}
{% if is_owner %} {% with op=op.remove_interface %}{% if op %}
<a href="{% url "dashboard.views.interface-delete" pk=i.pk %}?next={{ request.path }}" <span class="operation-wrapper">
class="btn btn-danger btn-xs interface-remove" <a href="{{op.get_url}}?interface={{ i.pk }}"
data-interface-pk="{{ i.pk }}"> class="btn btn-{{op.effect}} btn-xs operation interface-remove"
{% trans "remove" %} {% if op.disabled %}disabled{% endif %}>{% trans "remove" %}
</a> </a>
{% endif %} </span>
{% endif %}{% endwith %}
</h3> </h3>
{% if i.host %} {% if i.host %}
<div class="row"> <div class="row">
...@@ -78,7 +79,13 @@ ...@@ -78,7 +79,13 @@
{{ l.private }}/{{ l.proto }} {{ l.private }}/{{ l.proto }}
</td> </td>
<td> <td>
<a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv4.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a> <span class="operation-wrapper">
<a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}"
class="btn btn-link btn-xs operation"
title="{% trans "Remove" %}">
<i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i>
</a>
</span>
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
......
...@@ -22,12 +22,12 @@ ...@@ -22,12 +22,12 @@
<div id="vm-details-resources-disk"> <div id="vm-details-resources-disk">
<h3> <h3>
{% trans "Disks" %}
<div class="pull-right"> <div class="pull-right">
<div id="disk-ops"> <div id="disk-ops">
{% include "dashboard/vm-detail/_disk-operations.html" %} {% include "dashboard/vm-detail/_disk-operations.html" %}
</div> </div>
</div> </div>
{% trans "Disks" %}
</h3> </h3>
{% if not instance.disks.all %} {% if not instance.disks.all %}
......
...@@ -27,7 +27,7 @@ from django.contrib.auth import authenticate ...@@ -27,7 +27,7 @@ from django.contrib.auth import authenticate
from dashboard.views import VmAddInterfaceView from dashboard.views import VmAddInterfaceView
from vm.models import Instance, InstanceTemplate, Lease, Node, Trait from vm.models import Instance, InstanceTemplate, Lease, Node, Trait
from vm.operations import (WakeUpOperation, AddInterfaceOperation, from vm.operations import (WakeUpOperation, AddInterfaceOperation,
AddPortOperation) AddPortOperation, RemoveInterfaceOperation)
from ..models import Profile from ..models import Profile
from firewall.models import Vlan, Host, VlanGroup from firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch from mock import Mock, patch
...@@ -169,26 +169,12 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -169,26 +169,12 @@ class VmDetailTest(LoginMixin, TestCase):
inst.save() inst.save()
iface_count = inst.interface_set.count() iface_count = inst.interface_set.count()
c.post("/dashboard/interface/1/delete/") with patch.object(RemoveInterfaceOperation, 'async') as mock_method:
self.assertEqual(inst.interface_set.count(), iface_count - 1) mock_method.side_effect = inst.remove_interface
response = c.post("/dashboard/vm/1/op/remove_interface/",
def test_permitted_network_delete_w_ajax(self): {'interface': 1})
c = Client() self.assertEqual(response.status_code, 302)
self.login(c, "user1") assert mock_method.called
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
vlan = Vlan.objects.get(pk=1)
inst.add_interface(vlan=vlan, user=self.us)
inst.status = 'RUNNING'
inst.save()
iface_count = inst.interface_set.count()
response = c.post("/dashboard/interface/1/delete/",
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
removed_network = json.loads(response.content)['removed_network']
self.assertEqual(removed_network['vlan'], vlan.name)
self.assertEqual(removed_network['vlan_pk'], vlan.pk)
self.assertEqual(removed_network['managed'], vlan.managed)
self.assertEqual(inst.interface_set.count(), iface_count - 1) self.assertEqual(inst.interface_set.count(), iface_count - 1)
def test_unpermitted_network_delete(self): def test_unpermitted_network_delete(self):
...@@ -199,7 +185,10 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -199,7 +185,10 @@ class VmDetailTest(LoginMixin, TestCase):
inst.add_interface(vlan=Vlan.objects.get(pk=1), user=self.us) inst.add_interface(vlan=Vlan.objects.get(pk=1), user=self.us)
iface_count = inst.interface_set.count() iface_count = inst.interface_set.count()
response = c.post("/dashboard/interface/1/delete/") with patch.object(RemoveInterfaceOperation, 'async') as mock_method:
mock_method.side_effect = inst.remove_interface
response = c.post("/dashboard/vm/1/op/remove_interface/",
{'interface': 1})
self.assertEqual(iface_count, inst.interface_set.count()) self.assertEqual(iface_count, inst.interface_set.count())
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
...@@ -766,42 +755,6 @@ class NodeDetailTest(LoginMixin, TestCase): ...@@ -766,42 +755,6 @@ class NodeDetailTest(LoginMixin, TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(len(Node.objects.get(pk=1).traits.all()), trait_count) self.assertEqual(len(Node.objects.get(pk=1).traits.all()), trait_count)
def test_anon_change_node_status(self):
c = Client()
node = Node.objects.get(pk=1)
node_enabled = node.enabled
response = c.post("/dashboard/node/1/", {'change_status': ''})
self.assertEqual(response.status_code, 302)
self.assertEqual(node_enabled, Node.objects.get(pk=1).enabled)
def test_unpermitted_change_node_status(self):
c = Client()
self.login(c, "user2")
node = Node.objects.get(pk=1)
node_enabled = node.enabled
response = c.post("/dashboard/node/status/1/", {'change_status': ''})
self.assertEqual(response.status_code, 302)
self.assertEqual(node_enabled, Node.objects.get(pk=1).enabled)
def test_permitted_change_node_status(self):
c = Client()
self.login(c, "superuser")
node = Node.objects.get(pk=1)
node_enabled = node.enabled
response = c.post("/dashboard/node/status/1/", {'change_status': ''})
self.assertEqual(response.status_code, 302)
self.assertEqual(node_enabled, not Node.objects.get(pk=1).enabled)
def test_permitted_change_node_status_w_ajax(self):
c = Client()
self.login(c, "superuser")
node = Node.objects.get(pk=1)
node_enabled = node.enabled
response = c.post("/dashboard/node/status/1/", {'change_status': ''},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
self.assertEqual(node_enabled, not Node.objects.get(pk=1).enabled)
class GroupCreateTest(LoginMixin, TestCase): class GroupCreateTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json'] fixtures = ['test-vm-fixture.json', 'node.json']
...@@ -949,21 +902,26 @@ class GroupDeleteTest(LoginMixin, TestCase): ...@@ -949,21 +902,26 @@ class GroupDeleteTest(LoginMixin, TestCase):
def test_permitted_group_page(self): def test_permitted_group_page(self):
c = Client() c = Client()
self.login(c, 'user0') self.login(c, 'user0')
response = c.get('/dashboard/group/delete/' + str(self.g1.pk) + '/') with patch('dashboard.views.util.messages') as msg:
response = c.get('/dashboard/group/delete/%d/' % self.g1.pk)
assert not msg.error.called and not msg.warning.called
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_unpermitted_group_page(self): def test_unpermitted_group_page(self):
c = Client() c = Client()
self.login(c, 'user1') self.login(c, 'user1')
response = c.get('/dashboard/group/delete/' + str(self.g1.pk) + '/') with patch('dashboard.views.util.messages') as msg:
self.assertEqual(response.status_code, 403) response = c.get('/dashboard/group/delete/%d/' % self.g1.pk)
assert msg.error.called or msg.warning.called
self.assertEqual(response.status_code, 302)
def test_anon_group_delete(self): def test_anon_group_delete(self):
c = Client() c = Client()
groupnum = Group.objects.count() response = c.get('/dashboard/group/delete/%d/' % self.g1.pk)
response = c.post('/dashboard/group/delete/' + str(self.g1.pk) + '/') self.assertRedirects(
response, '/accounts/login/?next=/dashboard/group/delete/5/',
status_code=302)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(Group.objects.count(), groupnum)
def test_unpermitted_group_delete(self): def test_unpermitted_group_delete(self):
c = Client() c = Client()
...@@ -1484,38 +1442,6 @@ class TransferOwnershipViewTest(LoginMixin, TestCase): ...@@ -1484,38 +1442,6 @@ class TransferOwnershipViewTest(LoginMixin, TestCase):
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'}) response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
self.assertEqual(self.u2.notification_set.count(), c2 + 1) self.assertEqual(self.u2.notification_set.count(), c2 + 1)
def test_transfer(self):
self.skipTest("How did this ever pass?")
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
c = Client()
self.login(c, 'user2')
response = c.post(url)
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk)
def test_transfer_token_used_by_others(self):
self.skipTest("How did this ever pass?")
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
response = c.post(url) # token is for user2
assert response.status_code == 403
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u1.pk)
def test_transfer_by_superuser(self):
self.skipTest("How did this ever pass?")
c = Client()
self.login(c, 'superuser')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
c = Client()
self.login(c, 'user2')
response = c.post(url)
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk)
class IndexViewTest(LoginMixin, TestCase): class IndexViewTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json'] fixtures = ['test-vm-fixture.json', 'node.json']
......
...@@ -25,12 +25,12 @@ from .views import ( ...@@ -25,12 +25,12 @@ 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, NodeList, NodeStatus, NodeDetailView, NodeList,
NotificationView, TemplateAclUpdateView, TemplateCreate, NotificationView, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TemplateDelete, TemplateDetail, TemplateList,
vm_activity, VmCreate, VmDetailView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmList, VmDetailVncTokenView, VmList,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView, DiskRemoveView, get_disk_download_status,
GroupRemoveUserView, GroupRemoveUserView,
GroupRemoveFutureUserView, GroupRemoveFutureUserView,
GroupCreate, GroupProfileUpdate, GroupCreate, GroupProfileUpdate,
...@@ -51,6 +51,7 @@ from .views import ( ...@@ -51,6 +51,7 @@ from .views import (
TransferInstanceOwnershipView, TransferInstanceOwnershipConfirmView, TransferInstanceOwnershipView, TransferInstanceOwnershipConfirmView,
TransferTemplateOwnershipView, TransferTemplateOwnershipConfirmView, TransferTemplateOwnershipView, TransferTemplateOwnershipConfirmView,
OpenSearchDescriptionView, OpenSearchDescriptionView,
NodeActivityView,
) )
from .views.vm import vm_ops, vm_mass_ops from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops from .views.node import node_ops
...@@ -94,7 +95,8 @@ urlpatterns = patterns( ...@@ -94,7 +95,8 @@ urlpatterns = patterns(
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'), url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(), url(r'^vm/create/$', VmCreate.as_view(),
name='dashboard.views.vm-create'), name='dashboard.views.vm-create'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity), url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity,
name='dashboard.views.vm-activity-list'),
url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(), url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(),
name='dashboard.views.vm-activity'), name='dashboard.views.vm-activity'),
url(r'^vm/(?P<pk>\d+)/screenshot/$', get_vm_screenshot, url(r'^vm/(?P<pk>\d+)/screenshot/$', get_vm_screenshot,
...@@ -119,8 +121,8 @@ urlpatterns = patterns( ...@@ -119,8 +121,8 @@ urlpatterns = patterns(
name='dashboard.views.template-transfer-ownership-confirm'), name='dashboard.views.template-transfer-ownership-confirm'),
url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(), url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(),
name="dashboard.views.delete-node"), name="dashboard.views.delete-node"),
url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.as_view(), url(r'^node/(?P<pk>\d+)/activity/$', NodeActivityView.as_view(),
name="dashboard.views.status-node"), name='dashboard.views.node-activity-list'),
url(r'^node/create/$', NodeCreate.as_view(), url(r'^node/create/$', NodeCreate.as_view(),
name='dashboard.views.node-create'), name='dashboard.views.node-create'),
...@@ -156,9 +158,6 @@ urlpatterns = patterns( ...@@ -156,9 +158,6 @@ urlpatterns = patterns(
url(r'^disk/(?P<pk>\d+)/status/$', get_disk_download_status, url(r'^disk/(?P<pk>\d+)/status/$', get_disk_download_status,
name="dashboard.views.disk-status"), name="dashboard.views.disk-status"),
url(r'^interface/(?P<pk>\d+)/delete/$', InterfaceDeleteView.as_view(),
name="dashboard.views.interface-delete"),
url(r'^profile/$', MyPreferencesView.as_view(), url(r'^profile/$', MyPreferencesView.as_view(),
name="dashboard.views.profile-preferences"), name="dashboard.views.profile-preferences"),
url(r'^subscribe/(?P<token>.*)/$', UnsubscribeFormView.as_view(), url(r'^subscribe/(?P<token>.*)/$', UnsubscribeFormView.as_view(),
......
...@@ -29,7 +29,7 @@ from django.core.urlresolvers import reverse, reverse_lazy ...@@ -29,7 +29,7 @@ from django.core.urlresolvers import reverse, reverse_lazy
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import UpdateView, DeleteView, TemplateView from django.views.generic import UpdateView, TemplateView
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
...@@ -41,7 +41,8 @@ from ..forms import ( ...@@ -41,7 +41,8 @@ from ..forms import (
from ..models import FutureMember, GroupProfile from ..models import FutureMember, GroupProfile
from vm.models import Instance, InstanceTemplate from vm.models import Instance, InstanceTemplate
from ..tables import GroupListTable from ..tables import GroupListTable
from .util import CheckedDetailView, AclUpdateView, search_user, saml_available from .util import (CheckedDetailView, AclUpdateView, search_user,
saml_available, DeleteViewBase)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -224,15 +225,18 @@ class GroupList(LoginRequiredMixin, SingleTableView): ...@@ -224,15 +225,18 @@ class GroupList(LoginRequiredMixin, SingleTableView):
return groups return groups
class GroupRemoveUserView(CheckedDetailView, DeleteView): class GroupRemoveUserView(DeleteViewBase):
model = Group model = Group
slug_field = 'pk' slug_field = 'pk'
slug_url_kwarg = 'group_pk' slug_url_kwarg = 'group_pk'
read_level = 'operator' level = 'operator'
member_key = 'member_pk' member_key = 'member_pk'
success_message = _("Member successfully removed from group.")
def get_has_level(self): def check_auth(self):
return self.object.profile.has_level if not self.get_object().profile.has_level(
self.request.user, self.level):
raise PermissionDenied()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(GroupRemoveUserView, self).get_context_data(**kwargs) context = super(GroupRemoveUserView, self).get_context_data(**kwargs)
...@@ -243,50 +247,24 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView): ...@@ -243,50 +247,24 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView):
return context return context
def get_success_url(self): def get_success_url(self):
next = self.request.POST.get('next') return reverse_lazy("dashboard.views.group-detail",
if next: kwargs={'pk': self.get_object().pk})
return next
else:
return reverse_lazy("dashboard.views.group-detail",
kwargs={'pk': self.get_object().pk})
def get(self, request, member_pk, *args, **kwargs): def get(self, request, member_pk, *args, **kwargs):
self.member_pk = member_pk self.member_pk = member_pk
return super(GroupRemoveUserView, self).get(request, *args, **kwargs) return super(GroupRemoveUserView, self).get(request, *args, **kwargs)
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-remove.html']
else:
return ['dashboard/confirm/base-remove.html']
def remove_member(self, pk): def remove_member(self, pk):
container = self.get_object() container = self.get_object()
container.user_set.remove(User.objects.get(pk=pk)) container.user_set.remove(User.objects.get(pk=pk))
def get_success_message(self): def delete_obj(self, request, *args, **kwargs):
return _("Member successfully removed from group.")
def delete(self, request, *args, **kwargs):
object = self.get_object()
if not object.profile.has_level(request.user, 'operator'):
raise PermissionDenied()
self.remove_member(kwargs[self.member_key]) self.remove_member(kwargs[self.member_key])
success_url = self.get_success_url()
success_message = self.get_success_message()
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return redirect(success_url)
class GroupRemoveFutureUserView(GroupRemoveUserView): class GroupRemoveFutureUserView(GroupRemoveUserView):
member_key = 'member_org_id' member_key = 'member_org_id'
success_message = _("Future user successfully removed from group.")
def get(self, request, member_org_id, *args, **kwargs): def get(self, request, member_org_id, *args, **kwargs):
self.member_org_id = member_org_id self.member_org_id = member_org_id
...@@ -305,53 +283,17 @@ class GroupRemoveFutureUserView(GroupRemoveUserView): ...@@ -305,53 +283,17 @@ class GroupRemoveFutureUserView(GroupRemoveUserView):
FutureMember.objects.filter(org_id=org_id, FutureMember.objects.filter(org_id=org_id,
group=self.get_object()).delete() group=self.get_object()).delete()
def get_success_message(self):
return _("Future user successfully removed from group.")
class GroupDelete(DeleteViewBase):
class GroupDelete(CheckedDetailView, DeleteView):
"""This stuff deletes the group.
"""
model = Group model = Group
template_name = "dashboard/confirm/base-delete.html" success_message = _("Group successfully deleted.")
read_level = 'operator'
def get_has_level(self):
return self.object.profile.has_level
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
# github.com/django/django/blob/master/django/views/generic/edit.py#L245 def check_auth(self):
def delete(self, request, *args, **kwargs): if not self.get_object().profile.has_level(self.request.user, 'owner'):
object = self.get_object()
if not object.profile.has_level(request.user, 'owner'):
raise PermissionDenied() raise PermissionDenied()
object.delete()
success_url = self.get_success_url()
success_message = _("Group successfully deleted.")
if request.is_ajax():
if request.POST.get('redirect').lower() == "true":
messages.success(request, success_message)
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return redirect(success_url)
def get_success_url(self): def get_success_url(self):
next = self.request.POST.get('next') return reverse_lazy('dashboard.views.group-list')
if next:
return next
else:
return reverse_lazy('dashboard.index')
class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView): class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
...@@ -360,7 +302,7 @@ class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView): ...@@ -360,7 +302,7 @@ class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
def get_template_names(self): def get_template_names(self):
if self.request.is_ajax(): if self.request.is_ajax():
return ['dashboard/modal-wrapper.html'] return ['dashboard/_modal.html']
else: else:
return ['dashboard/nojs-wrapper.html'] return ['dashboard/nojs-wrapper.html']
......
...@@ -27,8 +27,10 @@ from django.db.models import Count ...@@ -27,8 +27,10 @@ from django.db.models import Count
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template import RequestContext
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import DetailView, TemplateView, DeleteView from django.views.generic import DetailView, TemplateView, View
from braces.views import LoginRequiredMixin, SuperuserRequiredMixin from braces.views import LoginRequiredMixin, SuperuserRequiredMixin
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
...@@ -38,7 +40,7 @@ from vm.models import Node, NodeActivity, Trait ...@@ -38,7 +40,7 @@ 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 AjaxOperationMixin, OperationView, GraphMixin from .util import AjaxOperationMixin, OperationView, GraphMixin, DeleteViewBase
def get_operations(instance, user): def get_operations(instance, user):
...@@ -59,6 +61,8 @@ class NodeOperationView(AjaxOperationMixin, OperationView): ...@@ -59,6 +61,8 @@ class NodeOperationView(AjaxOperationMixin, OperationView):
model = Node model = Node
context_object_name = 'node' # much simpler to mock object context_object_name = 'node' # much simpler to mock object
with_reload = True
wait_for_result = 1
node_ops = OrderedDict([ node_ops = OrderedDict([
...@@ -68,6 +72,8 @@ node_ops = OrderedDict([ ...@@ -68,6 +72,8 @@ node_ops = OrderedDict([
op='passivate', icon='play-circle-o', effect='info')), op='passivate', icon='play-circle-o', effect='info')),
('disable', NodeOperationView.factory( ('disable', NodeOperationView.factory(
op='disable', icon='times-circle-o', effect='danger')), op='disable', icon='times-circle-o', effect='danger')),
('update_node', NodeOperationView.factory(
op='update_node', icon='refresh', effect='warning')),
('reset', NodeOperationView.factory( ('reset', NodeOperationView.factory(
op='reset', icon='stethoscope', effect='danger')), op='reset', icon='stethoscope', effect='danger')),
('flush', NodeOperationView.factory( ('flush', NodeOperationView.factory(
...@@ -191,7 +197,7 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): ...@@ -191,7 +197,7 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
def get_template_names(self): def get_template_names(self):
if self.request.is_ajax(): if self.request.is_ajax():
return ['dashboard/modal-wrapper.html'] return ['dashboard/_modal.html']
else: else:
return ['dashboard/nojs-wrapper.html'] return ['dashboard/nojs-wrapper.html']
...@@ -203,7 +209,7 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): ...@@ -203,7 +209,7 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
context.update({ context.update({
'template': 'dashboard/node-create.html', 'template': 'dashboard/node-create.html',
'box_title': 'Create a Node', 'box_title': _('Create a node'),
'hostform': hostform, 'hostform': hostform,
'formset': formset, 'formset': formset,
...@@ -236,44 +242,16 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): ...@@ -236,44 +242,16 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
return redirect(path) return redirect(path)
class NodeDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): class NodeDelete(SuperuserRequiredMixin, DeleteViewBase):
"""This stuff deletes the node.
"""
model = Node model = Node
template_name = "dashboard/confirm/base-delete.html" success_message = _("Node successfully deleted.")
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
# github.com/django/django/blob/master/django/views/generic/edit.py#L245
def delete(self, request, *args, **kwargs):
object = self.get_object()
object.delete() def check_auth(self):
success_url = self.get_success_url() # SuperuserRequiredMixin
success_message = _("Node successfully deleted.") pass
if request.is_ajax():
if request.POST.get('redirect').lower() == "true":
messages.success(request, success_message)
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return redirect(success_url)
def get_success_url(self): def get_success_url(self):
next = self.request.POST.get('next') return reverse_lazy('dashboard.views.node-list')
if next:
return next
else:
return reverse_lazy('dashboard.index')
class NodeAddTraitView(SuperuserRequiredMixin, DetailView): class NodeAddTraitView(SuperuserRequiredMixin, DetailView):
...@@ -309,55 +287,20 @@ class NodeAddTraitView(SuperuserRequiredMixin, DetailView): ...@@ -309,55 +287,20 @@ class NodeAddTraitView(SuperuserRequiredMixin, DetailView):
return self.get(self, request, pk, *args, **kwargs) return self.get(self, request, pk, *args, **kwargs)
class NodeStatus(LoginRequiredMixin, SuperuserRequiredMixin, DetailView): class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View):
template_name = "dashboard/confirm/node-status.html" def get(self, request, pk):
model = Node node = Node.objects.get(pk=pk)
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-node-status.html']
else:
return ['dashboard/confirm/node-status.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(NodeStatus, self).get_context_data(**kwargs)
if self.object.enabled:
context['status'] = "disable"
else:
context['status'] = "enable"
return context
def post(self, request, *args, **kwargs): activities = NodeActivity.objects.filter(
if request.POST.get('change_status') is not None: node=node, parent=None).order_by('-started').select_related()
return self.__set_status(request)
return redirect(reverse_lazy("dashboard.views.node-detail",
kwargs={'pk': self.get_object().pk}))
def __set_status(self, request): response = {
self.object = self.get_object() 'activities': render_to_string(
if not self.object.enabled: "dashboard/node-detail/_activity-timeline.html",
self.object.enable(user=request.user) RequestContext(request, {'activities': activities}))
else: }
self.object.disable(user=request.user)
success_message = _("Node successfully changed status.")
if request.is_ajax(): return HttpResponse(
response = { json.dumps(response),
'message': success_message, content_type="application/json"
'node_pk': self.object.pk )
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(self.get_success_url())
...@@ -28,7 +28,7 @@ from django.http import HttpResponse, HttpResponseRedirect ...@@ -28,7 +28,7 @@ from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, get_object_or_404 from django.shortcuts import redirect, get_object_or_404
from django.utils.translation import ugettext as _, ugettext_noop from django.utils.translation import ugettext as _, ugettext_noop
from django.views.generic import ( from django.views.generic import (
TemplateView, CreateView, DeleteView, UpdateView, TemplateView, CreateView, UpdateView,
) )
from braces.views import ( from braces.views import (
...@@ -47,6 +47,7 @@ from ..tables import TemplateListTable, LeaseListTable ...@@ -47,6 +47,7 @@ from ..tables import TemplateListTable, LeaseListTable
from .util import ( from .util import (
AclUpdateView, FilterMixin, AclUpdateView, FilterMixin,
TransferOwnershipConfirmView, TransferOwnershipView, TransferOwnershipConfirmView, TransferOwnershipView,
DeleteViewBase
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -56,7 +57,7 @@ class TemplateChoose(LoginRequiredMixin, TemplateView): ...@@ -56,7 +57,7 @@ class TemplateChoose(LoginRequiredMixin, TemplateView):
def get_template_names(self): def get_template_names(self):
if self.request.is_ajax(): if self.request.is_ajax():
return ['dashboard/modal-wrapper.html'] return ['dashboard/_modal.html']
else: else:
return ['dashboard/nojs-wrapper.html'] return ['dashboard/nojs-wrapper.html']
...@@ -231,46 +232,17 @@ class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView): ...@@ -231,46 +232,17 @@ class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView):
return qs.select_related("lease", "owner", "owner__profile") return qs.select_related("lease", "owner", "owner__profile")
class TemplateDelete(LoginRequiredMixin, DeleteView): class TemplateDelete(DeleteViewBase):
model = InstanceTemplate model = InstanceTemplate
success_message = _("Template successfully deleted.")
def get_success_url(self): def get_success_url(self):
return reverse("dashboard.views.template-list") return reverse("dashboard.views.template-list")
def get_template_names(self): def delete_obj(self, request, *args, **kwargs):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get(self, request, *args, **kwargs):
if not self.get_object().has_level(request.user, "owner"):
message = _("Only the owners can delete the selected template.")
if request.is_ajax():
raise PermissionDenied()
else:
messages.warning(request, message)
return redirect(self.get_success_url())
return super(TemplateDelete, self).get(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
object = self.get_object() object = self.get_object()
if not object.has_level(request.user, 'owner'):
raise PermissionDenied()
object.destroy_disks() object.destroy_disks()
object.delete() object.delete()
success_url = self.get_success_url()
success_message = _("Template successfully deleted.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
...@@ -333,25 +305,24 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -333,25 +305,24 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return kwargs return kwargs
class DiskRemoveView(DeleteView): class DiskRemoveView(DeleteViewBase):
model = Disk model = Disk
success_message = _("Disk successfully removed.")
def get_queryset(self): def get_queryset(self):
qs = super(DiskRemoveView, self).get_queryset() qs = super(DiskRemoveView, self).get_queryset()
return qs.exclude(template_set=None) return qs.exclude(template_set=None)
def get_template_names(self): def check_auth(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get_context_data(self, **kwargs):
context = super(DiskRemoveView, self).get_context_data(**kwargs)
disk = self.get_object() disk = self.get_object()
template = disk.template_set.get() template = disk.template_set.get()
if not template.has_level(self.request.user, 'owner'): if not template.has_level(self.request.user, 'owner'):
raise PermissionDenied() raise PermissionDenied()
def get_context_data(self, **kwargs):
disk = self.get_object()
template = disk.template_set.get()
context = super(DiskRemoveView, self).get_context_data(**kwargs)
context['title'] = _("Disk remove confirmation") context['title'] = _("Disk remove confirmation")
context['text'] = _("Are you sure you want to remove " context['text'] = _("Are you sure you want to remove "
"<strong>%(disk)s</strong> from " "<strong>%(disk)s</strong> from "
...@@ -360,29 +331,12 @@ class DiskRemoveView(DeleteView): ...@@ -360,29 +331,12 @@ class DiskRemoveView(DeleteView):
) )
return context return context
def delete(self, request, *args, **kwargs): def delete_obj(self, request, *args, **kwargs):
disk = self.get_object() disk = self.get_object()
template = disk.template_set.get() template = disk.template_set.get()
template.remove_disk(disk)
if not template.has_level(request.user, 'owner'):
raise PermissionDenied()
template.remove_disk(disk=disk, user=request.user)
disk.destroy() disk.destroy()
next_url = request.POST.get("next")
success_url = next_url if next_url else template.get_absolute_url()
success_message = _("Disk successfully removed.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect("%s#resources" % success_url)
class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin, class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin,
SuccessMessageMixin, CreateView): SuccessMessageMixin, CreateView):
...@@ -435,18 +389,13 @@ class LeaseDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -435,18 +389,13 @@ class LeaseDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return super(LeaseDetail, self).post(request, *args, **kwargs) return super(LeaseDetail, self).post(request, *args, **kwargs)
class LeaseDelete(LoginRequiredMixin, DeleteView): class LeaseDelete(DeleteViewBase):
model = Lease model = Lease
success_message = _("Lease successfully deleted.")
def get_success_url(self): def get_success_url(self):
return reverse("dashboard.views.template-list") return reverse("dashboard.views.template-list")
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
c = super(LeaseDelete, self).get_context_data(*args, **kwargs) c = super(LeaseDelete, self).get_context_data(*args, **kwargs)
lease = self.get_object() lease = self.get_object()
...@@ -461,36 +410,11 @@ class LeaseDelete(LoginRequiredMixin, DeleteView): ...@@ -461,36 +410,11 @@ class LeaseDelete(LoginRequiredMixin, DeleteView):
c['disable_submit'] = True c['disable_submit'] = True
return c return c
def get(self, request, *args, **kwargs): def delete_obj(self, request, *args, **kwargs):
if not self.get_object().has_level(request.user, "owner"):
message = _("Only the owners can delete the selected lease.")
if request.is_ajax():
raise PermissionDenied()
else:
messages.warning(request, message)
return redirect(self.get_success_url())
return super(LeaseDelete, self).get(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
object = self.get_object() object = self.get_object()
if not object.has_level(request.user, "owner"):
raise PermissionDenied()
if object.instancetemplate_set.count() > 0: if object.instancetemplate_set.count() > 0:
raise SuspiciousOperation() raise SuspiciousOperation()
object.delete() object.delete()
success_url = self.get_success_url()
success_message = _("Lease successfully deleted.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class TransferTemplateOwnershipConfirmView(TransferOwnershipConfirmView): class TransferTemplateOwnershipConfirmView(TransferOwnershipConfirmView):
......
...@@ -35,7 +35,7 @@ from django.shortcuts import redirect, get_object_or_404 ...@@ -35,7 +35,7 @@ from django.shortcuts import redirect, get_object_or_404
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.views.generic import ( from django.views.generic import (
TemplateView, DetailView, View, DeleteView, UpdateView, CreateView, TemplateView, DetailView, View, UpdateView, CreateView,
) )
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
...@@ -50,7 +50,7 @@ from ..forms import ( ...@@ -50,7 +50,7 @@ from ..forms import (
from ..models import Profile, GroupProfile, ConnectCommand, create_profile from ..models import Profile, GroupProfile, ConnectCommand, create_profile
from ..tables import UserKeyListTable, ConnectCommandListTable from ..tables import UserKeyListTable, ConnectCommandListTable
from .util import saml_available from .util import saml_available, DeleteViewBase
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -299,7 +299,7 @@ class ProfileView(LoginRequiredMixin, DetailView): ...@@ -299,7 +299,7 @@ class ProfileView(LoginRequiredMixin, DetailView):
# if the intersection of the 2 lists is empty the logged in user # if the intersection of the 2 lists is empty the logged in user
# has no permission to check the target's profile # has no permission to check the target's profile
# (except if the user want to see his own profile) # (except if the user want to see his own profile)
if len(intersection) < 1 and target != user: if not intersection and target != user and not user.is_superuser:
raise PermissionDenied raise PermissionDenied
return super(ProfileView, self).get(*args, **kwargs) return super(ProfileView, self).get(*args, **kwargs)
...@@ -385,36 +385,17 @@ class UserKeyDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -385,36 +385,17 @@ class UserKeyDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return super(UserKeyDetail, self).post(self, request, args, kwargs) return super(UserKeyDetail, self).post(self, request, args, kwargs)
class UserKeyDelete(LoginRequiredMixin, DeleteView): class UserKeyDelete(DeleteViewBase):
model = UserKey model = UserKey
success_message = _("SSH key successfully deleted.")
def get_success_url(self): def get_success_url(self):
return reverse("dashboard.views.profile-preferences") return reverse("dashboard.views.profile-preferences")
def get_template_names(self): def check_auth(self):
if self.request.is_ajax(): if self.get_object().user != self.request.user:
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def delete(self, request, *args, **kwargs):
object = self.get_object()
if object.user != request.user:
raise PermissionDenied() raise PermissionDenied()
object.delete()
success_url = self.get_success_url()
success_message = _("SSH key successfully deleted.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class UserKeyCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView): class UserKeyCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = UserKey model = UserKey
...@@ -460,36 +441,17 @@ class ConnectCommandDetail(LoginRequiredMixin, SuccessMessageMixin, ...@@ -460,36 +441,17 @@ class ConnectCommandDetail(LoginRequiredMixin, SuccessMessageMixin,
return kwargs return kwargs
class ConnectCommandDelete(LoginRequiredMixin, DeleteView): class ConnectCommandDelete(DeleteViewBase):
model = ConnectCommand model = ConnectCommand
success_message = _("Command template successfully deleted.")
def get_success_url(self): def get_success_url(self):
return reverse("dashboard.views.profile-preferences") return reverse("dashboard.views.profile-preferences")
def get_template_names(self): def check_auth(self):
if self.request.is_ajax(): if self.get_object().user != self.request.user:
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def delete(self, request, *args, **kwargs):
object = self.get_object()
if object.user != request.user:
raise PermissionDenied() raise PermissionDenied()
object.delete()
success_url = self.get_success_url()
success_message = _("Command template successfully deleted.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin, class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin,
CreateView): CreateView):
......
...@@ -33,7 +33,7 @@ from django.db.models import Q ...@@ -33,7 +33,7 @@ from django.db.models import Q
from django.http import HttpResponse, Http404, HttpResponseRedirect from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django.views.generic import DetailView, View from django.views.generic import DetailView, View, DeleteView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from braces.views import LoginRequiredMixin from braces.views import LoginRequiredMixin
...@@ -694,3 +694,45 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): ...@@ -694,3 +694,45 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
unicode(user), user.pk, new_owner, key) unicode(user), user.pk, new_owner, key)
raise PermissionDenied() raise PermissionDenied()
return (instance, new_owner) return (instance, new_owner)
class DeleteViewBase(LoginRequiredMixin, DeleteView):
level = 'owner'
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def check_auth(self):
if not self.get_object().has_level(self.request.user, self.level):
raise PermissionDenied()
def get(self, request, *args, **kwargs):
try:
self.check_auth()
except PermissionDenied:
message = _("Only the owners can delete the selected object.")
if request.is_ajax():
raise PermissionDenied()
else:
messages.warning(request, message)
return redirect(self.get_success_url())
return super(DeleteViewBase, self).get(request, *args, **kwargs)
def delete_obj(self, request, *args, **kwargs):
self.get_object().delete()
def delete(self, request, *args, **kwargs):
self.check_auth()
self.delete_obj(request, *args, **kwargs)
if request.is_ajax():
return HttpResponse(
json.dumps({'message': self.success_message}),
content_type="application/json",
)
else:
messages.success(request, self.success_message)
return HttpResponseRedirect(self.get_success_url())
...@@ -37,7 +37,7 @@ from django.utils.translation import ( ...@@ -37,7 +37,7 @@ from django.utils.translation import (
) )
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
from django.views.generic import ( from django.views.generic import (
UpdateView, ListView, TemplateView, DeleteView UpdateView, ListView, TemplateView
) )
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
...@@ -64,6 +64,7 @@ from ..forms import ( ...@@ -64,6 +64,7 @@ from ..forms import (
VmDiskResizeForm, RedeployForm, VmDiskRemoveForm, VmDiskResizeForm, RedeployForm, VmDiskRemoveForm,
VmMigrateForm, VmDeployForm, VmMigrateForm, VmDeployForm,
VmPortRemoveForm, VmPortAddForm, VmPortRemoveForm, VmPortAddForm,
VmRemoveInterfaceForm,
) )
from ..models import Favourite from ..models import Favourite
...@@ -324,6 +325,32 @@ def get_operations(instance, user): ...@@ -324,6 +325,32 @@ def get_operations(instance, user):
return ops return ops
class VmRemoveInterfaceView(FormOperationMixin, VmOperationView):
op = 'remove_interface'
form_class = VmRemoveInterfaceForm
show_in_toolbar = False
wait_for_result = 0.5
icon = 'times'
effect = "danger"
with_reload = True
def get_form_kwargs(self):
instance = self.get_op().instance
choices = instance.interface_set.all()
interface_pk = self.request.GET.get('interface')
if interface_pk:
try:
default = choices.get(pk=interface_pk)
except (ValueError, Interface.DoesNotExist):
raise Http404()
else:
default = None
val = super(VmRemoveInterfaceView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
return val
class VmAddInterfaceView(FormOperationMixin, VmOperationView): class VmAddInterfaceView(FormOperationMixin, VmOperationView):
op = 'add_interface' op = 'add_interface'
...@@ -707,6 +734,7 @@ vm_ops = OrderedDict([ ...@@ -707,6 +734,7 @@ vm_ops = OrderedDict([
op='remove_disk', form_class=VmDiskRemoveForm, op='remove_disk', form_class=VmDiskRemoveForm,
icon='times', effect="danger")), icon='times', effect="danger")),
('add_interface', VmAddInterfaceView), ('add_interface', VmAddInterfaceView),
('remove_interface', VmRemoveInterfaceView),
('remove_port', VmPortRemoveView), ('remove_port', VmPortRemoveView),
('add_port', VmPortAddView), ('add_port', VmPortAddView),
('renew', VmRenewView), ('renew', VmRenewView),
...@@ -951,10 +979,21 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -951,10 +979,21 @@ class VmCreate(LoginRequiredMixin, TemplateView):
def get_template_names(self): def get_template_names(self):
if self.request.is_ajax(): if self.request.is_ajax():
return ['dashboard/modal-wrapper.html'] return ['dashboard/_modal.html']
else: else:
return ['dashboard/nojs-wrapper.html'] return ['dashboard/nojs-wrapper.html']
def get_template(self, request, pk):
try:
template = InstanceTemplate.objects.get(
pk=int(pk))
except (ValueError, InstanceTemplate.DoesNotExist):
raise Http404()
if not template.has_level(request.user, 'user'):
raise PermissionDenied()
return template
def get(self, request, form=None, *args, **kwargs): def get(self, request, form=None, *args, **kwargs):
if not request.user.has_perm('vm.create_vm'): if not request.user.has_perm('vm.create_vm'):
raise PermissionDenied() raise PermissionDenied()
...@@ -965,9 +1004,7 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -965,9 +1004,7 @@ class VmCreate(LoginRequiredMixin, TemplateView):
template_pk = form.template.pk template_pk = form.template.pk
if template_pk: if template_pk:
template = get_object_or_404(InstanceTemplate, pk=template_pk) template = self.get_template(request, template_pk)
if not template.has_level(request.user, 'user'):
raise PermissionDenied()
if form is None: if form is None:
form = self.form_class(user=request.user, template=template) form = self.form_class(user=request.user, template=template)
else: else:
...@@ -992,33 +1029,21 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -992,33 +1029,21 @@ class VmCreate(LoginRequiredMixin, TemplateView):
}) })
return self.render_to_response(context) return self.render_to_response(context)
def __create_normal(self, request, *args, **kwargs): def __create_normal(self, request, template, *args, **kwargs):
user = request.user instances = [Instance.create_from_template(
template = InstanceTemplate.objects.get( template=template,
pk=request.POST.get("template")) owner=request.user)]
# permission check
if not template.has_level(request.user, 'user'):
raise PermissionDenied()
args = {"template": template, "owner": user}
instances = [Instance.create_from_template(**args)]
return self.__deploy(request, instances) return self.__deploy(request, instances)
def __create_customized(self, request, *args, **kwargs): def __create_customized(self, request, template, *args, **kwargs):
user = request.user user = request.user
# no form yet, using POST directly: # no form yet, using POST directly:
template = get_object_or_404(InstanceTemplate,
pk=request.POST.get("template"))
form = self.form_class( form = self.form_class(
request.POST, user=request.user, template=template) request.POST, user=request.user, template=template)
if not form.is_valid(): if not form.is_valid():
return self.get(request, form, *args, **kwargs) return self.get(request, form, *args, **kwargs)
post = form.cleaned_data post = form.cleaned_data
if not template.has_level(user, 'user'):
raise PermissionDenied()
ikwargs = { ikwargs = {
'name': post['name'], 'name': post['name'],
'template': template, 'template': template,
...@@ -1071,6 +1096,8 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -1071,6 +1096,8 @@ class VmCreate(LoginRequiredMixin, TemplateView):
if not request.user.has_perm('vm.create_vm'): if not request.user.has_perm('vm.create_vm'):
raise PermissionDenied() raise PermissionDenied()
template = self.get_template(request, request.POST.get("template"))
# limit chekcs # limit chekcs
try: try:
limit = user.profile.instance_limit limit = user.profile.instance_limit
...@@ -1096,7 +1123,7 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -1096,7 +1123,7 @@ class VmCreate(LoginRequiredMixin, TemplateView):
request.POST.get("customized") is None else request.POST.get("customized") is None else
self.__create_customized) self.__create_customized)
return create_func(request, *args, **kwargs) return create_func(request, template, *args, **kwargs)
@require_GET @require_GET
...@@ -1111,56 +1138,6 @@ def get_vm_screenshot(request, pk): ...@@ -1111,56 +1138,6 @@ def get_vm_screenshot(request, pk):
return HttpResponse(image, mimetype="image/png") return HttpResponse(image, mimetype="image/png")
class InterfaceDeleteView(DeleteView):
model = Interface
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get_context_data(self, **kwargs):
context = super(InterfaceDeleteView, self).get_context_data(**kwargs)
interface = self.get_object()
context['text'] = _("Are you sure you want to remove this interface "
"from <strong>%(vm)s</strong>?" %
{'vm': interface.instance.name})
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
instance = self.object.instance
if not instance.has_level(request.user, "owner"):
raise PermissionDenied()
instance.remove_interface(interface=self.object, user=request.user)
success_url = self.get_success_url()
success_message = _("Interface successfully deleted.")
if request.is_ajax():
return HttpResponse(
json.dumps(
{'message': success_message,
'removed_network': {
'vlan': self.object.vlan.name,
'vlan_pk': self.object.vlan.pk,
'managed': self.object.host is not None,
}}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect("%s#network" % success_url)
def get_success_url(self):
redirect = self.request.POST.get("next")
if redirect:
return redirect
self.object.instance.get_absolute_url()
class InstanceActivityDetail(CheckedDetailView): class InstanceActivityDetail(CheckedDetailView):
model = InstanceActivity model = InstanceActivity
context_object_name = 'instanceactivity' # much simpler to mock object context_object_name = 'instanceactivity' # much simpler to mock object
......
...@@ -71,10 +71,17 @@ def compile_messages(): ...@@ -71,10 +71,17 @@ def compile_messages():
run("./manage.py compilemessages") run("./manage.py compilemessages")
def compile_less():
"Compile LESS files"
with _workon("circle"), cd("~/circle/circle"):
run("./manage.py compileless")
@roles('portal') @roles('portal')
def compile_things(): def compile_things():
"Compile translation and collect static files" "Compile translation and collect static files"
compile_js() compile_js()
compile_less()
collectstatic() collectstatic()
compile_messages() compile_messages()
......
...@@ -354,6 +354,12 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -354,6 +354,12 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def create(cls, params, disks, networks, req_traits, tags): def create(cls, params, disks, networks, req_traits, tags):
""" Create new Instance object. """ Create new Instance object.
""" """
# permission check
for network in networks:
if not network.vlan.has_level(params['owner'], 'user'):
raise PermissionDenied()
# create instance and do additional setup # create instance and do additional setup
inst = cls(**params) inst = cls(**params)
...@@ -408,10 +414,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -408,10 +414,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
networks = (template.interface_set.all() if networks is None networks = (template.interface_set.all() if networks is None
else networks) else networks)
for network in networks:
if not network.vlan.has_level(owner, 'user'):
raise PermissionDenied()
req_traits = (template.req_traits.all() if req_traits is None req_traits = (template.req_traits.all() if req_traits is None
else req_traits) else req_traits)
......
...@@ -17,9 +17,15 @@ ...@@ -17,9 +17,15 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from functools import update_wrapper from functools import update_wrapper
from glob import glob
from logging import getLogger from logging import getLogger
import os.path
from warnings import warn from warnings import warn
import requests import requests
from salt.client import LocalClient
from salt.exceptions import SaltClientError
import salt.utils
from time import time, sleep
from django.conf import settings from django.conf import settings
from django.db.models import ( from django.db.models import (
...@@ -44,6 +50,56 @@ from .common import Trait ...@@ -44,6 +50,56 @@ from .common import Trait
logger = getLogger(__name__) logger = getLogger(__name__)
class MyLocalClient(LocalClient):
def get_returns(self, jid, minions, timeout=None):
'''
Get the returns for the command line interface via the event system
'''
minions = set(minions)
if timeout is None:
timeout = self.opts['timeout']
jid_dir = salt.utils.jid_dir(jid,
self.opts['cachedir'],
self.opts['hash_type'])
start = time()
timeout_at = start + timeout
found = set()
ret = {}
wtag = os.path.join(jid_dir, 'wtag*')
# Check to see if the jid is real, if not return the empty dict
if not os.path.isdir(jid_dir):
logger.warning("jid_dir (%s) does not exist", jid_dir)
return ret
# Wait for the hosts to check in
while True:
time_left = timeout_at - time()
raw = self.event.get_event(time_left, jid)
if raw is not None and 'return' in raw:
found.add(raw['id'])
ret[raw['id']] = raw['return']
if len(found.intersection(minions)) >= len(minions):
# All minions have returned, break out of the loop
logger.debug("jid %s found all minions", jid)
break
continue
# Then event system timeout was reached and nothing was returned
if len(found.intersection(minions)) >= len(minions):
# All minions have returned, break out of the loop
logger.debug("jid %s found all minions", jid)
break
if glob(wtag) and time() <= timeout_at + 1:
# The timeout +1 has not been reached and there is still a
# write tag for the syndic
continue
if time() > timeout_at:
logger.info('jid %s minions %s did not return in time',
jid, (minions - found))
break
sleep(0.01)
return ret
def node_available(function): def node_available(function):
"""Decorate methods to ignore disabled Nodes. """Decorate methods to ignore disabled Nodes.
""" """
...@@ -111,6 +167,18 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -111,6 +167,18 @@ class Node(OperatedMixin, TimeStampedModel):
online = property(get_online) online = property(get_online)
@method_cache(20)
def get_minion_online(self):
name = self.host.hostname
try:
client = MyLocalClient()
client.opts['timeout'] = 0.2
return bool(client.cmd(name, 'test.ping')[name])
except (KeyError, SaltClientError):
return False
minion_online = property(get_minion_online)
@node_available @node_available
@method_cache(300) @method_cache(300)
def get_info(self): def get_info(self):
......
...@@ -26,6 +26,7 @@ from StringIO import StringIO ...@@ -26,6 +26,7 @@ from StringIO import StringIO
from tarfile import TarFile, TarInfo from tarfile import TarFile, TarInfo
import time import time
from urlparse import urlsplit from urlparse import urlsplit
from salt.client import LocalClient
from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.utils import timezone from django.utils import timezone
...@@ -90,6 +91,7 @@ class AbortableRemoteOperationMixin(object): ...@@ -90,6 +91,7 @@ class AbortableRemoteOperationMixin(object):
AbortableAsyncResult(remote.id).abort() AbortableAsyncResult(remote.id).abort()
raise humanize_exception(ugettext_noop( raise humanize_exception(ugettext_noop(
"Operation aborted by user."), e) "Operation aborted by user."), e)
raise TimeLimitExceeded()
class InstanceOperation(Operation): class InstanceOperation(Operation):
...@@ -722,8 +724,8 @@ class SaveAsTemplateOperation(InstanceOperation): ...@@ -722,8 +724,8 @@ class SaveAsTemplateOperation(InstanceOperation):
if with_shutdown: if with_shutdown:
try: try:
ShutdownOperation(self.instance).call(parent_activity=activity, self.instance.shutdown(parent_activity=activity,
user=user, task=task) user=user, task=task)
except Instance.WrongStateError: except Instance.WrongStateError:
pass pass
...@@ -803,7 +805,7 @@ class ShutdownOperation(AbortableRemoteOperationMixin, ...@@ -803,7 +805,7 @@ class ShutdownOperation(AbortableRemoteOperationMixin,
resultant_state = 'STOPPED' resultant_state = 'STOPPED'
task = vm_tasks.shutdown task = vm_tasks.shutdown
remote_queue = ("vm", "slow") remote_queue = ("vm", "slow")
remote_timeout = 120 remote_timeout = 180
def _operation(self, task): def _operation(self, task):
super(ShutdownOperation, self)._operation(task=task) super(ShutdownOperation, self)._operation(task=task)
...@@ -1191,6 +1193,70 @@ class DisableOperation(NodeOperation): ...@@ -1191,6 +1193,70 @@ class DisableOperation(NodeOperation):
@register_operation @register_operation
class UpdateNodeOperation(NodeOperation):
id = 'update_node'
name = _("update node")
description = _("Upgrade or install node software (vmdriver, agentdriver, "
"monitor-client) with Salt.")
required_perms = ()
online_required = False
async_queue = "localhost.man.slow"
def minion_cmd(self, module, params, timeout=3600):
name = self.node.host.hostname
client = LocalClient()
data = client.cmd(
name, module, params, timeout=timeout)
try:
data = data[name]
except KeyError:
raise HumanReadableException.create(ugettext_noop(
"No minions matched the target."))
if not isinstance(data, dict):
raise HumanReadableException.create(ugettext_noop(
"Unhandled exception: %(msg)s"), msg=unicode(data))
return data
def _operation(self, activity):
with activity.sub_activity(
'upgrade_packages',
readable_name=ugettext_noop('upgrade packages')) as sa:
data = self.minion_cmd('pkg.upgrade', [])
upgraded = len(filter(lambda x: x['old'] and x['new'],
data.values()))
installed = len(filter(lambda x: not x['old'] and x['new'],
data.values()))
removed = len(filter(lambda x: x['old'] and not x['new'],
data.values()))
sa.result = create_readable(ugettext_noop(
"Upgraded: %(upgraded)s, Installed: %(installed)s, "
"Removed: %(removed)s"), upgraded=upgraded,
installed=installed, removed=removed)
data = self.minion_cmd('state.sls', ['node', 'nfs-client'])
failed = 0
for k, v in data.iteritems():
logger.debug('salt state %s %s', k, v)
act_name = ': '.join(k.split('_|-')[:2])
if not v["result"] or v["changes"]:
act = activity.create_sub(
act_name[:70], readable_name=act_name)
act.result = create_readable(ugettext_noop(
"Changes: %(changes)s Comment: %(comment)s"),
changes=v["changes"], comment=v["comment"])
act.finish(v["result"])
if not v["result"]:
failed += 1
if failed:
raise HumanReadableException.create(ugettext_noop(
"Failed: %(failed)s"), failed=failed)
@register_operation
class ScreenshotOperation(RemoteInstanceOperation): class ScreenshotOperation(RemoteInstanceOperation):
id = 'screenshot' id = 'screenshot'
name = _("screenshot") name = _("screenshot")
......
...@@ -35,7 +35,8 @@ Update the package lists, and install the required system software:: ...@@ -35,7 +35,8 @@ Update the package lists, and install the required system software::
sudo apt-get update sudo apt-get update
sudo apt-get install --yes virtualenvwrapper postgresql git \ sudo apt-get install --yes virtualenvwrapper postgresql git \
python-pip rabbitmq-server libpq-dev python-dev ntp memcached \ python-pip rabbitmq-server libpq-dev python-dev ntp memcached \
libmemcached-dev gettext wget pwgen nginx libmemcached-dev gettext wget pwgen nginx npm nodejs-legacy
sudo npm -g install bower less yuglify
Set up *PostgreSQL* to listen on localhost and restart it:: Set up *PostgreSQL* to listen on localhost and restart it::
......
...@@ -61,8 +61,8 @@ Update the package lists, and install the required system software:: ...@@ -61,8 +61,8 @@ Update the package lists, and install the required system software::
sudo apt-get update sudo apt-get update
sudo apt-get install --yes virtualenvwrapper postgresql git \ sudo apt-get install --yes virtualenvwrapper postgresql git \
python-pip rabbitmq-server libpq-dev python-dev ntp memcached \ python-pip rabbitmq-server libpq-dev python-dev ntp memcached \
libmemcached-dev npm libmemcached-dev npm nodejs-legacy
sudo npm -g install bower less sudo npm -g install bower less yuglify
Set up *PostgreSQL* to listen on localhost and restart it:: Set up *PostgreSQL* to listen on localhost and restart it::
......
...@@ -28,6 +28,7 @@ pylibmc==1.3.0 ...@@ -28,6 +28,7 @@ pylibmc==1.3.0
python-dateutil==2.2 python-dateutil==2.2
pytz==2014.2 pytz==2014.2
requests==2.2.1 requests==2.2.1
salt==2014.1.0
simplejson==3.4.0 simplejson==3.4.0
six==1.6.1 six==1.6.1
South==0.8.4 South==0.8.4
......
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