Commit 3b72505e by Bach Dániel

Merge branch 'master' into feature-store

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/urls.py
	circle/dashboard/views.py
parents 822e58c5 89d48699
......@@ -260,6 +260,7 @@ THIRD_PARTY_APPS = (
'taggit',
'statici18n',
'django_sshkey',
'autocomplete_light',
)
# Apps specific for this project go here.
......
......@@ -35,7 +35,11 @@ SOUTH_TESTS_MIGRATE = False
INSTALLED_APPS += (
'acl.tests',
'django_nose',
)
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = ['--with-doctest']
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
CACHES = {
'default': {
......
......@@ -23,6 +23,7 @@ from logging import getLogger
from time import time
from warnings import warn
from django.contrib import messages
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.serializers.json import DjangoJSONEncoder
......@@ -46,17 +47,24 @@ class WorkerNotFound(Exception):
def activitycontextimpl(act, on_abort=None, on_commit=None):
try:
try:
yield act
except HumanReadableException as e:
result = e
raise
except BaseException as e:
# BaseException is the common parent of Exception and
# system-exiting exceptions, e.g. KeyboardInterrupt
handler = None if on_abort is None else lambda a: on_abort(a, e)
result = create_readable(ugettext_noop("Failure."),
ugettext_noop("Unhandled exception: "
"%(error)s"),
result = create_readable(
ugettext_noop("Failure."),
ugettext_noop("Unhandled exception: %(error)s"),
error=unicode(e))
raise
except:
logger.exception("Failed activity %s" % unicode(act))
handler = None if on_abort is None else lambda a: on_abort(a, e)
act.finish(succeeded=False, result=result, event_handler=handler)
raise e
raise
else:
act.finish(succeeded=True, event_handler=on_commit)
......@@ -70,11 +78,11 @@ activity_code_separator = '.'
def has_prefix(activity_code, *prefixes):
"""Determine whether the activity code has the specified prefix.
E.g.: has_prefix('foo.bar.buz', 'foo.bar') == True
has_prefix('foo.bar.buz', 'foo', 'bar') == True
has_prefix('foo.bar.buz', 'foo.bar', 'buz') == True
has_prefix('foo.bar.buz', 'foo', 'bar', 'buz') == True
has_prefix('foo.bar.buz', 'foo', 'buz') == False
>>> assert has_prefix('foo.bar.buz', 'foo.bar')
>>> assert has_prefix('foo.bar.buz', 'foo', 'bar')
>>> assert has_prefix('foo.bar.buz', 'foo.bar', 'buz')
>>> assert has_prefix('foo.bar.buz', 'foo', 'bar', 'buz')
>>> assert not has_prefix('foo.bar.buz', 'foo', 'buz')
"""
equal = lambda a, b: a == b
act_code_parts = split_activity_code(activity_code)
......@@ -85,11 +93,11 @@ def has_prefix(activity_code, *prefixes):
def has_suffix(activity_code, *suffixes):
"""Determine whether the activity code has the specified suffix.
E.g.: has_suffix('foo.bar.buz', 'bar.buz') == True
has_suffix('foo.bar.buz', 'bar', 'buz') == True
has_suffix('foo.bar.buz', 'foo.bar', 'buz') == True
has_suffix('foo.bar.buz', 'foo', 'bar', 'buz') == True
has_suffix('foo.bar.buz', 'foo', 'buz') == False
>>> assert has_suffix('foo.bar.buz', 'bar.buz')
>>> assert has_suffix('foo.bar.buz', 'bar', 'buz')
>>> assert has_suffix('foo.bar.buz', 'foo.bar', 'buz')
>>> assert has_suffix('foo.bar.buz', 'foo', 'bar', 'buz')
>>> assert not has_suffix('foo.bar.buz', 'foo', 'buz')
"""
equal = lambda a, b: a == b
act_code_parts = split_activity_code(activity_code)
......@@ -196,6 +204,10 @@ class ActivityModel(TimeStampedModel):
DeprecationWarning, stacklevel=2)
value = create_readable(user_text_template="",
admin_text_template=value)
elif not hasattr(value, "to_dict"):
warn("Use HumanReadableObject.", DeprecationWarning, stacklevel=2)
value = create_readable(user_text_template="",
admin_text_template=unicode(value))
self.result_data = None if value is None else value.to_dict()
......@@ -361,8 +373,9 @@ class HumanReadableObject(object):
@classmethod
def create(cls, user_text_template, admin_text_template=None, **params):
return cls(user_text_template,
admin_text_template or user_text_template, params)
return cls(user_text_template=user_text_template,
admin_text_template=(admin_text_template
or user_text_template), params=params)
def set(self, user_text_template, admin_text_template=None, **params):
self._set_values(user_text_template,
......@@ -375,12 +388,22 @@ class HumanReadableObject(object):
def get_admin_text(self):
if self.admin_text_template == "":
return ""
try:
return _(self.admin_text_template) % self.params
except KeyError:
logger.exception("Can't render admin_text_template '%s' %% %s",
self.admin_text_template, unicode(self.params))
return self.get_user_text()
def get_user_text(self):
if self.user_text_template == "":
return ""
try:
return _(self.user_text_template) % self.params
except KeyError:
logger.exception("Can't render user_text_template '%s' %% %s",
self.user_text_template, unicode(self.params))
return self.user_text_template
def to_dict(self):
return {"user_text_template": self.user_text_template,
......@@ -397,10 +420,28 @@ create_readable = HumanReadableObject.create
class HumanReadableException(HumanReadableObject, Exception):
"""HumanReadableObject that is an Exception so can used in except clause.
"""
pass
def __init__(self, level=None, *args, **kwargs):
super(HumanReadableException, self).__init__(*args, **kwargs)
if level is not None:
if hasattr(messages, level):
self.level = level
else:
raise ValueError(
"Level should be the name of an attribute of django."
"contrib.messages (and it should be callable with "
"(request, message)). Like 'error', 'warning'.")
else:
self.level = "error"
def send_message(self, request, level=None):
if request.user and request.user.is_superuser:
msg = self.get_admin_text()
else:
msg = self.get_user_text()
getattr(messages, level or self.level)(request, msg)
def humanize_exception(message, exception=None, **params):
def humanize_exception(message, exception=None, level=None, **params):
"""Return new dynamic-class exception which is based on
HumanReadableException and the original class with the dict of exception.
......@@ -409,8 +450,10 @@ def humanize_exception(message, exception=None, **params):
...
Welcome!
"""
Ex = type("HumanReadable" + type(exception).__name__,
(HumanReadableException, type(exception)),
exception.__dict__)
return Ex.create(message, **params)
ex = Ex.create(message, **params)
if level:
ex.level = level
return ex
import autocomplete_light
from django.utils.translation import ugettext as _
from .views import AclUpdateView
class AclUserAutocomplete(autocomplete_light.AutocompleteGenericBase):
search_fields = (
('^first_name', 'last_name', 'username', '^email', 'profile__org_id'),
('^name', 'groupprofile__org_id'),
)
autocomplete_js_attributes = {'placeholder': _("Name of group or user")}
choice_html_format = u'<span data-value="%s"><span>%s</span> %s</span>'
def choice_html(self, choice):
try:
name = choice.get_full_name()
except AttributeError:
name = _('group')
if name:
name = u'(%s)' % name
return self.choice_html_format % (
self.choice_value(choice), self.choice_label(choice), name)
def choices_for_request(self):
user = self.request.user
self.choices = (AclUpdateView.get_allowed_users(user),
AclUpdateView.get_allowed_groups(user))
return super(AclUserAutocomplete, self).choices_for_request()
autocomplete_light.register(AclUserAutocomplete)
......@@ -1322,7 +1322,7 @@
"user_permissions": [
115
],
"password": "pbkdf2_sha256$10000$KIoeMs78MiOj$PnVXn3YJMehbOciBO32CMzqL0ZnQrzrdb7+b5dE13os=",
"password": "md5$qLN4mQMOrsUJ$f07129fd1a289a0afb4e09f7a6816a4f",
"email": "test@example.org",
"date_joined": "2013-09-04T15:29:49.914Z"
}
......
......@@ -27,6 +27,7 @@ from django.contrib.auth.models import User, Group
from django.core.validators import URLValidator
from django.core.exceptions import PermissionDenied, ValidationError
import autocomplete_light
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (
Layout, Div, BaseInput, Field, HTML, Submit, Fieldset, TEMPLATE_PACK,
......@@ -44,7 +45,6 @@ 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, Instance
)
......@@ -54,6 +54,7 @@ from .models import Profile, GroupProfile
from circle.settings.base import LANGUAGES
from django.utils.translation import string_concat
from .virtvalidator import domain_validator
LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")"))
for l in LANGUAGES)
......@@ -78,7 +79,7 @@ class VmCustomizeForm(forms.Form):
amount = forms.IntegerField(min_value=0, initial=1)
disks = forms.ModelMultipleChoiceField(
queryset=None, required=True)
queryset=None, required=False)
networks = forms.ModelMultipleChoiceField(
queryset=None, required=False)
......@@ -91,8 +92,7 @@ class VmCustomizeForm(forms.Form):
super(VmCustomizeForm, self).__init__(*args, **kwargs)
# set displayed disk and network list
self.fields['disks'].queryset = Disk.get_objects_with_level(
'user', self.user).exclude(type="qcow2-snap")
self.fields['disks'].queryset = self.template.disks.all()
self.fields['networks'].queryset = Vlan.get_objects_with_level(
'user', self.user)
......@@ -596,6 +596,10 @@ class TemplateForm(forms.ModelForm):
n = self.instance.interface_set.values_list("vlan", flat=True)
self.initial['networks'] = n
if self.instance.pk and not self.instance.has_level(self.user,
'owner'):
self.allowed_fields = ()
else:
self.allowed_fields = (
'name', 'access_method', 'description', 'system', 'tags')
if self.user.has_perm('vm.change_template_resources'):
......@@ -675,6 +679,11 @@ class TemplateForm(forms.ModelForm):
@property
def helper(self):
submit_kwargs = {}
if self.instance.pk and not self.instance.has_level(self.user,
'owner'):
submit_kwargs['disabled'] = None
helper = FormHelper()
helper.layout = Layout(
Field("name"),
......@@ -739,7 +748,7 @@ class TemplateForm(forms.ModelForm):
Field("tags"),
),
)
helper.add_input(Submit('submit', 'Save changes'))
helper.add_input(Submit('submit', 'Save changes', **submit_kwargs))
return helper
class Meta:
......@@ -900,7 +909,8 @@ class VmRenewForm(forms.Form):
self.fields['lease'] = forms.ModelChoiceField(queryset=choices,
initial=default,
required=True,
required=False,
empty_label=None,
label=_('Length'))
if len(choices) < 2:
self.fields['lease'].widget = HiddenInput()
......@@ -944,6 +954,25 @@ class VmDownloadDiskForm(forms.Form):
return helper
class VmAddInterfaceForm(forms.Form):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
super(VmAddInterfaceForm, self).__init__(*args, **kwargs)
field = forms.ModelChoiceField(
queryset=choices, required=True, label=_('Vlan'))
if not choices:
field.widget.attrs['disabled'] = 'disabled'
field.empty_label = _('No more networks.')
self.fields['vlan'] = field
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class CircleAuthenticationForm(AuthenticationForm):
# fields: username, password
......@@ -1178,6 +1207,11 @@ class UserCreationForm(OrgUserCreationForm):
return user
class AclUserAddForm(forms.Form):
name = forms.CharField(widget=autocomplete_light.TextWidget(
'AclUserAutocomplete', attrs={'class': 'form-control'}))
class UserKeyForm(forms.ModelForm):
name = forms.CharField(required=True, label=_('Name'))
key = forms.CharField(
......@@ -1223,6 +1257,9 @@ class TraitsForm(forms.ModelForm):
class RawDataForm(forms.ModelForm):
raw_data = forms.CharField(validators=[domain_validator],
widget=forms.Textarea(attrs={'rows': 5}),
required=False)
class Meta:
model = Instance
......
......@@ -77,7 +77,7 @@ class Notification(TimeStampedModel):
def send(cls, user, subject, template, context,
valid_until=None, subject_context=None):
hro = create_readable(template, user=user, **context)
subject = create_readable(subject, subject_context or context)
subject = create_readable(subject, **(subject_context or context))
return cls.objects.create(to=user,
subject_data=subject.to_dict(),
message_data=hro.to_dict(),
......@@ -161,6 +161,11 @@ class Profile(Model):
def __unicode__(self):
return self.get_display_name()
class Meta:
permissions = (
('use_autocomplete', _('Can use autocomplete.')),
)
class FutureMember(Model):
org_id = CharField(max_length=64, help_text=_(
......
......@@ -844,3 +844,7 @@ textarea[name="list-new-namelist"] {
height: 20px;
position: absolute;
}
#show-all-activities-container {
margin: 20px 0 0 10px;
}
......@@ -56,8 +56,6 @@ $(function () {
url: '/dashboard/template/choose/',
success: function(data) {
$('body').append(data);
vmCreateLoaded();
addSliderMiscs();
$('#create-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
......@@ -372,6 +370,11 @@ $(function () {
return false;
});
/* don't close notifications window on missclick */
$(document).on("click", ".notification-messages", function() {
return false;
});
$("#notification-button a").click(function() {
$('.notification-messages').load("/dashboard/notifications/");
$('#notification-button a span[class*="badge-pulse"]').remove();
......
......@@ -3,7 +3,7 @@
$(function() {
/* vm operations */
$('#ops, #vm-details-resources-disk').on('click', '.operation.btn', function(e) {
$('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface').on('click', '.operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin');
$.ajax({
......@@ -50,6 +50,9 @@ $(function() {
*/
if(data.success) {
$('a[href="#activity"]').trigger("click");
if(data.with_reload) {
location.reload();
}
/* if there are messages display them */
if(data.messages && data.messages.length > 0) {
......
var show_all = false;
var in_progress = false;
$(function() {
/* do we need to check for new activities */
if(decideActivityRefresh()) {
checkNewActivity(false, 1);
if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
}
$('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('fa-spin');
checkNewActivity(false, 1);
if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
});
$("#activity-refresh").on("click", "#show-all-activities", function() {
$(this).find("i").addClass("fa-spinner fa-spin");
show_all = !show_all;
$('a[href="#activity"]').trigger("click");
return false;
});
/* save resources */
......@@ -134,11 +151,6 @@ $(function() {
return false;
});
/* show help */
$(".vm-details-help-button").click(function() {
$(".vm-details-help").stop().slideToggle();
});
/* for interface remove buttons */
$('.interface-remove').click(function() {
var interface_pk = $(this).data('interface-pk');
......@@ -295,6 +307,10 @@ $(function() {
$("#vm-details-connection-string").focus();
});
$("a.operation-password_reset").click(function() {
if(Boolean($(this).data("disabled"))) return false;
});
});
......@@ -315,12 +331,13 @@ function removePort(data) {
}
});
}
function decideActivityRefresh() {
var check = false;
/* if something is still spinning */
if($('.timeline .activity:first i:first').hasClass('fa-spin'))
if($('.timeline .activity i').hasClass('fa-spin'))
check = true;
/* if there is only one activity */
if($('#activity-timeline div[class="activity"]').length < 2)
......@@ -340,25 +357,25 @@ 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;
function checkNewActivity(runs) {
var instance = location.href.split('/'); instance = instance[instance.length - 2];
$.ajax({
type: 'GET',
url: '/dashboard/vm/' + instance + '/activity/',
data: {'only_status': only_status},
data: {'show_all': show_all},
success: function(data) {
if(!only_status) {
if(show_all) { /* replace on longer string freezes the spinning stuff */
$("#activity-refresh").html(data['activities']);
} else {
a = unescapeHTML(data['activities']);
b = changeHTML($("#activity-timeline").html());
b = changeHTML($("#activity-refresh").html());
if(a != b)
$("#activity-timeline").html(data['activities']);
$("#activity-refresh").html(data['activities']);
}
$("#ops").html(data['ops']);
$("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip();
}
$("#vm-details-state i").prop("class", "fa " + data['icon']);
$("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
......@@ -378,14 +395,16 @@ function checkNewActivity(only_status, runs) {
if(runs > 0 && decideActivityRefresh()) {
setTimeout(
function() {checkNewActivity(only_status, runs + 1)},
function() {checkNewActivity(runs + 1)},
1000 + Math.exp(runs * 0.05)
);
} else {
in_progress = false;
}
$('a[href="#activity"] i').removeClass('fa-spin');
},
error: function() {
in_progress = false;
}
});
}
......@@ -27,43 +27,6 @@ from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey
class VmListTable(Table):
pk = TemplateColumn(
template_name='dashboard/vm-list/column-id.html',
verbose_name="ID",
attrs={'th': {'class': 'vm-list-table-thin'}},
)
name = TemplateColumn(
template_name="dashboard/vm-list/column-name.html"
)
admin = TemplateColumn(
template_name='dashboard/vm-list/column-admin.html',
attrs={'th': {'class': 'vm-list-table-admin'}},
)
details = TemplateColumn(
template_name='dashboard/vm-list/column-details.html',
attrs={'th': {'class': 'vm-list-table-thin'}},
)
actions = TemplateColumn(
template_name='dashboard/vm-list/column-actions.html',
attrs={'th': {'class': 'vm-list-table-thin'}},
)
time_of_suspend = TemplateColumn(
'{{ record.time_of_suspend|timeuntil }}',
verbose_name=_("Suspend in"))
time_of_delete = TemplateColumn(
'{{ record.time_of_delete|timeuntil }}',
verbose_name=_("Delete in"))
class Meta:
model = Instance
attrs = {'class': ('table table-bordered table-striped table-hover '
'vm-list-table')}
fields = ('pk', 'name', 'state', 'time_of_suspend', 'time_of_delete', )
class NodeListTable(Table):
pk = Column(
......
......@@ -70,6 +70,7 @@
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script>
{% include 'autocomplete_light/static.html' %}
{% block extra_script %}
{% endblock %}
......@@ -79,4 +80,11 @@
{% block extra_etc %}
{% endblock %}
<script>
yourlabs.TextWidget.prototype.getValue = function(choice) {
return choice.children().html();
}
</script>
</html>
......@@ -10,10 +10,12 @@
<div class="label label-danger"{% if user.is_superuser %} title="{{ d.get_latest_activity_result }}"{% endif %}>{% trans "failed" %}</div>
{% endif %}
{% else %}<span class="disk-list-disk-percentage" data-disk-pk="{{ d.pk }}">{{ d.get_download_percentage }}</span>%{% endif %}
{% if is_owner != False %}
<a href="{% url "dashboard.views.disk-remove" pk=d.pk %}?next={{ request.path }}"
data-disk-pk="{{ d.pk }}" class="btn btn-xs btn-danger pull-right disk-remove"
{% if not long_remove %}title="{% trans "Remove" %}"{% endif %}
>
<i class="fa fa-times"></i>{% if long_remove %} {% trans "Remove" %}{% endif %}
</a>
{% endif %}
<div style="clear: both;"></div>
{% load i18n %}
<form action="{{ acl.url }}" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields" id="{{table_id}}">
<thead>
<tr>
<th></th>
<th>{% trans "Who" %}</th>
<th>{% trans "What" %}</th>
<th><i class="fa fa-times"></i></th>
</tr>
</thead>
<tbody>
{% for i in acl.users %}
<tr>
<td>
<i class="fa fa-user"></i>
</td>
<td>
<a href="{% url "dashboard.views.profile" username=i.user.username %}"
title="{{ i.user.username }}">
{% include "dashboard/_display-name.html" with user=i.user show_org=True %}
</a>
</td>
<td>
<select class="form-control" name="perm-u-{{i.user.id}}"{% if i.level not in acl.allowed_levels %} disabled{% endif %}>
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%}
{% if id not in acl.allowed_levels %} disabled{% endif %}
value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td>
<input type="checkbox" name="remove-u-{{i.user.id}}" title="{% trans "Remove" %}"/>
</td>
</tr>
{% endfor %}
{% for i in acl.groups %}
<tr>
<td><i class="fa fa-group"></i></td>
<td>
<a href="{% url "dashboard.views.group-detail" pk=i.group.pk %}">
{{i.group}}
</a>
</td>
<td>
<select class="form-control" name="perm-g-{{i.group.id}}{% if i.level not in acl.allowed_levels %} disabled{% endif %}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%}
{% if id not in acl.allowed_levels %} disabled{% endif %}
value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td>
<input type="checkbox" name="remove-g-{{i.group.id}}" title="{% trans "Remove" %}"/>
</td>
</tr>
{% endfor %}
<tr><td><i class="fa fa-plus"></i></td>
<td>{{aclform.name }}</td>
<td><select class="form-control" name="level">
{% for id, name in acl.levels %}
{% if id in acl.allowed_levels %}
<option value="{{id}}">{{name}}</option>
{% endif %}
{% endfor %}
</select></td><td></td>
</tr>
</tbody>
</table>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
{% load i18n %}
<div class="alert alert-info" id="template-choose-alert">
{% trans "Customize an existing template or create a brand new one from scratch!" %}
{% if perms.vm.create_base_template %}
{% trans "Customize an existing template or create a brand new one from scratch." %}
{% else %}
{% trans "Customize an existing template." %}
{% endif %}
</div>
<form action="{% url "dashboard.views.template-choose" %}" method="POST"
......
......@@ -18,8 +18,8 @@
{% endblock %}
{% block navbar %}
<ul class="nav navbar-nav pull-right">
{% if user.is_authenticated and user.pk and not request.token_user %}
<ul class="nav navbar-nav pull-right">
<li class="dropdown" id="notification-button">
<a href="{% url "dashboard.views.notifications" %}" style="color: white; font-size: 12px;"
class="dropdown-toggle" data-toggle="dropdown">
......@@ -32,9 +32,8 @@
<li>{% trans "Loading..." %}</li>
</ul>
</li>
</ul>
</ul>
{% if user.is_authenticated and user.pk %}
<a class="navbar-brand pull-right" href="{% url "logout" %}?next={% url "login" %}" style="color: white; font-size: 10px;">
<i class="fa fa-sign-out"></i> {% trans "Log out" %}
</a>
......@@ -48,7 +47,7 @@
<a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;"><i class="fa fa-cogs"></i> {% trans "Admin" %}</a>
{% endif %}
{% else %}
<a class="navbar-brand pull-right" href="{% url "login" %}?next={% url "dashboard.index" %}" style="color: white; font-size: 10px;"><i class="fa fa-sign-in"></i> {% trans "Log in " %}</a>
<a class="navbar-brand pull-right" href="{% url "login" %}?next={{ request.path }}" style="color: white; font-size: 10px;"><i class="fa fa-sign-in"></i> {% trans "Log in " %}</a>
{% endif %}
{% endblock %}
......
......@@ -7,7 +7,7 @@
<div class="page-header">
<div class="pull-right" style="padding-top: 15px;">
<a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs group-details-rename-button"><i class="fa fa-pencil"></i></a>
<a title="{% trans "Delete" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=group.pk %}"><i class="fa fa-trash"></i></a>
<a title="{% trans "Delete" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=group.pk %}"><i class="fa fa-trash-o"></i></a>
<a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs group-details-help-button"><i class="fa fa-question"></i></a>
</div>
<h1>
......@@ -104,76 +104,7 @@
<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>
<tr>
<th></th><th>{% trans "Who" %}</th><th>{% trans "What" %}</th><th>{% trans "Remove" %}</th>
</tr>
</thead>
<tbody>
{% for i in acl.users %}
<tr>
<td>
<i class="fa fa-user"></i>
</td>
<td>
<a href="{% url "dashboard.views.profile" username=i.user.username %}" title="{{ i.user.username }}"
>{% include "dashboard/_display-name.html" with user=i.user show_org=True %}</a>
</td>
<td>
<select class="form-control" name="perm-u-{{i.user.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td class="user-remove"><a data-group_pk="{{ group.pk }}" data-member_pk="{{i.user.pk }}" href="{% url "dashboard.views.remove-acluser" member_pk=i.user.pk group_pk=group.pk %}" class="real-link delete-from-group btn btn-link btn-xs"><i class="fa fa-times"><span class="sr-only">{% trans "remove" %}</span></i></a></td>
</tr>
{% endfor %}
{% for i in acl.groups %}
<tr>
<td>
<i class="fa fa-group"></i>
</td>
<td>
<a href="{% url "dashboard.views.group-detail" pk=i.group.pk %}">{{ i.group }}</a>
</td>
<td>
<select class="form-control" name="perm-g-{{ i.group.pk }}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td class="user-remove"><a data-group_pk="{{ i.pk }}"data-member_pk="{{i.group.pk }}" href="{% url "dashboard.views.remove-aclgroup" member_pk=i.group.pk group_pk=group.pk %}" class="real-link delete-from-group btn btn-link btn-xs"><i class="fa fa-times"><span class="sr-only">{% trans "remove" %}</span></i></a>
</td>
</tr>
{% endfor %}
<tr>
<td><i class="fa fa-plus"></i></td>
<td>
<input type="text" class="form-control" name="perm-new-name"
placeholder="{% trans "Name of group or user" %}">
</td>
<td>
<select class="form-control" name="perm-new">
{% for id, name in acl.levels %}
<option value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td></td>
</tr>
</tbody>
</table>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
{% include "dashboard/_manage_access.html" with table_id="group-detail-perm-table" %}
{% if user.is_superuser %}
<hr />
......
......@@ -20,11 +20,13 @@
<div class="clearfix"></div>
</a>
{% empty %}
<div class="list-group-item">
<div class="alert alert-warning" style="margin: 10px;">
<p>
{% trans "You don't have any templates, however you can still start virtual machines and even save them as new templates!" %}
</p>
</div>
</div>
{% endfor %}
</div>
<div href="#" class="list-group-item list-group-footer text-right">
......
......@@ -14,17 +14,17 @@
</h1>
</div>
<div class="row">
<div class="col-md-4" id="vm-info-pane">
<div class="col-md-5" id="vm-info-pane">
<div class="big">
<span id="vm-activity-state" class="label label-{% if object.get_status_id == 'wait' %}info{% else %}{% if object.succeeded %}success{% else %}error{% endif %}{% endif %}">
<span>{{ object.get_status_id|upper }}</span>
</span>
</div>
<div id="vm-activity-context" class="timeline">
{% include "dashboard/vm-detail/_activity-timeline.html" with active=object %}
</div>
</div>
<div class="col-md-8">
<div class="col-md-7">
<div class="panel panel-default">
<!--<div class="panel-heading"><h2 class="panel-title">{% trans "Activity" %}</h2></div> -->
<div class="panel-body">
......
......@@ -64,7 +64,7 @@
<i class="fa fa-desktop"></i>
{% trans "Virtual machines owned by the user" %} ({{ instances_owned|length }})
</h4>
<ul class="dashboard-profile-vm-list">
<ul class="dashboard-profile-vm-list fa-ul">
{% for i in instances_owned %}
<li>
<a href="{{ i.get_absolute_url }}">
......@@ -85,7 +85,7 @@
<i class="fa fa-desktop"></i>
{% trans "Virtual machines with access" %} ({{ instances_with_access|length }})
</h4>
<ul class="dashboard-profile-vm-list">
<ul class="dashboard-profile-vm-list fa-ul">
{% for i in instances_with_access %}
<li>
<a href="{{ i.get_absolute_url }}">
......
......@@ -29,75 +29,7 @@
<h4 class="no-margin"><i class="fa fa-group"></i> {% trans "Manage access" %}</h4>
</div>
<div class="panel-body">
<form action="{% url "dashboard.views.template-acl" pk=object.pk %}" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields" id="template-access-table">
<thead>
<tr>
<th></th>
<th>{% trans "Who" %}</th>
<th>{% trans "What" %}</th>
<th><i class="fa fa-times"></i></th>
</tr>
</thead>
<tbody>
{% for i in acl.users %}
<tr>
<td>
<i class="fa fa-user"></i>
</td>
<td>
<a href="{% url "dashboard.views.profile" username=i.user.username %}"
title="{{ i.user.username }}">
{% include "dashboard/_display-name.html" with user=i.user show_org=True %}
</a>
</td>
<td>
<select class="form-control" name="perm-u-{{i.user.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td>
<input type="checkbox" name="remove-u-{{i.user.id}}" title="{% trans "Remove" %}"/>
</td>
</tr>
{% endfor %}
{% for i in acl.groups %}
<tr>
<td><i class="fa fa-group"></i></td>
<td>
<a href="{% url "dashboard.views.group-detail" pk=i.group.pk %}">
{{i.group}}
</a>
</td>
<td>
<select class="form-control" name="perm-g-{{i.group.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td>
<input type="checkbox" name="remove-g-{{i.group.id}}" title="{% trans "Remove" %}"/>
</td>
</tr>
{% endfor %}
<tr><td><i class="fa fa-plus"></i></td>
<td><input type="text" class="form-control" name="perm-new-name"
placeholder="{% trans "Name of group or user" %}"></td>
<td><select class="form-control" name="perm-new">
{% for id, name in acl.levels %}
<option value="{{id}}">{{name}}</option>
{% endfor %}
</select></td><td></td>
</tr>
</tbody>
</table>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
{% include "dashboard/_manage_access.html" with table_id="template-access-table" %}
</div>
</div>
......
......@@ -10,8 +10,8 @@
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a href="{% url "dashboard.views.template-create" %}" class="pull-right btn btn-success btn-xs">
<i class="fa fa-plus"></i> {% trans "new base vm" %}
<a href="{% url "dashboard.views.template-choose" %}" class="pull-right btn btn-success btn-xs template-choose">
<i class="fa fa-plus"></i> {% trans "new template" %}
</a>
<h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %}</h3>
</div>
......
......@@ -98,17 +98,12 @@
</div>
</dd>
<dd style="font-size: 10px; text-align: right; padding-top: 8px;">
<a id="vm-details-pw-change" href="#">{% trans "Generate new password!" %}</a>
</dd>
<div id="vm-details-pw-confirm"> {% comment %} TODO Couldn't this use a modal? {% endcomment%}
<dt>
{% trans "Are you sure?" %}
</dt>
<dd>
<a href="#" class="vm-details-pw-confirm-choice label label-success" data-choice="1" data-vm="{{ instance.pk }}">{% trans "Yes" %}</a> /
<a href="#" class="vm-details-pw-confirm-choice label label-danger" data-choice="0">{% trans "No" %}</a>
</dd>
<div id="vm-details-pw-reset">
{% with op=op.password_reset %}{% if op %}
<a href="{% if op.disabled %}#{% else %}{{op.get_url}}{% endif %}" class="operation operation-{{op.op}}" data-disabled="{% if op.disabled %}true" title="{% trans "Start the VM to change the password." %}"{% else %}false" {% endif %}>{% trans "Generate new password!" %}</a>
{% endif %}{% endwith %}
</div>
</dd>
</dl>
<div class="input-group" id="dashboard-vm-details-connect-command">
......
{% load i18n %}
<div id="activity-timeline" class="timeline">
{% for a in activities %}
<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" data-activity-id="{{ a.pk }}">
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
......@@ -7,7 +10,7 @@
<strong{% if a.result %} title="{{ a.result.get_user_text }}"{% endif %}>
<a href="{{ a.get_absolute_url }}">
{% if a.times > 1 %}({{ a.times }}x){% endif %}
{{ a.readable_name.get_user_text }}</a>
{{ a.readable_name.get_user_text|capfirst }}</a>
{% if a.has_percent %}
- {{ a.percentage }}%
......@@ -32,7 +35,7 @@
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}{% if s.pk == active.pk %} sub-activity-active{% endif %}">
<span{% if s.result %} title="{{ s.result.get_user_text }}"{% endif %}>
<a href="{{ s.get_absolute_url }}">
{{ s.readable_name.get_user_text }}</a></span> &ndash;
{{ s.readable_name.get_user_text|capfirst }}</a></span> &ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
......@@ -47,3 +50,16 @@
{% endif %}
</div>
{% endfor %}
</div>
{% if show_show_all %}
<div id="show-all-activities-container">
<a id="show-all-activities" href="#">
{% if activities|length > 10 %}
{% trans "Show less activities" %} <i class="fa fa-angle-double-up"></i>
{% else %}
{% trans "Show all activities" %} <i class="fa fa-angle-double-down"></i>
{% endif %}
</a>
</div>
{% endif %}
......@@ -3,7 +3,7 @@
{% 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">
operation operation-{{op.op}}">
<i class="fa fa-{{op.icon}}"></i>
{{op.name}} </a>
{% endif %}
......
......@@ -14,64 +14,4 @@
{% endif %}
</p>
<h3>{% trans "Permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields" id="vm-access-table">
<thead><tr>
<th></th>
<th>{% trans "Who" %}</th>
<th>{% trans "What" %}</th>
<th>{% trans "Remove" %}</th>
</tr></thead>
<tbody>
{% for i in acl.users %}
<tr>
<td><i class="fa fa-user"></i></td>
<td>
<a href="{% url "dashboard.views.profile" username=i.user.username %}" title="{{ i.user.username }}"
>{% include "dashboard/_display-name.html" with user=i.user show_org=True %}</a>
</td>
<td>
<select class="form-control" name="perm-u-{{i.user.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td>
<input type="checkbox" name="remove-u-{{i.user.id}}"/>
</td>
</tr>
{% endfor %}
{% for i in acl.groups %}
<tr>
<td><i class="fa fa-group"></i></td>
<td>
<a href="{% url "dashboard.views.group-detail" pk=i.group.pk %}"
>{{ i.group.name }}</a>
</td>
<td>
<select class="form-control" name="perm-g-{{i.group.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select></td>
<td>
<input type="checkbox" name="remove-g-{{i.group.id}}"/>
</td>
</tr>
{% endfor %}
<tr><td><i class="fa fa-plus"></i></td>
<td><input type="text" class="form-control" name="perm-new-name"
placeholder="{% trans "Name of group or user" %}"></td>
<td><select class="form-control" name="perm-new">
{% for id, name in acl.levels %}
<option value="{{id}}">{{name}}</option>
{% endfor %}
</select></td><td></td>
</tr>
</tbody>
</table>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
{% include "dashboard/_manage_access.html" with table_id="vm-access-table" %}
......@@ -2,6 +2,6 @@
<h3>{% trans "Activity" %}</h3>
<div id="activity-timeline" class="timeline">
<div id="activity-refresh">
{% include "dashboard/vm-detail/_activity-timeline.html" %}
</div>
......@@ -47,12 +47,14 @@
</dl>
<h4>{% trans "Expiration" %} {% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %}
{% with op=op.renew %}
<span id="vm-details-renew-op">
{% with op=op.renew %}{% if op %}
<a href="{{op.get_url}}" class="btn btn-success btn-xs
operation operation-{{op.op}} btn btn-default">
operation operation-{{op.op}}">
<i class="fa fa-{{op.icon}}"></i>
{{op.name}} </a>
{% endwith %}
{% endif %}{% endwith %}
</span>
</h4>
<dl>
<dt>{% trans "Suspended at:" %}</dt>
......
{% load i18n %}
{% load network_tags %}
<h2>
<a href="#" id="vm-details-network-add" class="btn btn-success pull-right no-js-hidden">
<i class="fa fa-plus"></i> {% trans "add interface" %}
</a>
<div id="vm-details-add-interface">
{% with op=op.add_interface %}{% if op %}
<a href="{{op.get_url}}" class="btn btn-{{op.effect}} operation pull-right"
{% if op.disabled %}disabled{% endif %}>
<i class="fa fa-{{op.icon}}"></i> {% trans "add interface" %}</a>
{% endif %}{% endwith %}
</div>
{% trans "Interfaces" %}
</h2>
<div class="js-hidden row" id="vm-details-network-add-form">
<div class="col-md-12">
<div>
<hr />
<h3>
{% trans "Add new network interface!" %}
</h3>
<form method="POST" action="">
{% csrf_token %}
<div class="input-group" style="max-width: 330px;">
<select name="new_network_vlan" class="form-control font-awesome-font">
{% for v in vlans %}
<option value="{{ v.pk }}">
{% if v.managed %}
&#xf0ac;
{% else %}
&#xf0c1;
{% endif %}
{{ v.name }}
</option>
{% empty %}
<option value="-1">No more networks!</option>
{% endfor %}
</select>
<div class="input-group-btn">
<button {% if vlans|length == 0 %}disabled{% endif %}
type="submit" class="btn btn-success"><i class="fa fa-plus-circle"></i></button>
</div>
</div>
</form>
<hr />
</div>
</div>
</div>
{% for i in instance.interface_set.all %}
<div>
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title-page %}{% trans "Edit raw data" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-7">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin"><i class="fa fa-time"></i> {% trans "Edit raw data" %}</h3>
</div>
<div class="panel-body">
{% with form=form %}
{% include "display-form-errors.html" %}
{% endwith %}
{% crispy form %}
</div>
</div>
</div>
</div>
{% endblock %}
......@@ -2,8 +2,6 @@
<div class="btn-group">
<button type="button" class="btn btn-xs btn-warning nojs-dropdown-toogle dropdown-toggle" data-toggle="dropdown">Action <i class="fa fa-caret-down"></i></button>
<ul class="nojs-dropdown-toogle dropdown-menu" role="menu">
<li><a href="#"><i class="fa fa-refresh"></i> Reboot</a></li>
<li><a href="#"><i class="fa fa-off"></i> Shutdown</a></li>
<li><a data-vm-pk="{{ record.pk }}" class="real-link vm-delete" href="{% url "dashboard.views.delete-vm" pk=record.pk %}?next={{ request.path }}"><i class="fa fa-times"></i> Discard</a></li>
<li><a class="real-link" href="{%url "dashboard.vm.op.destroy" pk=record.pk %}?next={{ request.path }}"><i class="fa fa-times"></i> Discard</a></li>
</ul>
</div>
......@@ -30,6 +30,7 @@ from django.utils import baseconv
from ..models import Profile
from ..views import InstanceActivityDetail, InstanceActivity
from ..views import vm_ops, Instance, UnsubscribeFormView
from ..views import AclUpdateView
from .. import views
......@@ -279,6 +280,7 @@ class RenewViewTest(unittest.TestCase):
view = vm_ops['renew']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
......@@ -287,7 +289,10 @@ class RenewViewTest(unittest.TestCase):
inst.has_level.return_value = True
go.return_value = inst
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234).render().status_code == 200
assert view.as_view()(request, pk=1234)
assert not msg.error.called
assert inst.renew.async.called_with(user=request.user, lease=None)
assert inst.renew.async.return_value.get.called
# success would redirect
def test_renew_by_owner_w_param(self):
......@@ -429,6 +434,79 @@ class RenewViewTest(unittest.TestCase):
view.as_view()(request, pk=1234)['location'])
class AclUpdateViewTest(unittest.TestCase):
def test_has_next_level(self):
data = {None: 'user', 'user': 'operator', 'operator': 'owner',
'owner': 'owner'}
for k, v in data.items():
inst = MagicMock(spec=Instance)
inst.has_level.return_value = True
inst.ACL_LEVELS = Instance.ACL_LEVELS
self.assertTrue(AclUpdateView.has_next_level('dummy', inst, k))
inst.has_level.assert_called_with('dummy', v)
def test_set_level_mod_owner(self):
with patch('dashboard.views.messages') as msg:
request = FakeRequestFactory(POST={})
inst = MagicMock(spec=Instance)
inst.owner = request.user
v = AclUpdateView()
v.instance = inst
v.request = request
v.get_level = MagicMock(return_value='owner')
v.check_auth = MagicMock(side_effect=Exception(''))
v.set_level(request.user, 'user')
v.get_level.assert_called_with(request.user)
assert not v.check_auth.called
assert msg.warning.called
def test_set_level_permitted(self):
data = (('user', 'owner', ('user', 'operator', 'owner'), False),
(None, None, ('user', ), True),
('user', None, ('user', ), True),
(None, 'user', ('user', ), True),
('operator', 'owner', ('user', 'operator'), True),
(None, 'user', ('user', 'operator'), False))
for old_level, new_level, allowed_levels, fail in data:
with patch('dashboard.views.messages') as msg:
def has_level(user, level):
return level in allowed_levels
request = FakeRequestFactory(POST={})
inst = MagicMock(spec=Instance)
inst.has_level.side_effect = has_level
inst.ACL_LEVELS = Instance.ACL_LEVELS
v = AclUpdateView()
v.instance = inst
v.request = request
v.is_owner = True
v.get_level = MagicMock(return_value=old_level)
v.set_level(request.user, new_level)
v.get_level.assert_called_with(request.user)
assert (new_level == old_level) ^ inst.has_level.called
assert fail ^ inst.set_level.called
assert fail ^ msg.success.called
def test_readd(self):
request = FakeRequestFactory(POST={'name': 'user0', 'level': 'user'})
with patch('dashboard.views.messages') as msg:
with patch.object(AclUpdateView, 'get_object') as go:
view = AclUpdateView.as_view()
inst = MagicMock(spec=Instance)
go.return_value = inst
view(request)
assert msg.warning.called
def FakeRequestFactory(user=None, **kwargs):
''' FakeRequestFactory, FakeMessages and FakeRequestContext are good for
mocking out django views; they are MUCH faster than the Django test client.
......
......@@ -24,10 +24,10 @@ from django.contrib.auth.models import User, Group
from django.contrib.auth.models import Permission
from django.contrib.auth import authenticate
from dashboard.views import VmAddInterfaceView
from vm.models import Instance, InstanceTemplate, Lease, Node, Trait
from vm.operations import WakeUpOperation
from vm.operations import WakeUpOperation, AddInterfaceOperation
from ..models import Profile
from storage.models import Disk
from firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch
from django_sshkey.models import UserKey
......@@ -107,28 +107,6 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.get('/dashboard/vm/1/')
self.assertEqual(response.status_code, 200)
def test_permitted_vm_delete(self):
c = Client()
self.login(c, 'user2')
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
response = c.post('/dashboard/vm/delete/1/')
self.assertEqual(response.status_code, 302)
def test_not_permitted_vm_delete(self):
c = Client()
self.login(c, 'user2')
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'operator')
response = c.post('/dashboard/vm/delete/1/')
self.assertEqual(response.status_code, 403)
def test_unpermitted_vm_delete(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/delete/1/')
self.assertEqual(response.status_code, 403)
def test_unpermitted_vm_mass_delete(self):
c = Client()
self.login(c, 'user1')
......@@ -143,33 +121,21 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]})
self.assertEqual(response.status_code, 302)
def test_permitted_password_change(self):
c = Client()
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
inst.node = Node.objects.all()[0]
inst.save()
password = inst.pw
response = c.post("/dashboard/vm/1/", {'change_password': True})
self.assertTrue(Instance.get_remote_queue_name.called)
self.assertEqual(response.status_code, 302)
self.assertNotEqual(password, Instance.objects.get(pk=1).pw)
def test_unpermitted_password_change(self):
c = Client()
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
password = inst.pw
response = c.post("/dashboard/vm/1/", {'change_password': True})
response = c.post("/dashboard/vm/1/op/password_reset/")
self.assertEqual(response.status_code, 403)
self.assertEqual(password, Instance.objects.get(pk=1).pw)
def test_unpermitted_network_add_wo_perm(self):
c = Client()
self.login(c, "user2")
response = c.post("/dashboard/vm/1/", {'new_network_vlan': 1})
response = c.post("/dashboard/vm/1/op/add_interface/",
{'vlan': 1})
self.assertEqual(response.status_code, 403)
def test_unpermitted_network_add_wo_vlan_perm(self):
......@@ -177,8 +143,18 @@ class VmDetailTest(LoginMixin, TestCase):
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
response = c.post("/dashboard/vm/1/", {'new_network_vlan': 1})
self.assertEqual(response.status_code, 403)
interface_count = inst.interface_set.count()
with patch.object(AddInterfaceOperation, 'async') as async:
async.side_effect = inst.add_interface.call
with patch.object(VmAddInterfaceView, 'get_form_kwargs',
autospec=True) as get_form_kwargs:
get_form_kwargs.return_value = {'choices': Vlan.objects.all()}
response = c.post("/dashboard/vm/1/op/add_interface/",
{'vlan': 1})
self.assertEqual(response.status_code, 302)
assert async.called
self.assertEqual(inst.interface_set.count(), interface_count)
def test_permitted_network_add(self):
c = Client()
......@@ -188,9 +164,12 @@ class VmDetailTest(LoginMixin, TestCase):
vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u1, 'user')
interface_count = inst.interface_set.count()
response = c.post("/dashboard/vm/1/",
{'new_network_vlan': 1})
with patch.object(AddInterfaceOperation, 'async') as mock_method:
mock_method.side_effect = inst.add_interface
response = c.post("/dashboard/vm/1/op/add_interface/",
{'vlan': 1})
self.assertEqual(response.status_code, 302)
assert mock_method.called
self.assertEqual(inst.interface_set.count(), interface_count + 1)
def test_permitted_network_delete(self):
......@@ -249,7 +228,6 @@ class VmDetailTest(LoginMixin, TestCase):
def test_use_unpermitted_template(self):
c = Client()
self.login(c, 'user1')
Disk.objects.get(id=1).set_level(self.u1, 'user')
Vlan.objects.get(id=1).set_level(self.u1, 'user')
response = c.post('/dashboard/vm/create/',
{'template': 1,
......@@ -261,7 +239,6 @@ class VmDetailTest(LoginMixin, TestCase):
def test_use_permitted_template(self):
c = Client()
self.login(c, 'user1')
Disk.objects.get(id=1).set_level(self.u1, 'user')
InstanceTemplate.objects.get(id=1).set_level(self.u1, 'user')
Vlan.objects.get(id=1).set_level(self.u1, 'user')
response = c.post('/dashboard/vm/create/',
......@@ -293,7 +270,6 @@ class VmDetailTest(LoginMixin, TestCase):
self.login(c, 'user1')
tmpl = InstanceTemplate.objects.get(id=1)
tmpl.set_level(self.u1, 'owner')
tmpl.disks.get().set_level(self.u1, 'owner')
Vlan.objects.get(id=1).set_level(self.u1, 'user')
kwargs = tmpl.__dict__.copy()
kwargs.update(name='t1', lease=1, disks=1, raw_data='tst1')
......@@ -405,8 +381,7 @@ class VmDetailTest(LoginMixin, TestCase):
inst.set_level(self.u2, 'owner')
vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user')
response = c.post("/dashboard/vm/1/",
{'new_network_vlan': 1})
inst.add_interface(user=self.u2, vlan=vlan)
host = Host.objects.get(
interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get(
......@@ -425,8 +400,7 @@ class VmDetailTest(LoginMixin, TestCase):
inst.set_level(self.u2, 'owner')
vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user')
response = c.post("/dashboard/vm/1/",
{'new_network_vlan': 1})
inst.add_interface(user=self.u2, vlan=vlan)
host = Host.objects.get(
interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get(
......@@ -533,28 +507,29 @@ class VmDetailTest(LoginMixin, TestCase):
def test_permitted_wake_up_wrong_state(self):
c = Client()
self.login(c, "user2")
with patch.object(WakeUpOperation, 'async') as mock_method:
with patch.object(WakeUpOperation, 'async') as mock_method, \
patch.object(Instance.WrongStateError, 'send_message') as wro:
inst = Instance.objects.get(pk=1)
mock_method.side_effect = inst.wake_up
inst.manual_state_change('RUNNING')
inst.status = 'RUNNING'
inst.set_level(self.u2, 'owner')
with patch('dashboard.views.messages') as msg:
c.post("/dashboard/vm/1/op/wake_up/")
assert msg.error.called
inst = Instance.objects.get(pk=1)
self.assertEqual(inst.status, 'RUNNING') # mocked anyway
assert mock_method.called
assert wro.called
def test_permitted_wake_up(self):
c = Client()
self.login(c, "user2")
with patch.object(Instance, 'select_node', return_value=None):
with patch.object(WakeUpOperation, 'async') as new_wake_up:
with patch('vm.tasks.vm_tasks.wake_up.apply_async') as wuaa:
with patch.object(Instance, 'select_node', return_value=None), \
patch.object(WakeUpOperation, 'async') as new_wake_up, \
patch('vm.tasks.vm_tasks.wake_up.apply_async') as wuaa, \
patch.object(Instance.WrongStateError, 'send_message') as wro:
inst = Instance.objects.get(pk=1)
new_wake_up.side_effect = inst.wake_up
inst.get_remote_queue_name = Mock(return_value='test')
inst.manual_state_change('SUSPENDED')
inst.status = 'SUSPENDED'
inst.set_level(self.u2, 'owner')
with patch('dashboard.views.messages') as msg:
response = c.post("/dashboard/vm/1/op/wake_up/")
......@@ -563,17 +538,16 @@ class VmDetailTest(LoginMixin, TestCase):
self.assertEqual(inst.status, 'RUNNING')
assert new_wake_up.called
assert wuaa.called
assert not wro.called
def test_unpermitted_wake_up(self):
c = Client()
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.manual_state_change('SUSPENDED')
inst.status = 'SUSPENDED'
inst.set_level(self.u2, 'user')
response = c.post("/dashboard/vm/1/op/wake_up/")
self.assertEqual(response.status_code, 403)
inst = Instance.objects.get(pk=1)
self.assertEqual(inst.status, 'SUSPENDED')
def test_non_existing_template_get(self):
c = Client()
......@@ -593,7 +567,6 @@ class VmDetailTest(LoginMixin, TestCase):
'template': 1,
'cpu_priority': 1, 'cpu_count': 1, 'ram_size': 1,
'network': [],
'disks': [Disk.objects.get(id=1).pk],
})
self.assertEqual(response.status_code, 302)
......@@ -1250,7 +1223,7 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/',
{'perm-new-name': 'user3', 'perm-new': 'owner'})
{'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302)
......@@ -1261,9 +1234,9 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/',
{'perm-new-name': 'user3', 'perm-new': 'owner'})
{'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 302)
def test_superuser_add_acluser_to_group(self):
c = Client()
......@@ -1272,7 +1245,7 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/',
{'perm-new-name': 'user3', 'perm-new': 'owner'})
{'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users + 1, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302)
......@@ -1283,7 +1256,7 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/',
{'perm-new-name': 'user3', 'perm-new': 'owner'})
{'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users + 1, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302)
......@@ -1293,7 +1266,7 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/',
{'perm-new-name': 'group2', 'perm-new': 'owner'})
{'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302)
......@@ -1304,9 +1277,9 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/',
{'perm-new-name': 'group2', 'perm-new': 'owner'})
{'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 403)
self.assertEqual(response.status_code, 302)
def test_superuser_add_aclgroup_to_group(self):
c = Client()
......@@ -1315,7 +1288,7 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/',
{'perm-new-name': 'group2', 'perm-new': 'owner'})
{'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302)
......@@ -1326,7 +1299,7 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/',
{'perm-new-name': 'group2', 'perm-new': 'owner'})
{'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302)
......@@ -1368,84 +1341,6 @@ class GroupDetailTest(LoginMixin, TestCase):
self.assertEqual(user_in_group - 1, self.g1.user_set.count())
self.assertEqual(response.status_code, 302)
def test_anon_remove_acluser_from_group(self):
c = Client()
gp = self.g1.profile
acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/user/' + str(self.u4.pk) + '/')
self.assertEqual(acl_users, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302)
def test_unpermitted_remove_acluser_from_group(self):
c = Client()
self.login(c, 'user3')
gp = self.g1.profile
acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/user/' + str(self.u4.pk) + '/')
self.assertEqual(acl_users, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 403)
def test_superuser_remove_acluser_from_group(self):
c = Client()
gp = self.g1.profile
self.login(c, 'superuser')
acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/user/' + str(self.u4.pk) + '/')
self.assertEqual(acl_users - 1, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302)
def test_permitted_remove_acluser_from_group(self):
c = Client()
gp = self.g1.profile
self.login(c, 'user0')
acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/user/' + str(self.u4.pk) + '/')
self.assertEqual(acl_users - 1, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302)
def test_anon_remove_aclgroup_from_group(self):
c = Client()
gp = self.g1.profile
acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/group/' + str(self.g3.pk) + '/')
self.assertEqual(acl_groups, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302)
def test_unpermitted_remove_aclgroup_from_group(self):
c = Client()
self.login(c, 'user3')
gp = self.g1.profile
acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/group/' + str(self.g3.pk) + '/')
self.assertEqual(acl_groups, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 403)
def test_superuser_remove_aclgroup_from_group(self):
c = Client()
gp = self.g1.profile
acl_groups = len(gp.get_groups_with_level())
self.login(c, 'superuser')
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/group/' + str(self.g3.pk) + '/')
self.assertEqual(acl_groups - 1, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302)
def test_permitted_remove_aclgroup_from_group(self):
c = Client()
gp = self.g1.profile
acl_groups = len(gp.get_groups_with_level())
self.login(c, 'user0')
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/group/' + str(self.g3.pk) + '/')
self.assertEqual(acl_groups - 1, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302)
def test_unpermitted_user_add_wo_group_perm(self):
user_count = self.g1.user_set.count()
c = Client()
......@@ -1774,8 +1669,8 @@ class AclViewTest(LoginMixin, TestCase):
resp = c.post("/dashboard/vm/1/acl/", {
'remove-u-%d' % self.u1.pk: "",
'perm-new-name': "",
'perm-new': "",
'name': "",
'level': "",
})
self.assertFalse((self.u1, "user") in inst.get_users_with_level())
self.assertEqual(resp.status_code, 302)
......@@ -1788,11 +1683,11 @@ class AclViewTest(LoginMixin, TestCase):
resp = c.post("/dashboard/vm/1/acl/", {
'remove-u-%d' % self.u1.pk: "",
'perm-new-name': "",
'perm-new': "",
'name': "",
'level': "",
})
self.assertTrue((self.u1, "user") in inst.get_users_with_level())
self.assertEqual(resp.status_code, 403)
self.assertEqual(resp.status_code, 302)
def test_instance_original_owner_access_revoke(self):
c = Client()
......@@ -1802,8 +1697,8 @@ class AclViewTest(LoginMixin, TestCase):
inst.set_level(self.ut, "owner")
resp = c.post("/dashboard/vm/1/acl/", {
'remove-u-%d' % self.ut.pk: "",
'perm-new-name': "",
'perm-new': "",
'name': "",
'level': "",
})
self.assertEqual(self.ut, Instance.objects.get(id=1).owner)
self.assertTrue((self.ut, "owner") in inst.get_users_with_level())
......@@ -1818,8 +1713,8 @@ class AclViewTest(LoginMixin, TestCase):
resp = c.post("/dashboard/template/1/acl/", {
'remove-u-%d' % self.u1.pk: "",
'perm-new-name': "",
'perm-new': "",
'name': "",
'level': "",
})
self.assertFalse((self.u1, "user") in tmpl.get_users_with_level())
self.assertEqual(resp.status_code, 302)
......@@ -1832,11 +1727,11 @@ class AclViewTest(LoginMixin, TestCase):
resp = c.post("/dashboard/template/1/acl/", {
'remove-u-%d' % self.u1.pk: "",
'perm-new-name': "",
'perm-new': "",
'name': "",
'level': "",
})
self.assertTrue((self.u1, "user") in tmpl.get_users_with_level())
self.assertEqual(resp.status_code, 403)
self.assertEqual(resp.status_code, 302)
def test_template_original_owner_access_revoke(self):
c = Client()
......@@ -1846,8 +1741,8 @@ class AclViewTest(LoginMixin, TestCase):
tmpl.set_level(self.ut, "owner")
resp = c.post("/dashboard/template/1/acl/", {
'remove-u-%d' % self.ut.pk: "",
'perm-new-name': "",
'perm-new': "",
'name': "",
'level': "",
})
self.assertEqual(self.ut, InstanceTemplate.objects.get(id=1).owner)
self.assertTrue((self.ut, "owner") in tmpl.get_users_with_level())
......
......@@ -18,6 +18,7 @@
from __future__ import absolute_import
from django.conf.urls import patterns, url, include
import autocomplete_light
from vm.models import Instance
from .views import (
AclUpdateView, FavouriteView, GroupAclUpdateView, GroupDelete,
......@@ -27,10 +28,10 @@ from .views import (
NodeDetailView, NodeFlushView, NodeGraphView, NodeList, NodeStatus,
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
TransferOwnershipView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveAclUserView, GroupRemoveAclGroupView, GroupRemoveUserView,
GroupRemoveUserView,
GroupRemoveFutureUserView,
GroupCreate, GroupProfileUpdate,
TemplateChoose,
......@@ -45,7 +46,10 @@ from .views import (
LeaseAclUpdateView,
)
autocomplete_light.autodiscover()
urlpatterns = patterns(
'',
url(r'^$', IndexView.as_view(), name="dashboard.index"),
url(r'^lease/(?P<pk>\d+)/$', LeaseDetail.as_view(),
......@@ -84,8 +88,6 @@ urlpatterns = patterns(
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(),
name='dashboard.views.vm-create'),
url(r'^vm/delete/(?P<pk>\d+)/$', VmDelete.as_view(),
name="dashboard.views.delete-vm"),
url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity),
......@@ -153,12 +155,6 @@ urlpatterns = patterns(
name="dashboard.views.profile"),
url(r'^profile/(?P<username>[^/]+)/use_gravatar/$', toggle_use_gravatar),
url(r'^group/(?P<group_pk>\d+)/remove/acl/user/(?P<member_pk>\d+)/$',
GroupRemoveAclUserView.as_view(),
name="dashboard.views.remove-acluser"),
url(r'^group/(?P<group_pk>\d+)/remove/acl/group/(?P<member_pk>\d+)/$',
GroupRemoveAclGroupView.as_view(),
name="dashboard.views.remove-aclgroup"),
url(r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$',
GroupRemoveUserView.as_view(),
name="dashboard.views.remove-user"),
......@@ -184,6 +180,8 @@ urlpatterns = patterns(
UserKeyCreate.as_view(),
name="dashboard.views.userkey-create"),
url(r'^autocomplete/', include('autocomplete_light.urls')),
url(r"^store/list/$", StoreList.as_view(),
name="dashboard.views.store-list"),
url(r"^store/download/$", store_download,
......
......@@ -21,7 +21,11 @@ from __future__ import unicode_literals, absolute_import
from collections import OrderedDict
from itertools import chain
from os import getenv
<<<<<<< HEAD
from os.path import join, normpath, dirname, basename
=======
from urlparse import urljoin
>>>>>>> master
import json
import logging
import re
......@@ -30,8 +34,11 @@ import requests
from django.conf import settings
from django.contrib.auth.models import User, Group
from django.contrib.auth.views import login, redirect_to_login
<<<<<<< HEAD
from django.contrib.auth.decorators import login_required
from django.contrib.messages import warning
=======
>>>>>>> master
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import (
PermissionDenied, SuspiciousOperation,
......@@ -39,7 +46,7 @@ from django.core.exceptions import (
from django.core.cache import get_cache
from django.core import signing
from django.core.urlresolvers import reverse, reverse_lazy
from django.db.models import Count
from django.db.models import Count, Q
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import (
redirect, render, get_object_or_404, render_to_response,
......@@ -59,6 +66,7 @@ from django_tables2 import SingleTableView
from braces.views import (LoginRequiredMixin, SuperuserRequiredMixin,
PermissionRequiredMixin)
from braces.views._access import AccessMixin
from celery.exceptions import TimeoutError
from django_sshkey.models import UserKey
......@@ -68,13 +76,15 @@ from .forms import (
UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
VmSaveForm, UserKeyForm, VmRenewForm,
CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
TraitsForm, RawDataForm, GroupPermissionForm
TraitsForm, RawDataForm, GroupPermissionForm, AclUserAddForm,
VmAddInterfaceForm,
)
from .tables import (
NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable,
GroupListTable, UserKeyListTable
)
from common.models import HumanReadableObject, HumanReadableException
from vm.models import (
Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
InterfaceTemplate, Lease, Node, NodeActivity, Trait,
......@@ -248,27 +258,6 @@ class IndexView(LoginRequiredMixin, TemplateView):
return context
def get_vm_acl_data(obj):
levels = obj.ACL_LEVELS
users = obj.get_users_with_level()
users = [{'user': u, 'level': l} for u, l in users]
groups = obj.get_groups_with_level()
groups = [{'group': g, 'level': l} for g, l in groups]
return {'users': users, 'groups': groups, 'levels': levels,
'url': reverse('dashboard.views.vm-acl', args=[obj.pk])}
def get_group_acl_data(obj):
aclobj = obj.profile
levels = aclobj.ACL_LEVELS
users = aclobj.get_users_with_level()
users = [{'user': u, 'level': l} for u, l in users]
groups = aclobj.get_groups_with_level()
groups = [{'group': g, 'level': l} for g, l in groups]
return {'users': users, 'groups': groups, 'levels': levels,
'url': reverse('dashboard.views.group-acl', args=[obj.pk])}
class CheckedDetailView(LoginRequiredMixin, DetailView):
read_level = 'user'
......@@ -323,8 +312,11 @@ class VmDetailView(CheckedDetailView):
})
# activity data
context['activities'] = self.object.get_merged_activities(
self.request.user)
activities = instance.get_merged_activities(self.request.user)
show_show_all = len(activities) > 10
activities = activities[:10]
context['activities'] = activities
context['show_show_all'] = show_show_all
context['vlans'] = Vlan.get_objects_with_level(
'user', self.request.user
......@@ -332,7 +324,9 @@ class VmDetailView(CheckedDetailView):
pk__in=Interface.objects.filter(
instance=self.get_object()).values_list("vlan", flat=True)
).all()
context['acl'] = get_vm_acl_data(instance)
context['acl'] = AclUpdateView.get_acl_data(
instance, self.request.user, 'dashboard.views.vm-acl')
context['aclform'] = AclUserAddForm()
context['os_type_icon'] = instance.os_type.replace("unknown",
"question")
# ipv6 infos
......@@ -352,33 +346,19 @@ class VmDetailView(CheckedDetailView):
def post(self, request, *args, **kwargs):
options = {
'change_password': self.__change_password,
'new_name': self.__set_name,
'new_description': self.__set_description,
'new_tag': self.__add_tag,
'to_remove': self.__remove_tag,
'port': self.__add_port,
'new_network_vlan': self.__new_network,
'abort_operation': self.__abort_operation,
}
for k, v in options.iteritems():
if request.POST.get(k) is not None:
return v(request)
raise Http404()
def __change_password(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
self.object.change_password(user=request.user)
messages.success(request, _("Password changed."))
if request.is_ajax():
return HttpResponse("Success.")
else:
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.object.pk}))
raise Http404()
def __set_name(self, request):
self.object = self.get_object()
......@@ -502,24 +482,6 @@ class VmDetailView(CheckedDetailView):
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.get_object().pk}))
def __new_network(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
vlan = get_object_or_404(Vlan, pk=request.POST.get("new_network_vlan"))
if not vlan.has_level(request.user, 'user'):
raise PermissionDenied()
try:
self.object.add_interface(vlan=vlan, user=request.user)
messages.success(request, _("Successfully added new interface."))
except Exception, e:
error = u' '.join(e.messages)
messages.error(request, error)
return redirect("%s#network" % reverse_lazy(
"dashboard.views.detail", kwargs={'pk': self.object.pk}))
def __abort_operation(self, request):
self.object = self.get_object()
......@@ -542,6 +504,7 @@ class VmTraitsUpdate(SuperuserRequiredMixin, UpdateView):
class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
form_class = RawDataForm
model = Instance
template_name = 'dashboard/vm-detail/raw_data.html'
def get_success_url(self):
return self.get_object().get_absolute_url() + "#resources"
......@@ -552,6 +515,7 @@ class OperationView(RedirectToLoginMixin, DetailView):
template_name = 'dashboard/operate.html'
show_in_toolbar = True
effect = None
wait_for_result = None
@property
def name(self):
......@@ -613,18 +577,64 @@ class OperationView(RedirectToLoginMixin, DetailView):
self.check_auth()
return super(OperationView, self).get(request, *args, **kwargs)
def get_response_data(self, result, done, extra=None, **kwargs):
"""Return serializable data to return to agents requesting json
response to POST"""
if extra is None:
extra = {}
extra["success"] = not isinstance(result, Exception)
extra["done"] = done
if isinstance(result, HumanReadableObject):
extra["message"] = result.get_user_text()
return extra
def post(self, request, extra=None, *args, **kwargs):
self.check_auth()
self.object = self.get_object()
if extra is None:
extra = {}
result = None
done = False
try:
self.get_op().async(user=request.user, **extra)
task = self.get_op().async(user=request.user, **extra)
except HumanReadableException as e:
e.send_message(request)
logger.exception("Could not start operation")
result = e
except Exception as e:
messages.error(request, _('Could not start operation.'))
logger.exception("Could not start operation")
result = e
else:
wait = self.wait_for_result
if wait:
try:
result = task.get(timeout=wait,
interval=min((wait / 5, .5)))
except TimeoutError:
logger.debug("Result didn't arrive in %ss",
self.wait_for_result, exc_info=True)
except HumanReadableException as e:
e.send_message(request)
logger.exception(e)
result = e
except Exception as e:
messages.error(request, _('Operation failed.'))
logger.debug("Operation failed.", exc_info=True)
result = e
else:
done = True
messages.success(request, _('Operation succeeded.'))
if result is None and not done:
messages.success(request, _('Operation is started.'))
if "/json" in request.META.get("HTTP_ACCEPT", ""):
data = self.get_response_data(result, done,
post_extra=extra, **kwargs)
return HttpResponse(json.dumps(data),
content_type="application/json")
else:
return redirect("%s#activity" % self.object.get_absolute_url())
@classmethod
......@@ -652,6 +662,7 @@ class AjaxOperationMixin(object):
store.used = True
return HttpResponse(
json.dumps({'success': True,
'with_reload': getattr(self, 'with_reload', False),
'messages': [unicode(m) for m in store]}),
content_type="application=json"
)
......@@ -691,7 +702,9 @@ class FormOperationMixin(object):
request, extra, *args, **kwargs)
if request.is_ajax():
return HttpResponse(
json.dumps({'success': True}),
json.dumps({
'success': True,
'with_reload': getattr(self, 'with_reload', False)}),
content_type="application=json"
)
else:
......@@ -708,12 +721,32 @@ class RequestFormOperationMixin(FormOperationMixin):
return val
class VmAddInterfaceView(FormOperationMixin, VmOperationView):
op = 'add_interface'
form_class = VmAddInterfaceForm
show_in_toolbar = False
icon = 'globe'
effect = 'success'
with_reload = True
def get_form_kwargs(self):
inst = self.get_op().instance
choices = Vlan.get_objects_with_level(
"user", self.request.user).exclude(
vm_interface__instance__in=[inst])
val = super(VmAddInterfaceView, self).get_form_kwargs()
val.update({'choices': choices})
return val
class VmCreateDiskView(FormOperationMixin, VmOperationView):
op = 'create_disk'
form_class = VmCreateDiskForm
show_in_toolbar = False
icon = 'hdd-o'
effect = "success"
is_disk_operation = True
......@@ -723,6 +756,7 @@ class VmDownloadDiskView(FormOperationMixin, VmOperationView):
form_class = VmDownloadDiskForm
show_in_toolbar = False
icon = 'download'
effect = "success"
is_disk_operation = True
......@@ -820,6 +854,7 @@ class TokenOperationView(OperationView):
logger.info("Request user changed to %s at %s",
user, self.request.get_full_path())
self.request.user = user
self.request.token_user = True
else:
logger.debug("no token supplied to %s",
self.request.get_full_path())
......@@ -862,6 +897,7 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
effect = 'info'
show_in_toolbar = False
form_class = VmRenewForm
wait_for_result = 0.5
def get_form_kwargs(self):
choices = Lease.get_objects_with_level("user", self.request.user)
......@@ -874,6 +910,13 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
val.update({'choices': choices, 'default': default})
return val
def get_response_data(self, result, done, extra=None, **kwargs):
extra = super(VmRenewView, self).get_response_data(result, done,
extra, **kwargs)
extra["new_suspend_time"] = unicode(self.get_op().
instance.time_of_suspend)
return extra
vm_ops = OrderedDict([
('deploy', VmOperationView.factory(
......@@ -895,13 +938,19 @@ vm_ops = OrderedDict([
op='shut_off', icon='ban', effect='warning')),
('recover', VmOperationView.factory(
op='recover', icon='medkit', effect='warning')),
('nostate', VmOperationView.factory(
op='emergency_change_state', icon='legal', effect='danger')),
('destroy', VmOperationView.factory(
extra_bases=[TokenOperationView],
op='destroy', icon='times', effect='danger')),
('create_disk', VmCreateDiskView),
('download_disk', VmDownloadDiskView),
('add_interface', VmAddInterfaceView),
('renew', VmRenewView),
('resources_change', VmResourcesChangeView),
('password_reset', VmOperationView.factory(
op='password_reset', icon='unlock', effect='warning',
show_in_toolbar=False, wait_for_result=0.5, with_reload=True)),
])
......@@ -1005,7 +1054,10 @@ class GroupDetailView(CheckedDetailView):
context['users'] = self.object.user_set.all()
context['future_users'] = FutureMember.objects.filter(
group=self.object)
context['acl'] = get_group_acl_data(self.object)
context['acl'] = AclUpdateView.get_acl_data(
self.object.profile, self.request.user,
'dashboard.views.group-acl')
context['aclform'] = AclUserAddForm()
context['group_profile_form'] = GroupProfileUpdate.get_form_object(
self.request, self.object.profile)
......@@ -1048,7 +1100,7 @@ class GroupDetailView(CheckedDetailView):
FutureMember.objects.get_or_create(org_id=name,
group=self.object)
else:
warning(request, _('User "%s" not found.') % name)
messages.warning(request, _('User "%s" not found.') % name)
def __add_list(self, request):
if not self.get_has_level()(request.user, 'operator'):
......@@ -1093,120 +1145,169 @@ class GroupPermissionsView(SuperuserRequiredMixin, UpdateView):
class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
def send_success_message(self, whom, old_level, new_level):
if old_level and new_level:
msg = _("Acl user/group %(w)s successfully modified.")
elif not old_level and new_level:
msg = _("Acl user/group %(w)s successfully added.")
elif old_level and not new_level:
msg = _("Acl user/group %(w)s successfully removed.")
if msg:
messages.success(self.request, msg % {'w': whom})
def get_level(self, whom):
for u, level in self.acl_data:
if u == whom:
return level
return None
def post(self, request, *args, **kwargs):
instance = self.get_object()
if not (instance.has_level(request.user, "owner") or
getattr(instance, 'owner', None) == request.user):
logger.warning('Tried to set permissions of %s by non-owner %s.',
unicode(instance), unicode(request.user))
raise PermissionDenied()
self.set_levels(request, instance)
self.remove_levels(request, instance)
self.add_levels(request, instance)
return redirect("%s#access" % instance.get_absolute_url())
def set_levels(self, request, instance):
for key, value in request.POST.items():
m = re.match('perm-([ug])-(\d+)', key)
if m:
typ, id = m.groups()
entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
if getattr(instance, "owner", None) == entity:
@classmethod
def get_acl_data(cls, obj, user, url):
levels = obj.ACL_LEVELS
allowed_levels = list(l for l in OrderedDict(levels)
if cls.has_next_level(user, obj, l))
is_owner = 'owner' in allowed_levels
allowed_users = cls.get_allowed_users(user)
allowed_groups = cls.get_allowed_groups(user)
user_levels = list(
{'user': u, 'level': l} for u, l in obj.get_users_with_level()
if is_owner or u == user or u in allowed_users)
group_levels = list(
{'group': g, 'level': l} for g, l in obj.get_groups_with_level()
if is_owner or g in allowed_groups)
return {'users': user_levels,
'groups': group_levels,
'levels': levels,
'allowed_levels': allowed_levels,
'url': reverse(url, args=[obj.pk])}
@classmethod
def has_next_level(self, user, instance, level):
levels = OrderedDict(instance.ACL_LEVELS).keys()
next_levels = dict(zip([None] + levels, levels + levels[-1:]))
# {None: 'user', 'user': 'operator', 'operator: 'owner',
# 'owner: 'owner'}
next_level = next_levels[level]
return instance.has_level(user, next_level)
@classmethod
def get_allowed_groups(cls, user):
if user.has_perm('dashboard.use_autocomplete'):
return Group.objects.all()
else:
profiles = GroupProfile.get_objects_with_level('owner', user)
return Group.objects.filter(groupprofile__in=profiles).distinct()
@classmethod
def get_allowed_users(cls, user):
if user.has_perm('dashboard.use_autocomplete'):
return User.objects.all()
else:
groups = cls.get_allowed_groups(user)
return User.objects.filter(
Q(groups__in=groups) | Q(pk=user.pk)).distinct()
def check_auth(self, whom, old_level, new_level):
if isinstance(whom, Group):
if (not self.is_owner and whom not in
AclUpdateView.get_allowed_groups(self.request.user)):
return False
elif isinstance(whom, User):
if (not self.is_owner and whom not in
AclUpdateView.get_allowed_users(self.request.user)):
return False
return (
AclUpdateView.has_next_level(self.request.user,
self.instance, new_level) and
AclUpdateView.has_next_level(self.request.user,
self.instance, old_level))
def set_level(self, whom, new_level):
user = self.request.user
old_level = self.get_level(whom)
if old_level == new_level:
return
if getattr(self.instance, "owner", None) == whom:
logger.info("Tried to set owner's acl level for %s by %s.",
unicode(instance), unicode(request.user))
continue
instance.set_level(entity, value)
logger.info("Set %s's acl level for %s to %s by %s.",
unicode(entity), unicode(instance),
value, unicode(request.user))
def remove_levels(self, request, instance):
for key, value in request.POST.items():
if key.startswith("remove"):
typ = key[7:8] # len("remove-")
id = key[9:] # len("remove-x-")
entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
if getattr(instance, "owner", None) == entity:
logger.info("Tried to remove owner from %s by %s.",
unicode(instance), unicode(request.user))
unicode(self.instance), unicode(user))
msg = _("The original owner cannot be removed, however "
"you can transfer ownership.")
messages.warning(request, msg)
continue
instance.set_level(entity, None)
logger.info("Revoked %s's access to %s by %s.",
unicode(entity), unicode(instance),
unicode(request.user))
if not getattr(self, 'hide_messages', False):
messages.warning(self.request, msg)
elif self.check_auth(whom, old_level, new_level):
logger.info(
u"Set %s's acl level for %s to %s by %s.", unicode(whom),
unicode(self.instance), new_level, unicode(user))
if not getattr(self, 'hide_messages', False):
self.send_success_message(whom, old_level, new_level)
self.instance.set_level(whom, new_level)
else:
logger.warning(
u"Tried to set %s's acl_level for %s (%s->%s) by %s.",
unicode(whom), unicode(self.instance), old_level, new_level,
unicode(user))
def set_or_remove_levels(self):
for key, value in self.request.POST.items():
m = re.match('(perm|remove)-([ug])-(\d+)', key)
if m:
cmd, typ, id = m.groups()
if cmd == 'remove':
value = None
entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
self.set_level(entity, value)
def add_levels(self, request, instance):
name = request.POST['perm-new-name']
value = request.POST['perm-new']
if not name:
def add_levels(self):
name = self.request.POST.get('name', None)
level = self.request.POST.get('level', None)
if not name or not level:
return
try:
entity = search_user(name)
if self.instance.object_level_set.filter(users__in=[entity]):
messages.warning(
self.request, _('User "%s" has already '
'access to this object.') % name)
return
except User.DoesNotExist:
entity = None
try:
entity = Group.objects.get(name=name)
if self.instance.object_level_set.filter(groups__in=[entity]):
messages.warning(
self.request, _('Group "%s" has already '
'access to this object.') % name)
return
except Group.DoesNotExist:
warning(request, _('User or group "%s" not found.') % name)
messages.warning(
self.request, _('User or group "%s" not found.') % name)
return
self.set_level(entity, level)
instance.set_level(entity, value)
logger.info("Set %s's new acl level for %s to %s by %s.",
unicode(entity), unicode(instance),
value, unicode(request.user))
def post(self, request, *args, **kwargs):
self.instance = self.get_object()
self.is_owner = self.instance.has_level(request.user, 'owner')
self.acl_data = (self.instance.get_users_with_level() +
self.instance.get_groups_with_level())
self.set_or_remove_levels()
self.add_levels()
return redirect("%s#access" % self.instance.get_absolute_url())
class TemplateAclUpdateView(AclUpdateView):
model = InstanceTemplate
def post(self, request, *args, **kwargs):
template = self.get_object()
if not (template.has_level(request.user, "owner") or
getattr(template, 'owner', None) == request.user):
logger.warning('Tried to set permissions of %s by non-owner %s.',
unicode(template), unicode(request.user))
raise PermissionDenied()
name = request.POST['perm-new-name']
if (User.objects.filter(username=name).count() +
Group.objects.filter(name=name).count() < 1
and len(name) > 0):
warning(request, _('User or group "%s" not found.') % name)
else:
self.set_levels(request, template)
self.add_levels(request, template)
self.remove_levels(request, template)
post_for_disk = request.POST.copy()
post_for_disk['perm-new'] = 'user'
request.POST = post_for_disk
for d in template.disks.all():
self.set_levels(request, d)
self.add_levels(request, d)
self.remove_levels(request, d)
return redirect(template)
class GroupAclUpdateView(AclUpdateView):
model = Group
def post(self, request, *args, **kwargs):
instance = self.get_object().profile
if not (instance.has_level(request.user, "owner") or
getattr(instance, 'owner', None) == request.user):
logger.warning('Tried to set permissions of %s by non-owner %s.',
unicode(instance), unicode(request.user))
raise PermissionDenied()
self.set_levels(request, instance)
self.add_levels(request, instance)
return redirect(reverse("dashboard.views.group-detail",
kwargs=self.kwargs))
def get_object(self):
return super(GroupAclUpdateView, self).get_object().profile
class TemplateChoose(LoginRequiredMixin, TemplateView):
......@@ -1353,8 +1454,11 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
def get_context_data(self, **kwargs):
obj = self.get_object()
context = super(TemplateDetail, self).get_context_data(**kwargs)
context['acl'] = get_vm_acl_data(obj)
context['acl'] = AclUpdateView.get_acl_data(
obj, self.request.user, 'dashboard.views.template-acl')
context['disks'] = obj.disks.all()
context['is_owner'] = obj.has_level(self.request.user, 'owner')
context['aclform'] = AclUserAddForm()
return context
def get_success_url(self):
......@@ -1653,34 +1757,6 @@ class GroupRemoveFutureUserView(GroupRemoveUserView):
return _("Future user successfully removed from group.")
class GroupRemoveAclUserView(GroupRemoveUserView):
def remove_member(self, pk):
container = self.get_object().profile
container.set_level(User.objects.get(pk=pk), None)
def get_success_message(self):
return _("Acl user successfully removed from group.")
class GroupRemoveAclGroupView(GroupRemoveUserView):
def get_context_data(self, **kwargs):
context = super(GroupRemoveUserView, self).get_context_data(**kwargs)
try:
context['member'] = Group.objects.get(pk=self.member_pk)
except User.DoesNotExist:
raise Http404()
return context
def remove_member(self, pk):
container = self.get_object().profile
container.set_level(Group.objects.get(pk=pk), None)
def get_success_message(self):
return _("Acl group successfully removed from group.")
class GroupDelete(CheckedDetailView, DeleteView):
"""This stuff deletes the group.
......@@ -1807,13 +1883,12 @@ class VmCreate(LoginRequiredMixin, TemplateView):
}
networks = [InterfaceTemplate(vlan=l, managed=l.managed)
for l in post['networks']]
disks = post['disks']
ikwargs.update({
'template': template,
'owner': user,
'networks': networks,
'disks': disks,
'disks': list(template.disks.all()),
})
amount = post['amount']
......@@ -1855,9 +1930,13 @@ class VmCreate(LoginRequiredMixin, TemplateView):
except Exception as e:
logger.debug('No profile or instance limit: %s', e)
else:
try:
amount = int(request.POST.get("amount", 1))
except:
amount = limit # TODO this should definitely use a Form
current = Instance.active.filter(owner=user).count()
logger.debug('current use: %d, limit: %d', current, limit)
if limit < current:
if current + amount > limit:
messages.error(request,
_('Instance limit (%d) exceeded.') % limit)
if request.is_ajax():
......@@ -2021,53 +2100,6 @@ class GroupProfileUpdate(SuccessMessageMixin, GroupCodeMixin,
return self.form_valid(form)
class VmDelete(LoginRequiredMixin, DeleteView):
model = Instance
template_name = "dashboard/confirm/base-delete.html"
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get_success_url(self):
next = self.request.POST.get('next')
if next:
return next
else:
return reverse_lazy('dashboard.index')
def get_context_data(self, **kwargs):
object = self.get_object()
if not object.has_level(self.request.user, 'owner'):
raise PermissionDenied()
context = super(VmDelete, self).get_context_data(**kwargs)
return context
# github.com/django/django/blob/master/django/views/generic/edit.py#L245
def delete(self, request, *args, **kwargs):
object = self.get_object()
if not object.has_level(request.user, 'owner'):
raise PermissionDenied()
object.destroy.async(user=request.user)
success_url = self.get_success_url()
success_message = _("VM successfully deleted.")
if request.is_ajax():
if request.POST.get('redirect').lower() == "true":
messages.success(request, success_message)
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class NodeDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
"""This stuff deletes the node.
......@@ -2345,7 +2377,8 @@ class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin,
def get_context_data(self, *args, **kwargs):
obj = self.get_object()
context = super(LeaseDetail, self).get_context_data(*args, **kwargs)
context['acl'] = get_vm_acl_data(obj)
context['acl'] = AclUpdateView.get_acl_data(
obj, self.request.user, 'dashboard.views.lease-acl')
return context
def get_success_url(self):
......@@ -2405,15 +2438,20 @@ def vm_activity(request, pk):
raise PermissionDenied()
response = {}
only_status = request.GET.get("only_status", "false")
show_all = request.GET.get("show_all", "false") == "true"
activities = instance.get_merged_activities(request.user)
show_show_all = len(activities) > 10
if not show_all:
activities = activities[:10]
response['human_readable_status'] = instance.get_status_display()
response['status'] = instance.status
response['icon'] = instance.get_status_icon()
if only_status == "false": # instance activity
context = {
'instance': instance,
'activities': instance.get_merged_activities(request.user),
'activities': activities,
'show_show_all': show_show_all,
'ops': get_operations(instance, request.user),
}
......@@ -2818,12 +2856,11 @@ class DiskRemoveView(DeleteView):
def delete(self, request, *args, **kwargs):
disk = self.get_object()
if not disk.has_level(request.user, 'owner'):
raise PermissionDenied()
disk = self.get_object()
app = disk.get_appliance()
if not app.has_level(request.user, 'owner'):
raise PermissionDenied()
app.remove_disk(disk=disk, user=request.user)
disk.destroy()
......@@ -2844,7 +2881,7 @@ class DiskRemoveView(DeleteView):
@require_GET
def get_disk_download_status(request, pk):
disk = Disk.objects.get(pk=pk)
if not disk.has_level(request.user, 'owner'):
if not disk.get_appliance().has_level(request.user, 'owner'):
raise PermissionDenied()
return HttpResponse(
......@@ -3249,3 +3286,7 @@ def store_refresh_toplist(request):
cache.set(cache_key, files, 300)
return redirect(reverse("dashboard.index"))
def absolute_url(url):
return urljoin(settings.DJANGO_URL, url)
from django.core.exceptions import ValidationError
from lxml import etree as ET
import logging
rng_file = "/usr/share/libvirt/schemas/domain.rng"
# Mandatory xml elements dor parsing
header = "<domain type='kvm'><name>validator</name>\
<memory unit='KiB'>1024</memory>\
<os><type>hvm</type></os>"
footer = "</domain>"
logger = logging.getLogger()
def domain_validator(value):
xml = header + value + footer
try:
parsed_xml = ET.fromstring(xml)
except Exception as e:
raise ValidationError(e.message)
try:
relaxng = ET.RelaxNG(file=rng_file)
except:
logger.critical("%s RelaxNG libvirt domain schema file "
"is missing for validation.", rng_file)
else:
try:
relaxng.assertValid(parsed_xml)
except Exception as e:
raise ValidationError(e.message)
#!/bin/echo Usage: fab --list -f
import contextlib
import datetime
from fabric.api import env, run, settings, sudo, prefix, cd, execute
from fabric.context_managers import shell_env
from fabric.decorators import roles, parallel
env.roledefs['portal'] = ['localhost']
try:
from vm.models import Node as _Node
from storage.models import DataStore as _DataStore
except Exception as e:
print e
else:
env.roledefs['node'] = [unicode(n.host.ipv4)
for n in _Node.objects.filter(enabled=True)]
env.roledefs['storage'] = [_DataStore.objects.get().hostname]
def update_all():
"Update and restart portal+manager, nodes and storage"
execute(stop_portal)
execute(parallel(update_node))
execute(update_storage)
execute(update_portal)
def pip(env, req):
"Install pip requirements"
with _workon(env):
run("pip install -r %s" % req)
@roles('portal')
def migrate():
"Run db migrations"
with _workon("circle"), cd("~/circle/circle"):
run("./manage.py migrate")
@roles('portal')
def compile_js():
"Generate JS translation objects"
with _workon("circle"), cd("~/circle/circle"):
run("./manage.py compilejsi18n -o dashboard/static/jsi18n")
@roles('portal')
def collectstatic():
"Collect static files"
with _workon("circle"), cd("~/circle/circle"):
run("./manage.py collectstatic --noinput")
@roles('portal')
def compile_messages():
"Generate MO translation objects"
with _workon("circle"), cd("~/circle/circle"):
run("./manage.py compilemessages")
@roles('portal')
def compile_things():
"Compile translation and collect static files"
compile_js()
collectstatic()
compile_messages()
@roles('portal')
def make_messages():
"Update PO translation templates and commit"
with _workon("circle"), cd("~/circle/circle"):
run("git status")
run("./manage.py makemessages -d djangojs -a --ignore=jsi18n/*")
run("./manage.py makemessages -d django -a")
run("git commit -avm 'update PO templates'")
@roles('portal')
def test(test=""):
"Run portal tests"
with _workon("circle"), cd("~/circle/circle"):
run("./manage.py test --settings=circle.settings.test %s" % test)
def pull(dir="~/circle/circle"):
"Pull from upstream branch (stash any changes)"
now = unicode(datetime.datetime.now())
with cd(dir), shell_env(GIT_AUTHOR_NAME="fabric",
GIT_AUTHOR_EMAIL="fabric@local",
GIT_COMMITTER_NAME="fabric",
GIT_COMMITTER_EMAIL="fabric@local"):
run("git stash save update %s" % now)
run("git pull --ff-only")
@roles('portal')
def update_portal(test=False):
"Update and restart portal+manager"
with _stopped("portal", "mancelery"):
pull()
pip("circle", "~/circle/requirements.txt")
migrate()
compile_things()
if test:
test()
@roles('portal')
def stop_portal(test=False):
"Stop portal and manager"
_stop_services("portal", "mancelery")
@roles('node')
def update_node():
"Update and restart nodes"
with _stopped("node", "agentdriver"):
pull("~/vmdriver")
pip("vmdriver", "~/vmdriver/requirements/production.txt")
pull("~/agentdriver")
pip("agentdriver", "~/agentdriver/requirements.txt")
@parallel
@roles('storage')
def update_storage():
"Update and restart storagedriver"
with _stopped("storage"):
pull("~/storagedriver")
pip("storagedriver", "~/storagedriver/requirements/production.txt")
@parallel
@roles('node')
def checkout(vmdriver="master", agent="master"):
"""Checkout specific branch on nodes"""
with settings(warn_only=True), cd("~/vmdriver"):
run("git checkout %s" % vmdriver)
with settings(warn_only=True), cd("~/agentdriver"):
run("git checkout %s" % agent)
def _stop_services(*services):
"Stop given services (warn only if not running)"
with settings(warn_only=True):
for service in reversed(services):
sudo("stop %s" % service)
def _start_services(*services):
for service in services:
sudo("start %s" % service)
def _restart_service(*services):
"Stop and start services"
_stop_services(*services)
_start_services(*services)
@contextlib.contextmanager
def _stopped(*services):
_stop_services(*services)
yield
_start_services(*services)
def _workon(name):
return prefix("source ~/.virtualenvs/%s/bin/activate && "
"source ~/.virtualenvs/%s/bin/postactivate" % (name, name))
[General]
LangCode=hu
MailingList=cloud@ik.bme.hu
PotBaseDir=./
ProjectID=circle-hu
TargetLangCode=hu
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-05-07 14:25+0200\n"
"POT-Creation-Date: 2014-07-29 12:56+0200\n"
"PO-Revision-Date: 2014-05-07 15:32+0200\n"
"Last-Translator: Mate Ory <orymate@ik.bme.hu>\n"
"Language-Team: Hungarian <cloud@ik.bme.hu>\n"
......@@ -17,105 +17,144 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Lokalize 1.5\n"
#: dashboard/static/dashboard/dashboard.js:54
#: dashboard/static/dashboard/dashboard.js:68
#: static_collected/dashboard/dashboard.js:68
msgid "Select an option to proceed!"
msgstr "Válasszon a folytatáshoz."
#: dashboard/static/dashboard/dashboard.js:257
#: dashboard/static/dashboard/dashboard.js:304
#: dashboard/static/dashboard/dashboard.js:314
#: static_collected/dashboard/dashboard.js:257
#: static_collected/dashboard/dashboard.js:304
#: static_collected/dashboard/dashboard.js:314
msgid "No result"
msgstr ""
#: dashboard/static/dashboard/profile.js:18
#: static_collected/dashboard/profile.js:18
msgid "You have no permission to change this profile."
msgstr ""
#: dashboard/static/dashboard/profile.js:20
#: static_collected/dashboard/profile.js:20
msgid "Unknown error."
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:20
#: static_collected/dashboard/vm-tour.js:20
msgid "Prev"
msgstr "Vissza"
#: dashboard/static/dashboard/vm-tour.js:22
#: static_collected/dashboard/vm-tour.js:22
msgid "Next"
msgstr "Tovább"
#: dashboard/static/dashboard/vm-tour.js:26
#: static_collected/dashboard/vm-tour.js:26
msgid "End tour"
msgstr "Befejezés"
#: dashboard/static/dashboard/vm-tour.js:33
#: static_collected/dashboard/vm-tour.js:33
msgid "Template Tutorial Tour"
msgstr "Sablon-kalauz"
#: dashboard/static/dashboard/vm-tour.js:34
#: static_collected/dashboard/vm-tour.js:34
msgid ""
"Welcome to the template tutorial. In this quick tour, we gonna show you how "
"to do the steps described above."
msgstr ""
"Üdvözöli a sablon-kalauz. A túra során bemutatjuk, hogyan végezze el "
"a fenti lépéseket."
"Üdvözöli a sablon-kalauz. A túra során bemutatjuk, hogyan végezze el a fenti "
"lépéseket."
#: dashboard/static/dashboard/vm-tour.js:35
#: static_collected/dashboard/vm-tour.js:35
msgid ""
"For the next tour step press the \"Next\" button or the right arrow (or "
"\"Back\" button/left arrow for the previous step)."
msgstr ""
"A következő lépéshez kattintson a \"Tovább\" gombra vagy használja "
"a nyílbillentyűket."
"A következő lépéshez kattintson a \"Tovább\" gombra vagy használja a "
"nyílbillentyűket."
#: dashboard/static/dashboard/vm-tour.js:36
#: static_collected/dashboard/vm-tour.js:36
msgid ""
"During the tour please don't try the functions because it may lead to "
"graphical glitches, however "
msgstr "A túra során még ne próbálja ki a bemutatott funkciókat."
#: dashboard/static/dashboard/vm-tour.js:45
#: static_collected/dashboard/vm-tour.js:45
msgid "Home tab"
msgstr "Kezdőoldal"
#: dashboard/static/dashboard/vm-tour.js:46
#: static_collected/dashboard/vm-tour.js:46
msgid ""
"In this tab you can tag your virtual machine and modify the name and "
"description."
msgstr ""
"Ezen a lapon címkéket adhat a virtuális géphez, vagy módosíthatja "
"a nevét, leírását."
"Ezen a lapon címkéket adhat a virtuális géphez, vagy módosíthatja a nevét, "
"leírását."
#: dashboard/static/dashboard/vm-tour.js:55
#: static_collected/dashboard/vm-tour.js:55
msgid "Resources tab"
msgstr "Erőforrások lap"
#: dashboard/static/dashboard/vm-tour.js:58
#: static_collected/dashboard/vm-tour.js:58
msgid ""
"On the resources tab you can edit the CPU/RAM options and add/remove disks!"
msgstr ""
"Az erőforrások lapon szerkesztheti a CPU/memória-beállításokat, valamint "
"hozzáadhat "
"és törölhet lemezeket."
"hozzáadhat és törölhet lemezeket."
#: dashboard/static/dashboard/vm-tour.js:68
#: static_collected/dashboard/vm-tour.js:68
msgid "Resources"
msgstr "Erőforrások"
#: dashboard/static/dashboard/vm-tour.js:69
#: static_collected/dashboard/vm-tour.js:69
msgid "CPU priority"
msgstr "CPU prioritás"
#: dashboard/static/dashboard/vm-tour.js:69
#: static_collected/dashboard/vm-tour.js:69
msgid "higher is better"
msgstr "a nagyobb érték a jobb"
#: dashboard/static/dashboard/vm-tour.js:70
#: static_collected/dashboard/vm-tour.js:70
msgid "CPU count"
msgstr "CPU-k száma"
#: dashboard/static/dashboard/vm-tour.js:70
#: static_collected/dashboard/vm-tour.js:70
msgid "number of CPU cores."
msgstr "A CPU-magok száma."
#: dashboard/static/dashboard/vm-tour.js:71
#: static_collected/dashboard/vm-tour.js:71
msgid "RAM amount"
msgstr "RAM mennyiség"
#: dashboard/static/dashboard/vm-tour.js:71
#: static_collected/dashboard/vm-tour.js:71
msgid "amount of RAM."
msgstr "a memória mennyisége."
#: dashboard/static/dashboard/vm-tour.js:81
#: static_collected/dashboard/vm-tour.js:81
msgid "Disks"
msgstr "Lemezek"
#: dashboard/static/dashboard/vm-tour.js:82
#: static_collected/dashboard/vm-tour.js:82
msgid ""
"You can add empty disks, download new ones and remove existing ones here."
msgstr ""
......@@ -123,65 +162,75 @@ msgstr ""
"meglévőket."
#: dashboard/static/dashboard/vm-tour.js:92
#: static_collected/dashboard/vm-tour.js:92
msgid "Network tab"
msgstr "Hálózat lap"
#: dashboard/static/dashboard/vm-tour.js:93
#: static_collected/dashboard/vm-tour.js:93
msgid "You can add new network interfaces or remove existing ones here."
msgstr "Hozzáadhat új hálózati interfészeket, vagy törölheti a meglévőket."
#: dashboard/static/dashboard/vm-tour.js:102
#: static_collected/dashboard/vm-tour.js:102
msgid "Deploy"
msgstr "Indítás"
#: dashboard/static/dashboard/vm-tour.js:105
#: static_collected/dashboard/vm-tour.js:105
msgid "Deploy the virtual machine."
msgstr "A virtuális gép elindítása."
#: dashboard/static/dashboard/vm-tour.js:110
#: static_collected/dashboard/vm-tour.js:110
msgid "Connect"
msgstr "Csatlakozás"
#: dashboard/static/dashboard/vm-tour.js:113
#: static_collected/dashboard/vm-tour.js:113
msgid "Use the connection string or connect with your choice of client!"
msgstr "Használja a megadott parancsot, vagy kedvenc kliensét."
#: dashboard/static/dashboard/vm-tour.js:120
#: static_collected/dashboard/vm-tour.js:120
msgid "Customize the virtual machine"
msgstr "Szabja testre a gépet"
#: dashboard/static/dashboard/vm-tour.js:121
#: static_collected/dashboard/vm-tour.js:121
msgid ""
"After you have connected to the virtual machine do your modifications then "
"log off."
msgstr ""
"Miután csatlakozott, végezze el a szükséges módosításokat, majd "
"jelentkezzen ki."
"Miután csatlakozott, végezze el a szükséges módosításokat, majd jelentkezzen "
"ki."
#: dashboard/static/dashboard/vm-tour.js:126
#: static_collected/dashboard/vm-tour.js:126
msgid "Save as"
msgstr "Mentés sablonként"
#: dashboard/static/dashboard/vm-tour.js:129
#: static_collected/dashboard/vm-tour.js:129
msgid ""
"Press the \"Save as template\" button and wait until the activity finishes."
msgstr ""
"Kattintson a „mentés sablonként” gombra, majd várjon, amíg a lemez "
"mentése elkészül."
"Kattintson a „mentés sablonként” gombra, majd várjon, amíg a lemez mentése "
"elkészül."
#: dashboard/static/dashboard/vm-tour.js:135
#: static_collected/dashboard/vm-tour.js:135
msgid "Finish"
msgstr "Befejezés"
#: dashboard/static/dashboard/vm-tour.js:138
#: static_collected/dashboard/vm-tour.js:138
msgid ""
"This is the last message, if something is not clear you can do the the tour "
"again!"
msgstr ""
"A túra véget ért. Ha valami nem érthető, újrakezdheti az "
"útmutatót."
msgstr "A túra véget ért. Ha valami nem érthető, újrakezdheti az útmutatót."
#: network/static/js/host.js:10
#: network/static/js/host.js:10 static_collected/js/host.js:10
msgid ""
"Are you sure you want to remove host group <strong>\"%(group)s\"</strong> "
"from <strong>\"%(host)s\"</strong>?"
......@@ -189,19 +238,172 @@ msgstr ""
"Biztosan törli a(z)<strong>„%(host)s”</strong> gépet a(z) "
"<strong>„%(group)s”</strong> gépcsoportból?"
#: network/static/js/host.js:13
#: network/static/js/host.js:13 static_collected/js/host.js:13
msgid "Are you sure you want to delete this rule?"
msgstr "Biztosan törli ezt a szabályt?"
#: network/static/js/host.js:20 network/static/js/switch-port.js:14
#: static_collected/admin/js/admin/DateTimeShortcuts.js:95
#: static_collected/admin/js/admin/DateTimeShortcuts.js:208
#: static_collected/js/host.js:20 static_collected/js/switch-port.js:14
msgid "Cancel"
msgstr "Mégsem"
#: network/static/js/host.js:25 network/static/js/switch-port.js:19
#: static_collected/admin/js/SelectFilter2.js:69
#: static_collected/js/host.js:25 static_collected/js/switch-port.js:19
msgid "Remove"
msgstr "Eltávolítás"
#: network/static/js/switch-port.js:8
#: network/static/js/switch-port.js:8 static_collected/js/switch-port.js:8
msgid "Are you sure you want to delete this device?"
msgstr "Biztosan törli ezt az eszközt?"
#: static_collected/admin/js/SelectFilter2.js:45
#, c-format
msgid "Available %s"
msgstr ""
#: static_collected/admin/js/SelectFilter2.js:46
#, c-format
msgid ""
"This is the list of available %s. You may choose some by selecting them in "
"the box below and then clicking the \"Choose\" arrow between the two boxes."
msgstr ""
#: static_collected/admin/js/SelectFilter2.js:53
#, c-format
msgid "Type into this box to filter down the list of available %s."
msgstr ""
#: static_collected/admin/js/SelectFilter2.js:57
msgid "Filter"
msgstr ""
#: static_collected/admin/js/SelectFilter2.js:61
msgid "Choose all"
msgstr ""
#: static_collected/admin/js/SelectFilter2.js:61
#, c-format
msgid "Click to choose all %s at once."
msgstr ""
#: static_collected/admin/js/SelectFilter2.js:67
msgid "Choose"
msgstr ""
#: static_collected/admin/js/SelectFilter2.js:75
#, c-format
msgid "Chosen %s"
msgstr ""
#: static_collected/admin/js/SelectFilter2.js:76
#, c-format
msgid ""
"This is the list of chosen %s. You may remove some by selecting them in the "
"box below and then clicking the \"Remove\" arrow between the two boxes."
msgstr ""
#: static_collected/admin/js/SelectFilter2.js:80
#, fuzzy
msgid "Remove all"
msgstr "Eltávolítás"
#: static_collected/admin/js/SelectFilter2.js:80
#, c-format
msgid "Click to remove all chosen %s at once."
msgstr ""
#: static_collected/admin/js/actions.js:18
#: static_collected/admin/js/actions.min.js:1
msgid "%(sel)s of %(cnt)s selected"
msgid_plural "%(sel)s of %(cnt)s selected"
msgstr[0] ""
msgstr[1] ""
#: static_collected/admin/js/actions.js:109
#: static_collected/admin/js/actions.min.js:5
msgid ""
"You have unsaved changes on individual editable fields. If you run an "
"action, your unsaved changes will be lost."
msgstr ""
#: static_collected/admin/js/actions.js:121
#: static_collected/admin/js/actions.min.js:5
msgid ""
"You have selected an action, but you haven't saved your changes to "
"individual fields yet. Please click OK to save. You'll need to re-run the "
"action."
msgstr ""
#: static_collected/admin/js/actions.js:123
#: static_collected/admin/js/actions.min.js:6
msgid ""
"You have selected an action, and you haven't made any changes on individual "
"fields. You're probably looking for the Go button rather than the Save "
"button."
msgstr ""
#: static_collected/admin/js/calendar.js:8
msgid ""
"January February March April May June July August September October November "
"December"
msgstr ""
#: static_collected/admin/js/calendar.js:9
msgid "S M T W T F S"
msgstr ""
#: static_collected/admin/js/collapse.js:8
#: static_collected/admin/js/collapse.js:19
#: static_collected/admin/js/collapse.min.js:1
msgid "Show"
msgstr ""
#: static_collected/admin/js/collapse.js:16
#: static_collected/admin/js/collapse.min.js:1
msgid "Hide"
msgstr ""
#: static_collected/admin/js/admin/DateTimeShortcuts.js:52
#: static_collected/admin/js/admin/DateTimeShortcuts.js:88
msgid "Now"
msgstr ""
#: static_collected/admin/js/admin/DateTimeShortcuts.js:56
msgid "Clock"
msgstr ""
#: static_collected/admin/js/admin/DateTimeShortcuts.js:84
msgid "Choose a time"
msgstr ""
#: static_collected/admin/js/admin/DateTimeShortcuts.js:89
msgid "Midnight"
msgstr ""
#: static_collected/admin/js/admin/DateTimeShortcuts.js:90
msgid "6 a.m."
msgstr ""
#: static_collected/admin/js/admin/DateTimeShortcuts.js:91
msgid "Noon"
msgstr ""
#: static_collected/admin/js/admin/DateTimeShortcuts.js:148
#: static_collected/admin/js/admin/DateTimeShortcuts.js:201
msgid "Today"
msgstr ""
#: static_collected/admin/js/admin/DateTimeShortcuts.js:152
msgid "Calendar"
msgstr ""
#: static_collected/admin/js/admin/DateTimeShortcuts.js:199
msgid "Yesterday"
msgstr ""
#: static_collected/admin/js/admin/DateTimeShortcuts.js:203
msgid "Tomorrow"
msgstr ""
<!--
Collection name attribute represents the name of the menu, e.g., to use menu "File" use "file" or "Help" use "help". You can add new menus.
If you type a relative script file beware the this script is located in $KDEHOME/share/apps/applicationname/
The following example adds an action with the text "Export..." into the "File" menu
<KrossScripting>
<collection name="file" text="File" comment="File menu">
<script name="export" text="Export..." comment="Export content" file="export.py" />
</collection>
</KrossScripting>
-->
......@@ -23,61 +23,7 @@
<div class="page-header">
<h3>{% trans "Manage access" %}</h3>
</div>
<form action="{% url "network.vlan-acl" vid=vlan_vid %}" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields" id="vlan-access-table">
<thead>
<tr>
<th></th>
<th>{% trans "Who" %}</th>
<th>{% trans "What" %}</th>
<th><i class="fa fa-times"></i></th>
</tr></thead>
<tbody>
{% for i in acl.users %}
<tr>
<td><i class="fa fa-user"></i></td><td>{{i.user}}</td>
<td>
<select class="form-control" name="perm-u-{{i.user.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td>
<input type="checkbox" name="remove-u-{{i.user.id}}" title="{% trans "Remove" %}"/>
</td>
</tr>
{% endfor %}
{% for i in acl.groups %}
<tr>
<td><i class="fa fa-group"></i></td><td>{{i.group}}</td>
<td>
<select class="form-control" name="perm-g-{{i.group.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td>
<input type="checkbox" name="remove-g-{{i.group.id}}" title="{% trans "Remove" %}"/>
</td>
</tr>
{% endfor %}
<tr><td><i class="fa fa-plus"></i></td>
<td><input type="text" class="form-control" name="perm-new-name"
placeholder="{% trans "Name of group or user" %}"></td>
<td><select class="form-control" name="perm-new">
{% for id, name in acl.levels %}
<option value="{{id}}">{{name}}</option>
{% endfor %}
</select></td><td></td>
</tr>
</tbody>
</table>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
{% include "dashboard/_manage_access.html" with table_id="vlan-access-table" %}
</div>
</div>
{% endblock %}
......@@ -63,9 +63,9 @@ class VlanAclTest(LoginMixin, TestCase):
vlan = Vlan.objects.get(vid=1)
self.assertEqual([], vlan.get_users_with_level())
resp = c.post("/network/vlans/1/acl/", {
'perm-new-name': "user1",
'perm-new': "user",
resp = c.post("/network/vlans/2/acl/", {
'name': "user1",
'level': "user",
})
vlan = Vlan.objects.get(vid=1)
......@@ -80,10 +80,10 @@ class VlanAclTest(LoginMixin, TestCase):
vlan.set_level(self.u1, "user")
self.assertTrue((self.u1, "user") in vlan.get_users_with_level())
resp = c.post("/network/vlans/1/acl/", {
resp = c.post("/network/vlans/2/acl/", {
'perm-u-%d' % self.u1.pk: "operator",
'perm-new': "",
'perm-new-name': "",
'level': "",
'name': "",
})
self.assertTrue((self.u1, "operator") in vlan.get_users_with_level())
......@@ -97,10 +97,10 @@ class VlanAclTest(LoginMixin, TestCase):
vlan.set_level(self.u1, "user")
self.assertTrue((self.u1, "user") in vlan.get_users_with_level())
resp = c.post("/network/vlans/1/acl/", {
resp = c.post("/network/vlans/2/acl/", {
'remove-u-%d' % self.u1.pk: "",
'perm-new': "",
'perm-new-name': "",
'level': "",
'name': "",
})
self.assertTrue((self.u1, "user") not in vlan.get_users_with_level())
......
......@@ -84,7 +84,7 @@ urlpatterns = patterns(
url('^vlans/$', VlanList.as_view(), name='network.vlan_list'),
url('^vlans/create$', VlanCreate.as_view(), name='network.vlan_create'),
url('^vlans/(?P<vid>\d+)/$', VlanDetail.as_view(), name='network.vlan'),
url('^vlans/(?P<vid>\d+)/acl/$', VlanAclUpdateView.as_view(),
url('^vlans/(?P<pk>\d+)/acl/$', VlanAclUpdateView.as_view(),
name='network.vlan-acl'),
url('^vlans/delete/(?P<vid>\d+)/$', VlanDelete.as_view(),
name="network.vlan_delete"),
......
......@@ -42,6 +42,7 @@ from operator import itemgetter
from itertools import chain
import json
from dashboard.views import AclUpdateView
from dashboard.forms import AclUserAddForm
class SuccessMessageMixin(FormMixin):
......@@ -629,19 +630,8 @@ class VlanList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
table_pagination = False
def get_vlan_acl_data(obj):
levels = obj.ACL_LEVELS
users = obj.get_users_with_level()
users = [{'user': u, 'level': l} for u, l in users]
groups = obj.get_groups_with_level()
groups = [{'group': g, 'level': l} for g, l in groups]
return {'users': users, 'groups': groups, 'levels': levels}
class VlanAclUpdateView(AclUpdateView):
model = Vlan
slug_field = "vid"
slug_url_kwarg = "vid"
class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
......@@ -662,7 +652,9 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
context['host_list'] = SmallHostTable(q)
context['vlan_vid'] = self.kwargs.get('vid')
context['acl'] = get_vlan_acl_data(self.get_object())
context['acl'] = AclUpdateView.get_acl_data(
self.object, self.request.user, 'network.vlan-acl')
context['aclform'] = AclUserAddForm()
return context
success_url = reverse_lazy('network.vlan_list')
......
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
import logging
logging.basicConfig(filename='/var/tmp/0015_disk_migration.log',level=logging.INFO)
logger = logging.getLogger()
class Migration(DataMigration):
def forwards(self, orm):
"Write your forwards methods here."
# Note: Don't use "from appname.models import ModelName".
# Use orm.ModelName to refer to models in this application,
# and orm['appname.ModelName'] for models in other applications.
disks = orm.Disk
for disk in disks.objects.all():
if disk.base is None and disk.destroyed is None:
logger.info("Set disk %s (%s) with filename: %s to ready.",
disk.name, disk.pk, disk.filename)
disk.is_ready = True
disk.save()
def backwards(self, orm):
"Write your backwards methods here."
models = {
u'storage.datastore': {
'Meta': {'ordering': "[u'name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "u'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
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'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'size': ('sizefield.models.FileSizeField', [], {'default': 'None', 'null': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
}
}
complete_apps = ['storage']
symmetrical = True
......@@ -27,14 +27,13 @@ from celery.contrib.abortable import AbortableAsyncResult
from django.db.models import (Model, BooleanField, CharField, DateTimeField,
ForeignKey)
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from model_utils.models import TimeStampedModel
from sizefield.models import FileSizeField
from acl.models import AclBase
from .tasks import local_tasks, storage_tasks
from celery.exceptions import TimeoutError
from common.models import WorkerNotFound
from common.models import WorkerNotFound, HumanReadableException
logger = logging.getLogger(__name__)
......@@ -76,15 +75,10 @@ class DataStore(Model):
destroyed__isnull=False) if disk.is_deletable]
class Disk(AclBase, TimeStampedModel):
class Disk(TimeStampedModel):
"""A virtual disk.
"""
ACL_LEVELS = (
('user', _('user')), # see all details
('operator', _('operator')),
('owner', _('owner')), # superuser, can delete, delegate perms
)
TYPES = [('qcow2-norm', 'qcow2 normal'), ('qcow2-snap', 'qcow2 snapshot'),
('iso', 'iso'), ('raw-ro', 'raw read-only'), ('raw-rw', 'raw')]
name = CharField(blank=True, max_length=100, verbose_name=_("name"))
......@@ -110,43 +104,73 @@ class Disk(AclBase, TimeStampedModel):
('create_empty_disk', _('Can create an empty disk.')),
('download_disk', _('Can download a disk.')))
class WrongDiskTypeError(Exception):
def __init__(self, type, message=None):
if message is None:
message = ("Operation can't be invoked on a disk of type '%s'."
% type)
Exception.__init__(self, message)
self.type = type
class DiskInUseError(Exception):
def __init__(self, disk, message=None):
if message is None:
message = ("The requested operation can't be performed on "
"disk '%s (%s)' because it is in use." %
(disk.name, disk.filename))
Exception.__init__(self, message)
self.disk = disk
class DiskIsNotReady(Exception):
""" Exception for operations that need a deployed disk.
"""
def __init__(self, disk, message=None):
if message is None:
message = ("The requested operation can't be performed on "
"disk '%s (%s)' because it has never been"
"deployed." % (disk.name, disk.filename))
Exception.__init__(self, message)
self.disk = disk
class DiskError(HumanReadableException):
admin_message = None
def __init__(self, disk, params=None, level=None, **kwargs):
kwargs.update(params or {})
self.disc = kwargs["disk"] = disk
super(Disk.DiskError, self).__init__(
level, self.message, self.admin_message or self.message,
kwargs)
class WrongDiskTypeError(DiskError):
message = ugettext_noop("Operation can't be invoked on disk "
"'%(name)s' of type '%(type)s'.")
admin_message = ugettext_noop(
"Operation can't be invoked on disk "
"'%(name)s' (%(pk)s) of type '%(type)s'.")
def __init__(self, disk, params=None, **kwargs):
super(Disk.WrongDiskTypeError, self).__init__(
disk, params, type=disk.type, name=disk.name, pk=disk.pk)
class DiskInUseError(DiskError):
message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' because it is in use.")
admin_message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' (%(pk)s) because it is in use.")
def __init__(self, disk, params=None, **kwargs):
super(Disk.DiskInUseError, self).__init__(
disk, params, name=disk.name, pk=disk.pk)
class DiskIsNotReady(DiskError):
message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' because it has never been deployed.")
admin_message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' (%(pk)s) [%(filename)s] because it has never been"
"deployed.")
def __init__(self, disk, params=None, **kwargs):
super(Disk.DiskIsNotReady, self).__init__(
disk, params, name=disk.name, pk=disk.pk,
filename=disk.filename)
class DiskBaseIsNotReady(DiskError):
message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' because its base has never been deployed.")
admin_message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' (%(pk)s) [%(filename)s] because its base "
"'%(b_name)s' (%(b_pk)s) [%(b_filename)s] has never been"
"deployed.")
def __init__(self, disk, params=None, **kwargs):
base = kwargs.get('base')
super(Disk.DiskBaseIsNotReady, self).__init__(
disk, params, name=disk.name, pk=disk.pk,
filename=disk.filename, b_name=base.name,
b_pk=base.pk, b_filename=base.filename)
@property
def path(self):
......@@ -225,15 +249,14 @@ class Disk(AclBase, TimeStampedModel):
return any(i.state != 'STOPPED' for i in self.instance_set.all())
def get_appliance(self):
"""Return an Instance or InstanceTemplate object where the disk is used
"""Return the Instance or InstanceTemplate object where the disk
is used
"""
instance = self.instance_set.all()
template = self.template_set.all()
app = list(instance) + list(template)
if len(app) > 0:
return app[0]
else:
return None
from vm.models import Instance
try:
return self.instance_set.get()
except Instance.DoesNotExist:
return self.template_set.get()
def get_exclusive(self):
"""Get an instance of the disk for exclusive usage.
......@@ -247,7 +270,7 @@ class Disk(AclBase, TimeStampedModel):
}
if self.type not in type_mapping.keys():
raise self.WrongDiskTypeError(self.type)
raise self.WrongDiskTypeError(self)
new_type = type_mapping[self.type]
......@@ -320,6 +343,8 @@ class Disk(AclBase, TimeStampedModel):
if self.is_ready:
return True
if self.base and not self.base.is_ready:
raise self.DiskBaseIsNotReady(self, base=self.base)
queue_name = self.get_remote_queue_name('storage', priority="fast")
disk_desc = self.get_disk_desc()
if self.base is not None:
......@@ -424,7 +449,7 @@ class Disk(AclBase, TimeStampedModel):
'iso': ("iso", self),
}
if self.type not in mapping.keys():
raise self.WrongDiskTypeError(self.type)
raise self.WrongDiskTypeError(self)
if self.is_in_use:
raise self.DiskInUseError(self)
......@@ -440,7 +465,7 @@ class Disk(AclBase, TimeStampedModel):
disk = Disk.create(datastore=self.datastore,
base=new_base,
name=self.name, size=self.size,
type=new_type)
type=new_type, dev_num=self.dev_num)
queue_name = self.get_remote_queue_name("storage", priority="slow")
remote = storage_tasks.merge.apply_async(kwargs={
......@@ -457,4 +482,6 @@ class Disk(AclBase, TimeStampedModel):
AbortableAsyncResult(remote.id).abort()
disk.destroy()
raise Exception("Save as aborted by use.")
disk.is_ready = True
disk.save()
return disk
......@@ -13,6 +13,7 @@ from .instance import InstanceTemplate
from .instance import Instance
from .instance import post_state_changed
from .instance import pre_state_changed
from .instance import pwgen
from .network import InterfaceTemplate
from .network import Interface
from .node import Node
......@@ -22,5 +23,5 @@ __all__ = [
'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate',
'Instance', 'instance_activity', 'post_state_changed', 'pre_state_changed',
'InterfaceTemplate', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease',
'node_activity',
'node_activity', 'pwgen'
]
......@@ -30,7 +30,7 @@ from django.utils.translation import ugettext_lazy as _, ugettext_noop
from common.models import (
ActivityModel, activitycontextimpl, create_readable, join_activity_code,
HumanReadableObject,
HumanReadableObject, HumanReadableException,
)
from manager.mancelery import celery
......@@ -39,16 +39,17 @@ from manager.mancelery import celery
logger = getLogger(__name__)
class ActivityInProgressError(Exception):
class ActivityInProgressError(HumanReadableException):
def __init__(self, activity, message=None):
if message is None:
message = ("Another activity is currently in progress: '%s'."
% activity.activity_code)
Exception.__init__(self, message)
self.activity = activity
@classmethod
def create(cls, activity):
obj = super(ActivityInProgressError, cls).create(
ugettext_noop("%(activity)s activity is currently in progress."),
ugettext_noop("%(activity)s (%(pk)s) activity is currently "
"in progress."),
activity=activity.readable_name, pk=activity.pk)
obj.activity = activity
return obj
def _normalize_readable_name(name, default=None):
......@@ -95,7 +96,7 @@ class InstanceActivity(ActivityModel):
# Check for concurrent activities
active_activities = instance.activity_log.filter(finished__isnull=True)
if concurrency_check and active_activities.exists():
raise ActivityInProgressError(active_activities[0])
raise ActivityInProgressError.create(active_activities[0])
activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix)
act = cls(activity_code=activity_code, instance=instance, parent=None,
......@@ -112,7 +113,7 @@ class InstanceActivity(ActivityModel):
# Check for concurrent activities
active_children = self.children.filter(finished__isnull=True)
if concurrency_check and active_children.exists():
raise ActivityInProgressError(active_children[0])
raise ActivityInProgressError.create(active_children[0])
act = InstanceActivity(
activity_code=join_activity_code(self.activity_code, code_suffix),
......
......@@ -41,7 +41,7 @@ from model_utils.models import TimeStampedModel, StatusModel
from taggit.managers import TaggableManager
from acl.models import AclBase
from common.models import create_readable
from common.models import create_readable, HumanReadableException
from common.operations import OperatedMixin
from ..tasks import vm_tasks, agent_tasks
from .activity import (ActivityInProgressError, instance_activity,
......@@ -271,32 +271,31 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
('create_vm', _('Can create a new VM.')),
('config_ports', _('Can configure port forwards.')),
('recover', _('Can recover a destroyed VM.')),
('emergency_change_state', _('Can change VM state to NOSTATE.')),
)
verbose_name = _('instance')
verbose_name_plural = _('instances')
class InstanceDestroyedError(Exception):
class InstanceError(HumanReadableException):
def __init__(self, instance, message=None):
if message is None:
message = ("The instance (%s) has already been destroyed."
% instance)
def __init__(self, instance, params=None, level=None, **kwargs):
kwargs.update(params or {})
self.instance = kwargs["instance"] = instance
super(Instance.InstanceError, self).__init__(
level, self.message, self.message, kwargs)
Exception.__init__(self, message)
class InstanceDestroyedError(InstanceError):
message = ugettext_noop(
"Instance %(instance)s has already been destroyed.")
self.instance = instance
class WrongStateError(InstanceError):
message = ugettext_noop(
"Current state (%(state)s) of instance %(instance)s is "
"inappropriate for the invoked operation.")
class WrongStateError(Exception):
def __init__(self, instance, message=None):
if message is None:
message = ("The instance's current state (%s) is "
"inappropriate for the invoked operation."
% instance.status)
Exception.__init__(self, message)
self.instance = instance
def __init__(self, instance, params=None, **kwargs):
super(Instance.WrongStateError, self).__init__(
instance, params, state=instance.status)
def __unicode__(self):
parts = (self.name, "(" + str(self.id) + ")")
......@@ -404,13 +403,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
"""
disks = template.disks.all() if disks is None else disks
for disk in disks:
if not disk.has_level(owner, 'user'):
raise PermissionDenied()
elif (disk.type == 'qcow2-snap'
and not disk.has_level(owner, 'owner')):
raise PermissionDenied()
networks = (template.interface_set.all() if networks is None
else networks)
......@@ -444,26 +436,14 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
self.time_of_suspend, self.time_of_delete = self.get_renew_times()
super(Instance, self).clean(*args, **kwargs)
def manual_state_change(self, new_state="NOSTATE", reason=None, user=None):
""" Manually change state of an Instance.
Can be used to recover VM after administrator fixed problems.
"""
# TODO cancel concurrent activity (if exists)
act = InstanceActivity.create(
code_suffix='manual_state_change', instance=self, user=user,
readable_name=create_readable(ugettext_noop(
"force %(state)s state"), state=new_state))
act.finished = act.started
act.result = reason
act.resultant_state = new_state
act.succeeded = True
act.save()
def vm_state_changed(self, new_state):
# log state change
try:
act = InstanceActivity.create(code_suffix='vm_state_changed',
act = InstanceActivity.create(
code_suffix='vm_state_changed',
readable_name=create_readable(
ugettext_noop("vm state changed to %(state)s"),
state=new_state),
instance=self)
except ActivityInProgressError:
pass # discard state change if another activity is in progress.
......@@ -663,6 +643,13 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
:param again: Notify already notified owners.
"""
notification_msg = ugettext_noop(
'Your instance <a href="%(url)s">%(instance)s</a> is going to '
'expire. It will be suspended at %(suspend)s and destroyed at '
'%(delete)s. Please, either <a href="%(token)s">renew</a> '
'or <a href="%(url)s">destroy</a> it now.')
if not again and self._is_notified_about_expiration():
return False
success, failed = [], []
......@@ -687,20 +674,26 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
readable_name=ugettext_noop(
"notify owner about expiration"),
on_commit=on_commit):
from dashboard.views import VmRenewView
from dashboard.views import VmRenewView, absolute_url
level = self.get_level_object("owner")
for u, ulevel in self.get_users_with_level(level__pk=level.pk):
try:
token = VmRenewView.get_token_url(self, u)
u.profile.notify(
_('%s expiring soon') % unicode(self),
'dashboard/notifications/vm-expiring.html',
{'instance': self, 'token': token}, valid_until=min(
self.time_of_delete, self.time_of_suspend))
ugettext_noop('%(instance)s expiring soon'),
notification_msg, url=self.get_absolute_url(),
instance=self, suspend=self.time_of_suspend,
token=token, delete=self.time_of_delete)
except Exception as e:
failed.append((u, e))
else:
success.append(u)
if self.status == "RUNNING":
token = absolute_url(
VmRenewView.get_token_url(self, self.owner))
queue = self.get_remote_queue_name("agent")
agent_tasks.send_expiration.apply_async(
queue=queue, args=(self.vm_name, token))
return True
def is_expiring(self, threshold=0.1):
......@@ -740,24 +733,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
timezone.now() + lease.suspend_interval,
timezone.now() + lease.delete_interval)
def change_password(self, user=None):
"""Generate new password for the vm
:param self: The virtual machine.
:param user: The user who's issuing the command.
"""
self.pw = pwgen()
with instance_activity(code_suffix='change_password', instance=self,
readable_name=ugettext_noop("change password"),
user=user):
queue = self.get_remote_queue_name("agent")
agent_tasks.change_password.apply_async(queue=queue,
args=(self.vm_name,
self.pw))
self.save()
def select_node(self):
"""Returns the node the VM should be deployed or migrated to.
"""
......
......@@ -24,17 +24,20 @@ from django.core.exceptions import PermissionDenied
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from sizefield.utils import filesizeformat
from celery.exceptions import TimeLimitExceeded
from common.models import create_readable
from common.models import create_readable, humanize_exception
from common.operations import Operation, register_operation
from .tasks.local_tasks import (
abortable_async_instance_operation, abortable_async_node_operation,
)
from .models import (
Instance, InstanceActivity, InstanceTemplate, Interface, Node,
NodeActivity,
NodeActivity, pwgen
)
from .tasks import agent_tasks
logger = getLogger(__name__)
......@@ -44,6 +47,8 @@ class InstanceOperation(Operation):
async_operation = abortable_async_instance_operation
host_cls = Instance
concurrency_check = True
accept_states = None
deny_states = None
def __init__(self, instance):
super(InstanceOperation, self).__init__(subject=instance)
......@@ -52,11 +57,26 @@ class InstanceOperation(Operation):
def check_precond(self):
if self.instance.destroyed_at:
raise self.instance.InstanceDestroyedError(self.instance)
if self.accept_states:
if self.instance.status not in self.accept_states:
logger.debug("precond failed for %s: %s not in %s",
unicode(self.__class__),
unicode(self.instance.status),
unicode(self.accept_states))
raise self.instance.WrongStateError(self.instance)
if self.deny_states:
if self.instance.status in self.deny_states:
logger.debug("precond failed for %s: %s in %s",
unicode(self.__class__),
unicode(self.instance.status),
unicode(self.accept_states))
raise self.instance.WrongStateError(self.instance)
def check_auth(self, user):
if not self.instance.has_level(user, self.acl_level):
raise PermissionDenied("%s doesn't have the required ACL level." %
user)
raise humanize_exception(ugettext_noop(
"%(acl_level)s level is required for this operation."),
PermissionDenied(), acl_level=self.acl_level)
super(InstanceOperation, self).check_auth(user=user)
......@@ -93,13 +113,20 @@ class AddInterfaceOperation(InstanceOperation):
description = _("Add a new network interface for the specified VLAN to "
"the VM.")
required_perms = ()
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
def check_precond(self):
super(AddInterfaceOperation, self).check_precond()
if self.instance.status not in ['STOPPED', 'PENDING', 'RUNNING']:
raise self.instance.WrongStateError(self.instance)
def rollback(self, net, activity):
with activity.sub_activity(
'destroying_net',
readable_name=ugettext_noop("destroy network (rollback)")):
net.destroy()
net.delete()
def _operation(self, activity, user, system, vlan, managed=None):
if not vlan.has_level(user, 'user'):
raise humanize_exception(ugettext_noop(
"User acces to vlan %(vlan)s is required."),
PermissionDenied(), vlan=vlan)
if managed is None:
managed = vlan.managed
......@@ -107,12 +134,17 @@ class AddInterfaceOperation(InstanceOperation):
managed=managed, owner=user, vlan=vlan)
if self.instance.is_running:
with activity.sub_activity('attach_network'):
try:
with activity.sub_activity(
'attach_network',
readable_name=ugettext_noop("attach network")):
self.instance.attach_network(net)
except Exception as e:
if hasattr(e, 'libvirtError'):
self.rollback(net, activity)
raise
net.deploy()
return net
def get_activity_name(self, kwargs):
return create_readable(ugettext_noop("add %(vlan)s interface"),
vlan=kwargs['vlan'])
......@@ -128,11 +160,7 @@ class CreateDiskOperation(InstanceOperation):
name = _("create disk")
description = _("Create empty disk for the VM.")
required_perms = ('storage.create_empty_disk', )
def check_precond(self):
super(CreateDiskOperation, self).check_precond()
if self.instance.status not in ['STOPPED', 'PENDING', 'RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
def _operation(self, user, size, activity, name=None):
from storage.models import Disk
......@@ -149,14 +177,21 @@ class CreateDiskOperation(InstanceOperation):
self.instance.disks.add(disk)
if self.instance.is_running:
with activity.sub_activity('deploying_disk'):
with activity.sub_activity(
'deploying_disk',
readable_name=ugettext_noop("deploying disk")
):
disk.deploy()
with activity.sub_activity('attach_disk'):
with activity.sub_activity(
'attach_disk',
readable_name=ugettext_noop("attach disk")
):
self.instance.attach_disk(disk)
def get_activity_name(self, kwargs):
return create_readable(ugettext_noop("create %(size)s disk"),
size=kwargs['size'])
return create_readable(
ugettext_noop("create disk %(name)s (%(size)s)"),
size=filesizeformat(kwargs['size']), name=kwargs['name'])
register_operation(CreateDiskOperation)
......@@ -170,11 +205,7 @@ class DownloadDiskOperation(InstanceOperation):
abortable = True
has_percentage = True
required_perms = ('storage.download_disk', )
def check_precond(self):
super(DownloadDiskOperation, self).check_precond()
if self.instance.status not in ['STOPPED', 'PENDING', 'RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
def _operation(self, user, url, task, activity, name=None):
activity.result = url
......@@ -193,7 +224,10 @@ class DownloadDiskOperation(InstanceOperation):
# TODO iso (cd) hot-plug is not supported by kvm/guests
if self.instance.is_running and disk.type not in ["iso"]:
with activity.sub_activity('attach_disk'):
with activity.sub_activity(
'attach_disk',
readable_name=ugettext_noop("attach disk")
):
self.instance.attach_disk(disk)
register_operation(DownloadDiskOperation)
......@@ -205,11 +239,7 @@ class DeployOperation(InstanceOperation):
name = _("deploy")
description = _("Deploy new virtual machine with network.")
required_perms = ()
def check_precond(self):
super(DeployOperation, self).check_precond()
if self.instance.status in ['RUNNING', 'SUSPENDED']:
raise self.instance.WrongStateError(self.instance)
deny_states = ('SUSPENDED', 'RUNNING')
def is_preferred(self):
return self.instance.status in (self.instance.STATUS.STOPPED,
......@@ -310,6 +340,7 @@ class MigrateOperation(InstanceOperation):
name = _("migrate")
description = _("Live migrate running VM to another node.")
required_perms = ()
accept_states = ('RUNNING', )
def rollback(self, activity):
with activity.sub_activity(
......@@ -317,11 +348,6 @@ class MigrateOperation(InstanceOperation):
"redeploy network (rollback)")):
self.instance.deploy_net()
def check_precond(self):
super(MigrateOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
def check_auth(self, user):
if not user.is_superuser:
raise PermissionDenied()
......@@ -371,11 +397,7 @@ class RebootOperation(InstanceOperation):
name = _("reboot")
description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
required_perms = ()
def check_precond(self):
super(RebootOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('RUNNING', )
def _operation(self, timeout=5):
self.instance.reboot_vm(timeout=timeout)
......@@ -390,21 +412,24 @@ class RemoveInterfaceOperation(InstanceOperation):
name = _("remove interface")
description = _("Remove the specified network interface from the VM.")
required_perms = ()
def check_precond(self):
super(RemoveInterfaceOperation, self).check_precond()
if self.instance.status not in ['STOPPED', 'PENDING', 'RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
def _operation(self, activity, user, system, interface):
if self.instance.is_running:
with activity.sub_activity('detach_network'):
with activity.sub_activity(
'detach_network',
readable_name=ugettext_noop("detach network")
):
self.instance.detach_network(interface)
interface.shutdown()
interface.destroy()
interface.delete()
def get_activity_name(self, kwargs):
return create_readable(ugettext_noop("remove %(vlan)s interface"),
vlan=kwargs['interface'].vlan)
register_operation(RemoveInterfaceOperation)
......@@ -415,18 +440,24 @@ class RemoveDiskOperation(InstanceOperation):
name = _("remove disk")
description = _("Remove the specified disk from the VM.")
required_perms = ()
def check_precond(self):
super(RemoveDiskOperation, self).check_precond()
if self.instance.status not in ['STOPPED', 'PENDING', 'RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
def _operation(self, activity, user, system, disk):
if self.instance.is_running and disk.type not in ["iso"]:
with activity.sub_activity('detach_disk'):
with activity.sub_activity(
'detach_disk',
readable_name=ugettext_noop('detach disk')
):
self.instance.detach_disk(disk)
with activity.sub_activity(
'destroy_disk',
readable_name=ugettext_noop('destroy disk')
):
return self.instance.disks.remove(disk)
def get_activity_name(self, kwargs):
return create_readable(ugettext_noop('remove disk %(name)s'),
name=kwargs["disk"].name)
register_operation(RemoveDiskOperation)
......@@ -437,11 +468,7 @@ class ResetOperation(InstanceOperation):
name = _("reset")
description = _("Reset virtual machine (reset button).")
required_perms = ()
def check_precond(self):
super(ResetOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('RUNNING', )
def _operation(self, timeout=5):
self.instance.reset_vm(timeout=timeout)
......@@ -460,6 +487,7 @@ class SaveAsTemplateOperation(InstanceOperation):
""")
abortable = True
required_perms = ('vm.create_template', )
accept_states = ('RUNNING', 'PENDING', 'STOPPED')
def is_preferred(self):
return (self.instance.is_base and
......@@ -480,11 +508,6 @@ class SaveAsTemplateOperation(InstanceOperation):
for disk in self.disks:
disk.destroy()
def check_precond(self):
super(SaveAsTemplateOperation, self).check_precond()
if self.instance.status not in ['RUNNING', 'PENDING', 'STOPPED']:
raise self.instance.WrongStateError(self.instance)
def _operation(self, activity, user, system, timeout=300, name=None,
with_shutdown=True, task=None, **kwargs):
if with_shutdown:
......@@ -523,14 +546,15 @@ class SaveAsTemplateOperation(InstanceOperation):
return disk
self.disks = []
with activity.sub_activity('saving_disks',
readable_name=ugettext_noop("save disks")):
for disk in self.instance.disks.all():
with activity.sub_activity(
'saving_disk',
readable_name=create_readable(
ugettext_noop("saving disk %(name)s"),
name=disk.name)
):
self.disks.append(__try_save_disk(disk))
for disk in self.disks:
disk.set_level(user, 'owner')
# create template and do additional setup
tmpl = InstanceTemplate(**params)
tmpl.full_clean() # Avoiding database errors.
......@@ -557,11 +581,7 @@ class ShutdownOperation(InstanceOperation):
description = _("Shutdown virtual machine with ACPI signal.")
abortable = True
required_perms = ()
def check_precond(self):
super(ShutdownOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('RUNNING', )
def on_commit(self, activity):
activity.resultant_state = 'STOPPED'
......@@ -581,11 +601,7 @@ class ShutOffOperation(InstanceOperation):
name = _("shut off")
description = _("Shut off VM (plug-out).")
required_perms = ()
def check_precond(self):
super(ShutOffOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('RUNNING', )
def on_commit(self, activity):
activity.resultant_state = 'STOPPED'
......@@ -613,16 +629,12 @@ class SleepOperation(InstanceOperation):
name = _("sleep")
description = _("Suspend virtual machine with memory dump.")
required_perms = ()
accept_states = ('RUNNING', )
def is_preferred(self):
return (not self.instance.is_base and
self.instance.status == self.instance.STATUS.RUNNING)
def check_precond(self):
super(SleepOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
def on_abort(self, activity, error):
if isinstance(error, TimeLimitExceeded):
activity.resultant_state = None
......@@ -660,15 +672,10 @@ class WakeUpOperation(InstanceOperation):
Power on Virtual Machine and load its memory from dump.
""")
required_perms = ()
accept_states = ('SUSPENDED', )
def is_preferred(self):
return (self.instance.is_base and
self.instance.status == self.instance.STATUS.SUSPENDED)
def check_precond(self):
super(WakeUpOperation, self).check_precond()
if self.instance.status not in ['SUSPENDED']:
raise self.instance.WrongStateError(self.instance)
return self.instance.status == self.instance.STATUS.SUSPENDED
def on_abort(self, activity, error):
activity.resultant_state = 'ERROR'
......@@ -718,6 +725,21 @@ class RenewOperation(InstanceOperation):
register_operation(RenewOperation)
class ChangeStateOperation(InstanceOperation):
activity_code_suffix = 'emergency_change_state'
id = 'emergency_change_state'
name = _("emergency change state")
description = _("Change the virtual machine state to NOSTATE")
acl_level = "owner"
required_perms = ('vm.emergency_change_state', )
def _operation(self, user, activity, new_state="NOSTATE"):
activity.resultant_state = new_state
register_operation(ChangeStateOperation)
class NodeOperation(Operation):
async_operation = abortable_async_node_operation
host_cls = Node
......@@ -761,7 +783,8 @@ class FlushOperation(NodeOperation):
def check_auth(self, user):
if not user.is_superuser:
raise PermissionDenied()
raise humanize_exception(ugettext_noop(
"Superuser privileges are required."), PermissionDenied())
super(FlushOperation, self).check_auth(user=user)
......@@ -786,11 +809,7 @@ class ScreenshotOperation(InstanceOperation):
description = _("Get screenshot")
acl_level = "owner"
required_perms = ()
def check_precond(self):
super(ScreenshotOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('RUNNING', )
def _operation(self):
return self.instance.get_screenshot(timeout=20)
......@@ -806,10 +825,13 @@ class RecoverOperation(InstanceOperation):
description = _("Recover virtual machine from destroyed state.")
acl_level = "owner"
required_perms = ('vm.recover', )
accept_states = ('DESTROYED', )
def check_precond(self):
if not self.instance.destroyed_at:
raise self.instance.WrongStateError(self.instance)
try:
super(RecoverOperation, self).check_precond()
except Instance.InstanceDestroyedError:
pass
def on_commit(self, activity):
activity.resultant_state = 'PENDING'
......@@ -832,13 +854,8 @@ class ResourcesOperation(InstanceOperation):
name = _("resources change")
description = _("Change resources")
acl_level = "owner"
concurrency_check = False
required_perms = ('vm.change_resources', )
def check_precond(self):
super(ResourcesOperation, self).check_precond()
if self.instance.status not in ["STOPPED", "PENDING"]:
raise self.instance.WrongStateError(self.instance)
accept_states = ('STOPPED', 'PENDING', )
def _operation(self, user, num_cores, ram_size, max_ram_size, priority):
......@@ -852,3 +869,23 @@ class ResourcesOperation(InstanceOperation):
register_operation(ResourcesOperation)
class PasswordResetOperation(InstanceOperation):
activity_code_suffix = 'Password reset'
id = 'password_reset'
name = _("password reset")
description = _("Password reset")
acl_level = "owner"
required_perms = ()
accept_states = ('RUNNING', )
def _operation(self):
self.instance.pw = pwgen()
queue = self.instance.get_remote_queue_name("agent")
agent_tasks.change_password.apply_async(
queue=queue, args=(self.instance.vm_name, self.instance.pw))
self.instance.save()
register_operation(PasswordResetOperation)
......@@ -71,3 +71,8 @@ def del_keys(vm, keys):
@celery.task(name='agent.get_keys')
def get_keys(vm):
pass
@celery.task(name='agent.send_expiration')
def send_expiration(vm, url):
pass
......@@ -25,22 +25,25 @@ from StringIO import StringIO
from tarfile import TarFile, TarInfo
from django.conf import settings
from django.utils import timezone
from django.utils.translation import ugettext_noop
from celery.result import TimeoutError
from monitor.client import Client
def send_init_commands(instance, act, vm):
queue = instance.get_remote_queue_name("agent")
with act.sub_activity('cleanup'):
with act.sub_activity('cleanup', readable_name=ugettext_noop('cleanup')):
cleanup.apply_async(queue=queue, args=(vm, ))
with act.sub_activity('restart_networking'):
with act.sub_activity('restart_networking',
readable_name=ugettext_noop('restart networking')):
restart_networking.apply_async(queue=queue, args=(vm, ))
with act.sub_activity('change_password'):
with act.sub_activity('change_password',
readable_name=ugettext_noop('change password')):
change_password.apply_async(queue=queue, args=(vm, instance.pw))
with act.sub_activity('set_time'):
with act.sub_activity('set_time', readable_name=ugettext_noop('set time')):
set_time.apply_async(queue=queue, args=(vm, time.time()))
with act.sub_activity('set_hostname'):
with act.sub_activity('set_hostname',
readable_name=ugettext_noop('set hostname')):
set_hostname.apply_async(
queue=queue, args=(vm, instance.primary_host.hostname))
......@@ -73,13 +76,20 @@ def agent_started(vm, version=None):
initialized = InstanceActivity.objects.filter(
instance=instance, activity_code='vm.Instance.agent.cleanup').exists()
with instance_activity(code_suffix='agent', instance=instance) as act:
with act.sub_activity('starting'):
with instance_activity(code_suffix='agent',
readable_name=ugettext_noop('agent'),
instance=instance) as act:
with act.sub_activity('starting',
readable_name=ugettext_noop('starting')):
pass
if version and version != settings.AGENT_VERSION:
try:
with act.sub_activity('update'):
with act.sub_activity(
'update',
readable_name=ugettext_noop('update to %(version)s'),
version=settings.AGENT_VERSION
):
update.apply_async(
queue=queue,
args=(vm, create_agent_tar())).get(timeout=10)
......@@ -91,7 +101,10 @@ def agent_started(vm, version=None):
measure_boot_time(instance)
send_init_commands(instance, act, vm)
with act.sub_activity('start_access_server'):
with act.sub_activity(
'start_access_server',
readable_name=ugettext_noop('start access server')
):
start_access_server.apply_async(queue=queue, args=(vm, ))
......@@ -122,5 +135,5 @@ def agent_stopped(vm):
qs = InstanceActivity.objects.filter(instance=instance,
activity_code='vm.Instance.agent')
act = qs.latest('id')
with act.sub_activity('stopping'):
with act.sub_activity('stopping', readable_name=ugettext_noop('stopping')):
pass
......@@ -66,8 +66,8 @@ class InstanceTestCase(TestCase):
inst = MagicMock(spec=Instance, node=node, vnc_port=port)
inst.save.side_effect = AssertionError
with patch('vm.models.instance.InstanceActivity') as ia:
ia.create.side_effect = ActivityInProgressError(MagicMock())
Instance.vm_state_changed(inst, 'STOPPED')
ia.create.side_effect = ActivityInProgressError.create(MagicMock())
Instance.status = 'STOPPED'
self.assertEquals(inst.node, node)
self.assertEquals(inst.vnc_port, port)
......@@ -210,7 +210,7 @@ class NodeTestCase(TestCase):
node.enabled = True
node.STATES = Node.STATES
self.assertEqual(Node.get_state(node), "ONLINE")
assert isinstance(Node.get_status_display(node), _("").__class__)
assert isinstance(Node.get_status_display(node), _("x").__class__)
class InstanceActivityTestCase(TestCase):
......
......@@ -4,6 +4,7 @@ billiard==3.3.0.17
bpython==0.12
celery==3.1.11
Django==1.6.3
django-autocomplete-light==1.4.14
django-braces==1.4.0
django-celery==3.1.10
django-crispy-forms==1.4.0
......@@ -20,7 +21,6 @@ kombu==3.0.15
logutils==0.3.3
MarkupSafe==0.21
netaddr==0.7.11
nose==1.3.1
pip-tools==0.3.4
psycopg2==2.5.2
Pygments==1.6
......@@ -33,3 +33,5 @@ six==1.6.1
South==0.8.4
sqlparse==0.1.11
pika==0.9.13
Fabric==1.9.0
lxml==3.3.5
......@@ -3,3 +3,5 @@
coverage==3.7.1
factory-boy==2.3.1
mock==1.0.1
django-nose==1.2
nose==1.3.3
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