Commit 08a3542b by Őry Máté

Merge branch 'master' into connect-via-client

Conflicts:
	circle/dashboard/templates/dashboard/vm-detail.html
	circle/dashboard/views.py
parents dbcacac7 8eea8de7
......@@ -368,9 +368,9 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
from shutilwhich import which
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
# INSTALLED_APPS += ( # needed only for testing djangosaml2
# 'djangosaml',
# )
INSTALLED_APPS += (
'djangosaml2',
)
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'djangosaml2.backends.Saml2Backend',
......
......@@ -20,9 +20,12 @@
from os import environ
from sys import argv
from base import * # noqa
if 'runserver' in argv:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
########## HOST CONFIGURATION
# See: https://docs.djangoproject.com/en/1.5/releases/1.5/
......
......@@ -17,6 +17,7 @@
from collections import deque
from contextlib import contextmanager
from functools import update_wrapper
from hashlib import sha224
from itertools import chain, imap
from logging import getLogger
......@@ -26,6 +27,7 @@ from warnings import warn
from django.contrib import messages
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import (
CharField, DateTimeField, ForeignKey, NullBooleanField
......@@ -36,6 +38,7 @@ from django.utils.functional import Promise
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from jsonfield import JSONField
from manager.mancelery import celery
from model_utils.models import TimeStampedModel
logger = getLogger(__name__)
......@@ -212,6 +215,38 @@ class ActivityModel(TimeStampedModel):
self.result_data = None if value is None else value.to_dict()
@celery.task()
def compute_cached(method, instance, memcached_seconds,
key, start, *args, **kwargs):
"""Compute and store actual value of cached method."""
if isinstance(method, basestring):
model, id = instance
instance = model.objects.get(id=id)
try:
method = getattr(model, method)
while hasattr(method, '_original') or hasattr(method, 'fget'):
try:
method = method._original
except AttributeError:
method = method.fget
except AttributeError:
logger.exception("Couldnt get original method of %s",
unicode(method))
raise
# call the actual method
result = method(instance, *args, **kwargs)
# save to memcache
cache.set(key, result, memcached_seconds)
elapsed = time() - start
cache.set("%s.cached" % key, 2, max(memcached_seconds * 0.5,
memcached_seconds * 0.75 - elapsed))
logger.debug('Value of <%s>.%s(%s)=<%s> saved to cache (%s elapsed).',
unicode(instance), method.__name__, unicode(args),
unicode(result), elapsed)
return result
def method_cache(memcached_seconds=60, instance_seconds=5): # noqa
"""Cache return value of decorated method to memcached and memory.
......@@ -233,9 +268,11 @@ def method_cache(memcached_seconds=60, instance_seconds=5): # noqa
def inner_cache(method):
method_name = method.__name__
def get_key(instance, *args, **kwargs):
return sha224(unicode(method.__module__) +
unicode(method.__name__) +
method_name +
unicode(instance.id) +
unicode(args) +
unicode(kwargs)).hexdigest()
......@@ -254,21 +291,31 @@ def method_cache(memcached_seconds=60, instance_seconds=5): # noqa
if vals['time'] + instance_seconds > now:
# has valid on class cache, return that
result = vals['value']
setattr(instance, key, {'time': now, 'value': result})
if result is None:
result = cache.get(key)
if invalidate or (result is None):
# all caches failed, call the actual method
result = method(instance, *args, **kwargs)
# save to memcache and class attr
cache.set(key, result, memcached_seconds)
logger.debug("all caches failed, compute now")
result = compute_cached(method, instance, memcached_seconds,
key, time(), *args, **kwargs)
setattr(instance, key, {'time': now, 'value': result})
logger.debug('Value of <%s>.%s(%s)=<%s> saved to cache.',
unicode(instance), method.__name__,
unicode(args), unicode(result))
elif not cache.get("%s.cached" % key):
logger.debug("caches expiring, compute async")
cache.set("%s.cached" % key, 1, memcached_seconds * 0.5)
try:
compute_cached.apply_async(
queue='localhost.man', kwargs=kwargs, args=[
method_name, (instance.__class__, instance.id),
memcached_seconds, key, time()] + list(args))
except:
logger.exception("Couldnt compute async %s", method_name)
return result
update_wrapper(x, method)
x._original = method
return x
return inner_cache
......@@ -367,6 +414,10 @@ class HumanReadableObject(object):
self._set_values(user_text_template, admin_text_template, params)
def _set_values(self, user_text_template, admin_text_template, params):
if isinstance(user_text_template, Promise):
user_text_template = user_text_template._proxy____args[0]
if isinstance(admin_text_template, Promise):
admin_text_template = admin_text_template._proxy____args[0]
self.user_text_template = user_text_template
self.admin_text_template = admin_text_template
self.params = params
......@@ -405,6 +456,12 @@ class HumanReadableObject(object):
self.user_text_template, unicode(self.params))
return self.user_text_template
def get_text(self, user):
if user and user.is_superuser:
return self.get_admin_text()
else:
return self.get_user_text()
def to_dict(self):
return {"user_text_template": self.user_text_template,
"admin_text_template": self.admin_text_template,
......@@ -435,13 +492,34 @@ class HumanReadableException(HumanReadableObject, Exception):
self.level = "error"
def send_message(self, request, level=None):
if request.user and request.user.is_superuser:
msg = self.get_admin_text()
else:
msg = self.get_user_text()
msg = self.get_text(request.user)
getattr(messages, level or self.level)(request, msg)
def fetch_human_exception(exception, user=None):
"""Fetch user readable message from exception.
>>> r = humanize_exception("foo", Exception())
>>> fetch_human_exception(r, User())
u'foo'
>>> fetch_human_exception(r).get_text(User())
u'foo'
>>> fetch_human_exception(Exception(), User())
u'Unknown error'
>>> fetch_human_exception(PermissionDenied(), User())
u'Permission Denied'
"""
if not isinstance(exception, HumanReadableException):
if isinstance(exception, PermissionDenied):
exception = create_readable(ugettext_noop("Permission Denied"))
else:
exception = create_readable(ugettext_noop("Unknown error"),
ugettext_noop("Unknown error: %(ex)s"),
ex=unicode(exception))
return exception.get_text(user) if user else exception
def humanize_exception(message, exception=None, level=None, **params):
"""Return new dynamic-class exception which is based on
HumanReadableException and the original class with the dict of exception.
......
......@@ -18,10 +18,10 @@
from inspect import getargspec
from logging import getLogger
from .models import activity_context, has_suffix
from django.core.exceptions import PermissionDenied, ImproperlyConfigured
from django.utils.translation import ugettext_noop
from .models import activity_context, has_suffix, humanize_exception
logger = getLogger(__name__)
......@@ -31,6 +31,7 @@ class Operation(object):
"""
async_queue = 'localhost.man'
required_perms = None
superuser_required = False
do_not_call_in_templates = True
abortable = False
has_percentage = False
......@@ -143,13 +144,26 @@ class Operation(object):
def check_precond(self):
pass
def check_auth(self, user):
if self.required_perms is None:
@classmethod
def check_perms(cls, user):
"""Check if user is permitted to run this operation on any instance
"""
if cls.required_perms is None:
raise ImproperlyConfigured(
"Set required_perms to () if none needed.")
if not user.has_perms(self.required_perms):
if not user.has_perms(cls.required_perms):
raise PermissionDenied("%s doesn't have the required permissions."
% user)
if cls.superuser_required and not user.is_superuser:
raise humanize_exception(ugettext_noop(
"Superuser privileges are required."), PermissionDenied())
def check_auth(self, user):
"""Check if user is permitted to run this operation on this instance
"""
self.check_perms(user)
def create_activity(self, parent, user, kwargs):
raise NotImplementedError
......@@ -185,14 +199,17 @@ class OperatedMixin(object):
def __getattr__(self, name):
# NOTE: __getattr__ is only called if the attribute doesn't already
# exist in your __dict__
cls = self.__class__
return self.get_operation_class(name)(self)
@classmethod
def get_operation_class(cls, name):
ops = getattr(cls, operation_registry_name, {})
op = ops.get(name)
if op:
return op(self)
return op
else:
raise AttributeError("%r object has no attribute %r" %
(self.__class__.__name__, name))
(cls.__name__, name))
def get_available_operations(self, user):
"""Yield Operations that match permissions of user and preconditions.
......
......@@ -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)
......
......@@ -39,7 +39,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.forms.widgets import TextInput, HiddenInput
from django.template import Context
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy as _
from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy
......@@ -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)
......@@ -176,7 +178,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):
......@@ -634,12 +643,8 @@ class LeaseForm(forms.ModelForm):
Field('name'),
Field("suspend_interval_seconds", type="hidden", value="0"),
Field("delete_interval_seconds", type="hidden", value="0"),
HTML(string_concat("<label>", _("Suspend in"), "</label>")),
Div(
Div(
HTML(_("Suspend in")),
css_class="input-group-addon",
style="width: 100px;",
),
NumberField("suspend_hours", css_class="form-control"),
Div(
HTML(_("hours")),
......@@ -662,12 +667,8 @@ class LeaseForm(forms.ModelForm):
),
css_class="input-group interval-input",
),
HTML(string_concat("<label>", _("Delete in"), "</label>")),
Div(
Div(
HTML(_("Delete in")),
css_class="input-group-addon",
style="width: 100px;",
),
NumberField("delete_hours", css_class="form-control"),
Div(
HTML(_("hours")),
......@@ -691,7 +692,7 @@ class LeaseForm(forms.ModelForm):
css_class="input-group interval-input",
)
)
helper.add_input(Submit("submit", "Save changes"))
helper.add_input(Submit("submit", _("Save changes")))
return helper
class Meta:
......@@ -703,6 +704,8 @@ class VmRenewForm(forms.Form):
force = forms.BooleanField(required=False, label=_(
"Set expiration times even if they are shorter than "
"the current value."))
save = forms.BooleanField(required=False, label=_(
"Save selected lease."))
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
......@@ -714,6 +717,32 @@ class VmRenewForm(forms.Form):
empty_label=None, label=_('Length')))
if len(choices) < 2:
self.fields['lease'].widget = HiddenInput()
self.fields['save'].widget = HiddenInput()
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class VmStateChangeForm(forms.Form):
interrupt = forms.BooleanField(required=False, label=_(
"Forcibly interrupt all running activities."),
help_text=_("Set all activities to finished state, "
"but don't interrupt any tasks."))
new_state = forms.ChoiceField(Instance.STATUS, label=_(
"New status"))
def __init__(self, *args, **kwargs):
show_interrupt = kwargs.pop('show_interrupt')
status = kwargs.pop('status')
super(VmStateChangeForm, self).__init__(*args, **kwargs)
if not show_interrupt:
self.fields['interrupt'].widget = HiddenInput()
self.fields['new_state'].initial = status
@property
def helper(self):
......@@ -1037,6 +1066,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:
......@@ -1138,3 +1183,53 @@ class VmResourcesForm(forms.ModelForm):
class Meta:
model = Instance
fields = ('num_cores', 'priority', 'ram_size', )
vm_search_choices = (
("owned", _("owned")),
("shared", _("shared")),
("all", _("all")),
)
class VmListSearchForm(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 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):
super(VmListSearchForm, 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'] = "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 .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:
......
......@@ -654,7 +654,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 +682,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 {
......@@ -867,3 +867,92 @@ textarea[name="list-new-namelist"] {
border-bottom: 1px dotted #aaa;
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;
animation: passing 2s linear infinite;
}
@-webkit-keyframes passing {
0% {
-webkit-transform: translateX(50%);
transform: translateX(50%);
opacity: 0;
}
50% {
-webkit-transform: translateX(0%);
transform: translateX(0%);
opacity: 1;
}
100% {
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
opacity: 0;
}
}
@keyframes passing {
0% {
-webkit-transform: translateX(50%);
-ms-transform: translateX(50%);
transform: translateX(50%);
opacity: 0;
}
50% {
-webkit-transform: translateX(0%);
-ms-transform: translateX(0%);
transform: translateX(0%);
opacity: 1;
}
100% {
-webkit-transform: translateX(-50%);
-ms-transform: translateX(-50%);
transform: translateX(-50%);
opacity: 0;
}
}
.mass-migrate-node {
cursor: pointer;
}
.mass-op-panel {
padding: 6px 10px;
}
.mass-op-panel .check {
color: #449d44;
}
.mass-op-panel .minus {
color: #d9534f;
}
.mass-op-panel .status-icon {
font-size: .8em;
}
#vm-list-search, #vm-mass-ops {
margin-top: 8px;
}
#vm-list-search-checkbox {
margin-top: -1px;
display: inline-block;
vertical-align: middle;
}
#vm-list-search-checkbox-span {
cursor: pointer
}
......@@ -258,14 +258,15 @@ $(function () {
html += '<div class="list-group-item list-group-item-last">' + gettext("No result") + '</div>';
$("#dashboard-vm-list").html(html);
$('.title-favourite').tooltip({'placement': 'right'});
});
// if there is only one result and ENTER is pressed redirect
if(e.keyCode == 13 && search_result.length == 1) {
window.location.href = "/dashboard/vm/" + search_result[0].pk + "/";
}
if(e.keyCode == 13 && search_result.length > 1 && input.length > 0) {
window.location.href = "/dashboard/vm/list/?s=" + input;
$("#dashboard-vm-search-form").submit(function() {
var vm_list_items = $("#dashboard-vm-list .list-group-item");
if(vm_list_items.length == 1 && vm_list_items.first().prop("href")) {
window.location.href = vm_list_items.first().prop("href");
return false;
}
return true;
});
/* search for nodes */
......@@ -494,14 +495,19 @@ function addSliderMiscs() {
ram_fire = true;
$(".ram-slider").simpleSlider("setValue", parseInt(val));
});
$(".cpu-priority-input").trigger("change");
$(".cpu-count-input, .ram-input").trigger("input");
setDefaultSliderValues();
$(".cpu-priority-slider").simpleSlider("setDisabled", $(".cpu-priority-input").prop("disabled"));
$(".cpu-count-slider").simpleSlider("setDisabled", $(".cpu-count-input").prop("disabled"));
$(".ram-slider").simpleSlider("setDisabled", $(".ram-input").prop("disabled"));
}
function setDefaultSliderValues() {
$(".cpu-priority-input").trigger("change");
$(".ram-input, .cpu-count-input").trigger("input");
}
/* deletes the VM with the pk
* if dir is true, then redirect to the dashboard landing page
......@@ -632,3 +638,11 @@ 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, " "));
}
var ctrlDown, shiftDown = false;
var ctrlKey = 17;
var shiftKey = 16;
var selected = [];
$(function() {
$(document).keydown(function(e) {
if (e.keyCode == ctrlKey) ctrlDown = true;
if (e.keyCode == shiftKey) shiftDown = true;
}).keyup(function(e) {
if (e.keyCode == ctrlKey) ctrlDown = false;
if (e.keyCode == shiftKey) shiftDown = false;
});
$('.group-list-table tbody').find('tr').mousedown(function() {
var retval = true;
if (ctrlDown) {
setRowColor($(this));
if(!$(this).hasClass('group-list-selected')) {
selected.splice(selected.indexOf($(this).index()), 1);
} else {
selected.push($(this).index());
}
retval = false;
} else if(shiftDown) {
if(selected.length > 0) {
start = selected[selected.length - 1] + 1;
end = $(this).index();
if(start > end) {
var tmp = start - 1; start = end; end = tmp - 1;
}
for(var i = start; i <= end; i++) {
if(selected.indexOf(i) < 0) {
selected.push(i);
setRowColor($('.group-list-table tbody tr').eq(i));
}
}
}
retval = false;
} else {
$('.group-list-selected').removeClass('group-list-selected');
$(this).addClass('group-list-selected');
selected = [$(this).index()];
}
// reset btn disables
$('.group-list-table tbody tr .btn').attr('disabled', false);
// show/hide group controls
if(selected.length > 1) {
$('.group-list-group-control a').attr('disabled', false);
for(var i = 0; i < selected.length; i++) {
$('.group-list-table tbody tr').eq(selected[i]).find('.btn').attr('disabled', true);
}
} else {
$('.group-list-group-control a').attr('disabled', true);
}
return retval;
});
$('#group-list-group-migrate').click(function() {
console.log(collectIds(selected));
});
$('tbody a').mousedown(function(e) {
// parent tr doesn't get selected when clicked
e.stopPropagation();
});
$('tbody a').click(function(e) {
// browser doesn't jump to top when clicked the buttons
if(!$(this).hasClass('real-link')) {
return false;
}
});
/* rename */
$("#group-list-rename-button, .group-details-rename-button").click(function() {
$("#group-list-column-name", $(this).closest("tr")).hide();
......@@ -113,51 +34,4 @@ $(function() {
return false;
});
/* group actions */
/* select all */
$('#group-list-group-select-all').click(function() {
$('.group-list-table tbody tr').each(function() {
var index = $(this).index();
if(selected.indexOf(index) < 0) {
selected.push(index);
$(this).addClass('group-list-selected');
}
});
if(selected.length > 0)
$('.group-list-group-control a').attr('disabled', false);
return false;
});
/* mass vm delete */
$('#group-list-group-delete').click(function() {
addModalConfirmation(massDeleteVm,
{
'url': '/dashboard/group/mass-delete/',
'data': {
'selected': selected,
'v': collectIds(selected)
}
}
);
return false;
});
});
function collectIds(rows) {
var ids = [];
for(var i = 0; i < rows.length; i++) {
var div = $('td:first-child div', $('.group-list-table tbody tr').eq(rows[i]));
ids.push(div.prop('id').replace('node-', ''));
}
return ids;
}
function setRowColor(row) {
if(!row.hasClass('group-list-selected')) {
row.addClass('group-list-selected');
} else {
row.removeClass('group-list-selected');
}
}
......@@ -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();
});
});
......
......@@ -3,7 +3,7 @@
$(function() {
/* vm operations */
$('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface').on('click', '.operation', function(e) {
$('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface, .operation-wrapper').on('click', '.operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin');
$.ajax({
......
......@@ -28,6 +28,9 @@ function vmCreateLoaded() {
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
});
$("#create-modal").on("shown.bs.modal", function() {
setDefaultSliderValues();
});
});
return false;
});
......@@ -217,6 +220,8 @@ function vmCustomizeLoaded() {
});
if(error) return true;
$(this).find("i").prop("class", "fa fa-spinner fa-spin");
$.ajax({
url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')},
......
......@@ -105,19 +105,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 */
......@@ -186,18 +187,7 @@ $(function() {
success: function(re, textStatus, xhr) {
/* remove the html element */
$('a[data-interface-pk="' + data.pk + '"]').closest("div").fadeOut();
/* add the removed element to the list */
network_select = $('select[name="new_network_vlan"]');
name_html = (re.removed_network.managed ? "&#xf0ac;": "&#xf0c1;") + " " + re.removed_network.vlan;
option_html = '<option value="' + re.removed_network.vlan_pk + '">' + name_html + '</option>';
// if it's -1 then it's a dummy placeholder so we can use .html
if($("option", network_select)[0].value === "-1") {
network_select.html(option_html);
network_select.next("div").children("button").prop("disabled", false);
} else {
network_select.append(option_html);
}
location.reload();
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger')
......@@ -209,7 +199,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;
});
......@@ -217,7 +207,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;
});
......@@ -317,8 +307,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() {
......
......@@ -14,6 +14,7 @@ $(function() {
$('.vm-list-table tbody').find('tr').mousedown(function() {
var retval = true;
if(!$(this).data("vm-pk")) return;
if (ctrlDown) {
setRowColor($(this));
if(!$(this).hasClass('vm-list-selected')) {
......@@ -46,86 +47,20 @@ $(function() {
selected = [{'index': $(this).index(), 'vm': $(this).data("vm-pk")}];
}
// reset btn disables
$('.vm-list-table tbody tr .btn').attr('disabled', false);
// show/hide group controls
if(selected.length > 0) {
$('.vm-list-group-control a').attr('disabled', false);
for(var i = 0; i < selected.length; i++) {
$('.vm-list-table tbody tr').eq(selected[i]).find('.btn').attr('disabled', true);
}
$('#vm-mass-ops .mass-operation').attr('disabled', false);
} else {
$('.vm-list-group-control a').attr('disabled', true);
$('#vm-mass-ops .mass-operation').attr('disabled', true);
}
return retval;
});
$('#vm-list-group-migrate').click(function() {
// pass?
});
$('.vm-list-details').popover({
'placement': 'auto',
'html': true,
'trigger': 'hover'
});
$('.vm-list-connect').popover({
'placement': 'left',
'html': true,
'trigger': 'click'
});
$('tbody a').mousedown(function(e) {
// parent tr doesn't get selected when clicked
e.stopPropagation();
});
$('tbody a').click(function(e) {
// browser doesn't jump to top when clicked the buttons
if(!$(this).hasClass('real-link')) {
return false;
}
});
/* rename */
$("#vm-list-rename-button, .vm-details-rename-button").click(function() {
$("#vm-list-column-name", $(this).closest("tr")).hide();
$("#vm-list-rename", $(this).closest("tr")).css('display', 'inline');
$("#vm-list-rename-name", $(this).closest("tr")).focus();
});
/* rename ajax */
$('.vm-list-rename-submit').click(function() {
var row = $(this).closest("tr")
var name = $('#vm-list-rename-name', row).val();
var url = '/dashboard/vm/' + row.children("td:first-child").text().replace(" ", "") + '/';
$.ajax({
method: 'POST',
url: url,
data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
$("#vm-list-column-name", row).html(
$("<a/>", {
'class': "real-link",
href: "/dashboard/vm/" + data['vm_pk'] + "/",
text: data['new_name']
})
).show();
$('#vm-list-rename', row).hide();
// addMessage(data['message'], "success");
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
/* group actions */
/* select all */
......@@ -133,27 +68,69 @@ $(function() {
$('.vm-list-table tbody tr').each(function() {
var index = $(this).index();
var vm = $(this).data("vm-pk");
if(!isAlreadySelected(vm)) {
if(vm && !isAlreadySelected(vm)) {
selected.push({'index': index, 'vm': vm});
$(this).addClass('vm-list-selected');
}
});
if(selected.length > 0)
$('.vm-list-group-control a').attr('disabled', false);
$('#vm-mass-ops .mass-operation').attr('disabled', false);
return false;
});
/* mass vm delete */
$('#vm-list-group-delete').click(function() {
addModalConfirmation(massDeleteVm,
{
'url': '/dashboard/vm/mass-delete/',
'data': {
'selected': selected,
'v': collectIds(selected)
/* mass operations */
$("#vm-mass-ops").on('click', '.mass-operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin');
params = "?" + selected.map(function(a){return "vm=" + a.vm}).join("&");
$.ajax({
type: 'GET',
url: $(this).attr('href') + params,
success: function(data) {
icon.removeClass("fa-spinner fa-spin");
$('body').append(data);
$('#confirmation-modal').modal('show');
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
$("[title]").tooltip({'placement': "left"});
}
});
return false;
});
$("body").on("click", "#op-form-send", function() {
var url = $(this).closest("form").prop("action");
$(this).find("i").prop("class", "fa fa-fw fa-spinner fa-spin");
$.ajax({
url: url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
type: 'POST',
data: $(this).closest('form').serialize(),
success: function(data, textStatus, xhr) {
/* hide the modal we just submitted */
$('#confirmation-modal').modal("hide");
updateStatuses(1);
/* if there are messages display them */
if(data.messages && data.messages.length > 0) {
addMessage(data.messages.join("<br />"), "danger");
}
);
},
error: function(xhr, textStatus, error) {
$('#confirmation-modal').modal("hide");
if (xhr.status == 500) {
addMessage("500 Internal Server Error", "danger");
} else {
addMessage(xhr.status + " " + xhr.statusText, "danger");
}
}
});
return false;
});
......@@ -181,8 +158,68 @@ $(function() {
$(".vm-list-table th a").on("click", function(event) {
event.preventDefault();
});
$(document).on("click", ".mass-migrate-node", function() {
$(this).find('input[type="radio"]').prop("checked", true);
});
if(checkStatusUpdate() || $("#vm-list-table tbody tr").length >= 100) {
updateStatuses(1);
}
});
function checkStatusUpdate() {
icons = $("#vm-list-table tbody td.state i");
if(icons.hasClass("fa-spin") || icons.hasClass("migrating-icon")) {
return true;
}
}
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");
status_td = $(this).find("td.state");
status_icon = status_td.find("i");
status_text = status_td.find("span");
if(vm in result) {
if(result[vm].in_status_change) {
if(!status_icon.hasClass("fa-spin")) {
status_icon.prop("class", "fa fa-fw fa-spinner fa-spin");
}
}
else if(result[vm].status == "MIGRATING") {
if(!status_icon.hasClass("migrating-icon")) {
status_icon.prop("class", "fa fa-fw " + result[vm].icon + " migrating-icon");
}
} else {
status_icon.prop("class", "fa fa-fw " + result[vm].icon);
}
status_text.text(result[vm].status);
if("node" in result[vm]) {
$(this).find(".node").text(result[vm].node);
}
} else {
if(!include_deleted)
$(this).remove();
}
});
if(checkStatusUpdate()) {
setTimeout(
function() {updateStatuses(runs + 1)},
1000 + Math.exp(runs * 0.05)
);
}
});
}
function isAlreadySelected(vm) {
for(var i=0; i<selected.length; i++)
if(selected[i].vm == vm)
......
......@@ -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-"
......@@ -248,5 +250,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.")
......@@ -41,6 +41,7 @@ def send_email_notifications():
for i in Notification.objects.filter(q):
recipients.setdefault(i.to, [])
recipients[i.to].append(i)
logger.info("Delivering notifications to %d users", len(recipients))
for user, msgs in recipients.iteritems():
if (not user.profile or not user.email or not
......
{% 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 %}
......
{% load i18n %}
{% load hro %}
{% for n in notifications %}
<li class="notification-message" id="msg-{{n.id}}">
<span class="notification-message-subject">
{% if n.status == "new" %}<i class="fa fa-envelope-alt"></i> {% endif %}
{{ n.subject.get_user_text }}
{% if n.status == "new" %}<i class="fa fa-envelope-o"></i> {% endif %}
{{ n.subject|get_text:user }}
</span>
<span class="notification-message-date pull-right" title="{{n.created}}">
{{ n.created|timesince }}
</span>
<div style="clear: both;"></div>
<div class="notification-message-text">
{{ n.message.get_user_text|safe }}
{{ n.message|get_text:user|safe }}
</div>
</li>
{% empty %}
......
......@@ -7,7 +7,6 @@
{% csrf_token %}
{{ vm_create_form.template }}
{{ vm_create_form.customized }}
<div class="row">
<div class="col-sm-12">
......@@ -23,6 +22,8 @@
</div>
</div>
{% if perms.vm.set_resources %}
{{ vm_create_form.customized }}
<div class="row">
<div class="col-sm-10">
<div class="form-group">
......@@ -85,6 +86,7 @@
</div><!-- .no-js-hidden -->
</div><!-- .col-sm-8 -->
</div><!-- .row -->
{% endif %}
</form>
<script>
......
{% extends "dashboard/mass-operate.html" %}
{% load i18n %}
{% load sizefieldtags %}
{% block formfields %}
<hr />
<ul id="vm-migrate-node-list" class="list-unstyled">
<li class="panel panel-default panel-primary mass-migrate-node">
<div class="panel-body">
<label for="migrate-to-none">
<strong>{% trans "Reschedule" %}</strong>
</label>
<input id="migrate-to-none" type="radio" name="node" value="" style="float: right;" checked="checked">
<span class="vm-migrate-node-property">
{% trans "This option will reschedule each virtual machine to the optimal node." %}
</span>
<div style="clear: both;"></div>
</div>
</li>
{% for n in nodes %}
<li class="panel panel-default mass-migrate-node">
<div class="panel-body">
<label for="migrate-to-{{n.pk}}">
<strong>{{ n }}</strong>
</label>
<input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;"/>
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
<div style="clear: both;"></div>
</div>
</li>
{% endfor %}
</ul>
<hr />
{% endblock %}
{% 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 %}
......
......@@ -6,63 +6,24 @@
{% block content %}
<div class="alert alert-info">
Tip #1: you can select multiple vm instances while holding down the <strong>CTRL</strong> key!
</div>
<div class="alert alert-info">
Tip #2: if you want to select multiple instances by one click select an instance then hold down <strong>SHIFT</strong> key and select another one!
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin"><i class="fa fa-group"></i> Your groups</h3>
</div>
<div class="panel-body group-list-group-control">
<p>
<strong>Group actions</strong>
<button id="group-list-group-select-all" class="btn btn-info btn-xs">Select all</button>
<a id="group-list-group-delete" disabled href="#" class="btn btn-danger btn-xs"><i class="fa fa-times"></i> Discard</a>
</p>
</div>
<div class="panel-body">
<div id="table_container">
<div id="rendered_table" class="panel-body">
{% render_table table %}
</div>
</div>
</div><!-- .panel-body -->
</div>
</div>
</div>
<style>
.popover {
max-width: 600px;
}
.group-list-selected, .group-list-selected td {
background-color: #e8e8e8 !important;
}
.group-list-selected:hover, .group-list-selected:hover td {
background-color: #d0d0d0 !important;
}
.group-list-selected td:first-child {
font-weight: bold;
}
.group-list-table-thin {
width: 10px;
}
.group-list-table-admin {
width: 130px;
}
</style>
{% endblock %}
{% block extra_js %}
<script src="{{ STATIC_URL}}dashboard/group-list.js"></script>
<script src="{{ STATIC_URL}}dashboard/group-list.js"></script>
{% endblock %}
<a data-group-pk="{{ record.pk }}" class="btn btn-danger btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=record.pk %}?next={{ request.path }}"><i class="fa fa-trash"></i></a>
<a data-group-pk="{{ record.pk }}"
class="btn btn-danger btn-xs real-link group-delete"
href="{% url "dashboard.views.delete-group" pk=record.pk %}?next={{ request.path }}">
<i class="fa fa-trash-o"></i>
</a>
......@@ -3,8 +3,10 @@
<div class="panel-heading">
<div class="pull-right toolbar">
<div class="btn-group">
<a href="#index-graph-view" data-index-box="node" class="btn btn-default btn-xs"><i class="fa fa-dashboard"></i></a>
<a href="#index-list-view" data-index-box="node" class="btn btn-default btn-xs disabled"><i class="fa fa-list"></i></a>
<a href="#index-graph-view" data-index-box="node" class="btn btn-default btn-xs"
data-container="body"><i class="fa fa-dashboard"></i></a>
<a href="#index-list-view" data-index-box="node" class="btn btn-default btn-xs disabled"
data-container="body"><i class="fa fa-list"></i></a>
</div>
<span class="btn btn-default btn-xs infobtn" title="{% trans "List of compute nodes, also called worker nodes or hypervisors, which run the virtual machines." %}"><i class="fa fa-info-circle"></i></span>
......
......@@ -3,10 +3,12 @@
<div class="panel-heading">
<div class="pull-right toolbar">
<div class="btn-group">
<a href="#index-graph-view" data-index-box="vm" class="btn
btn-default btn-xs" 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" title="{% trans "list view" %}"><i class="fa fa-list"></i></a>
<a href="#index-graph-view" data-index-box="vm" class="btn btn-default btn-xs"
data-container="body"
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"
data-container="body"
title="{% trans "list view" %}"><i class="fa fa-list"></i></a>
</div>
<span class="btn btn-default btn-xs infobtn" title="{% trans "List of your current virtual machines. Favourited ones are ahead of others." %}"><i class="fa fa-info-circle"></i></span>
</div>
......@@ -46,12 +48,15 @@
</style>
<div href="#" class="list-group-item list-group-footer">
<div class="row">
<form action="{% url "dashboard.views.vm-list" %}" method="GET" id="dashboard-vm-search-form">
<div class="col-sm-6 col-xs-6 input-group input-group-sm">
<input id="dashboard-vm-search-input" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<input id="dashboard-vm-search-input" type="text" class="form-control" name="s"
placeholder="{% trans "Search..." %}" />
<div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</form>
<div class="col-sm-6 text-right">
<a class="btn btn-primary btn-xs" href="{% url "dashboard.views.vm-list" %}">
<i class="fa fa-chevron-circle-right"></i>
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load hro %}
{% block content %}
<div class="body-content">
<div class="page-header">
<h1>
{{ object.instance.name }}:
{% if user.is_superuser %}
{{object.readable_name.get_admin_text}}
{% else %}
{{object.readable_name.get_user_text}}
{% endif %}
<h1><i class="fa fa-{{icon}}"></i>
{{ object.instance.name }}: {{object.readable_name|get_text:user}}
</h1>
</div>
<div class="row">
......@@ -58,7 +54,7 @@
<dt>{% trans "result" %}</dt>
<dd><textarea class="form-control">{% if user.is_superuser %}{{object.result.get_admin_text}}{% else %}{{object.result.get_user_text}}{% endif %}</textarea></dd>
<dd><textarea class="form-control">{{object.result|get_text:user}}</textarea></dd>
<dt>{% trans "resultant state" %}</dt>
<dd>{{object.resultant_state|default:'n/a'}}</dd>
......
......@@ -11,7 +11,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="fa fa-time"></i> {% trans "Create lease" %}</h3>
<h3 class="no-margin"><i class="fa fa-clock-o"></i> {% trans "Create lease" %}</h3>
</div>
<div class="panel-body">
{% with form=form %}
......
......@@ -10,7 +10,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="fa fa-time"></i> {% trans "Edit lease" %}</h3>
<h3 class="no-margin"><i class="fa fa-clock-o"></i> {% trans "Edit lease" %}</h3>
</div>
<div class="panel-body">
{% with form=form %}
......
{% load i18n %}
{% load crispy_forms_tags %}
{% block question %}
<p>
{% blocktrans with op=op.name count count=vm_count %}
Do you want to perform the <strong>{{op}}</strong> operation on the following instance?
{% plural %}
Do you want to perform the <strong>{{op}}</strong> operation on the following {{ count }} instances?
{% endblocktrans %}
</p>
<p class="text-info">{{op.description}}</p>
{% endblock %}
<form method="POST" action="{{url}}">{% csrf_token %}
{% block formfields %}{% endblock %}
{% for i in instances %}
<div class="panel panel-default mass-op-panel">
<i class="fa {{ i.get_status_icon }} fa-fw"></i>
{{ i.name }} ({{ i.pk }})
<div style="float: right;" title="{{ i.disabled }}" class="status-icon">
<span class="fa-stack">
<i class="fa fa-stack-2x fa-square {{ i.disabled|yesno:"minus,check" }}"></i>
<i class="fa fa-stack-1x fa-inverse fa-{% if i.disabled %}{{i.disabled_icon|default:"minus"}}{% else %}check{% endif %}"></i>
</span>
</div>
</div>
<input type="checkbox" name="vm" value="{{ i.pk }}" {% if not i.disabled %}checked{% endif %}
style="display: none;"/>
{% endfor %}
<div class="pull-right">
<a class="btn btn-default" href="{% url "dashboard.views.vm-list" %}"
data-dismiss="modal">{% trans "Cancel" %}</a>
<button class="btn btn-{{ opview.effect }}" type="submit" id="op-form-send">
{% if opview.icon %}<i class="fa fa-fw fa-{{opview.icon}}"></i> {% endif %}{{ opview.name|capfirst }}
</button>
</div>
</form>
{% load i18n %}
{% load hro %}
<div id="activity-timeline" class="timeline">
{% for a in activities %}
<div class="activity" data-activity-id="{{ a.pk }}">
......@@ -16,10 +17,7 @@
<div data-activity-id="{{ s.pk }}"
class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}"
>
{% if user.is_superuser %}
{{ s.readable_name.get_admin_text }}
{% else %}
{{ s.readable_name.get_user_text }}{% endif %}
{{ s.readable_name|get_text:user }}
&ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
......
......@@ -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 %}
......@@ -16,6 +16,23 @@
<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>
......@@ -31,7 +48,7 @@
<i class="fa fa-plus"></i> {% trans "new lease" %}
</a>
{% endif %}
<h3 class="no-margin"><i class="fa fa-time"></i> {% trans "Leases" %}</h3>
<h3 class="no-margin"><i class="fa fa-clock-o"></i> {% trans "Leases" %}</h3>
</div>
<div class="panel-body">
<div class="" style="max-width: 600px;">
......
{% 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>
......@@ -98,8 +98,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>
......@@ -111,16 +112,28 @@
</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 %}
<div id="dashboard-vm-details-connect" class="operation-wrapper">
<a id="dashboard-vm-details-connect-button" class="btn btn-xs btn-default{% if not client_download %} operation{% endif %}{% if instance.status != "RUNNING" or not instance.get_connect_uri %} disabled{% endif %}" {% if client_download %}target="_blank"{% endif %} href="{% if not client_download %}{% url "dashboard.views.client-check" %}?vm={{ instance.pk }}{% else %}{{ instance.get_connect_uri}}{% endif %}" title="{% trans "Connect via the CIRCLE Client" %}">
<i class="fa fa-external-link"></i> {% trans "Connect" %}
......
{% load i18n %}
{% load hro %}
<div id="activity-timeline" class="timeline">
{% for a in activities %}
<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" data-activity-id="{{ a.pk }}">
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
<i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-plus{% endif %}"></i>
<i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-{{a.icon}}{% endif %}"></i>
</span>
<strong{% if a.result %} title="{{ a.result.get_user_text }}"{% endif %}>
<strong{% if a.result %} title="{{ a.result|get_text:user }}"{% endif %}>
<a href="{{ a.get_absolute_url }}">
{% if a.times > 1 %}({{ a.times }}x){% endif %}
{{ a.readable_name.get_user_text|capfirst }}</a>
{{ a.readable_name|get_text:user|capfirst }}</a>
{% if a.has_percent %}
- {{ a.percentage }}%
......@@ -33,9 +33,9 @@
<div class="sub-timeline">
{% for s in a.children.all %}
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}{% if s.pk == active.pk %} sub-activity-active{% endif %}">
<span{% if s.result %} title="{{ s.result.get_user_text }}"{% endif %}>
<span{% if s.result %} title="{{ s.result|get_text:user }}"{% endif %}>
<a href="{{ s.get_absolute_url }}">
{{ s.readable_name.get_user_text|capfirst }}</a></span> &ndash;
{{ s.readable_name|get_text:user|capfirst }}</a></span> &ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
......
......@@ -7,7 +7,7 @@
<span class="operation operation-{{op.op}} btn btn-default disabled btn-xs">
{% else %}
<a href="{{op.get_url}}" class="operation operation-{{op.op}} btn
btn-{{op.effect}} btn-xs" title="{{op.name|capfirst}}: {{op.description|truncatewords:20}}">
btn-{{op.effect}} btn-xs" title="{{op.name|capfirst}}: {{op.description|truncatewords:15}}">
{% endif %}
<i class="fa fa-{{op.icon}}"></i>
<span{% if not op.is_preferred %} class="sr-only"{% endif %}>{{op.name}}</span>
......
......@@ -94,12 +94,32 @@
<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 %}
</dd>
</dl>
{% if op.mount_store %}
<strong>{% trans "Store" %}</strong>
<p>
{{ op.mount_store.description }}
</p>
<div class="operation-wrapper">
<a href="{{ op.mount_store.get_url }}" class="btn btn-info btn-xs operation"
{% if op.mount_store.disabled %}disabled{% endif %}>
<i class="fa fa-{{op.mount_store.icon}}"></i>
{{ op.mount_store.name }}
</a>
</div>
{% endif %}
</div>
<div class="col-md-8">
{% if graphite_enabled %}
......
......@@ -15,26 +15,42 @@
</div>
<h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Virtual machines" %}</h3>
</div>
<div class="pull-right" style="max-width: 250px; margin-top: 15px; margin-right: 15px;">
<form action="" method="GET" class="input-group">
<input type="text" name="s"{% if request.GET.s %} value="{{ request.GET.s }}"{% endif %} class="form-control input-tags" placeholder="{% trans "Search..."%}" />
<div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary input-tags" title="search"><i class="fa fa-search"></i></button>
</div>
</form>
</div>
<div class="panel-body vm-list-group-control">
<p>
<div class="panel-body">
<div class="row">
<div class="col-md-8 vm-list-group-control" id="vm-mass-ops">
<strong>{% trans "Group actions" %}</strong>
<button id="vm-list-group-select-all" class="btn btn-info btn-xs">{% trans "Select all" %}</button>
<a class="btn btn-default btn-xs" id="vm-list-group-migrate" disabled><i class="fa fa-truck"></i> {% trans "Migrate" %}</a>
<a disabled href="#" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i> {% trans "Reboot" %}</a>
<a disabled href="#" class="btn btn-default btn-xs"><i class="fa fa-off"></i> {% trans "Shutdown" %}</a>
<a id="vm-list-group-delete" disabled href="#" class="btn btn-danger btn-xs"><i class="fa fa-times"></i> {% trans "Destroy" %}</a>
</p>
{% for o in ops %}
<a href="{{ o.get_url }}" class="btn btn-xs btn-{{ o.effect }} mass-operation"
title="{{ o.name|capfirst }}" disabled>
<i class="fa fa-{{ o.icon }}"></i>
</a>
{% endfor %}
</div><!-- .vm-list-group-control -->
<div class="col-md-4" id="vm-list-search">
<form action="" method="GET">
<div class="input-group">
{{ 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>
</div>
</div><!-- .input-group -->
</form>
</div><!-- .col-md-4 #vm-list-search -->
</div><!-- .row -->
</div><!-- .panel-body -->
<div class="panel-body">
<table class="table table-bordered table-striped table-hover vm-list-table">
<table class="table table-bordered table-striped table-hover vm-list-table"
id="vm-list-table">
<thead><tr>
<th data-sort="int" class="orderable pk sortable vm-list-table-thin" style="min-width: 50px;">
{% trans "ID" as t %}
......@@ -52,26 +68,56 @@
{% 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 }}">
<td class="pk"><div id="vm-{{i.pk}}">{{i.pk}}</div> </td>
<td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">{{ i.name }}</a> </td>
<td class="state">{{ i.get_status_display }}</td>
<td class="state">
<i class="fa fa-fw
{% 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 data-sort-value="{{ i.node.normalized_name }}">{{ i.node.name|default:"-" }}</td>
<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>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="5">
<td colspan="7">
{% if request.GET.s %}
<strong>{% trans "No result." %}</strong>
{% else %}
......@@ -87,6 +133,9 @@
</div>
</div>
<div class="alert alert-info">
You can filter the list by certain attributes (owner, name, status, tags) in the following way: "owner:John Doe name:my little server". If you don't specify any attribute names the filtering will be done by name.
</div>
<div class="alert alert-info">
{% trans "You can select multiple vm instances while holding down the <strong>CTRL</strong> key." %}
......@@ -97,6 +146,5 @@
{% block extra_js %}
<script src="{{ STATIC_URL}}dashboard/vm-list.js"></script>
<script src="{{ STATIC_URL}}dashboard/vm-common.js"></script>
<script src="{{ STATIC_URL}}dashboard/js/stupidtable.min.js"></script>
{% endblock %}
from django.template import Library
register = Library()
@register.filter
def get_text(human_readable, user):
if human_readable is None:
return u""
else:
return human_readable.get_text(user)
......@@ -29,7 +29,7 @@ from django.utils import baseconv
from ..models import Profile
from ..views import InstanceActivityDetail, InstanceActivity
from ..views import vm_ops, Instance, UnsubscribeFormView
from ..views import vm_ops, vm_mass_ops, Instance, UnsubscribeFormView
from ..views import AclUpdateView
from .. import views
......@@ -259,6 +259,114 @@ class VmOperationViewTestCase(unittest.TestCase):
self.assertEquals(rend.status_code, 200)
class VmMassOperationViewTestCase(unittest.TestCase):
def test_available(self):
request = FakeRequestFactory(superuser=True)
view = vm_mass_ops['destroy']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.destroy = Instance._ops['destroy'](inst)
go.return_value = [inst]
self.assertEquals(
view.as_view()(request, pk=1234).render().status_code, 200)
def test_unpermitted_choice(self):
"User has user level, but not the needed ownership."
request = FakeRequestFactory()
view = vm_mass_ops['destroy']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.has_level = lambda self, l: {"user": True, "owner": False}[l]
inst.destroy = Instance._ops['destroy'](inst)
inst.destroy._operate = MagicMock()
go.return_value = [inst]
view.as_view()(request, pk=1234).render()
assert not inst.destroy._operate.called
def test_unpermitted(self):
request = FakeRequestFactory()
view = vm_mass_ops['destroy']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.destroy = Instance._ops['destroy'](inst)
inst.has_level.return_value = False
go.return_value = [inst]
with self.assertRaises(PermissionDenied):
view.as_view()(request, pk=1234).render()
def test_migrate(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=True)
view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.has_level.return_value = True
go.return_value = [inst]
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)['location']
assert not msg.error.called
def test_migrate_failed(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=True)
view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.migrate.async.side_effect = Exception
inst.has_level.return_value = True
go.return_value = [inst]
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)['location']
assert msg.error.called
def test_migrate_wo_permission(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=False)
view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.has_level.return_value = True
go.return_value = [inst]
go4.return_value = MagicMock()
with self.assertRaises(PermissionDenied):
assert view.as_view()(request, pk=1234)['location']
def test_migrate_template(self):
"""check if GET dialog's template can be rendered"""
request = FakeRequestFactory(superuser=True)
view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.has_level.return_value = True
go.return_value = [inst]
self.assertEquals(
view.as_view()(request, pk=1234).render().status_code, 200)
class RenewViewTest(unittest.TestCase):
def test_renew_template(self):
......
......@@ -107,20 +107,6 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.get('/dashboard/vm/1/')
self.assertEqual(response.status_code, 200)
def test_unpermitted_vm_mass_delete(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]})
self.assertEqual(response.status_code, 403)
def test_permitted_vm_mass_delete(self):
c = Client()
self.login(c, 'user2')
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]})
self.assertEqual(response.status_code, 302)
def test_unpermitted_password_change(self):
c = Client()
self.login(c, "user2")
......
......@@ -29,7 +29,7 @@ from .views import (
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete,
VmDetailVncTokenView, VmGraphView, VmList,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveUserView,
GroupRemoveFutureUserView,
......@@ -39,6 +39,7 @@ 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,
......@@ -50,7 +51,6 @@ from .views import (
autocomplete_light.autodiscover()
urlpatterns = patterns(
'',
url(r'^$', IndexView.as_view(), name="dashboard.index"),
url(r'^lease/(?P<pk>\d+)/$', LeaseDetail.as_view(),
......@@ -75,7 +75,7 @@ urlpatterns = patterns(
url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(),
name="dashboard.views.template-delete"),
url(r'^vm/(?P<pk>\d+)/op/', include('dashboard.vm.urls')),
url(r'^vm/', include('dashboard.vm.urls')),
url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
......@@ -89,8 +89,6 @@ urlpatterns = patterns(
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(),
name='dashboard.views.vm-create'),
url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity),
url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(),
name='dashboard.views.vm-activity'),
......@@ -181,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(),
......
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."))
......@@ -17,9 +17,17 @@
from django.conf.urls import patterns, url
from ..views import vm_ops
from ..views import vm_ops, vm_mass_ops
urlpatterns = patterns('',
*(url(r'^%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems()))
urlpatterns = patterns(
'',
*(url(r'^(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems())
)
urlpatterns += patterns(
'',
*(url(r'^mass_op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_mass_ops.iteritems())
)
......@@ -101,7 +101,7 @@ def pull(dir="~/circle/circle"):
@roles('portal')
def update_portal(test=False):
"Update and restart portal+manager"
with _stopped("portal", "mancelery"):
with _stopped("portal", "manager"):
pull()
pip("circle", "~/circle/requirements.txt")
migrate()
......@@ -113,7 +113,7 @@ def update_portal(test=False):
@roles('portal')
def stop_portal(test=False):
"Stop portal and manager"
_stop_services("portal", "mancelery")
_stop_services("portal", "manager")
@roles('node')
......
......@@ -62,6 +62,15 @@ class BuildFirewall:
extra='-j DNAT --to-destination %s:%s' % (rule.host.ipv4,
rule.dport)))
# SNAT rules for machines with public IPv4
for host in Host.objects.exclude(external_ipv4=None).select_related(
'vlan').prefetch_related('vlan__snat_to'):
for vl_out in host.vlan.snat_to.all():
self.add_rules(POSTROUTING=IptRule(
priority=1500, src=(host.ipv4, None),
extra='-o %s -j SNAT --to-source %s' % (
vl_out.name, host.external_ipv4)))
# default outbound NAT rules for VLANs
for vl_in in Vlan.objects.exclude(
snat_ip=None).prefetch_related('snat_to'):
......@@ -183,9 +192,12 @@ def generate_ptr_records():
for host in Host.objects.order_by('vlan').all():
template = host.vlan.reverse_domain
i = host.get_external_ipv4().words
reverse = (host.reverse if host.reverse not in [None, '']
else host.get_fqdn())
if not host.shared_ip and host.external_ipv4: # DMZ
i = host.external_ipv4.words
reverse = host.get_hostname('ipv4', public=True)
else:
i = host.ipv4.words
reverse = host.get_hostname('ipv4', public=False)
# ipv4
if host.ipv4:
......@@ -194,7 +206,7 @@ def generate_ptr_records():
# ipv6
if host.ipv6:
DNS.append("^%s:%s:%s" % (host.ipv6.reverse_dns,
DNS.append("^%s:%s:%s" % (host.ipv6.reverse_dns.rstrip('.'),
reverse, settings['dns_ttl']))
return DNS
......@@ -211,14 +223,14 @@ def generate_records():
'CNAME': 'C%(fqdn)s:%(address)s:%(ttl)s',
'MX': '@%(fqdn)s::%(address)s:%(dist)s:%(ttl)s',
'PTR': '^%(fqdn)s:%(address)s:%(ttl)s',
'TXT': '%(fqdn)s:%(octal)s:%(ttl)s'}
'TXT': "'%(fqdn)s:%(octal)s:%(ttl)s"}
retval = []
for r in Record.objects.all():
params = {'fqdn': r.fqdn, 'address': r.address, 'ttl': r.ttl}
if r.type == 'MX':
params['address'], params['dist'] = r.address.split(':', 2)
params['dist'], params['address'] = r.address.split(':', 2)
if r.type == 'AAAA':
try:
params['octal'] = ipv6_to_octal(r.address)
......
......@@ -22,7 +22,7 @@ from collections import OrderedDict
logger = logging.getLogger()
ipv4_re = re.compile(
r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}')
r'(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}')
class InvalidRuleExcepion(Exception):
......
......@@ -203,12 +203,6 @@ class Rule(models.Model):
elif self.firewall_id:
return 'INPUT' if self.direction == 'in' else 'OUTPUT'
def get_dport_sport(self):
if self.direction == 'in':
return self.dport, self.sport
else:
return self.sport, self.dport
def get_ipt_rules(self, host=None):
# action
action = 'LOG_ACC' if self.action == 'accept' else 'LOG_DROP'
......@@ -221,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:
......@@ -235,9 +229,6 @@ class Rule(models.Model):
if vlan and not vlan.managed:
return retval
# src and dst ports
dport, sport = self.get_dport_sport()
# process foreign vlans
for foreign_vlan in self.foreign_network.vlans.all():
if not foreign_vlan.managed:
......@@ -246,7 +237,7 @@ class Rule(models.Model):
r = IptRule(priority=self.weight, action=action,
proto=self.proto, extra=self.extra,
comment='Rule #%s' % self.pk,
src=src, dst=dst, dport=dport, sport=sport)
src=src, dst=dst, dport=self.dport, sport=self.sport)
chain_name = self.get_chain_name(local=vlan, remote=foreign_vlan)
retval[chain_name] = r
......@@ -539,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
......@@ -575,10 +582,14 @@ class Host(models.Model):
# IPv4
if self.ipv4 is not None:
if not self.shared_ip and self.external_ipv4: # DMZ
ipv4 = self.external_ipv4
else:
ipv4 = self.ipv4
# update existing records
affected_records = Record.objects.filter(
host=self, name=self.hostname,
type='A').update(address=self.ipv4)
type='A').update(address=ipv4)
# create new record
if affected_records == 0:
Record(host=self,
......@@ -605,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))
......@@ -714,6 +738,8 @@ class Host(models.Model):
:type proto: str.
"""
assert proto in ('ipv6', 'ipv4', )
if self.reverse:
return self.reverse
try:
if proto == 'ipv6':
res = self.record_set.filter(type='AAAA',
......@@ -736,7 +762,7 @@ class Host(models.Model):
Return a list of ports with forwarding rules set.
"""
retval = []
for rule in self.rules.all():
for rule in self.rules.filter(dport__isnull=False, direction='in'):
forward = {
'proto': rule.proto,
'private': rule.dport,
......
......@@ -35,7 +35,7 @@ COMMIT
{% if proto == "ipv4" %}
-A FORWARD -p icmp --icmp-type echo-request -g LOG_ACC
{% else %}
-A FORWARD -p icmpv6 --icmpv6-type echo-request -g LOG_ACC
-A FORWARD -p icmpv6 -g LOG_ACC
{% endif %}
# initialize INPUT chain
......@@ -45,6 +45,11 @@ COMMIT
-A INPUT -m state --state INVALID -g LOG_DROP
-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
{% if proto == "ipv4" %}
-A INPUT -p icmp --icmp-type echo-request -g LOG_ACC
{% else %}
-A INPUT -p icmpv6 -g LOG_ACC
{% endif %}
# initialize OUTPUT chain
-A OUTPUT -m state --state INVALID -g LOG_DROP
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -31,7 +31,7 @@ celery = Celery('manager',
'storage.tasks.local_tasks',
'storage.tasks.periodic_tasks',
'firewall.tasks.local_tasks',
'monitor.tasks.local_periodic_tasks',
'dashboard.tasks.local_periodic_tasks',
])
celery.conf.update(
......@@ -41,49 +41,19 @@ celery.conf.update(
CELERY_QUEUES=(
Queue(HOSTNAME + '.man', Exchange('manager', type='direct'),
routing_key="manager"),
Queue(HOSTNAME + '.monitor', Exchange('monitor', type='direct'),
routing_key="monitor"),
),
CELERYBEAT_SCHEDULE={
'vm.update_domain_states': {
'task': 'vm.tasks.local_periodic_tasks.update_domain_states',
'schedule': timedelta(seconds=10),
'options': {'queue': 'localhost.man'}
},
'vm.garbage_collector': {
'task': 'vm.tasks.local_periodic_tasks.garbage_collector',
'schedule': timedelta(minutes=10),
'options': {'queue': 'localhost.man'}
},
'storage.periodic_tasks': {
'task': 'storage.tasks.periodic_tasks.garbage_collector',
'schedule': timedelta(hours=1),
'options': {'queue': 'localhost.man'}
},
'dashboard.local_periodic_tasks': {
'dashboard.send_email_notifications': {
'task': 'dashboard.tasks.local_periodic_tasks.'
'send_email_notifications',
'schedule': timedelta(hours=24),
'options': {'queue': 'localhost.man'}
},
'monitor.measure_response_time': {
'task': 'monitor.tasks.local_periodic_tasks.'
'measure_response_time',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.man'}
},
'monitor.check_celery_queues': {
'task': 'monitor.tasks.local_periodic_tasks.'
'check_celery_queues',
'schedule': timedelta(seconds=60),
'options': {'queue': 'localhost.man'}
},
'monitor.instance_per_template': {
'task': 'monitor.tasks.local_periodic_tasks.'
'instance_per_template',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.man'}
},
}
)
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from celery import Celery
from datetime import timedelta
from kombu import Queue, Exchange
from os import getenv
HOSTNAME = "localhost"
CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/")
celery = Celery('monitor',
broker=getenv("AMQP_URI"),
include=['vm.tasks.local_periodic_tasks',
'monitor.tasks.local_periodic_tasks',
])
celery.conf.update(
CELERY_RESULT_BACKEND='cache',
CELERY_CACHE_BACKEND=CACHE_URI,
CELERY_TASK_RESULT_EXPIRES=300,
CELERY_QUEUES=(
Queue(HOSTNAME + '.monitor', Exchange('monitor', type='direct'),
routing_key="monitor"),
),
CELERYBEAT_SCHEDULE={
'vm.update_domain_states': {
'task': 'vm.tasks.local_periodic_tasks.update_domain_states',
'schedule': timedelta(seconds=10),
'options': {'queue': 'localhost.monitor'}
},
'monitor.measure_response_time': {
'task': 'monitor.tasks.local_periodic_tasks.'
'measure_response_time',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.monitor'}
},
'monitor.check_celery_queues': {
'task': 'monitor.tasks.local_periodic_tasks.'
'check_celery_queues',
'schedule': timedelta(seconds=60),
'options': {'queue': 'localhost.monitor'}
},
'monitor.instance_per_template': {
'task': 'monitor.tasks.local_periodic_tasks.'
'instance_per_template',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.monitor'}
},
}
)
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from celery import Celery
from datetime import timedelta
from kombu import Queue, Exchange
from os import getenv
HOSTNAME = "localhost"
CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/")
celery = Celery('manager.slow',
broker=getenv("AMQP_URI"),
include=['vm.tasks.local_tasks',
'vm.tasks.local_periodic_tasks',
'storage.tasks.local_tasks',
'storage.tasks.periodic_tasks',
])
celery.conf.update(
CELERY_RESULT_BACKEND='cache',
CELERY_CACHE_BACKEND=CACHE_URI,
CELERY_TASK_RESULT_EXPIRES=300,
CELERY_QUEUES=(
Queue(HOSTNAME + '.man.slow', Exchange('manager.slow', type='direct'),
routing_key="manager.slow"),
),
CELERYBEAT_SCHEDULE={
'vm.garbage_collector': {
'task': 'vm.tasks.local_periodic_tasks.garbage_collector',
'schedule': timedelta(minutes=10),
'options': {'queue': 'localhost.man.slow'}
},
}
)
......@@ -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.
......
......@@ -28,7 +28,7 @@ def check_queue(storage, queue_id, priority):
if priority is not None:
queue_name = queue_name + "." + priority
inspect = celery.control.inspect()
inspect.timeout = 0.1
inspect.timeout = 0.5
active_queues = inspect.active_queues()
if active_queues is None:
return False
......
......@@ -90,7 +90,8 @@ class InstanceActivity(ActivityModel):
@classmethod
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):
readable_name = _normalize_readable_name(readable_name, code_suffix)
# Check for concurrent activities
......@@ -100,14 +101,14 @@ class InstanceActivity(ActivityModel):
activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix)
act = cls(activity_code=activity_code, instance=instance, parent=None,
resultant_state=None, started=timezone.now(),
resultant_state=resultant_state, started=timezone.now(),
readable_name_data=readable_name.to_dict(),
task_uuid=task_uuid, user=user)
act.save()
return act
def create_sub(self, code_suffix, task_uuid=None, concurrency_check=True,
readable_name=None):
readable_name=None, resultant_state=None):
readable_name = _normalize_readable_name(readable_name, code_suffix)
# Check for concurrent activities
......@@ -117,7 +118,8 @@ class InstanceActivity(ActivityModel):
act = InstanceActivity(
activity_code=join_activity_code(self.activity_code, code_suffix),
instance=self.instance, parent=self, resultant_state=None,
instance=self.instance, parent=self,
resultant_state=resultant_state,
readable_name_data=readable_name.to_dict(), started=timezone.now(),
task_uuid=task_uuid, user=self.user)
act.save()
......@@ -190,18 +192,23 @@ class InstanceActivity(ActivityModel):
readable_name=readable_name)
return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
def get_operation(self):
return self.instance.get_operation_from_activity_code(
self.activity_code)
@contextmanager
def instance_activity(code_suffix, instance, on_abort=None, on_commit=None,
task_uuid=None, user=None, concurrency_check=True,
readable_name=None):
readable_name=None, resultant_state=None):
"""Create a transactional context for an instance activity.
"""
if not readable_name:
warn("Set readable_name", stacklevel=3)
act = InstanceActivity.create(code_suffix, instance, task_uuid, user,
concurrency_check,
readable_name=readable_name)
readable_name=readable_name,
resultant_state=resultant_state)
return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
......
......@@ -32,6 +32,7 @@ from django.core.exceptions import PermissionDenied
from django.db.models import (BooleanField, CharField, DateTimeField,
IntegerField, ForeignKey, Manager,
ManyToManyField, permalink, SET_NULL, TextField)
from django.db import IntegrityError
from django.dispatch import Signal
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop
......@@ -293,6 +294,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
message = ugettext_noop(
"Instance %(instance)s has already been destroyed.")
class NoAgentError(InstanceError):
message = ugettext_noop(
"No agent software is running on instance %(instance)s.")
class WrongStateError(InstanceError):
message = ugettext_noop(
"Current state (%(state)s) of instance %(instance)s is "
......@@ -483,9 +488,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
"""
try:
datastore = self.disks.all()[0].datastore
except:
return None
else:
except IndexError:
from storage.models import DataStore
datastore = DataStore.objects.get()
path = datastore.path + '/' + self.vm_name + '.dump'
return {'datastore': datastore, 'path': path}
......@@ -507,7 +513,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):
......@@ -919,8 +929,16 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def allocate_vnc_port(self):
if self.vnc_port is None:
while True:
try:
self.vnc_port = find_unused_vnc_port()
self.save()
except IntegrityError:
# Another thread took this port get another one
logger.debug("Port %s is in use.", self.vnc_port)
pass
else:
break
def yield_vnc_port(self):
if self.vnc_port is not None:
......@@ -936,7 +954,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
'ERROR': 'fa-warning',
'PENDING': 'fa-rocket',
'DESTROYED': 'fa-trash-o',
'MIGRATING': 'fa-truck'}.get(self.status, 'fa-question')
'MIGRATING': 'fa-truck migrating-icon'
}.get(self.status, 'fa-question')
def get_activities(self, user=None):
acts = (self.activity_log.filter(parent=None).
......@@ -986,3 +1005,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
instance=self, succeeded=None, parent=None).latest("started")
except InstanceActivity.DoesNotExist:
return None
def is_in_status_change(self):
latest = self.get_latest_activity_in_progress()
return (latest and latest.resultant_state is not None
and self.status != latest.resultant_state)
......@@ -16,6 +16,7 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, unicode_literals
from functools import update_wrapper
from logging import getLogger
from warnings import warn
import requests
......@@ -51,6 +52,8 @@ def node_available(function):
return function(self, *args, **kwargs)
else:
return None
update_wrapper(decorate, function)
decorate._original = function
return decorate
......@@ -257,7 +260,7 @@ class Node(OperatedMixin, TimeStampedModel):
@method_cache(10)
def monitor_info(self):
metrics = ('cpu.usage', 'memory.usage')
prefix = 'circle.%s.' % self.name
prefix = 'circle.%s.' % self.host.hostname
params = [('target', '%s%s' % (prefix, metric))
for metric in metrics]
params.append(('from', '-5min'))
......
......@@ -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))
......@@ -49,6 +49,17 @@ def send_init_commands(instance, act, vm):
queue=queue, args=(vm, instance.primary_host.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 exclude(tarinfo):
if tarinfo.name.startswith('./.git'):
......@@ -86,7 +97,7 @@ def agent_started(vm, version=None):
if version and version != settings.AGENT_VERSION:
try:
update_agent(vm, instance, act)
update_agent(instance, act)
except TimeoutError:
pass
else:
......@@ -94,8 +105,9 @@ def agent_started(vm, version=None):
if not initialized:
measure_boot_time(instance)
send_init_commands(instance, act, vm)
send_init_commands(instance, act)
send_networking_commands(instance, act)
with act.sub_activity(
'start_access_server',
readable_name=ugettext_noop('start access server')
......@@ -134,9 +146,16 @@ def agent_stopped(vm):
pass
def update_agent(instance, vm, act=None):
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.sub_activity(
act = act.sub_activity(
'update',
readable_name=create_readable(
ugettext_noop('update to %(version)s'),
......@@ -150,5 +169,6 @@ def update_agent(instance, vm, act=None):
version=settings.AGENT_VERSION))
with act:
queue = instance.get_remote_queue_name("agent")
update.apply_async(queue=queue,
args=(vm, create_agent_tar())).get(timeout=10)
update.apply_async(
queue=queue,
args=(instance.vm_name, create_agent_tar())).get(timeout=10)
......@@ -55,7 +55,7 @@ def get_queues():
result = cache.get(key)
if result is None:
inspect = celery.control.inspect()
inspect.timeout = 0.1
inspect.timeout = 0.5
result = inspect.active_queues()
logger.debug('Queue list of length %d cached.', len(result))
cache.set(key, result, 10)
......
description "CIRCLE manager"
start on runlevel [2345]
stop on runlevel [!2345]
pre-start script
start moncelery
start mancelery
start slowcelery
end script
post-stop script
stop moncelery
stop mancelery
stop slowcelery
end script
description "CIRCLE mancelery"
start on runlevel [2345]
stop on runlevel [!2345]
description "CIRCLE mancelery for common jobs"
respawn
respawn limit 30 30
setgid cloud
setuid cloud
......@@ -12,6 +10,5 @@ script
cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 1
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 10
end script
description "CIRCLE moncelery for monitoring jobs"
respawn
respawn limit 30 30
setgid cloud
setuid cloud
script
cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate
. /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
description "CIRCLE mancelery for slow jobs"
respawn
respawn limit 30 30
setgid cloud
setuid cloud
script
cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate
. /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