Commit 7bb23000 by Kálmán Viktor

Merge branch 'master' into issue-sliders

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/views.py
parents 216378f7 b548b1ce
......@@ -10,6 +10,18 @@ class AclUserAutocomplete(autocomplete_light.AutocompleteGenericBase):
('^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
......
......@@ -54,6 +54,7 @@ from .models import Profile, GroupProfile
from circle.settings.base import LANGUAGES, MAX_NODE_RAM
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)
......@@ -908,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()
......@@ -952,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
......@@ -1236,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
......
......@@ -70,7 +70,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(),
......
......@@ -716,6 +716,10 @@ textarea[name="list-new-namelist"] {
margin-top: -6px;
}
#show-all-activities-container {
margin: 20px 0 0 10px;
}
.vm-resources-sliders .row {
margin-bottom: 15px;
}
......@@ -3,7 +3,7 @@
$(function() {
/* vm operations */
$('#ops, #vm-details-resources-disk, #vm-details-renew-op').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.btn', 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');
......@@ -320,12 +332,12 @@ 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)
check = true;
return check;
}
......@@ -340,25 +352,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']);
$("#ops").html(data['ops']);
$("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip();
$("#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 +390,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;
}
});
}
......@@ -81,4 +81,11 @@
{% block extra_etc %}
{% endblock %}
<script>
yourlabs.TextWidget.prototype.getValue = function(choice) {
return choice.children().html();
}
</script>
</html>
......@@ -18,23 +18,22 @@
{% endblock %}
{% block navbar %}
{% 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">
{% trans "Notifications" %}
{% if NEW_NOTIFICATIONS_COUNT > 0 %}
<span class="badge badge-pulse">{{ NEW_NOTIFICATIONS_COUNT }}</span>
{% endif %}
</a>
<ul class="dropdown-menu notification-messages">
<li>{% trans "Loading..." %}</li>
</ul>
</li>
</ul>
<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">
{% trans "Notifications" %}
{% if NEW_NOTIFICATIONS_COUNT > 0 %}
<span class="badge badge-pulse">{{ NEW_NOTIFICATIONS_COUNT }}</span>
{% endif %}
</a>
<ul class="dropdown-menu notification-messages">
<li>{% trans "Loading..." %}</li>
</ul>
</li>
</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>
......
......@@ -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>
{% include "dashboard/vm-detail/_activity-timeline.html" with active=object %}
</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 }}">
......
......@@ -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>
<div id="vm-details-pw-reset">
{% with op=op.password_reset %}{% if op %}
<a href="{{op.get_url}}" class="btn operation btn-default btn-xs" {% if op.disabled %}disabled{% endif %}>{% trans "Generate new password!" %}</a>
{% endif %}{% endwith %}
</div>
</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>
</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 %}
......@@ -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>
{% 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 %}
......@@ -280,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"
......@@ -288,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):
......
......@@ -24,8 +24,9 @@ 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 firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch
......@@ -142,33 +143,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):
......@@ -176,8 +165,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()
......@@ -187,9 +186,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):
......@@ -401,8 +403,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(
......@@ -421,8 +422,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(
......
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)
......@@ -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'
]
......@@ -642,6 +642,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 = [], []
......@@ -666,20 +673,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):
......@@ -719,24 +732,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.
"""
......
......@@ -33,8 +33,9 @@ from .tasks.local_tasks import (
)
from .models import (
Instance, InstanceActivity, InstanceTemplate, Interface, Node,
NodeActivity,
NodeActivity, pwgen
)
from .tasks import agent_tasks
logger = getLogger(__name__)
......@@ -94,12 +95,21 @@ class AddInterfaceOperation(InstanceOperation):
"the VM.")
required_perms = ()
def rollback(self, net, activity):
with activity.sub_activity(
'destroying_net',
readable_name=ugettext_noop("destroy network (rollback)")):
net.destroy()
net.delete()
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 _operation(self, activity, user, system, vlan, managed=None):
if not vlan.has_level(user, 'user'):
raise PermissionDenied()
if managed is None:
managed = vlan.managed
......@@ -107,12 +117,15 @@ class AddInterfaceOperation(InstanceOperation):
managed=managed, owner=user, vlan=vlan)
if self.instance.is_running:
with activity.sub_activity('attach_network'):
self.instance.attach_network(net)
try:
with activity.sub_activity('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'])
......@@ -867,3 +880,27 @@ 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 = ()
def check_precond(self):
super(PasswordResetOperation, self).check_precond()
if self.instance.status not in ["RUNNING"]:
raise self.instance.WrongStateError(self.instance)
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
......@@ -35,3 +35,4 @@ South==0.8.4
sqlparse==0.1.11
pika==0.9.13
Fabric==1.9.0
lxml==3.3.5
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