Commit 08a3542b by Őry Máté

Merge branch 'master' into connect-via-client

Conflicts:
	circle/dashboard/templates/dashboard/vm-detail.html
	circle/dashboard/views.py
parents dbcacac7 8eea8de7
......@@ -368,9 +368,9 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
from shutilwhich import which
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
# INSTALLED_APPS += ( # needed only for testing djangosaml2
# 'djangosaml',
# )
INSTALLED_APPS += (
'djangosaml2',
)
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'djangosaml2.backends.Saml2Backend',
......@@ -467,4 +467,4 @@ SESSION_COOKIE_NAME = "csessid%x" % (((getnode() // 139) ^
MAX_NODE_RAM = get_env_variable("MAX_NODE_RAM", 1024)
# Url to download the client: (e.g. http://circlecloud.org/client/download/)
CLIENT_DOWNLOAD_URL = get_env_variable('CLIENT_DOWNLOAD_URL', 'http://circlecloud.org/client/download/')
\ No newline at end of file
CLIENT_DOWNLOAD_URL = get_env_variable('CLIENT_DOWNLOAD_URL', 'http://circlecloud.org/client/download/')
......@@ -20,9 +20,12 @@
from os import environ
from sys import argv
from base import * # noqa
if 'runserver' in argv:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
########## HOST CONFIGURATION
# See: https://docs.djangoproject.com/en/1.5/releases/1.5/
......
......@@ -17,6 +17,7 @@
from collections import deque
from contextlib import contextmanager
from functools import update_wrapper
from hashlib import sha224
from itertools import chain, imap
from logging import getLogger
......@@ -26,6 +27,7 @@ from warnings import warn
from django.contrib import messages
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import (
CharField, DateTimeField, ForeignKey, NullBooleanField
......@@ -36,6 +38,7 @@ from django.utils.functional import Promise
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from jsonfield import JSONField
from manager.mancelery import celery
from model_utils.models import TimeStampedModel
logger = getLogger(__name__)
......@@ -212,6 +215,38 @@ class ActivityModel(TimeStampedModel):
self.result_data = None if value is None else value.to_dict()
@celery.task()
def compute_cached(method, instance, memcached_seconds,
key, start, *args, **kwargs):
"""Compute and store actual value of cached method."""
if isinstance(method, basestring):
model, id = instance
instance = model.objects.get(id=id)
try:
method = getattr(model, method)
while hasattr(method, '_original') or hasattr(method, 'fget'):
try:
method = method._original
except AttributeError:
method = method.fget
except AttributeError:
logger.exception("Couldnt get original method of %s",
unicode(method))
raise
# call the actual method
result = method(instance, *args, **kwargs)
# save to memcache
cache.set(key, result, memcached_seconds)
elapsed = time() - start
cache.set("%s.cached" % key, 2, max(memcached_seconds * 0.5,
memcached_seconds * 0.75 - elapsed))
logger.debug('Value of <%s>.%s(%s)=<%s> saved to cache (%s elapsed).',
unicode(instance), method.__name__, unicode(args),
unicode(result), elapsed)
return result
def method_cache(memcached_seconds=60, instance_seconds=5): # noqa
"""Cache return value of decorated method to memcached and memory.
......@@ -233,9 +268,11 @@ def method_cache(memcached_seconds=60, instance_seconds=5): # noqa
def inner_cache(method):
method_name = method.__name__
def get_key(instance, *args, **kwargs):
return sha224(unicode(method.__module__) +
unicode(method.__name__) +
method_name +
unicode(instance.id) +
unicode(args) +
unicode(kwargs)).hexdigest()
......@@ -254,21 +291,31 @@ def method_cache(memcached_seconds=60, instance_seconds=5): # noqa
if vals['time'] + instance_seconds > now:
# has valid on class cache, return that
result = vals['value']
setattr(instance, key, {'time': now, 'value': result})
if result is None:
result = cache.get(key)
if invalidate or (result is None):
# all caches failed, call the actual method
result = method(instance, *args, **kwargs)
# save to memcache and class attr
cache.set(key, result, memcached_seconds)
logger.debug("all caches failed, compute now")
result = compute_cached(method, instance, memcached_seconds,
key, time(), *args, **kwargs)
setattr(instance, key, {'time': now, 'value': result})
logger.debug('Value of <%s>.%s(%s)=<%s> saved to cache.',
unicode(instance), method.__name__,
unicode(args), unicode(result))
elif not cache.get("%s.cached" % key):
logger.debug("caches expiring, compute async")
cache.set("%s.cached" % key, 1, memcached_seconds * 0.5)
try:
compute_cached.apply_async(
queue='localhost.man', kwargs=kwargs, args=[
method_name, (instance.__class__, instance.id),
memcached_seconds, key, time()] + list(args))
except:
logger.exception("Couldnt compute async %s", method_name)
return result
update_wrapper(x, method)
x._original = method
return x
return inner_cache
......@@ -367,6 +414,10 @@ class HumanReadableObject(object):
self._set_values(user_text_template, admin_text_template, params)
def _set_values(self, user_text_template, admin_text_template, params):
if isinstance(user_text_template, Promise):
user_text_template = user_text_template._proxy____args[0]
if isinstance(admin_text_template, Promise):
admin_text_template = admin_text_template._proxy____args[0]
self.user_text_template = user_text_template
self.admin_text_template = admin_text_template
self.params = params
......@@ -405,6 +456,12 @@ class HumanReadableObject(object):
self.user_text_template, unicode(self.params))
return self.user_text_template
def get_text(self, user):
if user and user.is_superuser:
return self.get_admin_text()
else:
return self.get_user_text()
def to_dict(self):
return {"user_text_template": self.user_text_template,
"admin_text_template": self.admin_text_template,
......@@ -435,13 +492,34 @@ class HumanReadableException(HumanReadableObject, Exception):
self.level = "error"
def send_message(self, request, level=None):
if request.user and request.user.is_superuser:
msg = self.get_admin_text()
else:
msg = self.get_user_text()
msg = self.get_text(request.user)
getattr(messages, level or self.level)(request, msg)
def fetch_human_exception(exception, user=None):
"""Fetch user readable message from exception.
>>> r = humanize_exception("foo", Exception())
>>> fetch_human_exception(r, User())
u'foo'
>>> fetch_human_exception(r).get_text(User())
u'foo'
>>> fetch_human_exception(Exception(), User())
u'Unknown error'
>>> fetch_human_exception(PermissionDenied(), User())
u'Permission Denied'
"""
if not isinstance(exception, HumanReadableException):
if isinstance(exception, PermissionDenied):
exception = create_readable(ugettext_noop("Permission Denied"))
else:
exception = create_readable(ugettext_noop("Unknown error"),
ugettext_noop("Unknown error: %(ex)s"),
ex=unicode(exception))
return exception.get_text(user) if user else exception
def humanize_exception(message, exception=None, level=None, **params):
"""Return new dynamic-class exception which is based on
HumanReadableException and the original class with the dict of exception.
......
......@@ -18,10 +18,10 @@
from inspect import getargspec
from logging import getLogger
from .models import activity_context, has_suffix
from django.core.exceptions import PermissionDenied, ImproperlyConfigured
from django.utils.translation import ugettext_noop
from .models import activity_context, has_suffix, humanize_exception
logger = getLogger(__name__)
......@@ -31,6 +31,7 @@ class Operation(object):
"""
async_queue = 'localhost.man'
required_perms = None
superuser_required = False
do_not_call_in_templates = True
abortable = False
has_percentage = False
......@@ -143,13 +144,26 @@ class Operation(object):
def check_precond(self):
pass
def check_auth(self, user):
if self.required_perms is None:
@classmethod
def check_perms(cls, user):
"""Check if user is permitted to run this operation on any instance
"""
if cls.required_perms is None:
raise ImproperlyConfigured(
"Set required_perms to () if none needed.")
if not user.has_perms(self.required_perms):
if not user.has_perms(cls.required_perms):
raise PermissionDenied("%s doesn't have the required permissions."
% user)
if cls.superuser_required and not user.is_superuser:
raise humanize_exception(ugettext_noop(
"Superuser privileges are required."), PermissionDenied())
def check_auth(self, user):
"""Check if user is permitted to run this operation on this instance
"""
self.check_perms(user)
def create_activity(self, parent, user, kwargs):
raise NotImplementedError
......@@ -185,14 +199,17 @@ class OperatedMixin(object):
def __getattr__(self, name):
# NOTE: __getattr__ is only called if the attribute doesn't already
# exist in your __dict__
cls = self.__class__
return self.get_operation_class(name)(self)
@classmethod
def get_operation_class(cls, name):
ops = getattr(cls, operation_registry_name, {})
op = ops.get(name)
if op:
return op(self)
return op
else:
raise AttributeError("%r object has no attribute %r" %
(self.__class__.__name__, name))
(cls.__name__, name))
def get_available_operations(self, user):
"""Yield Operations that match permissions of user and preconditions.
......
......@@ -21,18 +21,22 @@ from django import contrib
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib.auth.models import User, Group
from dashboard.models import Profile, GroupProfile
from dashboard.models import Profile, GroupProfile, ConnectCommand
class ProfileInline(contrib.admin.TabularInline):
model = Profile
class CommandInline(contrib.admin.TabularInline):
model = ConnectCommand
class GroupProfileInline(contrib.admin.TabularInline):
model = GroupProfile
UserAdmin.inlines = (ProfileInline, )
UserAdmin.inlines = (ProfileInline, CommandInline, )
GroupAdmin.inlines = (GroupProfileInline, )
contrib.admin.site.unregister(User)
......
......@@ -39,7 +39,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.forms.widgets import TextInput, HiddenInput
from django.template import Context
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy as _
from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy
......@@ -54,7 +54,9 @@ from .models import Profile, GroupProfile
from circle.settings.base import LANGUAGES, MAX_NODE_RAM
from django.utils.translation import string_concat
from .virtvalidator import domain_validator
from .validators import domain_validator
from dashboard.models import ConnectCommand
LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")"))
for l in LANGUAGES)
......@@ -176,7 +178,14 @@ class GroupCreateForm(forms.ModelForm):
self.fields['org_id'] = forms.ChoiceField(
# TRANSLATORS: directory like in LDAP
choices=choices, required=False, label=_('Directory identifier'))
if not new_groups:
if new_groups:
self.fields['org_id'].help_text = _(
"If you select an item here, the members of this directory "
"group will be automatically added to the group at the time "
"they log in. Please note that other users (those with "
"permissions like yours) may also automatically become a "
"group co-owner).")
else:
self.fields['org_id'].widget = HiddenInput()
def save(self, commit=True):
......@@ -634,12 +643,8 @@ class LeaseForm(forms.ModelForm):
Field('name'),
Field("suspend_interval_seconds", type="hidden", value="0"),
Field("delete_interval_seconds", type="hidden", value="0"),
HTML(string_concat("<label>", _("Suspend in"), "</label>")),
Div(
Div(
HTML(_("Suspend in")),
css_class="input-group-addon",
style="width: 100px;",
),
NumberField("suspend_hours", css_class="form-control"),
Div(
HTML(_("hours")),
......@@ -662,12 +667,8 @@ class LeaseForm(forms.ModelForm):
),
css_class="input-group interval-input",
),
HTML(string_concat("<label>", _("Delete in"), "</label>")),
Div(
Div(
HTML(_("Delete in")),
css_class="input-group-addon",
style="width: 100px;",
),
NumberField("delete_hours", css_class="form-control"),
Div(
HTML(_("hours")),
......@@ -691,7 +692,7 @@ class LeaseForm(forms.ModelForm):
css_class="input-group interval-input",
)
)
helper.add_input(Submit("submit", "Save changes"))
helper.add_input(Submit("submit", _("Save changes")))
return helper
class Meta:
......@@ -703,6 +704,8 @@ class VmRenewForm(forms.Form):
force = forms.BooleanField(required=False, label=_(
"Set expiration times even if they are shorter than "
"the current value."))
save = forms.BooleanField(required=False, label=_(
"Save selected lease."))
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
......@@ -714,6 +717,32 @@ class VmRenewForm(forms.Form):
empty_label=None, label=_('Length')))
if len(choices) < 2:
self.fields['lease'].widget = HiddenInput()
self.fields['save'].widget = HiddenInput()
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class VmStateChangeForm(forms.Form):
interrupt = forms.BooleanField(required=False, label=_(
"Forcibly interrupt all running activities."),
help_text=_("Set all activities to finished state, "
"but don't interrupt any tasks."))
new_state = forms.ChoiceField(Instance.STATUS, label=_(
"New status"))
def __init__(self, *args, **kwargs):
show_interrupt = kwargs.pop('show_interrupt')
status = kwargs.pop('status')
super(VmStateChangeForm, self).__init__(*args, **kwargs)
if not show_interrupt:
self.fields['interrupt'].widget = HiddenInput()
self.fields['new_state'].initial = status
@property
def helper(self):
......@@ -1037,6 +1066,22 @@ class UserKeyForm(forms.ModelForm):
return super(UserKeyForm, self).clean()
class ConnectCommandForm(forms.ModelForm):
class Meta:
fields = ('name', 'access_method', 'template')
model = ConnectCommand
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super(ConnectCommandForm, self).__init__(*args, **kwargs)
def clean(self):
if self.user:
self.instance.user = self.user
return super(ConnectCommandForm, self).clean()
class TraitsForm(forms.ModelForm):
class Meta:
......@@ -1138,3 +1183,53 @@ class VmResourcesForm(forms.ModelForm):
class Meta:
model = Instance
fields = ('num_cores', 'priority', 'ram_size', )
vm_search_choices = (
("owned", _("owned")),
("shared", _("shared")),
("all", _("all")),
)
class VmListSearchForm(forms.Form):
s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags",
'placeholder': _("Search...")
}))
stype = forms.ChoiceField(vm_search_choices, widget=forms.Select(attrs={
'class': "btn btn-default form-control input-tags",
'style': "min-width: 80px;",
}))
include_deleted = forms.BooleanField(widget=forms.CheckboxInput(attrs={
'id': "vm-list-search-checkbox",
}))
def __init__(self, *args, **kwargs):
super(VmListSearchForm, self).__init__(*args, **kwargs)
# set initial value, otherwise it would be overwritten by request.GET
if not self.data.get("stype"):
data = self.data.copy()
data['stype'] = "all"
self.data = data
class TemplateListSearchForm(forms.Form):
s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags",
'placeholder': _("Search...")
}))
stype = forms.ChoiceField(vm_search_choices, widget=forms.Select(attrs={
'class': "btn btn-default input-tags",
}))
def __init__(self, *args, **kwargs):
super(TemplateListSearchForm, self).__init__(*args, **kwargs)
# set initial value, otherwise it would be overwritten by request.GET
if not self.data.get("stype"):
data = self.data.copy()
data['stype'] = "owned"
self.data = data
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'ConnectCommand'
db.create_table(u'dashboard_connectcommand', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='command_set', to=orm['auth.User'])),
('access_method', self.gf('django.db.models.fields.CharField')(max_length=10)),
('application', self.gf('django.db.models.fields.CharField')(max_length='128')),
('template', self.gf('django.db.models.fields.CharField')(max_length=256, null=True, blank=True)),
))
db.send_create_signal(u'dashboard', ['ConnectCommand'])
def backwards(self, orm):
# Deleting model 'ConnectCommand'
db.delete_table(u'dashboard_connectcommand')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'dashboard.connectcommand': {
'Meta': {'object_name': 'ConnectCommand'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'application': ('django.db.models.fields.CharField', [], {'max_length': "'128'"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'template': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'command_set'", 'to': u"orm['auth.User']"})
},
u'dashboard.favourite': {
'Meta': {'object_name': 'Favourite'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Instance']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
},
u'dashboard.futuremember': {
'Meta': {'unique_together': "(('org_id', 'group'),)", 'object_name': 'FutureMember'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64'})
},
u'dashboard.groupprofile': {
'Meta': {'object_name': 'GroupProfile'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'group': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.Group']", 'unique': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
u'dashboard.notification': {
'Meta': {'ordering': "['-created']", 'object_name': 'Notification'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'message_data': ('jsonfield.fields.JSONField', [], {'null': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'status': ('model_utils.fields.StatusField', [], {'default': "'new'", 'max_length': '100', u'no_check_for_status': 'True'}),
'subject_data': ('jsonfield.fields.JSONField', [], {'null': 'True'}),
'to': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'valid_until': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'})
},
u'dashboard.profile': {
'Meta': {'object_name': 'Profile'},
'disk_quota': ('sizefield.models.FileSizeField', [], {'default': '2147483648'}),
'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'preferred_language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '32'}),
'smb_password': ('django.db.models.fields.CharField', [], {'default': "u'uUmt7R9peX'", 'max_length': '20'}),
'use_gravatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
},
u'firewall.domain': {
'Meta': {'object_name': 'Domain'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'ttl': ('django.db.models.fields.IntegerField', [], {'default': '600'})
},
u'firewall.group': {
'Meta': {'object_name': 'Group'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'firewall.host': {
'Meta': {'ordering': "('normalized_hostname', 'vlan')", 'unique_together': "(('hostname', 'vlan'),)", 'object_name': 'Host'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'external_ipv4': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Group']", 'null': 'True', 'blank': 'True'}),
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv4': ('firewall.fields.IPAddressField', [], {'unique': 'True', 'max_length': '100'}),
'ipv6': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'mac': ('firewall.fields.MACAddressField', [], {'unique': 'True', 'max_length': '17'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'normalized_hostname': ('common.models.HumanSortField', [], {'default': "''", 'maximum_number_length': '4', 'max_length': '80', 'monitor': "'hostname'", 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'reverse': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
'shared_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'firewall.vlan': {
'Meta': {'object_name': 'Vlan'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'dhcp_pool': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Domain']"}),
'host_ipv6_prefixlen': ('django.db.models.fields.IntegerField', [], {'default': '112'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv6_template': ('django.db.models.fields.TextField', [], {'default': "'2001:738:2001:4031:%(b)d:%(c)d:%(d)d:0'"}),
'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'network4': ('firewall.fields.IPNetworkField', [], {'max_length': '100'}),
'network6': ('firewall.fields.IPNetworkField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'network_type': ('django.db.models.fields.CharField', [], {'default': "'portforward'", 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'reverse_domain': ('django.db.models.fields.TextField', [], {'default': "'%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa'"}),
'snat_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
'snat_to': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Vlan']", 'null': 'True', 'blank': 'True'}),
'vid': ('django.db.models.fields.IntegerField', [], {'unique': 'True'})
},
u'storage.datastore': {
'Meta': {'ordering': "[u'name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "u'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'size': ('sizefield.models.FileSizeField', [], {'default': 'None', 'null': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
},
u'vm.instance': {
'Meta': {'ordering': "(u'pk',)", 'object_name': 'Instance'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'active_since': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'destroyed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'instance_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_base': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'to': u"orm['vm.Node']"}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'pw': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'status': ('model_utils.fields.StatusField', [], {'default': "u'NOSTATE'", 'max_length': '100', u'no_check_for_status': 'True'}),
'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
'system': ('django.db.models.fields.TextField', [], {}),
'template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['vm.InstanceTemplate']"}),
'time_of_delete': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'time_of_suspend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'vnc_port': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
u'vm.instancetemplate': {
'Meta': {'ordering': "(u'name',)", 'object_name': 'InstanceTemplate'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'template_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.InstanceTemplate']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'system': ('django.db.models.fields.TextField', [], {})
},
u'vm.lease': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Lease'},
'delete_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'suspend_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
},
u'vm.node': {
'Meta': {'ordering': "(u'-enabled', u'normalized_name')", 'object_name': 'Node'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'normalized_name': ('common.models.HumanSortField', [], {'default': "''", 'maximum_number_length': '4', 'max_length': '100', 'monitor': "u'name'", 'blank': 'True'}),
'overcommit': ('django.db.models.fields.FloatField', [], {'default': '1.0'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'})
},
u'vm.trait': {
'Meta': {'object_name': 'Trait'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
}
}
complete_apps = ['dashboard']
\ No newline at end of file
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Deleting field 'ConnectCommand.application'
db.delete_column(u'dashboard_connectcommand', 'application')
# Adding field 'ConnectCommand.name'
db.add_column(u'dashboard_connectcommand', 'name',
self.gf('django.db.models.fields.CharField')(default='szia megint', max_length='128'),
keep_default=False)
def backwards(self, orm):
# Adding field 'ConnectCommand.application'
db.add_column(u'dashboard_connectcommand', 'application',
self.gf('django.db.models.fields.CharField')(default='szia', max_length='128'),
keep_default=False)
# Deleting field 'ConnectCommand.name'
db.delete_column(u'dashboard_connectcommand', 'name')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'dashboard.connectcommand': {
'Meta': {'object_name': 'ConnectCommand'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': "'128'"}),
'template': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'command_set'", 'to': u"orm['auth.User']"})
},
u'dashboard.favourite': {
'Meta': {'object_name': 'Favourite'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Instance']"}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
},
u'dashboard.futuremember': {
'Meta': {'unique_together': "(('org_id', 'group'),)", 'object_name': 'FutureMember'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64'})
},
u'dashboard.groupprofile': {
'Meta': {'object_name': 'GroupProfile'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'group': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.Group']", 'unique': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
u'dashboard.notification': {
'Meta': {'ordering': "['-created']", 'object_name': 'Notification'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'message_data': ('jsonfield.fields.JSONField', [], {'null': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'status': ('model_utils.fields.StatusField', [], {'default': "'new'", 'max_length': '100', u'no_check_for_status': 'True'}),
'subject_data': ('jsonfield.fields.JSONField', [], {'null': 'True'}),
'to': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'valid_until': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'})
},
u'dashboard.profile': {
'Meta': {'object_name': 'Profile'},
'disk_quota': ('sizefield.models.FileSizeField', [], {'default': '2147483648'}),
'email_notifications': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance_limit': ('django.db.models.fields.IntegerField', [], {'default': '5'}),
'org_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'preferred_language': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '32'}),
'smb_password': ('django.db.models.fields.CharField', [], {'default': "u'NRERukxe3Z'", 'max_length': '20'}),
'use_gravatar': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'})
},
u'firewall.domain': {
'Meta': {'object_name': 'Domain'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'ttl': ('django.db.models.fields.IntegerField', [], {'default': '600'})
},
u'firewall.group': {
'Meta': {'object_name': 'Group'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'firewall.host': {
'Meta': {'ordering': "('normalized_hostname', 'vlan')", 'unique_together': "(('hostname', 'vlan'),)", 'object_name': 'Host'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'external_ipv4': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Group']", 'null': 'True', 'blank': 'True'}),
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv4': ('firewall.fields.IPAddressField', [], {'unique': 'True', 'max_length': '100'}),
'ipv6': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'mac': ('firewall.fields.MACAddressField', [], {'unique': 'True', 'max_length': '17'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'normalized_hostname': ('common.models.HumanSortField', [], {'default': "''", 'maximum_number_length': '4', 'max_length': '80', 'monitor': "'hostname'", 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'reverse': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
'shared_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'firewall.vlan': {
'Meta': {'object_name': 'Vlan'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'dhcp_pool': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Domain']"}),
'host_ipv6_prefixlen': ('django.db.models.fields.IntegerField', [], {'default': '112'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv6_template': ('django.db.models.fields.TextField', [], {'default': "'2001:738:2001:4031:%(b)d:%(c)d:%(d)d:0'"}),
'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'network4': ('firewall.fields.IPNetworkField', [], {'max_length': '100'}),
'network6': ('firewall.fields.IPNetworkField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'network_type': ('django.db.models.fields.CharField', [], {'default': "'portforward'", 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'reverse_domain': ('django.db.models.fields.TextField', [], {'default': "'%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa'"}),
'snat_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
'snat_to': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Vlan']", 'null': 'True', 'blank': 'True'}),
'vid': ('django.db.models.fields.IntegerField', [], {'unique': 'True'})
},
u'storage.datastore': {
'Meta': {'ordering': "[u'name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "u'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'size': ('sizefield.models.FileSizeField', [], {'default': 'None', 'null': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
},
u'vm.instance': {
'Meta': {'ordering': "(u'pk',)", 'object_name': 'Instance'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'active_since': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'destroyed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'instance_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_base': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'to': u"orm['vm.Node']"}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'pw': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'status': ('model_utils.fields.StatusField', [], {'default': "u'NOSTATE'", 'max_length': '100', u'no_check_for_status': 'True'}),
'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
'system': ('django.db.models.fields.TextField', [], {}),
'template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['vm.InstanceTemplate']"}),
'time_of_delete': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'time_of_suspend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'vnc_port': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
u'vm.instancetemplate': {
'Meta': {'ordering': "(u'name',)", 'object_name': 'InstanceTemplate'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'template_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.InstanceTemplate']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'system': ('django.db.models.fields.TextField', [], {})
},
u'vm.lease': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Lease'},
'delete_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'suspend_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
},
u'vm.node': {
'Meta': {'ordering': "(u'-enabled', u'normalized_name')", 'object_name': 'Node'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'normalized_name': ('common.models.HumanSortField', [], {'default': "''", 'maximum_number_length': '4', 'max_length': '100', 'monitor': "u'name'", 'blank': 'True'}),
'overcommit': ('django.db.models.fields.FloatField', [], {'default': '1.0'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'})
},
u'vm.trait': {
'Meta': {'object_name': 'Trait'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
}
}
complete_apps = ['dashboard']
\ No newline at end of file
......@@ -46,8 +46,10 @@ from acl.models import AclBase
from common.models import HumanReadableObject, create_readable, Encoder
from vm.tasks.agent_tasks import add_keys, del_keys
from vm.models.instance import ACCESS_METHODS
from .store_api import Store, NoStoreException, NotOkException
from .validators import connect_command_template_validator
logger = getLogger(__name__)
......@@ -100,6 +102,25 @@ class Notification(TimeStampedModel):
self.message_data = None if value is None else value.to_dict()
class ConnectCommand(Model):
user = ForeignKey(User, related_name='command_set')
access_method = CharField(max_length=10, choices=ACCESS_METHODS,
verbose_name=_('access method'),
help_text=_('Type of the remote access method.'))
name = CharField(max_length="128", verbose_name=_('name'), blank=False,
help_text=_("Name of your custom command."))
template = CharField(blank=True, null=True, max_length=256,
verbose_name=_('command template'),
help_text=_('Template for connection command string. '
'Available parameters are: '
'username, password, '
'host, port.'),
validators=[connect_command_template_validator])
def __unicode__(self):
return self.template
class Profile(Model):
user = OneToOneField(User)
preferred_language = CharField(verbose_name=_('preferred language'),
......@@ -129,6 +150,25 @@ class Profile(Model):
default=2048 * 1024 * 1024,
help_text=_('Disk quota in mebibytes.'))
def get_connect_commands(self, instance, use_ipv6=False):
""" Generate connection command based on template."""
single_command = instance.get_connect_command(use_ipv6)
if single_command: # can we even connect to that VM
commands = self.user.command_set.filter(
access_method=instance.access_method)
if commands.count() < 1:
return [single_command]
else:
return [
command.template % {
'port': instance.get_connect_port(use_ipv6=use_ipv6),
'host': instance.get_connect_host(use_ipv6=use_ipv6),
'password': instance.pw,
'username': 'cloud',
} for command in commands]
else:
return []
def notify(self, subject, template, context=None, valid_until=None,
**kwargs):
if context is not None:
......
......@@ -654,7 +654,8 @@ textarea[name="list-new-namelist"] {
width: 130px;
}
#vm-details-connection-string-copy {
.vm-details-connection-string-copy,
#vm-details-pw-show {
cursor: pointer;
}
......@@ -681,10 +682,9 @@ textarea[name="list-new-namelist"] {
max-width: 200px;
}
#dashboard-vm-details-connect-command {
.dashboard-vm-details-connect-command {
/* for mobile view */
margin-bottom: 20px;
}
#store-list-list {
......@@ -867,3 +867,92 @@ textarea[name="list-new-namelist"] {
border-bottom: 1px dotted #aaa;
padding: 5px 0px;
}
#profile-key-list-table td:last-child, #profile-key-list-table th:last-child,
#profile-command-list-table td:last-child, #profile-command-list-table th:last-child,
#profile-command-list-table td:nth-child(2), #profile-command-list-table th:nth-child(2) {
text-align: center;
vertical-align: middle;
}
#vm-list-table .migrating-icon {
-webkit-animation: passing 2s linear infinite;
animation: passing 2s linear infinite;
}
@-webkit-keyframes passing {
0% {
-webkit-transform: translateX(50%);
transform: translateX(50%);
opacity: 0;
}
50% {
-webkit-transform: translateX(0%);
transform: translateX(0%);
opacity: 1;
}
100% {
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
opacity: 0;
}
}
@keyframes passing {
0% {
-webkit-transform: translateX(50%);
-ms-transform: translateX(50%);
transform: translateX(50%);
opacity: 0;
}
50% {
-webkit-transform: translateX(0%);
-ms-transform: translateX(0%);
transform: translateX(0%);
opacity: 1;
}
100% {
-webkit-transform: translateX(-50%);
-ms-transform: translateX(-50%);
transform: translateX(-50%);
opacity: 0;
}
}
.mass-migrate-node {
cursor: pointer;
}
.mass-op-panel {
padding: 6px 10px;
}
.mass-op-panel .check {
color: #449d44;
}
.mass-op-panel .minus {
color: #d9534f;
}
.mass-op-panel .status-icon {
font-size: .8em;
}
#vm-list-search, #vm-mass-ops {
margin-top: 8px;
}
#vm-list-search-checkbox {
margin-top: -1px;
display: inline-block;
vertical-align: middle;
}
#vm-list-search-checkbox-span {
cursor: pointer
}
......@@ -258,14 +258,15 @@ $(function () {
html += '<div class="list-group-item list-group-item-last">' + gettext("No result") + '</div>';
$("#dashboard-vm-list").html(html);
$('.title-favourite').tooltip({'placement': 'right'});
});
// if there is only one result and ENTER is pressed redirect
if(e.keyCode == 13 && search_result.length == 1) {
window.location.href = "/dashboard/vm/" + search_result[0].pk + "/";
}
if(e.keyCode == 13 && search_result.length > 1 && input.length > 0) {
window.location.href = "/dashboard/vm/list/?s=" + input;
$("#dashboard-vm-search-form").submit(function() {
var vm_list_items = $("#dashboard-vm-list .list-group-item");
if(vm_list_items.length == 1 && vm_list_items.first().prop("href")) {
window.location.href = vm_list_items.first().prop("href");
return false;
}
return true;
});
/* search for nodes */
......@@ -494,14 +495,19 @@ function addSliderMiscs() {
ram_fire = true;
$(".ram-slider").simpleSlider("setValue", parseInt(val));
});
$(".cpu-priority-input").trigger("change");
$(".cpu-count-input, .ram-input").trigger("input");
setDefaultSliderValues();
$(".cpu-priority-slider").simpleSlider("setDisabled", $(".cpu-priority-input").prop("disabled"));
$(".cpu-count-slider").simpleSlider("setDisabled", $(".cpu-count-input").prop("disabled"));
$(".ram-slider").simpleSlider("setDisabled", $(".ram-input").prop("disabled"));
}
function setDefaultSliderValues() {
$(".cpu-priority-input").trigger("change");
$(".ram-input, .cpu-count-input").trigger("input");
}
/* deletes the VM with the pk
* if dir is true, then redirect to the dashboard landing page
......@@ -632,3 +638,11 @@ function noJS() {
$('.no-js-hidden').show();
$('.js-hidden').hide();
}
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
var ctrlDown, shiftDown = false;
var ctrlKey = 17;
var shiftKey = 16;
var selected = [];
$(function() {
$(document).keydown(function(e) {
if (e.keyCode == ctrlKey) ctrlDown = true;
if (e.keyCode == shiftKey) shiftDown = true;
}).keyup(function(e) {
if (e.keyCode == ctrlKey) ctrlDown = false;
if (e.keyCode == shiftKey) shiftDown = false;
});
$('.group-list-table tbody').find('tr').mousedown(function() {
var retval = true;
if (ctrlDown) {
setRowColor($(this));
if(!$(this).hasClass('group-list-selected')) {
selected.splice(selected.indexOf($(this).index()), 1);
} else {
selected.push($(this).index());
}
retval = false;
} else if(shiftDown) {
if(selected.length > 0) {
start = selected[selected.length - 1] + 1;
end = $(this).index();
if(start > end) {
var tmp = start - 1; start = end; end = tmp - 1;
}
for(var i = start; i <= end; i++) {
if(selected.indexOf(i) < 0) {
selected.push(i);
setRowColor($('.group-list-table tbody tr').eq(i));
}
}
}
retval = false;
} else {
$('.group-list-selected').removeClass('group-list-selected');
$(this).addClass('group-list-selected');
selected = [$(this).index()];
}
// reset btn disables
$('.group-list-table tbody tr .btn').attr('disabled', false);
// show/hide group controls
if(selected.length > 1) {
$('.group-list-group-control a').attr('disabled', false);
for(var i = 0; i < selected.length; i++) {
$('.group-list-table tbody tr').eq(selected[i]).find('.btn').attr('disabled', true);
}
} else {
$('.group-list-group-control a').attr('disabled', true);
}
return retval;
});
$('#group-list-group-migrate').click(function() {
console.log(collectIds(selected));
});
$('tbody a').mousedown(function(e) {
// parent tr doesn't get selected when clicked
e.stopPropagation();
});
$('tbody a').click(function(e) {
// browser doesn't jump to top when clicked the buttons
if(!$(this).hasClass('real-link')) {
return false;
}
});
/* rename */
$("#group-list-rename-button, .group-details-rename-button").click(function() {
$("#group-list-column-name", $(this).closest("tr")).hide();
......@@ -113,51 +34,4 @@ $(function() {
return false;
});
/* group actions */
/* select all */
$('#group-list-group-select-all').click(function() {
$('.group-list-table tbody tr').each(function() {
var index = $(this).index();
if(selected.indexOf(index) < 0) {
selected.push(index);
$(this).addClass('group-list-selected');
}
});
if(selected.length > 0)
$('.group-list-group-control a').attr('disabled', false);
return false;
});
/* mass vm delete */
$('#group-list-group-delete').click(function() {
addModalConfirmation(massDeleteVm,
{
'url': '/dashboard/group/mass-delete/',
'data': {
'selected': selected,
'v': collectIds(selected)
}
}
);
return false;
});
});
function collectIds(rows) {
var ids = [];
for(var i = 0; i < rows.length; i++) {
var div = $('td:first-child div', $('.group-list-table tbody tr').eq(rows[i]));
ids.push(div.prop('id').replace('node-', ''));
}
return ids;
}
function setRowColor(row) {
if(!row.hasClass('group-list-selected')) {
row.addClass('group-list-selected');
} else {
row.removeClass('group-list-selected');
}
}
......@@ -39,6 +39,7 @@ $(function() {
$(".template-list-table thead th").css("cursor", "pointer");
$(".template-list-table th a").on("click", function(event) {
if(!$(this).closest("th").data("sort")) return true;
event.preventDefault();
});
});
......
......@@ -3,7 +3,7 @@
$(function() {
/* vm operations */
$('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface').on('click', '.operation', function(e) {
$('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface, .operation-wrapper').on('click', '.operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin');
$.ajax({
......
......@@ -28,6 +28,9 @@ function vmCreateLoaded() {
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
});
$("#create-modal").on("shown.bs.modal", function() {
setDefaultSliderValues();
});
});
return false;
});
......@@ -217,6 +220,8 @@ function vmCustomizeLoaded() {
});
if(error) return true;
$(this).find("i").prop("class", "fa fa-spinner fa-spin");
$.ajax({
url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')},
......
......@@ -105,19 +105,20 @@ $(function() {
$("#vm-details-pw-show").click(function() {
var input = $(this).parent("div").children("input");
var eye = $(this).children("#vm-details-pw-eye");
var span = $(this);
eye.tooltip("destroy")
span.tooltip("destroy")
if(eye.hasClass("fa-eye")) {
eye.removeClass("fa-eye").addClass("fa-eye-slash");
input.prop("type", "text");
input.focus();
eye.prop("title", "Hide password");
input.select();
span.prop("title", gettext("Hide password"));
} else {
eye.removeClass("fa-eye-slash").addClass("fa-eye");
input.prop("type", "password");
eye.prop("title", "Show password");
span.prop("title", gettext("Show password"));
}
eye.tooltip();
span.tooltip();
});
/* change password confirmation */
......@@ -186,18 +187,7 @@ $(function() {
success: function(re, textStatus, xhr) {
/* remove the html element */
$('a[data-interface-pk="' + data.pk + '"]').closest("div").fadeOut();
/* add the removed element to the list */
network_select = $('select[name="new_network_vlan"]');
name_html = (re.removed_network.managed ? "&#xf0ac;": "&#xf0c1;") + " " + re.removed_network.vlan;
option_html = '<option value="' + re.removed_network.vlan_pk + '">' + name_html + '</option>';
// if it's -1 then it's a dummy placeholder so we can use .html
if($("option", network_select)[0].value === "-1") {
network_select.html(option_html);
network_select.next("div").children("button").prop("disabled", false);
} else {
network_select.append(option_html);
}
location.reload();
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger')
......@@ -209,7 +199,7 @@ $(function() {
$("#vm-details-h1-name, .vm-details-rename-button").click(function() {
$("#vm-details-h1-name").hide();
$("#vm-details-rename").css('display', 'inline');
$("#vm-details-rename-name").focus();
$("#vm-details-rename-name").select();
return false;
});
......@@ -217,7 +207,7 @@ $(function() {
$(".vm-details-home-edit-name-click").click(function() {
$(".vm-details-home-edit-name-click").hide();
$("#vm-details-home-rename").show();
$("input", $("#vm-details-home-rename")).focus();
$("input", $("#vm-details-home-rename")).select();
return false;
});
......@@ -317,8 +307,8 @@ $(function() {
});
// select connection string
$("#vm-details-connection-string-copy").click(function() {
$("#vm-details-connection-string").focus();
$(".vm-details-connection-string-copy").click(function() {
$(this).parent("div").find("input").select();
});
$("a.operation-password_reset").click(function() {
......
......@@ -14,6 +14,7 @@ $(function() {
$('.vm-list-table tbody').find('tr').mousedown(function() {
var retval = true;
if(!$(this).data("vm-pk")) return;
if (ctrlDown) {
setRowColor($(this));
if(!$(this).hasClass('vm-list-selected')) {
......@@ -46,86 +47,20 @@ $(function() {
selected = [{'index': $(this).index(), 'vm': $(this).data("vm-pk")}];
}
// reset btn disables
$('.vm-list-table tbody tr .btn').attr('disabled', false);
// show/hide group controls
if(selected.length > 0) {
$('.vm-list-group-control a').attr('disabled', false);
for(var i = 0; i < selected.length; i++) {
$('.vm-list-table tbody tr').eq(selected[i]).find('.btn').attr('disabled', true);
}
$('#vm-mass-ops .mass-operation').attr('disabled', false);
} else {
$('.vm-list-group-control a').attr('disabled', true);
$('#vm-mass-ops .mass-operation').attr('disabled', true);
}
return retval;
});
$('#vm-list-group-migrate').click(function() {
// pass?
});
$('.vm-list-details').popover({
'placement': 'auto',
'html': true,
'trigger': 'hover'
});
$('.vm-list-connect').popover({
'placement': 'left',
'html': true,
'trigger': 'click'
});
$('tbody a').mousedown(function(e) {
// parent tr doesn't get selected when clicked
e.stopPropagation();
});
$('tbody a').click(function(e) {
// browser doesn't jump to top when clicked the buttons
if(!$(this).hasClass('real-link')) {
return false;
}
});
/* rename */
$("#vm-list-rename-button, .vm-details-rename-button").click(function() {
$("#vm-list-column-name", $(this).closest("tr")).hide();
$("#vm-list-rename", $(this).closest("tr")).css('display', 'inline');
$("#vm-list-rename-name", $(this).closest("tr")).focus();
});
/* rename ajax */
$('.vm-list-rename-submit').click(function() {
var row = $(this).closest("tr")
var name = $('#vm-list-rename-name', row).val();
var url = '/dashboard/vm/' + row.children("td:first-child").text().replace(" ", "") + '/';
$.ajax({
method: 'POST',
url: url,
data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
$("#vm-list-column-name", row).html(
$("<a/>", {
'class': "real-link",
href: "/dashboard/vm/" + data['vm_pk'] + "/",
text: data['new_name']
})
).show();
$('#vm-list-rename', row).hide();
// addMessage(data['message'], "success");
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
/* group actions */
/* select all */
......@@ -133,27 +68,69 @@ $(function() {
$('.vm-list-table tbody tr').each(function() {
var index = $(this).index();
var vm = $(this).data("vm-pk");
if(!isAlreadySelected(vm)) {
if(vm && !isAlreadySelected(vm)) {
selected.push({'index': index, 'vm': vm});
$(this).addClass('vm-list-selected');
}
});
if(selected.length > 0)
$('.vm-list-group-control a').attr('disabled', false);
$('#vm-mass-ops .mass-operation').attr('disabled', false);
return false;
});
/* mass operations */
$("#vm-mass-ops").on('click', '.mass-operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin');
params = "?" + selected.map(function(a){return "vm=" + a.vm}).join("&");
$.ajax({
type: 'GET',
url: $(this).attr('href') + params,
success: function(data) {
icon.removeClass("fa-spinner fa-spin");
$('body').append(data);
$('#confirmation-modal').modal('show');
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
$("[title]").tooltip({'placement': "left"});
}
});
return false;
});
/* mass vm delete */
$('#vm-list-group-delete').click(function() {
addModalConfirmation(massDeleteVm,
{
'url': '/dashboard/vm/mass-delete/',
'data': {
'selected': selected,
'v': collectIds(selected)
$("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;
});
......@@ -168,7 +145,7 @@ $(function() {
// this didn't work ;;
// var th = $("this").find("th");
$(".table-sorting").hide();
$(".vm-list-table thead th i").remove();
var icon_html = '<i class="fa fa-sort-' + (data.direction == "desc" ? "desc" : "asc") + ' pull-right"></i>';
......@@ -181,8 +158,68 @@ $(function() {
$(".vm-list-table th a").on("click", function(event) {
event.preventDefault();
});
$(document).on("click", ".mass-migrate-node", function() {
$(this).find('input[type="radio"]').prop("checked", true);
});
if(checkStatusUpdate() || $("#vm-list-table tbody tr").length >= 100) {
updateStatuses(1);
}
});
function checkStatusUpdate() {
icons = $("#vm-list-table tbody td.state i");
if(icons.hasClass("fa-spin") || icons.hasClass("migrating-icon")) {
return true;
}
}
function updateStatuses(runs) {
var include_deleted = getParameterByName("include_deleted");
$.get("/dashboard/vm/list/?compact", function(result) {
$("#vm-list-table tbody tr").each(function() {
vm = $(this).data("vm-pk");
status_td = $(this).find("td.state");
status_icon = status_td.find("i");
status_text = status_td.find("span");
if(vm in result) {
if(result[vm].in_status_change) {
if(!status_icon.hasClass("fa-spin")) {
status_icon.prop("class", "fa fa-fw fa-spinner fa-spin");
}
}
else if(result[vm].status == "MIGRATING") {
if(!status_icon.hasClass("migrating-icon")) {
status_icon.prop("class", "fa fa-fw " + result[vm].icon + " migrating-icon");
}
} else {
status_icon.prop("class", "fa fa-fw " + result[vm].icon);
}
status_text.text(result[vm].status);
if("node" in result[vm]) {
$(this).find(".node").text(result[vm].node);
}
} else {
if(!include_deleted)
$(this).remove();
}
});
if(checkStatusUpdate()) {
setTimeout(
function() {updateStatuses(runs + 1)},
1000 + Math.exp(runs * 0.05)
);
}
});
}
function isAlreadySelected(vm) {
for(var i=0; i<selected.length; i++)
if(selected[i].vm == vm)
......@@ -202,7 +239,7 @@ function collectIds(rows) {
for(var i = 0; i < rows.length; i++) {
ids.push(rows[i].vm);
}
return ids;
return ids;
}
function setRowColor(row) {
......
......@@ -25,6 +25,7 @@ from django_tables2.columns import (TemplateColumn, Column, BooleanColumn,
from vm.models import Node, InstanceTemplate, Lease
from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey
from dashboard.models import ConnectCommand
class NodeListTable(Table):
......@@ -146,13 +147,11 @@ class TemplateListTable(Table):
template_name="dashboard/template-list/column-template-name.html",
attrs={'th': {'data-sort': "string"}}
)
num_cores = Column(
verbose_name=_("Cores"),
attrs={'th': {'data-sort': "int"}}
)
ram_size = TemplateColumn(
"{{ record.ram_size }} MiB",
resources = TemplateColumn(
template_name="dashboard/template-list/column-template-resources.html",
verbose_name=_("Resources"),
attrs={'th': {'data-sort': "int"}},
order_by=("ram_size"),
)
lease = TemplateColumn(
"{{ record.lease.name }}",
......@@ -170,11 +169,14 @@ class TemplateListTable(Table):
verbose_name=_("Owner"),
attrs={'th': {'data-sort': "string"}}
)
created = TemplateColumn(
template_name="dashboard/template-list/column-template-created.html",
verbose_name=_("Created at"),
)
running = TemplateColumn(
template_name="dashboard/template-list/column-template-running.html",
verbose_name=_("Running"),
attrs={'th': {'data-sort': "int"}},
orderable=False,
)
actions = TemplateColumn(
verbose_name=_("Actions"),
......@@ -187,8 +189,8 @@ class TemplateListTable(Table):
model = InstanceTemplate
attrs = {'class': ('table table-bordered table-striped table-hover'
' template-list-table')}
fields = ('name', 'num_cores', 'ram_size', 'system',
'access_method', 'lease', 'owner', 'running', 'actions', )
fields = ('name', 'resources', 'system', 'access_method', 'lease',
'owner', 'created', 'running', 'actions', )
prefix = "template-"
......@@ -248,5 +250,41 @@ class UserKeyListTable(Table):
class Meta:
model = UserKey
attrs = {'class': ('table table-bordered table-striped table-hover')}
attrs = {'class': ('table table-bordered table-striped table-hover'),
'id': "profile-key-list-table"}
fields = ('name', 'fingerprint', 'created', 'actions')
prefix = "key-"
empty_text = _("You haven't added any public keys yet.")
class ConnectCommandListTable(Table):
name = LinkColumn(
'dashboard.views.connect-command-detail',
args=[A('pk')],
attrs={'th': {'data-sort': "string"}}
)
access_method = Column(
verbose_name=_("Access method"),
attrs={'th': {'data-sort': "string"}}
)
template = Column(
verbose_name=_("Template"),
attrs={'th': {'data-sort': "string"}}
)
actions = TemplateColumn(
verbose_name=_("Actions"),
template_name=("dashboard/connect-command-list/column-command"
"-actions.html"),
orderable=False,
)
class Meta:
model = ConnectCommand
attrs = {'class': ('table table-bordered table-striped table-hover'),
'id': "profile-command-list-table"}
fields = ('name', 'access_method', 'template', 'actions')
prefix = "cmd-"
empty_text = _(
"You don't have any custom connection commands yet. You can "
"specify commands to be displayed on VM detail pages instead of "
"the defaults.")
......@@ -41,6 +41,7 @@ def send_email_notifications():
for i in Notification.objects.filter(q):
recipients.setdefault(i.to, [])
recipients[i.to].append(i)
logger.info("Delivering notifications to %d users", len(recipients))
for user, msgs in recipients.iteritems():
if (not user.profile or not user.email or not
......
{% load i18n %}
{% if user and user.pk %}
{% if user.get_full_name %}
{{ user.get_full_name }}
{% else %}
{{ user.username }}
{% endif %}
{% if user.get_full_name %}{{ user.get_full_name }}{% else %}{{ user.username }}{% endif %}{% if new_line %}<br />{% endif %}
{% if show_org %}
{% if user.profile and user.profile.org_id %}
......
{% load i18n %}
{% load hro %}
{% for n in notifications %}
<li class="notification-message" id="msg-{{n.id}}">
<span class="notification-message-subject">
{% if n.status == "new" %}<i class="fa fa-envelope-alt"></i> {% endif %}
{{ n.subject.get_user_text }}
{% if n.status == "new" %}<i class="fa fa-envelope-o"></i> {% endif %}
{{ n.subject|get_text:user }}
</span>
<span class="notification-message-date pull-right" title="{{n.created}}">
{{ n.created|timesince }}
</span>
<div style="clear: both;"></div>
<div class="notification-message-text">
{{ n.message.get_user_text|safe }}
{{ n.message|get_text:user|safe }}
</div>
</li>
{% empty %}
......
......@@ -7,7 +7,6 @@
{% csrf_token %}
{{ vm_create_form.template }}
{{ vm_create_form.customized }}
<div class="row">
<div class="col-sm-12">
......@@ -23,6 +22,8 @@
</div>
</div>
{% if perms.vm.set_resources %}
{{ vm_create_form.customized }}
<div class="row">
<div class="col-sm-10">
<div class="form-group">
......@@ -85,6 +86,7 @@
</div><!-- .no-js-hidden -->
</div><!-- .col-sm-8 -->
</div><!-- .row -->
{% endif %}
</form>
<script>
......
{% extends "dashboard/mass-operate.html" %}
{% load i18n %}
{% load sizefieldtags %}
{% block formfields %}
<hr />
<ul id="vm-migrate-node-list" class="list-unstyled">
<li class="panel panel-default panel-primary mass-migrate-node">
<div class="panel-body">
<label for="migrate-to-none">
<strong>{% trans "Reschedule" %}</strong>
</label>
<input id="migrate-to-none" type="radio" name="node" value="" style="float: right;" checked="checked">
<span class="vm-migrate-node-property">
{% trans "This option will reschedule each virtual machine to the optimal node." %}
</span>
<div style="clear: both;"></div>
</div>
</li>
{% for n in nodes %}
<li class="panel panel-default mass-migrate-node">
<div class="panel-body">
<label for="migrate-to-{{n.pk}}">
<strong>{{ n }}</strong>
</label>
<input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;"/>
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
<div style="clear: both;"></div>
</div>
</li>
{% endfor %}
</ul>
<hr />
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title-page %}{% trans "Create command template" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.profile-preferences" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="fa fa-code"></i> {% trans "Create new command template" %}</h3>
</div>
<div class="panel-body">
<form method="POST">
{% csrf_token %}
{{ form.name|as_crispy_field }}
{{ form.access_method|as_crispy_field }}
{{ form.template|as_crispy_field }}
<p class="text-muted">
{% trans "Examples" %}
</p>
<p>
<strong>SSH:</strong>
<span class="text-muted">
sshpass -p %(password)s ssh -o StrictHostKeyChecking=no cloud@%(host)s -p %(port)d
</span>
</p>
<p>
<strong>RDP:</strong>
<span class="text-muted">
rdesktop %(host)s:%(port)d -u cloud -p %(password)s
</span>
</p>
<input type="submit" class="btn btn-primary" value="{% trans "Save" %}">
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title-page %}{% trans "Edit command template" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.profile-preferences" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="fa fa-code"></i> {% trans "Edit command template" %}</h3>
</div>
<div class="panel-body">
<form method="POST">
{% csrf_token %}
{{ form.name|as_crispy_field }}
{{ form.access_method|as_crispy_field }}
{{ form.template|as_crispy_field }}
<p class="text-muted">
{% trans "Examples" %}
</p>
<p>
<strong>SSH:</strong>
<span class="text-muted">
sshpass -p %(password)s ssh -o StrictHostKeyChecking=no cloud@%(host)s -p %(port)d
</span>
</p>
<p>
<strong>RDP:</strong>
<span class="text-muted">
rdesktop %(host)s:%(port)d -u cloud -p %(password)s
</span>
</p>
<input type="submit" class="btn btn-primary" value="{% trans "Save" %}">
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% load i18n %}
<a href="{% url "dashboard.views.connect-command-detail" pk=record.pk%}" id="template-list-edit-button" class="btn btn-default btn-xs" title="{% trans "Edit" %}">
<i class="fa fa-edit"></i>
</a>
<a data-template-pk="{{ record.pk }}" href="{% url "dashboard.views.connect-command-delete" pk=record.pk %}" class="btn btn-danger btn-xs template-delete" title="{% trans "Delete" %}">
<i class="fa fa-times"></i>
</a>
{% load crispy_forms_tags %}
{% load i18n %}
<p class="text-muted">
{% trans "User groups allow sharing templates or other resources with multiple users at once." %}
</p>
<form method="POST" action="{% url "dashboard.views.group-create" %}">
{% csrf_token %}
......
......@@ -6,63 +6,24 @@
{% block content %}
<div class="alert alert-info">
Tip #1: you can select multiple vm instances while holding down the <strong>CTRL</strong> key!
</div>
<div class="alert alert-info">
Tip #2: if you want to select multiple instances by one click select an instance then hold down <strong>SHIFT</strong> key and select another one!
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin"><i class="fa fa-group"></i> Your groups</h3>
</div>
<div class="panel-body group-list-group-control">
<p>
<strong>Group actions</strong>
<button id="group-list-group-select-all" class="btn btn-info btn-xs">Select all</button>
<a id="group-list-group-delete" disabled href="#" class="btn btn-danger btn-xs"><i class="fa fa-times"></i> Discard</a>
</p>
</div>
<div class="panel-body">
<div id="table_container">
<div id="rendered_table" class="panel-body">
{% render_table table %}
<div id="rendered_table" class="panel-body">
{% render_table table %}
</div>
</div>
</div>
</div><!-- .panel-body -->
</div>
</div>
</div>
<style>
.popover {
max-width: 600px;
}
.group-list-selected, .group-list-selected td {
background-color: #e8e8e8 !important;
}
.group-list-selected:hover, .group-list-selected:hover td {
background-color: #d0d0d0 !important;
}
.group-list-selected td:first-child {
font-weight: bold;
}
.group-list-table-thin {
width: 10px;
}
.group-list-table-admin {
width: 130px;
}
</style>
{% endblock %}
{% block extra_js %}
<script src="{{ STATIC_URL}}dashboard/group-list.js"></script>
<script src="{{ STATIC_URL}}dashboard/group-list.js"></script>
{% endblock %}
<a data-group-pk="{{ record.pk }}" class="btn btn-danger btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=record.pk %}?next={{ request.path }}"><i class="fa fa-trash"></i></a>
<a data-group-pk="{{ record.pk }}"
class="btn btn-danger btn-xs real-link group-delete"
href="{% url "dashboard.views.delete-group" pk=record.pk %}?next={{ request.path }}">
<i class="fa fa-trash-o"></i>
</a>
......@@ -3,8 +3,10 @@
<div class="panel-heading">
<div class="pull-right toolbar">
<div class="btn-group">
<a href="#index-graph-view" data-index-box="node" class="btn btn-default btn-xs"><i class="fa fa-dashboard"></i></a>
<a href="#index-list-view" data-index-box="node" class="btn btn-default btn-xs disabled"><i class="fa fa-list"></i></a>
<a href="#index-graph-view" data-index-box="node" class="btn btn-default btn-xs"
data-container="body"><i class="fa fa-dashboard"></i></a>
<a href="#index-list-view" data-index-box="node" class="btn btn-default btn-xs disabled"
data-container="body"><i class="fa fa-list"></i></a>
</div>
<span class="btn btn-default btn-xs infobtn" title="{% trans "List of compute nodes, also called worker nodes or hypervisors, which run the virtual machines." %}"><i class="fa fa-info-circle"></i></span>
......
......@@ -3,10 +3,12 @@
<div class="panel-heading">
<div class="pull-right toolbar">
<div class="btn-group">
<a href="#index-graph-view" data-index-box="vm" class="btn
btn-default btn-xs" title="{% trans "summary view" %}"><i class="fa fa-dashboard"></i></a>
<a href="#index-list-view" data-index-box="vm" class="btn
btn-default btn-xs disabled" title="{% trans "list view" %}"><i class="fa fa-list"></i></a>
<a href="#index-graph-view" data-index-box="vm" class="btn btn-default btn-xs"
data-container="body"
title="{% trans "summary view" %}"><i class="fa fa-dashboard"></i></a>
<a href="#index-list-view" data-index-box="vm" class="btn btn-default btn-xs disabled"
data-container="body"
title="{% trans "list view" %}"><i class="fa fa-list"></i></a>
</div>
<span class="btn btn-default btn-xs infobtn" title="{% trans "List of your current virtual machines. Favourited ones are ahead of others." %}"><i class="fa fa-info-circle"></i></span>
</div>
......@@ -46,12 +48,15 @@
</style>
<div href="#" class="list-group-item list-group-footer">
<div class="row">
<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..." %}" />
<div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary"><i class="fa fa-search"></i></button>
<form action="{% url "dashboard.views.vm-list" %}" method="GET" id="dashboard-vm-search-form">
<div class="col-sm-6 col-xs-6 input-group input-group-sm">
<input id="dashboard-vm-search-input" type="text" class="form-control" name="s"
placeholder="{% trans "Search..." %}" />
<div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
</form>
<div class="col-sm-6 text-right">
<a class="btn btn-primary btn-xs" href="{% url "dashboard.views.vm-list" %}">
<i class="fa fa-chevron-circle-right"></i>
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load hro %}
{% block content %}
<div class="body-content">
<div class="page-header">
<h1>
{{ object.instance.name }}:
{% if user.is_superuser %}
{{object.readable_name.get_admin_text}}
{% else %}
{{object.readable_name.get_user_text}}
{% endif %}
<h1><i class="fa fa-{{icon}}"></i>
{{ object.instance.name }}: {{object.readable_name|get_text:user}}
</h1>
</div>
<div class="row">
......@@ -58,7 +54,7 @@
<dt>{% trans "result" %}</dt>
<dd><textarea class="form-control">{% if user.is_superuser %}{{object.result.get_admin_text}}{% else %}{{object.result.get_user_text}}{% endif %}</textarea></dd>
<dd><textarea class="form-control">{{object.result|get_text:user}}</textarea></dd>
<dt>{% trans "resultant state" %}</dt>
<dd>{{object.resultant_state|default:'n/a'}}</dd>
......
......@@ -11,7 +11,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="fa fa-time"></i> {% trans "Create lease" %}</h3>
<h3 class="no-margin"><i class="fa fa-clock-o"></i> {% trans "Create lease" %}</h3>
</div>
<div class="panel-body">
{% with form=form %}
......
......@@ -10,7 +10,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="fa fa-time"></i> {% trans "Edit lease" %}</h3>
<h3 class="no-margin"><i class="fa fa-clock-o"></i> {% trans "Edit lease" %}</h3>
</div>
<div class="panel-body">
{% with form=form %}
......
{% load i18n %}
{% load crispy_forms_tags %}
{% block question %}
<p>
{% blocktrans with op=op.name count count=vm_count %}
Do you want to perform the <strong>{{op}}</strong> operation on the following instance?
{% plural %}
Do you want to perform the <strong>{{op}}</strong> operation on the following {{ count }} instances?
{% endblocktrans %}
</p>
<p class="text-info">{{op.description}}</p>
{% endblock %}
<form method="POST" action="{{url}}">{% csrf_token %}
{% block formfields %}{% endblock %}
{% for i in instances %}
<div class="panel panel-default mass-op-panel">
<i class="fa {{ i.get_status_icon }} fa-fw"></i>
{{ i.name }} ({{ i.pk }})
<div style="float: right;" title="{{ i.disabled }}" class="status-icon">
<span class="fa-stack">
<i class="fa fa-stack-2x fa-square {{ i.disabled|yesno:"minus,check" }}"></i>
<i class="fa fa-stack-1x fa-inverse fa-{% if i.disabled %}{{i.disabled_icon|default:"minus"}}{% else %}check{% endif %}"></i>
</span>
</div>
</div>
<input type="checkbox" name="vm" value="{{ i.pk }}" {% if not i.disabled %}checked{% endif %}
style="display: none;"/>
{% endfor %}
<div class="pull-right">
<a class="btn btn-default" href="{% url "dashboard.views.vm-list" %}"
data-dismiss="modal">{% trans "Cancel" %}</a>
<button class="btn btn-{{ opview.effect }}" type="submit" id="op-form-send">
{% if opview.icon %}<i class="fa fa-fw fa-{{opview.icon}}"></i> {% endif %}{{ opview.name|capfirst }}
</button>
</div>
</form>
{% load i18n %}
{% load hro %}
<div id="activity-timeline" class="timeline">
{% for a in activities %}
<div class="activity" data-activity-id="{{ a.pk }}">
......@@ -16,10 +17,7 @@
<div data-activity-id="{{ s.pk }}"
class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}"
>
{% if user.is_superuser %}
{{ s.readable_name.get_admin_text }}
{% else %}
{{ s.readable_name.get_user_text }}{% endif %}
{{ s.readable_name|get_text:user }}
&ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
......
......@@ -66,4 +66,20 @@
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<a href="{% url "dashboard.views.connect-command-create" %}"
class="pull-right btn btn-success btn-xs" style="margin-right: 10px;">
<i class="fa fa-plus"></i> {% trans "add command template" %}
</a>
<h3 class="no-margin"><i class="fa fa-code"></i> {% trans "Command templates" %}</h3>
</div>
<div class="panel-body">
{% render_table connectcommand_table %}
</div>
</div>
</div>
</div>
{% endblock %}
......@@ -16,6 +16,23 @@
<h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %}</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-offset-8 col-md-4" id="template-list-search">
<form action="" method="GET">
<div class="input-group">
{{ search_form.s }}
<div class="input-group-btn">
{{ search_form.stype }}
<button type="submit" class="btn btn-primary input-tags">
<i class="fa fa-search"></i>
</button>
</div>
</div><!-- .input-group -->
</form>
</div><!-- .col-md-4 #template-list-search -->
</div>
</div>
<div class="panel-body">
{% render_table table %}
</div>
</div>
......@@ -31,7 +48,7 @@
<i class="fa fa-plus"></i> {% trans "new lease" %}
</a>
{% endif %}
<h3 class="no-margin"><i class="fa fa-time"></i> {% trans "Leases" %}</h3>
<h3 class="no-margin"><i class="fa fa-clock-o"></i> {% trans "Leases" %}</h3>
</div>
<div class="panel-body">
<div class="" style="max-width: 600px;">
......
{% load i18n %}
<a href="{% url "dashboard.views.vm-create" %}?template={{ record.pk }}"
class="btn btn-success btn-xs customize-vm" title="{% trans "Start" %}">
<i class="fa fa-play"></i>
</a>
<a href="{% url "dashboard.views.template-detail" pk=record.pk%}" id="template-list-edit-button" class="btn btn-default btn-xs" title="{% trans "Edit" %}">
<i class="fa fa-edit"></i>
</a>
......
{{ record.created|date }}
<br />
{{ record.created|time }}
{% include "dashboard/_display-name.html" with user=record.owner show_org=True %}
{% include "dashboard/_display-name.html" with user=record.owner show_org=True new_line=True %}
{% load i18n %}
{{ record.ram_size }}MiB RAM
<br />
{% blocktrans with num_cores=record.num_cores count count=record.num_cores %}
{{ num_cores }} CPU core
{% plural %}
{{ num_cores }} CPU cores
{% endblocktrans %}
<a href="{% url "dashboard.views.vm-list" %}?s=template:{{ record.pk }}%20status:running">
{{ record.get_running_instances.count }}
{{ record.running }}
</a>
......@@ -98,8 +98,9 @@
<div class="input-group">
<input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags"
value="{{ instance.pw }}" spellcheck="false"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show">
<i class="fa fa-eye" id="vm-details-pw-eye" title="Show password"></i>
<span class="input-group-addon input-tags" id="vm-details-pw-show"
title="{% trans "Show password" %}" data-container="body">
<i class="fa fa-eye" id="vm-details-pw-eye"></i>
</span>
</div>
</dd>
......@@ -111,16 +112,28 @@
</div>
</dd>
</dl>
<div class="input-group" id="dashboard-vm-details-connect-command">
{% for c in connect_commands %}
<div class="input-group dashboard-vm-details-connect-command">
<span class="input-group-addon input-tags">{% trans "Command" %}</span>
<input type="text" spellcheck="false"
value="{% if instance.get_connect_command %}{{ instance.get_connect_command }}{% else %}{% trans "Connection is not possible." %}{% endif %}"
value="{{ c }}"
id="vm-details-connection-string" class="form-control input-tags" />
<span class="input-group-addon input-tags vm-details-connection-string-copy"
title="{% trans "Select all" %}" data-container="body">
<i class="fa fa-copy"></i>
</span>
</div>
{% empty %}
<div class="input-group dashboard-vm-details-connect-command">
<span class="input-group-addon input-tags">{% trans "Command" %}</span>
<input type="text" spellcheck="false" value="{% trans "Connection is not possible." %}"
id="vm-details-connection-string" class="form-control input-tags" />
<span class="input-group-addon input-tags" id="vm-details-connection-string-copy">
<i class="fa fa-copy" title="{% trans "Select all" %}"></i>
</span>
</div>
{% endfor %}
<div id="dashboard-vm-details-connect" class="operation-wrapper">
<a id="dashboard-vm-details-connect-button" class="btn btn-xs btn-default{% if not client_download %} operation{% endif %}{% if instance.status != "RUNNING" or not instance.get_connect_uri %} disabled{% endif %}" {% if client_download %}target="_blank"{% endif %} href="{% if not client_download %}{% url "dashboard.views.client-check" %}?vm={{ instance.pk }}{% else %}{{ instance.get_connect_uri}}{% endif %}" title="{% trans "Connect via the CIRCLE Client" %}">
<i class="fa fa-external-link"></i> {% trans "Connect" %}
......
{% load i18n %}
{% load hro %}
<div id="activity-timeline" class="timeline">
{% for a in activities %}
<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" data-activity-id="{{ a.pk }}">
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
<i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-plus{% endif %}"></i>
<i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-{{a.icon}}{% endif %}"></i>
</span>
<strong{% if a.result %} title="{{ a.result.get_user_text }}"{% endif %}>
<strong{% if a.result %} title="{{ a.result|get_text:user }}"{% endif %}>
<a href="{{ a.get_absolute_url }}">
{% if a.times > 1 %}({{ a.times }}x){% endif %}
{{ a.readable_name.get_user_text|capfirst }}</a>
{{ a.readable_name|get_text:user|capfirst }}</a>
{% if a.has_percent %}
- {{ a.percentage }}%
......@@ -33,9 +33,9 @@
<div class="sub-timeline">
{% for s in a.children.all %}
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}{% if s.pk == active.pk %} sub-activity-active{% endif %}">
<span{% if s.result %} title="{{ s.result.get_user_text }}"{% endif %}>
<span{% if s.result %} title="{{ s.result|get_text:user }}"{% endif %}>
<a href="{{ s.get_absolute_url }}">
{{ s.readable_name.get_user_text|capfirst }}</a></span> &ndash;
{{ s.readable_name|get_text:user|capfirst }}</a></span> &ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
......
......@@ -7,7 +7,7 @@
<span class="operation operation-{{op.op}} btn btn-default disabled btn-xs">
{% else %}
<a href="{{op.get_url}}" class="operation operation-{{op.op}} btn
btn-{{op.effect}} btn-xs" title="{{op.name|capfirst}}: {{op.description|truncatewords:20}}">
btn-{{op.effect}} btn-xs" title="{{op.name|capfirst}}: {{op.description|truncatewords:15}}">
{% endif %}
<i class="fa fa-{{op.icon}}"></i>
<span{% if not op.is_preferred %} class="sr-only"{% endif %}>{{op.name}}</span>
......
......@@ -94,12 +94,32 @@
<dt>{% trans "Template" %}:</dt>
<dd>
{% if instance.template %}
{{ instance.template.name }}
{% if can_link_template %}
<a href="{{ instance.template.get_absolute_url }}">
{{ instance.template.name }}
</a>
{% else %}
{{ instance.template.name }}
{% endif %}
{% else %}
-
{% endif %}
</dd>
</dl>
{% if op.mount_store %}
<strong>{% trans "Store" %}</strong>
<p>
{{ op.mount_store.description }}
</p>
<div class="operation-wrapper">
<a href="{{ op.mount_store.get_url }}" class="btn btn-info btn-xs operation"
{% if op.mount_store.disabled %}disabled{% endif %}>
<i class="fa fa-{{op.mount_store.icon}}"></i>
{{ op.mount_store.name }}
</a>
</div>
{% endif %}
</div>
<div class="col-md-8">
{% if graphite_enabled %}
......
......@@ -15,26 +15,42 @@
</div>
<h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Virtual machines" %}</h3>
</div>
<div class="pull-right" style="max-width: 250px; margin-top: 15px; margin-right: 15px;">
<form action="" method="GET" class="input-group">
<input type="text" name="s"{% if request.GET.s %} value="{{ request.GET.s }}"{% endif %} class="form-control input-tags" placeholder="{% trans "Search..."%}" />
<div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary input-tags" title="search"><i class="fa fa-search"></i></button>
</div>
</form>
</div>
<div class="panel-body vm-list-group-control">
<p>
<strong>{% trans "Group actions" %}</strong>
<button id="vm-list-group-select-all" class="btn btn-info btn-xs">{% trans "Select all" %}</button>
<a class="btn btn-default btn-xs" id="vm-list-group-migrate" disabled><i class="fa fa-truck"></i> {% trans "Migrate" %}</a>
<a disabled href="#" class="btn btn-default btn-xs"><i class="fa fa-refresh"></i> {% trans "Reboot" %}</a>
<a disabled href="#" class="btn btn-default btn-xs"><i class="fa fa-off"></i> {% trans "Shutdown" %}</a>
<a id="vm-list-group-delete" disabled href="#" class="btn btn-danger btn-xs"><i class="fa fa-times"></i> {% trans "Destroy" %}</a>
</p>
</div>
<div class="panel-body">
<table class="table table-bordered table-striped table-hover vm-list-table">
<div class="row">
<div class="col-md-8 vm-list-group-control" id="vm-mass-ops">
<strong>{% trans "Group actions" %}</strong>
<button id="vm-list-group-select-all" class="btn btn-info btn-xs">{% trans "Select all" %}</button>
{% for o in ops %}
<a href="{{ o.get_url }}" class="btn btn-xs btn-{{ o.effect }} mass-operation"
title="{{ o.name|capfirst }}" disabled>
<i class="fa fa-{{ o.icon }}"></i>
</a>
{% endfor %}
</div><!-- .vm-list-group-control -->
<div class="col-md-4" id="vm-list-search">
<form action="" method="GET">
<div class="input-group">
{{ search_form.s }}
<div class="input-group-btn">
{{ search_form.stype }}
</div>
<label class="input-group-addon input-tags" title="{% trans "Include deleted VMs" %}"
id="vm-list-search-checkbox-span" data-container="body">
{{ search_form.include_deleted }}
</label>
<div class="input-group-btn">
<button type="submit" class="btn btn-primary input-tags">
<i class="fa fa-search"></i>
</button>
</div>
</div><!-- .input-group -->
</form>
</div><!-- .col-md-4 #vm-list-search -->
</div><!-- .row -->
</div><!-- .panel-body -->
<div class="panel-body">
<table class="table table-bordered table-striped table-hover vm-list-table"
id="vm-list-table">
<thead><tr>
<th data-sort="int" class="orderable pk sortable vm-list-table-thin" style="min-width: 50px;">
{% trans "ID" as t %}
......@@ -52,26 +68,56 @@
{% trans "Owner" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="owner" %}
</th>
{% if user.is_superuser %}<th data-sort="string" class="orderable sortable">
{% trans "Node" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="node" %}
</th>{% endif %}
<th data-sort="string" class="orderable sortable">
{% trans "Lease" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="lease" %}
</th>
{% if user.is_superuser %}
<th data-sort="string" class="orderable sortable">
{% trans "IP address" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="ip_addr" %}
</th>
<th data-sort="string" class="orderable sortable">
{% trans "Node" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="node" %}
</th>
{% endif %}
</tr></thead><tbody>
{% for i in object_list %}
<tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}">
<td class="pk"><div id="vm-{{i.pk}}">{{i.pk}}</div> </td>
<td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">{{ i.name }}</a> </td>
<td class="state">{{ i.get_status_display }}</td>
<td class="state">
<i class="fa fa-fw
{% if show_acts_in_progress and i.is_in_status_change %}
fa-spin fa-spinner
{% else %}
{{ i.get_status_icon }}{% endif %}"></i>
<span>{{ i.get_status_display }}</span>
</td>
<td>
{% include "dashboard/_display-name.html" with user=i.owner show_org=True %}
{% if i.owner.profile %}
{{ i.owner.profile.get_display_name }}
{% else %}
{{ i.owner.username }}
{% endif %}
{# include "dashboard/_display-name.html" with user=i.owner show_org=True #}
</td>
<td class="lease "data-sort-value="{{ i.lease.name }}">
{{ i.lease.name }}
</td>
{% if user.is_superuser %}
<td data-sort-value="{{ i.node.normalized_name }}">{{ i.node.name|default:"-" }}</td>
<td class="ip_addr "data-sort-value="{{ i.ipv4 }}">
{{ i.ipv4|default:"-" }}
</td>
<td class="node "data-sort-value="{{ i.node.normalized_name }}">
{{ i.node.name|default:"-" }}
</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="5">
<td colspan="7">
{% if request.GET.s %}
<strong>{% trans "No result." %}</strong>
{% else %}
......@@ -87,6 +133,9 @@
</div>
</div>
<div class="alert alert-info">
You can filter the list by certain attributes (owner, name, status, tags) in the following way: "owner:John Doe name:my little server". If you don't specify any attribute names the filtering will be done by name.
</div>
<div class="alert alert-info">
{% trans "You can select multiple vm instances while holding down the <strong>CTRL</strong> key." %}
......@@ -97,6 +146,5 @@
{% block extra_js %}
<script src="{{ STATIC_URL}}dashboard/vm-list.js"></script>
<script src="{{ STATIC_URL}}dashboard/vm-common.js"></script>
<script src="{{ STATIC_URL}}dashboard/js/stupidtable.min.js"></script>
{% endblock %}
from django.template import Library
register = Library()
@register.filter
def get_text(human_readable, user):
if human_readable is None:
return u""
else:
return human_readable.get_text(user)
......@@ -29,7 +29,7 @@ from django.utils import baseconv
from ..models import Profile
from ..views import InstanceActivityDetail, InstanceActivity
from ..views import vm_ops, Instance, UnsubscribeFormView
from ..views import vm_ops, vm_mass_ops, Instance, UnsubscribeFormView
from ..views import AclUpdateView
from .. import views
......@@ -259,6 +259,114 @@ class VmOperationViewTestCase(unittest.TestCase):
self.assertEquals(rend.status_code, 200)
class VmMassOperationViewTestCase(unittest.TestCase):
def test_available(self):
request = FakeRequestFactory(superuser=True)
view = vm_mass_ops['destroy']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.destroy = Instance._ops['destroy'](inst)
go.return_value = [inst]
self.assertEquals(
view.as_view()(request, pk=1234).render().status_code, 200)
def test_unpermitted_choice(self):
"User has user level, but not the needed ownership."
request = FakeRequestFactory()
view = vm_mass_ops['destroy']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.has_level = lambda self, l: {"user": True, "owner": False}[l]
inst.destroy = Instance._ops['destroy'](inst)
inst.destroy._operate = MagicMock()
go.return_value = [inst]
view.as_view()(request, pk=1234).render()
assert not inst.destroy._operate.called
def test_unpermitted(self):
request = FakeRequestFactory()
view = vm_mass_ops['destroy']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.destroy = Instance._ops['destroy'](inst)
inst.has_level.return_value = False
go.return_value = [inst]
with self.assertRaises(PermissionDenied):
view.as_view()(request, pk=1234).render()
def test_migrate(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=True)
view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.has_level.return_value = True
go.return_value = [inst]
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)['location']
assert not msg.error.called
def test_migrate_failed(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=True)
view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.migrate.async.side_effect = Exception
inst.has_level.return_value = True
go.return_value = [inst]
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)['location']
assert msg.error.called
def test_migrate_wo_permission(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=False)
view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.has_level.return_value = True
go.return_value = [inst]
go4.return_value = MagicMock()
with self.assertRaises(PermissionDenied):
assert view.as_view()(request, pk=1234)['location']
def test_migrate_template(self):
"""check if GET dialog's template can be rendered"""
request = FakeRequestFactory(superuser=True)
view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.has_level.return_value = True
go.return_value = [inst]
self.assertEquals(
view.as_view()(request, pk=1234).render().status_code, 200)
class RenewViewTest(unittest.TestCase):
def test_renew_template(self):
......
......@@ -107,20 +107,6 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.get('/dashboard/vm/1/')
self.assertEqual(response.status_code, 200)
def test_unpermitted_vm_mass_delete(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]})
self.assertEqual(response.status_code, 403)
def test_permitted_vm_mass_delete(self):
c = Client()
self.login(c, 'user2')
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]})
self.assertEqual(response.status_code, 302)
def test_unpermitted_password_change(self):
c = Client()
self.login(c, "user2")
......
......@@ -29,7 +29,7 @@ from .views import (
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete,
VmDetailVncTokenView, VmGraphView, VmList,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveUserView,
GroupRemoveFutureUserView,
......@@ -39,6 +39,7 @@ from .views import (
get_vm_screenshot,
ProfileView, toggle_use_gravatar, UnsubscribeFormView,
UserKeyDelete, UserKeyDetail, UserKeyCreate,
ConnectCommandDelete, ConnectCommandDetail, ConnectCommandCreate,
StoreList, store_download, store_upload, store_get_upload_url, StoreRemove,
store_new_directory, store_refresh_toplist,
VmTraitsUpdate, VmRawDataUpdate,
......@@ -50,7 +51,6 @@ from .views import (
autocomplete_light.autodiscover()
urlpatterns = patterns(
'',
url(r'^$', IndexView.as_view(), name="dashboard.index"),
url(r'^lease/(?P<pk>\d+)/$', LeaseDetail.as_view(),
......@@ -75,7 +75,7 @@ urlpatterns = patterns(
url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(),
name="dashboard.views.template-delete"),
url(r'^vm/(?P<pk>\d+)/op/', include('dashboard.vm.urls')),
url(r'^vm/', include('dashboard.vm.urls')),
url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
......@@ -89,8 +89,6 @@ urlpatterns = patterns(
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(),
name='dashboard.views.vm-create'),
url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity),
url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(),
name='dashboard.views.vm-activity'),
......@@ -181,6 +179,16 @@ urlpatterns = patterns(
UserKeyCreate.as_view(),
name="dashboard.views.userkey-create"),
url(r'^conncmd/delete/(?P<pk>\d+)/$',
ConnectCommandDelete.as_view(),
name="dashboard.views.connect-command-delete"),
url(r'^conncmd/(?P<pk>\d+)/$',
ConnectCommandDetail.as_view(),
name="dashboard.views.connect-command-detail"),
url(r'^conncmd/create/$',
ConnectCommandCreate.as_view(),
name="dashboard.views.connect-command-create"),
url(r'^autocomplete/', include('autocomplete_light.urls')),
url(r"^store/list/$", StoreList.as_view(),
......
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from lxml import etree as ET
import logging
......@@ -29,3 +31,27 @@ def domain_validator(value):
relaxng.assertValid(parsed_xml)
except Exception as e:
raise ValidationError(e.message)
def connect_command_template_validator(value):
"""Validate value as a connect command template.
>>> try: connect_command_template_validator("%(host)s")
... except ValidationError as e: print e
...
>>> connect_command_template_validator("%(host)s")
>>> try: connect_command_template_validator("%(host)s %s")
... except ValidationError as e: print e
...
[u'Invalid template string.']
"""
try:
value % {
'username': "uname",
'password': "pw",
'host': "111.111.111.111",
'port': 12345,
}
except (KeyError, TypeError, ValueError):
raise ValidationError(_("Invalid template string."))
......@@ -67,24 +67,29 @@ from .forms import (
CircleAuthenticationForm, HostForm, LeaseForm, MyProfileForm,
NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm,
UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
VmSaveForm, UserKeyForm, VmRenewForm,
VmSaveForm, UserKeyForm, VmRenewForm, VmStateChangeForm,
CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
TraitsForm, RawDataForm, GroupPermissionForm, AclUserAddForm,
VmResourcesForm, VmAddInterfaceForm,
VmResourcesForm, VmAddInterfaceForm, VmListSearchForm,
TemplateListSearchForm, ConnectCommandForm
)
from .tables import (
NodeListTable, TemplateListTable, LeaseListTable,
GroupListTable, UserKeyListTable
GroupListTable, UserKeyListTable, ConnectCommandListTable,
)
from common.models import (
HumanReadableObject, HumanReadableException, fetch_human_exception,
create_readable,
)
from common.models import HumanReadableObject, HumanReadableException
from vm.models import (
Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
InterfaceTemplate, Lease, Node, NodeActivity, Trait,
)
from storage.models import Disk
from firewall.models import Vlan, Host, Rule
from .models import Favourite, Profile, GroupProfile, FutureMember
from .models import (Favourite, Profile, GroupProfile, FutureMember,
ConnectCommand)
from .store_api import Store, NoStoreException, NotOkException
......@@ -169,6 +174,65 @@ class FilterMixin(object):
return super(FilterMixin,
self).get_queryset().filter(**self.get_queryset_filters())
def create_fake_get(self):
self.request.GET = self._parse_get(self.request.GET)
def _parse_get(self, GET_dict):
"""
Returns a new dict from request's GET dict to filter the vm list
For example: "name:xy node:1" updates the GET dict
to resemble this URL ?name=xy&node=1
"name:xy node:1".split(":") becomes ["name", "xy node", "1"]
we pop the the first element and use it as the first dict key
then we iterate over the rest of the list and split by the last
whitespace, the first part of this list will be the previous key's
value, then last part of the list will be the next key.
The final dict looks like this: {'name': xy, 'node':1}
>>> f = FilterMixin()
>>> o = f._parse_get({'s': "hello"}).items()
>>> sorted(o) # doctest: +ELLIPSIS
[(u'name', u'hello'), (...)]
>>> o = f._parse_get({'s': "name:hello owner:test"}).items()
>>> sorted(o) # doctest: +ELLIPSIS
[(u'name', u'hello'), (u'owner', u'test'), (...)]
>>> o = f._parse_get({'s': "name:hello ws node:node 3 oh"}).items()
>>> sorted(o) # doctest: +ELLIPSIS
[(u'name', u'hello ws'), (u'node', u'node 3 oh'), (...)]
"""
s = GET_dict.get("s")
fake = GET_dict.copy()
if s:
s = s.split(":")
if len(s) < 2: # if there is no ':' in the string, filter by name
got = {'name': s[0]}
else:
latest = s.pop(0)
got = {'%s' % latest: None}
for i in s[:-1]:
new = i.rsplit(" ", 1)
got[latest] = new[0]
latest = new[1] if len(new) > 1 else None
got[latest] = s[-1]
# generate a new GET request, that is kinda fake
for k, v in got.iteritems():
fake[k] = v
return fake
def create_acl_queryset(self, model):
cleaned_data = self.search_form.cleaned_data
stype = cleaned_data.get('stype', "all")
superuser = stype == "all"
shared = stype == "shared"
level = "owner" if stype == "owned" else "user"
queryset = model.get_objects_with_level(
level, self.request.user,
group_also=shared, disregard_superuser=not superuser,
)
return queryset
class IndexView(LoginRequiredMixin, TemplateView):
template_name = "dashboard/index.html"
......@@ -225,7 +289,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
# template
if user.has_perm('vm.create_template'):
context['templates'] = InstanceTemplate.get_objects_with_level(
'operator', user).all()[:5]
'operator', user, disregard_superuser=True).all()[:5]
# toplist
if settings.STORE_URL:
......@@ -303,13 +367,14 @@ class VmDetailView(CheckedDetailView):
kwargs={'pk': self.object.pk}),
'ops': ops,
'op': {i.op: i for i in ops},
'connect_commands': user.profile.get_connect_commands(instance)
})
# activity data
activities = instance.get_merged_activities(user)
show_show_all = len(activities) > 10
activities = activities[:10]
context['activities'] = activities
context['activities'] = _format_activities(activities)
context['show_show_all'] = show_show_all
latest = instance.get_latest_activity_in_progress()
context['is_new_state'] = (latest and
......@@ -333,7 +398,7 @@ class VmDetailView(CheckedDetailView):
# resources forms
can_edit = (
instance in Instance.get_objects_with_level("owner", user)
instance.has_level(user, "owner")
and self.request.user.has_perm("vm.change_resources"))
context['resources_form'] = VmResourcesForm(
can_edit=can_edit, instance=instance)
......@@ -349,6 +414,10 @@ class VmDetailView(CheckedDetailView):
# client info
context['client_download'] = self.request.COOKIES.get(
'downloaded_client')
# can link template
context['can_link_template'] = (
instance.template and instance.template.has_level(user, "operator")
)
return context
......@@ -522,6 +591,7 @@ class OperationView(RedirectToLoginMixin, DetailView):
show_in_toolbar = True
effect = None
wait_for_result = None
with_reload = False
@property
def name(self):
......@@ -564,6 +634,10 @@ class OperationView(RedirectToLoginMixin, DetailView):
setattr(self, '_opobj', getattr(self.get_object(), self.op))
return self._opobj
@classmethod
def get_operation_class(cls):
return cls.model.get_operation_class(cls.op)
def get_context_data(self, **kwargs):
ctx = super(OperationView, self).get_context_data(**kwargs)
ctx['op'] = self.get_op()
......@@ -579,6 +653,10 @@ class OperationView(RedirectToLoginMixin, DetailView):
logger.debug("OperationView.check_auth(%s)", unicode(self))
self.get_op().check_auth(self.request.user)
@classmethod
def check_perms(cls, user):
cls.get_operation_class().check_perms(user)
def get(self, request, *args, **kwargs):
self.check_auth()
return super(OperationView, self).get(request, *args, **kwargs)
......@@ -664,11 +742,14 @@ class AjaxOperationMixin(object):
resp = super(AjaxOperationMixin, self).post(
request, extra, *args, **kwargs)
if request.is_ajax():
store = messages.get_messages(request)
store.used = True
if not self.with_reload:
store = messages.get_messages(request)
store.used = True
else:
store = []
return HttpResponse(
json.dumps({'success': True,
'with_reload': getattr(self, 'with_reload', False),
'with_reload': self.with_reload,
'messages': [unicode(m) for m in store]}),
content_type="application=json"
)
......@@ -710,9 +791,8 @@ class FormOperationMixin(object):
return HttpResponse(
json.dumps({
'success': True,
'with_reload': getattr(self, 'with_reload', False)}),
content_type="application=json"
)
'with_reload': self.with_reload}),
content_type="application=json")
else:
return resp
else:
......@@ -722,7 +802,7 @@ class FormOperationMixin(object):
class RequestFormOperationMixin(FormOperationMixin):
def get_form_kwargs(self):
val = super(FormOperationMixin, self).get_form_kwargs()
val = super(RequestFormOperationMixin, self).get_form_kwargs()
val.update({'request': self.request})
return val
......@@ -937,6 +1017,24 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
return extra
class VmStateChangeView(FormOperationMixin, VmOperationView):
op = 'emergency_change_state'
icon = 'legal'
effect = 'danger'
show_in_toolbar = True
form_class = VmStateChangeForm
wait_for_result = 0.5
def get_form_kwargs(self):
inst = self.get_op().instance
active_activities = InstanceActivity.objects.filter(
finished__isnull=True, instance=inst)
show_interrupt = active_activities.exists()
val = super(VmStateChangeView, self).get_form_kwargs()
val.update({'show_interrupt': show_interrupt, 'status': inst.status})
return val
vm_ops = OrderedDict([
('deploy', VmOperationView.factory(
op='deploy', icon='play', effect='success')),
......@@ -957,8 +1055,7 @@ vm_ops = OrderedDict([
op='shut_off', icon='ban', effect='warning')),
('recover', VmOperationView.factory(
op='recover', icon='medkit', effect='warning')),
('nostate', VmOperationView.factory(
op='emergency_change_state', icon='legal', effect='danger')),
('nostate', VmStateChangeView),
('destroy', VmOperationView.factory(
extra_bases=[TokenOperationView],
op='destroy', icon='times', effect='danger')),
......@@ -970,6 +1067,10 @@ vm_ops = OrderedDict([
('password_reset', VmOperationView.factory(
op='password_reset', icon='unlock', effect='warning',
show_in_toolbar=False, wait_for_result=0.5, with_reload=True)),
('mount_store', VmOperationView.factory(
op='mount_store', icon='briefcase', effect='info',
show_in_toolbar=False,
)),
])
......@@ -990,6 +1091,112 @@ def get_operations(instance, user):
return ops
class MassOperationView(OperationView):
template_name = 'dashboard/mass-operate.html'
def check_auth(self):
self.get_op().check_perms(self.request.user)
for i in self.get_object():
if not i.has_level(self.request.user, "user"):
raise PermissionDenied(
"You have no user access to instance %d" % i.pk)
@classmethod
def get_urlname(cls):
return 'dashboard.vm.mass-op.%s' % cls.op
@classmethod
def get_url(cls):
return reverse("dashboard.vm.mass-op.%s" % cls.op)
def get_op(self, instance=None):
if instance:
return getattr(instance, self.op)
else:
return Instance._ops[self.op]
def get_context_data(self, **kwargs):
ctx = super(MassOperationView, self).get_context_data(**kwargs)
instances = self.get_object()
ctx['instances'] = self._get_operable_instances(
instances, self.request.user)
ctx['vm_count'] = sum(1 for i in ctx['instances'] if not i.disabled)
return ctx
def _call_operations(self, extra):
request = self.request
user = request.user
instances = self.get_object()
for i in instances:
try:
self.get_op(i).async(user=user, **extra)
except HumanReadableException as e:
e.send_message(request)
except Exception as e:
# pre-existing errors should have been catched when the
# confirmation dialog was constructed
messages.error(request, _(
"Failed to execute %(op)s operation on "
"instance %(instance)s.") % {"op": self.name,
"instance": i})
def get_object(self):
vms = getattr(self.request, self.request.method).getlist("vm")
return Instance.objects.filter(pk__in=vms)
def _get_operable_instances(self, instances, user):
for i in instances:
try:
op = self.get_op(i)
op.check_auth(user)
op.check_precond()
except PermissionDenied as e:
i.disabled = create_readable(
_("You are not permitted to execute %(op)s on instance "
"%(instance)s."), instance=i.pk, op=self.name)
i.disabled_icon = "lock"
except Exception as e:
i.disabled = fetch_human_exception(e)
else:
i.disabled = None
return instances
def post(self, request, extra=None, *args, **kwargs):
self.check_auth()
if extra is None:
extra = {}
self._call_operations(extra)
if request.is_ajax():
store = messages.get_messages(request)
store.used = True
return HttpResponse(
json.dumps({'messages': [unicode(m) for m in store]}),
content_type="application/json"
)
else:
return redirect(reverse("dashboard.views.vm-list"))
@classmethod
def factory(cls, vm_op, extra_bases=(), **kwargs):
return type(str(cls.__name__ + vm_op.op),
tuple(list(extra_bases) + [cls, vm_op]), kwargs)
class MassMigrationView(MassOperationView, VmMigrateView):
template_name = 'dashboard/_vm-mass-migrate.html'
vm_mass_ops = OrderedDict([
('deploy', MassOperationView.factory(vm_ops['deploy'])),
('wake_up', MassOperationView.factory(vm_ops['wake_up'])),
('sleep', MassOperationView.factory(vm_ops['sleep'])),
('reboot', MassOperationView.factory(vm_ops['reboot'])),
('reset', MassOperationView.factory(vm_ops['reset'])),
('shut_off', MassOperationView.factory(vm_ops['shut_off'])),
('migrate', MassMigrationView),
('destroy', MassOperationView.factory(vm_ops['destroy'])),
])
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
template_name = "dashboard/node-detail.html"
model = Node
......@@ -1372,7 +1579,7 @@ class TemplateChoose(LoginRequiredMixin, TemplateView):
self.request.user)
context.update({
'box_title': _('Choose template'),
'ajax_title': False,
'ajax_title': True,
'template': "dashboard/_template-choose.html",
'templates': templates.all(),
})
......@@ -1528,23 +1735,60 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return kwargs
class TemplateList(LoginRequiredMixin, SingleTableView):
class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView):
template_name = "dashboard/template-list.html"
model = InstanceTemplate
table_class = TemplateListTable
table_pagination = False
allowed_filters = {
'name': "name__icontains",
'tags[]': "tags__name__in",
'tags': "tags__name__in", # for search string
'owner': "owner__username",
'ram': "ram_size",
'ram_size': "ram_size",
'cores': "num_cores",
'num_cores': "num_cores",
'access_method': "access_method__iexact",
}
def get_context_data(self, *args, **kwargs):
context = super(TemplateList, self).get_context_data(*args, **kwargs)
context['lease_table'] = LeaseListTable(Lease.objects.all(),
request=self.request)
context['lease_table'] = LeaseListTable(
Lease.get_objects_with_level("user", self.request.user),
request=self.request)
context['search_form'] = self.search_form
return context
def get(self, *args, **kwargs):
self.search_form = TemplateListSearchForm(self.request.GET)
self.search_form.full_clean()
return super(TemplateList, self).get(*args, **kwargs)
def create_acl_queryset(self, model):
queryset = super(TemplateList, self).create_acl_queryset(model)
sql = ("SELECT count(*) FROM vm_instance WHERE "
"vm_instance.template_id = vm_instancetemplate.id and "
"vm_instance.destroyed_at is null and "
"vm_instance.status = 'RUNNING'")
queryset = queryset.extra(select={'running': sql})
return queryset
def get_queryset(self):
logger.debug('TemplateList.get_queryset() called. User: %s',
unicode(self.request.user))
return InstanceTemplate.get_objects_with_level(
'user', self.request.user).all()
qs = self.create_acl_queryset(InstanceTemplate)
self.create_fake_get()
try:
qs = qs.filter(**self.get_queryset_filters()).distinct()
except ValueError:
messages.error(self.request, _("Error during filtering."))
return qs.select_related("lease", "owner", "owner__profile")
class TemplateDelete(LoginRequiredMixin, DeleteView):
......@@ -1591,8 +1835,44 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
'template': "template__pk",
}
def get_context_data(self, *args, **kwargs):
context = super(VmList, self).get_context_data(*args, **kwargs)
context['ops'] = []
for k, v in vm_mass_ops.iteritems():
try:
v.check_perms(user=self.request.user)
except PermissionDenied:
pass
else:
context['ops'].append(v)
context['search_form'] = self.search_form
context['show_acts_in_progress'] = self.object_list.count() < 100
return context
def get(self, *args, **kwargs):
if self.request.is_ajax():
return self._create_ajax_request()
else:
self.search_form = VmListSearchForm(self.request.GET)
self.search_form.full_clean()
return super(VmList, self).get(*args, **kwargs)
def _create_ajax_request(self):
if self.request.GET.get("compact") is not None:
instances = Instance.get_objects_with_level(
"user", self.request.user).filter(destroyed_at=None)
statuses = {}
for i in instances:
statuses[i.pk] = {
'status': i.get_status_display(),
'icon': i.get_status_icon(),
'in_status_change': i.is_in_status_change(),
}
if self.request.user.is_superuser:
statuses[i.pk]['node'] = i.node.name if i.node else "-"
return HttpResponse(json.dumps(statuses),
content_type="application/json")
else:
favs = Instance.objects.filter(
favourite__user=self.request.user).values_list('pk', flat=True)
instances = Instance.get_objects_with_level(
......@@ -1604,19 +1884,23 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
'icon': i.get_status_icon(),
'host': "" if not i.primary_host else i.primary_host.hostname,
'status': i.get_status_display(),
'fav': i.pk in favs} for i in instances]
'fav': i.pk in favs,
} for i in instances]
return HttpResponse(
json.dumps(list(instances)), # instances is ValuesQuerySet
content_type="application/json",
)
else:
return super(VmList, self).get(*args, **kwargs)
def create_acl_queryset(self, model):
queryset = super(VmList, self).create_acl_queryset(model)
if not self.search_form.cleaned_data.get("include_deleted"):
queryset = queryset.filter(destroyed_at=None)
return queryset
def get_queryset(self):
logger.debug('VmList.get_queryset() called. User: %s',
unicode(self.request.user))
queryset = Instance.get_objects_with_level(
'user', self.request.user).filter(destroyed_at=None)
queryset = self.create_acl_queryset(Instance)
self.create_fake_get()
sort = self.request.GET.get("sort")
......@@ -1628,42 +1912,9 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
queryset = queryset.order_by(sort)
return queryset.filter(
**self.get_queryset_filters()).select_related('owner', 'node'
).distinct()
def create_fake_get(self):
"""
Updates the request's GET dict to filter the vm list
For example: "name:xy node:1" updates the GET dict
to resemble this URL ?name=xy&node=1
"name:xy node:1".split(":") becomes ["name", "xy node", "1"]
we pop the the first element and use it as the first dict key
then we iterate over the rest of the list and split by the last
whitespace, the first part of this list will be the previous key's
value, then last part of the list will be the next key.
The final dict looks like this: {'name': xy, 'node':1}
"""
s = self.request.GET.get("s")
if s:
s = s.split(":")
if len(s) < 2: # if there is no ':' in the string, filter by name
got = {'name': s[0]}
else:
latest = s.pop(0)
got = {'%s' % latest: None}
for i in s[:-1]:
new = i.rsplit(" ", 1)
got[latest] = new[0]
latest = new[1] if len(new) > 1 else None
got[latest] = s[-1]
# generate a new GET request, that is kinda fake
fake = self.request.GET.copy()
for k, v in got.iteritems():
fake[k] = v
self.request.GET = fake
**self.get_queryset_filters()).prefetch_related(
"owner", "node", "owner__profile", "interface_set", "lease",
"interface_set__host").distinct()
class NodeList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
......@@ -1872,8 +2123,8 @@ class VmCreate(LoginRequiredMixin, TemplateView):
form_error = form is not None
template = (form.template.pk if form_error
else request.GET.get("template"))
templates = InstanceTemplate.get_objects_with_level('user',
request.user)
templates = InstanceTemplate.get_objects_with_level(
'user', request.user, disregard_superuser=True)
if form is None and template:
form = self.form_class(user=request.user,
template=templates.get(pk=template))
......@@ -1883,7 +2134,7 @@ class VmCreate(LoginRequiredMixin, TemplateView):
context.update({
'template': 'dashboard/_vm-create-2.html',
'box_title': _('Customize VM'),
'ajax_title': False,
'ajax_title': True,
'vm_create_form': form,
'template_o': templates.get(pk=template),
})
......@@ -1891,7 +2142,7 @@ class VmCreate(LoginRequiredMixin, TemplateView):
context.update({
'template': 'dashboard/_vm-create-1.html',
'box_title': _('Create a VM'),
'ajax_title': False,
'ajax_title': True,
'templates': templates.all(),
})
return self.render_to_response(context)
......@@ -1905,8 +2156,10 @@ class VmCreate(LoginRequiredMixin, TemplateView):
if not template.has_level(request.user, 'user'):
raise PermissionDenied()
instances = [Instance.create_from_template(
template=template, owner=user)]
args = {"template": template, "owner": user}
if "name" in request.POST:
args["name"] = request.POST.get("name")
instances = [Instance.create_from_template(**args)]
return self.__deploy(request, instances)
def __create_customized(self, request, *args, **kwargs):
......@@ -1932,6 +2185,7 @@ class VmCreate(LoginRequiredMixin, TemplateView):
'num_cores': post['cpu_count'],
'ram_size': post['ram_size'],
'priority': post['cpu_priority'],
'max_ram_size': post['ram_size'],
}
networks = [InterfaceTemplate(vlan=l, managed=l.managed)
for l in post['networks']]
......@@ -1959,7 +2213,7 @@ class VmCreate(LoginRequiredMixin, TemplateView):
"Successfully created %(count)d VM.", # this should not happen
"Successfully created %(count)d VMs.", len(instances)) % {
'count': len(instances)})
path = reverse("dashboard.index")
path = "%s?stype=owned" % reverse("dashboard.views.vm-list")
else:
messages.success(request, _("VM successfully created."))
path = instances[0].get_absolute_url()
......@@ -2078,9 +2332,9 @@ class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
context = self.get_context_data(**kwargs)
context.update({
'template': 'dashboard/group-create.html',
'box_title': 'Create a Group',
'box_title': _('Create a Group'),
'form': form,
'ajax_title': True,
})
return self.render_to_response(context)
......@@ -2361,48 +2615,6 @@ class PortDelete(LoginRequiredMixin, DeleteView):
kwargs={'pk': self.kwargs.get("pk")})
class VmMassDelete(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
vms = request.GET.getlist('v[]')
objects = Instance.objects.filter(pk__in=vms)
return render(request, "dashboard/confirm/mass-delete.html",
{'objects': objects})
def post(self, request, *args, **kwargs):
vms = request.POST.getlist('vms')
names = []
if vms is not None:
for i in Instance.objects.filter(pk__in=vms):
if not i.has_level(request.user, 'owner'):
logger.info('Tried to delete instance #%d without owner '
'permission by %s.', i.pk,
unicode(request.user))
# no need for rollback or proper error message, this can't
# normally happen:
raise PermissionDenied()
try:
i.destroy.async(user=request.user)
names.append(i.name)
except Exception as e:
logger.error(e)
success_message = ungettext_lazy(
"Mass delete complete, the following VM was deleted: %s.",
"Mass delete complete, the following VMs were deleted: %s.",
len(names)) % u', '.join(names)
# we can get this only via AJAX ...
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json"
)
else:
messages.success(request, success_message)
next = request.GET.get('next')
return redirect(next if next else reverse_lazy('dashboard.index'))
class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin,
SuccessMessageMixin, CreateView):
model = Lease
......@@ -2491,7 +2703,8 @@ def vm_activity(request, pk):
response = {}
show_all = request.GET.get("show_all", "false") == "true"
activities = instance.get_merged_activities(request.user)
activities = _format_activities(
instance.get_merged_activities(request.user))
show_show_all = len(activities) > 10
if not show_all:
activities = activities[:10]
......@@ -2734,7 +2947,7 @@ class NodeGraphView(SuperuserRequiredMixin, GraphViewBase):
model = Node
def get_prefix(self, instance):
return 'circle.%s' % instance.name
return 'circle.%s' % instance.host.hostname
def get_title(self, instance, metric):
return '%s - %s' % (instance.name, metric)
......@@ -2790,11 +3003,16 @@ class MyPreferencesView(UpdateView):
user=self.request.user),
'change_language': MyProfileForm(instance=self.get_object()),
}
table = UserKeyListTable(
key_table = UserKeyListTable(
UserKey.objects.filter(user=self.request.user),
request=self.request)
table.page = None
context['userkey_table'] = table
key_table.page = None
context['userkey_table'] = key_table
cmd_table = ConnectCommandListTable(
self.request.user.command_set.all(),
request=self.request)
cmd_table.page = None
context['connectcommand_table'] = cmd_table
return context
def get_object(self, queryset=None):
......@@ -2949,6 +3167,20 @@ def get_disk_download_status(request, pk):
)
def _get_activity_icon(act):
op = act.get_operation()
if op and op.id in vm_ops:
return vm_ops[op.id].icon
else:
return "cog"
def _format_activities(acts):
for i in acts:
i.icon = _get_activity_icon(i)
return acts
class InstanceActivityDetail(CheckedDetailView):
model = InstanceActivity
context_object_name = 'instanceactivity' # much simpler to mock object
......@@ -2959,8 +3191,9 @@ class InstanceActivityDetail(CheckedDetailView):
def get_context_data(self, **kwargs):
ctx = super(InstanceActivityDetail, self).get_context_data(**kwargs)
ctx['activities'] = self.object.instance.get_activities(
self.request.user)
ctx['activities'] = _format_activities(
self.object.instance.get_activities(self.request.user))
ctx['icon'] = _get_activity_icon(self.object)
return ctx
......@@ -3184,6 +3417,82 @@ class UserKeyCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView):
return kwargs
class ConnectCommandDetail(LoginRequiredMixin, SuccessMessageMixin,
UpdateView):
model = ConnectCommand
template_name = "dashboard/connect-command-edit.html"
form_class = ConnectCommandForm
success_message = _("Successfully modified command template.")
def get(self, request, *args, **kwargs):
object = self.get_object()
if object.user != request.user:
raise PermissionDenied()
return super(ConnectCommandDetail, self).get(request, *args, **kwargs)
def get_success_url(self):
return reverse_lazy("dashboard.views.connect-command-detail",
kwargs=self.kwargs)
def post(self, request, *args, **kwargs):
object = self.get_object()
if object.user != request.user:
raise PermissionDenied()
return super(ConnectCommandDetail, self).post(request, args, kwargs)
def get_form_kwargs(self):
kwargs = super(ConnectCommandDetail, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
class ConnectCommandDelete(LoginRequiredMixin, DeleteView):
model = ConnectCommand
def get_success_url(self):
return reverse("dashboard.views.profile-preferences")
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def delete(self, request, *args, **kwargs):
object = self.get_object()
if object.user != request.user:
raise PermissionDenied()
object.delete()
success_url = self.get_success_url()
success_message = _("Command template successfully deleted.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin,
CreateView):
model = ConnectCommand
form_class = ConnectCommandForm
template_name = "dashboard/connect-command-create.html"
success_message = _("Successfully created a new command template.")
def get_success_url(self):
return reverse_lazy("dashboard.views.profile-preferences")
def get_form_kwargs(self):
kwargs = super(ConnectCommandCreate, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
class HelpView(TemplateView):
def get_context_data(self, *args, **kwargs):
......
......@@ -17,9 +17,17 @@
from django.conf.urls import patterns, url
from ..views import vm_ops
from ..views import vm_ops, vm_mass_ops
urlpatterns = patterns('',
*(url(r'^%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems()))
urlpatterns = patterns(
'',
*(url(r'^(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems())
)
urlpatterns += patterns(
'',
*(url(r'^mass_op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_mass_ops.iteritems())
)
......@@ -101,7 +101,7 @@ def pull(dir="~/circle/circle"):
@roles('portal')
def update_portal(test=False):
"Update and restart portal+manager"
with _stopped("portal", "mancelery"):
with _stopped("portal", "manager"):
pull()
pip("circle", "~/circle/requirements.txt")
migrate()
......@@ -113,7 +113,7 @@ def update_portal(test=False):
@roles('portal')
def stop_portal(test=False):
"Stop portal and manager"
_stop_services("portal", "mancelery")
_stop_services("portal", "manager")
@roles('node')
......
......@@ -62,6 +62,15 @@ class BuildFirewall:
extra='-j DNAT --to-destination %s:%s' % (rule.host.ipv4,
rule.dport)))
# SNAT rules for machines with public IPv4
for host in Host.objects.exclude(external_ipv4=None).select_related(
'vlan').prefetch_related('vlan__snat_to'):
for vl_out in host.vlan.snat_to.all():
self.add_rules(POSTROUTING=IptRule(
priority=1500, src=(host.ipv4, None),
extra='-o %s -j SNAT --to-source %s' % (
vl_out.name, host.external_ipv4)))
# default outbound NAT rules for VLANs
for vl_in in Vlan.objects.exclude(
snat_ip=None).prefetch_related('snat_to'):
......@@ -183,9 +192,12 @@ def generate_ptr_records():
for host in Host.objects.order_by('vlan').all():
template = host.vlan.reverse_domain
i = host.get_external_ipv4().words
reverse = (host.reverse if host.reverse not in [None, '']
else host.get_fqdn())
if not host.shared_ip and host.external_ipv4: # DMZ
i = host.external_ipv4.words
reverse = host.get_hostname('ipv4', public=True)
else:
i = host.ipv4.words
reverse = host.get_hostname('ipv4', public=False)
# ipv4
if host.ipv4:
......@@ -194,7 +206,7 @@ def generate_ptr_records():
# ipv6
if host.ipv6:
DNS.append("^%s:%s:%s" % (host.ipv6.reverse_dns,
DNS.append("^%s:%s:%s" % (host.ipv6.reverse_dns.rstrip('.'),
reverse, settings['dns_ttl']))
return DNS
......@@ -211,14 +223,14 @@ def generate_records():
'CNAME': 'C%(fqdn)s:%(address)s:%(ttl)s',
'MX': '@%(fqdn)s::%(address)s:%(dist)s:%(ttl)s',
'PTR': '^%(fqdn)s:%(address)s:%(ttl)s',
'TXT': '%(fqdn)s:%(octal)s:%(ttl)s'}
'TXT': "'%(fqdn)s:%(octal)s:%(ttl)s"}
retval = []
for r in Record.objects.all():
params = {'fqdn': r.fqdn, 'address': r.address, 'ttl': r.ttl}
if r.type == 'MX':
params['address'], params['dist'] = r.address.split(':', 2)
params['dist'], params['address'] = r.address.split(':', 2)
if r.type == 'AAAA':
try:
params['octal'] = ipv6_to_octal(r.address)
......
......@@ -22,7 +22,7 @@ from collections import OrderedDict
logger = logging.getLogger()
ipv4_re = re.compile(
r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}')
r'(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}')
class InvalidRuleExcepion(Exception):
......
......@@ -203,12 +203,6 @@ class Rule(models.Model):
elif self.firewall_id:
return 'INPUT' if self.direction == 'in' else 'OUTPUT'
def get_dport_sport(self):
if self.direction == 'in':
return self.dport, self.sport
else:
return self.sport, self.dport
def get_ipt_rules(self, host=None):
# action
action = 'LOG_ACC' if self.action == 'accept' else 'LOG_DROP'
......@@ -221,7 +215,7 @@ class Rule(models.Model):
dst = None
if host:
ip = (host.ipv4, host.ipv6_with_prefixlen)
ip = (host.ipv4, host.ipv6_with_host_prefixlen)
if self.direction == 'in':
dst = ip
else:
......@@ -235,9 +229,6 @@ class Rule(models.Model):
if vlan and not vlan.managed:
return retval
# src and dst ports
dport, sport = self.get_dport_sport()
# process foreign vlans
for foreign_vlan in self.foreign_network.vlans.all():
if not foreign_vlan.managed:
......@@ -246,7 +237,7 @@ class Rule(models.Model):
r = IptRule(priority=self.weight, action=action,
proto=self.proto, extra=self.extra,
comment='Rule #%s' % self.pk,
src=src, dst=dst, dport=dport, sport=sport)
src=src, dst=dst, dport=self.dport, sport=self.sport)
chain_name = self.get_chain_name(local=vlan, remote=foreign_vlan)
retval[chain_name] = r
......@@ -539,14 +530,30 @@ class Host(models.Model):
def incoming_rules(self):
return self.rules.filter(direction='in')
@property
def ipv6_with_prefixlen(self):
@staticmethod
def create_ipnetwork(ip, prefixlen):
try:
net = IPNetwork(self.ipv6)
net.prefixlen = self.vlan.host_ipv6_prefixlen
return net
net = IPNetwork(ip)
net.prefixlen = prefixlen
except TypeError:
return None
else:
return net
@property
def ipv4_with_vlan_prefixlen(self):
return Host.create_ipnetwork(
self.ipv4, self.vlan.network4.prefixlen)
@property
def ipv6_with_vlan_prefixlen(self):
return Host.create_ipnetwork(
self.ipv6, self.vlan.network6.prefixlen)
@property
def ipv6_with_host_prefixlen(self):
return Host.create_ipnetwork(
self.ipv6, self.vlan.host_ipv6_prefixlen)
def get_external_ipv4(self):
return self.external_ipv4 if self.external_ipv4 else self.ipv4
......@@ -575,10 +582,14 @@ class Host(models.Model):
# IPv4
if self.ipv4 is not None:
if not self.shared_ip and self.external_ipv4: # DMZ
ipv4 = self.external_ipv4
else:
ipv4 = self.ipv4
# update existing records
affected_records = Record.objects.filter(
host=self, name=self.hostname,
type='A').update(address=self.ipv4)
type='A').update(address=ipv4)
# create new record
if affected_records == 0:
Record(host=self,
......@@ -605,6 +616,19 @@ class Host(models.Model):
description='created by host.save()',
type='AAAA').save()
def get_network_config(self):
interface = {'addresses': []}
if self.ipv4 and self.vlan.network4:
interface['addresses'].append(str(self.ipv4_with_vlan_prefixlen))
interface['gw4'] = str(self.vlan.network4.ip)
if self.ipv6 and self.vlan.network6:
interface['addresses'].append(str(self.ipv6_with_vlan_prefixlen))
interface['gw6'] = str(self.vlan.network6.ip)
return interface
def enable_net(self):
for i in settings.get('default_host_groups', []):
self.groups.add(Group.objects.get(name=i))
......@@ -714,6 +738,8 @@ class Host(models.Model):
:type proto: str.
"""
assert proto in ('ipv6', 'ipv4', )
if self.reverse:
return self.reverse
try:
if proto == 'ipv6':
res = self.record_set.filter(type='AAAA',
......@@ -736,7 +762,7 @@ class Host(models.Model):
Return a list of ports with forwarding rules set.
"""
retval = []
for rule in self.rules.all():
for rule in self.rules.filter(dport__isnull=False, direction='in'):
forward = {
'proto': rule.proto,
'private': rule.dport,
......
......@@ -35,7 +35,7 @@ COMMIT
{% if proto == "ipv4" %}
-A FORWARD -p icmp --icmp-type echo-request -g LOG_ACC
{% else %}
-A FORWARD -p icmpv6 --icmpv6-type echo-request -g LOG_ACC
-A FORWARD -p icmpv6 -g LOG_ACC
{% endif %}
# initialize INPUT chain
......@@ -45,6 +45,11 @@ COMMIT
-A INPUT -m state --state INVALID -g LOG_DROP
-A INPUT -i lo -j ACCEPT
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
{% if proto == "ipv4" %}
-A INPUT -p icmp --icmp-type echo-request -g LOG_ACC
{% else %}
-A INPUT -p icmpv6 -g LOG_ACC
{% endif %}
# initialize OUTPUT chain
-A OUTPUT -m state --state INVALID -g LOG_DROP
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-08-01 19:36+0200\n"
"PO-Revision-Date: 2014-08-01 21:03+0200\n"
"POT-Creation-Date: 2014-09-03 12:57+0200\n"
"PO-Revision-Date: 2014-09-03 12:51+0200\n"
"Last-Translator: Mate Ory <ory.mate@ik.bme.hu>\n"
"Language-Team: Hungarian <cloud@ik.bme.hu>\n"
"Language: en_US\n"
......@@ -18,59 +18,250 @@ msgstr ""
"X-Generator: Lokalize 1.5\n"
#: dashboard/static/dashboard/dashboard.js:68
#: static_collected/all.047675ebf594.js:3443
#: static_collected/all.0aecd87e873a.js:3443
#: static_collected/all.0db607331718.js:3443
#: static_collected/all.146abee8fe99.js:3443
#: static_collected/all.24315fee0c8e.js:3443
#: static_collected/all.483236be7507.js:19
#: static_collected/all.73e18dd7e023.js:3443
#: static_collected/all.a650c949d06f.js:3443
#: static_collected/all.b66e942cdb56.js:3443
#: static_collected/all.f62aa30bd0a4.js:3443 static_collected/all.js:3443
#: static_collected/dashboard/dashboard.32a5b5729414.js:68
#: static_collected/dashboard/dashboard.32fd2a07ee32.js:68
#: static_collected/dashboard/dashboard.b96d0ea9ce41.js:68
#: static_collected/dashboard/dashboard.be8725cd91bf.js:68
#: static_collected/dashboard/dashboard.e740d80401b2.js:68
#: static_collected/dashboard/dashboard.fe0a2f126346.js:68
#: static_collected/dashboard/dashboard.js:68
msgid "Select an option to proceed!"
msgstr "Válasszon a folytatáshoz."
#: dashboard/static/dashboard/dashboard.js:258
#: dashboard/static/dashboard/dashboard.js:305
#: dashboard/static/dashboard/dashboard.js:315
#: static_collected/dashboard/dashboard.js:257
#: static_collected/dashboard/dashboard.js:304
#: static_collected/dashboard/dashboard.js:314
#: dashboard/static/dashboard/dashboard.js:306
#: dashboard/static/dashboard/dashboard.js:316
#: static_collected/all.047675ebf594.js:3633
#: static_collected/all.047675ebf594.js:3681
#: static_collected/all.047675ebf594.js:3691
#: static_collected/all.0aecd87e873a.js:3633
#: static_collected/all.0aecd87e873a.js:3681
#: static_collected/all.0aecd87e873a.js:3691
#: static_collected/all.0db607331718.js:3633
#: static_collected/all.0db607331718.js:3681
#: static_collected/all.0db607331718.js:3691
#: static_collected/all.146abee8fe99.js:3633
#: static_collected/all.146abee8fe99.js:3681
#: static_collected/all.146abee8fe99.js:3691
#: static_collected/all.24315fee0c8e.js:3633
#: static_collected/all.24315fee0c8e.js:3681
#: static_collected/all.24315fee0c8e.js:3691
#: static_collected/all.483236be7507.js:19
#: static_collected/all.73e18dd7e023.js:3633
#: static_collected/all.73e18dd7e023.js:3681
#: static_collected/all.73e18dd7e023.js:3691
#: static_collected/all.a650c949d06f.js:3633
#: static_collected/all.a650c949d06f.js:3681
#: static_collected/all.a650c949d06f.js:3691
#: static_collected/all.b66e942cdb56.js:3633
#: static_collected/all.b66e942cdb56.js:3681
#: static_collected/all.b66e942cdb56.js:3691
#: static_collected/all.f62aa30bd0a4.js:3633
#: static_collected/all.f62aa30bd0a4.js:3681
#: static_collected/all.f62aa30bd0a4.js:3691 static_collected/all.js:3633
#: static_collected/all.js.c:3681 static_collected/all.js:3691
#: static_collected/dashboard/dashboard.32a5b5729414.js:258
#: static_collected/dashboard/dashboard.32a5b5729414.js:306
#: static_collected/dashboard/dashboard.32a5b5729414.js:316
#: static_collected/dashboard/dashboard.32fd2a07ee32.js:258
#: static_collected/dashboard/dashboard.32fd2a07ee32.js:306
#: static_collected/dashboard/dashboard.32fd2a07ee32.js:316
#: static_collected/dashboard/dashboard.b96d0ea9ce41.js:258
#: static_collected/dashboard/dashboard.b96d0ea9ce41.js:306
#: static_collected/dashboard/dashboard.b96d0ea9ce41.js:316
#: static_collected/dashboard/dashboard.be8725cd91bf.js:258
#: static_collected/dashboard/dashboard.be8725cd91bf.js:306
#: static_collected/dashboard/dashboard.be8725cd91bf.js:316
#: static_collected/dashboard/dashboard.e740d80401b2.js:258
#: static_collected/dashboard/dashboard.e740d80401b2.js:306
#: static_collected/dashboard/dashboard.e740d80401b2.js:316
#: static_collected/dashboard/dashboard.fe0a2f126346.js:258
#: static_collected/dashboard/dashboard.fe0a2f126346.js:306
#: static_collected/dashboard/dashboard.fe0a2f126346.js:316
#: static_collected/dashboard/dashboard.js:258
#: static_collected/dashboard/dashboard.js:306
#: static_collected/dashboard/dashboard.js:316
msgid "No result"
msgstr "Nincs eredmény"
#: dashboard/static/dashboard/profile.js:18
#: static_collected/all.047675ebf594.js:4459
#: static_collected/all.0aecd87e873a.js:4458
#: static_collected/all.0db607331718.js:4435
#: static_collected/all.146abee8fe99.js:4459
#: static_collected/all.24315fee0c8e.js:4458
#: static_collected/all.483236be7507.js:21
#: static_collected/all.73e18dd7e023.js:4435
#: static_collected/all.a650c949d06f.js:4458
#: static_collected/all.b66e942cdb56.js:4458
#: static_collected/all.f62aa30bd0a4.js:4459 static_collected/all.js:4435
#: static_collected/dashboard/profile.333ac16a5ce1.js:18
#: static_collected/dashboard/profile.b7bd994913eb.js:18
#: static_collected/dashboard/profile.js:18
msgid "You have no permission to change this profile."
msgstr "Nincs jogosultsága a profil módosításához."
#: dashboard/static/dashboard/profile.js:20
#: static_collected/all.047675ebf594.js:4461
#: static_collected/all.0aecd87e873a.js:4460
#: static_collected/all.0db607331718.js:4437
#: static_collected/all.146abee8fe99.js:4461
#: static_collected/all.24315fee0c8e.js:4460
#: static_collected/all.483236be7507.js:21
#: static_collected/all.73e18dd7e023.js:4437
#: static_collected/all.a650c949d06f.js:4460
#: static_collected/all.b66e942cdb56.js:4460
#: static_collected/all.f62aa30bd0a4.js:4461 static_collected/all.js:4437
#: static_collected/dashboard/profile.333ac16a5ce1.js:20
#: static_collected/dashboard/profile.b7bd994913eb.js:20
#: static_collected/dashboard/profile.js:20
msgid "Unknown error."
msgstr "Ismeretlen hiba."
#: dashboard/static/dashboard/vm-create.js:108
#: dashboard/static/dashboard/vm-create.js:171
#: dashboard/static/dashboard/vm-create.js:111
#: dashboard/static/dashboard/vm-create.js:174
#: static_collected/all.047675ebf594.js:4813
#: static_collected/all.047675ebf594.js:4876
#: static_collected/all.0aecd87e873a.js:4883
#: static_collected/all.0aecd87e873a.js:4946
#: static_collected/all.0db607331718.js:4789
#: static_collected/all.0db607331718.js:4852
#: static_collected/all.146abee8fe99.js:4813
#: static_collected/all.146abee8fe99.js:4876
#: static_collected/all.24315fee0c8e.js:4812
#: static_collected/all.24315fee0c8e.js:4875
#: static_collected/all.483236be7507.js:2
#: static_collected/all.73e18dd7e023.js:4789
#: static_collected/all.73e18dd7e023.js:4852
#: static_collected/all.a650c949d06f.js:4812
#: static_collected/all.a650c949d06f.js:4875
#: static_collected/all.b66e942cdb56.js:4812
#: static_collected/all.b66e942cdb56.js:4875
#: static_collected/all.f62aa30bd0a4.js:4813
#: static_collected/all.f62aa30bd0a4.js:4876 static_collected/all.js:4789
#: static_collected/all.js.c:4852
#: static_collected/dashboard/vm-create.1a1f6dae3556.js:111
#: static_collected/dashboard/vm-create.1a1f6dae3556.js:174
#: static_collected/dashboard/vm-create.7562c27e19a2.js:111
#: static_collected/dashboard/vm-create.7562c27e19a2.js:174
#: static_collected/dashboard/vm-create.js:111
#: static_collected/dashboard/vm-create.js:174
msgid "No more networks."
msgstr "Nincs több hálózat."
#: dashboard/static/dashboard/vm-create.js:140
#: dashboard/static/dashboard/vm-create.js:143
#: static_collected/all.047675ebf594.js:4845
#: static_collected/all.0aecd87e873a.js:4915
#: static_collected/all.0db607331718.js:4821
#: static_collected/all.146abee8fe99.js:4845
#: static_collected/all.24315fee0c8e.js:4844
#: static_collected/all.483236be7507.js:2
#: static_collected/all.73e18dd7e023.js:4821
#: static_collected/all.a650c949d06f.js:4844
#: static_collected/all.b66e942cdb56.js:4844
#: static_collected/all.f62aa30bd0a4.js:4845 static_collected/all.js:4821
#: static_collected/dashboard/vm-create.1a1f6dae3556.js:143
#: static_collected/dashboard/vm-create.7562c27e19a2.js:143
#: static_collected/dashboard/vm-create.js:143
msgid "Not added to any network"
msgstr "Nincs hálózathoz adva"
#: dashboard/static/dashboard/vm-details.js:115
#: static_collected/dashboard/vm-details.js:115
msgid "Hide password"
msgstr "Jelszó rejtése"
#: dashboard/static/dashboard/vm-details.js:119
#: static_collected/dashboard/vm-details.js:119
msgid "Show password"
msgstr "Jelszó megjelenítése"
#: dashboard/static/dashboard/vm-tour.js:20
#: static_collected/vm-detail.09737c69abc3.js:5853
#: static_collected/vm-detail.15d710d8ccf0.js:6389
#: static_collected/vm-detail.234990ca6ec1.js:6962
#: static_collected/vm-detail.47b1d21da259.js:5853
#: static_collected/vm-detail.9e1734ade019.js:5854
#: static_collected/vm-detail.c47949114749.js:6962
#: static_collected/vm-detail.e3f398067c8a.js:6891
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6389
#: static_collected/dashboard/vm-tour.1562cc89a659.js:20
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:20
#: static_collected/dashboard/vm-tour.js:20
msgid "Prev"
msgstr "Vissza"
#: dashboard/static/dashboard/vm-tour.js:22
#: static_collected/vm-detail.09737c69abc3.js:5855
#: static_collected/vm-detail.15d710d8ccf0.js:6391
#: static_collected/vm-detail.234990ca6ec1.js:6964
#: static_collected/vm-detail.47b1d21da259.js:5855
#: static_collected/vm-detail.9e1734ade019.js:5856
#: static_collected/vm-detail.c47949114749.js:6964
#: static_collected/vm-detail.e3f398067c8a.js:6893
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6391
#: static_collected/dashboard/vm-tour.1562cc89a659.js:22
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:22
#: static_collected/dashboard/vm-tour.js:22
msgid "Next"
msgstr "Tovább"
#: dashboard/static/dashboard/vm-tour.js:26
#: static_collected/vm-detail.09737c69abc3.js:5859
#: static_collected/vm-detail.15d710d8ccf0.js:6395
#: static_collected/vm-detail.234990ca6ec1.js:6968
#: static_collected/vm-detail.47b1d21da259.js:5859
#: static_collected/vm-detail.9e1734ade019.js:5860
#: static_collected/vm-detail.c47949114749.js:6968
#: static_collected/vm-detail.e3f398067c8a.js:6897
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6395
#: static_collected/dashboard/vm-tour.1562cc89a659.js:26
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:26
#: static_collected/dashboard/vm-tour.js:26
msgid "End tour"
msgstr "Befejezés"
#: dashboard/static/dashboard/vm-tour.js:33
#: static_collected/vm-detail.09737c69abc3.js:5866
#: static_collected/vm-detail.15d710d8ccf0.js:6402
#: static_collected/vm-detail.234990ca6ec1.js:6975
#: static_collected/vm-detail.47b1d21da259.js:5866
#: static_collected/vm-detail.9e1734ade019.js:5867
#: static_collected/vm-detail.c47949114749.js:6975
#: static_collected/vm-detail.e3f398067c8a.js:6904
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6402
#: static_collected/dashboard/vm-tour.1562cc89a659.js:33
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:33
#: static_collected/dashboard/vm-tour.js:33
msgid "Template Tutorial Tour"
msgstr "Sablon-kalauz"
#: dashboard/static/dashboard/vm-tour.js:34
#: static_collected/vm-detail.09737c69abc3.js:5867
#: static_collected/vm-detail.15d710d8ccf0.js:6403
#: static_collected/vm-detail.234990ca6ec1.js:6976
#: static_collected/vm-detail.47b1d21da259.js:5867
#: static_collected/vm-detail.9e1734ade019.js:5868
#: static_collected/vm-detail.c47949114749.js:6976
#: static_collected/vm-detail.e3f398067c8a.js:6905
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6403
#: static_collected/dashboard/vm-tour.1562cc89a659.js:34
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:34
#: static_collected/dashboard/vm-tour.js:34
msgid ""
"Welcome to the template tutorial. In this quick tour, we gonna show you how "
......@@ -80,6 +271,17 @@ msgstr ""
"lépéseket."
#: dashboard/static/dashboard/vm-tour.js:35
#: static_collected/vm-detail.09737c69abc3.js:5868
#: static_collected/vm-detail.15d710d8ccf0.js:6404
#: static_collected/vm-detail.234990ca6ec1.js:6977
#: static_collected/vm-detail.47b1d21da259.js:5868
#: static_collected/vm-detail.9e1734ade019.js:5869
#: static_collected/vm-detail.c47949114749.js:6977
#: static_collected/vm-detail.e3f398067c8a.js:6906
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6404
#: static_collected/dashboard/vm-tour.1562cc89a659.js:35
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:35
#: static_collected/dashboard/vm-tour.js:35
msgid ""
"For the next tour step press the \"Next\" button or the right arrow (or "
......@@ -89,6 +291,16 @@ msgstr ""
"nyílbillentyűket."
#: dashboard/static/dashboard/vm-tour.js:36
#: static_collected/vm-detail.09737c69abc3.js:5869
#: static_collected/vm-detail.15d710d8ccf0.js:6405
#: static_collected/vm-detail.234990ca6ec1.js:6978
#: static_collected/vm-detail.47b1d21da259.js:5869
#: static_collected/vm-detail.9e1734ade019.js:5870
#: static_collected/vm-detail.c47949114749.js:6978
#: static_collected/vm-detail.e3f398067c8a.js:6907
#: static_collected/vm-detail.js:6405
#: static_collected/dashboard/vm-tour.1562cc89a659.js:36
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:36
#: static_collected/dashboard/vm-tour.js:36
msgid ""
"During the tour please don't try the functions because it may lead to "
......@@ -96,11 +308,33 @@ msgid ""
msgstr "A túra során még ne próbálja ki a bemutatott funkciókat."
#: dashboard/static/dashboard/vm-tour.js:45
#: static_collected/vm-detail.09737c69abc3.js:5878
#: static_collected/vm-detail.15d710d8ccf0.js:6414
#: static_collected/vm-detail.234990ca6ec1.js:6987
#: static_collected/vm-detail.47b1d21da259.js:5878
#: static_collected/vm-detail.9e1734ade019.js:5879
#: static_collected/vm-detail.c47949114749.js:6987
#: static_collected/vm-detail.e3f398067c8a.js:6916
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6414
#: static_collected/dashboard/vm-tour.1562cc89a659.js:45
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:45
#: static_collected/dashboard/vm-tour.js:45
msgid "Home tab"
msgstr "Kezdőoldal"
#: dashboard/static/dashboard/vm-tour.js:46
#: static_collected/vm-detail.09737c69abc3.js:5879
#: static_collected/vm-detail.15d710d8ccf0.js:6415
#: static_collected/vm-detail.234990ca6ec1.js:6988
#: static_collected/vm-detail.47b1d21da259.js:5879
#: static_collected/vm-detail.9e1734ade019.js:5880
#: static_collected/vm-detail.c47949114749.js:6988
#: static_collected/vm-detail.e3f398067c8a.js:6917
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6415
#: static_collected/dashboard/vm-tour.1562cc89a659.js:46
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:46
#: static_collected/dashboard/vm-tour.js:46
msgid ""
"In this tab you can tag your virtual machine and modify the name and "
......@@ -110,11 +344,33 @@ msgstr ""
"leírását."
#: dashboard/static/dashboard/vm-tour.js:55
#: static_collected/vm-detail.09737c69abc3.js:5888
#: static_collected/vm-detail.15d710d8ccf0.js:6424
#: static_collected/vm-detail.234990ca6ec1.js:6997
#: static_collected/vm-detail.47b1d21da259.js:5888
#: static_collected/vm-detail.9e1734ade019.js:5889
#: static_collected/vm-detail.c47949114749.js:6997
#: static_collected/vm-detail.e3f398067c8a.js:6926
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6424
#: static_collected/dashboard/vm-tour.1562cc89a659.js:55
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:55
#: static_collected/dashboard/vm-tour.js:55
msgid "Resources tab"
msgstr "Erőforrások lap"
#: dashboard/static/dashboard/vm-tour.js:58
#: static_collected/vm-detail.09737c69abc3.js:5891
#: static_collected/vm-detail.15d710d8ccf0.js:6427
#: static_collected/vm-detail.234990ca6ec1.js:7000
#: static_collected/vm-detail.47b1d21da259.js:5891
#: static_collected/vm-detail.9e1734ade019.js:5892
#: static_collected/vm-detail.c47949114749.js:7000
#: static_collected/vm-detail.e3f398067c8a.js:6929
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6427
#: static_collected/dashboard/vm-tour.1562cc89a659.js:58
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:58
#: static_collected/dashboard/vm-tour.js:58
msgid ""
"On the resources tab you can edit the CPU/RAM options and add/remove disks!"
......@@ -123,46 +379,145 @@ msgstr ""
"hozzáadhat és törölhet lemezeket."
#: dashboard/static/dashboard/vm-tour.js:68
#: static_collected/vm-detail.09737c69abc3.js:5901
#: static_collected/vm-detail.15d710d8ccf0.js:6437
#: static_collected/vm-detail.234990ca6ec1.js:7010
#: static_collected/vm-detail.47b1d21da259.js:5901
#: static_collected/vm-detail.9e1734ade019.js:5902
#: static_collected/vm-detail.c47949114749.js:7010
#: static_collected/vm-detail.e3f398067c8a.js:6939
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6437
#: static_collected/dashboard/vm-tour.1562cc89a659.js:68
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:68
#: static_collected/dashboard/vm-tour.js:68
msgid "Resources"
msgstr "Erőforrások"
#: dashboard/static/dashboard/vm-tour.js:69
#: static_collected/vm-detail.09737c69abc3.js:5902
#: static_collected/vm-detail.15d710d8ccf0.js:6438
#: static_collected/vm-detail.234990ca6ec1.js:7011
#: static_collected/vm-detail.47b1d21da259.js:5902
#: static_collected/vm-detail.9e1734ade019.js:5903
#: static_collected/vm-detail.c47949114749.js:7011
#: static_collected/vm-detail.e3f398067c8a.js:6940
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6438
#: static_collected/dashboard/vm-tour.1562cc89a659.js:69
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:69
#: static_collected/dashboard/vm-tour.js:69
msgid "CPU priority"
msgstr "CPU prioritás"
#: dashboard/static/dashboard/vm-tour.js:69
#: static_collected/vm-detail.09737c69abc3.js:5902
#: static_collected/vm-detail.15d710d8ccf0.js:6438
#: static_collected/vm-detail.234990ca6ec1.js:7011
#: static_collected/vm-detail.47b1d21da259.js:5902
#: static_collected/vm-detail.9e1734ade019.js:5903
#: static_collected/vm-detail.c47949114749.js:7011
#: static_collected/vm-detail.e3f398067c8a.js:6940
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6438
#: static_collected/dashboard/vm-tour.1562cc89a659.js:69
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:69
#: static_collected/dashboard/vm-tour.js:69
msgid "higher is better"
msgstr "a nagyobb érték a jobb"
#: dashboard/static/dashboard/vm-tour.js:70
#: static_collected/vm-detail.09737c69abc3.js:5903
#: static_collected/vm-detail.15d710d8ccf0.js:6439
#: static_collected/vm-detail.234990ca6ec1.js:7012
#: static_collected/vm-detail.47b1d21da259.js:5903
#: static_collected/vm-detail.9e1734ade019.js:5904
#: static_collected/vm-detail.c47949114749.js:7012
#: static_collected/vm-detail.e3f398067c8a.js:6941
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6439
#: static_collected/dashboard/vm-tour.1562cc89a659.js:70
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:70
#: static_collected/dashboard/vm-tour.js:70
msgid "CPU count"
msgstr "CPU-k száma"
#: dashboard/static/dashboard/vm-tour.js:70
#: static_collected/vm-detail.09737c69abc3.js:5903
#: static_collected/vm-detail.15d710d8ccf0.js:6439
#: static_collected/vm-detail.234990ca6ec1.js:7012
#: static_collected/vm-detail.47b1d21da259.js:5903
#: static_collected/vm-detail.9e1734ade019.js:5904
#: static_collected/vm-detail.c47949114749.js:7012
#: static_collected/vm-detail.e3f398067c8a.js:6941
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6439
#: static_collected/dashboard/vm-tour.1562cc89a659.js:70
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:70
#: static_collected/dashboard/vm-tour.js:70
msgid "number of CPU cores."
msgstr "A CPU-magok száma."
#: dashboard/static/dashboard/vm-tour.js:71
#: static_collected/vm-detail.09737c69abc3.js:5904
#: static_collected/vm-detail.15d710d8ccf0.js:6440
#: static_collected/vm-detail.234990ca6ec1.js:7013
#: static_collected/vm-detail.47b1d21da259.js:5904
#: static_collected/vm-detail.9e1734ade019.js:5905
#: static_collected/vm-detail.c47949114749.js:7013
#: static_collected/vm-detail.e3f398067c8a.js:6942
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6440
#: static_collected/dashboard/vm-tour.1562cc89a659.js:71
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:71
#: static_collected/dashboard/vm-tour.js:71
msgid "RAM amount"
msgstr "RAM mennyiség"
#: dashboard/static/dashboard/vm-tour.js:71
#: static_collected/vm-detail.09737c69abc3.js:5904
#: static_collected/vm-detail.15d710d8ccf0.js:6440
#: static_collected/vm-detail.234990ca6ec1.js:7013
#: static_collected/vm-detail.47b1d21da259.js:5904
#: static_collected/vm-detail.9e1734ade019.js:5905
#: static_collected/vm-detail.c47949114749.js:7013
#: static_collected/vm-detail.e3f398067c8a.js:6942
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6440
#: static_collected/dashboard/vm-tour.1562cc89a659.js:71
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:71
#: static_collected/dashboard/vm-tour.js:71
msgid "amount of RAM."
msgstr "a memória mennyisége."
#: dashboard/static/dashboard/vm-tour.js:81
#: static_collected/vm-detail.09737c69abc3.js:5914
#: static_collected/vm-detail.15d710d8ccf0.js:6450
#: static_collected/vm-detail.234990ca6ec1.js:7023
#: static_collected/vm-detail.47b1d21da259.js:5914
#: static_collected/vm-detail.9e1734ade019.js:5915
#: static_collected/vm-detail.c47949114749.js:7023
#: static_collected/vm-detail.e3f398067c8a.js:6952
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6450
#: static_collected/dashboard/vm-tour.1562cc89a659.js:81
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:81
#: static_collected/dashboard/vm-tour.js:81
msgid "Disks"
msgstr "Lemezek"
#: dashboard/static/dashboard/vm-tour.js:82
#: static_collected/vm-detail.09737c69abc3.js:5915
#: static_collected/vm-detail.15d710d8ccf0.js:6451
#: static_collected/vm-detail.234990ca6ec1.js:7024
#: static_collected/vm-detail.47b1d21da259.js:5915
#: static_collected/vm-detail.9e1734ade019.js:5916
#: static_collected/vm-detail.c47949114749.js:7024
#: static_collected/vm-detail.e3f398067c8a.js:6953
#: static_collected/vm-detail.e81fe84bf4c0.js:9
#: static_collected/vm-detail.js:6451
#: static_collected/dashboard/vm-tour.1562cc89a659.js:82
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:82
#: static_collected/dashboard/vm-tour.js:82
msgid ""
"You can add empty disks, download new ones and remove existing ones here."
......@@ -171,41 +526,129 @@ msgstr ""
"meglévőket."
#: dashboard/static/dashboard/vm-tour.js:92
#: static_collected/vm-detail.09737c69abc3.js:5925
#: static_collected/vm-detail.15d710d8ccf0.js:6461
#: static_collected/vm-detail.234990ca6ec1.js:7034
#: static_collected/vm-detail.47b1d21da259.js:5925
#: static_collected/vm-detail.9e1734ade019.js:5926
#: static_collected/vm-detail.c47949114749.js:7034
#: static_collected/vm-detail.e3f398067c8a.js:6963
#: static_collected/vm-detail.e81fe84bf4c0.js:10
#: static_collected/vm-detail.js:6461
#: static_collected/dashboard/vm-tour.1562cc89a659.js:92
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:92
#: static_collected/dashboard/vm-tour.js:92
msgid "Network tab"
msgstr "Hálózat lap"
#: dashboard/static/dashboard/vm-tour.js:93
#: static_collected/vm-detail.09737c69abc3.js:5926
#: static_collected/vm-detail.15d710d8ccf0.js:6462
#: static_collected/vm-detail.234990ca6ec1.js:7035
#: static_collected/vm-detail.47b1d21da259.js:5926
#: static_collected/vm-detail.9e1734ade019.js:5927
#: static_collected/vm-detail.c47949114749.js:7035
#: static_collected/vm-detail.e3f398067c8a.js:6964
#: static_collected/vm-detail.e81fe84bf4c0.js:10
#: static_collected/vm-detail.js:6462
#: static_collected/dashboard/vm-tour.1562cc89a659.js:93
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:93
#: static_collected/dashboard/vm-tour.js:93
msgid "You can add new network interfaces or remove existing ones here."
msgstr "Hozzáadhat új hálózati interfészeket, vagy törölheti a meglévőket."
#: dashboard/static/dashboard/vm-tour.js:102
#: static_collected/vm-detail.09737c69abc3.js:5935
#: static_collected/vm-detail.15d710d8ccf0.js:6471
#: static_collected/vm-detail.234990ca6ec1.js:7044
#: static_collected/vm-detail.47b1d21da259.js:5935
#: static_collected/vm-detail.9e1734ade019.js:5936
#: static_collected/vm-detail.c47949114749.js:7044
#: static_collected/vm-detail.e3f398067c8a.js:6973
#: static_collected/vm-detail.e81fe84bf4c0.js:10
#: static_collected/vm-detail.js:6471
#: static_collected/dashboard/vm-tour.1562cc89a659.js:102
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:102
#: static_collected/dashboard/vm-tour.js:102
msgid "Deploy"
msgstr "Indítás"
#: dashboard/static/dashboard/vm-tour.js:105
#: static_collected/vm-detail.09737c69abc3.js:5938
#: static_collected/vm-detail.15d710d8ccf0.js:6474
#: static_collected/vm-detail.234990ca6ec1.js:7047
#: static_collected/vm-detail.47b1d21da259.js:5938
#: static_collected/vm-detail.9e1734ade019.js:5939
#: static_collected/vm-detail.c47949114749.js:7047
#: static_collected/vm-detail.e3f398067c8a.js:6976
#: static_collected/vm-detail.e81fe84bf4c0.js:10
#: static_collected/vm-detail.js:6474
#: static_collected/dashboard/vm-tour.1562cc89a659.js:105
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:105
#: static_collected/dashboard/vm-tour.js:105
msgid "Deploy the virtual machine."
msgstr "A virtuális gép elindítása."
#: dashboard/static/dashboard/vm-tour.js:110
#: static_collected/vm-detail.09737c69abc3.js:5943
#: static_collected/vm-detail.15d710d8ccf0.js:6479
#: static_collected/vm-detail.234990ca6ec1.js:7052
#: static_collected/vm-detail.47b1d21da259.js:5943
#: static_collected/vm-detail.9e1734ade019.js:5944
#: static_collected/vm-detail.c47949114749.js:7052
#: static_collected/vm-detail.e3f398067c8a.js:6981
#: static_collected/vm-detail.e81fe84bf4c0.js:10
#: static_collected/vm-detail.js:6479
#: static_collected/dashboard/vm-tour.1562cc89a659.js:110
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:110
#: static_collected/dashboard/vm-tour.js:110
msgid "Connect"
msgstr "Csatlakozás"
#: dashboard/static/dashboard/vm-tour.js:113
#: static_collected/vm-detail.09737c69abc3.js:5946
#: static_collected/vm-detail.15d710d8ccf0.js:6482
#: static_collected/vm-detail.234990ca6ec1.js:7055
#: static_collected/vm-detail.47b1d21da259.js:5946
#: static_collected/vm-detail.9e1734ade019.js:5947
#: static_collected/vm-detail.c47949114749.js:7055
#: static_collected/vm-detail.e3f398067c8a.js:6984
#: static_collected/vm-detail.e81fe84bf4c0.js:10
#: static_collected/vm-detail.js:6482
#: static_collected/dashboard/vm-tour.1562cc89a659.js:113
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:113
#: static_collected/dashboard/vm-tour.js:113
msgid "Use the connection string or connect with your choice of client!"
msgstr "Használja a megadott parancsot, vagy kedvenc kliensét."
#: dashboard/static/dashboard/vm-tour.js:120
#: static_collected/vm-detail.09737c69abc3.js:5953
#: static_collected/vm-detail.15d710d8ccf0.js:6489
#: static_collected/vm-detail.234990ca6ec1.js:7062
#: static_collected/vm-detail.47b1d21da259.js:5953
#: static_collected/vm-detail.9e1734ade019.js:5954
#: static_collected/vm-detail.c47949114749.js:7062
#: static_collected/vm-detail.e3f398067c8a.js:6991
#: static_collected/vm-detail.e81fe84bf4c0.js:10
#: static_collected/vm-detail.js:6489
#: static_collected/dashboard/vm-tour.1562cc89a659.js:120
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:120
#: static_collected/dashboard/vm-tour.js:120
msgid "Customize the virtual machine"
msgstr "Szabja testre a gépet"
#: dashboard/static/dashboard/vm-tour.js:121
#: static_collected/vm-detail.09737c69abc3.js:5954
#: static_collected/vm-detail.15d710d8ccf0.js:6490
#: static_collected/vm-detail.234990ca6ec1.js:7063
#: static_collected/vm-detail.47b1d21da259.js:5954
#: static_collected/vm-detail.9e1734ade019.js:5955
#: static_collected/vm-detail.c47949114749.js:7063
#: static_collected/vm-detail.e3f398067c8a.js:6992
#: static_collected/vm-detail.e81fe84bf4c0.js:10
#: static_collected/vm-detail.js:6490
#: static_collected/dashboard/vm-tour.1562cc89a659.js:121
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:121
#: static_collected/dashboard/vm-tour.js:121
msgid ""
"After you have connected to the virtual machine do your modifications then "
......@@ -215,11 +658,33 @@ msgstr ""
"ki."
#: dashboard/static/dashboard/vm-tour.js:126
#: static_collected/vm-detail.09737c69abc3.js:5959
#: static_collected/vm-detail.15d710d8ccf0.js:6495
#: static_collected/vm-detail.234990ca6ec1.js:7068
#: static_collected/vm-detail.47b1d21da259.js:5959
#: static_collected/vm-detail.9e1734ade019.js:5960
#: static_collected/vm-detail.c47949114749.js:7068
#: static_collected/vm-detail.e3f398067c8a.js:6997
#: static_collected/vm-detail.e81fe84bf4c0.js:10
#: static_collected/vm-detail.js:6495
#: static_collected/dashboard/vm-tour.1562cc89a659.js:126
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:126
#: static_collected/dashboard/vm-tour.js:126
msgid "Save as"
msgstr "Mentés sablonként"
#: dashboard/static/dashboard/vm-tour.js:129
#: static_collected/vm-detail.09737c69abc3.js:5962
#: static_collected/vm-detail.15d710d8ccf0.js:6498
#: static_collected/vm-detail.234990ca6ec1.js:7071
#: static_collected/vm-detail.47b1d21da259.js:5962
#: static_collected/vm-detail.9e1734ade019.js:5963
#: static_collected/vm-detail.c47949114749.js:7071
#: static_collected/vm-detail.e3f398067c8a.js:7000
#: static_collected/vm-detail.e81fe84bf4c0.js:10
#: static_collected/vm-detail.js:6498
#: static_collected/dashboard/vm-tour.1562cc89a659.js:129
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:129
#: static_collected/dashboard/vm-tour.js:129
msgid ""
"Press the \"Save as template\" button and wait until the activity finishes."
......@@ -228,18 +693,50 @@ msgstr ""
"elkészül."
#: dashboard/static/dashboard/vm-tour.js:135
#: static_collected/vm-detail.09737c69abc3.js:5968
#: static_collected/vm-detail.15d710d8ccf0.js:6504
#: static_collected/vm-detail.234990ca6ec1.js:7077
#: static_collected/vm-detail.47b1d21da259.js:5968
#: static_collected/vm-detail.9e1734ade019.js:5969
#: static_collected/vm-detail.c47949114749.js:7077
#: static_collected/vm-detail.e3f398067c8a.js:7006
#: static_collected/vm-detail.e81fe84bf4c0.js:10
#: static_collected/vm-detail.js:6504
#: static_collected/dashboard/vm-tour.1562cc89a659.js:135
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:135
#: static_collected/dashboard/vm-tour.js:135
msgid "Finish"
msgstr "Befejezés"
#: dashboard/static/dashboard/vm-tour.js:138
#: static_collected/vm-detail.09737c69abc3.js:5971
#: static_collected/vm-detail.15d710d8ccf0.js:6507
#: static_collected/vm-detail.234990ca6ec1.js:7080
#: static_collected/vm-detail.47b1d21da259.js:5971
#: static_collected/vm-detail.9e1734ade019.js:5972
#: static_collected/vm-detail.c47949114749.js:7080
#: static_collected/vm-detail.e3f398067c8a.js:7009
#: static_collected/vm-detail.e81fe84bf4c0.js:10
#: static_collected/vm-detail.js:6507
#: static_collected/dashboard/vm-tour.1562cc89a659.js:138
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:138
#: static_collected/dashboard/vm-tour.js:138
msgid ""
"This is the last message, if something is not clear you can do the the tour "
"again!"
msgstr "A túra véget ért. Ha valami nem érthető, újrakezdheti az útmutatót."
#: network/static/js/host.js:10 static_collected/js/host.js:10
#: network/static/js/host.js:10 static_collected/all.047675ebf594.js:5239
#: static_collected/all.0aecd87e873a.js:5309
#: static_collected/all.0db607331718.js:5215
#: static_collected/all.146abee8fe99.js:5239
#: static_collected/all.24315fee0c8e.js:5238
#: static_collected/all.483236be7507.js:22
#: static_collected/all.73e18dd7e023.js:5215
#: static_collected/all.a650c949d06f.js:5238
#: static_collected/all.b66e942cdb56.js:5238
#: static_collected/all.f62aa30bd0a4.js:5239 static_collected/all.js:5215
#: static_collected/js/host.e1b01efa0e67.js:10 static_collected/js/host.js:10
msgid ""
"Are you sure you want to remove host group <strong>\"%(group)s\"</strong> "
"from <strong>\"%(host)s\"</strong>?"
......@@ -247,32 +744,113 @@ msgstr ""
"Biztosan törli a(z)<strong>„%(host)s”</strong> gépet a(z) "
"<strong>„%(group)s”</strong> gépcsoportból?"
#: network/static/js/host.js:13 static_collected/js/host.js:13
#: network/static/js/host.js:13 static_collected/all.047675ebf594.js:5242
#: static_collected/all.0aecd87e873a.js:5312
#: static_collected/all.0db607331718.js:5218
#: static_collected/all.146abee8fe99.js:5242
#: static_collected/all.24315fee0c8e.js:5241
#: static_collected/all.483236be7507.js:22
#: static_collected/all.73e18dd7e023.js:5218
#: static_collected/all.a650c949d06f.js:5241
#: static_collected/all.b66e942cdb56.js:5241
#: static_collected/all.f62aa30bd0a4.js:5242 static_collected/all.js:5218
#: static_collected/js/host.e1b01efa0e67.js:13 static_collected/js/host.js:13
msgid "Are you sure you want to delete this rule?"
msgstr "Biztosan törli ezt a szabályt?"
#: network/static/js/host.js:20 network/static/js/switch-port.js:14
#: static_collected/all.047675ebf594.js:5249
#: static_collected/all.047675ebf594.js:5347
#: static_collected/all.0aecd87e873a.js:5319
#: static_collected/all.0aecd87e873a.js:5417
#: static_collected/all.0db607331718.js:5225
#: static_collected/all.0db607331718.js:5323
#: static_collected/all.146abee8fe99.js:5249
#: static_collected/all.146abee8fe99.js:5347
#: static_collected/all.24315fee0c8e.js:5248
#: static_collected/all.24315fee0c8e.js:5346
#: static_collected/all.483236be7507.js:22
#: static_collected/all.73e18dd7e023.js:5225
#: static_collected/all.73e18dd7e023.js:5323
#: static_collected/all.a650c949d06f.js:5248
#: static_collected/all.a650c949d06f.js:5346
#: static_collected/all.b66e942cdb56.js:5248
#: static_collected/all.b66e942cdb56.js:5346
#: static_collected/all.f62aa30bd0a4.js:5249
#: static_collected/all.f62aa30bd0a4.js:5347 static_collected/all.js:5225
#: static_collected/all.js.c:5323
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:95
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:208
#: static_collected/admin/js/admin/DateTimeShortcuts.js:95
#: static_collected/admin/js/admin/DateTimeShortcuts.js:208
#: static_collected/js/host.js:20 static_collected/js/switch-port.js:14
#: static_collected/js/host.e1b01efa0e67.js:20 static_collected/js/host.js:20
#: static_collected/js/switch-port.5eafbc56ee38.js:14
#: static_collected/js/switch-port.js:14
msgid "Cancel"
msgstr "Mégsem"
#: network/static/js/host.js:25 network/static/js/switch-port.js:19
#: static_collected/all.047675ebf594.js:5254
#: static_collected/all.047675ebf594.js:5352
#: static_collected/all.0aecd87e873a.js:5324
#: static_collected/all.0aecd87e873a.js:5422
#: static_collected/all.0db607331718.js:5230
#: static_collected/all.0db607331718.js:5328
#: static_collected/all.146abee8fe99.js:5254
#: static_collected/all.146abee8fe99.js:5352
#: static_collected/all.24315fee0c8e.js:5253
#: static_collected/all.24315fee0c8e.js:5351
#: static_collected/all.483236be7507.js:22
#: static_collected/all.73e18dd7e023.js:5230
#: static_collected/all.73e18dd7e023.js:5328
#: static_collected/all.a650c949d06f.js:5253
#: static_collected/all.a650c949d06f.js:5351
#: static_collected/all.b66e942cdb56.js:5253
#: static_collected/all.b66e942cdb56.js:5351
#: static_collected/all.f62aa30bd0a4.js:5254
#: static_collected/all.f62aa30bd0a4.js:5352 static_collected/all.js:5230
#: static_collected/all.js.c:5328
#: static_collected/admin/js/SelectFilter2.9825381ac407.js:69
#: static_collected/admin/js/SelectFilter2.js:69
#: static_collected/js/host.js:25 static_collected/js/switch-port.js:19
#: static_collected/js/host.e1b01efa0e67.js:25 static_collected/js/host.js:25
#: static_collected/js/switch-port.5eafbc56ee38.js:19
#: static_collected/js/switch-port.js:19
msgid "Remove"
msgstr "Eltávolítás"
#: network/static/js/switch-port.js:8 static_collected/js/switch-port.js:8
#: network/static/js/switch-port.js:8
#: static_collected/all.047675ebf594.js:5341
#: static_collected/all.0aecd87e873a.js:5411
#: static_collected/all.0db607331718.js:5317
#: static_collected/all.146abee8fe99.js:5341
#: static_collected/all.24315fee0c8e.js:5340
#: static_collected/all.483236be7507.js:22
#: static_collected/all.73e18dd7e023.js:5317
#: static_collected/all.a650c949d06f.js:5340
#: static_collected/all.b66e942cdb56.js:5340
#: static_collected/all.f62aa30bd0a4.js:5341 static_collected/all.js:5317
#: static_collected/js/switch-port.5eafbc56ee38.js:8
#: static_collected/js/switch-port.js:8
msgid "Are you sure you want to delete this device?"
msgstr "Biztosan törli ezt az eszközt?"
#: static_collected/vm-detail.e81fe84bf4c0.js:9
msgid ""
"During the tour please don't try the functions because it may lead to "
"graphical glitches, however you can end the tour any time you want with the "
"End Tour button!"
msgstr ""
"A túra során még ne próbálja ki a bemutatott funkciókat, mivel ez "
"megjelenítési problémákat okozhatnak. A túrát bármikor befejezheti a "
"Befejezés gombra kattintva."
#: static_collected/admin/js/SelectFilter2.9825381ac407.js:45
#: static_collected/admin/js/SelectFilter2.js:45
#, c-format
msgid "Available %s"
msgstr "Elérhető %s"
#: static_collected/admin/js/SelectFilter2.9825381ac407.js:46
#: static_collected/admin/js/SelectFilter2.js:46
#, c-format
msgid ""
......@@ -282,33 +860,40 @@ msgstr ""
"Ez az elérhető %s listája. Úgy választhat közülük, hogy rákattint az alábbi "
"dobozban, és megnyomja a dobozok közti \"Választás\" nyilat."
#: static_collected/admin/js/SelectFilter2.9825381ac407.js:53
#: static_collected/admin/js/SelectFilter2.js:53
#, c-format
msgid "Type into this box to filter down the list of available %s."
msgstr "Írjon a mezőbe az elérhető %s szűréséhez."
#: static_collected/admin/js/SelectFilter2.9825381ac407.js:57
#: static_collected/admin/js/SelectFilter2.js:57
msgid "Filter"
msgstr "Szűrő"
#: static_collected/admin/js/SelectFilter2.9825381ac407.js:61
#: static_collected/admin/js/SelectFilter2.js:61
msgid "Choose all"
msgstr "Mindet kijelölni"
#: static_collected/admin/js/SelectFilter2.9825381ac407.js:61
#: static_collected/admin/js/SelectFilter2.js:61
#, c-format
msgid "Click to choose all %s at once."
msgstr "Kattintson az összes %s kiválasztásához."
#: static_collected/admin/js/SelectFilter2.9825381ac407.js:67
#: static_collected/admin/js/SelectFilter2.js:67
msgid "Choose"
msgstr "Választás"
#: static_collected/admin/js/SelectFilter2.9825381ac407.js:75
#: static_collected/admin/js/SelectFilter2.js:75
#, c-format
msgid "Chosen %s"
msgstr "%s kiválasztva"
#: static_collected/admin/js/SelectFilter2.9825381ac407.js:76
#: static_collected/admin/js/SelectFilter2.js:76
#, c-format
msgid ""
......@@ -318,23 +903,29 @@ msgstr ""
"Ez a kiválasztott %s listája. Eltávolíthat közülük, ha rákattint, majd a két "
"doboz közti \"Eltávolítás\" nyílra kattint."
#: static_collected/admin/js/SelectFilter2.9825381ac407.js:80
#: static_collected/admin/js/SelectFilter2.js:80
msgid "Remove all"
msgstr "Összes törlése"
#: static_collected/admin/js/SelectFilter2.9825381ac407.js:80
#: static_collected/admin/js/SelectFilter2.js:80
#, c-format
msgid "Click to remove all chosen %s at once."
msgstr "Kattintson az összes %s eltávolításához."
#: static_collected/admin/js/actions.fd884781224d.js:18
#: static_collected/admin/js/actions.js:18
#: static_collected/admin/js/actions.min.6a5121336635.js:1
#: static_collected/admin/js/actions.min.js:1
msgid "%(sel)s of %(cnt)s selected"
msgid_plural "%(sel)s of %(cnt)s selected"
msgstr[0] "%(sel)s/%(cnt)s kijelölve"
msgstr[1] "%(sel)s/%(cnt)s kijelölve"
#: static_collected/admin/js/actions.fd884781224d.js:109
#: static_collected/admin/js/actions.js:109
#: static_collected/admin/js/actions.min.6a5121336635.js:5
#: static_collected/admin/js/actions.min.js:5
msgid ""
"You have unsaved changes on individual editable fields. If you run an "
......@@ -343,7 +934,9 @@ msgstr ""
"Még el nem mentett módosításai vannak egyes szerkeszthető mezőkön. Ha most "
"futtat egy műveletet, akkor a módosítások elvesznek. "
#: static_collected/admin/js/actions.fd884781224d.js:121
#: static_collected/admin/js/actions.js:121
#: static_collected/admin/js/actions.min.6a5121336635.js:5
#: static_collected/admin/js/actions.min.js:5
msgid ""
"You have selected an action, but you haven't saved your changes to "
......@@ -354,7 +947,9 @@ msgstr ""
"módosításait. Kattintson az OK gombra a mentéshez. Újra kell futtatnia az "
"műveletet."
#: static_collected/admin/js/actions.fd884781224d.js:123
#: static_collected/admin/js/actions.js:123
#: static_collected/admin/js/actions.min.6a5121336635.js:6
#: static_collected/admin/js/actions.min.js:6
msgid ""
"You have selected an action, and you haven't made any changes on individual "
......@@ -364,6 +959,7 @@ msgstr ""
"Kiválasztott egy műveletet, és nem módosított egyetlen mezőt sem. "
"Feltehetően a Mehet gombot keresi a Mentés helyett."
#: static_collected/admin/js/calendar.23d4bc1c37cd.js:8
#: static_collected/admin/js/calendar.js:8
msgid ""
"January February March April May June July August September October November "
......@@ -372,61 +968,80 @@ msgstr ""
"január február március április május június július augusztus szeptember "
"október november december"
#: static_collected/admin/js/calendar.23d4bc1c37cd.js:9
#: static_collected/admin/js/calendar.js:9
msgid "S M T W T F S"
msgstr "V H K Sz Cs P Szo"
#: static_collected/admin/js/collapse.c781bafaf192.js:8
#: static_collected/admin/js/collapse.c781bafaf192.js:19
#: static_collected/admin/js/collapse.js:8
#: static_collected/admin/js/collapse.js:19
#: static_collected/admin/js/collapse.min.ce55331a033c.js:1
#: static_collected/admin/js/collapse.min.js:1
msgid "Show"
msgstr "Mutat"
#: static_collected/admin/js/collapse.c781bafaf192.js:16
#: static_collected/admin/js/collapse.js:16
#: static_collected/admin/js/collapse.min.ce55331a033c.js:1
#: static_collected/admin/js/collapse.min.js:1
msgid "Hide"
msgstr "Elrejt"
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:52
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:88
#: static_collected/admin/js/admin/DateTimeShortcuts.js:52
#: static_collected/admin/js/admin/DateTimeShortcuts.js:88
msgid "Now"
msgstr "Most"
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:56
#: static_collected/admin/js/admin/DateTimeShortcuts.js:56
msgid "Clock"
msgstr "Óra"
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:84
#: static_collected/admin/js/admin/DateTimeShortcuts.js:84
msgid "Choose a time"
msgstr "Válassza ki az időt"
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:89
#: static_collected/admin/js/admin/DateTimeShortcuts.js:89
msgid "Midnight"
msgstr "Éjfél"
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:90
#: static_collected/admin/js/admin/DateTimeShortcuts.js:90
msgid "6 a.m."
msgstr "Reggel 6 óra"
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:91
#: static_collected/admin/js/admin/DateTimeShortcuts.js:91
msgid "Noon"
msgstr "Dél"
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:148
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:201
#: static_collected/admin/js/admin/DateTimeShortcuts.js:148
#: static_collected/admin/js/admin/DateTimeShortcuts.js:201
msgid "Today"
msgstr "Ma"
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:152
#: static_collected/admin/js/admin/DateTimeShortcuts.js:152
msgid "Calendar"
msgstr "Naptár"
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:199
#: static_collected/admin/js/admin/DateTimeShortcuts.js:199
msgid "Yesterday"
msgstr "Tegnap"
#: static_collected/admin/js/admin/DateTimeShortcuts.5d58f199725a.js:203
#: static_collected/admin/js/admin/DateTimeShortcuts.js:203
msgid "Tomorrow"
msgstr "Holnap"
# end
#~ msgid "Prevent newline at end of file"
#~ msgstr "x"
......@@ -31,7 +31,7 @@ celery = Celery('manager',
'storage.tasks.local_tasks',
'storage.tasks.periodic_tasks',
'firewall.tasks.local_tasks',
'monitor.tasks.local_periodic_tasks',
'dashboard.tasks.local_periodic_tasks',
])
celery.conf.update(
......@@ -41,49 +41,19 @@ celery.conf.update(
CELERY_QUEUES=(
Queue(HOSTNAME + '.man', Exchange('manager', type='direct'),
routing_key="manager"),
Queue(HOSTNAME + '.monitor', Exchange('monitor', type='direct'),
routing_key="monitor"),
),
CELERYBEAT_SCHEDULE={
'vm.update_domain_states': {
'task': 'vm.tasks.local_periodic_tasks.update_domain_states',
'schedule': timedelta(seconds=10),
'options': {'queue': 'localhost.man'}
},
'vm.garbage_collector': {
'task': 'vm.tasks.local_periodic_tasks.garbage_collector',
'schedule': timedelta(minutes=10),
'options': {'queue': 'localhost.man'}
},
'storage.periodic_tasks': {
'task': 'storage.tasks.periodic_tasks.garbage_collector',
'schedule': timedelta(hours=1),
'options': {'queue': 'localhost.man'}
},
'dashboard.local_periodic_tasks': {
'dashboard.send_email_notifications': {
'task': 'dashboard.tasks.local_periodic_tasks.'
'send_email_notifications',
'schedule': timedelta(hours=24),
'options': {'queue': 'localhost.man'}
},
'monitor.measure_response_time': {
'task': 'monitor.tasks.local_periodic_tasks.'
'measure_response_time',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.man'}
},
'monitor.check_celery_queues': {
'task': 'monitor.tasks.local_periodic_tasks.'
'check_celery_queues',
'schedule': timedelta(seconds=60),
'options': {'queue': 'localhost.man'}
},
'monitor.instance_per_template': {
'task': 'monitor.tasks.local_periodic_tasks.'
'instance_per_template',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.man'}
},
}
)
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from celery import Celery
from datetime import timedelta
from kombu import Queue, Exchange
from os import getenv
HOSTNAME = "localhost"
CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/")
celery = Celery('monitor',
broker=getenv("AMQP_URI"),
include=['vm.tasks.local_periodic_tasks',
'monitor.tasks.local_periodic_tasks',
])
celery.conf.update(
CELERY_RESULT_BACKEND='cache',
CELERY_CACHE_BACKEND=CACHE_URI,
CELERY_TASK_RESULT_EXPIRES=300,
CELERY_QUEUES=(
Queue(HOSTNAME + '.monitor', Exchange('monitor', type='direct'),
routing_key="monitor"),
),
CELERYBEAT_SCHEDULE={
'vm.update_domain_states': {
'task': 'vm.tasks.local_periodic_tasks.update_domain_states',
'schedule': timedelta(seconds=10),
'options': {'queue': 'localhost.monitor'}
},
'monitor.measure_response_time': {
'task': 'monitor.tasks.local_periodic_tasks.'
'measure_response_time',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.monitor'}
},
'monitor.check_celery_queues': {
'task': 'monitor.tasks.local_periodic_tasks.'
'check_celery_queues',
'schedule': timedelta(seconds=60),
'options': {'queue': 'localhost.monitor'}
},
'monitor.instance_per_template': {
'task': 'monitor.tasks.local_periodic_tasks.'
'instance_per_template',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.monitor'}
},
}
)
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from celery import Celery
from datetime import timedelta
from kombu import Queue, Exchange
from os import getenv
HOSTNAME = "localhost"
CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/")
celery = Celery('manager.slow',
broker=getenv("AMQP_URI"),
include=['vm.tasks.local_tasks',
'vm.tasks.local_periodic_tasks',
'storage.tasks.local_tasks',
'storage.tasks.periodic_tasks',
])
celery.conf.update(
CELERY_RESULT_BACKEND='cache',
CELERY_CACHE_BACKEND=CACHE_URI,
CELERY_TASK_RESULT_EXPIRES=300,
CELERY_QUEUES=(
Queue(HOSTNAME + '.man.slow', Exchange('manager.slow', type='direct'),
routing_key="manager.slow"),
),
CELERYBEAT_SCHEDULE={
'vm.garbage_collector': {
'task': 'vm.tasks.local_periodic_tasks.garbage_collector',
'schedule': timedelta(minutes=10),
'options': {'queue': 'localhost.man.slow'}
},
}
)
......@@ -278,7 +278,7 @@ class Disk(TimeStampedModel):
return Disk.create(base=self, datastore=self.datastore,
name=self.name, size=self.size,
type=new_type)
type=new_type, dev_num=self.dev_num)
def get_vmdisk_desc(self):
"""Serialize disk object to the vmdriver.
......
......@@ -28,7 +28,7 @@ def check_queue(storage, queue_id, priority):
if priority is not None:
queue_name = queue_name + "." + priority
inspect = celery.control.inspect()
inspect.timeout = 0.1
inspect.timeout = 0.5
active_queues = inspect.active_queues()
if active_queues is None:
return False
......
......@@ -90,7 +90,8 @@ class InstanceActivity(ActivityModel):
@classmethod
def create(cls, code_suffix, instance, task_uuid=None, user=None,
concurrency_check=True, readable_name=None):
concurrency_check=True, readable_name=None,
resultant_state=None):
readable_name = _normalize_readable_name(readable_name, code_suffix)
# Check for concurrent activities
......@@ -100,14 +101,14 @@ class InstanceActivity(ActivityModel):
activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix)
act = cls(activity_code=activity_code, instance=instance, parent=None,
resultant_state=None, started=timezone.now(),
resultant_state=resultant_state, started=timezone.now(),
readable_name_data=readable_name.to_dict(),
task_uuid=task_uuid, user=user)
act.save()
return act
def create_sub(self, code_suffix, task_uuid=None, concurrency_check=True,
readable_name=None):
readable_name=None, resultant_state=None):
readable_name = _normalize_readable_name(readable_name, code_suffix)
# Check for concurrent activities
......@@ -117,7 +118,8 @@ class InstanceActivity(ActivityModel):
act = InstanceActivity(
activity_code=join_activity_code(self.activity_code, code_suffix),
instance=self.instance, parent=self, resultant_state=None,
instance=self.instance, parent=self,
resultant_state=resultant_state,
readable_name_data=readable_name.to_dict(), started=timezone.now(),
task_uuid=task_uuid, user=self.user)
act.save()
......@@ -190,18 +192,23 @@ class InstanceActivity(ActivityModel):
readable_name=readable_name)
return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
def get_operation(self):
return self.instance.get_operation_from_activity_code(
self.activity_code)
@contextmanager
def instance_activity(code_suffix, instance, on_abort=None, on_commit=None,
task_uuid=None, user=None, concurrency_check=True,
readable_name=None):
readable_name=None, resultant_state=None):
"""Create a transactional context for an instance activity.
"""
if not readable_name:
warn("Set readable_name", stacklevel=3)
act = InstanceActivity.create(code_suffix, instance, task_uuid, user,
concurrency_check,
readable_name=readable_name)
readable_name=readable_name,
resultant_state=resultant_state)
return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
......
......@@ -32,6 +32,7 @@ from django.core.exceptions import PermissionDenied
from django.db.models import (BooleanField, CharField, DateTimeField,
IntegerField, ForeignKey, Manager,
ManyToManyField, permalink, SET_NULL, TextField)
from django.db import IntegrityError
from django.dispatch import Signal
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop
......@@ -293,6 +294,10 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
message = ugettext_noop(
"Instance %(instance)s has already been destroyed.")
class NoAgentError(InstanceError):
message = ugettext_noop(
"No agent software is running on instance %(instance)s.")
class WrongStateError(InstanceError):
message = ugettext_noop(
"Current state (%(state)s) of instance %(instance)s is "
......@@ -483,11 +488,12 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
"""
try:
datastore = self.disks.all()[0].datastore
except:
return None
else:
path = datastore.path + '/' + self.vm_name + '.dump'
return {'datastore': datastore, 'path': path}
except IndexError:
from storage.models import DataStore
datastore = DataStore.objects.get()
path = datastore.path + '/' + self.vm_name + '.dump'
return {'datastore': datastore, 'path': path}
@property
def primary_host(self):
......@@ -507,7 +513,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def ipv4(self):
"""Primary IPv4 address of the instance.
"""
return self.primary_host.ipv4 if self.primary_host else None
# return self.primary_host.ipv4 if self.primary_host else None
for i in self.interface_set.all():
if i.host:
return i.host.ipv4
return None
@property
def ipv6(self):
......@@ -919,8 +929,16 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def allocate_vnc_port(self):
if self.vnc_port is None:
self.vnc_port = find_unused_vnc_port()
self.save()
while True:
try:
self.vnc_port = find_unused_vnc_port()
self.save()
except IntegrityError:
# Another thread took this port get another one
logger.debug("Port %s is in use.", self.vnc_port)
pass
else:
break
def yield_vnc_port(self):
if self.vnc_port is not None:
......@@ -936,7 +954,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
'ERROR': 'fa-warning',
'PENDING': 'fa-rocket',
'DESTROYED': 'fa-trash-o',
'MIGRATING': 'fa-truck'}.get(self.status, 'fa-question')
'MIGRATING': 'fa-truck migrating-icon'
}.get(self.status, 'fa-question')
def get_activities(self, user=None):
acts = (self.activity_log.filter(parent=None).
......@@ -986,3 +1005,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
instance=self, succeeded=None, parent=None).latest("started")
except InstanceActivity.DoesNotExist:
return None
def is_in_status_change(self):
latest = self.get_latest_activity_in_progress()
return (latest and latest.resultant_state is not None
and self.status != latest.resultant_state)
......@@ -16,6 +16,7 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import, unicode_literals
from functools import update_wrapper
from logging import getLogger
from warnings import warn
import requests
......@@ -51,6 +52,8 @@ def node_available(function):
return function(self, *args, **kwargs)
else:
return None
update_wrapper(decorate, function)
decorate._original = function
return decorate
......@@ -257,7 +260,7 @@ class Node(OperatedMixin, TimeStampedModel):
@method_cache(10)
def monitor_info(self):
metrics = ('cpu.usage', 'memory.usage')
prefix = 'circle.%s.' % self.name
prefix = 'circle.%s.' % self.host.hostname
params = [('target', '%s%s' % (prefix, metric))
for metric in metrics]
params.append(('from', '-5min'))
......
......@@ -19,10 +19,12 @@ from __future__ import absolute_import, unicode_literals
from logging import getLogger
from re import search
from string import ascii_lowercase
from urlparse import urlsplit
from django.core.exceptions import PermissionDenied
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django.conf import settings
from sizefield.utils import filesizeformat
......@@ -39,7 +41,9 @@ from .models import (
Instance, InstanceActivity, InstanceTemplate, Interface, Node,
NodeActivity, pwgen
)
from .tasks import agent_tasks
from .tasks import agent_tasks, local_agent_tasks
from dashboard.store_api import Store, NoStoreException
logger = getLogger(__name__)
......@@ -51,6 +55,7 @@ class InstanceOperation(Operation):
concurrency_check = True
accept_states = None
deny_states = None
resultant_state = None
def __init__(self, instance):
super(InstanceOperation, self).__init__(subject=instance)
......@@ -95,12 +100,14 @@ class InstanceOperation(Operation):
"provided as parameter.")
return parent.create_sub(code_suffix=self.activity_code_suffix,
readable_name=name)
readable_name=name,
resultant_state=self.resultant_state)
else:
return InstanceActivity.create(
code_suffix=self.activity_code_suffix, instance=self.instance,
readable_name=name, user=user,
concurrency_check=self.concurrency_check)
concurrency_check=self.concurrency_check,
resultant_state=self.resultant_state)
def is_preferred(self):
"""If this is the recommended op in the current state of the instance.
......@@ -146,6 +153,7 @@ class AddInterfaceOperation(InstanceOperation):
self.rollback(net, activity)
raise
net.deploy()
local_agent_tasks.send_networking_commands(self.instance, activity)
def get_activity_name(self, kwargs):
return create_readable(ugettext_noop("add %(vlan)s interface"),
......@@ -211,6 +219,7 @@ class DownloadDiskOperation(InstanceOperation):
has_percentage = True
required_perms = ('storage.download_disk', )
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
async_queue = "localhost.man.slow"
def _operation(self, user, url, task, activity, name=None):
activity.result = url
......@@ -246,14 +255,22 @@ class DeployOperation(InstanceOperation):
"and network configuration).")
required_perms = ()
deny_states = ('SUSPENDED', 'RUNNING')
resultant_state = 'RUNNING'
def is_preferred(self):
return self.instance.status in (self.instance.STATUS.STOPPED,
self.instance.STATUS.PENDING,
self.instance.STATUS.ERROR)
def on_abort(self, activity, error):
activity.resultant_state = 'STOPPED'
def on_commit(self, activity):
activity.resultant_state = 'RUNNING'
activity.result = create_readable(
ugettext_noop("virtual machine successfully "
"deployed to node: %(node)s"),
node=self.instance.node)
def _operation(self, activity, timeout=15):
# Allocate VNC port and host node
......@@ -268,9 +285,11 @@ class DeployOperation(InstanceOperation):
# Deploy VM on remote machine
if self.instance.state not in ['PAUSED']:
rn = create_readable(ugettext_noop("deploy virtual machine"),
ugettext_noop("deploy vm to %(node)s"),
node=self.instance.node)
with activity.sub_activity(
'deploying_vm', readable_name=ugettext_noop(
"deploy virtual machine")) as deploy_act:
'deploying_vm', readable_name=rn) as deploy_act:
deploy_act.result = self.instance.deploy_vm(timeout=timeout)
# Establish network connection (vmdriver)
......@@ -301,9 +320,7 @@ class DestroyOperation(InstanceOperation):
description = _("Permanently destroy virtual machine, its network "
"settings and disks.")
required_perms = ()
def on_commit(self, activity):
activity.resultant_state = 'DESTROYED'
resultant_state = 'DESTROYED'
def _operation(self, activity):
# Destroy networks
......@@ -351,7 +368,9 @@ class MigrateOperation(InstanceOperation):
description = _("Move virtual machine to an other worker node with a few "
"seconds of interruption (live migration).")
required_perms = ()
superuser_required = True
accept_states = ('RUNNING', )
async_queue = "localhost.man.slow"
def rollback(self, activity):
with activity.sub_activity(
......@@ -359,12 +378,6 @@ class MigrateOperation(InstanceOperation):
"redeploy network (rollback)")):
self.instance.deploy_net()
def check_auth(self, user):
if not user.is_superuser:
raise PermissionDenied()
super(MigrateOperation, self).check_auth(user=user)
def _operation(self, activity, to_node=None, timeout=120):
if not to_node:
with activity.sub_activity('scheduling',
......@@ -502,6 +515,7 @@ class SaveAsTemplateOperation(InstanceOperation):
abortable = True
required_perms = ('vm.create_template', )
accept_states = ('RUNNING', 'PENDING', 'STOPPED')
async_queue = "localhost.man.slow"
def is_preferred(self):
return (self.instance.is_base and
......@@ -599,9 +613,7 @@ class ShutdownOperation(InstanceOperation):
abortable = True
required_perms = ()
accept_states = ('RUNNING', )
def on_commit(self, activity):
activity.resultant_state = 'STOPPED'
resultant_state = 'STOPPED'
def _operation(self, task=None):
self.instance.shutdown_vm(task=task)
......@@ -625,9 +637,7 @@ class ShutOffOperation(InstanceOperation):
"of a physical machine.")
required_perms = ()
accept_states = ('RUNNING', )
def on_commit(self, activity):
activity.resultant_state = 'STOPPED'
resultant_state = 'STOPPED'
def _operation(self, activity):
# Shutdown networks
......@@ -660,6 +670,8 @@ class SleepOperation(InstanceOperation):
"storage resources, and keep network resources allocated.")
required_perms = ()
accept_states = ('RUNNING', )
resultant_state = 'SUSPENDED'
async_queue = "localhost.man.slow"
def is_preferred(self):
return (not self.instance.is_base and
......@@ -671,9 +683,6 @@ class SleepOperation(InstanceOperation):
else:
activity.resultant_state = 'ERROR'
def on_commit(self, activity):
activity.resultant_state = 'SUSPENDED'
def _operation(self, activity, timeout=240):
# Destroy networks
with activity.sub_activity('shutdown_net', readable_name=ugettext_noop(
......@@ -702,6 +711,7 @@ class WakeUpOperation(InstanceOperation):
"virtual machine from this state.")
required_perms = ()
accept_states = ('SUSPENDED', )
resultant_state = 'RUNNING'
def is_preferred(self):
return self.instance.status == self.instance.STATUS.SUSPENDED
......@@ -709,9 +719,6 @@ class WakeUpOperation(InstanceOperation):
def on_abort(self, activity, error):
activity.resultant_state = 'ERROR'
def on_commit(self, activity):
activity.resultant_state = 'RUNNING'
def _operation(self, activity, timeout=60):
# Schedule vm
self.instance.allocate_vnc_port()
......@@ -750,7 +757,7 @@ class RenewOperation(InstanceOperation):
required_perms = ()
concurrency_check = False
def _operation(self, activity, lease=None, force=False):
def _operation(self, activity, lease=None, force=False, save=False):
suspend, delete = self.instance.get_renew_times(lease)
if (not force and suspend and self.instance.time_of_suspend and
suspend < self.instance.time_of_suspend):
......@@ -764,6 +771,8 @@ class RenewOperation(InstanceOperation):
"in its delete time get earlier than before."))
self.instance.time_of_suspend = suspend
self.instance.time_of_delete = delete
if save:
self.instance.lease = lease
self.instance.save()
activity.result = create_readable(ugettext_noop(
"Renewed to suspend at %(suspend)s and destroy at %(delete)s."),
......@@ -784,9 +793,17 @@ class ChangeStateOperation(InstanceOperation):
"resources.")
acl_level = "owner"
required_perms = ('vm.emergency_change_state', )
concurrency_check = False
def _operation(self, user, activity, new_state="NOSTATE"):
def _operation(self, user, activity, new_state="NOSTATE", interrupt=False):
activity.resultant_state = new_state
if interrupt:
msg_txt = ugettext_noop("Activity is forcibly interrupted.")
message = create_readable(msg_txt, msg_txt)
for i in InstanceActivity.objects.filter(
finished__isnull=True, instance=self.instance):
i.finish(False, result=message)
logger.error('Forced finishing activity %s', i)
register_operation(ChangeStateOperation)
......@@ -826,6 +843,8 @@ class FlushOperation(NodeOperation):
name = _("flush")
description = _("Disable node and move all instances to other ones.")
required_perms = ()
superuser_required = True
async_queue = "localhost.man.slow"
def on_abort(self, activity, error):
from manager.scheduler import TraitsUnsatisfiableException
......@@ -833,13 +852,6 @@ class FlushOperation(NodeOperation):
if self.node_enabled:
self.node.enable(activity.user, activity)
def check_auth(self, user):
if not user.is_superuser:
raise humanize_exception(ugettext_noop(
"Superuser privileges are required."), PermissionDenied())
super(FlushOperation, self).check_auth(user=user)
def _operation(self, activity, user):
self.node_enabled = self.node.enabled
self.node.disable(user, activity)
......@@ -882,6 +894,7 @@ class RecoverOperation(InstanceOperation):
acl_level = "owner"
required_perms = ('vm.recover', )
accept_states = ('DESTROYED', )
resultant_state = 'PENDING'
def check_precond(self):
try:
......@@ -889,9 +902,6 @@ class RecoverOperation(InstanceOperation):
except Instance.InstanceDestroyedError:
pass
def on_commit(self, activity):
activity.resultant_state = 'PENDING'
def _operation(self):
for disk in self.instance.disks.all():
disk.destroyed = None
......@@ -934,8 +944,27 @@ class ResourcesOperation(InstanceOperation):
register_operation(ResourcesOperation)
class PasswordResetOperation(InstanceOperation):
activity_code_suffix = 'Password reset'
class EnsureAgentMixin(object):
accept_states = ('RUNNING', )
def check_precond(self):
super(EnsureAgentMixin, self).check_precond()
last_boot_time = self.instance.activity_log.filter(
succeeded=True, activity_code__in=(
"vm.Instance.deploy", "vm.Instance.reset",
"vm.Instance.reboot")).latest("finished").finished
try:
InstanceActivity.objects.filter(
activity_code="vm.Instance.agent.starting",
started__gt=last_boot_time).latest("started")
except InstanceActivity.DoesNotExist: # no agent since last boot
raise self.instance.NoAgentError(self.instance)
class PasswordResetOperation(EnsureAgentMixin, InstanceOperation):
activity_code_suffix = 'password_reset'
id = 'password_reset'
name = _("password reset")
description = _("Generate and set a new login password on the virtual "
......@@ -945,7 +974,6 @@ class PasswordResetOperation(InstanceOperation):
"it.")
acl_level = "owner"
required_perms = ()
accept_states = ('RUNNING', )
def _operation(self):
self.instance.pw = pwgen()
......@@ -956,3 +984,34 @@ class PasswordResetOperation(InstanceOperation):
register_operation(PasswordResetOperation)
class MountStoreOperation(EnsureAgentMixin, InstanceOperation):
activity_code_suffix = 'mount_store'
id = 'mount_store'
name = _("mount store")
description = _(
"This operation attaches your personal file store. Other users who "
"have access to this machine can see these files as well."
)
acl_level = "owner"
required_perms = ()
def check_auth(self, user):
super(MountStoreOperation, self).check_auth(user)
try:
Store(user)
except NoStoreException:
raise PermissionDenied # not show the button at all
def _operation(self, user):
inst = self.instance
queue = self.instance.get_remote_queue_name("agent")
host = urlsplit(settings.STORE_URL).hostname
username = Store(user).username
password = user.profile.smb_password
agent_tasks.mount_store.apply_async(
queue=queue, args=(inst.vm_name, host, username, password))
register_operation(MountStoreOperation)
......@@ -76,3 +76,8 @@ def get_keys(vm):
@celery.task(name='agent.send_expiration')
def send_expiration(vm, url):
pass
@celery.task(name='agent.change_ip')
def change_ip(vm, interfaces, dns):
pass
......@@ -19,7 +19,9 @@ from common.models import create_readable
from manager.mancelery import celery
from vm.tasks.agent_tasks import (restart_networking, change_password,
set_time, set_hostname, start_access_server,
cleanup, update)
cleanup, update, change_ip)
from firewall.models import Host
import time
from base64 import encodestring
from StringIO import StringIO
......@@ -31,13 +33,11 @@ from celery.result import TimeoutError
from monitor.client import Client
def send_init_commands(instance, act, vm):
def send_init_commands(instance, act):
vm = instance.vm_name
queue = instance.get_remote_queue_name("agent")
with act.sub_activity('cleanup', readable_name=ugettext_noop('cleanup')):
cleanup.apply_async(queue=queue, args=(vm, ))
with act.sub_activity('restart_networking',
readable_name=ugettext_noop('restart networking')):
restart_networking.apply_async(queue=queue, args=(vm, ))
with act.sub_activity('change_password',
readable_name=ugettext_noop('change password')):
change_password.apply_async(queue=queue, args=(vm, instance.pw))
......@@ -49,6 +49,17 @@ def send_init_commands(instance, act, vm):
queue=queue, args=(vm, instance.primary_host.hostname))
def send_networking_commands(instance, act):
queue = instance.get_remote_queue_name("agent")
with act.sub_activity('change_ip',
readable_name=ugettext_noop('change ip')):
change_ip.apply_async(queue=queue, args=(
instance.vm_name, ) + get_network_configs(instance))
with act.sub_activity('restart_networking',
readable_name=ugettext_noop('restart networking')):
restart_networking.apply_async(queue=queue, args=(instance.vm_name, ))
def create_agent_tar():
def exclude(tarinfo):
if tarinfo.name.startswith('./.git'):
......@@ -86,7 +97,7 @@ def agent_started(vm, version=None):
if version and version != settings.AGENT_VERSION:
try:
update_agent(vm, instance, act)
update_agent(instance, act)
except TimeoutError:
pass
else:
......@@ -94,8 +105,9 @@ def agent_started(vm, version=None):
if not initialized:
measure_boot_time(instance)
send_init_commands(instance, act, vm)
send_init_commands(instance, act)
send_networking_commands(instance, act)
with act.sub_activity(
'start_access_server',
readable_name=ugettext_noop('start access server')
......@@ -134,9 +146,16 @@ def agent_stopped(vm):
pass
def update_agent(instance, vm, act=None):
def get_network_configs(instance):
interfaces = {}
for host in Host.objects.filter(interface__instance=instance):
interfaces[str(host.mac)] = host.get_network_config()
return (interfaces, settings.FIREWALL_SETTINGS['rdns_ip'])
def update_agent(instance, act=None):
if act:
act.sub_activity(
act = act.sub_activity(
'update',
readable_name=create_readable(
ugettext_noop('update to %(version)s'),
......@@ -150,5 +169,6 @@ def update_agent(instance, vm, act=None):
version=settings.AGENT_VERSION))
with act:
queue = instance.get_remote_queue_name("agent")
update.apply_async(queue=queue,
args=(vm, create_agent_tar())).get(timeout=10)
update.apply_async(
queue=queue,
args=(instance.vm_name, create_agent_tar())).get(timeout=10)
......@@ -55,7 +55,7 @@ def get_queues():
result = cache.get(key)
if result is None:
inspect = celery.control.inspect()
inspect.timeout = 0.1
inspect.timeout = 0.5
result = inspect.active_queues()
logger.debug('Queue list of length %d cached.', len(result))
cache.set(key, result, 10)
......
description "CIRCLE manager"
start on runlevel [2345]
stop on runlevel [!2345]
pre-start script
start moncelery
start mancelery
start slowcelery
end script
post-stop script
stop moncelery
stop mancelery
stop slowcelery
end script
description "CIRCLE mancelery"
start on runlevel [2345]
stop on runlevel [!2345]
description "CIRCLE mancelery for common jobs"
respawn
respawn limit 30 30
setgid cloud
setuid cloud
......@@ -12,6 +10,5 @@ script
cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 1
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 10
end script
description "CIRCLE moncelery for monitoring jobs"
respawn
respawn limit 30 30
setgid cloud
setuid cloud
script
cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.moncelery worker --autoreload --loglevel=info --hostname=moncelery -B -c 3
end script
......@@ -12,4 +12,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec /home/cloud/.virtualenvs/circle/bin/uwsgi --chdir=/home/cloud/circle/circle -H /home/cloud/.virtualenvs/circle --socket /tmp/uwsgi.sock --wsgi-file circle/wsgi.py --chmod-socket=666
end script
......@@ -14,4 +14,3 @@ script
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py runserver '[::]:8080'
end script
description "CIRCLE mancelery for slow jobs"
respawn
respawn limit 30 30
setgid cloud
setuid cloud
script
cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.slowcelery worker --autoreload --loglevel=info --hostname=slowcelery -B -c 5
end script
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment