Commit 5e3ad1e8 by Fukász Rómeó Ervin

Merge branch 'master' into occi-fromeo

parents dd4c07db 3f1b8167
"""
Creates Levels for all installed apps that have levels.
"""
from django.db.models import get_models, signals
from django.db.models import signals
from django.apps import apps
from django.db import DEFAULT_DB_ALIAS
from django.core.exceptions import ImproperlyConfigured
from ..models import Level, AclBase
def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS,
def create_levels(app_config, verbosity=False, using=DEFAULT_DB_ALIAS,
**kwargs):
"""Create and set the weights of the configured Levels.
Based on django.contrib.auth.management.__init__.create_permissions"""
# if not router.allow_migrate(db, auth_app.Permission):
# if not router.allow_migrate(using, auth_app.Permission):
# return
from django.contrib.contenttypes.models import ContentType
app_models = [k for k in get_models(app) if AclBase in k.__bases__]
app_models = [k for k in apps.get_models(app_config)
if AclBase in k.__bases__]
print "Creating levels for models: %s." % ", ".join(
[m.__name__ for m in app_models])
......@@ -31,7 +33,7 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS,
for klass in app_models:
# Force looking up the content types in the current database
# before creating foreign keys to them.
ctype1 = ContentType.objects.db_manager(db).get_for_model(klass)
ctype1 = ContentType.objects.db_manager(using).get_for_model(klass)
ctypes.add(ctype1)
weight = 0
try:
......@@ -46,7 +48,7 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS,
# Find all the Levels that have a content_type for a model we're
# looking for. We don't need to check for codenames since we already have
# a list of the ones we're going to create.
all_levels = set(Level.objects.using(db).filter(
all_levels = set(Level.objects.using(using).filter(
content_type__in=ctypes,
).values_list(
"content_type", "codename"
......@@ -57,7 +59,7 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS,
for ctype, (codename, name) in searched_levels
if (ctype.pk, codename) not in all_levels
]
Level.objects.using(db).bulk_create(levels)
Level.objects.using(using).bulk_create(levels)
if verbosity >= 2:
print("Adding levels [%s]." % ", ".join(unicode(l) for l in levels))
print("Searched: [%s]." % ", ".join(
......@@ -70,5 +72,5 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS,
content_type=ctype).update(weight=weight)
signals.post_syncdb.connect(
signals.post_migrate.connect(
create_levels, dispatch_uid="circle.acl.management.create_levels")
......@@ -18,7 +18,7 @@
import logging
from django.contrib.auth.models import User, Group
from django.contrib.contenttypes.generic import (
from django.contrib.contenttypes.fields import (
GenericForeignKey, GenericRelation
)
from django.contrib.contenttypes.models import ContentType
......
......@@ -14,12 +14,14 @@
"bootstrap": "~3.2.0",
"fontawesome": "~4.3.0",
"jquery": "~2.1.1",
"no-vnc": "*",
"no-vnc": "0.5.1",
"jquery-knob": "~1.2.9",
"jquery-simple-slider": "https://github.com/BME-IK/jquery-simple-slider.git",
"bootbox": "~4.3.0",
"intro.js": "0.9.0",
"favico.js": "~0.3.5",
"datatables": "~1.10.4"
"datatables": "~1.10.4",
"chart.js": "2.3.0",
"clipboard": "~1.6.1"
}
}
......@@ -12,9 +12,10 @@ def update_permissions_after_migration(sender, **kwargs):
"""
from django.conf import settings
from django.db.models import get_models
from django.apps import apps
from django.contrib.auth.management import create_permissions
create_permissions(sender, get_models(), 2 if settings.DEBUG else 0)
create_permissions(sender, apps.get_models(), 2 if settings.DEBUG else 0)
post_migrate.connect(update_permissions_after_migration)
......@@ -27,7 +27,7 @@ from base import * # noqa
DEBUG = True
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug
TEMPLATE_DEBUG = DEBUG
TEMPLATES[0]['OPTIONS']['debug'] = DEBUG
########## END DEBUG CONFIGURATION
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
......@@ -110,8 +110,10 @@ if DEBUG:
from django.dispatch import Signal
Signal.send_robust = Signal.send
PIPELINE_COMPILERS = (
PIPELINE["COMPILERS"] = (
'dashboard.compilers.DummyLessCompiler',
)
ADMIN_ENABLED = True
ALLOWED_HOSTS = ['*']
......@@ -14,9 +14,10 @@
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
import os
from .base import * # noqa
from .base import * # flake8:noqa
# fix https://github.com/django-nose/django-nose/issues/197
......
......@@ -38,7 +38,11 @@ INSTALLED_APPS += (
'django_nose',
)
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = ['--with-doctest', '--exclude-dir=dashboard/tests/selenium']
NOSE_ARGS = [
'--with-doctest',
'--exclude-dir=dashboard/tests/selenium',
'--exclude=circle'
]
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
CACHES = {
......@@ -59,7 +63,7 @@ for i in LOCAL_APPS:
# don't print SQL queries
LOGGING['handlers']['null'] = {'level': "DEBUG",
'class': "django.utils.log.NullHandler"}
'class': "logging.NullHandler"}
LOGGING['loggers']['django.db.backends'] = {
'handlers': ['null'],
'propagate': False,
......
......@@ -15,26 +15,29 @@
# 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.conf.urls import patterns, include, url
from django.conf.urls import include, url
from django.views.generic import TemplateView
from django.conf import settings
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from django.contrib.auth.views import (
password_reset_confirm, password_reset
)
from circle.settings.base import get_env_variable
from dashboard.views import circle_login, HelpView, ResizeHelpView
from dashboard.views import (
CircleLoginView, HelpView, ResizeHelpView, TwoFactorLoginView
)
from dashboard.forms import CirclePasswordResetForm, CircleSetPasswordForm
from firewall.views import add_blacklist_item
admin.autodiscover()
urlpatterns = patterns(
'',
urlpatterns = [
url(r'^$', lambda x: redirect(reverse("dashboard.index"))),
url(r'^network/', include('network.urls')),
url(r'^blacklist-add/', add_blacklist_item),
......@@ -45,17 +48,20 @@ urlpatterns = patterns(
# django/contrib/auth/urls.py (care when new version)
url((r'^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/'
r'(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$'),
'django.contrib.auth.views.password_reset_confirm',
password_reset_confirm,
{'set_password_form': CircleSetPasswordForm},
name='accounts.password_reset_confirm'
),
url(r'^accounts/password/reset/$', ("django.contrib.auth.views."
"password_reset"),
url(r'^accounts/password/reset/$', password_reset,
{'password_reset_form': CirclePasswordResetForm},
name="accounts.password-reset",
),
url(r'^accounts/login/?$', circle_login, name="accounts.login"),
url(r'^accounts/login/?$', CircleLoginView.as_view(),
name="accounts.login"),
url(r'^accounts/', include('django.contrib.auth.urls')),
url(r'^two-factor-login/$', TwoFactorLoginView.as_view(),
name="two-factor-login"),
url(r'^info/help/$', HelpView.as_view(template_name="info/help.html"),
name="info.help"),
url(r'^info/policy/$',
......@@ -69,27 +75,24 @@ urlpatterns = patterns(
name="info.support"),
url(r'^info/resize-how-to/$', ResizeHelpView.as_view(),
name="info.resize"),
)
]
if 'rosetta' in settings.INSTALLED_APPS:
urlpatterns += patterns(
'',
urlpatterns += [
url(r'^rosetta/', include('rosetta.urls')),
)
]
if settings.ADMIN_ENABLED:
urlpatterns += patterns(
'',
urlpatterns += [
url(r'^admin/', include(admin.site.urls)),
)
]
if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
urlpatterns += patterns(
'',
(r'^saml2/', include('djangosaml2.urls')),
)
urlpatterns += [
url(r'^saml2/', include('djangosaml2.urls')),
]
handler500 = 'common.views.handler500'
handler403 = 'common.views.handler403'
......@@ -232,6 +232,14 @@ class ActivityModel(TimeStampedModel):
else:
return code
def get_status_id(self):
if self.succeeded is None:
return 'wait'
elif self.succeeded:
return 'success'
else:
return 'failed'
@celery.task()
def compute_cached(method, instance, memcached_seconds,
......
......@@ -175,8 +175,9 @@ class Operation(object):
raise ImproperlyConfigured(
"Set required_perms to () if none needed.")
if not user.has_perms(cls.required_perms):
raise PermissionDenied(
u"%s doesn't have the required permissions." % user)
raise humanize_exception(ugettext_noop(
"You don't have the required permissions."),
PermissionDenied())
if cls.superuser_required and not user.is_superuser:
raise humanize_exception(ugettext_noop(
"Superuser privileges are required."), PermissionDenied())
......
......@@ -45,7 +45,8 @@ def handler500(request):
logger.exception("unhandled exception")
ctx = get_context(request, exception)
try:
resp = render_to_response("500.html", ctx, RequestContext(request))
resp = render_to_response("500.html", ctx,
RequestContext(request).flatten())
except:
resp = render_to_response("500.html", ctx)
resp.status_code = 500
......
......@@ -20,6 +20,8 @@ from __future__ import absolute_import
from datetime import timedelta
from urlparse import urlparse
import pyotp
from django.forms import ModelForm
from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm,
......@@ -29,7 +31,7 @@ from django.contrib.auth.models import User, Group
from django.core.validators import URLValidator
from django.core.exceptions import PermissionDenied, ValidationError
import autocomplete_light
from dal import autocomplete
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (
Layout, Div, BaseInput, Field, HTML, Submit, TEMPLATE_PACK, Fieldset
......@@ -41,7 +43,6 @@ from crispy_forms.bootstrap import FormActions
from django import forms
from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.forms.widgets import TextInput, HiddenInput
from django.template import Context
from django.template.loader import render_to_string
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
......@@ -65,6 +66,7 @@ from .validators import domain_validator
from dashboard.models import ConnectCommand, create_profile
LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")"))
for l in LANGUAGES)
......@@ -1178,8 +1180,7 @@ class AnyTag(Div):
fields += render_field(field, form, form_style, context,
template_pack=template_pack)
return render_to_string(self.template, Context({'tag': self,
'fields': fields}))
return render_to_string(self.template, {'tag': self, 'fields': fields})
class WorkingBaseInput(BaseInput):
......@@ -1223,7 +1224,7 @@ class MyProfileForm(forms.ModelForm):
class Meta:
fields = ('preferred_language', 'email_notifications',
'use_gravatar', )
'desktop_notifications', 'use_gravatar', )
model = Profile
@property
......@@ -1298,21 +1299,29 @@ class UserEditForm(forms.ModelForm):
instance_limit = forms.IntegerField(
label=_('Instance limit'),
min_value=0, widget=NumberInput)
two_factor_secret = forms.CharField(
label=_('Two-factor authentication secret'),
help_text=_("Remove the secret key to disable two-factor "
"authentication for this user."), required=False)
def __init__(self, *args, **kwargs):
super(UserEditForm, self).__init__(*args, **kwargs)
self.fields["instance_limit"].initial = (
self.instance.profile.instance_limit)
self.fields["two_factor_secret"].initial = (
self.instance.profile.two_factor_secret)
class Meta:
model = User
fields = ('email', 'first_name', 'last_name', 'instance_limit',
'is_active')
'is_active', "two_factor_secret", )
def save(self, commit=True):
user = super(UserEditForm, self).save()
user.profile.instance_limit = (
self.cleaned_data['instance_limit'] or None)
user.profile.two_factor_secret = (
self.cleaned_data['two_factor_secret'] or None)
user.profile.save()
return user
......@@ -1324,27 +1333,31 @@ class UserEditForm(forms.ModelForm):
class AclUserOrGroupAddForm(forms.Form):
name = forms.CharField(widget=autocomplete_light.TextWidget(
'AclUserGroupAutocomplete',
attrs={'class': 'form-control',
'placeholder': _("Name of group or user")}))
name = forms.CharField(
widget=autocomplete.ListSelect2(
url='autocomplete.acl.user-group',
attrs={'class': 'form-control',
'data-html': 'true',
'data-placeholder': _("Name of group or user")}))
class TransferOwnershipForm(forms.Form):
name = forms.CharField(
widget=autocomplete_light.TextWidget(
'AclUserAutocomplete',
widget=autocomplete.ListSelect2(
url='autocomplete.acl.user',
attrs={'class': 'form-control',
'placeholder': _("Name of user")}),
'data-html': 'true',
'data-placeholder': _("Name of user")}),
label=_("E-mail address or identifier of user"))
class AddGroupMemberForm(forms.Form):
new_member = forms.CharField(
widget=autocomplete_light.TextWidget(
'AclUserAutocomplete',
widget=autocomplete.ListSelect2(
url='autocomplete.acl.user',
attrs={'class': 'form-control',
'placeholder': _("Name of user")}),
'data-html': 'true',
'data-placeholder': _("Name of user")}),
label=_("E-mail address or identifier of user"))
......@@ -1528,14 +1541,21 @@ class VmResourcesForm(forms.ModelForm):
fields = ('num_cores', 'priority', 'ram_size', )
class VmRenameForm(forms.Form):
new_name = forms.CharField()
vm_search_choices = (
("owned", _("owned")),
("shared", _("shared")),
("shared_with_me", _("shared with me")),
("all", _("all")),
)
class VmListSearchForm(forms.Form):
use_required_attribute = False
s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags",
'placeholder': _("Search...")
......@@ -1560,6 +1580,8 @@ class VmListSearchForm(forms.Form):
class TemplateListSearchForm(forms.Form):
use_required_attribute = False
s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags",
'placeholder': _("Search...")
......@@ -1579,6 +1601,8 @@ class TemplateListSearchForm(forms.Form):
class UserListSearchForm(forms.Form):
use_required_attribute = False
s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags",
'placeholder': _("Search...")
......@@ -1632,9 +1656,9 @@ class MessageForm(ModelForm):
fields = ("message", "enabled", "effect", "start", "end")
help_texts = {
'start': _("Start time of the message in "
"YYYY.DD.MM. hh.mm.ss format."),
"YYYY-MM-DD hh:mm:ss format."),
'end': _("End time of the message in "
"YYYY.DD.MM. hh.mm.ss format."),
"YYYY-MM-DD hh:mm:ss format."),
'effect': _('The color of the message box defined by the '
'respective '
'<a href="http://getbootstrap.com/components/#alerts">'
......@@ -1650,3 +1674,26 @@ class MessageForm(ModelForm):
helper = FormHelper()
helper.add_input(Submit("submit", _("Save")))
return helper
class TwoFactorForm(ModelForm):
class Meta:
model = Profile
fields = ["two_factor_secret", ]
class TwoFactorConfirmationForm(forms.Form):
confirmation_code = forms.CharField(
label=_('Two-factor authentication passcode'),
help_text=_("Get the code from your authenticator."),
widget=forms.TextInput(attrs={'autofocus': True})
)
def __init__(self, user, *args, **kwargs):
self.user = user
super(TwoFactorConfirmationForm, self).__init__(*args, **kwargs)
def clean_confirmation_code(self):
totp = pyotp.TOTP(self.user.profile.two_factor_secret)
if not totp.verify(self.cleaned_data.get('confirmation_code')):
raise ValidationError(_("Invalid confirmation code."))
......@@ -18,7 +18,6 @@
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
......@@ -32,19 +31,18 @@ logger = logging.getLogger(__name__)
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--force', action="store_true"),
make_option('--external-net'),
make_option('--management-net'),
make_option('--vm-net'),
make_option('--external-if'),
make_option('--management-if'),
make_option('--vm-if'),
make_option('--datastore-queue'),
make_option('--firewall-queue'),
make_option('--admin-user'),
make_option('--admin-pass'),
)
def add_arguments(self, parser):
parser.add_argument('--force', action="store_true")
parser.add_argument('--external-net')
parser.add_argument('--management-net')
parser.add_argument('--vm-net')
parser.add_argument('--external-if')
parser.add_argument('--management-if')
parser.add_argument('--vm-if')
parser.add_argument('--datastore-queue')
parser.add_argument('--firewall-queue')
parser.add_argument('--admin-user')
parser.add_argument('--admin-pass')
def create(self, model, field, **kwargs):
value = kwargs[field]
......@@ -59,14 +57,15 @@ class Command(BaseCommand):
# http://docs.saltstack.com/en/latest/ref/states/all/salt.states.cmd.html
def print_state(self):
print "\nchanged=%s" % ("yes" if self.changed else "no")
self.stdout.write("\nchanged=%s" % ("yes" if self.changed else "no"))
def handle(self, *args, **options):
self.changed = False
if (DataStore.objects.exists() and Vlan.objects.exists() and
not options['force']):
return self.print_state()
self.print_state()
return
admin = self.create(User, 'username', username=options['admin_user'],
is_superuser=True, is_staff=True)
......@@ -153,4 +152,4 @@ class Command(BaseCommand):
direction='out', action='accept',
foreign_network=vg_net, vlan=man)
return self.print_state()
self.print_state()
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0003_message'),
]
operations = [
migrations.AddField(
model_name='profile',
name='desktop_notifications',
field=models.BooleanField(default=False, help_text='Whether user wants to get desktop notification when an activity has finished and the window is not in focus.', verbose_name='Desktop notifications'),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0004_profile_desktop_notifications'),
]
operations = [
migrations.AddField(
model_name='profile',
name='two_factor_secret',
field=models.CharField(max_length=32, null=True, verbose_name='two factor secret key', blank=True),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-07 19:09
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0005_profile_two_factor_secret'),
]
operations = [
migrations.AlterField(
model_name='connectcommand',
name='name',
field=models.CharField(help_text='Name of your custom command.', max_length=128, verbose_name='name'),
),
]
......@@ -151,7 +151,7 @@ class ConnectCommand(Model):
access_method = CharField(max_length=10, choices=ACCESS_METHODS,
verbose_name=_('access method'),
help_text=_('Type of the remote access method.'))
name = CharField(max_length="128", verbose_name=_('name'), blank=False,
name = CharField(max_length=128, verbose_name=_('name'), blank=False,
help_text=_("Name of your custom command."))
template = CharField(blank=True, null=True, max_length=256,
verbose_name=_('command template'),
......@@ -184,6 +184,10 @@ class Profile(Model):
email_notifications = BooleanField(
verbose_name=_("Email notifications"), default=True,
help_text=_('Whether user wants to get digested email notifications.'))
desktop_notifications = BooleanField(
verbose_name=_("Desktop notifications"), default=False,
help_text=_('Whether user wants to get desktop notification when an '
'activity has finished and the window is not in focus.'))
smb_password = CharField(
max_length=20,
verbose_name=_('Samba password'),
......@@ -196,6 +200,10 @@ class Profile(Model):
verbose_name=_('disk quota'),
default=2048 * 1024 * 1024,
help_text=_('Disk quota in mebibytes.'))
two_factor_secret = CharField(
verbose_name=_("two factor secret key"),
max_length=32, null=True, blank=True,
)
def get_connect_commands(self, instance, use_ipv6=False):
""" Generate connection command based on template."""
......@@ -204,15 +212,16 @@ class Profile(Model):
commands = self.user.command_set.filter(
access_method=instance.access_method)
if commands.count() < 1:
return [single_command]
return [{'id': 0, 'cmd': single_command}]
else:
return [
command.template % {
return [{
'id': command.id,
'cmd': command.template % {
'port': instance.get_connect_port(use_ipv6=use_ipv6),
'host': instance.get_connect_host(use_ipv6=use_ipv6),
'password': instance.pw,
'username': 'cloud',
} for command in commands]
}} for command in commands]
else:
return []
......@@ -312,6 +321,7 @@ def get_or_create_profile(self):
obj, created = GroupProfile.objects.get_or_create(group_id=self.pk)
return obj
Group.profile = property(get_or_create_profile)
......@@ -330,15 +340,15 @@ def create_profile(user):
def create_profile_hook(sender, user, request, **kwargs):
return create_profile(user)
user_logged_in.connect(create_profile_hook)
if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
logger.debug("Register save_org_id to djangosaml2 pre_user_save")
from djangosaml2.signals import pre_user_save
def save_org_id(sender, **kwargs):
logger.debug("save_org_id called by %s", sender.username)
attributes = kwargs.pop('attributes')
def save_org_id(sender, instance, attributes, **kwargs):
logger.debug("save_org_id called by %s", instance.username)
atr = settings.SAML_ORG_ID_ATTRIBUTE
try:
value = attributes[atr][0].upper()
......@@ -346,19 +356,19 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
value = None
logger.info("save_org_id couldn't find attribute. %s", unicode(e))
if sender.pk is None:
sender.save()
logger.debug("save_org_id saved user %s", unicode(sender))
if instance.pk is None:
instance.save()
logger.debug("save_org_id saved user %s", unicode(instance))
profile, created = Profile.objects.get_or_create(user=sender)
profile, created = Profile.objects.get_or_create(user=instance)
if created or profile.org_id != value:
logger.info("org_id of %s added to user %s's profile",
value, sender.username)
value, instance.username)
profile.org_id = value
profile.save()
else:
logger.debug("org_id of %s already added to user %s's profile",
value, sender.username)
value, instance.username)
memberatrs = getattr(settings, 'SAML_GROUP_ATTRIBUTES', [])
for group in chain(*[attributes[i]
for i in memberatrs if i in attributes]):
......@@ -369,10 +379,10 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
else:
logger.debug('could find membergroup %s (%s)',
group, unicode(g))
g.user_set.add(sender)
g.user_set.add(instance)
for i in FutureMember.objects.filter(org_id__iexact=value):
i.group.user_set.add(sender)
i.group.user_set.add(instance)
i.delete()
owneratrs = getattr(settings, 'SAML_GROUP_OWNER_ATTRIBUTES', [])
......@@ -385,15 +395,12 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
else:
logger.debug('could find ownergroup %s (%s)',
group, unicode(g))
g.profile.set_level(sender, 'owner')
g.profile.set_level(instance, 'owner')
return False # User did not change
pre_user_save.connect(save_org_id)
else:
logger.debug("Do not register save_org_id to djangosaml2 pre_user_save")
def update_store_profile(sender, **kwargs):
profile = kwargs.get('instance')
......
......@@ -169,6 +169,9 @@ $(function() {
);
} else {
in_progress = false;
if(document.hasFocus() === false && userWantNotifications()){
sendNotification(generateMessageFromLastActivity());
}
if(reload_vm_detail) location.reload();
if(runs > 1) addConnectText();
}
......@@ -181,18 +184,49 @@ $(function() {
}
});
// Notification init
$(function(){
if(userWantNotifications())
Notification.requestPermission();
});
function generateMessageFromLastActivity(){
var ac = $("div.activity").first();
var error = ac.children(".timeline-icon-failed").length;
var sign = (error === 1) ? "❌ " : "✓ ";
var msg = ac.children("strong").text().replace(/\s+/g, " ");
return sign + msg;
}
function sendNotification(message) {
var options = { icon: "/static/dashboard/img/favicon.png"};
if (Notification.permission === "granted") {
var notification = new Notification(message, options);
}
else if (Notification.permission !== "denied") {
Notification.requestPermission(function (permission) {
if (permission === "granted") {
var notification = new Notification(message, options);
}
});
}
}
function userWantNotifications(){
var dn = $("#user-options").data("desktop_notifications");
return dn === "True";
}
function addConnectText() {
var activities = $(".timeline .activity");
if(activities.length > 1) {
if(activities.eq(0).data("activity-code") == "vm.Instance.wake_up" ||
activities.eq(0).data("activity-code") == "vm.Instance.agent") {
$("#vm-detail-successfull-boot").slideDown(500);
$("#vm-detail-successful-boot").slideDown(500);
}
}
}
String.prototype.hashCode = function() {
var hash = 0, i, chr, len;
if (this.length === 0) return hash;
......
......@@ -508,13 +508,6 @@ $.ajaxSetup({
}
});
/* for autocomplete */
$(function() {
yourlabs.TextWidget.prototype.getValue = function(choice) {
return choice.children().html();
};
});
var tagsToReplace = {
'&': '&amp;',
'<': '&lt;',
......@@ -557,3 +550,11 @@ $(function () {
"alert-" + $(this).val());
});
});
/* select all in template list */
$(function() {
$("#manage-access-select-all").click(function(e) {
var inputs = $(this).closest("table").find('input[type="checkbox"]');
inputs.prop("checked", !inputs.prop("checked"));
});
});
......@@ -104,7 +104,6 @@ html {
.timeline > div {
margin: 0;
padding: 0;
margin-left: -34px;
margin-bottom: .5em;
line-height: 24px;
}
......@@ -113,7 +112,7 @@ html {
}
.timeline .timeline-icon {
margin: 0;
margin-left: -34px;
padding: 0;
width: 24px;
height: 24px;
......@@ -153,7 +152,7 @@ html {
}
.sub-activity {
margin-left: 30px;
margin-left: -4px;
padding-left: 10px;
border-left: 3px solid green;
}
......@@ -284,7 +283,7 @@ a.hover-black {
}
.hover-black:hover {
color: black /*#d9534f*/;
color: black; /*#d9534f*/
text-decoration: none;
}
......@@ -1032,7 +1031,7 @@ textarea[name="new_members"] {
font-weight: bold;
}
.hilight .autocomplete-hl {
.select2-results__option--highlighted .autocomplete-hl {
color: orange;
}
......@@ -1285,9 +1284,16 @@ textarea[name="new_members"] {
}
}
#vm-detail-successfull-boot {
#vm-detail-successful-boot {
margin-bottom: 20px;
display: none;
.label {
width: 100%;
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
#vm-detail-access-help {
......@@ -1523,3 +1529,62 @@ textarea[name="new_members"] {
text-align: center;
width: 100%;
}
#manage-access-select-all {
cursor: pointer;
}
#two-factor-qr {
text-align: center;
span, small {
display: block;
}
}
#two-factor-confirm {
text-align: center;
button {
margin-left: 15px;
}
}
#two-factor-box {
.help-block {
display: block;
}
h4 {
margin: 0;
}
hr {
margin: 15px 0 2px 0;
}
}
#datastore-chart-legend {
width: 350px;
margin-top: 100px;
margin-left: -120px;
/* Landscape phones and down */
@media (max-width: 992px) {
margin-left: -25px;
}
ul {
list-style: none;
}
li {
font-size: 18px;
margin-bottom: 2px;
span {
display: inline-block;
width: 30px;
height: 18px;
margin-right: 8px;
}
}
}
$(function() {
var data = JSON.parse($("#chart-data").data("data"));
var labels = [];
for(var i=0; i<data.labels.length; i++) {
labels.push(data.labels[i] + " (" + data.readable_data[i] + ")");
}
var pieChart = new Chart(document.getElementById("datastore-chart"), {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data.data,
backgroundColor: [
"#57b257",
"#538ccc",
"#f0df24",
"#ff9a38",
"#7f7f7f",
]
}]
},
options: {
legend: {
display: false,
},
tooltips: {
callbacks: {
label: function(item, chartData) {
return data.labels[item.index] + ": " + data.readable_data[item.index];
}
}
},
}
});
$("#datastore-chart-legend").html(pieChart.generateLegend());
});
......@@ -22,6 +22,5 @@ $(function() {
}
});
});
});
......@@ -37,6 +37,7 @@ function vmCreateLoaded() {
/* start vm button clicks */
$('.vm-create-start').click(function() {
$(this).prop("disabled", true).find("i").prop("class", "fa fa-spinner fa-spin");
template = $(this).data("template-pk");
$.ajax({
url: '/dashboard/vm/create/',
......
......@@ -70,12 +70,12 @@ $(function() {
});
/* for js fallback */
$("#vm-details-pw-show").parent("div").children("input").prop("type", "password");
$(".vm-details-show-password").parent("div").children("input").prop("type", "password");
/* show password */
$("#vm-details-pw-show").click(function() {
$(".vm-details-show-password").click(function() {
var input = $(this).parent("div").children("input");
var eye = $(this).children("#vm-details-pw-eye");
var eye = $(this).children(".vm-details-password-eye");
var span = $(this);
span.tooltip("destroy");
......@@ -111,9 +111,10 @@ $(function() {
/* rename ajax */
$('.vm-details-rename-submit').click(function() {
var name = $(this).parent("span").prev("input").val();
var url = $("#vm-details-rename-form").attr("action");
$.ajax({
method: 'POST',
url: location.href,
url: url,
data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
......@@ -252,4 +253,12 @@ $(function() {
return e.preventDefault();
});
// Clipboard for connection strings
if(Clipboard.isSupported()) {
new Clipboard(".vm-details-connection-string-copy", {
text: function(trigger) {
return $($(trigger).data("clipboard-target")).val();
}
});
}
});
......@@ -14,5 +14,4 @@
({% trans "username" %}: {{ user.username }})
{% endif %}
{% endif %}
{% endif %}
{% for t in templates %}
<a href="{{ t.get_absolute_url }}">
{{ t.name }}</a>{% if not forloop.last %},{% endif %}
{% empty %}
-
{% endfor %}
......@@ -6,7 +6,7 @@
<th></th>
<th>{% trans "Who" %}</th>
<th>{% trans "What" %}</th>
<th><i class="fa fa-times"></i></th>
<th><i id="manage-access-select-all" class="fa fa-times"></i></th>
</tr>
</thead>
<tbody>
......@@ -24,7 +24,7 @@
<td>
<select class="form-control" name="perm-u-{{i.user.id}}"{% if i.level not in acl.allowed_levels %} disabled{% endif %}>
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%}
<option{%if id == i.level%} selected="selected"{%endif%}
{% if id not in acl.allowed_levels %} disabled{% endif %}
value="{{id}}">{{name}}</option>
{% endfor %}
......@@ -46,7 +46,7 @@
<td>
<select class="form-control" name="perm-g-{{i.group.id}}{% if i.level not in acl.allowed_levels %} disabled{% endif %}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%}
<option{%if id == i.level%} selected="selected"{%endif%}
{% if id not in acl.allowed_levels %} disabled{% endif %}
value="{{id}}">{{name}}</option>
{% endfor %}
......
......@@ -8,7 +8,7 @@
<a class="btn btn-default" href="{{object.get_absolute_url}}" data-dismiss="modal">
{% trans "Cancel" %}
</a>
{% if lease_types and not request.token_user %}
{% if object.active and lease_types and not request.token_user %}
<a class="btn btn-primary" id="vm-renew-request-lease-button"
href="{% url "request.views.request-lease" vm_pk=object.pk %}">
<i class="fa fa-forward"></i>
......
......@@ -12,6 +12,9 @@
{% block navbar %}
{% if request.user.is_authenticated and request.user.pk and not request.token_user %}
<span id="user-options" data-desktop_notifications="{{ request.user.profile.desktop_notifications }}"><span>
<ul class="nav navbar-nav navbar-right" id="dashboard-menu">
{% if request.user.is_superuser %}
{% if ADMIN_ENABLED %}
......
{% 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-unlock"></i>
{% trans "Disable two-factor authentication" %}
</h3>
</div>
<div class="panel-body">
<form action="" method="POST">
{% csrf_token %}
<input type="hidden" value="" name="{{ form.two_factor_secret.name }}"/>
{{ form.confirmation_code|as_crispy_field }}
<button type="submit" class="btn btn-warning">
<i class="fa fa-unlock"></i>
{% trans "Disable" %}
</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
<i class="fa fa-lock"></i>
{% trans "Enable two-factor authentication" %}
</h3>
</div>
<div class="panel-body">
{% blocktrans with lang=LANGUAGE_CODE %}
To use two-factor authentication you need to download Google Authenticator
and use the following qr code, secret key or link to set it up.
If you need help with the download or setup check out the
<a href="https://support.google.com/accounts/answer/1066447?hl={{ lang }}">
official help page.
</a>
{% endblocktrans %}
<hr />
<div id="two-factor-qr">
<span>
{% blocktrans with secret=secret %}
Your secret key is: <strong>{{ secret }}</strong>
{% endblocktrans %}
</span>
<img src="//chart.googleapis.com/chart?chs=255x255&chld=L|0&cht=qr&chl={{ uri }}"/>
<small><a href="{{ uri }}">{{ uri }}</a></small>
</div>
<hr />
<div id="two-factor-confirm">
<form action="" method="POST">
{% csrf_token %}
<input type="hidden" value="{{ secret }}" name="{{ form.two_factor_secret.name }}"/>
{% blocktrans %}
If you managed to set up the authenticator click enable to finalize two-factor
authentication for this account.
{% endblocktrans %}
<button type="submit" class="btn btn-success">
<i class="fa fa-lock"></i>
{% trans "Enable" %}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
......@@ -137,8 +137,10 @@
{% if user.is_superuser %}
<hr />
<script type="text/javascript" src="/static/admin/js/jquery.min.js"></script>
<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script>
<script type="text/javascript" src="/static/admin/js/vendor/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script>
<script type="text/javascript" src="/static/autocomplete_light/jquery.init.js"></script>
<script type="text/javascript" src="/static/autocomplete_light/vendor/select2/dist/js/select2.js"></script>
{{ group_perm_form.media }}
<h3>{% trans "Group permissions" %}</h3>
......
......@@ -52,7 +52,7 @@
<td>
<select class="form-control" name="perm-u-{{i.user.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
<option{%if id == i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
......@@ -72,7 +72,7 @@
<td>
<select class="form-control" name="perm-g-{{i.group.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
<option{%if id == i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
......
......@@ -56,6 +56,11 @@
<span class="label label-warning">{% trans "Offline" %}</span>
{% endif %}
</div>
<div>
{% for k, v in queues.iteritems %}
<span class="label label-{% if v %}success{% else %}danger{% endif %}">{{ k }}</span>
{% endfor %}
</div>
</div>
<div class="col-md-10" id="node-detail-pane">
<div class="panel panel-default" id="node-detail-panel">
......
......@@ -5,10 +5,11 @@
{% for a in activities %}
<div class="activity" data-activity-id="{{ a.pk }}">
<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-plus{% endif %}"></i>
<i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-{{a.icon}}{% endif %}"></i>
</span>
<strong title="{{ a.result.get_admin_text }}">
{{ a.readable_name.get_admin_text|capfirst }}
<a href="{{ a.get_absolute_url }}">
{{ a.readable_name.get_admin_text|capfirst }}</a>
</strong>
<span title="{{ a.started }}">{{ a.started|arrowfilter:LANGUAGE_CODE }}</span>{% if a.user %}, {{ a.user }}{% endif %}
......@@ -19,7 +20,8 @@
<div data-activity-id="{{ s.pk }}"
class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}">
<span title="{{ s.result.get_admin_text }}">
{{ s.readable_name|get_text:user }}
<a href="{{ s.get_absolute_url }}">
{{ s.readable_name|get_text:user }}</a>
</span>
&ndash;
{% if s.finished %}
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load hro %}
{% block content %}
<div class="body-content">
<div class="page-header">
<h1>
<div class="pull-right" id="vm-activity-state">
<span class="label label-{% if object.get_status_id == 'wait' %}info{% else %}{% if object.succeeded %}success{% else %}danger{% endif %}{% endif %}">
<span>{{ object.get_status_id|upper }}</span>
</span>
</div>
<i class="fa fa-{{icon}}"></i>
{{ object.node.name }}: {{object.readable_name|get_text:user}}
</h1>
</div>
<div class="row">
<div class="col-md-6" id="vm-info-pane">
{% include "dashboard/vm-detail/_activity-timeline.html" with active=object %}
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-body">
<dl>
<dt>{% trans "activity code" %}</dt>
<dd>{{object.activity_code}}</dd>
<dt>{% trans "node" %}</dt>
<dd><a href="{{object.node.get_absolute_url}}">{{object.node}}</a></dd>
<dt>{% trans "time" %}</dt>
<dd>{{object.started|default:'n/a'}} → {{object.finished|default:'n/a'}}</dd>
<dt>{% trans "user" %}</dt>
<dd>
<a href="{{ object.user.profile.get_absolute_url }}">
{{object.user|default:'(system)'}}</a></dd>
<dt>{% trans "type" %}</dt>
<dd>
{% if object.parent %}
{% blocktrans with url=object.parent.get_absolute_url name=object.parent %}
subactivity of <a href="{{url}}">{{name}}</a>
{% endblocktrans %}
{% else %}{% trans "top level activity" %}{% endif %}
</dd>
<dt>{% trans "task uuid" %}</dt>
<dd>{{ object.task_uuid|default:'n/a' }}</dd>
<dt>{% trans "status" %}</dt>
<dd id="activity_status">{{ object.get_status_id }}</dd>
<dt>{% trans "result" %}</dt>
<dd><textarea class="form-control" id="activity_result_text">{{object.result|get_text:user}}</textarea></dd>
<dt>{% trans "subactivities" %}</dt>
{% for s in object.children.all %}
<dd>
<span{% if s.result %} title="{{ s.result|get_text:user }}"{% endif %}>
<a href="{{ s.get_absolute_url }}">
{{ s.readable_name|get_text:user|capfirst }}</a></span> &ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
<i class="fa fa-refresh fa-spin" class="sub-activity-loading-icon"></i>
{% endif %}
{% if s.has_failed %}
<div class="label label-danger">{% trans "failed" %}</div>
{% endif %}
</dd>
{% empty %}
<dd>{% trans "none" %}</dd>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
......@@ -23,6 +23,28 @@
<legend>{% trans "Password change" %}</legend>
{% crispy forms.change_password %}
</fieldset>
<fieldset style="margin-top: 25px;">
<legend>{% trans "Two-factor authentication" %}</legend>
{% if profile.two_factor_secret %}
{% blocktrans %}
Two-factor authentication is currently enabled on your account. To disable it
click the button
{% endblocktrans %}
<a href="{% url "dashboard.views.profile-disable-two-factor" %}" class="btn btn-warning btn-xs">
<i class="fa fa-unlock"></i>
{% trans "Disable" %}
</a>
{% else %}
{% blocktrans %}
Two-factor authentication is currently disabled on your account. To enable it
click the button
{% endblocktrans %}
<a href="{% url "dashboard.views.profile-enable-two-factor" %}" class="btn btn-success btn-xs">
<i class="fa fa-lock"></i>
{% trans "Enable" %}
</a>
{% endif %}
</fieldset>
</div>
<div class="col-md-4" style="margin-bottom: 50px;">
<fieldset>
......
{% extends "dashboard/base.html" %}
{% load staticfiles %}
{% load pipeline %}
{% load sizefieldtags %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% load crispy_forms_tags %}
......@@ -105,4 +106,57 @@
</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-pie-chart"></i>
{% trans "Disk usage breakdown" %}
</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-9">
<canvas id="datastore-chart"></canvas>
</div>
<div class="col-md-3">
<div id="datastore-chart-legend"></div>
</div>
</div>
<div id="chart-data" data-data='{
"data": [{{stats.template_actual_size}},
{{stats.vm_actual_size}},
{{stats.dumps}},
{{stats.iso_raw}},
{{stats.trash}}],
"readable_data": ["{{stats.template_actual_size|filesize}}",
"{{stats.vm_actual_size|filesize}}",
"{{stats.dumps|filesize}}",
"{{stats.iso_raw|filesize}}",
"{{stats.trash|filesize}}"],
"labels": ["{% trans "Templates" %}",
"{% trans "Virtual machines" %}",
"{% trans "Memory dumps" %}",
"{% trans "ISO + Raw images" %}",
"{% trans "Trash" %}"]
}
'>
</div>
<div>
{% trans "Total disk usage of virtual machines" %}:
<strong>{{ stats.vm_actual_size|filesize }}</strong>
<br />
{% trans "Total virtual disk usage of virtual machines" %}:
<strong>{{ stats.vm_size|filesize}}</strong>
</div>
</div><!-- .panel-body -->
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{% javascript "datastore" %}
{% endblock %}
......@@ -63,18 +63,36 @@
</div>
</div>
{% comment %}
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin"><i class="fa fa-desktop"></i> Placeholder</h3>
<h3 class="no-margin">
<i class="fa fa-puzzle-piece"></i>
{% trans "Rarely used templates" %}
</h3>
</div>
<div class="panel-body">
???
<dl>
<dt>{% trans "Never instantiated" %}</dd>
<dd>
{% include "dashboard/_list-templates.html" with templates=unused_templates.never_instantiated %}
</dd>
<dt>{% trans "Templates without running instances" %}</dd>
<dd>
{% include "dashboard/_list-templates.html" with templates=unused_templates.templates_wo_instances %}
</dd>
<dt>{% trans "Templates without instances, last instance created more than 90 days ago" %}</dd>
<dd>
{% include "dashboard/_list-templates.html" with templates=unused_templates.templates_wo_instances_90 %}
</dd>
<dt>{% trans "Templates without instances, last instance created more than 180 days ago" %}</dd>
<dd>
{% include "dashboard/_list-templates.html" with templates=unused_templates.templates_wo_instances_180 %}
</dd>
</dl>
</div>
</div>
</div>
{% endcomment %}
</div>
{% endif %}
{% endblock %}
......@@ -55,7 +55,7 @@
</div>
<h1>
<div id="vm-details-rename" class="vm-details-home-rename-form-div">
<form action="" method="POST" id="vm-details-rename-form">
<form action="{{ op.rename.get_url }}" method="POST" id="vm-details-rename-form">
{% csrf_token %}
<div class="input-group vm-details-home-name">
<input id="vm-details-rename-name" class="form-control input-sm" name="new_name" type="text" value="{{ instance.name }}"/>
......@@ -132,10 +132,10 @@
<dd>
<div class="input-group">
<input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags"
value="{{ instance.pw }}" spellcheck="false"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show"
value="{{ instance.pw }}" spellcheck="false" autocomplete="new-password"/>
<span class="input-group-addon btn btn-default input-tags vm-details-show-password"
title="{% trans "Show password" %}" data-container="body">
<i class="fa fa-eye" id="vm-details-pw-eye"></i>
<i class="fa fa-eye vm-details-password-eye"></i>
</span>
</div>
</dd>
......@@ -158,22 +158,24 @@
<div class="input-group dashboard-vm-details-connect-command">
<span class="input-group-addon input-tags">{% trans "Command" %}</span>
<input type="text" spellcheck="false"
value="{{ c }}"
id="vm-details-connection-string" class="form-control input-tags" />
<span class="input-group-addon input-tags vm-details-connection-string-copy"
title="{% trans "Select all" %}" data-container="body">
value="{{ c.cmd }}"
id="vm-details-connection-string-{{ c.id }}" class="form-control input-tags" />
<span class="input-group-addon btn btn-default input-tags vm-details-show-password"
title="{% trans "Show password" %}" data-container="body">
<i class="fa fa-eye vm-details-password-eye"></i>
</span>
<span class="input-group-addon input-tags btn btn-default vm-details-connection-string-copy"
title="{% trans "Copy to clipboard" %}"
data-container="body"
data-clipboard-target="#vm-details-connection-string-{{ c.id }}">
<i class="fa fa-copy"></i>
</span>
</div>
{% empty %}
<div class="input-group dashboard-vm-details-connect-command">
<span class="input-group-addon input-tags">{% trans "Command" %}</span>
<input type="text" spellcheck="false" value="{% trans "Connection is not possible." %}"
id="vm-details-connection-string" class="form-control input-tags" />
<span class="input-group-addon input-tags" id="vm-details-connection-string-copy">
<i class="fa fa-copy" title="{% trans "Select all" %}"></i>
</span>
</div>
{% endfor %}
{% if instance.get_connect_uri %}
......@@ -192,11 +194,11 @@
{% endif %}
</div>
<div class="col-md-8" id="vm-detail-pane">
<div class="big" id="vm-detail-successfull-boot">
<span class="label label-info" data-status="{{ instance.status }}">
<div class="big" id="vm-detail-successful-boot">
<div class="label label-info" data-status="{{ instance.status }}">
<i class="fa fa-check"></i>
{% trans "The virtual machine successfully started, you can connect now." %}
</span>
</div>
</div>
<div class="panel panel-default" id="vm-detail-panel">
<ul class="nav nav-pills panel-heading">
......
......@@ -13,7 +13,7 @@
<select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select>
<div class="input-group-btn">
<button type="submit" class="btn btn-success btn-sm
{% if not is_operator %}disabled{% endif %}">
{% if not op.add_port %}disabled{% endif %}">
<span class="hidden-xs">{% trans "Add" %}</span>
<span class="visible-xs"><i class="fa fa-plus-circle"></i></span>
</button>
......
......@@ -16,7 +16,7 @@
<small class="vm-details-home-edit-name">{{ instance.name }}</small>
</div>
<div class="js-hidden vm-details-home-rename-form-div" id="vm-details-home-rename">
<form method="POST">
<form action="{{ op.rename.get_url }}" method="POST">
{% csrf_token %}
<div class="input-group">
<input type="text" name="new_name" value="{{ instance.name }}" class="form-control input-sm"/>
......@@ -59,8 +59,8 @@
{% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %}
<span id="vm-details-renew-op">
{% with op=op.renew %}{% if op %}
<a href="{{op.get_url}}" class="btn btn-{{op.effect}} btn-xs
operation operation-{{op.op}}">
<a href="{{op.get_url}}" class="btn btn-xs operation operation-{{ op.op }}
{% if op.disabled %}btn-default disabled{% else %}btn-{{op.effect}}{% endif %}">
<i class="fa fa-{{op.icon}}"></i>
{{op.name}}
</a>
......
......@@ -18,7 +18,7 @@
<h3 class="list-group-item-heading dashboard-vm-details-network-h3">
<i class="fa fa-{% if i.host %}globe{% else %}link{% endif %}"></i> {{ i.vlan.name }}
{% if not i.host%}({% trans "unmanaged" %}){% endif %}
{% if user.is_superuser %}
{% if user.is_superuser and i.host %}
<a href="{{ i.host.get_absolute_url }}"
class="btn btn-default btn-xs">{% trans "edit" %}</a>
{% endif %}
......@@ -83,7 +83,8 @@
<span class="operation-wrapper">
<a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}"
class="btn btn-link btn-xs operation"
title="{% trans "Remove" %}">
title="{% trans "Remove" %}"
{% if not op.remove_port %}disabled{% endif %}>
<i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i>
</a>
</span>
......@@ -118,7 +119,9 @@
{{ l.private }}/{{ l.proto }}
</td>
<td>
<a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv6.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a>
<a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv6.pk }}" title="{% trans "Remove" %}" {% if not op.remove_port %}disabled{% endif %}>
<i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i>
</a>
</td>
</tr>
{% endif %}
......
......@@ -14,6 +14,7 @@
{% trans "Sorting ... " %}
<!--<i class="fa fa-refresh fa-spin fa-2x"></i>-->
</div>
<a class="pull-right btn btn-success btn-xs vm-create" href="{% url "dashboard.views.vm-create" %}"><i class="fa fa-plus-circle"></i> {% trans "new virtual machine" %}</a>
<h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Virtual machines" %}</h3>
</div>
<div class="panel-body">
......
......@@ -4,9 +4,9 @@
{% if table.page %}
<div class="table-container">
{% endif %}
{% endspaceless %}
{% block table %}
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
{% nospaceless %}
{% block table.thead %}
<thead>
<tr>
......@@ -42,10 +42,9 @@
{% block table.tfoot %}
<tfoot></tfoot>
{% endblock table.tfoot %}
{% endnospaceless %}
</table>
{% endblock table %}
{% spaceless %}
{% if table.page %}
</div>
{% endif %}
......
......@@ -21,7 +21,7 @@ import warnings
from factory import Factory, Sequence
from mock import patch, MagicMock
from django.contrib.auth.models import User
from django.contrib.auth.models import User, AnonymousUser
from django.core.exceptions import PermissionDenied
from django.core.signing import TimestampSigner, JSONSerializer, b64_encode
from django.http import HttpRequest, Http404, QueryDict
......@@ -52,7 +52,8 @@ class ViewUserTestCase(unittest.TestCase):
def test_not_superuser(self):
request = FakeRequestFactory(superuser=False)
with patch.object(InstanceActivityDetail, 'get_object') as go:
go.return_value = MagicMock(spec=InstanceActivity)
go.return_value = MagicMock(spec=InstanceActivity,
activity_code='test.test')
go.return_value._meta.object_name = "InstanceActivity"
view = InstanceActivityDetail.as_view()
self.assertEquals(view(request, pk=1234).status_code, 200)
......@@ -61,7 +62,8 @@ class ViewUserTestCase(unittest.TestCase):
request = FakeRequestFactory(superuser=True)
with patch.object(InstanceActivityDetail, 'get_object') as go:
act = MagicMock(spec=InstanceActivity)
act = MagicMock(spec=InstanceActivity,
activity_code='test.test')
act._meta.object_name = "InstanceActivity"
act.user.pk = 1
go.return_value = act
......@@ -627,13 +629,13 @@ def FakeRequestFactory(user=None, **kwargs):
'''
if user is None:
user = UserFactory()
auth = kwargs.pop('authenticated', True)
user.is_authenticated = lambda: auth
user = UserFactory() if auth else AnonymousUser()
user.is_superuser = kwargs.pop('superuser', False)
if kwargs.pop('has_perms_mock', False):
user.has_perms = MagicMock(return_value=True)
user.save()
if auth:
user.save()
request = HttpRequest()
request.user = user
......
......@@ -29,22 +29,22 @@ class TemplateSyntaxTestCase(unittest.TestCase):
def test_templates(self):
"""Test all templates for syntax errors."""
for loader in Engine.get_default().template_loaders:
print loader
print(loader)
self._test_dir(loader.get_template_sources(''))
def _test_dir(self, dir, path="/"):
for i in dir:
i = join(path, i)
i = join(path, str(i))
if isfile(i):
self._test_template(join(path, i))
elif isdir(i):
print "%s:" % i
print("%s:" % i)
self._test_dir(listdir(i), i)
def _test_template(self, path):
print path
print(path)
try:
Template(open(path).read()).render(Context({}))
except (NoReverseMatch, VariableDoesNotExist, KeyError, AttributeError,
ValueError, ) as e:
print e
print(e)
......@@ -16,16 +16,15 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
from django.conf.urls import patterns, url, include
from django.conf.urls import url
import autocomplete_light
from vm.models import Instance
from .views import (
AclUpdateView, FavouriteView, GroupAclUpdateView, GroupDelete,
GroupDetailView, GroupList, IndexView,
InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail,
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeList,
NodeDetailView, NodeList, NodeActivityDetail,
NotificationView, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList,
vm_activity, VmCreate, VmDetailView,
......@@ -55,14 +54,14 @@ from .views import (
UserList,
StorageDetail, DiskDetail,
MessageList, MessageDetail, MessageCreate, MessageDelete,
EnableTwoFactorView, DisableTwoFactorView,
AclUserGroupAutocomplete, AclUserAutocomplete,
)
from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops
autocomplete_light.autodiscover()
urlpatterns = patterns(
'',
urlpatterns = [
url(r'^$', IndexView.as_view(), name="dashboard.index"),
url(r"^profile/list/$", UserList.as_view(),
name="dashboard.views.user-list"),
......@@ -133,6 +132,8 @@ urlpatterns = patterns(
name='dashboard.views.node-activity-list'),
url(r'^node/create/$', NodeCreate.as_view(),
name='dashboard.views.node-create'),
url(r'^node/activity/(?P<pk>\d+)/$', NodeActivityDetail.as_view(),
name='dashboard.views.node-activity'),
url(r'^favourite/$', FavouriteView.as_view(),
name='dashboard.views.favourite'),
......@@ -177,6 +178,10 @@ urlpatterns = patterns(
url(r'^profile/(?P<username>[^/]+)/$', ProfileView.as_view(),
name="dashboard.views.profile"),
url(r'^profile/(?P<username>[^/]+)/use_gravatar/$', toggle_use_gravatar),
url(r'^profile/two-factor/enable/$', EnableTwoFactorView.as_view(),
name="dashboard.views.profile-enable-two-factor"),
url(r'^profile/two-factor/disable/$', DisableTwoFactorView.as_view(),
name="dashboard.views.profile-disable-two-factor"),
url(r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$',
GroupRemoveUserView.as_view(),
......@@ -210,8 +215,6 @@ urlpatterns = patterns(
ConnectCommandCreate.as_view(),
name="dashboard.views.connect-command-create"),
url(r'^autocomplete/', include('autocomplete_light.urls')),
url(r"^store/list/$", StoreList.as_view(),
name="dashboard.views.store-list"),
url(r"^store/download/$", store_download,
......@@ -246,22 +249,26 @@ urlpatterns = patterns(
name="dashboard.views.message-create"),
url(r'^message/delete/(?P<pk>\d+)/$', MessageDelete.as_view(),
name="dashboard.views.message-delete"),
)
urlpatterns += patterns(
'',
*(url(r'^vm/(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems())
)
url(r'^autocomplete/acl/user-group/$',
AclUserGroupAutocomplete.as_view(),
name='autocomplete.acl.user-group'),
url(r'^autocomplete/acl/user/$',
AclUserAutocomplete.as_view(),
name='autocomplete.acl.user'),
]
urlpatterns += patterns(
'',
*(url(r'^vm/mass_op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_mass_ops.iteritems())
)
urlpatterns += [
url(r'^vm/(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems()
]
urlpatterns += patterns(
'',
*(url(r'^node/(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in node_ops.iteritems())
)
urlpatterns += [
url(r'^vm/mass_op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_mass_ops.iteritems()
]
urlpatterns += [
url(r'^node/(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in node_ops.iteritems()
]
......@@ -15,3 +15,4 @@ from graph import *
from storage import *
from request import *
from message import *
from autocomplete import *
......@@ -15,13 +15,16 @@
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
import autocomplete_light
import json
from dal import autocomplete
from django.contrib.auth.models import User
from django.utils.html import escape
from django.utils.translation import ugettext as _
from django.db.models import Q
from django.http import HttpResponse
from .views import AclUpdateView
from .models import Profile
from ..views import AclUpdateView
from ..models import Profile
def highlight(field, q, none_wo_match=True):
......@@ -48,13 +51,21 @@ def highlight(field, q, none_wo_match=True):
return escape(field)
class AclUserGroupAutocomplete(autocomplete_light.AutocompleteGenericBase):
search_fields = (
('first_name', 'last_name', 'username', 'email', 'profile__org_id'),
('name', 'groupprofile__org_id'),
)
choice_html_format = (u'<span data-value="%s"><span style="display:none"'
u'>%s</span>%s</span>')
class AclUserAutocomplete(autocomplete.Select2ListView):
search_fields = ('first_name', 'last_name', 'username',
'email', 'profile__org_id')
def filter(self, qs, search_fields):
if self.q:
condition = Q()
for field in search_fields:
condition |= Q(**{field + '__icontains': unicode(self.q)})
return list(qs.filter(condition))
return []
def get_list(self):
users = AclUpdateView.get_allowed_users(self.request.user)
return self.filter(users, self.search_fields)
def choice_displayed_text(self, choice):
q = unicode(self.request.GET.get('q', ''))
......@@ -71,35 +82,17 @@ class AclUserGroupAutocomplete(autocomplete_light.AutocompleteGenericBase):
else:
return _('%s (group)') % name
def choice_html(self, choice):
return self.choice_html_format % (
self.choice_value(choice), self.choice_label(choice),
self.choice_displayed_text(choice))
def choices_for_request(self):
user = self.request.user
self.choices = (AclUpdateView.get_allowed_users(user),
AclUpdateView.get_allowed_groups(user))
return super(AclUserGroupAutocomplete, self).choices_for_request()
def autocomplete_html(self):
html = []
for choice in self.choices_for_request():
html.append(self.choice_html(choice))
if not html:
html = self.empty_html_format % _('no matches found').capitalize()
return self.autocomplete_html_format % ''.join(html)
def get(self, *args, **kwargs):
return HttpResponse(json.dumps({
'results': [dict(id=unicode(r), text=self.choice_displayed_text(r))
for r in self.get_list()]
}), content_type="application/json")
class AclUserAutocomplete(AclUserGroupAutocomplete):
def choices_for_request(self):
user = self.request.user
self.choices = (AclUpdateView.get_allowed_users(user), )
return super(AclUserGroupAutocomplete, self).choices_for_request()
class AclUserGroupAutocomplete(AclUserAutocomplete):
group_search_fields = ('name', 'groupprofile__org_id')
autocomplete_light.register(AclUserGroupAutocomplete)
autocomplete_light.register(AclUserAutocomplete)
def get_list(self):
groups = AclUpdateView.get_allowed_groups(self.request.user)
groups = self.filter(groups, self.group_search_fields)
return super(AclUserGroupAutocomplete, self).get_list() + groups
......@@ -171,6 +171,7 @@ class TemplateVms(object):
def get_minmax(self):
return (0, None)
register_graph(TemplateVms, 'instances', TemplateGraphView)
......@@ -197,6 +198,7 @@ class Ram(object):
def get_minmax(self):
return (0, 105)
register_graph(Ram, 'memory', VmGraphView)
register_graph(Ram, 'memory', NodeGraphView)
......@@ -212,6 +214,7 @@ class Cpu(object):
else:
return (0, self.obj.num_cores * 100 + 5)
register_graph(Cpu, 'cpu', VmGraphView)
register_graph(Cpu, 'cpu', NodeGraphView)
......@@ -236,6 +239,7 @@ class VmNetwork(object):
params))
return 'group(%s)' % ','.join(metrics) if metrics else None
register_graph(VmNetwork, 'network', VmGraphView)
......@@ -251,6 +255,7 @@ class NodeNetwork(object):
'10), ".*\.bytes_(sent|recv)-([a-zA-Z0-9]+).*", "\\2 \\1")' % (
self.obj.metric_prefix))
register_graph(NodeNetwork, 'network', NodeGraphView)
......@@ -262,6 +267,7 @@ class NodeVms(object):
def get_minmax(self):
return (0, None)
register_graph(NodeVms, 'vm', NodeGraphView)
......@@ -282,6 +288,7 @@ class NodeAllocated(object):
def get_minmax(self):
return (0, None)
register_graph(NodeAllocated, 'alloc', NodeGraphView)
......@@ -302,6 +309,7 @@ class NodeListAllocated(object):
def get_minmax(self):
return (0, None)
register_graph(NodeListAllocated, 'alloc', NodeListGraphView)
......@@ -315,4 +323,5 @@ class NodeListVms(object):
def get_minmax(self):
return (0, None)
register_graph(NodeListVms, 'vm', NodeListGraphView)
......@@ -18,7 +18,7 @@ from __future__ import unicode_literals, absolute_import
import logging
from django.core.cache import get_cache
from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.conf import settings
from django.contrib.auth.models import Group, User
......@@ -103,7 +103,6 @@ class IndexView(LoginRequiredMixin, TemplateView):
# toplist
if settings.STORE_URL:
cache_key = "files-%d" % self.request.user.pk
cache = get_cache("default")
files = cache.get(cache_key)
if not files:
try:
......
......@@ -27,7 +27,6 @@ from django.db.models import Count
from django.forms.models import inlineformset_factory
from django.http import HttpResponse
from django.shortcuts import redirect
from django.template import RequestContext
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from django.views.generic import DetailView, TemplateView, View
......@@ -37,6 +36,7 @@ from django_tables2 import SingleTableView
from firewall.models import Host
from vm.models import Node, NodeActivity, Trait
from vm.tasks.vm_tasks import check_queue
from ..forms import TraitForm, HostForm, NodeForm
from ..tables import NodeListTable
......@@ -81,6 +81,20 @@ node_ops = OrderedDict([
])
def _get_activity_icon(act):
op = act.get_operation()
if op and op.id in node_ops:
return node_ops[op.id].icon
else:
return "cog"
def _format_activities(acts):
for i in acts:
i.icon = _get_activity_icon(i)
return acts
class NodeDetailView(LoginRequiredMixin,
GraphMixin, DetailView):
template_name = "dashboard/node-detail.html"
......@@ -103,10 +117,17 @@ class NodeDetailView(LoginRequiredMixin,
context['ops'] = get_operations(self.object, self.request.user)
context['op'] = {i.op: i for i in context['ops']}
context['show_show_all'] = len(na) > 10
context['activities'] = na[:10]
context['activities'] = _format_activities(na[:10])
context['trait_form'] = form
context['graphite_enabled'] = (
settings.GRAPHITE_URL is not None)
node_hostname = self.object.host.hostname
context['queues'] = {
'vmcelery.fast': check_queue(node_hostname, "vm", "fast"),
'vmcelery.slow': check_queue(node_hostname, "vm", "slow"),
'netcelery.fast': check_queue(node_hostname, "net", "fast"),
}
return context
def post(self, request, *args, **kwargs):
......@@ -298,8 +319,8 @@ class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View):
show_all = request.GET.get("show_all", "false") == "true"
node = Node.objects.get(pk=pk)
activities = NodeActivity.objects.filter(
node=node, parent=None).order_by('-started').select_related()
activities = _format_activities(NodeActivity.objects.filter(
node=node, parent=None).order_by('-started').select_related())
show_show_all = len(activities) > 10
if not show_all:
......@@ -308,11 +329,30 @@ class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View):
response = {
'activities': render_to_string(
"dashboard/node-detail/_activity-timeline.html",
RequestContext(request, {'activities': activities,
'show_show_all': show_show_all}))
{
'activities': activities,
'show_show_all': show_show_all
},
request
)
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
class NodeActivityDetail(LoginRequiredMixin, SuperuserRequiredMixin,
DetailView):
model = NodeActivity
context_object_name = 'nodeactivity' # much simpler to mock object
template_name = 'dashboard/nodeactivity_detail.html'
def get_context_data(self, **kwargs):
ctx = super(NodeActivityDetail, self).get_context_data(**kwargs)
ctx['activities'] = _format_activities(NodeActivity.objects.filter(
node=self.object.node, parent=None
).order_by('-started').select_related())
ctx['icon'] = _get_activity_icon(self.object)
return ctx
......@@ -91,6 +91,7 @@ class StorageDetail(SuperuserRequiredMixin, UpdateView):
return qs
def _get_stats(self):
# datastore stats
stats = self.object.get_statistics()
free_space = int(stats['free_space'])
free_percent = float(stats['free_percent'])
......@@ -98,11 +99,32 @@ class StorageDetail(SuperuserRequiredMixin, UpdateView):
total_space = free_space / (free_percent/100.0)
used_space = total_space - free_space
# file stats
data = self.get_object().get_file_statistics()
dumps_size = sum(d['size'] for d in data['dumps'])
trash = sum(d['size'] for d in data['trash'])
iso_raw = sum(d['size'] for d in data['disks']
if d['format'] in ("iso", "raw"))
vm_size = vm_actual_size = template_actual_size = 0
for d in data['disks']:
if d['format'] == "qcow2" and d['type'] == "normal":
template_actual_size += d['actual_size']
else:
vm_size += d['size']
vm_actual_size += d['actual_size']
return {
'used_percent': int(100 - free_percent),
'free_space': filesizeformat(free_space),
'used_space': filesizeformat(used_space),
'total_space': filesizeformat(total_space),
'dumps': dumps_size,
'trash': trash,
'iso_raw': iso_raw,
'vm_size': vm_size,
'vm_actual_size': vm_actual_size,
'template_actual_size': template_actual_size,
}
def get_success_url(self):
......
......@@ -24,12 +24,11 @@ from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.template.defaultfilters import urlencode
from django.core.cache import get_cache
from django.core.cache import cache
from django.core.exceptions import SuspiciousOperation
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from django.shortcuts import redirect, render_to_response, render
from django.template import RequestContext
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_GET, require_POST
from django.views.generic import TemplateView
......@@ -65,7 +64,7 @@ class StoreList(LoginRequiredMixin, TemplateView):
context = self.get_context_data(**kwargs)
return render_to_response(
"dashboard/store/_list-box.html",
RequestContext(self.request, context),
context, self.request
)
else:
return super(StoreList, self).get(*args, **kwargs)
......@@ -193,7 +192,6 @@ def store_new_directory(request):
@login_required
def store_refresh_toplist(request):
cache_key = "files-%d" % request.user.pk
cache = get_cache("default")
try:
store = Store(request.user)
toplist = store.toplist()
......
......@@ -16,6 +16,7 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals, absolute_import
from datetime import timedelta
import json
import logging
......@@ -24,8 +25,10 @@ from django.contrib.auth.models import User
from django.contrib.messages.views import SuccessMessageMixin
from django.core.urlresolvers import reverse, reverse_lazy
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.db.models import Count
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, get_object_or_404
from django.utils import timezone
from django.utils.translation import ugettext as _, ugettext_noop
from django.views.generic import (
TemplateView, CreateView, UpdateView,
......@@ -36,7 +39,9 @@ from braces.views import (
)
from django_tables2 import SingleTableView
from vm.models import InstanceTemplate, InterfaceTemplate, Instance, Lease
from vm.models import (
InstanceTemplate, InterfaceTemplate, Instance, Lease, InstanceActivity
)
from storage.models import Disk
from ..forms import (
......@@ -203,6 +208,41 @@ class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView):
context['search_form'] = self.search_form
# templates without any instances
# [t for t in InstanceTemplate.objects.all()
# if t.instance_set.count() < 1]
never_instantiated = context['object_list'].annotate(
instance_count=Count("instance_set")).filter(instance_count__lt=1)
# templates without active virtual machines
active_statuses = Instance.STATUS._db_values - set(["DESTROYED"])
templates_wo_instances = context['object_list'].exclude(
pk__in=InstanceTemplate.objects.filter(
instance_set__status__in=active_statuses)
).exclude(pk__in=never_instantiated)
def get_create_acts_younger_than(days):
return InstanceActivity.objects.filter(
activity_code="vm.Instance.create",
finished__gt=timezone.now() - timedelta(days=days))
# templates without active virtual machines
# last machine started later than 90 days
templates_wo_i_90 = templates_wo_instances.exclude(
instance_set__activity_log__in=get_create_acts_younger_than(90))
# templates without active virtual machines
# last machine started later than 180 days
templates_wo_i_180 = templates_wo_instances.exclude(
instance_set__activity_log__in=get_create_acts_younger_than(180))
context['unused_templates'] = {
'never_instantiated': never_instantiated,
'templates_wo_instances': templates_wo_instances,
'templates_wo_instances_90': templates_wo_i_90,
'templates_wo_instances_180': templates_wo_i_180,
}
return context
def get(self, *args, **kwargs):
......
......@@ -23,17 +23,28 @@ from collections import OrderedDict
from urlparse import urljoin
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import User, Group
from django.contrib.auth.views import redirect_to_login
from django.contrib.sites.shortcuts import get_current_site
from django.core import signing
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse
from django.contrib import messages
from django.contrib.auth.views import redirect_to_login
from django.db.models import Q
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import redirect, render
from django.db.models import Q, Count, Sum
from django.http import (
HttpResponse, Http404, HttpResponseRedirect, JsonResponse
)
from django.shortcuts import redirect, render, resolve_url
from django.utils.http import is_safe_url
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django.views.generic import DetailView, View, DeleteView
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import DetailView, View, DeleteView, FormView
from django.views.generic.detail import SingleObjectMixin
from braces.views import LoginRequiredMixin
......@@ -44,6 +55,7 @@ from common.models import HumanReadableException, HumanReadableObject
from ..models import GroupProfile, Profile
from ..forms import TransferOwnershipForm
logger = logging.getLogger(__name__)
saml_available = hasattr(settings, "SAML_CONFIG")
......@@ -156,14 +168,31 @@ class FilterMixin(object):
def create_acl_queryset(self, model):
cleaned_data = self.search_form.cleaned_data
stype = cleaned_data.get('stype', "all")
superuser = stype == "all"
shared = stype == "shared" or stype == "all"
level = "owner" if stype == "owned" else "user"
stype = cleaned_data.get('stype', 'all')
superuser = stype == 'all'
shared = stype == 'shared' or stype == 'all'
level = 'owner' if stype == 'owned' else 'user'
user = self.request.user
queryset = model.get_objects_with_level(
level, self.request.user,
group_also=shared, disregard_superuser=not superuser,
)
level, user, group_also=shared, disregard_superuser=not superuser)
if stype == 'owned':
queryset = queryset.filter(owner=user)
elif stype == 'shared':
queryset = queryset.filter(owner=user)
pk_list = []
for record in queryset:
count = record.object_level_set.annotate(
Count('users'), Count('groups')).aggregate(
Sum('users__count'), Sum('groups__count'))
if (count['users__count__sum'] > 1 or
count['groups__count__sum'] > 0):
pk_list.append(record.pk)
queryset = queryset.filter(pk__in=pk_list)
elif stype == 'shared_with_me':
queryset = queryset.exclude(owner=user)
return queryset
......@@ -343,12 +372,9 @@ class AjaxOperationMixin(object):
store.used = True
else:
store = []
return HttpResponse(
json.dumps({'success': True,
'with_reload': self.with_reload,
'messages': [unicode(m) for m in store]}),
content_type="application=json"
)
return JsonResponse({'success': True,
'with_reload': self.with_reload,
'messages': [unicode(m) for m in store]})
else:
return resp
......@@ -378,11 +404,8 @@ class FormOperationMixin(object):
resp = super(FormOperationMixin, self).post(
request, extra, *args, **kwargs)
if request.is_ajax():
return HttpResponse(
json.dumps({
'success': True,
'with_reload': self.with_reload}),
content_type="application=json")
return JsonResponse({'success': True,
'with_reload': self.with_reload})
else:
return resp
else:
......@@ -751,3 +774,61 @@ class DeleteViewBase(LoginRequiredMixin, DeleteView):
else:
messages.success(request, self.success_message)
return HttpResponseRedirect(self.get_success_url())
# only in Django 1.9
class LoginView(FormView):
"""
Displays the login form and handles the login action.
"""
form_class = AuthenticationForm
authentication_form = None
redirect_field_name = REDIRECT_FIELD_NAME
template_name = 'registration/login.html'
redirect_authenticated_user = False
extra_context = None
@method_decorator(sensitive_post_parameters())
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
if (self.redirect_authenticated_user and
self.request.user.is_authenticated):
redirect_to = self.get_success_url()
if redirect_to == self.request.path:
raise ValueError(
"Redirection loop for authenticated user detected. Check "
"your LOGIN_REDIRECT_URL doesn't point to a login page."
)
return HttpResponseRedirect(redirect_to)
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_success_url(self):
"""Ensure the user-originating redirection URL is safe."""
redirect_to = self.request.POST.get(
self.redirect_field_name,
self.request.GET.get(self.redirect_field_name, '')
)
if not is_safe_url(url=redirect_to, host=self.request.get_host()):
return resolve_url(settings.LOGIN_REDIRECT_URL)
return redirect_to
def get_form_class(self):
return self.authentication_form or self.form_class
def form_valid(self, form):
"""Security check complete. Log the user in."""
auth_login(self.request, form.get_user())
return HttpResponseRedirect(self.get_success_url())
def get_context_data(self, **kwargs):
context = super(LoginView, self).get_context_data(**kwargs)
current_site = get_current_site(self.request)
context.update({
self.redirect_field_name: self.get_success_url(),
'site': current_site,
'site_name': current_site.name,
})
if self.extra_context is not None:
context.update(self.extra_context)
return context
......@@ -28,9 +28,10 @@ from django.contrib.auth.decorators import login_required
from django.core import signing
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse, reverse_lazy
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.http import (
HttpResponse, Http404, HttpResponseRedirect, JsonResponse
)
from django.shortcuts import redirect, get_object_or_404
from django.template import RequestContext
from django.template.loader import render_to_string
from django.utils.translation import (
ugettext as _, ugettext_noop, ungettext_lazy,
......@@ -44,6 +45,7 @@ from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from common.models import (
create_readable, HumanReadableException, fetch_human_exception,
split_activity_code,
)
from firewall.models import Vlan, Host, Rule
from manager.scheduler import SchedulerError
......@@ -65,6 +67,7 @@ from ..forms import (
VmMigrateForm, VmDeployForm,
VmPortRemoveForm, VmPortAddForm,
VmRemoveInterfaceForm,
VmRenameForm,
)
from request.models import TemplateAccessType, LeaseType
from request.forms import LeaseRequestForm, TemplateRequestForm
......@@ -102,6 +105,19 @@ class VmDetailView(GraphMixin, CheckedDetailView):
template_name = "dashboard/vm-detail.html"
model = Instance
def get(self, *args, **kwargs):
if self.request.is_ajax():
return JsonResponse(self.get_json_data())
else:
return super(VmDetailView, self).get(*args, **kwargs)
def get_json_data(self):
instance = self.get_object()
return {"status": instance.status,
"host": instance.get_connect_host(),
"port": instance.get_connect_port(),
"password": instance.pw}
def get_context_data(self, **kwargs):
context = super(VmDetailView, self).get_context_data(**kwargs)
instance = context['instance']
......@@ -183,7 +199,6 @@ class VmDetailView(GraphMixin, CheckedDetailView):
def post(self, request, *args, **kwargs):
options = {
'new_name': self.__set_name,
'new_description': self.__set_description,
'new_tag': self.__add_tag,
'to_remove': self.__remove_tag,
......@@ -194,29 +209,6 @@ class VmDetailView(GraphMixin, CheckedDetailView):
return v(request)
raise Http404()
def __set_name(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, "operator"):
raise PermissionDenied()
new_name = request.POST.get("new_name")
Instance.objects.filter(pk=self.object.pk).update(
**{'name': new_name})
success_message = _("VM successfully renamed.")
if request.is_ajax():
response = {
'message': success_message,
'new_name': new_name,
'vm_pk': self.object.pk
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(self.object.get_absolute_url())
def __set_description(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, "operator"):
......@@ -274,10 +266,7 @@ class VmDetailView(GraphMixin, CheckedDetailView):
message = u"Not success"
if request.is_ajax():
return HttpResponse(
json.dumps({'message': message}),
content_type="application=json"
)
return JsonResponse({'message': message})
else:
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.object.pk}))
......@@ -563,11 +552,8 @@ class VmResourcesChangeView(VmOperationView):
if request.is_ajax(): # this is not too nice
store = messages.get_messages(request)
store.used = True
return HttpResponse(
json.dumps({'success': False,
'messages': [unicode(m) for m in store]}),
content_type="application=json"
)
return JsonResponse({'success': False,
'messages': [unicode(m) for m in store]})
else:
return HttpResponseRedirect(instance.get_absolute_url() +
"#resources")
......@@ -733,6 +719,31 @@ class VmDeployView(FormOperationMixin, VmOperationView):
return kwargs
class VmRenameView(FormOperationMixin, VmOperationView):
op = 'rename'
icon = 'pencil'
effect = 'success'
show_in_toolbar = False
form_class = VmRenameForm
def post(self, request, extra=None, *args, **kwargs):
if extra is None:
extra = {}
form = self.form_class(self.request.POST, **self.get_form_kwargs())
if form.is_valid():
extra.update(form.cleaned_data)
resp = super(FormOperationMixin, self).post(
request, extra, *args, **kwargs)
success_message = _('VM successfully renamed.')
if request.is_ajax():
return JsonResponse({'new_name': extra['new_name']})
else:
messages.success(request, success_message)
return resp
else:
return self.get(request)
vm_ops = OrderedDict([
('deploy', VmDeployView),
('wake_up', VmOperationView.factory(
......@@ -782,6 +793,7 @@ vm_ops = OrderedDict([
op='install_keys', icon='key', effect='info',
show_in_toolbar=False,
)),
('rename', VmRenameView),
])
......@@ -789,6 +801,8 @@ def _get_activity_icon(act):
op = act.get_operation()
if op and op.id in vm_ops:
return vm_ops[op.id].icon
elif split_activity_code(act.activity_code)[-1] == u'console-accessed':
return "terminal"
else:
return "cog"
......@@ -1274,15 +1288,15 @@ def vm_activity(request, pk):
response['activities'] = render_to_string(
"dashboard/vm-detail/_activity-timeline.html",
RequestContext(request, context),
context, request
)
response['ops'] = render_to_string(
"dashboard/vm-detail/_operations.html",
RequestContext(request, context),
context, request
)
response['disk_ops'] = render_to_string(
"dashboard/vm-detail/_disk-operations.html",
RequestContext(request, context),
context, request
)
return HttpResponse(
......
......@@ -143,6 +143,7 @@ class SwitchPortAdmin(admin.ModelAdmin):
class EthernetDeviceAdmin(admin.ModelAdmin):
list_display = ('name', )
admin.site.register(Host, HostAdmin)
admin.site.register(Vlan, VlanAdmin)
admin.site.register(Rule, RuleAdmin)
......
......@@ -52,12 +52,19 @@ class MACAddressFormField(forms.Field):
class MACAddressField(models.Field):
description = _('MAC Address object')
__metaclass__ = models.SubfieldBase
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 17
super(MACAddressField, self).__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super(MACAddressField, self).deconstruct()
del kwargs['max_length']
return name, path, args, kwargs
def from_db_value(self, value, expression, connection, context):
return self.to_python(value)
def to_python(self, value):
if not value:
return None
......@@ -105,16 +112,25 @@ class IPAddressFormField(forms.Field):
class IPAddressField(models.Field):
description = _('IP Network object')
__metaclass__ = models.SubfieldBase
def __init__(self, version=4, serialize=True, *args, **kwargs):
kwargs['max_length'] = 100
self.version = version
super(IPAddressField, self).__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super(IPAddressField, self).deconstruct()
del kwargs['max_length']
if self.version != 4:
kwargs['version'] = self.version
return name, path, args, kwargs
def get_internal_type(self):
return "CharField"
def from_db_value(self, value, expression, connection, context):
return self.to_python(value)
def to_python(self, value):
if not value:
return None
......@@ -163,13 +179,22 @@ class IPNetworkFormField(forms.Field):
class IPNetworkField(models.Field):
description = _('IP Network object')
__metaclass__ = models.SubfieldBase
def __init__(self, version=4, serialize=True, *args, **kwargs):
kwargs['max_length'] = 100
self.version = version
super(IPNetworkField, self).__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super(IPNetworkField, self).deconstruct()
del kwargs['max_length']
if self.version != 4:
kwargs['version'] = self.version
return name, path, args, kwargs
def from_db_value(self, value, expression, connection, context):
return self.to_python(value)
def to_python(self, value):
if not value:
return None
......
......@@ -25,7 +25,7 @@ from .models import (Host, Rule, Vlan, Domain, Record, BlacklistItem,
SwitchPort)
from .iptables import IptRule, IptChain
import django.conf
from django.template import loader, Context
from django.template import loader
from django.utils import timezone
......@@ -152,9 +152,9 @@ class BuildFirewall:
template = loader.get_template('firewall/iptables.conf')
context['proto'] = 'ipv4'
ipv4 = unicode(template.render(Context(context)))
ipv4 = unicode(template.render(context))
context['proto'] = 'ipv6'
ipv6 = unicode(template.render(Context(context)))
ipv6 = unicode(template.render(context))
return (ipv4, ipv6)
......
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals, absolute_import
from django.core.management.base import BaseCommand, CommandError
import logging
from firewall.models import Firewall, VlanGroup, Rule
from django.contrib.auth.models import User
logger = logging.getLogger(__name__)
class Command(BaseCommand):
def add_arguments(self, parser):
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--port',
action='store',
dest='port',
type=int,
help='port which will open (0-65535)')
group.add_argument('--port-range',
action='store',
dest='range',
type=int,
nargs=2,
help='closed port range which will open (0-65535)',
metavar=('LOWER', 'HIGHER'))
parser.add_argument('--protocol',
action='store',
dest='proto',
required=True,
choices=('tcp', 'udp', 'icmp'),
help='protocol name')
parser.add_argument('--action',
action='store',
dest='action',
default='accept',
choices=('accept', 'drop', 'ignore'),
help='action of the rule')
parser.add_argument('--dir',
action='store',
dest='dir',
default='in',
choices=('in', 'out'),
help='direction of the rule')
parser.add_argument('--firewall',
action='store',
dest='firewall',
required=True,
help='firewall name which open the given port')
parser.add_argument('--vlan-group',
action='store',
dest='vlan_group',
required=True,
help='vlan group name where the port will open')
parser.add_argument('--owner',
action='store',
dest='owner',
required=True,
help='name of user who owns the rule')
def handle(self, *args, **options):
port = options['port']
range = options['range']
proto = options['proto']
action = options['action']
dir = options['dir']
owner = options['owner']
firewall = options['firewall']
fnet = options['vlan_group']
try:
owner = User.objects.get(username=owner)
firewall = Firewall.objects.get(name=firewall)
fnet = VlanGroup.objects.get(name=fnet)
except User.DoesNotExist:
raise CommandError("User '%s' does not exist" % owner)
except Firewall.DoesNotExist:
raise CommandError("Firewall '%s' does not exist" % firewall)
except VlanGroup.DoesNotExist:
raise CommandError("VlanGroup '%s' does not exist" % fnet)
if port:
self.validate_port(port)
try:
rule = self.make_rule(dport=port, proto=proto, action=action,
direction=dir, owner=owner,
firewall=firewall, foreign_network=fnet)
rule.save()
except Warning as e:
logger.warning(e)
else:
lower = min(range)
higher = max(range)
self.validate_port(lower)
self.validate_port(higher)
rules = []
for port in xrange(lower, higher+1):
try:
rule = self.make_rule(port, proto, action, dir,
owner, firewall, fnet)
rules.append(rule)
except Warning as e:
logger.warning(e)
Rule.objects.bulk_create(rules)
def make_rule(self, **kwargs):
rule, created = Rule.objects.get_or_create(**kwargs)
if not created:
raise Warning(('Rule does exist: %s' %
unicode(rule)).encode('utf-8'))
rule.full_clean()
return rule
def validate_port(self, port):
if port < 0 or port > 65535:
raise CommandError("Port '%i' not in range [0-65535]" % port)
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-07 19:09
from __future__ import unicode_literals
from django.db import migrations
import firewall.fields
class Migration(migrations.Migration):
dependencies = [
('firewall', '0005_auto_20150520_2250'),
]
operations = [
migrations.AlterField(
model_name='host',
name='ipv6',
field=firewall.fields.IPAddressField(blank=True, help_text='The global IPv6 address of the host, for example 2001:db:88:200::10.', null=True, unique=True, verbose_name='IPv6 address', version=6),
),
migrations.AlterField(
model_name='vlan',
name='network6',
field=firewall.fields.IPNetworkField(blank=True, help_text='The IPv6 address and the prefix length of the gateway.', null=True, verbose_name='IPv6 address/prefix', version=6),
),
]
......@@ -499,7 +499,11 @@ class Vlan(AclBase, models.Model):
def get_new_address(self):
hosts = self.host_set
used_v4 = IPSet(hosts.values_list('ipv4', flat=True))
used_ext_addrs = Host.objects.filter(
external_ipv4__isnull=False).values_list(
'external_ipv4', flat=True)
used_v4 = IPSet(hosts.values_list('ipv4', flat=True)).union(
used_ext_addrs).union([self.network4.ip])
used_v6 = IPSet(hosts.exclude(ipv6__isnull=True)
.values_list('ipv6', flat=True))
......
......@@ -77,13 +77,13 @@ class GetNewAddressTestCase(MockCeleryMixin, TestCase):
d = Domain(name='example.org', owner=self.u1)
d.save()
# /29 = .1-.6 = 6 hosts/subnet + broadcast + network id
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/29',
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.1/29',
network6='2001:738:2001:4031::/80', domain=d,
owner=self.u1)
self.vlan.clean()
self.vlan.full_clean()
self.vlan.save()
self.vlan.host_set.all().delete()
for i in [1] + range(3, 6):
for i in range(3, 6):
Host(hostname='h-%d' % i, mac='01:02:03:04:05:%02d' % i,
ipv4='10.0.0.%d' % i, vlan=self.vlan,
owner=self.u1).save()
......@@ -102,6 +102,15 @@ class GetNewAddressTestCase(MockCeleryMixin, TestCase):
owner=self.u1).save()
self.assertRaises(ValidationError, self.vlan.get_new_address)
def test_all_addr_in_use2(self):
Host(hostname='h-xd', mac='01:02:03:04:05:06',
ipv4='10.0.0.6', vlan=self.vlan,
owner=self.u1).save()
Host(hostname='h-arni', mac='01:02:03:04:05:02',
ipv4='100.0.0.1', vlan=self.vlan, external_ipv4='10.0.0.2',
owner=self.u1).save()
self.assertRaises(ValidationError, self.vlan.get_new_address)
def test_new_addr(self):
used_v4 = IPSet(self.vlan.host_set.values_list('ipv4', flat=True))
assert self.vlan.get_new_address()['ipv4'] not in used_v4
......@@ -114,7 +123,7 @@ class HostGetHostnameTestCase(MockCeleryMixin, TestCase):
self.d = Domain(name='example.org', owner=self.u1)
self.d.save()
Record.objects.all().delete()
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/24',
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.1/24',
network6='2001:738:2001:4031::/80', domain=self.d,
owner=self.u1, network_type='portforward',
snat_ip='10.1.1.1')
......@@ -194,13 +203,13 @@ class ReloadTestCase(MockCeleryMixin, TestCase):
self.u1 = User.objects.create(username='user1')
self.u1.save()
d = Domain.objects.create(name='example.org', owner=self.u1)
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/29',
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.1/29',
snat_ip='152.66.243.99',
network6='2001:738:2001:4031::/80', domain=d,
owner=self.u1, network_type='portforward',
dhcp_pool='manual')
self.vlan.save()
self.vlan2 = Vlan(vid=2, name='pub', network4='10.1.0.0/29',
self.vlan2 = Vlan(vid=2, name='pub', network4='10.1.0.1/29',
network6='2001:738:2001:4032::/80', domain=d,
owner=self.u1, network_type='public')
self.vlan2.save()
......
......@@ -15,7 +15,7 @@
# 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.conf.urls import patterns, url
from django.conf.urls import url
from .views import (
IndexView,
HostList, HostDetail, HostCreate, HostDelete,
......@@ -33,8 +33,7 @@ from .views import (
VlanAclUpdateView
)
urlpatterns = patterns(
'',
urlpatterns = [
url('^$', IndexView.as_view(), name='network.index'),
# blacklist
url('^blacklist/$', BlacklistList.as_view(),
......@@ -135,4 +134,4 @@ urlpatterns = patterns(
remove_switch_port_device, name='network.remove_switch_port_device'),
url('^switchports/(?P<pk>\d+)/add/$', add_switch_port_device,
name='network.add_switch_port_device'),
)
]
......@@ -19,7 +19,6 @@ from django.forms import (
Textarea, ValidationError
)
from django.utils.translation import ugettext_lazy as _
from django.template import RequestContext
from django.template.loader import render_to_string
from sizefield.widgets import FileSizeWidget
......@@ -68,13 +67,15 @@ class InitialFromFileMixin(object):
super(InitialFromFileMixin, self).__init__(*args, **kwargs)
self.initial['message'] = render_to_string(
self.initial_template,
RequestContext(request, {}),
self.initial_template, {}, request
)
def clean_message(self):
def comp(x):
return "".join(x.strip().splitlines())
message = self.cleaned_data['message']
if message.strip() == self.initial['message'].strip():
if comp(message) == comp(self.initial['message']):
raise ValidationError(_("Fill in the message."), code="invalid")
return message.strip()
......
......@@ -38,6 +38,7 @@ class RequestTable(Table):
template_name="request/columns/user.html",
verbose_name=_("User"),
)
created = Column(verbose_name=_("Date"))
type = TemplateColumn(
template_name="request/columns/type.html",
verbose_name=_("Type"),
......@@ -48,7 +49,7 @@ class RequestTable(Table):
template = "django_tables2/with_pagination.html"
attrs = {'class': ('table table-bordered table-striped table-hover'),
'id': "request-list-table"}
fields = ("pk", "status", "type", "user", )
fields = ("pk", "status", "type", "created", "user", )
order_by = ("-pk", )
empty_text = _("No more requests.")
per_page = 10
......
......@@ -38,6 +38,9 @@
<pre>{{ object.message }}</pre>
</p>
<hr />
<div class="pull-right">
<strong>{% trans "Submitted" %}:</strong> {{ object.created }}
</div>
{% if object.type == "lease" %}
<dl>
<dt>{% trans "VM name" %}</dt>
......
......@@ -16,7 +16,7 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
from django.conf.urls import patterns, url
from django.conf.urls import url
from .views import (
RequestList, RequestDetail, RequestTypeList,
......@@ -26,8 +26,7 @@ from .views import (
LeaseTypeDelete, TemplateAccessTypeDelete, ResizeRequestView,
)
urlpatterns = patterns(
'',
urlpatterns = [
url(r'^list/$', RequestList.as_view(),
name="request.views.request-list"),
url(r'^(?P<pk>\d+)/$', RequestDetail.as_view(),
......@@ -62,4 +61,4 @@ urlpatterns = patterns(
name="request.views.request-resource"),
url(r'resize/(?P<vm_pk>\d+)/(?P<disk_pk>\d+)/$',
ResizeRequestView.as_view(), name="request.views.request-resize"),
)
]
......@@ -208,6 +208,12 @@ class VmRequestMixin(LoginRequiredMixin, object):
user = self.request.user
if not vm.has_level(user, self.user_level):
raise PermissionDenied()
if vm.destroyed_at:
message = _("Instance %(instance)s has already been destroyed.")
messages.error(self.request, message % {'instance': vm.name})
return redirect(vm.get_absolute_url())
return super(VmRequestMixin, self).dispatch(*args, **kwargs)
def get_context_data(self, **kwargs):
......
......@@ -110,6 +110,13 @@ class DataStore(Model):
disks = Disk.objects.filter(destroyed__isnull=True, is_ready=True)
return disks.exclude(filename__in=files)
@method_cache(120)
def get_file_statistics(self, timeout=30):
queue_name = self.get_remote_queue_name('storage', "slow")
data = storage_tasks.get_file_statistics.apply_async(
args=[self.path], queue=queue_name).get(timeout=timeout)
return data
class Disk(TimeStampedModel):
......@@ -542,4 +549,4 @@ class Disk(TimeStampedModel):
@property
def is_resizable(self):
return self.type in ('qcow2-norm', 'raw-rw')
return self.type in ('qcow2-norm', 'raw-rw', 'qcow2-snap', )
......@@ -81,3 +81,8 @@ def recover_from_trash(datastore, disk_path):
@celery.task(name='storagedriver.get_storage_stat')
def get_storage_stat(path):
pass
@celery.task(name='storagedriver.get_file_statistics')
def get_file_statistics(datastore):
pass
{% extends "registration/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load crispy_forms_tags %}
{% get_current_language as LANGUAGE_CODE %}
{% block title-page %}{% trans "Two-factor authentication" %}{% endblock %}
{% block content_box %}
<div class="row" id="two-factor-box">
<div class="col-md-12">
<h4>
{% blocktrans with username=user.username full_name=user.get_full_name %}
Welcome {{ full_name }} ({{ username }})!
{% endblocktrans %}
</h4>
<hr/>
<form action="" method="POST">
{% csrf_token %}
{{ form.confirmation_code|as_crispy_field }}
<button type="submit" class="btn btn-success">
{% trans "Confirm" %}
</button>
</form>
</div>
</div>
{% endblock %}
# This import is responsible for running the operations' registration code.
from . import operations # noqa
# noqa
......@@ -135,14 +135,6 @@ class InstanceActivity(ActivityModel):
def get_absolute_url(self):
return reverse('dashboard.views.vm-activity', args=[self.pk])
def get_status_id(self):
if self.succeeded is None:
return 'wait'
elif self.succeeded:
return 'success'
else:
return 'failed'
def has_percentage(self):
op = self.instance.get_operation_from_activity_code(self.activity_code)
return (self.task_uuid and op and op.has_percentage and
......@@ -215,6 +207,13 @@ class NodeActivity(ActivityModel):
app_label = 'vm'
db_table = 'vm_nodeactivity'
def get_operation(self):
return self.node.get_operation_from_activity_code(
self.activity_code)
def get_absolute_url(self):
return reverse('dashboard.views.node-activity', args=[self.pk])
def __unicode__(self):
if self.parent:
return '{}({})->{}'.format(self.parent.activity_code,
......
......@@ -447,12 +447,17 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
if new_node is False: # None would be a valid value
new_node = self.node
# log state change
if new_node:
msg = ugettext_noop("vm state changed to %(state)s on %(node)s")
else:
msg = ugettext_noop("vm state changed to %(state)s")
try:
act = InstanceActivity.create(
code_suffix='vm_state_changed',
readable_name=create_readable(
ugettext_noop("vm state changed to %(state)s on %(node)s"),
state=new_state, node=new_node),
readable_name=create_readable(msg, state=new_state,
node=new_node),
instance=self)
except ActivityInProgressError:
pass # discard state change if another activity is in progress.
......@@ -675,7 +680,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
with self.activity('notification_about_expiration',
readable_name=ugettext_noop(
"notify owner about expiration"),
on_commit=on_commit):
on_commit=on_commit, concurrency_check=False):
from dashboard.views import VmRenewView, absolute_url
level = self.get_level_object("owner")
for u, ulevel in self.get_users_with_level(level__pk=level.pk):
......@@ -710,7 +715,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def _is_suspend_expiring(self, threshold=0.1):
interval = self.lease.suspend_interval
if self.time_of_suspend is not None and interval is not None:
if (self.status != "SUSPENDED" and
self.time_of_suspend is not None and interval is not None):
limit = timezone.now() + timedelta(seconds=(
threshold * self.lease.suspend_interval.total_seconds()))
return limit > self.time_of_suspend
......
......@@ -160,6 +160,8 @@ class Node(OperatedMixin, TimeStampedModel):
"""
try:
self.get_remote_queue_name("vm", "fast")
self.get_remote_queue_name("vm", "slow")
self.get_remote_queue_name("net", "fast")
except:
return False
else:
......
......@@ -629,6 +629,7 @@ class RemovePortOperation(InstanceOperation):
name = _("close port")
description = _("Close the specified port.")
concurrency_check = False
acl_level = "operator"
required_perms = ('vm.config_ports', )
def _operation(self, activity, rule):
......@@ -647,6 +648,7 @@ class AddPortOperation(InstanceOperation):
name = _("open port")
description = _("Open the specified port.")
concurrency_check = False
acl_level = "operator"
required_perms = ('vm.config_ports', )
def _operation(self, activity, host, proto, port):
......@@ -859,7 +861,9 @@ class ShutOffOperation(InstanceOperation):
def _operation(self, activity):
# Shutdown networks
with activity.sub_activity('shutdown_net'):
with activity.sub_activity('shutdown_net',
readable_name=ugettext_noop(
"shutdown network")):
self.instance.shutdown_net()
self.instance._delete_vm(parent_activity=activity)
......@@ -1399,6 +1403,27 @@ class ResourcesOperation(InstanceOperation):
@register_operation
class RenameOperation(InstanceOperation):
id = "rename"
name = _("rename")
description = _("Change the name of virtual machine.")
acl_level = "operator"
required_perms = ()
def _operation(self, user, activity, new_name):
old_name = self.instance.name
self.instance.name = new_name
self.instance.full_clean()
self.instance.save()
return create_readable(ugettext_noop(
"Changed name from '%(old_name)s' to '%(new_name)s'."),
old_name=old_name, new_name=new_name
)
@register_operation
class PasswordResetOperation(RemoteAgentOperation):
id = 'password_reset'
name = _("password reset")
......
......@@ -57,7 +57,7 @@ def get_queues():
inspect = celery.control.inspect()
inspect.timeout = 0.5
result = inspect.active_queues()
logger.debug('Queue list of length %d cached.', len(result))
logger.debug('Queue list of length %d cached.', result and len(result))
cache.set(key, result, 10)
return result
......
......@@ -177,9 +177,9 @@ class InterfaceTestCase(MockCeleryMixin, TestCase):
i = Instance(id=10, owner=owner, access_method='rdp')
d = Domain(owner=owner)
d.save()
v = Vlan(vid=55, network4='127.0.0.1/8',
v = Vlan(name='vlan', vid=55, network4='127.0.0.1/8',
network6='2001::1/32', domain=d)
v.clean()
v.full_clean()
v.save()
Interface.create(i, v, managed=True, owner=owner)
......
[Unit]
Description=CIRCLE portal
After=network.target
[Service]
User=cloud
Group=cloud
WorkingDirectory=/home/cloud/circle/circle
ExecStart=/bin/bash -c "source /etc/profile; workon circle; exec /home/cloud/.virtualenvs/circle/bin/uwsgi --chdir=/home/cloud/circle/circle -H /home/cloud/.virtualenvs/circle --socket /tmp/uwsgi.sock --wsgi-file circle/wsgi.py --chmod-socket=666"
Restart=always
[Install]
WantedBy=multi-user.target
amqp==1.4.6
cryptography==2.0
amqp==1.4.7
anyjson==0.3.3
arrow==0.6.0
arrow==0.7.0
billiard==3.3.0.20
bpython==0.14.1
celery==3.1.18
Django==1.8.2
django-appconf==1.0.1
django-autocomplete-light==2.1.1
django-braces==1.8.0
django-crispy-forms==1.4.0
django-model-utils==2.2
djangosaml2==0.13.0
django-sizefield==0.7
Django==1.11.6
django-appconf==1.0.2
django-autocomplete-light==3.2.9
django-braces==1.11.0
django-crispy-forms==1.6.1
django-model-utils==3.0.0
django-pipeline==1.6.13
django-sizefield==0.9.1
django-statici18n==1.4.0
django-tables2==1.10.0
django-taggit==0.22.1
djangosaml2==0.16.10
git+https://git.ik.bme.hu/circle/django-sshkey.git
django-statici18n==1.1.3
django-tables2==0.16.0
django-taggit==0.14.0
docutils==0.12
Jinja2==2.7.3
jsonfield==1.0.3
kombu==3.0.26
kombu==3.0.30
logutils==0.3.3
MarkupSafe==0.23
netaddr==0.7.14
......@@ -29,6 +31,7 @@ Pygments==2.0.2
pylibmc==1.4.3
python-dateutil==2.4.2
pyinotify==0.9.5
pyotp==2.1.1
pytz==2015.4
requests==2.7.0
salt==2014.7.1
......@@ -38,6 +41,8 @@ six==1.9.0
slimit==0.8.1
sqlparse==0.1.15
pika==0.9.14
django-pipeline==1.4.7
Fabric==1.10.1
lxml==3.4.4
python-memcached==1.58
enum34==1.1.6
ipaddress==1.0.18
# Local development dependencies go here
-r base.txt
coverage==3.7.1
django-debug-toolbar==1.3.0
django-rosetta==0.7.6
django-debug-toolbar==1.8
django-rosetta==0.7.13
Sphinx==1.3.1
# Pro-tip: Try not to put anything here. There should be no dependency in
# production that isn't in development.
-r base.txt
uWSGI==2.0.10
uWSGI==2.0.13.1
......@@ -3,9 +3,9 @@
coverage==3.7.1
factory-boy==2.4.1
mock==1.0.1
django-nose==1.4
nose==1.3.6
nose-exclude==0.2.0
django-nose==1.4.4
nose==1.3.7
nose-exclude==0.5.0
selenium==2.45.0
selenose==1.3
#selenose==1.3
-e git+https://github.com/kmmbvnr/django-jenkins.git@019774dc2f668bc66b66f90f97eb8e14ae9566a4#egg=django_jenkins-dev
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment