Commit 8e1635ef by Kálmán Viktor

Merge branch 'master' into feature-pipeline

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/static/dashboard/vm-details.js
	circle/dashboard/templates/base.html
	circle/dashboard/templates/dashboard/index-vm.html
parents 9bcc0385 ea3039d6
...@@ -538,3 +538,6 @@ SESSION_COOKIE_NAME = "csessid%x" % (((getnode() // 139) ^ ...@@ -538,3 +538,6 @@ SESSION_COOKIE_NAME = "csessid%x" % (((getnode() // 139) ^
(getnode() % 983)) & 0xffff) (getnode() % 983)) & 0xffff)
MAX_NODE_RAM = get_env_variable("MAX_NODE_RAM", 1024) MAX_NODE_RAM = get_env_variable("MAX_NODE_RAM", 1024)
# Url to download the client: (e.g. http://circlecloud.org/client/download/)
CLIENT_DOWNLOAD_URL = get_env_variable('CLIENT_DOWNLOAD_URL', 'http://circlecloud.org/client/download/')
...@@ -214,6 +214,14 @@ class ActivityModel(TimeStampedModel): ...@@ -214,6 +214,14 @@ class ActivityModel(TimeStampedModel):
self.result_data = None if value is None else value.to_dict() self.result_data = None if value is None else value.to_dict()
@classmethod
def construct_activity_code(cls, code_suffix, sub_suffix=None):
code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix)
if sub_suffix:
return join_activity_code(code, sub_suffix)
else:
return code
@celery.task() @celery.task()
def compute_cached(method, instance, memcached_seconds, def compute_cached(method, instance, memcached_seconds,
...@@ -488,7 +496,7 @@ class HumanReadableException(HumanReadableObject, Exception): ...@@ -488,7 +496,7 @@ class HumanReadableException(HumanReadableObject, Exception):
"Level should be the name of an attribute of django." "Level should be the name of an attribute of django."
"contrib.messages (and it should be callable with " "contrib.messages (and it should be callable with "
"(request, message)). Like 'error', 'warning'.") "(request, message)). Like 'error', 'warning'.")
else: elif not hasattr(self, "level"):
self.level = "error" self.level = "error"
def send_message(self, request, level=None): def send_message(self, request, level=None):
......
...@@ -21,18 +21,22 @@ from django import contrib ...@@ -21,18 +21,22 @@ from django import contrib
from django.contrib.auth.admin import UserAdmin, GroupAdmin from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from dashboard.models import Profile, GroupProfile from dashboard.models import Profile, GroupProfile, ConnectCommand
class ProfileInline(contrib.admin.TabularInline): class ProfileInline(contrib.admin.TabularInline):
model = Profile model = Profile
class CommandInline(contrib.admin.TabularInline):
model = ConnectCommand
class GroupProfileInline(contrib.admin.TabularInline): class GroupProfileInline(contrib.admin.TabularInline):
model = GroupProfile model = GroupProfile
UserAdmin.inlines = (ProfileInline, ) UserAdmin.inlines = (ProfileInline, CommandInline, )
GroupAdmin.inlines = (GroupProfileInline, ) GroupAdmin.inlines = (GroupProfileInline, )
contrib.admin.site.unregister(User) contrib.admin.site.unregister(User)
......
import autocomplete_light import autocomplete_light
from django.contrib.auth.models import User
from django.utils.html import escape
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from .views import AclUpdateView from .views import AclUpdateView
from .models import Profile
class AclUserAutocomplete(autocomplete_light.AutocompleteGenericBase): def highlight(field, q, none_wo_match=True):
"""
>>> highlight('<b>Akkount Krokodil', 'kro', False)
u'&lt;b&gt;Akkount <span class="autocomplete-hl">Kro</span>kodil'
"""
if not field:
return None
try:
match = field.lower().index(q.lower())
except ValueError:
match = None
if q and match is not None:
match_end = match + len(q)
return (escape(field[:match])
+ '<span class="autocomplete-hl">'
+ escape(field[match:match_end])
+ '</span>' + escape(field[match_end:]))
elif none_wo_match:
return None
else:
return escape(field)
class AclUserGroupAutocomplete(autocomplete_light.AutocompleteGenericBase):
search_fields = ( search_fields = (
('^first_name', 'last_name', 'username', '^email', 'profile__org_id'), ('first_name', 'last_name', 'username', 'email', 'profile__org_id'),
('^name', 'groupprofile__org_id'), ('name', 'groupprofile__org_id'),
) )
autocomplete_js_attributes = {'placeholder': _("Name of group or user")} choice_html_format = (u'<span data-value="%s"><span style="display:none"'
choice_html_format = u'<span data-value="%s"><span>%s</span> %s</span>' u'>%s</span>%s</span>')
def choice_html(self, choice): def choice_displayed_text(self, choice):
try: q = unicode(self.request.GET.get('q', ''))
name = choice.get_full_name() name = highlight(unicode(choice), q, False)
except AttributeError: if isinstance(choice, User):
name = _('group') extra_fields = [highlight(choice.get_full_name(), q, False),
if name: highlight(choice.email, q)]
name = u'(%s)' % name try:
extra_fields.append(highlight(choice.profile.org_id, q))
except Profile.DoesNotExist:
pass
return '%s (%s)' % (name, ', '.join(f for f in extra_fields
if f))
else:
return _('%s (group)') % name
def choice_html(self, choice):
return self.choice_html_format % ( return self.choice_html_format % (
self.choice_value(choice), self.choice_label(choice), name) self.choice_value(choice), self.choice_label(choice),
self.choice_displayed_text(choice))
def choices_for_request(self): def choices_for_request(self):
user = self.request.user user = self.request.user
self.choices = (AclUpdateView.get_allowed_users(user), self.choices = (AclUpdateView.get_allowed_users(user),
AclUpdateView.get_allowed_groups(user)) AclUpdateView.get_allowed_groups(user))
return super(AclUserAutocomplete, self).choices_for_request() 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)
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()
autocomplete_light.register(AclUserGroupAutocomplete)
autocomplete_light.register(AclUserAutocomplete) autocomplete_light.register(AclUserAutocomplete)
...@@ -1383,7 +1383,6 @@ ...@@ -1383,7 +1383,6 @@
"time_of_suspend": null, "time_of_suspend": null,
"ram_size": 200, "ram_size": 200,
"priority": 10, "priority": 10,
"active_since": null,
"template": null, "template": null,
"access_method": "nx", "access_method": "nx",
"lease": 1, "lease": 1,
...@@ -1413,7 +1412,6 @@ ...@@ -1413,7 +1412,6 @@
"time_of_suspend": null, "time_of_suspend": null,
"ram_size": 200, "ram_size": 200,
"priority": 10, "priority": 10,
"active_since": null,
"template": null, "template": null,
"access_method": "nx", "access_method": "nx",
"lease": 1, "lease": 1,
......
...@@ -54,7 +54,9 @@ from .models import Profile, GroupProfile ...@@ -54,7 +54,9 @@ from .models import Profile, GroupProfile
from circle.settings.base import LANGUAGES, MAX_NODE_RAM from circle.settings.base import LANGUAGES, MAX_NODE_RAM
from django.utils.translation import string_concat from django.utils.translation import string_concat
from .virtvalidator import domain_validator from .validators import domain_validator
from dashboard.models import ConnectCommand
LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")")) LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")"))
for l in LANGUAGES) for l in LANGUAGES)
...@@ -141,25 +143,46 @@ class VmCustomizeForm(forms.Form): ...@@ -141,25 +143,46 @@ class VmCustomizeForm(forms.Form):
self.template = kwargs.pop("template", None) self.template = kwargs.pop("template", None)
super(VmCustomizeForm, self).__init__(*args, **kwargs) super(VmCustomizeForm, self).__init__(*args, **kwargs)
# set displayed disk and network list if self.user.has_perm("vm.set_resources"):
self.fields['disks'].queryset = self.template.disks.all() self.allowed_fields = tuple(self.fields.keys())
self.fields['networks'].queryset = Vlan.get_objects_with_level( # set displayed disk and network list
'user', self.user) self.fields['disks'].queryset = self.template.disks.all()
self.fields['networks'].queryset = Vlan.get_objects_with_level(
'user', self.user)
# set initial for disk and network list # set initial for disk and network list
self.initial['disks'] = self.template.disks.all() self.initial['disks'] = self.template.disks.all()
self.initial['networks'] = InterfaceTemplate.objects.filter( self.initial['networks'] = InterfaceTemplate.objects.filter(
template=self.template).values_list("vlan", flat=True) template=self.template).values_list("vlan", flat=True)
# set initial for resources # set initial for resources
self.initial['cpu_priority'] = self.template.priority self.initial['cpu_priority'] = self.template.priority
self.initial['cpu_count'] = self.template.num_cores self.initial['cpu_count'] = self.template.num_cores
self.initial['ram_size'] = self.template.ram_size self.initial['ram_size'] = self.template.ram_size
else:
self.allowed_fields = ("name", "template", "customized", )
# initial name and template pk # initial name and template pk
self.initial['name'] = self.template.name self.initial['name'] = self.template.name
self.initial['template'] = self.template.pk self.initial['template'] = self.template.pk
self.initial['customized'] = self.template.pk self.initial['customized'] = True
def _clean_fields(self):
for name, field in self.fields.items():
if name in self.allowed_fields:
value = field.widget.value_from_datadict(
self.data, self.files, self.add_prefix(name))
try:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self._errors[name] = self.error_class(e.messages)
if name in self.cleaned_data:
del self.cleaned_data[name]
class GroupCreateForm(forms.ModelForm): class GroupCreateForm(forms.ModelForm):
...@@ -176,7 +199,14 @@ class GroupCreateForm(forms.ModelForm): ...@@ -176,7 +199,14 @@ class GroupCreateForm(forms.ModelForm):
self.fields['org_id'] = forms.ChoiceField( self.fields['org_id'] = forms.ChoiceField(
# TRANSLATORS: directory like in LDAP # TRANSLATORS: directory like in LDAP
choices=choices, required=False, label=_('Directory identifier')) choices=choices, required=False, label=_('Directory identifier'))
if not new_groups: if new_groups:
self.fields['org_id'].help_text = _(
"If you select an item here, the members of this directory "
"group will be automatically added to the group at the time "
"they log in. Please note that other users (those with "
"permissions like yours) may also automatically become a "
"group co-owner).")
else:
self.fields['org_id'].widget = HiddenInput() self.fields['org_id'].widget = HiddenInput()
def save(self, commit=True): def save(self, commit=True):
...@@ -451,7 +481,7 @@ class TemplateForm(forms.ModelForm): ...@@ -451,7 +481,7 @@ class TemplateForm(forms.ModelForm):
else: else:
self.allowed_fields = ( self.allowed_fields = (
'name', 'access_method', 'description', 'system', 'tags', 'name', 'access_method', 'description', 'system', 'tags',
'arch', 'lease') 'arch', 'lease', 'has_agent')
if (self.user.has_perm('vm.change_template_resources') if (self.user.has_perm('vm.change_template_resources')
or not self.instance.pk): or not self.instance.pk):
self.allowed_fields += tuple(set(self.fields.keys()) - self.allowed_fields += tuple(set(self.fields.keys()) -
...@@ -1025,9 +1055,29 @@ class UserCreationForm(OrgUserCreationForm): ...@@ -1025,9 +1055,29 @@ class UserCreationForm(OrgUserCreationForm):
return user return user
class AclUserAddForm(forms.Form): class AclUserOrGroupAddForm(forms.Form):
name = forms.CharField(widget=autocomplete_light.TextWidget( name = forms.CharField(widget=autocomplete_light.TextWidget(
'AclUserAutocomplete', attrs={'class': 'form-control'})) 'AclUserGroupAutocomplete',
autocomplete_js_attributes={'placeholder': _("Name of group or user")},
attrs={'class': 'form-control'}))
class TransferOwnershipForm(forms.Form):
name = forms.CharField(
widget=autocomplete_light.TextWidget(
'AclUserAutocomplete',
autocomplete_js_attributes={"placeholder": _("Name of user")},
attrs={'class': 'form-control'}),
label=_("E-mail address or identifier of user"))
class AddGroupMemberForm(forms.Form):
new_member = forms.CharField(
widget=autocomplete_light.TextWidget(
'AclUserAutocomplete',
autocomplete_js_attributes={"placeholder": _("Name of user")},
attrs={'class': 'form-control'}),
label=_("E-mail address or identifier of user"))
class UserKeyForm(forms.ModelForm): class UserKeyForm(forms.ModelForm):
...@@ -1057,6 +1107,22 @@ class UserKeyForm(forms.ModelForm): ...@@ -1057,6 +1107,22 @@ class UserKeyForm(forms.ModelForm):
return super(UserKeyForm, self).clean() return super(UserKeyForm, self).clean()
class ConnectCommandForm(forms.ModelForm):
class Meta:
fields = ('name', 'access_method', 'template')
model = ConnectCommand
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super(ConnectCommandForm, self).__init__(*args, **kwargs)
def clean(self):
if self.user:
self.instance.user = self.user
return super(ConnectCommandForm, self).clean()
class TraitsForm(forms.ModelForm): class TraitsForm(forms.ModelForm):
class Meta: class Meta:
...@@ -1174,7 +1240,12 @@ class VmListSearchForm(forms.Form): ...@@ -1174,7 +1240,12 @@ class VmListSearchForm(forms.Form):
})) }))
stype = forms.ChoiceField(vm_search_choices, widget=forms.Select(attrs={ stype = forms.ChoiceField(vm_search_choices, widget=forms.Select(attrs={
'class': "btn btn-default input-tags", 'class': "btn btn-default form-control input-tags",
'style': "min-width: 80px;",
}))
include_deleted = forms.BooleanField(widget=forms.CheckboxInput(attrs={
'id': "vm-list-search-checkbox",
})) }))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
...@@ -1184,3 +1255,22 @@ class VmListSearchForm(forms.Form): ...@@ -1184,3 +1255,22 @@ class VmListSearchForm(forms.Form):
data = self.data.copy() data = self.data.copy()
data['stype'] = "all" data['stype'] = "all"
self.data = data self.data = data
class TemplateListSearchForm(forms.Form):
s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags",
'placeholder': _("Search...")
}))
stype = forms.ChoiceField(vm_search_choices, widget=forms.Select(attrs={
'class': "btn btn-default input-tags",
}))
def __init__(self, *args, **kwargs):
super(TemplateListSearchForm, self).__init__(*args, **kwargs)
# set initial value, otherwise it would be overwritten by request.GET
if not self.data.get("stype"):
data = self.data.copy()
data['stype'] = "owned"
self.data = data
...@@ -46,8 +46,10 @@ from acl.models import AclBase ...@@ -46,8 +46,10 @@ from acl.models import AclBase
from common.models import HumanReadableObject, create_readable, Encoder from common.models import HumanReadableObject, create_readable, Encoder
from vm.tasks.agent_tasks import add_keys, del_keys from vm.tasks.agent_tasks import add_keys, del_keys
from vm.models.instance import ACCESS_METHODS
from .store_api import Store, NoStoreException, NotOkException from .store_api import Store, NoStoreException, NotOkException, Timeout
from .validators import connect_command_template_validator
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -100,6 +102,25 @@ class Notification(TimeStampedModel): ...@@ -100,6 +102,25 @@ class Notification(TimeStampedModel):
self.message_data = None if value is None else value.to_dict() self.message_data = None if value is None else value.to_dict()
class ConnectCommand(Model):
user = ForeignKey(User, related_name='command_set')
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,
help_text=_("Name of your custom command."))
template = CharField(blank=True, null=True, max_length=256,
verbose_name=_('command template'),
help_text=_('Template for connection command string. '
'Available parameters are: '
'username, password, '
'host, port.'),
validators=[connect_command_template_validator])
def __unicode__(self):
return self.template
class Profile(Model): class Profile(Model):
user = OneToOneField(User) user = OneToOneField(User)
preferred_language = CharField(verbose_name=_('preferred language'), preferred_language = CharField(verbose_name=_('preferred language'),
...@@ -129,6 +150,25 @@ class Profile(Model): ...@@ -129,6 +150,25 @@ class Profile(Model):
default=2048 * 1024 * 1024, default=2048 * 1024 * 1024,
help_text=_('Disk quota in mebibytes.')) help_text=_('Disk quota in mebibytes.'))
def get_connect_commands(self, instance, use_ipv6=False):
""" Generate connection command based on template."""
single_command = instance.get_connect_command(use_ipv6)
if single_command: # can we even connect to that VM
commands = self.user.command_set.filter(
access_method=instance.access_method)
if commands.count() < 1:
return [single_command]
else:
return [
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]
else:
return []
def notify(self, subject, template, context=None, valid_until=None, def notify(self, subject, template, context=None, valid_until=None,
**kwargs): **kwargs):
if context is not None: if context is not None:
...@@ -161,6 +201,11 @@ class Profile(Model): ...@@ -161,6 +201,11 @@ class Profile(Model):
def __unicode__(self): def __unicode__(self):
return self.get_display_name() return self.get_display_name()
def save(self, *args, **kwargs):
if self.org_id == "":
self.org_id = None
super(Profile, self).save(*args, **kwargs)
class Meta: class Meta:
permissions = ( permissions = (
('use_autocomplete', _('Can use autocomplete.')), ('use_autocomplete', _('Can use autocomplete.')),
...@@ -216,7 +261,7 @@ def get_or_create_profile(self): ...@@ -216,7 +261,7 @@ def get_or_create_profile(self):
Group.profile = property(get_or_create_profile) Group.profile = property(get_or_create_profile)
def create_profile(sender, user, request, **kwargs): def create_profile(user):
if not user.pk: if not user.pk:
return False return False
profile, created = Profile.objects.get_or_create(user=user) profile, created = Profile.objects.get_or_create(user=user)
...@@ -227,7 +272,11 @@ def create_profile(sender, user, request, **kwargs): ...@@ -227,7 +272,11 @@ def create_profile(sender, user, request, **kwargs):
logger.exception("Can't create user %s", unicode(user)) logger.exception("Can't create user %s", unicode(user))
return created return created
user_logged_in.connect(create_profile)
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'): if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
logger.debug("Register save_org_id to djangosaml2 pre_user_save") logger.debug("Register save_org_id to djangosaml2 pre_user_save")
...@@ -301,7 +350,7 @@ def update_store_profile(sender, **kwargs): ...@@ -301,7 +350,7 @@ def update_store_profile(sender, **kwargs):
profile.disk_quota) profile.disk_quota)
except NoStoreException: except NoStoreException:
logger.debug("Store is not available.") logger.debug("Store is not available.")
except NotOkException: except (NotOkException, Timeout):
logger.critical("Store is not accepting connections.") logger.critical("Store is not accepting connections.")
......
...@@ -591,11 +591,15 @@ footer a, footer a:hover, footer a:visited { ...@@ -591,11 +591,15 @@ footer a, footer a:hover, footer a:visited {
width: 100px; width: 100px;
} }
#group-detail-user-table tr:last-child td:nth-child(2) {
text-align: left;
}
#group-detail-perm-header { #group-detail-perm-header {
margin-top: 25px; margin-top: 25px;
} }
textarea[name="list-new-namelist"] { textarea[name="new_members"] {
max-width: 500px; max-width: 500px;
min-height: 80px; min-height: 80px;
margin-bottom: 10px; margin-bottom: 10px;
...@@ -654,7 +658,8 @@ textarea[name="list-new-namelist"] { ...@@ -654,7 +658,8 @@ textarea[name="list-new-namelist"] {
width: 130px; width: 130px;
} }
#vm-details-connection-string-copy { .vm-details-connection-string-copy,
#vm-details-pw-show {
cursor: pointer; cursor: pointer;
} }
...@@ -681,10 +686,9 @@ textarea[name="list-new-namelist"] { ...@@ -681,10 +686,9 @@ textarea[name="list-new-namelist"] {
max-width: 200px; max-width: 200px;
} }
#dashboard-vm-details-connect-command { .dashboard-vm-details-connect-command {
/* for mobile view */ /* for mobile view */
margin-bottom: 20px; margin-bottom: 20px;
} }
#store-list-list { #store-list-list {
...@@ -868,6 +872,12 @@ textarea[name="list-new-namelist"] { ...@@ -868,6 +872,12 @@ textarea[name="list-new-namelist"] {
padding: 5px 0px; padding: 5px 0px;
} }
#profile-key-list-table td:last-child, #profile-key-list-table th:last-child,
#profile-command-list-table td:last-child, #profile-command-list-table th:last-child,
#profile-command-list-table td:nth-child(2), #profile-command-list-table th:nth-child(2) {
text-align: center;
vertical-align: middle;
}
#vm-list-table .migrating-icon { #vm-list-table .migrating-icon {
-webkit-animation: passing 2s linear infinite; -webkit-animation: passing 2s linear infinite;
...@@ -982,3 +992,26 @@ textarea[name="list-new-namelist"] { ...@@ -982,3 +992,26 @@ textarea[name="list-new-namelist"] {
.slider { .slider {
width: 100%; width: 100%;
} }
#vm-list-search-checkbox {
margin-top: -1px;
display: inline-block;
vertical-align: middle;
}
#vm-list-search-checkbox-span {
cursor: pointer
}
#vm-activity-state {
margin-bottom: 15px;
}
.autocomplete-hl {
color: #b20000;
font-weight: bold;
}
.hilight .autocomplete-hl {
color: orange;
}
...@@ -244,7 +244,7 @@ $(function () { ...@@ -244,7 +244,7 @@ $(function () {
var search_result = []; var search_result = [];
var html = ''; var html = '';
for(var i in my_vms) { for(var i in my_vms) {
if(my_vms[i].name.indexOf(input) != -1) { if(my_vms[i].name.indexOf(input) != -1 || my_vms[i].host.indexOf(input) != -1) {
search_result.push(my_vms[i]); search_result.push(my_vms[i]);
} }
} }
...@@ -383,6 +383,19 @@ $(function () { ...@@ -383,6 +383,19 @@ $(function () {
$('.notification-messages').load("/dashboard/notifications/"); $('.notification-messages').load("/dashboard/notifications/");
$('#notification-button a span[class*="badge-pulse"]').remove(); $('#notification-button a span[class*="badge-pulse"]').remove();
}); });
/* on the client confirmation button fire the clientInstalledAction */
$(document).on("click", "#client-check-button", function(event) {
var connectUri = $('#connect-uri').val();
clientInstalledAction(connectUri);
return false;
});
$("#dashboard-vm-details-connect-button").click(function(event) {
var connectUri = $(this).attr("href");
clientInstalledAction(connectUri);
return false;
});
}); });