Commit 482c59f5 by Kálmán Viktor

Merge branch 'master' into feature-store

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/urls.py
parents 9fabdf4b 61b90140
# register a signal do update permissions every migration.
# This is based on app django_extensions update_permissions command
from south.signals import post_migrate
def update_permissions_after_migration(app, **kwargs):
"""
Update app permission just after every migration.
This is based on app django_extensions update_permissions
management command.
"""
from django.conf import settings
from django.db.models import get_app, get_models
from django.contrib.auth.management import create_permissions
create_permissions(get_app(app), get_models(), 2 if settings.DEBUG else 0)
post_migrate.connect(update_permissions_after_migration)
......@@ -271,6 +271,7 @@ LOCAL_APPS = (
'dashboard',
'manager',
'acl',
'monitor',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
......
......@@ -20,7 +20,7 @@ from logging import getLogger
from .models import activity_context, has_suffix
from django.core.exceptions import PermissionDenied
from django.core.exceptions import PermissionDenied, ImproperlyConfigured
logger = getLogger(__name__)
......@@ -30,7 +30,7 @@ class Operation(object):
"""Base class for VM operations.
"""
async_queue = 'localhost.man'
required_perms = ()
required_perms = None
do_not_call_in_templates = True
abortable = False
has_percentage = False
......@@ -141,6 +141,9 @@ class Operation(object):
pass
def check_auth(self, user):
if self.required_perms is None:
raise ImproperlyConfigured(
"Set required_perms to () if none needed.")
if not user.has_perms(self.required_perms):
raise PermissionDenied("%s doesn't have the required permissions."
% user)
......
......@@ -1240,6 +1240,24 @@
}
},
{
"pk": 1367,
"model": "auth.permission",
"fields": {
"codename": "create_vm",
"name": "Can create a new VM.",
"content_type": 28
}
},
{
"pk": 1368,
"model": "auth.permission",
"fields": {
"codename": "access_console",
"name": "Can access the graphical console of a VM.",
"content_type": 28
}
},
{
"pk": 1,
"model": "auth.group",
"fields": {
......
......@@ -25,6 +25,7 @@ from django.contrib.auth.forms import (
)
from django.contrib.auth.models import User, Group
from django.core.validators import URLValidator
from django.core.exceptions import PermissionDenied, ValidationError
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (
......@@ -39,13 +40,16 @@ from django.template import Context
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy
from django_sshkey.models import UserKey
from firewall.models import Vlan, Host
from storage.models import Disk
from vm.models import (
InstanceTemplate, Lease, InterfaceTemplate, Node, Trait
InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance
)
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.models import Permission
from .models import Profile, GroupProfile
from circle.settings.base import LANGUAGES
from django.utils.translation import string_concat
......@@ -592,6 +596,17 @@ class TemplateForm(forms.ModelForm):
n = self.instance.interface_set.values_list("vlan", flat=True)
self.initial['networks'] = n
self.allowed_fields = (
'name', 'access_method', 'description', 'system', 'tags')
if self.user.has_perm('vm.change_template_resources'):
self.allowed_fields += tuple(set(self.fields.keys()) -
set(['raw_data']))
if self.user.is_superuser:
self.allowed_fields += ('raw_data', )
for name, field in self.fields.items():
if name not in self.allowed_fields:
field.widget.attrs['disabled'] = 'disabled'
if not self.instance.pk and len(self.errors) < 1:
self.instance.priority = 20
self.instance.ram_size = 512
......@@ -602,14 +617,35 @@ class TemplateForm(forms.ModelForm):
return User.objects.get(pk=self.instance.owner.pk)
return self.user
def clean_raw_data(self):
# if raw_data has changed and the user is not superuser
if "raw_data" in self.changed_data and not self.user.is_superuser:
old_raw_data = InstanceTemplate.objects.get(
pk=self.instance.pk).raw_data
return old_raw_data
else:
return self.cleaned_data['raw_data']
def _clean_fields(self):
try:
old = InstanceTemplate.objects.get(pk=self.instance.pk)
except InstanceTemplate.DoesNotExist:
old = None
for name, field in self.fields.items():
if name in self.allowed_fields:
value = field.widget.value_from_datadict(
self.data, self.files, self.add_prefix(name))
try:
if isinstance(field, forms.FileField):
initial = self.initial.get(name, field.initial)
value = field.clean(value, initial)
else:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self._errors[name] = self.error_class(e.messages)
if name in self.cleaned_data:
del self.cleaned_data[name]
elif old:
if name == 'networks':
self.cleaned_data[name] = [
i.vlan for i in self.instance.interface_set.all()]
else:
self.cleaned_data[name] = getattr(old, name)
def save(self, commit=True):
data = self.cleaned_data
......@@ -623,6 +659,8 @@ class TemplateForm(forms.ModelForm):
networks = InterfaceTemplate.objects.filter(
template=self.instance).values_list("vlan", flat=True)
for m in data['networks']:
if not m.has_level(self.user, "user"):
raise PermissionDenied()
if m.pk not in networks:
InterfaceTemplate(vlan=m, managed=m.managed,
template=self.instance).save()
......@@ -634,10 +672,6 @@ class TemplateForm(forms.ModelForm):
@property
def helper(self):
kwargs_raw_data = {}
if not self.user.is_superuser:
kwargs_raw_data['readonly'] = None
helper = FormHelper()
helper.layout = Layout(
Field("name"),
......@@ -689,7 +723,7 @@ class TemplateForm(forms.ModelForm):
_("Virtual machine settings"),
Field('access_method'),
Field('boot_menu'),
Field('raw_data', **kwargs_raw_data),
Field('raw_data'),
Field('req_traits'),
Field('description'),
Field("parent", type="hidden"),
......@@ -882,8 +916,6 @@ class VmDownloadDiskForm(forms.Form):
@property
def helper(self):
helper = FormHelper(self)
helper.add_input(Submit("submit", _("Create"),
css_class="btn btn-success"))
helper.form_tag = False
return helper
......@@ -1147,3 +1179,66 @@ class UserKeyForm(forms.ModelForm):
if self.user:
self.instance.user = self.user
return super(UserKeyForm, self).clean()
class TraitsForm(forms.ModelForm):
class Meta:
model = Instance
fields = ('req_traits', )
@property
def helper(self):
helper = FormHelper()
helper.form_show_labels = False
helper.form_action = reverse_lazy("dashboard.views.vm-traits",
kwargs={'pk': self.instance.pk})
helper.add_input(Submit("submit", _("Save"),
css_class="btn btn-success", ))
return helper
class RawDataForm(forms.ModelForm):
class Meta:
model = Instance
fields = ('raw_data', )
@property
def helper(self):
helper = FormHelper()
helper.form_show_labels = False
helper.form_action = reverse_lazy("dashboard.views.vm-raw-data",
kwargs={'pk': self.instance.pk})
helper.add_input(Submit("submit", _("Save"),
css_class="btn btn-success",
css_id="submit-password-button"))
return helper
permissions_filtered = Permission.objects.exclude(
codename__startswith="add_").exclude(
codename__startswith="delete_").exclude(
codename__startswith="change_")
class GroupPermissionForm(forms.ModelForm):
permissions = forms.ModelMultipleChoiceField(
queryset=permissions_filtered,
widget=FilteredSelectMultiple(_("permissions"), is_stacked=False)
)
class Meta:
model = Group
fields = ('permissions', )
@property
def helper(self):
helper = FormHelper()
helper.form_show_labels = False
helper.form_action = reverse_lazy(
"dashboard.views.group-permissions",
kwargs={'group_pk': self.instance.pk})
helper.add_input(Submit("submit", _("Save"),
css_class="btn btn-success", ))
return helper
......@@ -140,6 +140,18 @@ class Profile(Model):
return self.get_display_name()
class FutureMember(Model):
org_id = CharField(max_length=64, help_text=_(
'Unique identifier of the person, e.g. a student number.'))
group = ForeignKey(Group)
class Meta:
unique_together = ('org_id', 'group')
def __unicode__(self):
return u"%s (%s)" % (self.org_id, self.group)
class GroupProfile(AclBase):
ACL_LEVELS = (
('operator', _('operator')),
......@@ -224,6 +236,10 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
group, unicode(g))
g.user_set.add(sender)
for i in FutureMember.objects.filter(org_id=value):
i.group.user_set.add(sender)
i.delete()
owneratrs = getattr(settings, 'SAML_GROUP_OWNER_ATTRIBUTES', [])
for group in chain(*[attributes[i]
for i in owneratrs if i in attributes]):
......
......@@ -192,6 +192,9 @@
},
mousedown: function(ev) {
if (this.element[0].disabled) {
return false;
}
// Touch: Get the original event:
if (this.touchCapable && ev.type === 'touchstart') {
......
......@@ -723,6 +723,7 @@ textarea[name="list-new-namelist"] {
}
<<<<<<< HEAD
#store-list-list {
list-style: none;
}
......@@ -792,3 +793,32 @@ textarea[name="list-new-namelist"] {
.no-hover:hover {
background: none !important;
}
#group-detail-permissions .filtered {
margin: 2px 0;
padding: 2px 3px;
vertical-align: middle;
font-family: "Lucida Grande", Verdana, Arial, sans-serif;
font-weight: normal;
font-size: 11px;
border: 1px solid #ccc;
}
#group-detail-permissions .selector-available h2,
#group-detail-permissions .selector-chosen h2 {
margin: 0;
padding: 5px 8px 5px 8px;
font-size: 12px;
text-align: left;
font-weight: bold;
background: #7CA0C7;
color: white;
}
#group-detail-user-table {
margin-top: 20px;
}
#group-detail-permissions input[type="submit"]{
margin-top: -6px;
}
......@@ -512,7 +512,10 @@ function addMessage(text, type) {
$('body').animate({scrollTop: 0});
div = '<div style="display: none;" class="alert alert-' + type + '">' + text + '</div>';
$('.messagelist').html('').append(div);
$('.messagelist div').fadeIn();
var div = $('.messagelist div').fadeIn();
setTimeout(function() {
$(div).fadeOut();
}, 9000);
}
......
......@@ -30,4 +30,56 @@ $(function() {
});
return false;
});
/* if the operation fails show the modal again */
$("body").on("click", "#op-form-send", function() {
var url = $(this).closest("form").prop("action");
$.ajax({
url: url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
type: 'POST',
data: $(this).closest('form').serialize(),
success: function(data, textStatus, xhr) {
/* hide the modal we just submitted */
$('#confirmation-modal').modal("hide");
/* if it was successful trigger a click event on activity, this will
* - go to that tab
* - starts refreshing the activity
*/
if(data.success) {
$('a[href="#activity"]').trigger("click");
/* if there are messages display them */
if(data.messages && data.messages.length > 0) {
addMessage(data.messages.join("<br />"), "danger");
}
}
else {
/* if the post was not successful wait for the modal to disappear
* then append the new modal
*/
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('body').append(data);
$('#confirmation-modal').modal('show');
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
});
}
},
error: function(xhr, textStatus, error) {
$('#confirmation-modal').modal("hide");
if (xhr.status == 500) {
addMessage("500 Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
}
});
return false;
});
});
......@@ -5,19 +5,20 @@ $(function() {
}
$('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('icon-spin');
checkNewActivity(false,0);
checkNewActivity(false, 1);
});
/* save resources */
$('#vm-details-resources-save').click(function() {
$('i.icon-save', this).removeClass("icon-save").addClass("icon-refresh icon-spin");
var vm = $(this).data("vm");
$.ajax({
type: 'POST',
url: location.href,
url: "/dashboard/vm/" + vm + "/op/resources_change/",
data: $('#vm-details-resources-form').serialize(),
success: function(data, textStatus, xhr) {
addMessage(data['message'], 'success');
$("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save");
$('a[href="#activity"]').trigger("click");
},
error: function(xhr, textStatus, error) {
$("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save");
......@@ -328,6 +329,17 @@ function decideActivityRefresh() {
return check;
}
/* unescapes html got via the request, also removes whitespaces and replaces all ' with " */
function unescapeHTML(html) {
return html.replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&').replace(/&ndash;/g, "–").replace(/\//g, "").replace(/'/g, '"').replace(/&#39;/g, "'").replace(/ /g, '');
}
/* the html page contains some tags that were modified via js (titles for example), we delete these
also some html tags are closed with / */
function changeHTML(html) {
return html.replace(/data-original-title/g, "title").replace(/title=""/g, "").replace(/\//g, '').replace(/ /g, '');
}
function checkNewActivity(only_status, runs) {
// set default only_status to false
only_status = typeof only_status !== 'undefined' ? only_status : false;
......@@ -339,8 +351,12 @@ function checkNewActivity(only_status, runs) {
data: {'only_status': only_status},
success: function(data) {
if(!only_status) {
$("#activity-timeline").html(data['activities']);
a = unescapeHTML(data['activities']);
b = changeHTML($("#activity-timeline").html());
if(a != b)
$("#activity-timeline").html(data['activities']);
$("#ops").html(data['ops']);
$("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip();
}
......@@ -352,6 +368,14 @@ function checkNewActivity(only_status, runs) {
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
}
if(data['status'] == "STOPPED") {
$(".enabled-when-stopped").prop("disabled", false);
$(".hide-when-stopped").hide();
} else {
$(".enabled-when-stopped").prop("disabled", true);
$(".hide-when-stopped").show();
}
if(runs > 0 && decideActivityRefresh()) {
setTimeout(
function() {checkNewActivity(only_status, runs + 1)},
......
......@@ -9,8 +9,7 @@
<title>{% block title %}{% block title-page %}{% endblock %} | {% block title-site %}CIRCLE{% endblock %}{% endblock %}</title>
<script src="//code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="//code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
......
......@@ -14,7 +14,12 @@
</h3>
</div>
<div class="panel-body">
{{ body|safe|default:"(body missing from context.)" }}
{% if template %}
{% include template %}
{% else %}
{{ body|safe|default:"(body missing from context.)" }}
{% endif %}
</div>
</div>
</div>
{% endblock %}
......@@ -3,7 +3,11 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
{{ body|safe|default:"(body missing from context.)" }}
{% if template %}
{% include template %}
{% else %}
{{ body|safe|default:"(body missing from context.)" }}
{% endif %}
<div class="clearfix"></div>
</div>
<div class="clearfix"></div>
......
......@@ -16,10 +16,12 @@
<div class="clearfix"></div>
</div>
{% endfor %}
{% if perms.vm.create_base_template %}
<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>
{% endif %}
<button type="submit" id="template-choose-next-button" class="btn btn-success pull-right">{% trans "Next" %}</button>
<div class="clearfix"></div>
</div>
......
{% 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 %}
......@@ -48,6 +48,8 @@
{% crispy group_profile_form %}
</form>
<hr />
<h3>{% trans "User list"|capfirst %}
{% if perms.auth.add_user %}
<a href="{% url "dashboard.views.create-user" group.pk %}" class="btn btn-success pull-right">{% trans "Create user" %}</a>
......@@ -71,23 +73,37 @@
</td>
</tr>
{% endfor %}
{% for i in future_users %}
<tr>
<td>
<i class="icon-user text-muted"></i>
</td>
<td> {{ i.org_id }} </td>
<td>
<a href="{% url "dashboard.views.remove-future-user" member_org_id=i.org_id group_pk=group.pk %}"
class="real-link btn-link btn-xs">
<i class="icon-remove"><span class="sr-only">{% trans "remove" %}</span></i></a>
</td>
</tr>
{% endfor %}
<tr>
<td><i class="icon-plus"></i></td>
<td colspan="2">
<input type="text" class="form-control" name="list-new-name"placeholder="{% trans "Name of user" %}">
<input type="text" class="form-control" name="list-new-name"
placeholder="{% trans "Name of user" %}">
</td>
</tr>
</tbody>
</table>
<textarea name="list-new-namelist" class="form-control"
placeholder="{% trans "List of usernames (one per line)." %}"></textarea>
placeholder="{% trans "Add multiple users at once (one identifier per line)." %}"></textarea>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
<h3 id="group-detail-perm-header">{% trans "Permissions"|capfirst %}</h3>
<hr />
<h3 id="group-detail-perm-header">{% trans "Access permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields table-bordered" id="group-detail-perm-table">
<thead>
......@@ -158,11 +174,25 @@
</div>
</form>
{% if user.is_superuser %}
<hr />
<script type="text/javascript" src="/static/admin/js/jquery.min.js"></script>
<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script>
{{ group_perm_form.media }}
<h3>{% trans "Group permissions" %}</h3>
<div id="group-detail-permissions">
{% crispy group_perm_form %}
</div>
<link rel="stylesheet" type="text/css" href="/static/admin/css/widgets.css" />
{% endif %}
</div>
</div>
</div>
</div>
</div>
<script src="{{ STATIC_URL}}dashboard/group-details.js"></script>
{% endblock %}
......@@ -3,8 +3,8 @@
{% block question %}
<p>
{% blocktrans with obj=object op=op.name %}
Do you want to do the following operation on {{obj}}:
{% blocktrans with obj=object url=object.get_absolute_url op=op.name %}
Do you want to do the following operation on <a href="{{url}}">{{obj}}</a>:
<strong>{{op}}</strong>?
{% endblocktrans %}
</p>
......@@ -19,6 +19,8 @@ Do you want to do the following operation on {{obj}}:
<div class="pull-right">
<a class="btn btn-default" href="{{object.get_absolute_url}}"
data-dismiss="modal">{% trans "Cancel" %}</a>
<button class="btn btn-danger" type="submit">{% if op.icon %}<i class="icon-{{op.icon}}"></i> {% endif %}{{ op|capfirst }}</button>
<button class="btn btn-{{ opview.effect }}" type="submit" id="op-form-send">
{% if opview.icon %}<i class="icon-{{opview.icon}}"></i> {% endif %}{{ op|capfirst }}
</button>
</div>
</form>
......@@ -80,9 +80,9 @@
{% endif %}
</dd>
{% if instance.ipv6 %}
{% if instance.ipv6 and instance.get_connect_port %}
<dt>{% trans "Host (IPv6)" %}</dt>
<dd>{{ ipv6_host }}:<strong>{{ instance.ipv6_port }}</strong></dd>
<dd>{{ ipv6_host }}:<strong>{{ ipv6_port }}</strong></dd>
{% endif %}
<dt>{% trans "Username" %}</dt>
......@@ -90,7 +90,8 @@
<dt>{% trans "Password" %}</dt>
<dd>
<div class="input-group">
<input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags" value="{{ instance.pw }}"/>
<input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags"
value="{{ instance.pw }}" spellcheck="false"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show">
<i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i>
</span>
......@@ -112,7 +113,7 @@
<div class="input-group" id="dashboard-vm-details-connect-command">
<span class="input-group-addon input-tags">{% trans "Command" %}</span>
<input type="text"
<input type="text" spellcheck="false"
value="{% if instance.get_connect_command %}{{ instance.get_connect_command }}{% else %}
{% trans "Connection is not possible." %}{% endif %}"
id="vm-details-connection-string" class="form-control input-tags" />
......
......@@ -6,16 +6,18 @@
</span>
<strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% endif %}>
{% if user.is_superuser %}<a href="{{ a.get_absolute_url }}">{% endif %}
{% if a.times > 1 %}({{ a.times }}x){% endif %}
{{ a.get_readable_name }}{% if user.is_superuser %}</a>{% endif %}
{% if a.has_percent %}
- {{ a.percentage }}%
{% endif %}
</strong>
{{ a.started|date:"Y-m-d H:i" }}{% if a.user %},
{% if a.times < 2%}{{ a.started|date:"Y-m-d H:i" }}{% endif %}{% if a.user %},
<a class="no-style-link" href="{% url "dashboard.views.profile" username=a.user.username %}">
{% include "dashboard/_display-name.html" with user=a.user show_org=True %}
</a>
{% endif %}
{% if a.has_percent %}
{{ a.percentage }}%
{% endif %}
{% if a.is_abortable_for_user %}
<form action="{{ a.instance.get_absolute_url }}" method="POST" class="pull-right">
{% csrf_token %}
......
{% load i18n %}
{% for op in ops %}
{% if op.is_disk_operation %}
<a href="{{op.get_url}}" class="btn btn-success btn-xs
operation operation-{{op.op}} btn btn-default">
<i class="icon-{{op.icon}}"