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) ^
(getnode() % 983)) & 0xffff)
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):
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()
def compute_cached(method, instance, memcached_seconds,
......@@ -488,7 +496,7 @@ class HumanReadableException(HumanReadableObject, Exception):
"Level should be the name of an attribute of django."
"contrib.messages (and it should be callable with "
"(request, message)). Like 'error', 'warning'.")
else:
elif not hasattr(self, "level"):
self.level = "error"
def send_message(self, request, level=None):
......
......@@ -21,18 +21,22 @@ from django import contrib
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib.auth.models import User, Group
from dashboard.models import Profile, GroupProfile
from dashboard.models import Profile, GroupProfile, ConnectCommand
class ProfileInline(contrib.admin.TabularInline):
model = Profile
class CommandInline(contrib.admin.TabularInline):
model = ConnectCommand
class GroupProfileInline(contrib.admin.TabularInline):
model = GroupProfile
UserAdmin.inlines = (ProfileInline, )
UserAdmin.inlines = (ProfileInline, CommandInline, )
GroupAdmin.inlines = (GroupProfileInline, )
contrib.admin.site.unregister(User)
......
import autocomplete_light
from django.contrib.auth.models import User
from django.utils.html import escape
from django.utils.translation import ugettext as _
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 = (
('^first_name', 'last_name', 'username', '^email', 'profile__org_id'),
('^name', 'groupprofile__org_id'),
('first_name', 'last_name', 'username', 'email', 'profile__org_id'),
('name', 'groupprofile__org_id'),
)
autocomplete_js_attributes = {'placeholder': _("Name of group or user")}
choice_html_format = u'<span data-value="%s"><span>%s</span> %s</span>'
choice_html_format = (u'<span data-value="%s"><span style="display:none"'
u'>%s</span>%s</span>')
def choice_html(self, choice):
def choice_displayed_text(self, choice):
q = unicode(self.request.GET.get('q', ''))
name = highlight(unicode(choice), q, False)
if isinstance(choice, User):
extra_fields = [highlight(choice.get_full_name(), q, False),
highlight(choice.email, q)]
try:
name = choice.get_full_name()
except AttributeError:
name = _('group')
if name:
name = u'(%s)' % name
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 % (
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):
user = self.request.user
self.choices = (AclUpdateView.get_allowed_users(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)
......@@ -1383,7 +1383,6 @@
"time_of_suspend": null,
"ram_size": 200,
"priority": 10,
"active_since": null,
"template": null,
"access_method": "nx",
"lease": 1,
......@@ -1413,7 +1412,6 @@
"time_of_suspend": null,
"ram_size": 200,
"priority": 10,
"active_since": null,
"template": null,
"access_method": "nx",
"lease": 1,
......
......@@ -54,7 +54,9 @@ from .models import Profile, GroupProfile
from circle.settings.base import LANGUAGES, MAX_NODE_RAM
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], ")"))
for l in LANGUAGES)
......@@ -141,6 +143,8 @@ class VmCustomizeForm(forms.Form):
self.template = kwargs.pop("template", None)
super(VmCustomizeForm, self).__init__(*args, **kwargs)
if self.user.has_perm("vm.set_resources"):
self.allowed_fields = tuple(self.fields.keys())
# set displayed disk and network list
self.fields['disks'].queryset = self.template.disks.all()
self.fields['networks'].queryset = Vlan.get_objects_with_level(
......@@ -156,10 +160,29 @@ class VmCustomizeForm(forms.Form):
self.initial['cpu_count'] = self.template.num_cores
self.initial['ram_size'] = self.template.ram_size
else:
self.allowed_fields = ("name", "template", "customized", )
# initial name and template pk
self.initial['name'] = self.template.name
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):
......@@ -176,7 +199,14 @@ class GroupCreateForm(forms.ModelForm):
self.fields['org_id'] = forms.ChoiceField(
# TRANSLATORS: directory like in LDAP
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()
def save(self, commit=True):
......@@ -451,7 +481,7 @@ class TemplateForm(forms.ModelForm):
else:
self.allowed_fields = (
'name', 'access_method', 'description', 'system', 'tags',
'arch', 'lease')
'arch', 'lease', 'has_agent')
if (self.user.has_perm('vm.change_template_resources')
or not self.instance.pk):
self.allowed_fields += tuple(set(self.fields.keys()) -
......@@ -1025,9 +1055,29 @@ class UserCreationForm(OrgUserCreationForm):
return user
class AclUserAddForm(forms.Form):
class AclUserOrGroupAddForm(forms.Form):
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):
......@@ -1057,6 +1107,22 @@ class UserKeyForm(forms.ModelForm):
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 Meta:
......@@ -1174,7 +1240,12 @@ class VmListSearchForm(forms.Form):
}))
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):
......@@ -1184,3 +1255,22 @@ class VmListSearchForm(forms.Form):
data = self.data.copy()
data['stype'] = "all"
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
from common.models import HumanReadableObject, create_readable, Encoder
from vm.tasks.agent_tasks import add_keys, del_keys
from vm.models.instance import ACCESS_METHODS
from .store_api import Store, NoStoreException, NotOkException
from .store_api import Store, NoStoreException, NotOkException, Timeout
from .validators import connect_command_template_validator
logger = getLogger(__name__)
......@@ -100,6 +102,25 @@ class Notification(TimeStampedModel):
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):
user = OneToOneField(User)
preferred_language = CharField(verbose_name=_('preferred language'),
......@@ -129,6 +150,25 @@ class Profile(Model):
default=2048 * 1024 * 1024,
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,
**kwargs):
if context is not None:
......@@ -161,6 +201,11 @@ class Profile(Model):
def __unicode__(self):
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:
permissions = (
('use_autocomplete', _('Can use autocomplete.')),
......@@ -216,7 +261,7 @@ def get_or_create_profile(self):
Group.profile = property(get_or_create_profile)
def create_profile(sender, user, request, **kwargs):
def create_profile(user):
if not user.pk:
return False
profile, created = Profile.objects.get_or_create(user=user)
......@@ -227,7 +272,11 @@ def create_profile(sender, user, request, **kwargs):
logger.exception("Can't create user %s", unicode(user))
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'):
logger.debug("Register save_org_id to djangosaml2 pre_user_save")
......@@ -301,7 +350,7 @@ def update_store_profile(sender, **kwargs):
profile.disk_quota)
except NoStoreException:
logger.debug("Store is not available.")
except NotOkException:
except (NotOkException, Timeout):
logger.critical("Store is not accepting connections.")
......
......@@ -591,11 +591,15 @@ footer a, footer a:hover, footer a:visited {
width: 100px;
}
#group-detail-user-table tr:last-child td:nth-child(2) {
text-align: left;
}
#group-detail-perm-header {
margin-top: 25px;
}
textarea[name="list-new-namelist"] {
textarea[name="new_members"] {
max-width: 500px;
min-height: 80px;
margin-bottom: 10px;
......@@ -654,7 +658,8 @@ textarea[name="list-new-namelist"] {
width: 130px;
}
#vm-details-connection-string-copy {
.vm-details-connection-string-copy,
#vm-details-pw-show {
cursor: pointer;
}
......@@ -681,10 +686,9 @@ textarea[name="list-new-namelist"] {
max-width: 200px;
}
#dashboard-vm-details-connect-command {
.dashboard-vm-details-connect-command {
/* for mobile view */
margin-bottom: 20px;
}
#store-list-list {
......@@ -868,6 +872,12 @@ textarea[name="list-new-namelist"] {
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 {
-webkit-animation: passing 2s linear infinite;
......@@ -982,3 +992,26 @@ textarea[name="list-new-namelist"] {
.slider {
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 () {
var search_result = [];
var html = '';
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]);
}
}
......@@ -383,6 +383,19 @@ $(function () {
$('.notification-messages').load("/dashboard/notifications/");
$('#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) {
......@@ -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
/**
* Getter for user cookies
......@@ -613,9 +632,25 @@ function getCookie(name) {
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 */
function noJS() {
$('.no-js-hidden').show();
$('.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() {
$(".template-list-table thead th").css("cursor", "pointer");
$(".template-list-table th a").on("click", function(event) {
if(!$(this).closest("th").data("sort")) return true;
event.preventDefault();
});
});
......
......@@ -222,6 +222,8 @@ function vmCustomizeLoaded() {
$(this).find("i").prop("class", "fa fa-spinner fa-spin");
if($("#create-modal")) return true;
$.ajax({
url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')},
......
......@@ -106,19 +106,20 @@ $(function() {
$("#vm-details-pw-show").click(function() {
var input = $(this).parent("div").children("input");
var eye = $(this).children("#vm-details-pw-eye");
var span = $(this);
eye.tooltip("destroy");
span.tooltip("destroy")
if(eye.hasClass("fa-eye")) {
eye.removeClass("fa-eye").addClass("fa-eye-slash");
input.prop("type", "text");
input.focus();
eye.prop("title", "Hide password");
input.select();
span.prop("title", gettext("Hide password"));
} else {
eye.removeClass("fa-eye-slash").addClass("fa-eye");
input.prop("type", "password");
eye.prop("title", "Show password");
span.prop("title", gettext("Show password"));
}
eye.tooltip();
span.tooltip();
});
/* change password confirmation */
......@@ -199,7 +200,7 @@ $(function() {
$("#vm-details-h1-name, .vm-details-rename-button").click(function() {
$("#vm-details-h1-name").hide();
$("#vm-details-rename").css('display', 'inline');
$("#vm-details-rename-name").focus();
$("#vm-details-rename-name").select();
return false;
});
......@@ -207,7 +208,7 @@ $(function() {
$(".vm-details-home-edit-name-click").click(function() {
$(".vm-details-home-edit-name-click").hide();
$("#vm-details-home-rename").show();
$("input", $("#vm-details-home-rename")).focus();
$("input", $("#vm-details-home-rename")).select();
return false;
});
......@@ -307,8 +308,8 @@ $(function() {
});
// select connection string
$("#vm-details-connection-string-copy").click(function() {
$("#vm-details-connection-string").focus();
$(".vm-details-connection-string-copy").click(function() {
$(this).parent("div").find("input").select();
});
$("a.operation-password_reset").click(function() {
......@@ -378,8 +379,14 @@ function checkNewActivity(runs) {
}
$("#vm-details-state span").html(data.human_readable_status.toUpperCase());
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");
} 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");
}
......
......@@ -163,9 +163,10 @@ $(function() {
$(this).find('input[type="radio"]').prop("checked", true);
});
if(checkStatusUpdate()) {
if(checkStatusUpdate() || $("#vm-list-table tbody tr").length >= 100) {
updateStatuses(1);
}
});
......@@ -178,6 +179,7 @@ function checkStatusUpdate() {
function updateStatuses(runs) {
var include_deleted = getParameterByName("include_deleted");
$.get("/dashboard/vm/list/?compact", function(result) {
$("#vm-list-table tbody tr").each(function() {
vm = $(this).data("vm-pk");
......@@ -203,6 +205,7 @@ function updateStatuses(runs) {
$(this).find(".node").text(result[vm].node);
}
} else {
if(!include_deleted)
$(this).remove();
}
});
......
......@@ -7,6 +7,7 @@ from datetime import datetime
from django.http import Http404
from django.conf import settings
from requests import get, post, codes
from requests.exceptions import Timeout # noqa
from sizefield.utils import filesizeformat
logger = logging.getLogger(__name__)
......
......@@ -25,6 +25,7 @@ from django_tables2.columns import (TemplateColumn, Column, BooleanColumn,
from vm.models import Node, InstanceTemplate, Lease
from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey
from dashboard.models import ConnectCommand
class NodeListTable(Table):
......@@ -146,13 +147,11 @@ class TemplateListTable(Table):
template_name="dashboard/template-list/column-template-name.html",
attrs={'th': {'data-sort': "string"}}
)
num_cores = Column(
verbose_name=_("Cores"),
attrs={'th': {'data-sort': "int"}}
)
ram_size = TemplateColumn(
"{{ record.ram_size }} MiB",
resources = TemplateColumn(
template_name="dashboard/template-list/column-template-resources.html",
verbose_name=_("Resources"),
attrs={'th': {'data-sort': "int"}},
order_by=("ram_size"),
)
lease = TemplateColumn(
"{{ record.lease.name }}",
......@@ -170,11 +169,14 @@ class TemplateListTable(Table):
verbose_name=_("Owner"),
attrs={'th': {'data-sort': "string"}}
)
created = TemplateColumn(
template_name="dashboard/template-list/column-template-created.html",
verbose_name=_("Created at"),
)
running = TemplateColumn(
template_name="dashboard/template-list/column-template-running.html",
verbose_name=_("Running"),
attrs={'th': {'data-sort': "int"}},
orderable=False,
)
actions = TemplateColumn(
verbose_name=_("Actions"),
......@@ -187,8 +189,8 @@ class TemplateListTable(Table):
model = InstanceTemplate
attrs = {'class': ('table table-bordered table-striped table-hover'
' template-list-table')}
fields = ('name', 'num_cores', 'ram_size', 'system',
'access_method', 'lease', 'owner', 'running', 'actions', )
fields = ('name', 'resources', 'system', 'access_method', 'lease',
'owner', 'created', 'running', 'actions', )
prefix = "template-"
......@@ -220,6 +222,7 @@ class LeaseListTable(Table):
fields = ('name', 'suspend_interval_seconds',
'delete_interval_seconds', )
prefix = "lease-"
empty_text = _("No available leases.")
class UserKeyListTable(Table):
......@@ -248,5 +251,41 @@ class UserKeyListTable(Table):
class Meta:
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')
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 %}
{% if user and user.pk %}
{% if user.get_full_name %}
{{ user.get_full_name }}
{% else %}
{{ user.username }}
{% endif %}
{% if user.get_full_name %}{{ user.get_full_name }}{% else %}{{ user.username }}{% endif %}{% if new_line %}<br />{% endif %}
{% if show_org %}
{% if user.profile and user.profile.org_id %}
......
......@@ -2,6 +2,12 @@
<div class="modal fade" id="confirmation-modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<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">
{% if template %}
{% include template %}
......
......@@ -3,10 +3,11 @@
{% load sizefieldtags %}
{% include "display-form-errors.html" with form=vm_create_form %}
<form method="POST">
<form method="POST" action="{% url "dashboard.views.vm-create" %}">
{% csrf_token %}
{{ vm_create_form.template }}
{{ vm_create_form.customized }}
<div class="row">
<div class="col-sm-12">
......@@ -23,7 +24,6 @@
</div>
{% if perms.vm.set_resources %}
{{ vm_create_form.customized }}
<div class="row">
<div class="col-sm-10">
<div class="form-group">
......
......@@ -10,8 +10,9 @@
</h3>
</div>
<div class="panel-body">
{% blocktrans with owner=instance.owner fqdn=instance.primary_host %}
{{ owner }} offered to take the ownership of virtual machine {{fqdn}}.
{% blocktrans with owner=instance.owner name=instance.name id=instance.id%}
<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?
{% endblocktrans %}
<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 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" %}">
{% csrf_token %}
......
......@@ -89,13 +89,12 @@
<tr>
<td><i class="fa fa-plus"></i></td>
<td colspan="2">
<input type="text" class="form-control" name="list-new-name"
placeholder="{% trans "Name of user" %}">
{{addmemberform.new_member}}
</td>
</tr>
</tbody>
</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>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
......
......@@ -25,7 +25,7 @@
<i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i>
{{ i.name }}
</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 }}">
{% if i.fav %}
<i class="fa fa-star text-primary title-favourite" title="{% trans "Unfavourite" %}"></i>
......
......@@ -10,9 +10,9 @@
</h1>
</div>
<div class="row">
<div class="col-md-5" id="vm-info-pane">
<div class="big">
<span id="vm-activity-state" class="label label-{% if object.get_status_id == 'wait' %}info{% else %}{% if object.succeeded %}success{% else %}error{% endif %}{% endif %}">
<div class="col-md-6" id="vm-info-pane">
<div class="big" id="vm-activity-state">
<span class="label label-{% if object.get_status_id == 'wait' %}info{% else %}{% if object.succeeded %}success{% else %}danger{% endif %}{% endif %}">
<span>{{ object.get_status_id|upper }}</span>
</span>
</div>
......@@ -20,7 +20,7 @@
{% include "dashboard/vm-detail/_activity-timeline.html" with active=object %}
</div>
<div class="col-md-7">
<div class="col-md-6">
<div class="panel panel-default">
<!--<div class="panel-heading"><h2 class="panel-title">{% trans "Activity" %}</h2></div> -->
<div class="panel-body">
......
......@@ -11,6 +11,12 @@
<div class="col-md-12">
<div class="panel panel-default">
<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>
<h3 class="no-margin">
<i class="fa fa-user"></i>
......
......@@ -66,4 +66,20 @@
</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 %}
......@@ -52,6 +52,7 @@
{{ form.req_traits|as_crispy_field }}
{{ form.description|as_crispy_field }}
{{ form.system|as_crispy_field }}
{{ form.has_agent|as_crispy_field }}
</fieldset>
<fieldset>
<legend>{% trans "External resources" %}</legend>
......
......@@ -17,18 +17,37 @@
<h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %}</h3>
</div>
<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 %}
</div>
</div>
</div>
</div>
{% if show_lease_table %}
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
{% 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" %}
</a>
{% endif %}
......@@ -55,6 +74,7 @@
</div>
{% endcomment %}
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
......
{% 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" %}">
<i class="fa fa-edit"></i>
</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">
{{ record.get_running_instances.count }}
{{ record.running }}
</a>
......@@ -100,8 +100,9 @@
<div class="input-group">
<input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags"
value="{{ instance.pw }}" spellcheck="false"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show">
<i class="fa fa-eye" id="vm-details-pw-eye" title="Show password"></i>
<span class="input-group-addon input-tags" id="vm-details-pw-show"
title="{% trans "Show password" %}" data-container="body">
<i class="fa fa-eye" id="vm-details-pw-eye"></i>
</span>
</div>
</dd>
......@@ -113,16 +114,42 @@
</div>
</dd>
</dl>
<div class="input-group" id="dashboard-vm-details-connect-command">
{% for c in connect_commands %}
<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="{% 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" />
<span class="input-group-addon input-tags" id="vm-details-connection-string-copy">
<i class="fa fa-copy" title="{% trans "Select all" %}"></i>
</span>
</div>
{% endfor %}
{% if instance.get_connect_uri %}
<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 class="col-md-8" id="vm-detail-pane">
<div class="panel panel-default" id="vm-detail-panel">
......
......@@ -9,8 +9,10 @@
{% endblocktrans %}
{% endif %}
{% if user == instance.owner or user.is_superuser %}
<span class="operation-wrapper">
<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 %}
</p>
<h3>{% trans "Permissions"|capfirst %}</h3>
......
......@@ -94,7 +94,13 @@
<dt>{% trans "Template" %}:</dt>
<dd>
{% if instance.template %}
{% if can_link_template %}
<a href="{{ instance.template.get_absolute_url }}">
{{ instance.template.name }}
</a>
{% else %}
{{ instance.template.name }}
{% endif %}
{% else %}
-
{% endif %}
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{% trans "Transfer ownership" %}
</h3>
</div>
<div class="panel-body">
<div class="pull-right">
<form action="" method="POST">
<div class="pull-right">
<form action="{% url "dashboard.views.vm-transfer-ownership" pk=instance.pk %}" method="POST" style="max-width: 400px;">
{% csrf_token %}
<label>
{% trans "E-mail address or identifier of user" %}:
<input name="name">
{{ form.name.label }}
</label>
<input type="submit">
</form>
<div class="input-group">
{{form.name}}
<div class="input-group-btn">
<input type="submit" value="{% trans "Save" %}" class="btn btn-primary">
</div>
</div>
</div>
{% endblock %}
</form>
</div>
......@@ -34,6 +34,12 @@
{{ search_form.s }}
<div class="input-group-btn">
{{ search_form.stype }}
</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>
</button>
......@@ -63,10 +69,20 @@
{% trans "Owner" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="owner" %}
</th>
{% if user.is_superuser %}<th data-sort="string" class="orderable sortable">
<th data-sort="string" class="orderable sortable">
{% trans "Lease" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="lease" %}
</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 %}
</th>
{% endif %}
</tr></thead><tbody>
{% for i in object_list %}
<tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}">
......@@ -74,16 +90,27 @@
<td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">{{ i.name }}</a> </td>
<td class="state">
<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
{% else %}
{{ i.get_status_icon }}{% endif %}"></i>
<span>{{ i.get_status_display }}</span>
</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>
{% 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 }}">
{{ i.node.name|default:"-" }}
</td>
......@@ -91,7 +118,7 @@
</tr>
{% empty %}
<tr>
<td colspan="5">
<td colspan="7">
{% if request.GET.s %}
<strong>{% trans "No result." %}</strong>
{% 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):
c = Client()
user_in_group = self.g1.user_set.count()
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(response.status_code, 302)
......@@ -1144,7 +1144,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'user3')
user_in_group = self.g1.user_set.count()
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(response.status_code, 403)
......@@ -1153,7 +1153,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'superuser')
user_in_group = self.g1.user_set.count()
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(response.status_code, 302)
......@@ -1162,7 +1162,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'user0')
user_in_group = self.g1.user_set.count()
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(response.status_code, 302)
......@@ -1172,7 +1172,7 @@ class GroupDetailTest(LoginMixin, TestCase):
user_in_group = self.g1.user_set.count()
response = c.post('/dashboard/group/' +
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(response.status_code, 302)
......@@ -1182,7 +1182,7 @@ class GroupDetailTest(LoginMixin, TestCase):
user_in_group = self.g1.user_set.count()
response = c.post('/dashboard/group/' +
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(response.status_code, 302)
......@@ -1192,7 +1192,7 @@ class GroupDetailTest(LoginMixin, TestCase):
user_in_group = self.g1.user_set.count()
response = c.post('/dashboard/group/' +
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(response.status_code, 403)
......@@ -1201,7 +1201,7 @@ class GroupDetailTest(LoginMixin, TestCase):
user_in_group = self.g1.user_set.count()
response = c.post('/dashboard/group/' +
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(response.status_code, 302)
......@@ -1471,8 +1471,8 @@ class TransferOwnershipViewTest(LoginMixin, TestCase):
c2 = self.u2.notification_set.count()
c = Client()
self.login(c, 'user2')
response = c.post('/dashboard/vm/1/tx/')
assert response.status_code == 400
response = c.post('/dashboard/vm/1/tx/', {'name': 'userx'})
assert response.status_code == 403
self.assertEqual(self.u2.notification_set.count(), c2)
def test_owned_offer(self):
......
......@@ -39,11 +39,13 @@ from .views import (
get_vm_screenshot,
ProfileView, toggle_use_gravatar, UnsubscribeFormView,
UserKeyDelete, UserKeyDetail, UserKeyCreate,
ConnectCommandDelete, ConnectCommandDetail, ConnectCommandCreate,
StoreList, store_download, store_upload, store_get_upload_url, StoreRemove,
store_new_directory, store_refresh_toplist,
VmTraitsUpdate, VmRawDataUpdate,
GroupPermissionsView,
LeaseAclUpdateView,
ClientCheck, TokenLogin,
)
autocomplete_light.autodiscover()
......@@ -177,6 +179,16 @@ urlpatterns = patterns(
UserKeyCreate.as_view(),
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"^store/list/$", StoreList.as_view(),
......@@ -193,4 +205,8 @@ urlpatterns = patterns(
name="dashboard.views.store-new-directory"),
url(r"^store/refresh_toplist$", 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.utils.translation import ugettext_lazy as _
from lxml import etree as ET
import logging
......@@ -29,3 +31,27 @@ def domain_validator(value):
relaxng.assertValid(parsed_xml)
except Exception as e:
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):
dst = None
if host:
ip = (host.ipv4, host.ipv6_with_prefixlen)
ip = (host.ipv4, host.ipv6_with_host_prefixlen)
if self.direction == 'in':
dst = ip
else:
......@@ -530,14 +530,30 @@ class Host(models.Model):
def incoming_rules(self):
return self.rules.filter(direction='in')
@property
def ipv6_with_prefixlen(self):
@staticmethod
def create_ipnetwork(ip, prefixlen):
try:
net = IPNetwork(self.ipv6)
net.prefixlen = self.vlan.host_ipv6_prefixlen
return net
net = IPNetwork(ip)
net.prefixlen = prefixlen
except TypeError:
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):
return self.external_ipv4 if self.external_ipv4 else self.ipv4
......@@ -600,6 +616,19 @@ class Host(models.Model):
description='created by host.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):
for i in settings.get('default_host_groups', []):
self.groups.add(Group.objects.get(name=i))
......
......@@ -18,26 +18,37 @@
from logging import getLogger
from django.db.models import Sum
from django.utils.translation import ugettext_noop
from common.models import HumanReadableException
logger = getLogger(__name__)
class NotEnoughMemoryException(Exception):
class SchedulerError(HumanReadableException):
admin_message = None
def __init__(self, message=None):
if message is None:
message = "No node has enough memory to accomodate the guest."
def __init__(self, params=None, level=None, **kwargs):
kwargs.update(params or {})
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):
......@@ -77,18 +88,26 @@ def has_enough_ram(ram_size, node):
"""True, if the node has enough memory to accomodate a guest requiring
ram_size mebibytes of memory; otherwise, false.
"""
ram_size = ram_size * 1024 * 1024
try:
total = node.ram_size
used = (node.ram_usage / 100) * total
used = node.byte_ram_usage
unused = total - used
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
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:
logger.warning('Got incorrect monitoring data for node %s. %s',
logger.exception('Got incorrect monitoring data for node %s. %s',
unicode(node), unicode(e))
return False
......
......@@ -17,6 +17,7 @@
from django.forms import ModelForm
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset, Div, Submit, BaseInput
......@@ -56,8 +57,9 @@ class BlacklistItemForm(ModelForm):
)
),
FormActions(
Submit('submit', 'Save changes'),
LinkButton('back', 'Back', reverse_lazy('network.blacklist_list'))
Submit('submit', _('Save changes')),
LinkButton('back', _("Back"),
reverse_lazy('network.blacklist_list'))
)
)
......@@ -77,8 +79,8 @@ class DomainForm(ModelForm):
),
),
FormActions(
Submit('submit', 'Save'),
LinkButton('back', 'Back', reverse_lazy('network.domain_list'))
Submit('submit', _('Save')),
LinkButton('back', _("Back"), reverse_lazy('network.domain_list'))
)
)
......@@ -91,15 +93,15 @@ class GroupForm(ModelForm):
helper.layout = Layout(
Div(
Fieldset(
'Identity',
'',
'name',
'description',
'owner',
),
),
FormActions(
Submit('submit', 'Save'),
LinkButton('back', 'Back', reverse_lazy('network.group_list'))
Submit('submit', _('Save')),
LinkButton('back', _("Back"), reverse_lazy('network.group_list'))
)
)
......@@ -112,13 +114,13 @@ class HostForm(ModelForm):
helper.layout = Layout(
Div(
Fieldset(
'Identity',
'',
'hostname',
'reverse',
'mac',
),
Fieldset(
'Network',
_('Network'),
'vlan',
'ipv4',
'ipv6',
......@@ -126,7 +128,7 @@ class HostForm(ModelForm):
'external_ipv4',
),
Fieldset(
'Information',
_('Information'),
'description',
'location',
'comment',
......@@ -134,8 +136,8 @@ class HostForm(ModelForm):
),
),
FormActions(
Submit('submit', 'Save'),
LinkButton('back', 'Back', reverse_lazy('network.host_list')))
Submit('submit', _('Save')),
LinkButton('back', _('Back'), reverse_lazy('network.host_list')))
)
class Meta:
......@@ -159,8 +161,8 @@ class RecordForm(ModelForm):
)
),
FormActions(
Submit('submit', 'Save'),
LinkButton('back', 'Back', reverse_lazy('network.record_list'))
Submit('submit', _("Save")),
LinkButton('back', _("Back"), reverse_lazy('network.record_list'))
)
)
......@@ -173,7 +175,7 @@ class RuleForm(ModelForm):
helper.layout = Layout(
Div(
Fieldset(
'Identity',
'',
'direction',
'description',
'foreign_network',
......@@ -189,7 +191,7 @@ class RuleForm(ModelForm):
'nat_external_ipv4',
),
Fieldset(
'External',
_('External'),
'vlan',
'vlangroup',
'host',
......@@ -198,8 +200,8 @@ class RuleForm(ModelForm):
)
),
FormActions(
Submit('submit', 'Save'),
LinkButton('back', 'Back', reverse_lazy('network.rule_list'))
Submit('submit', _("Save")),
LinkButton('back', _("Back"), reverse_lazy('network.rule_list'))
)
)
......@@ -219,8 +221,8 @@ class SwitchPortForm(ModelForm):
)
),
FormActions(
Submit('submit', 'Save'),
LinkButton('back', 'Back',
Submit('submit', _("Save")),
LinkButton('back', _("Back"),
reverse_lazy('network.switch_port_list'))
)
)
......@@ -234,41 +236,42 @@ class VlanForm(ModelForm):
helper.layout = Layout(
Div(
Fieldset(
'Identity',
'',
'name',
'vid',
'network_type',
'managed',
),
Fieldset(
'IPv4',
_('IPv4'),
'network4',
'snat_to',
'snat_ip',
'dhcp_pool',
),
Fieldset(
'IPv6',
_('IPv6'),
'network6',
'ipv6_template',
'host_ipv6_prefixlen',
),
Fieldset(
'Domain name service',
_('Domain name service'),
'domain',
'reverse_domain',
),
Fieldset(
'Info',
_('Info'),
'description',
'comment',
'owner',
# 'created_at',
# 'modified_at',
),
),
FormActions(
Submit('submit', 'Save'),
LinkButton('back', 'Back', reverse_lazy('network.vlan_list'))
Submit('submit', _("Save")),
LinkButton('back', _("Back"), reverse_lazy('network.vlan_list'))
)
)
......@@ -289,8 +292,8 @@ class VlanGroupForm(ModelForm):
)
),
FormActions(
Submit('submit', 'Save'),
LinkButton('back', 'Back', reverse_lazy(
Submit('submit', _("Save")),
LinkButton('back', _("Back"), reverse_lazy(
'network.vlan_group_list'))
)
)
......
......@@ -6,3 +6,8 @@
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 @@
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext_lazy as _
from django_tables2 import Table, A
from django_tables2.columns import LinkColumn, TemplateColumn
......@@ -181,3 +183,20 @@ class VlanGroupTable(Table):
attrs = {'class': 'table table-striped table-condensed'}
fields = ('name', 'vlans', 'description', 'owner', )
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 l10n %}
{# <span style="color: #FF0000;">[{{ record.r_type }}]</span> #}
{% if record.direction == "1" %}
{% if record.direction == "in" %}
{{ record.foreign_network }}
[{% for v in record.foreign_network.vlans.all %}
{{ v.name }}{% if not forloop.last %},{% endif %}
{% endfor %}]
{% else %}
{% if record.r_type == "host" %}
{{ record.host.get_fqdn }}
......@@ -11,10 +13,10 @@
{{ record.r_type }}
{% endif %}
{% endif %}
{#<span style="color: #0000FF;"> ▸ </span>#}
<i class="fa fa-arrow-right"></i>
{% if record.direction == "0" %}
{% if record.direction == "out" %}
{{ record.foreign_network }}
{% else %}
{% if record.r_type == "host" %}
......@@ -33,6 +35,11 @@
{% if record.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 %}
......@@ -12,10 +12,10 @@
</div>
<div class="row">
<div class="col-sm-7">
<div class="col-md-6">
{% crispy form %}
</div>
<div class="col-sm-5">
<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>
......@@ -23,7 +23,7 @@
{% if rule_list.data.data.count > 0 %}
{% render_table rule_list %}
{% else %}
{% trans "No rules associated with this host!" %}
{% trans "No rules associated with this host." %}
{% endif %}
<div class="page-header">
......@@ -45,10 +45,10 @@
{% endif %}
<div class="page-header">
<h3>Add host group</h3>
<h3>{% trans "Add host group" %}</h3>
</div>
{% if not_used_groups|length == 0 %}
No more groups to add!
{% trans "No more groups to add" %}
{% else %}
<form action="{% url "network.add_host_group" pk=host_pk %}" method="POST">
{% csrf_token %}
......@@ -64,11 +64,20 @@
</div><!-- input-group -->
</form>
{% 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 -->
{% endblock %}
{% block extra_etc %}
<script src="{% static "js/host.js" %}"></script>
<script src="{% static "js/host.js" %}"></script>
{% endblock %}
......@@ -34,6 +34,7 @@ from .forms import (HostForm, VlanForm, DomainForm, GroupForm, RecordForm,
BlacklistItemForm, RuleForm, VlanGroupForm, SwitchPortForm)
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.views.generic.edit import FormMixin
from django.utils.translation import ugettext_lazy as _
from braces.views import LoginRequiredMixin, SuperuserRequiredMixin
......@@ -42,25 +43,14 @@ from operator import itemgetter
from itertools import chain
import json
from dashboard.views import AclUpdateView
from dashboard.forms import AclUserAddForm
from dashboard.forms import AclUserOrGroupAddForm
class SuccessMessageMixin(FormMixin):
"""
Adds a success message on successful form submission.
From django/contrib/messages/views.py@9a85ad89
"""
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 InitialOwnerMixin(FormMixin):
def get_initial(self):
initial = super(InitialOwnerMixin, self).get_initial()
initial['owner'] = self.request.user
return initial
class IndexView(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
......@@ -190,7 +180,7 @@ class DomainDetail(LoginRequiredMixin, SuperuserRequiredMixin,
class DomainCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView):
SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Domain
template_name = "network/domain-create.html"
form_class = DomainForm
......@@ -274,7 +264,7 @@ class GroupList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
class GroupCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView):
SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Group
template_name = "network/group-create.html"
form_class = GroupForm
......@@ -399,6 +389,12 @@ class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin,
# set host pk (we need this for URL-s)
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
def get_success_url(self):
......@@ -407,7 +403,7 @@ class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin,
class HostCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView):
SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Host
template_name = "network/host-create.html"
form_class = HostForm
......@@ -491,7 +487,7 @@ class RecordDetail(LoginRequiredMixin, SuperuserRequiredMixin,
class RecordCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView):
SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Record
template_name = "network/record-create.html"
form_class = RecordForm
......@@ -499,10 +495,23 @@ class RecordCreate(LoginRequiredMixin, SuperuserRequiredMixin,
success_message = _(u'Successfully created record!')
def get_initial(self):
return {
# 'owner': 1,
'domain': self.request.GET.get('domain'),
}
initial = super(RecordCreate, self).get_initial()
initial['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):
......@@ -550,18 +559,19 @@ class RuleDetail(LoginRequiredMixin, SuperuserRequiredMixin,
class RuleCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView):
SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Rule
template_name = "network/rule-create.html"
form_class = RuleForm
success_message = _(u'Successfully created rule!')
def get_initial(self):
return {
# 'owner': 1,
initial = super(RuleCreate, self).get_initial()
initial.update({
'host': self.request.GET.get('host'),
'hostgroup': self.request.GET.get('hostgroup')
}
})
return initial
class RuleDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
......@@ -654,14 +664,14 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
context['vlan_vid'] = self.kwargs.get('vid')
context['acl'] = AclUpdateView.get_acl_data(
self.object, self.request.user, 'network.vlan-acl')
context['aclform'] = AclUserAddForm()
context['aclform'] = AclUserOrGroupAddForm()
return context
success_url = reverse_lazy('network.vlan_list')
class VlanCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView):
SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Vlan
template_name = "network/vlan-create.html"
form_class = VlanForm
......@@ -741,7 +751,7 @@ class VlanGroupDetail(LoginRequiredMixin, SuperuserRequiredMixin,
class VlanGroupCreate(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, CreateView):
SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = VlanGroup
template_name = "network/vlan-group-create.html"
form_class = VlanGroupForm
......
......@@ -278,7 +278,7 @@ class Disk(TimeStampedModel):
return Disk.create(base=self, datastore=self.datastore,
name=self.name, size=self.size,
type=new_type)
type=new_type, dev_num=self.dev_num)
def get_vmdisk_desc(self):
"""Serialize disk object to the vmdriver.
......@@ -367,7 +367,8 @@ class Disk(TimeStampedModel):
disk = cls.__create(user, params)
disk.clean()
disk.save()
logger.debug("Disk created: %s", params)
logger.debug(u"Disk created from: %s",
unicode(params.get("base", "nobase")))
return disk
@classmethod
......
......@@ -11,10 +11,6 @@
body {
margin-top: 40px;
}
.container {
width: 600px;
}
ul, li {
list-style: none;
margin: 0;
......@@ -22,6 +18,8 @@
}
.container > .content {
max-width: 570px;
margin: auto;
background-color: #fff;
padding: 20px;
-webkit-border-radius: 10px 10px 10px 10px;
......@@ -38,8 +36,10 @@
}
.login-form {
margin-top: 40px;
margin: 20px auto 0 auto;
padding: 0 10px;
max-width: 250px;
}
.login-form form {
......@@ -79,3 +79,10 @@
<img src="{% static "dashboard/img/logo.png" %}" style="height: 25px;"/>
</a>
{% endblock %}
{% block content %}
<div class="content">
{% block content_box %}{% endblock %}
</div>
{% endblock %}
......@@ -12,15 +12,14 @@
</a>
{% endblock %}
{% block content %}
<div class="content">
{% block content_box %}
<div class="row">
{% if form.password.errors or form.username.errors %}
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
{% 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">
<form action="" method="POST">
{% csrf_token %}
......@@ -29,8 +28,8 @@
</div>
</div>
{% if saml2 %}
<div class="col-sm-6">
<h4 style="padding-top: 0; margin-top: 0;">{% trans "Login with SSO" %}</h4>
<div class="col-xs-6">
<h4 style="padding-top: 0; margin-top: 20px;">{% trans "Login with SSO" %}</h4>
<a href="{% url "saml2_login" %}">{% trans "Click here!" %}</a>
</div>
{% endif %}
......
......@@ -5,9 +5,8 @@
{% block title-page %}{% trans "Password reset complete" %}{% endblock %}
{% block content %}
<div class="content">
<div class="row">
{% block content_box %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
......@@ -17,6 +16,5 @@
<a href="{% url "accounts.login" %}">{% trans "Click here to login" %}</a>
</div>
</div>
</div>
</div>
{% endblock %}
......@@ -5,15 +5,16 @@
{% block title-page %}{% trans "Password reset confirm" %}{% endblock %}
{% block content %}
<div>
<div class="row">
{% block content_box %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</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>
{% blocktrans %}
Please enter your new password twice so we can verify you typed it in correctly.
{% endblocktrans %}
</div>
{% if form %}
......@@ -21,10 +22,9 @@
{% else %}
<div class="alert alert-warning">
{% 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>
{% endif %}
</div>
</div>
</div>
{% endblock %}
......@@ -5,16 +5,14 @@
{% block title-page %}{% trans "Password reset done" %}{% endblock %}
{% block content %}
<div class="content">
<div class="row">
{% block content_box %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
<div class="col-sm-12">
<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!" %}
</div>
{% trans "We have sent you an email about your next steps." %}
</div>
</div>
{% endblock %}
......@@ -5,20 +5,20 @@
{% block title-page %}{% trans "Password reset" %}{% endblock %}
{% block content %}
<div class="content">
<div class="row">
{% block content_box %}
<div class="row">
<div class="login-form-errors">
{% include "display-form-errors.html" %}
</div>
<div class="col-sm-12">
<div class="pull-right"><a href="{% url "accounts.login" %}">{% trans "Back to login" %}</a></div>
<h4 style="margin: 0 0 25px 0;">{% blocktrans %}Enter your email address to reset your password!{% endblocktrans %}</h4>
<h4 style="margin: 0 0 25px 0;">
{% blocktrans %}Enter your email address to reset your password.{% endblocktrans %}
</h4>
<form action="" method="POST">
{% csrf_token %}
{% crispy form %}
</form>
</div>
</div>
</div>
{% endblock %}
......@@ -24,7 +24,7 @@ from celery.signals import worker_ready
from celery.contrib.abortable import AbortableAsyncResult
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.translation import ugettext_lazy as _, ugettext_noop
......@@ -70,6 +70,8 @@ class InstanceActivity(ActivityModel):
help_text=_('Instance this activity works on.'),
verbose_name=_('instance'))
resultant_state = CharField(blank=True, max_length=20, null=True)
interruptible = BooleanField(default=False, help_text=_(
'Other activities can interrupt this one.'))
class Meta:
app_label = 'vm'
......@@ -91,24 +93,30 @@ class InstanceActivity(ActivityModel):
@classmethod
def create(cls, code_suffix, instance, task_uuid=None, user=None,
concurrency_check=True, readable_name=None,
resultant_state=None):
resultant_state=None, interruptible=False):
readable_name = _normalize_readable_name(readable_name, code_suffix)
# Check for concurrent activities
active_activities = instance.activity_log.filter(finished__isnull=True)
if concurrency_check and active_activities.exists():
raise ActivityInProgressError.create(active_activities[0])
for i in active_activities:
if i.interruptible:
i.finish(False, result=ugettext_noop(
"Interrupted by other activity."))
else:
raise ActivityInProgressError.create(i)
activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix)
activity_code = cls.construct_activity_code(code_suffix)
act = cls(activity_code=activity_code, instance=instance, parent=None,
resultant_state=resultant_state, started=timezone.now(),
readable_name_data=readable_name.to_dict(),
task_uuid=task_uuid, user=user)
task_uuid=task_uuid, user=user, interruptible=interruptible)
act.save()
return act
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)
# Check for concurrent activities
......@@ -119,7 +127,7 @@ class InstanceActivity(ActivityModel):
act = InstanceActivity(
activity_code=join_activity_code(self.activity_code, code_suffix),
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(),
task_uuid=task_uuid, user=self.user)
act.save()
......@@ -183,13 +191,14 @@ class InstanceActivity(ActivityModel):
@contextmanager
def sub_activity(self, code_suffix, on_abort=None, on_commit=None,
readable_name=None, task_uuid=None,
concurrency_check=True):
concurrency_check=True, interruptible=False):
"""Create a transactional context for a nested instance activity.
"""
if not readable_name:
warn("Set readable_name", stacklevel=3)
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)
def get_operation(self):
......
......@@ -123,6 +123,10 @@ class VirtualMachineDescModel(BaseResourceConfigModel):
'format like "%s".') %
'Ubuntu 12.04 LTS Desktop amd64'))
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:
abstract = True
......@@ -244,10 +248,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
verbose_name=_('time of delete'),
help_text=_("Proposed time of automatic "
"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,
related_name='instance_set',
help_text=_("Current hypervisor of this instance."),
......@@ -428,7 +428,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
# prepare parameters
common_fields = ['name', 'description', 'num_cores', 'ram_size',
'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.update([(f, getattr(template, f)) for f in common_fields])
params.update(kwargs) # override defaults w/ user supplied values
......@@ -513,7 +514,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def ipv4(self):
"""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
def ipv6(self):
......@@ -528,15 +533,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
return self.primary_host.mac if self.primary_host else None
@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):
"""Get the type of the instance's operating system.
"""
......@@ -545,13 +541,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
else:
return self.template.os_type
def get_age(self):
"""Deprecated. Use uptime instead.
Get age of VM in seconds.
"""
return self.uptime.seconds
@property
def waiting(self):
"""Indicates whether the instance's waiting for an operation to finish.
......@@ -603,14 +592,19 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
port = self.get_connect_port(use_ipv6=use_ipv6)
host = self.get_connect_host(use_ipv6=use_ipv6)
proto = self.access_method
if proto == 'ssh':
proto = 'sshterm'
return ('%(proto)s:cloud:%(pw)s:%(host)s:%(port)d' %
return ('circle:%(proto)s:cloud:%(pw)s:%(host)s:%(port)d' %
{'port': port, 'proto': proto, 'pw': self.pw,
'host': host})
except:
return
@property
def short_hostname(self):
try:
return self.primary_host.hostname
except AttributeError:
return self.vm_name
def get_vm_desc(self):
"""Serialize Instance object to vmdriver.
"""
......
......@@ -34,6 +34,7 @@ from common.models import (
create_readable, humanize_exception, HumanReadableException
)
from common.operations import Operation, register_operation
from manager.scheduler import SchedulerError
from .tasks.local_tasks import (
abortable_async_instance_operation, abortable_async_node_operation,
)
......@@ -41,7 +42,7 @@ from .models import (
Instance, InstanceActivity, InstanceTemplate, Interface, Node,
NodeActivity, pwgen
)
from .tasks import agent_tasks
from .tasks import agent_tasks, local_agent_tasks
from dashboard.store_api import Store, NoStoreException
......@@ -153,6 +154,7 @@ class AddInterfaceOperation(InstanceOperation):
self.rollback(net, activity)
raise
net.deploy()
local_agent_tasks.send_networking_commands(self.instance, activity)
def get_activity_name(self, kwargs):
return create_readable(ugettext_noop("add %(vlan)s interface"),
......@@ -297,16 +299,20 @@ class DeployOperation(InstanceOperation):
"deploy network")):
self.instance.deploy_net()
try:
self.instance.renew(parent_activity=activity)
except:
pass
# Resume vm
with activity.sub_activity(
'booting', readable_name=ugettext_noop(
"boot virtual machine")):
self.instance.resume_vm(timeout=timeout)
try:
self.instance.renew(parent_activity=activity)
except:
pass
if self.instance.has_agent:
activity.sub_activity('os_boot', readable_name=ugettext_noop(
"wait operating system loading"), interruptible=True)
register_operation(DeployOperation)
......@@ -423,8 +429,11 @@ class RebootOperation(InstanceOperation):
required_perms = ()
accept_states = ('RUNNING', )
def _operation(self, timeout=5):
def _operation(self, activity, timeout=5):
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)
......@@ -497,8 +506,11 @@ class ResetOperation(InstanceOperation):
required_perms = ()
accept_states = ('RUNNING', )
def _operation(self, timeout=5):
def _operation(self, activity, timeout=5):
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)
......@@ -619,6 +631,17 @@ class ShutdownOperation(InstanceOperation):
self.instance.yield_node()
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)
......@@ -716,6 +739,9 @@ class WakeUpOperation(InstanceOperation):
return self.instance.status == self.instance.STATUS.SUSPENDED
def on_abort(self, activity, error):
if isinstance(error, SchedulerError):
activity.resultant_state = None
else:
activity.resultant_state = 'ERROR'
def _operation(self, activity, timeout=60):
......@@ -1003,12 +1029,12 @@ class MountStoreOperation(EnsureAgentMixin, InstanceOperation):
except NoStoreException:
raise PermissionDenied # not show the button at all
def _operation(self):
def _operation(self, user):
inst = self.instance
queue = self.instance.get_remote_queue_name("agent")
host = urlsplit(settings.STORE_URL).hostname
username = Store(inst.owner).username
password = inst.owner.profile.smb_password
username = Store(user).username
password = user.profile.smb_password
agent_tasks.mount_store.apply_async(
queue=queue, args=(inst.vm_name, host, username, password))
......
......@@ -76,3 +76,8 @@ def get_keys(vm):
@celery.task(name='agent.send_expiration')
def send_expiration(vm, url):
pass
@celery.task(name='agent.change_ip')
def change_ip(vm, interfaces, dns):
pass
......@@ -19,7 +19,9 @@ from common.models import create_readable
from manager.mancelery import celery
from vm.tasks.agent_tasks import (restart_networking, change_password,
set_time, set_hostname, start_access_server,
cleanup, update)
cleanup, update, change_ip)
from firewall.models import Host
import time
from base64 import encodestring
from StringIO import StringIO
......@@ -31,13 +33,11 @@ from celery.result import TimeoutError
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")
with act.sub_activity('cleanup', readable_name=ugettext_noop('cleanup')):
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',
readable_name=ugettext_noop('change password')):
change_password.apply_async(queue=queue, args=(vm, instance.pw))
......@@ -46,7 +46,18 @@ def send_init_commands(instance, act, vm):
with act.sub_activity('set_hostname',
readable_name=ugettext_noop('set hostname')):
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():
......@@ -74,16 +85,22 @@ def agent_started(vm, version=None):
from vm.models import Instance, instance_activity, InstanceActivity
instance = Instance.objects.get(id=int(vm.split('-')[-1]))
queue = instance.get_remote_queue_name("agent")
initialized = InstanceActivity.objects.filter(
instance=instance, activity_code='vm.Instance.agent.cleanup').exists()
initialized = instance.activity_log.filter(
activity_code='vm.Instance.agent.cleanup').exists()
with instance_activity(code_suffix='agent',
readable_name=ugettext_noop('agent'),
concurrency_check=False,
instance=instance) as act:
with act.sub_activity('starting',
readable_name=ugettext_noop('starting')):
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:
try:
update_agent(instance, act)
......@@ -94,12 +111,12 @@ def agent_started(vm, version=None):
if not initialized:
measure_boot_time(instance)
send_init_commands(instance, act, vm)
send_init_commands(instance, act)
with act.sub_activity(
'start_access_server',
readable_name=ugettext_noop('start access server')
):
send_networking_commands(instance, act)
with act.sub_activity('start_access_server',
readable_name=ugettext_noop(
'start access server')):
start_access_server.apply_async(queue=queue, args=(vm, ))
......@@ -134,6 +151,13 @@ def agent_stopped(vm):
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):
if act:
act = act.sub_activity(
......
......@@ -217,6 +217,8 @@ class InstanceActivityTestCase(TestCase):
def test_create_concurrency_check(self):
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
with self.assertRaises(ActivityInProgressError):
......
......@@ -14,4 +14,3 @@ post-stop script
stop mancelery
stop slowcelery
end script
......@@ -12,4 +12,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 10
end script
......@@ -12,4 +12,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.moncelery worker --autoreload --loglevel=info --hostname=moncelery -B -c 3
end script
......@@ -12,4 +12,3 @@ script
. /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
end script
......@@ -14,4 +14,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py runserver '[::]:8080'
end script
......@@ -12,4 +12,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.slowcelery worker --autoreload --loglevel=info --hostname=slowcelery -B -c 5
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