Commit e00e2679 by Kálmán Viktor

Merge branch 'feature-template-wizard' into 'master'

Feature Template Wizard

  pip install and migrate required

django-admin.py makemessages -d djangojs -l hu --settings=circle.settings.local --ignore=jsi18n/*
python manage.py compilejsi18n --settings=circle.settings.local -o dashboard/static/jsi18n -l hu
python manage.py compilejsi18n --settings=circle.settings.local -o dashboard/static/jsi18n -l en
parents be367e47 c9d678ce
...@@ -35,3 +35,6 @@ circle/*.pem ...@@ -35,3 +35,6 @@ circle/*.pem
# collected static files: # collected static files:
circle/static circle/static
circle/static_collected circle/static_collected
# jsi18n files
jsi18n
...@@ -253,6 +253,7 @@ THIRD_PARTY_APPS = ( ...@@ -253,6 +253,7 @@ THIRD_PARTY_APPS = (
'djcelery', 'djcelery',
'sizefield', 'sizefield',
'taggit', 'taggit',
'statici18n',
) )
# Apps specific for this project go here. # Apps specific for this project go here.
......
...@@ -27,8 +27,9 @@ from django.contrib.auth.forms import ( ...@@ -27,8 +27,9 @@ from django.contrib.auth.forms import (
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import ( from crispy_forms.layout import (
Layout, Div, BaseInput, Field, HTML, Submit, Fieldset, TEMPLATE_PACK Layout, Div, BaseInput, Field, HTML, Submit, Fieldset, TEMPLATE_PACK,
) )
from crispy_forms.utils import render_field from crispy_forms.utils import render_field
from django import forms from django import forms
from django.forms.widgets import TextInput from django.forms.widgets import TextInput
...@@ -44,9 +45,6 @@ from vm.models import ( ...@@ -44,9 +45,6 @@ from vm.models import (
) )
from .models import Profile from .models import Profile
VLANS = Vlan.objects.all()
DISKS = Disk.objects.exclude(type="qcow2-snap")
class VmCustomizeForm(forms.Form): class VmCustomizeForm(forms.Form):
name = forms.CharField() name = forms.CharField()
...@@ -479,35 +477,21 @@ class NodeForm(forms.ModelForm): ...@@ -479,35 +477,21 @@ class NodeForm(forms.ModelForm):
class TemplateForm(forms.ModelForm): class TemplateForm(forms.ModelForm):
networks = forms.ModelMultipleChoiceField( networks = forms.ModelMultipleChoiceField(
queryset=VLANS, required=False) queryset=None, required=False, label=_("Networks"))
system = forms.CharField(widget=forms.TextInput)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
parent = kwargs.pop("parent", None)
self.user = kwargs.pop("user", None) self.user = kwargs.pop("user", None)
super(TemplateForm, self).__init__(*args, **kwargs) super(TemplateForm, self).__init__(*args, **kwargs)
self.fields['networks'].queryset = Vlan.get_objects_with_level(
'user', self.user)
data = self.data.copy() data = self.data.copy()
data['owner'] = self.user.pk data['owner'] = self.user.pk
self.data = data self.data = data
if parent is not None: if self.instance.pk:
template = InstanceTemplate.objects.get(pk=parent) n = self.instance.interface_set.values_list("vlan", flat=True)
parent = template.__dict__
fields = ["system", "name", "num_cores", "boot_menu", "ram_size",
"priority", "access_method", "raw_data",
"arch", "description"]
for f in fields:
self.initial[f] = parent[f]
self.initial['lease'] = parent['lease_id']
self.initial['parent'] = template
self.initial['name'] = "Clone of %s" % self.initial['name']
self.for_networks = template
else:
self.for_networks = self.instance
if self.instance.pk or parent is not None:
n = self.for_networks.interface_set.values_list("vlan", flat=True)
self.initial['networks'] = n self.initial['networks'] = n
if not self.instance.pk and len(self.errors) < 1: if not self.instance.pk and len(self.errors) < 1:
...@@ -604,7 +588,7 @@ class TemplateForm(forms.ModelForm): ...@@ -604,7 +588,7 @@ class TemplateForm(forms.ModelForm):
Field('arch'), Field('arch'),
), ),
Fieldset( Fieldset(
"stuff", _("Virtual machine settings"),
Field('access_method'), Field('access_method'),
Field('boot_menu'), Field('boot_menu'),
Field('raw_data', **kwargs_raw_data), Field('raw_data', **kwargs_raw_data),
...@@ -614,7 +598,7 @@ class TemplateForm(forms.ModelForm): ...@@ -614,7 +598,7 @@ class TemplateForm(forms.ModelForm):
Field("system"), Field("system"),
), ),
Fieldset( Fieldset(
_("External"), _("External resources"),
Field("networks"), Field("networks"),
Field("lease"), Field("lease"),
Field("tags"), Field("tags"),
...@@ -626,6 +610,9 @@ class TemplateForm(forms.ModelForm): ...@@ -626,6 +610,9 @@ class TemplateForm(forms.ModelForm):
class Meta: class Meta:
model = InstanceTemplate model = InstanceTemplate
exclude = ('state', 'disks', ) exclude = ('state', 'disks', )
widgets = {
'system': forms.TextInput
}
class LeaseForm(forms.ModelForm): class LeaseForm(forms.ModelForm):
......
...@@ -35,14 +35,13 @@ from model_utils.models import TimeStampedModel ...@@ -35,14 +35,13 @@ from model_utils.models import TimeStampedModel
from model_utils.fields import StatusField from model_utils.fields import StatusField
from model_utils import Choices from model_utils import Choices
from vm.models import Instance
from acl.models import AclBase from acl.models import AclBase
logger = getLogger(__name__) logger = getLogger(__name__)
class Favourite(Model): class Favourite(Model):
instance = ForeignKey(Instance) instance = ForeignKey("vm.Instance")
user = ForeignKey(User) user = ForeignKey(User)
......
/* ===========================================================
# bootstrap-tour - v0.9.1
# http://bootstraptour.com
# ==============================================================
# Copyright 2012-2013 Ulrich Sossou
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
*/
.tour-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1100;background-color:#000;opacity:.8}.tour-step-backdrop{position:relative;z-index:1101;background:inherit}.tour-step-background{position:absolute;z-index:1100;background:inherit;border-radius:6px}.popover[class*=tour-]{z-index:1100}.popover[class*=tour-] .popover-navigation{padding:9px 14px}.popover[class*=tour-] .popover-navigation [data-role=end]{float:right}.popover[class*=tour-] .popover-navigation [data-role=prev],.popover[class*=tour-] .popover-navigation [data-role=next],.popover[class*=tour-] .popover-navigation [data-role=end]{cursor:pointer}.popover[class*=tour-] .popover-navigation [data-role=prev].disabled,.popover[class*=tour-] .popover-navigation [data-role=next].disabled,.popover[class*=tour-] .popover-navigation [data-role=end].disabled{cursor:default}.popover[class*=tour-].orphan{position:fixed;margin-top:0}.popover[class*=tour-].orphan .arrow{display:none}
\ No newline at end of file
...@@ -486,6 +486,55 @@ footer a, footer a:hover, footer a:visited { ...@@ -486,6 +486,55 @@ footer a, footer a:hover, footer a:visited {
margin-bottom: 20px; margin-bottom: 20px;
} }
.template-choose-list {
max-width: 600px;
}
.template-choose-list-element small {
display: none;
float: right;
padding-right: 50px;
}
.template-choose-list-element {
padding: 6px 10px;
cursor: pointer;
margin-bottom: 15px; /* bootstrap panel default is 20px */
}
.template-choose-list input[type="radio"] {
float: right;
}
/* template create vm help */
.alert-new-template {
background: #3071a9;
color: white;
font-size: 22px;
}
.alert-new-template ol {
margin-left: 25px;
}
/* bootstrap tour */
.tour-template {
max-width: 400px;
min-width: 270px;
font-size: 16px;
}
.tour-template {
text-align: justify;
}
.tour-template .popover-title {
font-weight: bold;
}
#vm-details-resources-form {
padding: 5px; /* it's nice this way in the tour */
}
.index-vm-list-name { .index-vm-list-name {
display: inline-block; display: inline-block;
max-width: 70%; max-width: 70%;
...@@ -546,6 +595,10 @@ footer a, footer a:hover, footer a:visited { ...@@ -546,6 +595,10 @@ footer a, footer a:hover, footer a:visited {
display: none; display: none;
} }
#ops {
padding: 15px 0 15px 15px;
}
#vm-access-table th:last-child, #vm-access-table td:last-child, #vm-access-table th:last-child, #vm-access-table td:last-child,
#template-access-table th:last-child, #template-access-table td:last-child { #template-access-table th:last-child, #template-access-table td:last-child {
text-align: center; text-align: center;
......
...@@ -33,6 +33,34 @@ $(function () { ...@@ -33,6 +33,34 @@ $(function () {
}); });
return false; return false;
}); });
$('.template-choose').click(function(e) {
$.ajax({
type: 'GET',
url: '/dashboard/template/choose/',
success: function(data) {
$('body').append(data);
vmCreateLoaded();
addSliderMiscs();
$('#create-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
});
// check if user selected anything
$("#template-choose-next-button").click(function() {
var radio = $('input[type="radio"]:checked', "#template-choose-form").val();
if(!radio) {
$("#template-choose-alert").addClass("alert-warning")
.text(gettext("Select an option to proceed!"));
return false;
}
return true;
});
}
});
return false;
});
$('[href=#index-graph-view]').click(function (e) { $('[href=#index-graph-view]').click(function (e) {
var box = $(this).data('index-box'); var box = $(this).data('index-box');
$("#" + box + "-list-view").hide(); $("#" + box + "-list-view").hide();
...@@ -327,10 +355,10 @@ function massDeleteVm(data) { ...@@ -327,10 +355,10 @@ function massDeleteVm(data) {
type: 'POST', type: 'POST',
data: {'vms': data['data']['v']}, data: {'vms': data['data']['v']},
success: function(re, textStatus, xhr) { success: function(re, textStatus, xhr) {
for(var i=0; i< selected.length; i++) for(var i=0; i< data['data']['v'].length; i++)
$('.vm-list-table tbody tr').eq(data['data']['selected'][i]).fadeOut(500, function() { $('.vm-list-table tbody tr[data-vm-pk="' + data['data']['v'][i] + '"]').fadeOut(500, function() {
selected = [];
// reset group buttons // reset group buttons
selected = []
$('.vm-list-group-control a').attr('disabled', true); $('.vm-list-group-control a').attr('disabled', true);
$(this).remove(); $(this).remove();
}); });
......
$(function() {
$(".vm-details-start-template-tour").click(function() {
ttour = createTemplateTour();
ttour.init();
ttour.start();
});
});
function createTemplateTour() {
var ttour = new Tour({
storage: false,
name: "template",
template: "<div class='popover'>" +
"<div class='arrow'></div>" +
"<h3 class='popover-title'></h3>" +
"<div class='popover-content'></div>" +
"<div class='popover-navigation'>" +
"<div class='btn-group'>" +
"<button class='btn btn-sm btn-default' data-role='prev'>" +
'<i class="icon-chevron-left"></i> ' + gettext("Prev") + "</button> " +
"<button class='btn btn-sm btn-default' data-role='next'>" +
gettext("Next") + ' <i class="icon-chevron-right"></i></button> ' +
"<button class='btn btn-sm btn-default' data-role='pause-resume' data-pause-text='Pause' data-resume-text='Resume'>Pause</button> " +
"</div>" +
"<button class='btn btn-sm btn-default' data-role='end'>" +
gettext("End tour") + ' <i class="icon-flag-checkered"></i></button>' +
"</div>" +
"</div>",
});
ttour.addStep({
element: "#vm-details-template-tour-button",
title: gettext("Template Tutorial Tour"),
content: "<p>" + gettext("Welcome to the template tutorial. In this quick tour, we gonna show you how to do the steps described above.") + "</p>" +
"<p>" + gettext('For the next tour step press the "Next" button or the right arrow (or "Back" button/left arrow for the previous step).') + "</p>" +
"<p>" + gettext("During the tour please don't try the functions because it may lead to graphical glitches, however " +
"you can end the tour any time you want with the End Tour button!") + "</p>",
placement: "bottom",
backdrop: true,
});
ttour.addStep({
backdrop: true,
element: 'a[href="#home"]',
title: gettext("Home tab"),
content: gettext("In this tab you can tag your virtual machine and modify the name and description."),
placement: 'top',
onShow: function() {
$('a[href="#home"]').trigger("click");
},
});
ttour.addStep({
element: 'a[href="#resources"]',
title: gettext("Resources tab"),
backdrop: true,
placement: 'top',
content: gettext("On the resources tab you can edit the CPU/RAM options and add/remove disks!"),
onShow: function() {
$('a[href="#resources"]').trigger("click");
},
});
ttour.addStep({
element: '#vm-details-resources-form',
placement: 'top',
backdrop: true,
title: gettext("Resources"),
content: '<p><strong>' + gettext("CPU priority") + ":</strong> " + gettext("higher is better") + "</p>" +
'<p><strong>' + gettext("CPU count") + ":</strong> " + gettext("number of CPU cores.") + "</p>" +
'<p><strong>' + gettext("RAM amount") + ":</strong> " + gettext("amount of RAM.") + "</p>",
onShow: function() {
$('a[href="#resources"]').trigger("click");
},
});
ttour.addStep({
element: '#vm-details-resources-disk',
backdrop: true,
placement: 'top',
title: gettext("Disks"),
content: gettext("You can add empty disks, download new ones and remove existing ones here."),
onShow: function() {
$('a[href="#resources"]').trigger("click");
},
});
ttour.addStep({
element: 'a[href="#network"]',
backdrop: true,
placement: 'top',
title: gettext("Network tab"),
content: gettext('You can add new network interfaces or remove existing ones here.'),
onShow: function() {
$('a[href="#network"]').trigger("click");
},
});
ttour.addStep({
element: "#ops",
title: '<i class="icon-play"></i> ' + gettext("Deploy"),
placement: "left",
backdrop: true,
content: gettext("Deploy the virtual machine."),
});
ttour.addStep({
element: "#vm-info-pane",
title: gettext("Connect"),
placement: "top",
backdrop: true,
content: gettext("Use the connection string or connect with your choice of client!"),
});
ttour.addStep({
element: "#vm-info-pane",
placement: "top",
title: gettext("Customize the virtual machine"),
content: gettext("After you have connected to the virtual machine do your modifications then log off."),
});
ttour.addStep({
element: "#ops",
title: '<i class="icon-save"></i> ' + gettext("Save as"),
placement: "left",
backdrop: true,
content: gettext('Press the "Save as template" button and wait until the activity finishes.'),
});
ttour.addStep({
element: ".alert-new-template",
title: gettext("Finish"),
backdrop: true,
placement: "bottom",
content: gettext("This is the last message, if something is not clear you can do the the tour again!"),
});
return ttour;
}
...@@ -17,8 +17,10 @@ ...@@ -17,8 +17,10 @@
<script src="{{ STATIC_URL }}dashboard/js/jquery.knob.js"></script> <script src="{{ STATIC_URL }}dashboard/js/jquery.knob.js"></script>
<script src="{{ STATIC_URL}}dashboard/bootstrap-slider/bootstrap-slider.js"></script> <script src="{{ STATIC_URL}}dashboard/bootstrap-slider/bootstrap-slider.js"></script>
<link rel="stylesheet" href="{{ STATIC_URL }}dashboard/bootstrap-slider/slider.css"/> <link rel="stylesheet" href="{{ STATIC_URL }}dashboard/bootstrap-slider/slider.css"/>
<link href="{{ STATIC_URL }}dashboard/bootstrap-tour.min.css" rel="stylesheet">
<link href="{{ STATIC_URL }}dashboard/dashboard.css" rel="stylesheet"> <link href="{{ STATIC_URL }}dashboard/dashboard.css" rel="stylesheet">
<script src="{{ STATIC_URL }}dashboard/dashboard.js"></script> <script src="{{ STATIC_URL }}dashboard/dashboard.js"></script>
<script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script>
</head> </head>
<body> <body>
......
{% load i18n %}
<div class="alert alert-info" id="template-choose-alert">
{% trans "Customize an existing template or create a brand new one from scratch!" %}
</div>
<form action="{% url "dashboard.views.template-choose" %}" method="POST"
id="template-choose-form">
{% csrf_token %}
<div class="template-choose-list">
{% for t in templates %}
<div class="panel panel-default template-choose-list-element">
<input type="radio" name="parent" value="{{ t.pk }}"/>
{{ t.name }} - {{ t.system }}
<small>Cores: {{ t.num_cores }} RAM: {{ t.ram_size }}</small>
<div class="clearfix"></div>
</div>
{% endfor %}
<div class="panel panel-default template-choose-list-element">
<input type="radio" name="parent" value="base_vm"/>
{% trans "Create a new base VM without disk" %}
</div>
<button type="submit" id="template-choose-next-button" class="btn btn-success pull-right">{% trans "Next" %}</button>
<div class="clearfix"></div>
</div>
</form>
<script>
$(function() {
$(".template-choose-list-element").click(function() {
$("input", $(this)).prop("checked", true);
});
$(".template-choose-list-element").hover(
function() {
$("small", $(this)).stop().fadeIn(200);
},
function() {
$("small", $(this)).stop().fadeOut(200);
}
);
});
</script>
{% extends "dashboard/base.html" %}
{% load i18n %} {% load i18n %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title-page %}{% trans "Create base VM" %}{% endblock %} {% if leases < 1 %}
<div class="alert alert-warning">
{% block content %} {% trans "You haven't created any leases yet, but you need one to create a template!" %}
<div class="row"> <a href="{% url "dashboard.views.lease-create" %}">{% trans "Create a new lease now." %}</a>
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="icon-desktop"></i> {% trans "Create base VM" %}</h3>
</div> </div>
<div class="panel-body"> {% endif %}
{% with form=form %}
{% with form=form %}
{% include "display-form-errors.html" %} {% include "display-form-errors.html" %}
{% endwith %} {% endwith %}
{% crispy form %} {% crispy form %}
</div>
</div>
</div>
</div>
<style> <style>
fieldset { fieldset {
...@@ -35,5 +26,3 @@ ...@@ -35,5 +26,3 @@
$("#hint_id_num_cores, #hint_id_priority, #hint_id_ram_size").hide(); $("#hint_id_num_cores, #hint_id_priority, #hint_id_ram_size").hide();
}); });
</script> </script>
{% endblock %}
...@@ -13,7 +13,9 @@ ...@@ -13,7 +13,9 @@
<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> <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> <button id="confirmation-modal-button" type="button" class="btn btn-danger"
{% if disable_submit %}disabled{% endif %}
>{% trans "Delete" %}</button>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
......
...@@ -26,9 +26,12 @@ ...@@ -26,9 +26,12 @@
{% 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 }}"/>
<button class="btn btn-danger">{% trans "Yes" %}</button> <button class="btn btn-danger"
{% if disable_submit %}disabled{% endif %}
>{% trans "Yes" %}</button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}
...@@ -22,15 +22,16 @@ ...@@ -22,15 +22,16 @@
<p> <p>
{% trans "You don't have any templates, however you can still start virtual machines and even save them as new templates!" %} {% trans "You don't have any templates, however you can still start virtual machines and even save them as new templates!" %}
</p> </p>
<p>
{% trans "The new button below creates a new base vm, please use only if necessary!" %}
</p>
</div> </div>
{% endfor %} {% endfor %}
<div href="#" class="list-group-item list-group-footer text-right"> <div href="#" class="list-group-item list-group-footer text-right">
<p> <p>
<a href="{% url "dashboard.views.template-list" %}" class="btn btn-primary btn-xs"><i class="icon-chevron-sign-right"></i> {% trans "show all" %}</a> <a href="{% url "dashboard.views.template-list" %}" class="btn btn-primary btn-xs">
<a href="{% url "dashboard.views.template-create" %}" class="btn btn-success btn-xs"><i class="icon-plus-sign"></i> {% trans "new" %}</a> <i class="icon-chevron-sign-right"></i> {% trans "show all" %}
</a>
<a href="{% url "dashboard.views.template-choose" %}" class="btn btn-success btn-xs template-choose">
<i class="icon-plus-sign"></i> {% trans "new" %}
</a>
</p> </p>
</div> </div>
</div> </div>
......
{% load i18n %} {% load i18n %}
<a href="{% url "dashboard.views.template-create" %}?parent={{ record.pk }}" id="template-list-clone-button" class="btn btn-default btn-xs" title="{% trans "Clone" %}">
<i class="icon-copy"></i>
</a>
<a href="{% url "dashboard.views.template-detail" pk=record.pk%}" id="template-list-edit-button" class="btn btn-default btn-xs" title="{% trans "Edit" %}"> <a href="{% url "dashboard.views.template-detail" pk=record.pk%}" id="template-list-edit-button" class="btn btn-default btn-xs" title="{% trans "Edit" %}">
<i class="icon-edit"></i> <i class="icon-edit"></i>
</a> </a>
......
...@@ -4,9 +4,40 @@ ...@@ -4,9 +4,40 @@
{% block title-page %}{{ instance.name }} | vm{% endblock %} {% block title-page %}{{ instance.name }} | vm{% endblock %}
{% block content %} {% block content %}
{% if instance.is_base %}
<div class="alert alert-info alert-new-template">
<strong>{% trans "This is the master vm of your new template" %}</strong>
<div id="vm-details-template-tour-button" class="pull-right">
<a href="#" class="btn btn-default btn-lg pull-right vm-details-start-template-tour">
<i class="icon-play"></i> {% trans "Start template tutorial" %}
</a>
</div>
<ol>
<li>{% trans "Modify the virtual machine to suit your needs <strong>(optional)</strong>" %}
<ul>
<li>{% trans "Change the name and description" %}</li>
<li>{% trans "Change the resources (CPU and RAM)" %}</li>
<li>{% trans "Attach or detach disks" %}</li>
<li>{% trans "Add or remove network interfaces" %}</li>
</ul>
</li>
<li>{% trans "Deploy the virtual machine" %}</li>
<li>{% trans "Connect to the machine" %}</li>
<li>{% trans "Do all the needed installations/customizations" %}</li>
<li>{% trans "Log off from the machine" %}</li>
<li>
{% trans "Press the Save as template button" %}
</li>
<li>
{% trans "Delete this virtual machine <strong>(optional)</strong>" %}
</li>
</ol>
</div>
{% endif %}
<div class="body-content"> <div class="body-content">
<div class="page-header"> <div class="page-header">
<div class="pull-right" style="padding-top: 15px;" id="ops"> <div class="pull-right" id="ops">
{% include "dashboard/vm-detail/_operations.html" %} {% include "dashboard/vm-detail/_operations.html" %}
</div> </div>
<h1> <h1>
...@@ -112,8 +143,10 @@ ...@@ -112,8 +143,10 @@
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<script src="{{ STATIC_URL }}dashboard/bootstrap-tour.min.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-details.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-details.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-common.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-common.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-console.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-console.js"></script>
<script src="{{ STATIC_URL }}dashboard/disk-list.js"></script> <script src="{{ STATIC_URL }}dashboard/disk-list.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-tour.js"></script>
{% endblock %} {% endblock %}
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
<hr /> <hr />
<div class="row"> <div class="row" id="vm-details-resources-disk">
<div class="col-sm-11"> <div class="col-sm-11">
<h3> <h3>
{% trans "Disks" %} {% trans "Disks" %}
......
...@@ -289,6 +289,7 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -289,6 +289,7 @@ class VmDetailTest(LoginMixin, TestCase):
tmpl = InstanceTemplate.objects.get(id=1) tmpl = InstanceTemplate.objects.get(id=1)
tmpl.set_level(self.u1, 'owner') tmpl.set_level(self.u1, 'owner')
tmpl.disks.get().set_level(self.u1, 'owner') tmpl.disks.get().set_level(self.u1, 'owner')
Vlan.objects.get(id=1).set_level(self.u1, 'user')
kwargs = tmpl.__dict__.copy() kwargs = tmpl.__dict__.copy()
kwargs.update(name='t1', lease=1, disks=1, raw_data='tst1') kwargs.update(name='t1', lease=1, disks=1, raw_data='tst1')
response = c.post('/dashboard/template/1/', kwargs) response = c.post('/dashboard/template/1/', kwargs)
...@@ -305,11 +306,21 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -305,11 +306,21 @@ class VmDetailTest(LoginMixin, TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(InstanceTemplate.objects.get(id=1).raw_data, 'tst2') self.assertEqual(InstanceTemplate.objects.get(id=1).raw_data, 'tst2')
def test_permitted_lease_delete(self): def test_permitted_lease_delete_w_template_using_it(self):
c = Client() c = Client()
self.login(c, 'superuser') self.login(c, 'superuser')
leases = Lease.objects.count() leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/1/") response = c.post("/dashboard/lease/delete/1/")
self.assertEqual(response.status_code, 400)
self.assertEqual(leases, Lease.objects.count())
def test_permitted_lease_delete_w_template_not_using_it(self):
c = Client()
self.login(c, 'superuser')
lease = Lease.objects.create(name="yay")
leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/%d/" % lease.pk)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(leases - 1, Lease.objects.count()) self.assertEqual(leases - 1, Lease.objects.count())
......
...@@ -30,6 +30,7 @@ from .views import ( ...@@ -30,6 +30,7 @@ from .views import (
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView, TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView, VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView, VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
TemplateChoose,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -44,6 +45,8 @@ urlpatterns = patterns( ...@@ -44,6 +45,8 @@ urlpatterns = patterns(
url(r'^template/create/$', TemplateCreate.as_view(), url(r'^template/create/$', TemplateCreate.as_view(),
name="dashboard.views.template-create"), name="dashboard.views.template-create"),
url(r'^template/choose/$', TemplateChoose.as_view(),
name="dashboard.views.template-choose"),
url(r'template/(?P<pk>\d+)/acl/$', TemplateAclUpdateView.as_view(), url(r'template/(?P<pk>\d+)/acl/$', TemplateAclUpdateView.as_view(),
name='dashboard.views.template-acl'), name='dashboard.views.template-acl'),
url(r'^template/(?P<pk>\d+)/$', TemplateDetail.as_view(), url(r'^template/(?P<pk>\d+)/$', TemplateDetail.as_view(),
...@@ -52,6 +55,7 @@ urlpatterns = patterns( ...@@ -52,6 +55,7 @@ urlpatterns = patterns(
name="dashboard.views.template-list"), name="dashboard.views.template-list"),
url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(), url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(),
name="dashboard.views.template-delete"), name="dashboard.views.template-delete"),
url(r'^vm/(?P<pk>\d+)/op/', include('dashboard.vm.urls')), url(r'^vm/(?P<pk>\d+)/op/', include('dashboard.vm.urls')),
url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(), url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'), name='dashboard.views.remove-port'),
......
...@@ -852,22 +852,73 @@ class GroupAclUpdateView(AclUpdateView): ...@@ -852,22 +852,73 @@ class GroupAclUpdateView(AclUpdateView):
kwargs=self.kwargs)) kwargs=self.kwargs))
class TemplateChoose(TemplateView):
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/modal-wrapper.html']
else:
return ['dashboard/nojs-wrapper.html']
def get_context_data(self, *args, **kwargs):
context = super(TemplateChoose, self).get_context_data(*args, **kwargs)
templates = InstanceTemplate.get_objects_with_level("user",
self.request.user)
context.update({
'box_title': _('Choose template'),
'ajax_title': False,
'template': "dashboard/_template-choose.html",
'templates': templates.all(),
})
return context
def post(self, request, *args, **kwargs):
if not request.user.has_perm('vm.create_template'):
raise PermissionDenied()
template = request.POST.get("parent")
if template == "base_vm":
return redirect(reverse("dashboard.views.template-create"))
elif template is None:
messages.warning(request, _("Select an option to proceed!"))
return redirect(reverse("dashboard.views.template-choose"))
else:
template = get_object_or_404(InstanceTemplate, pk=template)
instance = Instance.create_from_template(
template=template, owner=request.user, is_base=True)
return redirect(instance.get_absolute_url())
class TemplateCreate(SuccessMessageMixin, CreateView): class TemplateCreate(SuccessMessageMixin, CreateView):
model = InstanceTemplate model = InstanceTemplate
form_class = TemplateForm form_class = TemplateForm
template_name = "dashboard/template-create.html"
success_message = _("Successfully created a new template!") def get_template_names(self):
if self.request.is_ajax():
pass
else:
return ['dashboard/nojs-wrapper.html']
def get_context_data(self, *args, **kwargs):
context = super(TemplateCreate, self).get_context_data(*args, **kwargs)
context.update({
'box_title': _("Create a new base VM"),
'template': "dashboard/_template-create.html",
'leases': Lease.objects.count()
})
return context
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
if not self.request.user.has_perm('vm.create_template'): if not self.request.user.has_perm('vm.create_template'):
raise PermissionDenied() raise PermissionDenied()
self.parent = self.request.GET.get("parent")
return super(TemplateCreate, self).get(*args, **kwargs) return super(TemplateCreate, self).get(*args, **kwargs)
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super(TemplateCreate, self).get_form_kwargs() kwargs = super(TemplateCreate, self).get_form_kwargs()
kwargs['parent'] = getattr(self, "parent", None)
kwargs['user'] = self.request.user kwargs['user'] = self.request.user
return kwargs return kwargs
...@@ -880,24 +931,27 @@ class TemplateCreate(SuccessMessageMixin, CreateView): ...@@ -880,24 +931,27 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
return self.get(request, form, *args, **kwargs) return self.get(request, form, *args, **kwargs)
else: else:
post = form.cleaned_data post = form.cleaned_data
networks = self.__create_networks(post.pop("networks"),
networks = self.__create_networks(post.pop("networks")) request.user)
post.pop("parent")
post['max_ram_size'] = post['ram_size']
req_traits = post.pop("req_traits") req_traits = post.pop("req_traits")
tags = post.pop("tags") tags = post.pop("tags")
post['pw'] = User.objects.make_random_password() post['pw'] = User.objects.make_random_password()
post.pop("parent") post['is_base'] = True
post['max_ram_size'] = post['ram_size'] inst = Instance.create(params=post, disks=[],
inst = Instance.create(params=post, disks=[], networks=networks, networks=networks,
tags=tags, req_traits=req_traits) tags=tags, req_traits=req_traits)
messages.success(request, _("The template has been created, "
"you can now add disks to it!"))
return redirect("%s#resources" % inst.get_absolute_url()) return redirect("%s#resources" % inst.get_absolute_url())
return super(TemplateCreate, self).post(self, request, args, kwargs) return super(TemplateCreate, self).post(self, request, args, kwargs)
def __create_networks(self, vlans): def __create_networks(self, vlans, user):
networks = [] networks = []
for v in vlans: for v in vlans:
if not v.has_level(user, "user"):
raise PermissionDenied()
networks.append(InterfaceTemplate(vlan=v, managed=v.managed)) networks.append(InterfaceTemplate(vlan=v, managed=v.managed))
return networks return networks
...@@ -962,6 +1016,9 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -962,6 +1016,9 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
for disk in self.get_object().disks.all(): for disk in self.get_object().disks.all():
if not disk.has_level(request.user, 'user'): if not disk.has_level(request.user, 'user'):
raise PermissionDenied() raise PermissionDenied()
for network in self.get_object().interface_set.all():
if not network.vlan.has_level(request.user, "user"):
raise PermissionDenied()
return super(TemplateDetail, self).post(self, request, args, kwargs) return super(TemplateDetail, self).post(self, request, args, kwargs)
def get_form_kwargs(self): def get_form_kwargs(self):
...@@ -1711,9 +1768,26 @@ class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): ...@@ -1711,9 +1768,26 @@ class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
else: else:
return ['dashboard/confirm/base-delete.html'] return ['dashboard/confirm/base-delete.html']
def get_context_data(self, *args, **kwargs):
c = super(LeaseDelete, self).get_context_data(*args, **kwargs)
lease = self.get_object()
templates = lease.instancetemplate_set
if templates.count() > 0:
text = _("You can't delete this lease because some templates "
"are still using it, modify these to proceed: ")
c['text'] = text + ", ".join("<strong>%s (#%d)</strong>"
"" % (o.name, o.pk)
for o in templates.all())
c['disable_submit'] = True
return c
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
object = self.get_object() object = self.get_object()
if (object.instancetemplate_set.count() > 0):
raise SuspiciousOperation()
object.delete() object.delete()
success_url = self.get_success_url() success_url = self.get_success_url()
success_message = _("Lease successfully deleted!") success_message = _("Lease successfully deleted!")
......
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-04-16 08:59+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: dashboard/static/dashboard/vm-tour.js:23
msgid "Prev"
msgstr "Előző"
#: dashboard/static/dashboard/vm-tour.js:25
msgid "Next"
msgstr "Következő"
#: dashboard/static/dashboard/vm-tour.js:29
msgid "End tour"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:36
msgid "Template Tutorial Tour"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:37
msgid ""
"Welcome to the template tutorial. In this quick tour, we gonna show you how "
"to do the steps described above."
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:38
msgid ""
"For the next tour step press the \"Next\" button or the right arrow (or "
"\"Back\" button/left arrow for the previous step)."
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:39
msgid ""
"During the tour please don't try the functions because it may lead to "
"graphical glitches, however "
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:48
msgid "Home tab"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:49
msgid ""
"In this tab you can tag your virtual machine and modify the description."
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:58
msgid "Resources tab"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:61
msgid ""
"On the resources tab you can edit the CPU/RAM options and add/remove disks!"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:71
msgid "Resources"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:72
msgid "CPU priority"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:72
msgid "higher (or lower?) is better"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:73
msgid "CPU count"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:73
msgid "number of CPU cores."
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:74
msgid "RAM amount"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:74
msgid "amount of RAM."
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:84
msgid "Disks"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:85
msgid ""
"You can add empty disks, download new ones and remove existing ones here."
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:95
msgid "Network tab"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:96
msgid "You can add new network interfaces or remove existing ones here."
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:105
msgid "Deploy"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:108
msgid "Deploy the virtual machine."
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:113
msgid "Connect"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:116
msgid "Use the connection string or connect with your choice of client!"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:123
msgid "Customize the virtual machine"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:124
msgid "After you have connected to the virtual do you modifications."
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:129
msgid "Save as"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:132
msgid ""
"Press the \"Save as template\" button and wait until the activity finishes."
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:138
msgid "Finisih"
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:141
msgid ""
"This is the last message, if something is not clear you can do the the tour "
"again!"
msgstr ""
#: network/static/js/host.js:10
msgid ""
"Are you sure you want to remove host group <strong>\"%(group)s\"</strong> "
"from <strong>\"%(host)s\"</strong>?"
msgstr ""
#: network/static/js/host.js:13
msgid "Are you sure you want to delete this rule?"
msgstr ""
#: network/static/js/host.js:20 network/static/js/switch-port.js:14
msgid "Cancel"
msgstr ""
#: network/static/js/host.js:25 network/static/js/switch-port.js:19
msgid "Remove"
msgstr ""
#: network/static/js/switch-port.js:8
msgid "Are you sure you want to delete this device?"
msgstr ""
...@@ -82,7 +82,7 @@ ...@@ -82,7 +82,7 @@
</div><!-- .footer-container .container --> </div><!-- .footer-container .container -->
<script src="//code.jquery.com/jquery-latest.js"></script> <script src="//code.jquery.com/jquery-latest.js"></script>
<script src="{% url "network.js_catalog" %}"></script> <script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="{% static "js/bootbox.min.js" %}"></script> <script src="{% static "js/bootbox.min.js" %}"></script>
<script src="{% static "js/network.js" %}"></script> <script src="{% static "js/network.js" %}"></script>
......
...@@ -32,10 +32,6 @@ from .views import (IndexView, ...@@ -32,10 +32,6 @@ from .views import (IndexView,
remove_host_group, add_host_group, remove_host_group, add_host_group,
remove_switch_port_device, add_switch_port_device) remove_switch_port_device, add_switch_port_device)
js_info_dict = {
'packages': ('network', ),
}
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url('^$', IndexView.as_view(), name='network.index'), url('^$', IndexView.as_view(), name='network.index'),
...@@ -109,8 +105,4 @@ urlpatterns = patterns( ...@@ -109,8 +105,4 @@ urlpatterns = patterns(
remove_switch_port_device, name='network.remove_switch_port_device'), remove_switch_port_device, name='network.remove_switch_port_device'),
url('^switchports/(?P<pk>\d+)/add/$', add_switch_port_device, url('^switchports/(?P<pk>\d+)/add/$', add_switch_port_device,
name='network.add_switch_port_device'), name='network.add_switch_port_device'),
# js gettext catalog
url(r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict,
name="network.js_catalog"),
) )
...@@ -103,7 +103,8 @@ class VirtualMachineDescModel(BaseResourceConfigModel): ...@@ -103,7 +103,8 @@ class VirtualMachineDescModel(BaseResourceConfigModel):
boot_menu = BooleanField(verbose_name=_('boot menu'), default=False, boot_menu = BooleanField(verbose_name=_('boot menu'), default=False,
help_text=_( help_text=_(
'Show boot device selection menu on boot.')) 'Show boot device selection menu on boot.'))
lease = ForeignKey(Lease, help_text=_("Preferred expiration periods.")) lease = ForeignKey(Lease, help_text=_("Preferred expiration periods."),
verbose_name=_("Lease"))
raw_data = TextField(verbose_name=_('raw_data'), blank=True, help_text=_( raw_data = TextField(verbose_name=_('raw_data'), blank=True, help_text=_(
'Additional libvirt domain parameters in XML format.')) 'Additional libvirt domain parameters in XML format.'))
req_traits = ManyToManyField(Trait, blank=True, req_traits = ManyToManyField(Trait, blank=True,
...@@ -238,6 +239,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -238,6 +239,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
vnc_port = IntegerField(blank=True, default=None, null=True, vnc_port = IntegerField(blank=True, default=None, null=True,
help_text=_("TCP port where VNC console listens."), help_text=_("TCP port where VNC console listens."),
unique=True, verbose_name=_('vnc_port')) unique=True, verbose_name=_('vnc_port'))
is_base = BooleanField(default=False)
owner = ForeignKey(User) owner = ForeignKey(User)
destroyed_at = DateTimeField(blank=True, null=True, destroyed_at = DateTimeField(blank=True, null=True,
help_text=_("The virtual machine's time of " help_text=_("The virtual machine's time of "
......
...@@ -3,14 +3,15 @@ anyjson==0.3.3 ...@@ -3,14 +3,15 @@ anyjson==0.3.3
billiard==3.3.0.17 billiard==3.3.0.17
bpython==0.12 bpython==0.12
celery==3.1.11 celery==3.1.11
Django==1.6.3
django-braces==1.4.0 django-braces==1.4.0
django-celery==3.1.10 django-celery==3.1.10
django-crispy-forms==1.4.0 django-crispy-forms==1.4.0
django-model-utils==2.0.3 django-model-utils==2.0.3
django-sizefield==0.4 django-sizefield==0.4
django-statici18n==1.1
django-tables2==0.15.0 django-tables2==0.15.0
django-taggit==0.12 django-taggit==0.12
Django==1.6.3
docutils==0.11 docutils==0.11
Jinja2==2.7.2 Jinja2==2.7.2
kombu==3.0.15 kombu==3.0.15
......
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