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 = ( ...@@ -271,6 +271,7 @@ LOCAL_APPS = (
'dashboard', 'dashboard',
'manager', 'manager',
'acl', 'acl',
'monitor',
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
......
...@@ -20,7 +20,7 @@ from logging import getLogger ...@@ -20,7 +20,7 @@ from logging import getLogger
from .models import activity_context, has_suffix from .models import activity_context, has_suffix
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied, ImproperlyConfigured
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -30,7 +30,7 @@ class Operation(object): ...@@ -30,7 +30,7 @@ class Operation(object):
"""Base class for VM operations. """Base class for VM operations.
""" """
async_queue = 'localhost.man' async_queue = 'localhost.man'
required_perms = () required_perms = None
do_not_call_in_templates = True do_not_call_in_templates = True
abortable = False abortable = False
has_percentage = False has_percentage = False
...@@ -141,6 +141,9 @@ class Operation(object): ...@@ -141,6 +141,9 @@ class Operation(object):
pass pass
def check_auth(self, user): 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): if not user.has_perms(self.required_perms):
raise PermissionDenied("%s doesn't have the required permissions." raise PermissionDenied("%s doesn't have the required permissions."
% user) % user)
......
...@@ -1240,6 +1240,24 @@ ...@@ -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, "pk": 1,
"model": "auth.group", "model": "auth.group",
"fields": { "fields": {
......
...@@ -25,6 +25,7 @@ from django.contrib.auth.forms import ( ...@@ -25,6 +25,7 @@ from django.contrib.auth.forms import (
) )
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.core.exceptions import PermissionDenied, ValidationError
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import ( from crispy_forms.layout import (
...@@ -39,13 +40,16 @@ from django.template import Context ...@@ -39,13 +40,16 @@ from django.template import Context
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from sizefield.widgets import FileSizeWidget from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
from firewall.models import Vlan, Host from firewall.models import Vlan, Host
from storage.models import Disk from storage.models import Disk
from vm.models import ( 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 .models import Profile, GroupProfile
from circle.settings.base import LANGUAGES from circle.settings.base import LANGUAGES
from django.utils.translation import string_concat from django.utils.translation import string_concat
...@@ -592,6 +596,17 @@ class TemplateForm(forms.ModelForm): ...@@ -592,6 +596,17 @@ class TemplateForm(forms.ModelForm):
n = self.instance.interface_set.values_list("vlan", flat=True) n = self.instance.interface_set.values_list("vlan", flat=True)
self.initial['networks'] = n 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: if not self.instance.pk and len(self.errors) < 1:
self.instance.priority = 20 self.instance.priority = 20
self.instance.ram_size = 512 self.instance.ram_size = 512
...@@ -602,14 +617,35 @@ class TemplateForm(forms.ModelForm): ...@@ -602,14 +617,35 @@ class TemplateForm(forms.ModelForm):
return User.objects.get(pk=self.instance.owner.pk) return User.objects.get(pk=self.instance.owner.pk)
return self.user return self.user
def clean_raw_data(self): def _clean_fields(self):
# if raw_data has changed and the user is not superuser try:
if "raw_data" in self.changed_data and not self.user.is_superuser: old = InstanceTemplate.objects.get(pk=self.instance.pk)
old_raw_data = InstanceTemplate.objects.get( except InstanceTemplate.DoesNotExist:
pk=self.instance.pk).raw_data old = None
return old_raw_data for name, field in self.fields.items():
else: if name in self.allowed_fields:
return self.cleaned_data['raw_data'] 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): def save(self, commit=True):
data = self.cleaned_data data = self.cleaned_data
...@@ -623,6 +659,8 @@ class TemplateForm(forms.ModelForm): ...@@ -623,6 +659,8 @@ class TemplateForm(forms.ModelForm):
networks = InterfaceTemplate.objects.filter( networks = InterfaceTemplate.objects.filter(
template=self.instance).values_list("vlan", flat=True) template=self.instance).values_list("vlan", flat=True)
for m in data['networks']: for m in data['networks']:
if not m.has_level(self.user, "user"):
raise PermissionDenied()
if m.pk not in networks: if m.pk not in networks:
InterfaceTemplate(vlan=m, managed=m.managed, InterfaceTemplate(vlan=m, managed=m.managed,
template=self.instance).save() template=self.instance).save()
...@@ -634,10 +672,6 @@ class TemplateForm(forms.ModelForm): ...@@ -634,10 +672,6 @@ class TemplateForm(forms.ModelForm):
@property @property
def helper(self): def helper(self):
kwargs_raw_data = {}
if not self.user.is_superuser:
kwargs_raw_data['readonly'] = None
helper = FormHelper() helper = FormHelper()
helper.layout = Layout( helper.layout = Layout(
Field("name"), Field("name"),
...@@ -689,7 +723,7 @@ class TemplateForm(forms.ModelForm): ...@@ -689,7 +723,7 @@ class TemplateForm(forms.ModelForm):
_("Virtual machine settings"), _("Virtual machine settings"),
Field('access_method'), Field('access_method'),
Field('boot_menu'), Field('boot_menu'),
Field('raw_data', **kwargs_raw_data), Field('raw_data'),
Field('req_traits'), Field('req_traits'),
Field('description'), Field('description'),
Field("parent", type="hidden"), Field("parent", type="hidden"),
...@@ -882,8 +916,6 @@ class VmDownloadDiskForm(forms.Form): ...@@ -882,8 +916,6 @@ class VmDownloadDiskForm(forms.Form):
@property @property
def helper(self): def helper(self):
helper = FormHelper(self) helper = FormHelper(self)
helper.add_input(Submit("submit", _("Create"),
css_class="btn btn-success"))
helper.form_tag = False helper.form_tag = False
return helper return helper
...@@ -1147,3 +1179,66 @@ class UserKeyForm(forms.ModelForm): ...@@ -1147,3 +1179,66 @@ class UserKeyForm(forms.ModelForm):
if self.user: if self.user:
self.instance.user = self.user self.instance.user = self.user
return super(UserKeyForm, self).clean() 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): ...@@ -140,6 +140,18 @@ class Profile(Model):
return self.get_display_name() 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): class GroupProfile(AclBase):
ACL_LEVELS = ( ACL_LEVELS = (
('operator', _('operator')), ('operator', _('operator')),
...@@ -224,6 +236,10 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'): ...@@ -224,6 +236,10 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
group, unicode(g)) group, unicode(g))
g.user_set.add(sender) 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', []) owneratrs = getattr(settings, 'SAML_GROUP_OWNER_ATTRIBUTES', [])
for group in chain(*[attributes[i] for group in chain(*[attributes[i]
for i in owneratrs if i in attributes]): for i in owneratrs if i in attributes]):
......
...@@ -192,6 +192,9 @@ ...@@ -192,6 +192,9 @@
}, },
mousedown: function(ev) { mousedown: function(ev) {
if (this.element[0].disabled) {
return false;
}
// Touch: Get the original event: // Touch: Get the original event:
if (this.touchCapable && ev.type === 'touchstart') { if (this.touchCapable && ev.type === 'touchstart') {
......
...@@ -723,6 +723,7 @@ textarea[name="list-new-namelist"] { ...@@ -723,6 +723,7 @@ textarea[name="list-new-namelist"] {
} }
<<<<<<< HEAD
#store-list-list { #store-list-list {
list-style: none; list-style: none;
} }
...@@ -792,3 +793,32 @@ textarea[name="list-new-namelist"] { ...@@ -792,3 +793,32 @@ textarea[name="list-new-namelist"] {
.no-hover:hover { .no-hover:hover {
background: none !important; 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) { ...@@ -512,7 +512,10 @@ function addMessage(text, type) {
$('body').animate({scrollTop: 0}); $('body').animate({scrollTop: 0});
div = '<div style="display: none;" class="alert alert-' + type + '">' + text + '</div>'; div = '<div style="display: none;" class="alert alert-' + type + '">' + text + '</div>';
$('.messagelist').html('').append(div); $('.messagelist').html('').append(div);
$('.messagelist div').fadeIn(); var div = $('.messagelist div').fadeIn();
setTimeout(function() {
$(div).fadeOut();
}, 9000);
} }
......
...@@ -30,4 +30,56 @@ $(function() { ...@@ -30,4 +30,56 @@ $(function() {
}); });
return false; 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() { ...@@ -5,19 +5,20 @@ $(function() {
} }
$('a[href="#activity"]').click(function(){ $('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('icon-spin'); $('a[href="#activity"] i').addClass('icon-spin');
checkNewActivity(false,0); checkNewActivity(false, 1);
}); });
/* save resources */ /* save resources */
$('#vm-details-resources-save').click(function() { $('#vm-details-resources-save').click(function() {
$('i.icon-save', this).removeClass("icon-save").addClass("icon-refresh icon-spin"); $('i.icon-save', this).removeClass("icon-save").addClass("icon-refresh icon-spin");
var vm = $(this).data("vm");
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: location.href, url: "/dashboard/vm/" + vm + "/op/resources_change/",
data: $('#vm-details-resources-form').serialize(), data: $('#vm-details-resources-form').serialize(),
success: function(data, textStatus, xhr) { success: function(data, textStatus, xhr) {
addMessage(data['message'], 'success');
$("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save"); $("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save");
$('a[href="#activity"]').trigger("click");
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
$("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save"); $("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save");
...@@ -328,6 +329,17 @@ function decideActivityRefresh() { ...@@ -328,6 +329,17 @@ function decideActivityRefresh() {
return check; 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) { function checkNewActivity(only_status, runs) {
// set default only_status to false // set default only_status to false
only_status = typeof only_status !== 'undefined' ? only_status : false; only_status = typeof only_status !== 'undefined' ? only_status : false;
...@@ -339,8 +351,12 @@ function checkNewActivity(only_status, runs) { ...@@ -339,8 +351,12 @@ function checkNewActivity(only_status, runs) {
data: {'only_status': only_status}, data: {'only_status': only_status},
success: function(data) { success: function(data) {
if(!only_status) { 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']); $("#ops").html(data['ops']);
$("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip(); $("[title]").tooltip();
} }
...@@ -352,6 +368,14 @@ function checkNewActivity(only_status, runs) { ...@@ -352,6 +368,14 @@ function checkNewActivity(only_status, runs) {
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled"); $("[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()) { if(runs > 0 && decideActivityRefresh()) {
setTimeout( setTimeout(
function() {checkNewActivity(only_status, runs + 1)}, function() {checkNewActivity(only_status, runs + 1)},
......
...@@ -9,8 +9,7 @@ ...@@ -9,8 +9,7 @@
<title>{% block title %}{% block title-page %}{% endblock %} | {% block title-site %}CIRCLE{% endblock %}{% endblock %}</title> <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-1.11.1.min.js"></script>
<script src="//code.jquery.com/jquery-migrate-1.2.1.min.js"></script>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"> <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> <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"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
......
...@@ -14,7 +14,12 @@ ...@@ -14,7 +14,12 @@
</h3> </h3>
</div> </div>
<div class="panel-body"> <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>
</div> </div>
{% endblock %} {% endblock %}
...@@ -3,7 +3,11 @@ ...@@ -3,7 +3,11 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-body"> <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 class="clearfix"></div>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
......
...@@ -16,10 +16,12 @@ ...@@ -16,10 +16,12 @@
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
{% endfor %} {% endfor %}
{% if perms.vm.create_base_template %}
<div class="panel panel-default template-choose-list-element"> <div class="panel panel-default template-choose-list-element">
<input type="radio" name="parent" value="base_vm"/> <input type="radio" name="parent" value="base_vm"/>
{% trans "Create a new base VM without disk" %} {% trans "Create a new base VM without disk" %}
</div> </div>
{% endif %}
<button type="submit" id="template-choose-next-button" class="btn btn-success pull-right">{% trans "Next" %}</button> <button type="submit" id="template-choose-next-button" class="btn btn-success pull-right">{% trans "Next" %}</button>
<div class="clearfix"></div> <div class="clearfix"></div>
</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 @@ ...@@ -48,6 +48,8 @@
{% crispy group_profile_form %} {% crispy group_profile_form %}
</form> </form>
<hr />
<h3>{% trans "User list"|capfirst %} <h3>{% trans "User list"|capfirst %}
{% if perms.auth.add_user %} {% if perms.auth.add_user %}
<a href="{% url "dashboard.views.create-user" group.pk %}" class="btn btn-success pull-right">{% trans "Create user" %}</a> <a href="{% url "dashboard.views.create-user" group.pk %}" class="btn btn-success pull-right">{% trans "Create user" %}</a>
...@@ -71,23 +73,37 @@ ...@@ -71,23 +73,37 @@
</td> </td>
</tr> </tr>
{% endfor %} {% 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> <tr>
<td><i class="icon-plus"></i></td> <td><i class="icon-plus"></i></td>
<td colspan="2"> <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> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<textarea name="list-new-namelist" class="form-control" <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"> <div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button> <button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div> </div>
</form> </form>
<hr />
<h3 id="group-detail-perm-header">{% trans "Permissions"|capfirst %}</h3> <h3 id="group-detail-perm-header">{% trans "Access permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %} <form action="{{acl.url}}" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields table-bordered" id="group-detail-perm-table"> <table class="table table-striped table-with-form-fields table-bordered" id="group-detail-perm-table">
<thead> <thead>
...@@ -158,11 +174,25 @@ ...@@ -158,11 +174,25 @@
</div> </div>
</form> </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> </div>
</div> </div>
</div> </div>
<script src="{{ STATIC_URL}}dashboard/group-details.js"></script>
{% endblock %} {% endblock %}
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
{% block question %} {% block question %}
<p> <p>
{% blocktrans with obj=object op=op.name %} {% blocktrans with obj=object url=object.get_absolute_url op=op.name %}
Do you want to do the following operation on {{obj}}: Do you want to do the following operation on <a href="{{url}}">{{obj}}</a>:
<strong>{{op}}</strong>? <strong>{{op}}</strong>?
{% endblocktrans %} {% endblocktrans %}
</p> </p>
...@@ -19,6 +19,8 @@ Do you want to do the following operation on {{obj}}: ...@@ -19,6 +19,8 @@ Do you want to do the following operation on {{obj}}:
<div class="pull-right"> <div class="pull-right">
<a class="btn btn-default" href="{{object.get_absolute_url}}" <a class="btn btn-default" href="{{object.get_absolute_url}}"
data-dismiss="modal">{% trans "Cancel" %}</a> 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> </div>
</form> </form>
...@@ -80,9 +80,9 @@ ...@@ -80,9 +80,9 @@
{% endif %} {% endif %}
</dd> </dd>
{% if instance.ipv6 %} {% if instance.ipv6 and instance.get_connect_port %}
<dt>{% trans "Host (IPv6)" %}</dt> <dt>{% trans "Host (IPv6)" %}</dt>
<dd>{{ ipv6_host }}:<strong>{{ instance.ipv6_port }}</strong></dd> <dd>{{ ipv6_host }}:<strong>{{ ipv6_port }}</strong></dd>
{% endif %} {% endif %}
<dt>{% trans "Username" %}</dt> <dt>{% trans "Username" %}</dt>
...@@ -90,7 +90,8 @@ ...@@ -90,7 +90,8 @@
<dt>{% trans "Password" %}</dt> <dt>{% trans "Password" %}</dt>
<dd> <dd>
<div class="input-group"> <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"> <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> <i class="icon-eye-open" id="vm-details-pw-eye" title="Show password"></i>
</span> </span>
...@@ -112,7 +113,7 @@ ...@@ -112,7 +113,7 @@
<div class="input-group" id="dashboard-vm-details-connect-command"> <div class="input-group" id="dashboard-vm-details-connect-command">
<span class="input-group-addon input-tags">{% trans "Command" %}</span> <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 %} value="{% if instance.get_connect_command %}{{ instance.get_connect_command }}{% else %}
{% trans "Connection is not possible." %}{% endif %}" {% trans "Connection is not possible." %}{% endif %}"
id="vm-details-connection-string" class="form-control input-tags" /> id="vm-details-connection-string" class="form-control input-tags" />
......
...@@ -6,16 +6,18 @@ ...@@ -6,16 +6,18 @@
</span> </span>
<strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% endif %}> <strong{% if user.is_superuser and a.result %} title="{{ a.result }}"{% endif %}>
{% if user.is_superuser %}<a href="{{ a.get_absolute_url }}">{% 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 %} {{ a.get_readable_name }}{% if user.is_superuser %}</a>{% endif %}
{% if a.has_percent %}
- {{ a.percentage }}%
{% endif %}
</strong> </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 %}"> <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 %} {% include "dashboard/_display-name.html" with user=a.user show_org=True %}
</a> </a>
{% endif %} {% endif %}
{% if a.has_percent %}
{{ a.percentage }}%
{% endif %}
{% if a.is_abortable_for_user %} {% if a.is_abortable_for_user %}
<form action="{{ a.instance.get_absolute_url }}" method="POST" class="pull-right"> <form action="{{ a.instance.get_absolute_url }}" method="POST" class="pull-right">
{% csrf_token %} {% 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}}"></i>
{{op.name}} </a>
{% endif %}
{% endfor %}
...@@ -2,10 +2,20 @@ ...@@ -2,10 +2,20 @@
{% for op in ops %} {% for op in ops %}
{% if op.show_in_toolbar %} {% if op.show_in_toolbar %}
<a href="{{op.get_url}}" class="operation operation-{{op.op}} btn btn-default btn-xs"
title="{{op.name}}: {{op.description}}"> {% if op.disabled %}
<span class="operation operation-{{op.op}} btn btn-default disabled btn-xs">
{% else %}
<a href="{{op.get_url}}" class="operation operation-{{op.op}} btn
btn-{{op.effect}} btn-xs" title="{{op.name}}: {{op.description}}">
{% endif %}
<i class="icon-{{op.icon}}"></i> <i class="icon-{{op.icon}}"></i>
<span class="sr-only">{{op.name}}</span> <span{% if not op.is_preferred %} class="sr-only"{% endif %}>{{op.name}}</span>
{% if op.disabled %}
</span>
{% else %}
</a> </a>
{% endif %} {% endif %}
{% endif %}
{% endfor %} {% endfor %}
{% load i18n %} {% load i18n %}
<div class="btn-toolbar"> <div class="btn-toolbar">
{% if perms.vm.access_console %}
<button id="sendCtrlAltDelButton" class="btn btn-danger btn-sm">{% trans "Send Ctrl+Alt+Del" %}</button> <button id="sendCtrlAltDelButton" class="btn btn-danger btn-sm">{% trans "Send Ctrl+Alt+Del" %}</button>
<button id="sendPasswordButton" class="btn btn-default btn-sm">{% trans "Type password" %}</button> <button id="sendPasswordButton" class="btn btn-default btn-sm">{% trans "Type password" %}</button>
{% endif %}
<button id="getScreenshotButton" class="btn btn-info btn-sm pull-right" data-vm-pk="{{ instance.pk }}"><i class="icon-picture"></i> {% trans "Screenshot" %}</button> <button id="getScreenshotButton" class="btn btn-info btn-sm pull-right" data-vm-pk="{{ instance.pk }}"><i class="icon-picture"></i> {% trans "Screenshot" %}</button>
</div> </div>
{% if perms.vm.access_console %}
<div class="alert alert-info" id="noVNC_status"> <div class="alert alert-info" id="noVNC_status">
</div> </div>
{% endif %}
<div id="vm-console-screenshot"> <div id="vm-console-screenshot">
<button class="btn btn-danger btn-sm pull-right">{% trans "Close" %}</button> <button class="btn btn-danger btn-sm pull-right">{% trans "Close" %}</button>
...@@ -14,6 +18,7 @@ ...@@ -14,6 +18,7 @@
<hr /> <hr />
</div> </div>
{% if perms.vm.access_console %}
<canvas id="noVNC_canvas" width="640px" height="20px">Canvas not supported. <canvas id="noVNC_canvas" width="640px" height="20px">Canvas not supported.
</canvas> </canvas>
...@@ -22,3 +27,4 @@ ...@@ -22,3 +27,4 @@
var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/'; var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/';
var VNC_URL = "{{ vnc_url }}"; var VNC_URL = "{{ vnc_url }}";
</script> </script>
{% endif %}
...@@ -47,7 +47,12 @@ ...@@ -47,7 +47,12 @@
<h3 class="list-group-item-heading dashboard-vm-details-network-h3"> <h3 class="list-group-item-heading dashboard-vm-details-network-h3">
<i class="icon-{% if i.host %}globe{% else %}link{% endif %}"></i> {{ i.vlan.name }} <i class="icon-{% if i.host %}globe{% else %}link{% endif %}"></i> {{ i.vlan.name }}
{% if not i.host%}({% trans "unmanaged" %}){% endif %} {% if not i.host%}({% trans "unmanaged" %}){% endif %}
<a href="{% url "dashboard.views.interface-delete" pk=i.pk %}?next={{ request.path }}" class="btn btn-danger btn-xs interface-remove" {% if user.is_superuser %}
<a href="{{ i.host.get_absolute_url }}"
class="btn btn-default btn-xs">{% trans "edit" %}</a>
{% endif %}
<a href="{% url "dashboard.views.interface-delete" pk=i.pk %}?next={{ request.path }}"
class="btn btn-danger btn-xs interface-remove"
data-interface-pk="{{ i.pk }}"> data-interface-pk="{{ i.pk }}">
{% trans "remove" %} {% trans "remove" %}
</a> </a>
...@@ -63,6 +68,8 @@ ...@@ -63,6 +68,8 @@
<dd> <dd>
{% for g in i.host.groups.all %} {% for g in i.host.groups.all %}
{{ g }}{% if not forloop.last %},{% endif %} {{ g }}{% if not forloop.last %},{% endif %}
{% empty %}
-
{% endfor %} {% endfor %}
</dd> </dd>
</dl> </dl>
...@@ -92,7 +99,7 @@ ...@@ -92,7 +99,7 @@
{% if l.ipv4 %} {% if l.ipv4 %}
<tr> <tr>
<td> <td>
{% display_portforward l %} {% display_portforward4 l %}
</td> </td>
<td><i class="icon-long-arrow-right"></i></td> <td><i class="icon-long-arrow-right"></i></td>
<td> <td>
...@@ -124,7 +131,7 @@ ...@@ -124,7 +131,7 @@
{% if l.ipv6 %} {% if l.ipv6 %}
<tr> <tr>
<td> <td>
{% display_portforward l %} {% display_portforward6 l %}
</td> </td>
<td><i class="icon-long-arrow-right"></i></td> <td><i class="icon-long-arrow-right"></i></td>
<td> <td>
......
...@@ -33,11 +33,20 @@ ...@@ -33,11 +33,20 @@
</div> </div>
</p> </p>
{% if can_change_resources %}
<p class="row"> <p class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="submit" class="btn btn-success btn-sm" id="vm-details-resources-save"><i class="icon-save"></i> {% trans "Save resources" %}</button> <button type="submit" class="btn btn-success btn-sm enabled-when-stopped" id="vm-details-resources-save"
data-vm="{{ instance.pk }}"
{% if not op.resources_change %}disabled{% endif %}>
<i class="icon-save"></i> {% trans "Save resources" %}
</button>
<span class="hide-when-stopped"
{% if op.resources_change %}style="display: none;"{% endif %}
>{% trans "Stop your VM to change resources." %}</span>
</div> </div>
</p> </p>
{% endif %}
</form> </form>
<hr /> <hr />
...@@ -47,18 +56,9 @@ ...@@ -47,18 +56,9 @@
<h3> <h3>
{% trans "Disks" %} {% trans "Disks" %}
<div class="pull-right"> <div class="pull-right">
{% if op.download_disk %} <div id="disk-ops">
<a href="{{op.download_disk.get_url}}" class="btn btn-success btn-xs {% include "dashboard/vm-detail/_disk-operations.html" %}
operation operation-{{op.download_disk.op}} btn btn-default"> </div>
<i class="icon-{{op.download_disk.icon}}"></i>
{{op.download_disk.name}} </a>
{% endif %}
{% if op.create_disk %}
<a href="{{op.create_disk.get_url}}" class="btn btn-success btn-xs
operation operation-{{op.create_disk.op}} btn btn-default">
<i class="icon-{{op.create_disk.icon}}"></i>
{{op.create_disk.name}} </a>
{% endif %}
</div> </div>
</h3> </h3>
...@@ -77,6 +77,32 @@ ...@@ -77,6 +77,32 @@
</div> </div>
</div> </div>
{% if user.is_superuser %}
<hr/>
<div class="row" id="">
<div class="col-sm-12">
<h3>
{% trans "Required traits" %}
</h3>
{% crispy traits_form %}
</div>
</div>
<hr/>
<div class="row" id="">
<div class="col-sm-12">
<h3>
{% trans "Raw data" %}
</h3>
{% crispy raw_data_form %}
</div>
</div>
{% endif %}
{% block extra_js %} {% block extra_js %}
<style> <style>
......
<a href="{% url "dashboard.views.vm-migrate" pk=record.pk %}" class="btn btn-default btn-xs vm-migrate" data-vm-pk="{{ record.pk }}" title data-original-title="Migrate"> <a href="{% url "dashboard.vm.op.migrate" pk=record.pk %}" class="btn btn-default btn-xs vm-migrate" data-vm-pk="{{ record.pk }}" title data-original-title="Migrate">
<i class="icon-truck"></i> <i class="icon-truck"></i>
</a> </a>
<a id="vm-list-rename-button" class="btn btn-default btn-xs" title data-original-title="Rename"> <a id="vm-list-rename-button" class="btn btn-default btn-xs" title data-original-title="Rename">
......
...@@ -6,10 +6,18 @@ register = template.Library() ...@@ -6,10 +6,18 @@ register = template.Library()
LINKABLE_PORTS = {80: "http", 8080: "http", 443: "https", 21: "ftp"} LINKABLE_PORTS = {80: "http", 8080: "http", 443: "https", 21: "ftp"}
@register.simple_tag(name="display_portforward") @register.simple_tag(name="display_portforward4")
def display_pf(ports): def display_pf4(ports):
is_ipv6 = "ipv6" in ports return display_pf(ports, 'ipv4')
data = ports["ipv6" if is_ipv6 else "ipv4"]
@register.simple_tag(name="display_portforward6")
def display_pf6(ports):
return display_pf(ports, 'ipv6')
def display_pf(ports, proto):
data = ports[proto]
if ports['private'] in LINKABLE_PORTS.keys(): if ports['private'] in LINKABLE_PORTS.keys():
href = "%s:%d" % (data['host'], data['port']) href = "%s:%d" % (data['host'], data['port'])
......
...@@ -159,7 +159,7 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -159,7 +159,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert not msg.error.called assert not msg.error.called
def test_migrate_failed(self): def test_migrate_failed(self):
request = FakeRequestFactory(POST={'node': 1}) request = FakeRequestFactory(POST={'node': 1}, superuser=True)
view = vm_ops['migrate'] view = vm_ops['migrate']
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
...@@ -177,7 +177,7 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -177,7 +177,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert msg.error.called assert msg.error.called
def test_migrate_template(self): def test_migrate_template(self):
request = FakeRequestFactory() request = FakeRequestFactory(superuser=True)
view = vm_ops['migrate'] view = vm_ops['migrate']
with patch.object(view, 'get_object') as go: with patch.object(view, 'get_object') as go:
...@@ -190,7 +190,7 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -190,7 +190,7 @@ class VmOperationViewTestCase(unittest.TestCase):
view.as_view()(request, pk=1234).render().status_code, 200) view.as_view()(request, pk=1234).render().status_code, 200)
def test_save_as_wo_name(self): def test_save_as_wo_name(self):
request = FakeRequestFactory(POST={}) request = FakeRequestFactory(POST={}, has_perms_mock=True)
view = vm_ops['save_as_template'] view = vm_ops['save_as_template']
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
...@@ -224,7 +224,7 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -224,7 +224,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert not msg.error.called assert not msg.error.called
def test_save_as_template(self): def test_save_as_template(self):
request = FakeRequestFactory() request = FakeRequestFactory(has_perms_mock=True)
view = vm_ops['save_as_template'] view = vm_ops['save_as_template']
with patch.object(view, 'get_object') as go: with patch.object(view, 'get_object') as go:
...@@ -246,6 +246,8 @@ def FakeRequestFactory(*args, **kwargs): ...@@ -246,6 +246,8 @@ def FakeRequestFactory(*args, **kwargs):
user = UserFactory() user = UserFactory()
user.is_authenticated = lambda: kwargs.get('authenticated', True) user.is_authenticated = lambda: kwargs.get('authenticated', True)
user.is_superuser = kwargs.get('superuser', False) user.is_superuser = kwargs.get('superuser', False)
if kwargs.get('has_perms_mock', False):
user.has_perms = MagicMock(return_value=True)
request = HttpRequest() request = HttpRequest()
request.user = user request.user = user
......
...@@ -63,6 +63,8 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -63,6 +63,8 @@ class VmDetailTest(LoginMixin, TestCase):
self.g1.user_set.add(self.u1) self.g1.user_set.add(self.u1)
self.g1.user_set.add(self.u2) self.g1.user_set.add(self.u2)
self.g1.save() self.g1.save()
self.u1.user_permissions.add(Permission.objects.get(
codename='create_vm'))
settings["default_vlangroup"] = 'public' settings["default_vlangroup"] = 'public'
VlanGroup.objects.create(name='public') VlanGroup.objects.create(name='public')
...@@ -1544,6 +1546,8 @@ class VmDetailVncTest(LoginMixin, TestCase): ...@@ -1544,6 +1546,8 @@ class VmDetailVncTest(LoginMixin, TestCase):
inst.node = Node.objects.all()[0] inst.node = Node.objects.all()[0]
inst.save() inst.save()
inst.set_level(self.u1, 'operator') inst.set_level(self.u1, 'operator')
self.u1.user_permissions.add(Permission.objects.get(
codename='access_console'))
response = c.get('/dashboard/vm/1/vnctoken/') response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
...@@ -1554,6 +1558,8 @@ class VmDetailVncTest(LoginMixin, TestCase): ...@@ -1554,6 +1558,8 @@ class VmDetailVncTest(LoginMixin, TestCase):
inst.node = Node.objects.all()[0] inst.node = Node.objects.all()[0]
inst.save() inst.save()
inst.set_level(self.u1, 'user') inst.set_level(self.u1, 'user')
self.u1.user_permissions.add(Permission.objects.get(
codename='access_console'))
response = c.get('/dashboard/vm/1/vnctoken/') response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
......
...@@ -28,9 +28,10 @@ from .views import ( ...@@ -28,9 +28,10 @@ from .views import (
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate, NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView, TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView, TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView, VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete,
VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView, VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveAclUserView, GroupRemoveAclGroupView, GroupRemoveUserView, GroupRemoveAclUserView, GroupRemoveAclGroupView, GroupRemoveUserView,
GroupRemoveFutureUserView,
GroupCreate, GroupProfileUpdate, GroupCreate, GroupProfileUpdate,
TemplateChoose, TemplateChoose,
UserCreationView, UserCreationView,
...@@ -38,7 +39,9 @@ from .views import ( ...@@ -38,7 +39,9 @@ from .views import (
ProfileView, toggle_use_gravatar, UnsubscribeFormView, ProfileView, toggle_use_gravatar, UnsubscribeFormView,
UserKeyDelete, UserKeyDetail, UserKeyCreate, UserKeyDelete, UserKeyDetail, UserKeyCreate,
StoreList, store_download, store_upload, store_get_upload_url, StoreRemove, StoreList, store_download, store_upload, store_get_upload_url, StoreRemove,
store_new_directory, store_refresh_toplist store_new_directory, store_refresh_toplist,
VmTraitsUpdate, VmRawDataUpdate,
GroupPermissionsView,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -83,14 +86,16 @@ urlpatterns = patterns( ...@@ -83,14 +86,16 @@ urlpatterns = patterns(
url(r'^vm/mass-delete/', VmMassDelete.as_view(), url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'), name='dashboard.view.mass-delete-vm'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity), url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity),
url(r'^vm/(?P<pk>\d+)/migrate/$', VmMigrateView.as_view(),
name='dashboard.views.vm-migrate'),
url(r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$', VmRenewView.as_view(), url(r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$', VmRenewView.as_view(),
name='dashboard.views.vm-renew'), name='dashboard.views.vm-renew'),
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,
name='dashboard.views.vm-get-screenshot'), name='dashboard.views.vm-get-screenshot'),
url(r'^vm/(?P<pk>\d+)/traits/$', VmTraitsUpdate.as_view(),
name='dashboard.views.vm-traits'),
url(r'^vm/(?P<pk>\d+)/raw_data/$', VmRawDataUpdate.as_view(),
name='dashboard.views.vm-raw-data'),
url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'), url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'),
url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(), url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(),
...@@ -156,11 +161,17 @@ urlpatterns = patterns( ...@@ -156,11 +161,17 @@ urlpatterns = patterns(
url(r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$', url(r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$',
GroupRemoveUserView.as_view(), GroupRemoveUserView.as_view(),
name="dashboard.views.remove-user"), name="dashboard.views.remove-user"),
url(r'^group/(?P<group_pk>\d+)/remove/futureuser/(?P<member_org_id>.+)/$',
GroupRemoveFutureUserView.as_view(),
name="dashboard.views.remove-future-user"),
url(r'^group/create/$', GroupCreate.as_view(), url(r'^group/create/$', GroupCreate.as_view(),
name='dashboard.views.group-create'), name='dashboard.views.group-create'),
url(r'^group/(?P<group_pk>\d+)/create/$', url(r'^group/(?P<group_pk>\d+)/create/$',
UserCreationView.as_view(), UserCreationView.as_view(),
name="dashboard.views.create-user"), name="dashboard.views.create-user"),
url(r'^group/(?P<group_pk>\d+)/permissions/$',
GroupPermissionsView.as_view(),
name="dashboard.views.group-permissions"),
url(r'^sshkey/delete/(?P<pk>\d+)/$', url(r'^sshkey/delete/(?P<pk>\d+)/$',
UserKeyDelete.as_view(), UserKeyDelete.as_view(),
......
...@@ -28,6 +28,7 @@ import re ...@@ -28,6 +28,7 @@ import re
alfanum_re = re.compile(r'^[A-Za-z0-9_-]+$') alfanum_re = re.compile(r'^[A-Za-z0-9_-]+$')
domain_re = re.compile(r'^([A-Za-z0-9_-]\.?)+$') domain_re = re.compile(r'^([A-Za-z0-9_-]\.?)+$')
domain_wildcard_re = re.compile(r'^(\*\.)?([A-Za-z0-9_-]\.?)+$')
ipv4_re = re.compile('^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$') ipv4_re = re.compile('^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$')
reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$') reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$')
ipv6_template_re = re.compile(r'^(%\([abcd]\)[dxX]|[A-Za-z0-9:-])+$') ipv6_template_re = re.compile(r'^(%\([abcd]\)[dxX]|[A-Za-z0-9:-])+$')
...@@ -216,12 +217,23 @@ def is_valid_domain(value): ...@@ -216,12 +217,23 @@ def is_valid_domain(value):
return domain_re.match(value) is not None return domain_re.match(value) is not None
def is_valid_domain_wildcard(value):
"""Check whether the parameter is a valid domain name."""
return domain_wildcard_re.match(value) is not None
def val_domain(value): def val_domain(value):
"""Validate whether the parameter is a valid domin name.""" """Validate whether the parameter is a valid domin name."""
if not is_valid_domain(value): if not is_valid_domain(value):
raise ValidationError(_(u'%s - invalid domain name') % value) raise ValidationError(_(u'%s - invalid domain name') % value)
def val_domain_wildcard(value):
"""Validate whether the parameter is a valid domin name."""
if not is_valid_domain_wildcard(value):
raise ValidationError(_(u'%s - invalid domain name') % value)
def is_valid_reverse_domain(value): def is_valid_reverse_domain(value):
"""Check whether the parameter is a valid reverse domain name.""" """Check whether the parameter is a valid reverse domain name."""
return reverse_domain_re.match(value) is not None return reverse_domain_re.match(value) is not None
......
...@@ -27,6 +27,7 @@ from django.forms import ValidationError ...@@ -27,6 +27,7 @@ from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from firewall.fields import (MACAddressField, val_alfanum, val_reverse_domain, from firewall.fields import (MACAddressField, val_alfanum, val_reverse_domain,
val_ipv6_template, val_domain, val_ipv4, val_ipv6_template, val_domain, val_ipv4,
val_domain_wildcard,
val_ipv6, val_mx, convert_ipv4_to_ipv6, val_ipv6, val_mx, convert_ipv4_to_ipv6,
IPNetworkField, IPAddressField) IPNetworkField, IPAddressField)
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
...@@ -695,8 +696,7 @@ class Host(models.Model): ...@@ -695,8 +696,7 @@ class Host(models.Model):
:param private: Port number of host in subject. :param private: Port number of host in subject.
""" """
self.rules.filter(owner=self.owner, proto=proto, host=self, self.rules.filter(proto=proto, dport=private).delete()
dport=private).delete()
def get_hostname(self, proto, public=True): def get_hostname(self, proto, public=True):
""" """
...@@ -728,7 +728,7 @@ class Host(models.Model): ...@@ -728,7 +728,7 @@ class Host(models.Model):
Return a list of ports with forwarding rules set. Return a list of ports with forwarding rules set.
""" """
retval = [] retval = []
for rule in self.rules.filter(owner=self.owner): for rule in self.rules.all():
forward = { forward = {
'proto': rule.proto, 'proto': rule.proto,
'private': rule.dport, 'private': rule.dport,
...@@ -770,9 +770,7 @@ class Host(models.Model): ...@@ -770,9 +770,7 @@ class Host(models.Model):
if public_port else if public_port else
None) None)
# IPv6 # IPv6
blocked = self.incoming_rules.exclude( endpoints['ipv6'] = (self.ipv6, port) if public_port else None
action='accept').filter(dport=port, proto=protocol).exists()
endpoints['ipv6'] = (self.ipv6, port) if not blocked else None
return endpoints return endpoints
@models.permalink @models.permalink
...@@ -821,7 +819,7 @@ class Domain(models.Model): ...@@ -821,7 +819,7 @@ class Domain(models.Model):
class Record(models.Model): class Record(models.Model):
CHOICES_type = (('A', 'A'), ('CNAME', 'CNAME'), ('AAAA', 'AAAA'), CHOICES_type = (('A', 'A'), ('CNAME', 'CNAME'), ('AAAA', 'AAAA'),
('MX', 'MX'), ('NS', 'NS'), ('PTR', 'PTR'), ('TXT', 'TXT')) ('MX', 'MX'), ('NS', 'NS'), ('PTR', 'PTR'), ('TXT', 'TXT'))
name = models.CharField(max_length=40, validators=[val_domain], name = models.CharField(max_length=40, validators=[val_domain_wildcard],
blank=True, null=True, verbose_name=_('name')) blank=True, null=True, verbose_name=_('name'))
domain = models.ForeignKey('Domain', verbose_name=_('domain')) domain = models.ForeignKey('Domain', verbose_name=_('domain'))
host = models.ForeignKey('Host', blank=True, null=True, host = models.ForeignKey('Host', blank=True, null=True,
......
...@@ -30,7 +30,9 @@ celery = Celery('manager', ...@@ -30,7 +30,9 @@ celery = Celery('manager',
'vm.tasks.local_agent_tasks', 'vm.tasks.local_agent_tasks',
'storage.tasks.local_tasks', 'storage.tasks.local_tasks',
'storage.tasks.periodic_tasks', 'storage.tasks.periodic_tasks',
'firewall.tasks.local_tasks', ]) 'firewall.tasks.local_tasks',
'monitor.tasks.local_periodic_tasks',
])
celery.conf.update( celery.conf.update(
CELERY_RESULT_BACKEND='cache', CELERY_RESULT_BACKEND='cache',
...@@ -69,6 +71,24 @@ celery.conf.update( ...@@ -69,6 +71,24 @@ celery.conf.update(
'schedule': timedelta(hours=24), 'schedule': timedelta(hours=24),
'options': {'queue': 'localhost.man'} 'options': {'queue': 'localhost.man'}
}, },
'monitor.measure_response_time': {
'task': 'monitor.tasks.local_periodic_tasks.'
'measure_response_time',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.man'}
},
'monitor.check_celery_queues': {
'task': 'monitor.tasks.local_periodic_tasks.'
'check_celery_queues',
'schedule': timedelta(seconds=60),
'options': {'queue': 'localhost.man'}
},
'monitor.instance_per_template': {
'task': 'monitor.tasks.local_periodic_tasks.'
'instance_per_template',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.man'}
},
} }
) )
from itertools import islice
from socket import gethostname
import logging
import os
import pika
logger = logging.getLogger(__name__)
class Client:
env_config = {
"server_address": "GRAPHITE_HOST",
"server_port": "GRAPHITE_AMQP_PORT",
"amqp_user": "GRAPHITE_AMQP_USER",
"amqp_pass": "GRAPHITE_AMQP_PASSWORD",
"amqp_queue": "GRAPHITE_AMQP_QUEUE",
"amqp_vhost": "GRAPHITE_AMQP_VHOST",
}
def __init__(self):
"""
Constructor of the client class that is responsible for handling the
communication between the graphite server and the data source. In
order to initialize a client you must have the following
environmental varriables:
- GRAPHITE_SERVER_ADDRESS:
- GRAPHITE_SERVER_PORT:
- GRAPHITE_AMQP_USER:
- GRAPHITE_AMQP_PASSWORD:
- GRAPHITE_AMQP_QUEUE:
- GRAPHITE_AMQP_VHOST:
Missing only one of these variables will cause the client not to work.
"""
self.name = 'circle.%s' % gethostname()
for var, env_var in self.env_config.items():
value = os.getenv(env_var, "")
if value:
setattr(self, var, value)
else:
raise RuntimeError('%s environment variable missing' % env_var)
def connect(self):
"""
This method creates the connection to the queue of the graphite
server using the environmental variables given in the constructor.
"""
try:
credentials = pika.PlainCredentials(self.amqp_user, self.amqp_pass)
params = pika.ConnectionParameters(host=self.server_address,
port=int(self.server_port),
virtual_host=self.amqp_vhost,
credentials=credentials)
self.connection = pika.BlockingConnection(params)
self.channel = self.connection.channel()
logger.info('Connection established to %s.', self.server_address)
except RuntimeError:
logger.error('Cannot connect to the server. '
'Parameters may be wrong.')
logger.error("An error has occured while connecting to the server")
raise
except: # FIXME
logger.error('Cannot connect to the server. There is no one '
'listening on the other side.')
raise
def disconnect(self):
"""
Break up the connection to the graphite server. If something went
wrong while disconnecting it simply cut the connection up.
"""
try:
self.channel.close()
self.connection.close()
except RuntimeError as e:
logger.error('An error has occured while disconnecting. %s',
unicode(e))
raise
def _send(self, message):
"""
Send the message given in the parameters given in the message
parameter. This function expects that the graphite server want the
metric name given in the message body. (This option must be enabled
on the server. Otherwise it can't parse the data sent.)
"""
body = "\n".join(message)
try:
self.channel.basic_publish(exchange=self.amqp_queue,
routing_key='', body=body)
except:
logger.error('An error has occured while sending metrics (%dB).',
len(body))
raise
@staticmethod
def _chunker(seq, size):
"""Yield seq in size-long chunks.
"""
for pos in xrange(0, len(seq), size):
yield islice(seq, pos, pos + size)
def send(self, message):
self.connect()
try:
for chunk in self._chunker(message, 100):
self._send(chunk)
finally:
self.disconnect()
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
import logging
import requests
from time import time
from django.conf import settings
from manager.mancelery import celery
from vm.tasks.vm_tasks import check_queue
from vm.models import Node, InstanceTemplate
from storage.models import DataStore
from monitor.client import Client
logger = logging.getLogger(__name__)
@celery.task(ignore_result=True)
def measure_response_time():
try:
r = requests.get(settings.DJANGO_URL, verify=False,
timeout=0.5)
except requests.exceptions.Timeout:
return
total_miliseconds = (
r.elapsed.seconds * 10**6 +
r.elapsed.microseconds) / 1000
Client().send([
"%(name)s %(val)d %(time)s" % {
'name': "portal.response_time",
'val': total_miliseconds,
'time': time(),
}
])
@celery.task(ignore_result=True)
def check_celery_queues():
graphite_string = lambda component, hostname, celery, is_alive, time: (
"%s.%s.celery-queues.%s %d %s" % (
component, hostname, celery, 1 if is_alive else 0, time)
)
metrics = []
for n in Node.objects.all(): # disabled, offline nodes?
for s in ["fast", "slow"]:
is_queue_alive = check_queue(n.host.hostname, "vm", s)
metrics.append(graphite_string("circle", n.host.hostname,
"vm-" + s, is_queue_alive, time()))
is_net_queue_alive = check_queue(n.host.hostname, "net", "fast")
metrics.append(graphite_string("circle", n.host.hostname,
"net-fast", is_net_queue_alive, time()))
is_agent_queue_alive = check_queue(n.host.hostname, "agent")
metrics.append(graphite_string("circle", n.host.hostname, "agent",
is_agent_queue_alive, time()))
for ds in DataStore.objects.all():
for s in ["fast", "slow"]:
is_queue_alive = check_queue(ds.hostname, "vm", s)
metrics.append(graphite_string("storage", ds.hostname,
"storage-" + s, is_queue_alive,
time()))
Client().send(metrics)
@celery.task(ignore_result=True)
def instance_per_template():
graphite_string = lambda pk, state, val, time: (
"template.%d.instances.%s %d %s" % (
pk, state, val, time)
)
metrics = []
for t in InstanceTemplate.objects.all():
base = t.instance_set.filter(destroyed_at=None)
running = base.filter(status="RUNNING").count()
not_running = base.exclude(status="RUNNING").count()
metrics.append(graphite_string(t.pk, "running", running, time()))
metrics.append(graphite_string(t.pk, "not_running", not_running,
time()))
Client().send(metrics)
...@@ -106,6 +106,9 @@ class Disk(AclBase, TimeStampedModel): ...@@ -106,6 +106,9 @@ class Disk(AclBase, TimeStampedModel):
ordering = ['name'] ordering = ['name']
verbose_name = _('disk') verbose_name = _('disk')
verbose_name_plural = _('disks') verbose_name_plural = _('disks')
permissions = (
('create_empty_disk', _('Can create an empty disk.')),
('download_disk', _('Can download a disk.')))
class WrongDiskTypeError(Exception): class WrongDiskTypeError(Exception):
...@@ -131,6 +134,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -131,6 +134,7 @@ class Disk(AclBase, TimeStampedModel):
self.disk = disk self.disk = disk
class DiskIsNotReady(Exception): class DiskIsNotReady(Exception):
""" Exception for operations that need a deployed disk. """ Exception for operations that need a deployed disk.
""" """
...@@ -380,13 +384,18 @@ class Disk(AclBase, TimeStampedModel): ...@@ -380,13 +384,18 @@ class Disk(AclBase, TimeStampedModel):
self.save() self.save()
return True return True
def restore(self, user=None, task_uuid=None): def restore(self, user=None, task_uuid=None, timeout=15):
"""Recover destroyed disk from trash if possible. """Recover destroyed disk from trash if possible.
""" """
# TODO queue_name = self.datastore.get_remote_queue_name(
pass 'storage', priority='slow')
logger.info("Image: %s at Datastore: %s recovered from trash." %
def save_as(self, user=None, task_uuid=None, timeout=300): (self.filename, self.datastore.path))
storage_tasks.recover_from_trash.apply_async(
args=[self.datastore.path, self.filename],
queue=queue_name).get(timeout=timeout)
def save_as(self, task=None, user=None, task_uuid=None, timeout=300):
"""Save VM as template. """Save VM as template.
Based on disk type: Based on disk type:
...@@ -421,10 +430,18 @@ class Disk(AclBase, TimeStampedModel): ...@@ -421,10 +430,18 @@ class Disk(AclBase, TimeStampedModel):
type=new_type) type=new_type)
queue_name = self.get_remote_queue_name("storage", priority="slow") queue_name = self.get_remote_queue_name("storage", priority="slow")
storage_tasks.merge.apply_async(args=[self.get_disk_desc(), remote = storage_tasks.merge.apply_async(kwargs={
disk.get_disk_desc()], "old_json": self.get_disk_desc(),
queue=queue_name "new_json": disk.get_disk_desc()},
).get() # Timeout queue=queue_name
disk.is_ready = True ) # Timeout
disk.save() while True:
try:
remote.get(timeout=5)
break
except TimeoutError:
if task is not None and task.is_aborted():
AbortableAsyncResult(remote.id).abort()
disk.destroy()
raise Exception("Save as aborted by use.")
return disk return disk
...@@ -73,6 +73,11 @@ def move_to_trash(datastore, disk_path): ...@@ -73,6 +73,11 @@ def move_to_trash(datastore, disk_path):
pass pass
@celery.task(name='storagedriver.recover_from_trash')
def recover_from_trash(datastore, disk_path):
pass
@celery.task(name='storagedriver.get_storage_stat') @celery.task(name='storagedriver.get_storage_stat')
def get_storage_stat(path): def get_storage_stat(path):
pass pass
...@@ -11,8 +11,14 @@ class Migration(SchemaMigration): ...@@ -11,8 +11,14 @@ class Migration(SchemaMigration):
# Removing unique constraint on 'InstanceTemplate', fields ['name'] # Removing unique constraint on 'InstanceTemplate', fields ['name']
db.delete_unique(u'vm_instancetemplate', ['name']) db.delete_unique(u'vm_instancetemplate', ['name'])
# Changing field 'InstanceTemplate.parent'
db.alter_column(u'vm_instancetemplate', 'parent_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['vm.InstanceTemplate'], null=True, on_delete=models.SET_NULL))
def backwards(self, orm): def backwards(self, orm):
# Changing field 'InstanceTemplate.parent'
db.alter_column(u'vm_instancetemplate', 'parent_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['vm.InstanceTemplate'], null=True))
# Adding unique constraint on 'InstanceTemplate', fields ['name'] # Adding unique constraint on 'InstanceTemplate', fields ['name']
db.create_unique(u'vm_instancetemplate', ['name']) db.create_unique(u'vm_instancetemplate', ['name'])
...@@ -131,6 +137,7 @@ class Migration(SchemaMigration): ...@@ -131,6 +137,7 @@ class Migration(SchemaMigration):
'dev_num': ('django.db.models.fields.CharField', [], {'default': "u'a'", 'max_length': '1'}), 'dev_num': ('django.db.models.fields.CharField', [], {'default': "u'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}), 'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'size': ('sizefield.models.FileSizeField', [], {'default': 'None', 'null': 'True'}), 'size': ('sizefield.models.FileSizeField', [], {'default': 'None', 'null': 'True'}),
...@@ -147,6 +154,7 @@ class Migration(SchemaMigration): ...@@ -147,6 +154,7 @@ class Migration(SchemaMigration):
'destroyed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 'destroyed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'instance_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}), 'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'instance_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_base': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}), 'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}), 'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}), 'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
...@@ -273,4 +281,4 @@ class Migration(SchemaMigration): ...@@ -273,4 +281,4 @@ class Migration(SchemaMigration):
} }
} }
complete_apps = ['vm'] complete_apps = ['vm']
\ No newline at end of file
...@@ -151,6 +151,10 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -151,6 +151,10 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
ordering = ('name', ) ordering = ('name', )
permissions = ( permissions = (
('create_template', _('Can create an instance template.')), ('create_template', _('Can create an instance template.')),
('create_base_template',
_('Can create an instance template (base).')),
('change_template_resources',
_('Can change resources of a template.')),
) )
verbose_name = _('template') verbose_name = _('template')
verbose_name_plural = _('templates') verbose_name_plural = _('templates')
...@@ -263,7 +267,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -263,7 +267,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
('access_console', _('Can access the graphical console of a VM.')), ('access_console', _('Can access the graphical console of a VM.')),
('change_resources', _('Can change resources of a running VM.')), ('change_resources', _('Can change resources of a running VM.')),
('set_resources', _('Can change resources of a new VM.')), ('set_resources', _('Can change resources of a new VM.')),
('create_vm', _('Can create a new VM.')),
('config_ports', _('Can configure port forwards.')), ('config_ports', _('Can configure port forwards.')),
('recover', _('Can recover a destroyed VM.')),
) )
verbose_name = _('instance') verbose_name = _('instance')
verbose_name_plural = _('instances') verbose_name_plural = _('instances')
...@@ -574,11 +580,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -574,11 +580,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def get_connect_host(self, use_ipv6=False): def get_connect_host(self, use_ipv6=False):
"""Get public hostname. """Get public hostname.
""" """
if not self.interface_set.exclude(host=None): if not self.primary_host:
return _('None') return None
proto = 'ipv6' if use_ipv6 else 'ipv4' proto = 'ipv6' if use_ipv6 else 'ipv4'
return self.interface_set.exclude(host=None)[0].host.get_hostname( return self.primary_host.get_hostname(proto=proto)
proto=proto)
def get_connect_command(self, use_ipv6=False): def get_connect_command(self, use_ipv6=False):
"""Returns a formatted connect string. """Returns a formatted connect string.
...@@ -648,11 +653,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -648,11 +653,9 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
raise Node.DoesNotExist() raise Node.DoesNotExist()
def _is_notified_about_expiration(self): def _is_notified_about_expiration(self):
renews = self.activity_log.filter(activity_code__endswith='renew') last_activity = self.activity_log.latest('pk')
cond = {'activity_code__endswith': 'notification_about_expiration'} return (last_activity.activity_code ==
if len(renews) > 0: 'vm.Instance.notification_about_expiration')
cond['finished__gt'] = renews[0].started
return self.activity_log.filter(**cond).exists()
def notify_owners_about_expiration(self, again=False): def notify_owners_about_expiration(self, again=False):
"""Notify owners about vm expiring soon if they aren't already. """Notify owners about vm expiring soon if they aren't already.
...@@ -825,7 +828,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -825,7 +828,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def migrate_vm(self, to_node, timeout=120): def migrate_vm(self, to_node, timeout=120):
queue_name = self.get_remote_queue_name('vm', 'slow') queue_name = self.get_remote_queue_name('vm', 'slow')
return vm_tasks.migrate.apply_async(args=[self.vm_name, return vm_tasks.migrate.apply_async(args=[self.vm_name,
to_node.host.hostname], to_node.host.hostname,
True],
queue=queue_name queue=queue_name
).get(timeout=timeout) ).get(timeout=timeout)
...@@ -862,7 +866,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -862,7 +866,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
AbortableAsyncResult(remote.id).abort() AbortableAsyncResult(remote.id).abort()
raise Exception("Shutdown aborted by user.") raise Exception("Shutdown aborted by user.")
def suspend_vm(self, timeout=60): def suspend_vm(self, timeout=230):
queue_name = self.get_remote_queue_name('vm', 'slow') queue_name = self.get_remote_queue_name('vm', 'slow')
return vm_tasks.sleep.apply_async(args=[self.vm_name, return vm_tasks.sleep.apply_async(args=[self.vm_name,
self.mem_dump['path']], self.mem_dump['path']],
...@@ -929,8 +933,29 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -929,8 +933,29 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
user=user) user=user)
return acts return acts
def get_merged_activities(self, user=None):
whitelist = ("create_disk", "download_disk")
acts = self.get_activities(user)
merged_acts = []
latest = None
for a in acts:
if (latest == a.activity_code and
merged_acts[-1].result == a.result and
a.finished and merged_acts[-1].finished and
a.user == merged_acts[-1].user and
(merged_acts[-1].finished - a.finished).days < 7 and
not a.activity_code.endswith(whitelist)):
merged_acts[-1].times += 1
else:
merged_acts.append(a)
merged_acts[-1].times = 1
latest = a.activity_code
return merged_acts
def get_screenshot(self, timeout=5): def get_screenshot(self, timeout=5):
queue_name = self.get_remote_queue_name('vm') queue_name = self.get_remote_queue_name("vm", "fast")
return vm_tasks.screenshot.apply_async(args=[self.vm_name], return vm_tasks.screenshot.apply_async(args=[self.vm_name],
queue=queue_name queue=queue_name
).get(timeout=timeout) ).get(timeout=timeout)
...@@ -148,10 +148,14 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -148,10 +148,14 @@ class Node(OperatedMixin, TimeStampedModel):
self.enabled = False self.enabled = False
self.save() self.save()
def enable(self, user=None): def enable(self, user=None, base_activity=None):
''' Enable the node. ''' ''' Enable the node. '''
if self.enabled is not True: if self.enabled is not True:
with node_activity(code_suffix='enable', node=self, user=user): if base_activity:
act_ctx = base_activity.sub_activity('enable')
else:
act_ctx = node_activity('enable', node=self, user=user)
with act_ctx:
self.enabled = True self.enabled = True
self.save() self.save()
self.get_info(invalidate_cache=True) self.get_info(invalidate_cache=True)
......
...@@ -24,7 +24,9 @@ from base64 import encodestring ...@@ -24,7 +24,9 @@ from base64 import encodestring
from StringIO import StringIO from StringIO import StringIO
from tarfile import TarFile, TarInfo from tarfile import TarFile, TarInfo
from django.conf import settings from django.conf import settings
from django.utils import timezone
from celery.result import TimeoutError from celery.result import TimeoutError
from monitor.client import Client
def send_init_commands(instance, act, vm): def send_init_commands(instance, act, vm):
...@@ -86,12 +88,33 @@ def agent_started(vm, version=None): ...@@ -86,12 +88,33 @@ def agent_started(vm, version=None):
pass pass
if not initialized: if not initialized:
measure_boot_time(instance)
send_init_commands(instance, act, vm) send_init_commands(instance, act, vm)
with act.sub_activity('start_access_server'): with act.sub_activity('start_access_server'):
start_access_server.apply_async(queue=queue, args=(vm, )) start_access_server.apply_async(queue=queue, args=(vm, ))
def measure_boot_time(instance):
if not instance.template:
return
from vm.models import InstanceActivity
deploy_time = InstanceActivity.objects.filter(
instance=instance, activity_code="vm.Instance.deploy"
).latest("finished").finished
total_boot_time = (timezone.now() - deploy_time).total_seconds()
Client().send([
"template.%(pk)d.boot_time %(val)f %(time)s" % {
'pk': instance.template.pk,
'val': total_boot_time,
'time': time.time(),
}
])
@celery.task @celery.task
def agent_stopped(vm): def agent_stopped(vm):
from vm.models import Instance, InstanceActivity from vm.models import Instance, InstanceActivity
......
...@@ -103,6 +103,7 @@ class InstanceTestCase(TestCase): ...@@ -103,6 +103,7 @@ class InstanceTestCase(TestCase):
inst = Mock(destroyed_at=None, spec=Instance) inst = Mock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = [] inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node) inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING'
migrate_op = MigrateOperation(inst) migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr: with patch('vm.models.instance.vm_tasks.migrate') as migr:
act = MagicMock() act = MagicMock()
...@@ -118,6 +119,7 @@ class InstanceTestCase(TestCase): ...@@ -118,6 +119,7 @@ class InstanceTestCase(TestCase):
inst = MagicMock(destroyed_at=None, spec=Instance) inst = MagicMock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = [] inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node) inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING'
migrate_op = MigrateOperation(inst) migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr: with patch('vm.models.instance.vm_tasks.migrate') as migr:
inst.select_node.side_effect = AssertionError inst.select_node.side_effect = AssertionError
...@@ -133,6 +135,7 @@ class InstanceTestCase(TestCase): ...@@ -133,6 +135,7 @@ class InstanceTestCase(TestCase):
inst = Mock(destroyed_at=None, spec=Instance) inst = Mock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = [] inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node) inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING'
e = Exception('abc') e = Exception('abc')
setattr(e, 'libvirtError', '') setattr(e, 'libvirtError', '')
inst.migrate_vm.side_effect = e inst.migrate_vm.side_effect = e
...@@ -372,6 +375,7 @@ class InstanceActivityTestCase(TestCase): ...@@ -372,6 +375,7 @@ class InstanceActivityTestCase(TestCase):
node = MagicMock(spec=Node, enabled=True) node = MagicMock(spec=Node, enabled=True)
node.instance_set.all.return_value = insts node.instance_set.all.return_value = insts
user = MagicMock(spec=User) user = MagicMock(spec=User)
user.is_superuser = MagicMock(return_value=True)
flush_op = FlushOperation(node) flush_op = FlushOperation(node)
with patch.object(FlushOperation, 'create_activity') as create_act: with patch.object(FlushOperation, 'create_activity') as create_act:
...@@ -383,6 +387,7 @@ class InstanceActivityTestCase(TestCase): ...@@ -383,6 +387,7 @@ class InstanceActivityTestCase(TestCase):
node.disable.assert_called_with(user, act) node.disable.assert_called_with(user, act)
for i in insts: for i in insts:
i.migrate.assert_called() i.migrate.assert_called()
user.is_superuser.assert_called()
def test_flush_disabled_wo_user(self): def test_flush_disabled_wo_user(self):
insts = [MagicMock(spec=Instance, migrate=MagicMock()), insts = [MagicMock(spec=Instance, migrate=MagicMock()),
......
...@@ -31,3 +31,4 @@ simplejson==3.4.0 ...@@ -31,3 +31,4 @@ simplejson==3.4.0
six==1.6.1 six==1.6.1
South==0.8.4 South==0.8.4
sqlparse==0.1.11 sqlparse==0.1.11
pika==0.9.13
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