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 @@ ...@@ -15,4 +15,8 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # 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/') ...@@ -156,6 +156,7 @@ STATIC_URL = get_env_variable('DJANGO_STATIC_URL', default='/static/')
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'pipeline.finders.PipelineFinder',
) )
########## END STATIC FILE CONFIGURATION ########## END STATIC FILE CONFIGURATION
STATICFILES_DIRS = [normpath(join(SITE_ROOT, 'bower_components'))] STATICFILES_DIRS = [normpath(join(SITE_ROOT, 'bower_components'))]
...@@ -282,6 +283,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -282,6 +283,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.request', 'django.core.context_processors.request',
'dashboard.context_processors.notifications', 'dashboard.context_processors.notifications',
'dashboard.context_processors.extract_settings', 'dashboard.context_processors.extract_settings',
'dashboard.context_processors.broadcast_messages',
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
...@@ -355,6 +357,7 @@ LOCAL_APPS = ( ...@@ -355,6 +357,7 @@ LOCAL_APPS = (
'manager', 'manager',
'acl', 'acl',
'monitor', 'monitor',
'request',
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
...@@ -449,7 +452,7 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': ...@@ -449,7 +452,7 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
) )
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'djangosaml2.backends.Saml2Backend', 'common.backends.Saml2Backend',
) )
remote_metadata = join(SITE_ROOT, 'remote_metadata.xml') remote_metadata = join(SITE_ROOT, 'remote_metadata.xml')
...@@ -527,6 +530,10 @@ except: ...@@ -527,6 +530,10 @@ except:
LOCALE_PATHS = (join(SITE_ROOT, 'locale'), ) LOCALE_PATHS = (join(SITE_ROOT, 'locale'), )
COMPANY_NAME = get_env_variable("COMPANY_NAME", "BME IK 2015") 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_host = environ.get("GRAPHITE_HOST", None)
graphite_port = environ.get("GRAPHITE_PORT", None) graphite_port = environ.get("GRAPHITE_PORT", None)
if graphite_host and graphite_port: if graphite_host and graphite_port:
...@@ -555,3 +562,4 @@ ADMIN_ENABLED = False ...@@ -555,3 +562,4 @@ ADMIN_ENABLED = False
BLACKLIST_PASSWORD = get_env_variable("BLACKLIST_PASSWORD", "") BLACKLIST_PASSWORD = get_env_variable("BLACKLIST_PASSWORD", "")
BLACKLIST_HOOK_URL = get_env_variable("BLACKLIST_HOOK_URL", "") BLACKLIST_HOOK_URL = get_env_variable("BLACKLIST_HOOK_URL", "")
REQUEST_HOOK_URL = get_env_variable("REQUEST_HOOK_URL", "")
...@@ -110,8 +110,8 @@ if DEBUG: ...@@ -110,8 +110,8 @@ if DEBUG:
from django.dispatch import Signal from django.dispatch import Signal
Signal.send_robust = Signal.send Signal.send_robust = Signal.send
PIPELINE_DISABLED_COMPILERS = ( PIPELINE_COMPILERS = (
'pipeline.compilers.less.LessCompiler', 'dashboard.compilers.DummyLessCompiler',
) )
ADMIN_ENABLED = True ADMIN_ENABLED = True
...@@ -40,7 +40,8 @@ INSTALLED_APPS += ( ...@@ -40,7 +40,8 @@ INSTALLED_APPS += (
) )
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 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] NOSE_ARGS = ['--stop', '--with-doctest', '--with-selenium-driver', '--selenium-driver=firefox', '-w%s' % path_to_selenium_test]
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher'] PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
......
...@@ -56,6 +56,16 @@ LOGGING['handlers']['console'] = {'level': level, ...@@ -56,6 +56,16 @@ LOGGING['handlers']['console'] = {'level': level,
'formatter': 'simple'} 'formatter': 'simple'}
for i in LOCAL_APPS: for i in LOCAL_APPS:
LOGGING['loggers'][i] = {'handlers': ['console'], 'level': level} 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 # Forbid store usage
STORE_URL = "" STORE_URL = ""
......
...@@ -38,6 +38,7 @@ urlpatterns = patterns( ...@@ -38,6 +38,7 @@ urlpatterns = patterns(
url(r'^network/', include('network.urls')), url(r'^network/', include('network.urls')),
url(r'^blacklist-add/', add_blacklist_item), url(r'^blacklist-add/', add_blacklist_item),
url(r'^dashboard/', include('dashboard.urls')), url(r'^dashboard/', include('dashboard.urls')),
url(r'^request/', include('request.urls')),
# django/contrib/auth/urls.py (care when new version) # django/contrib/auth/urls.py (care when new version)
url((r'^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/' url((r'^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/'
...@@ -87,3 +88,4 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': ...@@ -87,3 +88,4 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
) )
handler500 = 'common.views.handler500' 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): ...@@ -76,7 +76,7 @@ class Operation(object):
user = auxargs.pop('user') user = auxargs.pop('user')
parent_activity = auxargs.pop('parent_activity') parent_activity = auxargs.pop('parent_activity')
if parent_activity and user is None and not skip_auth_check: 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 if user is None: # parent was a system call
skip_auth_check = True skip_auth_check = True
...@@ -170,8 +170,8 @@ class Operation(object): ...@@ -170,8 +170,8 @@ class Operation(object):
raise ImproperlyConfigured( raise ImproperlyConfigured(
"Set required_perms to () if none needed.") "Set required_perms to () if none needed.")
if not user.has_perms(cls.required_perms): if not user.has_perms(cls.required_perms):
raise PermissionDenied("%s doesn't have the required permissions." raise PermissionDenied(
% user) u"%s doesn't have the required permissions." % user)
if cls.superuser_required and not user.is_superuser: if cls.superuser_required and not user.is_superuser:
raise humanize_exception(ugettext_noop( raise humanize_exception(ugettext_noop(
"Superuser privileges are required."), PermissionDenied()) "Superuser privileges are required."), PermissionDenied())
......
...@@ -19,32 +19,42 @@ from sys import exc_info ...@@ -19,32 +19,42 @@ from sys import exc_info
import logging import logging
from django.template import RequestContext
from django.shortcuts import render_to_response from django.shortcuts import render_to_response
from django.template import RequestContext
from .models import HumanReadableException from .models import HumanReadableException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def handler500(request): def get_context(request, exception):
cls, exception, traceback = exc_info()
logger.exception("unhandled exception")
ctx = {} ctx = {}
if isinstance(exception, HumanReadableException): if issubclass(exception.__class__, HumanReadableException):
try: try:
ctx['error'] = exception.get_user_text() if request.user.is_superuser:
except:
pass
else:
try:
if request.user.is_superuser():
ctx['error'] = exception.get_admin_text() ctx['error'] = exception.get_admin_text()
else:
ctx['error'] = exception.get_user_text()
except: except:
pass pass
return ctx
def handler500(request):
cls, exception, traceback = exc_info()
logger.exception("unhandled exception")
ctx = get_context(request, exception)
try: try:
resp = render_to_response("500.html", ctx, RequestContext(request)) resp = render_to_response("500.html", ctx, RequestContext(request))
except: except:
resp = render_to_response("500.html", ctx) resp = render_to_response("500.html", ctx)
resp.status_code = 500 resp.status_code = 500
return resp 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 ...@@ -21,7 +21,7 @@ from django import contrib
from django.contrib.auth.admin import UserAdmin, GroupAdmin from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib.auth.models import User, Group 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): class ProfileInline(contrib.admin.TabularInline):
...@@ -43,3 +43,5 @@ contrib.admin.site.unregister(User) ...@@ -43,3 +43,5 @@ contrib.admin.site.unregister(User)
contrib.admin.site.register(User, UserAdmin) contrib.admin.site.register(User, UserAdmin)
contrib.admin.site.unregister(Group) contrib.admin.site.unregister(Group)
contrib.admin.site.register(Group, GroupAdmin) 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 @@ ...@@ -17,6 +17,8 @@
from django.conf import settings from django.conf import settings
from .models import Message
def notifications(request): def notifications(request):
count = (request.user.notification_set.filter(status="new").count() count = (request.user.notification_set.filter(status="new").count()
...@@ -31,3 +33,7 @@ def extract_settings(request): ...@@ -31,3 +33,7 @@ def extract_settings(request):
'COMPANY_NAME': getattr(settings, "COMPANY_NAME", None), 'COMPANY_NAME': getattr(settings, "COMPANY_NAME", None),
'ADMIN_ENABLED': getattr(settings, "ADMIN_ENABLED", False), 'ADMIN_ENABLED': getattr(settings, "ADMIN_ENABLED", False),
} }
def broadcast_messages(request):
return {'broadcast_messages': Message.timeframed.filter(enabled=True)}
...@@ -1395,6 +1395,7 @@ ...@@ -1395,6 +1395,7 @@
"vnc_port": 1234, "vnc_port": 1234,
"num_cores": 2, "num_cores": 2,
"status": "RUNNING", "status": "RUNNING",
"system": "system pls",
"modified": "2013-10-14T07:27:38.192Z" "modified": "2013-10-14T07:27:38.192Z"
} }
}, },
......
...@@ -57,7 +57,7 @@ from vm.models import ( ...@@ -57,7 +57,7 @@ from vm.models import (
from storage.models import DataStore, Disk from storage.models import DataStore, Disk
from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.models import Permission 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 circle.settings.base import LANGUAGES, MAX_NODE_RAM
from django.utils.translation import string_concat from django.utils.translation import string_concat
...@@ -739,6 +739,7 @@ class LeaseForm(forms.ModelForm): ...@@ -739,6 +739,7 @@ class LeaseForm(forms.ModelForm):
class Meta: class Meta:
model = Lease model = Lease
exclude = ()
class VmRenewForm(OperationForm): class VmRenewForm(OperationForm):
...@@ -1604,6 +1605,7 @@ class DataStoreForm(ModelForm): ...@@ -1604,6 +1605,7 @@ class DataStoreForm(ModelForm):
class Meta: class Meta:
model = DataStore model = DataStore
fields = ("name", "path", "hostname", )
class DiskForm(ModelForm): class DiskForm(ModelForm):
...@@ -1620,3 +1622,17 @@ class DiskForm(ModelForm): ...@@ -1620,3 +1622,17 @@ class DiskForm(ModelForm):
class Meta: class Meta:
model = Disk 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 @@ ...@@ -17,27 +17,29 @@
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import logging
from optparse import make_option from optparse import make_option
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from firewall.models import (Vlan, VlanGroup, Domain, Firewall, Rule, from firewall.models import Vlan, VlanGroup, Domain, Firewall, Rule
SwitchPort, EthernetDevice, Host)
from storage.models import DataStore from storage.models import DataStore
from vm.models import Lease from vm.models import Lease
logger = logging.getLogger(__name__)
class Command(BaseCommand): class Command(BaseCommand):
option_list = BaseCommand.option_list + ( option_list = BaseCommand.option_list + (
make_option('--force', action="store_true"), make_option('--force', action="store_true"),
make_option('--portal-ip'),
make_option('--external-net'), make_option('--external-net'),
make_option('--management-net'), make_option('--management-net'),
make_option('--vm-net'), make_option('--vm-net'),
make_option('--external-if'), make_option('--external-if'),
make_option('--management-if'), make_option('--management-if'),
make_option('--trunk-if'), make_option('--vm-if'),
make_option('--datastore-queue'), make_option('--datastore-queue'),
make_option('--firewall-queue'), make_option('--firewall-queue'),
make_option('--admin-user'), make_option('--admin-user'),
...@@ -49,18 +51,18 @@ class Command(BaseCommand): ...@@ -49,18 +51,18 @@ class Command(BaseCommand):
qs = model.objects.filter(**{field: value})[:1] qs = model.objects.filter(**{field: value})[:1]
if not qs.exists(): if not qs.exists():
obj = model.objects.create(**kwargs) 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 return obj
else: else:
return qs[0] return qs[0]
# http://docs.saltstack.com/en/latest/ref/states/all/salt.states.cmd.html # http://docs.saltstack.com/en/latest/ref/states/all/salt.states.cmd.html
def print_state(self): def print_state(self):
changed = "yes" if len(self.changed) else "no" print "\nchanged=%s" % ("yes" if self.changed else "no")
print "\nchanged=%s comment='%s'" % (changed, ", ".join(self.changed))
def handle(self, *args, **options): def handle(self, *args, **options):
self.changed = [] self.changed = False
if (DataStore.objects.exists() and Vlan.objects.exists() if (DataStore.objects.exists() and Vlan.objects.exists()
and not options['force']): and not options['force']):
...@@ -87,20 +89,28 @@ class Command(BaseCommand): ...@@ -87,20 +89,28 @@ class Command(BaseCommand):
suspend_interval_seconds=3600 * 24 * 365, suspend_interval_seconds=3600 * 24 * 365,
delete_interval_seconds=3600 * 24 * 365 * 3) 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 # vlans
net = self.create(Vlan, 'name', name='net', vid=4, net = self.create(Vlan, 'vid', name=options['external_if'], vid=4,
network4=options['external_net'], domain=domain) network4=options['external_net'], domain=net_domain)
man = self.create(Vlan, 'name', name='man', vid=3, dhcp_pool='manual', man = self.create(Vlan, 'vid', name=options['management_if'], vid=3,
network4=options['management_net'], domain=domain, dhcp_pool='manual',
network4=options['management_net'],
domain=man_domain,
snat_ip=options['external_net'].split('/')[0]) snat_ip=options['external_net'].split('/')[0])
man.snat_to.add(net) man.snat_to.add(net)
man.snat_to.add(man) man.snat_to.add(man)
vm = self.create(Vlan, 'name', name='vm', vid=2, dhcp_pool='manual', vm = self.create(Vlan, 'vid', name=options['vm_if'], vid=2,
network4=options['vm_net'], domain=domain, dhcp_pool='manual',
network4=options['vm_net'], domain=vm_domain,
snat_ip=options['external_net'].split('/')[0]) snat_ip=options['external_net'].split('/')[0])
vm.snat_to.add(net) vm.snat_to.add(net)
vm.snat_to.add(vm) vm.snat_to.add(vm)
...@@ -115,14 +125,6 @@ class Command(BaseCommand): ...@@ -115,14 +125,6 @@ class Command(BaseCommand):
vg_net = self.create(VlanGroup, 'name', name='net') vg_net = self.create(VlanGroup, 'name', name='net')
vg_net.vlans.add(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 # firewall rules
fw = self.create(Firewall, 'name', name=options['firewall_queue']) fw = self.create(Firewall, 'name', name=options['firewall_queue'])
...@@ -130,8 +132,16 @@ class Command(BaseCommand): ...@@ -130,8 +132,16 @@ class Command(BaseCommand):
direction='out', action='accept', direction='out', action='accept',
foreign_network=vg_all, firewall=fw) foreign_network=vg_all, firewall=fw)
self.create(Rule, 'description', description='default input rule', self.create(Rule, 'description', description='portal https',
direction='in', action='accept', 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) foreign_network=vg_all, firewall=fw)
# vlan rules # vlan rules
...@@ -143,23 +153,4 @@ class Command(BaseCommand): ...@@ -143,23 +153,4 @@ class Command(BaseCommand):
direction='out', action='accept', direction='out', action='accept',
foreign_network=vg_net, vlan=man) 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() 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 ...@@ -27,7 +27,7 @@ from django.contrib.auth.signals import user_logged_in
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import ( from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField, 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.db.models.signals import post_save, pre_delete, post_delete
from django.templatetags.static import static from django.templatetags.static import static
...@@ -39,14 +39,13 @@ from django.core.exceptions import ObjectDoesNotExist ...@@ -39,14 +39,13 @@ from django.core.exceptions import ObjectDoesNotExist
from sizefield.models import FileSizeField from sizefield.models import FileSizeField
from jsonfield import JSONField 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.fields import StatusField
from model_utils import Choices from model_utils import Choices
from acl.models import AclBase from acl.models import AclBase
from common.models import HumanReadableObject, create_readable, Encoder 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 vm.models.instance import ACCESS_METHODS
from .store_api import Store, NoStoreException, NotOkException, Timeout from .store_api import Store, NoStoreException, NotOkException, Timeout
...@@ -59,6 +58,27 @@ def pwgen(): ...@@ -59,6 +58,27 @@ def pwgen():
return User.objects.make_random_password() 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): class Favourite(Model):
instance = ForeignKey("vm.Instance") instance = ForeignKey("vm.Instance")
user = ForeignKey(User) user = ForeignKey(User)
...@@ -270,10 +290,9 @@ class GroupProfile(AclBase): ...@@ -270,10 +290,9 @@ class GroupProfile(AclBase):
except cls.DoesNotExist: except cls.DoesNotExist:
return Group.objects.get(name=name) return Group.objects.get(name=name)
@permalink
def get_absolute_url(self): def get_absolute_url(self):
return ('dashboard.views.group-detail', None, return reverse('dashboard.views.group-detail',
{'pk': self.group.pk}) kwargs={'pk': self.group.pk})
def get_or_create_profile(self): def get_or_create_profile(self):
...@@ -309,7 +328,7 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'): ...@@ -309,7 +328,7 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
attributes = kwargs.pop('attributes') attributes = kwargs.pop('attributes')
atr = settings.SAML_ORG_ID_ATTRIBUTE atr = settings.SAML_ORG_ID_ATTRIBUTE
try: try:
value = attributes[atr][0] value = attributes[atr][0].upper()
except Exception as e: except Exception as e:
value = None value = None
logger.info("save_org_id couldn't find attribute. %s", unicode(e)) logger.info("save_org_id couldn't find attribute. %s", unicode(e))
...@@ -339,7 +358,7 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'): ...@@ -339,7 +358,7 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
group, unicode(g)) group, unicode(g))
g.user_set.add(sender) 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.group.user_set.add(sender)
i.delete() i.delete()
...@@ -409,9 +428,7 @@ def add_ssh_keys(sender, **kwargs): ...@@ -409,9 +428,7 @@ def add_ssh_keys(sender, **kwargs):
'user', userkey.user).filter(status='RUNNING') 'user', userkey.user).filter(status='RUNNING')
for i in instances: for i in instances:
logger.info('called add_keys(%s, %s)', i, userkey) logger.info('called add_keys(%s, %s)', i, userkey)
queue = i.get_remote_queue_name("agent") i.install_keys(user=userkey.user, keys=[userkey.key])
add_keys.apply_async(args=(i.vm_name, [userkey.key]),
queue=queue)
def del_ssh_keys(sender, **kwargs): def del_ssh_keys(sender, **kwargs):
...@@ -422,9 +439,7 @@ def del_ssh_keys(sender, **kwargs): ...@@ -422,9 +439,7 @@ def del_ssh_keys(sender, **kwargs):
'user', userkey.user).filter(status='RUNNING') 'user', userkey.user).filter(status='RUNNING')
for i in instances: for i in instances:
logger.info('called del_keys(%s, %s)', i, userkey) logger.info('called del_keys(%s, %s)', i, userkey)
queue = i.get_remote_queue_name("agent") i.remove_keys(user=userkey.user, keys=[userkey.key])
del_keys.apply_async(args=(i.vm_name, [userkey.key]),
queue=queue)
post_save.connect(add_ssh_keys, sender=UserKey) post_save.connect(add_ssh_keys, sender=UserKey)
......
...@@ -145,11 +145,13 @@ $(function() { ...@@ -145,11 +145,13 @@ $(function() {
$("#dashboard-vm-details-connect-button").removeClass('disabled'); $("#dashboard-vm-details-connect-button").removeClass('disabled');
} }
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled"); $("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
$("#getScreenshotButton").prop("disabled", false);
} else { } else {
if(data.connect_uri) { if(data.connect_uri) {
$("#dashboard-vm-details-connect-button").addClass('disabled'); $("#dashboard-vm-details-connect-button").addClass('disabled');
} }
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").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") { if(data.status == "STOPPED" || data.status == "PENDING") {
......
...@@ -10,10 +10,11 @@ $(function () { ...@@ -10,10 +10,11 @@ $(function () {
$(".not-tab-pane").removeClass("not-tab-pane").addClass("tab-pane"); $(".not-tab-pane").removeClass("not-tab-pane").addClass("tab-pane");
$('.vm-create').click(function(e) { $('.vm-create').click(function(e) {
var template = $(this).data("template"); var url = $(this).data("href");
if(!url) url = $(this).prop("href");
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: $(this).attr('href'), url: url,
success: function(data) { success: function(data) {
$('body').append(data); $('body').append(data);
vmCreateLoaded(); vmCreateLoaded();
...@@ -140,7 +141,7 @@ $(function () { ...@@ -140,7 +141,7 @@ $(function () {
// success // success
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
console.log("oh babám"); addMessage(gettext("An error occurred. (") + xhr.status + ")", 'danger');
} }
}); });
$(star).tooltip('destroy').tooltip({'placement': 'right'}); $(star).tooltip('destroy').tooltip({'placement': 'right'});
...@@ -528,6 +529,7 @@ function safe_tags_replace(str) { ...@@ -528,6 +529,7 @@ function safe_tags_replace(str) {
return str.replace(/[&<>]/g, replaceTag); return str.replace(/[&<>]/g, replaceTag);
} }
$('.crosslink').click(function(e) { $('.crosslink').click(function(e) {
// Don't follow the link // Don't follow the link
event.preventDefault(); event.preventDefault();
...@@ -535,3 +537,33 @@ $('.crosslink').click(function(e) { ...@@ -535,3 +537,33 @@ $('.crosslink').click(function(e) {
$(menu).click(); $(menu).click();
window.location = this.href; 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 { ...@@ -10,6 +10,21 @@ html {
min-height: 100%; 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 */ /* Set widths on the navbar form inputs since otherwise they're 100% wide */
.navbar-form input[type="text"], .navbar-form input[type="text"],
.navbar-form input[type="password"] { .navbar-form input[type="password"] {
...@@ -32,7 +47,7 @@ html { ...@@ -32,7 +47,7 @@ html {
#dashboard-menu { #dashboard-menu {
margin-right: 15px; margin-right: 0px;
} }
/* we need this for mobile view */ /* we need this for mobile view */
...@@ -56,6 +71,9 @@ html { ...@@ -56,6 +71,9 @@ html {
padding-bottom: 12.5px; padding-bottom: 12.5px;
} }
#dashboard-menu > li:last-child > a {
padding: 4px 0 0 0;
}
} }
.no-margin { .no-margin {
...@@ -110,6 +128,11 @@ html { ...@@ -110,6 +128,11 @@ html {
background-color: black!important; background-color: black!important;
} }
.timeline img{
border-radius: 50%;
margin-bottom: 2px;
}
.timeline a { .timeline a {
color: black; color: black;
} }
...@@ -1272,6 +1295,25 @@ textarea[name="new_members"] { ...@@ -1272,6 +1295,25 @@ textarea[name="new_members"] {
margin-top: 20px; 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 { #disk-list-table {
td:last-child { td:last-child {
text-align: center; text-align: center;
...@@ -1442,3 +1484,32 @@ height: calc(100% - 130px); ...@@ -1442,3 +1484,32 @@ height: calc(100% - 130px);
.overview_href { .overview_href {
cursor: pointer; 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() { ...@@ -38,6 +38,13 @@ $(function() {
e.preventDefault(); 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 */ /* remove tag */
$('.vm-details-remove-tag').click(function() { $('.vm-details-remove-tag').click(function() {
var to_remove = $.trim($(this).parent('div').text()); var to_remove = $.trim($(this).parent('div').text());
...@@ -178,12 +185,13 @@ $(function() { ...@@ -178,12 +185,13 @@ $(function() {
$("i", this).addClass("fa-spinner fa-spin"); $("i", this).addClass("fa-spinner fa-spin");
$(this).prop("disabled", true); $(this).prop("disabled", true);
ct.slideDown(); 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 // if the image is loaded remove the spinning stuff
// note: this should not work if the image is cached, but it's not // note: this should not work if the image is cached, but it's not
// see: http://stackoverflow.com/a/3877079/1112653 // 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) { $("#vm-console-screenshot img").load(function(e) {
$("#getScreenshotButton").prop("disabled", false) $("#getScreenshotButton").prop("disabled", false)
.find("i").removeClass("fa-spinner fa-spin"); .find("i").removeClass("fa-spinner fa-spin");
...@@ -193,7 +201,7 @@ $(function() { ...@@ -193,7 +201,7 @@ $(function() {
// screenshot close // screenshot close
$("#vm-console-screenshot button").click(function() { $("#vm-console-screenshot button").click(function() {
$(this).parent("div").slideUp(); $(this).closest("div").slideUp();
}); });
// select connection string // select connection string
...@@ -223,4 +231,25 @@ $(function() { ...@@ -223,4 +231,25 @@ $(function() {
return false; 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 { ...@@ -55,6 +55,9 @@ html {
.bigbig { .bigbig {
font-size: 3em; font-size: 3em;
} }
.big-tag {
font-size: 1.2em;
}
/* small buttons for tags, copied from Bootstraps input-sm, bnt-sm */ /* small buttons for tags, copied from Bootstraps input-sm, bnt-sm */
.btn-tags, .btn-traits { .btn-tags, .btn-traits {
padding: 3px 6px; padding: 3px 6px;
...@@ -148,3 +151,7 @@ footer a, footer a:hover, footer a:visited { ...@@ -148,3 +151,7 @@ footer a, footer a:hover, footer a:visited {
.btn-toolbar { .btn-toolbar {
margin-bottom: 5px; margin-bottom: 5px;
} }
.vm-details-home-edit-description {
font-size: 85%; /* ~ small tag */
}
...@@ -29,7 +29,7 @@ from django_sshkey.models import UserKey ...@@ -29,7 +29,7 @@ from django_sshkey.models import UserKey
from storage.models import Disk from storage.models import Disk
from vm.models import Node, InstanceTemplate, Lease from vm.models import Node, InstanceTemplate, Lease
from dashboard.models import ConnectCommand from dashboard.models import ConnectCommand, Message
class FileSizeColumn(Column): class FileSizeColumn(Column):
...@@ -354,3 +354,19 @@ class DiskListTable(Table): ...@@ -354,3 +354,19 @@ class DiskListTable(Table):
order_by = ("-pk", ) order_by = ("-pk", )
per_page = 15 per_page = 15
empty_text = _("No disk found.") 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 i18n %}
{% load staticfiles %} {% load staticfiles %}
{% load compressed %} {% load cache %}
{% load pipeline %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{lang}}"> <html lang="{{lang}}">
<head> <head>
...@@ -9,17 +10,17 @@ ...@@ -9,17 +10,17 @@
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" content=""> <meta name="author" content="">
<link rel="icon" type="image/png" href="{% static "dashboard/img/favicon.png" %}"/> <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> <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 --> <!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script> <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]--> <![endif]-->
{% block extra_link %}{% endblock %}
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
...@@ -27,7 +28,12 @@ ...@@ -27,7 +28,12 @@
<body> <body>
<div class="navbar navbar-inverse navbar-fixed-top"> <div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-header"> <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"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
...@@ -40,6 +46,22 @@ ...@@ -40,6 +46,22 @@
</div><!-- navbar navbar-inverse navbar-fixed-top --> </div><!-- navbar navbar-inverse navbar-fixed-top -->
<div class="container"> <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 %} {% block messages %}
<div class="messagelist"> <div class="messagelist">
{% if messages %} {% if messages %}
...@@ -67,7 +89,7 @@ ...@@ -67,7 +89,7 @@
<script src="{% static "jquery/dist/jquery.min.js" %}"></script> <script src="{% static "jquery/dist/jquery.min.js" %}"></script>
<script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script> <script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script>
{% compressed_js 'all' %} {% javascript 'all' %}
{% block extra_script %} {% block extra_script %}
{% endblock %} {% endblock %}
......
{% load i18n %} {% load i18n %}
{% if user and user.pk %} {% 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 user.get_full_name %}{{ user.get_full_name }}{% else %}{{ user.username }}{% endif %}{% if new_line %}<br />{% endif %}
{% if show_org %} {% if show_org %}
...@@ -10,4 +14,5 @@ ...@@ -10,4 +14,5 @@
({% trans "username" %}: {{ user.username }}) ({% trans "username" %}: {{ user.username }})
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% load i18n %} {% load i18n %}
<form action="{{ acl.url }}" method="post">{% csrf_token %} <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> <thead>
<tr> <tr>
<th></th> <th></th>
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
{% for i in acl.users %} {% for i in acl.users %}
<tr> <tr>
<td> <td>
<i class="fa fa-user"></i> <img class="profile-avatar" src="{{ i.user.profile.get_avatar_url }}"/>
</td> </td>
<td> <td>
<a href="{% url "dashboard.views.profile" username=i.user.username %}" <a href="{% url "dashboard.views.profile" username=i.user.username %}"
......
...@@ -79,10 +79,26 @@ ...@@ -79,10 +79,26 @@
</div> </div>
</div> </div>
{% empty %} {% empty %}
{% if not template_access_types %}
{% trans "You can't start new virtual machines because no templates are shared with you." %} {% 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 %} {% endfor %}
</div> </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> <style>
.progress { .progress {
position: relative; 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 @@ ...@@ -10,28 +10,47 @@
{% endblock %} {% endblock %}
{% block navbar-brand %}
<a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;">
{% include "branding.html" %}
</a>
{% endblock %}
{% block navbar %} {% block navbar %}
{% if user.is_authenticated and user.pk and not request.token_user %} {% if user.is_authenticated and user.pk and not request.token_user %}
<ul class="nav navbar-nav navbar-right" id="dashboard-menu"> <ul class="nav navbar-nav navbar-right" id="dashboard-menu">
{% if user.is_superuser %} {% if user.is_superuser %}
{% if ADMIN_ENABLED %} {% if ADMIN_ENABLED %}
<li> <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> </li>
{% endif %} {% endif %}
<li> <li>
<a href="{% url "dashboard.views.storage" %}"><i class="fa fa-database"></i> <a href="{% url "dashboard.views.message-list" %}">
{% trans "Storage" %} <i class="fa fa-bullhorn"></i>
<span class="hidden-sm">{% trans "Messages" %}</span>
</a> </a>
</li> </li>
<li> <li>
<a href="/network/"><i class="fa fa-globe"></i> {% trans "Network" %}</a> <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="{% url "request.views.request-list" %}">
<i class="fa fa-phone"></i>
<span class="hidden-sm">{% trans "Requests" %}</span>
</a>
</li> </li>
{% endif %} {% endif %}
<li> <li>
...@@ -68,6 +87,11 @@ ...@@ -68,6 +87,11 @@
<li>{% trans "Loading..." %}</li> <li>{% trans "Loading..." %}</li>
</ul> </ul>
</li> </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> </ul>
{% else %} {% else %}
......
...@@ -90,7 +90,7 @@ ...@@ -90,7 +90,7 @@
{% for i in users %} {% for i in users %}
<tr> <tr>
<td> <td>
<i class="fa fa-user"></i> <img class="profile-avatar" src="{{ i.profile.get_avatar_url}}"/>
</td> </td>
<td> <td>
<a href="{% url "dashboard.views.profile" username=i.username %}" title="{{ i.username }}" <a href="{% url "dashboard.views.profile" username=i.username %}" title="{{ i.username }}"
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
<div class="col-xs-6"> <div class="col-xs-6">
<form action="{% url "dashboard.views.group-list" %}" method="GET" id="dashboard-group-search-form"> <form action="{% url "dashboard.views.group-list" %}" method="GET" id="dashboard-group-search-form">
<div class="input-group input-group-sm"> <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"> <div class="input-group-btn">
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button> <button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div> </div>
......
...@@ -72,11 +72,15 @@ ...@@ -72,11 +72,15 @@
value="{% widthratio node_num.running sum_node_num 100 %}"> value="{% widthratio node_num.running sum_node_num 100 %}">
</p> </p>
<p> <p>
{% blocktrans with running=node_num.running missing=node_num.missing disabled=node_num.disabled offline=node_num.offline %}
<span class="big"> <span class="big">
<big>{{ node_num.running }}</big> running <span class="big-tag">{{ running }}</span> running
</span> </span> +
+ <big>{{ node_num.missing }}</big> <span class="big-tag">{{ missing }}</span> missing +
missing + <br><big>{{ node_num.disabled }}</big> disabled + <big>{{ node_num.offline }}</big> offline <br>
<span class="big-tag">{{ disabled }}</span> disabled +
<span class="big-tag">{{ offline }}</span> offline
{% endblocktrans %}
</p> </p>
<ul class="list-inline" id="dashboard-node-taglist"> <ul class="list-inline" id="dashboard-node-taglist">
{% for i in nodes %} {% for i in nodes %}
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<i class="fa fa-{{ t.os_type }}"></i> {{ t.name }} <i class="fa fa-{{ t.os_type }}"></i> {{ t.name }}
</span> </span>
<small class="text-muted index-template-list-system">{{ t.system }}</small> <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" %}" <i data-container="body" title="{% trans "Start VM instance" %}"
class="fa fa-play"></i> class="fa fa-play"></i>
</div> </div>
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
<div class="col-xs-5 col-sm-6"> <div class="col-xs-5 col-sm-6">
<form action="{% url "dashboard.views.template-list" %}" method="GET" id="dashboard-template-search-form"> <form action="{% url "dashboard.views.template-list" %}" method="GET" id="dashboard-template-search-form">
<div class="input-group input-group-sm"> <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"> <div class="input-group-btn">
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button> <button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div> </div>
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<div class="col-xs-5 col-sm-6"> <div class="col-xs-5 col-sm-6">
<form action="{% url "dashboard.views.user-list" %}" method="GET" id="dashboard-user-search-form"> <form action="{% url "dashboard.views.user-list" %}" method="GET" id="dashboard-user-search-form">
<div class="input-group input-group-sm"> <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"> <div class="input-group-btn">
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button> <button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div> </div>
......
...@@ -90,7 +90,7 @@ ...@@ -90,7 +90,7 @@
<p class="pull-right"> <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 }}"> <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> </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;"> <ul class="list-inline" style="max-height: 95px; overflow: hidden;">
{% for vm in running_vms %} {% for vm in running_vms %}
<li style="display: inline-block; padding: 2px;"> <li style="display: inline-block; padding: 2px;">
...@@ -111,7 +111,9 @@ ...@@ -111,7 +111,9 @@
<strong>{{ counter }}</strong> machines total <strong>{{ counter }}</strong> machines total
{% endblocktrans %} {% endblocktrans %}
</a> </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> </div>
</div> </div>
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
<dt>{% trans "result" %}</dt> <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> <dt>{% trans "resultant state" %}</dt>
<dd>{{object.resultant_state|default:'n/a'}}</dd> <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 ...@@ -16,6 +16,7 @@ Do you want to perform the following operation on
{% crispy form %} {% crispy form %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block formbuttons %}
<div class="pull-right"> <div class="pull-right">
<a class="btn btn-default" href="{{object.get_absolute_url}}" <a class="btn btn-default" href="{{object.get_absolute_url}}"
data-dismiss="modal">{% trans "Cancel" %}</a> data-dismiss="modal">{% trans "Cancel" %}</a>
...@@ -23,4 +24,7 @@ Do you want to perform the following operation on ...@@ -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 }} {% if opview.icon %}<i class="fa fa-fw fa-{{opview.icon}}"></i> {% endif %}{{ op.name|capfirst }}
</button> </button>
</div> </div>
{% endblock %}
</form> </form>
{% block extra %}{% endblock %}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
{% load staticfiles %} {% load staticfiles %}
{% load i18n %} {% load i18n %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load arrowfilter %}
{% block title-page %}{{ profile.username}} | {% trans "Profile" %}{% endblock %} {% block title-page %}{{ profile.username}} | {% trans "Profile" %}{% endblock %}
...@@ -23,7 +24,6 @@ ...@@ -23,7 +24,6 @@
{% trans "Back" %}</a> {% trans "Back" %}</a>
</div> </div>
<h3 class="no-margin"> <h3 class="no-margin">
<i class="fa fa-user"></i>
{% include "dashboard/_display-name.html" with user=profile show_org=True %} {% include "dashboard/_display-name.html" with user=profile show_org=True %}
</h3> </h3>
</div> </div>
...@@ -42,6 +42,7 @@ ...@@ -42,6 +42,7 @@
{% trans "Email address" %}: {{ profile.email }} {% trans "Email address" %}: {{ profile.email }}
{% endif %} {% endif %}
</p> </p>
<p>{% trans "Last login" %}: <span title="{{ request.user.last_login }}">{{ request.user.last_login|arrowfilter:LANGUAGE_CODE}}</span></p>
{% if request.user == profile %} {% if request.user == profile %}
<p> <p>
{% trans "Use email address as Gravatar profile image" %}: {% trans "Use email address as Gravatar profile image" %}:
......
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
<div class="text-right"> <div class="text-right">
<form class="pull-left" method="POST" action="{% url "dashboard.views.store-refresh-toplist" %}"> <form class="pull-left" method="POST" action="{% url "dashboard.views.store-refresh-toplist" %}">
{% csrf_token %} {% 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> <i class="fa fa-refresh"></i>
</button> </button>
</form> </form>
......
{% extends "dashboard/base.html" %} {% extends "dashboard/base.html" %}
{% load staticfiles %} {% load staticfiles %}
{% load i18n %} {% load i18n %}
{% load compressed %} {% load pipeline %}
{% block title-page %}{{ instance.name }} | vm{% endblock %} {% block title-page %}{{ instance.name }} | vm{% endblock %}
...@@ -242,5 +242,5 @@ ...@@ -242,5 +242,5 @@
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
{% compressed_js 'vm-detail' %} {% javascript 'vm-detail' %}
{% endblock %} {% endblock %}
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
{% for a in activities %} {% for a in activities %}
<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" <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 %}"> <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> <i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-{{a.icon}}{% endif %}"></i>
</span> </span>
...@@ -19,9 +19,9 @@ ...@@ -19,9 +19,9 @@
- {{ a.percentage }}% - {{ a.percentage }}%
{% endif %} {% endif %}
</strong> </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 %}"> <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> </a>
{% endif %} {% endif %}
{% if a.is_abortable_for_user %} {% if a.is_abortable_for_user %}
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
{{ s.finished|arrowfilter:LANGUAGE_CODE }} {{ s.finished|arrowfilter:LANGUAGE_CODE }}
</span> </span>
{% else %} {% else %}
<i class="fa fa-refresh fa-spin" class="sub-activity-loading-icon"></i> <i class="fa fa-refresh fa-spin"></i>
{% endif %} {% endif %}
{% if s.has_failed %} {% if s.has_failed %}
<div class="label label-danger">{% trans "failed" %}</div> <div class="label label-danger">{% trans "failed" %}</div>
......
{% load i18n %} {% load i18n %}
{% load staticfiles %} {% load staticfiles %}
<div class="btn-toolbar">
{% if perms.vm.access_console %} {% if not perms.vm.access_console %}
<button id="sendCtrlAltDelButton" class="btn btn-danger btn-sm">{% trans "Send Ctrl+Alt+Del" %}</button> <div class="alert alert-warning">
<button id="sendPasswordButton" class="btn btn-default btn-sm">{% trans "Type password" %}</button> {% trans "You are not authorized to access the VNC console." %}
</div>
{% endif %} {% 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> </div>
{% if perms.vm.access_console %} {% if perms.vm.access_console %}
<div class="alert alert-info" id="noVNC_status"> <div class="alert alert-info" id="noVNC_status"></div>
</div>
{% endif %} {% endif %}
<div id="vm-console-screenshot"> <div id="vm-console-screenshot">
<h3>
<button class="btn btn-danger btn-sm pull-right">{% trans "Close" %}</button> <button class="btn btn-danger btn-sm pull-right">{% trans "Close" %}</button>
<h3>{% trans "Screenshot" %}</h3> {% trans "Screenshot" %}
<img /> </h3>
<img alt="{% trans "Screenshot" %}"/>
<hr /> <hr />
</div> </div>
{% if perms.vm.access_console %} {% 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> </canvas>
<script> <script>
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
<dd> <dd>
{% csrf_token %} {% csrf_token %}
<div class="vm-details-home-edit-description-click"> <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>
<div id="vm-details-home-description" class="js-hidden"> <div id="vm-details-home-description" class="js-hidden">
<form method="POST"> <form method="POST">
...@@ -59,10 +59,11 @@ ...@@ -59,10 +59,11 @@
{% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %} {% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %}
<span id="vm-details-renew-op"> <span id="vm-details-renew-op">
{% with op=op.renew %}{% if 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}}"> operation operation-{{op.op}}">
<i class="fa fa-{{op.icon}}"></i> <i class="fa fa-{{op.icon}}"></i>
{{op.name}} </a> {{op.name}}
</a>
{% endif %}{% endwith %} {% endif %}{% endwith %}
</span> </span>
</h4> </h4>
...@@ -156,6 +157,16 @@ ...@@ -156,6 +157,16 @@
</a> </a>
</div> </div>
{% endif %} {% 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>
<div class="col-md-8"> <div class="col-md-8">
{% if graphite_enabled %} {% if graphite_enabled %}
...@@ -163,9 +174,12 @@ ...@@ -163,9 +174,12 @@
{% include "dashboard/_graph-time-buttons.html" %} {% include "dashboard/_graph-time-buttons.html" %}
</div> </div>
<div class="graph-images text-center"> <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 "cpu" graph_time %}"
<img src="{% url "dashboard.views.vm-graph" instance.pk "memory" graph_time %}"/> alt="{% trans "CPU usage" %}"/>
<img src="{% url "dashboard.views.vm-graph" instance.pk "network" graph_time %}"/> <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> </div>
{% endif %} {% endif %}
</div> </div>
......
{% load i18n %} {% load i18n %}
{% load network_tags %} {% load network_tags %}
<h2>
<div id="vm-details-add-interface"> <div id="vm-details-add-interface">
{% with op=op.add_interface %}{% if op %} {% with op=op.add_interface %}{% if op %}
<a href="{{op.get_url}}" class="btn btn-{{op.effect}} operation pull-right" <a href="{{op.get_url}}" class="btn btn-{{op.effect}} operation pull-right"
{% if op.disabled %}disabled{% endif %}> {% if op.disabled %}disabled{% endif %}>
<i class="fa fa-{{op.icon}}"></i> {% trans "add interface" %}</a> <i class="fa fa-{{op.icon}}"></i> {% trans "add interface" %}</a>
{% endif %}{% endwith %} {% endif %}{% endwith %}
</div> </div>
<h2>
{% trans "Interfaces" %} {% trans "Interfaces" %}
</h2> </h2>
......
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
{% load sizefieldtags %} {% load sizefieldtags %}
{% load crispy_forms_tags %} {% 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"> <form method="POST" action="{{ op.resources_change.get_url }}" id="vm-details-resources-form">
{% csrf_token %} {% 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 %} {% 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 %}
...@@ -9,12 +13,31 @@ ...@@ -9,12 +13,31 @@
{% if op.resources_change %} {% if op.resources_change %}
<button type="submit" class="btn btn-success btn-sm change-resources-button" <button type="submit" class="btn btn-success btn-sm change-resources-button"
id="vm-details-resources-save" data-vm="{{ instance.pk }}" id="vm-details-resources-save" data-vm="{{ instance.pk }}"
{% if op.resources_change.disabled %}disabled{% endif %}> {% if not save_resources_enabled %}disabled{% endif %}>
<i class="fa fa-floppy-o"></i> {% trans "Save resources" %} <i class="fa fa-floppy-o"></i> {% trans "Save resources" %}
</button> </button>
<span class="change-resources-help" <span class="change-resources-help"
{% if not op.resources_change.disabled %}style="display: none;"{% endif %} {% if save_resources_enabled %}style="display: none;"{% endif %}>
>{% trans "Stop your VM to change resources." %}</span> {% 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 %} {% endif %}
</form> </form>
...@@ -22,12 +45,12 @@ ...@@ -22,12 +45,12 @@
<div id="vm-details-resources-disk"> <div id="vm-details-resources-disk">
<h3> <h3>
{% trans "Disks" %}
<div class="pull-right"> <div class="pull-right">
<div id="disk-ops"> <div id="disk-ops">
{% include "dashboard/vm-detail/_disk-operations.html" %} {% include "dashboard/vm-detail/_disk-operations.html" %}
</div> </div>
</div> </div>
{% trans "Disks" %}
</h3> </h3>
<div class="clearfix"></div> <div class="clearfix"></div>
...@@ -46,7 +69,7 @@ ...@@ -46,7 +69,7 @@
{% if user.is_superuser %} {% if user.is_superuser %}
<hr/> <hr/>
<div class="row" id=""> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<h3> <h3>
{% trans "Required traits" %} {% trans "Required traits" %}
...@@ -58,7 +81,7 @@ ...@@ -58,7 +81,7 @@
<hr/> <hr/>
<div class="row" id=""> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<h3> <h3>
{% trans "Raw data" %} {% 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 ...@@ -20,8 +20,7 @@ import json
# from unittest import skip # from unittest import skip
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group, Permission
from django.contrib.auth.models import Permission
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from common.tests.celery_mock import MockCeleryMixin from common.tests.celery_mock import MockCeleryMixin
......
...@@ -54,6 +54,7 @@ from .views import ( ...@@ -54,6 +54,7 @@ from .views import (
NodeActivityView, NodeActivityView,
UserList, UserList,
StorageDetail, DiskDetail, StorageDetail, DiskDetail,
MessageList, MessageDetail, MessageCreate, MessageDelete,
) )
from .views.vm import vm_ops, vm_mass_ops from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops from .views.node import node_ops
...@@ -228,11 +229,19 @@ urlpatterns = patterns( ...@@ -228,11 +229,19 @@ urlpatterns = patterns(
url(r'^vm/opensearch.xml$', OpenSearchDescriptionView.as_view(), url(r'^vm/opensearch.xml$', OpenSearchDescriptionView.as_view(),
name="dashboard.views.vm-opensearch"), name="dashboard.views.vm-opensearch"),
url(r'^storage/$', StorageDetail.as_view(), url(r'^storage/$', StorageDetail.as_view(),
name="dashboard.views.storage"), name="dashboard.views.storage"),
url(r'^disk/(?P<pk>\d+)/$', DiskDetail.as_view(), url(r'^disk/(?P<pk>\d+)/$', DiskDetail.as_view(),
name="dashboard.views.disk-detail"), 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( urlpatterns += patterns(
......
...@@ -13,3 +13,5 @@ from util import * ...@@ -13,3 +13,5 @@ from util import *
from vm import * from vm import *
from graph import * from graph import *
from storage import * from storage import *
from request import *
from message import *
...@@ -212,7 +212,7 @@ class VmNetwork(object): ...@@ -212,7 +212,7 @@ class VmNetwork(object):
'alias(scaleToSeconds(nonNegativeDerivative(' 'alias(scaleToSeconds(nonNegativeDerivative('
'%s.network.bytes_sent-%s), 10), "in - %s (bits/s)")' % ( '%s.network.bytes_sent-%s), 10), "in - %s (bits/s)")' % (
params)) params))
return 'group(%s)' % ','.join(metrics) return 'group(%s)' % ','.join(metrics) if metrics else None
register_graph(VmNetwork, 'network', VmGraphView) register_graph(VmNetwork, 'network', VmGraphView)
......
...@@ -63,6 +63,8 @@ class GroupCodeMixin(object): ...@@ -63,6 +63,8 @@ class GroupCodeMixin(object):
client = Saml2Client(conf, state_cache=state, client = Saml2Client(conf, state_cache=state,
identity_cache=IdentityCache(request.session)) identity_cache=IdentityCache(request.session))
subject_id = _get_subject_id(request.session) subject_id = _get_subject_id(request.session)
if not subject_id:
return newgroups
identity = client.users.get_identity(subject_id, identity = client.users.get_identity(subject_id,
check_not_on_or_after=False) check_not_on_or_after=False)
if identity: if identity:
...@@ -144,7 +146,7 @@ class GroupDetailView(CheckedDetailView): ...@@ -144,7 +146,7 @@ class GroupDetailView(CheckedDetailView):
self.object.user_set.add(entity) self.object.user_set.add(entity)
except User.DoesNotExist: except User.DoesNotExist:
if saml_available: if saml_available:
FutureMember.objects.get_or_create(org_id=name, FutureMember.objects.get_or_create(org_id=name.upper(),
group=self.object) group=self.object)
else: else:
messages.warning(request, _('User "%s" not found.') % name) 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): ...@@ -441,7 +441,7 @@ class TransferTemplateOwnershipView(TransferOwnershipView):
confirm_view = TransferTemplateOwnershipConfirmView confirm_view = TransferTemplateOwnershipConfirmView
model = InstanceTemplate model = InstanceTemplate
notification_msg = ugettext_noop( 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. ' 'his/her template called %(instance)s. '
'<a href="%(token)s" ' '<a href="%(token)s" '
'class="btn btn-success btn-small">Accept</a>') 'class="btn btn-success btn-small">Accept</a>')
......
...@@ -34,6 +34,7 @@ from django.core.paginator import Paginator, InvalidPage ...@@ -34,6 +34,7 @@ from django.core.paginator import Paginator, InvalidPage
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import redirect, get_object_or_404 from django.shortcuts import redirect, get_object_or_404
from django.templatetags.static import static
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.views.generic import ( from django.views.generic import (
...@@ -101,6 +102,8 @@ def circle_login(request): ...@@ -101,6 +102,8 @@ def circle_login(request):
authentication_form = CircleAuthenticationForm authentication_form = CircleAuthenticationForm
extra_context = { extra_context = {
'saml2': saml_available, 'saml2': saml_available,
'og_image': (settings.DJANGO_URL.rstrip("/") +
static("dashboard/img/og.png"))
} }
response = login_view(request, authentication_form=authentication_form, response = login_view(request, authentication_form=authentication_form,
extra_context=extra_context) extra_context=extra_context)
......
...@@ -70,7 +70,7 @@ def search_user(keyword): ...@@ -70,7 +70,7 @@ def search_user(keyword):
return User.objects.get(username=keyword) return User.objects.get(username=keyword)
except User.DoesNotExist: except User.DoesNotExist:
try: try:
return User.objects.get(profile__org_id=keyword) return User.objects.get(profile__org_id__iexact=keyword)
except User.DoesNotExist: except User.DoesNotExist:
return User.objects.get(email=keyword) return User.objects.get(email=keyword)
...@@ -610,7 +610,7 @@ class TransferOwnershipView(CheckedDetailView, DetailView): ...@@ -610,7 +610,7 @@ class TransferOwnershipView(CheckedDetailView, DetailView):
new_owner.profile.notify( new_owner.profile.notify(
ugettext_noop('Ownership offer'), ugettext_noop('Ownership offer'),
self.notification_msg, self.notification_msg,
{'instance': obj, 'token': token_path}) {'instance': obj, 'token': token_path, 'owner': request.user})
except Profile.DoesNotExist: except Profile.DoesNotExist:
messages.error(request, _('Can not notify selected user.')) messages.error(request, _('Can not notify selected user.'))
else: else:
...@@ -665,8 +665,8 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): ...@@ -665,8 +665,8 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
old.profile.notify( old.profile.notify(
ugettext_noop('Ownership accepted'), ugettext_noop('Ownership accepted'),
ugettext_noop('Your ownership offer of %(instance)s has been ' ugettext_noop('Your ownership offer of %(instance)s has been '
'accepted by %(user)s.'), 'accepted by %(owner)s.'),
{'instance': instance}) {'instance': instance, 'owner': request.user})
return redirect(instance.get_absolute_url()) return redirect(instance.get_absolute_url())
def get_instance(self, key, user): def get_instance(self, key, user):
......
...@@ -66,6 +66,8 @@ from ..forms import ( ...@@ -66,6 +66,8 @@ from ..forms import (
VmPortRemoveForm, VmPortAddForm, VmPortRemoveForm, VmPortAddForm,
VmRemoveInterfaceForm, VmRemoveInterfaceForm,
) )
from request.models import TemplateAccessType, LeaseType
from request.forms import LeaseRequestForm, TemplateRequestForm
from ..models import Favourite from ..models import Favourite
from manager.scheduler import has_traits from manager.scheduler import has_traits
...@@ -171,6 +173,10 @@ class VmDetailView(GraphMixin, CheckedDetailView): ...@@ -171,6 +173,10 @@ class VmDetailView(GraphMixin, CheckedDetailView):
context['is_operator'] = is_operator context['is_operator'] = is_operator
context['is_owner'] = is_owner 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 return context
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
...@@ -651,10 +657,12 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView): ...@@ -651,10 +657,12 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
op = 'renew' op = 'renew'
icon = 'calendar' icon = 'calendar'
effect = 'info' effect = 'success'
show_in_toolbar = False show_in_toolbar = False
form_class = VmRenewForm form_class = VmRenewForm
wait_for_result = 0.5 wait_for_result = 0.5
template_name = 'dashboard/_vm-renew.html'
with_reload = True
def get_form_kwargs(self): def get_form_kwargs(self):
choices = Lease.get_objects_with_level("user", self.request.user) choices = Lease.get_objects_with_level("user", self.request.user)
...@@ -674,6 +682,12 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView): ...@@ -674,6 +682,12 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
instance.time_of_suspend) instance.time_of_suspend)
return extra 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): class VmStateChangeView(FormOperationMixin, VmOperationView):
op = 'emergency_change_state' op = 'emergency_change_state'
...@@ -762,6 +776,10 @@ vm_ops = OrderedDict([ ...@@ -762,6 +776,10 @@ vm_ops = OrderedDict([
op='mount_store', icon='briefcase', effect='info', op='mount_store', icon='briefcase', effect='info',
show_in_toolbar=False, 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): ...@@ -1043,6 +1061,8 @@ class VmCreate(LoginRequiredMixin, TemplateView):
'box_title': _('Create a VM'), 'box_title': _('Create a VM'),
'ajax_title': True, 'ajax_title': True,
'templates': templates.all(), 'templates': templates.all(),
'template_access_types': TemplateAccessType.objects.exists(),
'form': TemplateRequestForm(request=request),
}) })
return self.render_to_response(context) return self.render_to_response(context)
...@@ -1297,7 +1317,7 @@ class TransferInstanceOwnershipView(TransferOwnershipView): ...@@ -1297,7 +1317,7 @@ class TransferInstanceOwnershipView(TransferOwnershipView):
confirm_view = TransferInstanceOwnershipConfirmView confirm_view = TransferInstanceOwnershipConfirmView
model = Instance model = Instance
notification_msg = ugettext_noop( 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. ' 'his/her virtual machine called %(instance)s. '
'<a href="%(token)s" ' '<a href="%(token)s" '
'class="btn btn-success btn-small">Accept</a>') 'class="btn btn-success btn-small">Accept</a>')
......
...@@ -143,8 +143,8 @@ def selenium(test=""): ...@@ -143,8 +143,8 @@ def selenium(test=""):
test = "--failed" test = "--failed"
else: else:
test += " --with-id" test += " --with-id"
run("xvfb-run ./manage.py test " run('xvfb-run --server-args="-screen 0, 1920x1080x24" ./manage.py'
"--settings=circle.settings.selenium_test %s" % test) ' test --settings=circle.settings.selenium_test %s' % test)
def pull(dir="~/circle/circle"): 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): ...@@ -69,6 +69,8 @@ class BlacklistItemForm(ModelForm):
class Meta: class Meta:
model = BlacklistItem model = BlacklistItem
fields = ("ipv4", "host", "expires_at", "whitelisted", "reason",
"snort_message", )
class DomainForm(ModelForm): class DomainForm(ModelForm):
...@@ -90,6 +92,7 @@ class DomainForm(ModelForm): ...@@ -90,6 +92,7 @@ class DomainForm(ModelForm):
class Meta: class Meta:
model = Domain model = Domain
fields = ("name", "ttl", "owner", )
class FirewallForm(ModelForm): class FirewallForm(ModelForm):
...@@ -105,6 +108,7 @@ class FirewallForm(ModelForm): ...@@ -105,6 +108,7 @@ class FirewallForm(ModelForm):
class Meta: class Meta:
model = Firewall model = Firewall
fields = ("name", )
class GroupForm(ModelForm): class GroupForm(ModelForm):
...@@ -126,6 +130,7 @@ class GroupForm(ModelForm): ...@@ -126,6 +130,7 @@ class GroupForm(ModelForm):
class Meta: class Meta:
model = Group model = Group
fields = ("name", "description", "owner", )
class HostForm(ModelForm): class HostForm(ModelForm):
...@@ -165,6 +170,9 @@ class HostForm(ModelForm): ...@@ -165,6 +170,9 @@ class HostForm(ModelForm):
class Meta: class Meta:
model = Host model = Host
fields = ("hostname", "reverse", "mac", "vlan", "shared_ip", "ipv4",
"ipv6", "external_ipv4", "description", "location",
"comment", "owner", )
class RecordForm(ModelForm): class RecordForm(ModelForm):
...@@ -191,6 +199,8 @@ class RecordForm(ModelForm): ...@@ -191,6 +199,8 @@ class RecordForm(ModelForm):
class Meta: class Meta:
model = Record model = Record
fields = ("type", "host", "name", "domain", "address", "ttl",
"description", "owner", )
class RuleForm(ModelForm): class RuleForm(ModelForm):
...@@ -230,6 +240,10 @@ class RuleForm(ModelForm): ...@@ -230,6 +240,10 @@ class RuleForm(ModelForm):
class Meta: class Meta:
model = Rule 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): class SwitchPortForm(ModelForm):
...@@ -252,6 +266,7 @@ class SwitchPortForm(ModelForm): ...@@ -252,6 +266,7 @@ class SwitchPortForm(ModelForm):
class Meta: class Meta:
model = SwitchPort model = SwitchPort
fields = ("untagged_vlan", "tagged_vlans", "description", )
class VlanForm(ModelForm): class VlanForm(ModelForm):
...@@ -305,6 +320,10 @@ class VlanForm(ModelForm): ...@@ -305,6 +320,10 @@ class VlanForm(ModelForm):
widgets = { widgets = {
'ipv6_template': widgets.TextInput, '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): class VlanGroupForm(ModelForm):
...@@ -328,3 +347,4 @@ class VlanGroupForm(ModelForm): ...@@ -328,3 +347,4 @@ class VlanGroupForm(ModelForm):
class Meta: class Meta:
model = VlanGroup 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 @@ ...@@ -6,5 +6,14 @@
{% block page_title %}{% trans "Page not found" %}{% endblock page_title %} {% block page_title %}{% trans "Page not found" %}{% endblock page_title %}
{% block content %} {% 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 %} {% endblock content %}
{% extends "dashboard/base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}HTTP 500{% endblock %} {% block title %}HTTP 500{% endblock %}
......
...@@ -74,13 +74,6 @@ ...@@ -74,13 +74,6 @@
{% endblock %} {% 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 %} {% block content %}
<div class="content"> <div class="content">
{% block content_box %}{% endblock %} {% block content_box %}{% endblock %}
......
...@@ -6,10 +6,8 @@ ...@@ -6,10 +6,8 @@
{% block title-page %}{% trans "Login" %}{% endblock %} {% block title-page %}{% trans "Login" %}{% endblock %}
{% block navbar-brand %} {% block extra_link %}
<a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;"> {% include "open-graph.html" %}
<img src="{% static "dashboard/img/logo.png" %}" style="height: 25px;"/>
</a>
{% endblock %} {% endblock %}
{% block content_box %} {% block content_box %}
...@@ -21,10 +19,7 @@ ...@@ -21,10 +19,7 @@
{% endif %} {% endif %}
<div class="col-xs-{% if saml2 %}6{% else %}12{% endif %}"> <div class="col-xs-{% if saml2 %}6{% else %}12{% endif %}">
<div class="login-form"> <div class="login-form">
<form action="" method="POST">
{% csrf_token %}
{% crispy form %} {% crispy form %}
</form>
</div> </div>
</div> </div>
{% if saml2 %} {% if saml2 %}
...@@ -33,11 +28,12 @@ ...@@ -33,11 +28,12 @@
<a href="{% url "saml2_login" %}">{% trans "Click here!" %}</a> <a href="{% url "saml2_login" %}">{% trans "Click here!" %}</a>
</div> </div>
{% endif %} {% endif %}
</div>
<div class="row"> <div class="row">
<div class="col-sm-12"> <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> </div>
</div>
{% endblock %} {% endblock %}
...@@ -62,7 +62,6 @@ scheduler = import_module(name=django.conf.settings.VM_SCHEDULER) ...@@ -62,7 +62,6 @@ scheduler = import_module(name=django.conf.settings.VM_SCHEDULER)
ACCESS_PROTOCOLS = django.conf.settings.VM_ACCESS_PROTOCOLS ACCESS_PROTOCOLS = django.conf.settings.VM_ACCESS_PROTOCOLS
ACCESS_METHODS = [(key, name) for key, (name, port, transport) ACCESS_METHODS = [(key, name) for key, (name, port, transport)
in ACCESS_PROTOCOLS.iteritems()] in ACCESS_PROTOCOLS.iteritems()]
VNC_PORT_RANGE = (20000, 65536) # inclusive start, exclusive end
def find_unused_port(port_range, used_ports=[]): def find_unused_port(port_range, used_ports=[]):
...@@ -81,7 +80,7 @@ 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(): def find_unused_vnc_port():
port = find_unused_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)) used_ports=Instance.objects.values_list('vnc_port', flat=True))
if port is None: 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