Commit f5eb9edb by Kálmán Viktor

Merge branch 'master' into issue-397

parents 925cd76e ff91b0ff
......@@ -15,4 +15,8 @@
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from .test_acl import TestModel, Test2Model # noqa
from django.conf import settings
# https://code.djangoproject.com/ticket/7835
if settings.SETTINGS_MODULE == 'circle.settings.test':
from .test_acl import TestModel, Test2Model # noqa
......@@ -355,6 +355,7 @@ LOCAL_APPS = (
'manager',
'acl',
'monitor',
'request',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
......@@ -449,7 +450,7 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
)
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'djangosaml2.backends.Saml2Backend',
'common.backends.Saml2Backend',
)
remote_metadata = join(SITE_ROOT, 'remote_metadata.xml')
......@@ -527,6 +528,10 @@ except:
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 = (int(first), int(last)) # inclusive start, exclusive end
graphite_host = environ.get("GRAPHITE_HOST", None)
graphite_port = environ.get("GRAPHITE_PORT", None)
if graphite_host and graphite_port:
......@@ -555,3 +560,4 @@ ADMIN_ENABLED = False
BLACKLIST_PASSWORD = get_env_variable("BLACKLIST_PASSWORD", "")
BLACKLIST_HOOK_URL = get_env_variable("BLACKLIST_HOOK_URL", "")
REQUEST_HOOK_URL = get_env_variable("REQUEST_HOOK_URL", "")
......@@ -40,7 +40,8 @@ INSTALLED_APPS += (
)
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
path_to_selenium_test = os.path.expanduser('~/circle/circle/dashboard/tests/selenium')
path_to_selenium_test = os.path.join(SITE_ROOT, "dashboard/tests/selenium")
NOSE_ARGS = ['--stop', '--with-doctest', '--with-selenium-driver', '--selenium-driver=firefox', '-w%s' % path_to_selenium_test]
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
......
......@@ -56,6 +56,16 @@ LOGGING['handlers']['console'] = {'level': level,
'formatter': 'simple'}
for i in LOCAL_APPS:
LOGGING['loggers'][i] = {'handlers': ['console'], 'level': level}
# don't print SQL queries
LOGGING['handlers']['null'] = {'level': "DEBUG",
'class': "django.utils.log.NullHandler"}
LOGGING['loggers']['django.db.backends'] = {
'handlers': ['null'],
'propagate': False,
'level': 'DEBUG',
}
# Forbid store usage
STORE_URL = ""
......
......@@ -38,6 +38,7 @@ urlpatterns = patterns(
url(r'^network/', include('network.urls')),
url(r'^blacklist-add/', add_blacklist_item),
url(r'^dashboard/', include('dashboard.urls')),
url(r'^request/', include('request.urls')),
# django/contrib/auth/urls.py (care when new version)
url((r'^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/'
......@@ -87,3 +88,4 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
)
handler500 = 'common.views.handler500'
handler403 = 'common.views.handler403'
# -*- coding: utf-8 -*-
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
import re
from djangosaml2.backends import Saml2Backend as Saml2BackendBase
class Saml2Backend(Saml2BackendBase):
u"""
>>> b = Saml2Backend()
>>> b.clean_user_main_attribute(u'Ékezetes Enikő')
u'+00c9kezetes+0020Enik+0151'
>>> b.clean_user_main_attribute(u'Cé++')
u'C+00e9+002b+002b'
>>> b.clean_user_main_attribute(u'test')
u'test'
>>> b.clean_user_main_attribute(u'3+4')
u'3+002b4'
"""
def clean_user_main_attribute(self, main_attribute):
def replace(match):
match = match.group()
return '+%04x' % ord(match)
if isinstance(main_attribute, str):
main_attribute = main_attribute.decode('UTF-8')
assert isinstance(main_attribute, unicode)
return re.sub(r'[^\w.@-]', replace, main_attribute)
def _set_attribute(self, obj, attr, value):
if attr == 'username':
value = self.clean_user_main_attribute(value)
return super(Saml2Backend, self)._set_attribute(obj, attr, value)
......@@ -170,8 +170,8 @@ class Operation(object):
raise ImproperlyConfigured(
"Set required_perms to () if none needed.")
if not user.has_perms(cls.required_perms):
raise PermissionDenied("%s doesn't have the required permissions."
% user)
raise PermissionDenied(
u"%s doesn't have the required permissions." % user)
if cls.superuser_required and not user.is_superuser:
raise humanize_exception(ugettext_noop(
"Superuser privileges are required."), PermissionDenied())
......
......@@ -19,32 +19,42 @@ from sys import exc_info
import logging
from django.template import RequestContext
from django.shortcuts import render_to_response
from django.template import RequestContext
from .models import HumanReadableException
logger = logging.getLogger(__name__)
def handler500(request):
cls, exception, traceback = exc_info()
logger.exception("unhandled exception")
def get_context(request, exception):
ctx = {}
if isinstance(exception, HumanReadableException):
if issubclass(exception.__class__, HumanReadableException):
try:
ctx['error'] = exception.get_user_text()
if request.user.is_superuser:
ctx['error'] = exception.get_admin_text()
else:
ctx['error'] = exception.get_user_text()
except:
pass
else:
try:
if request.user.is_superuser():
ctx['error'] = exception.get_admin_text()
except:
pass
return ctx
def handler500(request):
cls, exception, traceback = exc_info()
logger.exception("unhandled exception")
ctx = get_context(request, exception)
try:
resp = render_to_response("500.html", ctx, RequestContext(request))
except:
resp = render_to_response("500.html", ctx)
resp.status_code = 500
return resp
def handler403(request):
cls, exception, traceback = exc_info()
ctx = get_context(request, exception)
resp = render_to_response("403.html", ctx)
resp.status_code = 403
return resp
......@@ -1395,6 +1395,7 @@
"vnc_port": 1234,
"num_cores": 2,
"status": "RUNNING",
"system": "system pls",
"modified": "2013-10-14T07:27:38.192Z"
}
},
......
......@@ -739,6 +739,7 @@ class LeaseForm(forms.ModelForm):
class Meta:
model = Lease
exclude = ()
class VmRenewForm(OperationForm):
......@@ -1604,6 +1605,7 @@ class DataStoreForm(ModelForm):
class Meta:
model = DataStore
fields = ("name", "path", "hostname", )
class DiskForm(ModelForm):
......@@ -1620,3 +1622,5 @@ class DiskForm(ModelForm):
class Meta:
model = Disk
fields = ("name", "filename", "datastore", "type", "bus", "size",
"base", "dev_num", "destroyed", "is_ready", )
......@@ -309,7 +309,7 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
attributes = kwargs.pop('attributes')
atr = settings.SAML_ORG_ID_ATTRIBUTE
try:
value = attributes[atr][0]
value = attributes[atr][0].upper()
except Exception as e:
value = None
logger.info("save_org_id couldn't find attribute. %s", unicode(e))
......@@ -339,7 +339,7 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
group, unicode(g))
g.user_set.add(sender)
for i in FutureMember.objects.filter(org_id=value):
for i in FutureMember.objects.filter(org_id__iexact=value):
i.group.user_set.add(sender)
i.delete()
......
......@@ -1272,8 +1272,46 @@ textarea[name="new_members"] {
margin-top: 20px;
}
#vm-renew-request-lease, #vm-request-resource-form {
display: none;
}
.label-100 {
display: block;
width: 100%;
}
#modify-the-resources {
font-size: 18px;
display: none;
}
#vm-request-resource-form textarea {
max-width: 500px;
height: 150px;
}
#disk-list-table {
td:last-child {
text-align: center;
}
}
#request-buttons {
form {
display: inline;
}
textarea {
resize: none;
min-height: 80px;
}
}
.nowrap {
white-space: nowrap;
}
.little-margin-bottom {
margin-bottom: 5px;
}
......@@ -38,6 +38,13 @@ $(function() {
e.preventDefault();
});
/* save as (close vnc console) */
$('.operation-save_as_template').click(function(e) {
if ($('li.active > a[href$="console"]').length > 0) {
$('a[data-toggle$="pill"][href$="#activity"]').click();
}
});
/* remove tag */
$('.vm-details-remove-tag').click(function() {
var to_remove = $.trim($(this).parent('div').text());
......@@ -223,4 +230,25 @@ $(function() {
return false;
});
$(document).on("click", "#vm-renew-request-lease-button", function(e) {
$("#vm-renew-request-lease").stop().slideToggle();
e.preventDefault();
});
$("#vm-request-resource").click(function(e) {
$(".cpu-priority-slider, .cpu-count-slider, .ram-slider").simpleSlider("setDisabled", false);
$(".ram-input, .cpu-count-input, .cpu-priority-input").prop("disabled", false);
$("#vm-details-resources-form").prop("action", $(this).prop("href"));
$("#vm-request-resource-form").show();
$("#modify-the-resources").show();
$(this).hide();
$("html, body").animate({
scrollTop: $("#modify-the-resources").offset().top - 60
});
return e.preventDefault();
});
});
......@@ -79,10 +79,26 @@
</div>
</div>
{% empty %}
{% trans "You can't start new virtual machines because no templates are shared with you." %}
{% if not template_access_types %}
{% trans "You can't start new virtual machines because no templates are shared with you." %}
{% else %}
{% trans "You can't start new virtual machines because no templates are shared with you however you can request them via the form below." %}
<hr />
{% include "request/_request-template-form.html" %}
{% endif %}
{% endfor %}
</div>
{% if templates and template_access_types %}
{% url "request.views.request-template" as request_url %}
<hr />
<p class="text-right">
{% blocktrans with url=request_url %}
Need other templates? Submit a new <a href="{{ url }}">request</a>.
{% endblocktrans %}
</p>
{% endif %}
<style>
.progress {
position: relative;
......
{% extends "dashboard/operate.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block formbuttons %}
<div class="pull-right">
<a class="btn btn-default" href="{{object.get_absolute_url}}" data-dismiss="modal">
{% trans "Cancel" %}
</a>
{% if lease_types %}
<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>
{% trans "Request longer lease" %}
</a>
{% endif %}
<button class="btn btn-{{ opview.effect }} btn-op-form-send" type="submit" id="op-form-send">
{% if opview.icon %}<i class="fa fa-fw fa-{{opview.icon}}"></i> {% endif %}{{ op.name|capfirst }}
</button>
</div>
{% endblock %}
{% block extra %}
<div class="clearfix"></div>
<div id="vm-renew-request-lease">
<hr />
{% include "request/_request-lease-form.html" with form=lease_request_form vm=object %}
</div>
{% endblock %}
......@@ -22,16 +22,29 @@
{% if user.is_superuser %}
{% if ADMIN_ENABLED %}
<li>
<a href="/admin/"><i class="fa fa-cogs"></i> {% trans "Admin" %}</a>
<a href="/admin/">
<i class="fa fa-cogs"></i>
<span class="hidden-sm">{% trans "Admin" %}</span>
</a>
</li>
{% endif %}
<li>
<a href="{% url "dashboard.views.storage" %}"><i class="fa fa-database"></i>
{% trans "Storage" %}
<a href="{% url "dashboard.views.storage" %}">
<i class="fa fa-database"></i>
<span class="hidden-sm">{% trans "Storage" %}</span>
</a>
</li>
<li>
<a href="/network/"><i class="fa fa-globe"></i> {% trans "Network" %}</a>
<a href="{% url "network.index" %}">
<i class="fa fa-globe"></i>
<span class="hidden-sm">{% trans "Network" %}</span>
</a>
</li>
<li>
<a href="{% url "request.views.request-list" %}">
<i class="fa fa-phone"></i>
<span class="hidden-sm">{% trans "Requests" %}</span>
</a>
</li>
{% endif %}
<li>
......
......@@ -54,7 +54,7 @@
<dt>{% trans "result" %}</dt>
<dd><textarea class="form-control">{{object.result|get_text:user}}</textarea></dd>
<dd><textarea class="form-control" id="activity_result_text">{{object.result|get_text:user}}</textarea></dd>
<dt>{% trans "resultant state" %}</dt>
<dd>{{object.resultant_state|default:'n/a'}}</dd>
......
......@@ -16,6 +16,7 @@ Do you want to perform the following operation on
{% crispy form %}
{% endif %}
{% endblock %}
{% block formbuttons %}
<div class="pull-right">
<a class="btn btn-default" href="{{object.get_absolute_url}}"
data-dismiss="modal">{% trans "Cancel" %}</a>
......@@ -23,4 +24,7 @@ Do you want to perform the following operation on
{% if opview.icon %}<i class="fa fa-fw fa-{{opview.icon}}"></i> {% endif %}{{ op.name|capfirst }}
</button>
</div>
{% endblock %}
</form>
{% block extra %}{% endblock %}
......@@ -2,6 +2,7 @@
{% load staticfiles %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load arrowfilter %}
{% block title-page %}{{ profile.username}} | {% trans "Profile" %}{% endblock %}
......@@ -42,6 +43,7 @@
{% trans "Email address" %}: {{ profile.email }}
{% endif %}
</p>
<p>{% trans "Last login" %}: <span title="{{ request.user.last_login }}">{{ request.user.last_login|arrowfilter:LANGUAGE_CODE}}</span></p>
{% if request.user == profile %}
<p>
{% trans "Use email address as Gravatar profile image" %}:
......
......@@ -5,7 +5,7 @@
{% for a in activities %}
<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}"
data-activity-id="{{ a.pk }}" data-activity-code="{{ a.activity_code }}">
data-activity-id="{{ a.pk }}" data-activity-code="{{ a.activity_code }}" data-timestamp="{{ a.started|date:"U" }}">
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
<i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-{{a.icon}}{% endif %}"></i>
</span>
......
......@@ -59,10 +59,11 @@
{% 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-success btn-xs
<a href="{{op.get_url}}" class="btn btn-{{op.effect}} btn-xs
operation operation-{{op.op}}">
<i class="fa fa-{{op.icon}}"></i>
{{op.name}} </a>
{{op.name}}
</a>
{% endif %}{% endwith %}
</span>
</h4>
......
......@@ -2,19 +2,42 @@
{% load sizefieldtags %}
{% load crispy_forms_tags %}
<div class="label label-info label-100" id="modify-the-resources">
{% trans "Modify the resources" %}
</div>
<form method="POST" action="{{ op.resources_change.get_url }}" id="vm-details-resources-form">
{% csrf_token %}
{% include "dashboard/_resources-sliders.html" with field_priority=resources_form.priority field_num_cores=resources_form.num_cores field_ram_size=resources_form.ram_size %}
{% if op.resources_change %}
<button type="submit" class="btn btn-success btn-sm change-resources-button"
id="vm-details-resources-save" data-vm="{{ instance.pk }}"
{% if op.resources_change.disabled %}disabled{% endif %}>
<i class="fa fa-floppy-o"></i> {% trans "Save resources" %}
</button>
<span class="change-resources-help"
{% if not op.resources_change.disabled %}style="display: none;"{% endif %}
>{% trans "Stop your VM to change resources." %}</span>
<button type="submit" class="btn btn-success btn-sm change-resources-button"
id="vm-details-resources-save" data-vm="{{ instance.pk }}"
{% if not save_resources_enabled %}disabled{% endif %}>
<i class="fa fa-floppy-o"></i> {% trans "Save resources" %}
</button>
<span class="change-resources-help"
{% if save_resources_enabled %}style="display: none;"{% endif %}>
{% trans "Stop your VM to change resources." %}
</span>
{% else %}
<div id="vm-request-resource-form">
<div class="alert alert-info text-justify">
{% trans "Changing resources is only possible on virtual machines with STOPPED state. We suggest to turn off the VM after submitting the request otherwise it will be automatically stopped in the future when the request is accepted." %}
</div>
<div class="form-group">
<label>{% trans "Message" %}*</label>
<textarea class="form-control" name="message">{% include "request/initials/resources.html" %}</textarea>
</div>
<input type="submit" class="btn btn-success btn-sm"/>
</div>
<a href="{% url "request.views.request-resource" vm_pk=object.pk %}"
class="btn btn-primary btn-sm" id="vm-request-resource">
<i class="fa fa-tasks"></i>
{% trans "Request resources" %}
</a>
{% endif %}
</form>
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
import random
class SeleniumConfig(object):
# How many sec can selenium wait till certain parts of a page appears
wait_max_sec = 10
# How much sec can pass before the activity is no longer happened recently
recently_sec = 90
# Name of the logger (necessary to override test logger)
logger_name = "selenium"
# File where the log should be stored
log_file = "selenium.log"
# Log file max size in Bytes
log_size = 1024 * 1024 * 10
# Format of the log file
log_format = "%(asctime)s: %(name)s: %(levelname)s: %(message)s"
# Backup count of the logfiles
log_backup = 5
# Accented letters from which selenium can choose to name stuff
accents = u"áéíöóúűÁÉÍÖÓÜÚŰ"
# Non accented letters from which selenium can choose to name stuff
valid_chars = "0123456789abcdefghijklmnopqrstvwxyz"
# First we choose 10 random normal letters
random_pass = "".join([random.choice(
valid_chars) for n in xrange(10)])
# Then we append it with 5 random accented one
random_pass += "".join([random.choice(
accents) for n in xrange(5)])
# Then we name our client as test_%(password)s
client_name = 'test_%s' % random_pass
# Which webpage should selenium use (localhost is recommended)
host = 'https://127.0.0.1'
# In default the tests create a new user then delete it afteword
# Disable this if selenium cannot acces the database
create_user = True
"""
Note: It's possible to setup that selenium uses a distant web server
for testing. If you choose this method you must provide a distant superuser
account info for that server by overriding random_pass and client_name by
uncommenting the lines below.
"""
# client_name = "user name here"
# random_pass = "password here"
......@@ -20,8 +20,7 @@ import json
# from unittest import skip
from django.test import TestCase
from django.test.client import Client
from django.contrib.auth.models import User, Group
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User, Group, Permission
from django.contrib.auth import authenticate
from common.tests.celery_mock import MockCeleryMixin
......
......@@ -228,7 +228,6 @@ urlpatterns = patterns(
url(r'^vm/opensearch.xml$', OpenSearchDescriptionView.as_view(),
name="dashboard.views.vm-opensearch"),
url(r'^storage/$', StorageDetail.as_view(),
name="dashboard.views.storage"),
url(r'^disk/(?P<pk>\d+)/$', DiskDetail.as_view(),
......
......@@ -13,3 +13,4 @@ from util import *
from vm import *
from graph import *
from storage import *
from request import *
......@@ -63,6 +63,8 @@ class GroupCodeMixin(object):
client = Saml2Client(conf, state_cache=state,
identity_cache=IdentityCache(request.session))
subject_id = _get_subject_id(request.session)
if not subject_id:
return newgroups
identity = client.users.get_identity(subject_id,
check_not_on_or_after=False)
if identity:
......@@ -144,7 +146,7 @@ class GroupDetailView(CheckedDetailView):
self.object.user_set.add(entity)
except User.DoesNotExist:
if saml_available:
FutureMember.objects.get_or_create(org_id=name,
FutureMember.objects.get_or_create(org_id=name.upper(),
group=self.object)
else:
messages.warning(request, _('User "%s" not found.') % name)
......
......@@ -441,7 +441,7 @@ class TransferTemplateOwnershipView(TransferOwnershipView):
confirm_view = TransferTemplateOwnershipConfirmView
model = InstanceTemplate
notification_msg = ugettext_noop(
'%(user)s offered you to take the ownership of '
'%(owner)s offered you to take the ownership of '
'his/her template called %(instance)s. '
'<a href="%(token)s" '
'class="btn btn-success btn-small">Accept</a>')
......
......@@ -70,7 +70,7 @@ def search_user(keyword):
return User.objects.get(username=keyword)
except User.DoesNotExist:
try:
return User.objects.get(profile__org_id=keyword)
return User.objects.get(profile__org_id__iexact=keyword)
except User.DoesNotExist:
return User.objects.get(email=keyword)
......@@ -610,7 +610,7 @@ class TransferOwnershipView(CheckedDetailView, DetailView):
new_owner.profile.notify(
ugettext_noop('Ownership offer'),
self.notification_msg,
{'instance': obj, 'token': token_path})
{'instance': obj, 'token': token_path, 'owner': request.user})
except Profile.DoesNotExist:
messages.error(request, _('Can not notify selected user.'))
else:
......@@ -665,8 +665,8 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
old.profile.notify(
ugettext_noop('Ownership accepted'),
ugettext_noop('Your ownership offer of %(instance)s has been '
'accepted by %(user)s.'),
{'instance': instance})
'accepted by %(owner)s.'),
{'instance': instance, 'owner': request.user})
return redirect(instance.get_absolute_url())
def get_instance(self, key, user):
......
......@@ -66,6 +66,8 @@ from ..forms import (
VmPortRemoveForm, VmPortAddForm,
VmRemoveInterfaceForm,
)
from request.models import TemplateAccessType, LeaseType
from request.forms import LeaseRequestForm, TemplateRequestForm
from ..models import Favourite
from manager.scheduler import has_traits
......@@ -171,6 +173,10 @@ class VmDetailView(GraphMixin, CheckedDetailView):
context['is_operator'] = is_operator
context['is_owner'] = is_owner
# operation also allows RUNNING (if with_shutdown is present)
context['save_resources_enabled'] = instance.status not in ("RUNNING",
"PENDING")
return context
def post(self, request, *args, **kwargs):
......@@ -651,10 +657,12 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
op = 'renew'
icon = 'calendar'
effect = 'info'
effect = 'success'
show_in_toolbar = False
form_class = VmRenewForm
wait_for_result = 0.5
template_name = 'dashboard/_vm-renew.html'
with_reload = True
def get_form_kwargs(self):
choices = Lease.get_objects_with_level("user", self.request.user)
......@@ -674,6 +682,12 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
instance.time_of_suspend)
return extra
def get_context_data(self, **kwargs):
context = super(VmRenewView, self).get_context_data(**kwargs)
context['lease_request_form'] = LeaseRequestForm(request=self.request)
context['lease_types'] = LeaseType.objects.exists()
return context
class VmStateChangeView(FormOperationMixin, VmOperationView):
op = 'emergency_change_state'
......@@ -1043,6 +1057,8 @@ class VmCreate(LoginRequiredMixin, TemplateView):
'box_title': _('Create a VM'),
'ajax_title': True,
'templates': templates.all(),
'template_access_types': TemplateAccessType.objects.exists(),
'form': TemplateRequestForm(request=request),
})
return self.render_to_response(context)
......@@ -1297,7 +1313,7 @@ class TransferInstanceOwnershipView(TransferOwnershipView):
confirm_view = TransferInstanceOwnershipConfirmView
model = Instance
notification_msg = ugettext_noop(
'%(user)s offered you to take the ownership of '
'%(owner)s offered you to take the ownership of '
'his/her virtual machine called %(instance)s. '
'<a href="%(token)s" '
'class="btn btn-success btn-small">Accept</a>')
......
......@@ -143,8 +143,8 @@ def selenium(test=""):
test = "--failed"
else:
test += " --with-id"
run("xvfb-run ./manage.py test "
"--settings=circle.settings.selenium_test %s" % test)
run('xvfb-run --server-args="-screen 0, 1920x1080x24" ./manage.py'
' test --settings=circle.settings.selenium_test %s' % test)
def pull(dir="~/circle/circle"):
......
......@@ -69,6 +69,8 @@ class BlacklistItemForm(ModelForm):
class Meta:
model = BlacklistItem
fields = ("ipv4", "host", "expires_at", "whitelisted", "reason",
"snort_message", )
class DomainForm(ModelForm):
......@@ -90,6 +92,7 @@ class DomainForm(ModelForm):
class Meta:
model = Domain
fields = ("name", "ttl", "owner", )
class FirewallForm(ModelForm):
......@@ -105,6 +108,7 @@ class FirewallForm(ModelForm):
class Meta:
model = Firewall
fields = ("name", )
class GroupForm(ModelForm):
......@@ -126,6 +130,7 @@ class GroupForm(ModelForm):
class Meta:
model = Group
fields = ("name", "description", "owner", )
class HostForm(ModelForm):
......@@ -165,6 +170,9 @@ class HostForm(ModelForm):
class Meta:
model = Host
fields = ("hostname", "reverse", "mac", "vlan", "shared_ip", "ipv4",
"ipv6", "external_ipv4", "description", "location",
"comment", "owner", )
class RecordForm(ModelForm):
......@@ -191,6 +199,8 @@ class RecordForm(ModelForm):
class Meta:
model = Record
fields = ("type", "host", "name", "domain", "address", "ttl",
"description", "owner", )
class RuleForm(ModelForm):
......@@ -230,6 +240,10 @@ class RuleForm(ModelForm):
class Meta:
model = Rule
fields = ("direction", "description", "foreign_network", "dport",
"sport", "weight", "proto", "extra", "action", "owner",
"nat", "nat_external_port", "nat_external_ipv4", "vlan",
"vlangroup", "host", "hostgroup", "firewall", )
class SwitchPortForm(ModelForm):
......@@ -252,6 +266,7 @@ class SwitchPortForm(ModelForm):
class Meta:
model = SwitchPort
fields = ("untagged_vlan", "tagged_vlans", "description", )
class VlanForm(ModelForm):
......@@ -305,6 +320,10 @@ class VlanForm(ModelForm):
widgets = {
'ipv6_template': widgets.TextInput,
}
fields = ("name", "vid", "network_type", "managed", "network4",
"snat_to", "snat_ip", "dhcp_pool", "network6",
"ipv6_template", "host_ipv6_prefixlen", "domain",
"reverse_domain", "description", "comment", "owner", )
class VlanGroupForm(ModelForm):
......@@ -328,3 +347,4 @@ class VlanGroupForm(ModelForm):
class Meta:
model = VlanGroup
fields = ("name", "vlans", "description", "owner", )
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django.forms import (
ModelForm, ModelChoiceField, ChoiceField, Form, CharField, RadioSelect,
Textarea,
)
from django.utils.translation import ugettext_lazy as _
from django.template import RequestContext
from django.template.loader import render_to_string
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from request.models import (
LeaseType, TemplateAccessType, TemplateAccessAction,
)
from dashboard.forms import VmResourcesForm
class LeaseTypeForm(ModelForm):
@property
def helper(self):
helper = FormHelper()
helper.add_input(Submit("submit", _("Save"),
css_class="btn btn-success", ))
return helper
class Meta:
model = LeaseType
fields = ["name", "lease", ]
class TemplateAccessTypeForm(ModelForm):
def __init__(self, *args, **kwargs):
super(TemplateAccessTypeForm, self).__init__(*args, **kwargs)
@property
def helper(self):
helper = FormHelper()
helper.add_input(Submit("submit", _("Save"),
css_class="btn btn-success", ))
return helper
class Meta:
model = TemplateAccessType
fields = ["name", "templates", ]
class InitialFromFileMixin(object):
def __init__(self, *args, **kwargs):
request = kwargs.pop("request", None)
super(InitialFromFileMixin, self).__init__(*args, **kwargs)
self.initial['message'] = render_to_string(
self.initial_template,
RequestContext(request, {}),
)
class TemplateRequestForm(InitialFromFileMixin, Form):
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)
initial_template = "request/initials/lease.html"
class ResourceRequestForm(InitialFromFileMixin, VmResourcesForm):
message = CharField(widget=Textarea)
initial_template = "request/initials/resources.html"
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.utils.timezone
from django.conf import settings
import model_utils.fields
import django.core.validators
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0001_initial'),
('vm', '0002_interface_model'),
]
operations = [
migrations.CreateModel(
name='ExtendLeaseAction',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('instance', models.ForeignKey(to='vm.Instance')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='LeaseType',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=25)),
('lease', models.ForeignKey(to='vm.Lease')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Request',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
('status', models.CharField(default=b'PENDING', max_length=10, choices=[(b'PENDING', 'pending'), (b'ACCEPTED', 'accepted'), (b'DECLINED', 'declined')])),
('type', models.CharField(max_length=10, choices=[(b'resource', 'resource request'), (b'lease', 'lease request'), (b'template', 'template access')])),
('message', models.TextField(verbose_name='Message')),
('reason', models.TextField(verbose_name='Reason')),
('object_id', models.IntegerField()),
('closed_by', models.ForeignKey(related_name='closed_by', to=settings.AUTH_USER_MODEL, null=True)),
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
('user', models.ForeignKey(related_name='user', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='ResourceChangeAction',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('num_cores', models.IntegerField(help_text='Number of virtual CPU cores available to the virtual machine.', verbose_name='number of cores', validators=[django.core.validators.MinValueValidator(0)])),
('ram_size', models.IntegerField(help_text='Mebibytes of memory.', verbose_name='RAM size', validators=[django.core.validators.MinValueValidator(0)])),
('priority', models.IntegerField(help_text='CPU priority.', verbose_name='priority', validators=[django.core.validators.MinValueValidator(0)])),
('instance', models.ForeignKey(to='vm.Instance')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='TemplateAccessAction',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('level', models.CharField(default=b'user', max_length=10, choices=[(b'user', 'user'), (b'operator', 'operator')])),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='TemplateAccessType',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=25)),
('templates', models.ManyToManyField(to='vm.InstanceTemplate')),
],
options={
'abstract': False,
},
bases=(models.Model,),
),
migrations.AddField(
model_name='templateaccessaction',
name='template_type',
field=models.ForeignKey(to='request.TemplateAccessType'),
preserve_default=True,
),
migrations.AddField(
model_name='templateaccessaction',
name='user',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
preserve_default=True,
),
migrations.AddField(
model_name='extendleaseaction',
name='lease_type',
field=models.ForeignKey(to='request.LeaseType'),
preserve_default=True,
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('request', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='leasetype',
name='lease',
field=models.ForeignKey(verbose_name='Lease', to='vm.Lease'),
preserve_default=True,
),
migrations.AlterField(
model_name='leasetype',
name='name',
field=models.CharField(max_length=25, verbose_name='Name'),
preserve_default=True,
),
migrations.AlterField(
model_name='templateaccesstype',
name='name',
field=models.CharField(max_length=25, verbose_name='Name'),
preserve_default=True,
),
migrations.AlterField(
model_name='templateaccesstype',
name='templates',
field=models.ManyToManyField(to='vm.InstanceTemplate', verbose_name='Templates'),
preserve_default=True,
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('request', '0002_auto_20150407_1117'),
]
operations = [
migrations.AlterField(
model_name='leasetype',
name='name',
field=models.CharField(max_length=100, verbose_name='Name'),
preserve_default=True,
),
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')]),
preserve_default=True,
),
migrations.AlterField(
model_name='templateaccesstype',
name='name',
field=models.CharField(max_length=100, verbose_name='Name'),
preserve_default=True,
),
]
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
import json
import logging
from django.db.models import (
Model, CharField, IntegerField, TextField, ForeignKey, ManyToManyField,
)
from django.db.models.signals import post_save
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from django.core.validators import MinValueValidator
from django.utils.translation import (
ugettext_lazy as _, ugettext_noop, ungettext
)
from django.core.urlresolvers import reverse
import requests
from model_utils.models import TimeStampedModel
from model_utils import Choices
from vm.models import Instance, InstanceTemplate, Lease
logger = logging.getLogger(__name__)
class RequestAction(Model):
def accept(self):
raise NotImplementedError
@property
def accept_msg(self):
raise NotImplementedError
class Meta:
abstract = True
class RequestType(Model):
name = CharField(max_length=100, verbose_name=_("Name"))
def __unicode__(self):
return self.name
class Meta:
abstract = True
class Request(TimeStampedModel):
STATUSES = Choices(
('PENDING', _('pending')),
('ACCEPTED', _('accepted')),
('DECLINED', _('declined')),
)
status = CharField(choices=STATUSES, default=STATUSES.PENDING,
max_length=10)
user = ForeignKey(User, related_name="user")
closed_by = ForeignKey(User, related_name="closed_by", null=True)
TYPES = Choices(
('resource', _('resource request')),
('lease', _("lease request")),
('template', _("template access request")),
)
type = CharField(choices=TYPES, max_length=10)
message = TextField(verbose_name=_("Message"))
reason = TextField(verbose_name=_("Reason"))
content_type = ForeignKey(ContentType)
object_id = IntegerField()
action = GenericForeignKey("content_type", "object_id")
def get_absolute_url(self):
return reverse("request.views.request-detail", kwargs={'pk': self.pk})
def get_readable_status(self):
return self.STATUSES[self.status]
def get_readable_type(self):
return self.TYPES[self.type]
def get_request_icon(self):
return {
'resource': "tasks",
'lease': "clock-o",
'template': "puzzle-piece"
}.get(self.type)
def get_effect(self):
return {
"PENDING": "warning",
"ACCEPTED": "success",
"DECLINED": "danger",
}.get(self.status)
def get_status_icon(self):
return {
"PENDING": "exclamation-triangle",
"ACCEPTED": "check",
"DECLINED": "times",
}.get(self.status)
def accept(self, user):
self.action.accept(user)
self.status = "ACCEPTED"
self.closed_by = user
self.save()
self.user.profile.notify(
ugettext_noop("Request accepted"),
self.action.accept_msg
)
def decline(self, user, reason):
self.status = "DECLINED"
self.closed_by = user
self.reason = reason
self.save()
decline_msg = ugettext_noop(
'Your <a href="%(url)s">request</a> was declined because of the '
'following reason: %(reason)s'
)
self.user.profile.notify(
ugettext_noop("Request declined"),
decline_msg, url=self.get_absolute_url(), reason=self.reason,
)
class LeaseType(RequestType):
lease = ForeignKey(Lease, verbose_name=_("Lease"))
def __unicode__(self):
return _("%(name)s (suspend: %(s)s, remove: %(r)s)") % {
'name': self.name,
's': self.lease.get_readable_suspend_time(),
'r': self.lease.get_readable_delete_time()}
def get_absolute_url(self):
return reverse("request.views.lease-type-detail",
kwargs={'pk': self.pk})
class TemplateAccessType(RequestType):
templates = ManyToManyField(InstanceTemplate, verbose_name=_("Templates"))
def get_absolute_url(self):
return reverse("request.views.template-type-detail",
kwargs={'pk': self.pk})
class ResourceChangeAction(RequestAction):
instance = ForeignKey(Instance)
num_cores = IntegerField(verbose_name=_('number of cores'),
help_text=_('Number of virtual CPU cores '
'available to the virtual machine.'),
validators=[MinValueValidator(0)])
ram_size = IntegerField(verbose_name=_('RAM size'),
help_text=_('Mebibytes of memory.'),
validators=[MinValueValidator(0)])
priority = IntegerField(verbose_name=_('priority'),
help_text=_('CPU priority.'),
validators=[MinValueValidator(0)])
def accept(self, user):
self.instance.resources_change.async(
user=user, num_cores=self.num_cores, ram_size=self.ram_size,
max_ram_size=self.ram_size, priority=self.priority,
with_shutdown=True)
@property
def accept_msg(self):
return _(
'The resources of <a href="%(url)s">%(name)s</a> were changed. '
'Number of cores: %(num_cores)d, RAM size: '
'<span class="nowrap">%(ram_size)d MiB</span>, '
'CPU priority: %(priority)d/100.'
) % {
'url': self.instance.get_absolute_url(),
'name': self.instance.name,
'num_cores': self.num_cores,
'ram_size': self.ram_size,
'priority': self.priority,
}
class ExtendLeaseAction(RequestAction):
instance = ForeignKey(Instance)
lease_type = ForeignKey(LeaseType)
def accept(self, user):
self.instance.renew(lease=self.lease_type.lease, save=True, force=True,
user=user)
@property
def accept_msg(self):
return _(
'The lease of <a href="%(url)s">%(name)s</a> got extended. '
'(suspend: %(suspend)s, remove: %(remove)s)'
) % {'name': self.instance.name,
'url': self.instance.get_absolute_url(),
'suspend': self.lease_type.lease.get_readable_suspend_time(),
'remove': self.lease_type.lease.get_readable_delete_time(), }
class TemplateAccessAction(RequestAction):
template_type = ForeignKey(TemplateAccessType)
LEVELS = Choices(
('user', _('user')),
('operator', _('operator')),
)
level = CharField(choices=LEVELS, default=LEVELS.user,
max_length=10)
user = ForeignKey(User)
def get_readable_level(self):
return self.LEVELS[self.level]
def accept(self, user):
for t in self.template_type.templates.all():
t.set_user_level(self.user, self.level)
@property
def accept_msg(self):
return ungettext(
"You got access to the following template: %s",
"You got access to the following templates: %s",
self.template_type.templates.count()
) % ", ".join([x.name for x in self.template_type.templates.all()])
def send_notifications(sender, instance, created, **kwargs):
if not created:
return
notification_msg = ugettext_noop(
'A new <a href="%(request_url)s">%(request_type)s</a> was submitted '
'by <a href="%(user_url)s">%(display_name)s</a>.')
context = {
'display_name': instance.user.profile.get_display_name(),
'user_url': instance.user.profile.get_absolute_url(),
'request_url': instance.get_absolute_url(),
'request_type': u"%s" % instance.get_readable_type()
}
for u in User.objects.filter(is_superuser=True):
u.profile.notify(
ugettext_noop("New %(request_type)s"), notification_msg, context
)
instance.user.profile.notify(
ugettext_noop("Request submitted"),
ugettext_noop('You can view the request\'s status at this '
'<a href="%(request_url)s">link</a>.'), context
)
if settings.REQUEST_HOOK_URL:
context.update({
'object_kind': "request",
'site_url': settings.DJANGO_URL,
})
try:
r = requests.post(settings.REQUEST_HOOK_URL, timeout=3,
data=json.dumps(context, indent=2))
r.raise_for_status()
except requests.RequestException as e:
logger.warning("Error in HTTP POST: %s. url: %s params: %s",
str(e), settings.REQUEST_HOOK_URL, context)
else:
logger.info("Successful HTTP POST. url: %s params: %s",
settings.REQUEST_HOOK_URL, context)
post_save.connect(send_notifications, sender=Request)
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext_lazy as _
from django_tables2 import Table, A
from django_tables2.columns import (
Column, TemplateColumn, LinkColumn
)
from request.models import Request, LeaseType, TemplateAccessType
class RequestTable(Table):
pk = LinkColumn(
'request.views.request-detail',
args=[A('pk')],
verbose_name=_("ID"),
)
status = TemplateColumn(
template_name="request/columns/status.html",
verbose_name=_("Status"),
)
user = TemplateColumn(
template_name="request/columns/user.html",
verbose_name=_("User"),
)
type = TemplateColumn(
template_name="request/columns/type.html",
verbose_name=_("Type"),
)
class Meta:
model = Request
template = "django_tables2/with_pagination.html"
attrs = {'class': ('table table-bordered table-striped table-hover'),
'id': "request-list-table"}
fields = ("pk", "status", "type", "user", )
order_by = ("-pk", )
empty_text = _("No more requests.")
per_page = 10
class LeaseTypeTable(Table):
pk = LinkColumn(
'request.views.lease-type-detail',
args=[A('pk')],
verbose_name=_("ID"),
)
lease = Column(verbose_name=_("Lease"))
class Meta:
model = LeaseType
attrs = {'class': "table table-bordered table-striped table-hover"}
fields = ('pk', 'name', 'lease', )
prefix = "lease-"
template = "django_tables2/with_pagination.html"
class TemplateAccessTypeTable(Table):
pk = LinkColumn(
'request.views.template-type-detail',
args=[A('pk')],
verbose_name=_("ID"),
)
templates = TemplateColumn(
template_name="request/columns/templates.html",
verbose_name=_("Templates"),
)
class Meta:
model = TemplateAccessType
attrs = {'class': "table table-bordered table-striped table-hover"}
fields = ('pk', 'name', 'templates', )
prefix = "template-"
template = "django_tables2/with_pagination.html"
{% load i18n %}
{% load crispy_forms_tags %}
<form action="{% url "request.views.request-lease" vm_pk=vm.pk %}" method="POST">
{% include "display-form-errors.html" %}
{% csrf_token %}
{{ form.lease|as_crispy_field }}
{{ form.message|as_crispy_field }}
<input type="submit" class="btn btn-primary"/>
</form>
{% load i18n %}
{% load crispy_forms_tags %}
<form action="{% url "request.views.request-template" %}" method="POST">
{% include "display-form-errors.html" %}
{% csrf_token %}
{{ form.template|as_crispy_field }}
<div style="font-weight: bold;">{% trans "Level" %}*</div>
{% for radio in form.level %}
<div class="myradio" style="display: inline-block; padding-left: 20px;">
<label>
{{ radio }}
<div class="text-muted" style="padding-left: 16px; font-weight: normal;">
{% if forloop.last %}
{% trans "For users who want to share the template with others." %}
{% else %}
{% trans "For users who want to start a virtual machine." %}
{% endif %}
</div>
</label>
</div>
{% endfor %}
{{ form.message|as_crispy_field }}
<input type="submit" class="btn btn-primary"/>
</form>
<span class="label label-{{ record.get_effect }}" style="font-size: 1.2em;">
<i class="fa fa-{{ record.get_status_icon }}"></i>
{{ record.get_readable_status|upper }}
</span>
{% for t in record.templates.all %}
<a href="{% url "dashboard.views.template-detail" pk=t.pk %}">
{{ t.name }}</a>
{% if not forloop.last %} | {% endif %}
{% endfor %}
<i class="fa fa-{{ record.get_request_icon }}"></i>
{{ record.get_readable_type|capfirst }}
<img src="{{ record.user.profile.get_avatar_url }}" width="20" height="20"/>
<a href="{{ record.user.profile.get_absolute_url }}">
{{ record.user.profile.get_display_name }}
</a>
{% extends "dashboard/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% load arrowfilter %}
{% block title-page %}{% trans "Request" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
{% if request.user.is_superuser %}
<a href="{% url "request.views.request-list" %}" class="btn btn-default btn-xs pull-right">
{% trans "Back" %}
</a>
{% endif %}
<h3 class="no-margin">
<i class="fa fa-{{ object.get_request_icon }}"></i>
{{ object.get_readable_type|capfirst }}
</h3>
</div>
<div class="panel-body">
<div class="label label-{{ object.get_effect }} pull-right" style="font-size: 1.5em; margin-top: 10px;">
<i class="fa fa-{{ object.get_status_icon }}"></i>
{{ object.get_readable_status|upper }}
</div>
<p>
<img src="{{ object.user.profile.get_avatar_url }}" width="50" height="50"/>
<a href="{{ object.user.profile.get_absolute_url }}">
{{ object.user.profile.get_display_name }}
</a>
</p>
<p>
<pre>{{ object.message }}</pre>
</p>
<hr />
{% if object.type == "lease" %}
<dl>
<dt>{% trans "VM name" %}</dt>
<dd><a href="{{ action.instance.get_absolute_url }}">{{ action.instance.name }}</a></dd>
<dt>{% trans "VM description" %}</dt>
<dd>{{ action.instance.description }}</dd>
<dt>{% trans "Current lease" %}</dt>
<dd>{{ action.instance.lease }}</dd>
<dt>{% trans "Requested lease" %}</dt>
<dd>{{ action.lease_type.lease }}</dd>
</dl>
{% elif object.type == "template" %}
<dl>
<dt>
{% trans "Template type" %}:
<span style="font-weight: normal;">{{ action.template_type.name }}</span>
</dt>
<dd>
<ul>
{% for t in action.template_type.templates.all %}
<li><a href="{{ t.get_absolute_url }}">{{ t }}</a></li>
{% endfor %}
</ul>
</dd>
<dt>{% trans "Level" %}<dt>
<dd>{{ action.get_readable_level }}</dd>
</dl>
{% elif object.type == "resource" %}
<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 }}</dd>
<dt>
{% trans "Priority" %}
<span class="text-muted" style="font-weight: normal;">{% trans "(old values in parentheses)" %}</span>
</dt>
<dd>{{ action.priority }} ({{ action.instance.priority }})</dd>
<dt>{% trans "Number of cores" %}</dt>
<dd>{{ action.num_cores }} ({{ action.instance.num_cores }})</dd>
<dt>{% trans "Ram size" %}</dt>
<dd>{{ action.ram_size }} ({{ action.instance.ram_size }}) MiB</dd>
</dl>
{% else %}
hacks!!!
{% endif %}
{% if object.status == "PENDING" and request.user.is_superuser %}
<hr />
<div class="pull-right" id="request-buttons">
<form method="POST">
{% csrf_token %}
<p>
<textarea class="form-control" placeholder="{% trans "Reason (sent to the user if the request is declined)" %}" name="reason"></textarea>
</p>
<button class="btn btn-danger" type="submit">
<i class="fa fa-thumbs-down"></i>
{% trans "Decline" %}
</button>
</form>
{% if object.type == "resource" and action.instance.status not in accept_states %}
{% trans "You can't accept this request because of the VM's state." %}
{% else %}
<form method="POST">
{% csrf_token %}
<input type="hidden" name="accept" value="1"/>
<button class="btn btn-success">
<i class="fa fa-thumbs-up"></i>
{% trans "Accept" %}
</button>
</form>
{% endif %}
</div>
{% endif %}
{% if object.status != "PENDING" %}
<div class="text-right">
{% blocktrans with closed=object.modified|arrowfilter:LANGUAGE_CODE user=object.closed_by.profile.get_display_name %}
Closed {{ closed }} by <a href="{{ user.profile.get_absolute_url }}">{{ user }}</a>
{% endblocktrans %}
{% if object.status == "DECLINED" %}
<p>
<strong>{% trans "Reason" %}:</strong> {{ object.reason }}
</p>
{% endif %}
</div>
{% endif %}
</div><!-- .panel-body -->
</div>
</div>
</div>
{% endblock %}
{% spaceless %}
{% if LANGUAGE_CODE == "en" %}
Why do you need this lease?
{% else %} {# place your translations here #}
Why do you need this lease?
{% endif %}
{% endspaceless %}
{% spaceless %}
{% if LANGUAGE_CODE == "en" %}
Why do you need these resources?
{% else %} {# place your translations here #}
Why do you need these resources?
{% endif %}
{% endspaceless %}
{% spaceless %}
{% if LANGUAGE_CODE == "en" %}
Why do you need this template?
{% else %} {# place your translations here #}
Why do you need this template?
{% endif %}
{% endspaceless %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load render_table from django_tables2 %}
{% block title-page %}
{% if form.instance.pk %}{{ form.instance.name }}{% else %}{% trans "Create" %}{% endif %}
| {% trans "lease type" %}
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<div class="pull-right">
{% if object.pk %}
<a class="btn btn-xs btn-danger"
href="{% url "request.views.lease-type-delete" pk=object.pk %}">
<i class="fa fa-times"></i> {% trans "Delete" %}
</a>
{% endif %}
<a class="btn btn-xs btn-default" href="{% url "request.views.type-list" %}">
{% trans "Back" %}
</a>
</div>
<h3 class="no-margin">
<i class="fa fa-clock-o"></i>
{% if form.instance.pk %}
{{ form.instance.name }}
{% else %}
{% trans "New lease type" %}
{% endif %}
</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-8">
{% crispy form %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block title-page %}{% trans "Requests" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a class="btn btn-xs btn-primary pull-right "href="{% url "request.views.type-list" %}">
{% trans "Request types" %}
</a>
<h3 class="no-margin"><i class="fa fa-phone"></i> {% trans "Requests" %}</h3>
</div>
<div class="panel-body">
<div class="panel-body">
{% trans "Filter by status" %}:
<a href="{{ request.path }}">{% trans "ALL" %}</a>
{% for s in statuses %}
<a href="?status={{ s.0 }}">{{ s.1|upper }}</a>
{% endfor %}
<div class="table-responsive">
{% render_table table %}
</div>
</div>
</div><!-- .panel-body -->
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
<i class="fa fa-puzzle-piece"></i> {% trans "Request new lease" %}
</h3>
</div>
<div class="panel-body">
<div class="form-group">
<label>{% trans "Virtual machine" %}</label>
<div class="controls">
<a href="{{ vm.get_absolute_url }}">{{ vm.name }}</a>
</div>
</div>
{% include "request/_request-lease-form.html" %}
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
<i class="fa fa-tasks"></i> {% trans "Request new resources" %}
</h3>
</div>
<div class="panel-body">
<form action="{% url "request.views.request-resource" vm_pk=vm.pk %}" method="POST">
{% csrf_token %}
<div class="form-group">
<label>{% trans "Virtual machine" %}</label>
<div class="controls">
<a href="{{ vm.get_absolute_url }}">{{ vm.name }}</a>
</div>
</div>
{% include "display-form-errors.html" %}
{% include "dashboard/_resources-sliders.html" with field_priority=form.priority field_num_cores=form.num_cores field_ram_size=form.ram_size %}
{{ form.message|as_crispy_field }}
<button type="submit" class="btn btn-success">
{% trans "Request new resources" %}
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
<i class="fa fa-puzzle-piece"></i> {% trans "Request template access" %}
</h3>
</div>
<div class="panel-body">
{% include "request/_request-template-form.html" %}
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load render_table from django_tables2 %}
{% block title-page %}
{% if form.instance.pk %}{{ form.instance.name }}{% else %}{% trans "Create" %}{% endif %}
| {% trans "template access type" %}
{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<div class="pull-right">
{% if object.pk %}
<a class="btn btn-xs btn-danger"
href="{% url "request.views.template-type-delete" pk=object.pk %}">
<i class="fa fa-times"></i> {% trans "Delete" %}
</a>
{% endif %}
<a class="btn btn-xs btn-default" href="{% url "request.views.type-list" %}">
{% trans "Back" %}
</a>
</div>
<h3 class="no-margin">
<i class="fa fa-puzzle-piece"></i>
{% if form.instance.pk %}
{{ form.instance.name }}
{% else %}
{% trans "New Template Access type" %}
{% endif %}
</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-8">
{% crispy form %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block title-page %}{% trans "Request types" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<div class="pull-right">
<a class="btn btn-xs btn-success" href="{% url "request.views.lease-type-create" %}">
<i class="fa fa-plus-circle"></i>
{% trans "new lease type" %}
</a>
<a class="btn btn-xs btn-success" href="{% url "request.views.template-type-create" %}">
<i class="fa fa-plus-circle"></i>
{% trans "new template access type" %}
</a>
</div>
<h3 class="no-margin"><i class="fa fa-phone"></i> {% trans "Request types" %}</h3>
</div>
<div class="panel-body">
<div class="text-muted little-margin-bottom">
{% blocktrans %}
Lease types are used for sharing leases. User can request longer ones via these.
{% endblocktrans %}
</div>
<div class="table-responsive">
{% render_table lease_table %}
</div>
<div class="text-muted little-margin-bottom">
{% blocktrans %}
Using template access types users can request multiple templates with user with operator or user level access.
{% endblocktrans %}
</div>
<div class="table-responsive">
{% render_table template_table %}
</div>
</div><!-- .panel-body -->
</div>
</div>
</div>
{% endblock %}
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django.test import TestCase
from django.test.client import Client
from django.contrib.auth.models import User, Permission
from mock import Mock, patch
from common.tests.celery_mock import MockCeleryMixin
from vm.models import Instance, InstanceTemplate, Lease
from dashboard.models import Profile
from request.models import Request, LeaseType, TemplateAccessType
from dashboard.tests.test_views import LoginMixin
from vm.operations import ResourcesOperation
class RequestTest(LoginMixin, MockCeleryMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json']
def setUp(self):
Instance.get_remote_queue_name = Mock(return_value='test')
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
self.us = User.objects.create(username='superuser', is_superuser=True)
self.us.set_password('password')
self.us.save()
self.u1.user_permissions.add(Permission.objects.get(
codename='create_vm'))
# superusers are notified uppon
for u in User.objects.filter(is_superuser=True):
p = Profile(user=u)
p.save()
self.lease = Lease(name="new lease", suspend_interval_seconds=1,
delete_interval_seconds=1)
self.lease.save()
LeaseType(name="lease type #1", lease=self.lease).save()
tat = TemplateAccessType(name="a")
tat.save()
tat.templates.add(InstanceTemplate.objects.get(pk=1))
def tearDown(self):
super(RequestTest, self).tearDown()
self.u1.delete()
self.us.delete()
def test_resources_request(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
req_count = Request.objects.count()
resp = c.post("/request/resource/1/", {
'num_cores': 5,
'ram_size': 512,
'priority': 30,
'message': "szia",
})
self.assertEqual(resp.status_code, 302)
self.assertEqual(req_count + 1, Request.objects.count())
new_request = Request.objects.latest("pk")
self.assertEqual(new_request.status, "PENDING")
self.assertEqual(inst.num_cores, 2)
self.assertEqual(inst.ram_size, 200)
self.assertEqual(inst.priority, 10)
# workaround for NOSTATE
inst.emergency_change_state(new_state="STOPPED", system=True)
with patch.object(ResourcesOperation, 'async') as mock_method:
mock_method.side_effect = (
new_request.action.instance.resources_change)
new_request.accept(self.us)
inst = Instance.objects.get(pk=1)
self.assertEqual(inst.num_cores, 5)
self.assertEqual(inst.ram_size, 512)
self.assertEqual(inst.priority, 30)
new_request = Request.objects.latest("pk")
self.assertEqual(new_request.status, "ACCEPTED")
def test_template_access_request(self):
c = Client()
self.login(c, "user1")
template = InstanceTemplate.objects.get(pk=1)
self.assertFalse(template.has_level(self.u1, "user"))
req_count = Request.objects.count()
resp = c.post("/request/template/", {
'template': 1,
'level': "user",
'message': "szia",
})
self.assertEqual(resp.status_code, 302)
self.assertEqual(req_count + 1, Request.objects.count())
new_request = Request.objects.latest("pk")
self.assertEqual(new_request.status, "PENDING")
new_request.accept(self.us)
new_request = Request.objects.latest("pk")
self.assertEqual(new_request.status, "ACCEPTED")
self.assertTrue(template.has_level(self.u1, "user"))
def test_lease_request(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
req_count = Request.objects.count()
resp = c.post("/request/lease/1/", {
'lease': 1,
'message': "szia",
})
self.assertEqual(resp.status_code, 302)
self.assertEqual(req_count + 1, Request.objects.count())
new_request = Request.objects.latest("pk")
self.assertEqual(new_request.status, "PENDING")
new_request.accept(self.us)
inst = Instance.objects.get(pk=1)
new_request = Request.objects.latest("pk")
self.assertEqual(new_request.status, "ACCEPTED")
self.assertEqual(inst.lease, self.lease)
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
from django.conf.urls import patterns, url
from .views import (
RequestList, RequestDetail, RequestTypeList,
LeaseTypeCreate, LeaseTypeDetail,
TemplateAccessTypeCreate, TemplateAccessTypeDetail,
TemplateRequestView, LeaseRequestView, ResourceRequestView,
LeaseTypeDelete, TemplateAccessTypeDelete,
)
urlpatterns = patterns(
'',
url(r'^list/$', RequestList.as_view(),
name="request.views.request-list"),
url(r'^(?P<pk>\d+)/$', RequestDetail.as_view(),
name="request.views.request-detail"),
url(r'^type/list/$', RequestTypeList.as_view(),
name="request.views.type-list"),
# request types
url(r'^type/lease/create/$', LeaseTypeCreate.as_view(),
name="request.views.lease-type-create"),
url(r'^type/lease/(?P<pk>\d+)/$', LeaseTypeDetail.as_view(),
name="request.views.lease-type-detail"),
url(r'^type/lease/delete/(?P<pk>\d+)/$', LeaseTypeDelete.as_view(),
name="request.views.lease-type-delete"),
url(r'^type/template/create/$', TemplateAccessTypeCreate.as_view(),
name="request.views.template-type-create"),
url(r'^type/template/(?P<pk>\d+)/$',
TemplateAccessTypeDetail.as_view(),
name="request.views.template-type-detail"),
url(r'^type/template/delete/(?P<pk>\d+)/$',
TemplateAccessTypeDelete.as_view(),
name="request.views.template-type-delete"),
# request views (visible for users)
url(r'template/$', TemplateRequestView.as_view(),
name="request.views.request-template"),
url(r'lease/(?P<vm_pk>\d+)/$', LeaseRequestView.as_view(),
name="request.views.request-lease"),
url(r'resource/(?P<vm_pk>\d+)/$', ResourceRequestView.as_view(),
name="request.views.request-resource"),
)
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals, absolute_import
from django.views.generic import (
UpdateView, TemplateView, DetailView, CreateView, FormView, DeleteView,
)
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 braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from django_tables2 import SingleTableView
from request.models import (
Request, TemplateAccessType, LeaseType, TemplateAccessAction,
ExtendLeaseAction, ResourceChangeAction,
)
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,
)
class RequestList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
model = Request
table_class = RequestTable
template_name = "request/list.html"
def get_context_data(self, **kwargs):
context = super(RequestList, self).get_context_data(**kwargs)
context['statuses'] = Request.STATUSES
return context
def get_table_data(self):
data = Request.objects.all()
status = self.request.GET.get("status")
if status:
data = data.filter(status=status)
return data
class RequestDetail(LoginRequiredMixin, DetailView):
model = Request
template_name = "request/detail.html"
def post(self, *args, **kwargs):
user = self.request.user
request = self.get_object() # not self.request!
if not user.is_superuser:
raise SuspiciousOperation
if self.get_object().status == "PENDING":
accept = self.request.POST.get("accept")
reason = self.request.POST.get("reason")
if accept:
request.accept(user)
else:
request.decline(user, reason)
return redirect(request.get_absolute_url())
def get_context_data(self, **kwargs):
request = self.object
user = self.request.user
if not user.is_superuser and request.user != user:
raise SuspiciousOperation
context = super(RequestDetail, self).get_context_data(**kwargs)
context['action'] = request.action
context['accept_states'] = ResourcesOperation.accept_states
return context
class TemplateAccessTypeDetail(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, UpdateView):
model = TemplateAccessType
template_name = "request/template-type-form.html"
form_class = TemplateAccessTypeForm
success_message = _("Template access type successfully updated.")
class TemplateAccessTypeCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView):
model = TemplateAccessType
template_name = "request/template-type-form.html"
form_class = TemplateAccessTypeForm
success_message = _("New template access type successfully created.")
class TemplateAccessTypeDelete(LoginRequiredMixin, SuperuserRequiredMixin,
DeleteView):
model = TemplateAccessType
template_name = "dashboard/confirm/base-delete.html"
def get_success_url(self):
return reverse("request.views.type-list")
class LeaseTypeDetail(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, UpdateView):
model = LeaseType
template_name = "request/lease-type-form.html"
form_class = LeaseTypeForm
success_message = _("Lease type successfully updated.")
class LeaseTypeCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView):
model = LeaseType
template_name = "request/lease-type-form.html"
form_class = LeaseTypeForm
success_message = _("New lease type successfully created.")
class LeaseTypeDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
model = LeaseType
template_name = "dashboard/confirm/base-delete.html"
def get_success_url(self):
return reverse("request.views.type-list")
class RequestTypeList(LoginRequiredMixin, SuperuserRequiredMixin,
TemplateView):
template_name = "request/type-list.html"
def get_context_data(self, **kwargs):
context = super(RequestTypeList, self).get_context_data(**kwargs)
context['lease_table'] = LeaseTypeTable(
LeaseType.objects.all(), request=self.request)
context['template_table'] = TemplateAccessTypeTable(
TemplateAccessType.objects.all(), request=self.request)
return context
class TemplateRequestView(LoginRequiredMixin, FormView):
form_class = TemplateRequestForm
template_name = "request/request-template.html"
def get_form_kwargs(self):
kwargs = super(TemplateRequestView, self).get_form_kwargs()
kwargs['request'] = self.request
return kwargs
def form_valid(self, form):
data = form.cleaned_data
user = self.request.user
ta = TemplateAccessAction(
template_type=data['template'],
level=data['level'],
user=user,
)
ta.save()
req = Request(
user=user,
message=data['message'],
type=Request.TYPES.template,
action=ta
)
req.save()
return redirect("/")
class VmRequestMixin(LoginRequiredMixin, object):
def get_vm(self):
return get_object_or_404(Instance, pk=self.kwargs['vm_pk'])
def dispatch(self, *args, **kwargs):
vm = self.get_vm()
user = self.request.user
if not vm.has_level(user, self.user_level):
raise PermissionDenied()
return super(VmRequestMixin, self).dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
context = super(VmRequestMixin, self).get_context_data(**kwargs)
context['vm'] = self.get_vm()
return context
def get_form_kwargs(self):
kwargs = super(VmRequestMixin, self).get_form_kwargs()
kwargs['request'] = self.request
return kwargs
def form_valid(self, form):
raise NotImplementedError
class LeaseRequestView(VmRequestMixin, FormView):
form_class = LeaseRequestForm
template_name = "request/request-lease.html"
user_level = "operator"
def form_valid(self, form):
data = form.cleaned_data
user = self.request.user
vm = self.get_vm()
el = ExtendLeaseAction(
lease_type=data['lease'],
instance=vm,
)
el.save()
req = Request(
user=user,
message=data['message'],
type=Request.TYPES.lease,
action=el
)
req.save()
return redirect(vm.get_absolute_url())
class ResourceRequestView(VmRequestMixin, FormView):
form_class = ResourceRequestForm
template_name = "request/request-resource.html"
user_level = "user"
def get_form_kwargs(self):
kwargs = super(ResourceRequestView, self).get_form_kwargs()
kwargs['can_edit'] = True
kwargs['instance'] = self.get_vm()
return kwargs
def get_initial(self):
vm = self.get_vm()
initial = super(ResourceRequestView, self).get_initial()
initial['num_cores'] = vm.num_cores
initial['priority'] = vm.priority
initial['ram_size'] = vm.ram_size
return initial
def form_valid(self, form):
vm = self.get_vm()
data = form.cleaned_data
user = self.request.user
rc = ResourceChangeAction(
instance=vm,
num_cores=data['num_cores'],
priority=data['priority'],
ram_size=data['ram_size'],
)
rc.save()
req = Request(
user=user,
message=data['message'],
type=Request.TYPES.resource,
action=rc
)
req.save()
return redirect(vm.get_absolute_url())
{% extends "base.html" %}
{% load i18n %}
{% block title %}HTTP 403{% endblock %}
{% block page_title %}{% trans ":(" %}{% endblock page_title %}
{% block content %}
<div class="alert alert-danger" style="font-size: 22px; margin-top: 2em;">
<div class="row">
<div class="col-md-2" style="text-align: center;">
HTTP 403
</div>
<div class="col-md-10" style="text-align: center;">
{% if error %}
{{ error }}
{% else %}
{% trans "Forbidden" %}
{% endif %}
</div>
</div>
</div>
{% endblock content %}
......@@ -6,5 +6,14 @@
{% block page_title %}{% trans "Page not found" %}{% endblock page_title %}
{% block content %}
<p>{% trans "This page does not exist." %}</p>
<div class="alert alert-warning" style="font-size: 22px; margin-top: 2em;">
<div class="row">
<div class="col-md-2" style="text-align: center;">
HTTP 404
</div>
<div class="col-md-10" style="text-align: center;">
{% trans "This page does not exist." %}
</div>
</div>
</div>
{% endblock content %}
{% extends "dashboard/base.html" %}
{% extends "base.html" %}
{% load i18n %}
{% block title %}HTTP 500{% endblock %}
......
......@@ -62,7 +62,6 @@ scheduler = import_module(name=django.conf.settings.VM_SCHEDULER)
ACCESS_PROTOCOLS = django.conf.settings.VM_ACCESS_PROTOCOLS
ACCESS_METHODS = [(key, name) for key, (name, port, transport)
in ACCESS_PROTOCOLS.iteritems()]
VNC_PORT_RANGE = (20000, 65536) # inclusive start, exclusive end
def find_unused_port(port_range, used_ports=[]):
......@@ -81,7 +80,7 @@ def find_unused_port(port_range, used_ports=[]):
def find_unused_vnc_port():
port = find_unused_port(
port_range=VNC_PORT_RANGE,
port_range=django.conf.settings.VNC_PORT_RANGE,
used_ports=Instance.objects.values_list('vnc_port', flat=True))
if port is None:
......
......@@ -1334,10 +1334,20 @@ class ResourcesOperation(InstanceOperation):
description = _("Change resources of a stopped virtual machine.")
acl_level = "owner"
required_perms = ('vm.change_resources', )
accept_states = ('STOPPED', 'PENDING', )
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
def _operation(self, user, activity,
num_cores, ram_size, max_ram_size, priority):
num_cores, ram_size, max_ram_size, priority,
with_shutdown=False, task=None):
if self.instance.status == 'RUNNING' and not with_shutdown:
raise Instance.WrongStateError(self.instance)
try:
self.instance.shutdown(parent_activity=activity, task=task)
except Instance.WrongStateError:
pass
self.instance._update_status()
self.instance.num_cores = num_cores
self.instance.ram_size = ram_size
......
......@@ -11,11 +11,12 @@ django-braces==1.4.0
django-celery==3.1.16
django-crispy-forms==1.4.0
django-model-utils==2.2
djangosaml2==0.13.0
django-sizefield==0.6
django-sshkey==2.2.0
django-statici18n==1.1
django-tables2==0.15.0
git+https://git.ik.bme.hu/circle/django-taggit.git
django-taggit==0.13.0
docutils==0.12
Jinja2==2.7.3
jsonfield==1.0.0
......@@ -32,6 +33,7 @@ pyinotify==0.9.4
pytz==2014.7
requests==2.5.3
salt==2014.1.0
shutilwhich==1.0.1
simplejson==3.6.5
six==1.8.0
slimit==0.8.1
......
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