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;
});
}); });
function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { function generateVmHTML(pk, name, host, icon, _status, fav, is_last) {
...@@ -591,6 +604,12 @@ function addModalConfirmation(func, data) { ...@@ -591,6 +604,12 @@ function addModalConfirmation(func, data) {
}); });
} }
function clientInstalledAction(location) {
setCookie('downloaded_client', true, 365 * 24 * 60 * 60, "/");
window.location.href = location;
$('#confirmation-modal').modal("hide");
}
// for AJAX calls // for AJAX calls
/** /**
* Getter for user cookies * Getter for user cookies
...@@ -613,9 +632,25 @@ function getCookie(name) { ...@@ -613,9 +632,25 @@ function getCookie(name) {
return cookieValue; return cookieValue;
} }
function setCookie(name, value, seconds, path) {
if (seconds!=null) {
var today = new Date();
var expire = new Date();
expire.setTime(today.getTime() + seconds);
}
document.cookie = name+"="+escape(value)+"; expires="+expire.toUTCString()+"; path="+path;
}
/* no js compatibility */ /* no js compatibility */
function noJS() { function noJS() {
$('.no-js-hidden').show(); $('.no-js-hidden').show();
$('.js-hidden').hide(); $('.js-hidden').hide();
} }
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
...@@ -39,6 +39,7 @@ $(function() { ...@@ -39,6 +39,7 @@ $(function() {
$(".template-list-table thead th").css("cursor", "pointer"); $(".template-list-table thead th").css("cursor", "pointer");
$(".template-list-table th a").on("click", function(event) { $(".template-list-table th a").on("click", function(event) {
if(!$(this).closest("th").data("sort")) return true;
event.preventDefault(); event.preventDefault();
}); });
}); });
......
...@@ -222,6 +222,8 @@ function vmCustomizeLoaded() { ...@@ -222,6 +222,8 @@ function vmCustomizeLoaded() {
$(this).find("i").prop("class", "fa fa-spinner fa-spin"); $(this).find("i").prop("class", "fa fa-spinner fa-spin");
if($("#create-modal")) return true;
$.ajax({ $.ajax({
url: '/dashboard/vm/create/', url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
......
...@@ -106,19 +106,20 @@ $(function() { ...@@ -106,19 +106,20 @@ $(function() {
$("#vm-details-pw-show").click(function() { $("#vm-details-pw-show").click(function() {
var input = $(this).parent("div").children("input"); var input = $(this).parent("div").children("input");
var eye = $(this).children("#vm-details-pw-eye"); var eye = $(this).children("#vm-details-pw-eye");
var span = $(this);
eye.tooltip("destroy");
span.tooltip("destroy")
if(eye.hasClass("fa-eye")) { if(eye.hasClass("fa-eye")) {
eye.removeClass("fa-eye").addClass("fa-eye-slash"); eye.removeClass("fa-eye").addClass("fa-eye-slash");
input.prop("type", "text"); input.prop("type", "text");
input.focus(); input.select();
eye.prop("title", "Hide password"); span.prop("title", gettext("Hide password"));
} else { } else {
eye.removeClass("fa-eye-slash").addClass("fa-eye"); eye.removeClass("fa-eye-slash").addClass("fa-eye");
input.prop("type", "password"); input.prop("type", "password");
eye.prop("title", "Show password"); span.prop("title", gettext("Show password"));
} }
eye.tooltip(); span.tooltip();
}); });
/* change password confirmation */ /* change password confirmation */
...@@ -199,7 +200,7 @@ $(function() { ...@@ -199,7 +200,7 @@ $(function() {
$("#vm-details-h1-name, .vm-details-rename-button").click(function() { $("#vm-details-h1-name, .vm-details-rename-button").click(function() {
$("#vm-details-h1-name").hide(); $("#vm-details-h1-name").hide();
$("#vm-details-rename").css('display', 'inline'); $("#vm-details-rename").css('display', 'inline');
$("#vm-details-rename-name").focus(); $("#vm-details-rename-name").select();
return false; return false;
}); });
...@@ -207,7 +208,7 @@ $(function() { ...@@ -207,7 +208,7 @@ $(function() {
$(".vm-details-home-edit-name-click").click(function() { $(".vm-details-home-edit-name-click").click(function() {
$(".vm-details-home-edit-name-click").hide(); $(".vm-details-home-edit-name-click").hide();
$("#vm-details-home-rename").show(); $("#vm-details-home-rename").show();
$("input", $("#vm-details-home-rename")).focus(); $("input", $("#vm-details-home-rename")).select();
return false; return false;
}); });
...@@ -307,8 +308,8 @@ $(function() { ...@@ -307,8 +308,8 @@ $(function() {
}); });
// select connection string // select connection string
$("#vm-details-connection-string-copy").click(function() { $(".vm-details-connection-string-copy").click(function() {
$("#vm-details-connection-string").focus(); $(this).parent("div").find("input").select();
}); });
$("a.operation-password_reset").click(function() { $("a.operation-password_reset").click(function() {
...@@ -378,8 +379,14 @@ function checkNewActivity(runs) { ...@@ -378,8 +379,14 @@ function checkNewActivity(runs) {
} }
$("#vm-details-state span").html(data.human_readable_status.toUpperCase()); $("#vm-details-state span").html(data.human_readable_status.toUpperCase());
if(data.status == "RUNNING") { if(data.status == "RUNNING") {
if(data['connect_uri']) {
$("#dashboard-vm-details-connect-button").removeClass('disabled');
}
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled"); $("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
} else { } else {
if(data['connect_uri']) {
$("#dashboard-vm-details-connect-button").addClass('disabled');
}
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled"); $("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
} }
......
...@@ -163,9 +163,10 @@ $(function() { ...@@ -163,9 +163,10 @@ $(function() {
$(this).find('input[type="radio"]').prop("checked", true); $(this).find('input[type="radio"]').prop("checked", true);
}); });
if(checkStatusUpdate()) { if(checkStatusUpdate() || $("#vm-list-table tbody tr").length >= 100) {
updateStatuses(1); updateStatuses(1);
} }
}); });
...@@ -178,6 +179,7 @@ function checkStatusUpdate() { ...@@ -178,6 +179,7 @@ function checkStatusUpdate() {
function updateStatuses(runs) { function updateStatuses(runs) {
var include_deleted = getParameterByName("include_deleted");
$.get("/dashboard/vm/list/?compact", function(result) { $.get("/dashboard/vm/list/?compact", function(result) {
$("#vm-list-table tbody tr").each(function() { $("#vm-list-table tbody tr").each(function() {
vm = $(this).data("vm-pk"); vm = $(this).data("vm-pk");
...@@ -203,7 +205,8 @@ function updateStatuses(runs) { ...@@ -203,7 +205,8 @@ function updateStatuses(runs) {
$(this).find(".node").text(result[vm].node); $(this).find(".node").text(result[vm].node);
} }
} else { } else {
$(this).remove(); if(!include_deleted)
$(this).remove();
} }
}); });
......
...@@ -7,6 +7,7 @@ from datetime import datetime ...@@ -7,6 +7,7 @@ from datetime import datetime
from django.http import Http404 from django.http import Http404
from django.conf import settings from django.conf import settings
from requests import get, post, codes from requests import get, post, codes
from requests.exceptions import Timeout # noqa
from sizefield.utils import filesizeformat from sizefield.utils import filesizeformat
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
......
...@@ -25,6 +25,7 @@ from django_tables2.columns import (TemplateColumn, Column, BooleanColumn, ...@@ -25,6 +25,7 @@ from django_tables2.columns import (TemplateColumn, Column, BooleanColumn,
from vm.models import Node, InstanceTemplate, Lease from vm.models import Node, InstanceTemplate, Lease
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
from dashboard.models import ConnectCommand
class NodeListTable(Table): class NodeListTable(Table):
...@@ -146,13 +147,11 @@ class TemplateListTable(Table): ...@@ -146,13 +147,11 @@ class TemplateListTable(Table):
template_name="dashboard/template-list/column-template-name.html", template_name="dashboard/template-list/column-template-name.html",
attrs={'th': {'data-sort': "string"}} attrs={'th': {'data-sort': "string"}}
) )
num_cores = Column( resources = TemplateColumn(
verbose_name=_("Cores"), template_name="dashboard/template-list/column-template-resources.html",
attrs={'th': {'data-sort': "int"}} verbose_name=_("Resources"),
)
ram_size = TemplateColumn(
"{{ record.ram_size }} MiB",
attrs={'th': {'data-sort': "int"}}, attrs={'th': {'data-sort': "int"}},
order_by=("ram_size"),
) )
lease = TemplateColumn( lease = TemplateColumn(
"{{ record.lease.name }}", "{{ record.lease.name }}",
...@@ -170,11 +169,14 @@ class TemplateListTable(Table): ...@@ -170,11 +169,14 @@ class TemplateListTable(Table):
verbose_name=_("Owner"), verbose_name=_("Owner"),
attrs={'th': {'data-sort': "string"}} attrs={'th': {'data-sort': "string"}}
) )
created = TemplateColumn(
template_name="dashboard/template-list/column-template-created.html",
verbose_name=_("Created at"),
)
running = TemplateColumn( running = TemplateColumn(
template_name="dashboard/template-list/column-template-running.html", template_name="dashboard/template-list/column-template-running.html",
verbose_name=_("Running"), verbose_name=_("Running"),
attrs={'th': {'data-sort': "int"}}, attrs={'th': {'data-sort': "int"}},
orderable=False,
) )
actions = TemplateColumn( actions = TemplateColumn(
verbose_name=_("Actions"), verbose_name=_("Actions"),
...@@ -187,8 +189,8 @@ class TemplateListTable(Table): ...@@ -187,8 +189,8 @@ class TemplateListTable(Table):
model = InstanceTemplate model = InstanceTemplate
attrs = {'class': ('table table-bordered table-striped table-hover' attrs = {'class': ('table table-bordered table-striped table-hover'
' template-list-table')} ' template-list-table')}
fields = ('name', 'num_cores', 'ram_size', 'system', fields = ('name', 'resources', 'system', 'access_method', 'lease',
'access_method', 'lease', 'owner', 'running', 'actions', ) 'owner', 'created', 'running', 'actions', )
prefix = "template-" prefix = "template-"
...@@ -220,6 +222,7 @@ class LeaseListTable(Table): ...@@ -220,6 +222,7 @@ class LeaseListTable(Table):
fields = ('name', 'suspend_interval_seconds', fields = ('name', 'suspend_interval_seconds',
'delete_interval_seconds', ) 'delete_interval_seconds', )
prefix = "lease-" prefix = "lease-"
empty_text = _("No available leases.")
class UserKeyListTable(Table): class UserKeyListTable(Table):
...@@ -248,5 +251,41 @@ class UserKeyListTable(Table): ...@@ -248,5 +251,41 @@ class UserKeyListTable(Table):
class Meta: class Meta:
model = UserKey model = UserKey
attrs = {'class': ('table table-bordered table-striped table-hover')} attrs = {'class': ('table table-bordered table-striped table-hover'),
'id': "profile-key-list-table"}
fields = ('name', 'fingerprint', 'created', 'actions') fields = ('name', 'fingerprint', 'created', 'actions')
prefix = "key-"
empty_text = _("You haven't added any public keys yet.")
class ConnectCommandListTable(Table):
name = LinkColumn(
'dashboard.views.connect-command-detail',
args=[A('pk')],
attrs={'th': {'data-sort': "string"}}
)
access_method = Column(
verbose_name=_("Access method"),
attrs={'th': {'data-sort': "string"}}
)
template = Column(
verbose_name=_("Template"),
attrs={'th': {'data-sort': "string"}}
)
actions = TemplateColumn(
verbose_name=_("Actions"),
template_name=("dashboard/connect-command-list/column-command"
"-actions.html"),
orderable=False,
)
class Meta:
model = ConnectCommand
attrs = {'class': ('table table-bordered table-striped table-hover'),
'id': "profile-command-list-table"}
fields = ('name', 'access_method', 'template', 'actions')
prefix = "cmd-"
empty_text = _(
"You don't have any custom connection commands yet. You can "
"specify commands to be displayed on VM detail pages instead of "
"the defaults.")
{% load i18n %}
<p>
{% blocktrans %}
To effortlessly connect to all kind of virtual machines you have to install the <strong>CIRCLE Client</strong>.
{% endblocktrans %}
</p>
<p class="text-info">
{% blocktrans %}
To install the <strong>CIRCLE Client</strong> click on the <strong>Download the Client</strong> button.
The button takes you to the installation detail page, where you can choose your operating system and start
the download or read more detailed information about the <strong>Client</strong>. The program can be installed on Windows XP (and above)
or Debian based Linux operating systems. To successfully install the client you have to have admin (root or elevated) rights.
After the installation complete clicking on the <strong>I have the Client installed</strong> button will launch the appropriate tool
designed for that connection with necessarily predefined configurations. This option will also save your answer and this prompt about
installation will not pop up again.
{% endblocktrans %}
</p>
<br>
<div class="pull-right">
<form method="POST" id="dashboard-client-check" action="">
{% csrf_token %}
<a class="btn btn-default" href="{% url "dashboard.views.detail" pk=instance.pk %}" data-dismiss="modal">{% trans "Cancel" %}</a>
<a class="btn btn-info" href="{{ client_download_url }}" traget="_blank">{% trans "Download the Client" %}</a>
<button data-dismiss="modal" id="client-check-button" type="submit" class="btn btn-success" title="{% trans "I downloaded and installed the client and I want to connect using it. This choice will be saved to your compuer" %}">
<i class="fa fa-external-link"></i> {% trans "I have the Client installed" %}
</button>
<input id="connect-uri" name="connect-uri" type="hidden" value="{% if instance.get_connect_uri %}{{ instance.get_connect_uri}}{% endif %}" />
<input name="vm" type="hidden" value="{% if instance.get_connect_uri %}{{ instance.pk}}{% endif %}" />
</form>
</div>
\ No newline at end of file
{% load i18n %} {% load i18n %}
{% if user and user.pk %} {% if user and user.pk %}
{% if user.get_full_name %} {% if user.get_full_name %}{{ user.get_full_name }}{% else %}{{ user.username }}{% endif %}{% if new_line %}<br />{% endif %}
{{ user.get_full_name }}
{% else %}
{{ user.username }}
{% endif %}
{% if show_org %} {% if show_org %}
{% if user.profile and user.profile.org_id %} {% if user.profile and user.profile.org_id %}
......
...@@ -2,6 +2,12 @@ ...@@ -2,6 +2,12 @@
<div class="modal fade" id="confirmation-modal" tabindex="-1" role="dialog"> <div class="modal fade" id="confirmation-modal" tabindex="-1" role="dialog">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
{% if box_title and ajax_title %}
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{{ box_title }}</h4>
</div>
{% endif %}
<div class="modal-body"> <div class="modal-body">
{% if template %} {% if template %}
{% include template %} {% include template %}
......
...@@ -3,10 +3,11 @@ ...@@ -3,10 +3,11 @@
{% load sizefieldtags %} {% load sizefieldtags %}
{% include "display-form-errors.html" with form=vm_create_form %} {% include "display-form-errors.html" with form=vm_create_form %}
<form method="POST"> <form method="POST" action="{% url "dashboard.views.vm-create" %}">
{% csrf_token %} {% csrf_token %}
{{ vm_create_form.template }} {{ vm_create_form.template }}
{{ vm_create_form.customized }}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
...@@ -23,7 +24,6 @@ ...@@ -23,7 +24,6 @@
</div> </div>
{% if perms.vm.set_resources %} {% if perms.vm.set_resources %}
{{ vm_create_form.customized }}
<div class="row"> <div class="row">
<div class="col-sm-10"> <div class="col-sm-10">
<div class="form-group"> <div class="form-group">
......
...@@ -10,8 +10,9 @@ ...@@ -10,8 +10,9 @@
</h3> </h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% blocktrans with owner=instance.owner fqdn=instance.primary_host %} {% blocktrans with owner=instance.owner name=instance.name id=instance.id%}
{{ owner }} offered to take the ownership of virtual machine {{fqdn}}. <strong>{{ owner }}</strong> offered to take the ownership of
virtual machine <strong>{{name}} ({{id}})</strong>.
Do you accept the responsility of being the host's owner? Do you accept the responsility of being the host's owner?
{% endblocktrans %} {% endblocktrans %}
<div class="pull-right"> <div class="pull-right">
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title-page %}{% trans "Create command template" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.profile-preferences" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="fa fa-code"></i> {% trans "Create new command template" %}</h3>
</div>
<div class="panel-body">
<form method="POST">
{% csrf_token %}
{{ form.name|as_crispy_field }}
{{ form.access_method|as_crispy_field }}
{{ form.template|as_crispy_field }}
<p class="text-muted">
{% trans "Examples" %}
</p>
<p>
<strong>SSH:</strong>
<span class="text-muted">
sshpass -p %(password)s ssh -o StrictHostKeyChecking=no cloud@%(host)s -p %(port)d
</span>
</p>
<p>
<strong>RDP:</strong>
<span class="text-muted">
rdesktop %(host)s:%(port)d -u cloud -p %(password)s
</span>
</p>
<input type="submit" class="btn btn-primary" value="{% trans "Save" %}">
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title-page %}{% trans "Edit command template" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.profile-preferences" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="fa fa-code"></i> {% trans "Edit command template" %}</h3>
</div>
<div class="panel-body">
<form method="POST">
{% csrf_token %}
{{ form.name|as_crispy_field }}
{{ form.access_method|as_crispy_field }}
{{ form.template|as_crispy_field }}
<p class="text-muted">
{% trans "Examples" %}
</p>
<p>
<strong>SSH:</strong>
<span class="text-muted">
sshpass -p %(password)s ssh -o StrictHostKeyChecking=no cloud@%(host)s -p %(port)d
</span>
</p>
<p>
<strong>RDP:</strong>
<span class="text-muted">
rdesktop %(host)s:%(port)d -u cloud -p %(password)s
</span>
</p>
<input type="submit" class="btn btn-primary" value="{% trans "Save" %}">
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% load i18n %}
<a href="{% url "dashboard.views.connect-command-detail" pk=record.pk%}" id="template-list-edit-button" class="btn btn-default btn-xs" title="{% trans "Edit" %}">
<i class="fa fa-edit"></i>
</a>
<a data-template-pk="{{ record.pk }}" href="{% url "dashboard.views.connect-command-delete" pk=record.pk %}" class="btn btn-danger btn-xs template-delete" title="{% trans "Delete" %}">
<i class="fa fa-times"></i>
</a>
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load i18n %}
<p class="text-muted">
{% trans "User groups allow sharing templates or other resources with multiple users at once." %}
</p>
<form method="POST" action="{% url "dashboard.views.group-create" %}"> <form method="POST" action="{% url "dashboard.views.group-create" %}">
{% csrf_token %} {% csrf_token %}
......
...@@ -89,13 +89,12 @@ ...@@ -89,13 +89,12 @@
<tr> <tr>
<td><i class="fa fa-plus"></i></td> <td><i class="fa fa-plus"></i></td>
<td colspan="2"> <td colspan="2">
<input type="text" class="form-control" name="list-new-name" {{addmemberform.new_member}}
placeholder="{% trans "Name of user" %}">
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<textarea name="list-new-namelist" class="form-control" <textarea name="new_members" class="form-control"
placeholder="{% trans "Add multiple users at once (one identifier per line)." %}"></textarea> placeholder="{% trans "Add multiple users at once (one identifier per line)." %}"></textarea>
<div class="form-actions"> <div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button> <button type="submit" class="btn btn-success">{% trans "Save" %}</button>
......
<a data-group-pk="{{ record.pk }}" <a data-group-pk="{{ record.pk }}"
class="btn btn-danger btn-xs real-link group-delete" class="btn btn-danger btn-xs real-link group-delete"
href="{% url "dashboard.views.delete-group" pk=record.pk %}?next={{ request.path }}"> href="{% url "dashboard.views.delete-group" pk=record.pk %}?next={{ request.path }}">
<i class="fa fa-trash-o"></i> <i class="fa fa-trash-o"></i>
</a> </a>
...@@ -3,10 +3,10 @@ ...@@ -3,10 +3,10 @@
<div class="panel-heading"> <div class="panel-heading">
<div class="pull-right toolbar"> <div class="pull-right toolbar">
<div class="btn-group"> <div class="btn-group">
<a href="#index-graph-view" data-index-box="vm" class="btn btn-default btn-xs" <a href="#index-graph-view" data-index-box="vm" class="btn btn-default btn-xs"
data-container="body" data-container="body"
title="{% trans "summary view" %}"><i class="fa fa-dashboard"></i></a> title="{% trans "summary view" %}"><i class="fa fa-dashboard"></i></a>
<a href="#index-list-view" data-index-box="vm" class="btn btn-default btn-xs disabled" <a href="#index-list-view" data-index-box="vm" class="btn btn-default btn-xs disabled"
data-container="body" data-container="body"
title="{% trans "list view" %}"><i class="fa fa-list"></i></a> title="{% trans "list view" %}"><i class="fa fa-list"></i></a>
</div> </div>
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
<i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i> <i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i>
{{ i.name }} {{ i.name }}
</span> </span>
<small class="text-muted"> {{ i.primary_host.hostname }}</small> <small class="text-muted"> {{ i.short_hostname }}</small>
<div class="pull-right dashboard-vm-favourite" data-vm="{{ i.pk }}"> <div class="pull-right dashboard-vm-favourite" data-vm="{{ i.pk }}">
{% if i.fav %} {% if i.fav %}
<i class="fa fa-star text-primary title-favourite" title="{% trans "Unfavourite" %}"></i> <i class="fa fa-star text-primary title-favourite" title="{% trans "Unfavourite" %}"></i>
......
...@@ -10,9 +10,9 @@ ...@@ -10,9 +10,9 @@
</h1> </h1>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-5" id="vm-info-pane"> <div class="col-md-6" id="vm-info-pane">
<div class="big"> <div class="big" id="vm-activity-state">
<span id="vm-activity-state" class="label label-{% if object.get_status_id == 'wait' %}info{% else %}{% if object.succeeded %}success{% else %}error{% endif %}{% endif %}"> <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>{{ object.get_status_id|upper }}</span>
</span> </span>
</div> </div>
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
{% include "dashboard/vm-detail/_activity-timeline.html" with active=object %} {% include "dashboard/vm-detail/_activity-timeline.html" with active=object %}
</div> </div>
<div class="col-md-7"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<!--<div class="panel-heading"><h2 class="panel-title">{% trans "Activity" %}</h2></div> --> <!--<div class="panel-heading"><h2 class="panel-title">{% trans "Activity" %}</h2></div> -->
<div class="panel-body"> <div class="panel-body">
......
...@@ -11,6 +11,12 @@ ...@@ -11,6 +11,12 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
{% if request.user.is_superuser %}
<a href="{{ login_token }}"
class="pull-right btn btn-danger btn-xs"
title="{% trans "Log in as this user. Recommended to open in an incognito window." %}">
{% trans "Login as this user" %}</a>
{% endif %}
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.index" %}">{% trans "Back" %}</a> <a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.index" %}">{% trans "Back" %}</a>
<h3 class="no-margin"> <h3 class="no-margin">
<i class="fa fa-user"></i> <i class="fa fa-user"></i>
......
...@@ -66,4 +66,20 @@ ...@@ -66,4 +66,20 @@
</div> </div>
</div> </div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a href="{% url "dashboard.views.connect-command-create" %}"
class="pull-right btn btn-success btn-xs" style="margin-right: 10px;">
<i class="fa fa-plus"></i> {% trans "add command template" %}
</a>
<h3 class="no-margin"><i class="fa fa-code"></i> {% trans "Command templates" %}</h3>
</div>
<div class="panel-body">
{% render_table connectcommand_table %}
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
...@@ -52,6 +52,7 @@ ...@@ -52,6 +52,7 @@
{{ form.req_traits|as_crispy_field }} {{ form.req_traits|as_crispy_field }}
{{ form.description|as_crispy_field }} {{ form.description|as_crispy_field }}
{{ form.system|as_crispy_field }} {{ form.system|as_crispy_field }}
{{ form.has_agent|as_crispy_field }}
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "External resources" %}</legend> <legend>{% trans "External resources" %}</legend>
......
...@@ -17,18 +17,37 @@ ...@@ -17,18 +17,37 @@
<h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %}</h3> <h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="row">
<div class="col-md-offset-8 col-md-4" id="template-list-search">
<form action="" method="GET">
<div class="input-group">
{{ search_form.s }}
<div class="input-group-btn">
{{ search_form.stype }}
<button type="submit" class="btn btn-primary input-tags">
<i class="fa fa-search"></i>
</button>
</div>
</div><!-- .input-group -->
</form>
</div><!-- .col-md-4 #template-list-search -->
</div>
</div>
<div class="panel-body">
{% render_table table %} {% render_table table %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% if show_lease_table %}
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
{% if perms.vm.create_leases %} {% if perms.vm.create_leases %}
<a href="{% url "dashboard.views.lease-create" %}" class="pull-right btn btn-success btn-xs" style="margin-right: 10px;"> <a href="{% url "dashboard.views.lease-create" %}"
class="pull-right btn btn-success btn-xs" style="margin-right: 10px;">
<i class="fa fa-plus"></i> {% trans "new lease" %} <i class="fa fa-plus"></i> {% trans "new lease" %}
</a> </a>
{% endif %} {% endif %}
...@@ -55,6 +74,7 @@ ...@@ -55,6 +74,7 @@
</div> </div>
{% endcomment %} {% endcomment %}
</div> </div>
{% endif %}
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
......
{% load i18n %} {% load i18n %}
<a href="{% url "dashboard.views.vm-create" %}?template={{ record.pk }}"
class="btn btn-success btn-xs customize-vm" title="{% trans "Start" %}">
<i class="fa fa-play"></i>
</a>
<a href="{% url "dashboard.views.template-detail" pk=record.pk%}" id="template-list-edit-button" class="btn btn-default btn-xs" title="{% trans "Edit" %}"> <a href="{% url "dashboard.views.template-detail" pk=record.pk%}" id="template-list-edit-button" class="btn btn-default btn-xs" title="{% trans "Edit" %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
</a> </a>
......
{{ record.created|date }}
<br />
{{ record.created|time }}
{% include "dashboard/_display-name.html" with user=record.owner show_org=True %} {% include "dashboard/_display-name.html" with user=record.owner show_org=True new_line=True %}
{% load i18n %}
{{ record.ram_size }}MiB RAM
<br />
{% blocktrans with num_cores=record.num_cores count count=record.num_cores %}
{{ num_cores }} CPU core
{% plural %}
{{ num_cores }} CPU cores
{% endblocktrans %}
<a href="{% url "dashboard.views.vm-list" %}?s=template:{{ record.pk }}%20status:running"> <a href="{% url "dashboard.views.vm-list" %}?s=template:{{ record.pk }}%20status:running">
{{ record.get_running_instances.count }} {{ record.running }}
</a> </a>
...@@ -100,8 +100,9 @@ ...@@ -100,8 +100,9 @@
<div class="input-group"> <div class="input-group">
<input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags" <input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags"
value="{{ instance.pw }}" spellcheck="false"/> value="{{ instance.pw }}" spellcheck="false"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show"> <span class="input-group-addon input-tags" id="vm-details-pw-show"
<i class="fa fa-eye" id="vm-details-pw-eye" title="Show password"></i> title="{% trans "Show password" %}" data-container="body">
<i class="fa fa-eye" id="vm-details-pw-eye"></i>
</span> </span>
</div> </div>
</dd> </dd>
...@@ -113,16 +114,42 @@ ...@@ -113,16 +114,42 @@
</div> </div>
</dd> </dd>
</dl> </dl>
{% for c in connect_commands %}
<div class="input-group" id="dashboard-vm-details-connect-command"> <div class="input-group dashboard-vm-details-connect-command">
<span class="input-group-addon input-tags">{% trans "Command" %}</span> <span class="input-group-addon input-tags">{% trans "Command" %}</span>
<input type="text" spellcheck="false" <input type="text" spellcheck="false"
value="{% if instance.get_connect_command %}{{ instance.get_connect_command }}{% else %}{% trans "Connection is not possible." %}{% endif %}" 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">
<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" /> id="vm-details-connection-string" class="form-control input-tags" />
<span class="input-group-addon input-tags" id="vm-details-connection-string-copy"> <span class="input-group-addon input-tags" id="vm-details-connection-string-copy">
<i class="fa fa-copy" title="{% trans "Select all" %}"></i> <i class="fa fa-copy" title="{% trans "Select all" %}"></i>
</span> </span>
</div> </div>
{% endfor %}
{% if instance.get_connect_uri %}
<div id="dashboard-vm-details-connect" class="operation-wrapper">
{% if client_download %}
<a id="dashboard-vm-details-connect-button" class="btn btn-xs btn-default operation " href="{{ instance.get_connect_uri}}" title="{% trans "Connect via the CIRCLE Client" %}">
<i class="fa fa-external-link"></i> {% trans "Connect" %}
</a>
<a href="{% url "dashboard.views.client-check" %}?vm={{ instance.pk }}">{% trans "Download client" %}</a>
{% else %}
<a id="dashboard-vm-details-connect-download-button" class="btn btn-xs btn-default operation " href="{% url "dashboard.views.client-check" %}?vm={{ instance.pk }}" title="{% trans "Download the CIRCLE Client" %}">
<i class="fa fa-external-link"></i> {% trans "Connect (download client)" %}
</a>
{% endif %}
</div>
{% endif %}
</div> </div>
<div class="col-md-8" id="vm-detail-pane"> <div class="col-md-8" id="vm-detail-pane">
<div class="panel panel-default" id="vm-detail-panel"> <div class="panel panel-default" id="vm-detail-panel">
......
...@@ -9,8 +9,10 @@ ...@@ -9,8 +9,10 @@
{% endblocktrans %} {% endblocktrans %}
{% endif %} {% endif %}
{% if user == instance.owner or user.is_superuser %} {% if user == instance.owner or user.is_superuser %}
<span class="operation-wrapper">
<a href="{% url "dashboard.views.vm-transfer-ownership" instance.pk %}" <a href="{% url "dashboard.views.vm-transfer-ownership" instance.pk %}"
class="btn btn-link">{% trans "Transfer ownership..." %}</a> class="btn btn-link operation">{% trans "Transfer ownership..." %}</a>
</span>
{% endif %} {% endif %}
</p> </p>
<h3>{% trans "Permissions"|capfirst %}</h3> <h3>{% trans "Permissions"|capfirst %}</h3>
......
...@@ -94,7 +94,13 @@ ...@@ -94,7 +94,13 @@
<dt>{% trans "Template" %}:</dt> <dt>{% trans "Template" %}:</dt>
<dd> <dd>
{% if instance.template %} {% if instance.template %}
{{ instance.template.name }} {% if can_link_template %}
<a href="{{ instance.template.get_absolute_url }}">
{{ instance.template.name }}
</a>
{% else %}
{{ instance.template.name }}
{% endif %}
{% else %} {% else %}
- -
{% endif %} {% endif %}
......
{% extends "dashboard/base.html" %}
{% load i18n %} {% load i18n %}
{% block content %} <div class="pull-right">
<div class="body-content"> <form action="{% url "dashboard.views.vm-transfer-ownership" pk=instance.pk %}" method="POST" style="max-width: 400px;">
<div class="panel panel-default"> {% csrf_token %}
<div class="panel-heading"> <label>
<h3 class="no-margin"> {{ form.name.label }}
{% trans "Transfer ownership" %} </label>
</h3> <div class="input-group">
</div> {{form.name}}
<div class="panel-body"> <div class="input-group-btn">
<div class="pull-right"> <input type="submit" value="{% trans "Save" %}" class="btn btn-primary">
<form action="" method="POST">
{% csrf_token %}
<label>
{% trans "E-mail address or identifier of user" %}:
<input name="name">
</label>
<input type="submit">
</form>
</div> </div>
</div> </div>
</div> </form>
{% endblock %} </div>
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<strong>{% trans "Group actions" %}</strong> <strong>{% trans "Group actions" %}</strong>
<button id="vm-list-group-select-all" class="btn btn-info btn-xs">{% trans "Select all" %}</button> <button id="vm-list-group-select-all" class="btn btn-info btn-xs">{% trans "Select all" %}</button>
{% for o in ops %} {% for o in ops %}
<a href="{{ o.get_url }}" class="btn btn-xs btn-{{ o.effect }} mass-operation" <a href="{{ o.get_url }}" class="btn btn-xs btn-{{ o.effect }} mass-operation"
title="{{ o.name|capfirst }}" disabled> title="{{ o.name|capfirst }}" disabled>
<i class="fa fa-{{ o.icon }}"></i> <i class="fa fa-{{ o.icon }}"></i>
</a> </a>
...@@ -34,7 +34,13 @@ ...@@ -34,7 +34,13 @@
{{ search_form.s }} {{ search_form.s }}
<div class="input-group-btn"> <div class="input-group-btn">
{{ search_form.stype }} {{ search_form.stype }}
<button type="submit" class="btn btn-primary input-tags"> </div>
<label class="input-group-addon input-tags" title="{% trans "Include deleted VMs" %}"
id="vm-list-search-checkbox-span" data-container="body">
{{ search_form.include_deleted }}
</label>
<div class="input-group-btn">
<button type="submit" class="btn btn-primary input-tags">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
</button> </button>
</div> </div>
...@@ -63,10 +69,20 @@ ...@@ -63,10 +69,20 @@
{% trans "Owner" as t %} {% trans "Owner" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="owner" %} {% include "dashboard/vm-list/header-link.html" with name=t sort="owner" %}
</th> </th>
{% if user.is_superuser %}<th data-sort="string" class="orderable sortable"> <th data-sort="string" class="orderable sortable">
{% trans "Node" as t %} {% trans "Lease" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="node" %} {% include "dashboard/vm-list/header-link.html" with name=t sort="lease" %}
</th>{% endif %} </th>
{% if user.is_superuser %}
<th data-sort="string" class="orderable sortable">
{% trans "IP address" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="ip_addr" %}
</th>
<th data-sort="string" class="orderable sortable">
{% trans "Node" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="node" %}
</th>
{% endif %}
</tr></thead><tbody> </tr></thead><tbody>
{% for i in object_list %} {% for i in object_list %}
<tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}"> <tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}">
...@@ -74,16 +90,27 @@ ...@@ -74,16 +90,27 @@
<td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">{{ i.name }}</a> </td> <td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">{{ i.name }}</a> </td>
<td class="state"> <td class="state">
<i class="fa fa-fw <i class="fa fa-fw
{% if i.is_in_status_change %} {% if show_acts_in_progress and i.is_in_status_change %}
fa-spin fa-spinner fa-spin fa-spinner
{% else %} {% else %}
{{ i.get_status_icon }}{% endif %}"></i> {{ i.get_status_icon }}{% endif %}"></i>
<span>{{ i.get_status_display }}</span> <span>{{ i.get_status_display }}</span>
</td> </td>
<td> <td>
{% include "dashboard/_display-name.html" with user=i.owner show_org=True %} {% if i.owner.profile %}
{{ i.owner.profile.get_display_name }}
{% else %}
{{ i.owner.username }}
{% endif %}
{# include "dashboard/_display-name.html" with user=i.owner show_org=True #}
</td>
<td class="lease "data-sort-value="{{ i.lease.name }}">
{{ i.lease.name }}
</td> </td>
{% if user.is_superuser %} {% if user.is_superuser %}
<td class="ip_addr "data-sort-value="{{ i.ipv4 }}">
{{ i.ipv4|default:"-" }}
</td>
<td class="node "data-sort-value="{{ i.node.normalized_name }}"> <td class="node "data-sort-value="{{ i.node.normalized_name }}">
{{ i.node.name|default:"-" }} {{ i.node.name|default:"-" }}
</td> </td>
...@@ -91,7 +118,7 @@ ...@@ -91,7 +118,7 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="5"> <td colspan="7">
{% if request.GET.s %} {% if request.GET.s %}
<strong>{% trans "No result." %}</strong> <strong>{% trans "No result." %}</strong>
{% else %} {% else %}
......
{% spaceless %}
{% load django_tables2 %}
{% load i18n %}
{% if table.page %}
<div class="table-container">
{% endif %}
{% block table %}
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
{% nospaceless %}
{% block table.thead %}
<thead>
<tr>
{% for column in table.columns %}
{% if column.orderable %}
<th {{ column.attrs.th.as_html }}><a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a></th>
{% else %}
<th {{ column.attrs.th.as_html }}>{{ column.header }}</th>
{% endif %}
{% endfor %}
</tr>
</thead>
{% endblock table.thead %}
{% block table.tbody %}
<tbody>
{% for row in table.page.object_list|default:table.rows %} {# support pagination #}
{% block table.tbody.row %}
<tr class="{{ forloop.counter|divisibleby:2|yesno:"even,odd" }}"> {# avoid cycle for Django 1.2-1.6 compatibility #}
{% for column, cell in row.items %}
<td {{ column.attrs.td.as_html }}>{% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %}</td>
{% endfor %}
</tr>
{% endblock table.tbody.row %}
{% empty %}
{% if table.empty_text %}
{% block table.tbody.empty_text %}
<tr><td colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr>
{% endblock table.tbody.empty_text %}
{% endif %}
{% endfor %}
</tbody>
{% endblock table.tbody %}
{% block table.tfoot %}
<tfoot></tfoot>
{% endblock table.tfoot %}
{% endnospaceless %}
</table>
{% endblock table %}
{% if table.page %}
</div>
{% endif %}
{% endspaceless %}
...@@ -1134,7 +1134,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1134,7 +1134,7 @@ class GroupDetailTest(LoginMixin, TestCase):
c = Client() c = Client()
user_in_group = self.g1.user_set.count() user_in_group = self.g1.user_set.count()
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/', {'list-new-name': 'user3'}) str(self.g1.pk) + '/', {'new_member': 'user3'})
self.assertEqual(user_in_group, self.assertEqual(user_in_group,
self.g1.user_set.count()) self.g1.user_set.count())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1144,7 +1144,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1144,7 +1144,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'user3') self.login(c, 'user3')
user_in_group = self.g1.user_set.count() user_in_group = self.g1.user_set.count()
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/', {'list-new-name': 'user3'}) str(self.g1.pk) + '/', {'new_member': 'user3'})
self.assertEqual(user_in_group, self.g1.user_set.count()) self.assertEqual(user_in_group, self.g1.user_set.count())
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
...@@ -1153,7 +1153,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1153,7 +1153,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'superuser') self.login(c, 'superuser')
user_in_group = self.g1.user_set.count() user_in_group = self.g1.user_set.count()
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/', {'list-new-name': 'user3'}) str(self.g1.pk) + '/', {'new_member': 'user3'})
self.assertEqual(user_in_group + 1, self.g1.user_set.count()) self.assertEqual(user_in_group + 1, self.g1.user_set.count())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1162,7 +1162,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1162,7 +1162,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'user0') self.login(c, 'user0')
user_in_group = self.g1.user_set.count() user_in_group = self.g1.user_set.count()
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/', {'list-new-name': 'user3'}) str(self.g1.pk) + '/', {'new_member': 'user3'})
self.assertEqual(user_in_group + 1, self.g1.user_set.count()) self.assertEqual(user_in_group + 1, self.g1.user_set.count())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1172,7 +1172,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1172,7 +1172,7 @@ class GroupDetailTest(LoginMixin, TestCase):
user_in_group = self.g1.user_set.count() user_in_group = self.g1.user_set.count()
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/', str(self.g1.pk) + '/',
{'list-new-namelist': 'user1\r\nuser2'}) {'new_members': 'user1\r\nuser2'})
self.assertEqual(user_in_group + 2, self.g1.user_set.count()) self.assertEqual(user_in_group + 2, self.g1.user_set.count())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1182,7 +1182,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1182,7 +1182,7 @@ class GroupDetailTest(LoginMixin, TestCase):
user_in_group = self.g1.user_set.count() user_in_group = self.g1.user_set.count()
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/', str(self.g1.pk) + '/',
{'list-new-namelist': 'user1\r\nnoname\r\nuser2'}) {'new_members': 'user1\r\nnoname\r\nuser2'})
self.assertEqual(user_in_group + 2, self.g1.user_set.count()) self.assertEqual(user_in_group + 2, self.g1.user_set.count())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1192,7 +1192,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1192,7 +1192,7 @@ class GroupDetailTest(LoginMixin, TestCase):
user_in_group = self.g1.user_set.count() user_in_group = self.g1.user_set.count()
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/', str(self.g1.pk) + '/',
{'list-new-namelist': 'user1\r\nuser2'}) {'new_members': 'user1\r\nuser2'})
self.assertEqual(user_in_group, self.g1.user_set.count()) self.assertEqual(user_in_group, self.g1.user_set.count())
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
...@@ -1201,7 +1201,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1201,7 +1201,7 @@ class GroupDetailTest(LoginMixin, TestCase):
user_in_group = self.g1.user_set.count() user_in_group = self.g1.user_set.count()
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/', str(self.g1.pk) + '/',
{'list-new-namelist': 'user1\r\nuser2'}) {'new_members': 'user1\r\nuser2'})
self.assertEqual(user_in_group, self.g1.user_set.count()) self.assertEqual(user_in_group, self.g1.user_set.count())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1471,8 +1471,8 @@ class TransferOwnershipViewTest(LoginMixin, TestCase): ...@@ -1471,8 +1471,8 @@ class TransferOwnershipViewTest(LoginMixin, TestCase):
c2 = self.u2.notification_set.count() c2 = self.u2.notification_set.count()
c = Client() c = Client()
self.login(c, 'user2') self.login(c, 'user2')
response = c.post('/dashboard/vm/1/tx/') response = c.post('/dashboard/vm/1/tx/', {'name': 'userx'})
assert response.status_code == 400 assert response.status_code == 403
self.assertEqual(self.u2.notification_set.count(), c2) self.assertEqual(self.u2.notification_set.count(), c2)
def test_owned_offer(self): def test_owned_offer(self):
......
...@@ -39,11 +39,13 @@ from .views import ( ...@@ -39,11 +39,13 @@ from .views import (
get_vm_screenshot, get_vm_screenshot,
ProfileView, toggle_use_gravatar, UnsubscribeFormView, ProfileView, toggle_use_gravatar, UnsubscribeFormView,
UserKeyDelete, UserKeyDetail, UserKeyCreate, UserKeyDelete, UserKeyDetail, UserKeyCreate,
ConnectCommandDelete, ConnectCommandDetail, ConnectCommandCreate,
StoreList, store_download, store_upload, store_get_upload_url, StoreRemove, StoreList, store_download, store_upload, store_get_upload_url, StoreRemove,
store_new_directory, store_refresh_toplist, store_new_directory, store_refresh_toplist,
VmTraitsUpdate, VmRawDataUpdate, VmTraitsUpdate, VmRawDataUpdate,
GroupPermissionsView, GroupPermissionsView,
LeaseAclUpdateView, LeaseAclUpdateView,
ClientCheck, TokenLogin,
) )
autocomplete_light.autodiscover() autocomplete_light.autodiscover()
...@@ -177,6 +179,16 @@ urlpatterns = patterns( ...@@ -177,6 +179,16 @@ urlpatterns = patterns(
UserKeyCreate.as_view(), UserKeyCreate.as_view(),
name="dashboard.views.userkey-create"), name="dashboard.views.userkey-create"),
url(r'^conncmd/delete/(?P<pk>\d+)/$',
ConnectCommandDelete.as_view(),
name="dashboard.views.connect-command-delete"),
url(r'^conncmd/(?P<pk>\d+)/$',
ConnectCommandDetail.as_view(),
name="dashboard.views.connect-command-detail"),
url(r'^conncmd/create/$',
ConnectCommandCreate.as_view(),
name="dashboard.views.connect-command-create"),
url(r'^autocomplete/', include('autocomplete_light.urls')), url(r'^autocomplete/', include('autocomplete_light.urls')),
url(r"^store/list/$", StoreList.as_view(), url(r"^store/list/$", StoreList.as_view(),
...@@ -193,4 +205,8 @@ urlpatterns = patterns( ...@@ -193,4 +205,8 @@ urlpatterns = patterns(
name="dashboard.views.store-new-directory"), name="dashboard.views.store-new-directory"),
url(r"^store/refresh_toplist$", store_refresh_toplist, url(r"^store/refresh_toplist$", store_refresh_toplist,
name="dashboard.views.store-refresh-toplist"), name="dashboard.views.store-refresh-toplist"),
url(r"^client/check$", ClientCheck.as_view(),
name="dashboard.views.client-check"),
url(r'^token-login/(?P<token>.*)/$', TokenLogin.as_view(),
name="dashboard.views.token-login"),
) )
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from lxml import etree as ET from lxml import etree as ET
import logging import logging
...@@ -29,3 +31,27 @@ def domain_validator(value): ...@@ -29,3 +31,27 @@ def domain_validator(value):
relaxng.assertValid(parsed_xml) relaxng.assertValid(parsed_xml)
except Exception as e: except Exception as e:
raise ValidationError(e.message) raise ValidationError(e.message)
def connect_command_template_validator(value):
"""Validate value as a connect command template.
>>> try: connect_command_template_validator("%(host)s")
... except ValidationError as e: print e
...
>>> connect_command_template_validator("%(host)s")
>>> try: connect_command_template_validator("%(host)s %s")
... except ValidationError as e: print e
...
[u'Invalid template string.']
"""
try:
value % {
'username': "uname",
'password': "pw",
'host': "111.111.111.111",
'port': 12345,
}
except (KeyError, TypeError, ValueError):
raise ValidationError(_("Invalid template string."))
...@@ -215,7 +215,7 @@ class Rule(models.Model): ...@@ -215,7 +215,7 @@ class Rule(models.Model):
dst = None dst = None
if host: if host:
ip = (host.ipv4, host.ipv6_with_prefixlen) ip = (host.ipv4, host.ipv6_with_host_prefixlen)
if self.direction == 'in': if self.direction == 'in':
dst = ip dst = ip
else: else:
...@@ -530,14 +530,30 @@ class Host(models.Model): ...@@ -530,14 +530,30 @@ class Host(models.Model):
def incoming_rules(self): def incoming_rules(self):
return self.rules.filter(direction='in') return self.rules.filter(direction='in')
@property @staticmethod
def ipv6_with_prefixlen(self): def create_ipnetwork(ip, prefixlen):
try: try:
net = IPNetwork(self.ipv6) net = IPNetwork(ip)
net.prefixlen = self.vlan.host_ipv6_prefixlen net.prefixlen = prefixlen
return net
except TypeError: except TypeError:
return None return None
else:
return net
@property
def ipv4_with_vlan_prefixlen(self):
return Host.create_ipnetwork(
self.ipv4, self.vlan.network4.prefixlen)
@property
def ipv6_with_vlan_prefixlen(self):
return Host.create_ipnetwork(
self.ipv6, self.vlan.network6.prefixlen)
@property
def ipv6_with_host_prefixlen(self):
return Host.create_ipnetwork(
self.ipv6, self.vlan.host_ipv6_prefixlen)
def get_external_ipv4(self): def get_external_ipv4(self):
return self.external_ipv4 if self.external_ipv4 else self.ipv4 return self.external_ipv4 if self.external_ipv4 else self.ipv4
...@@ -600,6 +616,19 @@ class Host(models.Model): ...@@ -600,6 +616,19 @@ class Host(models.Model):
description='created by host.save()', description='created by host.save()',
type='AAAA').save() type='AAAA').save()
def get_network_config(self):
interface = {'addresses': []}
if self.ipv4 and self.vlan.network4:
interface['addresses'].append(str(self.ipv4_with_vlan_prefixlen))
interface['gw4'] = str(self.vlan.network4.ip)
if self.ipv6 and self.vlan.network6:
interface['addresses'].append(str(self.ipv6_with_vlan_prefixlen))
interface['gw6'] = str(self.vlan.network6.ip)
return interface
def enable_net(self): def enable_net(self):
for i in settings.get('default_host_groups', []): for i in settings.get('default_host_groups', []):
self.groups.add(Group.objects.get(name=i)) self.groups.add(Group.objects.get(name=i))
......
...@@ -18,26 +18,37 @@ ...@@ -18,26 +18,37 @@
from logging import getLogger from logging import getLogger
from django.db.models import Sum from django.db.models import Sum
from django.utils.translation import ugettext_noop
from common.models import HumanReadableException
logger = getLogger(__name__) logger = getLogger(__name__)
class NotEnoughMemoryException(Exception): class SchedulerError(HumanReadableException):
admin_message = None
def __init__(self, message=None): def __init__(self, params=None, level=None, **kwargs):
if message is None: kwargs.update(params or {})
message = "No node has enough memory to accomodate the guest." super(SchedulerError, self).__init__(
level, self.message, self.admin_message or self.message,
kwargs)
Exception.__init__(self, message)
class NotEnoughMemoryException(SchedulerError):
message = ugettext_noop(
"The resources required for launching the virtual machine are not "
"available currently. Please try again later.")
class TraitsUnsatisfiableException(Exception): admin_message = ugettext_noop(
"The required free memory for launching the virtual machine is not "
"available on any usable node currently. Please try again later.")
def __init__(self, message=None):
if message is None:
message = "No node can satisfy all required traits of the guest."
Exception.__init__(self, message) class TraitsUnsatisfiableException(SchedulerError):
message = ugettext_noop(
"No node can satisfy the required traits of the "
"new vitual machine currently.")
def select_node(instance, nodes): def select_node(instance, nodes):
...@@ -77,19 +88,27 @@ def has_enough_ram(ram_size, node): ...@@ -77,19 +88,27 @@ def has_enough_ram(ram_size, node):
"""True, if the node has enough memory to accomodate a guest requiring """True, if the node has enough memory to accomodate a guest requiring
ram_size mebibytes of memory; otherwise, false. ram_size mebibytes of memory; otherwise, false.
""" """
ram_size = ram_size * 1024 * 1024
try: try:
total = node.ram_size total = node.ram_size
used = (node.ram_usage / 100) * total used = node.byte_ram_usage
unused = total - used unused = total - used
overcommit = node.ram_size_with_overcommit overcommit = node.ram_size_with_overcommit
reserved = node.instance_set.aggregate(r=Sum('ram_size'))['r'] or 0 reserved = (node.instance_set.aggregate(
r=Sum('ram_size'))['r'] or 0) * 1024 * 1024
free = overcommit - reserved free = overcommit - reserved
return ram_size < unused and ram_size < free retval = ram_size < unused and ram_size < free
logger.debug('has_enough_ram(%d, %s)=%s (total=%s unused=%s'
' overcommit=%s free=%s free_ok=%s overcommit_ok=%s)',
ram_size, node, retval, total, unused, overcommit, free,
ram_size < unused, ram_size < free)
return retval
except TypeError as e: except TypeError as e:
logger.warning('Got incorrect monitoring data for node %s. %s', logger.exception('Got incorrect monitoring data for node %s. %s',
unicode(node), unicode(e)) unicode(node), unicode(e))
return False return False
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
from django.forms import ModelForm from django.forms import ModelForm
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset, Div, Submit, BaseInput from crispy_forms.layout import Layout, Fieldset, Div, Submit, BaseInput
...@@ -56,8 +57,9 @@ class BlacklistItemForm(ModelForm): ...@@ -56,8 +57,9 @@ class BlacklistItemForm(ModelForm):
) )
), ),
FormActions( FormActions(
Submit('submit', 'Save changes'), Submit('submit', _('Save changes')),
LinkButton('back', 'Back', reverse_lazy('network.blacklist_list')) LinkButton('back', _("Back"),
reverse_lazy('network.blacklist_list'))
) )
) )
...@@ -77,8 +79,8 @@ class DomainForm(ModelForm): ...@@ -77,8 +79,8 @@ class DomainForm(ModelForm):
), ),
), ),
FormActions( FormActions(
Submit('submit', 'Save'), Submit('submit', _('Save')),
LinkButton('back', 'Back', reverse_lazy('network.domain_list')) LinkButton('back', _("Back"), reverse_lazy('network.domain_list'))
) )
) )
...@@ -91,15 +93,15 @@ class GroupForm(ModelForm): ...@@ -91,15 +93,15 @@ class GroupForm(ModelForm):
helper.layout = Layout( helper.layout = Layout(
Div( Div(
Fieldset( Fieldset(
'Identity', '',
'name', 'name',
'description', 'description',
'owner', 'owner',
), ),
), ),
FormActions( FormActions(
Submit('submit', 'Save'), Submit('submit', _('Save')),
LinkButton('back', 'Back', reverse_lazy('network.group_list')) LinkButton('back', _("Back"), reverse_lazy('network.group_list'))
) )
) )
...@@ -112,13 +114,13 @@ class HostForm(ModelForm): ...@@ -112,13 +114,13 @@ class HostForm(ModelForm):
helper.layout = Layout( helper.layout = Layout(
Div( Div(
Fieldset( Fieldset(
'Identity', '',
'hostname', 'hostname',
'reverse', 'reverse',
'mac', 'mac',
), ),
Fieldset( Fieldset(
'Network', _('Network'),
'vlan', 'vlan',
'ipv4', 'ipv4',
'ipv6', 'ipv6',
...@@ -126,7 +128,7 @@ class HostForm(ModelForm): ...@@ -126,7 +128,7 @@ class HostForm(ModelForm):
'external_ipv4', 'external_ipv4',
), ),
Fieldset( Fieldset(
'Information', _('Information'),
'description', 'description',
'location', 'location',
'comment', 'comment',
...@@ -134,8 +136,8 @@ class HostForm(ModelForm): ...@@ -134,8 +136,8 @@ class HostForm(ModelForm):
), ),
), ),
FormActions( FormActions(
Submit('submit', 'Save'), Submit('submit', _('Save')),
LinkButton('back', 'Back', reverse_lazy('network.host_list'))) LinkButton('back', _('Back'), reverse_lazy('network.host_list')))
) )
class Meta: class Meta:
...@@ -159,8 +161,8 @@ class RecordForm(ModelForm): ...@@ -159,8 +161,8 @@ class RecordForm(ModelForm):
) )
), ),
FormActions( FormActions(
Submit('submit', 'Save'), Submit('submit', _("Save")),
LinkButton('back', 'Back', reverse_lazy('network.record_list')) LinkButton('back', _("Back"), reverse_lazy('network.record_list'))
) )
) )
...@@ -173,7 +175,7 @@ class RuleForm(ModelForm): ...@@ -173,7 +175,7 @@ class RuleForm(ModelForm):
helper.layout = Layout( helper.layout = Layout(
Div( Div(
Fieldset( Fieldset(
'Identity', '',
'direction', 'direction',
'description', 'description',
'foreign_network', 'foreign_network',
...@@ -189,7 +191,7 @@ class RuleForm(ModelForm): ...@@ -189,7 +191,7 @@ class RuleForm(ModelForm):
'nat_external_ipv4', 'nat_external_ipv4',
), ),
Fieldset( Fieldset(
'External', _('External'),
'vlan', 'vlan',
'vlangroup', 'vlangroup',
'host', 'host',
...@@ -198,8 +200,8 @@ class RuleForm(ModelForm): ...@@ -198,8 +200,8 @@ class RuleForm(ModelForm):
) )
), ),
FormActions( FormActions(
Submit('submit', 'Save'), Submit('submit', _("Save")),
LinkButton('back', 'Back', reverse_lazy('network.rule_list')) LinkButton('back', _("Back"), reverse_lazy('network.rule_list'))
) )
) )
...@@ -219,8 +221,8 @@ class SwitchPortForm(ModelForm): ...@@ -219,8 +221,8 @@ class SwitchPortForm(ModelForm):
) )
), ),
FormActions( FormActions(
Submit('submit', 'Save'), Submit('submit', _("Save")),
LinkButton('back', 'Back', LinkButton('back', _("Back"),
reverse_lazy('network.switch_port_list')) reverse_lazy('network.switch_port_list'))
) )
) )
...@@ -234,41 +236,42 @@ class VlanForm(ModelForm): ...@@ -234,41 +236,42 @@ class VlanForm(ModelForm):
helper.layout = Layout( helper.layout = Layout(
Div( Div(
Fieldset( Fieldset(
'Identity', '',
'name', 'name',
'vid', 'vid',
'network_type', 'network_type',
'managed', 'managed',
), ),
Fieldset( Fieldset(
'IPv4', _('IPv4'),
'network4', 'network4',
'snat_to', 'snat_to',
'snat_ip', 'snat_ip',
'dhcp_pool', 'dhcp_pool',
), ),
Fieldset( Fieldset(
'IPv6', _('IPv6'),
'network6', 'network6',
'ipv6_template', 'ipv6_template',
'host_ipv6_prefixlen', 'host_ipv6_prefixlen',
), ),
Fieldset( Fieldset(
'Domain name service', _('Domain name service'),
'domain', 'domain',
'reverse_domain', 'reverse_domain',
), ),
Fieldset( Fieldset(
'Info', _('Info'),
'description', 'description',
'comment', 'comment',
'owner',
# 'created_at', # 'created_at',
# 'modified_at', # 'modified_at',
), ),
), ),
FormActions( FormActions(
Submit('submit', 'Save'), Submit('submit', _("Save")),
LinkButton('back', 'Back', reverse_lazy('network.vlan_list')) LinkButton('back', _("Back"), reverse_lazy('network.vlan_list'))
) )
) )
...@@ -289,8 +292,8 @@ class VlanGroupForm(ModelForm): ...@@ -289,8 +292,8 @@ class VlanGroupForm(ModelForm):
) )
), ),
FormActions( FormActions(
Submit('submit', 'Save'), Submit('submit', _("Save")),
LinkButton('back', 'Back', reverse_lazy( LinkButton('back', _("Back"), reverse_lazy(
'network.vlan_group_list')) 'network.vlan_group_list'))
) )
) )
......
...@@ -6,3 +6,8 @@ ...@@ -6,3 +6,8 @@
text-align: center; text-align: center;
} }
#host-detail-records-table td:first-child,
#host-detail-records-table th:first-child {
text-align: center;
width: 60px;
}
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext_lazy as _
from django_tables2 import Table, A from django_tables2 import Table, A
from django_tables2.columns import LinkColumn, TemplateColumn from django_tables2.columns import LinkColumn, TemplateColumn
...@@ -181,3 +183,20 @@ class VlanGroupTable(Table): ...@@ -181,3 +183,20 @@ class VlanGroupTable(Table):
attrs = {'class': 'table table-striped table-condensed'} attrs = {'class': 'table table-striped table-condensed'}
fields = ('name', 'vlans', 'description', 'owner', ) fields = ('name', 'vlans', 'description', 'owner', )
order_by = 'name' order_by = 'name'
class HostRecordsTable(Table):
fqdn = LinkColumn(
"network.record", args=[A("pk")],
order_by=("name", ),
)
class Meta:
model = Record
attrs = {
'class': "table table-striped table-bordered",
'id': "host-detail-records-table",
}
fields = ("type", "fqdn")
order_by = ("name", )
empty_text = _("No records.")
{% load i18n %} {% load i18n %}
{% load l10n %} {% load l10n %}
{# <span style="color: #FF0000;">[{{ record.r_type }}]</span> #} {% if record.direction == "in" %}
{% if record.direction == "1" %}
{{ record.foreign_network }} {{ record.foreign_network }}
[{% for v in record.foreign_network.vlans.all %}
{{ v.name }}{% if not forloop.last %},{% endif %}
{% endfor %}]
{% else %} {% else %}
{% if record.r_type == "host" %} {% if record.r_type == "host" %}
{{ record.host.get_fqdn }} {{ record.host.get_fqdn }}
...@@ -11,10 +13,10 @@ ...@@ -11,10 +13,10 @@
{{ record.r_type }} {{ record.r_type }}
{% endif %} {% endif %}
{% endif %} {% endif %}
{#<span style="color: #0000FF;"> ▸ </span>#}
<i class="fa fa-arrow-right"></i> <i class="fa fa-arrow-right"></i>
{% if record.direction == "0" %}
{% if record.direction == "out" %}
{{ record.foreign_network }} {{ record.foreign_network }}
{% else %} {% else %}
{% if record.r_type == "host" %} {% if record.r_type == "host" %}
...@@ -28,11 +30,16 @@ ...@@ -28,11 +30,16 @@
{% endif %} {% endif %}
{% if record.extra %} {% if record.extra %}
<span class="label label-default">{{ record.extra }}</span> <span class="label label-default">{{ record.extra }}</span>
{% endif %} {% endif %}
{% if record.nat %} {% if record.nat %}
<span class="label label-success">NAT <span class="label label-success">NAT
[ {{ record.dport }} <i class="fa fa-arrow-right"></i> [
{{record.nat_external_port}} ]</span> {{record.nat_external_port}}
<i class="fa fa-arrow-right"></i>
{{ record.dport }}
]
{{ record.proto|upper }}
</span>
{% endif %} {% endif %}
...@@ -7,68 +7,77 @@ ...@@ -7,68 +7,77 @@
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<a href="{% url "network.host_delete" pk=host_pk%}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this host" %}</a> <a href="{% url "network.host_delete" pk=host_pk%}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this host" %}</a>
<h2>{{ form.hostname.value }}</h2> <h2>{{ form.hostname.value }}</h2>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm-7"> <div class="col-md-6">
{% crispy form %} {% crispy form %}
</div>
<div class="col-md-6">
<div class="page-header">
<a href="{% url "network.rule_create" %}?host={{ host_pk }}" class="btn btn-success pull-right btn-xs"><i class="fa fa-plus-circle"></i> {% trans "Add new rule" %}</a>
<h3>{% trans "Rules" %}</h3>
</div> </div>
<div class="col-sm-5"> {% if rule_list.data.data.count > 0 %}
<div class="page-header"> {% render_table rule_list %}
<a href="{% url "network.rule_create" %}?host={{ host_pk }}" class="btn btn-success pull-right btn-xs"><i class="fa fa-plus-circle"></i> {% trans "Add new rule" %}</a> {% else %}
<h3>{% trans "Rules" %}</h3> {% trans "No rules associated with this host." %}
</div> {% endif %}
{% if rule_list.data.data.count > 0 %}
{% render_table rule_list %}
{% else %}
{% trans "No rules associated with this host!" %}
{% endif %}
<div class="page-header"> <div class="page-header">
<h3>{% trans "Groups" %}</h3> <h3>{% trans "Groups" %}</h3>
</div>
{% if group_rule_list|length > 0 %}
{% for group in group_rule_list %}
<div>
<h4 id="{{ group.pk }}_group_pk">{{ group.name }}
<a href="{% url "network.remove_host_group" pk=host_pk group_pk=group.pk %}?from={{ request.path }}">
<i class="fa fa-times" style="vertical-align: middle;"></i></a>
<a href="{% url "network.group" group.pk %}">
<i class="fa fa-pencil" style="vertical-align: middle;"></i></a>
</h4>
</div> </div>
{% if group_rule_list|length > 0 %} {% endfor %}
{% for group in group_rule_list %} {% else %}
<div> {% trans "This host is not added to any host groups!" %}
<h4 id="{{ group.pk }}_group_pk">{{ group.name }} {% endif %}
<a href="{% url "network.remove_host_group" pk=host_pk group_pk=group.pk %}?from={{ request.path }}">
<i class="fa fa-times" style="vertical-align: middle;"></i></a>
<a href="{% url "network.group" group.pk %}">
<i class="fa fa-pencil" style="vertical-align: middle;"></i></a>
</h4>
</div>
{% endfor %}
{% else %}
{% trans "This host is not added to any host groups!" %}
{% endif %}
<div class="page-header"> <div class="page-header">
<h3>Add host group</h3> <h3>{% trans "Add host group" %}</h3>
</div> </div>
{% if not_used_groups|length == 0 %} {% if not_used_groups|length == 0 %}
No more groups to add! {% trans "No more groups to add" %}
{% else %} {% else %}
<form action="{% url "network.add_host_group" pk=host_pk %}" method="POST"> <form action="{% url "network.add_host_group" pk=host_pk %}" method="POST">
{% csrf_token %} {% csrf_token %}
<div class="input-group"> <div class="input-group">
<select name="group" id="add_group" class="form-control"> <select name="group" id="add_group" class="form-control">
{% for rest in not_used_groups %} {% for rest in not_used_groups %}
<option value="{{ rest.pk }}">{{ rest }}</option> <option value="{{ rest.pk }}">{{ rest }}</option>
{% endfor %} {% endfor %}
</select> </select>
<div class="input-group-btn"> <div class="input-group-btn">
<input type="submit" value="{% trans "Add group" %}" class="btn btn-default"></input> <input type="submit" value="{% trans "Add group" %}" class="btn btn-default"></input>
</div> </div>
</div><!-- input-group --> </div><!-- input-group -->
</form> </form>
{% endif %} {% endif %}
</div><!-- col-sm-4 --> <div class="page-header">
<a href="{% url "network.record_create" %}?host={{ host_pk }}"
class="btn btn-xs btn-success pull-right">
<i class="fa fa-plus-circle"></i>
{% trans "Add new CNAME record" %}
</a>
<h3>{% trans "Records" %}</h3>
</div>
{% render_table records_table %}
</div><!-- col-sm-5 -->
</div><!-- row --> </div><!-- row -->
{% endblock %} {% endblock %}
{% block extra_etc %} {% block extra_etc %}
<script src="{% static "js/host.js" %}"></script> <script src="{% static "js/host.js" %}"></script>
{% endblock %} {% endblock %}
...@@ -34,6 +34,7 @@ from .forms import (HostForm, VlanForm, DomainForm, GroupForm, RecordForm, ...@@ -34,6 +34,7 @@ from .forms import (HostForm, VlanForm, DomainForm, GroupForm, RecordForm,
BlacklistItemForm, RuleForm, VlanGroupForm, SwitchPortForm) BlacklistItemForm, RuleForm, VlanGroupForm, SwitchPortForm)
from django.contrib import messages from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic.edit import FormMixin from django.views.generic.edit import FormMixin
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from braces.views import LoginRequiredMixin, SuperuserRequiredMixin from braces.views import LoginRequiredMixin, SuperuserRequiredMixin
...@@ -42,25 +43,14 @@ from operator import itemgetter ...@@ -42,25 +43,14 @@ from operator import itemgetter
from itertools import chain from itertools import chain
import json import json
from dashboard.views import AclUpdateView from dashboard.views import AclUpdateView
from dashboard.forms import AclUserAddForm from dashboard.forms import AclUserOrGroupAddForm
class SuccessMessageMixin(FormMixin): class InitialOwnerMixin(FormMixin):
""" def get_initial(self):
Adds a success message on successful form submission. initial = super(InitialOwnerMixin, self).get_initial()
From django/contrib/messages/views.py@9a85ad89 initial['owner'] = self.request.user
""" return initial
success_message = ''
def form_valid(self, form):
response = super(SuccessMessageMixin, self).form_valid(form)
success_message = self.get_success_message(form.cleaned_data)
if success_message:
messages.success(self.request, success_message)
return response
def get_success_message(self, cleaned_data):
return self.success_message % cleaned_data
class IndexView(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): class IndexView(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
...@@ -190,7 +180,7 @@ class DomainDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -190,7 +180,7 @@ class DomainDetail(LoginRequiredMixin, SuperuserRequiredMixin,
class DomainCreate(LoginRequiredMixin, SuperuserRequiredMixin, class DomainCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView): SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Domain model = Domain
template_name = "network/domain-create.html" template_name = "network/domain-create.html"
form_class = DomainForm form_class = DomainForm
...@@ -274,7 +264,7 @@ class GroupList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView): ...@@ -274,7 +264,7 @@ class GroupList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
class GroupCreate(LoginRequiredMixin, SuperuserRequiredMixin, class GroupCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView): SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Group model = Group
template_name = "network/group-create.html" template_name = "network/group-create.html"
form_class = GroupForm form_class = GroupForm
...@@ -399,6 +389,12 @@ class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -399,6 +389,12 @@ class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin,
# set host pk (we need this for URL-s) # set host pk (we need this for URL-s)
context['host_pk'] = self.kwargs['pk'] context['host_pk'] = self.kwargs['pk']
from network.tables import HostRecordsTable
context['records_table'] = HostRecordsTable(
Record.objects.filter(host=self.get_object()),
request=self.request, template="django_tables2/table_no_page.html"
)
return context return context
def get_success_url(self): def get_success_url(self):
...@@ -407,7 +403,7 @@ class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -407,7 +403,7 @@ class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin,
class HostCreate(LoginRequiredMixin, SuperuserRequiredMixin, class HostCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView): SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Host model = Host
template_name = "network/host-create.html" template_name = "network/host-create.html"
form_class = HostForm form_class = HostForm
...@@ -491,7 +487,7 @@ class RecordDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -491,7 +487,7 @@ class RecordDetail(LoginRequiredMixin, SuperuserRequiredMixin,
class RecordCreate(LoginRequiredMixin, SuperuserRequiredMixin, class RecordCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView): SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Record model = Record
template_name = "network/record-create.html" template_name = "network/record-create.html"
form_class = RecordForm form_class = RecordForm
...@@ -499,10 +495,23 @@ class RecordCreate(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -499,10 +495,23 @@ class RecordCreate(LoginRequiredMixin, SuperuserRequiredMixin,
success_message = _(u'Successfully created record!') success_message = _(u'Successfully created record!')
def get_initial(self): def get_initial(self):
return { initial = super(RecordCreate, self).get_initial()
# 'owner': 1, initial['domain'] = self.request.GET.get('domain')
'domain': self.request.GET.get('domain'),
} host_pk = self.request.GET.get("host")
try:
host = Host.objects.get(pk=host_pk)
except (Host.DoesNotExist, ValueError):
host = None
if host:
initial.update({
'type': "CNAME",
'host': host,
'address': host.get_fqdn(),
})
return initial
class RecordDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): class RecordDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
...@@ -550,18 +559,19 @@ class RuleDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -550,18 +559,19 @@ class RuleDetail(LoginRequiredMixin, SuperuserRequiredMixin,
class RuleCreate(LoginRequiredMixin, SuperuserRequiredMixin, class RuleCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView): SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Rule model = Rule
template_name = "network/rule-create.html" template_name = "network/rule-create.html"
form_class = RuleForm form_class = RuleForm
success_message = _(u'Successfully created rule!') success_message = _(u'Successfully created rule!')
def get_initial(self): def get_initial(self):
return { initial = super(RuleCreate, self).get_initial()
# 'owner': 1, initial.update({
'host': self.request.GET.get('host'), 'host': self.request.GET.get('host'),
'hostgroup': self.request.GET.get('hostgroup') 'hostgroup': self.request.GET.get('hostgroup')
} })
return initial
class RuleDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): class RuleDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
...@@ -654,14 +664,14 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -654,14 +664,14 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
context['vlan_vid'] = self.kwargs.get('vid') context['vlan_vid'] = self.kwargs.get('vid')
context['acl'] = AclUpdateView.get_acl_data( context['acl'] = AclUpdateView.get_acl_data(
self.object, self.request.user, 'network.vlan-acl') self.object, self.request.user, 'network.vlan-acl')
context['aclform'] = AclUserAddForm() context['aclform'] = AclUserOrGroupAddForm()
return context return context
success_url = reverse_lazy('network.vlan_list') success_url = reverse_lazy('network.vlan_list')
class VlanCreate(LoginRequiredMixin, SuperuserRequiredMixin, class VlanCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView): SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Vlan model = Vlan
template_name = "network/vlan-create.html" template_name = "network/vlan-create.html"
form_class = VlanForm form_class = VlanForm
...@@ -741,7 +751,7 @@ class VlanGroupDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -741,7 +751,7 @@ class VlanGroupDetail(LoginRequiredMixin, SuperuserRequiredMixin,
class VlanGroupCreate(LoginRequiredMixin, SuperuserRequiredMixin, class VlanGroupCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView): SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = VlanGroup model = VlanGroup
template_name = "network/vlan-group-create.html" template_name = "network/vlan-group-create.html"
form_class = VlanGroupForm form_class = VlanGroupForm
......
...@@ -278,7 +278,7 @@ class Disk(TimeStampedModel): ...@@ -278,7 +278,7 @@ class Disk(TimeStampedModel):
return Disk.create(base=self, datastore=self.datastore, return Disk.create(base=self, datastore=self.datastore,
name=self.name, size=self.size, name=self.name, size=self.size,
type=new_type) type=new_type, dev_num=self.dev_num)
def get_vmdisk_desc(self): def get_vmdisk_desc(self):
"""Serialize disk object to the vmdriver. """Serialize disk object to the vmdriver.
...@@ -367,7 +367,8 @@ class Disk(TimeStampedModel): ...@@ -367,7 +367,8 @@ class Disk(TimeStampedModel):
disk = cls.__create(user, params) disk = cls.__create(user, params)
disk.clean() disk.clean()
disk.save() disk.save()
logger.debug("Disk created: %s", params) logger.debug(u"Disk created from: %s",
unicode(params.get("base", "nobase")))
return disk return disk
@classmethod @classmethod
......
...@@ -11,10 +11,6 @@ ...@@ -11,10 +11,6 @@
body { body {
margin-top: 40px; margin-top: 40px;
} }
.container {
width: 600px;
}
ul, li { ul, li {
list-style: none; list-style: none;
margin: 0; margin: 0;
...@@ -22,6 +18,8 @@ ...@@ -22,6 +18,8 @@
} }
.container > .content { .container > .content {
max-width: 570px;
margin: auto;
background-color: #fff; background-color: #fff;
padding: 20px; padding: 20px;
-webkit-border-radius: 10px 10px 10px 10px; -webkit-border-radius: 10px 10px 10px 10px;
...@@ -38,8 +36,10 @@ ...@@ -38,8 +36,10 @@
} }
.login-form { .login-form {
margin-top: 40px; margin: 20px auto 0 auto;
padding: 0 10px; padding: 0 10px;
max-width: 250px;
} }
.login-form form { .login-form form {
...@@ -79,3 +79,10 @@ ...@@ -79,3 +79,10 @@
<img src="{% static "dashboard/img/logo.png" %}" style="height: 25px;"/> <img src="{% static "dashboard/img/logo.png" %}" style="height: 25px;"/>
</a> </a>
{% endblock %} {% endblock %}
{% block content %}
<div class="content">
{% block content_box %}{% endblock %}
</div>
{% endblock %}
...@@ -12,15 +12,14 @@ ...@@ -12,15 +12,14 @@
</a> </a>
{% endblock %} {% endblock %}
{% block content %} {% block content_box %}
<div class="content">
<div class="row"> <div class="row">
{% if form.password.errors or form.username.errors %} {% if form.password.errors or form.username.errors %}
<div class="login-form-errors"> <div class="login-form-errors">
{% include "display-form-errors.html" %} {% include "display-form-errors.html" %}
</div> </div>
{% endif %} {% endif %}
<div class="col-sm-{% if saml2 %}6{% else %}12{% endif %}"> <div class="col-xs-{% if saml2 %}6{% else %}12{% endif %}">
<div class="login-form"> <div class="login-form">
<form action="" method="POST"> <form action="" method="POST">
{% csrf_token %} {% csrf_token %}
...@@ -29,8 +28,8 @@ ...@@ -29,8 +28,8 @@
</div> </div>
</div> </div>
{% if saml2 %} {% if saml2 %}
<div class="col-sm-6"> <div class="col-xs-6">
<h4 style="padding-top: 0; margin-top: 0;">{% trans "Login with SSO" %}</h4> <h4 style="padding-top: 0; margin-top: 20px;">{% trans "Login with SSO" %}</h4>
<a href="{% url "saml2_login" %}">{% trans "Click here!" %}</a> <a href="{% url "saml2_login" %}">{% trans "Click here!" %}</a>
</div> </div>
{% endif %} {% endif %}
......
...@@ -5,17 +5,15 @@ ...@@ -5,17 +5,15 @@
{% block title-page %}{% trans "Password reset complete" %}{% endblock %} {% block title-page %}{% trans "Password reset complete" %}{% endblock %}
{% block content %} {% block content_box %}
<div class="content"> <div class="row">
<div class="row"> <div class="login-form-errors">
<div class="login-form-errors"> {% include "display-form-errors.html" %}
{% include "display-form-errors.html" %} </div>
</div> <div class="col-sm-12">
<div class="col-sm-12"> <div class="alert alert-success">
<div class="alert alert-success"> {% trans "Password change successful!" %}
{% trans "Password change successful!" %} <a href="{% url "accounts.login" %}">{% trans "Click here to login" %}</a>
<a href="{% url "accounts.login" %}">{% trans "Click here to login" %}</a>
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -5,26 +5,26 @@ ...@@ -5,26 +5,26 @@
{% block title-page %}{% trans "Password reset confirm" %}{% endblock %} {% block title-page %}{% trans "Password reset confirm" %}{% endblock %}
{% block content %} {% block content_box %}
<div> <div class="row">
<div class="row"> <div class="login-form-errors">
<div class="login-form-errors"> {% include "display-form-errors.html" %}
{% include "display-form-errors.html" %} </div>
<div class="col-sm-12">
<div>
{% blocktrans %}
Please enter your new password twice so we can verify you typed it in correctly.
{% endblocktrans %}
</div> </div>
<div class="col-sm-12">
<div style="margin: 0 0 25px 0;">
{% blocktrans %}Please enter your new password twice so we can verify you typed it in correctly!{% endblocktrans %}
</div>
{% if form %} {% if form %}
{% crispy form %} {% crispy form %}
{% else %} {% else %}
<div class="alert alert-warning"> <div class="alert alert-warning">
{% url "accounts.password-reset" as url %} {% url "accounts.password-reset" as url %}
{% blocktrans with url=url %}This token is expired, please <a href="{{ url }}">request</a> a new password reset link again!{% endblocktrans %} {% blocktrans with url=url %}This token is expired, please <a href="{{ url }}">request</a> a new password reset link again.{% endblocktrans %}
</div> </div>
{% endif %} {% endif %}
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
...@@ -5,16 +5,14 @@ ...@@ -5,16 +5,14 @@
{% block title-page %}{% trans "Password reset done" %}{% endblock %} {% block title-page %}{% trans "Password reset done" %}{% endblock %}
{% block content %} {% block content_box %}
<div class="content"> <div class="row">
<div class="row"> <div class="login-form-errors">
<div class="login-form-errors"> {% include "display-form-errors.html" %}
{% include "display-form-errors.html" %} </div>
</div> <div class="col-sm-12">
<div class="col-sm-12"> <div class="pull-right"><a href="{% url "accounts.login" %}">{% trans "Back to login" %}</a></div>
<div class="pull-right"><a href="{% url "accounts.login" %}">{% trans "Back to login" %}</a></div> {% trans "We have sent you an email about your next steps." %}
{% trans "We have sent you an email about your next steps!" %}
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
...@@ -5,20 +5,20 @@ ...@@ -5,20 +5,20 @@
{% block title-page %}{% trans "Password reset" %}{% endblock %} {% block title-page %}{% trans "Password reset" %}{% endblock %}
{% block content %} {% block content_box %}
<div class="content"> <div class="row">
<div class="row"> <div class="login-form-errors">
<div class="login-form-errors"> {% include "display-form-errors.html" %}
{% include "display-form-errors.html" %} </div>
</div> <div class="col-sm-12">
<div class="col-sm-12"> <div class="pull-right"><a href="{% url "accounts.login" %}">{% trans "Back to login" %}</a></div>
<div class="pull-right"><a href="{% url "accounts.login" %}">{% trans "Back to login" %}</a></div> <h4 style="margin: 0 0 25px 0;">
<h4 style="margin: 0 0 25px 0;">{% blocktrans %}Enter your email address to reset your password!{% endblocktrans %}</h4> {% blocktrans %}Enter your email address to reset your password.{% endblocktrans %}
<form action="" method="POST"> </h4>
{% csrf_token %} <form action="" method="POST">
{% crispy form %} {% csrf_token %}
</form> {% crispy form %}
</div> </form>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
...@@ -24,7 +24,7 @@ from celery.signals import worker_ready ...@@ -24,7 +24,7 @@ from celery.signals import worker_ready
from celery.contrib.abortable import AbortableAsyncResult from celery.contrib.abortable import AbortableAsyncResult
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import CharField, ForeignKey from django.db.models import CharField, ForeignKey, BooleanField
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.utils.translation import ugettext_lazy as _, ugettext_noop
...@@ -70,6 +70,8 @@ class InstanceActivity(ActivityModel): ...@@ -70,6 +70,8 @@ class InstanceActivity(ActivityModel):
help_text=_('Instance this activity works on.'), help_text=_('Instance this activity works on.'),
verbose_name=_('instance')) verbose_name=_('instance'))
resultant_state = CharField(blank=True, max_length=20, null=True) resultant_state = CharField(blank=True, max_length=20, null=True)
interruptible = BooleanField(default=False, help_text=_(
'Other activities can interrupt this one.'))
class Meta: class Meta:
app_label = 'vm' app_label = 'vm'
...@@ -91,24 +93,30 @@ class InstanceActivity(ActivityModel): ...@@ -91,24 +93,30 @@ class InstanceActivity(ActivityModel):
@classmethod @classmethod
def create(cls, code_suffix, instance, task_uuid=None, user=None, def create(cls, code_suffix, instance, task_uuid=None, user=None,
concurrency_check=True, readable_name=None, concurrency_check=True, readable_name=None,
resultant_state=None): resultant_state=None, interruptible=False):
readable_name = _normalize_readable_name(readable_name, code_suffix) readable_name = _normalize_readable_name(readable_name, code_suffix)
# Check for concurrent activities # Check for concurrent activities
active_activities = instance.activity_log.filter(finished__isnull=True) active_activities = instance.activity_log.filter(finished__isnull=True)
if concurrency_check and active_activities.exists(): if concurrency_check and active_activities.exists():
raise ActivityInProgressError.create(active_activities[0]) for i in active_activities:
if i.interruptible:
activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix) i.finish(False, result=ugettext_noop(
"Interrupted by other activity."))
else:
raise ActivityInProgressError.create(i)
activity_code = cls.construct_activity_code(code_suffix)
act = cls(activity_code=activity_code, instance=instance, parent=None, act = cls(activity_code=activity_code, instance=instance, parent=None,
resultant_state=resultant_state, started=timezone.now(), resultant_state=resultant_state, started=timezone.now(),
readable_name_data=readable_name.to_dict(), readable_name_data=readable_name.to_dict(),
task_uuid=task_uuid, user=user) task_uuid=task_uuid, user=user, interruptible=interruptible)
act.save() act.save()
return act return act
def create_sub(self, code_suffix, task_uuid=None, concurrency_check=True, def create_sub(self, code_suffix, task_uuid=None, concurrency_check=True,
readable_name=None, resultant_state=None): readable_name=None, resultant_state=None,
interruptible=False):
readable_name = _normalize_readable_name(readable_name, code_suffix) readable_name = _normalize_readable_name(readable_name, code_suffix)
# Check for concurrent activities # Check for concurrent activities
...@@ -119,7 +127,7 @@ class InstanceActivity(ActivityModel): ...@@ -119,7 +127,7 @@ class InstanceActivity(ActivityModel):
act = InstanceActivity( act = InstanceActivity(
activity_code=join_activity_code(self.activity_code, code_suffix), activity_code=join_activity_code(self.activity_code, code_suffix),
instance=self.instance, parent=self, instance=self.instance, parent=self,
resultant_state=resultant_state, resultant_state=resultant_state, interruptible=interruptible,
readable_name_data=readable_name.to_dict(), started=timezone.now(), readable_name_data=readable_name.to_dict(), started=timezone.now(),
task_uuid=task_uuid, user=self.user) task_uuid=task_uuid, user=self.user)
act.save() act.save()
...@@ -183,13 +191,14 @@ class InstanceActivity(ActivityModel): ...@@ -183,13 +191,14 @@ class InstanceActivity(ActivityModel):
@contextmanager @contextmanager
def sub_activity(self, code_suffix, on_abort=None, on_commit=None, def sub_activity(self, code_suffix, on_abort=None, on_commit=None,
readable_name=None, task_uuid=None, readable_name=None, task_uuid=None,
concurrency_check=True): concurrency_check=True, interruptible=False):
"""Create a transactional context for a nested instance activity. """Create a transactional context for a nested instance activity.
""" """
if not readable_name: if not readable_name:
warn("Set readable_name", stacklevel=3) warn("Set readable_name", stacklevel=3)
act = self.create_sub(code_suffix, task_uuid, concurrency_check, act = self.create_sub(code_suffix, task_uuid, concurrency_check,
readable_name=readable_name) readable_name=readable_name,
interruptible=interruptible)
return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit) return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
def get_operation(self): def get_operation(self):
......
...@@ -123,6 +123,10 @@ class VirtualMachineDescModel(BaseResourceConfigModel): ...@@ -123,6 +123,10 @@ class VirtualMachineDescModel(BaseResourceConfigModel):
'format like "%s".') % 'format like "%s".') %
'Ubuntu 12.04 LTS Desktop amd64')) 'Ubuntu 12.04 LTS Desktop amd64'))
tags = TaggableManager(blank=True, verbose_name=_("tags")) tags = TaggableManager(blank=True, verbose_name=_("tags"))
has_agent = BooleanField(verbose_name=_('has agent'), default=True,
help_text=_(
'If the machine has agent installed, and '
'the manager should wait for its start.'))
class Meta: class Meta:
abstract = True abstract = True
...@@ -244,10 +248,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -244,10 +248,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
verbose_name=_('time of delete'), verbose_name=_('time of delete'),
help_text=_("Proposed time of automatic " help_text=_("Proposed time of automatic "
"deletion.")) "deletion."))
active_since = DateTimeField(blank=True, null=True,
help_text=_("Time stamp of successful "
"boot report."),
verbose_name=_('active since'))
node = ForeignKey(Node, blank=True, null=True, node = ForeignKey(Node, blank=True, null=True,
related_name='instance_set', related_name='instance_set',
help_text=_("Current hypervisor of this instance."), help_text=_("Current hypervisor of this instance."),
...@@ -428,7 +428,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -428,7 +428,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
# prepare parameters # prepare parameters
common_fields = ['name', 'description', 'num_cores', 'ram_size', common_fields = ['name', 'description', 'num_cores', 'ram_size',
'max_ram_size', 'arch', 'priority', 'boot_menu', 'max_ram_size', 'arch', 'priority', 'boot_menu',
'raw_data', 'lease', 'access_method', 'system'] 'raw_data', 'lease', 'access_method', 'system',
'has_agent']
params = dict(template=template, owner=owner, pw=pwgen()) params = dict(template=template, owner=owner, pw=pwgen())
params.update([(f, getattr(template, f)) for f in common_fields]) params.update([(f, getattr(template, f)) for f in common_fields])
params.update(kwargs) # override defaults w/ user supplied values params.update(kwargs) # override defaults w/ user supplied values
...@@ -513,7 +514,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -513,7 +514,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def ipv4(self): def ipv4(self):
"""Primary IPv4 address of the instance. """Primary IPv4 address of the instance.
""" """
return self.primary_host.ipv4 if self.primary_host else None # return self.primary_host.ipv4 if self.primary_host else None
for i in self.interface_set.all():
if i.host:
return i.host.ipv4
return None
@property @property
def ipv6(self): def ipv6(self):
...@@ -528,15 +533,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -528,15 +533,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
return self.primary_host.mac if self.primary_host else None return self.primary_host.mac if self.primary_host else None
@property @property
def uptime(self):
"""Uptime of the instance.
"""
if self.active_since:
return timezone.now() - self.active_since
else:
return timedelta() # zero
@property
def os_type(self): def os_type(self):
"""Get the type of the instance's operating system. """Get the type of the instance's operating system.
""" """
...@@ -545,13 +541,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -545,13 +541,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
else: else:
return self.template.os_type return self.template.os_type
def get_age(self):
"""Deprecated. Use uptime instead.
Get age of VM in seconds.
"""
return self.uptime.seconds
@property @property
def waiting(self): def waiting(self):
"""Indicates whether the instance's waiting for an operation to finish. """Indicates whether the instance's waiting for an operation to finish.
...@@ -603,14 +592,19 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -603,14 +592,19 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
port = self.get_connect_port(use_ipv6=use_ipv6) port = self.get_connect_port(use_ipv6=use_ipv6)
host = self.get_connect_host(use_ipv6=use_ipv6) host = self.get_connect_host(use_ipv6=use_ipv6)
proto = self.access_method proto = self.access_method
if proto == 'ssh': return ('circle:%(proto)s:cloud:%(pw)s:%(host)s:%(port)d' %
proto = 'sshterm'
return ('%(proto)s:cloud:%(pw)s:%(host)s:%(port)d' %
{'port': port, 'proto': proto, 'pw': self.pw, {'port': port, 'proto': proto, 'pw': self.pw,
'host': host}) 'host': host})
except: except:
return return
@property
def short_hostname(self):
try:
return self.primary_host.hostname
except AttributeError:
return self.vm_name
def get_vm_desc(self): def get_vm_desc(self):
"""Serialize Instance object to vmdriver. """Serialize Instance object to vmdriver.
""" """
......
...@@ -34,6 +34,7 @@ from common.models import ( ...@@ -34,6 +34,7 @@ from common.models import (
create_readable, humanize_exception, HumanReadableException create_readable, humanize_exception, HumanReadableException
) )
from common.operations import Operation, register_operation from common.operations import Operation, register_operation
from manager.scheduler import SchedulerError
from .tasks.local_tasks import ( from .tasks.local_tasks import (
abortable_async_instance_operation, abortable_async_node_operation, abortable_async_instance_operation, abortable_async_node_operation,
) )
...@@ -41,7 +42,7 @@ from .models import ( ...@@ -41,7 +42,7 @@ from .models import (
Instance, InstanceActivity, InstanceTemplate, Interface, Node, Instance, InstanceActivity, InstanceTemplate, Interface, Node,
NodeActivity, pwgen NodeActivity, pwgen
) )
from .tasks import agent_tasks from .tasks import agent_tasks, local_agent_tasks
from dashboard.store_api import Store, NoStoreException from dashboard.store_api import Store, NoStoreException
...@@ -153,6 +154,7 @@ class AddInterfaceOperation(InstanceOperation): ...@@ -153,6 +154,7 @@ class AddInterfaceOperation(InstanceOperation):
self.rollback(net, activity) self.rollback(net, activity)
raise raise
net.deploy() net.deploy()
local_agent_tasks.send_networking_commands(self.instance, activity)
def get_activity_name(self, kwargs): def get_activity_name(self, kwargs):
return create_readable(ugettext_noop("add %(vlan)s interface"), return create_readable(ugettext_noop("add %(vlan)s interface"),
...@@ -297,16 +299,20 @@ class DeployOperation(InstanceOperation): ...@@ -297,16 +299,20 @@ class DeployOperation(InstanceOperation):
"deploy network")): "deploy network")):
self.instance.deploy_net() self.instance.deploy_net()
try:
self.instance.renew(parent_activity=activity)
except:
pass
# Resume vm # Resume vm
with activity.sub_activity( with activity.sub_activity(
'booting', readable_name=ugettext_noop( 'booting', readable_name=ugettext_noop(
"boot virtual machine")): "boot virtual machine")):
self.instance.resume_vm(timeout=timeout) self.instance.resume_vm(timeout=timeout)
try: if self.instance.has_agent:
self.instance.renew(parent_activity=activity) activity.sub_activity('os_boot', readable_name=ugettext_noop(
except: "wait operating system loading"), interruptible=True)
pass
register_operation(DeployOperation) register_operation(DeployOperation)
...@@ -423,8 +429,11 @@ class RebootOperation(InstanceOperation): ...@@ -423,8 +429,11 @@ class RebootOperation(InstanceOperation):
required_perms = () required_perms = ()
accept_states = ('RUNNING', ) accept_states = ('RUNNING', )
def _operation(self, timeout=5): def _operation(self, activity, timeout=5):
self.instance.reboot_vm(timeout=timeout) self.instance.reboot_vm(timeout=timeout)
if self.instance.has_agent:
activity.sub_activity('os_boot', readable_name=ugettext_noop(
"wait operating system loading"), interruptible=True)
register_operation(RebootOperation) register_operation(RebootOperation)
...@@ -497,8 +506,11 @@ class ResetOperation(InstanceOperation): ...@@ -497,8 +506,11 @@ class ResetOperation(InstanceOperation):
required_perms = () required_perms = ()
accept_states = ('RUNNING', ) accept_states = ('RUNNING', )
def _operation(self, timeout=5): def _operation(self, activity, timeout=5):
self.instance.reset_vm(timeout=timeout) self.instance.reset_vm(timeout=timeout)
if self.instance.has_agent:
activity.sub_activity('os_boot', readable_name=ugettext_noop(
"wait operating system loading"), interruptible=True)
register_operation(ResetOperation) register_operation(ResetOperation)
...@@ -619,6 +631,17 @@ class ShutdownOperation(InstanceOperation): ...@@ -619,6 +631,17 @@ class ShutdownOperation(InstanceOperation):
self.instance.yield_node() self.instance.yield_node()
self.instance.yield_vnc_port() self.instance.yield_vnc_port()
def on_abort(self, activity, error):
if isinstance(error, TimeLimitExceeded):
activity.result = humanize_exception(ugettext_noop(
"The virtual machine did not switch off in the provided time "
"limit. Most of the time this is caused by incorrect ACPI "
"settings. You can also try to power off the machine from the "
"operating system manually."), error)
activity.resultant_state = None
else:
super(ShutdownOperation, self).on_abort(activity, error)
register_operation(ShutdownOperation) register_operation(ShutdownOperation)
...@@ -716,7 +739,10 @@ class WakeUpOperation(InstanceOperation): ...@@ -716,7 +739,10 @@ class WakeUpOperation(InstanceOperation):
return self.instance.status == self.instance.STATUS.SUSPENDED return self.instance.status == self.instance.STATUS.SUSPENDED
def on_abort(self, activity, error): def on_abort(self, activity, error):
activity.resultant_state = 'ERROR' if isinstance(error, SchedulerError):
activity.resultant_state = None
else:
activity.resultant_state = 'ERROR'
def _operation(self, activity, timeout=60): def _operation(self, activity, timeout=60):
# Schedule vm # Schedule vm
...@@ -1003,12 +1029,12 @@ class MountStoreOperation(EnsureAgentMixin, InstanceOperation): ...@@ -1003,12 +1029,12 @@ class MountStoreOperation(EnsureAgentMixin, InstanceOperation):
except NoStoreException: except NoStoreException:
raise PermissionDenied # not show the button at all raise PermissionDenied # not show the button at all
def _operation(self): def _operation(self, user):
inst = self.instance inst = self.instance
queue = self.instance.get_remote_queue_name("agent") queue = self.instance.get_remote_queue_name("agent")
host = urlsplit(settings.STORE_URL).hostname host = urlsplit(settings.STORE_URL).hostname
username = Store(inst.owner).username username = Store(user).username
password = inst.owner.profile.smb_password password = user.profile.smb_password
agent_tasks.mount_store.apply_async( agent_tasks.mount_store.apply_async(
queue=queue, args=(inst.vm_name, host, username, password)) queue=queue, args=(inst.vm_name, host, username, password))
......
...@@ -76,3 +76,8 @@ def get_keys(vm): ...@@ -76,3 +76,8 @@ def get_keys(vm):
@celery.task(name='agent.send_expiration') @celery.task(name='agent.send_expiration')
def send_expiration(vm, url): def send_expiration(vm, url):
pass pass
@celery.task(name='agent.change_ip')
def change_ip(vm, interfaces, dns):
pass
...@@ -19,7 +19,9 @@ from common.models import create_readable ...@@ -19,7 +19,9 @@ from common.models import create_readable
from manager.mancelery import celery from manager.mancelery import celery
from vm.tasks.agent_tasks import (restart_networking, change_password, from vm.tasks.agent_tasks import (restart_networking, change_password,
set_time, set_hostname, start_access_server, set_time, set_hostname, start_access_server,
cleanup, update) cleanup, update, change_ip)
from firewall.models import Host
import time import time
from base64 import encodestring from base64 import encodestring
from StringIO import StringIO from StringIO import StringIO
...@@ -31,13 +33,11 @@ from celery.result import TimeoutError ...@@ -31,13 +33,11 @@ from celery.result import TimeoutError
from monitor.client import Client from monitor.client import Client
def send_init_commands(instance, act, vm): def send_init_commands(instance, act):
vm = instance.vm_name
queue = instance.get_remote_queue_name("agent") queue = instance.get_remote_queue_name("agent")
with act.sub_activity('cleanup', readable_name=ugettext_noop('cleanup')): with act.sub_activity('cleanup', readable_name=ugettext_noop('cleanup')):
cleanup.apply_async(queue=queue, args=(vm, )) cleanup.apply_async(queue=queue, args=(vm, ))
with act.sub_activity('restart_networking',
readable_name=ugettext_noop('restart networking')):
restart_networking.apply_async(queue=queue, args=(vm, ))
with act.sub_activity('change_password', with act.sub_activity('change_password',
readable_name=ugettext_noop('change password')): readable_name=ugettext_noop('change password')):
change_password.apply_async(queue=queue, args=(vm, instance.pw)) change_password.apply_async(queue=queue, args=(vm, instance.pw))
...@@ -46,7 +46,18 @@ def send_init_commands(instance, act, vm): ...@@ -46,7 +46,18 @@ def send_init_commands(instance, act, vm):
with act.sub_activity('set_hostname', with act.sub_activity('set_hostname',
readable_name=ugettext_noop('set hostname')): readable_name=ugettext_noop('set hostname')):
set_hostname.apply_async( set_hostname.apply_async(
queue=queue, args=(vm, instance.primary_host.hostname)) queue=queue, args=(vm, instance.short_hostname))
def send_networking_commands(instance, act):
queue = instance.get_remote_queue_name("agent")
with act.sub_activity('change_ip',
readable_name=ugettext_noop('change ip')):
change_ip.apply_async(queue=queue, args=(
instance.vm_name, ) + get_network_configs(instance))
with act.sub_activity('restart_networking',
readable_name=ugettext_noop('restart networking')):
restart_networking.apply_async(queue=queue, args=(instance.vm_name, ))
def create_agent_tar(): def create_agent_tar():
...@@ -74,16 +85,22 @@ def agent_started(vm, version=None): ...@@ -74,16 +85,22 @@ def agent_started(vm, version=None):
from vm.models import Instance, instance_activity, InstanceActivity from vm.models import Instance, instance_activity, InstanceActivity
instance = Instance.objects.get(id=int(vm.split('-')[-1])) instance = Instance.objects.get(id=int(vm.split('-')[-1]))
queue = instance.get_remote_queue_name("agent") queue = instance.get_remote_queue_name("agent")
initialized = InstanceActivity.objects.filter( initialized = instance.activity_log.filter(
instance=instance, activity_code='vm.Instance.agent.cleanup').exists() activity_code='vm.Instance.agent.cleanup').exists()
with instance_activity(code_suffix='agent', with instance_activity(code_suffix='agent',
readable_name=ugettext_noop('agent'), readable_name=ugettext_noop('agent'),
concurrency_check=False,
instance=instance) as act: instance=instance) as act:
with act.sub_activity('starting', with act.sub_activity('starting',
readable_name=ugettext_noop('starting')): readable_name=ugettext_noop('starting')):
pass pass
for i in InstanceActivity.objects.filter(
instance=instance, activity_code__endswith='.os_boot',
finished__isnull=True):
i.finish(True)
if version and version != settings.AGENT_VERSION: if version and version != settings.AGENT_VERSION:
try: try:
update_agent(instance, act) update_agent(instance, act)
...@@ -94,12 +111,12 @@ def agent_started(vm, version=None): ...@@ -94,12 +111,12 @@ def agent_started(vm, version=None):
if not initialized: if not initialized:
measure_boot_time(instance) measure_boot_time(instance)
send_init_commands(instance, act, vm) send_init_commands(instance, act)
with act.sub_activity( send_networking_commands(instance, act)
'start_access_server', with act.sub_activity('start_access_server',
readable_name=ugettext_noop('start access server') readable_name=ugettext_noop(
): 'start access server')):
start_access_server.apply_async(queue=queue, args=(vm, )) start_access_server.apply_async(queue=queue, args=(vm, ))
...@@ -134,6 +151,13 @@ def agent_stopped(vm): ...@@ -134,6 +151,13 @@ def agent_stopped(vm):
pass pass
def get_network_configs(instance):
interfaces = {}
for host in Host.objects.filter(interface__instance=instance):
interfaces[str(host.mac)] = host.get_network_config()
return (interfaces, settings.FIREWALL_SETTINGS['rdns_ip'])
def update_agent(instance, act=None): def update_agent(instance, act=None):
if act: if act:
act = act.sub_activity( act = act.sub_activity(
......
...@@ -217,6 +217,8 @@ class InstanceActivityTestCase(TestCase): ...@@ -217,6 +217,8 @@ class InstanceActivityTestCase(TestCase):
def test_create_concurrency_check(self): def test_create_concurrency_check(self):
instance = MagicMock(spec=Instance) instance = MagicMock(spec=Instance)
instance.activity_log.filter.return_value.__iter__.return_value = iter(
[MagicMock(spec=InstanceActivity, interruptible=False)])
instance.activity_log.filter.return_value.exists.return_value = True instance.activity_log.filter.return_value.exists.return_value = True
with self.assertRaises(ActivityInProgressError): with self.assertRaises(ActivityInProgressError):
......
...@@ -14,4 +14,3 @@ post-stop script ...@@ -14,4 +14,3 @@ post-stop script
stop mancelery stop mancelery
stop slowcelery stop slowcelery
end script end script
...@@ -12,4 +12,3 @@ script ...@@ -12,4 +12,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate . /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 10 exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 10
end script end script
...@@ -12,4 +12,3 @@ script ...@@ -12,4 +12,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate . /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.moncelery worker --autoreload --loglevel=info --hostname=moncelery -B -c 3 exec ./manage.py celery --app=manager.moncelery worker --autoreload --loglevel=info --hostname=moncelery -B -c 3
end script end script
...@@ -12,4 +12,3 @@ script ...@@ -12,4 +12,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate . /home/cloud/.virtualenvs/circle/bin/postactivate
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 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
end script end script
...@@ -14,4 +14,3 @@ script ...@@ -14,4 +14,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate . /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py runserver '[::]:8080' exec ./manage.py runserver '[::]:8080'
end script end script
...@@ -12,4 +12,3 @@ script ...@@ -12,4 +12,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate . /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.slowcelery worker --autoreload --loglevel=info --hostname=slowcelery -B -c 5 exec ./manage.py celery --app=manager.slowcelery worker --autoreload --loglevel=info --hostname=slowcelery -B -c 5
end script end script
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