diff --git a/circle/acl/tests/models.py b/circle/acl/tests/models.py index 851ad7e..52324f3 100644 --- a/circle/acl/tests/models.py +++ b/circle/acl/tests/models.py @@ -15,4 +15,8 @@ # You should have received a copy of the GNU General Public License along # with CIRCLE. If not, see <http://www.gnu.org/licenses/>. -from .test_acl import TestModel, Test2Model # noqa +from django.conf import settings + +# https://code.djangoproject.com/ticket/7835 +if settings.SETTINGS_MODULE == 'circle.settings.test': + from .test_acl import TestModel, Test2Model # noqa diff --git a/circle/circle/settings/base.py b/circle/circle/settings/base.py index d9e4a8c..6ef2b24 100644 --- a/circle/circle/settings/base.py +++ b/circle/circle/settings/base.py @@ -156,6 +156,7 @@ STATIC_URL = get_env_variable('DJANGO_STATIC_URL', default='/static/') STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'pipeline.finders.PipelineFinder', ) ########## END STATIC FILE CONFIGURATION STATICFILES_DIRS = [normpath(join(SITE_ROOT, 'bower_components'))] @@ -282,6 +283,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.request', 'dashboard.context_processors.notifications', 'dashboard.context_processors.extract_settings', + 'dashboard.context_processors.broadcast_messages', ) # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs @@ -355,6 +357,7 @@ LOCAL_APPS = ( 'manager', 'acl', 'monitor', + 'request', ) # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps @@ -449,7 +452,7 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': ) AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', - 'djangosaml2.backends.Saml2Backend', + 'common.backends.Saml2Backend', ) remote_metadata = join(SITE_ROOT, 'remote_metadata.xml') @@ -527,6 +530,10 @@ except: LOCALE_PATHS = (join(SITE_ROOT, 'locale'), ) COMPANY_NAME = get_env_variable("COMPANY_NAME", "BME IK 2015") +first, last = get_env_variable( + 'VNC_PORT_RANGE', '20000, 65536').replace(' ', '').split(',') +VNC_PORT_RANGE = (int(first), int(last)) # inclusive start, exclusive end + graphite_host = environ.get("GRAPHITE_HOST", None) graphite_port = environ.get("GRAPHITE_PORT", None) if graphite_host and graphite_port: @@ -555,3 +562,4 @@ ADMIN_ENABLED = False BLACKLIST_PASSWORD = get_env_variable("BLACKLIST_PASSWORD", "") BLACKLIST_HOOK_URL = get_env_variable("BLACKLIST_HOOK_URL", "") +REQUEST_HOOK_URL = get_env_variable("REQUEST_HOOK_URL", "") diff --git a/circle/circle/settings/local.py b/circle/circle/settings/local.py index ead29f4..96f2322 100644 --- a/circle/circle/settings/local.py +++ b/circle/circle/settings/local.py @@ -110,8 +110,8 @@ if DEBUG: from django.dispatch import Signal Signal.send_robust = Signal.send -PIPELINE_DISABLED_COMPILERS = ( - 'pipeline.compilers.less.LessCompiler', +PIPELINE_COMPILERS = ( + 'dashboard.compilers.DummyLessCompiler', ) ADMIN_ENABLED = True diff --git a/circle/circle/settings/selenium_test.py b/circle/circle/settings/selenium_test.py index 8c4d100..81d30c9 100644 --- a/circle/circle/settings/selenium_test.py +++ b/circle/circle/settings/selenium_test.py @@ -40,7 +40,8 @@ INSTALLED_APPS += ( ) TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' -path_to_selenium_test = os.path.expanduser('~/circle/circle/dashboard/tests/selenium') + +path_to_selenium_test = os.path.join(SITE_ROOT, "dashboard/tests/selenium") NOSE_ARGS = ['--stop', '--with-doctest', '--with-selenium-driver', '--selenium-driver=firefox', '-w%s' % path_to_selenium_test] PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher'] diff --git a/circle/circle/settings/test.py b/circle/circle/settings/test.py index 3f3f978..931eee5 100644 --- a/circle/circle/settings/test.py +++ b/circle/circle/settings/test.py @@ -56,6 +56,16 @@ LOGGING['handlers']['console'] = {'level': level, 'formatter': 'simple'} for i in LOCAL_APPS: LOGGING['loggers'][i] = {'handlers': ['console'], 'level': level} + +# don't print SQL queries +LOGGING['handlers']['null'] = {'level': "DEBUG", + 'class': "django.utils.log.NullHandler"} +LOGGING['loggers']['django.db.backends'] = { + 'handlers': ['null'], + 'propagate': False, + 'level': 'DEBUG', +} + # Forbid store usage STORE_URL = "" diff --git a/circle/circle/urls.py b/circle/circle/urls.py index 7ffdbab..6447a52 100644 --- a/circle/circle/urls.py +++ b/circle/circle/urls.py @@ -38,6 +38,7 @@ urlpatterns = patterns( url(r'^network/', include('network.urls')), url(r'^blacklist-add/', add_blacklist_item), url(r'^dashboard/', include('dashboard.urls')), + url(r'^request/', include('request.urls')), # django/contrib/auth/urls.py (care when new version) url((r'^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/' @@ -87,3 +88,4 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': ) handler500 = 'common.views.handler500' +handler403 = 'common.views.handler403' diff --git a/circle/common/backends.py b/circle/common/backends.py new file mode 100644 index 0000000..3d6e53e --- /dev/null +++ b/circle/common/backends.py @@ -0,0 +1,49 @@ +# -*- 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) diff --git a/circle/common/operations.py b/circle/common/operations.py index 4d5e19e..195afea 100644 --- a/circle/common/operations.py +++ b/circle/common/operations.py @@ -76,7 +76,7 @@ class Operation(object): user = auxargs.pop('user') parent_activity = auxargs.pop('parent_activity') if parent_activity and user is None and not skip_auth_check: - user = parent_activity.user + user = allargs['user'] = parent_activity.user if user is None: # parent was a system call skip_auth_check = True @@ -170,8 +170,8 @@ class Operation(object): raise ImproperlyConfigured( "Set required_perms to () if none needed.") if not user.has_perms(cls.required_perms): - raise PermissionDenied("%s doesn't have the required permissions." - % user) + raise PermissionDenied( + u"%s doesn't have the required permissions." % user) if cls.superuser_required and not user.is_superuser: raise humanize_exception(ugettext_noop( "Superuser privileges are required."), PermissionDenied()) diff --git a/circle/common/views.py b/circle/common/views.py index 616bca1..5a6a3a2 100644 --- a/circle/common/views.py +++ b/circle/common/views.py @@ -19,32 +19,42 @@ from sys import exc_info import logging -from django.template import RequestContext from django.shortcuts import render_to_response +from django.template import RequestContext from .models import HumanReadableException logger = logging.getLogger(__name__) -def handler500(request): - cls, exception, traceback = exc_info() - logger.exception("unhandled exception") +def get_context(request, exception): ctx = {} - if isinstance(exception, HumanReadableException): + if issubclass(exception.__class__, HumanReadableException): try: - ctx['error'] = exception.get_user_text() + if request.user.is_superuser: + ctx['error'] = exception.get_admin_text() + else: + ctx['error'] = exception.get_user_text() except: pass - else: - try: - if request.user.is_superuser(): - ctx['error'] = exception.get_admin_text() - except: - pass + return ctx + + +def handler500(request): + cls, exception, traceback = exc_info() + logger.exception("unhandled exception") + ctx = get_context(request, exception) try: resp = render_to_response("500.html", ctx, RequestContext(request)) except: resp = render_to_response("500.html", ctx) resp.status_code = 500 return resp + + +def handler403(request): + cls, exception, traceback = exc_info() + ctx = get_context(request, exception) + resp = render_to_response("403.html", ctx) + resp.status_code = 403 + return resp diff --git a/circle/dashboard/admin.py b/circle/dashboard/admin.py index ac798ca..f2baf13 100644 --- a/circle/dashboard/admin.py +++ b/circle/dashboard/admin.py @@ -21,7 +21,7 @@ from django import contrib from django.contrib.auth.admin import UserAdmin, GroupAdmin from django.contrib.auth.models import User, Group -from dashboard.models import Profile, GroupProfile, ConnectCommand +from dashboard.models import Profile, GroupProfile, ConnectCommand, Message class ProfileInline(contrib.admin.TabularInline): @@ -43,3 +43,5 @@ contrib.admin.site.unregister(User) contrib.admin.site.register(User, UserAdmin) contrib.admin.site.unregister(Group) contrib.admin.site.register(Group, GroupAdmin) + +contrib.admin.site.register(Message) diff --git a/circle/dashboard/compilers.py b/circle/dashboard/compilers.py new file mode 100644 index 0000000..723f48a --- /dev/null +++ b/circle/dashboard/compilers.py @@ -0,0 +1,23 @@ +# 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 diff --git a/circle/dashboard/context_processors.py b/circle/dashboard/context_processors.py index 3777694..b4def8f 100644 --- a/circle/dashboard/context_processors.py +++ b/circle/dashboard/context_processors.py @@ -17,6 +17,8 @@ from django.conf import settings +from .models import Message + def notifications(request): count = (request.user.notification_set.filter(status="new").count() @@ -31,3 +33,7 @@ def extract_settings(request): 'COMPANY_NAME': getattr(settings, "COMPANY_NAME", None), 'ADMIN_ENABLED': getattr(settings, "ADMIN_ENABLED", False), } + + +def broadcast_messages(request): + return {'broadcast_messages': Message.timeframed.filter(enabled=True)} diff --git a/circle/dashboard/fixtures/test-vm-fixture.json b/circle/dashboard/fixtures/test-vm-fixture.json index 0aa52b8..dc547b2 100644 --- a/circle/dashboard/fixtures/test-vm-fixture.json +++ b/circle/dashboard/fixtures/test-vm-fixture.json @@ -1395,6 +1395,7 @@ "vnc_port": 1234, "num_cores": 2, "status": "RUNNING", + "system": "system pls", "modified": "2013-10-14T07:27:38.192Z" } }, diff --git a/circle/dashboard/forms.py b/circle/dashboard/forms.py index 444b08d..fcbc6eb 100644 --- a/circle/dashboard/forms.py +++ b/circle/dashboard/forms.py @@ -57,7 +57,7 @@ from vm.models import ( from storage.models import DataStore, Disk from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth.models import Permission -from .models import Profile, GroupProfile +from .models import Profile, GroupProfile, Message from circle.settings.base import LANGUAGES, MAX_NODE_RAM from django.utils.translation import string_concat @@ -739,6 +739,7 @@ class LeaseForm(forms.ModelForm): class Meta: model = Lease + exclude = () class VmRenewForm(OperationForm): @@ -1604,6 +1605,7 @@ class DataStoreForm(ModelForm): class Meta: model = DataStore + fields = ("name", "path", "hostname", ) class DiskForm(ModelForm): @@ -1620,3 +1622,17 @@ class DiskForm(ModelForm): class Meta: model = Disk + fields = ("name", "filename", "datastore", "type", "bus", "size", + "base", "dev_num", "destroyed", "is_ready", ) + + +class MessageForm(ModelForm): + class Meta: + model = Message + fields = ("message", "enabled", "effect", "start", "end") + + @property + def helper(self): + helper = FormHelper() + helper.add_input(Submit("submit", _("Save"))) + return helper diff --git a/circle/dashboard/management/commands/init.py b/circle/dashboard/management/commands/init.py index 9cb5121..44580af 100644 --- a/circle/dashboard/management/commands/init.py +++ b/circle/dashboard/management/commands/init.py @@ -17,27 +17,29 @@ from __future__ import unicode_literals, absolute_import +import logging from optparse import make_option from django.contrib.auth.models import User from django.core.management.base import BaseCommand -from firewall.models import (Vlan, VlanGroup, Domain, Firewall, Rule, - SwitchPort, EthernetDevice, Host) +from firewall.models import Vlan, VlanGroup, Domain, Firewall, Rule from storage.models import DataStore from vm.models import Lease +logger = logging.getLogger(__name__) + + class Command(BaseCommand): option_list = BaseCommand.option_list + ( make_option('--force', action="store_true"), - make_option('--portal-ip'), make_option('--external-net'), make_option('--management-net'), make_option('--vm-net'), make_option('--external-if'), make_option('--management-if'), - make_option('--trunk-if'), + make_option('--vm-if'), make_option('--datastore-queue'), make_option('--firewall-queue'), make_option('--admin-user'), @@ -49,18 +51,18 @@ class Command(BaseCommand): qs = model.objects.filter(**{field: value})[:1] if not qs.exists(): obj = model.objects.create(**kwargs) - self.changed.append('New %s: %s' % (model, obj)) + logger.info('New %s: %s', model, obj) + self.changed = True return obj else: return qs[0] # http://docs.saltstack.com/en/latest/ref/states/all/salt.states.cmd.html def print_state(self): - changed = "yes" if len(self.changed) else "no" - print "\nchanged=%s comment='%s'" % (changed, ", ".join(self.changed)) + print "\nchanged=%s" % ("yes" if self.changed else "no") def handle(self, *args, **options): - self.changed = [] + self.changed = False if (DataStore.objects.exists() and Vlan.objects.exists() and not options['force']): @@ -87,20 +89,28 @@ class Command(BaseCommand): suspend_interval_seconds=3600 * 24 * 365, delete_interval_seconds=3600 * 24 * 365 * 3) - domain = self.create(Domain, 'name', name='example.com', owner=admin) + net_domain = self.create(Domain, 'name', name='net.example.com', + owner=admin) + man_domain = self.create(Domain, 'name', name='man.example.com', + owner=admin) + vm_domain = self.create(Domain, 'name', name='vm.example.com', + owner=admin) # vlans - net = self.create(Vlan, 'name', name='net', vid=4, - network4=options['external_net'], domain=domain) + net = self.create(Vlan, 'vid', name=options['external_if'], vid=4, + network4=options['external_net'], domain=net_domain) - man = self.create(Vlan, 'name', name='man', vid=3, dhcp_pool='manual', - network4=options['management_net'], domain=domain, + man = self.create(Vlan, 'vid', name=options['management_if'], vid=3, + dhcp_pool='manual', + network4=options['management_net'], + domain=man_domain, snat_ip=options['external_net'].split('/')[0]) man.snat_to.add(net) man.snat_to.add(man) - vm = self.create(Vlan, 'name', name='vm', vid=2, dhcp_pool='manual', - network4=options['vm_net'], domain=domain, + vm = self.create(Vlan, 'vid', name=options['vm_if'], vid=2, + dhcp_pool='manual', + network4=options['vm_net'], domain=vm_domain, snat_ip=options['external_net'].split('/')[0]) vm.snat_to.add(net) vm.snat_to.add(vm) @@ -115,14 +125,6 @@ class Command(BaseCommand): vg_net = self.create(VlanGroup, 'name', name='net') vg_net.vlans.add(net) - # portal host - portal = self.create(Host, 'hostname', hostname='portal', vlan=man, - mac='11:22:33:44:55:66', owner=admin, - shared_ip=True, external_ipv4=man.snat_ip, - ipv4=options['portal_ip']) - portal.add_port(proto='tcp', public=443, private=443) - portal.add_port(proto='tcp', public=22, private=22) - # firewall rules fw = self.create(Firewall, 'name', name=options['firewall_queue']) @@ -130,8 +132,16 @@ class Command(BaseCommand): direction='out', action='accept', foreign_network=vg_all, firewall=fw) - self.create(Rule, 'description', description='default input rule', - direction='in', action='accept', + self.create(Rule, 'description', description='portal https', + direction='in', action='accept', proto='tcp', dport=443, + foreign_network=vg_all, firewall=fw) + + self.create(Rule, 'description', description='portal http', + direction='in', action='accept', proto='tcp', dport=80, + foreign_network=vg_all, firewall=fw) + + self.create(Rule, 'description', description='ssh', + direction='in', action='accept', proto='tcp', dport=22, foreign_network=vg_all, firewall=fw) # vlan rules @@ -143,23 +153,4 @@ class Command(BaseCommand): direction='out', action='accept', foreign_network=vg_net, vlan=man) - # switch - # uplink interface - sp_net = self.create(SwitchPort, 'untagged_vlan', untagged_vlan=net) - self.create(EthernetDevice, 'switch_port', switch_port=sp_net, - name=options['external_if']) - - # management interface - if options['management_if']: - sp_man = self.create( - SwitchPort, 'untagged_vlan', untagged_vlan=man) - self.create(EthernetDevice, 'switch_port', switch_port=sp_man, - name=options['management_if']) - - # vm interface - sp_trunk = self.create( - SwitchPort, 'tagged_vlans', untagged_vlan=man, tagged_vlans=vg_all) - self.create(EthernetDevice, 'switch_port', switch_port=sp_trunk, - name=options['trunk_if']) - return self.print_state() diff --git a/circle/dashboard/migrations/0003_message.py b/circle/dashboard/migrations/0003_message.py new file mode 100644 index 0000000..18adadf --- /dev/null +++ b/circle/dashboard/migrations/0003_message.py @@ -0,0 +1,35 @@ +# -*- 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,), + ), + ] diff --git a/circle/dashboard/models.py b/circle/dashboard/models.py index 6231f88..6563fc2 100644 --- a/circle/dashboard/models.py +++ b/circle/dashboard/models.py @@ -27,7 +27,7 @@ from django.contrib.auth.signals import user_logged_in from django.core.urlresolvers import reverse from django.db.models import ( Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField, - DateTimeField, permalink, BooleanField + DateTimeField, BooleanField ) from django.db.models.signals import post_save, pre_delete, post_delete from django.templatetags.static import static @@ -39,14 +39,13 @@ from django.core.exceptions import ObjectDoesNotExist from sizefield.models import FileSizeField from jsonfield import JSONField -from model_utils.models import TimeStampedModel +from model_utils.models import TimeFramedModel, TimeStampedModel from model_utils.fields import StatusField from model_utils import Choices from acl.models import AclBase from common.models import HumanReadableObject, create_readable, Encoder -from vm.tasks.agent_tasks import add_keys, del_keys from vm.models.instance import ACCESS_METHODS from .store_api import Store, NoStoreException, NotOkException, Timeout @@ -59,6 +58,27 @@ def pwgen(): return User.objects.make_random_password() +class Message(TimeStampedModel, TimeFramedModel): + message = CharField(max_length=500, verbose_name=_('message')) + effect = CharField( + default='info', max_length=10, verbose_name=_('effect'), + choices=(('success', _('success')), ('info', _('info')), + ('warning', _('warning')), ('danger', _('danger')))) + enabled = BooleanField(default=False, verbose_name=_('enabled')) + + class Meta: + ordering = ["id"] + verbose_name = _('message') + verbose_name_plural = _('messages') + + def __unicode__(self): + return self.message + + def get_absolute_url(self): + return reverse('dashboard.views.message-detail', + kwargs={'pk': self.pk}) + + class Favourite(Model): instance = ForeignKey("vm.Instance") user = ForeignKey(User) @@ -270,10 +290,9 @@ class GroupProfile(AclBase): except cls.DoesNotExist: return Group.objects.get(name=name) - @permalink def get_absolute_url(self): - return ('dashboard.views.group-detail', None, - {'pk': self.group.pk}) + return reverse('dashboard.views.group-detail', + kwargs={'pk': self.group.pk}) def get_or_create_profile(self): @@ -309,7 +328,7 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'): attributes = kwargs.pop('attributes') atr = settings.SAML_ORG_ID_ATTRIBUTE try: - value = attributes[atr][0] + value = attributes[atr][0].upper() except Exception as e: value = None logger.info("save_org_id couldn't find attribute. %s", unicode(e)) @@ -339,7 +358,7 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'): group, unicode(g)) g.user_set.add(sender) - for i in FutureMember.objects.filter(org_id=value): + for i in FutureMember.objects.filter(org_id__iexact=value): i.group.user_set.add(sender) i.delete() @@ -409,9 +428,7 @@ def add_ssh_keys(sender, **kwargs): 'user', userkey.user).filter(status='RUNNING') for i in instances: logger.info('called add_keys(%s, %s)', i, userkey) - queue = i.get_remote_queue_name("agent") - add_keys.apply_async(args=(i.vm_name, [userkey.key]), - queue=queue) + i.install_keys(user=userkey.user, keys=[userkey.key]) def del_ssh_keys(sender, **kwargs): @@ -422,9 +439,7 @@ def del_ssh_keys(sender, **kwargs): 'user', userkey.user).filter(status='RUNNING') for i in instances: logger.info('called del_keys(%s, %s)', i, userkey) - queue = i.get_remote_queue_name("agent") - del_keys.apply_async(args=(i.vm_name, [userkey.key]), - queue=queue) + i.remove_keys(user=userkey.user, keys=[userkey.key]) post_save.connect(add_ssh_keys, sender=UserKey) diff --git a/circle/dashboard/static/dashboard/activity.js b/circle/dashboard/static/dashboard/activity.js index 31bc4d5..0e1b80c 100644 --- a/circle/dashboard/static/dashboard/activity.js +++ b/circle/dashboard/static/dashboard/activity.js @@ -145,11 +145,13 @@ $(function() { $("#dashboard-vm-details-connect-button").removeClass('disabled'); } $("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled"); + $("#getScreenshotButton").prop("disabled", false); } else { if(data.connect_uri) { $("#dashboard-vm-details-connect-button").addClass('disabled'); } $("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled"); + $("#getScreenshotButton").prop("disabled", true); } if(data.status == "STOPPED" || data.status == "PENDING") { diff --git a/circle/dashboard/static/dashboard/dashboard.js b/circle/dashboard/static/dashboard/dashboard.js index 93ee76f..afac648 100644 --- a/circle/dashboard/static/dashboard/dashboard.js +++ b/circle/dashboard/static/dashboard/dashboard.js @@ -10,10 +10,11 @@ $(function () { $(".not-tab-pane").removeClass("not-tab-pane").addClass("tab-pane"); $('.vm-create').click(function(e) { - var template = $(this).data("template"); + var url = $(this).data("href"); + if(!url) url = $(this).prop("href"); $.ajax({ type: 'GET', - url: $(this).attr('href'), + url: url, success: function(data) { $('body').append(data); vmCreateLoaded(); @@ -140,7 +141,7 @@ $(function () { // success }, error: function(xhr, textStatus, error) { - console.log("oh babám"); + addMessage(gettext("An error occurred. (") + xhr.status + ")", 'danger'); } }); $(star).tooltip('destroy').tooltip({'placement': 'right'}); @@ -528,6 +529,7 @@ function safe_tags_replace(str) { return str.replace(/[&<>]/g, replaceTag); } + $('.crosslink').click(function(e) { // Don't follow the link event.preventDefault(); @@ -535,3 +537,33 @@ $('.crosslink').click(function(e) { $(menu).click(); window.location = this.href; }); + + +$(function () { + var closed = JSON.parse(getCookie('broadcast-messages')); + $('.broadcast-message').each(function() { + var id = $(this).data('id'); + if (closed && closed.indexOf(id) != -1) { + $(this).remove(); + } + }); + + $('.broadcast-message').on('closed.bs.alert', function () { + var closed = JSON.parse(getCookie('broadcast-messages')); + if (!closed) { + closed = []; + } + closed.push($(this).data('id')); + setCookie('broadcast-messages', JSON.stringify(closed), 7 * 24 * 60 * 60 * 1000, "/"); + }); + + $("#id_message").on('input', function() { + $('.broadcast-message').html($(this).val()); + }); + + $("#id_effect").on('input', function() { + $('.broadcast-message').removeClass( + 'alert-info alert-warning alert-success alert-danger').addClass( + "alert-" + $(this).val()); + }); +}); diff --git a/circle/dashboard/static/dashboard/dashboard.less b/circle/dashboard/static/dashboard/dashboard.less index 784289d..3bd3e2a 100644 --- a/circle/dashboard/static/dashboard/dashboard.less +++ b/circle/dashboard/static/dashboard/dashboard.less @@ -10,6 +10,21 @@ html { min-height: 100%; } +.navbar-nav img { + width: 46px; + height: 46px; + margin-top: -4px; +} + +.profile-avatar { + width: 24px; + height: 24px; +} + +.navbar-fixed-top { + border: 0px; +} + /* Set widths on the navbar form inputs since otherwise they're 100% wide */ .navbar-form input[type="text"], .navbar-form input[type="password"] { @@ -32,7 +47,7 @@ html { #dashboard-menu { - margin-right: 15px; + margin-right: 0px; } /* we need this for mobile view */ @@ -56,6 +71,9 @@ html { padding-bottom: 12.5px; } + #dashboard-menu > li:last-child > a { + padding: 4px 0 0 0; + } } .no-margin { @@ -110,6 +128,11 @@ html { background-color: black!important; } +.timeline img{ + border-radius: 50%; + margin-bottom: 2px; +} + .timeline a { color: black; } @@ -1272,6 +1295,25 @@ textarea[name="new_members"] { margin-top: 20px; } +#vm-renew-request-lease, #vm-request-resource-form { + display: none; +} + +.label-100 { + display: block; + width: 100%; +} + +#modify-the-resources { + font-size: 18px; + display: none; +} + +#vm-request-resource-form textarea { + max-width: 500px; + height: 150px; +} + #disk-list-table { td:last-child { text-align: center; @@ -1442,3 +1484,32 @@ height: calc(100% - 130px); .overview_href { cursor: pointer; } + +#request-buttons { + form { + display: inline; + } + + textarea { + resize: none; + min-height: 80px; + } +} + +.nowrap { + white-space: nowrap; +} + +.little-margin-bottom { + margin-bottom: 5px; +} + +.broadcast-message { + margin-bottom: 5px; + padding-top: 5px; + padding-bottom: 5px; +} + +.acl-table td:first-child { + text-align: center; +} diff --git a/circle/dashboard/static/dashboard/img/og.png b/circle/dashboard/static/dashboard/img/og.png new file mode 100644 index 0000000..c1dbf23 Binary files /dev/null and b/circle/dashboard/static/dashboard/img/og.png differ diff --git a/circle/dashboard/static/dashboard/vm-details.js b/circle/dashboard/static/dashboard/vm-details.js index 7f9e4e0..f22336f 100644 --- a/circle/dashboard/static/dashboard/vm-details.js +++ b/circle/dashboard/static/dashboard/vm-details.js @@ -38,6 +38,13 @@ $(function() { e.preventDefault(); }); + /* save as (close vnc console) */ + $('.operation-save_as_template').click(function(e) { + if ($('li.active > a[href$="console"]').length > 0) { + $('a[data-toggle$="pill"][href$="#activity"]').click(); + } + }); + /* remove tag */ $('.vm-details-remove-tag').click(function() { var to_remove = $.trim($(this).parent('div').text()); @@ -178,12 +185,13 @@ $(function() { $("i", this).addClass("fa-spinner fa-spin"); $(this).prop("disabled", true); ct.slideDown(); - var img = $("img", ct).prop("src", '/dashboard/vm/' + vm + '/screenshot/'); + var img = $("img", ct).prop("src", '/dashboard/vm/' + vm + '/screenshot/?rnd=' + Math.random()); }); // if the image is loaded remove the spinning stuff // note: this should not work if the image is cached, but it's not // see: http://stackoverflow.com/a/3877079/1112653 + // note #2: it actually gets cached, so a random number is appended $("#vm-console-screenshot img").load(function(e) { $("#getScreenshotButton").prop("disabled", false) .find("i").removeClass("fa-spinner fa-spin"); @@ -193,7 +201,7 @@ $(function() { // screenshot close $("#vm-console-screenshot button").click(function() { - $(this).parent("div").slideUp(); + $(this).closest("div").slideUp(); }); // select connection string @@ -223,4 +231,25 @@ $(function() { return false; }); + $(document).on("click", "#vm-renew-request-lease-button", function(e) { + $("#vm-renew-request-lease").stop().slideToggle(); + e.preventDefault(); + }); + + $("#vm-request-resource").click(function(e) { + $(".cpu-priority-slider, .cpu-count-slider, .ram-slider").simpleSlider("setDisabled", false); + $(".ram-input, .cpu-count-input, .cpu-priority-input").prop("disabled", false); + + $("#vm-details-resources-form").prop("action", $(this).prop("href")); + $("#vm-request-resource-form").show(); + $("#modify-the-resources").show(); + $(this).hide(); + + $("html, body").animate({ + scrollTop: $("#modify-the-resources").offset().top - 60 + }); + + return e.preventDefault(); + }); + }); diff --git a/circle/dashboard/static/template.less b/circle/dashboard/static/template.less index e54adc2..4df636e 100644 --- a/circle/dashboard/static/template.less +++ b/circle/dashboard/static/template.less @@ -55,6 +55,9 @@ html { .bigbig { font-size: 3em; } +.big-tag { + font-size: 1.2em; +} /* small buttons for tags, copied from Bootstraps input-sm, bnt-sm */ .btn-tags, .btn-traits { padding: 3px 6px; @@ -148,3 +151,7 @@ footer a, footer a:hover, footer a:visited { .btn-toolbar { margin-bottom: 5px; } + +.vm-details-home-edit-description { + font-size: 85%; /* ~ small tag */ +} diff --git a/circle/dashboard/tables.py b/circle/dashboard/tables.py index b5059cd..fe2dbef 100644 --- a/circle/dashboard/tables.py +++ b/circle/dashboard/tables.py @@ -29,7 +29,7 @@ from django_sshkey.models import UserKey from storage.models import Disk from vm.models import Node, InstanceTemplate, Lease -from dashboard.models import ConnectCommand +from dashboard.models import ConnectCommand, Message class FileSizeColumn(Column): @@ -354,3 +354,19 @@ class DiskListTable(Table): order_by = ("-pk", ) per_page = 15 empty_text = _("No disk found.") + + +class MessageListTable(Table): + message = LinkColumn( + 'dashboard.views.message-detail', + args=[A('pk')], + attrs={'th': {'data-sort': "string"}} + ) + + class Meta: + template = "django_tables2/with_pagination.html" + model = Message + attrs = {'class': "table table-bordered table-striped table-hover", + 'id': "disk-list-table"} + order_by = ("-pk", ) + fields = ('pk', 'message', 'enabled', 'effect') diff --git a/circle/dashboard/templates/base.html b/circle/dashboard/templates/base.html index b3bf340..206dd15 100644 --- a/circle/dashboard/templates/base.html +++ b/circle/dashboard/templates/base.html @@ -1,6 +1,7 @@ {% load i18n %} {% load staticfiles %} -{% load compressed %} +{% load cache %} +{% load pipeline %} <!DOCTYPE html> <html lang="{{lang}}"> <head> @@ -9,17 +10,17 @@ <meta name="description" content=""> <meta name="author" content=""> <link rel="icon" type="image/png" href="{% static "dashboard/img/favicon.png" %}"/> + {% block extra_link %}{% endblock %} <title>{% block title %}{% block title-page %}{% endblock %} | {% block title-site %}CIRCLE{% endblock %}{% endblock %}</title> - {% compressed_css 'all' %} + {% stylesheet 'all' %} <!-- HTML5 shim, for IE6-8 support of HTML5 elements --> <!--[if lt IE 9]> <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> - {% block extra_link %}{% endblock %} {% block extra_css %}{% endblock %} </head> @@ -27,7 +28,12 @@ <body> <div class="navbar navbar-inverse navbar-fixed-top"> <div class="navbar-header"> - {% block navbar-brand %}{% endblock %} + {% block navbar-brand %} + <a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;"> + {% include "branding.html" %} + </a> + {% endblock %} + <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> @@ -40,6 +46,22 @@ </div><!-- navbar navbar-inverse navbar-fixed-top --> <div class="container"> + {% block broadcast_messages %} + {% cache 30 broadcast_messages %} + <div id="broadcast-messages"> + {% for message in broadcast_messages %} + <div data-id={{ message.id }} class="alert alert-{{ message.effect }} + text-center broadcast-message"> + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + {{ message.message|safe }} + </div> + {% endfor %} + </div> + {% endcache %} + {% endblock broadcast_messages %} + {% block messages %} <div class="messagelist"> {% if messages %} @@ -50,7 +72,7 @@ </div> {% endfor %} {% endif %} - </div> + </div> {% endblock messages %} {% block content %} <h1 class="alert alert-error">Please override "content" block.</h1> @@ -67,7 +89,7 @@ <script src="{% static "jquery/dist/jquery.min.js" %}"></script> <script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script> - {% compressed_js 'all' %} + {% javascript 'all' %} {% block extra_script %} {% endblock %} diff --git a/circle/dashboard/templates/dashboard/_display-name.html b/circle/dashboard/templates/dashboard/_display-name.html index 240594f..c315f81 100644 --- a/circle/dashboard/templates/dashboard/_display-name.html +++ b/circle/dashboard/templates/dashboard/_display-name.html @@ -1,6 +1,10 @@ {% load i18n %} {% if user and user.pk %} + {% if show_pic %} + <img class="profile-avatar" src="{{ user.profile.get_avatar_url }}" /> + {% endif%} + {% if user.get_full_name %}{{ user.get_full_name }}{% else %}{{ user.username }}{% endif %}{% if new_line %}<br />{% endif %} {% if show_org %} @@ -10,4 +14,5 @@ ({% trans "username" %}: {{ user.username }}) {% endif %} {% endif %} + {% endif %} diff --git a/circle/dashboard/templates/dashboard/_manage_access.html b/circle/dashboard/templates/dashboard/_manage_access.html index f23b160..d111599 100644 --- a/circle/dashboard/templates/dashboard/_manage_access.html +++ b/circle/dashboard/templates/dashboard/_manage_access.html @@ -1,6 +1,6 @@ {% load i18n %} <form action="{{ acl.url }}" method="post">{% csrf_token %} - <table class="table table-striped table-with-form-fields" id="{{table_id}}"> + <table class="table table-striped table-with-form-fields acl-table" id="{{table_id}}"> <thead> <tr> <th></th> @@ -13,7 +13,7 @@ {% for i in acl.users %} <tr> <td> - <i class="fa fa-user"></i> + <img class="profile-avatar" src="{{ i.user.profile.get_avatar_url }}"/> </td> <td> <a href="{% url "dashboard.views.profile" username=i.user.username %}" diff --git a/circle/dashboard/templates/dashboard/_vm-create-1.html b/circle/dashboard/templates/dashboard/_vm-create-1.html index 9a7c54f..dc046be 100644 --- a/circle/dashboard/templates/dashboard/_vm-create-1.html +++ b/circle/dashboard/templates/dashboard/_vm-create-1.html @@ -79,10 +79,26 @@ </div> </div> {% empty %} - {% trans "You can't start new virtual machines because no templates are shared with you." %} + {% if not template_access_types %} + {% trans "You can't start new virtual machines because no templates are shared with you." %} + {% else %} + {% trans "You can't start new virtual machines because no templates are shared with you however you can request them via the form below." %} + <hr /> + {% include "request/_request-template-form.html" %} + {% endif %} {% endfor %} </div> + {% if templates and template_access_types %} + {% url "request.views.request-template" as request_url %} + <hr /> + <p class="text-right"> + {% blocktrans with url=request_url %} + Need other templates? Submit a new <a href="{{ url }}">request</a>. + {% endblocktrans %} + </p> + {% endif %} + <style> .progress { position: relative; diff --git a/circle/dashboard/templates/dashboard/_vm-renew.html b/circle/dashboard/templates/dashboard/_vm-renew.html new file mode 100644 index 0000000..5ef1511 --- /dev/null +++ b/circle/dashboard/templates/dashboard/_vm-renew.html @@ -0,0 +1,30 @@ +{% 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 %} diff --git a/circle/dashboard/templates/dashboard/base.html b/circle/dashboard/templates/dashboard/base.html index 542339c..41c88ed 100644 --- a/circle/dashboard/templates/dashboard/base.html +++ b/circle/dashboard/templates/dashboard/base.html @@ -10,28 +10,47 @@ {% endblock %} -{% block navbar-brand %} - <a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;"> - {% include "branding.html" %} - </a> -{% endblock %} - {% block navbar %} {% if user.is_authenticated and user.pk and not request.token_user %} <ul class="nav navbar-nav navbar-right" id="dashboard-menu"> {% if user.is_superuser %} {% if ADMIN_ENABLED %} <li> - <a href="/admin/"><i class="fa fa-cogs"></i> {% trans "Admin" %}</a> + <a href="/admin/"> + <i class="fa fa-cogs"></i> + <span class="hidden-sm">{% trans "Admin" %}</span> + </a> + </li> + <li> + <a href="/rosetta/"> + <i class="fa fa-space-shuttle"></i> + <span class="hidden-sm">{% trans "Translations" %}</span> + </a> </li> {% endif %} <li> - <a href="{% url "dashboard.views.storage" %}"><i class="fa fa-database"></i> - {% trans "Storage" %} + <a href="{% url "dashboard.views.message-list" %}"> + <i class="fa fa-bullhorn"></i> + <span class="hidden-sm">{% trans "Messages" %}</span> + </a> + </li> + <li> + <a href="{% url "dashboard.views.storage" %}"> + <i class="fa fa-database"></i> + <span class="hidden-sm">{% trans "Storage" %}</span> + </a> + </li> + <li> + <a href="{% url "network.index" %}"> + <i class="fa fa-globe"></i> + <span class="hidden-sm">{% trans "Network" %}</span> </a> </li> <li> - <a href="/network/"><i class="fa fa-globe"></i> {% trans "Network" %}</a> + <a href="{% url "request.views.request-list" %}"> + <i class="fa fa-phone"></i> + <span class="hidden-sm">{% trans "Requests" %}</span> + </a> </li> {% endif %} <li> @@ -65,9 +84,14 @@ {% endif %} </a> <ul class="dropdown-menu" id="notification-messages"> - <li>{% trans "Loading..." %}</li> + <li>{% trans "Loading..." %}</li> </ul> </li> + <li class="hidden-xs"> + <a href="{% url "dashboard.views.profile-preferences" %}"> + <img class="profile-avatar" src="{{ user.profile.get_avatar_url }}" /> + </a> + </li> </ul> {% else %} diff --git a/circle/dashboard/templates/dashboard/group-detail.html b/circle/dashboard/templates/dashboard/group-detail.html index 5399cdb..19119d9 100644 --- a/circle/dashboard/templates/dashboard/group-detail.html +++ b/circle/dashboard/templates/dashboard/group-detail.html @@ -90,7 +90,7 @@ {% for i in users %} <tr> <td> - <i class="fa fa-user"></i> + <img class="profile-avatar" src="{{ i.profile.get_avatar_url}}"/> </td> <td> <a href="{% url "dashboard.views.profile" username=i.username %}" title="{{ i.username }}" diff --git a/circle/dashboard/templates/dashboard/index-groups.html b/circle/dashboard/templates/dashboard/index-groups.html index 462ca6f..146bb66 100644 --- a/circle/dashboard/templates/dashboard/index-groups.html +++ b/circle/dashboard/templates/dashboard/index-groups.html @@ -20,7 +20,7 @@ <div class="col-xs-6"> <form action="{% url "dashboard.views.group-list" %}" method="GET" id="dashboard-group-search-form"> <div class="input-group input-group-sm"> - <input id="dashboard-group-search-input" name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" /> + <input name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" /> <div class="input-group-btn"> <button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button> </div> diff --git a/circle/dashboard/templates/dashboard/index-nodes.html b/circle/dashboard/templates/dashboard/index-nodes.html index 1966ca0..d387131 100644 --- a/circle/dashboard/templates/dashboard/index-nodes.html +++ b/circle/dashboard/templates/dashboard/index-nodes.html @@ -72,11 +72,15 @@ value="{% widthratio node_num.running sum_node_num 100 %}"> </p> <p> + {% blocktrans with running=node_num.running missing=node_num.missing disabled=node_num.disabled offline=node_num.offline %} <span class="big"> - <big>{{ node_num.running }}</big> running - </span> - + <big>{{ node_num.missing }}</big> - missing + <br><big>{{ node_num.disabled }}</big> disabled + <big>{{ node_num.offline }}</big> offline + <span class="big-tag">{{ running }}</span> running + </span> + + <span class="big-tag">{{ missing }}</span> missing + + <br> + <span class="big-tag">{{ disabled }}</span> disabled + + <span class="big-tag">{{ offline }}</span> offline + {% endblocktrans %} </p> <ul class="list-inline" id="dashboard-node-taglist"> {% for i in nodes %} diff --git a/circle/dashboard/templates/dashboard/index-templates.html b/circle/dashboard/templates/dashboard/index-templates.html index 2dd7575..4e9ae19 100644 --- a/circle/dashboard/templates/dashboard/index-templates.html +++ b/circle/dashboard/templates/dashboard/index-templates.html @@ -16,7 +16,7 @@ <i class="fa fa-{{ t.os_type }}"></i> {{ t.name }} </span> <small class="text-muted index-template-list-system">{{ t.system }}</small> - <div href="{% url "dashboard.views.vm-create" %}?template={{ t.pk }}" class="pull-right vm-create"> + <div data-href="{% url "dashboard.views.vm-create" %}?template={{ t.pk }}" class="pull-right vm-create"> <i data-container="body" title="{% trans "Start VM instance" %}" class="fa fa-play"></i> </div> @@ -37,7 +37,7 @@ <div class="col-xs-5 col-sm-6"> <form action="{% url "dashboard.views.template-list" %}" method="GET" id="dashboard-template-search-form"> <div class="input-group input-group-sm"> - <input id="dashboard-group-search-input" name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" /> + <input name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" /> <div class="input-group-btn"> <button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button> </div> diff --git a/circle/dashboard/templates/dashboard/index-users.html b/circle/dashboard/templates/dashboard/index-users.html index 6d47a74..64bdc31 100644 --- a/circle/dashboard/templates/dashboard/index-users.html +++ b/circle/dashboard/templates/dashboard/index-users.html @@ -25,7 +25,7 @@ <div class="col-xs-5 col-sm-6"> <form action="{% url "dashboard.views.user-list" %}" method="GET" id="dashboard-user-search-form"> <div class="input-group input-group-sm"> - <input id="dashboard-group-search-input" name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" /> + <input name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" /> <div class="input-group-btn"> <button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button> </div> diff --git a/circle/dashboard/templates/dashboard/index-vm.html b/circle/dashboard/templates/dashboard/index-vm.html index 4700531..39f36b2 100644 --- a/circle/dashboard/templates/dashboard/index-vm.html +++ b/circle/dashboard/templates/dashboard/index-vm.html @@ -90,7 +90,7 @@ <p class="pull-right"> <input class="knob" data-fgColor="chartreuse" data-thickness=".4" data-max="{{ request.user.profile.instance_limit }}" data-width="100" data-height="100" data-readOnly="true" value="{{ instances|length|add:more_instances }}"> </p> - <span class="bigbig">{% blocktrans with count=running_vm_num %}<big>{{ count }}</big> running{% endblocktrans %}</span> + <span class="bigbig">{% blocktrans with count=running_vm_num %}<span class="big-tag">{{ count }}</span> running{% endblocktrans %}</span> <ul class="list-inline" style="max-height: 95px; overflow: hidden;"> {% for vm in running_vms %} <li style="display: inline-block; padding: 2px;"> @@ -111,7 +111,9 @@ <strong>{{ counter }}</strong> machines total {% endblocktrans %} </a> - <p class="big text-warning">{% blocktrans with count=stopped_vm_num %}<big>{{ count }}</big> stopped{% endblocktrans %}</p> + <p class="big text-warning"> + {% blocktrans with count=stopped_vm_num %}{{ count }} stopped{% endblocktrans %} + </p> </div> </div> </div> diff --git a/circle/dashboard/templates/dashboard/instanceactivity_detail.html b/circle/dashboard/templates/dashboard/instanceactivity_detail.html index 2784e99..c41325f 100644 --- a/circle/dashboard/templates/dashboard/instanceactivity_detail.html +++ b/circle/dashboard/templates/dashboard/instanceactivity_detail.html @@ -54,7 +54,7 @@ <dt>{% trans "result" %}</dt> - <dd><textarea class="form-control">{{object.result|get_text:user}}</textarea></dd> + <dd><textarea class="form-control" id="activity_result_text">{{object.result|get_text:user}}</textarea></dd> <dt>{% trans "resultant state" %}</dt> <dd>{{object.resultant_state|default:'n/a'}}</dd> diff --git a/circle/dashboard/templates/dashboard/message-create.html b/circle/dashboard/templates/dashboard/message-create.html new file mode 100644 index 0000000..4a3f199 --- /dev/null +++ b/circle/dashboard/templates/dashboard/message-create.html @@ -0,0 +1,27 @@ +{% 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 %} diff --git a/circle/dashboard/templates/dashboard/message-edit.html b/circle/dashboard/templates/dashboard/message-edit.html new file mode 100644 index 0000000..71e4f6c --- /dev/null +++ b/circle/dashboard/templates/dashboard/message-edit.html @@ -0,0 +1,56 @@ +{% 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 %} diff --git a/circle/dashboard/templates/dashboard/message-list.html b/circle/dashboard/templates/dashboard/message-list.html new file mode 100644 index 0000000..a3834a5 --- /dev/null +++ b/circle/dashboard/templates/dashboard/message-list.html @@ -0,0 +1,28 @@ +{% 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 %} diff --git a/circle/dashboard/templates/dashboard/operate.html b/circle/dashboard/templates/dashboard/operate.html index f535b0f..9627f83 100644 --- a/circle/dashboard/templates/dashboard/operate.html +++ b/circle/dashboard/templates/dashboard/operate.html @@ -16,6 +16,7 @@ Do you want to perform the following operation on {% crispy form %} {% endif %} {% endblock %} + {% block formbuttons %} <div class="pull-right"> <a class="btn btn-default" href="{{object.get_absolute_url}}" data-dismiss="modal">{% trans "Cancel" %}</a> @@ -23,4 +24,7 @@ Do you want to perform the following operation on {% if opview.icon %}<i class="fa fa-fw fa-{{opview.icon}}"></i> {% endif %}{{ op.name|capfirst }} </button> </div> + {% endblock %} </form> + +{% block extra %}{% endblock %} diff --git a/circle/dashboard/templates/dashboard/profile.html b/circle/dashboard/templates/dashboard/profile.html index c1d3922..fafee81 100644 --- a/circle/dashboard/templates/dashboard/profile.html +++ b/circle/dashboard/templates/dashboard/profile.html @@ -2,6 +2,7 @@ {% load staticfiles %} {% load i18n %} {% load crispy_forms_tags %} +{% load arrowfilter %} {% block title-page %}{{ profile.username}} | {% trans "Profile" %}{% endblock %} @@ -23,7 +24,6 @@ {% trans "Back" %}</a> </div> <h3 class="no-margin"> - <i class="fa fa-user"></i> {% include "dashboard/_display-name.html" with user=profile show_org=True %} </h3> </div> @@ -42,6 +42,7 @@ {% trans "Email address" %}: {{ profile.email }} {% endif %} </p> + <p>{% trans "Last login" %}: <span title="{{ request.user.last_login }}">{{ request.user.last_login|arrowfilter:LANGUAGE_CODE}}</span></p> {% if request.user == profile %} <p> {% trans "Use email address as Gravatar profile image" %}: diff --git a/circle/dashboard/templates/dashboard/store/index-files.html b/circle/dashboard/templates/dashboard/store/index-files.html index 622afcb..1dca366 100644 --- a/circle/dashboard/templates/dashboard/store/index-files.html +++ b/circle/dashboard/templates/dashboard/store/index-files.html @@ -58,7 +58,7 @@ <div class="text-right"> <form class="pull-left" method="POST" action="{% url "dashboard.views.store-refresh-toplist" %}"> {% csrf_token %} - <button class="btn btn-success btn-xs" type="submit" title="{% trans "Refresh" %}"/> + <button class="btn btn-success btn-xs" type="submit" title="{% trans "Refresh" %}"> <i class="fa fa-refresh"></i> </button> </form> diff --git a/circle/dashboard/templates/dashboard/vm-detail.html b/circle/dashboard/templates/dashboard/vm-detail.html index 0f15bf8..46dfbe5 100644 --- a/circle/dashboard/templates/dashboard/vm-detail.html +++ b/circle/dashboard/templates/dashboard/vm-detail.html @@ -1,7 +1,7 @@ {% extends "dashboard/base.html" %} {% load staticfiles %} {% load i18n %} -{% load compressed %} +{% load pipeline %} {% block title-page %}{{ instance.name }} | vm{% endblock %} @@ -242,5 +242,5 @@ {% endblock %} {% block extra_js %} - {% compressed_js 'vm-detail' %} + {% javascript 'vm-detail' %} {% endblock %} diff --git a/circle/dashboard/templates/dashboard/vm-detail/_activity-timeline.html b/circle/dashboard/templates/dashboard/vm-detail/_activity-timeline.html index 6efbf35..65753be 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/_activity-timeline.html +++ b/circle/dashboard/templates/dashboard/vm-detail/_activity-timeline.html @@ -5,7 +5,7 @@ {% for a in activities %} <div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" - data-activity-id="{{ a.pk }}" data-activity-code="{{ a.activity_code }}"> + data-activity-id="{{ a.pk }}" data-activity-code="{{ a.activity_code }}" data-timestamp="{{ a.started|date:"U" }}"> <span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}"> <i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-{{a.icon}}{% endif %}"></i> </span> @@ -19,9 +19,9 @@ - {{ a.percentage }}% {% endif %} </strong> -{% endspaceless %}{% if a.times < 2 %} <span title="{{ a.started }}">{{ a.started|arrowfilter:LANGUAGE_CODE }}</span>{% endif %}{% if a.user %}, +{% endspaceless %}{% if a.times < 2 %} <span title="{{ a.started }}">{{ a.started|arrowfilter:LANGUAGE_CODE }}</span>{% endif %}{% if a.user %}, <a class="no-style-link" href="{% url "dashboard.views.profile" username=a.user.username %}"> - {% include "dashboard/_display-name.html" with user=a.user show_org=True %} + {% include "dashboard/_display-name.html" with user=a.user show_org=True show_pic=True %} </a> {% endif %} {% if a.is_abortable_for_user %} @@ -45,7 +45,7 @@ {{ s.finished|arrowfilter:LANGUAGE_CODE }} </span> {% else %} - <i class="fa fa-refresh fa-spin" class="sub-activity-loading-icon"></i> + <i class="fa fa-refresh fa-spin"></i> {% endif %} {% if s.has_failed %} <div class="label label-danger">{% trans "failed" %}</div> diff --git a/circle/dashboard/templates/dashboard/vm-detail/console.html b/circle/dashboard/templates/dashboard/vm-detail/console.html index a9a71ae..a61785d 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/console.html +++ b/circle/dashboard/templates/dashboard/vm-detail/console.html @@ -1,26 +1,47 @@ {% load i18n %} {% load staticfiles %} -<div class="btn-toolbar"> -{% if perms.vm.access_console %} - <button id="sendCtrlAltDelButton" class="btn btn-danger btn-sm">{% trans "Send Ctrl+Alt+Del" %}</button> - <button id="sendPasswordButton" class="btn btn-default btn-sm">{% trans "Type password" %}</button> + +{% if not perms.vm.access_console %} + <div class="alert alert-warning"> + {% trans "You are not authorized to access the VNC console." %} + </div> {% endif %} - <button id="getScreenshotButton" class="btn btn-info btn-sm pull-right" data-vm-pk="{{ instance.pk }}"><i class="fa fa-picture"></i> {% trans "Screenshot" %}</button> +<div class="row"> + <div class="col-xs-7"> + <div class="btn-toolbar"> + {% if perms.vm.access_console %} + <button id="sendCtrlAltDelButton" class="btn btn-danger btn-sm"> + {% trans "Send Ctrl+Alt+Del" %} + </button> + <button id="sendPasswordButton" class="btn btn-default btn-sm"> + {% trans "Type password" %} + </button> + {% endif %} + </div> + </div> + <div class="col-xs-5 text-right"> + <button id="getScreenshotButton" class="btn btn-info btn-sm" data-vm-pk="{{ instance.pk }}"> + <i class="fa fa-photo"></i> {% trans "Screenshot" %} + </button> + </div> </div> + + {% if perms.vm.access_console %} -<div class="alert alert-info" id="noVNC_status"> -</div> + <div class="alert alert-info" id="noVNC_status"></div> {% endif %} <div id="vm-console-screenshot"> - <button class="btn btn-danger btn-sm pull-right">{% trans "Close" %}</button> - <h3>{% trans "Screenshot" %}</h3> - <img /> + <h3> + <button class="btn btn-danger btn-sm pull-right">{% trans "Close" %}</button> + {% trans "Screenshot" %} + </h3> + <img alt="{% trans "Screenshot" %}"/> <hr /> </div> {% if perms.vm.access_console %} -<canvas id="noVNC_canvas" width="640px" height="20px">Canvas not supported. +<canvas id="noVNC_canvas" width="640" height="20">Canvas not supported. </canvas> <script> diff --git a/circle/dashboard/templates/dashboard/vm-detail/home.html b/circle/dashboard/templates/dashboard/vm-detail/home.html index eab1b03..d2ddc0a 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/home.html +++ b/circle/dashboard/templates/dashboard/vm-detail/home.html @@ -39,7 +39,7 @@ <dd> {% csrf_token %} <div class="vm-details-home-edit-description-click"> - <small class="vm-details-home-edit-description">{{ instance.description|linebreaks }}</small> + <div class="vm-details-home-edit-description">{{ instance.description|linebreaks }}</div> </div> <div id="vm-details-home-description" class="js-hidden"> <form method="POST"> @@ -59,10 +59,11 @@ {% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %} <span id="vm-details-renew-op"> {% with op=op.renew %}{% if op %} - <a href="{{op.get_url}}" class="btn btn-success btn-xs + <a href="{{op.get_url}}" class="btn btn-{{op.effect}} btn-xs operation operation-{{op.op}}"> <i class="fa fa-{{op.icon}}"></i> - {{op.name}} </a> + {{op.name}} + </a> {% endif %}{% endwith %} </span> </h4> @@ -156,6 +157,16 @@ </a> </div> {% endif %} + {% if op.install_keys %} + <strong>{% trans "SSH keys" %}</strong> + <div class="operation-wrapper"> + <a href="{{ op.install_keys.get_url }}" class="btn btn-info btn-xs operation" + {% if op.install_keys.disabled %}disabled{% endif %}> + <i class="fa fa-{{op.install_keys.icon}}"></i> + {{ op.install_keys.name }} + </a> + </div> + {% endif %} </div> <div class="col-md-8"> {% if graphite_enabled %} @@ -163,9 +174,12 @@ {% include "dashboard/_graph-time-buttons.html" %} </div> <div class="graph-images text-center"> - <img src="{% url "dashboard.views.vm-graph" instance.pk "cpu" graph_time %}"/> - <img src="{% url "dashboard.views.vm-graph" instance.pk "memory" graph_time %}"/> - <img src="{% url "dashboard.views.vm-graph" instance.pk "network" graph_time %}"/> + <img src="{% url "dashboard.views.vm-graph" instance.pk "cpu" graph_time %}" + alt="{% trans "CPU usage" %}"/> + <img src="{% url "dashboard.views.vm-graph" instance.pk "memory" graph_time %}" + alt="{% trans "Memory usage" %}"/> + <img src="{% url "dashboard.views.vm-graph" instance.pk "network" graph_time %}" + alt="{% trans "Network usage" %}"/> </div> {% endif %} </div> diff --git a/circle/dashboard/templates/dashboard/vm-detail/network.html b/circle/dashboard/templates/dashboard/vm-detail/network.html index 4c0abeb..c8df8d5 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/network.html +++ b/circle/dashboard/templates/dashboard/vm-detail/network.html @@ -1,13 +1,14 @@ {% load i18n %} {% load network_tags %} + +<div id="vm-details-add-interface"> + {% with op=op.add_interface %}{% if op %} + <a href="{{op.get_url}}" class="btn btn-{{op.effect}} operation pull-right" + {% if op.disabled %}disabled{% endif %}> + <i class="fa fa-{{op.icon}}"></i> {% trans "add interface" %}</a> + {% endif %}{% endwith %} +</div> <h2> - <div id="vm-details-add-interface"> - {% with op=op.add_interface %}{% if op %} - <a href="{{op.get_url}}" class="btn btn-{{op.effect}} operation pull-right" - {% if op.disabled %}disabled{% endif %}> - <i class="fa fa-{{op.icon}}"></i> {% trans "add interface" %}</a> - {% endif %}{% endwith %} - </div> {% trans "Interfaces" %} </h2> diff --git a/circle/dashboard/templates/dashboard/vm-detail/resources.html b/circle/dashboard/templates/dashboard/vm-detail/resources.html index baddddb..e2b3bd1 100644 --- a/circle/dashboard/templates/dashboard/vm-detail/resources.html +++ b/circle/dashboard/templates/dashboard/vm-detail/resources.html @@ -2,19 +2,42 @@ {% load sizefieldtags %} {% load crispy_forms_tags %} + +<div class="label label-info label-100" id="modify-the-resources"> + {% trans "Modify the resources" %} +</div> <form method="POST" action="{{ op.resources_change.get_url }}" id="vm-details-resources-form"> {% csrf_token %} {% include "dashboard/_resources-sliders.html" with field_priority=resources_form.priority field_num_cores=resources_form.num_cores field_ram_size=resources_form.ram_size %} {% if op.resources_change %} - <button type="submit" class="btn btn-success btn-sm change-resources-button" - id="vm-details-resources-save" data-vm="{{ instance.pk }}" - {% if op.resources_change.disabled %}disabled{% endif %}> - <i class="fa fa-floppy-o"></i> {% trans "Save resources" %} - </button> - <span class="change-resources-help" - {% if not op.resources_change.disabled %}style="display: none;"{% endif %} - >{% trans "Stop your VM to change resources." %}</span> + <button type="submit" class="btn btn-success btn-sm change-resources-button" + id="vm-details-resources-save" data-vm="{{ instance.pk }}" + {% if not save_resources_enabled %}disabled{% endif %}> + <i class="fa fa-floppy-o"></i> {% trans "Save resources" %} + </button> + <span class="change-resources-help" + {% if save_resources_enabled %}style="display: none;"{% endif %}> + {% trans "Stop your VM to change resources." %} + </span> +{% else %} + <div id="vm-request-resource-form"> + <div class="alert alert-info text-justify"> + {% trans "Changing resources is only possible on virtual machines with STOPPED state. We suggest to turn off the VM after submitting the request otherwise it will be automatically stopped in the future when the request is accepted." %} + </div> + <div class="form-group"> + <label>{% trans "Message" %}*</label> + <textarea class="form-control" name="message">{% include "request/initials/resources.html" %}</textarea> + </div> + <input type="submit" class="btn btn-success btn-sm"/> + </div> + + <a href="{% url "request.views.request-resource" vm_pk=object.pk %}" + class="btn btn-primary btn-sm" id="vm-request-resource"> + <i class="fa fa-tasks"></i> + {% trans "Request resources" %} + </a> + {% endif %} </form> @@ -22,12 +45,12 @@ <div id="vm-details-resources-disk"> <h3> - {% trans "Disks" %} <div class="pull-right"> <div id="disk-ops"> {% include "dashboard/vm-detail/_disk-operations.html" %} </div> </div> + {% trans "Disks" %} </h3> <div class="clearfix"></div> @@ -46,7 +69,7 @@ {% if user.is_superuser %} <hr/> -<div class="row" id=""> +<div class="row"> <div class="col-sm-12"> <h3> {% trans "Required traits" %} @@ -58,7 +81,7 @@ <hr/> -<div class="row" id=""> +<div class="row"> <div class="col-sm-12"> <h3> {% trans "Raw data" %} diff --git a/circle/dashboard/templates/open-graph.html b/circle/dashboard/templates/open-graph.html new file mode 100644 index 0000000..583d62d --- /dev/null +++ b/circle/dashboard/templates/open-graph.html @@ -0,0 +1,4 @@ +<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"/> diff --git a/circle/dashboard/tests/selenium/basic_tests.py b/circle/dashboard/tests/selenium/basic_tests.py index 5b0ee8b..75c6abf 100644 --- a/circle/dashboard/tests/selenium/basic_tests.py +++ b/circle/dashboard/tests/selenium/basic_tests.py @@ -16,546 +16,78 @@ # # You should have received a copy of the GNU General Public License along # with CIRCLE. If not, see <http://www.gnu.org/licenses/>. -from selenose.cases import SeleniumTestCase -from django.contrib.auth.models import User +from datetime import datetime +import logging +from sys import _getframe import random -import urlparse import re -import time -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as ec -from selenium.webdriver.support.select import Select -from selenium.webdriver.common.by import By -from datetime import datetime -from selenium.common.exceptions import NoSuchElementException -random_pass = "".join([random.choice( - '0123456789abcdefghijklmnopqrstvwxyz') for n in xrange(10)]) -random_accents = random_pass + "".join([random.choice( - u"áéíöóúűÁÉÍÖÓÜÚŰ") for n in xrange(5)]) -wait_max_sec = 10 -host = 'https:127.0.0.1' -client_name = 'test_%s' % random_accents - - -class UtilityMixin(object): - def login(self, username, password='password', location=None): - driver = self.driver - if location is None: - location = '/dashboard/' - driver.get('%s%s' % (host, location)) - # Only if we aren't logged in already - if location not in urlparse.urlparse(self.driver.current_url).path: - try: - name_input = driver.find_element_by_id("id_username") - password_input = driver.find_element_by_id("id_password") - submit_input = driver.find_element_by_id("submit-id-submit") - except: - inputs = driver.find_elements_by_tag_name("input") - for current_input in inputs: - input_type = current_input.get_attribute("type") - if input_type == "text": - name_input = current_input - if input_type == "password": - password_input = current_input - if input_type == "submit": - submit_input = current_input - try: - name_input.clear() - name_input.send_keys(username) - password_input.clear() - password_input.send_keys(password) - submit_input.click() - try: - # If selenium runs only in a small (virtual) screen - driver.find_element_by_class_name('navbar-toggle').click() - WebDriverWait(self.driver, wait_max_sec).until( - ec.element_to_be_clickable(( - By.CSS_SELECTOR, - "a[href*='/dashboard/profile/']"))) - except: - time.sleep(0.5) - except: - raise Exception('Selenium cannot find the form controls') - - def list_options(self, select): - try: - option_dic = {} - select = Select(select) - for option in select.options: - key = option.get_attribute('value') - if key is not None and key: - option_dic[key] = [option.text] - return option_dic - except: - raise Exception( - 'Selenium cannot list the select possibilities') - - def select_option(self, select, what=None): - """ - From an HTML select imput type try to choose the specified one. - Select is a selenium web element type. What represent both the - text of the option and it's ID. - """ - try: - my_choice = None - options = self.list_options(select) - select = Select(select) - if what is not None: - for key, value in options.iteritems(): - if what in key: - my_choice = key - else: - if isinstance(value, list): - for single_value in value: - if what in single_value: - my_choice = key - else: - if what in value: - my_choice = key - if my_choice is None: - my_choose_list = options.keys() - my_choice = my_choose_list[random.randint( - 0, len(my_choose_list) - 1)] - select.select_by_value(my_choice) - except: - raise Exception( - 'Selenium cannot select the chosen one') - - def get_link_by_href(self, target_href, attributes=None): - try: - links = self.driver.find_elements_by_tag_name('a') - for link in links: - href = link.get_attribute('href') - if href is not None and href: - if target_href in href: - perfect_fit = True - if isinstance(attributes, dict): - for key, target_value in attributes.iteritems(): - attr_check = link.get_attribute(key) - if attr_check is not None and attr_check: - if target_value not in attr_check: - perfect_fit = False - if perfect_fit: - return link - except: - raise Exception( - 'Selenium cannot find the href=%s link' % target_href) - - def click_on_link(self, link): - """ - There are situations when selenium built in click() function - doesn't work as intended, that's when this function is used. - Fires a click event via javascript injection. - """ - try: - # Javascript function to simulate a click on a link - javascript = ( - "var link = arguments[0];" - "var cancelled = false;" - "if(document.createEvent) {" - " var event = document.createEvent(\"MouseEvents\");" - " event.initMouseEvent(" - " \"click\", true, true, window, 0, 0, 0, 0, 0," - " false,false,false,false,0,null);" - " cancelled = !link.dispatchEvent(event);" - "} else if(link.fireEvent) {" - " cancelled = !link.fireEvent(\"onclick\");" - "} if (!cancelled) {" - " window.location = link.href;" - "}") - self.driver.execute_script(javascript, link) - except: - raise Exception( - 'Selenium cannot inject javascript to the page') - - def wait_and_accept_operation(self, argument=None): - """ - Accepts the operation confirmation pop up window. - Fills out the text inputs before accepting if argument is given. - """ - try: - accept = WebDriverWait(self.driver, wait_max_sec).until( - ec.element_to_be_clickable(( - By.CLASS_NAME, "modal-accept"))) - if argument is not None: - possible = self.driver.find_elements_by_css_selector( - "div.controls > input[type='text']") - if isinstance(argument, list): - for x in range(0, len(possible)): - possible[x].clear() - possible[x].send_keys(argument[x % len(argument)]) - else: - for form in possible: - form.clear() - form.send_keys(argument) - accept.click() - except: - raise Exception("Selenium cannot accept the" - " operation confirmation") - - def save_template_from_vm(self, name): - try: - WebDriverWait(self.driver, wait_max_sec).until( - ec.element_to_be_clickable(( - By.CSS_SELECTOR, - "a[href$='/op/deploy/']"))) - self.click_on_link(self.get_link_by_href("/op/deploy/")) - self.wait_and_accept_operation() - recent_deploy = self.recently(self.get_timeline_elements( - "vm.Instance.deploy")) - if not self.check_operation_result(recent_deploy): - print ("Selenium cannot deploy the " - "chosen template virtual machine") - raise Exception('Cannot deploy the virtual machine') - self.click_on_link(WebDriverWait(self.driver, wait_max_sec).until( - ec.element_to_be_clickable(( - By.CSS_SELECTOR, - "a[href$='/op/shut_off/']")))) - self.wait_and_accept_operation() - recent_shut_off = self.recently(self.get_timeline_elements( - "vm.Instance.shut_off")) - if not self.check_operation_result(recent_shut_off): - print ("Selenium cannot shut off the " - "chosen template virtual machine") - raise Exception('Cannot shut off the virtual machine') - self.click_on_link(WebDriverWait(self.driver, wait_max_sec).until( - ec.element_to_be_clickable(( - By.CSS_SELECTOR, - "a[href$='/op/save_as_template/']")))) - self.wait_and_accept_operation(name) - return name - except: - raise Exception( - 'Selenium cannot save a vm as a template') - - def create_base_template(self, name=None, architecture="x86-64", - method=None, op_system=None, lease=None, - network="vm"): - if name is None: - name = "template_new_%s" % client_name - if op_system is None: - op_system = "!os %s" % client_name - try: - self.driver.get('%s/dashboard/template/choose/' % host) - self.driver.find_element_by_css_selector( - "input[type='radio'][value='base_vm']").click() - self.driver.find_element_by_id( - "template-choose-next-button").click() - template_name = WebDriverWait(self.driver, wait_max_sec).until( - ec.visibility_of_element_located(( - By.ID, 'id_name'))) - template_name.clear() - template_name.send_keys(name) - self.select_option(self.driver.find_element_by_id( - "id_arch"), architecture) - self.select_option(self.driver.find_element_by_id( - "id_access_method"), method) - system_name = self.driver.find_element_by_id("id_system") - system_name.clear() - system_name.send_keys(op_system) - self.select_option(self.driver.find_element_by_id( - "id_lease"), lease) - self.select_option(self.driver.find_element_by_id( - "id_networks"), network) - self.driver.find_element_by_css_selector( - "input.btn[type='submit']").click() - return self.save_template_from_vm(name) - except: - raise Exception( - 'Selenium cannot create a base template virtual machine') - - def get_template_id(self, name=None, from_all=False): - """ - In default settings find all templates ID in the template list. - If name is specified searches that specific template's ID - from_all sets whether to use owned templates or all of them - Returns list of the templates ID - """ - try: - self.driver.get('%s/dashboard/template/list/' % host) - css_selector_of_a_template = ("a[data-original-title]" - "[href*='/dashboard/template/']") - if from_all: - self.select_option(self.driver.find_element_by_id( - 'id_stype'), "all") - self.driver.find_element_by_css_selector( - "button[type='submit']").click() - try: - WebDriverWait(self.driver, wait_max_sec).until( - ec.presence_of_element_located(( - By.CSS_SELECTOR, css_selector_of_a_template))) - except: - print "Selenium could not locate any templates" - template_table = self.driver.find_element_by_css_selector( - "table[class*='template-list-table']") - templates = template_table.find_elements_by_css_selector("td.name") - found_template_ids = [] - for template in templates: - if name is None or name in template.text: - try: - template_link = template.find_element_by_css_selector( - css_selector_of_a_template) - template_id = re.search( - r'\d+', - template_link.get_attribute("outerHTML")).group() - found_template_ids.append(template_id) - print ("Found '%(name)s' template's ID as %(id)s" % { - 'name': template.text, - 'id': template_id}) - except NoSuchElementException: - pass - except: - raise - if not found_template_ids and name is not None: - print ("Selenium could not find the specified " - "%(name)s template in the list" % { - 'name': name}) - return found_template_ids - except: - raise Exception( - 'Selenium cannot found the template\'s id') - - def check_operation_result(self, operation_id, restore=True): - """ - Returns wheter the operation_id result is success (returns: boolean) - """ - try: - if restore: - url_base = urlparse.urlparse(self.driver.current_url) - url_save = ("%(host)s%(url)s" % { - 'host': host, - 'url': urlparse.urljoin(url_base.path, url_base.query)}) - if url_base.fragment: - url_save = ("%(url)s#%(fragment)s" % { - 'url': url_save, - 'fragment': url_base.fragment}) - self.driver.get('%(host)s/dashboard/vm/activity/%(id)s/' % { - 'host': host, - 'id': operation_id}) - result = WebDriverWait(self.driver, wait_max_sec).until( - ec.visibility_of_element_located(( - By.ID, "activity_status"))) - print ("%(id)s result text is '%(result)s'" % { - 'id': operation_id, - 'result': result.text}) - if (result.text == "success"): - out = True - elif (result.text == "wait"): - time.sleep(2) - out = self.check_operation_result(operation_id, False) - else: - out = False - if restore: - print "Restoring to %s url" % url_save - self.driver.get(url_save) - return out - except: - raise Exception( - 'Selenium cannot check the result of an operation') - - def recently(self, timeline_dict, second=90): - try: - if isinstance(timeline_dict, dict): - for key, value in timeline_dict.iteritems(): - time = datetime.strptime(key, '%Y-%m-%d %H:%M') - delta = datetime.now() - time - if delta.total_seconds() <= second: - return value - except: - raise Exception( - 'Selenium cannot filter timeline activities to recent') - - def get_timeline_elements(self, code=None): - try: - if code is None: - css_activity_selector = "div[data-activity-code]" - code = "all activity" - else: - css_activity_selector = ("div[data-activity-code=" - "'%(code)s']" % { - 'code': code}) - WebDriverWait(self.driver, wait_max_sec).until( - ec.element_to_be_clickable(( - By.CSS_SELECTOR, "a[href*='#activity']"))).click() - activity_dict = {} - timeline = WebDriverWait(self.driver, wait_max_sec).until( - ec.visibility_of_element_located(( - By.ID, "activity-timeline"))) - searched_activity = timeline.find_elements_by_css_selector( - css_activity_selector) - print "Found activity list for %s:" % code - for activity in searched_activity: - activity_id = activity.get_attribute('data-activity-id') - activity_text = activity.text - key = re.search( - r'\d+-\d+-\d+ \d+:\d+,', activity_text).group()[:-1] - print ("%(id)s @ %(activity)s" % { - 'id': activity_id, - 'activity': key}) - activity_dict[key] = activity_id - return activity_dict - except: - raise Exception('Selenium cannot find the searched activity') - - def create_template_from_base(self, delete_disk=True, name=None): - try: - if name is None: - name = "template_from_base_%s" % client_name - self.driver.get('%s/dashboard/template/choose/' % host) - choice_list = [] - choices = self.driver.find_elements_by_css_selector( - "input[type='radio']") - choice_list = [item for item in choices if ( - 'test' not in item.get_attribute('value') - and item.get_attribute('value') != 'base_vm')] - chosen = random.randint(0, len(choice_list) - 1) - choice_list[chosen].click() - self.driver.find_element_by_id( - "template-choose-next-button").click() - if delete_disk: - self.click_on_link( - self.get_link_by_href("#resources")) - disks = WebDriverWait(self.driver, wait_max_sec).until( - ec.visibility_of_element_located(( - By.ID, 'vm-details-resources-disk'))) - disk_list = disks.find_elements_by_css_selector( - "h4[class*='list-group-item-heading']") - if len(disk_list) > 0: - self.click_on_link( - self.get_link_by_href("/op/remove_disk/")) - self.wait_and_accept_operation() - WebDriverWait(self.driver, wait_max_sec).until( - ec.visibility_of_element_located(( - By.ID, "_activity"))) - recent_remove_disk = self.recently( - self.get_timeline_elements( - "vm.Instance.remove_disk")) - if not self.check_operation_result(recent_remove_disk): - print ("Selenium cannot delete disk " - "of the chosen template") - raise Exception('Cannot delete disk') - return self.save_template_from_vm(name) - except: - raise Exception('Selenium cannot start a template from a base one') +import urlparse - def delete_template(self, template_id): - try: - self.driver.get('%s/dashboard/template/%s/' % (host, template_id)) - url = urlparse.urlparse(self.driver.current_url) - self.click_on_link( - self.get_link_by_href( - "/dashboard/template/delete/%s/" % template_id)) - self.wait_and_accept_operation() - WebDriverWait(self.driver, wait_max_sec).until( - ec.visibility_of_element_located(( - By.CLASS_NAME, 'alert-success'))) - url = urlparse.urlparse(self.driver.current_url) - if "/template/list/" not in url.path: - raise Exception() - except: - raise Exception('Selenium cannot delete the desired template') +from django.contrib.auth.models import User +from django.db.models import Q - def create_random_vm(self): - try: - self.driver.get('%s/dashboard/vm/create/' % host) - vm_list = [] - pk = None - vm_list = self.driver.find_elements_by_class_name( - 'vm-create-template-summary') - choice = random.randint(0, len(vm_list) - 1) - vm_list[choice].click() - WebDriverWait(self.driver, wait_max_sec).until( - ec.element_to_be_clickable(( - By.CLASS_NAME, 'vm-create-start'))).click() - WebDriverWait(self.driver, wait_max_sec).until( - ec.visibility_of_element_located(( - By.CLASS_NAME, 'alert-success'))) - url = urlparse.urlparse(self.driver.current_url) - pk = re.search(r'\d+', url.path).group() - return pk - except: - raise Exception('Selenium cannot start a VM') +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as ec +from selenium.webdriver.support.ui import WebDriverWait +from selenose.cases import SeleniumTestCase - def view_change(self, target_box): - driver = self.driver - driver.get('%s/dashboard/' % host) - list_view = driver.find_element_by_id('%s-list-view' % target_box) - graph_view = driver.find_element_by_id('%s-graph-view' % target_box) - js_script = 'return arguments[0].style.display;' - required_attributes = {'data-index-box': target_box} - graph_view_link = self.get_link_by_href( - '#index-graph-view', - required_attributes).find_element_by_tag_name('i') - list_view_link = self.get_link_by_href( - '#index-list-view', - required_attributes).find_element_by_tag_name('i') - self.click_on_link(list_view_link) - states = [driver.execute_script(js_script, list_view), - driver.execute_script(js_script, graph_view)] - self.click_on_link(graph_view_link) - states.extend([driver.execute_script(js_script, list_view), - driver.execute_script(js_script, graph_view)]) - self.click_on_link(list_view_link) - states.extend([driver.execute_script(js_script, list_view), - driver.execute_script(js_script, graph_view)]) - return states +from vm.models import Instance +from .config import SeleniumConfig +from .util import CircleSeleniumMixin - def delete_vm(self, pk): - try: - # For relability reasons instead of using the JS operatation - self.driver.get("%(host)s/dashboard/vm/%(id)s/op/destroy/" % { - 'host': host, - 'id': pk}) - self.wait_and_accept_operation() - try: - status_span = WebDriverWait(self.driver, wait_max_sec).until( - ec.visibility_of_element_located(( - By.ID, 'vm-details-state'))) - WebDriverWait(status_span, wait_max_sec).until( - ec.visibility_of_element_located(( - By.CLASS_NAME, 'fa-trash-o'))) - except: - # Selenium can time-out by not realising the JS refresh - recent_destroy_vm = self.recently( - self.get_timeline_elements("vm.Instance.destroy")) - if not self.check_operation_result(recent_destroy_vm): - print ("Selenium cannot destroy " - "the chosen %(id)s vm" % { - 'id': pk}) - raise Exception('Cannot destroy the specified vm') - self.driver.get('%s/dashboard/vm/%s/' % (host, pk)) - try: - WebDriverWait(self.driver, wait_max_sec).until( - ec.visibility_of_element_located(( - By.CSS_SELECTOR, - "span[data-status*='DESTROYED']"))) - return True - except: - return False - except: - raise Exception("Selenium can not destroy a VM") +conf = SeleniumConfig() +log_formatter = logging.Formatter(conf.log_format) +logger = logging.getLogger(conf.logger_name) +fileHandler = logging.handlers.RotatingFileHandler( + conf.log_file, maxBytes=conf.log_size, backupCount=conf.log_backup) +fileHandler.setFormatter(log_formatter) +fileHandler.setLevel(logging.WARNING) +logger.addHandler(fileHandler) -class VmDetailTest(UtilityMixin, SeleniumTestCase): +class BasicSeleniumTests(SeleniumTestCase, CircleSeleniumMixin): template_ids = [] vm_ids = [] + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self.conf = conf + @classmethod def setup_class(cls): - cls._user = User.objects.create(username=client_name, - is_superuser=True) - cls._user.set_password(random_accents) - cls._user.save() + logger.warning("Selenium test started @ %(time)s" % { + 'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')}) + if conf.create_user: + logger.warning( + "Creating selenium test user %(name)s:%(password)s" % { + 'name': conf.client_name, + 'password': conf.random_pass}) + cls._user = User.objects.create(username=conf.client_name, + is_superuser=True) + cls._user.set_password(conf.random_pass) + cls._user.save() @classmethod def teardown_class(cls): - cls._user.delete() + if conf.create_user: + for instance in Instance.objects.all().filter( + ~Q(status=u'DESTROYED'), owner=cls._user): + logger.warning( + "Destroying the test virtual machine: %(id)s" % { + 'id': instance.pk}) + instance.destroy(system=True) + logger.warning("Deleting test user %(name)s" % { + 'name': conf.client_name}) + cls._user.delete() + logger.warning("Selenium test finished @ %(time)s" % { + 'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')}) def test_01_login(self): + logger.warning("Starting test %s" % _getframe().f_code.co_name) title = 'Dashboard | CIRCLE' location = '/dashboard/' - self.login(client_name, random_accents) - self.driver.get('%s/dashboard/' % host) + self.login() + self.driver.get('%s/dashboard/' % conf.host) url = urlparse.urlparse(self.driver.current_url) (self.assertIn('%s' % title, self.driver.title, '%s is not found in the title' % title) or @@ -563,62 +95,69 @@ class VmDetailTest(UtilityMixin, SeleniumTestCase): 'URL path is not equal with %s' % location)) def test_02_add_template_rights(self): - self.login(client_name, random_accents) + logger.warning("Starting test %s" % _getframe().f_code.co_name) + self.login() template_pool = self.get_template_id(from_all=True) if len(template_pool) > 1: chosen = template_pool[random.randint(0, len(template_pool) - 1)] elif len(template_pool) == 1: chosen = template_pool[0] else: - print "Selenium did not found any templates" + logger.exception("Selenium did not found any templates") raise Exception( - "System did not meet required conditions to continue") - self.driver.get('%s/dashboard/template/%s/' % (host, chosen)) - acces_form = self.driver.find_element_by_css_selector( + "Selenium did not found any templates") + self.driver.get('%s/dashboard/template/%s/' % (conf.host, chosen)) + acces_form_css = ( "form[action*='/dashboard/template/%(template_id)s/acl/']" "[method='post']" % { 'template_id': chosen}) + acces_form = self.driver.find_element_by_css_selector(acces_form_css) user_name = acces_form.find_element_by_css_selector( "input[type='text'][id='id_name']") user_status = acces_form.find_element_by_css_selector( "select[name='level']") user_name.clear() - user_name.send_keys(client_name) - self.select_option(user_status) + user_name.send_keys(conf.client_name) + self.select_option(user_status, 'user') # For strange reasons clicking on submit button doesn't work anymore acces_form.submit() found_users = [] - acl_users = self.driver.find_elements_by_css_selector( + acces_form = self.driver.find_element_by_css_selector(acces_form_css) + acl_users = acces_form.find_elements_by_css_selector( "a[href*='/dashboard/profile/']") for user in acl_users: user_text = re.split(r':[ ]?', user.text) if len(user_text) == 2: found_name = re.search(r'[\w\W]+(?=\))', user_text[1]).group() - print ("'%(user)s' found in ACL list for template %(id)s" % { - 'user': found_name, - 'id': chosen}) + logger.warning("'%(user)s' found in ACL " + "list for template %(id)s" % { + 'user': found_name, + 'id': chosen}) found_users.append(found_name) - self.assertIn(client_name, found_users, + self.assertIn(conf.client_name, found_users, "Could not add user to template's ACL") def test_03_able_to_create_template(self): - self.login(client_name, random_accents) + logger.warning("Starting test %s" % _getframe().f_code.co_name) + self.login() template_list = None create_template = self.get_link_by_href('/dashboard/template/choose/') self.click_on_link(create_template) - WebDriverWait(self.driver, wait_max_sec).until( + WebDriverWait(self.driver, conf.wait_max_sec).until( ec.visibility_of_element_located(( By.ID, 'confirmation-modal'))) template_list = self.driver.find_elements_by_class_name( 'template-choose-list-element') - print 'Selenium found %s template possibilities' % len(template_list) + logger.warning('Selenium found %(count)s template possibilities' % { + 'count': len(template_list)}) (self.assertIsNotNone( template_list, "Selenium can not find the create template list") or self.assertGreater(len(template_list), 0, "The create template list is empty")) def test_04_create_base_template(self): - self.login(client_name, random_accents) + logger.warning("Starting test %s" % _getframe().f_code.co_name) + self.login() created_template_id = self.get_template_id( self.create_base_template()) found = created_template_id is not None @@ -629,7 +168,8 @@ class VmDetailTest(UtilityMixin, SeleniumTestCase): "Could not found the created template in the template list") def test_05_create_template_from_base(self): - self.login(client_name, random_accents) + logger.warning("Starting test %s" % _getframe().f_code.co_name) + self.login() created_template_id = self.get_template_id( self.create_template_from_base()) found = created_template_id is not None @@ -640,10 +180,11 @@ class VmDetailTest(UtilityMixin, SeleniumTestCase): "Could not found the created template in the template list") def test_06_delete_templates(self): + logger.warning("Starting test %s" % _getframe().f_code.co_name) success = False - self.login(client_name, random_accents) + self.login() for template_id in self.template_ids: - print "Deleting template %s" % template_id + logger.warning("Deleting template %s" % template_id) self.delete_template(template_id) existing_templates = self.get_template_id() if len(existing_templates) == 0: @@ -658,52 +199,57 @@ class VmDetailTest(UtilityMixin, SeleniumTestCase): success, "Could not delete (all) the test template(s)") def test_07_able_to_create_vm(self): - self.login(client_name, random_accents) + logger.warning("Starting test %s" % _getframe().f_code.co_name) + self.login() vm_list = None create_vm_link = self.get_link_by_href('/dashboard/vm/create/') create_vm_link.click() - WebDriverWait(self.driver, wait_max_sec).until( + WebDriverWait(self.driver, conf.wait_max_sec).until( ec.visibility_of_element_located(( By.ID, 'confirmation-modal'))) vm_list = self.driver.find_elements_by_class_name( 'vm-create-template-summary') - print ("Selenium found %(vm_number)s virtual machine template " - " possibilities" % { - 'vm_number': len(vm_list)}) + logger.warning("Selenium found %(vm_number)s virtual machine" + " template possibilities" % { + 'vm_number': len(vm_list)}) (self.assertIsNotNone( vm_list, "Selenium can not find the VM list") or self.assertGreater(len(vm_list), 0, "The create VM list is empty")) def test_08_create_vm(self): - self.login(client_name, random_accents) + logger.warning("Starting test %s" % _getframe().f_code.co_name) + self.login() pk = self.create_random_vm() self.vm_ids.append(pk) self.assertIsNotNone(pk, "Can not create a VM") def test_09_vm_view_change(self): - self.login(client_name, random_accents) + logger.warning("Starting test %s" % _getframe().f_code.co_name) + self.login() expected_states = ["", "none", "none", "", "block", "none"] states = self.view_change("vm") - print 'states: [%s]' % ', '.join(map(str, states)) - print 'expected: [%s]' % ', '.join(map(str, expected_states)) + logger.warning('states: [%s]' % ', '.join(map(str, states))) + logger.warning('expected: [%s]' % ', '.join(map(str, expected_states))) self.assertListEqual(states, expected_states, "The view mode does not change for VM listing") def test_10_node_view_change(self): - self.login(client_name, random_accents) + logger.warning("Starting test %s" % _getframe().f_code.co_name) + self.login() expected_states = ["", "none", "none", "", "block", "none"] states = self.view_change("node") - print 'states: [%s]' % ', '.join(map(str, states)) - print 'expected: [%s]' % ', '.join(map(str, expected_states)) + logger.warning('states: [%s]' % ', '.join(map(str, states))) + logger.warning('expected: [%s]' % ', '.join(map(str, expected_states))) self.assertListEqual(states, expected_states, "The view mode does not change for NODE listing") def test_11_delete_vm(self): - self.login(client_name, random_accents) + logger.warning("Starting test %s" % _getframe().f_code.co_name) + self.login() succes = True for vm in self.vm_ids: if not self.delete_vm(vm): diff --git a/circle/dashboard/tests/selenium/config.py b/circle/dashboard/tests/selenium/config.py new file mode 100644 index 0000000..3c5489f --- /dev/null +++ b/circle/dashboard/tests/selenium/config.py @@ -0,0 +1,65 @@ +#!/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" diff --git a/circle/dashboard/tests/selenium/util.py b/circle/dashboard/tests/selenium/util.py new file mode 100644 index 0000000..ed1b1e4 --- /dev/null +++ b/circle/dashboard/tests/selenium/util.py @@ -0,0 +1,763 @@ +#!/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/>. +from datetime import datetime +import inspect +import logging +import random +import re +import time +import urlparse + +from selenium.common.exceptions import ( + NoSuchElementException, StaleElementReferenceException, + TimeoutException) +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as ec +from selenium.webdriver.support.select import Select +from selenium.webdriver.support.ui import WebDriverWait + +from .config import SeleniumConfig + +logger = logging.getLogger(SeleniumConfig.logger_name) + + +class SeleniumMixin(object): + def create_screenshot(self): + name = 'ss_from_%(caller_name)s.png' % { + 'caller_name': inspect.stack()[1][3]} + logger.warning('Creating screenshot "%s"' % name) + self.driver.save_screenshot(name) + + def get_url(self, fragment_needed=False, fragment=None): + url_base = urlparse.urlparse(self.driver.current_url) + url_save = ("%(host)s%(url)s" % { + 'host': self.conf.host, + 'url': urlparse.urljoin(url_base.path, url_base.query)}) + if fragment is None: + fragment = url_base.fragment + else: + fragment_needed = True + if fragment_needed and fragment: + url_save = ("%(url)s#%(fragment)s" % { + 'url': url_save, + 'fragment': fragment}) + return url_save + + def list_options(self, select): + try: + option_dic = {} + select = Select(select) + for option in select.options: + key = option.get_attribute('value') + if key is not None and key: + option_dic[key] = [option.text] + return option_dic + except: + logger.exception("Selenium cannot list the" + " select possibilities") + self.create_screenshot() + raise Exception( + 'Cannot list the select possibilities') + + def select_option(self, select, what=None): + """ + From an HTML select imput type try to choose the specified one. + Select is a selenium web element type. What represent both the + text of the option and it's ID. + """ + try: + my_choice = None + options = self.list_options(select) + select = Select(select) + if what is not None: + for key, value in options.iteritems(): + if what in key: + my_choice = key + else: + if isinstance(value, list): + for single_value in value: + if what in single_value: + my_choice = key + else: + if what in value: + my_choice = key + if my_choice is None: + my_choose_list = options.keys() + my_choice = my_choose_list[random.randint( + 0, len(my_choose_list) - 1)] + select.select_by_value(my_choice) + except: + logger.exception("Selenium cannot select the chosen one") + self.create_screenshot() + raise Exception( + 'Cannot select the chosen one') + + def get_link_by_href(self, target_href, attributes=None): + try: + links = self.driver.find_elements_by_tag_name('a') + for link in links: + href = link.get_attribute('href') + if href is not None and href: + if target_href in href: + perfect_fit = True + if isinstance(attributes, dict): + for key, target_value in attributes.iteritems(): + attr_check = link.get_attribute(key) + if attr_check is not None and attr_check: + if target_value not in attr_check: + perfect_fit = False + if perfect_fit: + return link + except: + logger.exception( + "Selenium cannot find the href=%s link" % target_href) + self.create_screenshot() + raise Exception('Cannot find the requested href') + + def click_on_link(self, link): + """ + There are situations when selenium built in click() function + doesn't work as intended, that's when this function is used. + Fires a click event via javascript injection. + """ + try: + # Javascript function to simulate a click on a link + javascript = """ + var link = arguments[0]; + var cancelled = false; + if(document.createEvent) { + var event = document.createEvent("MouseEvents"); + event.initMouseEvent( + "click", true, true, window, 0, 0, 0, 0, 0, + false,false,false,false,0,null); + cancelled = !link.dispatchEvent(event); + } else if(link.fireEvent) { + cancelled = !link.fireEvent("onclick"); + } if (!cancelled) { + window.location = link.href; + }""" + self.driver.execute_script(javascript, link) + except: + logger.exception("Selenium cannot inject javascript to the page") + self.create_screenshot() + raise Exception( + 'Cannot inject javascript to the page') + + def get_text(self, node, tag): + """ + There are some cases where selenium default WebElement text() + method returns less then it actually could contain. Solving that + here is a simple regular expression. Give the closest html element + then specify the html tag of the enclosed text. + """ + text = "" + try: + text_whole = re.search( + r'<%(tag)s[^>]*>([^<]+)</%(tag)s>' % { + 'tag': tag}, + node.get_attribute("outerHTML")).group() + text_parts = text_whole.splitlines() + for part in text_parts: + if '<' not in part and '>' not in part: + text += part + text = text.replace(" ", "") + except: + return node.text + if len(node.text) >= len(text): + text = node.text + else: + logger.warning("Better text found which is '%s'" % text) + return text.strip() + + +class CircleSeleniumMixin(SeleniumMixin): + def login(self, location=None): + driver = self.driver + if location is None: + location = '/dashboard/' + driver.get('%s%s' % (self.conf.host, location)) + # Only if we aren't logged in already + if location not in urlparse.urlparse(self.driver.current_url).path: + try: + name_input = driver.find_element_by_id("id_username") + password_input = driver.find_element_by_id("id_password") + submit_input = driver.find_element_by_id("submit-id-submit") + except: + inputs = driver.find_elements_by_tag_name("input") + for current_input in inputs: + input_type = current_input.get_attribute("type") + if input_type == "text": + name_input = current_input + if input_type == "password": + password_input = current_input + if input_type == "submit": + submit_input = current_input + try: + name_input.clear() + name_input.send_keys(self.conf.client_name) + password_input.clear() + password_input.send_keys(self.conf.random_pass) + submit_input.click() + try: + # If selenium runs only in a small (virtual) screen + driver.find_element_by_class_name('navbar-toggle').click() + WebDriverWait(self.driver, self.conf.wait_max_sec).until( + ec.element_to_be_clickable(( + By.CSS_SELECTOR, + "a[href*='/dashboard/profile/']"))) + except: + time.sleep(0.5) + except: + logger.exception("Selenium cannot find the form controls") + self.create_screenshot() + raise Exception('Cannot find the form controls') + + def fallback(self, fallback_url, fallback_function): + logger.warning( + "However error was anticipated falling back to %(url)s" % { + 'url': fallback_url}) + self.driver.get(fallback_url) + return fallback_function() + + def wait_and_accept_operation(self, argument=None, try_wait=None, + fallback_url=None): + """ + Accepts the operation confirmation pop up window. + Fills out the text inputs before accepting if argument is given. + """ + try: + accept = WebDriverWait(self.driver, self.conf.wait_max_sec).until( + ec.element_to_be_clickable(( + By.CLASS_NAME, "modal-accept"))) + if argument is not None: + possible = self.driver.find_elements_by_css_selector( + "div.controls > input[type='text']") + if isinstance(argument, list): + for x in range(0, len(possible)): + possible[x].clear() + possible[x].send_keys(argument[x % len(argument)]) + else: + for form in possible: + form.clear() + form.send_keys(argument) + accept.click() + if try_wait is not None: + WebDriverWait(self.driver, self.conf.wait_max_sec).until( + ec.visibility_of_element_located(( + By.CSS_SELECTOR, try_wait))) + except TimeoutException: + logger.exception("Selenium cannot accept the" + " operation confirmation") + if fallback_url is not None: + self.fallback( + fallback_url, + lambda: self.wait_and_accept_operation(argument)) + else: + self.create_screenshot() + raise Exception( + 'Cannot accept the operation confirmation') + except: + logger.exception("Selenium cannot accept the" + " operation confirmation") + if fallback_url is not None: + self.fallback( + fallback_url, + lambda: self.wait_and_accept_operation(argument, try_wait)) + else: + self.create_screenshot() + raise Exception( + 'Cannot accept the operation confirmation') + + def save_template_from_vm(self, name): + try: + WebDriverWait(self.driver, self.conf.wait_max_sec).until( + ec.element_to_be_clickable(( + By.CSS_SELECTOR, + "a[href$='/op/deploy/']"))) + url_save = self.get_url() + self.click_on_link(self.get_link_by_href("/op/deploy/")) + fallback_url = "%sop/deploy/" % url_save + self.wait_and_accept_operation( + try_wait="a[href$='/op/shut_off/']", fallback_url=fallback_url) + recent_deploy = self.recently(self.get_timeline_elements( + "vm.Instance.deploy", url_save)) + if not self.check_operation_result( + recent_deploy, "a[href*='#activity']"): + logger.warning("Selenium cannot deploy the " + "chosen template virtual machine") + raise Exception('Cannot deploy the virtual machine') + self.click_on_link(WebDriverWait( + self.driver, self.conf.wait_max_sec).until( + ec.element_to_be_clickable(( + By.CSS_SELECTOR, + "a[href$='/op/shut_off/']")))) + fallback_url = "%sop/shut_off/" % url_save + self.wait_and_accept_operation( + try_wait="a[href$='/op/deploy/']", fallback_url=fallback_url) + recent_shut_off = self.recently(self.get_timeline_elements( + "vm.Instance.shut_off", url_save)) + if not self.check_operation_result( + recent_shut_off, "a[href*='#activity']"): + logger.warning("Selenium cannot shut off the " + "chosen template virtual machine") + raise Exception('Cannot shut off the virtual machine') + self.click_on_link(WebDriverWait( + self.driver, self.conf.wait_max_sec).until( + ec.element_to_be_clickable(( + By.CSS_SELECTOR, + "a[href$='/op/save_as_template/']")))) + fallback_url = "%sop/save_as_template/" % url_save + self.wait_and_accept_operation( + argument=name, fallback_url=fallback_url) + recent_save_template = self.recently(self.get_timeline_elements( + "vm.Instance.save_as_template", url_save)) + if not self.check_operation_result( + recent_save_template, "a[href*='#activity']"): + logger.warning("Selenium cannot save the " + "chosen virtual machine as a template") + raise Exception( + 'Cannot save the virtual machine as a template') + logger.warning("Selenium created %(name)s template" % { + 'name': name}) + return name + except: + logger.exception("Selenium cannot save a vm as a template") + self.create_screenshot() + raise Exception( + 'Cannot save a vm as a template') + + def create_base_template(self, name=None, architecture="x86-64", + method=None, op_system=None, lease=None, + network="vm"): + if name is None: + name = "new_%s" % self.conf.client_name + if op_system is None: + op_system = "!os %s" % self.conf.client_name + try: + self.driver.get('%s/dashboard/template/choose/' % self.conf.host) + self.driver.find_element_by_css_selector( + "input[type='radio'][value='base_vm']").click() + self.driver.find_element_by_id( + "template-choose-next-button").click() + template_name = WebDriverWait( + self.driver, self.conf.wait_max_sec).until( + ec.visibility_of_element_located(( + By.ID, 'id_name'))) + template_name.clear() + template_name.send_keys(name) + + self.select_option(self.driver.find_element_by_id( + "id_arch"), architecture) + self.select_option(self.driver.find_element_by_id( + "id_access_method"), method) + system_name = self.driver.find_element_by_id("id_system") + system_name.clear() + system_name.send_keys(op_system) + self.select_option(self.driver.find_element_by_id( + "id_lease"), lease) + self.select_option(self.driver.find_element_by_id( + "id_networks"), network) + self.driver.find_element_by_css_selector( + "input.btn[type='submit']").click() + return self.save_template_from_vm(name) + except: + logger.exception("Selenium cannot create a base" + " template virtual machine") + self.create_screenshot() + raise Exception( + 'Cannot create a base template virtual machine') + + def get_template_id(self, name=None, from_all=False): + """ + In default settings find all templates ID in the template list. + If name is specified searches that specific template's ID + from_all sets whether to use owned templates or all of them + Returns list of the templates ID + """ + try: + self.driver.get('%s/dashboard/template/list/' % self.conf.host) + css_selector_of_a_template = ("a[data-original-title]" + "[href*='/dashboard/template/']") + if from_all: + self.select_option(self.driver.find_element_by_id( + 'id_stype'), "all") + self.driver.find_element_by_css_selector( + "button[type='submit']").click() + try: + WebDriverWait(self.driver, self.conf.wait_max_sec).until( + ec.presence_of_element_located(( + By.CSS_SELECTOR, css_selector_of_a_template))) + except: + logger.warning("Selenium could not locate any templates") + raise Exception("Could not locate any templates") + template_table = self.driver.find_element_by_css_selector( + "table[class*='template-list-table']") + templates = template_table.find_elements_by_css_selector("td.name") + found_template_ids = [] + for template in templates: + # Little magic to outsmart accented naming errors + template_name = self.get_text(template, "a") + if name is None or name in template_name: + try: + template_link = template.find_element_by_css_selector( + css_selector_of_a_template) + template_id = re.search( + r'\d+', + template_link.get_attribute("outerHTML")).group() + found_template_ids.append(template_id) + logger.warning("Found '%(name)s' " + "template's ID as %(id)s" % { + 'name': template_name, + 'id': template_id}) + except NoSuchElementException: + pass + except: + raise + else: + logger.warning( + "Searching for %(searched)s so" + " %(name)s is dismissed" % { + 'searched': name, + 'name': template_name}) + logger.warning( + "Dismissed template html code: %(code)s" % { + 'code': template.get_attribute("outerHTML")}) + if not found_template_ids and name is not None: + logger.warning("Selenium could not find the specified " + "%(name)s template in the list" % { + 'name': name}) + raise Exception("Could not find the specified template") + return found_template_ids + except: + logger.exception('Selenium cannot find the template\'s id') + self.create_screenshot() + raise Exception( + 'Cannot find the template\'s id') + + def check_operation_result(self, operation_id, restore_selector=None, + restore=True): + """ + Returns wheter the operation_id result is success (returns: boolean) + """ + try: + if restore: + url_save = self.get_url(True) + self.driver.get('%(host)s/dashboard/vm/activity/%(id)s/' % { + 'host': self.conf.host, + 'id': operation_id}) + result = WebDriverWait(self.driver, self.conf.wait_max_sec).until( + ec.visibility_of_element_located(( + By.ID, "activity_status"))) + logger.warning("%(id)s's result is '%(result)s'" % { + 'id': operation_id, + 'result': result.text}) + if (result.text == "success"): + out = True + elif (result.text == "wait"): + time.sleep(2) + out = self.check_operation_result( + operation_id=operation_id, restore=False) + else: + try: + result_text = WebDriverWait( + self.driver, self.conf.wait_max_sec).until( + ec.visibility_of_element_located(( + By.ID, "activity_result_text"))) + logger.warning( + "%(id)s's result text is: '%(result_text)s'" % { + 'id': operation_id, + 'result_text': result_text.text}) + except: + logger.warning("Cannot read %(id)s's result text" % { + 'id': operation_id}) + out = False + if restore: + logger.warning("Restoring to %s url" % url_save) + self.driver.get(url_save) + if restore_selector is not None and restore_selector: + WebDriverWait(self.driver, self.conf.wait_max_sec).until( + ec.visibility_of_element_located(( + By.CSS_SELECTOR, restore_selector))) + return out + except: + logger.exception("Selenium cannot check the" + " result of an operation") + self.create_screenshot() + raise Exception( + 'Cannot check the result of an operation') + + def recently(self, timeline_dict, second=None): + if second is None: + second = self.conf.recently_sec + try: + if isinstance(timeline_dict, dict): + recent = None + for key, value in timeline_dict.iteritems(): + if recent is None or int(key) > int(recent): + recent = key + if len(timeline_dict) > 1: + logger.warning( + "Searching for most recent activity" + " from the received %(count)s pieces" % { + 'count': len(timeline_dict)}) + logger.warning("Found at %(id)s @ %(time)s" % { + 'id': timeline_dict[recent], + 'time': datetime.fromtimestamp( + int(recent)).strftime('%Y-%m-%d %H:%M:%S')}) + logger.warning( + "Checking wheter %(id)s started in the" + " recent %(second)s seconds" % { + 'id': timeline_dict[recent], + 'second': second}) + delta = datetime.now() - datetime.fromtimestamp(int(recent)) + if delta.total_seconds() <= second: + return timeline_dict[recent] + except: + logger.exception("Selenium cannot filter timeline " + "activities to find most recent") + self.create_screenshot() + raise Exception( + 'Cannot filter timeline activities to find most recent') + + def get_timeline_elements(self, code=None, fallback_url=None): + try: + if code is None: + css_activity_selector = "div[data-activity-code]" + code_text = "all activity" + else: + code_text = code + css_activity_selector = ("div[data-activity-code=" + "'%(code)s']" % { + 'code': code}) + try: + self.click_on_link(WebDriverWait( + self.driver, self.conf.wait_max_sec).until( + ec.element_to_be_clickable(( + By.CSS_SELECTOR, "a[href*='#activity']")))) + activity_dict = {} + timeline = WebDriverWait( + self.driver, self.conf.wait_max_sec).until( + ec.visibility_of_element_located(( + By.ID, "activity-timeline"))) + searched_activity = timeline.find_elements_by_css_selector( + css_activity_selector) + logger.warning("Found activity list for %s:" % code_text) + for activity in searched_activity: + activity_id = activity.get_attribute('data-activity-id') + key = activity.get_attribute('data-timestamp') + logger.warning("%(id)s @ %(activity)s" % { + 'id': activity_id, + 'activity': datetime.fromtimestamp( + int(key)).strftime('%Y-%m-%d %H:%M:%S')}) + activity_dict[key] = activity_id + except StaleElementReferenceException: + logger.warning('Timeline changed while processing it') + return self.get_timeline_elements(code, fallback_url) + except TimeoutException: + logger.warning('Can not found timeline in the page') + if fallback_url is not None: + return self.fallback( + fallback_url, + lambda: self.get_timeline_elements(code)) + else: + self.create_screenshot() + raise Exception('Selenium could not locate the timeline') + except: + logger.exception('Selenium cannot get timeline elemets') + self.create_screenshot() + raise Exception('Cannot get timeline elements') + if len(activity_dict) == 0: + logger.warning('Found activity list is empty') + self.create_screenshot() + raise Exception('Selenium did not found any activity') + return activity_dict + except: + logger.exception('Selenium cannot find the searched activity') + self.create_screenshot() + raise Exception('Cannot find the searched activity') + + def create_template_from_base(self, delete_disk=True, name=None): + try: + if name is None: + name = "from_%s" % self.conf.client_name + self.driver.get('%s/dashboard/template/choose/' % self.conf.host) + choice_list = [] + choices = self.driver.find_elements_by_css_selector( + "input[type='radio']") + choice_list = [item for item in choices if ( + 'test' not in item.get_attribute('value') + and item.get_attribute('value') != 'base_vm')] + chosen = random.randint(0, len(choice_list) - 1) + choice_list[chosen].click() + self.driver.find_element_by_id( + "template-choose-next-button").click() + if delete_disk: + url_save = self.get_url(fragment='activity') + self.click_on_link( + self.get_link_by_href("#resources")) + disks = WebDriverWait( + self.driver, self.conf.wait_max_sec).until( + ec.visibility_of_element_located(( + By.ID, 'vm-details-resources-disk'))) + disk_list = disks.find_elements_by_css_selector( + "h4[class*='list-group-item-heading']") + if len(disk_list) > 0: + self.click_on_link( + self.get_link_by_href("/op/remove_disk/")) + self.wait_and_accept_operation( + try_wait="a[href*='#activity']") + recent_remove_disk = self.recently( + self.get_timeline_elements( + "vm.Instance.remove_disk", url_save)) + if not self.check_operation_result( + recent_remove_disk, "a[href*='#activity']"): + logger.warning("Selenium cannot delete disk " + "of the chosen template") + raise Exception('Cannot delete disk') + return self.save_template_from_vm(name) + except: + logger.exception("Selenium cannot start a" + " template from a base one") + self.create_screenshot() + raise Exception( + 'Cannot start a template from a base one') + + def delete_template(self, template_id): + try: + self.driver.get( + '%s/dashboard/template/%s/' % (self.conf.host, template_id)) + url_save = "%(host)s/dashboard/template/delete/%(pk)s/" % { + 'host': self.conf.host, + 'pk': template_id} + self.click_on_link( + self.get_link_by_href( + "/dashboard/template/delete/%s/" % template_id)) + self.wait_and_accept_operation(fallback_url=url_save) + WebDriverWait(self.driver, self.conf.wait_max_sec).until( + ec.visibility_of_element_located(( + By.CLASS_NAME, 'alert-success'))) + url = urlparse.urlparse(self.driver.current_url) + if "/template/list/" not in url.path: + logger.warning('CIRCLE does not redirect to /template/list/') + raise Exception( + 'System does not redirect to template listing') + logger.warning('Successfully deleted template: id - %(pk)s' % { + 'pk': template_id}) + except: + logger.exception("Selenium cannot delete the desired template") + self.create_screenshot() + raise Exception('Cannot delete the desired template') + + def create_random_vm(self): + try: + self.driver.get('%s/dashboard/vm/create/' % self.conf.host) + vm_list = [] + pk = None + vm_list = self.driver.find_elements_by_class_name( + 'vm-create-template-summary') + choice = random.randint(0, len(vm_list) - 1) + vm_list[choice].click() + try: + WebDriverWait(self.driver, self.conf.wait_max_sec).until( + ec.element_to_be_clickable(( + By.CLASS_NAME, "vm-create-start"))).click() + except TimeoutException: + # Selenium can time out not findig it even though it is present + self.driver.find_element_by_tag_name('form').submit() + except: + logger.exception("Selenium could not submit create vm form") + raise Exception('Could not submit a form') + WebDriverWait(self.driver, self.conf.wait_max_sec).until( + ec.visibility_of_element_located(( + By.CLASS_NAME, 'alert-success'))) + url = urlparse.urlparse(self.driver.current_url) + pk = re.search(r'\d+', url.path).group() + return pk + except: + logger.exception("Selenium cannot start a VM") + self.create_screenshot() + raise Exception('Cannot start a VM') + + def view_change(self, target_box): + driver = self.driver + driver.get('%s/dashboard/' % self.conf.host) + list_view = driver.find_element_by_id('%s-list-view' % target_box) + graph_view = driver.find_element_by_id('%s-graph-view' % target_box) + js_script = 'return arguments[0].style.display;' + required_attributes = {'data-index-box': target_box} + graph_view_link = self.get_link_by_href( + '#index-graph-view', + required_attributes).find_element_by_tag_name('i') + list_view_link = self.get_link_by_href( + '#index-list-view', + required_attributes).find_element_by_tag_name('i') + self.click_on_link(list_view_link) + states = [driver.execute_script(js_script, list_view), + driver.execute_script(js_script, graph_view)] + self.click_on_link(graph_view_link) + states.extend([driver.execute_script(js_script, list_view), + driver.execute_script(js_script, graph_view)]) + self.click_on_link(list_view_link) + states.extend([driver.execute_script(js_script, list_view), + driver.execute_script(js_script, graph_view)]) + return states + + def delete_vm(self, pk): + try: + # For relability reasons instead of using the JS operatation + self.driver.get("%(host)s/dashboard/vm/%(id)s/op/destroy/" % { + 'host': self.conf.host, + 'id': pk}) + self.wait_and_accept_operation(try_wait="a[href*='/op/recover/']") + try: + status_span = WebDriverWait( + self.driver, self.conf.wait_max_sec).until( + ec.visibility_of_element_located(( + By.ID, 'vm-details-state'))) + WebDriverWait(status_span, self.conf.wait_max_sec).until( + ec.visibility_of_element_located(( + By.CLASS_NAME, 'fa-trash-o'))) + except: + # Selenium can time-out by not realising the JS refresh + url_save = self.get_url(fragment='activity') + recent_destroy_vm = self.recently( + self.get_timeline_elements( + "vm.Instance.destroy", url_save)) + if not self.check_operation_result( + recent_destroy_vm, "a[href*='#activity']"): + logger.warning("Selenium cannot destroy " + "the chosen %(id)s vm" % { + 'id': pk}) + raise Exception('Cannot destroy the specified vm') + self.driver.get('%s/dashboard/vm/%s/' % (self.conf.host, pk)) + try: + WebDriverWait(self.driver, self.conf.wait_max_sec).until( + ec.visibility_of_element_located(( + By.CSS_SELECTOR, + "span[data-status*='DESTROYED']"))) + logger.warning( + 'Successfully deleted virtual machine: id - %(pk)s' % { + 'pk': pk}) + return True + except: + return False + except: + logger.exception("Selenium can not destroy a VM") + self.create_screenshot() + raise Exception("Cannot destroy a VM") diff --git a/circle/dashboard/tests/test_views.py b/circle/dashboard/tests/test_views.py index 85127c2..c522ad1 100644 --- a/circle/dashboard/tests/test_views.py +++ b/circle/dashboard/tests/test_views.py @@ -20,8 +20,7 @@ import json # from unittest import skip from django.test import TestCase from django.test.client import Client -from django.contrib.auth.models import User, Group -from django.contrib.auth.models import Permission +from django.contrib.auth.models import User, Group, Permission from django.contrib.auth import authenticate from common.tests.celery_mock import MockCeleryMixin diff --git a/circle/dashboard/urls.py b/circle/dashboard/urls.py index 2519fa3..11d2f11 100644 --- a/circle/dashboard/urls.py +++ b/circle/dashboard/urls.py @@ -54,6 +54,7 @@ from .views import ( NodeActivityView, UserList, StorageDetail, DiskDetail, + MessageList, MessageDetail, MessageCreate, MessageDelete, ) from .views.vm import vm_ops, vm_mass_ops from .views.node import node_ops @@ -228,11 +229,19 @@ urlpatterns = patterns( url(r'^vm/opensearch.xml$', OpenSearchDescriptionView.as_view(), name="dashboard.views.vm-opensearch"), - url(r'^storage/$', StorageDetail.as_view(), name="dashboard.views.storage"), url(r'^disk/(?P<pk>\d+)/$', DiskDetail.as_view(), name="dashboard.views.disk-detail"), + + url(r'^message/list/$', MessageList.as_view(), + name="dashboard.views.message-list"), + url(r'^message/(?P<pk>\d+)/$', MessageDetail.as_view(), + name="dashboard.views.message-detail"), + url(r'^message/create/$', MessageCreate.as_view(), + name="dashboard.views.message-create"), + url(r'^message/delete/(?P<pk>\d+)/$', MessageDelete.as_view(), + name="dashboard.views.message-delete"), ) urlpatterns += patterns( diff --git a/circle/dashboard/views/__init__.py b/circle/dashboard/views/__init__.py index 46877f4..f799d8d 100644 --- a/circle/dashboard/views/__init__.py +++ b/circle/dashboard/views/__init__.py @@ -13,3 +13,5 @@ from util import * from vm import * from graph import * from storage import * +from request import * +from message import * diff --git a/circle/dashboard/views/graph.py b/circle/dashboard/views/graph.py index a590762..a09dd54 100644 --- a/circle/dashboard/views/graph.py +++ b/circle/dashboard/views/graph.py @@ -212,7 +212,7 @@ class VmNetwork(object): 'alias(scaleToSeconds(nonNegativeDerivative(' '%s.network.bytes_sent-%s), 10), "in - %s (bits/s)")' % ( params)) - return 'group(%s)' % ','.join(metrics) + return 'group(%s)' % ','.join(metrics) if metrics else None register_graph(VmNetwork, 'network', VmGraphView) diff --git a/circle/dashboard/views/group.py b/circle/dashboard/views/group.py index b5f193c..b4356a9 100644 --- a/circle/dashboard/views/group.py +++ b/circle/dashboard/views/group.py @@ -63,6 +63,8 @@ class GroupCodeMixin(object): client = Saml2Client(conf, state_cache=state, identity_cache=IdentityCache(request.session)) subject_id = _get_subject_id(request.session) + if not subject_id: + return newgroups identity = client.users.get_identity(subject_id, check_not_on_or_after=False) if identity: @@ -144,7 +146,7 @@ class GroupDetailView(CheckedDetailView): self.object.user_set.add(entity) except User.DoesNotExist: if saml_available: - FutureMember.objects.get_or_create(org_id=name, + FutureMember.objects.get_or_create(org_id=name.upper(), group=self.object) else: messages.warning(request, _('User "%s" not found.') % name) diff --git a/circle/dashboard/views/message.py b/circle/dashboard/views/message.py new file mode 100644 index 0000000..e2f06dd --- /dev/null +++ b/circle/dashboard/views/message.py @@ -0,0 +1,68 @@ +# 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") diff --git a/circle/dashboard/views/template.py b/circle/dashboard/views/template.py index 4f3309f..4bc6c43 100644 --- a/circle/dashboard/views/template.py +++ b/circle/dashboard/views/template.py @@ -441,7 +441,7 @@ class TransferTemplateOwnershipView(TransferOwnershipView): confirm_view = TransferTemplateOwnershipConfirmView model = InstanceTemplate notification_msg = ugettext_noop( - '%(user)s offered you to take the ownership of ' + '%(owner)s offered you to take the ownership of ' 'his/her template called %(instance)s. ' '<a href="%(token)s" ' 'class="btn btn-success btn-small">Accept</a>') diff --git a/circle/dashboard/views/user.py b/circle/dashboard/views/user.py index 7aa9942..14cce91 100644 --- a/circle/dashboard/views/user.py +++ b/circle/dashboard/views/user.py @@ -34,6 +34,7 @@ from django.core.paginator import Paginator, InvalidPage from django.db.models import Q from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.shortcuts import redirect, get_object_or_404 +from django.templatetags.static import static from django.utils.translation import ugettext as _ from django.views.decorators.http import require_POST from django.views.generic import ( @@ -101,6 +102,8 @@ def circle_login(request): authentication_form = CircleAuthenticationForm extra_context = { 'saml2': saml_available, + 'og_image': (settings.DJANGO_URL.rstrip("/") + + static("dashboard/img/og.png")) } response = login_view(request, authentication_form=authentication_form, extra_context=extra_context) diff --git a/circle/dashboard/views/util.py b/circle/dashboard/views/util.py index 16ca9b1..6388370 100644 --- a/circle/dashboard/views/util.py +++ b/circle/dashboard/views/util.py @@ -70,7 +70,7 @@ def search_user(keyword): return User.objects.get(username=keyword) except User.DoesNotExist: try: - return User.objects.get(profile__org_id=keyword) + return User.objects.get(profile__org_id__iexact=keyword) except User.DoesNotExist: return User.objects.get(email=keyword) @@ -610,7 +610,7 @@ class TransferOwnershipView(CheckedDetailView, DetailView): new_owner.profile.notify( ugettext_noop('Ownership offer'), self.notification_msg, - {'instance': obj, 'token': token_path}) + {'instance': obj, 'token': token_path, 'owner': request.user}) except Profile.DoesNotExist: messages.error(request, _('Can not notify selected user.')) else: @@ -665,8 +665,8 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): old.profile.notify( ugettext_noop('Ownership accepted'), ugettext_noop('Your ownership offer of %(instance)s has been ' - 'accepted by %(user)s.'), - {'instance': instance}) + 'accepted by %(owner)s.'), + {'instance': instance, 'owner': request.user}) return redirect(instance.get_absolute_url()) def get_instance(self, key, user): diff --git a/circle/dashboard/views/vm.py b/circle/dashboard/views/vm.py index 0cd3a71..69dc7b3 100644 --- a/circle/dashboard/views/vm.py +++ b/circle/dashboard/views/vm.py @@ -66,6 +66,8 @@ from ..forms import ( VmPortRemoveForm, VmPortAddForm, VmRemoveInterfaceForm, ) +from request.models import TemplateAccessType, LeaseType +from request.forms import LeaseRequestForm, TemplateRequestForm from ..models import Favourite from manager.scheduler import has_traits @@ -171,6 +173,10 @@ class VmDetailView(GraphMixin, CheckedDetailView): context['is_operator'] = is_operator context['is_owner'] = is_owner + # operation also allows RUNNING (if with_shutdown is present) + context['save_resources_enabled'] = instance.status not in ("RUNNING", + "PENDING") + return context def post(self, request, *args, **kwargs): @@ -651,10 +657,12 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView): op = 'renew' icon = 'calendar' - effect = 'info' + effect = 'success' show_in_toolbar = False form_class = VmRenewForm wait_for_result = 0.5 + template_name = 'dashboard/_vm-renew.html' + with_reload = True def get_form_kwargs(self): choices = Lease.get_objects_with_level("user", self.request.user) @@ -674,6 +682,12 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView): instance.time_of_suspend) return extra + def get_context_data(self, **kwargs): + context = super(VmRenewView, self).get_context_data(**kwargs) + context['lease_request_form'] = LeaseRequestForm(request=self.request) + context['lease_types'] = LeaseType.objects.exists() + return context + class VmStateChangeView(FormOperationMixin, VmOperationView): op = 'emergency_change_state' @@ -762,6 +776,10 @@ vm_ops = OrderedDict([ op='mount_store', icon='briefcase', effect='info', show_in_toolbar=False, )), + ('install_keys', VmOperationView.factory( + op='install_keys', icon='key', effect='info', + show_in_toolbar=False, + )), ]) @@ -1043,6 +1061,8 @@ class VmCreate(LoginRequiredMixin, TemplateView): 'box_title': _('Create a VM'), 'ajax_title': True, 'templates': templates.all(), + 'template_access_types': TemplateAccessType.objects.exists(), + 'form': TemplateRequestForm(request=request), }) return self.render_to_response(context) @@ -1297,7 +1317,7 @@ class TransferInstanceOwnershipView(TransferOwnershipView): confirm_view = TransferInstanceOwnershipConfirmView model = Instance notification_msg = ugettext_noop( - '%(user)s offered you to take the ownership of ' + '%(owner)s offered you to take the ownership of ' 'his/her virtual machine called %(instance)s. ' '<a href="%(token)s" ' 'class="btn btn-success btn-small">Accept</a>') diff --git a/circle/fabfile.py b/circle/fabfile.py index 4141a70..9420917 100755 --- a/circle/fabfile.py +++ b/circle/fabfile.py @@ -143,8 +143,8 @@ def selenium(test=""): test = "--failed" else: test += " --with-id" - run("xvfb-run ./manage.py test " - "--settings=circle.settings.selenium_test %s" % test) + run('xvfb-run --server-args="-screen 0, 1920x1080x24" ./manage.py' + ' test --settings=circle.settings.selenium_test %s' % test) def pull(dir="~/circle/circle"): diff --git a/circle/locale/hu/LC_MESSAGES/django.po b/circle/locale/hu/LC_MESSAGES/django.po index f46322e..1b8f695 100644 --- a/circle/locale/hu/LC_MESSAGES/django.po +++ b/circle/locale/hu/LC_MESSAGES/django.po @@ -6,9 +6,9 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-03-05 14:32+0100\n" -"PO-Revision-Date: 2015-03-05 14:42+0116\n" -"Last-Translator: Daniel Bach <bd@doszgep.hu>\n" +"POT-Creation-Date: 2015-04-20 13:16+0200\n" +"PO-Revision-Date: 2015-04-20 13:31+0116\n" +"Last-Translator: Test Viktor <kviktor@cloud.bme.hu>\n" "Language-Team: Hungarian <cloud@ik.bme.hu>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -32,79 +32,79 @@ msgstr "Hiba." #: common/models.py:72 #, python-format -msgid "Unhandled exception: %(error)s" -msgstr "Kezeletlen kivétel: %(error)s" +msgid "Unhandled exception: %(e)s: %(error)s" +msgstr "Kezeletlen kivétel: %(e)s: %(error)s" -#: common/models.py:147 +#: common/models.py:148 #: dashboard/templates/dashboard/instanceactivity_detail.html:28 msgid "activity code" msgstr "tevékenységkód" -#: common/models.py:150 +#: common/models.py:151 msgid "human readable name" msgstr "olvasható név" -#: common/models.py:151 +#: common/models.py:152 msgid "Human readable name of activity." msgstr "A tevékenység neve olvasható formában." -#: common/models.py:155 +#: common/models.py:156 msgid "Celery task unique identifier." msgstr "Celery feladat egyedi azonosítója." -#: common/models.py:156 +#: common/models.py:157 msgid "task_uuid" msgstr "feladat uuid" -#: common/models.py:157 +#: common/models.py:158 #: dashboard/templates/dashboard/instanceactivity_detail.html:37 -#: firewall/models.py:284 vm/models/common.py:84 vm/models/instance.py:130 -#: vm/models/instance.py:211 +#: firewall/models.py:284 request/models.py:226 vm/models/common.py:84 +#: vm/models/instance.py:129 vm/models/instance.py:210 msgid "user" msgstr "felhasználó" -#: common/models.py:158 +#: common/models.py:159 msgid "The person who started this activity." msgstr "A tevékenységet indító felhasználó." -#: common/models.py:159 +#: common/models.py:160 msgid "started at" msgstr "indítás ideje" -#: common/models.py:161 +#: common/models.py:162 msgid "Time of activity initiation." msgstr "A tevékenység megkezdésének időpontja." -#: common/models.py:162 +#: common/models.py:163 msgid "finished at" msgstr "befejezés ideje" -#: common/models.py:164 +#: common/models.py:165 msgid "Time of activity finalization." msgstr "A tevékenység befejeztének ideje." -#: common/models.py:166 +#: common/models.py:167 msgid "True, if the activity has finished successfully." msgstr "Igaz, ha a tevékenység sikeresen befejeződött." -#: common/models.py:168 +#: common/models.py:169 #: dashboard/templates/dashboard/instanceactivity_detail.html:56 msgid "result" msgstr "eredmény" -#: common/models.py:170 +#: common/models.py:171 msgid "Human readable result of activity." msgstr "A tevékenység eredménye olvasható formában." -#: common/models.py:535 +#: common/models.py:536 msgid "Permission Denied" msgstr "Hozzáférés megtagadva" -#: common/models.py:537 +#: common/models.py:538 msgid "Unknown error" msgstr "Ismeretlen hiba" -#: common/models.py:538 +#: common/models.py:539 #, python-format msgid "Unknown error: %(ex)s" msgstr "Ismeretlen hiba: %(ex)s" @@ -138,17 +138,17 @@ msgstr "szerver" msgid "realtime" msgstr "valós idejű" -#: dashboard/forms.py:93 dashboard/forms.py:810 dashboard/forms.py:900 -#: dashboard/forms.py:1351 dashboard/tables.py:268 +#: dashboard/forms.py:93 dashboard/forms.py:811 dashboard/forms.py:901 +#: dashboard/forms.py:1352 dashboard/tables.py:270 #: dashboard/templates/dashboard/_vm-create-2.html:20 #: dashboard/templates/dashboard/vm-detail/home.html:9 #: dashboard/templates/dashboard/vm-list.html:62 firewall/models.py:296 #: network/templates/network/index.html:24 -#: network/templates/network/switch-port-edit.html:45 +#: network/templates/network/switch-port-edit.html:45 request/models.py:57 msgid "Name" msgstr "Név" -#: dashboard/forms.py:94 vm/models/instance.py:135 +#: dashboard/forms.py:94 vm/models/instance.py:134 msgid "Human readable name of template." msgstr "A sablon olvasható neve." @@ -195,13 +195,15 @@ msgstr "" #: network/templates/network/switch-port-create.html:8 #: network/templates/network/vlan-create.html:8 #: network/templates/network/vlan-group-create.html:8 +#: request/templates/request/lease-type-form.html:7 +#: request/templates/request/template-type-form.html:7 msgid "Create" msgstr "Létrehozás" -#: dashboard/forms.py:282 dashboard/forms.py:1231 dashboard/forms.py:1248 -#: dashboard/forms.py:1283 dashboard/forms.py:1321 dashboard/forms.py:1364 -#: dashboard/forms.py:1405 dashboard/forms.py:1425 dashboard/forms.py:1488 -#: dashboard/forms.py:1600 +#: dashboard/forms.py:282 dashboard/forms.py:1232 dashboard/forms.py:1249 +#: dashboard/forms.py:1284 dashboard/forms.py:1322 dashboard/forms.py:1365 +#: dashboard/forms.py:1406 dashboard/forms.py:1426 dashboard/forms.py:1489 +#: dashboard/forms.py:1601 dashboard/forms.py:1637 #: dashboard/templates/dashboard/_manage_access.html:73 #: dashboard/templates/dashboard/connect-command-create.html:37 #: dashboard/templates/dashboard/connect-command-edit.html:37 @@ -209,21 +211,22 @@ msgstr "Létrehozás" #: dashboard/templates/dashboard/lease-edit.html:96 #: dashboard/templates/dashboard/template-tx-owner.html:12 #: dashboard/templates/dashboard/vm-detail/tx-owner.html:12 -#: network/forms.py:86 network/forms.py:100 network/forms.py:122 -#: network/forms.py:162 network/forms.py:187 network/forms.py:226 -#: network/forms.py:247 network/forms.py:298 network/forms.py:323 +#: network/forms.py:88 network/forms.py:103 network/forms.py:126 +#: network/forms.py:167 network/forms.py:195 network/forms.py:236 +#: network/forms.py:261 network/forms.py:313 network/forms.py:342 +#: request/forms.py:38 request/forms.py:54 msgid "Save" msgstr "Mentés" -#: dashboard/forms.py:312 dashboard/forms.py:1028 +#: dashboard/forms.py:312 dashboard/forms.py:1029 #: dashboard/templates/dashboard/_vm-remove-port.html:15 #: dashboard/templates/dashboard/vm-detail.html:102 network/views.py:659 msgid "Host" msgstr "Gép" -#: dashboard/forms.py:383 dashboard/forms.py:781 dashboard/forms.py:991 +#: dashboard/forms.py:383 dashboard/forms.py:782 dashboard/forms.py:992 #: dashboard/templates/dashboard/node-detail.html:5 -#: dashboard/templates/dashboard/vm-detail/home.html:117 +#: dashboard/templates/dashboard/vm-detail/home.html:118 #: dashboard/templates/dashboard/vm-list.html:87 msgid "Node" msgstr "Csomópont" @@ -261,24 +264,24 @@ msgstr "Törlés ideje" msgid "Save changes" msgstr "Változások mentése" -#: dashboard/forms.py:747 +#: dashboard/forms.py:748 msgid "Set expiration times even if they are shorter than the current value." msgstr "" "Akkor is állítsa át a lejárati időket, ha rövidebbek lesznek a jelenleginél." -#: dashboard/forms.py:750 +#: dashboard/forms.py:751 msgid "Save selected lease." msgstr "Kiválasztott bérlet mentése." -#: dashboard/forms.py:759 +#: dashboard/forms.py:760 msgid "Length" msgstr "Hossz" -#: dashboard/forms.py:767 +#: dashboard/forms.py:768 msgid "Live migration" msgstr "Live migration" -#: dashboard/forms.py:769 +#: dashboard/forms.py:770 msgid "" "Live migration is a way of moving virtual machines between hosts with a " "service interruption of at most some seconds. Please note that it can take " @@ -289,205 +292,238 @@ msgstr "" "hogy ez terhelt gépek esetén sokáig tarthat és nagy hálózati forgalommal " "jár." -#: dashboard/forms.py:787 +#: dashboard/forms.py:788 msgid "Forcibly interrupt all running activities." msgstr "Futó tevékenységek erőltetett befejezése." -#: dashboard/forms.py:788 +#: dashboard/forms.py:789 msgid "Set all activities to finished state, but don't interrupt any tasks." msgstr "" "Minden tevékenység befejezettre állítása (a feladatok megszakítása nélkül)." -#: dashboard/forms.py:791 +#: dashboard/forms.py:792 msgid "New status" msgstr "Új állapot" -#: dashboard/forms.py:792 +#: dashboard/forms.py:793 msgid "Reset node" msgstr "Csomópont visszaállítása" -#: dashboard/forms.py:806 +#: dashboard/forms.py:807 msgid "use emergency state change" msgstr "vész-állapotváltás használata" -#: dashboard/forms.py:812 dashboard/forms.py:832 +#: dashboard/forms.py:813 dashboard/forms.py:833 #: dashboard/templates/dashboard/store/_list-box.html:117 msgid "Size" msgstr "Méret" -#: dashboard/forms.py:813 +#: dashboard/forms.py:814 msgid "Size of disk to create in bytes or with units like MB or GB." msgstr "Létrehozandó lemez mérete byte-okban vagy mértékegységgel (MB, GB)." -#: dashboard/forms.py:825 dashboard/forms.py:854 +#: dashboard/forms.py:826 dashboard/forms.py:855 msgid "Invalid format, you can use GB or MB!" msgstr "Érvénytelen formátum. „GB” és „MB” is használható." -#: dashboard/forms.py:833 +#: dashboard/forms.py:834 msgid "Size to resize the disk in bytes or with units like MB or GB." msgstr "A lemez kívánt mérete byte-okban vagy mértékegységgel (MB, GB)." -#: dashboard/forms.py:844 dashboard/forms.py:880 +#: dashboard/forms.py:845 dashboard/forms.py:881 #: dashboard/templates/dashboard/storage/disk.html:7 #: dashboard/templates/dashboard/storage/disk.html:24 msgid "Disk" msgstr "Lemez" -#: dashboard/forms.py:857 +#: dashboard/forms.py:858 msgid "Disk size must be greater than the actual size." msgstr "A lemez mérete nagyobb kell legyen a jelenleginél." -#: dashboard/forms.py:866 dashboard/forms.py:891 +#: dashboard/forms.py:867 dashboard/forms.py:892 #, python-format msgid "<label>Disk:</label> %s" msgstr "<label>Lemez:</label> %s" -#: dashboard/forms.py:901 +#: dashboard/forms.py:902 msgid "URL" msgstr "URL" -#: dashboard/forms.py:911 +#: dashboard/forms.py:912 msgid "Could not find filename in URL, please specify a name explicitly." msgstr "Az URL-ben nem található fájlnév. Kérem adja meg explicite." -#: dashboard/forms.py:925 +#: dashboard/forms.py:926 msgid "Interface" msgstr "Interfészek" -#: dashboard/forms.py:937 +#: dashboard/forms.py:938 #, python-brace-format msgid "<label>Vlan:</label> {0}" msgstr "<label>Vlan:</label> {0}" -#: dashboard/forms.py:952 +#: dashboard/forms.py:953 #: dashboard/templates/dashboard/_vm-remove-port.html:17 #: dashboard/templates/dashboard/node-detail/resources.html:28 #: network/views.py:658 msgid "Vlan" msgstr "Vlan" -#: dashboard/forms.py:955 +#: dashboard/forms.py:956 msgid "No more networks." msgstr "Nincs több hálózat." -#: dashboard/forms.py:976 +#: dashboard/forms.py:977 #, python-format msgid " (missing_traits: %s)" msgstr "(hiányzó jellemzők: %s)" -#: dashboard/forms.py:992 +#: dashboard/forms.py:993 msgid "" "Deploy virtual machine to this node (blank allows scheduling automatically)." msgstr "" "A virtuális gép elindítása ezen a csomóponton (üresen hagyva automatikus " "ütemezés)." -#: dashboard/forms.py:1009 dashboard/forms.py:1015 +#: dashboard/forms.py:1010 dashboard/forms.py:1016 #: dashboard/templates/dashboard/_vm-remove-port.html:13 msgid "Port" msgstr "Port" -#: dashboard/forms.py:1018 dashboard/templates/dashboard/vm-detail.html:100 +#: dashboard/forms.py:1019 dashboard/templates/dashboard/vm-detail.html:100 msgid "Protocol" msgstr "Protokoll" -#: dashboard/forms.py:1040 +#: dashboard/forms.py:1041 #, python-brace-format msgid "<label>Host:</label> {0}" msgstr "<label>Gép:</label> {0}" -#: dashboard/forms.py:1068 dashboard/templates/dashboard/profile.html:36 +#: dashboard/forms.py:1069 dashboard/templates/dashboard/profile.html:37 #: dashboard/templates/dashboard/vm-detail.html:118 msgid "Username" msgstr "Felhasználónév" -#: dashboard/forms.py:1082 dashboard/templates/dashboard/vm-detail.html:120 +#: dashboard/forms.py:1083 dashboard/templates/dashboard/vm-detail.html:120 msgid "Password" msgstr "Jelszó" -#: dashboard/forms.py:1087 +#: dashboard/forms.py:1088 msgid "Sign in" msgstr "Bejelentkezés" -#: dashboard/forms.py:1110 dashboard/templates/dashboard/profile.html:42 +#: dashboard/forms.py:1111 dashboard/templates/dashboard/profile.html:43 msgid "Email address" msgstr "E-mail cím" -#: dashboard/forms.py:1115 +#: dashboard/forms.py:1116 msgid "Reset password" msgstr "Új jelszó" -#: dashboard/forms.py:1131 dashboard/forms.py:1257 +#: dashboard/forms.py:1132 dashboard/forms.py:1258 msgid "Change password" msgstr "Jelszóváltoztatás" -#: dashboard/forms.py:1203 +#: dashboard/forms.py:1204 msgid "Add trait" msgstr "Jellemző hozzáadása" -#: dashboard/forms.py:1220 +#: dashboard/forms.py:1221 msgid "Preferred language" msgstr "Választott nyelv" -#: dashboard/forms.py:1272 dashboard/templates/dashboard/group-list.html:14 +#: dashboard/forms.py:1273 dashboard/templates/dashboard/group-list.html:14 #: dashboard/templates/dashboard/index-groups.html:7 -#: dashboard/templates/dashboard/profile.html:60 +#: dashboard/templates/dashboard/profile.html:62 #: dashboard/templates/dashboard/vm-detail/network.html:40 #: network/templates/network/host-edit.html:32 templates/info/help.html:192 msgid "Groups" msgstr "Csoportok" -#: dashboard/forms.py:1298 +#: dashboard/forms.py:1299 msgid "Instance limit" msgstr "Példány limit" -#: dashboard/forms.py:1328 dashboard/templates/dashboard/lease-edit.html:86 +#: dashboard/forms.py:1329 dashboard/templates/dashboard/lease-edit.html:86 msgid "Name of group or user" msgstr "Csoport vagy felhasználó neve" -#: dashboard/forms.py:1336 dashboard/forms.py:1345 +#: dashboard/forms.py:1337 dashboard/forms.py:1346 msgid "Name of user" msgstr "Felhasználó neve" -#: dashboard/forms.py:1338 dashboard/forms.py:1347 +#: dashboard/forms.py:1339 dashboard/forms.py:1348 msgid "E-mail address or identifier of user" msgstr "A felhasználó e-mail címe vagy azonosítója" -#: dashboard/forms.py:1353 +#: dashboard/forms.py:1354 msgid "Key" msgstr "Kulcs" -#: dashboard/forms.py:1354 +#: dashboard/forms.py:1355 msgid "For example: ssh-rsa AAAAB3NzaC1yc2ED..." msgstr "Például: ssh-rsa AAAAB3NzaC1yc2ED…" -#: dashboard/forms.py:1434 +#: dashboard/forms.py:1435 msgid "permissions" msgstr "jogosultságok" -#: dashboard/forms.py:1531 +#: dashboard/forms.py:1532 msgid "owned" msgstr "saját" -#: dashboard/forms.py:1532 +#: dashboard/forms.py:1533 msgid "shared" msgstr "osztott" -#: dashboard/forms.py:1533 +#: dashboard/forms.py:1534 msgid "all" msgstr "összes" -#: dashboard/forms.py:1540 dashboard/forms.py:1564 dashboard/forms.py:1583 +#: dashboard/forms.py:1541 dashboard/forms.py:1565 dashboard/forms.py:1584 #: dashboard/templates/dashboard/index-groups.html:23 #: dashboard/templates/dashboard/index-nodes.html:40 #: dashboard/templates/dashboard/index-templates.html:40 #: dashboard/templates/dashboard/index-users.html:28 #: dashboard/templates/dashboard/index-vm.html:64 +#: dashboard/templates/dashboard/storage/detail.html:91 #: network/templates/network/host-list.html:39 msgid "Search..." msgstr "Keresés..." -#: dashboard/models.py:68 dashboard/templates/dashboard/index-groups.html:43 +#: dashboard/models.py:62 dashboard/models.py:71 +msgid "message" +msgstr "üzenet" + +#: dashboard/models.py:64 +msgid "effect" +msgstr "hatás" + +#: dashboard/models.py:65 +msgid "success" +msgstr "siker" + +#: dashboard/models.py:65 +msgid "info" +msgstr "infó" + +#: dashboard/models.py:66 +msgid "warning" +msgstr "figyelmeztetés" + +#: dashboard/models.py:66 +msgid "danger" +msgstr "veszély" + +#: dashboard/models.py:67 vm/models/node.py:128 +msgid "enabled" +msgstr "engedélyezve" + +#: dashboard/models.py:72 +msgid "messages" +msgstr "üzenetek" + +#: dashboard/models.py:88 dashboard/templates/dashboard/index-groups.html:43 #: dashboard/templates/dashboard/index-nodes.html:60 #: dashboard/templates/dashboard/index-templates.html:52 #: dashboard/templates/dashboard/index-users.html:48 @@ -505,39 +541,39 @@ msgstr "Keresés..." msgid "new" msgstr "új" -#: dashboard/models.py:69 +#: dashboard/models.py:89 msgid "delivered" msgstr "kézbesített" -#: dashboard/models.py:70 +#: dashboard/models.py:90 msgid "read" msgstr "olvasott" -#: dashboard/models.py:119 vm/models/instance.py:98 +#: dashboard/models.py:139 vm/models/instance.py:97 msgid "access method" msgstr "elérés módja" -#: dashboard/models.py:120 +#: dashboard/models.py:140 msgid "Type of the remote access method." msgstr "Távoli elérési mód típusa." -#: dashboard/models.py:121 firewall/models.py:528 firewall/models.py:554 -#: firewall/models.py:936 firewall/models.py:975 firewall/models.py:995 +#: dashboard/models.py:141 firewall/models.py:533 firewall/models.py:564 +#: firewall/models.py:951 firewall/models.py:995 firewall/models.py:1020 #: storage/models.py:49 storage/models.py:120 vm/models/common.py:65 -#: vm/models/common.py:89 vm/models/common.py:165 vm/models/instance.py:134 -#: vm/models/instance.py:224 vm/models/node.py:121 +#: vm/models/common.py:89 vm/models/common.py:165 vm/models/instance.py:133 +#: vm/models/instance.py:223 vm/models/node.py:121 msgid "name" msgstr "név" -#: dashboard/models.py:122 +#: dashboard/models.py:142 msgid "Name of your custom command." msgstr "Egyedi parancs neve" -#: dashboard/models.py:124 +#: dashboard/models.py:144 msgid "command template" msgstr "parancssablon" -#: dashboard/models.py:125 +#: dashboard/models.py:145 msgid "" "Template for connection command string. Available parameters are: username, " "password, host, port." @@ -545,69 +581,69 @@ msgstr "" "Sablon a csatlakozási parancshoz. Elérhető paraméterek: username, password, " "host, port." -#: dashboard/models.py:137 +#: dashboard/models.py:160 msgid "preferred language" msgstr "választott nyelv" -#: dashboard/models.py:143 dashboard/models.py:228 +#: dashboard/models.py:166 dashboard/models.py:252 msgid "Unique identifier of the person, e.g. a student number." msgstr "A személy egyedi azonosítója, például hallgatói azonosító." -#: dashboard/models.py:146 +#: dashboard/models.py:169 msgid "Use Gravatar" msgstr "Gravatar használata" -#: dashboard/models.py:147 +#: dashboard/models.py:170 msgid "Whether to use email address as Gravatar profile image" msgstr "Használható-e az e-mail cím a Gravatar profilkép betöltésére" -#: dashboard/models.py:149 +#: dashboard/models.py:172 msgid "Email notifications" msgstr "E-mail értesítések" -#: dashboard/models.py:150 +#: dashboard/models.py:173 msgid "Whether user wants to get digested email notifications." msgstr "A felhasználó kéri-e tömbösített e-mail értesítések küldését." -#: dashboard/models.py:153 +#: dashboard/models.py:176 msgid "Samba password" msgstr "Samba jelszó" -#: dashboard/models.py:155 +#: dashboard/models.py:178 msgid "Generated password for accessing store from virtual machines." msgstr "A tárhely virtuális gépekről való eléréséhez generált jelszó." -#: dashboard/models.py:160 +#: dashboard/models.py:183 msgid "disk quota" msgstr "lemezkvóta" -#: dashboard/models.py:162 +#: dashboard/models.py:185 msgid "Disk quota in mebibytes." msgstr "Lemezkvóta mebibyte-okban." -#: dashboard/models.py:222 +#: dashboard/models.py:246 msgid "Can use autocomplete." msgstr "Használhat automatikus kiegészítést." -#: dashboard/models.py:240 firewall/models.py:285 vm/models/common.py:85 -#: vm/models/instance.py:131 vm/models/instance.py:212 +#: dashboard/models.py:265 firewall/models.py:285 request/models.py:227 +#: vm/models/common.py:85 vm/models/instance.py:130 vm/models/instance.py:211 msgid "operator" msgstr "operátor" -#: dashboard/models.py:241 firewall/models.py:105 firewall/models.py:390 -#: firewall/models.py:537 firewall/models.py:559 firewall/models.py:623 -#: firewall/models.py:976 firewall/models.py:1004 vm/models/common.py:86 -#: vm/models/instance.py:132 vm/models/instance.py:213 +#: dashboard/models.py:266 firewall/models.py:105 firewall/models.py:390 +#: firewall/models.py:542 firewall/models.py:569 firewall/models.py:638 +#: firewall/models.py:996 firewall/models.py:1029 vm/models/common.py:86 +#: vm/models/instance.py:131 vm/models/instance.py:212 msgid "owner" msgstr "tulajdonos" -#: dashboard/models.py:247 +#: dashboard/models.py:272 msgid "Unique identifier of the group at the organization." msgstr "A csoport egyedi szervezeti azonosítója." #. Translators: [T] as Template -#: dashboard/tables.py:53 dashboard/tables.py:308 -#: dashboard/templates/dashboard/vm-detail/home.html:130 +#: dashboard/tables.py:53 dashboard/tables.py:310 +#: dashboard/templates/dashboard/vm-detail/home.html:131 msgid "Template" msgstr "Sablon" @@ -616,19 +652,21 @@ msgid "[T]" msgstr "[S]" #. Translators: [VM] as Virtual Machine -#: dashboard/tables.py:56 +#: dashboard/tables.py:56 request/templates/request/request-lease.html:17 +#: request/templates/request/request-resource.html:20 msgid "Virtual machine" msgstr "Virtuális gép" #: dashboard/tables.py:56 msgid "[VM]" -msgstr "" +msgstr "[VM]" #: dashboard/tables.py:68 msgid "Overcommit" msgstr "Túlfoglalás" -#: dashboard/tables.py:73 +#: dashboard/tables.py:73 request/tables.py:35 +#: request/templates/request/detail.html:71 msgid "Status" msgstr "Állapot" @@ -649,35 +687,36 @@ msgstr "Elérhető minionok" msgid "Number of users" msgstr "Felhasználók száma" -#: dashboard/tables.py:134 dashboard/templates/dashboard/base.html:25 +#: dashboard/tables.py:134 dashboard/templates/dashboard/base.html:27 msgid "Admin" msgstr "Adminisztráció" -#: dashboard/tables.py:141 dashboard/tables.py:218 dashboard/tables.py:249 -#: dashboard/tables.py:283 dashboard/tables.py:312 network/tables.py:272 +#: dashboard/tables.py:141 dashboard/tables.py:220 dashboard/tables.py:251 +#: dashboard/tables.py:285 dashboard/tables.py:314 network/tables.py:272 msgid "Actions" msgstr "Műveletek" -#: dashboard/tables.py:163 dashboard/templates/dashboard/profile.html:37 +#: dashboard/tables.py:164 dashboard/templates/dashboard/profile.html:38 msgid "Organization ID" msgstr "Címtári azonosító" -#: dashboard/tables.py:168 +#: dashboard/tables.py:169 msgid "<abbr data-placement=\"left\" title=\"Superuser status\">SU</abbr>" msgstr "<abbr data-placement=\"left\" title=\"Rendszergazda státusz\">SU</abbr>" -#: dashboard/tables.py:188 dashboard/templates/dashboard/_vm-create-2.html:38 +#: dashboard/tables.py:190 dashboard/templates/dashboard/_vm-create-2.html:38 #: dashboard/templates/dashboard/node-detail.html:72 #: dashboard/templates/dashboard/vm-detail.html:200 msgid "Resources" msgstr "Erőforrások" -#: dashboard/tables.py:194 dashboard/templates/dashboard/vm-list.html:74 -#: vm/models/instance.py:104 +#: dashboard/tables.py:196 dashboard/templates/dashboard/vm-list.html:74 +#: request/forms.py:85 request/models.py:148 request/tables.py:63 +#: vm/models/instance.py:103 msgid "Lease" msgstr "Bérlet" -#: dashboard/tables.py:205 dashboard/templates/dashboard/template-edit.html:85 +#: dashboard/tables.py:207 dashboard/templates/dashboard/template-edit.html:85 #: dashboard/templates/dashboard/template-edit.html:133 #: dashboard/templates/dashboard/vm-detail/access.html:2 #: dashboard/templates/dashboard/vm-detail/access.html:37 @@ -685,31 +724,31 @@ msgstr "Bérlet" msgid "Owner" msgstr "Tulajdonos" -#: dashboard/tables.py:210 dashboard/tables.py:278 +#: dashboard/tables.py:212 dashboard/tables.py:280 msgid "Created at" msgstr "Létrehozva" -#: dashboard/tables.py:214 +#: dashboard/tables.py:216 msgid "Running" msgstr "Fut" -#: dashboard/tables.py:261 +#: dashboard/tables.py:263 msgid "No available leases." msgstr "Nincs elérhető bérlési mód." -#: dashboard/tables.py:273 +#: dashboard/tables.py:275 msgid "Fingerprint" msgstr "Ujjlenyomat" -#: dashboard/tables.py:294 +#: dashboard/tables.py:296 msgid "You haven't added any public keys yet." msgstr "Még nem adott meg publikus kulcsot." -#: dashboard/tables.py:304 +#: dashboard/tables.py:306 msgid "Access method" msgstr "Elérés módja" -#: dashboard/tables.py:325 +#: dashboard/tables.py:327 msgid "" "You don't have any custom connection commands yet. You can specify commands " "to be displayed on VM detail pages instead of the defaults." @@ -717,18 +756,23 @@ msgstr "" "Még nincs egyedi csatlakozási parancsa. Az itt megadott parancsok fognak " "megjelenni a VM-részletező oldalon az alapértelmezettek helyett." -#: dashboard/tables.py:334 dashboard/templates/dashboard/vm-list.html:58 +#: dashboard/tables.py:336 dashboard/templates/dashboard/vm-list.html:58 +#: request/tables.py:31 request/tables.py:61 request/tables.py:77 msgid "ID" msgstr "ID" -#: dashboard/tables.py:339 dashboard/templates/dashboard/storage/disk.html:18 +#: dashboard/tables.py:341 dashboard/templates/dashboard/storage/disk.html:18 msgid "Appliance" msgstr "Felhasználás" -#: dashboard/tables.py:343 +#: dashboard/tables.py:345 msgid "ready" msgstr "kész" +#: dashboard/tables.py:356 +msgid "No disk found." +msgstr "Nem található lemez." + #: dashboard/tasks/local_periodic_tasks.py:60 #, python-format msgid "%d new notification" @@ -736,22 +780,22 @@ msgid_plural "%d new notifications" msgstr[0] "%d új értesítés" msgstr[1] "%d új értesítés" -#: dashboard/templates/base.html:61 templates/info/legal.html:4 +#: dashboard/templates/base.html:78 templates/info/legal.html:4 #: templates/info/legal.html.py:9 msgid "Legal notice" msgstr "Impresszum" -#: dashboard/templates/base.html:62 templates/info/policy.html:4 +#: dashboard/templates/base.html:79 templates/info/policy.html:4 #: templates/info/policy.html.py:9 msgid "Policy" msgstr "Szabályzat" -#: dashboard/templates/base.html:63 templates/info/help.html:4 +#: dashboard/templates/base.html:80 templates/info/help.html:4 #: templates/info/help.html.py:9 msgid "Help" msgstr "Súgó" -#: dashboard/templates/base.html:64 templates/info/support.html:4 +#: dashboard/templates/base.html:81 templates/info/support.html:4 #: templates/info/support.html.py:9 msgid "Support" msgstr "Támogatás" @@ -789,10 +833,11 @@ msgstr "" " " #: dashboard/templates/dashboard/_client-check.html:23 +#: dashboard/templates/dashboard/_vm-renew.html:9 #: dashboard/templates/dashboard/confirm/ajax-delete.html:19 #: dashboard/templates/dashboard/confirm/base-delete.html:33 #: dashboard/templates/dashboard/mass-operate.html:33 -#: dashboard/templates/dashboard/operate.html:21 +#: dashboard/templates/dashboard/operate.html:22 #: dashboard/templates/dashboard/store/remove.html:34 msgid "Cancel" msgstr "Mégsem" @@ -949,22 +994,22 @@ msgstr "Memória" #: dashboard/templates/dashboard/_vm-create-1.html:39 #: dashboard/templates/dashboard/_vm-create-2.html:49 -#: dashboard/templates/dashboard/storage/detail.html:68 -#: dashboard/templates/dashboard/vm-detail/resources.html:25 +#: dashboard/templates/dashboard/storage/detail.html:70 +#: dashboard/templates/dashboard/vm-detail/resources.html:48 msgid "Disks" msgstr "Lemezek" #: dashboard/templates/dashboard/_vm-create-1.html:46 #: dashboard/templates/dashboard/_vm-create-2.html:65 -#: dashboard/templates/dashboard/base.html:34 +#: dashboard/templates/dashboard/base.html:52 #: dashboard/templates/dashboard/vm-detail.html:214 #: dashboard/views/graph.py:198 dashboard/views/graph.py:221 -#: network/forms.py:142 network/templates/network/base.html:7 +#: network/forms.py:147 network/templates/network/base.html:7 msgid "Network" msgstr "Hálózat" #: dashboard/templates/dashboard/_vm-create-1.html:52 -#: network/templates/network/index.html:46 +#: network/templates/network/index.html:46 request/tables.py:43 msgid "Type" msgstr "Típus" @@ -978,19 +1023,37 @@ msgstr "Testreszabás" msgid "Start" msgstr "Indítás" -#: dashboard/templates/dashboard/_vm-create-1.html:82 +#: dashboard/templates/dashboard/_vm-create-1.html:83 msgid "" "You can't start new virtual machines because no templates are shared with " "you." msgstr "" "Még nem tud virtuális gépet indítani, mivel egy sablonhoz sincs hozzáférése." +#: dashboard/templates/dashboard/_vm-create-1.html:85 +msgid "" +"You can't start new virtual machines because no templates are shared with " +"you however you can request them via the form below." +msgstr "" +"Még nem tud virtuális gépet indítani, mivel egy sablonhoz sincs hozzáférése " +"azonban a lenti űrlap segítségével igényelhet." + +#: dashboard/templates/dashboard/_vm-create-1.html:96 +#, python-format +msgid "" +"\n" +" Need other templates? Submit a new <a href=\"%(url)s\">request</a>.\n" +" " +msgstr "" +"\n" +"Másfajta sablonra van szüksége? <a href=\"%(url)s\">Igényeljen</a>." + #: dashboard/templates/dashboard/_vm-create-2.html:30 msgid "Amount" msgstr "Mennyiség" #: dashboard/templates/dashboard/_vm-create-2.html:56 -#: dashboard/templates/dashboard/vm-detail/resources.html:35 +#: dashboard/templates/dashboard/vm-detail/resources.html:58 msgid "No disks are added." msgstr "Egy lemez sincs hozzáadva." @@ -1043,10 +1106,14 @@ msgid "Node traits" msgstr "Csomópontjellemzők" #: dashboard/templates/dashboard/_vm-migrate.html:46 -#: dashboard/templates/dashboard/vm-detail/resources.html:52 +#: dashboard/templates/dashboard/vm-detail/resources.html:75 msgid "Required traits" msgstr "Elvárt jellemzők" +#: dashboard/templates/dashboard/_vm-renew.html:15 +msgid "Request longer lease" +msgstr "Hosszabb bérlet igénylése" + #: dashboard/templates/dashboard/_vm-save.html:7 msgid "" "\n" @@ -1059,28 +1126,43 @@ msgstr "" msgid "Name of template" msgstr "Sablon neve" -#: dashboard/templates/dashboard/base.html:30 +#: dashboard/templates/dashboard/base.html:33 +#| msgid "location" +msgid "Translations" +msgstr "Fordítások" + +#: dashboard/templates/dashboard/base.html:40 +msgid "Messages" +msgstr "Üzenetek" + +#: dashboard/templates/dashboard/base.html:46 #: dashboard/templates/dashboard/storage/detail.html:7 #: dashboard/templates/dashboard/storage/disk.html:7 msgid "Storage" msgstr "Tárhely" -#: dashboard/templates/dashboard/base.html:45 +#: dashboard/templates/dashboard/base.html:58 +#: request/templates/request/list.html:6 +#: request/templates/request/list.html:17 +msgid "Requests" +msgstr "Igénylések" + +#: dashboard/templates/dashboard/base.html:70 msgid "Log out" msgstr "Kijelentkezés" -#: dashboard/templates/dashboard/base.html:50 -#: dashboard/templates/dashboard/base.html:60 +#: dashboard/templates/dashboard/base.html:75 +#: dashboard/templates/dashboard/base.html:85 #: dashboard/templates/dashboard/notifications.html:4 #: dashboard/templates/dashboard/notifications.html:24 msgid "Notifications" msgstr "Értesítések" -#: dashboard/templates/dashboard/base.html:68 +#: dashboard/templates/dashboard/base.html:93 msgid "Loading..." msgstr "Betöltés..." -#: dashboard/templates/dashboard/base.html:74 +#: dashboard/templates/dashboard/base.html:99 msgid "Log in " msgstr "Bejelentkezés" @@ -1109,11 +1191,14 @@ msgstr "" #: dashboard/templates/dashboard/confirm/ajax-delete.html:23 #: dashboard/templates/dashboard/connect-command-list/column-command-actions.html:5 #: dashboard/templates/dashboard/group-detail.html:15 +#: dashboard/templates/dashboard/message-edit.html:20 #: dashboard/templates/dashboard/node-detail.html:16 #: dashboard/templates/dashboard/template-edit.html:76 #: dashboard/templates/dashboard/template-list/column-lease-actions.html:5 #: dashboard/templates/dashboard/template-list/column-template-actions.html:9 #: dashboard/templates/dashboard/userkey-list/column-userkey-actions.html:5 +#: request/templates/request/lease-type-form.html:21 +#: request/templates/request/template-type-form.html:21 msgid "Delete" msgstr "Törlés" @@ -1193,13 +1278,18 @@ msgstr "Parancssablon létrehozása" #: dashboard/templates/dashboard/connect-command-edit.html:13 #: dashboard/templates/dashboard/lease-create.html:13 #: dashboard/templates/dashboard/lease-edit.html:12 -#: dashboard/templates/dashboard/profile.html:23 +#: dashboard/templates/dashboard/message-create.html:14 +#: dashboard/templates/dashboard/message-edit.html:16 +#: dashboard/templates/dashboard/profile.html:24 #: dashboard/templates/dashboard/template-edit.html:16 #: dashboard/templates/dashboard/userkey-create.html:13 #: dashboard/templates/dashboard/userkey-edit.html:14 network/forms.py:65 -#: network/forms.py:87 network/forms.py:101 network/forms.py:123 -#: network/forms.py:163 network/forms.py:188 network/forms.py:227 -#: network/forms.py:248 network/forms.py:299 network/forms.py:324 +#: network/forms.py:89 network/forms.py:104 network/forms.py:127 +#: network/forms.py:168 network/forms.py:196 network/forms.py:237 +#: network/forms.py:262 network/forms.py:314 network/forms.py:343 +#: request/templates/request/detail.html:17 +#: request/templates/request/lease-type-form.html:25 +#: request/templates/request/template-type-form.html:25 msgid "Back" msgstr "Vissza" @@ -1362,8 +1452,8 @@ msgstr "" #: dashboard/templates/dashboard/index-templates.html:7 #: dashboard/templates/dashboard/template-list.html:6 -#: dashboard/templates/dashboard/template-list.html:17 -#: templates/info/help.html:156 +#: dashboard/templates/dashboard/template-list.html:17 request/models.py:162 +#: request/tables.py:81 templates/info/help.html:156 msgid "Templates" msgstr "Sablonok" @@ -1408,7 +1498,7 @@ msgstr "A meglévő virtuális gépei. A megjelöltek megelőzik a többit." #: dashboard/templates/dashboard/index-vm.html:18 msgid "VMs" -msgstr "" +msgstr "VM-ek" #: dashboard/templates/dashboard/index-vm.html:22 #: dashboard/templates/dashboard/vm-list.html:5 @@ -1506,7 +1596,7 @@ msgid "Activity" msgstr "Tevékenységek" #: dashboard/templates/dashboard/instanceactivity_detail.html:31 -#: vm/models/activity.py:70 vm/models/instance.py:273 vm/models/network.py:70 +#: vm/models/activity.py:70 vm/models/instance.py:272 vm/models/network.py:70 msgid "instance" msgstr "példány" @@ -1515,7 +1605,7 @@ msgid "time" msgstr "idő" #: dashboard/templates/dashboard/instanceactivity_detail.html:40 -#: firewall/models.py:1000 network/tables.py:181 +#: firewall/models.py:1025 network/tables.py:181 msgid "type" msgstr "típus" @@ -1557,6 +1647,7 @@ msgid "failed" msgstr "meghiúsult" #: dashboard/templates/dashboard/instanceactivity_detail.html:78 +#: dashboard/views/storage.py:59 msgid "none" msgstr "nincs" @@ -1591,6 +1682,29 @@ msgstr[1] "" "\n" "Biztosan végrehajtja a(z) <strong>%(op)s</strong> műveletet a következő %(count)s példányon?\n" +#: dashboard/templates/dashboard/message-create.html:5 +#: dashboard/templates/dashboard/message-edit.html:5 +#: dashboard/templates/dashboard/message-list.html:6 +#: dashboard/templates/dashboard/message-list.html:17 +msgid "Broadcast Messages" +msgstr "Üzenetek" + +#: dashboard/templates/dashboard/message-create.html:18 +msgid "New message" +msgstr "Új üzenet" + +#: dashboard/templates/dashboard/message-edit.html:25 +msgid "Edit message" +msgstr "Üzenet szerkesztése" + +#: dashboard/templates/dashboard/message-edit.html:41 +msgid "Preview" +msgstr "Előnézet" + +#: dashboard/templates/dashboard/message-list.html:15 +msgid "new message" +msgstr "új üzenet" + #: dashboard/templates/dashboard/node-add-trait.html:19 msgid "Add Trait" msgstr "Jellemző hozzáadása" @@ -1629,6 +1743,16 @@ msgstr "Kezdőoldal" msgid "Virtual Machines" msgstr "Virtuális gépek" +#: dashboard/templates/dashboard/node-detail/_activity-timeline.html:47 +#: dashboard/templates/dashboard/vm-detail/_activity-timeline.html:65 +msgid "Show less activities" +msgstr "Kevesebb tevékenység megjelenítése" + +#: dashboard/templates/dashboard/node-detail/_activity-timeline.html:49 +#: dashboard/templates/dashboard/vm-detail/_activity-timeline.html:67 +msgid "Show all activities" +msgstr "Összes tevékenység megjelenítése" + #: dashboard/templates/dashboard/node-detail/home.html:4 msgid "Traits the node provides" msgstr "A csomópont által biztosított jellemzők" @@ -1646,7 +1770,7 @@ msgid "CPU cores" msgstr "CPU-magok" #: dashboard/templates/dashboard/node-detail/resources.html:7 -#: vm/models/common.py:43 +#: request/models.py:175 vm/models/common.py:43 msgid "RAM size" msgstr "RAM-méret" @@ -1663,6 +1787,7 @@ msgid "Host online" msgstr "Gép elérhető" #: dashboard/templates/dashboard/node-detail/resources.html:13 +#: request/templates/request/detail.html:79 msgid "Priority" msgstr "Prioritás" @@ -1728,61 +1853,65 @@ msgstr "" "Biztosan végrehajtja a(z) <strong>%(op)s</strong> műveletet\n" "a következőn: <a data-dismiss=\"modal\" href=\"%(url)s\">%(obj)s</a>?\n" -#: dashboard/templates/dashboard/profile.html:6 +#: dashboard/templates/dashboard/profile.html:7 #: dashboard/templates/dashboard/profile_form.html:6 msgid "Profile" msgstr "Profil" -#: dashboard/templates/dashboard/profile.html:17 +#: dashboard/templates/dashboard/profile.html:18 msgid "Log in as this user. Recommended to open in an incognito window." msgstr "" "Bejelentkezés a felhasználó nevében. Ajánlott inkognitóablakban megnyitni." -#: dashboard/templates/dashboard/profile.html:19 +#: dashboard/templates/dashboard/profile.html:20 msgid "Login as this user" msgstr "Bejelentkezés a felhasználó nevében" -#: dashboard/templates/dashboard/profile.html:38 +#: dashboard/templates/dashboard/profile.html:39 msgid "First name" msgstr "Keresztnév" -#: dashboard/templates/dashboard/profile.html:39 +#: dashboard/templates/dashboard/profile.html:40 msgid "Last name" msgstr "Vezetéknév" -#: dashboard/templates/dashboard/profile.html:47 +#: dashboard/templates/dashboard/profile.html:46 +msgid "Last login" +msgstr "Utolsó belépés" + +#: dashboard/templates/dashboard/profile.html:49 msgid "Use email address as Gravatar profile image" msgstr "E-mail cím használata a Gravatar profilkép betöltésére" -#: dashboard/templates/dashboard/profile.html:50 +#: dashboard/templates/dashboard/profile.html:52 msgid "What's Gravatar?" msgstr "Mi az a Gravatar?" -#: dashboard/templates/dashboard/profile.html:52 +#: dashboard/templates/dashboard/profile.html:54 msgid "Change my preferences" msgstr "Személyes beállítások" -#: dashboard/templates/dashboard/profile.html:66 +#: dashboard/templates/dashboard/profile.html:68 msgid "This user is not in any group." msgstr "A felhasználó nem tagja csoportnak." -#: dashboard/templates/dashboard/profile.html:75 +#: dashboard/templates/dashboard/profile.html:77 msgid "Virtual machines owned by the user" msgstr "A felhasználó virtuális gépei" -#: dashboard/templates/dashboard/profile.html:87 +#: dashboard/templates/dashboard/profile.html:89 msgid "This user have no virtual machines." msgstr "A felhasználónak nincs virtuális gépe." -#: dashboard/templates/dashboard/profile.html:96 +#: dashboard/templates/dashboard/profile.html:98 msgid "Virtual machines with access" msgstr "Elérhető virtuális gépek" -#: dashboard/templates/dashboard/profile.html:108 +#: dashboard/templates/dashboard/profile.html:110 msgid "This user have no access to any virtual machine." msgstr "A felhasználónak egy géphez sincs hozzáférése." -#: dashboard/templates/dashboard/profile.html:122 +#: dashboard/templates/dashboard/profile.html:124 msgid "Edit user" msgstr "Felhasználó szerkesztése" @@ -1840,7 +1969,7 @@ msgid "disk objects without images files" msgstr "lemezkép nélküli lemez objektumok" #: dashboard/templates/dashboard/storage/detail.html:47 -#: dashboard/templates/dashboard/storage/detail.html:57 +#: dashboard/templates/dashboard/storage/detail.html:59 msgid "None" msgstr "Nincs" @@ -1850,7 +1979,20 @@ msgstr "Árva lemezek" #: dashboard/templates/dashboard/storage/detail.html:52 msgid "image files without disk object in the database" -msgstr "" +msgstr "lemez objektum nélküli lemezképek" + +#: dashboard/templates/dashboard/storage/detail.html:76 +#: network/templates/network/record-list.html:21 +msgid "Filter by type" +msgstr "Típus szerinti szűrés" + +#: dashboard/templates/dashboard/storage/detail.html:78 +#: network/templates/network/host-list.html:26 +#: network/templates/network/record-list.html:22 +#: network/templates/network/rule-list.html:23 +#: request/templates/request/list.html:22 +msgid "ALL" +msgstr "MIND" #: dashboard/templates/dashboard/store/_list-box.html:9 #: dashboard/templates/dashboard/store/_list-box.html:27 @@ -1943,7 +2085,7 @@ msgstr "Felsorolás" #: dashboard/templates/dashboard/store/list.html:5 #: dashboard/templates/dashboard/store/upload.html:4 -#: dashboard/templates/dashboard/vm-detail/home.html:147 +#: dashboard/templates/dashboard/vm-detail/home.html:148 msgid "Store" msgstr "Tárhely" @@ -2009,7 +2151,8 @@ msgid "Currently uploading to" msgstr "Feltöltés helye:" #: dashboard/templates/dashboard/template-edit.html:7 -#: vm/models/instance.py:157 vm/models/instance.py:230 vm/models/network.py:44 +#: dashboard/views/storage.py:58 vm/models/instance.py:156 +#: vm/models/instance.py:229 vm/models/network.py:44 msgid "template" msgstr "sablon" @@ -2049,7 +2192,7 @@ msgid "Access level rights" msgstr "Hozzáférési jogosultsági szintek" #: dashboard/templates/dashboard/template-edit.html:121 -#: dashboard/templates/dashboard/vm-detail/access.html:27 +#: dashboard/templates/dashboard/vm-detail/access.html:27 request/tables.py:39 msgid "User" msgstr "Felhasználó" @@ -2283,14 +2426,6 @@ msgstr "Hozzáférés" msgid "Abort" msgstr "Megszakítás" -#: dashboard/templates/dashboard/vm-detail/_activity-timeline.html:65 -msgid "Show less activities" -msgstr "Kevesebb tevékenység megjelenítése" - -#: dashboard/templates/dashboard/vm-detail/_activity-timeline.html:67 -msgid "Show all activities" -msgstr "Összes tevékenység megjelenítése" - #: dashboard/templates/dashboard/vm-detail/_network-port-add.html:17 msgid "Add" msgstr "Hozzáadás" @@ -2379,26 +2514,30 @@ msgstr "Frissítés" msgid "Expiration" msgstr "Lejárat" -#: dashboard/templates/dashboard/vm-detail/home.html:70 +#: dashboard/templates/dashboard/vm-detail/home.html:71 msgid "Suspended at:" msgstr "Felfüggesztve:" -#: dashboard/templates/dashboard/vm-detail/home.html:76 +#: dashboard/templates/dashboard/vm-detail/home.html:77 msgid "Destroyed at:" msgstr "Megsemmisítve:" -#: dashboard/templates/dashboard/vm-detail/home.html:85 +#: dashboard/templates/dashboard/vm-detail/home.html:86 msgid "Tags" msgstr "Címkék" -#: dashboard/templates/dashboard/vm-detail/home.html:98 +#: dashboard/templates/dashboard/vm-detail/home.html:99 msgid "No tag added." msgstr "Nincs címke." -#: dashboard/templates/dashboard/vm-detail/home.html:110 +#: dashboard/templates/dashboard/vm-detail/home.html:111 msgid "Add tag" msgstr "Címke hozzáadása" +#: dashboard/templates/dashboard/vm-detail/home.html:161 +msgid "SSH keys" +msgstr "SSH kulcsok" + #: dashboard/templates/dashboard/vm-detail/network.html:8 vm/operations.py:202 msgid "add interface" msgstr "új interfész" @@ -2416,12 +2555,12 @@ msgid "edit" msgstr "szerkesztés" #: dashboard/templates/dashboard/vm-detail/network.html:37 -#: firewall/models.py:595 +#: firewall/models.py:610 msgid "IPv4 address" msgstr "IPv4 cím" #: dashboard/templates/dashboard/vm-detail/network.html:38 -#: firewall/models.py:605 +#: firewall/models.py:620 msgid "IPv6 address" msgstr "IPv6 cím" @@ -2430,12 +2569,12 @@ msgid "DNS name" msgstr "DNS név" #: dashboard/templates/dashboard/vm-detail/network.html:52 -#: network/forms.py:269 +#: network/forms.py:284 msgid "IPv4" msgstr "IPv4" #: dashboard/templates/dashboard/vm-detail/network.html:53 -#: network/forms.py:276 +#: network/forms.py:291 msgid "IPv6" msgstr "IPv6" @@ -2452,15 +2591,38 @@ msgstr "A VM-nek nincs IPv6 címe." msgid "Edit raw data" msgstr "Nyers adat szerkesztése" -#: dashboard/templates/dashboard/vm-detail/resources.html:13 +#: dashboard/templates/dashboard/vm-detail/resources.html:7 +msgid "Modify the resources" +msgstr "Módosítsa az erőforrásokat" + +#: dashboard/templates/dashboard/vm-detail/resources.html:17 msgid "Save resources" msgstr "Erőforrások mentése" -#: dashboard/templates/dashboard/vm-detail/resources.html:17 +#: dashboard/templates/dashboard/vm-detail/resources.html:21 msgid "Stop your VM to change resources." msgstr "Állítsa le a VM-et az erőforrások módosításához." -#: dashboard/templates/dashboard/vm-detail/resources.html:64 +#: dashboard/templates/dashboard/vm-detail/resources.html:26 +msgid "" +"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." +msgstr "" +"Erőforrás módosítás csak LEÁLLÍTVA állapotú gépen lehetséges. A virtuális " +"gépet ajánlott leállítani a kérés beküldése után másképpen valamikor a " +"jövőben, a kérés elfogadásakor lesz automatikusan leállítva." + +#: dashboard/templates/dashboard/vm-detail/resources.html:29 +#: request/forms.py:79 request/models.py:82 +msgid "Message" +msgstr "Üzenet" + +#: dashboard/templates/dashboard/vm-detail/resources.html:38 +msgid "Request resources" +msgstr "Erőforrások igénylése" + +#: dashboard/templates/dashboard/vm-detail/resources.html:87 msgid "Raw data" msgstr "Nyers adat" @@ -2511,6 +2673,8 @@ msgid "" " %(current)s/%(total)s\n" " " msgstr "" +"\n" +"%(current)s/%(total)s" #: dashboard/validators.py:74 msgid "Invalid template string." @@ -2536,59 +2700,75 @@ msgstr "példányok száma" msgid "Allocated memory (bytes)" msgstr "Foglalt memória (byte)" -#: dashboard/views/group.py:150 +#: dashboard/views/group.py:152 #, python-format msgid "User \"%s\" not found." msgstr "Nem található „%s” felhasználó." -#: dashboard/views/group.py:164 +#: dashboard/views/group.py:166 msgid "Group successfully renamed." msgstr "A csoport átnevezésre került." -#: dashboard/views/group.py:233 +#: dashboard/views/group.py:235 msgid "Member successfully removed from group." msgstr "A csoporttag eltávolításra került." -#: dashboard/views/group.py:266 +#: dashboard/views/group.py:268 msgid "Future user successfully removed from group." msgstr "A leendő csoporttag eltávolításra került." -#: dashboard/views/group.py:288 +#: dashboard/views/group.py:290 msgid "Group successfully deleted." msgstr "A csoport törlésre került." -#: dashboard/views/group.py:317 +#: dashboard/views/group.py:319 msgid "Create a Group" msgstr "Csoport létrehozása" -#: dashboard/views/group.py:333 +#: dashboard/views/group.py:335 msgid "Group successfully created." msgstr "A csoport létrehozásra került." -#: dashboard/views/group.py:347 +#: dashboard/views/group.py:349 msgid "Group is successfully updated." msgstr "A csoport frissítésre került." -#: dashboard/views/node.py:127 +#: dashboard/views/message.py:51 +msgid "Broadcast message successfully updated." +msgstr "Az üzenet frissítésre került." + +#: dashboard/views/message.py:59 +msgid "New broadcast message successfully created." +msgstr "Az üzenet létrehozása került." + +#: dashboard/views/node.py:128 msgid "Node successfully renamed." msgstr "A csomópont átnevezésre került." -#: dashboard/views/node.py:217 +#: dashboard/views/node.py:218 msgid "Create a node" msgstr "Új csomópont hozzáadása" -#: dashboard/views/node.py:241 +#: dashboard/views/node.py:242 msgid "Node successfully created." msgstr "A csomópont létrehozásra került." -#: dashboard/views/node.py:252 +#: dashboard/views/node.py:253 msgid "Node successfully deleted." msgstr "A csomópont törlésre került." -#: dashboard/views/node.py:288 +#: dashboard/views/node.py:289 msgid "Trait successfully added to node." msgstr "A csomópontjellemző hozzáadásra került." +#: dashboard/views/storage.py:51 +msgid "The DataStore is offline." +msgstr "Az adattár nem elérhető." + +#: dashboard/views/storage.py:57 +msgid "virtual machine" +msgstr "virtuális gép" + #: dashboard/views/store.py:73 msgid "No store." msgstr "Nincs tárhely." @@ -2686,11 +2866,11 @@ msgstr "" #: dashboard/views/template.py:444 #, python-format msgid "" -"%(user)s offered you to take the ownership of his/her template called " +"%(owner)s offered you to take the ownership of his/her template called " "%(instance)s. <a href=\"%(token)s\" class=\"btn btn-success btn-" "small\">Accept</a>" msgstr "" -"%(user)s át kívánja ruházni %(instance)s nevű sablonját Önre. <a " +"%(owner)s át kívánja ruházni %(instance)s nevű sablonját Önre. <a " "href=\"%(token)s\" class=\"btn btn-success btn-small\">Elfogadás</a>" #: dashboard/views/user.py:150 @@ -2856,79 +3036,80 @@ msgstr "Átruházás elfogadva" #: dashboard/views/util.py:667 #, python-format -msgid "Your ownership offer of %(instance)s has been accepted by %(user)s." -msgstr "%(instance)s gépre vonatkozó átruházási ajánlatát elfogadta %(user)s." +msgid "Your ownership offer of %(instance)s has been accepted by %(owner)s." +msgstr "" +"%(instance)s gépre vonatkozó átruházási ajánlatát elfogadta %(owner)s." #: dashboard/views/util.py:716 msgid "Only the owners can delete the selected object." msgstr "Csak a tulajdonos törölheti a kiválasztott objektumot." -#: dashboard/views/vm.py:88 +#: dashboard/views/vm.py:90 msgid "console access" msgstr "konzolhozzáférés" -#: dashboard/views/vm.py:197 +#: dashboard/views/vm.py:203 msgid "VM successfully renamed." msgstr "A virtuális gép átnevezésre került." -#: dashboard/views/vm.py:221 +#: dashboard/views/vm.py:227 msgid "VM description successfully updated." msgstr "A VM leírása megváltoztatásra került." -#: dashboard/views/vm.py:609 +#: dashboard/views/vm.py:615 msgid "The token has expired." msgstr "A token lejárt." -#: dashboard/views/vm.py:827 +#: dashboard/views/vm.py:845 #, python-format msgid "Failed to execute %(op)s operation on instance %(instance)s." msgstr "%(op)s végrehajtása meghiúsult a következőn: %(instance)s." -#: dashboard/views/vm.py:843 +#: dashboard/views/vm.py:861 #, python-format msgid "You are not permitted to execute %(op)s on instance %(instance)s." msgstr "Nem engedélyezett a(z) %(op)s végrehajtása a(z) %(instance)s gépen." -#: dashboard/views/vm.py:1035 +#: dashboard/views/vm.py:1053 msgid "Customize VM" msgstr "VM testreszabása" -#: dashboard/views/vm.py:1043 +#: dashboard/views/vm.py:1061 msgid "Create a VM" msgstr "VM létrehozása" -#: dashboard/views/vm.py:1096 +#: dashboard/views/vm.py:1116 #, python-format msgid "Successfully created %(count)d VM." msgid_plural "Successfully created %(count)d VMs." msgstr[0] "%(count)d VM létrehozásra került." msgstr[1] "%(count)d VM létrehozásra került." -#: dashboard/views/vm.py:1101 +#: dashboard/views/vm.py:1121 msgid "VM successfully created." msgstr "VM létrehozásra került." -#: dashboard/views/vm.py:1132 +#: dashboard/views/vm.py:1152 #, python-format msgid "Instance limit (%d) exceeded." msgstr "A példányok létrehozási korlátját (%d) túllépte." -#: dashboard/views/vm.py:1200 +#: dashboard/views/vm.py:1220 msgid "About CIRCLE Client" msgstr "A CIRCLE kliensről" -#: dashboard/views/vm.py:1290 +#: dashboard/views/vm.py:1310 msgid "transfer ownership" msgstr "tulajdon átruházása" -#: dashboard/views/vm.py:1300 +#: dashboard/views/vm.py:1320 #, python-format msgid "" -"%(user)s offered you to take the ownership of his/her virtual machine called" -" %(instance)s. <a href=\"%(token)s\" class=\"btn btn-success btn-" +"%(owner)s offered you to take the ownership of his/her virtual machine " +"called %(instance)s. <a href=\"%(token)s\" class=\"btn btn-success btn-" "small\">Accept</a>" msgstr "" -"%(user)s át kívánja ruházni %(instance)s nevű virtuális gépét Önre. <a " +"%(owner)s át kívánja ruházni %(instance)s nevű virtuális gépét Önre. <a " "href=\"%(token)s\" class=\"btn btn-success btn-small\">Elfogadás</a>" #: firewall/fields.py:42 @@ -3024,10 +3205,10 @@ msgstr "irány" msgid "If the rule matches egress or ingress packets." msgstr "A szabály kimenő vagy bejövő csomagokra illeszkedik." -#: firewall/models.py:73 firewall/models.py:345 firewall/models.py:534 -#: firewall/models.py:556 firewall/models.py:612 firewall/models.py:982 -#: firewall/models.py:1005 firewall/models.py:1074 vm/models/instance.py:136 -#: vm/models/instance.py:226 +#: firewall/models.py:73 firewall/models.py:345 firewall/models.py:539 +#: firewall/models.py:566 firewall/models.py:627 firewall/models.py:1002 +#: firewall/models.py:1030 firewall/models.py:1101 vm/models/instance.py:135 +#: vm/models/instance.py:225 msgid "description" msgstr "leírás" @@ -3114,17 +3295,17 @@ msgstr "Célport számának átírása a megadottra NAT esetén." msgid "external IPv4 address" msgstr "külső IPv4 cím" -#: firewall/models.py:123 firewall/models.py:388 firewall/models.py:539 -#: firewall/models.py:561 firewall/models.py:631 +#: firewall/models.py:123 firewall/models.py:388 firewall/models.py:544 +#: firewall/models.py:571 firewall/models.py:646 msgid "created at" msgstr "létrehozva" -#: firewall/models.py:126 firewall/models.py:392 firewall/models.py:541 -#: firewall/models.py:563 firewall/models.py:633 +#: firewall/models.py:126 firewall/models.py:392 firewall/models.py:546 +#: firewall/models.py:573 firewall/models.py:648 msgid "modified at" msgstr "módosítva" -#: firewall/models.py:129 firewall/models.py:620 +#: firewall/models.py:129 firewall/models.py:395 firewall/models.py:635 #: network/templates/network/vlan-create.html:8 #: network/templates/network/vlan-edit.html:8 vm/models/network.py:39 #: vm/models/network.py:67 @@ -3135,7 +3316,8 @@ msgstr "vlan" msgid "Vlan the rule applies to (if type is vlan)." msgstr "Erre a vlanra vonatkozik a szabály (ha a típus vlan)." -#: firewall/models.py:134 network/templates/network/vlan-group-create.html:8 +#: firewall/models.py:134 firewall/models.py:549 +#: network/templates/network/vlan-group-create.html:8 #: network/templates/network/vlan-group-edit.html:8 msgid "vlan group" msgstr "vlan-csoport" @@ -3144,7 +3326,7 @@ msgstr "vlan-csoport" msgid "Group of vlans the rule applies to (if type is vlan)." msgstr "Erre a vlan-csoportra vonatkozik a szabály (ha a típus vlan)." -#: firewall/models.py:138 firewall/models.py:998 firewall/models.py:1115 +#: firewall/models.py:138 firewall/models.py:1023 firewall/models.py:1152 #: network/templates/network/host-create.html:8 #: network/templates/network/host-edit.html:8 vm/models/network.py:69 #: vm/models/node.py:126 @@ -3155,7 +3337,8 @@ msgstr "gép" msgid "Host the rule applies to (if type is host)." msgstr "Erre a gépre vonatkozik a szabály (ha a típus gép)." -#: firewall/models.py:142 network/templates/network/group-create.html:8 +#: firewall/models.py:142 firewall/models.py:576 +#: network/templates/network/group-create.html:8 #: network/templates/network/group-edit.html:8 msgid "host group" msgstr "gépcsoport" @@ -3164,7 +3347,8 @@ msgstr "gépcsoport" msgid "Group of hosts the rule applies to (if type is host)." msgstr "Erre a gépcsoportra vonatkozik a szabály (ha a típus gép)." -#: firewall/models.py:146 network/templates/network/firewall-create.html:6 +#: firewall/models.py:146 firewall/models.py:954 +#: network/templates/network/firewall-create.html:6 #: network/templates/network/firewall-edit.html:7 msgid "firewall" msgstr "tűzfal" @@ -3350,52 +3534,60 @@ msgstr "" "tiltásához adja meg a „manual” értéket, engedélyezéséhez az első és utolsó " "érvényes címet szóközzel elválasztva." -#: firewall/models.py:399 +#: firewall/models.py:396 firewall/models.py:536 +msgid "vlans" +msgstr "vlanok" + +#: firewall/models.py:404 msgid "You cannot specify an IPv6 template if there is no IPv6 network set." msgstr "Nem adhat meg IPv6 sablont, ha nincs IPv6 hálózat beállítva." -#: firewall/models.py:405 +#: firewall/models.py:410 #, python-format msgid "%(ip6)s (translated from %(ip4)s) is outside of the IPv6 network." msgstr "%(ip6)s (ebből képezve: %(ip4)s) kívül esik az IPv6 hálózaton." -#: firewall/models.py:449 +#: firewall/models.py:454 msgid "IPv6 network is too small to map IPv4 addresses to it." msgstr "Az IPv6 hálózat túl kicsi az IPv4 címek leképezéséhez." -#: firewall/models.py:506 +#: firewall/models.py:511 msgid "All IP addresses are already in use." msgstr "Minden IP cím használatban van." -#: firewall/models.py:529 firewall/models.py:555 +#: firewall/models.py:534 firewall/models.py:565 msgid "The name of the group." msgstr "A csoport neve." -#: firewall/models.py:531 -msgid "vlans" -msgstr "vlanok" - -#: firewall/models.py:532 +#: firewall/models.py:537 msgid "The vlans which are members of the group." msgstr "A csoport tagjait képező vlanok." -#: firewall/models.py:535 firewall/models.py:557 +#: firewall/models.py:540 firewall/models.py:567 msgid "Description of the group." msgstr "A csoport leírása." -#: firewall/models.py:578 network/tables.py:139 storage/models.py:52 +#: firewall/models.py:550 +msgid "vlan groups" +msgstr "vlan-csoportok" + +#: firewall/models.py:577 +msgid "host groups" +msgstr "gépcsoportok" + +#: firewall/models.py:593 network/tables.py:139 storage/models.py:52 msgid "hostname" msgstr "gépnév" -#: firewall/models.py:579 +#: firewall/models.py:594 msgid "The alphanumeric hostname of the host, the first part of the FQDN." msgstr "A gép alfanumerikus gépneve, az FQDN első része." -#: firewall/models.py:585 +#: firewall/models.py:600 msgid "reverse" msgstr "reverz" -#: firewall/models.py:586 +#: firewall/models.py:601 msgid "" "The fully qualified reverse hostname of the host, if different than " "hostname.domain." @@ -3403,137 +3595,165 @@ msgstr "" "A gép teljes reverz tartományneve, amennyiben különbözik ettől: " "gépnév.tartomány." -#: firewall/models.py:590 network/tables.py:138 +#: firewall/models.py:605 network/tables.py:138 msgid "MAC address" msgstr "MAC cím" -#: firewall/models.py:591 +#: firewall/models.py:606 msgid "" "The MAC (Ethernet) address of the network interface. For example: " "99:AA:BB:CC:DD:EE." msgstr "A hálózati interfész MAC (Ethernet) címe. Például 99:AA:BB:CC:DD:EE." -#: firewall/models.py:596 +#: firewall/models.py:611 msgid "The real IPv4 address of the host, for example 10.5.1.34." msgstr "A gép valódi IPv4 címe, például 10.5.1.34." -#: firewall/models.py:600 +#: firewall/models.py:615 msgid "WAN IPv4 address" msgstr "WAN IPv4 cím" -#: firewall/models.py:601 +#: firewall/models.py:616 msgid "" "The public IPv4 address of the host on the wide area network, if different." msgstr "A gép nyilvános IPv4 címe a nagy kiterjedésű hálózaton, ha eltér." -#: firewall/models.py:606 +#: firewall/models.py:621 msgid "The global IPv6 address of the host, for example 2001:db:88:200::10." msgstr "A gép globális IPv6 címe, például 2001:db:88:200::10." -#: firewall/models.py:608 +#: firewall/models.py:623 msgid "shared IP" msgstr "osztott IP" -#: firewall/models.py:610 +#: firewall/models.py:625 msgid "If the given WAN IPv4 address is used by multiple hosts." msgstr "A WAN IPv4 címet több gép használja-e." -#: firewall/models.py:613 +#: firewall/models.py:628 msgid "What is this host for, what kind of machine is it." msgstr "Mi a gép célja, milyen gép ez." -#: firewall/models.py:616 +#: firewall/models.py:631 msgid "Notes" msgstr "Jegyzetek" -#: firewall/models.py:617 +#: firewall/models.py:632 msgid "location" msgstr "elhelyezés" -#: firewall/models.py:619 +#: firewall/models.py:634 msgid "The physical location of the machine." msgstr "A gép fizikai helye." -#: firewall/models.py:622 +#: firewall/models.py:637 msgid "Vlan network that the host is part of." msgstr "Az a vlan hálózat, amelynek a gép része." -#: firewall/models.py:625 +#: firewall/models.py:640 msgid "The person responsible for this host." msgstr "A gépért felelős személy." -#: firewall/models.py:627 +#: firewall/models.py:642 msgid "groups" msgstr "csoportok" -#: firewall/models.py:629 +#: firewall/models.py:644 msgid "Host groups the machine is part of." msgstr "Gépcsoportok, amelyeknek tagja a gép." -#: firewall/models.py:682 +#: firewall/models.py:697 msgid "If shared_ip has been checked, external_ipv4 has to be unique." msgstr "" "Amennyiben az osztott IP mező igaz, a külső IPv4 cím mező egyedi kell " "legyen." -#: firewall/models.py:685 +#: firewall/models.py:700 msgid "You can't use another host's NAT'd address as your own IPv4." msgstr "Nem használható másik gép NAT-olt címe saját IPv4 címként." -#: firewall/models.py:790 +#: firewall/models.py:805 #, python-format msgid "All %s ports are already in use." msgstr "Minden %s port használatban van." -#: firewall/models.py:808 +#: firewall/models.py:823 #, python-format msgid "Port %(proto)s %(public)s is already in use." msgstr "A(z) %(public)s %(proto)s port használatban van." -#: firewall/models.py:978 firewall/models.py:1007 firewall/models.py:1076 -#: firewall/models.py:1103 firewall/models.py:1124 +#: firewall/models.py:955 +msgid "firewalls" +msgstr "tűzfalak" + +#: firewall/models.py:998 firewall/models.py:1032 firewall/models.py:1103 +#: firewall/models.py:1135 firewall/models.py:1161 msgid "created_at" msgstr "létrehozva" -#: firewall/models.py:980 firewall/models.py:1009 firewall/models.py:1078 -#: firewall/models.py:1105 firewall/models.py:1126 +#: firewall/models.py:1000 firewall/models.py:1034 firewall/models.py:1105 +#: firewall/models.py:1137 firewall/models.py:1163 msgid "modified_at" msgstr "módosítva" -#: firewall/models.py:981 firewall/models.py:1003 +#: firewall/models.py:1001 firewall/models.py:1028 msgid "ttl" msgstr "ttl" -#: firewall/models.py:996 network/templates/network/domain-create.html:8 +#: firewall/models.py:1005 firewall/models.py:1021 +#: network/templates/network/domain-create.html:8 #: network/templates/network/domain-edit.html:8 msgid "domain" msgstr "tartomány" -#: firewall/models.py:1002 +#: firewall/models.py:1006 +msgid "domains" +msgstr "tartományok" + +#: firewall/models.py:1027 msgid "address" msgstr "cím" -#: firewall/models.py:1024 +#: firewall/models.py:1049 msgid "Address must be specified!" msgstr "A cím megadása kötelező." -#: firewall/models.py:1037 +#: firewall/models.py:1062 msgid "Unknown record type." msgstr "Ismeretlen rekordtípus." -#: firewall/models.py:1070 +#: firewall/models.py:1086 network/templates/network/record-create.html:8 +#: network/templates/network/record-edit.html:8 +msgid "record" +msgstr "rekord" + +#: firewall/models.py:1087 +msgid "records" +msgstr "rekordok" + +#: firewall/models.py:1097 msgid "untagged vlan" msgstr "untagged vlan" -#: firewall/models.py:1073 +#: firewall/models.py:1100 msgid "tagged vlans" msgstr "tagged vlanok" -#: firewall/models.py:1095 +#: firewall/models.py:1108 firewall/models.py:1133 +#: network/templates/network/switch-port-create.html:8 +#: network/templates/network/switch-port-edit.html:8 +msgid "switch port" +msgstr "switch port" + +#: firewall/models.py:1109 +msgid "switch ports" +msgstr "switch portok" + +#: firewall/models.py:1127 msgid "interface" msgstr "interfész" -#: firewall/models.py:1096 +#: firewall/models.py:1128 msgid "" "The name of network interface the gateway should serve this network on. For " "example eth2." @@ -3541,39 +3761,39 @@ msgstr "" "Azon hálózati interfész nevve, amelyen az útválasztó ezt a hálózatot " "kiszolgálja. Például eth2." -#: firewall/models.py:1101 network/templates/network/switch-port-create.html:8 -#: network/templates/network/switch-port-edit.html:8 -msgid "switch port" -msgstr "switch port" +#: firewall/models.py:1140 +msgid "ethernet device" +msgstr "ethernet-eszköz" -#: firewall/models.py:1117 +#: firewall/models.py:1141 +msgid "ethernet devices" +msgstr "ethernet-eszközök" + +#: firewall/models.py:1154 msgid "reason" msgstr "indok" -#: firewall/models.py:1119 +#: firewall/models.py:1156 msgid "short message" msgstr "rövid üzenet" -#: firewall/models.py:1122 +#: firewall/models.py:1159 msgid "whitelisted" -msgstr "" +msgstr "engedélyezőlistán" -#: firewall/models.py:1128 -#| msgid "Expiration" +#: firewall/models.py:1165 msgid "expires at" msgstr "lejár" -#: firewall/models.py:1138 +#: firewall/models.py:1175 msgid "blacklist item" msgstr "tiltólista eleme" -#: firewall/models.py:1139 network/templates/network/blacklist-create.html:8 -#: network/templates/network/blacklist-edit.html:8 -msgid "blacklist" -msgstr "tiltólista" +#: firewall/models.py:1176 +msgid "blacklist items" +msgstr "tiltólista elemek" #: firewall/views.py:79 -#| msgid "Enter a valid IP address. %s" msgid "Invalid IP address." msgstr "Érvénytelen IP cím." @@ -3605,31 +3825,31 @@ msgstr "" "Egy csomópont sem biztosítja a virtuális gép indításához szükséges " "jellemzőket." -#: network/forms.py:146 +#: network/forms.py:151 msgid "Generate random address." msgstr "Véletlenszerű cím generálása." -#: network/forms.py:149 +#: network/forms.py:154 msgid "Generate IPv6 pair of IPv4 address." msgstr "IPv4-es cím IPv6-os párjának generálása." -#: network/forms.py:154 +#: network/forms.py:159 msgid "Information" msgstr "Információ" -#: network/forms.py:217 +#: network/forms.py:227 msgid "External" msgstr "Külső" -#: network/forms.py:280 +#: network/forms.py:295 msgid "Generate sensible template." msgstr "Ésszerű sablon generálása." -#: network/forms.py:284 +#: network/forms.py:299 msgid "Domain name service" msgstr "DNS szolgáltatás" -#: network/forms.py:289 +#: network/forms.py:304 msgid "Info" msgstr "Infó" @@ -3643,7 +3863,7 @@ msgstr "kért IP" #: network/tables.py:155 msgid "TTL" -msgstr "" +msgstr "TTL" #: network/tables.py:186 network/tables.py:267 msgid "Short description" @@ -3661,6 +3881,11 @@ msgstr "Nincs kapcsolódó szabály." msgid "dashboard" msgstr "műszerfal" +#: network/templates/network/blacklist-create.html:8 +#: network/templates/network/blacklist-edit.html:8 +msgid "blacklist" +msgstr "tiltólista" + #: network/templates/network/blacklist-create.html:12 msgid "Create a blacklist item" msgstr "Tiltólista elemének létrehozása" @@ -3729,10 +3954,8 @@ msgstr "Gépek" #: network/templates/network/dashboard.html:107 #: network/templates/network/dashboard.html:122 #: network/templates/network/dashboard.html:137 -#, fuzzy -#| msgid "Notes, comments about the network" msgid "Hosts are machines on the network" -msgstr "Jegyzetek, megjegyzések a hálózatról" +msgstr "" #: network/templates/network/dashboard.html:30 #: network/templates/network/menu.html:8 @@ -3887,12 +4110,6 @@ msgstr "összes gép" msgid "Filter by vlans" msgstr "Vlan szerinti szűrés" -#: network/templates/network/host-list.html:26 -#: network/templates/network/record-list.html:22 -#: network/templates/network/rule-list.html:23 -msgid "ALL" -msgstr "MIND" - #: network/templates/network/index.html:11 templates/info/help.html:32 msgid "Dashboard" msgstr "Műszerfal" @@ -3930,15 +4147,11 @@ msgstr "Legutóbbi tiltólisták" msgid "IP" msgstr "IP" -#: network/templates/network/index.html:45 +#: network/templates/network/index.html:45 request/models.py:83 +#: request/templates/request/detail.html:127 msgid "Reason" msgstr "Indok" -#: network/templates/network/record-create.html:8 -#: network/templates/network/record-edit.html:8 -msgid "record" -msgstr "rekord" - #: network/templates/network/record-create.html:12 #: network/templates/network/record-list.html:11 msgid "Create a new record" @@ -3952,10 +4165,6 @@ msgstr "Rekord törlése" msgid "list of all records" msgstr "összes rekord" -#: network/templates/network/record-list.html:21 -msgid "Filter by type" -msgstr "Típus szerinti szűrés" - #: network/templates/network/rule-create.html:12 #: network/templates/network/rule-list.html:12 msgid "Create a new rule" @@ -4026,35 +4235,29 @@ msgstr "összes vlan" #: network/views.py:140 #, python-format -#| msgid "Successfully modified blacklist item%(ipv4)s - %(type)s!" msgid "Successfully modified blacklist item %(ipv4)s." msgstr "Tiltólista eleme sikeresen módosítva (%(ipv4)s)." #: network/views.py:157 #, python-format -#| msgid "Successfully created blacklist item %(ipv4)s - %(type)s!" msgid "Successfully created blacklist item %(ipv4)s" msgstr "Tiltólista eleme sikeresen létrehozva (%(ipv4)s)." #: network/views.py:192 #, python-format -#| msgid "Successfully modified domain %(name)s!" msgid "Successfully modified domain %(name)s." msgstr "A(z) %(name)s tartománynév módosításra került." #: network/views.py:219 #, python-format -#| msgid "Successfully created domain %(name)s!" msgid "Successfully created domain %(name)s." msgstr "A(z) %(name)s tartománynév létrehozásra került." #: network/views.py:236 network/views.py:563 network/views.py:845 -#| msgid "Object name does not match!" msgid "Object name does not match." msgstr "Az objektum neve nem egyezik." #: network/views.py:240 -#| msgid "Domain successfully deleted!" msgid "Domain successfully deleted." msgstr "A tartománynév törlésre került." @@ -4072,40 +4275,33 @@ msgstr "A tűzfal létrehozásra került." #: network/views.py:350 #, python-format -#| msgid "Successfully created host group %(name)s!" msgid "Successfully created host group %(name)s." msgstr "%(name)s gépcsoport létrehozásra került." #: network/views.py:358 #, python-format -#| msgid "Successfully modified host group %(name)s!" msgid "Successfully modified host group %(name)s." msgstr "%(name)s gépcsoport módosításra került." #: network/views.py:442 #, python-format -#| msgid "Successfully modified host %(hostname)s!" msgid "Successfully modified host %(hostname)s." msgstr "%(hostname)s gép módosításra került." #: network/views.py:515 #, python-format -#| msgid "Successfully created host %(hostname)s!" msgid "Successfully created host %(hostname)s." msgstr "%(hostname)s gép létrehozásra került." #: network/views.py:567 -#| msgid "Host successfully deleted!" msgid "Host successfully deleted." msgstr "A gép törlésre került." #: network/views.py:597 -#| msgid "Successfully modified record!" msgid "Successfully modified record." msgstr "A rekord módosításra került." #: network/views.py:616 -#| msgid "Successfully created record!" msgid "Successfully created record." msgstr "A rekord létrehozásra került." @@ -4122,69 +4318,57 @@ msgid "Firewall" msgstr "Tűzfal" #: network/views.py:682 -#| msgid "Successfully modified rule!" msgid "Successfully modified rule." msgstr "A szabály módosításra került." #: network/views.py:702 -#| msgid "Successfully created rule!" msgid "Successfully created rule." msgstr "A szabály létrehozásra került." #: network/views.py:738 -#| msgid "Succesfully modified switch port!" msgid "Succesfully modified switch port." msgstr "A switch-port módosításra került." #: network/views.py:757 -#| msgid "Successfully created switch port!" msgid "Successfully created switch port." msgstr "A switch-port létrehozásra került." #: network/views.py:804 #, python-format -#| msgid "Succesfully modified vlan %(name)s!" msgid "Succesfully modified vlan %(name)s." msgstr "A(z) %(name)s vlan módosításra került." #: network/views.py:824 #, python-format -#| msgid "Successfully created vlan %(name)s!" msgid "Successfully created vlan %(name)s." msgstr "A(z) %(name)s vlan létrehozásra került." #: network/views.py:849 -#| msgid "Vlan successfully deleted!" msgid "Vlan successfully deleted." msgstr "A vlan törlésre került." #: network/views.py:891 #, python-format -#| msgid "Successfully modified vlan group %(name)s!" msgid "Successfully modified vlan group %(name)s." msgstr "A(z) %(name)s vlan-csoport módosításra került." #: network/views.py:904 #, python-format -#| msgid "Successfully created vlan group %(name)s!" msgid "Successfully created vlan group %(name)s." msgstr "A(z) %(name)s vlan-csoport módosításra került." #: network/views.py:936 #, python-format -#| msgid "Successfully removed %(host)s from %(group)s group!" msgid "Successfully removed %(host)s from %(group)s group." msgstr "A(z) %(host)s csoport törlésre került a(z) %(group)s csoportból." #: network/views.py:952 #, python-format -#| msgid "Successfully added %(host)s to group %(group)s!" msgid "Successfully added %(host)s to group %(group)s." msgstr "A(z) %(host)s csoport hozzáadásra került a(z) %(group)s csoporthoz." #: network/views.py:971 #, python-format -#| msgid "Successfully deleted ethernet device %(name)s!" msgid "Successfully deleted ethernet device %(name)s." msgstr "A(z) %(name)s ethernet-eszköz törlésre került." @@ -4194,15 +4378,300 @@ msgid "Successfully added %(name)s to this switch port" msgstr "%(name)s hozzáadásra került a switch-porthoz." #: network/views.py:996 -#| msgid "Ethernet device name cannot be empty!" msgid "Ethernet device name cannot be empty." msgstr "Az ethernet-eszköz megadása kötelező." #: network/views.py:999 -#| msgid "There is already an ethernet device with that name!" msgid "There is already an ethernet device with that name." msgstr "Már létezik a megadott nevű ethernet-eszköz." +#: request/forms.py:76 +msgid "Template share" +msgstr "Sablon megosztás" + +#: request/models.py:68 vm/models/instance.py:220 +msgid "pending" +msgstr "függő" + +#: request/models.py:69 +msgid "accepted" +msgstr "elfogadott" + +#: request/models.py:70 +msgid "declined" +msgstr "elutasított" + +#: request/models.py:77 +msgid "resource request" +msgstr "erőforrás igénylés" + +#: request/models.py:78 +msgid "lease request" +msgstr "bérlet igénylés" + +#: request/models.py:79 +msgid "template access request" +msgstr "sablon hozzáférés igénylés" + +#: request/models.py:126 +msgid "Request accepted" +msgstr "Kérés elfogadva" + +#: request/models.py:137 +#, python-format +msgid "" +"Your <a href=\"%(url)s\">request</a> was declined because of the following " +"reason: %(reason)s" +msgstr "" +"Az <a href=\"%(url)s\">igénylése</a> el lett utasítva az alábbi indokkal: " +"%(reason)s" + +#: request/models.py:142 +msgid "Request declined" +msgstr "Igénylés elutasítva" + +#: request/models.py:151 vm/models/common.py:154 +#, python-format +msgid "%(name)s (suspend: %(s)s, remove: %(r)s)" +msgstr "%(name)s (felfüggesztés: %(s)s, törlés: %(r)s)" + +#: request/models.py:171 vm/models/common.py:39 +msgid "number of cores" +msgstr "magok száma" + +#: request/models.py:172 vm/models/common.py:40 +msgid "Number of virtual CPU cores available to the virtual machine." +msgstr "A virtuális gép számára elérhető CPU-magok száma." + +#: request/models.py:176 vm/models/common.py:44 +msgid "Mebibytes of memory." +msgstr "Memória mebibyte-okban." + +#: request/models.py:178 vm/models/common.py:52 vm/models/node.py:124 +msgid "priority" +msgstr "prioritás" + +#: request/models.py:179 vm/models/common.py:53 +msgid "CPU priority." +msgstr "CPU prioritás." + +#: request/models.py:191 +#, python-format +msgid "" +"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." +msgstr "" +"<a href=\"%(url)s\">%(name)s</a> erőforrásai módosultak. Magok száma: " +"%(num_cores)d, RAM-mennyiség: <span class=\"nowrap\">%(ram_size)d " +"MiB</span>, CPU prioritás: %(priority)d/100." + +#: request/models.py:215 +#, python-format +msgid "" +"The lease of <a href=\"%(url)s\">%(name)s</a> got extended. (suspend: " +"%(suspend)s, remove: %(remove)s)" +msgstr "" +"<a href=\"%(url)s\">%(name)s</a> bérlete meghosszabbítva. (felfüggesztés: " +"%(suspend)s, törlés: %(remove)s)" + +#: request/models.py:243 +#, python-format +msgid "You got access to the following template: %s" +msgid_plural "You got access to the following templates: %s" +msgstr[0] "Az alábbi sablonhoz szerzett hozzáférést: %s" +msgstr[1] "Az alábbi sablonokhoz szerzett hozzáférést: %s" + +#: request/models.py:254 +#, python-format +msgid "" +"A new <a href=\"%(request_url)s\">%(request_type)s</a> was submitted by <a " +"href=\"%(user_url)s\">%(display_name)s</a>." +msgstr "" +"Egy új <a href=\"%(request_url)s\">%(request_type)s</a> lett beküldve <a " +"href=\"%(user_url)s\">%(display_name)s</a> által." + +#: request/models.py:265 +#, python-format +msgid "New %(request_type)s" +msgstr "Új %(request_type)s" + +#: request/models.py:269 +msgid "Request submitted" +msgstr "Igénylés beküldve" + +#: request/models.py:270 +#, python-format +msgid "" +"You can view the request's status at this <a " +"href=\"%(request_url)s\">link</a>." +msgstr "" +"Az igénylés állapota megtekinthető az alábbi <a " +"href=\"%(request_url)s\">linken</a>." + +#: request/tables.py:53 +msgid "No more requests." +msgstr "Nincs több igénylés." + +#: request/templates/request/_request-template-form.html:9 +#: request/templates/request/detail.html:64 +msgid "Level" +msgstr "Szint" + +#: request/templates/request/_request-template-form.html:16 +msgid "For users who want to share the template with others." +msgstr "" +"Azon felhasználók számára akik a sablonokat meg szeretnék osztani másokkal." + +#: request/templates/request/_request-template-form.html:18 +msgid "For users who want to start a virtual machine." +msgstr "Azon felhasználók számára akik virtuális gépet akarnak indítani." + +#: request/templates/request/detail.html:7 +msgid "Request" +msgstr "Igénylés" + +#: request/templates/request/detail.html:42 +#: request/templates/request/detail.html:69 +msgid "VM name" +msgstr "Virtuális gép neve" + +#: request/templates/request/detail.html:44 +#: request/templates/request/detail.html:76 +msgid "VM description" +msgstr "Virtuális gép leírása" + +#: request/templates/request/detail.html:46 +msgid "Current lease" +msgstr "Jelenlegi bérlet" + +#: request/templates/request/detail.html:48 +msgid "Requested lease" +msgstr "Igényelt bérlet" + +#: request/templates/request/detail.html:54 +msgid "Template type" +msgstr "Sablon típus" + +#: request/templates/request/detail.html:80 +msgid "(old values in parentheses)" +msgstr "(régi értékek zárójelben)" + +#: request/templates/request/detail.html:83 +msgid "Number of cores" +msgstr "Magok száma" + +#: request/templates/request/detail.html:85 +msgid "Ram size" +msgstr "RAM-méret" + +#: request/templates/request/detail.html:99 +msgid "Reason (sent to the user if the request is declined)" +msgstr "Indok (elutasítás esetén a felhasználó megkapja)" + +#: request/templates/request/detail.html:103 +msgid "Decline" +msgstr "Elutasít" + +#: request/templates/request/detail.html:107 +msgid "You can't accept this request because of the VM's state." +msgstr "A virtuális gép állapota miatt a kérés nem elfogadható." + +#: request/templates/request/detail.html:114 +msgid "Accept" +msgstr "Elfogadás" + +#: request/templates/request/detail.html:122 +#, python-format +msgid "" +"\n" +" Closed %(closed)s by <a href=\"%(user.profile.get_absolute_url)s\">%(user)s</a>\n" +" " +msgstr "" +"\n" +"Lezárva <a href=\"%(user.profile.get_absolute_url)s\">%(user)s</a> által %(closed)s" + +#: request/templates/request/lease-type-form.html:8 +msgid "lease type" +msgstr "bérlet típus" + +#: request/templates/request/lease-type-form.html:33 +msgid "New lease type" +msgstr "Új bérlet típus" + +#: request/templates/request/list.html:15 +#: request/templates/request/type-list.html:6 +#: request/templates/request/type-list.html:24 +msgid "Request types" +msgstr "Kérés típusok" + +#: request/templates/request/list.html:21 +msgid "Filter by status" +msgstr "Állapot szerinti szűrés" + +#: request/templates/request/request-lease.html:12 +msgid "Request new lease" +msgstr "Új bérlet igénylése" + +#: request/templates/request/request-resource.html:13 +#: request/templates/request/request-resource.html:29 +msgid "Request new resources" +msgstr "Új erőforrások igénylése" + +#: request/templates/request/request-template.html:12 +msgid "Request template access" +msgstr "Sablon hozzáférés igénylése" + +#: request/templates/request/template-type-form.html:8 +msgid "template access type" +msgstr "sablon hozzáférés típus" + +#: request/templates/request/template-type-form.html:33 +msgid "New Template Access type" +msgstr "Új sablon hozzáférés típus" + +#: request/templates/request/type-list.html:17 +msgid "new lease type" +msgstr "új bérlet típus" + +#: request/templates/request/type-list.html:21 +msgid "new template access type" +msgstr "új sablon hozzáférés típus" + +#: request/templates/request/type-list.html:28 +msgid "" +"\n" +" Lease types are used for sharing leases. User can request longer ones via these.\n" +" " +msgstr "" +"\n" +"A bérlet típusok segítségével a felhasználók hosszabb lejárati időket tudnak igényelni." + +#: request/templates/request/type-list.html:37 +msgid "" +"\n" +" Using template access types users can request multiple templates with user with operator or user level access.\n" +" " +msgstr "" +"\n" +"A sablon hozzáférés típus segítségével a felhasználók felhasználó vagy operátor jogosultságot igényelhetnek a sablonokhoz." + +#: request/views.py:106 +msgid "Template access type successfully updated." +msgstr "A sablon hozzáférés típus frissítésre került." + +#: request/views.py:114 +msgid "New template access type successfully created." +msgstr "A sablon hozzáférés típus létrehozásra került." + +#: request/views.py:131 +msgid "Lease type successfully updated." +msgstr "A bérlet típus frissítésre kerül." + +#: request/views.py:139 +msgid "New lease type successfully created." +msgstr "A bérlet típus létrehozása került." + #: storage/models.py:50 msgid "path" msgstr "útvonal" @@ -4231,7 +4700,7 @@ msgstr "eszközazonosító" msgid "disk" msgstr "lemez" -#: storage/models.py:140 vm/models/instance.py:141 vm/models/instance.py:247 +#: storage/models.py:140 vm/models/instance.py:140 vm/models/instance.py:246 msgid "disks" msgstr "lemezek" @@ -4321,18 +4790,22 @@ msgstr "" msgid "Operation aborted by user." msgstr "A műveletet a felhasználó megszakította." +#: templates/403.html:6 templates/500.html:6 +msgid ":(" +msgstr ":(" + +#: templates/403.html:18 +msgid "Forbidden" +msgstr "Tiltva" + #: templates/404.html:4 templates/404.html.py:6 msgid "Page not found" msgstr "Az oldal nem található" -#: templates/404.html:9 +#: templates/404.html:15 msgid "This page does not exist." msgstr "Az oldal nem létezik." -#: templates/500.html:6 -msgid ":(" -msgstr ":(" - #: templates/500.html:18 msgid "Internal Server Error... Please leave the server alone..." msgstr "Kiszolgálóoldali hiba. Ne bántsa a szervert." @@ -4909,18 +5382,6 @@ msgstr "" "A menedzser újraindítása miatt a tevékenység lezárásra került. Próbálja " "újra." -#: vm/models/common.py:39 -msgid "number of cores" -msgstr "magok száma" - -#: vm/models/common.py:40 -msgid "Number of virtual CPU cores available to the virtual machine." -msgstr "A virtuális gép számára elérhető CPU-magok száma." - -#: vm/models/common.py:44 -msgid "Mebibytes of memory." -msgstr "Memória mebibyte-okban." - #: vm/models/common.py:46 msgid "maximal RAM size" msgstr "maximális RAM-méret" @@ -4933,14 +5394,6 @@ msgstr "Felső memóriaméret-korlát ballooning esetén." msgid "architecture" msgstr "architektúra" -#: vm/models/common.py:52 vm/models/node.py:124 -msgid "priority" -msgstr "prioritás" - -#: vm/models/common.py:53 -msgid "CPU priority." -msgstr "CPU prioritás." - #: vm/models/common.py:66 msgid "Name of base resource configuration." msgstr "Alap erőforráskonfiguráció neve." @@ -4969,65 +5422,60 @@ msgstr "Létrehozhat bérlési módot." msgid "never" msgstr "soha" -#: vm/models/common.py:154 -#, python-format -msgid "%(name)s (suspend: %(s)s, remove: %(r)s)" -msgstr "%(name)s (felfüggesztés: %(s)s, törlés: %(r)s)" - -#: vm/models/instance.py:99 +#: vm/models/instance.py:98 msgid "Primary remote access method." msgstr "Elsődleges távoli elérési mód." -#: vm/models/instance.py:100 +#: vm/models/instance.py:99 msgid "boot menu" msgstr "rendszerbetöltő menüje" -#: vm/models/instance.py:102 +#: vm/models/instance.py:101 msgid "Show boot device selection menu on boot." msgstr "" "A rendszerbetöltés eszközének kiválasztását lehetővé tevő menü megjelenítése" " indításkor." -#: vm/models/instance.py:103 +#: vm/models/instance.py:102 msgid "Preferred expiration periods." msgstr "Javasolt bérlési mód." -#: vm/models/instance.py:105 +#: vm/models/instance.py:104 msgid "raw_data" msgstr "nyers adat" -#: vm/models/instance.py:106 +#: vm/models/instance.py:105 msgid "Additional libvirt domain parameters in XML format." msgstr "További libvirt domain-paraméterek XML formátumban." -#: vm/models/instance.py:108 +#: vm/models/instance.py:107 msgid "" "A set of traits required for a node to declare to be suitable for hosting " "the VM." msgstr "A VM indításához szükséges csomópontjellemzők halmaza." -#: vm/models/instance.py:111 +#: vm/models/instance.py:110 msgid "required traits" msgstr "elvárt jellemzők" -#: vm/models/instance.py:112 +#: vm/models/instance.py:111 msgid "operating system" msgstr "operációs rendszer" -#: vm/models/instance.py:113 +#: vm/models/instance.py:112 msgid "" "Name of operating system in format like \"Ubuntu 12.04 LTS Desktop amd64\"." msgstr "Az operációs rendszer neve. Például „Ubuntu 12.04 LTS Desktop amd64”." -#: vm/models/instance.py:115 vm/models/node.py:139 +#: vm/models/instance.py:114 vm/models/node.py:139 msgid "tags" msgstr "címkék" -#: vm/models/instance.py:116 +#: vm/models/instance.py:115 msgid "has agent" msgstr "van ügynöke" -#: vm/models/instance.py:118 +#: vm/models/instance.py:117 msgid "" "If the machine has agent installed, and the manager should wait for its " "start." @@ -5035,165 +5483,161 @@ msgstr "" "A gépre telepítve van-e az ügynökszoftver, vagyis a menedzser várjon-e az " "indulására." -#: vm/models/instance.py:138 +#: vm/models/instance.py:137 msgid "parent template" msgstr "szülősablon" -#: vm/models/instance.py:140 +#: vm/models/instance.py:139 msgid "Template which this one is derived of." msgstr "Az a sablon, amelyből az aktuális származik." -#: vm/models/instance.py:143 +#: vm/models/instance.py:142 msgid "Disks which are to be mounted." msgstr "A csatolandó lemezek." -#: vm/models/instance.py:151 +#: vm/models/instance.py:150 msgid "Can create an instance template." msgstr "Létrehozhat példánysablont." -#: vm/models/instance.py:153 +#: vm/models/instance.py:152 msgid "Can create an instance template (base)." msgstr "Létrehozhat példánysablont (alapokból)." -#: vm/models/instance.py:155 +#: vm/models/instance.py:154 msgid "Can change resources of a template." msgstr "Változtathatja egy sablon erőforrásait." -#: vm/models/instance.py:158 +#: vm/models/instance.py:157 msgid "templates" msgstr "sablonok" -#: vm/models/instance.py:216 +#: vm/models/instance.py:215 msgid "no state" msgstr "nincs állapot" -#: vm/models/instance.py:217 +#: vm/models/instance.py:216 msgid "running" msgstr "fut" -#: vm/models/instance.py:218 +#: vm/models/instance.py:217 msgid "stopped" msgstr "leállítva" -#: vm/models/instance.py:219 +#: vm/models/instance.py:218 msgid "suspended" msgstr "felfüggesztve" -#: vm/models/instance.py:220 +#: vm/models/instance.py:219 msgid "error" msgstr "hiba" #: vm/models/instance.py:221 -msgid "pending" -msgstr "függő" - -#: vm/models/instance.py:222 msgid "destroyed" msgstr "megsemmisítve" -#: vm/models/instance.py:225 +#: vm/models/instance.py:224 msgid "Human readable name of instance." msgstr "A példány olvasható neve." -#: vm/models/instance.py:229 +#: vm/models/instance.py:228 msgid "Template the instance derives from." msgstr "Az a sablon, amelyből a példány származik." -#: vm/models/instance.py:231 +#: vm/models/instance.py:230 msgid "Original password of the instance." msgstr "A példány eredeti jelszava." -#: vm/models/instance.py:232 +#: vm/models/instance.py:231 msgid "password" msgstr "jelszó" -#: vm/models/instance.py:234 +#: vm/models/instance.py:233 msgid "time of suspend" msgstr "felfüggesztés ideje" -#: vm/models/instance.py:235 +#: vm/models/instance.py:234 msgid "Proposed time of automatic suspension." msgstr "A felfüggesztés kijelölt ideje." -#: vm/models/instance.py:238 +#: vm/models/instance.py:237 msgid "time of delete" msgstr "törlés ideje" -#: vm/models/instance.py:239 +#: vm/models/instance.py:238 msgid "Proposed time of automatic deletion." msgstr "Automatikus törlés kijelölt ideje." -#: vm/models/instance.py:243 +#: vm/models/instance.py:242 msgid "Current hypervisor of this instance." msgstr "A példány jelenlegi hypervisorja." -#: vm/models/instance.py:244 +#: vm/models/instance.py:243 msgid "host node" msgstr "csomópont" -#: vm/models/instance.py:246 +#: vm/models/instance.py:245 msgid "Set of mounted disks." msgstr "1Csatolt lemezek halmaza." -#: vm/models/instance.py:249 +#: vm/models/instance.py:248 msgid "TCP port where VNC console listens." msgstr "Az a TCP port, amelyen a VNC konzol hallgat." -#: vm/models/instance.py:250 +#: vm/models/instance.py:249 msgid "vnc_port" msgstr "VNC port" -#: vm/models/instance.py:254 +#: vm/models/instance.py:253 msgid "The virtual machine's time of destruction." msgstr "A virtuális gép megsemmisítésének ideje." -#: vm/models/instance.py:264 +#: vm/models/instance.py:263 msgid "Can access the graphical console of a VM." msgstr "Elérheti a VM grafikus konzolját." -#: vm/models/instance.py:265 +#: vm/models/instance.py:264 msgid "Can change resources of a running VM." msgstr "Megváltoztathatja a VM erőforrásait." -#: vm/models/instance.py:266 +#: vm/models/instance.py:265 msgid "Can change resources of a new VM." msgstr "Megválaszthatja egy új VM erőforrásait." -#: vm/models/instance.py:267 +#: vm/models/instance.py:266 msgid "Can create a new VM." msgstr "Létrehozhat új VM-et." -#: vm/models/instance.py:268 +#: vm/models/instance.py:267 msgid "Can redeploy a VM." msgstr "Újból létrehozhat futó VM-et." -#: vm/models/instance.py:269 +#: vm/models/instance.py:268 msgid "Can configure port forwards." msgstr "Beállíthat porttovábbításokat." -#: vm/models/instance.py:270 +#: vm/models/instance.py:269 msgid "Can recover a destroyed VM." msgstr "Visszaállíthat egy megsemmisített VM-et." -#: vm/models/instance.py:271 +#: vm/models/instance.py:270 msgid "Can change VM state to NOSTATE." msgstr "Átállíthatja a VM állapotát NOSTATE-re." -#: vm/models/instance.py:274 +#: vm/models/instance.py:273 msgid "instances" msgstr "példányok" -#: vm/models/instance.py:286 +#: vm/models/instance.py:285 #, python-format msgid "Instance %(instance)s has already been destroyed." msgstr "%(instance)s példány már meg van semmisítve." -#: vm/models/instance.py:290 +#: vm/models/instance.py:289 #, python-format msgid "No agent software is running on instance %(instance)s." msgstr "Nem fut ügynökszoftver a következőn: %(instance)s." -#: vm/models/instance.py:294 +#: vm/models/instance.py:293 #, python-format msgid "" "Current state (%(state)s) of instance %(instance)s is inappropriate for the " @@ -5202,16 +5646,16 @@ msgstr "" "A(z) %(instance)s példány aktuális állapota (%(state)s) nem megfelelő a " "választott művelethez." -#: vm/models/instance.py:374 +#: vm/models/instance.py:373 msgid "create instance" msgstr "példány létrehozása" -#: vm/models/instance.py:451 +#: vm/models/instance.py:450 #, python-format msgid "vm state changed to %(state)s on %(node)s" msgstr "VM állapota erre változott: %(state)s (ezen: %(node)s)" -#: vm/models/instance.py:647 +#: vm/models/instance.py:646 #, python-format msgid "" "Your instance <a href=\"%(url)s\">%(instance)s</a> is going to expire. It " @@ -5221,7 +5665,7 @@ msgstr "" "<a href=\"%(url)s\">%(instance)s</a> gépe hamarosan lejár:\n" "%(suspend)s időpontban felfüggesztésre, %(delete)s időpontban törlésre kerül. Kérjük, <a href=\"%(token)s\">újítsa meg</a> vagy <a href=\"%(url)s\">törölje</a> most." -#: vm/models/instance.py:659 +#: vm/models/instance.py:658 #, python-format msgid "" "%(failed)s notifications failed and %(success) succeeded. Failed ones are: " @@ -5230,7 +5674,7 @@ msgstr "" "%(failed)s értesítés sikertelen és %(success) sikeres. A sikertelenek: " "%(faileds)s." -#: vm/models/instance.py:661 +#: vm/models/instance.py:660 #, python-format msgid "" "%(failed)s notifications failed and %(success) succeeded. Failed ones are: " @@ -5239,16 +5683,16 @@ msgstr "" "%(failed)s értesítés sikertelen és %(success) sikeres. A sikertelenek: " "%(faileds_ex)s." -#: vm/models/instance.py:669 +#: vm/models/instance.py:668 #, python-format msgid "%(success)s notifications succeeded." msgstr "%(success)s sikeres értesítés." -#: vm/models/instance.py:674 +#: vm/models/instance.py:673 msgid "notify owner about expiration" msgstr "tulaj értesítése a lejáratról" -#: vm/models/instance.py:682 +#: vm/models/instance.py:681 #, python-format msgid "%(instance)s expiring soon" msgstr "%(instance)s hamarosan lejár" @@ -5302,10 +5746,6 @@ msgstr "Csomóponthasználat prioritása." msgid "Host in firewall." msgstr "Tűzfalbeli gép." -#: vm/models/node.py:128 -msgid "enabled" -msgstr "engedélyezve" - #: vm/models/node.py:129 msgid "Indicates whether the node can be used for hosting." msgstr "A csomópont használható-e gépek fogadására." @@ -5455,73 +5895,73 @@ msgstr "" msgid "virtual machine successfully deployed to node: %(node)s" msgstr "a virtuális gép sikeresen elindítva a következő csomóponton: %(node)s" -#: vm/operations.py:389 vm/operations.py:564 vm/operations.py:930 +#: vm/operations.py:394 vm/operations.py:572 vm/operations.py:938 msgid "deploy network" msgstr "hálózati kapcsolat létrehozása" -#: vm/operations.py:401 vm/operations.py:582 vm/operations.py:681 +#: vm/operations.py:406 vm/operations.py:590 vm/operations.py:689 msgid "wait operating system loading" msgstr "várakozás az operációs rendszer betöltésére" -#: vm/operations.py:406 +#: vm/operations.py:411 msgid "deploy vm" msgstr "vm indítása" -#: vm/operations.py:407 +#: vm/operations.py:412 msgid "Deploy virtual machine." msgstr "Virtuális gép létrehozása." -#: vm/operations.py:416 +#: vm/operations.py:421 msgid "deploy virtual machine" msgstr "virtuális gép létrehozása" -#: vm/operations.py:417 +#: vm/operations.py:422 #, python-format msgid "deploy vm to %(node)s" msgstr "vm létrehozása: %(node)s" -#: vm/operations.py:423 +#: vm/operations.py:428 msgid "deploy disks" msgstr "lemez létrehozása" -#: vm/operations.py:424 +#: vm/operations.py:429 msgid "Deploy all associated disks." msgstr "Csatolt lemezek létrehozása." -#: vm/operations.py:441 +#: vm/operations.py:446 msgid "boot virtual machine" msgstr "virtuális gép indítása" -#: vm/operations.py:449 +#: vm/operations.py:454 msgid "destroy" msgstr "megsemmisítés" -#: vm/operations.py:450 +#: vm/operations.py:455 msgid "Permanently destroy virtual machine, its network settings and disks." msgstr "" "Virtuális gép és lemezeinek, hálózati beállításainak végleges eltávolítása." -#: vm/operations.py:459 +#: vm/operations.py:467 msgid "destroy network" msgstr "hálózat megsemmisítése" -#: vm/operations.py:470 +#: vm/operations.py:478 msgid "destroy disks" msgstr "lemez megsemmisítése" -#: vm/operations.py:489 +#: vm/operations.py:497 msgid "destroy virtual machine" msgstr "virtuális gép megsemmisítése" -#: vm/operations.py:497 +#: vm/operations.py:505 msgid "removing memory dump" msgstr "memóriamentés törlése" -#: vm/operations.py:511 +#: vm/operations.py:519 msgid "migrate" msgstr "migrálás" -#: vm/operations.py:512 +#: vm/operations.py:520 msgid "" "Move a running virtual machine to an other worker node keeping its full " "state." @@ -5529,39 +5969,39 @@ msgstr "" "A virtuális gép mozgatása egy másik számítási csomópontra állapotának " "megtartásával." -#: vm/operations.py:529 +#: vm/operations.py:537 msgid "redeploy network (rollback)" msgstr "hálózati kapcsolat újraépítése (visszagörgetés)" -#: vm/operations.py:536 +#: vm/operations.py:544 msgid "schedule" msgstr "ütemezés" -#: vm/operations.py:543 +#: vm/operations.py:551 #, python-format msgid "migrate to %(node)s" msgstr "migrálás %(node)s csomópontra" -#: vm/operations.py:554 vm/operations.py:879 +#: vm/operations.py:562 vm/operations.py:887 msgid "shutdown network" msgstr "hálózati kapcsolat leállítása" -#: vm/operations.py:571 +#: vm/operations.py:579 msgid "reboot" msgstr "újraindítás" -#: vm/operations.py:572 +#: vm/operations.py:580 msgid "" "Warm reboot virtual machine by sending Ctrl+Alt+Del signal to its console." msgstr "" "Virtuális gép újraindítása a konzoljára a Ctrl+Alt+Del kombináció " "küldésével." -#: vm/operations.py:588 +#: vm/operations.py:596 msgid "remove interface" msgstr "interfész törlése" -#: vm/operations.py:589 +#: vm/operations.py:597 msgid "" "Remove the specified network interface and erase IP address allocations, " "related firewall rules and hostnames." @@ -5569,68 +6009,68 @@ msgstr "" "A kiválasztott hálózati interfész eltávolítása, a foglalt IP címek, " "tűzfalszabályok és gépnevek törlése." -#: vm/operations.py:605 +#: vm/operations.py:613 #, python-format msgid "remove %(vlan)s interface" msgstr "%(vlan)s interfész törlése" -#: vm/operations.py:612 +#: vm/operations.py:620 msgid "close port" msgstr "port bezárása" -#: vm/operations.py:613 +#: vm/operations.py:621 msgid "Close the specified port." msgstr "A megadott port bezárása." -#: vm/operations.py:622 +#: vm/operations.py:630 #, python-format msgid "close %(proto)s/%(port)d on %(host)s" msgstr "%(proto)s/%(port)d bezárása ezen: %(host)s" -#: vm/operations.py:630 +#: vm/operations.py:638 msgid "open port" msgstr "port nyitása" -#: vm/operations.py:631 +#: vm/operations.py:639 msgid "Open the specified port." msgstr "A megadott port kinyitása." -#: vm/operations.py:640 +#: vm/operations.py:648 #, python-format msgid "open %(proto)s/%(port)d on %(host)s" msgstr "%(proto)s/%(port)d kinyitása ezen: %(host)s" -#: vm/operations.py:647 +#: vm/operations.py:655 msgid "remove disk" msgstr "lemez eltávolítása" -#: vm/operations.py:648 +#: vm/operations.py:656 msgid "" "Remove the specified disk from the virtual machine, and destroy the data." msgstr "A megadott lemez eltávolítása a virtuális gépből és az adat törlése." -#: vm/operations.py:658 +#: vm/operations.py:666 msgid "destroy disk" msgstr "lemez megsemmisítése" -#: vm/operations.py:664 +#: vm/operations.py:672 #, python-format msgid "remove disk %(name)s" msgstr "%(name)s lemez eltávolítása" -#: vm/operations.py:671 vm/operations.py:1082 +#: vm/operations.py:679 vm/operations.py:1090 msgid "reset" msgstr "reset" -#: vm/operations.py:672 +#: vm/operations.py:680 msgid "Cold reboot virtual machine (power cycle)." msgstr "Virtuális gép hideg újraindítása (hálózati tápellátás megszakítása)." -#: vm/operations.py:687 +#: vm/operations.py:695 msgid "save as template" msgstr "mentés sablonként" -#: vm/operations.py:688 +#: vm/operations.py:696 msgid "" "Save virtual machine as a template so they can be shared with users and " "groups. Anyone who has access to a template (and to the networks it uses) " @@ -5640,16 +6080,16 @@ msgstr "" "felhasználókkal és csoportokkal. Mindenki, aki hozzáférést kap egy sablonhoz" " (és az általa használt hálózatokhoz), képes lesz egy példányát elindítani." -#: vm/operations.py:764 +#: vm/operations.py:772 #, python-format msgid "saving disk %(name)s" msgstr "%(name)s lemez mentése" -#: vm/operations.py:796 +#: vm/operations.py:804 msgid "shutdown" msgstr "leállítás" -#: vm/operations.py:797 +#: vm/operations.py:805 msgid "" "Try to halt virtual machine by a standard ACPI signal, allowing the " "operating system to keep a consistent state. The operation will fail if the " @@ -5659,7 +6099,7 @@ msgstr "" "operációs rendszer számár a szabályos leállást. A művelet meghiúsul, ha a " "gép nem áll le." -#: vm/operations.py:816 +#: vm/operations.py:824 msgid "" "The virtual machine did not switch off in the provided time limit. Most of " "the time this is caused by incorrect ACPI settings. You can also try to " @@ -5669,11 +6109,11 @@ msgstr "" " ez a nem megfelelő ACPI beállítások miatt van. Megpróbálhatja a gépet az " "operációs rendszerből, kézzel leállítani." -#: vm/operations.py:828 +#: vm/operations.py:836 msgid "shut off" msgstr "kikapcsolás" -#: vm/operations.py:829 +#: vm/operations.py:837 msgid "" "Forcibly halt a virtual machine without notifying the operating system. This" " operation will even work in cases when shutdown does not, but the operating" @@ -5686,11 +6126,11 @@ msgstr "" "rendszer és a fájlrendszer sérülhet, adatvesztés történhet. A művelet hatása" " hasonló, mint egy fizikai gép tápellátásának megszüntetése." -#: vm/operations.py:852 +#: vm/operations.py:860 msgid "sleep" msgstr "altatás" -#: vm/operations.py:853 +#: vm/operations.py:861 msgid "" "Suspend virtual machine. This means the machine is stopped and its memory is" " saved to disk, so if the machine is waked up, all the applications will " @@ -5706,15 +6146,15 @@ msgstr "" "megállhatnak visszaállítás után. A felfüggesztés ideje alatt a virtuális gép" " csak tárterületet és hálózati erőforrásokat foglal." -#: vm/operations.py:887 +#: vm/operations.py:895 msgid "suspend virtual machine" msgstr "virtuális gép felfüggesztése" -#: vm/operations.py:901 +#: vm/operations.py:909 msgid "wake up" msgstr "virtuális gép ébresztése" -#: vm/operations.py:902 +#: vm/operations.py:910 msgid "" "Wake up sleeping (suspended) virtual machine. This will load the saved " "memory of the system and start the virtual machine from this state." @@ -5722,15 +6162,15 @@ msgstr "" "Alvó (felfüggesztett) gép ébresztése: az elmentett memóriatartalom " "visszatöltése és a virtuális gép indítása ebből a mentett állapotból." -#: vm/operations.py:941 +#: vm/operations.py:949 msgid "resume virtual machine" msgstr "virtuális gép ébresztése" -#: vm/operations.py:955 +#: vm/operations.py:963 msgid "renew" msgstr "megújítás" -#: vm/operations.py:956 +#: vm/operations.py:964 msgid "" "Virtual machines are suspended and destroyed after they expire. This " "operation renews expiration times according to the lease type. If the " @@ -5740,7 +6180,7 @@ msgstr "" " a művelet megújítja a bérletet a kiválasztott típusnak megfelelően. Ha egy " "gép közeledik a lejárathoz, a tulajdonost értesítjük." -#: vm/operations.py:969 +#: vm/operations.py:977 msgid "" "Renewing the machine with the selected lease would result in its suspension " "time get earlier than before." @@ -5748,7 +6188,7 @@ msgstr "" "A gép megújítása a kiválasztott bérleti mód mellett a felfüggesztési időt " "korábbra állította volna, mint a jelenlegi érték." -#: vm/operations.py:974 +#: vm/operations.py:982 msgid "" "Renewing the machine with the selected lease would result in its delete time" " get earlier than before." @@ -5756,17 +6196,17 @@ msgstr "" "A gép megújítása a kiválasztott bérleti mód mellett a törlési időt korábbra " "állította volna, mint a jelenlegi érték." -#: vm/operations.py:982 +#: vm/operations.py:990 #, python-format msgid "Renewed to suspend at %(suspend)s and destroy at %(delete)s." msgstr "" "Megújítás után felfüggesztés ideje: %(suspend)s, a törlésé: %(delete)s." -#: vm/operations.py:989 +#: vm/operations.py:997 msgid "emergency state change" msgstr "vész-állapotváltás" -#: vm/operations.py:990 +#: vm/operations.py:998 msgid "" "Change the virtual machine state to NOSTATE. This should only be used if " "manual intervention was needed in the virtualization layer, and the machine " @@ -5777,15 +6217,15 @@ msgstr "" "rétegben, és úgy szeretné a gépet újból elindítani, hogy ne vesszenek el " "lemezei vagy hálózati erőforrásai." -#: vm/operations.py:1003 +#: vm/operations.py:1011 msgid "Activity is forcibly interrupted." msgstr "A tevékenység erőszakos megszakításra került." -#: vm/operations.py:1018 +#: vm/operations.py:1026 msgid "redeploy" msgstr "újbóli létrehozás" -#: vm/operations.py:1019 +#: vm/operations.py:1027 msgid "" "Change the virtual machine state to NOSTATE and redeploy the VM. This " "operation allows starting machines formerly running on a failed node." @@ -5794,52 +6234,57 @@ msgstr "" "létrehozása. Ez a művelet lehetővé teszi olyan gépek elindítását, amelyek " "korábban egy meghibásodott csomóponton futnak." -#: vm/operations.py:1055 +#: vm/operations.py:1063 msgid "You cannot call this operation on an offline node." msgstr "Nem hívható ez a művelet elérhetetlen csomópontra." -#: vm/operations.py:1083 +#: vm/operations.py:1091 msgid "Disable missing node and redeploy all instances on other ones." msgstr "Hiányzó csomópont letiltása és az összes példány elindítása a többin." -#: vm/operations.py:1093 +#: vm/operations.py:1101 msgid "You cannot reset a disabled or online node." msgstr "Tiltott vagy elérhető csomópont resetelése nem lehetséges." -#: vm/operations.py:1101 vm/operations.py:1121 +#: vm/operations.py:1106 #, python-format -msgid "migrate %(instance)s (%(pk)s)" -msgstr "%(instance)s (%(pk)s) migrálása" +msgid "redeploy %(instance)s (%(pk)s)" +msgstr "%(instance)s (%(pk)s) újbóli létrehozása" -#: vm/operations.py:1110 +#: vm/operations.py:1119 msgid "flush" msgstr "ürítés" -#: vm/operations.py:1111 +#: vm/operations.py:1120 msgid "Passivate node and move all instances to other ones." msgstr "" "A csomópont passzívra állítása és az összes példány másikakra mozgatása." #: vm/operations.py:1130 +#, python-format +msgid "migrate %(instance)s (%(pk)s)" +msgstr "%(instance)s (%(pk)s) migrálása" + +#: vm/operations.py:1139 msgid "activate" msgstr "aktiválás" -#: vm/operations.py:1131 +#: vm/operations.py:1140 msgid "" "Make node active, i.e. scheduler is allowed to deploy virtual machines to " "it." msgstr "" "Csomópont aktívvá tétele: az ütemező indíthat virtuális gépeket rajta." -#: vm/operations.py:1139 +#: vm/operations.py:1148 msgid "You cannot activate an active node." msgstr "Aktív csomópont aktiválása nem lehetséges." -#: vm/operations.py:1151 +#: vm/operations.py:1160 msgid "passivate" msgstr "passziválás" -#: vm/operations.py:1152 +#: vm/operations.py:1161 msgid "" "Make node passive, i.e. scheduler is denied to deploy virtual machines to " "it, but remaining instances and the ones manually migrated will continue " @@ -5848,31 +6293,31 @@ msgstr "" "Csomópont passzívvá tétele: az ütemező nem indíthat rajta virtuális gépeket," " azonban a megmaradt példányok és a kézzel idemigráltak tovább működnek." -#: vm/operations.py:1160 +#: vm/operations.py:1169 msgid "You cannot passivate a passive node." msgstr "Passzív csomópont passziválása nem lehetséges." -#: vm/operations.py:1173 +#: vm/operations.py:1182 msgid "disable" msgstr "tiltás" -#: vm/operations.py:1174 +#: vm/operations.py:1183 msgid "Disable node." msgstr "Csomópont tiltása." -#: vm/operations.py:1181 +#: vm/operations.py:1190 msgid "You cannot disable a disabled node." msgstr "Tiltott csomópont tiltása nem lehetséges." -#: vm/operations.py:1184 +#: vm/operations.py:1193 msgid "You cannot disable a node which is hosting instances." msgstr "Nem tiltható le olyan csomópont, amelyen még futnak példányok." -#: vm/operations.py:1197 +#: vm/operations.py:1206 msgid "update node" msgstr "csomópont frissítése" -#: vm/operations.py:1198 +#: vm/operations.py:1207 msgid "" "Upgrade or install node software (vmdriver, agentdriver, monitor-client) " "with Salt." @@ -5880,41 +6325,41 @@ msgstr "" "Csomópont frissítése vagy telepítése (vmdriver, agentdriver, monitor-client)" " Salt segítségével." -#: vm/operations.py:1216 +#: vm/operations.py:1225 #, python-format msgid "No minions matched the target (%(target)s). Data: (%(data)s)" msgstr "" -#: vm/operations.py:1221 vm/operations.py:1232 +#: vm/operations.py:1230 vm/operations.py:1241 #, python-format msgid "Unhandled exception: %(msg)s" msgstr "Kezeletlen kivétel: %(msg)s" -#: vm/operations.py:1228 +#: vm/operations.py:1237 msgid "upgrade packages" msgstr "csomagok frissítése" -#: vm/operations.py:1243 +#: vm/operations.py:1252 #, python-format msgid "Upgraded: %(upgraded)s, Installed: %(installed)s, Removed: %(removed)s" msgstr "" "Frissítve: %(upgraded)s, Telepítve: %(installed)s, Törölve: %(removed)s" -#: vm/operations.py:1256 +#: vm/operations.py:1265 #, python-format msgid "Changes: %(changes)s Comment: %(comment)s" -msgstr "" +msgstr "Változások: %(changes)s Megjegyzés: %(comment)s" -#: vm/operations.py:1264 +#: vm/operations.py:1273 #, python-format msgid "Failed: %(failed)s" msgstr "Meghiúsult: %(failed)s" -#: vm/operations.py:1270 +#: vm/operations.py:1279 msgid "screenshot" msgstr "képernyőkép" -#: vm/operations.py:1271 +#: vm/operations.py:1280 msgid "" "Get a screenshot about the virtual machine's console. A key will be pressed " "on the keyboard to stop screensaver." @@ -5922,11 +6367,11 @@ msgstr "" "Képernyőkép készítése a virtuális gép konzoljáról. Egy billentyűnyomást " "követően készül a kép a képernyővédő miatt." -#: vm/operations.py:1283 +#: vm/operations.py:1292 msgid "recover" msgstr "visszaállítás" -#: vm/operations.py:1284 +#: vm/operations.py:1293 msgid "" "Try to recover virtual machine disks from destroyed state. Network resources" " (allocations) are already lost, so you will have to manually add interfaces" @@ -5936,19 +6381,19 @@ msgstr "" "hálózati erőforrások foglalásai már végleg elvesztek, így az interfészeket " "kézzel kell a visszaállítás után pótolni." -#: vm/operations.py:1301 +#: vm/operations.py:1310 msgid "recover instance" msgstr "példány helyreállítása" -#: vm/operations.py:1324 +#: vm/operations.py:1333 msgid "resources change" msgstr "erőforrások módosítása" -#: vm/operations.py:1325 +#: vm/operations.py:1334 msgid "Change resources of a stopped virtual machine." msgstr "Leállított virtuális gép erőforrásainak változtatása." -#: vm/operations.py:1342 +#: vm/operations.py:1361 #, python-format msgid "" "Priority: %(priority)s, Num cores: %(num_cores)s, Ram size: %(ram_size)s" @@ -5956,11 +6401,11 @@ msgstr "" "Prioritás: %(priority)s, magok száma: %(num_cores)s, memória mérete: " "%(ram_size)s" -#: vm/operations.py:1351 +#: vm/operations.py:1370 msgid "password reset" msgstr "jelszó visszaállítása" -#: vm/operations.py:1352 +#: vm/operations.py:1371 msgid "" "Generate and set a new login password on the virtual machine. This operation" " requires the agent running. Resetting the password is not warranted to " @@ -5970,52 +6415,60 @@ msgstr "" "művelet megköveteli az ügynök futását. A jelszó átállítása nem garantálja a " "sikeres belépést, mivel más beállítások is megakadályozhatják ezt." -#: vm/operations.py:1376 +#: vm/operations.py:1395 +msgid "install SSH keys" +msgstr "SSH kulcs hozzáadása" + +#: vm/operations.py:1410 +msgid "remove SSH keys" +msgstr "SSH kulcs törlése" + +#: vm/operations.py:1423 msgid "agent" msgstr "ügynök" -#: vm/operations.py:1417 +#: vm/operations.py:1464 msgid "starting" msgstr "indítás" -#: vm/operations.py:1435 +#: vm/operations.py:1482 msgid "wait agent restarting" msgstr "várakozás az ügynök újraindulására" -#: vm/operations.py:1452 +#: vm/operations.py:1500 msgid "cleanup" msgstr "takarítás" -#: vm/operations.py:1458 +#: vm/operations.py:1506 msgid "set time" msgstr "óra beállítása" -#: vm/operations.py:1469 +#: vm/operations.py:1517 msgid "set hostname" msgstr "gépnév beállítása" -#: vm/operations.py:1480 +#: vm/operations.py:1528 msgid "restart networking" msgstr "hálózat újratöltése" -#: vm/operations.py:1486 +#: vm/operations.py:1534 msgid "change ip" msgstr "IP cím beállítása" -#: vm/operations.py:1501 +#: vm/operations.py:1549 msgid "update agent" msgstr "ügynök frissítése" -#: vm/operations.py:1507 +#: vm/operations.py:1555 #, python-format msgid "update agent to %(version)s" msgstr "ügynökfrissítés erre: %(version)s" -#: vm/operations.py:1590 +#: vm/operations.py:1638 msgid "mount store" msgstr "tárhely csatolása" -#: vm/operations.py:1592 +#: vm/operations.py:1640 msgid "" "This operation attaches your personal file store. Other users who have " "access to this machine can see these files as well." @@ -6023,28 +6476,28 @@ msgstr "" "Ez a művelet csatolja az ön személyes tárhelyét. A gép más felhasználói is " "elérhetik fájljait." -#: vm/operations.py:1626 +#: vm/operations.py:1674 msgid "attach disk" msgstr "lemez csatolása" -#: vm/operations.py:1637 +#: vm/operations.py:1685 msgid "Resource was not found." msgstr "Nem található az erőforrás." -#: vm/operations.py:1638 +#: vm/operations.py:1686 #, python-format msgid "Resource was not found. %(exception)s" msgstr "Nem található az erőforrás. %(exception)s" -#: vm/operations.py:1647 +#: vm/operations.py:1695 msgid "detach disk" msgstr "lemez leválasztása" -#: vm/operations.py:1662 +#: vm/operations.py:1710 msgid "attach network" msgstr "hálózat csatolása" -#: vm/operations.py:1669 +#: vm/operations.py:1717 msgid "detach network" msgstr "hálózat lecsatolása" @@ -6080,10 +6533,13 @@ msgstr "" "<a href=\"%(url)s\">%(instance)s</a> gépe felfüggesztésre került, mivel " "lejárt. Felébresztheti vagy megsemmisítheti." -#: vm/tests/test_models.py:219 +#: vm/tests/test_models.py:221 msgid "x" msgstr "x" +#~ msgid "resources request" +#~ msgstr "erőforrás igénylés" + #~ msgid "" #~ "Dear %s, you've signed in as administrator!<br />Reloading in 10 seconds..." #~ msgstr "Kedves %s, Ön rendszergazda!<br />Újratöltés 10 másodpercen belül..." @@ -6339,9 +6795,6 @@ msgstr "x" #~ msgid "Recover virtual machine from destroyed state." #~ msgstr "Megsemmisített virtuális gép visszaállítása." -#~ msgid "Change resources" -#~ msgstr "Erőforrások módosítása" - #~ msgid "VM successfully deleted." #~ msgstr "A VM törlésre került." @@ -6397,9 +6850,6 @@ msgstr "x" #~ msgid "Are you sure?" #~ msgstr "Biztos benne?" -#~ msgid "Accept" -#~ msgstr "Elfogadás" - #~ msgid "" #~ "\n" #~ "Please, either <a href=\"%(token)s\">renew</a> or <a href=\"%(url)s\">destroy</a>\n" diff --git a/circle/network/forms.py b/circle/network/forms.py index 6cf0735..d3e5ecf 100644 --- a/circle/network/forms.py +++ b/circle/network/forms.py @@ -69,6 +69,8 @@ class BlacklistItemForm(ModelForm): class Meta: model = BlacklistItem + fields = ("ipv4", "host", "expires_at", "whitelisted", "reason", + "snort_message", ) class DomainForm(ModelForm): @@ -90,6 +92,7 @@ class DomainForm(ModelForm): class Meta: model = Domain + fields = ("name", "ttl", "owner", ) class FirewallForm(ModelForm): @@ -105,6 +108,7 @@ class FirewallForm(ModelForm): class Meta: model = Firewall + fields = ("name", ) class GroupForm(ModelForm): @@ -126,6 +130,7 @@ class GroupForm(ModelForm): class Meta: model = Group + fields = ("name", "description", "owner", ) class HostForm(ModelForm): @@ -165,6 +170,9 @@ class HostForm(ModelForm): class Meta: model = Host + fields = ("hostname", "reverse", "mac", "vlan", "shared_ip", "ipv4", + "ipv6", "external_ipv4", "description", "location", + "comment", "owner", ) class RecordForm(ModelForm): @@ -191,6 +199,8 @@ class RecordForm(ModelForm): class Meta: model = Record + fields = ("type", "host", "name", "domain", "address", "ttl", + "description", "owner", ) class RuleForm(ModelForm): @@ -230,6 +240,10 @@ class RuleForm(ModelForm): class Meta: model = Rule + fields = ("direction", "description", "foreign_network", "dport", + "sport", "weight", "proto", "extra", "action", "owner", + "nat", "nat_external_port", "nat_external_ipv4", "vlan", + "vlangroup", "host", "hostgroup", "firewall", ) class SwitchPortForm(ModelForm): @@ -252,6 +266,7 @@ class SwitchPortForm(ModelForm): class Meta: model = SwitchPort + fields = ("untagged_vlan", "tagged_vlans", "description", ) class VlanForm(ModelForm): @@ -305,6 +320,10 @@ class VlanForm(ModelForm): widgets = { 'ipv6_template': widgets.TextInput, } + fields = ("name", "vid", "network_type", "managed", "network4", + "snat_to", "snat_ip", "dhcp_pool", "network6", + "ipv6_template", "host_ipv6_prefixlen", "domain", + "reverse_domain", "description", "comment", "owner", ) class VlanGroupForm(ModelForm): @@ -328,3 +347,4 @@ class VlanGroupForm(ModelForm): class Meta: model = VlanGroup + fields = ("name", "vlans", "description", "owner", ) diff --git a/circle/request/__init__.py b/circle/request/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/circle/request/__init__.py diff --git a/circle/request/forms.py b/circle/request/forms.py new file mode 100644 index 0000000..438c89a --- /dev/null +++ b/circle/request/forms.py @@ -0,0 +1,94 @@ +# 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" diff --git a/circle/request/migrations/0001_initial.py b/circle/request/migrations/0001_initial.py new file mode 100644 index 0000000..dd10a0f --- /dev/null +++ b/circle/request/migrations/0001_initial.py @@ -0,0 +1,118 @@ +# -*- 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, + ), + ] diff --git a/circle/request/migrations/0002_auto_20150407_1117.py b/circle/request/migrations/0002_auto_20150407_1117.py new file mode 100644 index 0000000..7641c8e --- /dev/null +++ b/circle/request/migrations/0002_auto_20150407_1117.py @@ -0,0 +1,38 @@ +# -*- 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, + ), + ] diff --git a/circle/request/migrations/0003_auto_20150410_1917.py b/circle/request/migrations/0003_auto_20150410_1917.py new file mode 100644 index 0000000..1434e62 --- /dev/null +++ b/circle/request/migrations/0003_auto_20150410_1917.py @@ -0,0 +1,32 @@ +# -*- 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, + ), + ] diff --git a/circle/request/migrations/__init__.py b/circle/request/migrations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/circle/request/migrations/__init__.py diff --git a/circle/request/models.py b/circle/request/models.py new file mode 100644 index 0000000..81d3229 --- /dev/null +++ b/circle/request/models.py @@ -0,0 +1,291 @@ +# 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) diff --git a/circle/request/tables.py b/circle/request/tables.py new file mode 100644 index 0000000..b2a0a33 --- /dev/null +++ b/circle/request/tables.py @@ -0,0 +1,89 @@ +# 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" diff --git a/circle/request/templates/request/_request-lease-form.html b/circle/request/templates/request/_request-lease-form.html new file mode 100644 index 0000000..2eac032 --- /dev/null +++ b/circle/request/templates/request/_request-lease-form.html @@ -0,0 +1,10 @@ +{% 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> diff --git a/circle/request/templates/request/_request-template-form.html b/circle/request/templates/request/_request-template-form.html new file mode 100644 index 0000000..a7cc90e --- /dev/null +++ b/circle/request/templates/request/_request-template-form.html @@ -0,0 +1,26 @@ +{% 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> diff --git a/circle/request/templates/request/columns/status.html b/circle/request/templates/request/columns/status.html new file mode 100644 index 0000000..b4cce09 --- /dev/null +++ b/circle/request/templates/request/columns/status.html @@ -0,0 +1,4 @@ +<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> diff --git a/circle/request/templates/request/columns/templates.html b/circle/request/templates/request/columns/templates.html new file mode 100644 index 0000000..49e27d4 --- /dev/null +++ b/circle/request/templates/request/columns/templates.html @@ -0,0 +1,5 @@ +{% 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 %} diff --git a/circle/request/templates/request/columns/type.html b/circle/request/templates/request/columns/type.html new file mode 100644 index 0000000..b096a93 --- /dev/null +++ b/circle/request/templates/request/columns/type.html @@ -0,0 +1,2 @@ +<i class="fa fa-{{ record.get_request_icon }}"></i> +{{ record.get_readable_type|capfirst }} diff --git a/circle/request/templates/request/columns/user.html b/circle/request/templates/request/columns/user.html new file mode 100644 index 0000000..d8a202b --- /dev/null +++ b/circle/request/templates/request/columns/user.html @@ -0,0 +1,4 @@ +<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> diff --git a/circle/request/templates/request/detail.html b/circle/request/templates/request/detail.html new file mode 100644 index 0000000..5092377 --- /dev/null +++ b/circle/request/templates/request/detail.html @@ -0,0 +1,136 @@ +{% 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 %} diff --git a/circle/request/templates/request/initials/lease.html b/circle/request/templates/request/initials/lease.html new file mode 100644 index 0000000..1ccbb34 --- /dev/null +++ b/circle/request/templates/request/initials/lease.html @@ -0,0 +1,7 @@ +{% spaceless %} +{% if LANGUAGE_CODE == "en" %} +Why do you need this lease? +{% else %} {# place your translations here #} +Why do you need this lease? +{% endif %} +{% endspaceless %} diff --git a/circle/request/templates/request/initials/resources.html b/circle/request/templates/request/initials/resources.html new file mode 100644 index 0000000..a69e387 --- /dev/null +++ b/circle/request/templates/request/initials/resources.html @@ -0,0 +1,7 @@ +{% spaceless %} +{% if LANGUAGE_CODE == "en" %} +Why do you need these resources? +{% else %} {# place your translations here #} +Why do you need these resources? +{% endif %} +{% endspaceless %} diff --git a/circle/request/templates/request/initials/template.html b/circle/request/templates/request/initials/template.html new file mode 100644 index 0000000..26324d0 --- /dev/null +++ b/circle/request/templates/request/initials/template.html @@ -0,0 +1,7 @@ +{% spaceless %} +{% if LANGUAGE_CODE == "en" %} +Why do you need this template? +{% else %} {# place your translations here #} +Why do you need this template? +{% endif %} +{% endspaceless %} diff --git a/circle/request/templates/request/lease-type-form.html b/circle/request/templates/request/lease-type-form.html new file mode 100644 index 0000000..fd99eb9 --- /dev/null +++ b/circle/request/templates/request/lease-type-form.html @@ -0,0 +1,48 @@ +{% 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 %} diff --git a/circle/request/templates/request/list.html b/circle/request/templates/request/list.html new file mode 100644 index 0000000..585e8a8 --- /dev/null +++ b/circle/request/templates/request/list.html @@ -0,0 +1,34 @@ +{% 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 %} diff --git a/circle/request/templates/request/request-lease.html b/circle/request/templates/request/request-lease.html new file mode 100644 index 0000000..955b977 --- /dev/null +++ b/circle/request/templates/request/request-lease.html @@ -0,0 +1,28 @@ +{% 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 %} diff --git a/circle/request/templates/request/request-resource.html b/circle/request/templates/request/request-resource.html new file mode 100644 index 0000000..f3dbdd5 --- /dev/null +++ b/circle/request/templates/request/request-resource.html @@ -0,0 +1,37 @@ +{% 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 %} diff --git a/circle/request/templates/request/request-template.html b/circle/request/templates/request/request-template.html new file mode 100644 index 0000000..e1de549 --- /dev/null +++ b/circle/request/templates/request/request-template.html @@ -0,0 +1,23 @@ +{% 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 %} diff --git a/circle/request/templates/request/template-type-form.html b/circle/request/templates/request/template-type-form.html new file mode 100644 index 0000000..91cd7f4 --- /dev/null +++ b/circle/request/templates/request/template-type-form.html @@ -0,0 +1,48 @@ +{% 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 %} diff --git a/circle/request/templates/request/type-list.html b/circle/request/templates/request/type-list.html new file mode 100644 index 0000000..a7233b1 --- /dev/null +++ b/circle/request/templates/request/type-list.html @@ -0,0 +1,48 @@ +{% 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 %} diff --git a/circle/request/tests.py b/circle/request/tests.py new file mode 100644 index 0000000..113782e --- /dev/null +++ b/circle/request/tests.py @@ -0,0 +1,145 @@ +# 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) diff --git a/circle/request/urls.py b/circle/request/urls.py new file mode 100644 index 0000000..14ff19b --- /dev/null +++ b/circle/request/urls.py @@ -0,0 +1,63 @@ +# 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"), +) diff --git a/circle/request/views.py b/circle/request/views.py new file mode 100644 index 0000000..5ea5d77 --- /dev/null +++ b/circle/request/views.py @@ -0,0 +1,288 @@ +# 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()) diff --git a/circle/templates/403.html b/circle/templates/403.html new file mode 100644 index 0000000..59a65da --- /dev/null +++ b/circle/templates/403.html @@ -0,0 +1,23 @@ +{% 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 %} diff --git a/circle/templates/404.html b/circle/templates/404.html index 6f57b74..87199ff 100644 --- a/circle/templates/404.html +++ b/circle/templates/404.html @@ -6,5 +6,14 @@ {% block page_title %}{% trans "Page not found" %}{% endblock page_title %} {% block content %} -<p>{% trans "This page does not exist." %}</p> +<div class="alert alert-warning" style="font-size: 22px; margin-top: 2em;"> + <div class="row"> + <div class="col-md-2" style="text-align: center;"> + HTTP 404 + </div> + <div class="col-md-10" style="text-align: center;"> + {% trans "This page does not exist." %} + </div> + </div> +</div> {% endblock content %} diff --git a/circle/templates/500.html b/circle/templates/500.html index 875d456..13533c0 100644 --- a/circle/templates/500.html +++ b/circle/templates/500.html @@ -1,4 +1,4 @@ -{% extends "dashboard/base.html" %} +{% extends "base.html" %} {% load i18n %} {% block title %}HTTP 500{% endblock %} diff --git a/circle/templates/registration/base.html b/circle/templates/registration/base.html index a626320..1575ea5 100644 --- a/circle/templates/registration/base.html +++ b/circle/templates/registration/base.html @@ -74,13 +74,6 @@ {% endblock %} -{% block navbar-brand %} - <a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;"> - <img src="{% static "dashboard/img/logo.png" %}" style="height: 25px;"/> - </a> -{% endblock %} - - {% block content %} <div class="content"> {% block content_box %}{% endblock %} diff --git a/circle/templates/registration/login.html b/circle/templates/registration/login.html index f6a09da..8daa0cd 100644 --- a/circle/templates/registration/login.html +++ b/circle/templates/registration/login.html @@ -6,10 +6,8 @@ {% block title-page %}{% trans "Login" %}{% endblock %} -{% block navbar-brand %} - <a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;"> - <img src="{% static "dashboard/img/logo.png" %}" style="height: 25px;"/> - </a> +{% block extra_link %} + {% include "open-graph.html" %} {% endblock %} {% block content_box %} @@ -21,10 +19,7 @@ {% endif %} <div class="col-xs-{% if saml2 %}6{% else %}12{% endif %}"> <div class="login-form"> - <form action="" method="POST"> - {% csrf_token %} - {% crispy form %} - </form> + {% crispy form %} </div> </div> {% if saml2 %} @@ -33,11 +28,12 @@ <a href="{% url "saml2_login" %}">{% trans "Click here!" %}</a> </div> {% endif %} - </div> <div class="row"> <div class="col-sm-12"> - <a class="pull-right" href="{% url "accounts.password-reset" %}">{% trans "Forgot your password?" %}</a> + <a class="pull-right" href="{% url "accounts.password-reset" %}" style="margin-right: 15px;"> + {% trans "Forgot your password?" %} + </a> </div> + </div> </div> -</div> {% endblock %} diff --git a/circle/vm/models/instance.py b/circle/vm/models/instance.py index b39a78b..0fb86c4 100644 --- a/circle/vm/models/instance.py +++ b/circle/vm/models/instance.py @@ -62,7 +62,6 @@ scheduler = import_module(name=django.conf.settings.VM_SCHEDULER) ACCESS_PROTOCOLS = django.conf.settings.VM_ACCESS_PROTOCOLS ACCESS_METHODS = [(key, name) for key, (name, port, transport) in ACCESS_PROTOCOLS.iteritems()] -VNC_PORT_RANGE = (20000, 65536) # inclusive start, exclusive end def find_unused_port(port_range, used_ports=[]): @@ -81,7 +80,7 @@ def find_unused_port(port_range, used_ports=[]): def find_unused_vnc_port(): port = find_unused_port( - port_range=VNC_PORT_RANGE, + port_range=django.conf.settings.VNC_PORT_RANGE, used_ports=Instance.objects.values_list('vnc_port', flat=True)) if port is None: diff --git a/circle/vm/operations.py b/circle/vm/operations.py index 3e606c9..43f9f59 100644 --- a/circle/vm/operations.py +++ b/circle/vm/operations.py @@ -1334,10 +1334,20 @@ class ResourcesOperation(InstanceOperation): description = _("Change resources of a stopped virtual machine.") acl_level = "owner" required_perms = ('vm.change_resources', ) - accept_states = ('STOPPED', 'PENDING', ) + accept_states = ('STOPPED', 'PENDING', 'RUNNING') def _operation(self, user, activity, - num_cores, ram_size, max_ram_size, priority): + num_cores, ram_size, max_ram_size, priority, + with_shutdown=False, task=None): + if self.instance.status == 'RUNNING' and not with_shutdown: + raise Instance.WrongStateError(self.instance) + + try: + self.instance.shutdown(parent_activity=activity, task=task) + except Instance.WrongStateError: + pass + + self.instance._update_status() self.instance.num_cores = num_cores self.instance.ram_size = ram_size @@ -1380,6 +1390,34 @@ class PasswordResetOperation(RemoteAgentOperation): @register_operation +class InstallKeysOperation(RemoteAgentOperation): + id = 'install_keys' + name = _("install SSH keys") + acl_level = "user" + task = agent_tasks.add_keys + required_perms = () + + def _get_remote_args(self, user, keys=None, **kwargs): + if keys is None: + keys = list(user.userkey_set.values_list('key', flat=True)) + return (super(InstallKeysOperation, self)._get_remote_args(**kwargs) + + [keys]) + + +@register_operation +class RemoveKeysOperation(RemoteAgentOperation): + id = 'remove_keys' + name = _("remove SSH keys") + acl_level = "user" + task = agent_tasks.del_keys + required_perms = () + + def _get_remote_args(self, user, keys, **kwargs): + return (super(RemoveKeysOperation, self)._get_remote_args(**kwargs) + + [keys]) + + +@register_operation class AgentStartedOperation(InstanceOperation): id = 'agent_started' name = _("agent") @@ -1452,6 +1490,7 @@ class AgentStartedOperation(InstanceOperation): self.instance._cleanup(parent_activity=activity) self.instance.password_reset( parent_activity=activity, password=self.instance.pw) + self.instance.install_keys(parent_activity=activity) self.instance._set_time(parent_activity=activity) self.instance._set_hostname(parent_activity=activity) diff --git a/requirements/base.txt b/requirements/base.txt index e9ab1c6..ef93a6f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -11,11 +11,12 @@ django-braces==1.4.0 django-celery==3.1.16 django-crispy-forms==1.4.0 django-model-utils==2.2 +djangosaml2==0.13.0 django-sizefield==0.6 django-sshkey==2.2.0 django-statici18n==1.1 django-tables2==0.15.0 -git+https://git.ik.bme.hu/circle/django-taggit.git +django-taggit==0.13.0 docutils==0.12 Jinja2==2.7.3 jsonfield==1.0.0 @@ -32,6 +33,7 @@ pyinotify==0.9.4 pytz==2014.7 requests==2.5.3 salt==2014.1.0 +shutilwhich==1.0.1 simplejson==3.6.5 six==1.8.0 slimit==0.8.1 @@ -40,4 +42,4 @@ sqlparse==0.1.13 pika==0.9.14 Fabric==1.10.0 lxml==3.4.0 -git+https://github.com/BME-IK/django-pipeline.git +django-pipeline==1.4.7