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': ...@@ -368,9 +368,9 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
from shutilwhich import which from shutilwhich import which
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
# INSTALLED_APPS += ( # needed only for testing djangosaml2 INSTALLED_APPS += (
# 'djangosaml', 'djangosaml2',
# ) )
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'djangosaml2.backends.Saml2Backend', 'djangosaml2.backends.Saml2Backend',
......
...@@ -20,9 +20,12 @@ ...@@ -20,9 +20,12 @@
from os import environ from os import environ
from sys import argv
from base import * # noqa from base import * # noqa
if 'runserver' in argv:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
########## HOST CONFIGURATION ########## HOST CONFIGURATION
# See: https://docs.djangoproject.com/en/1.5/releases/1.5/ # See: https://docs.djangoproject.com/en/1.5/releases/1.5/
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
from collections import deque from collections import deque
from contextlib import contextmanager from contextlib import contextmanager
from functools import update_wrapper
from hashlib import sha224 from hashlib import sha224
from itertools import chain, imap from itertools import chain, imap
from logging import getLogger from logging import getLogger
...@@ -26,6 +27,7 @@ from warnings import warn ...@@ -26,6 +27,7 @@ from warnings import warn
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import ( from django.db.models import (
CharField, DateTimeField, ForeignKey, NullBooleanField CharField, DateTimeField, ForeignKey, NullBooleanField
...@@ -36,6 +38,7 @@ from django.utils.functional import Promise ...@@ -36,6 +38,7 @@ from django.utils.functional import Promise
from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.utils.translation import ugettext_lazy as _, ugettext_noop
from jsonfield import JSONField from jsonfield import JSONField
from manager.mancelery import celery
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -212,6 +215,38 @@ class ActivityModel(TimeStampedModel): ...@@ -212,6 +215,38 @@ class ActivityModel(TimeStampedModel):
self.result_data = None if value is None else value.to_dict() self.result_data = None if value is None else value.to_dict()
@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 def method_cache(memcached_seconds=60, instance_seconds=5): # noqa
"""Cache return value of decorated method to memcached and memory. """Cache return value of decorated method to memcached and memory.
...@@ -233,9 +268,11 @@ def method_cache(memcached_seconds=60, instance_seconds=5): # noqa ...@@ -233,9 +268,11 @@ def method_cache(memcached_seconds=60, instance_seconds=5): # noqa
def inner_cache(method): def inner_cache(method):
method_name = method.__name__
def get_key(instance, *args, **kwargs): def get_key(instance, *args, **kwargs):
return sha224(unicode(method.__module__) + return sha224(unicode(method.__module__) +
unicode(method.__name__) + method_name +
unicode(instance.id) + unicode(instance.id) +
unicode(args) + unicode(args) +
unicode(kwargs)).hexdigest() unicode(kwargs)).hexdigest()
...@@ -254,21 +291,31 @@ def method_cache(memcached_seconds=60, instance_seconds=5): # noqa ...@@ -254,21 +291,31 @@ def method_cache(memcached_seconds=60, instance_seconds=5): # noqa
if vals['time'] + instance_seconds > now: if vals['time'] + instance_seconds > now:
# has valid on class cache, return that # has valid on class cache, return that
result = vals['value'] result = vals['value']
setattr(instance, key, {'time': now, 'value': result})
if result is None: if result is None:
result = cache.get(key) result = cache.get(key)
if invalidate or (result is None): if invalidate or (result is None):
# all caches failed, call the actual method logger.debug("all caches failed, compute now")
result = method(instance, *args, **kwargs) result = compute_cached(method, instance, memcached_seconds,
# save to memcache and class attr key, time(), *args, **kwargs)
cache.set(key, result, memcached_seconds)
setattr(instance, key, {'time': now, 'value': result}) setattr(instance, key, {'time': now, 'value': result})
logger.debug('Value of <%s>.%s(%s)=<%s> saved to cache.', elif not cache.get("%s.cached" % key):
unicode(instance), method.__name__, logger.debug("caches expiring, compute async")
unicode(args), unicode(result)) 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 return result
update_wrapper(x, method)
x._original = method
return x return x
return inner_cache return inner_cache
...@@ -367,6 +414,10 @@ class HumanReadableObject(object): ...@@ -367,6 +414,10 @@ class HumanReadableObject(object):
self._set_values(user_text_template, admin_text_template, params) self._set_values(user_text_template, admin_text_template, params)
def _set_values(self, 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.user_text_template = user_text_template
self.admin_text_template = admin_text_template self.admin_text_template = admin_text_template
self.params = params self.params = params
...@@ -405,6 +456,12 @@ class HumanReadableObject(object): ...@@ -405,6 +456,12 @@ class HumanReadableObject(object):
self.user_text_template, unicode(self.params)) self.user_text_template, unicode(self.params))
return self.user_text_template 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): def to_dict(self):
return {"user_text_template": self.user_text_template, return {"user_text_template": self.user_text_template,
"admin_text_template": self.admin_text_template, "admin_text_template": self.admin_text_template,
...@@ -435,13 +492,34 @@ class HumanReadableException(HumanReadableObject, Exception): ...@@ -435,13 +492,34 @@ class HumanReadableException(HumanReadableObject, Exception):
self.level = "error" self.level = "error"
def send_message(self, request, level=None): def send_message(self, request, level=None):
if request.user and request.user.is_superuser: msg = self.get_text(request.user)
msg = self.get_admin_text()
else:
msg = self.get_user_text()
getattr(messages, level or self.level)(request, msg) 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): def humanize_exception(message, exception=None, level=None, **params):
"""Return new dynamic-class exception which is based on """Return new dynamic-class exception which is based on
HumanReadableException and the original class with the dict of exception. HumanReadableException and the original class with the dict of exception.
......
...@@ -18,10 +18,10 @@ ...@@ -18,10 +18,10 @@
from inspect import getargspec from inspect import getargspec
from logging import getLogger from logging import getLogger
from .models import activity_context, has_suffix
from django.core.exceptions import PermissionDenied, ImproperlyConfigured 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__) logger = getLogger(__name__)
...@@ -31,6 +31,7 @@ class Operation(object): ...@@ -31,6 +31,7 @@ class Operation(object):
""" """
async_queue = 'localhost.man' async_queue = 'localhost.man'
required_perms = None required_perms = None
superuser_required = False
do_not_call_in_templates = True do_not_call_in_templates = True
abortable = False abortable = False
has_percentage = False has_percentage = False
...@@ -143,13 +144,26 @@ class Operation(object): ...@@ -143,13 +144,26 @@ class Operation(object):
def check_precond(self): def check_precond(self):
pass pass
def check_auth(self, user): @classmethod
if self.required_perms is None: 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( raise ImproperlyConfigured(
"Set required_perms to () if none needed.") "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." raise PermissionDenied("%s doesn't have the required permissions."
% user) % 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): def create_activity(self, parent, user, kwargs):
raise NotImplementedError raise NotImplementedError
...@@ -185,14 +199,17 @@ class OperatedMixin(object): ...@@ -185,14 +199,17 @@ class OperatedMixin(object):
def __getattr__(self, name): def __getattr__(self, name):
# NOTE: __getattr__ is only called if the attribute doesn't already # NOTE: __getattr__ is only called if the attribute doesn't already
# exist in your __dict__ # 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, {}) ops = getattr(cls, operation_registry_name, {})
op = ops.get(name) op = ops.get(name)
if op: if op:
return op(self) return op
else: else:
raise AttributeError("%r object has no attribute %r" % raise AttributeError("%r object has no attribute %r" %
(self.__class__.__name__, name)) (cls.__name__, name))
def get_available_operations(self, user): def get_available_operations(self, user):
"""Yield Operations that match permissions of user and preconditions. """Yield Operations that match permissions of user and preconditions.
......
...@@ -21,18 +21,22 @@ from django import contrib ...@@ -21,18 +21,22 @@ from django import contrib
from django.contrib.auth.admin import UserAdmin, GroupAdmin from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from dashboard.models import Profile, GroupProfile from dashboard.models import Profile, GroupProfile, ConnectCommand
class ProfileInline(contrib.admin.TabularInline): class ProfileInline(contrib.admin.TabularInline):
model = Profile model = Profile
class CommandInline(contrib.admin.TabularInline):
model = ConnectCommand
class GroupProfileInline(contrib.admin.TabularInline): class GroupProfileInline(contrib.admin.TabularInline):
model = GroupProfile model = GroupProfile
UserAdmin.inlines = (ProfileInline, ) UserAdmin.inlines = (ProfileInline, CommandInline, )
GroupAdmin.inlines = (GroupProfileInline, ) GroupAdmin.inlines = (GroupProfileInline, )
contrib.admin.site.unregister(User) contrib.admin.site.unregister(User)
......
...@@ -39,7 +39,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm ...@@ -39,7 +39,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.forms.widgets import TextInput, HiddenInput from django.forms.widgets import TextInput, HiddenInput
from django.template import Context from django.template import Context
from django.template.loader import render_to_string 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 sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
...@@ -54,7 +54,9 @@ from .models import Profile, GroupProfile ...@@ -54,7 +54,9 @@ from .models import Profile, GroupProfile
from circle.settings.base import LANGUAGES, MAX_NODE_RAM from circle.settings.base import LANGUAGES, MAX_NODE_RAM
from django.utils.translation import string_concat from django.utils.translation import string_concat
from .virtvalidator import domain_validator from .validators import domain_validator
from dashboard.models import ConnectCommand
LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")")) LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")"))
for l in LANGUAGES) for l in LANGUAGES)
...@@ -176,7 +178,14 @@ class GroupCreateForm(forms.ModelForm): ...@@ -176,7 +178,14 @@ class GroupCreateForm(forms.ModelForm):
self.fields['org_id'] = forms.ChoiceField( self.fields['org_id'] = forms.ChoiceField(
# TRANSLATORS: directory like in LDAP # TRANSLATORS: directory like in LDAP
choices=choices, required=False, label=_('Directory identifier')) choices=choices, required=False, label=_('Directory identifier'))
if not new_groups: if new_groups:
self.fields['org_id'].help_text = _(
"If you select an item here, the members of this directory "
"group will be automatically added to the group at the time "
"they log in. Please note that other users (those with "
"permissions like yours) may also automatically become a "
"group co-owner).")
else:
self.fields['org_id'].widget = HiddenInput() self.fields['org_id'].widget = HiddenInput()
def save(self, commit=True): def save(self, commit=True):
...@@ -634,12 +643,8 @@ class LeaseForm(forms.ModelForm): ...@@ -634,12 +643,8 @@ class LeaseForm(forms.ModelForm):
Field('name'), Field('name'),
Field("suspend_interval_seconds", type="hidden", value="0"), Field("suspend_interval_seconds", type="hidden", value="0"),
Field("delete_interval_seconds", type="hidden", value="0"), Field("delete_interval_seconds", type="hidden", value="0"),
HTML(string_concat("<label>", _("Suspend in"), "</label>")),
Div( Div(
Div(
HTML(_("Suspend in")),
css_class="input-group-addon",
style="width: 100px;",
),
NumberField("suspend_hours", css_class="form-control"), NumberField("suspend_hours", css_class="form-control"),
Div( Div(
HTML(_("hours")), HTML(_("hours")),
...@@ -662,12 +667,8 @@ class LeaseForm(forms.ModelForm): ...@@ -662,12 +667,8 @@ class LeaseForm(forms.ModelForm):
), ),
css_class="input-group interval-input", css_class="input-group interval-input",
), ),
HTML(string_concat("<label>", _("Delete in"), "</label>")),
Div( Div(
Div(
HTML(_("Delete in")),
css_class="input-group-addon",
style="width: 100px;",
),
NumberField("delete_hours", css_class="form-control"), NumberField("delete_hours", css_class="form-control"),
Div( Div(
HTML(_("hours")), HTML(_("hours")),
...@@ -691,7 +692,7 @@ class LeaseForm(forms.ModelForm): ...@@ -691,7 +692,7 @@ class LeaseForm(forms.ModelForm):
css_class="input-group interval-input", css_class="input-group interval-input",
) )
) )
helper.add_input(Submit("submit", "Save changes")) helper.add_input(Submit("submit", _("Save changes")))
return helper return helper
class Meta: class Meta:
...@@ -703,6 +704,8 @@ class VmRenewForm(forms.Form): ...@@ -703,6 +704,8 @@ class VmRenewForm(forms.Form):
force = forms.BooleanField(required=False, label=_( force = forms.BooleanField(required=False, label=_(
"Set expiration times even if they are shorter than " "Set expiration times even if they are shorter than "
"the current value.")) "the current value."))
save = forms.BooleanField(required=False, label=_(
"Save selected lease."))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices') choices = kwargs.pop('choices')
...@@ -714,6 +717,32 @@ class VmRenewForm(forms.Form): ...@@ -714,6 +717,32 @@ class VmRenewForm(forms.Form):
empty_label=None, label=_('Length'))) empty_label=None, label=_('Length')))
if len(choices) < 2: if len(choices) < 2:
self.fields['lease'].widget = HiddenInput() 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 @property
def helper(self): def helper(self):
...@@ -1037,6 +1066,22 @@ class UserKeyForm(forms.ModelForm): ...@@ -1037,6 +1066,22 @@ class UserKeyForm(forms.ModelForm):
return super(UserKeyForm, self).clean() return super(UserKeyForm, self).clean()
class ConnectCommandForm(forms.ModelForm):
class Meta:
fields = ('name', 'access_method', 'template')
model = ConnectCommand
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super(ConnectCommandForm, self).__init__(*args, **kwargs)
def clean(self):
if self.user:
self.instance.user = self.user
return super(ConnectCommandForm, self).clean()
class TraitsForm(forms.ModelForm): class TraitsForm(forms.ModelForm):
class Meta: class Meta:
...@@ -1138,3 +1183,53 @@ class VmResourcesForm(forms.ModelForm): ...@@ -1138,3 +1183,53 @@ class VmResourcesForm(forms.ModelForm):
class Meta: class Meta:
model = Instance model = Instance
fields = ('num_cores', 'priority', 'ram_size', ) 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 ...@@ -46,8 +46,10 @@ from acl.models import AclBase
from common.models import HumanReadableObject, create_readable, Encoder from common.models import HumanReadableObject, create_readable, Encoder
from vm.tasks.agent_tasks import add_keys, del_keys from vm.tasks.agent_tasks import add_keys, del_keys
from vm.models.instance import ACCESS_METHODS
from .store_api import Store, NoStoreException, NotOkException from .store_api import Store, NoStoreException, NotOkException
from .validators import connect_command_template_validator
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -100,6 +102,25 @@ class Notification(TimeStampedModel): ...@@ -100,6 +102,25 @@ class Notification(TimeStampedModel):
self.message_data = None if value is None else value.to_dict() self.message_data = None if value is None else value.to_dict()
class ConnectCommand(Model):
user = ForeignKey(User, related_name='command_set')
access_method = CharField(max_length=10, choices=ACCESS_METHODS,
verbose_name=_('access method'),
help_text=_('Type of the remote access method.'))
name = CharField(max_length="128", verbose_name=_('name'), blank=False,
help_text=_("Name of your custom command."))
template = CharField(blank=True, null=True, max_length=256,
verbose_name=_('command template'),
help_text=_('Template for connection command string. '
'Available parameters are: '
'username, password, '
'host, port.'),
validators=[connect_command_template_validator])
def __unicode__(self):
return self.template
class Profile(Model): class Profile(Model):
user = OneToOneField(User) user = OneToOneField(User)
preferred_language = CharField(verbose_name=_('preferred language'), preferred_language = CharField(verbose_name=_('preferred language'),
...@@ -129,6 +150,25 @@ class Profile(Model): ...@@ -129,6 +150,25 @@ class Profile(Model):
default=2048 * 1024 * 1024, default=2048 * 1024 * 1024,
help_text=_('Disk quota in mebibytes.')) help_text=_('Disk quota in mebibytes.'))
def get_connect_commands(self, instance, use_ipv6=False):
""" Generate connection command based on template."""
single_command = instance.get_connect_command(use_ipv6)
if single_command: # can we even connect to that VM
commands = self.user.command_set.filter(
access_method=instance.access_method)
if commands.count() < 1:
return [single_command]
else:
return [
command.template % {
'port': instance.get_connect_port(use_ipv6=use_ipv6),
'host': instance.get_connect_host(use_ipv6=use_ipv6),
'password': instance.pw,
'username': 'cloud',
} for command in commands]
else:
return []
def notify(self, subject, template, context=None, valid_until=None, def notify(self, subject, template, context=None, valid_until=None,
**kwargs): **kwargs):
if context is not None: if context is not None:
......
...@@ -654,7 +654,8 @@ textarea[name="list-new-namelist"] { ...@@ -654,7 +654,8 @@ textarea[name="list-new-namelist"] {
width: 130px; width: 130px;
} }
#vm-details-connection-string-copy { .vm-details-connection-string-copy,
#vm-details-pw-show {
cursor: pointer; cursor: pointer;
} }
...@@ -681,10 +682,9 @@ textarea[name="list-new-namelist"] { ...@@ -681,10 +682,9 @@ textarea[name="list-new-namelist"] {
max-width: 200px; max-width: 200px;
} }
#dashboard-vm-details-connect-command { .dashboard-vm-details-connect-command {
/* for mobile view */ /* for mobile view */
margin-bottom: 20px; margin-bottom: 20px;
} }
#store-list-list { #store-list-list {
...@@ -867,3 +867,92 @@ textarea[name="list-new-namelist"] { ...@@ -867,3 +867,92 @@ textarea[name="list-new-namelist"] {
border-bottom: 1px dotted #aaa; border-bottom: 1px dotted #aaa;
padding: 5px 0px; padding: 5px 0px;
} }
#profile-key-list-table td:last-child, #profile-key-list-table th:last-child,
#profile-command-list-table td:last-child, #profile-command-list-table th:last-child,
#profile-command-list-table td:nth-child(2), #profile-command-list-table th:nth-child(2) {
text-align: center;
vertical-align: middle;
}
#vm-list-table .migrating-icon {
-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 () { ...@@ -258,14 +258,15 @@ $(function () {
html += '<div class="list-group-item list-group-item-last">' + gettext("No result") + '</div>'; html += '<div class="list-group-item list-group-item-last">' + gettext("No result") + '</div>';
$("#dashboard-vm-list").html(html); $("#dashboard-vm-list").html(html);
$('.title-favourite').tooltip({'placement': 'right'}); $('.title-favourite').tooltip({'placement': 'right'});
});
// if there is only one result and ENTER is pressed redirect $("#dashboard-vm-search-form").submit(function() {
if(e.keyCode == 13 && search_result.length == 1) { var vm_list_items = $("#dashboard-vm-list .list-group-item");
window.location.href = "/dashboard/vm/" + search_result[0].pk + "/"; if(vm_list_items.length == 1 && vm_list_items.first().prop("href")) {
} window.location.href = vm_list_items.first().prop("href");
if(e.keyCode == 13 && search_result.length > 1 && input.length > 0) { return false;
window.location.href = "/dashboard/vm/list/?s=" + input;
} }
return true;
}); });
/* search for nodes */ /* search for nodes */
...@@ -494,14 +495,19 @@ function addSliderMiscs() { ...@@ -494,14 +495,19 @@ function addSliderMiscs() {
ram_fire = true; ram_fire = true;
$(".ram-slider").simpleSlider("setValue", parseInt(val)); $(".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-priority-slider").simpleSlider("setDisabled", $(".cpu-priority-input").prop("disabled"));
$(".cpu-count-slider").simpleSlider("setDisabled", $(".cpu-count-input").prop("disabled")); $(".cpu-count-slider").simpleSlider("setDisabled", $(".cpu-count-input").prop("disabled"));
$(".ram-slider").simpleSlider("setDisabled", $(".ram-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 /* deletes the VM with the pk
* if dir is true, then redirect to the dashboard landing page * if dir is true, then redirect to the dashboard landing page
...@@ -632,3 +638,11 @@ function noJS() { ...@@ -632,3 +638,11 @@ function noJS() {
$('.no-js-hidden').show(); $('.no-js-hidden').show();
$('.js-hidden').hide(); $('.js-hidden').hide();
} }
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
var ctrlDown, shiftDown = false;
var ctrlKey = 17;
var shiftKey = 16;
var selected = [];
$(function() { $(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 */ /* rename */
$("#group-list-rename-button, .group-details-rename-button").click(function() { $("#group-list-rename-button, .group-details-rename-button").click(function() {
$("#group-list-column-name", $(this).closest("tr")).hide(); $("#group-list-column-name", $(this).closest("tr")).hide();
...@@ -113,51 +34,4 @@ $(function() { ...@@ -113,51 +34,4 @@ $(function() {
return false; 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() { ...@@ -39,6 +39,7 @@ $(function() {
$(".template-list-table thead th").css("cursor", "pointer"); $(".template-list-table thead th").css("cursor", "pointer");
$(".template-list-table th a").on("click", function(event) { $(".template-list-table th a").on("click", function(event) {
if(!$(this).closest("th").data("sort")) return true;
event.preventDefault(); event.preventDefault();
}); });
}); });
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
$(function() { $(function() {
/* vm operations */ /* 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'); var icon = $(this).children("i").addClass('fa-spinner fa-spin');
$.ajax({ $.ajax({
......
...@@ -28,6 +28,9 @@ function vmCreateLoaded() { ...@@ -28,6 +28,9 @@ function vmCreateLoaded() {
$('#create-modal').on('hidden.bs.modal', function() { $('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove(); $('#create-modal').remove();
}); });
$("#create-modal").on("shown.bs.modal", function() {
setDefaultSliderValues();
});
}); });
return false; return false;
}); });
...@@ -217,6 +220,8 @@ function vmCustomizeLoaded() { ...@@ -217,6 +220,8 @@ function vmCustomizeLoaded() {
}); });
if(error) return true; if(error) return true;
$(this).find("i").prop("class", "fa fa-spinner fa-spin");
$.ajax({ $.ajax({
url: '/dashboard/vm/create/', url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
......
...@@ -105,19 +105,20 @@ $(function() { ...@@ -105,19 +105,20 @@ $(function() {
$("#vm-details-pw-show").click(function() { $("#vm-details-pw-show").click(function() {
var input = $(this).parent("div").children("input"); var input = $(this).parent("div").children("input");
var eye = $(this).children("#vm-details-pw-eye"); var eye = $(this).children("#vm-details-pw-eye");
var span = $(this);
eye.tooltip("destroy") span.tooltip("destroy")
if(eye.hasClass("fa-eye")) { if(eye.hasClass("fa-eye")) {
eye.removeClass("fa-eye").addClass("fa-eye-slash"); eye.removeClass("fa-eye").addClass("fa-eye-slash");
input.prop("type", "text"); input.prop("type", "text");
input.focus(); input.select();
eye.prop("title", "Hide password"); span.prop("title", gettext("Hide password"));
} else { } else {
eye.removeClass("fa-eye-slash").addClass("fa-eye"); eye.removeClass("fa-eye-slash").addClass("fa-eye");
input.prop("type", "password"); input.prop("type", "password");
eye.prop("title", "Show password"); span.prop("title", gettext("Show password"));
} }
eye.tooltip(); span.tooltip();
}); });
/* change password confirmation */ /* change password confirmation */
...@@ -186,18 +187,7 @@ $(function() { ...@@ -186,18 +187,7 @@ $(function() {
success: function(re, textStatus, xhr) { success: function(re, textStatus, xhr) {
/* remove the html element */ /* remove the html element */
$('a[data-interface-pk="' + data.pk + '"]').closest("div").fadeOut(); $('a[data-interface-pk="' + data.pk + '"]').closest("div").fadeOut();
location.reload();
/* 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);
}
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger') addMessage('Uh oh :(', 'danger')
...@@ -209,7 +199,7 @@ $(function() { ...@@ -209,7 +199,7 @@ $(function() {
$("#vm-details-h1-name, .vm-details-rename-button").click(function() { $("#vm-details-h1-name, .vm-details-rename-button").click(function() {
$("#vm-details-h1-name").hide(); $("#vm-details-h1-name").hide();
$("#vm-details-rename").css('display', 'inline'); $("#vm-details-rename").css('display', 'inline');
$("#vm-details-rename-name").focus(); $("#vm-details-rename-name").select();
return false; return false;
}); });
...@@ -217,7 +207,7 @@ $(function() { ...@@ -217,7 +207,7 @@ $(function() {
$(".vm-details-home-edit-name-click").click(function() { $(".vm-details-home-edit-name-click").click(function() {
$(".vm-details-home-edit-name-click").hide(); $(".vm-details-home-edit-name-click").hide();
$("#vm-details-home-rename").show(); $("#vm-details-home-rename").show();
$("input", $("#vm-details-home-rename")).focus(); $("input", $("#vm-details-home-rename")).select();
return false; return false;
}); });
...@@ -317,8 +307,8 @@ $(function() { ...@@ -317,8 +307,8 @@ $(function() {
}); });
// select connection string // select connection string
$("#vm-details-connection-string-copy").click(function() { $(".vm-details-connection-string-copy").click(function() {
$("#vm-details-connection-string").focus(); $(this).parent("div").find("input").select();
}); });
$("a.operation-password_reset").click(function() { $("a.operation-password_reset").click(function() {
......
...@@ -14,6 +14,7 @@ $(function() { ...@@ -14,6 +14,7 @@ $(function() {
$('.vm-list-table tbody').find('tr').mousedown(function() { $('.vm-list-table tbody').find('tr').mousedown(function() {
var retval = true; var retval = true;
if(!$(this).data("vm-pk")) return;
if (ctrlDown) { if (ctrlDown) {
setRowColor($(this)); setRowColor($(this));
if(!$(this).hasClass('vm-list-selected')) { if(!$(this).hasClass('vm-list-selected')) {
...@@ -46,86 +47,20 @@ $(function() { ...@@ -46,86 +47,20 @@ $(function() {
selected = [{'index': $(this).index(), 'vm': $(this).data("vm-pk")}]; 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 // show/hide group controls
if(selected.length > 0) { if(selected.length > 0) {
$('.vm-list-group-control a').attr('disabled', false); $('#vm-mass-ops .mass-operation').attr('disabled', false);
for(var i = 0; i < selected.length; i++) {
$('.vm-list-table tbody tr').eq(selected[i]).find('.btn').attr('disabled', true);
}
} else { } else {
$('.vm-list-group-control a').attr('disabled', true); $('#vm-mass-ops .mass-operation').attr('disabled', true);
} }
return retval; 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) { $('tbody a').mousedown(function(e) {
// parent tr doesn't get selected when clicked // parent tr doesn't get selected when clicked
e.stopPropagation(); 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 */ /* group actions */
/* select all */ /* select all */
...@@ -133,27 +68,69 @@ $(function() { ...@@ -133,27 +68,69 @@ $(function() {
$('.vm-list-table tbody tr').each(function() { $('.vm-list-table tbody tr').each(function() {
var index = $(this).index(); var index = $(this).index();
var vm = $(this).data("vm-pk"); var vm = $(this).data("vm-pk");
if(!isAlreadySelected(vm)) { if(vm && !isAlreadySelected(vm)) {
selected.push({'index': index, 'vm': vm}); selected.push({'index': index, 'vm': vm});
$(this).addClass('vm-list-selected'); $(this).addClass('vm-list-selected');
} }
}); });
if(selected.length > 0) if(selected.length > 0)
$('.vm-list-group-control a').attr('disabled', false); $('#vm-mass-ops .mass-operation').attr('disabled', false);
return false; return false;
}); });
/* mass vm delete */
$('#vm-list-group-delete').click(function() { /* mass operations */
addModalConfirmation(massDeleteVm, $("#vm-mass-ops").on('click', '.mass-operation', function(e) {
{ var icon = $(this).children("i").addClass('fa-spinner fa-spin');
'url': '/dashboard/vm/mass-delete/', params = "?" + selected.map(function(a){return "vm=" + a.vm}).join("&");
'data': {
'selected': selected, $.ajax({
'v': collectIds(selected) 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; return false;
}); });
...@@ -181,8 +158,68 @@ $(function() { ...@@ -181,8 +158,68 @@ $(function() {
$(".vm-list-table th a").on("click", function(event) { $(".vm-list-table th a").on("click", function(event) {
event.preventDefault(); 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) { function isAlreadySelected(vm) {
for(var i=0; i<selected.length; i++) for(var i=0; i<selected.length; i++)
if(selected[i].vm == vm) if(selected[i].vm == vm)
......
...@@ -25,6 +25,7 @@ from django_tables2.columns import (TemplateColumn, Column, BooleanColumn, ...@@ -25,6 +25,7 @@ from django_tables2.columns import (TemplateColumn, Column, BooleanColumn,
from vm.models import Node, InstanceTemplate, Lease from vm.models import Node, InstanceTemplate, Lease
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
from dashboard.models import ConnectCommand
class NodeListTable(Table): class NodeListTable(Table):
...@@ -146,13 +147,11 @@ class TemplateListTable(Table): ...@@ -146,13 +147,11 @@ class TemplateListTable(Table):
template_name="dashboard/template-list/column-template-name.html", template_name="dashboard/template-list/column-template-name.html",
attrs={'th': {'data-sort': "string"}} attrs={'th': {'data-sort': "string"}}
) )
num_cores = Column( resources = TemplateColumn(
verbose_name=_("Cores"), template_name="dashboard/template-list/column-template-resources.html",
attrs={'th': {'data-sort': "int"}} verbose_name=_("Resources"),
)
ram_size = TemplateColumn(
"{{ record.ram_size }} MiB",
attrs={'th': {'data-sort': "int"}}, attrs={'th': {'data-sort': "int"}},
order_by=("ram_size"),
) )
lease = TemplateColumn( lease = TemplateColumn(
"{{ record.lease.name }}", "{{ record.lease.name }}",
...@@ -170,11 +169,14 @@ class TemplateListTable(Table): ...@@ -170,11 +169,14 @@ class TemplateListTable(Table):
verbose_name=_("Owner"), verbose_name=_("Owner"),
attrs={'th': {'data-sort': "string"}} attrs={'th': {'data-sort': "string"}}
) )
created = TemplateColumn(
template_name="dashboard/template-list/column-template-created.html",
verbose_name=_("Created at"),
)
running = TemplateColumn( running = TemplateColumn(
template_name="dashboard/template-list/column-template-running.html", template_name="dashboard/template-list/column-template-running.html",
verbose_name=_("Running"), verbose_name=_("Running"),
attrs={'th': {'data-sort': "int"}}, attrs={'th': {'data-sort': "int"}},
orderable=False,
) )
actions = TemplateColumn( actions = TemplateColumn(
verbose_name=_("Actions"), verbose_name=_("Actions"),
...@@ -187,8 +189,8 @@ class TemplateListTable(Table): ...@@ -187,8 +189,8 @@ class TemplateListTable(Table):
model = InstanceTemplate model = InstanceTemplate
attrs = {'class': ('table table-bordered table-striped table-hover' attrs = {'class': ('table table-bordered table-striped table-hover'
' template-list-table')} ' template-list-table')}
fields = ('name', 'num_cores', 'ram_size', 'system', fields = ('name', 'resources', 'system', 'access_method', 'lease',
'access_method', 'lease', 'owner', 'running', 'actions', ) 'owner', 'created', 'running', 'actions', )
prefix = "template-" prefix = "template-"
...@@ -248,5 +250,41 @@ class UserKeyListTable(Table): ...@@ -248,5 +250,41 @@ class UserKeyListTable(Table):
class Meta: class Meta:
model = UserKey model = UserKey
attrs = {'class': ('table table-bordered table-striped table-hover')} attrs = {'class': ('table table-bordered table-striped table-hover'),
'id': "profile-key-list-table"}
fields = ('name', 'fingerprint', 'created', 'actions') fields = ('name', 'fingerprint', 'created', 'actions')
prefix = "key-"
empty_text = _("You haven't added any public keys yet.")
class ConnectCommandListTable(Table):
name = LinkColumn(
'dashboard.views.connect-command-detail',
args=[A('pk')],
attrs={'th': {'data-sort': "string"}}
)
access_method = Column(
verbose_name=_("Access method"),
attrs={'th': {'data-sort': "string"}}
)
template = Column(
verbose_name=_("Template"),
attrs={'th': {'data-sort': "string"}}
)
actions = TemplateColumn(
verbose_name=_("Actions"),
template_name=("dashboard/connect-command-list/column-command"
"-actions.html"),
orderable=False,
)
class Meta:
model = ConnectCommand
attrs = {'class': ('table table-bordered table-striped table-hover'),
'id': "profile-command-list-table"}
fields = ('name', 'access_method', 'template', 'actions')
prefix = "cmd-"
empty_text = _(
"You don't have any custom connection commands yet. You can "
"specify commands to be displayed on VM detail pages instead of "
"the defaults.")
...@@ -41,6 +41,7 @@ def send_email_notifications(): ...@@ -41,6 +41,7 @@ def send_email_notifications():
for i in Notification.objects.filter(q): for i in Notification.objects.filter(q):
recipients.setdefault(i.to, []) recipients.setdefault(i.to, [])
recipients[i.to].append(i) recipients[i.to].append(i)
logger.info("Delivering notifications to %d users", len(recipients))
for user, msgs in recipients.iteritems(): for user, msgs in recipients.iteritems():
if (not user.profile or not user.email or not if (not user.profile or not user.email or not
......
{% load i18n %} {% load i18n %}
{% if user and user.pk %} {% if user and user.pk %}
{% if user.get_full_name %} {% if user.get_full_name %}{{ user.get_full_name }}{% else %}{{ user.username }}{% endif %}{% if new_line %}<br />{% endif %}
{{ user.get_full_name }}
{% else %}
{{ user.username }}
{% endif %}
{% if show_org %} {% if show_org %}
{% if user.profile and user.profile.org_id %} {% if user.profile and user.profile.org_id %}
......
{% load i18n %} {% load i18n %}
{% load hro %}
{% for n in notifications %} {% for n in notifications %}
<li class="notification-message" id="msg-{{n.id}}"> <li class="notification-message" id="msg-{{n.id}}">
<span class="notification-message-subject"> <span class="notification-message-subject">
{% if n.status == "new" %}<i class="fa fa-envelope-alt"></i> {% endif %} {% if n.status == "new" %}<i class="fa fa-envelope-o"></i> {% endif %}
{{ n.subject.get_user_text }} {{ n.subject|get_text:user }}
</span> </span>
<span class="notification-message-date pull-right" title="{{n.created}}"> <span class="notification-message-date pull-right" title="{{n.created}}">
{{ n.created|timesince }} {{ n.created|timesince }}
</span> </span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
<div class="notification-message-text"> <div class="notification-message-text">
{{ n.message.get_user_text|safe }} {{ n.message|get_text:user|safe }}
</div> </div>
</li> </li>
{% empty %} {% empty %}
......
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
{% csrf_token %} {% csrf_token %}
{{ vm_create_form.template }} {{ vm_create_form.template }}
{{ vm_create_form.customized }}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
...@@ -23,6 +22,8 @@ ...@@ -23,6 +22,8 @@
</div> </div>
</div> </div>
{% if perms.vm.set_resources %}
{{ vm_create_form.customized }}
<div class="row"> <div class="row">
<div class="col-sm-10"> <div class="col-sm-10">
<div class="form-group"> <div class="form-group">
...@@ -85,6 +86,7 @@ ...@@ -85,6 +86,7 @@
</div><!-- .no-js-hidden --> </div><!-- .no-js-hidden -->
</div><!-- .col-sm-8 --> </div><!-- .col-sm-8 -->
</div><!-- .row --> </div><!-- .row -->
{% endif %}
</form> </form>
<script> <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 crispy_forms_tags %}
{% load i18n %}
<p class="text-muted">
{% trans "User groups allow sharing templates or other resources with multiple users at once." %}
</p>
<form method="POST" action="{% url "dashboard.views.group-create" %}"> <form method="POST" action="{% url "dashboard.views.group-create" %}">
{% csrf_token %} {% csrf_token %}
......
...@@ -6,63 +6,24 @@ ...@@ -6,63 +6,24 @@
{% block content %} {% 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="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="no-margin"><i class="fa fa-group"></i> Your groups</h3> <h3 class="no-margin"><i class="fa fa-group"></i> Your groups</h3>
</div> </div>
<div class="panel-body group-list-group-control"> <div class="panel-body">
<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 id="table_container"> <div id="table_container">
<div id="rendered_table" class="panel-body"> <div id="rendered_table" class="panel-body">
{% render_table table %} {% render_table table %}
</div> </div>
</div> </div>
</div><!-- .panel-body -->
</div> </div>
</div> </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 %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<script src="{{ STATIC_URL}}dashboard/group-list.js"></script> <script src="{{ STATIC_URL}}dashboard/group-list.js"></script>
{% endblock %} {% 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 @@ ...@@ -3,8 +3,10 @@
<div class="panel-heading"> <div class="panel-heading">
<div class="pull-right toolbar"> <div class="pull-right toolbar">
<div class="btn-group"> <div class="btn-group">
<a href="#index-graph-view" data-index-box="node" class="btn btn-default btn-xs"><i class="fa fa-dashboard"></i></a> <a href="#index-graph-view" data-index-box="node" class="btn btn-default btn-xs"
<a href="#index-list-view" data-index-box="node" class="btn btn-default btn-xs disabled"><i class="fa fa-list"></i></a> 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> </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> <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 @@ ...@@ -3,10 +3,12 @@
<div class="panel-heading"> <div class="panel-heading">
<div class="pull-right toolbar"> <div class="pull-right toolbar">
<div class="btn-group"> <div class="btn-group">
<a href="#index-graph-view" data-index-box="vm" class="btn <a href="#index-graph-view" data-index-box="vm" class="btn btn-default btn-xs"
btn-default btn-xs" title="{% trans "summary view" %}"><i class="fa fa-dashboard"></i></a> data-container="body"
<a href="#index-list-view" data-index-box="vm" class="btn title="{% trans "summary view" %}"><i class="fa fa-dashboard"></i></a>
btn-default btn-xs disabled" title="{% trans "list view" %}"><i class="fa fa-list"></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> </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> <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> </div>
...@@ -46,12 +48,15 @@ ...@@ -46,12 +48,15 @@
</style> </style>
<div href="#" class="list-group-item list-group-footer"> <div href="#" class="list-group-item list-group-footer">
<div class="row"> <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"> <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"> <div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary"><i class="fa fa-search"></i></button> <button type="submit" class="form-control btn btn-primary"><i class="fa fa-search"></i></button>
</div> </div>
</div> </div>
</form>
<div class="col-sm-6 text-right"> <div class="col-sm-6 text-right">
<a class="btn btn-primary btn-xs" href="{% url "dashboard.views.vm-list" %}"> <a class="btn btn-primary btn-xs" href="{% url "dashboard.views.vm-list" %}">
<i class="fa fa-chevron-circle-right"></i> <i class="fa fa-chevron-circle-right"></i>
......
{% extends "dashboard/base.html" %} {% extends "dashboard/base.html" %}
{% load i18n %} {% load i18n %}
{% load hro %}
{% block content %} {% block content %}
<div class="body-content"> <div class="body-content">
<div class="page-header"> <div class="page-header">
<h1> <h1><i class="fa fa-{{icon}}"></i>
{{ object.instance.name }}: {{ object.instance.name }}: {{object.readable_name|get_text:user}}
{% if user.is_superuser %}
{{object.readable_name.get_admin_text}}
{% else %}
{{object.readable_name.get_user_text}}
{% endif %}
</h1> </h1>
</div> </div>
<div class="row"> <div class="row">
...@@ -58,7 +54,7 @@ ...@@ -58,7 +54,7 @@
<dt>{% trans "result" %}</dt> <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> <dt>{% trans "resultant state" %}</dt>
<dd>{{object.resultant_state|default:'n/a'}}</dd> <dd>{{object.resultant_state|default:'n/a'}}</dd>
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a> <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>
<div class="panel-body"> <div class="panel-body">
{% with form=form %} {% with form=form %}
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a> <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>
<div class="panel-body"> <div class="panel-body">
{% with form=form %} {% 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 i18n %}
{% load hro %}
<div id="activity-timeline" class="timeline"> <div id="activity-timeline" class="timeline">
{% for a in activities %} {% for a in activities %}
<div class="activity" data-activity-id="{{ a.pk }}"> <div class="activity" data-activity-id="{{ a.pk }}">
...@@ -16,10 +17,7 @@ ...@@ -16,10 +17,7 @@
<div data-activity-id="{{ s.pk }}" <div data-activity-id="{{ s.pk }}"
class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}"
> >
{% if user.is_superuser %} {{ s.readable_name|get_text:user }}
{{ s.readable_name.get_admin_text }}
{% else %}
{{ s.readable_name.get_user_text }}{% endif %}
&ndash; &ndash;
{% if s.finished %} {% if s.finished %}
{{ s.finished|time:"H:i:s" }} {{ s.finished|time:"H:i:s" }}
......
...@@ -66,4 +66,20 @@ ...@@ -66,4 +66,20 @@
</div> </div>
</div> </div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a href="{% url "dashboard.views.connect-command-create" %}"
class="pull-right btn btn-success btn-xs" style="margin-right: 10px;">
<i class="fa fa-plus"></i> {% trans "add command template" %}
</a>
<h3 class="no-margin"><i class="fa fa-code"></i> {% trans "Command templates" %}</h3>
</div>
<div class="panel-body">
{% render_table connectcommand_table %}
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
...@@ -16,6 +16,23 @@ ...@@ -16,6 +16,23 @@
<h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %}</h3> <h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="row">
<div class="col-md-offset-8 col-md-4" id="template-list-search">
<form action="" method="GET">
<div class="input-group">
{{ search_form.s }}
<div class="input-group-btn">
{{ search_form.stype }}
<button type="submit" class="btn btn-primary input-tags">
<i class="fa fa-search"></i>
</button>
</div>
</div><!-- .input-group -->
</form>
</div><!-- .col-md-4 #template-list-search -->
</div>
</div>
<div class="panel-body">
{% render_table table %} {% render_table table %}
</div> </div>
</div> </div>
...@@ -31,7 +48,7 @@ ...@@ -31,7 +48,7 @@
<i class="fa fa-plus"></i> {% trans "new lease" %} <i class="fa fa-plus"></i> {% trans "new lease" %}
</a> </a>
{% endif %} {% 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>
<div class="panel-body"> <div class="panel-body">
<div class="" style="max-width: 600px;"> <div class="" style="max-width: 600px;">
......
{% load i18n %} {% load i18n %}
<a href="{% url "dashboard.views.vm-create" %}?template={{ record.pk }}"
class="btn btn-success btn-xs customize-vm" title="{% trans "Start" %}">
<i class="fa fa-play"></i>
</a>
<a href="{% url "dashboard.views.template-detail" pk=record.pk%}" id="template-list-edit-button" class="btn btn-default btn-xs" title="{% trans "Edit" %}"> <a href="{% url "dashboard.views.template-detail" pk=record.pk%}" id="template-list-edit-button" class="btn btn-default btn-xs" title="{% trans "Edit" %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
</a> </a>
......
{{ record.created|date }}
<br />
{{ record.created|time }}
{% include "dashboard/_display-name.html" with user=record.owner show_org=True %} {% include "dashboard/_display-name.html" with user=record.owner show_org=True new_line=True %}
{% load i18n %}
{{ record.ram_size }}MiB RAM
<br />
{% blocktrans with num_cores=record.num_cores count count=record.num_cores %}
{{ num_cores }} CPU core
{% plural %}
{{ num_cores }} CPU cores
{% endblocktrans %}
<a href="{% url "dashboard.views.vm-list" %}?s=template:{{ record.pk }}%20status:running"> <a href="{% url "dashboard.views.vm-list" %}?s=template:{{ record.pk }}%20status:running">
{{ record.get_running_instances.count }} {{ record.running }}
</a> </a>
...@@ -98,8 +98,9 @@ ...@@ -98,8 +98,9 @@
<div class="input-group"> <div class="input-group">
<input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags" <input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags"
value="{{ instance.pw }}" spellcheck="false"/> value="{{ instance.pw }}" spellcheck="false"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show"> <span class="input-group-addon input-tags" id="vm-details-pw-show"
<i class="fa fa-eye" id="vm-details-pw-eye" title="Show password"></i> title="{% trans "Show password" %}" data-container="body">
<i class="fa fa-eye" id="vm-details-pw-eye"></i>
</span> </span>
</div> </div>
</dd> </dd>
...@@ -111,16 +112,28 @@ ...@@ -111,16 +112,28 @@
</div> </div>
</dd> </dd>
</dl> </dl>
{% for c in connect_commands %}
<div class="input-group" id="dashboard-vm-details-connect-command"> <div class="input-group dashboard-vm-details-connect-command">
<span class="input-group-addon input-tags">{% trans "Command" %}</span> <span class="input-group-addon input-tags">{% trans "Command" %}</span>
<input type="text" spellcheck="false" <input type="text" spellcheck="false"
value="{% if instance.get_connect_command %}{{ instance.get_connect_command }}{% else %}{% trans "Connection is not possible." %}{% endif %}" value="{{ c }}"
id="vm-details-connection-string" class="form-control input-tags" />
<span class="input-group-addon input-tags vm-details-connection-string-copy"
title="{% trans "Select all" %}" data-container="body">
<i class="fa fa-copy"></i>
</span>
</div>
{% empty %}
<div class="input-group dashboard-vm-details-connect-command">
<span class="input-group-addon input-tags">{% trans "Command" %}</span>
<input type="text" spellcheck="false" value="{% trans "Connection is not possible." %}"
id="vm-details-connection-string" class="form-control input-tags" /> id="vm-details-connection-string" class="form-control input-tags" />
<span class="input-group-addon input-tags" id="vm-details-connection-string-copy"> <span class="input-group-addon input-tags" id="vm-details-connection-string-copy">
<i class="fa fa-copy" title="{% trans "Select all" %}"></i> <i class="fa fa-copy" title="{% trans "Select all" %}"></i>
</span> </span>
</div> </div>
{% endfor %}
<div id="dashboard-vm-details-connect" class="operation-wrapper"> <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" %}"> <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" %} <i class="fa fa-external-link"></i> {% trans "Connect" %}
......
{% load i18n %} {% load i18n %}
{% load hro %}
<div id="activity-timeline" class="timeline"> <div id="activity-timeline" class="timeline">
{% for a in activities %} {% for a in activities %}
<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" data-activity-id="{{ a.pk }}"> <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 %}"> <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> </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 }}"> <a href="{{ a.get_absolute_url }}">
{% if a.times > 1 %}({{ a.times }}x){% endif %} {% 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 %} {% if a.has_percent %}
- {{ a.percentage }}% - {{ a.percentage }}%
...@@ -33,9 +33,9 @@ ...@@ -33,9 +33,9 @@
<div class="sub-timeline"> <div class="sub-timeline">
{% for s in a.children.all %} {% 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 %}"> <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 }}"> <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 %} {% if s.finished %}
{{ s.finished|time:"H:i:s" }} {{ s.finished|time:"H:i:s" }}
{% else %} {% else %}
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<span class="operation operation-{{op.op}} btn btn-default disabled btn-xs"> <span class="operation operation-{{op.op}} btn btn-default disabled btn-xs">
{% else %} {% else %}
<a href="{{op.get_url}}" class="operation operation-{{op.op}} btn <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 %} {% endif %}
<i class="fa fa-{{op.icon}}"></i> <i class="fa fa-{{op.icon}}"></i>
<span{% if not op.is_preferred %} class="sr-only"{% endif %}>{{op.name}}</span> <span{% if not op.is_preferred %} class="sr-only"{% endif %}>{{op.name}}</span>
......
...@@ -94,12 +94,32 @@ ...@@ -94,12 +94,32 @@
<dt>{% trans "Template" %}:</dt> <dt>{% trans "Template" %}:</dt>
<dd> <dd>
{% if instance.template %} {% if instance.template %}
{% if can_link_template %}
<a href="{{ instance.template.get_absolute_url }}">
{{ instance.template.name }} {{ instance.template.name }}
</a>
{% else %}
{{ instance.template.name }}
{% endif %}
{% else %} {% else %}
- -
{% endif %} {% endif %}
</dd> </dd>
</dl> </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>
<div class="col-md-8"> <div class="col-md-8">
{% if graphite_enabled %} {% if graphite_enabled %}
......
...@@ -15,26 +15,42 @@ ...@@ -15,26 +15,42 @@
</div> </div>
<h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Virtual machines" %}</h3> <h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Virtual machines" %}</h3>
</div> </div>
<div class="pull-right" style="max-width: 250px; margin-top: 15px; margin-right: 15px;"> <div class="panel-body">
<form action="" method="GET" class="input-group"> <div class="row">
<input type="text" name="s"{% if request.GET.s %} value="{{ request.GET.s }}"{% endif %} class="form-control input-tags" placeholder="{% trans "Search..."%}" /> <div class="col-md-8 vm-list-group-control" id="vm-mass-ops">
<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>
<strong>{% trans "Group actions" %}</strong> <strong>{% trans "Group actions" %}</strong>
<button id="vm-list-group-select-all" class="btn btn-info btn-xs">{% trans "Select all" %}</button> <button id="vm-list-group-select-all" class="btn btn-info btn-xs">{% trans "Select all" %}</button>
<a class="btn btn-default btn-xs" id="vm-list-group-migrate" disabled><i class="fa fa-truck"></i> {% trans "Migrate" %}</a> {% for o in ops %}
<a disabled href="#" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i> {% trans "Reboot" %}</a> <a href="{{ o.get_url }}" class="btn btn-xs btn-{{ o.effect }} mass-operation"
<a disabled href="#" class="btn btn-default btn-xs"><i class="fa fa-off"></i> {% trans "Shutdown" %}</a> title="{{ o.name|capfirst }}" disabled>
<a id="vm-list-group-delete" disabled href="#" class="btn btn-danger btn-xs"><i class="fa fa-times"></i> {% trans "Destroy" %}</a> <i class="fa fa-{{ o.icon }}"></i>
</p> </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> </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"> <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> <thead><tr>
<th data-sort="int" class="orderable pk sortable vm-list-table-thin" style="min-width: 50px;"> <th data-sort="int" class="orderable pk sortable vm-list-table-thin" style="min-width: 50px;">
{% trans "ID" as t %} {% trans "ID" as t %}
...@@ -52,26 +68,56 @@ ...@@ -52,26 +68,56 @@
{% trans "Owner" as t %} {% trans "Owner" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="owner" %} {% include "dashboard/vm-list/header-link.html" with name=t sort="owner" %}
</th> </th>
{% if user.is_superuser %}<th data-sort="string" class="orderable sortable"> <th data-sort="string" class="orderable sortable">
{% trans "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 %} {% trans "Node" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="node" %} {% include "dashboard/vm-list/header-link.html" with name=t sort="node" %}
</th>{% endif %} </th>
{% endif %}
</tr></thead><tbody> </tr></thead><tbody>
{% for i in object_list %} {% for i in object_list %}
<tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}"> <tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}">
<td class="pk"><div id="vm-{{i.pk}}">{{i.pk}}</div> </td> <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="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> <td>
{% include "dashboard/_display-name.html" with user=i.owner show_org=True %} {% if i.owner.profile %}
{{ i.owner.profile.get_display_name }}
{% else %}
{{ i.owner.username }}
{% endif %}
{# include "dashboard/_display-name.html" with user=i.owner show_org=True #}
</td>
<td class="lease "data-sort-value="{{ i.lease.name }}">
{{ i.lease.name }}
</td> </td>
{% if user.is_superuser %} {% if user.is_superuser %}
<td 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 %} {% endif %}
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="5"> <td colspan="7">
{% if request.GET.s %} {% if request.GET.s %}
<strong>{% trans "No result." %}</strong> <strong>{% trans "No result." %}</strong>
{% else %} {% else %}
...@@ -87,6 +133,9 @@ ...@@ -87,6 +133,9 @@
</div> </div>
</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"> <div class="alert alert-info">
{% trans "You can select multiple vm instances while holding down the <strong>CTRL</strong> key." %} {% trans "You can select multiple vm instances while holding down the <strong>CTRL</strong> key." %}
...@@ -97,6 +146,5 @@ ...@@ -97,6 +146,5 @@
{% block extra_js %} {% block extra_js %}
<script src="{{ STATIC_URL}}dashboard/vm-list.js"></script> <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> <script src="{{ STATIC_URL}}dashboard/js/stupidtable.min.js"></script>
{% endblock %} {% 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 ...@@ -29,7 +29,7 @@ from django.utils import baseconv
from ..models import Profile from ..models import Profile
from ..views import InstanceActivityDetail, InstanceActivity 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 ..views import AclUpdateView
from .. import views from .. import views
...@@ -259,6 +259,114 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -259,6 +259,114 @@ class VmOperationViewTestCase(unittest.TestCase):
self.assertEquals(rend.status_code, 200) 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): class RenewViewTest(unittest.TestCase):
def test_renew_template(self): def test_renew_template(self):
......
...@@ -107,20 +107,6 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -107,20 +107,6 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.get('/dashboard/vm/1/') response = c.get('/dashboard/vm/1/')
self.assertEqual(response.status_code, 200) 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): def test_unpermitted_password_change(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
......
...@@ -29,7 +29,7 @@ from .views import ( ...@@ -29,7 +29,7 @@ from .views import (
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate, NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView, TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDetailView, TransferOwnershipView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmDetailVncTokenView, VmGraphView, VmList,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveUserView, GroupRemoveUserView,
GroupRemoveFutureUserView, GroupRemoveFutureUserView,
...@@ -39,6 +39,7 @@ from .views import ( ...@@ -39,6 +39,7 @@ from .views import (
get_vm_screenshot, get_vm_screenshot,
ProfileView, toggle_use_gravatar, UnsubscribeFormView, ProfileView, toggle_use_gravatar, UnsubscribeFormView,
UserKeyDelete, UserKeyDetail, UserKeyCreate, UserKeyDelete, UserKeyDetail, UserKeyCreate,
ConnectCommandDelete, ConnectCommandDetail, ConnectCommandCreate,
StoreList, store_download, store_upload, store_get_upload_url, StoreRemove, StoreList, store_download, store_upload, store_get_upload_url, StoreRemove,
store_new_directory, store_refresh_toplist, store_new_directory, store_refresh_toplist,
VmTraitsUpdate, VmRawDataUpdate, VmTraitsUpdate, VmRawDataUpdate,
...@@ -50,7 +51,6 @@ from .views import ( ...@@ -50,7 +51,6 @@ from .views import (
autocomplete_light.autodiscover() autocomplete_light.autodiscover()
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^$', IndexView.as_view(), name="dashboard.index"), url(r'^$', IndexView.as_view(), name="dashboard.index"),
url(r'^lease/(?P<pk>\d+)/$', LeaseDetail.as_view(), url(r'^lease/(?P<pk>\d+)/$', LeaseDetail.as_view(),
...@@ -75,7 +75,7 @@ urlpatterns = patterns( ...@@ -75,7 +75,7 @@ urlpatterns = patterns(
url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(), url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(),
name="dashboard.views.template-delete"), 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(), url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'), name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(), url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
...@@ -89,8 +89,6 @@ urlpatterns = patterns( ...@@ -89,8 +89,6 @@ urlpatterns = patterns(
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'), url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(), url(r'^vm/create/$', VmCreate.as_view(),
name='dashboard.views.vm-create'), 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/(?P<pk>\d+)/activity/$', vm_activity),
url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(), url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(),
name='dashboard.views.vm-activity'), name='dashboard.views.vm-activity'),
...@@ -181,6 +179,16 @@ urlpatterns = patterns( ...@@ -181,6 +179,16 @@ urlpatterns = patterns(
UserKeyCreate.as_view(), UserKeyCreate.as_view(),
name="dashboard.views.userkey-create"), name="dashboard.views.userkey-create"),
url(r'^conncmd/delete/(?P<pk>\d+)/$',
ConnectCommandDelete.as_view(),
name="dashboard.views.connect-command-delete"),
url(r'^conncmd/(?P<pk>\d+)/$',
ConnectCommandDetail.as_view(),
name="dashboard.views.connect-command-detail"),
url(r'^conncmd/create/$',
ConnectCommandCreate.as_view(),
name="dashboard.views.connect-command-create"),
url(r'^autocomplete/', include('autocomplete_light.urls')), url(r'^autocomplete/', include('autocomplete_light.urls')),
url(r"^store/list/$", StoreList.as_view(), url(r"^store/list/$", StoreList.as_view(),
......
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from lxml import etree as ET from lxml import etree as ET
import logging import logging
...@@ -29,3 +31,27 @@ def domain_validator(value): ...@@ -29,3 +31,27 @@ def domain_validator(value):
relaxng.assertValid(parsed_xml) relaxng.assertValid(parsed_xml)
except Exception as e: except Exception as e:
raise ValidationError(e.message) raise ValidationError(e.message)
def connect_command_template_validator(value):
"""Validate value as a connect command template.
>>> try: connect_command_template_validator("%(host)s")
... except ValidationError as e: print e
...
>>> connect_command_template_validator("%(host)s")
>>> try: connect_command_template_validator("%(host)s %s")
... except ValidationError as e: print e
...
[u'Invalid template string.']
"""
try:
value % {
'username': "uname",
'password': "pw",
'host': "111.111.111.111",
'port': 12345,
}
except (KeyError, TypeError, ValueError):
raise ValidationError(_("Invalid template string."))
...@@ -17,9 +17,17 @@ ...@@ -17,9 +17,17 @@
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from ..views import vm_ops from ..views import vm_ops, vm_mass_ops
urlpatterns = patterns('', urlpatterns = patterns(
*(url(r'^%s/$' % op, v.as_view(), name=v.get_urlname()) '',
for op, v in vm_ops.iteritems())) *(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"): ...@@ -101,7 +101,7 @@ def pull(dir="~/circle/circle"):
@roles('portal') @roles('portal')
def update_portal(test=False): def update_portal(test=False):
"Update and restart portal+manager" "Update and restart portal+manager"
with _stopped("portal", "mancelery"): with _stopped("portal", "manager"):
pull() pull()
pip("circle", "~/circle/requirements.txt") pip("circle", "~/circle/requirements.txt")
migrate() migrate()
...@@ -113,7 +113,7 @@ def update_portal(test=False): ...@@ -113,7 +113,7 @@ def update_portal(test=False):
@roles('portal') @roles('portal')
def stop_portal(test=False): def stop_portal(test=False):
"Stop portal and manager" "Stop portal and manager"
_stop_services("portal", "mancelery") _stop_services("portal", "manager")
@roles('node') @roles('node')
......
...@@ -62,6 +62,15 @@ class BuildFirewall: ...@@ -62,6 +62,15 @@ class BuildFirewall:
extra='-j DNAT --to-destination %s:%s' % (rule.host.ipv4, extra='-j DNAT --to-destination %s:%s' % (rule.host.ipv4,
rule.dport))) 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 # default outbound NAT rules for VLANs
for vl_in in Vlan.objects.exclude( for vl_in in Vlan.objects.exclude(
snat_ip=None).prefetch_related('snat_to'): snat_ip=None).prefetch_related('snat_to'):
...@@ -183,9 +192,12 @@ def generate_ptr_records(): ...@@ -183,9 +192,12 @@ def generate_ptr_records():
for host in Host.objects.order_by('vlan').all(): for host in Host.objects.order_by('vlan').all():
template = host.vlan.reverse_domain template = host.vlan.reverse_domain
i = host.get_external_ipv4().words if not host.shared_ip and host.external_ipv4: # DMZ
reverse = (host.reverse if host.reverse not in [None, ''] i = host.external_ipv4.words
else host.get_fqdn()) reverse = host.get_hostname('ipv4', public=True)
else:
i = host.ipv4.words
reverse = host.get_hostname('ipv4', public=False)
# ipv4 # ipv4
if host.ipv4: if host.ipv4:
...@@ -194,7 +206,7 @@ def generate_ptr_records(): ...@@ -194,7 +206,7 @@ def generate_ptr_records():
# ipv6 # ipv6
if host.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'])) reverse, settings['dns_ttl']))
return DNS return DNS
...@@ -211,14 +223,14 @@ def generate_records(): ...@@ -211,14 +223,14 @@ def generate_records():
'CNAME': 'C%(fqdn)s:%(address)s:%(ttl)s', 'CNAME': 'C%(fqdn)s:%(address)s:%(ttl)s',
'MX': '@%(fqdn)s::%(address)s:%(dist)s:%(ttl)s', 'MX': '@%(fqdn)s::%(address)s:%(dist)s:%(ttl)s',
'PTR': '^%(fqdn)s:%(address)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 = [] retval = []
for r in Record.objects.all(): for r in Record.objects.all():
params = {'fqdn': r.fqdn, 'address': r.address, 'ttl': r.ttl} params = {'fqdn': r.fqdn, 'address': r.address, 'ttl': r.ttl}
if r.type == 'MX': if r.type == 'MX':
params['address'], params['dist'] = r.address.split(':', 2) params['dist'], params['address'] = r.address.split(':', 2)
if r.type == 'AAAA': if r.type == 'AAAA':
try: try:
params['octal'] = ipv6_to_octal(r.address) params['octal'] = ipv6_to_octal(r.address)
......
...@@ -22,7 +22,7 @@ from collections import OrderedDict ...@@ -22,7 +22,7 @@ from collections import OrderedDict
logger = logging.getLogger() logger = logging.getLogger()
ipv4_re = re.compile( 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): class InvalidRuleExcepion(Exception):
......
...@@ -203,12 +203,6 @@ class Rule(models.Model): ...@@ -203,12 +203,6 @@ class Rule(models.Model):
elif self.firewall_id: elif self.firewall_id:
return 'INPUT' if self.direction == 'in' else 'OUTPUT' 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): def get_ipt_rules(self, host=None):
# action # action
action = 'LOG_ACC' if self.action == 'accept' else 'LOG_DROP' action = 'LOG_ACC' if self.action == 'accept' else 'LOG_DROP'
...@@ -221,7 +215,7 @@ class Rule(models.Model): ...@@ -221,7 +215,7 @@ class Rule(models.Model):
dst = None dst = None
if host: if host:
ip = (host.ipv4, host.ipv6_with_prefixlen) ip = (host.ipv4, host.ipv6_with_host_prefixlen)
if self.direction == 'in': if self.direction == 'in':
dst = ip dst = ip
else: else:
...@@ -235,9 +229,6 @@ class Rule(models.Model): ...@@ -235,9 +229,6 @@ class Rule(models.Model):
if vlan and not vlan.managed: if vlan and not vlan.managed:
return retval return retval
# src and dst ports
dport, sport = self.get_dport_sport()
# process foreign vlans # process foreign vlans
for foreign_vlan in self.foreign_network.vlans.all(): for foreign_vlan in self.foreign_network.vlans.all():
if not foreign_vlan.managed: if not foreign_vlan.managed:
...@@ -246,7 +237,7 @@ class Rule(models.Model): ...@@ -246,7 +237,7 @@ class Rule(models.Model):
r = IptRule(priority=self.weight, action=action, r = IptRule(priority=self.weight, action=action,
proto=self.proto, extra=self.extra, proto=self.proto, extra=self.extra,
comment='Rule #%s' % self.pk, 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) chain_name = self.get_chain_name(local=vlan, remote=foreign_vlan)
retval[chain_name] = r retval[chain_name] = r
...@@ -539,14 +530,30 @@ class Host(models.Model): ...@@ -539,14 +530,30 @@ class Host(models.Model):
def incoming_rules(self): def incoming_rules(self):
return self.rules.filter(direction='in') return self.rules.filter(direction='in')
@property @staticmethod
def ipv6_with_prefixlen(self): def create_ipnetwork(ip, prefixlen):
try: try:
net = IPNetwork(self.ipv6) net = IPNetwork(ip)
net.prefixlen = self.vlan.host_ipv6_prefixlen net.prefixlen = prefixlen
return net
except TypeError: except TypeError:
return None return None
else:
return net
@property
def ipv4_with_vlan_prefixlen(self):
return Host.create_ipnetwork(
self.ipv4, self.vlan.network4.prefixlen)
@property
def ipv6_with_vlan_prefixlen(self):
return Host.create_ipnetwork(
self.ipv6, self.vlan.network6.prefixlen)
@property
def ipv6_with_host_prefixlen(self):
return Host.create_ipnetwork(
self.ipv6, self.vlan.host_ipv6_prefixlen)
def get_external_ipv4(self): def get_external_ipv4(self):
return self.external_ipv4 if self.external_ipv4 else self.ipv4 return self.external_ipv4 if self.external_ipv4 else self.ipv4
...@@ -575,10 +582,14 @@ class Host(models.Model): ...@@ -575,10 +582,14 @@ class Host(models.Model):
# IPv4 # IPv4
if self.ipv4 is not None: 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 # update existing records
affected_records = Record.objects.filter( affected_records = Record.objects.filter(
host=self, name=self.hostname, host=self, name=self.hostname,
type='A').update(address=self.ipv4) type='A').update(address=ipv4)
# create new record # create new record
if affected_records == 0: if affected_records == 0:
Record(host=self, Record(host=self,
...@@ -605,6 +616,19 @@ class Host(models.Model): ...@@ -605,6 +616,19 @@ class Host(models.Model):
description='created by host.save()', description='created by host.save()',
type='AAAA').save() type='AAAA').save()
def get_network_config(self):
interface = {'addresses': []}
if self.ipv4 and self.vlan.network4:
interface['addresses'].append(str(self.ipv4_with_vlan_prefixlen))
interface['gw4'] = str(self.vlan.network4.ip)
if self.ipv6 and self.vlan.network6:
interface['addresses'].append(str(self.ipv6_with_vlan_prefixlen))
interface['gw6'] = str(self.vlan.network6.ip)
return interface
def enable_net(self): def enable_net(self):
for i in settings.get('default_host_groups', []): for i in settings.get('default_host_groups', []):
self.groups.add(Group.objects.get(name=i)) self.groups.add(Group.objects.get(name=i))
...@@ -714,6 +738,8 @@ class Host(models.Model): ...@@ -714,6 +738,8 @@ class Host(models.Model):
:type proto: str. :type proto: str.
""" """
assert proto in ('ipv6', 'ipv4', ) assert proto in ('ipv6', 'ipv4', )
if self.reverse:
return self.reverse
try: try:
if proto == 'ipv6': if proto == 'ipv6':
res = self.record_set.filter(type='AAAA', res = self.record_set.filter(type='AAAA',
...@@ -736,7 +762,7 @@ class Host(models.Model): ...@@ -736,7 +762,7 @@ class Host(models.Model):
Return a list of ports with forwarding rules set. Return a list of ports with forwarding rules set.
""" """
retval = [] retval = []
for rule in self.rules.all(): for rule in self.rules.filter(dport__isnull=False, direction='in'):
forward = { forward = {
'proto': rule.proto, 'proto': rule.proto,
'private': rule.dport, 'private': rule.dport,
......
...@@ -35,7 +35,7 @@ COMMIT ...@@ -35,7 +35,7 @@ COMMIT
{% if proto == "ipv4" %} {% if proto == "ipv4" %}
-A FORWARD -p icmp --icmp-type echo-request -g LOG_ACC -A FORWARD -p icmp --icmp-type echo-request -g LOG_ACC
{% else %} {% else %}
-A FORWARD -p icmpv6 --icmpv6-type echo-request -g LOG_ACC -A FORWARD -p icmpv6 -g LOG_ACC
{% endif %} {% endif %}
# initialize INPUT chain # initialize INPUT chain
...@@ -45,6 +45,11 @@ COMMIT ...@@ -45,6 +45,11 @@ COMMIT
-A INPUT -m state --state INVALID -g LOG_DROP -A INPUT -m state --state INVALID -g LOG_DROP
-A INPUT -i lo -j ACCEPT -A INPUT -i lo -j ACCEPT
-A INPUT -m state --state ESTABLISHED,RELATED -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 # initialize OUTPUT chain
-A OUTPUT -m state --state INVALID -g LOG_DROP -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', ...@@ -31,7 +31,7 @@ celery = Celery('manager',
'storage.tasks.local_tasks', 'storage.tasks.local_tasks',
'storage.tasks.periodic_tasks', 'storage.tasks.periodic_tasks',
'firewall.tasks.local_tasks', 'firewall.tasks.local_tasks',
'monitor.tasks.local_periodic_tasks', 'dashboard.tasks.local_periodic_tasks',
]) ])
celery.conf.update( celery.conf.update(
...@@ -41,49 +41,19 @@ celery.conf.update( ...@@ -41,49 +41,19 @@ celery.conf.update(
CELERY_QUEUES=( CELERY_QUEUES=(
Queue(HOSTNAME + '.man', Exchange('manager', type='direct'), Queue(HOSTNAME + '.man', Exchange('manager', type='direct'),
routing_key="manager"), routing_key="manager"),
Queue(HOSTNAME + '.monitor', Exchange('monitor', type='direct'),
routing_key="monitor"),
), ),
CELERYBEAT_SCHEDULE={ 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': { 'storage.periodic_tasks': {
'task': 'storage.tasks.periodic_tasks.garbage_collector', 'task': 'storage.tasks.periodic_tasks.garbage_collector',
'schedule': timedelta(hours=1), 'schedule': timedelta(hours=1),
'options': {'queue': 'localhost.man'} 'options': {'queue': 'localhost.man'}
}, },
'dashboard.local_periodic_tasks': { 'dashboard.send_email_notifications': {
'task': 'dashboard.tasks.local_periodic_tasks.' 'task': 'dashboard.tasks.local_periodic_tasks.'
'send_email_notifications', 'send_email_notifications',
'schedule': timedelta(hours=24), 'schedule': timedelta(hours=24),
'options': {'queue': 'localhost.man'} '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): ...@@ -278,7 +278,7 @@ class Disk(TimeStampedModel):
return Disk.create(base=self, datastore=self.datastore, return Disk.create(base=self, datastore=self.datastore,
name=self.name, size=self.size, name=self.name, size=self.size,
type=new_type) type=new_type, dev_num=self.dev_num)
def get_vmdisk_desc(self): def get_vmdisk_desc(self):
"""Serialize disk object to the vmdriver. """Serialize disk object to the vmdriver.
......
...@@ -28,7 +28,7 @@ def check_queue(storage, queue_id, priority): ...@@ -28,7 +28,7 @@ def check_queue(storage, queue_id, priority):
if priority is not None: if priority is not None:
queue_name = queue_name + "." + priority queue_name = queue_name + "." + priority
inspect = celery.control.inspect() inspect = celery.control.inspect()
inspect.timeout = 0.1 inspect.timeout = 0.5
active_queues = inspect.active_queues() active_queues = inspect.active_queues()
if active_queues is None: if active_queues is None:
return False return False
......
...@@ -90,7 +90,8 @@ class InstanceActivity(ActivityModel): ...@@ -90,7 +90,8 @@ class InstanceActivity(ActivityModel):
@classmethod @classmethod
def create(cls, code_suffix, instance, task_uuid=None, user=None, def create(cls, code_suffix, instance, task_uuid=None, user=None,
concurrency_check=True, readable_name=None): concurrency_check=True, readable_name=None,
resultant_state=None):
readable_name = _normalize_readable_name(readable_name, code_suffix) readable_name = _normalize_readable_name(readable_name, code_suffix)
# Check for concurrent activities # Check for concurrent activities
...@@ -100,14 +101,14 @@ class InstanceActivity(ActivityModel): ...@@ -100,14 +101,14 @@ class InstanceActivity(ActivityModel):
activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix) activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix)
act = cls(activity_code=activity_code, instance=instance, parent=None, 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(), readable_name_data=readable_name.to_dict(),
task_uuid=task_uuid, user=user) task_uuid=task_uuid, user=user)
act.save() act.save()
return act return act
def create_sub(self, code_suffix, task_uuid=None, concurrency_check=True, def create_sub(self, code_suffix, task_uuid=None, concurrency_check=True,
readable_name=None): readable_name=None, resultant_state=None):
readable_name = _normalize_readable_name(readable_name, code_suffix) readable_name = _normalize_readable_name(readable_name, code_suffix)
# Check for concurrent activities # Check for concurrent activities
...@@ -117,7 +118,8 @@ class InstanceActivity(ActivityModel): ...@@ -117,7 +118,8 @@ class InstanceActivity(ActivityModel):
act = InstanceActivity( act = InstanceActivity(
activity_code=join_activity_code(self.activity_code, code_suffix), activity_code=join_activity_code(self.activity_code, code_suffix),
instance=self.instance, parent=self, resultant_state=None, instance=self.instance, parent=self,
resultant_state=resultant_state,
readable_name_data=readable_name.to_dict(), started=timezone.now(), readable_name_data=readable_name.to_dict(), started=timezone.now(),
task_uuid=task_uuid, user=self.user) task_uuid=task_uuid, user=self.user)
act.save() act.save()
...@@ -190,18 +192,23 @@ class InstanceActivity(ActivityModel): ...@@ -190,18 +192,23 @@ class InstanceActivity(ActivityModel):
readable_name=readable_name) readable_name=readable_name)
return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit) return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
def get_operation(self):
return self.instance.get_operation_from_activity_code(
self.activity_code)
@contextmanager @contextmanager
def instance_activity(code_suffix, instance, on_abort=None, on_commit=None, def instance_activity(code_suffix, instance, on_abort=None, on_commit=None,
task_uuid=None, user=None, concurrency_check=True, 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. """Create a transactional context for an instance activity.
""" """
if not readable_name: if not readable_name:
warn("Set readable_name", stacklevel=3) warn("Set readable_name", stacklevel=3)
act = InstanceActivity.create(code_suffix, instance, task_uuid, user, act = InstanceActivity.create(code_suffix, instance, task_uuid, user,
concurrency_check, 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) return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
......
...@@ -32,6 +32,7 @@ from django.core.exceptions import PermissionDenied ...@@ -32,6 +32,7 @@ from django.core.exceptions import PermissionDenied
from django.db.models import (BooleanField, CharField, DateTimeField, from django.db.models import (BooleanField, CharField, DateTimeField,
IntegerField, ForeignKey, Manager, IntegerField, ForeignKey, Manager,
ManyToManyField, permalink, SET_NULL, TextField) ManyToManyField, permalink, SET_NULL, TextField)
from django.db import IntegrityError
from django.dispatch import Signal from django.dispatch import Signal
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.utils.translation import ugettext_lazy as _, ugettext_noop
...@@ -293,6 +294,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -293,6 +294,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
message = ugettext_noop( message = ugettext_noop(
"Instance %(instance)s has already been destroyed.") "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): class WrongStateError(InstanceError):
message = ugettext_noop( message = ugettext_noop(
"Current state (%(state)s) of instance %(instance)s is " "Current state (%(state)s) of instance %(instance)s is "
...@@ -483,9 +488,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -483,9 +488,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
""" """
try: try:
datastore = self.disks.all()[0].datastore datastore = self.disks.all()[0].datastore
except: except IndexError:
return None from storage.models import DataStore
else: datastore = DataStore.objects.get()
path = datastore.path + '/' + self.vm_name + '.dump' path = datastore.path + '/' + self.vm_name + '.dump'
return {'datastore': datastore, 'path': path} return {'datastore': datastore, 'path': path}
...@@ -507,7 +513,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -507,7 +513,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def ipv4(self): def ipv4(self):
"""Primary IPv4 address of the instance. """Primary IPv4 address of the instance.
""" """
return self.primary_host.ipv4 if self.primary_host else None # return self.primary_host.ipv4 if self.primary_host else None
for i in self.interface_set.all():
if i.host:
return i.host.ipv4
return None
@property @property
def ipv6(self): def ipv6(self):
...@@ -919,8 +929,16 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -919,8 +929,16 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def allocate_vnc_port(self): def allocate_vnc_port(self):
if self.vnc_port is None: if self.vnc_port is None:
while True:
try:
self.vnc_port = find_unused_vnc_port() self.vnc_port = find_unused_vnc_port()
self.save() 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): def yield_vnc_port(self):
if self.vnc_port is not None: if self.vnc_port is not None:
...@@ -936,7 +954,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -936,7 +954,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
'ERROR': 'fa-warning', 'ERROR': 'fa-warning',
'PENDING': 'fa-rocket', 'PENDING': 'fa-rocket',
'DESTROYED': 'fa-trash-o', '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): def get_activities(self, user=None):
acts = (self.activity_log.filter(parent=None). acts = (self.activity_log.filter(parent=None).
...@@ -986,3 +1005,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -986,3 +1005,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
instance=self, succeeded=None, parent=None).latest("started") instance=self, succeeded=None, parent=None).latest("started")
except InstanceActivity.DoesNotExist: except InstanceActivity.DoesNotExist:
return None 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 @@ ...@@ -16,6 +16,7 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from functools import update_wrapper
from logging import getLogger from logging import getLogger
from warnings import warn from warnings import warn
import requests import requests
...@@ -51,6 +52,8 @@ def node_available(function): ...@@ -51,6 +52,8 @@ def node_available(function):
return function(self, *args, **kwargs) return function(self, *args, **kwargs)
else: else:
return None return None
update_wrapper(decorate, function)
decorate._original = function
return decorate return decorate
...@@ -257,7 +260,7 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -257,7 +260,7 @@ class Node(OperatedMixin, TimeStampedModel):
@method_cache(10) @method_cache(10)
def monitor_info(self): def monitor_info(self):
metrics = ('cpu.usage', 'memory.usage') metrics = ('cpu.usage', 'memory.usage')
prefix = 'circle.%s.' % self.name prefix = 'circle.%s.' % self.host.hostname
params = [('target', '%s%s' % (prefix, metric)) params = [('target', '%s%s' % (prefix, metric))
for metric in metrics] for metric in metrics]
params.append(('from', '-5min')) params.append(('from', '-5min'))
......
...@@ -76,3 +76,8 @@ def get_keys(vm): ...@@ -76,3 +76,8 @@ def get_keys(vm):
@celery.task(name='agent.send_expiration') @celery.task(name='agent.send_expiration')
def send_expiration(vm, url): def send_expiration(vm, url):
pass pass
@celery.task(name='agent.change_ip')
def change_ip(vm, interfaces, dns):
pass
...@@ -19,7 +19,9 @@ from common.models import create_readable ...@@ -19,7 +19,9 @@ from common.models import create_readable
from manager.mancelery import celery from manager.mancelery import celery
from vm.tasks.agent_tasks import (restart_networking, change_password, from vm.tasks.agent_tasks import (restart_networking, change_password,
set_time, set_hostname, start_access_server, set_time, set_hostname, start_access_server,
cleanup, update) cleanup, update, change_ip)
from firewall.models import Host
import time import time
from base64 import encodestring from base64 import encodestring
from StringIO import StringIO from StringIO import StringIO
...@@ -31,13 +33,11 @@ from celery.result import TimeoutError ...@@ -31,13 +33,11 @@ from celery.result import TimeoutError
from monitor.client import Client from monitor.client import Client
def send_init_commands(instance, act, vm): def send_init_commands(instance, act):
vm = instance.vm_name
queue = instance.get_remote_queue_name("agent") queue = instance.get_remote_queue_name("agent")
with act.sub_activity('cleanup', readable_name=ugettext_noop('cleanup')): with act.sub_activity('cleanup', readable_name=ugettext_noop('cleanup')):
cleanup.apply_async(queue=queue, args=(vm, )) cleanup.apply_async(queue=queue, args=(vm, ))
with act.sub_activity('restart_networking',
readable_name=ugettext_noop('restart networking')):
restart_networking.apply_async(queue=queue, args=(vm, ))
with act.sub_activity('change_password', with act.sub_activity('change_password',
readable_name=ugettext_noop('change password')): readable_name=ugettext_noop('change password')):
change_password.apply_async(queue=queue, args=(vm, instance.pw)) change_password.apply_async(queue=queue, args=(vm, instance.pw))
...@@ -49,6 +49,17 @@ def send_init_commands(instance, act, vm): ...@@ -49,6 +49,17 @@ def send_init_commands(instance, act, vm):
queue=queue, args=(vm, instance.primary_host.hostname)) 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 create_agent_tar():
def exclude(tarinfo): def exclude(tarinfo):
if tarinfo.name.startswith('./.git'): if tarinfo.name.startswith('./.git'):
...@@ -86,7 +97,7 @@ def agent_started(vm, version=None): ...@@ -86,7 +97,7 @@ def agent_started(vm, version=None):
if version and version != settings.AGENT_VERSION: if version and version != settings.AGENT_VERSION:
try: try:
update_agent(vm, instance, act) update_agent(instance, act)
except TimeoutError: except TimeoutError:
pass pass
else: else:
...@@ -94,8 +105,9 @@ def agent_started(vm, version=None): ...@@ -94,8 +105,9 @@ def agent_started(vm, version=None):
if not initialized: if not initialized:
measure_boot_time(instance) measure_boot_time(instance)
send_init_commands(instance, act, vm) send_init_commands(instance, act)
send_networking_commands(instance, act)
with act.sub_activity( with act.sub_activity(
'start_access_server', 'start_access_server',
readable_name=ugettext_noop('start access server') readable_name=ugettext_noop('start access server')
...@@ -134,9 +146,16 @@ def agent_stopped(vm): ...@@ -134,9 +146,16 @@ def agent_stopped(vm):
pass 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: if act:
act.sub_activity( act = act.sub_activity(
'update', 'update',
readable_name=create_readable( readable_name=create_readable(
ugettext_noop('update to %(version)s'), ugettext_noop('update to %(version)s'),
...@@ -150,5 +169,6 @@ def update_agent(instance, vm, act=None): ...@@ -150,5 +169,6 @@ def update_agent(instance, vm, act=None):
version=settings.AGENT_VERSION)) version=settings.AGENT_VERSION))
with act: with act:
queue = instance.get_remote_queue_name("agent") queue = instance.get_remote_queue_name("agent")
update.apply_async(queue=queue, update.apply_async(
args=(vm, create_agent_tar())).get(timeout=10) queue=queue,
args=(instance.vm_name, create_agent_tar())).get(timeout=10)
...@@ -55,7 +55,7 @@ def get_queues(): ...@@ -55,7 +55,7 @@ def get_queues():
result = cache.get(key) result = cache.get(key)
if result is None: if result is None:
inspect = celery.control.inspect() inspect = celery.control.inspect()
inspect.timeout = 0.1 inspect.timeout = 0.5
result = inspect.active_queues() result = inspect.active_queues()
logger.debug('Queue list of length %d cached.', len(result)) logger.debug('Queue list of length %d cached.', len(result))
cache.set(key, result, 10) 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" description "CIRCLE mancelery for common jobs"
start on runlevel [2345]
stop on runlevel [!2345]
respawn respawn
respawn limit 30 30 respawn limit 30 30
setgid cloud setgid cloud
setuid cloud setuid cloud
...@@ -12,6 +10,5 @@ script ...@@ -12,6 +10,5 @@ script
cd /home/cloud/circle/circle cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate . /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate . /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 1 exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 10
end script 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 ...@@ -12,4 +12,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate . /home/cloud/.virtualenvs/circle/bin/postactivate
exec /home/cloud/.virtualenvs/circle/bin/uwsgi --chdir=/home/cloud/circle/circle -H /home/cloud/.virtualenvs/circle --socket /tmp/uwsgi.sock --wsgi-file circle/wsgi.py --chmod-socket=666 exec /home/cloud/.virtualenvs/circle/bin/uwsgi --chdir=/home/cloud/circle/circle -H /home/cloud/.virtualenvs/circle --socket /tmp/uwsgi.sock --wsgi-file circle/wsgi.py --chmod-socket=666
end script end script
...@@ -14,4 +14,3 @@ script ...@@ -14,4 +14,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate . /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py runserver '[::]:8080' exec ./manage.py runserver '[::]:8080'
end script end script
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