Commit 996d1360 by Czémán Arnold

Merge branch 'master' into ceph

Conflicts:
	circle/storage/models.py
parents a228d7ae 28154397
......@@ -535,7 +535,7 @@ LOCALE_PATHS = (join(SITE_ROOT, 'locale'), )
COMPANY_NAME = get_env_variable("COMPANY_NAME", "BME IK 2015")
first, last = get_env_variable(
'VNC_PORT_RANGE', '20000, 65536').replace(' ', '').split(',')
'VNC_PORT_RANGE', '50000, 60000').replace(' ', '').split(',')
VNC_PORT_RANGE = (int(first), int(last)) # inclusive start, exclusive end
graphite_host = environ.get("GRAPHITE_HOST", None)
......
......@@ -25,7 +25,7 @@ from django.shortcuts import redirect
from circle.settings.base import get_env_variable
from dashboard.views import circle_login, HelpView
from dashboard.views import circle_login, HelpView, ResizeHelpView
from dashboard.forms import CirclePasswordResetForm, CircleSetPasswordForm
from firewall.views import add_blacklist_item
......@@ -65,6 +65,8 @@ urlpatterns = patterns(
url(r'^info/support/$',
TemplateView.as_view(template_name="info/support.html"),
name="info.support"),
url(r'^info/resize-how-to/$', ResizeHelpView.as_view(),
name="info.resize"),
)
......
......@@ -175,8 +175,9 @@ class Operation(object):
raise ImproperlyConfigured(
"Set required_perms to () if none needed.")
if not user.has_perms(cls.required_perms):
raise PermissionDenied(
u"%s doesn't have the required permissions." % user)
raise humanize_exception(ugettext_noop(
"You don't have the required permissions."),
PermissionDenied())
if cls.superuser_required and not user.is_superuser:
raise humanize_exception(ugettext_noop(
"Superuser privileges are required."), PermissionDenied())
......
......@@ -1244,7 +1244,7 @@ class MyProfileForm(forms.ModelForm):
class Meta:
fields = ('preferred_language', 'email_notifications',
'use_gravatar', )
'desktop_notifications', 'use_gravatar', )
model = Profile
@property
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0003_message'),
]
operations = [
migrations.AddField(
model_name='profile',
name='desktop_notifications',
field=models.BooleanField(default=False, help_text='Whether user wants to get desktop notification when an activity has finished and the window is not in focus.', verbose_name='Desktop notifications'),
),
]
......@@ -184,6 +184,10 @@ class Profile(Model):
email_notifications = BooleanField(
verbose_name=_("Email notifications"), default=True,
help_text=_('Whether user wants to get digested email notifications.'))
desktop_notifications = BooleanField(
verbose_name=_("Desktop notifications"), default=False,
help_text=_('Whether user wants to get desktop notification when an '
'activity has finished and the window is not in focus.'))
smb_password = CharField(
max_length=20,
verbose_name=_('Samba password'),
......
......@@ -169,6 +169,9 @@ $(function() {
);
} else {
in_progress = false;
if(document.hasFocus() === false && userWantNotifications()){
sendNotification(generateMessageFromLastActivity());
}
if(reload_vm_detail) location.reload();
if(runs > 1) addConnectText();
}
......@@ -181,6 +184,38 @@ $(function() {
}
});
// Notification init
$(function(){
if(userWantNotifications())
Notification.requestPermission();
});
function generateMessageFromLastActivity(){
var ac = $("div.activity").first();
var error = ac.children(".timeline-icon-failed").length;
var sign = (error === 1) ? "❌ " : "✓ ";
var msg = ac.children("strong").text().replace(/\s+/g, " ");
return sign + msg;
}
function sendNotification(message) {
var options = { icon: "/static/dashboard/img/favicon.png"};
if (Notification.permission === "granted") {
var notification = new Notification(message, options);
}
else if (Notification.permission !== "denied") {
Notification.requestPermission(function (permission) {
if (permission === "granted") {
var notification = new Notification(message, options);
}
});
}
}
function userWantNotifications(){
var dn = $("#user-options").data("desktop_notifications");
return dn === "True";
}
function addConnectText() {
var activities = $(".timeline .activity");
......@@ -192,7 +227,6 @@ function addConnectText() {
}
}
String.prototype.hashCode = function() {
var hash = 0, i, chr, len;
if (this.length === 0) return hash;
......
......@@ -608,3 +608,11 @@ $(function () {
"alert-" + $(this).val());
});
});
/* select all in template list */
$(function() {
$("#manage-access-select-all").click(function(e) {
var inputs = $(this).closest("table").find('input[type="checkbox"]');
inputs.prop("checked", !inputs.prop("checked"));
});
});
......@@ -284,7 +284,7 @@ a.hover-black {
}
.hover-black:hover {
color: black /*#d9534f*/;
color: black; /*#d9534f*/
text-decoration: none;
}
......@@ -1488,3 +1488,42 @@ textarea[name="new_members"] {
.acl-table td:first-child {
text-align: center;
}
#resize-help {
table {
background-color: #f5f5f5;
}
.panel {
padding: 2px 20px;
background-color: #f5f5f5;
margin: 20px 0px;
}
ol li {
margin-top: 15px;
}
img {
display: block;
margin: 15px 0 5px 0;
}
pre {
margin-top: 5px;
}
hr {
margin: 50px 0;
}
}
#vm-details-resize-how-to {
font-size: 1.5em;
text-align: center;
width: 100%;
}
#manage-access-select-all {
cursor: pointer;
}
......@@ -4,24 +4,33 @@
<i class="fa fa-file"></i>
{{ d.name }} (#{{ d.id }}) - {{ d.size|filesize }}
{% if op.remove_disk %}
<span class="operation-wrapper">
<a href="{{ op.remove_disk.get_url }}?disk={{d.pk}}"
class="btn btn-xs btn-{{ op.remove_disk.effect}} pull-right operation disk-remove-btn
{% if op.remove_disk.disabled %}disabled{% endif %}">
<i class="fa fa-{{ op.remove_disk.icon }} fa-fw-12"></i> {% trans "Remove" %}
</a>
</span>
{% endif %}
{% if op.resize_disk %}
<span class="operation-wrapper">
<span class="operation-wrapper pull-right">
{% if d.is_resizable %}
{% if op.resize_disk %}
<a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}"
class="btn btn-xs btn-{{ op.resize_disk.effect }} pull-right operation disk-resize-btn
class="btn btn-xs btn-{{ op.resize_disk.effect }} operation disk-resize-btn
{% if op.resize_disk.disabled %}disabled{% endif %}">
<i class="fa fa-{{ op.resize_disk.icon }} fa-fw-12"></i> {% trans "Resize" %}
</a>
</span>
{% endif %}
{% else %}
<a href="{% url "request.views.request-resize" vm_pk=instance.pk disk_pk=d.pk %}" class="btn btn-xs btn-primary operation">
<i class="fa fa-arrows-alt fa-fw-12"></i> {% trans "Request resize" %}
</a>
{% endif %}
{% else %}
<small class="btn-xs">
{% trans "Not resizable" %}
</small>
{% endif %}
{% if op.remove_disk %}
<a href="{{ op.remove_disk.get_url }}?disk={{d.pk}}"
class="btn btn-xs btn-{{ op.remove_disk.effect}} operation disk-remove-btn
{% if op.remove_disk.disabled %}disabled{% endif %}">
<i class="fa fa-{{ op.remove_disk.icon }} fa-fw-12"></i> {% trans "Remove" %}
</a>
{% endif %}
</span>
<div style="clear: both;"></div>
{% if request.user.is_superuser %}
......
......@@ -14,5 +14,4 @@
({% trans "username" %}: {{ user.username }})
{% endif %}
{% endif %}
{% endif %}
......@@ -6,7 +6,7 @@
<th></th>
<th>{% trans "Who" %}</th>
<th>{% trans "What" %}</th>
<th><i class="fa fa-times"></i></th>
<th><i id="manage-access-select-all" class="fa fa-times"></i></th>
</tr>
</thead>
<tbody>
......
......@@ -8,7 +8,7 @@
<a class="btn btn-default" href="{{object.get_absolute_url}}" data-dismiss="modal">
{% trans "Cancel" %}
</a>
{% if lease_types and not request.token_user %}
{% if object.active and lease_types and not request.token_user %}
<a class="btn btn-primary" id="vm-renew-request-lease-button"
href="{% url "request.views.request-lease" vm_pk=object.pk %}">
<i class="fa fa-forward"></i>
......
......@@ -12,6 +12,9 @@
{% block navbar %}
{% if request.user.is_authenticated and request.user.pk and not request.token_user %}
<span id="user-options" data-desktop_notifications="{{ request.user.profile.desktop_notifications }}"><span>
<ul class="nav navbar-nav navbar-right" id="dashboard-menu">
{% if request.user.is_superuser %}
{% if ADMIN_ENABLED %}
......
......@@ -13,7 +13,7 @@
<select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select>
<div class="input-group-btn">
<button type="submit" class="btn btn-success btn-sm
{% if not is_operator %}disabled{% endif %}">
{% if not op.add_port %}disabled{% endif %}">
<span class="hidden-xs">{% trans "Add" %}</span>
<span class="visible-xs"><i class="fa fa-plus-circle"></i></span>
</button>
......
......@@ -59,8 +59,8 @@
{% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %}
<span id="vm-details-renew-op">
{% with op=op.renew %}{% if op %}
<a href="{{op.get_url}}" class="btn btn-{{op.effect}} btn-xs
operation operation-{{op.op}}">
<a href="{{op.get_url}}" class="btn btn-xs operation operation-{{ op.op }}
{% if op.disabled %}btn-default disabled{% else %}btn-{{op.effect}}{% endif %}">
<i class="fa fa-{{op.icon}}"></i>
{{op.name}}
</a>
......
......@@ -18,7 +18,7 @@
<h3 class="list-group-item-heading dashboard-vm-details-network-h3">
<i class="fa fa-{% if i.host %}globe{% else %}link{% endif %}"></i> {{ i.vlan.name }}
{% if not i.host%}({% trans "unmanaged" %}){% endif %}
{% if user.is_superuser %}
{% if user.is_superuser and i.host %}
<a href="{{ i.host.get_absolute_url }}"
class="btn btn-default btn-xs">{% trans "edit" %}</a>
{% endif %}
......@@ -83,7 +83,8 @@
<span class="operation-wrapper">
<a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}"
class="btn btn-link btn-xs operation"
title="{% trans "Remove" %}">
title="{% trans "Remove" %}"
{% if not op.remove_port %}disabled{% endif %}>
<i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i>
</a>
</span>
......@@ -118,7 +119,9 @@
{{ l.private }}/{{ l.proto }}
</td>
<td>
<a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv6.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a>
<a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv6.pk }}" title="{% trans "Remove" %}" {% if not op.remove_port %}disabled{% endif %}>
<i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i>
</a>
</td>
</tr>
{% endif %}
......
......@@ -66,6 +66,17 @@
{% endfor %}
</div>
<hr />
{% if instance.disks.all %}
<div id="vm-details-resize-how-to">
<i class="fa fa-question"></i>
{% url "info.resize" as resize_url %}
{% blocktrans with url=resize_url %}
If you need help resizing the disks check out our <a href="{{ url }}">resize how-to.</a>
{% endblocktrans %}
</div>
{% endif %}
{% if user.is_superuser %}
<hr/>
......
......@@ -270,33 +270,6 @@ class VmDetailTest(LoginMixin, MockCeleryMixin, TestCase):
self.assertEqual(InstanceTemplate.objects.get(id=1).raw_data,
"<devices></devices>")
def test_permitted_lease_delete_w_template_using_it(self):
c = Client()
self.login(c, 'superuser')
leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/1/")
self.assertEqual(response.status_code, 400)
self.assertEqual(leases, Lease.objects.count())
def test_permitted_lease_delete_w_template_not_using_it(self):
c = Client()
self.login(c, 'superuser')
lease = Lease.objects.create(name="yay")
leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/%d/" % lease.pk)
self.assertEqual(response.status_code, 302)
self.assertEqual(leases - 1, Lease.objects.count())
def test_unpermitted_lease_delete(self):
c = Client()
self.login(c, 'user1')
leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/1/")
# redirect to the login page
self.assertEqual(response.status_code, 403)
self.assertEqual(leases, Lease.objects.count())
def test_notification_read(self):
c = Client()
self.login(c, "user1")
......@@ -1758,3 +1731,76 @@ class SshKeyTest(LoginMixin, TestCase):
resp = c.post("/dashboard/sshkey/delete/1/")
self.assertEqual(403, resp.status_code)
class LeaseDetailTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', ]
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
self.u2 = User.objects.create(username='user2', is_staff=True)
self.u2.set_password('password')
self.u2.save()
self.us = User.objects.create(username='superuser', is_superuser=True)
self.us.set_password('password')
self.us.save()
def tearDown(self):
super(LeaseDetailTest, self).tearDown()
self.u1.delete()
self.u2.delete()
self.us.delete()
def test_anon_view(self):
c = Client()
response = c.get("/dashboard/lease/1/")
self.assertEqual(response.status_code, 302)
def test_unpermitted_view(self):
c = Client()
self.login(c, 'user1')
response = c.get("/dashboard/lease/1/")
self.assertEqual(response.status_code, 302)
def test_operator_view(self):
c = Client()
self.login(c, 'user2')
lease = Lease.objects.get()
lease.set_level(self.u2, "owner")
response = c.get("/dashboard/lease/1/")
self.assertEqual(response.status_code, 200)
def test_superuser_view(self):
c = Client()
self.login(c, 'superuser')
response = c.get("/dashboard/lease/1/")
self.assertEqual(response.status_code, 200)
def test_permitted_lease_delete_w_template_using_it(self):
c = Client()
self.login(c, 'superuser')
leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/1/")
self.assertEqual(response.status_code, 400)
self.assertEqual(leases, Lease.objects.count())
def test_permitted_lease_delete_w_template_not_using_it(self):
c = Client()
self.login(c, 'superuser')
lease = Lease.objects.create(name="yay")
leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/%d/" % lease.pk)
self.assertEqual(response.status_code, 302)
self.assertEqual(leases - 1, Lease.objects.count())
def test_unpermitted_lease_delete(self):
c = Client()
self.login(c, 'user1')
leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/1/")
# redirect to the login page
self.assertEqual(response.status_code, 403)
self.assertEqual(leases, Lease.objects.count())
......@@ -136,6 +136,10 @@ class HelpView(TemplateView):
return ctx
class ResizeHelpView(TemplateView):
template_name = "info/resize.html"
class OpenSearchDescriptionView(TemplateView):
template_name = "dashboard/vm-opensearch.xml"
content_type = "application/opensearchdescription+xml"
......
......@@ -30,7 +30,9 @@ from django.core.urlresolvers import reverse
from django.contrib import messages
from django.contrib.auth.views import redirect_to_login
from django.db.models import Q
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.http import (
HttpResponse, Http404, HttpResponseRedirect, JsonResponse
)
from django.shortcuts import redirect, render
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django.views.generic import DetailView, View, DeleteView
......@@ -343,12 +345,9 @@ class AjaxOperationMixin(object):
store.used = True
else:
store = []
return HttpResponse(
json.dumps({'success': True,
return JsonResponse({'success': True,
'with_reload': self.with_reload,
'messages': [unicode(m) for m in store]}),
content_type="application=json"
)
'messages': [unicode(m) for m in store]})
else:
return resp
......@@ -378,11 +377,8 @@ class FormOperationMixin(object):
resp = super(FormOperationMixin, self).post(
request, extra, *args, **kwargs)
if request.is_ajax():
return HttpResponse(
json.dumps({
'success': True,
'with_reload': self.with_reload}),
content_type="application=json")
return JsonResponse({'success': True,
'with_reload': self.with_reload})
else:
return resp
else:
......
......@@ -28,7 +28,9 @@ from django.contrib.auth.decorators import login_required
from django.core import signing
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse, reverse_lazy
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.http import (
HttpResponse, Http404, HttpResponseRedirect, JsonResponse
)
from django.shortcuts import redirect, get_object_or_404
from django.template import RequestContext
from django.template.loader import render_to_string
......@@ -102,6 +104,19 @@ class VmDetailView(GraphMixin, CheckedDetailView):
template_name = "dashboard/vm-detail.html"
model = Instance
def get(self, *args, **kwargs):
if self.request.is_ajax():
return JsonResponse(self.get_json_data())
else:
return super(VmDetailView, self).get(*args, **kwargs)
def get_json_data(self):
instance = self.get_object()
return {"status": instance.status,
"host": instance.get_connect_host(),
"port": instance.get_connect_port(),
"password": instance.pw}
def get_context_data(self, **kwargs):
context = super(VmDetailView, self).get_context_data(**kwargs)
instance = context['instance']
......@@ -275,10 +290,7 @@ class VmDetailView(GraphMixin, CheckedDetailView):
message = u"Not success"
if request.is_ajax():
return HttpResponse(
json.dumps({'message': message}),
content_type="application=json"
)
return JsonResponse({'message': message})
else:
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.object.pk}))
......@@ -578,11 +590,8 @@ class VmResourcesChangeView(VmOperationView):
if request.is_ajax(): # this is not too nice
store = messages.get_messages(request)
store.used = True
return HttpResponse(
json.dumps({'success': False,
'messages': [unicode(m) for m in store]}),
content_type="application=json"
)
return JsonResponse({'success': False,
'messages': [unicode(m) for m in store]})
else:
return HttpResponseRedirect(instance.get_absolute_url() +
"#resources")
......
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals, absolute_import
from django.core.management.base import BaseCommand, CommandError
import logging
from firewall.models import Firewall, VlanGroup, Rule
from django.contrib.auth.models import User
logger = logging.getLogger(__name__)
class Command(BaseCommand):
def add_arguments(self, parser):
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--port',
action='store',
dest='port',
type=int,
help='port which will open (0-65535)')
group.add_argument('--port-range',
action='store',
dest='range',
type=int,
nargs=2,
help='closed port range which will open (0-65535)',
metavar=('LOWER', 'HIGHER'))
parser.add_argument('--protocol',
action='store',
dest='proto',
required=True,
choices=('tcp', 'udp', 'icmp'),
help='protocol name')
parser.add_argument('--action',
action='store',
dest='action',
default='accept',
choices=('accept', 'drop', 'ignore'),
help='action of the rule')
parser.add_argument('--dir',
action='store',
dest='dir',
default='in',
choices=('in', 'out'),
help='direction of the rule')
parser.add_argument('--firewall',
action='store',
dest='firewall',
required=True,
help='firewall name which open the given port')
parser.add_argument('--vlan-group',
action='store',
dest='vlan_group',
required=True,
help='vlan group name where the port will open')
parser.add_argument('--owner',
action='store',
dest='owner',
required=True,
help='name of user who owns the rule')
def handle(self, *args, **options):
port = options['port']
range = options['range']
proto = options['proto']
action = options['action']
dir = options['dir']
owner = options['owner']
firewall = options['firewall']
fnet = options['vlan_group']
try:
owner = User.objects.get(username=owner)
firewall = Firewall.objects.get(name=firewall)
fnet = VlanGroup.objects.get(name=fnet)
except User.DoesNotExist:
raise CommandError("User '%s' does not exist" % owner)
except Firewall.DoesNotExist:
raise CommandError("Firewall '%s' does not exist" % firewall)
except VlanGroup.DoesNotExist:
raise CommandError("VlanGroup '%s' does not exist" % fnet)
if port:
self.validate_port(port)
try:
rule = self.make_rule(dport=port, proto=proto, action=action,
direction=dir, owner=owner,
firewall=firewall, foreign_network=fnet)
rule.save()
except Warning as e:
logger.warning(e)
else:
lower = min(range)
higher = max(range)
self.validate_port(lower)
self.validate_port(higher)
rules = []
for port in xrange(lower, higher+1):
try:
rule = self.make_rule(port, proto, action, dir,
owner, firewall, fnet)
rules.append(rule)
except Warning as e:
logger.warning(e)
Rule.objects.bulk_create(rules)
def make_rule(self, **kwargs):
rule, created = Rule.objects.get_or_create(**kwargs)
if not created:
raise Warning(('Rule does exist: %s' %
unicode(rule)).encode('utf-8'))
rule.full_clean()
return rule
def validate_port(self, port):
if port < 0 or port > 65535:
raise CommandError("Port '%i' not in range [0-65535]" % port)
......@@ -2576,7 +2576,7 @@ msgid ""
" "
msgstr ""
"\n"
"A Tulajdonos szint minden műveletet engedélyez. A Tulajdonosok adhatnak/visszavonhatnak Felhasználó, Operátor is Tulajdonos szintű hozzáféréseket. A virtuális gépért felelős tulajdonos nem fokozható le. A felelős tulajdonosi cím átruházható másik felhasználó számára, a \"Tulajdon átruházása\" gombbal."
"A Tulajdonos szint minden műveletet engedélyez. A Tulajdonosok adhatnak/visszavonhatnak Felhasználó, Operátor és Tulajdonos szintű hozzáféréseket. A virtuális gépért felelős tulajdonos nem fokozható le. A felelős tulajdonosi cím átruházható másik felhasználó számára, a \"Tulajdon átruházása\" gombbal."
#: dashboard/templates/dashboard/vm-detail/console.html:6
msgid "You are not authorized to access the VNC console."
......
......@@ -22,6 +22,8 @@ from django.utils.translation import ugettext_lazy as _
from django.template import RequestContext
from django.template.loader import render_to_string
from sizefield.widgets import FileSizeWidget
from sizefield.utils import filesizeformat
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
......@@ -70,34 +72,32 @@ class InitialFromFileMixin(object):
RequestContext(request, {}),
)
def clean(self):
cleaned_data = super(InitialFromFileMixin, self).clean()
if cleaned_data['message'].strip() == self.initial['message'].strip():
raise ValidationError(
_("Fill in the message."),
code="invalid")
return cleaned_data
def clean_message(self):
message = self.cleaned_data['message']
if message.strip() == self.initial['message'].strip():
raise ValidationError(_("Fill in the message."), code="invalid")
return message.strip()
class TemplateRequestForm(InitialFromFileMixin, Form):
message = CharField(widget=Textarea, label=_("Message"))
template = ModelChoiceField(TemplateAccessType.objects.all(),
label=_("Template share"))
level = ChoiceField(TemplateAccessAction.LEVELS, widget=RadioSelect,
initial=TemplateAccessAction.LEVELS.user)
message = CharField(widget=Textarea, label=_("Message"))
initial_template = "request/initials/template.html"
class LeaseRequestForm(InitialFromFileMixin, Form):
lease = ModelChoiceField(LeaseType.objects.all(), label=_("Lease"))
message = CharField(widget=Textarea)
message = CharField(widget=Textarea, label=_("Message"))
initial_template = "request/initials/lease.html"
class ResourceRequestForm(InitialFromFileMixin, VmResourcesForm):
message = CharField(widget=Textarea)
message = CharField(widget=Textarea, label=_("Message"))
initial_template = "request/initials/resources.html"
......@@ -110,3 +110,28 @@ class ResourceRequestForm(InitialFromFileMixin, VmResourcesForm):
raise ValidationError(
_("You haven't changed any of the resources."),
code="invalid")
class ResizeRequestForm(InitialFromFileMixin, Form):
message = CharField(widget=Textarea, label=_("Message"))
size = CharField(widget=FileSizeWidget, label=_('Size'),
help_text=_('Size to resize the disk in bytes or with'
' units like MB or GB.'))
initial_template = "request/initials/resize.html"
def __init__(self, *args, **kwargs):
self.disk = kwargs.pop("disk")
super(ResizeRequestForm, self).__init__(*args, **kwargs)
def clean_size(self):
cleaned_data = super(ResizeRequestForm, self).clean()
disk = self.disk
size_in_bytes = cleaned_data.get("size")
if not size_in_bytes.isdigit() and len(size_in_bytes) > 0:
raise ValidationError(_("Invalid format, you can use GB or MB!"))
if int(size_in_bytes) < int(disk.size):
raise ValidationError(_("Disk size must be greater than the actual"
"size (%s).") % filesizeformat(disk.size))
return size_in_bytes
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import sizefield.models
class Migration(migrations.Migration):
dependencies = [
('vm', '0002_interface_model'),
('storage', '0002_disk_bus'),
('request', '0003_auto_20150410_1917'),
]
operations = [
migrations.CreateModel(
name='DiskResizeAction',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('size', sizefield.models.FileSizeField(default=None, null=True)),
('disk', models.ForeignKey(to='storage.Disk')),
('instance', models.ForeignKey(to='vm.Instance')),
],
options={
'abstract': False,
},
),
migrations.AlterField(
model_name='request',
name='type',
field=models.CharField(max_length=10, choices=[(b'resource', 'resource request'), (b'lease', 'lease request'), (b'template', 'template access request'), (b'resize', 'disk resize request')]),
),
]
......@@ -32,10 +32,14 @@ from django.utils.translation import (
from django.core.urlresolvers import reverse
import requests
from sizefield.models import FileSizeField
from model_utils.models import TimeStampedModel
from model_utils import Choices
from sizefield.utils import filesizeformat
from vm.models import Instance, InstanceTemplate, Lease
from vm.operations import ResourcesOperation, ResizeDiskOperation
from storage.models import Disk
logger = logging.getLogger(__name__)
......@@ -49,6 +53,9 @@ class RequestAction(Model):
def accept_msg(self):
raise NotImplementedError
def is_acceptable(self):
return True
class Meta:
abstract = True
......@@ -77,6 +84,7 @@ class Request(TimeStampedModel):
('resource', _('resource request')),
('lease', _("lease request")),
('template', _("template access request")),
('resize', _("disk resize request")),
)
type = CharField(choices=TYPES, max_length=10)
message = TextField(verbose_name=_("Message"))
......@@ -99,7 +107,8 @@ class Request(TimeStampedModel):
return {
'resource': "tasks",
'lease': "clock-o",
'template': "puzzle-piece"
'template': "puzzle-piece",
'resize': "arrows-alt",
}.get(self.type)
def get_effect(self):
......@@ -143,6 +152,10 @@ class Request(TimeStampedModel):
decline_msg, url=self.get_absolute_url(), reason=self.reason,
)
@property
def is_acceptable(self):
return self.action.is_acceptable()
class LeaseType(RequestType):
lease = ForeignKey(Lease, verbose_name=_("Lease"))
......@@ -200,6 +213,9 @@ class ResourceChangeAction(RequestAction):
'priority': self.priority,
}
def is_acceptable(self):
return self.instance.status in ResourcesOperation.accept_states
class ExtendLeaseAction(RequestAction):
instance = ForeignKey(Instance)
......@@ -246,6 +262,30 @@ class TemplateAccessAction(RequestAction):
) % ", ".join([x.name for x in self.template_type.templates.all()])
class DiskResizeAction(RequestAction):
instance = ForeignKey(Instance)
disk = ForeignKey(Disk)
size = FileSizeField(null=True, default=None)
def accept(self, user):
self.instance.resize_disk(disk=self.disk, size=self.size, user=user)
@property
def accept_msg(self):
return _(
'The disk <em class="text-muted">%(disk_name)s (#%(id)d)</em> of '
'<a href="%(url)s">%(vm_name)s</a> got resized. '
'The new size is: %(bytes)d bytes (%(size)s).'
) % {'disk_name': self.disk.name, 'id': self.disk.id,
'url': self.instance.get_absolute_url(),
'vm_name': self.instance.name,
'bytes': self.size, 'size': filesizeformat(self.size),
}
def is_acceptable(self):
return self.instance.status in ResizeDiskOperation.accept_states
def send_notifications(sender, instance, created, **kwargs):
if not created:
return
......
{% load i18n %}
{% load crispy_forms_tags %}
{% load sizefieldtags %}
<dl>
<dt>{% trans "Virtual machine" %}</dt>
<dd><a href="{{ vm.get_absolute_url }}">{{ vm.name }}</a></dd>
<dt>{% trans "Disk" %}</dt>
<dd>
{% if request.user.is_superuser %}
<a href="{{ disk.get_absolute_url }}">{{ disk.name }} (#{{ disk.id }})</a>
{% else %}
{{ disk.name }} (#{{ disk.id }})
{% endif %}
- {{ disk.size|filesize }}
</dd>
</dl>
<form action="{% url "request.views.request-resize" vm_pk=vm.pk disk_pk=disk.pk %}" method="POST">
{% include "display-form-errors.html" %}
{% csrf_token %}
{{ form.size|as_crispy_field }}
{{ form.message|as_crispy_field }}
<input type="submit" class="btn btn-primary" id="op-form-send"/>
</form>
......@@ -3,6 +3,7 @@
{% load i18n %}
{% load render_table from django_tables2 %}
{% load arrowfilter %}
{% load sizefieldtags %}
{% block title-page %}{% trans "Request" %}{% endblock %}
......@@ -65,6 +66,15 @@
<dd>{{ action.get_readable_level }}</dd>
</dl>
{% elif object.type == "resource" %}
{% if not is_acceptable %}
<div class="alert alert-warning">
{% blocktrans %}
To change the resources the virtual machine must be in one of the following states:
STOPPED, PENDING, RUNNING. If the virtual machine is running it will be
automatically stopped when accepting the request.
{% endblocktrans %}
</div>
{% endif %}
<dl>
<dt>{% trans "VM name" %}</dt>
<dd><a href="{{ action.instance.get_absolute_url }}">{{ action.instance.name }}</a></dd>
......@@ -74,7 +84,7 @@
{{ action.instance.get_status_display|upper }}
</dd>
<dt>{% trans "VM description" %}</dt>
<dd>{{ action.instance.description }}</dd>
<dd>{{ action.instance.description|default:"-" }}</dd>
<dt>
{% trans "Priority" %}
<span class="text-muted" style="font-weight: normal;">{% trans "(old values in parentheses)" %}</span>
......@@ -85,8 +95,39 @@
<dt>{% trans "Ram size" %}</dt>
<dd>{{ action.ram_size }} ({{ action.instance.ram_size }}) MiB</dd>
</dl>
{% elif object.type == "resize" %}
{% if not is_acceptable %}
<div class="alert alert-warning">
{% trans "To resize the disk the virtual machine must be in RUNNING state." %}
</div>
{% endif %}
<dl>
<dt>{% trans "VM name" %}</dt>
<dd><a href="{{ action.instance.get_absolute_url }}">{{ action.instance.name }}</a></dd>
<dt>{% trans "Status" %}</dt>
<dd>
<i class="fa {{ action.instance.get_status_icon }}"></i>
{{ action.instance.get_status_display|upper }}
</dd>
<dt>{% trans "VM description" %}</dt>
<dd>{{ action.instance.description|default:"-" }}</dd>
<dt>{% trans "Disk" %}</dt>
<dd>
{% if request.user.is_superuser %}
<a href="{{ action.disk.get_absolute_url }}">
{{ action.disk.name }} (#{{ action.disk.id}})
</a>
{% else %}
{{ action.disk.name }} (#{{ action.disk.id}})</dd>
{% endif %}
</dd>
<dt>{% trans "Current size" %}</dt>
<dd>{{ action.disk.size|filesize}} ({{ action.disk.size }} bytes)</dd>
<dt>{% trans "Requested size" %}</dt>
<dd>{{ action.size|filesize}} ({{ action.size }} bytes)</dd>
</dl>
{% else %}
hacks!!!
Are you adding a new action type?
{% endif %}
{% if object.status == "PENDING" and request.user.is_superuser %}
......@@ -103,7 +144,7 @@
{% trans "Decline" %}
</button>
</form>
{% if object.type == "resource" and action.instance.status not in accept_states %}
{% if not is_acceptable %}
{% trans "You can't accept this request because of the VM's state." %}
{% else %}
<form method="POST">
......
{% spaceless %}
{% if LANGUAGE_CODE == "en" %}
Why do you need a bigger disk?
{% else %} {# place your translations here #}
Why do you need a bigger disk?
{% endif %}
{% endspaceless %}
......@@ -23,7 +23,7 @@ from .views import (
LeaseTypeCreate, LeaseTypeDetail,
TemplateAccessTypeCreate, TemplateAccessTypeDetail,
TemplateRequestView, LeaseRequestView, ResourceRequestView,
LeaseTypeDelete, TemplateAccessTypeDelete,
LeaseTypeDelete, TemplateAccessTypeDelete, ResizeRequestView,
)
urlpatterns = patterns(
......@@ -60,4 +60,6 @@ urlpatterns = patterns(
name="request.views.request-lease"),
url(r'resource/(?P<vm_pk>\d+)/$', ResourceRequestView.as_view(),
name="request.views.request-resource"),
url(r'resize/(?P<vm_pk>\d+)/(?P<disk_pk>\d+)/$',
ResizeRequestView.as_view(), name="request.views.request-resize"),
)
......@@ -19,27 +19,29 @@ from __future__ import unicode_literals, absolute_import
from django.views.generic import (
UpdateView, TemplateView, DetailView, CreateView, FormView, DeleteView,
)
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.shortcuts import redirect, get_object_or_404
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.http import JsonResponse
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from django_tables2 import SingleTableView
from request.models import (
Request, TemplateAccessType, LeaseType, TemplateAccessAction,
ExtendLeaseAction, ResourceChangeAction,
ExtendLeaseAction, ResourceChangeAction, DiskResizeAction
)
from storage.models import Disk
from vm.models import Instance
from vm.operations import ResourcesOperation
from request.tables import (
RequestTable, TemplateAccessTypeTable, LeaseTypeTable,
)
from request.forms import (
LeaseTypeForm, TemplateAccessTypeForm, TemplateRequestForm,
LeaseRequestForm, ResourceRequestForm,
LeaseRequestForm, ResourceRequestForm, ResizeRequestForm,
)
......@@ -93,7 +95,7 @@ class RequestDetail(LoginRequiredMixin, DetailView):
context = super(RequestDetail, self).get_context_data(**kwargs)
context['action'] = request.action
context['accept_states'] = ResourcesOperation.accept_states
context['is_acceptable'] = request.is_acceptable
# workaround for http://git.io/vIIYi
context['request'] = self.request
......@@ -167,6 +169,7 @@ class RequestTypeList(LoginRequiredMixin, SuperuserRequiredMixin,
class TemplateRequestView(LoginRequiredMixin, FormView):
form_class = TemplateRequestForm
template_name = "request/request-template.html"
success_message = _("Request successfully sent.")
def get_form_kwargs(self):
kwargs = super(TemplateRequestView, self).get_form_kwargs()
......@@ -192,7 +195,8 @@ class TemplateRequestView(LoginRequiredMixin, FormView):
)
req.save()
return redirect("/")
messages.success(self.request, self.success_message)
return redirect(reverse("dashboard.index"))
class VmRequestMixin(LoginRequiredMixin, object):
......@@ -204,6 +208,12 @@ class VmRequestMixin(LoginRequiredMixin, object):
user = self.request.user
if not vm.has_level(user, self.user_level):
raise PermissionDenied()
if vm.destroyed_at:
message = _("Instance %(instance)s has already been destroyed.")
messages.error(self.request, message % {'instance': vm.name})
return redirect(vm.get_absolute_url())
return super(VmRequestMixin, self).dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
......@@ -224,6 +234,7 @@ class LeaseRequestView(VmRequestMixin, FormView):
form_class = LeaseRequestForm
template_name = "request/request-lease.html"
user_level = "operator"
success_message = _("Request successfully sent.")
def form_valid(self, form):
data = form.cleaned_data
......@@ -244,6 +255,7 @@ class LeaseRequestView(VmRequestMixin, FormView):
)
req.save()
messages.success(self.request, self.success_message)
return redirect(vm.get_absolute_url())
......@@ -251,6 +263,7 @@ class ResourceRequestView(VmRequestMixin, FormView):
form_class = ResourceRequestForm
template_name = "request/request-resource.html"
user_level = "user"
success_message = _("Request successfully sent.")
def get_form_kwargs(self):
kwargs = super(ResourceRequestView, self).get_form_kwargs()
......@@ -287,4 +300,60 @@ class ResourceRequestView(VmRequestMixin, FormView):
)
req.save()
messages.success(self.request, self.success_message)
return redirect(vm.get_absolute_url())
class ResizeRequestView(VmRequestMixin, FormView):
form_class = ResizeRequestForm
template_name = "request/_request-resize-form.html"
user_level = "owner"
success_message = _("Request successfully sent.")
def get_disk(self, *args, **kwargs):
disk = get_object_or_404(Disk, pk=self.kwargs['disk_pk'])
if disk not in self.get_vm().disks.all():
raise SuspiciousOperation
return disk
def get_form_kwargs(self):
kwargs = super(ResizeRequestView, self).get_form_kwargs()
kwargs['disk'] = self.get_disk()
return kwargs
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_modal.html']
else:
return ['dashboard/_base.html']
def get_context_data(self, **kwargs):
context = super(ResizeRequestView, self).get_context_data(**kwargs)
context['disk'] = self.get_disk()
context['template'] = self.template_name
context['box_title'] = context['title'] = _("Disk resize request")
context['ajax_title'] = True
return context
def form_valid(self, form):
disk = self.get_disk()
if not disk.is_resizable:
raise SuspiciousOperation
vm = self.get_vm()
data = form.cleaned_data
user = self.request.user
dra = DiskResizeAction(instance=vm, disk=disk, size=data['size'])
dra.save()
req = Request(user=user, message=data['message'], action=dra,
type=Request.TYPES.resize)
req.save()
if self.request.is_ajax():
return JsonResponse({'success': True,
'messages': [self.success_message]})
else:
messages.success(self.request, self.success_message)
return redirect(vm.get_absolute_url())
......@@ -29,6 +29,7 @@ from celery.contrib.abortable import AbortableAsyncResult
from django.db.models import (Model, BooleanField, CharField, DateTimeField,
ForeignKey, IntegerField, ManyToManyField)
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import reverse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from model_utils.models import TimeStampedModel
......@@ -679,3 +680,10 @@ class Disk(TimeStampedModel):
disk.is_ready = True
disk.save()
return disk
def get_absolute_url(self):
return reverse('dashboard.views.disk-detail', kwargs={'pk': self.pk})
@property
def is_resizable(self):
return self.type in ('qcow2-norm', 'raw-rw', 'qcow2-snap', )
......@@ -708,7 +708,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def _is_suspend_expiring(self, threshold=0.1):
interval = self.lease.suspend_interval
if self.time_of_suspend is not None and interval is not None:
if (self.status != "SUSPENDED" and
self.time_of_suspend is not None and interval is not None):
limit = timezone.now() + timedelta(seconds=(
threshold * self.lease.suspend_interval.total_seconds()))
return limit > self.time_of_suspend
......
......@@ -314,6 +314,9 @@ class ResizeDiskOperation(RemoteInstanceOperation):
size=filesizeformat(kwargs['size']), name=kwargs['disk'].name)
def _operation(self, disk, size):
if not disk.is_resizable:
raise HumanReadableException.create(ugettext_noop(
'Disk type "%(type)s" is not resizable.'), type=disk.type)
super(ResizeDiskOperation, self)._operation(disk=disk, size=size)
disk.size = size
disk.save()
......@@ -642,6 +645,7 @@ class RemovePortOperation(InstanceOperation):
name = _("close port")
description = _("Close the specified port.")
concurrency_check = False
acl_level = "operator"
required_perms = ('vm.config_ports', )
def _operation(self, activity, rule):
......@@ -660,6 +664,7 @@ class AddPortOperation(InstanceOperation):
name = _("open port")
description = _("Open the specified port.")
concurrency_check = False
acl_level = "operator"
required_perms = ('vm.config_ports', )
def _operation(self, activity, host, proto, port):
......@@ -873,7 +878,9 @@ class ShutOffOperation(InstanceOperation):
def _operation(self, activity):
# Shutdown networks
with activity.sub_activity('shutdown_net'):
with activity.sub_activity('shutdown_net',
readable_name=ugettext_noop(
"shutdown network")):
self.instance.shutdown_net()
self.instance._delete_vm(parent_activity=activity)
......
[Unit]
Description=CIRCLE portal
After=network.target
[Service]
User=cloud
Group=cloud
WorkingDirectory=/home/cloud/circle/circle
ExecStart=/bin/bash -c "source /etc/profile; workon circle; exec /home/cloud/.virtualenvs/circle/bin/uwsgi --chdir=/home/cloud/circle/circle -H /home/cloud/.virtualenvs/circle --socket /tmp/uwsgi.sock --wsgi-file circle/wsgi.py --chmod-socket=666"
Restart=always
[Install]
WantedBy=multi-user.target
amqp==1.4.6
anyjson==0.3.3
arrow==0.6.0
arrow==0.7.0
billiard==3.3.0.20
bpython==0.14.1
celery==3.1.18
Django==1.8.2
Django==1.8.12
django-appconf==1.0.1
django-autocomplete-light==2.1.1
django-braces==1.8.0
django-crispy-forms==1.4.0
django-crispy-forms==1.6.0
django-model-utils==2.2
djangosaml2==0.13.0
django-sizefield==0.7
......
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