Commit fee9c1a3 by Kálmán Viktor

Merge branch 'master' into feature-helppage

Conflicts:
	circle/dashboard/static/dashboard/dashboard.js
	circle/dashboard/static/dashboard/dashboard.less
parents f6293056 0c706599
......@@ -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
......@@ -156,6 +156,7 @@ STATIC_URL = get_env_variable('DJANGO_STATIC_URL', default='/static/')
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'pipeline.finders.PipelineFinder',
)
########## END STATIC FILE CONFIGURATION
STATICFILES_DIRS = [normpath(join(SITE_ROOT, 'bower_components'))]
......@@ -282,6 +283,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request',
'dashboard.context_processors.notifications',
'dashboard.context_processors.extract_settings',
'dashboard.context_processors.broadcast_messages',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
......@@ -355,6 +357,7 @@ LOCAL_APPS = (
'manager',
'acl',
'monitor',
'request',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
......@@ -449,7 +452,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 +530,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 +562,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", "")
......@@ -110,8 +110,8 @@ if DEBUG:
from django.dispatch import Signal
Signal.send_robust = Signal.send
PIPELINE_DISABLED_COMPILERS = (
'pipeline.compilers.less.LessCompiler',
PIPELINE_COMPILERS = (
'dashboard.compilers.DummyLessCompiler',
)
ADMIN_ENABLED = True
......@@ -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)
......@@ -76,7 +76,7 @@ class Operation(object):
user = auxargs.pop('user')
parent_activity = auxargs.pop('parent_activity')
if parent_activity and user is None and not skip_auth_check:
user = parent_activity.user
user = allargs['user'] = parent_activity.user
if user is None: # parent was a system call
skip_auth_check = True
......@@ -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
......@@ -21,7 +21,7 @@ from django import contrib
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib.auth.models import User, Group
from dashboard.models import Profile, GroupProfile, ConnectCommand
from dashboard.models import Profile, GroupProfile, ConnectCommand, Message
class ProfileInline(contrib.admin.TabularInline):
......@@ -43,3 +43,5 @@ contrib.admin.site.unregister(User)
contrib.admin.site.register(User, UserAdmin)
contrib.admin.site.unregister(Group)
contrib.admin.site.register(Group, GroupAdmin)
contrib.admin.site.register(Message)
# 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 pipeline.compilers.less import LessCompiler
class DummyLessCompiler(LessCompiler):
def compile_file(self, *args, **kwargs):
pass
......@@ -17,6 +17,8 @@
from django.conf import settings
from .models import Message
def notifications(request):
count = (request.user.notification_set.filter(status="new").count()
......@@ -31,3 +33,7 @@ def extract_settings(request):
'COMPANY_NAME': getattr(settings, "COMPANY_NAME", None),
'ADMIN_ENABLED': getattr(settings, "ADMIN_ENABLED", False),
}
def broadcast_messages(request):
return {'broadcast_messages': Message.timeframed.filter(enabled=True)}
......@@ -1395,6 +1395,7 @@
"vnc_port": 1234,
"num_cores": 2,
"status": "RUNNING",
"system": "system pls",
"modified": "2013-10-14T07:27:38.192Z"
}
},
......
......@@ -57,7 +57,7 @@ from vm.models import (
from storage.models import DataStore, Disk
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.models import Permission
from .models import Profile, GroupProfile
from .models import Profile, GroupProfile, Message
from circle.settings.base import LANGUAGES, MAX_NODE_RAM
from django.utils.translation import string_concat
......@@ -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,17 @@ class DiskForm(ModelForm):
class Meta:
model = Disk
fields = ("name", "filename", "datastore", "type", "bus", "size",
"base", "dev_num", "destroyed", "is_ready", )
class MessageForm(ModelForm):
class Meta:
model = Message
fields = ("message", "enabled", "effect", "start", "end")
@property
def helper(self):
helper = FormHelper()
helper.add_input(Submit("submit", _("Save")))
return helper
......@@ -17,27 +17,29 @@
from __future__ import unicode_literals, absolute_import
import logging
from optparse import make_option
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand
from firewall.models import (Vlan, VlanGroup, Domain, Firewall, Rule,
SwitchPort, EthernetDevice, Host)
from firewall.models import Vlan, VlanGroup, Domain, Firewall, Rule
from storage.models import DataStore
from vm.models import Lease
logger = logging.getLogger(__name__)
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--force', action="store_true"),
make_option('--portal-ip'),
make_option('--external-net'),
make_option('--management-net'),
make_option('--vm-net'),
make_option('--external-if'),
make_option('--management-if'),
make_option('--trunk-if'),
make_option('--vm-if'),
make_option('--datastore-queue'),
make_option('--firewall-queue'),
make_option('--admin-user'),
......@@ -49,18 +51,18 @@ class Command(BaseCommand):
qs = model.objects.filter(**{field: value})[:1]
if not qs.exists():
obj = model.objects.create(**kwargs)
self.changed.append('New %s: %s' % (model, obj))
logger.info('New %s: %s', model, obj)
self.changed = True
return obj
else:
return qs[0]
# http://docs.saltstack.com/en/latest/ref/states/all/salt.states.cmd.html
def print_state(self):
changed = "yes" if len(self.changed) else "no"
print "\nchanged=%s comment='%s'" % (changed, ", ".join(self.changed))
print "\nchanged=%s" % ("yes" if self.changed else "no")
def handle(self, *args, **options):
self.changed = []
self.changed = False
if (DataStore.objects.exists() and Vlan.objects.exists()
and not options['force']):
......@@ -87,20 +89,28 @@ class Command(BaseCommand):
suspend_interval_seconds=3600 * 24 * 365,
delete_interval_seconds=3600 * 24 * 365 * 3)
domain = self.create(Domain, 'name', name='example.com', owner=admin)
net_domain = self.create(Domain, 'name', name='net.example.com',
owner=admin)
man_domain = self.create(Domain, 'name', name='man.example.com',
owner=admin)
vm_domain = self.create(Domain, 'name', name='vm.example.com',
owner=admin)
# vlans
net = self.create(Vlan, 'name', name='net', vid=4,
network4=options['external_net'], domain=domain)
net = self.create(Vlan, 'vid', name=options['external_if'], vid=4,
network4=options['external_net'], domain=net_domain)
man = self.create(Vlan, 'name', name='man', vid=3, dhcp_pool='manual',
network4=options['management_net'], domain=domain,
man = self.create(Vlan, 'vid', name=options['management_if'], vid=3,
dhcp_pool='manual',
network4=options['management_net'],
domain=man_domain,
snat_ip=options['external_net'].split('/')[0])
man.snat_to.add(net)
man.snat_to.add(man)
vm = self.create(Vlan, 'name', name='vm', vid=2, dhcp_pool='manual',
network4=options['vm_net'], domain=domain,
vm = self.create(Vlan, 'vid', name=options['vm_if'], vid=2,
dhcp_pool='manual',
network4=options['vm_net'], domain=vm_domain,
snat_ip=options['external_net'].split('/')[0])
vm.snat_to.add(net)
vm.snat_to.add(vm)
......@@ -115,14 +125,6 @@ class Command(BaseCommand):
vg_net = self.create(VlanGroup, 'name', name='net')
vg_net.vlans.add(net)
# portal host
portal = self.create(Host, 'hostname', hostname='portal', vlan=man,
mac='11:22:33:44:55:66', owner=admin,
shared_ip=True, external_ipv4=man.snat_ip,
ipv4=options['portal_ip'])
portal.add_port(proto='tcp', public=443, private=443)
portal.add_port(proto='tcp', public=22, private=22)
# firewall rules
fw = self.create(Firewall, 'name', name=options['firewall_queue'])
......@@ -130,8 +132,16 @@ class Command(BaseCommand):
direction='out', action='accept',
foreign_network=vg_all, firewall=fw)
self.create(Rule, 'description', description='default input rule',
direction='in', action='accept',
self.create(Rule, 'description', description='portal https',
direction='in', action='accept', proto='tcp', dport=443,
foreign_network=vg_all, firewall=fw)
self.create(Rule, 'description', description='portal http',
direction='in', action='accept', proto='tcp', dport=80,
foreign_network=vg_all, firewall=fw)
self.create(Rule, 'description', description='ssh',
direction='in', action='accept', proto='tcp', dport=22,
foreign_network=vg_all, firewall=fw)
# vlan rules
......@@ -143,23 +153,4 @@ class Command(BaseCommand):
direction='out', action='accept',
foreign_network=vg_net, vlan=man)
# switch
# uplink interface
sp_net = self.create(SwitchPort, 'untagged_vlan', untagged_vlan=net)
self.create(EthernetDevice, 'switch_port', switch_port=sp_net,
name=options['external_if'])
# management interface
if options['management_if']:
sp_man = self.create(
SwitchPort, 'untagged_vlan', untagged_vlan=man)
self.create(EthernetDevice, 'switch_port', switch_port=sp_man,
name=options['management_if'])
# vm interface
sp_trunk = self.create(
SwitchPort, 'tagged_vlans', untagged_vlan=man, tagged_vlans=vg_all)
self.create(EthernetDevice, 'switch_port', switch_port=sp_trunk,
name=options['trunk_if'])
return self.print_state()
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.utils.timezone
import model_utils.fields
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0002_auto_20150318_1317'),
]
operations = [
migrations.CreateModel(
name='Message',
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)),
('start', models.DateTimeField(null=True, verbose_name='start', blank=True)),
('end', models.DateTimeField(null=True, verbose_name='end', blank=True)),
('message', models.CharField(max_length=500, verbose_name='message')),
('effect', models.CharField(default=b'info', max_length=10, verbose_name='effect', choices=[(b'success', 'success'), (b'info', 'info'), (b'warning', 'warning'), (b'danger', 'danger')])),
('enabled', models.BooleanField(default=False, verbose_name='enabled')),
],
options={
'ordering': ['id'],
'verbose_name': 'message',
'verbose_name_plural': 'messages',
},
bases=(models.Model,),
),
]
......@@ -27,7 +27,7 @@ from django.contrib.auth.signals import user_logged_in
from django.core.urlresolvers import reverse
from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField,
DateTimeField, permalink, BooleanField
DateTimeField, BooleanField
)
from django.db.models.signals import post_save, pre_delete, post_delete
from django.templatetags.static import static
......@@ -39,14 +39,13 @@ from django.core.exceptions import ObjectDoesNotExist
from sizefield.models import FileSizeField
from jsonfield import JSONField
from model_utils.models import TimeStampedModel
from model_utils.models import TimeFramedModel, TimeStampedModel
from model_utils.fields import StatusField
from model_utils import Choices
from acl.models import AclBase
from common.models import HumanReadableObject, create_readable, Encoder
from vm.tasks.agent_tasks import add_keys, del_keys
from vm.models.instance import ACCESS_METHODS
from .store_api import Store, NoStoreException, NotOkException, Timeout
......@@ -59,6 +58,27 @@ def pwgen():
return User.objects.make_random_password()
class Message(TimeStampedModel, TimeFramedModel):
message = CharField(max_length=500, verbose_name=_('message'))
effect = CharField(
default='info', max_length=10, verbose_name=_('effect'),
choices=(('success', _('success')), ('info', _('info')),
('warning', _('warning')), ('danger', _('danger'))))
enabled = BooleanField(default=False, verbose_name=_('enabled'))
class Meta:
ordering = ["id"]
verbose_name = _('message')
verbose_name_plural = _('messages')
def __unicode__(self):
return self.message
def get_absolute_url(self):
return reverse('dashboard.views.message-detail',
kwargs={'pk': self.pk})
class Favourite(Model):
instance = ForeignKey("vm.Instance")
user = ForeignKey(User)
......@@ -270,10 +290,9 @@ class GroupProfile(AclBase):
except cls.DoesNotExist:
return Group.objects.get(name=name)
@permalink
def get_absolute_url(self):
return ('dashboard.views.group-detail', None,
{'pk': self.group.pk})
return reverse('dashboard.views.group-detail',
kwargs={'pk': self.group.pk})
def get_or_create_profile(self):
......@@ -309,7 +328,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 +358,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()
......@@ -409,9 +428,7 @@ def add_ssh_keys(sender, **kwargs):
'user', userkey.user).filter(status='RUNNING')
for i in instances:
logger.info('called add_keys(%s, %s)', i, userkey)
queue = i.get_remote_queue_name("agent")
add_keys.apply_async(args=(i.vm_name, [userkey.key]),
queue=queue)
i.install_keys(user=userkey.user, keys=[userkey.key])
def del_ssh_keys(sender, **kwargs):
......@@ -422,9 +439,7 @@ def del_ssh_keys(sender, **kwargs):
'user', userkey.user).filter(status='RUNNING')
for i in instances:
logger.info('called del_keys(%s, %s)', i, userkey)
queue = i.get_remote_queue_name("agent")
del_keys.apply_async(args=(i.vm_name, [userkey.key]),
queue=queue)
i.remove_keys(user=userkey.user, keys=[userkey.key])
post_save.connect(add_ssh_keys, sender=UserKey)
......
......@@ -145,11 +145,13 @@ $(function() {
$("#dashboard-vm-details-connect-button").removeClass('disabled');
}
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
$("#getScreenshotButton").prop("disabled", false);
} else {
if(data.connect_uri) {
$("#dashboard-vm-details-connect-button").addClass('disabled');
}
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
$("#getScreenshotButton").prop("disabled", true);
}
if(data.status == "STOPPED" || data.status == "PENDING") {
......
......@@ -10,10 +10,11 @@ $(function () {
$(".not-tab-pane").removeClass("not-tab-pane").addClass("tab-pane");
$('.vm-create').click(function(e) {
var template = $(this).data("template");
var url = $(this).data("href");
if(!url) url = $(this).prop("href");
$.ajax({
type: 'GET',
url: $(this).attr('href'),
url: url,
success: function(data) {
$('body').append(data);
vmCreateLoaded();
......@@ -140,7 +141,7 @@ $(function () {
// success
},
error: function(xhr, textStatus, error) {
console.log("oh babám");
addMessage(gettext("An error occurred. (") + xhr.status + ")", 'danger');
}
});
$(star).tooltip('destroy').tooltip({'placement': 'right'});
......@@ -528,6 +529,7 @@ function safe_tags_replace(str) {
return str.replace(/[&<>]/g, replaceTag);
}
$('.crosslink').click(function(e) {
// Don't follow the link
event.preventDefault();
......@@ -535,3 +537,33 @@ $('.crosslink').click(function(e) {
$(menu).click();
window.location = this.href;
});
$(function () {
var closed = JSON.parse(getCookie('broadcast-messages'));
$('.broadcast-message').each(function() {
var id = $(this).data('id');
if (closed && closed.indexOf(id) != -1) {
$(this).remove();
}
});
$('.broadcast-message').on('closed.bs.alert', function () {
var closed = JSON.parse(getCookie('broadcast-messages'));
if (!closed) {
closed = [];
}
closed.push($(this).data('id'));
setCookie('broadcast-messages', JSON.stringify(closed), 7 * 24 * 60 * 60 * 1000, "/");
});
$("#id_message").on('input', function() {
$('.broadcast-message').html($(this).val());
});
$("#id_effect").on('input', function() {
$('.broadcast-message').removeClass(
'alert-info alert-warning alert-success alert-danger').addClass(
"alert-" + $(this).val());
});
});
......@@ -10,6 +10,21 @@ html {
min-height: 100%;
}
.navbar-nav img {
width: 46px;
height: 46px;
margin-top: -4px;
}
.profile-avatar {
width: 24px;
height: 24px;
}
.navbar-fixed-top {
border: 0px;
}
/* Set widths on the navbar form inputs since otherwise they're 100% wide */
.navbar-form input[type="text"],
.navbar-form input[type="password"] {
......@@ -32,7 +47,7 @@ html {
#dashboard-menu {
margin-right: 15px;
margin-right: 0px;
}
/* we need this for mobile view */
......@@ -56,6 +71,9 @@ html {
padding-bottom: 12.5px;
}
#dashboard-menu > li:last-child > a {
padding: 4px 0 0 0;
}
}
.no-margin {
......@@ -110,6 +128,11 @@ html {
background-color: black!important;
}
.timeline img{
border-radius: 50%;
margin-bottom: 2px;
}
.timeline a {
color: black;
}
......@@ -1272,6 +1295,25 @@ 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;
......@@ -1442,3 +1484,32 @@ height: calc(100% - 130px);
.overview_href {
cursor: pointer;
}
#request-buttons {
form {
display: inline;
}
textarea {
resize: none;
min-height: 80px;
}
}
.nowrap {
white-space: nowrap;
}
.little-margin-bottom {
margin-bottom: 5px;
}
.broadcast-message {
margin-bottom: 5px;
padding-top: 5px;
padding-bottom: 5px;
}
.acl-table td:first-child {
text-align: center;
}
......@@ -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());
......@@ -178,12 +185,13 @@ $(function() {
$("i", this).addClass("fa-spinner fa-spin");
$(this).prop("disabled", true);
ct.slideDown();
var img = $("img", ct).prop("src", '/dashboard/vm/' + vm + '/screenshot/');
var img = $("img", ct).prop("src", '/dashboard/vm/' + vm + '/screenshot/?rnd=' + Math.random());
});
// if the image is loaded remove the spinning stuff
// note: this should not work if the image is cached, but it's not
// see: http://stackoverflow.com/a/3877079/1112653
// note #2: it actually gets cached, so a random number is appended
$("#vm-console-screenshot img").load(function(e) {
$("#getScreenshotButton").prop("disabled", false)
.find("i").removeClass("fa-spinner fa-spin");
......@@ -193,7 +201,7 @@ $(function() {
// screenshot close
$("#vm-console-screenshot button").click(function() {
$(this).parent("div").slideUp();
$(this).closest("div").slideUp();
});
// select connection string
......@@ -223,4 +231,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();
});
});
......@@ -55,6 +55,9 @@ html {
.bigbig {
font-size: 3em;
}
.big-tag {
font-size: 1.2em;
}
/* small buttons for tags, copied from Bootstraps input-sm, bnt-sm */
.btn-tags, .btn-traits {
padding: 3px 6px;
......@@ -148,3 +151,7 @@ footer a, footer a:hover, footer a:visited {
.btn-toolbar {
margin-bottom: 5px;
}
.vm-details-home-edit-description {
font-size: 85%; /* ~ small tag */
}
......@@ -29,7 +29,7 @@ from django_sshkey.models import UserKey
from storage.models import Disk
from vm.models import Node, InstanceTemplate, Lease
from dashboard.models import ConnectCommand
from dashboard.models import ConnectCommand, Message
class FileSizeColumn(Column):
......@@ -354,3 +354,19 @@ class DiskListTable(Table):
order_by = ("-pk", )
per_page = 15
empty_text = _("No disk found.")
class MessageListTable(Table):
message = LinkColumn(
'dashboard.views.message-detail',
args=[A('pk')],
attrs={'th': {'data-sort': "string"}}
)
class Meta:
template = "django_tables2/with_pagination.html"
model = Message
attrs = {'class': "table table-bordered table-striped table-hover",
'id': "disk-list-table"}
order_by = ("-pk", )
fields = ('pk', 'message', 'enabled', 'effect')
{% load i18n %}
{% load staticfiles %}
{% load compressed %}
{% load cache %}
{% load pipeline %}
<!DOCTYPE html>
<html lang="{{lang}}">
<head>
......@@ -9,17 +10,17 @@
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" type="image/png" href="{% static "dashboard/img/favicon.png" %}"/>
{% block extra_link %}{% endblock %}
<title>{% block title %}{% block title-page %}{% endblock %} | {% block title-site %}CIRCLE{% endblock %}{% endblock %}</title>
{% compressed_css 'all' %}
{% stylesheet 'all' %}
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
{% block extra_link %}{% endblock %}
{% block extra_css %}{% endblock %}
</head>
......@@ -27,7 +28,12 @@
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-header">
{% block navbar-brand %}{% endblock %}
{% block navbar-brand %}
<a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;">
{% include "branding.html" %}
</a>
{% endblock %}
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
......@@ -40,6 +46,22 @@
</div><!-- navbar navbar-inverse navbar-fixed-top -->
<div class="container">
{% block broadcast_messages %}
{% cache 30 broadcast_messages %}
<div id="broadcast-messages">
{% for message in broadcast_messages %}
<div data-id={{ message.id }} class="alert alert-{{ message.effect }}
text-center broadcast-message">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
{{ message.message|safe }}
</div>
{% endfor %}
</div>
{% endcache %}
{% endblock broadcast_messages %}
{% block messages %}
<div class="messagelist">
{% if messages %}
......@@ -50,7 +72,7 @@
</div>
{% endfor %}
{% endif %}
</div>
</div>
{% endblock messages %}
{% block content %}
<h1 class="alert alert-error">Please override "content" block.</h1>
......@@ -67,7 +89,7 @@
<script src="{% static "jquery/dist/jquery.min.js" %}"></script>
<script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script>
{% compressed_js 'all' %}
{% javascript 'all' %}
{% block extra_script %}
{% endblock %}
......
{% load i18n %}
{% if user and user.pk %}
{% if show_pic %}
<img class="profile-avatar" src="{{ user.profile.get_avatar_url }}" />
{% endif%}
{% if user.get_full_name %}{{ user.get_full_name }}{% else %}{{ user.username }}{% endif %}{% if new_line %}<br />{% endif %}
{% if show_org %}
......@@ -10,4 +14,5 @@
({% trans "username" %}: {{ user.username }})
{% endif %}
{% endif %}
{% endif %}
{% load i18n %}
<form action="{{ acl.url }}" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields" id="{{table_id}}">
<table class="table table-striped table-with-form-fields acl-table" id="{{table_id}}">
<thead>
<tr>
<th></th>
......@@ -13,7 +13,7 @@
{% for i in acl.users %}
<tr>
<td>
<i class="fa fa-user"></i>
<img class="profile-avatar" src="{{ i.user.profile.get_avatar_url }}"/>
</td>
<td>
<a href="{% url "dashboard.views.profile" username=i.user.username %}"
......
......@@ -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 %}
......@@ -10,28 +10,47 @@
{% endblock %}
{% block navbar-brand %}
<a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;">
{% include "branding.html" %}
</a>
{% endblock %}
{% block navbar %}
{% if user.is_authenticated and user.pk and not request.token_user %}
<ul class="nav navbar-nav navbar-right" id="dashboard-menu">
{% 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>
<li>
<a href="/rosetta/">
<i class="fa fa-space-shuttle"></i>
<span class="hidden-sm">{% trans "Translations" %}</span>
</a>
</li>
{% endif %}
<li>
<a href="{% url "dashboard.views.storage" %}"><i class="fa fa-database"></i>
{% trans "Storage" %}
<a href="{% url "dashboard.views.message-list" %}">
<i class="fa fa-bullhorn"></i>
<span class="hidden-sm">{% trans "Messages" %}</span>
</a>
</li>
<li>
<a href="{% url "dashboard.views.storage" %}">
<i class="fa fa-database"></i>
<span class="hidden-sm">{% trans "Storage" %}</span>
</a>
</li>
<li>
<a href="{% url "network.index" %}">
<i class="fa fa-globe"></i>
<span class="hidden-sm">{% trans "Network" %}</span>
</a>
</li>
<li>
<a href="/network/"><i class="fa fa-globe"></i> {% trans "Network" %}</a>
<a href="{% url "request.views.request-list" %}">
<i class="fa fa-phone"></i>
<span class="hidden-sm">{% trans "Requests" %}</span>
</a>
</li>
{% endif %}
<li>
......@@ -65,9 +84,14 @@
{% endif %}
</a>
<ul class="dropdown-menu" id="notification-messages">
<li>{% trans "Loading..." %}</li>
<li>{% trans "Loading..." %}</li>
</ul>
</li>
<li class="hidden-xs">
<a href="{% url "dashboard.views.profile-preferences" %}">
<img class="profile-avatar" src="{{ user.profile.get_avatar_url }}" />
</a>
</li>
</ul>
{% else %}
......
......@@ -90,7 +90,7 @@
{% for i in users %}
<tr>
<td>
<i class="fa fa-user"></i>
<img class="profile-avatar" src="{{ i.profile.get_avatar_url}}"/>
</td>
<td>
<a href="{% url "dashboard.views.profile" username=i.username %}" title="{{ i.username }}"
......
......@@ -20,7 +20,7 @@
<div class="col-xs-6">
<form action="{% url "dashboard.views.group-list" %}" method="GET" id="dashboard-group-search-form">
<div class="input-group input-group-sm">
<input id="dashboard-group-search-input" name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<input name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<div class="input-group-btn">
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
......
......@@ -72,11 +72,15 @@
value="{% widthratio node_num.running sum_node_num 100 %}">
</p>
<p>
{% blocktrans with running=node_num.running missing=node_num.missing disabled=node_num.disabled offline=node_num.offline %}
<span class="big">
<big>{{ node_num.running }}</big> running
</span>
+ <big>{{ node_num.missing }}</big>
missing + <br><big>{{ node_num.disabled }}</big> disabled + <big>{{ node_num.offline }}</big> offline
<span class="big-tag">{{ running }}</span> running
</span> +
<span class="big-tag">{{ missing }}</span> missing +
<br>
<span class="big-tag">{{ disabled }}</span> disabled +
<span class="big-tag">{{ offline }}</span> offline
{% endblocktrans %}
</p>
<ul class="list-inline" id="dashboard-node-taglist">
{% for i in nodes %}
......
......@@ -16,7 +16,7 @@
<i class="fa fa-{{ t.os_type }}"></i> {{ t.name }}
</span>
<small class="text-muted index-template-list-system">{{ t.system }}</small>
<div href="{% url "dashboard.views.vm-create" %}?template={{ t.pk }}" class="pull-right vm-create">
<div data-href="{% url "dashboard.views.vm-create" %}?template={{ t.pk }}" class="pull-right vm-create">
<i data-container="body" title="{% trans "Start VM instance" %}"
class="fa fa-play"></i>
</div>
......@@ -37,7 +37,7 @@
<div class="col-xs-5 col-sm-6">
<form action="{% url "dashboard.views.template-list" %}" method="GET" id="dashboard-template-search-form">
<div class="input-group input-group-sm">
<input id="dashboard-group-search-input" name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<input name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<div class="input-group-btn">
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
......
......@@ -25,7 +25,7 @@
<div class="col-xs-5 col-sm-6">
<form action="{% url "dashboard.views.user-list" %}" method="GET" id="dashboard-user-search-form">
<div class="input-group input-group-sm">
<input id="dashboard-group-search-input" name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<input name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<div class="input-group-btn">
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
......
......@@ -90,7 +90,7 @@
<p class="pull-right">
<input class="knob" data-fgColor="chartreuse" data-thickness=".4" data-max="{{ request.user.profile.instance_limit }}" data-width="100" data-height="100" data-readOnly="true" value="{{ instances|length|add:more_instances }}">
</p>
<span class="bigbig">{% blocktrans with count=running_vm_num %}<big>{{ count }}</big> running{% endblocktrans %}</span>
<span class="bigbig">{% blocktrans with count=running_vm_num %}<span class="big-tag">{{ count }}</span> running{% endblocktrans %}</span>
<ul class="list-inline" style="max-height: 95px; overflow: hidden;">
{% for vm in running_vms %}
<li style="display: inline-block; padding: 2px;">
......@@ -111,7 +111,9 @@
<strong>{{ counter }}</strong> machines total
{% endblocktrans %}
</a>
<p class="big text-warning">{% blocktrans with count=stopped_vm_num %}<big>{{ count }}</big> stopped{% endblocktrans %}</p>
<p class="big text-warning">
{% blocktrans with count=stopped_vm_num %}{{ count }} stopped{% endblocktrans %}
</p>
</div>
</div>
</div>
......@@ -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>
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title-page %}{% trans "Broadcast Messages" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a href="{% url "dashboard.views.message-list" %}" class="btn btn-default btn-xs pull-right">
{% trans "Back" %}
</a>
<h3 class="no-margin">
<i class="fa fa-bullhorn"></i>
{% trans "New message" %}
</h3>
</div>
<div class="panel-body">
{% crispy form %}
</div><!-- .panel-body -->
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title-page %}{% trans "Broadcast Messages" %}{% 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 href="{% url "dashboard.views.message-list" %}"
class="btn btn-default btn-xs">
{% trans "Back" %}
</a>
<a href="{% url "dashboard.views.message-delete" pk=object.pk %}"
class="btn btn-danger btn-xs">
{% trans "Delete" %}
</a>
</div>
<h3 class="no-margin">
<i class="fa fa-bullhorn"></i>
{% trans "Edit message" %}
</h3>
</div>
<div class="panel-body">
{% crispy form %}
</div><!-- .panel-body -->
</div>
</div>
</div>
<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-eye"></i>
{% trans "Preview" %}
</h3>
</div>
<div class="panel-body">
<div class="alert alert-{{ message.effect }} text-center broadcast-message">
{{ message.message|safe }}
</div>
</div><!-- .panel-body -->
</div>
</div>
</div>
{% endblock %}
{% block broadcast_messages %}
{% endblock %}
{% extends "dashboard/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block title-page %}{% trans "Broadcast Messages" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a href="{% url "dashboard.views.message-create" %}" class="pull-right btn btn-success btn-xs">
<i class="fa fa-plus"></i> {% trans "new message" %}
</a>
<h3 class="no-margin"><i class="fa fa-bullhorn"></i> {% trans "Broadcast Messages" %}</h3>
</div>
<div class="panel-body">
<div class="table-responsive">
{% render_table table %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
......@@ -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 %}
......@@ -23,7 +24,6 @@
{% trans "Back" %}</a>
</div>
<h3 class="no-margin">
<i class="fa fa-user"></i>
{% include "dashboard/_display-name.html" with user=profile show_org=True %}
</h3>
</div>
......@@ -42,6 +42,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" %}:
......
......@@ -58,7 +58,7 @@
<div class="text-right">
<form class="pull-left" method="POST" action="{% url "dashboard.views.store-refresh-toplist" %}">
{% csrf_token %}
<button class="btn btn-success btn-xs" type="submit" title="{% trans "Refresh" %}"/>
<button class="btn btn-success btn-xs" type="submit" title="{% trans "Refresh" %}">
<i class="fa fa-refresh"></i>
</button>
</form>
......
{% extends "dashboard/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load compressed %}
{% load pipeline %}
{% block title-page %}{{ instance.name }} | vm{% endblock %}
......@@ -242,5 +242,5 @@
{% endblock %}
{% block extra_js %}
{% compressed_js 'vm-detail' %}
{% javascript 'vm-detail' %}
{% endblock %}
......@@ -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>
......@@ -19,9 +19,9 @@
- {{ a.percentage }}%
{% endif %}
</strong>
{% endspaceless %}{% if a.times < 2 %} <span title="{{ a.started }}">{{ a.started|arrowfilter:LANGUAGE_CODE }}</span>{% endif %}{% if a.user %},
{% endspaceless %}{% if a.times < 2 %}&nbsp;<span title="{{ a.started }}">{{ a.started|arrowfilter:LANGUAGE_CODE }}</span>{% endif %}{% if a.user %},
<a class="no-style-link" href="{% url "dashboard.views.profile" username=a.user.username %}">
{% include "dashboard/_display-name.html" with user=a.user show_org=True %}
{% include "dashboard/_display-name.html" with user=a.user show_org=True show_pic=True %}
</a>
{% endif %}
{% if a.is_abortable_for_user %}
......@@ -45,7 +45,7 @@
{{ s.finished|arrowfilter:LANGUAGE_CODE }}
</span>
{% else %}
<i class="fa fa-refresh fa-spin" class="sub-activity-loading-icon"></i>
<i class="fa fa-refresh fa-spin"></i>
{% endif %}
{% if s.has_failed %}
<div class="label label-danger">{% trans "failed" %}</div>
......
{% load i18n %}
{% load staticfiles %}
<div class="btn-toolbar">
{% if perms.vm.access_console %}
<button id="sendCtrlAltDelButton" class="btn btn-danger btn-sm">{% trans "Send Ctrl+Alt+Del" %}</button>
<button id="sendPasswordButton" class="btn btn-default btn-sm">{% trans "Type password" %}</button>
{% if not perms.vm.access_console %}
<div class="alert alert-warning">
{% trans "You are not authorized to access the VNC console." %}
</div>
{% endif %}
<button id="getScreenshotButton" class="btn btn-info btn-sm pull-right" data-vm-pk="{{ instance.pk }}"><i class="fa fa-picture"></i> {% trans "Screenshot" %}</button>
<div class="row">
<div class="col-xs-7">
<div class="btn-toolbar">
{% if perms.vm.access_console %}
<button id="sendCtrlAltDelButton" class="btn btn-danger btn-sm">
{% trans "Send Ctrl+Alt+Del" %}
</button>
<button id="sendPasswordButton" class="btn btn-default btn-sm">
{% trans "Type password" %}
</button>
{% endif %}
</div>
</div>
<div class="col-xs-5 text-right">
<button id="getScreenshotButton" class="btn btn-info btn-sm" data-vm-pk="{{ instance.pk }}">
<i class="fa fa-photo"></i> {% trans "Screenshot" %}
</button>
</div>
</div>
{% if perms.vm.access_console %}
<div class="alert alert-info" id="noVNC_status">
</div>
<div class="alert alert-info" id="noVNC_status"></div>
{% endif %}
<div id="vm-console-screenshot">
<button class="btn btn-danger btn-sm pull-right">{% trans "Close" %}</button>
<h3>{% trans "Screenshot" %}</h3>
<img />
<h3>
<button class="btn btn-danger btn-sm pull-right">{% trans "Close" %}</button>
{% trans "Screenshot" %}
</h3>
<img alt="{% trans "Screenshot" %}"/>
<hr />
</div>
{% if perms.vm.access_console %}
<canvas id="noVNC_canvas" width="640px" height="20px">Canvas not supported.
<canvas id="noVNC_canvas" width="640" height="20">Canvas not supported.
</canvas>
<script>
......
......@@ -39,7 +39,7 @@
<dd>
{% csrf_token %}
<div class="vm-details-home-edit-description-click">
<small class="vm-details-home-edit-description">{{ instance.description|linebreaks }}</small>
<div class="vm-details-home-edit-description">{{ instance.description|linebreaks }}</div>
</div>
<div id="vm-details-home-description" class="js-hidden">
<form method="POST">
......@@ -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>
......@@ -156,6 +157,16 @@
</a>
</div>
{% endif %}
{% if op.install_keys %}
<strong>{% trans "SSH keys" %}</strong>
<div class="operation-wrapper">
<a href="{{ op.install_keys.get_url }}" class="btn btn-info btn-xs operation"
{% if op.install_keys.disabled %}disabled{% endif %}>
<i class="fa fa-{{op.install_keys.icon}}"></i>
{{ op.install_keys.name }}
</a>
</div>
{% endif %}
</div>
<div class="col-md-8">
{% if graphite_enabled %}
......@@ -163,9 +174,12 @@
{% include "dashboard/_graph-time-buttons.html" %}
</div>
<div class="graph-images text-center">
<img src="{% url "dashboard.views.vm-graph" instance.pk "cpu" graph_time %}"/>
<img src="{% url "dashboard.views.vm-graph" instance.pk "memory" graph_time %}"/>
<img src="{% url "dashboard.views.vm-graph" instance.pk "network" graph_time %}"/>
<img src="{% url "dashboard.views.vm-graph" instance.pk "cpu" graph_time %}"
alt="{% trans "CPU usage" %}"/>
<img src="{% url "dashboard.views.vm-graph" instance.pk "memory" graph_time %}"
alt="{% trans "Memory usage" %}"/>
<img src="{% url "dashboard.views.vm-graph" instance.pk "network" graph_time %}"
alt="{% trans "Network usage" %}"/>
</div>
{% endif %}
</div>
......
{% load i18n %}
{% load network_tags %}
<div id="vm-details-add-interface">
{% with op=op.add_interface %}{% if op %}
<a href="{{op.get_url}}" class="btn btn-{{op.effect}} operation pull-right"
{% if op.disabled %}disabled{% endif %}>
<i class="fa fa-{{op.icon}}"></i> {% trans "add interface" %}</a>
{% endif %}{% endwith %}
</div>
<h2>
<div id="vm-details-add-interface">
{% with op=op.add_interface %}{% if op %}
<a href="{{op.get_url}}" class="btn btn-{{op.effect}} operation pull-right"
{% if op.disabled %}disabled{% endif %}>
<i class="fa fa-{{op.icon}}"></i> {% trans "add interface" %}</a>
{% endif %}{% endwith %}
</div>
{% trans "Interfaces" %}
</h2>
......
......@@ -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>
......@@ -22,12 +45,12 @@
<div id="vm-details-resources-disk">
<h3>
{% trans "Disks" %}
<div class="pull-right">
<div id="disk-ops">
{% include "dashboard/vm-detail/_disk-operations.html" %}
</div>
</div>
{% trans "Disks" %}
</h3>
<div class="clearfix"></div>
......@@ -46,7 +69,7 @@
{% if user.is_superuser %}
<hr/>
<div class="row" id="">
<div class="row">
<div class="col-sm-12">
<h3>
{% trans "Required traits" %}
......@@ -58,7 +81,7 @@
<hr/>
<div class="row" id="">
<div class="row">
<div class="col-sm-12">
<h3>
{% trans "Raw data" %}
......
<meta property="og:image" content="{{ og_image }}"/>
<meta property="og:site_name" content="CIRCLE Cloud - devenv"/>
<meta property="og:title" content="CIRCLE Cloud - devenv"/>
<meta property="og:type" content="website"/>
#!/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
......
......@@ -54,6 +54,7 @@ from .views import (
NodeActivityView,
UserList,
StorageDetail, DiskDetail,
MessageList, MessageDetail, MessageCreate, MessageDelete,
)
from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops
......@@ -228,11 +229,19 @@ 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(),
name="dashboard.views.disk-detail"),
url(r'^message/list/$', MessageList.as_view(),
name="dashboard.views.message-list"),
url(r'^message/(?P<pk>\d+)/$', MessageDetail.as_view(),
name="dashboard.views.message-detail"),
url(r'^message/create/$', MessageCreate.as_view(),
name="dashboard.views.message-create"),
url(r'^message/delete/(?P<pk>\d+)/$', MessageDelete.as_view(),
name="dashboard.views.message-delete"),
)
urlpatterns += patterns(
......
......@@ -13,3 +13,5 @@ from util import *
from vm import *
from graph import *
from storage import *
from request import *
from message import *
......@@ -212,7 +212,7 @@ class VmNetwork(object):
'alias(scaleToSeconds(nonNegativeDerivative('
'%s.network.bytes_sent-%s), 10), "in - %s (bits/s)")' % (
params))
return 'group(%s)' % ','.join(metrics)
return 'group(%s)' % ','.join(metrics) if metrics else None
register_graph(VmNetwork, 'network', VmGraphView)
......
......@@ -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)
......
# 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.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.views.generic import CreateView, DeleteView, UpdateView
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from django_tables2 import SingleTableView
from ..forms import MessageForm
from ..models import Message
from ..tables import MessageListTable
class InvalidateMessageCacheMixin(object):
def post(self, *args, **kwargs):
key = make_template_fragment_key('broadcast_messages')
cache.delete(key)
return super(InvalidateMessageCacheMixin, self).post(*args, **kwargs)
class MessageList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
template_name = "dashboard/message-list.html"
model = Message
table_class = MessageListTable
class MessageDetail(InvalidateMessageCacheMixin, LoginRequiredMixin,
SuperuserRequiredMixin, SuccessMessageMixin, UpdateView):
model = Message
template_name = "dashboard/message-edit.html"
form_class = MessageForm
success_message = _("Broadcast message successfully updated.")
class MessageCreate(InvalidateMessageCacheMixin, LoginRequiredMixin,
SuperuserRequiredMixin, SuccessMessageMixin, CreateView):
model = Message
template_name = "dashboard/message-create.html"
form_class = MessageForm
success_message = _("New broadcast message successfully created.")
class MessageDelete(InvalidateMessageCacheMixin, LoginRequiredMixin,
SuperuserRequiredMixin, DeleteView):
model = Message
template_name = "dashboard/confirm/base-delete.html"
def get_success_url(self):
return reverse("dashboard.views.message-list")
......@@ -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>')
......
......@@ -34,6 +34,7 @@ from django.core.paginator import Paginator, InvalidPage
from django.db.models import Q
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import redirect, get_object_or_404
from django.templatetags.static import static
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST
from django.views.generic import (
......@@ -101,6 +102,8 @@ def circle_login(request):
authentication_form = CircleAuthenticationForm
extra_context = {
'saml2': saml_available,
'og_image': (settings.DJANGO_URL.rstrip("/") +
static("dashboard/img/og.png"))
}
response = login_view(request, authentication_form=authentication_form,
extra_context=extra_context)
......
......@@ -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'
......@@ -762,6 +776,10 @@ vm_ops = OrderedDict([
op='mount_store', icon='briefcase', effect='info',
show_in_toolbar=False,
)),
('install_keys', VmOperationView.factory(
op='install_keys', icon='key', effect='info',
show_in_toolbar=False,
)),
])
......@@ -1043,6 +1061,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 +1317,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"):
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -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 %}
......
......@@ -74,13 +74,6 @@
{% endblock %}
{% block navbar-brand %}
<a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;">
<img src="{% static "dashboard/img/logo.png" %}" style="height: 25px;"/>
</a>
{% endblock %}
{% block content %}
<div class="content">
{% block content_box %}{% endblock %}
......
......@@ -6,10 +6,8 @@
{% block title-page %}{% trans "Login" %}{% endblock %}
{% block navbar-brand %}
<a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;">
<img src="{% static "dashboard/img/logo.png" %}" style="height: 25px;"/>
</a>
{% block extra_link %}
{% include "open-graph.html" %}
{% endblock %}
{% block content_box %}
......@@ -21,10 +19,7 @@
{% endif %}
<div class="col-xs-{% if saml2 %}6{% else %}12{% endif %}">
<div class="login-form">
<form action="" method="POST">
{% csrf_token %}
{% crispy form %}
</form>
{% crispy form %}
</div>
</div>
{% if saml2 %}
......@@ -33,11 +28,12 @@
<a href="{% url "saml2_login" %}">{% trans "Click here!" %}</a>
</div>
{% endif %}
</div>
<div class="row">
<div class="col-sm-12">
<a class="pull-right" href="{% url "accounts.password-reset" %}">{% trans "Forgot your password?" %}</a>
<a class="pull-right" href="{% url "accounts.password-reset" %}" style="margin-right: 15px;">
{% trans "Forgot your password?" %}
</a>
</div>
</div>
</div>
</div>
{% 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:
......
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